Added files.
This commit is contained in:
23
LICENSE
Normal file
23
LICENSE
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2013 The Go-IMAP Authors
|
||||||
|
Copyright (c) 2016 Proton Technologies AG
|
||||||
|
Copyright (c) 2023 Simon Ser
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
29
README.md
Normal file
29
README.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# go-imap
|
||||||
|
|
||||||
|
[](https://pkg.go.dev/github.com/emersion/go-imap/v2)
|
||||||
|
|
||||||
|
An [IMAP4rev2] library for Go.
|
||||||
|
|
||||||
|
> **Note**
|
||||||
|
> This is the README for go-imap v2. This new major version is still in
|
||||||
|
> development. For go-imap v1, see the [v1 branch].
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
To add go-imap to your project, run:
|
||||||
|
|
||||||
|
go get github.com/emersion/go-imap/v2
|
||||||
|
|
||||||
|
Documentation and examples for the module are available here:
|
||||||
|
|
||||||
|
- [Client docs]
|
||||||
|
- [Server docs]
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
|
[IMAP4rev2]: https://www.rfc-editor.org/rfc/rfc9051.html
|
||||||
|
[v1 branch]: https://github.com/emersion/go-imap/tree/v1
|
||||||
|
[Client docs]: https://pkg.go.dev/github.com/emersion/go-imap/v2/imapclient
|
||||||
|
[Server docs]: https://pkg.go.dev/github.com/emersion/go-imap/v2/imapserver
|
||||||
104
acl.go
Normal file
104
acl.go
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
package imap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IMAP4 ACL extension (RFC 2086)
|
||||||
|
|
||||||
|
// Right describes a set of operations controlled by the IMAP ACL extension.
|
||||||
|
type Right byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Standard rights
|
||||||
|
RightLookup = Right('l') // mailbox is visible to LIST/LSUB commands
|
||||||
|
RightRead = Right('r') // SELECT the mailbox, perform CHECK, FETCH, PARTIAL, SEARCH, COPY from mailbox
|
||||||
|
RightSeen = Right('s') // keep seen/unseen information across sessions (STORE SEEN flag)
|
||||||
|
RightWrite = Right('w') // STORE flags other than SEEN and DELETED
|
||||||
|
RightInsert = Right('i') // perform APPEND, COPY into mailbox
|
||||||
|
RightPost = Right('p') // send mail to submission address for mailbox, not enforced by IMAP4 itself
|
||||||
|
RightCreate = Right('c') // CREATE new sub-mailboxes in any implementation-defined hierarchy
|
||||||
|
RightDelete = Right('d') // STORE DELETED flag, perform EXPUNGE
|
||||||
|
RightAdminister = Right('a') // perform SETACL
|
||||||
|
)
|
||||||
|
|
||||||
|
// RightSetAll contains all standard rights.
|
||||||
|
var RightSetAll = RightSet("lrswipcda")
|
||||||
|
|
||||||
|
// RightsIdentifier is an ACL identifier.
|
||||||
|
type RightsIdentifier string
|
||||||
|
|
||||||
|
// RightsIdentifierAnyone is the universal identity (matches everyone).
|
||||||
|
const RightsIdentifierAnyone = RightsIdentifier("anyone")
|
||||||
|
|
||||||
|
// NewRightsIdentifierUsername returns a rights identifier referring to a
|
||||||
|
// username, checking for reserved values.
|
||||||
|
func NewRightsIdentifierUsername(username string) (RightsIdentifier, error) {
|
||||||
|
if username == string(RightsIdentifierAnyone) || strings.HasPrefix(username, "-") {
|
||||||
|
return "", fmt.Errorf("imap: reserved rights identifier")
|
||||||
|
}
|
||||||
|
return RightsIdentifier(username), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RightModification indicates how to mutate a right set.
|
||||||
|
type RightModification byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
RightModificationReplace = RightModification(0)
|
||||||
|
RightModificationAdd = RightModification('+')
|
||||||
|
RightModificationRemove = RightModification('-')
|
||||||
|
)
|
||||||
|
|
||||||
|
// A RightSet is a set of rights.
|
||||||
|
type RightSet []Right
|
||||||
|
|
||||||
|
// String returns a string representation of the right set.
|
||||||
|
func (r RightSet) String() string {
|
||||||
|
return string(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add returns a new right set containing rights from both sets.
|
||||||
|
func (r RightSet) Add(rights RightSet) RightSet {
|
||||||
|
newRights := make(RightSet, len(r), len(r)+len(rights))
|
||||||
|
copy(newRights, r)
|
||||||
|
|
||||||
|
for _, right := range rights {
|
||||||
|
if !strings.ContainsRune(string(r), rune(right)) {
|
||||||
|
newRights = append(newRights, right)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newRights
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove returns a new right set containing all rights in r except these in
|
||||||
|
// the provided set.
|
||||||
|
func (r RightSet) Remove(rights RightSet) RightSet {
|
||||||
|
newRights := make(RightSet, 0, len(r))
|
||||||
|
|
||||||
|
for _, right := range r {
|
||||||
|
if !strings.ContainsRune(string(rights), rune(right)) {
|
||||||
|
newRights = append(newRights, right)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newRights
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal returns true if both right sets contain exactly the same rights.
|
||||||
|
func (rs1 RightSet) Equal(rs2 RightSet) bool {
|
||||||
|
for _, r := range rs1 {
|
||||||
|
if !strings.ContainsRune(string(rs2), rune(r)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range rs2 {
|
||||||
|
if !strings.ContainsRune(string(rs1), rune(r)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
18
append.go
Normal file
18
append.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package imap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AppendOptions contains options for the APPEND command.
|
||||||
|
type AppendOptions struct {
|
||||||
|
Flags []Flag
|
||||||
|
Time time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendData is the data returned by an APPEND command.
|
||||||
|
type AppendData struct {
|
||||||
|
// requires UIDPLUS or IMAP4rev2
|
||||||
|
UID UID
|
||||||
|
UIDValidity uint32
|
||||||
|
}
|
||||||
212
capability.go
Normal file
212
capability.go
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
package imap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cap represents an IMAP capability.
|
||||||
|
type Cap string
|
||||||
|
|
||||||
|
// Registered capabilities.
|
||||||
|
//
|
||||||
|
// See: https://www.iana.org/assignments/imap-capabilities/
|
||||||
|
const (
|
||||||
|
CapIMAP4rev1 Cap = "IMAP4rev1" // RFC 3501
|
||||||
|
CapIMAP4rev2 Cap = "IMAP4rev2" // RFC 9051
|
||||||
|
|
||||||
|
CapStartTLS Cap = "STARTTLS"
|
||||||
|
CapLoginDisabled Cap = "LOGINDISABLED"
|
||||||
|
|
||||||
|
// Folded in IMAP4rev2
|
||||||
|
CapNamespace Cap = "NAMESPACE" // RFC 2342
|
||||||
|
CapUnselect Cap = "UNSELECT" // RFC 3691
|
||||||
|
CapUIDPlus Cap = "UIDPLUS" // RFC 4315
|
||||||
|
CapESearch Cap = "ESEARCH" // RFC 4731
|
||||||
|
CapSearchRes Cap = "SEARCHRES" // RFC 5182
|
||||||
|
CapEnable Cap = "ENABLE" // RFC 5161
|
||||||
|
CapIdle Cap = "IDLE" // RFC 2177
|
||||||
|
CapSASLIR Cap = "SASL-IR" // RFC 4959
|
||||||
|
CapListExtended Cap = "LIST-EXTENDED" // RFC 5258
|
||||||
|
CapListStatus Cap = "LIST-STATUS" // RFC 5819
|
||||||
|
CapMove Cap = "MOVE" // RFC 6851
|
||||||
|
CapLiteralMinus Cap = "LITERAL-" // RFC 7888
|
||||||
|
CapStatusSize Cap = "STATUS=SIZE" // RFC 8438
|
||||||
|
CapChildren Cap = "CHILDREN" // RFC 3348
|
||||||
|
|
||||||
|
CapACL Cap = "ACL" // RFC 4314
|
||||||
|
CapAppendLimit Cap = "APPENDLIMIT" // RFC 7889
|
||||||
|
CapBinary Cap = "BINARY" // RFC 3516
|
||||||
|
CapCatenate Cap = "CATENATE" // RFC 4469
|
||||||
|
CapCondStore Cap = "CONDSTORE" // RFC 7162
|
||||||
|
CapConvert Cap = "CONVERT" // RFC 5259
|
||||||
|
CapCreateSpecialUse Cap = "CREATE-SPECIAL-USE" // RFC 6154
|
||||||
|
CapESort Cap = "ESORT" // RFC 5267
|
||||||
|
CapFilters Cap = "FILTERS" // RFC 5466
|
||||||
|
CapID Cap = "ID" // RFC 2971
|
||||||
|
CapLanguage Cap = "LANGUAGE" // RFC 5255
|
||||||
|
CapListMyRights Cap = "LIST-MYRIGHTS" // RFC 8440
|
||||||
|
CapLiteralPlus Cap = "LITERAL+" // RFC 7888
|
||||||
|
CapLoginReferrals Cap = "LOGIN-REFERRALS" // RFC 2221
|
||||||
|
CapMailboxReferrals Cap = "MAILBOX-REFERRALS" // RFC 2193
|
||||||
|
CapMetadata Cap = "METADATA" // RFC 5464
|
||||||
|
CapMetadataServer Cap = "METADATA-SERVER" // RFC 5464
|
||||||
|
CapMultiAppend Cap = "MULTIAPPEND" // RFC 3502
|
||||||
|
CapMultiSearch Cap = "MULTISEARCH" // RFC 7377
|
||||||
|
CapNotify Cap = "NOTIFY" // RFC 5465
|
||||||
|
CapObjectID Cap = "OBJECTID" // RFC 8474
|
||||||
|
CapPreview Cap = "PREVIEW" // RFC 8970
|
||||||
|
CapQResync Cap = "QRESYNC" // RFC 7162
|
||||||
|
CapQuota Cap = "QUOTA" // RFC 9208
|
||||||
|
CapQuotaSet Cap = "QUOTASET" // RFC 9208
|
||||||
|
CapReplace Cap = "REPLACE" // RFC 8508
|
||||||
|
CapSaveDate Cap = "SAVEDATE" // RFC 8514
|
||||||
|
CapSearchFuzzy Cap = "SEARCH=FUZZY" // RFC 6203
|
||||||
|
CapSort Cap = "SORT" // RFC 5256
|
||||||
|
CapSortDisplay Cap = "SORT=DISPLAY" // RFC 5957
|
||||||
|
CapSpecialUse Cap = "SPECIAL-USE" // RFC 6154
|
||||||
|
CapUnauthenticate Cap = "UNAUTHENTICATE" // RFC 8437
|
||||||
|
CapURLPartial Cap = "URL-PARTIAL" // RFC 5550
|
||||||
|
CapURLAuth Cap = "URLAUTH" // RFC 4467
|
||||||
|
CapUTF8Accept Cap = "UTF8=ACCEPT" // RFC 6855
|
||||||
|
CapUTF8Only Cap = "UTF8=ONLY" // RFC 6855
|
||||||
|
CapWithin Cap = "WITHIN" // RFC 5032
|
||||||
|
CapUIDOnly Cap = "UIDONLY" // RFC 9586
|
||||||
|
CapListMetadata Cap = "LIST-METADATA" // RFC 9590
|
||||||
|
CapInProgress Cap = "INPROGRESS" // RFC 9585
|
||||||
|
)
|
||||||
|
|
||||||
|
var imap4rev2Caps = CapSet{
|
||||||
|
CapNamespace: {},
|
||||||
|
CapUnselect: {},
|
||||||
|
CapUIDPlus: {},
|
||||||
|
CapESearch: {},
|
||||||
|
CapSearchRes: {},
|
||||||
|
CapEnable: {},
|
||||||
|
CapIdle: {},
|
||||||
|
CapSASLIR: {},
|
||||||
|
CapListExtended: {},
|
||||||
|
CapListStatus: {},
|
||||||
|
CapMove: {},
|
||||||
|
CapLiteralMinus: {},
|
||||||
|
CapStatusSize: {},
|
||||||
|
CapChildren: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthCap returns the capability name for an SASL authentication mechanism.
|
||||||
|
func AuthCap(mechanism string) Cap {
|
||||||
|
return Cap("AUTH=" + mechanism)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CapSet is a set of capabilities.
|
||||||
|
type CapSet map[Cap]struct{}
|
||||||
|
|
||||||
|
func (set CapSet) has(c Cap) bool {
|
||||||
|
_, ok := set[c]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (set CapSet) Copy() CapSet {
|
||||||
|
newSet := make(CapSet, len(set))
|
||||||
|
for c := range set {
|
||||||
|
newSet[c] = struct{}{}
|
||||||
|
}
|
||||||
|
return newSet
|
||||||
|
}
|
||||||
|
|
||||||
|
// Has checks whether a capability is supported.
|
||||||
|
//
|
||||||
|
// Some capabilities are implied by others, as such Has may return true even if
|
||||||
|
// the capability is not in the map.
|
||||||
|
func (set CapSet) Has(c Cap) bool {
|
||||||
|
if set.has(c) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if set.has(CapIMAP4rev2) && imap4rev2Caps.has(c) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if c == CapLiteralMinus && set.has(CapLiteralPlus) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if c == CapCondStore && set.has(CapQResync) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if c == CapUTF8Accept && set.has(CapUTF8Only) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if c == CapAppendLimit {
|
||||||
|
_, ok := set.AppendLimit()
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthMechanisms returns the list of supported SASL mechanisms for
|
||||||
|
// authentication.
|
||||||
|
func (set CapSet) AuthMechanisms() []string {
|
||||||
|
var l []string
|
||||||
|
for c := range set {
|
||||||
|
if !strings.HasPrefix(string(c), "AUTH=") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mech := strings.TrimPrefix(string(c), "AUTH=")
|
||||||
|
l = append(l, mech)
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendLimit checks the APPENDLIMIT capability.
|
||||||
|
//
|
||||||
|
// If the server supports APPENDLIMIT, ok is true. If the server doesn't have
|
||||||
|
// the same upload limit for all mailboxes, limit is nil and per-mailbox
|
||||||
|
// limits must be queried via STATUS.
|
||||||
|
func (set CapSet) AppendLimit() (limit *uint32, ok bool) {
|
||||||
|
if set.has(CapAppendLimit) {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
|
||||||
|
for c := range set {
|
||||||
|
if !strings.HasPrefix(string(c), "APPENDLIMIT=") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
limitStr := strings.TrimPrefix(string(c), "APPENDLIMIT=")
|
||||||
|
limit64, err := strconv.ParseUint(limitStr, 10, 32)
|
||||||
|
if err == nil && limit64 > 0 {
|
||||||
|
limit32 := uint32(limit64)
|
||||||
|
return &limit32, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
limit32 := ^uint32(0)
|
||||||
|
return &limit32, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuotaResourceTypes returns the list of supported QUOTA resource types.
|
||||||
|
func (set CapSet) QuotaResourceTypes() []QuotaResourceType {
|
||||||
|
var l []QuotaResourceType
|
||||||
|
for c := range set {
|
||||||
|
if !strings.HasPrefix(string(c), "QUOTA=RES-") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t := strings.TrimPrefix(string(c), "QUOTA=RES-")
|
||||||
|
l = append(l, QuotaResourceType(t))
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
// ThreadAlgorithms returns the list of supported threading algorithms.
|
||||||
|
func (set CapSet) ThreadAlgorithms() []ThreadAlgorithm {
|
||||||
|
var l []ThreadAlgorithm
|
||||||
|
for c := range set {
|
||||||
|
if !strings.HasPrefix(string(c), "THREAD=") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
alg := strings.TrimPrefix(string(c), "THREAD=")
|
||||||
|
l = append(l, ThreadAlgorithm(alg))
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
128
cmd/imapmemserver/main.go
Normal file
128
cmd/imapmemserver/main.go
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"flag"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/imapserver"
|
||||||
|
"github.com/emersion/go-imap/v2/imapserver/imapmemserver"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
listen string
|
||||||
|
tlsCert string
|
||||||
|
tlsKey string
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
debug bool
|
||||||
|
insecureAuth bool
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.StringVar(&listen, "listen", "localhost:143", "listening address")
|
||||||
|
flag.StringVar(&tlsCert, "tls-cert", "", "TLS certificate")
|
||||||
|
flag.StringVar(&tlsKey, "tls-key", "", "TLS key")
|
||||||
|
flag.StringVar(&username, "username", "user", "Username")
|
||||||
|
flag.StringVar(&password, "password", "user", "Password")
|
||||||
|
flag.BoolVar(&debug, "debug", false, "Print all commands and responses")
|
||||||
|
flag.BoolVar(&insecureAuth, "insecure-auth", false, "Allow authentication without TLS")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
var tlsConfig *tls.Config
|
||||||
|
if tlsCert != "" || tlsKey != "" {
|
||||||
|
cert, err := tls.LoadX509KeyPair(tlsCert, tlsKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load TLS key pair: %v", err)
|
||||||
|
}
|
||||||
|
tlsConfig = &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{cert},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ln, err := net.Listen("tcp", listen)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to listen: %v", err)
|
||||||
|
}
|
||||||
|
log.Printf("IMAP server listening on %v", ln.Addr())
|
||||||
|
|
||||||
|
memServer := imapmemserver.New()
|
||||||
|
|
||||||
|
if username != "" || password != "" {
|
||||||
|
user := imapmemserver.NewUser(username, password)
|
||||||
|
|
||||||
|
// Create standard mailboxes with special-use attributes as per RFC 6154
|
||||||
|
if err := user.Create("INBOX", nil); err != nil {
|
||||||
|
log.Printf("Failed to create INBOX: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.Create("Drafts", &imap.CreateOptions{
|
||||||
|
SpecialUse: []imap.MailboxAttr{imap.MailboxAttrDrafts},
|
||||||
|
}); err != nil {
|
||||||
|
log.Printf("Failed to create Drafts mailbox: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.Create("Sent", &imap.CreateOptions{
|
||||||
|
SpecialUse: []imap.MailboxAttr{imap.MailboxAttrSent},
|
||||||
|
}); err != nil {
|
||||||
|
log.Printf("Failed to create Sent mailbox: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.Create("Archive", &imap.CreateOptions{
|
||||||
|
SpecialUse: []imap.MailboxAttr{imap.MailboxAttrArchive},
|
||||||
|
}); err != nil {
|
||||||
|
log.Printf("Failed to create Archive mailbox: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.Create("Junk", &imap.CreateOptions{
|
||||||
|
SpecialUse: []imap.MailboxAttr{imap.MailboxAttrJunk},
|
||||||
|
}); err != nil {
|
||||||
|
log.Printf("Failed to create Junk mailbox: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.Create("Trash", &imap.CreateOptions{
|
||||||
|
SpecialUse: []imap.MailboxAttr{imap.MailboxAttrTrash},
|
||||||
|
}); err != nil {
|
||||||
|
log.Printf("Failed to create Trash mailbox: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.Create("Flagged", &imap.CreateOptions{
|
||||||
|
SpecialUse: []imap.MailboxAttr{imap.MailboxAttrFlagged},
|
||||||
|
}); err != nil {
|
||||||
|
log.Printf("Failed to create Flagged mailbox: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to the most commonly used mailboxes
|
||||||
|
_ = user.Subscribe("INBOX")
|
||||||
|
_ = user.Subscribe("Drafts")
|
||||||
|
_ = user.Subscribe("Sent")
|
||||||
|
_ = user.Subscribe("Trash")
|
||||||
|
|
||||||
|
memServer.AddUser(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
var debugWriter io.Writer
|
||||||
|
if debug {
|
||||||
|
debugWriter = os.Stdout
|
||||||
|
}
|
||||||
|
|
||||||
|
server := imapserver.New(&imapserver.Options{
|
||||||
|
NewSession: func(conn *imapserver.Conn) (imapserver.Session, *imapserver.GreetingData, error) {
|
||||||
|
return memServer.NewSession(), nil, nil
|
||||||
|
},
|
||||||
|
Caps: imap.CapSet{
|
||||||
|
imap.CapIMAP4rev1: {},
|
||||||
|
imap.CapIMAP4rev2: {},
|
||||||
|
},
|
||||||
|
TLSConfig: tlsConfig,
|
||||||
|
InsecureAuth: insecureAuth,
|
||||||
|
DebugWriter: debugWriter,
|
||||||
|
})
|
||||||
|
if err := server.Serve(ln); err != nil {
|
||||||
|
log.Fatalf("Serve() = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
9
copy.go
Normal file
9
copy.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package imap
|
||||||
|
|
||||||
|
// CopyData is the data returned by a COPY command.
|
||||||
|
type CopyData struct {
|
||||||
|
// requires UIDPLUS or IMAP4rev2
|
||||||
|
UIDValidity uint32
|
||||||
|
SourceUIDs UIDSet
|
||||||
|
DestUIDs UIDSet
|
||||||
|
}
|
||||||
6
create.go
Normal file
6
create.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package imap
|
||||||
|
|
||||||
|
// CreateOptions contains options for the CREATE command.
|
||||||
|
type CreateOptions struct {
|
||||||
|
SpecialUse []MailboxAttr // requires CREATE-SPECIAL-USE
|
||||||
|
}
|
||||||
284
fetch.go
Normal file
284
fetch.go
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
package imap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FetchOptions contains options for the FETCH command.
|
||||||
|
type FetchOptions struct {
|
||||||
|
// Fields to fetch
|
||||||
|
BodyStructure *FetchItemBodyStructure
|
||||||
|
Envelope bool
|
||||||
|
Flags bool
|
||||||
|
InternalDate bool
|
||||||
|
RFC822Size bool
|
||||||
|
UID bool
|
||||||
|
BodySection []*FetchItemBodySection
|
||||||
|
BinarySection []*FetchItemBinarySection // requires IMAP4rev2 or BINARY
|
||||||
|
BinarySectionSize []*FetchItemBinarySectionSize // requires IMAP4rev2 or BINARY
|
||||||
|
ModSeq bool // requires CONDSTORE
|
||||||
|
|
||||||
|
ChangedSince uint64 // requires CONDSTORE
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchItemBodyStructure contains FETCH options for the body structure.
|
||||||
|
type FetchItemBodyStructure struct {
|
||||||
|
Extended bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// PartSpecifier describes whether to fetch a part's header, body, or both.
|
||||||
|
type PartSpecifier string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PartSpecifierNone PartSpecifier = ""
|
||||||
|
PartSpecifierHeader PartSpecifier = "HEADER"
|
||||||
|
PartSpecifierMIME PartSpecifier = "MIME"
|
||||||
|
PartSpecifierText PartSpecifier = "TEXT"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SectionPartial describes a byte range when fetching a message's payload.
|
||||||
|
type SectionPartial struct {
|
||||||
|
Offset, Size int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchItemBodySection is a FETCH BODY[] data item.
|
||||||
|
//
|
||||||
|
// To fetch the whole body of a message, use the zero FetchItemBodySection:
|
||||||
|
//
|
||||||
|
// imap.FetchItemBodySection{}
|
||||||
|
//
|
||||||
|
// To fetch only a specific part, use the Part field:
|
||||||
|
//
|
||||||
|
// imap.FetchItemBodySection{Part: []int{1, 2, 3}}
|
||||||
|
//
|
||||||
|
// To fetch only the header of the message, use the Specifier field:
|
||||||
|
//
|
||||||
|
// imap.FetchItemBodySection{Specifier: imap.PartSpecifierHeader}
|
||||||
|
type FetchItemBodySection struct {
|
||||||
|
Specifier PartSpecifier
|
||||||
|
Part []int
|
||||||
|
HeaderFields []string
|
||||||
|
HeaderFieldsNot []string
|
||||||
|
Partial *SectionPartial
|
||||||
|
Peek bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchItemBinarySection is a FETCH BINARY[] data item.
|
||||||
|
type FetchItemBinarySection struct {
|
||||||
|
Part []int
|
||||||
|
Partial *SectionPartial
|
||||||
|
Peek bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchItemBinarySectionSize is a FETCH BINARY.SIZE[] data item.
|
||||||
|
type FetchItemBinarySectionSize struct {
|
||||||
|
Part []int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Envelope is the envelope structure of a message.
|
||||||
|
//
|
||||||
|
// The subject and addresses are UTF-8 (ie, not in their encoded form). The
|
||||||
|
// In-Reply-To and Message-ID values contain message identifiers without angle
|
||||||
|
// brackets.
|
||||||
|
type Envelope struct {
|
||||||
|
Date time.Time
|
||||||
|
Subject string
|
||||||
|
From []Address
|
||||||
|
Sender []Address
|
||||||
|
ReplyTo []Address
|
||||||
|
To []Address
|
||||||
|
Cc []Address
|
||||||
|
Bcc []Address
|
||||||
|
InReplyTo []string
|
||||||
|
MessageID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Address represents a sender or recipient of a message.
|
||||||
|
type Address struct {
|
||||||
|
Name string
|
||||||
|
Mailbox string
|
||||||
|
Host string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Addr returns the e-mail address in the form "foo@example.org".
|
||||||
|
//
|
||||||
|
// If the address is a start or end of group, the empty string is returned.
|
||||||
|
func (addr *Address) Addr() string {
|
||||||
|
if addr.Mailbox == "" || addr.Host == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return addr.Mailbox + "@" + addr.Host
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsGroupStart returns true if this address is a start of group marker.
|
||||||
|
//
|
||||||
|
// In that case, Mailbox contains the group name phrase.
|
||||||
|
func (addr *Address) IsGroupStart() bool {
|
||||||
|
return addr.Host == "" && addr.Mailbox != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsGroupEnd returns true if this address is a end of group marker.
|
||||||
|
func (addr *Address) IsGroupEnd() bool {
|
||||||
|
return addr.Host == "" && addr.Mailbox == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// BodyStructure describes the body structure of a message.
|
||||||
|
//
|
||||||
|
// A BodyStructure value is either a *BodyStructureSinglePart or a
|
||||||
|
// *BodyStructureMultiPart.
|
||||||
|
type BodyStructure interface {
|
||||||
|
// MediaType returns the MIME type of this body structure, e.g. "text/plain".
|
||||||
|
MediaType() string
|
||||||
|
// Walk walks the body structure tree, calling f for each part in the tree,
|
||||||
|
// including bs itself. The parts are visited in DFS pre-order.
|
||||||
|
Walk(f BodyStructureWalkFunc)
|
||||||
|
// Disposition returns the body structure disposition, if available.
|
||||||
|
Disposition() *BodyStructureDisposition
|
||||||
|
|
||||||
|
bodyStructure()
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ BodyStructure = (*BodyStructureSinglePart)(nil)
|
||||||
|
_ BodyStructure = (*BodyStructureMultiPart)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
// BodyStructureSinglePart is a body structure with a single part.
|
||||||
|
type BodyStructureSinglePart struct {
|
||||||
|
Type, Subtype string
|
||||||
|
Params map[string]string
|
||||||
|
ID string
|
||||||
|
Description string
|
||||||
|
Encoding string
|
||||||
|
Size uint32
|
||||||
|
|
||||||
|
MessageRFC822 *BodyStructureMessageRFC822 // only for "message/rfc822"
|
||||||
|
Text *BodyStructureText // only for "text/*"
|
||||||
|
Extended *BodyStructureSinglePartExt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs *BodyStructureSinglePart) MediaType() string {
|
||||||
|
return strings.ToLower(bs.Type) + "/" + strings.ToLower(bs.Subtype)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs *BodyStructureSinglePart) Walk(f BodyStructureWalkFunc) {
|
||||||
|
f([]int{1}, bs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs *BodyStructureSinglePart) Disposition() *BodyStructureDisposition {
|
||||||
|
if bs.Extended == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return bs.Extended.Disposition
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filename decodes the body structure's filename, if any.
|
||||||
|
func (bs *BodyStructureSinglePart) Filename() string {
|
||||||
|
var filename string
|
||||||
|
if bs.Extended != nil && bs.Extended.Disposition != nil {
|
||||||
|
filename = bs.Extended.Disposition.Params["filename"]
|
||||||
|
}
|
||||||
|
if filename == "" {
|
||||||
|
// Note: using "name" in Content-Type is discouraged
|
||||||
|
filename = bs.Params["name"]
|
||||||
|
}
|
||||||
|
return filename
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*BodyStructureSinglePart) bodyStructure() {}
|
||||||
|
|
||||||
|
// BodyStructureMessageRFC822 contains metadata specific to RFC 822 parts for
|
||||||
|
// BodyStructureSinglePart.
|
||||||
|
type BodyStructureMessageRFC822 struct {
|
||||||
|
Envelope *Envelope
|
||||||
|
BodyStructure BodyStructure
|
||||||
|
NumLines int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// BodyStructureText contains metadata specific to text parts for
|
||||||
|
// BodyStructureSinglePart.
|
||||||
|
type BodyStructureText struct {
|
||||||
|
NumLines int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// BodyStructureSinglePartExt contains extended body structure data for
|
||||||
|
// BodyStructureSinglePart.
|
||||||
|
type BodyStructureSinglePartExt struct {
|
||||||
|
Disposition *BodyStructureDisposition
|
||||||
|
Language []string
|
||||||
|
Location string
|
||||||
|
}
|
||||||
|
|
||||||
|
// BodyStructureMultiPart is a body structure with multiple parts.
|
||||||
|
type BodyStructureMultiPart struct {
|
||||||
|
Children []BodyStructure
|
||||||
|
Subtype string
|
||||||
|
|
||||||
|
Extended *BodyStructureMultiPartExt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs *BodyStructureMultiPart) MediaType() string {
|
||||||
|
return "multipart/" + strings.ToLower(bs.Subtype)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs *BodyStructureMultiPart) Walk(f BodyStructureWalkFunc) {
|
||||||
|
bs.walk(f, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs *BodyStructureMultiPart) walk(f BodyStructureWalkFunc, path []int) {
|
||||||
|
if !f(path, bs) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pathBuf := make([]int, len(path))
|
||||||
|
copy(pathBuf, path)
|
||||||
|
for i, part := range bs.Children {
|
||||||
|
num := i + 1
|
||||||
|
partPath := append(pathBuf, num)
|
||||||
|
|
||||||
|
switch part := part.(type) {
|
||||||
|
case *BodyStructureSinglePart:
|
||||||
|
f(partPath, part)
|
||||||
|
case *BodyStructureMultiPart:
|
||||||
|
part.walk(f, partPath)
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("unsupported body structure type %T", part))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs *BodyStructureMultiPart) Disposition() *BodyStructureDisposition {
|
||||||
|
if bs.Extended == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return bs.Extended.Disposition
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*BodyStructureMultiPart) bodyStructure() {}
|
||||||
|
|
||||||
|
// BodyStructureMultiPartExt contains extended body structure data for
|
||||||
|
// BodyStructureMultiPart.
|
||||||
|
type BodyStructureMultiPartExt struct {
|
||||||
|
Params map[string]string
|
||||||
|
Disposition *BodyStructureDisposition
|
||||||
|
Language []string
|
||||||
|
Location string
|
||||||
|
}
|
||||||
|
|
||||||
|
// BodyStructureDisposition describes the content disposition of a part
|
||||||
|
// (specified in the Content-Disposition header field).
|
||||||
|
type BodyStructureDisposition struct {
|
||||||
|
Value string
|
||||||
|
Params map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// BodyStructureWalkFunc is a function called for each body structure visited
|
||||||
|
// by BodyStructure.Walk.
|
||||||
|
//
|
||||||
|
// The path argument contains the IMAP part path.
|
||||||
|
//
|
||||||
|
// The function should return true to visit all of the part's children or false
|
||||||
|
// to skip them.
|
||||||
|
type BodyStructureWalkFunc func(path []int, part BodyStructure) (walkChildren bool)
|
||||||
8
go.mod
Normal file
8
go.mod
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
module github.com/emersion/go-imap/v2
|
||||||
|
|
||||||
|
go 1.18
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/emersion/go-message v0.18.2
|
||||||
|
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6
|
||||||
|
)
|
||||||
35
go.sum
Normal file
35
go.sum
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
github.com/emersion/go-message v0.18.2 h1:rl55SQdjd9oJcIoQNhubD2Acs1E6IzlZISRTK7x/Lpg=
|
||||||
|
github.com/emersion/go-message v0.18.2/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA=
|
||||||
|
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
|
||||||
|
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
15
id.go
Normal file
15
id.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package imap
|
||||||
|
|
||||||
|
type IDData struct {
|
||||||
|
Name string
|
||||||
|
Version string
|
||||||
|
OS string
|
||||||
|
OSVersion string
|
||||||
|
Vendor string
|
||||||
|
SupportURL string
|
||||||
|
Address string
|
||||||
|
Date string
|
||||||
|
Command string
|
||||||
|
Arguments string
|
||||||
|
Environment string
|
||||||
|
}
|
||||||
105
imap.go
Normal file
105
imap.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
// Package imap implements IMAP4rev2.
|
||||||
|
//
|
||||||
|
// IMAP4rev2 is defined in RFC 9051.
|
||||||
|
//
|
||||||
|
// This package contains types and functions common to both the client and
|
||||||
|
// server. See the imapclient and imapserver sub-packages.
|
||||||
|
package imap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConnState describes the connection state.
|
||||||
|
//
|
||||||
|
// See RFC 9051 section 3.
|
||||||
|
type ConnState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ConnStateNone ConnState = iota
|
||||||
|
ConnStateNotAuthenticated
|
||||||
|
ConnStateAuthenticated
|
||||||
|
ConnStateSelected
|
||||||
|
ConnStateLogout
|
||||||
|
)
|
||||||
|
|
||||||
|
// String implements fmt.Stringer.
|
||||||
|
func (state ConnState) String() string {
|
||||||
|
switch state {
|
||||||
|
case ConnStateNone:
|
||||||
|
return "none"
|
||||||
|
case ConnStateNotAuthenticated:
|
||||||
|
return "not authenticated"
|
||||||
|
case ConnStateAuthenticated:
|
||||||
|
return "authenticated"
|
||||||
|
case ConnStateSelected:
|
||||||
|
return "selected"
|
||||||
|
case ConnStateLogout:
|
||||||
|
return "logout"
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("imap: unknown connection state %v", int(state)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MailboxAttr is a mailbox attribute.
|
||||||
|
//
|
||||||
|
// Mailbox attributes are defined in RFC 9051 section 7.3.1.
|
||||||
|
type MailboxAttr string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Base attributes
|
||||||
|
MailboxAttrNonExistent MailboxAttr = "\\NonExistent"
|
||||||
|
MailboxAttrNoInferiors MailboxAttr = "\\Noinferiors"
|
||||||
|
MailboxAttrNoSelect MailboxAttr = "\\Noselect"
|
||||||
|
MailboxAttrHasChildren MailboxAttr = "\\HasChildren"
|
||||||
|
MailboxAttrHasNoChildren MailboxAttr = "\\HasNoChildren"
|
||||||
|
MailboxAttrMarked MailboxAttr = "\\Marked"
|
||||||
|
MailboxAttrUnmarked MailboxAttr = "\\Unmarked"
|
||||||
|
MailboxAttrSubscribed MailboxAttr = "\\Subscribed"
|
||||||
|
MailboxAttrRemote MailboxAttr = "\\Remote"
|
||||||
|
|
||||||
|
// Role (aka. "special-use") attributes
|
||||||
|
MailboxAttrAll MailboxAttr = "\\All"
|
||||||
|
MailboxAttrArchive MailboxAttr = "\\Archive"
|
||||||
|
MailboxAttrDrafts MailboxAttr = "\\Drafts"
|
||||||
|
MailboxAttrFlagged MailboxAttr = "\\Flagged"
|
||||||
|
MailboxAttrJunk MailboxAttr = "\\Junk"
|
||||||
|
MailboxAttrSent MailboxAttr = "\\Sent"
|
||||||
|
MailboxAttrTrash MailboxAttr = "\\Trash"
|
||||||
|
MailboxAttrImportant MailboxAttr = "\\Important" // RFC 8457
|
||||||
|
)
|
||||||
|
|
||||||
|
// Flag is a message flag.
|
||||||
|
//
|
||||||
|
// Message flags are defined in RFC 9051 section 2.3.2.
|
||||||
|
type Flag string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// System flags
|
||||||
|
FlagSeen Flag = "\\Seen"
|
||||||
|
FlagAnswered Flag = "\\Answered"
|
||||||
|
FlagFlagged Flag = "\\Flagged"
|
||||||
|
FlagDeleted Flag = "\\Deleted"
|
||||||
|
FlagDraft Flag = "\\Draft"
|
||||||
|
|
||||||
|
// Widely used flags
|
||||||
|
FlagForwarded Flag = "$Forwarded"
|
||||||
|
FlagMDNSent Flag = "$MDNSent" // Message Disposition Notification sent
|
||||||
|
FlagJunk Flag = "$Junk"
|
||||||
|
FlagNotJunk Flag = "$NotJunk"
|
||||||
|
FlagPhishing Flag = "$Phishing"
|
||||||
|
FlagImportant Flag = "$Important" // RFC 8457
|
||||||
|
|
||||||
|
// Permanent flags
|
||||||
|
FlagWildcard Flag = "\\*"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LiteralReader is a reader for IMAP literals.
|
||||||
|
type LiteralReader interface {
|
||||||
|
io.Reader
|
||||||
|
Size() int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// UID is a message unique identifier.
|
||||||
|
type UID uint32
|
||||||
138
imapclient/acl.go
Normal file
138
imapclient/acl.go
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
package imapclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MyRights sends a MYRIGHTS command.
|
||||||
|
//
|
||||||
|
// This command requires support for the ACL extension.
|
||||||
|
func (c *Client) MyRights(mailbox string) *MyRightsCommand {
|
||||||
|
cmd := &MyRightsCommand{}
|
||||||
|
enc := c.beginCommand("MYRIGHTS", cmd)
|
||||||
|
enc.SP().Mailbox(mailbox)
|
||||||
|
enc.end()
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetACL sends a SETACL command.
|
||||||
|
//
|
||||||
|
// This command requires support for the ACL extension.
|
||||||
|
func (c *Client) SetACL(mailbox string, ri imap.RightsIdentifier, rm imap.RightModification, rs imap.RightSet) *SetACLCommand {
|
||||||
|
cmd := &SetACLCommand{}
|
||||||
|
enc := c.beginCommand("SETACL", cmd)
|
||||||
|
enc.SP().Mailbox(mailbox).SP().String(string(ri)).SP()
|
||||||
|
enc.String(internal.FormatRights(rm, rs))
|
||||||
|
enc.end()
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetACLCommand is a SETACL command.
|
||||||
|
type SetACLCommand struct {
|
||||||
|
commandBase
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *SetACLCommand) Wait() error {
|
||||||
|
return cmd.wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetACL sends a GETACL command.
|
||||||
|
//
|
||||||
|
// This command requires support for the ACL extension.
|
||||||
|
func (c *Client) GetACL(mailbox string) *GetACLCommand {
|
||||||
|
cmd := &GetACLCommand{}
|
||||||
|
enc := c.beginCommand("GETACL", cmd)
|
||||||
|
enc.SP().Mailbox(mailbox)
|
||||||
|
enc.end()
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetACLCommand is a GETACL command.
|
||||||
|
type GetACLCommand struct {
|
||||||
|
commandBase
|
||||||
|
data GetACLData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *GetACLCommand) Wait() (*GetACLData, error) {
|
||||||
|
return &cmd.data, cmd.wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) handleMyRights() error {
|
||||||
|
data, err := readMyRights(c.dec)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("in myrights-response: %v", err)
|
||||||
|
}
|
||||||
|
if cmd := findPendingCmdByType[*MyRightsCommand](c); cmd != nil {
|
||||||
|
cmd.data = *data
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) handleGetACL() error {
|
||||||
|
data, err := readGetACL(c.dec)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("in getacl-response: %v", err)
|
||||||
|
}
|
||||||
|
if cmd := findPendingCmdByType[*GetACLCommand](c); cmd != nil {
|
||||||
|
cmd.data = *data
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MyRightsCommand is a MYRIGHTS command.
|
||||||
|
type MyRightsCommand struct {
|
||||||
|
commandBase
|
||||||
|
data MyRightsData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *MyRightsCommand) Wait() (*MyRightsData, error) {
|
||||||
|
return &cmd.data, cmd.wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MyRightsData is the data returned by the MYRIGHTS command.
|
||||||
|
type MyRightsData struct {
|
||||||
|
Mailbox string
|
||||||
|
Rights imap.RightSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func readMyRights(dec *imapwire.Decoder) (*MyRightsData, error) {
|
||||||
|
var (
|
||||||
|
rights string
|
||||||
|
data MyRightsData
|
||||||
|
)
|
||||||
|
if !dec.ExpectMailbox(&data.Mailbox) || !dec.ExpectSP() || !dec.ExpectAString(&rights) {
|
||||||
|
return nil, dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Rights = imap.RightSet(rights)
|
||||||
|
return &data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetACLData is the data returned by the GETACL command.
|
||||||
|
type GetACLData struct {
|
||||||
|
Mailbox string
|
||||||
|
Rights map[imap.RightsIdentifier]imap.RightSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func readGetACL(dec *imapwire.Decoder) (*GetACLData, error) {
|
||||||
|
data := &GetACLData{Rights: make(map[imap.RightsIdentifier]imap.RightSet)}
|
||||||
|
|
||||||
|
if !dec.ExpectMailbox(&data.Mailbox) {
|
||||||
|
return nil, dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
for dec.SP() {
|
||||||
|
var rsStr, riStr string
|
||||||
|
if !dec.ExpectAString(&riStr) || !dec.ExpectSP() || !dec.ExpectAString(&rsStr) {
|
||||||
|
return nil, dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Rights[imap.RightsIdentifier(riStr)] = imap.RightSet(rsStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
115
imapclient/acl_test.go
Normal file
115
imapclient/acl_test.go
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package imapclient_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// order matters
|
||||||
|
var testCases = []struct {
|
||||||
|
name string
|
||||||
|
mailbox string
|
||||||
|
setRightsModification imap.RightModification
|
||||||
|
setRights imap.RightSet
|
||||||
|
expectedRights imap.RightSet
|
||||||
|
execStatusCmd bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "inbox",
|
||||||
|
mailbox: "INBOX",
|
||||||
|
setRightsModification: imap.RightModificationReplace,
|
||||||
|
setRights: imap.RightSet("akxeilprwtscd"),
|
||||||
|
expectedRights: imap.RightSet("akxeilprwtscd"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom_folder",
|
||||||
|
mailbox: "MyFolder",
|
||||||
|
setRightsModification: imap.RightModificationReplace,
|
||||||
|
setRights: imap.RightSet("ailw"),
|
||||||
|
expectedRights: imap.RightSet("ailw"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom_child_folder",
|
||||||
|
mailbox: "MyFolder/Child",
|
||||||
|
setRightsModification: imap.RightModificationReplace,
|
||||||
|
setRights: imap.RightSet("aelrwtd"),
|
||||||
|
expectedRights: imap.RightSet("aelrwtd"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "add_rights",
|
||||||
|
mailbox: "MyFolder",
|
||||||
|
setRightsModification: imap.RightModificationAdd,
|
||||||
|
setRights: imap.RightSet("rwi"),
|
||||||
|
expectedRights: imap.RightSet("ailwr"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "remove_rights",
|
||||||
|
mailbox: "MyFolder",
|
||||||
|
setRightsModification: imap.RightModificationRemove,
|
||||||
|
setRights: imap.RightSet("iwc"),
|
||||||
|
expectedRights: imap.RightSet("alr"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty_rights",
|
||||||
|
mailbox: "MyFolder/Child",
|
||||||
|
setRightsModification: imap.RightModificationReplace,
|
||||||
|
setRights: imap.RightSet("a"),
|
||||||
|
expectedRights: imap.RightSet("a"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestACL runs tests on SetACL, GetACL and MyRights commands.
|
||||||
|
func TestACL(t *testing.T) {
|
||||||
|
client, server := newClientServerPair(t, imap.ConnStateAuthenticated)
|
||||||
|
defer client.Close()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
if !client.Caps().Has(imap.CapACL) {
|
||||||
|
t.Skipf("server doesn't support ACL")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.Create("MyFolder", nil).Wait(); err != nil {
|
||||||
|
t.Fatalf("create MyFolder error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.Create("MyFolder/Child", nil).Wait(); err != nil {
|
||||||
|
t.Fatalf("create MyFolder/Child error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
// execute SETACL command
|
||||||
|
err := client.SetACL(tc.mailbox, testUsername, tc.setRightsModification, tc.setRights).Wait()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SetACL().Wait() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// execute GETACL command to reset cache on server
|
||||||
|
getACLData, err := client.GetACL(tc.mailbox).Wait()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetACL().Wait() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectedRights.Equal(getACLData.Rights[testUsername]) {
|
||||||
|
t.Errorf("GETACL returned wrong rights; expected: %s, got: %s", tc.expectedRights, getACLData.Rights[testUsername])
|
||||||
|
}
|
||||||
|
|
||||||
|
// execute MYRIGHTS command
|
||||||
|
myRightsData, err := client.MyRights(tc.mailbox).Wait()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("MyRights().Wait() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectedRights.Equal(myRightsData.Rights) {
|
||||||
|
t.Errorf("MYRIGHTS returned wrong rights; expected: %s, got: %s", tc.expectedRights, myRightsData.Rights)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("nonexistent_mailbox", func(t *testing.T) {
|
||||||
|
if client.SetACL("BibiMailbox", testUsername, imap.RightModificationReplace, nil).Wait() == nil {
|
||||||
|
t.Errorf("expected error")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
58
imapclient/append.go
Normal file
58
imapclient/append.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package imapclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Append sends an APPEND command.
|
||||||
|
//
|
||||||
|
// The caller must call AppendCommand.Close.
|
||||||
|
//
|
||||||
|
// The options are optional.
|
||||||
|
func (c *Client) Append(mailbox string, size int64, options *imap.AppendOptions) *AppendCommand {
|
||||||
|
cmd := &AppendCommand{}
|
||||||
|
cmd.enc = c.beginCommand("APPEND", cmd)
|
||||||
|
cmd.enc.SP().Mailbox(mailbox).SP()
|
||||||
|
if options != nil && len(options.Flags) > 0 {
|
||||||
|
cmd.enc.List(len(options.Flags), func(i int) {
|
||||||
|
cmd.enc.Flag(options.Flags[i])
|
||||||
|
}).SP()
|
||||||
|
}
|
||||||
|
if options != nil && !options.Time.IsZero() {
|
||||||
|
cmd.enc.String(options.Time.Format(internal.DateTimeLayout)).SP()
|
||||||
|
}
|
||||||
|
// TODO: literal8 for BINARY
|
||||||
|
// TODO: UTF8 data ext for UTF8=ACCEPT, with literal8
|
||||||
|
cmd.wc = cmd.enc.Literal(size)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendCommand is an APPEND command.
|
||||||
|
//
|
||||||
|
// Callers must write the message contents, then call Close.
|
||||||
|
type AppendCommand struct {
|
||||||
|
commandBase
|
||||||
|
enc *commandEncoder
|
||||||
|
wc io.WriteCloser
|
||||||
|
data imap.AppendData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *AppendCommand) Write(b []byte) (int, error) {
|
||||||
|
return cmd.wc.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *AppendCommand) Close() error {
|
||||||
|
err := cmd.wc.Close()
|
||||||
|
if cmd.enc != nil {
|
||||||
|
cmd.enc.end()
|
||||||
|
cmd.enc = nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *AppendCommand) Wait() (*imap.AppendData, error) {
|
||||||
|
return &cmd.data, cmd.wait()
|
||||||
|
}
|
||||||
28
imapclient/append_test.go
Normal file
28
imapclient/append_test.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package imapclient_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAppend(t *testing.T) {
|
||||||
|
client, server := newClientServerPair(t, imap.ConnStateSelected)
|
||||||
|
defer client.Close()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
body := "This is a test message."
|
||||||
|
|
||||||
|
appendCmd := client.Append("INBOX", int64(len(body)), nil)
|
||||||
|
if _, err := appendCmd.Write([]byte(body)); err != nil {
|
||||||
|
t.Fatalf("AppendCommand.Write() = %v", err)
|
||||||
|
}
|
||||||
|
if err := appendCmd.Close(); err != nil {
|
||||||
|
t.Fatalf("AppendCommand.Close() = %v", err)
|
||||||
|
}
|
||||||
|
if _, err := appendCmd.Wait(); err != nil {
|
||||||
|
t.Fatalf("AppendCommand.Wait() = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: fetch back message and check body
|
||||||
|
}
|
||||||
100
imapclient/authenticate.go
Normal file
100
imapclient/authenticate.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
package imapclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/emersion/go-sasl"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Authenticate sends an AUTHENTICATE command.
|
||||||
|
//
|
||||||
|
// Unlike other commands, this method blocks until the SASL exchange completes.
|
||||||
|
func (c *Client) Authenticate(saslClient sasl.Client) error {
|
||||||
|
mech, initialResp, err := saslClient.Start()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// c.Caps may send a CAPABILITY command, so check it before c.beginCommand
|
||||||
|
var hasSASLIR bool
|
||||||
|
if initialResp != nil {
|
||||||
|
hasSASLIR = c.Caps().Has(imap.CapSASLIR)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := &authenticateCommand{}
|
||||||
|
contReq := c.registerContReq(cmd)
|
||||||
|
enc := c.beginCommand("AUTHENTICATE", cmd)
|
||||||
|
enc.SP().Atom(mech)
|
||||||
|
if initialResp != nil && hasSASLIR {
|
||||||
|
enc.SP().Atom(internal.EncodeSASL(initialResp))
|
||||||
|
initialResp = nil
|
||||||
|
}
|
||||||
|
enc.flush()
|
||||||
|
defer enc.end()
|
||||||
|
|
||||||
|
for {
|
||||||
|
challengeStr, err := contReq.Wait()
|
||||||
|
if err != nil {
|
||||||
|
return cmd.wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
if challengeStr == "" {
|
||||||
|
if initialResp == nil {
|
||||||
|
return fmt.Errorf("imapclient: server requested SASL initial response, but we don't have one")
|
||||||
|
}
|
||||||
|
|
||||||
|
contReq = c.registerContReq(cmd)
|
||||||
|
if err := c.writeSASLResp(initialResp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
initialResp = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
challenge, err := internal.DecodeSASL(challengeStr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := saslClient.Next(challenge)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
contReq = c.registerContReq(cmd)
|
||||||
|
if err := c.writeSASLResp(resp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type authenticateCommand struct {
|
||||||
|
commandBase
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) writeSASLResp(resp []byte) error {
|
||||||
|
respStr := internal.EncodeSASL(resp)
|
||||||
|
if _, err := c.bw.WriteString(respStr + "\r\n"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.bw.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unauthenticate sends an UNAUTHENTICATE command.
|
||||||
|
//
|
||||||
|
// This command requires support for the UNAUTHENTICATE extension.
|
||||||
|
func (c *Client) Unauthenticate() *Command {
|
||||||
|
cmd := &unauthenticateCommand{}
|
||||||
|
c.beginCommand("UNAUTHENTICATE", cmd).end()
|
||||||
|
return &cmd.Command
|
||||||
|
}
|
||||||
|
|
||||||
|
type unauthenticateCommand struct {
|
||||||
|
Command
|
||||||
|
}
|
||||||
24
imapclient/authenticate_test.go
Normal file
24
imapclient/authenticate_test.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package imapclient_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/emersion/go-sasl"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClient_Authenticate(t *testing.T) {
|
||||||
|
client, server := newClientServerPair(t, imap.ConnStateNotAuthenticated)
|
||||||
|
defer client.Close()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
saslClient := sasl.NewPlainClient("", testUsername, testPassword)
|
||||||
|
if err := client.Authenticate(saslClient); err != nil {
|
||||||
|
t.Fatalf("Authenticate() = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if state := client.State(); state != imap.ConnStateAuthenticated {
|
||||||
|
t.Errorf("State() = %v, want %v", state, imap.ConnStateAuthenticated)
|
||||||
|
}
|
||||||
|
}
|
||||||
56
imapclient/capability.go
Normal file
56
imapclient/capability.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package imapclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Capability sends a CAPABILITY command.
|
||||||
|
func (c *Client) Capability() *CapabilityCommand {
|
||||||
|
cmd := &CapabilityCommand{}
|
||||||
|
c.beginCommand("CAPABILITY", cmd).end()
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) handleCapability() error {
|
||||||
|
caps, err := readCapabilities(c.dec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.setCaps(caps)
|
||||||
|
if cmd := findPendingCmdByType[*CapabilityCommand](c); cmd != nil {
|
||||||
|
cmd.caps = caps
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CapabilityCommand is a CAPABILITY command.
|
||||||
|
type CapabilityCommand struct {
|
||||||
|
commandBase
|
||||||
|
caps imap.CapSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *CapabilityCommand) Wait() (imap.CapSet, error) {
|
||||||
|
err := cmd.wait()
|
||||||
|
return cmd.caps, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func readCapabilities(dec *imapwire.Decoder) (imap.CapSet, error) {
|
||||||
|
caps := make(imap.CapSet)
|
||||||
|
for dec.SP() {
|
||||||
|
// Some IMAP servers send multiple SP between caps:
|
||||||
|
// https://github.com/emersion/go-imap/pull/652
|
||||||
|
for dec.SP() {
|
||||||
|
}
|
||||||
|
|
||||||
|
cap, err := internal.ExpectCap(dec)
|
||||||
|
if err != nil {
|
||||||
|
return caps, fmt.Errorf("in capability-data: %w", err)
|
||||||
|
}
|
||||||
|
caps[cap] = struct{}{}
|
||||||
|
}
|
||||||
|
return caps, nil
|
||||||
|
}
|
||||||
1243
imapclient/client.go
Normal file
1243
imapclient/client.go
Normal file
File diff suppressed because it is too large
Load Diff
277
imapclient/client_test.go
Normal file
277
imapclient/client_test.go
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
package imapclient_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/imapclient"
|
||||||
|
"github.com/emersion/go-imap/v2/imapserver"
|
||||||
|
"github.com/emersion/go-imap/v2/imapserver/imapmemserver"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
testUsername = "test-user"
|
||||||
|
testPassword = "test-password"
|
||||||
|
)
|
||||||
|
|
||||||
|
const simpleRawMessage = `MIME-Version: 1.0
|
||||||
|
Message-Id: <191101702316132@example.com>
|
||||||
|
Content-Transfer-Encoding: 8bit
|
||||||
|
Content-Type: text/plain; charset=utf-8
|
||||||
|
|
||||||
|
This is my letter!`
|
||||||
|
|
||||||
|
var rsaCertPEM = `-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDOTCCAiGgAwIBAgIQSRJrEpBGFc7tNb1fb5pKFzANBgkqhkiG9w0BAQsFADAS
|
||||||
|
MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw
|
||||||
|
MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
|
||||||
|
MIIBCgKCAQEA6Gba5tHV1dAKouAaXO3/ebDUU4rvwCUg/CNaJ2PT5xLD4N1Vcb8r
|
||||||
|
bFSW2HXKq+MPfVdwIKR/1DczEoAGf/JWQTW7EgzlXrCd3rlajEX2D73faWJekD0U
|
||||||
|
aUgz5vtrTXZ90BQL7WvRICd7FlEZ6FPOcPlumiyNmzUqtwGhO+9ad1W5BqJaRI6P
|
||||||
|
YfouNkwR6Na4TzSj5BrqUfP0FwDizKSJ0XXmh8g8G9mtwxOSN3Ru1QFc61Xyeluk
|
||||||
|
POGKBV/q6RBNklTNe0gI8usUMlYyoC7ytppNMW7X2vodAelSu25jgx2anj9fDVZu
|
||||||
|
h7AXF5+4nJS4AAt0n1lNY7nGSsdZas8PbQIDAQABo4GIMIGFMA4GA1UdDwEB/wQE
|
||||||
|
AwICpDATBgNVHSUEDDAKBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud
|
||||||
|
DgQWBBStsdjh3/JCXXYlQryOrL4Sh7BW5TAuBgNVHREEJzAlggtleGFtcGxlLmNv
|
||||||
|
bYcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG9w0BAQsFAAOCAQEAxWGI
|
||||||
|
5NhpF3nwwy/4yB4i/CwwSpLrWUa70NyhvprUBC50PxiXav1TeDzwzLx/o5HyNwsv
|
||||||
|
cxv3HdkLW59i/0SlJSrNnWdfZ19oTcS+6PtLoVyISgtyN6DpkKpdG1cOkW3Cy2P2
|
||||||
|
+tK/tKHRP1Y/Ra0RiDpOAmqn0gCOFGz8+lqDIor/T7MTpibL3IxqWfPrvfVRHL3B
|
||||||
|
grw/ZQTTIVjjh4JBSW3WyWgNo/ikC1lrVxzl4iPUGptxT36Cr7Zk2Bsg0XqwbOvK
|
||||||
|
5d+NTDREkSnUbie4GeutujmX3Dsx88UiV6UY/4lHJa6I5leHUNOHahRbpbWeOfs/
|
||||||
|
WkBKOclmOV2xlTVuPw==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
`
|
||||||
|
|
||||||
|
var rsaKeyPEM = `-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDoZtrm0dXV0Aqi
|
||||||
|
4Bpc7f95sNRTiu/AJSD8I1onY9PnEsPg3VVxvytsVJbYdcqr4w99V3AgpH/UNzMS
|
||||||
|
gAZ/8lZBNbsSDOVesJ3euVqMRfYPvd9pYl6QPRRpSDPm+2tNdn3QFAvta9EgJ3sW
|
||||||
|
URnoU85w+W6aLI2bNSq3AaE771p3VbkGolpEjo9h+i42TBHo1rhPNKPkGupR8/QX
|
||||||
|
AOLMpInRdeaHyDwb2a3DE5I3dG7VAVzrVfJ6W6Q84YoFX+rpEE2SVM17SAjy6xQy
|
||||||
|
VjKgLvK2mk0xbtfa+h0B6VK7bmODHZqeP18NVm6HsBcXn7iclLgAC3SfWU1jucZK
|
||||||
|
x1lqzw9tAgMBAAECggEABWzxS1Y2wckblnXY57Z+sl6YdmLV+gxj2r8Qib7g4ZIk
|
||||||
|
lIlWR1OJNfw7kU4eryib4fc6nOh6O4AWZyYqAK6tqNQSS/eVG0LQTLTTEldHyVJL
|
||||||
|
dvBe+MsUQOj4nTndZW+QvFzbcm2D8lY5n2nBSxU5ypVoKZ1EqQzytFcLZpTN7d89
|
||||||
|
EPj0qDyrV4NZlWAwL1AygCwnlwhMQjXEalVF1ylXwU3QzyZ/6MgvF6d3SSUlh+sq
|
||||||
|
XefuyigXw484cQQgbzopv6niMOmGP3of+yV4JQqUSb3IDmmT68XjGd2Dkxl4iPki
|
||||||
|
6ZwXf3CCi+c+i/zVEcufgZ3SLf8D99kUGE7v7fZ6AQKBgQD1ZX3RAla9hIhxCf+O
|
||||||
|
3D+I1j2LMrdjAh0ZKKqwMR4JnHX3mjQI6LwqIctPWTU8wYFECSh9klEclSdCa64s
|
||||||
|
uI/GNpcqPXejd0cAAdqHEEeG5sHMDt0oFSurL4lyud0GtZvwlzLuwEweuDtvT9cJ
|
||||||
|
Wfvl86uyO36IW8JdvUprYDctrQKBgQDycZ697qutBieZlGkHpnYWUAeImVA878sJ
|
||||||
|
w44NuXHvMxBPz+lbJGAg8Cn8fcxNAPqHIraK+kx3po8cZGQywKHUWsxi23ozHoxo
|
||||||
|
+bGqeQb9U661TnfdDspIXia+xilZt3mm5BPzOUuRqlh4Y9SOBpSWRmEhyw76w4ZP
|
||||||
|
OPxjWYAgwQKBgA/FehSYxeJgRjSdo+MWnK66tjHgDJE8bYpUZsP0JC4R9DL5oiaA
|
||||||
|
brd2fI6Y+SbyeNBallObt8LSgzdtnEAbjIH8uDJqyOmknNePRvAvR6mP4xyuR+Bv
|
||||||
|
m+Lgp0DMWTw5J9CKpydZDItc49T/mJ5tPhdFVd+am0NAQnmr1MCZ6nHxAoGABS3Y
|
||||||
|
LkaC9FdFUUqSU8+Chkd/YbOkuyiENdkvl6t2e52jo5DVc1T7mLiIrRQi4SI8N9bN
|
||||||
|
/3oJWCT+uaSLX2ouCtNFunblzWHBrhxnZzTeqVq4SLc8aESAnbslKL4i8/+vYZlN
|
||||||
|
s8xtiNcSvL+lMsOBORSXzpj/4Ot8WwTkn1qyGgECgYBKNTypzAHeLE6yVadFp3nQ
|
||||||
|
Ckq9yzvP/ib05rvgbvrne00YeOxqJ9gtTrzgh7koqJyX1L4NwdkEza4ilDWpucn0
|
||||||
|
xiUZS4SoaJq6ZvcBYS62Yr1t8n09iG47YL8ibgtmH3L+svaotvpVxVK+d7BLevA/
|
||||||
|
ZboOWVe3icTy64BT3OQhmg==
|
||||||
|
-----END RSA PRIVATE KEY-----
|
||||||
|
`
|
||||||
|
|
||||||
|
func newMemClientServerPair(t *testing.T) (net.Conn, io.Closer) {
|
||||||
|
memServer := imapmemserver.New()
|
||||||
|
|
||||||
|
user := imapmemserver.NewUser(testUsername, testPassword)
|
||||||
|
user.Create("INBOX", nil)
|
||||||
|
|
||||||
|
memServer.AddUser(user)
|
||||||
|
|
||||||
|
cert, err := tls.X509KeyPair([]byte(rsaCertPEM), []byte(rsaKeyPEM))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("tls.X509KeyPair() = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
server := imapserver.New(&imapserver.Options{
|
||||||
|
NewSession: func(conn *imapserver.Conn) (imapserver.Session, *imapserver.GreetingData, error) {
|
||||||
|
return memServer.NewSession(), nil, nil
|
||||||
|
},
|
||||||
|
TLSConfig: &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{cert},
|
||||||
|
},
|
||||||
|
InsecureAuth: true,
|
||||||
|
Caps: imap.CapSet{
|
||||||
|
imap.CapIMAP4rev1: {},
|
||||||
|
imap.CapIMAP4rev2: {},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
ln, err := net.Listen("tcp", "localhost:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("net.Listen() = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := server.Serve(ln); err != nil {
|
||||||
|
t.Errorf("Serve() = %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
conn, err := net.Dial("tcp", ln.Addr().String())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("net.Dial() = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn, server
|
||||||
|
}
|
||||||
|
|
||||||
|
func newClientServerPair(t *testing.T, initialState imap.ConnState) (*imapclient.Client, io.Closer) {
|
||||||
|
var useDovecot bool
|
||||||
|
switch os.Getenv("GOIMAP_TEST_DOVECOT") {
|
||||||
|
case "0", "":
|
||||||
|
// ok
|
||||||
|
case "1":
|
||||||
|
useDovecot = true
|
||||||
|
default:
|
||||||
|
t.Fatalf("invalid GOIMAP_TEST_DOVECOT env var")
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
conn net.Conn
|
||||||
|
server io.Closer
|
||||||
|
)
|
||||||
|
if useDovecot {
|
||||||
|
if initialState < imap.ConnStateAuthenticated {
|
||||||
|
t.Skip("Dovecot connections are pre-authenticated")
|
||||||
|
}
|
||||||
|
conn, server = newDovecotClientServerPair(t)
|
||||||
|
} else {
|
||||||
|
conn, server = newMemClientServerPair(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
var debugWriter swapWriter
|
||||||
|
debugWriter.Swap(io.Discard)
|
||||||
|
|
||||||
|
var options imapclient.Options
|
||||||
|
if testing.Verbose() {
|
||||||
|
options.DebugWriter = &debugWriter
|
||||||
|
}
|
||||||
|
client := imapclient.New(conn, &options)
|
||||||
|
|
||||||
|
if initialState >= imap.ConnStateAuthenticated {
|
||||||
|
// Dovecot connections are pre-authenticated
|
||||||
|
if !useDovecot {
|
||||||
|
if err := client.Login(testUsername, testPassword).Wait(); err != nil {
|
||||||
|
t.Fatalf("Login().Wait() = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appendCmd := client.Append("INBOX", int64(len(simpleRawMessage)), nil)
|
||||||
|
appendCmd.Write([]byte(simpleRawMessage))
|
||||||
|
appendCmd.Close()
|
||||||
|
if _, err := appendCmd.Wait(); err != nil {
|
||||||
|
t.Fatalf("AppendCommand.Wait() = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if initialState >= imap.ConnStateSelected {
|
||||||
|
if _, err := client.Select("INBOX", nil).Wait(); err != nil {
|
||||||
|
t.Fatalf("Select().Wait() = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Turn on debug logs after we're done initializing the test
|
||||||
|
debugWriter.Swap(os.Stderr)
|
||||||
|
|
||||||
|
return client, server
|
||||||
|
}
|
||||||
|
|
||||||
|
// swapWriter is an io.Writer which can be swapped at runtime.
|
||||||
|
type swapWriter struct {
|
||||||
|
w io.Writer
|
||||||
|
mutex sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sw *swapWriter) Write(b []byte) (int, error) {
|
||||||
|
sw.mutex.Lock()
|
||||||
|
w := sw.w
|
||||||
|
sw.mutex.Unlock()
|
||||||
|
|
||||||
|
return w.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sw *swapWriter) Swap(w io.Writer) {
|
||||||
|
sw.mutex.Lock()
|
||||||
|
sw.w = w
|
||||||
|
sw.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogin(t *testing.T) {
|
||||||
|
client, server := newClientServerPair(t, imap.ConnStateNotAuthenticated)
|
||||||
|
defer client.Close()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
if err := client.Login(testUsername, testPassword).Wait(); err != nil {
|
||||||
|
t.Errorf("Login().Wait() = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogout(t *testing.T) {
|
||||||
|
client, server := newClientServerPair(t, imap.ConnStateAuthenticated)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
if _, ok := server.(*dovecotServer); ok {
|
||||||
|
t.Skip("Dovecot connections don't reply to LOGOUT")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.Logout().Wait(); err != nil {
|
||||||
|
t.Errorf("Logout().Wait() = %v", err)
|
||||||
|
}
|
||||||
|
if err := client.Close(); err != nil {
|
||||||
|
t.Errorf("Close() = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/emersion/go-imap/issues/562
|
||||||
|
func TestFetch_invalid(t *testing.T) {
|
||||||
|
client, server := newClientServerPair(t, imap.ConnStateSelected)
|
||||||
|
defer client.Close()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
_, err := client.Fetch(imap.UIDSet(nil), nil).Collect()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("UIDFetch().Collect() = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetch_closeUnreadBody(t *testing.T) {
|
||||||
|
client, server := newClientServerPair(t, imap.ConnStateSelected)
|
||||||
|
defer client.Close()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
fetchCmd := client.Fetch(imap.SeqSetNum(1), &imap.FetchOptions{
|
||||||
|
BodySection: []*imap.FetchItemBodySection{
|
||||||
|
{
|
||||||
|
Specifier: imap.PartSpecifierNone,
|
||||||
|
Peek: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err := fetchCmd.Close(); err != nil {
|
||||||
|
t.Fatalf("UIDFetch().Close() = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWaitGreeting_eof(t *testing.T) {
|
||||||
|
// bad server: connected but without greeting
|
||||||
|
clientConn, serverConn := net.Pipe()
|
||||||
|
|
||||||
|
client := imapclient.New(clientConn, nil)
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
if err := serverConn.Close(); err != nil {
|
||||||
|
t.Fatalf("serverConn.Close() = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.WaitGreeting(); err == nil {
|
||||||
|
t.Fatalf("WaitGreeting() should fail")
|
||||||
|
}
|
||||||
|
}
|
||||||
37
imapclient/connection_test.go
Normal file
37
imapclient/connection_test.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package imapclient_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestClient_Closed tests that the Closed() channel is closed when the
|
||||||
|
// connection is explicitly closed via Close().
|
||||||
|
func TestClient_Closed(t *testing.T) {
|
||||||
|
client, server := newClientServerPair(t, imap.ConnStateAuthenticated)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
closedCh := client.Closed()
|
||||||
|
if closedCh == nil {
|
||||||
|
t.Fatal("Closed() returned nil channel")
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-closedCh:
|
||||||
|
t.Fatal("Closed() channel closed before calling Close()")
|
||||||
|
default: // Expected
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.Close(); err != nil {
|
||||||
|
t.Fatalf("Close() = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-closedCh:
|
||||||
|
t.Log("Closed() channel properly closed after Close()")
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("Closed() channel not closed after Close()")
|
||||||
|
}
|
||||||
|
}
|
||||||
37
imapclient/copy.go
Normal file
37
imapclient/copy.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package imapclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Copy sends a COPY command.
|
||||||
|
func (c *Client) Copy(numSet imap.NumSet, mailbox string) *CopyCommand {
|
||||||
|
cmd := &CopyCommand{}
|
||||||
|
enc := c.beginCommand(uidCmdName("COPY", imapwire.NumSetKind(numSet)), cmd)
|
||||||
|
enc.SP().NumSet(numSet).SP().Mailbox(mailbox)
|
||||||
|
enc.end()
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyCommand is a COPY command.
|
||||||
|
type CopyCommand struct {
|
||||||
|
commandBase
|
||||||
|
data imap.CopyData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *CopyCommand) Wait() (*imap.CopyData, error) {
|
||||||
|
return &cmd.data, cmd.wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func readRespCodeCopyUID(dec *imapwire.Decoder) (uidValidity uint32, srcUIDs, dstUIDs imap.UIDSet, err error) {
|
||||||
|
if !dec.ExpectNumber(&uidValidity) || !dec.ExpectSP() || !dec.ExpectUIDSet(&srcUIDs) || !dec.ExpectSP() || !dec.ExpectUIDSet(&dstUIDs) {
|
||||||
|
return 0, nil, nil, dec.Err()
|
||||||
|
}
|
||||||
|
if srcUIDs.Dynamic() || dstUIDs.Dynamic() {
|
||||||
|
return 0, nil, nil, fmt.Errorf("imapclient: server returned dynamic number set in COPYUID response")
|
||||||
|
}
|
||||||
|
return uidValidity, srcUIDs, dstUIDs, nil
|
||||||
|
}
|
||||||
21
imapclient/create.go
Normal file
21
imapclient/create.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package imapclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create sends a CREATE command.
|
||||||
|
//
|
||||||
|
// A nil options pointer is equivalent to a zero options value.
|
||||||
|
func (c *Client) Create(mailbox string, options *imap.CreateOptions) *Command {
|
||||||
|
cmd := &Command{}
|
||||||
|
enc := c.beginCommand("CREATE", cmd)
|
||||||
|
enc.SP().Mailbox(mailbox)
|
||||||
|
if options != nil && len(options.SpecialUse) > 0 {
|
||||||
|
enc.SP().Special('(').Atom("USE").SP().List(len(options.SpecialUse), func(i int) {
|
||||||
|
enc.MailboxAttr(options.SpecialUse[i])
|
||||||
|
}).Special(')')
|
||||||
|
}
|
||||||
|
enc.end()
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
57
imapclient/create_test.go
Normal file
57
imapclient/create_test.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package imapclient_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testCreate(t *testing.T, name string, utf8Accept bool) {
|
||||||
|
client, server := newClientServerPair(t, imap.ConnStateAuthenticated)
|
||||||
|
defer client.Close()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
if utf8Accept {
|
||||||
|
if !client.Caps().Has(imap.CapUTF8Accept) {
|
||||||
|
t.Skipf("missing UTF8=ACCEPT support")
|
||||||
|
}
|
||||||
|
if data, err := client.Enable(imap.CapUTF8Accept).Wait(); err != nil {
|
||||||
|
t.Fatalf("Enable(CapUTF8Accept) = %v", err)
|
||||||
|
} else if !data.Caps.Has(imap.CapUTF8Accept) {
|
||||||
|
t.Fatalf("server refused to enable UTF8=ACCEPT")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.Create(name, nil).Wait(); err != nil {
|
||||||
|
t.Fatalf("Create() = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
listCmd := client.List("", name, nil)
|
||||||
|
mailboxes, err := listCmd.Collect()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("List() = %v", err)
|
||||||
|
} else if len(mailboxes) != 1 || mailboxes[0].Mailbox != name {
|
||||||
|
t.Errorf("List() = %v, want exactly one entry with correct name", mailboxes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreate(t *testing.T) {
|
||||||
|
t.Run("basic", func(t *testing.T) {
|
||||||
|
testCreate(t, "Test mailbox", false)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("unicode_utf7", func(t *testing.T) {
|
||||||
|
testCreate(t, "Cafè", false)
|
||||||
|
})
|
||||||
|
t.Run("unicode_utf8", func(t *testing.T) {
|
||||||
|
testCreate(t, "Cafè", true)
|
||||||
|
})
|
||||||
|
|
||||||
|
// '&' is the UTF-7 escape character
|
||||||
|
t.Run("ampersand_utf7", func(t *testing.T) {
|
||||||
|
testCreate(t, "Angus & Julia", false)
|
||||||
|
})
|
||||||
|
t.Run("ampersand_utf8", func(t *testing.T) {
|
||||||
|
testCreate(t, "Angus & Julia", true)
|
||||||
|
})
|
||||||
|
}
|
||||||
72
imapclient/dovecot_test.go
Normal file
72
imapclient/dovecot_test.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package imapclient_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newDovecotClientServerPair(t *testing.T) (net.Conn, io.Closer) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
cfgFilename := filepath.Join(tempDir, "dovecot.conf")
|
||||||
|
cfg := `dovecot_config_version = 2.4.0
|
||||||
|
dovecot_storage_version = 2.4.0
|
||||||
|
|
||||||
|
log_path = "` + tempDir + `/dovecot.log"
|
||||||
|
ssl = no
|
||||||
|
mail_home = "` + tempDir + `/%{user}"
|
||||||
|
mail_driver = maildir
|
||||||
|
mail_path = "~/Mail"
|
||||||
|
|
||||||
|
namespace inbox {
|
||||||
|
separator = /
|
||||||
|
prefix =
|
||||||
|
inbox = yes
|
||||||
|
}
|
||||||
|
|
||||||
|
mail_plugins {
|
||||||
|
acl = yes
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol imap {
|
||||||
|
mail_plugins {
|
||||||
|
imap_acl = yes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
acl_driver = vfile
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(cfgFilename, []byte(cfg), 0666); err != nil {
|
||||||
|
t.Fatalf("failed to write Dovecot config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
clientConn, serverConn := net.Pipe()
|
||||||
|
|
||||||
|
cmd := exec.Command("doveadm", "-c", cfgFilename, "exec", "imap")
|
||||||
|
cmd.Env = []string{"USER=" + testUsername, "PATH=" + os.Getenv("PATH")}
|
||||||
|
cmd.Dir = tempDir
|
||||||
|
cmd.Stdin = serverConn
|
||||||
|
cmd.Stdout = serverConn
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
t.Fatalf("failed to start Dovecot: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return clientConn, &dovecotServer{cmd, serverConn}
|
||||||
|
}
|
||||||
|
|
||||||
|
type dovecotServer struct {
|
||||||
|
cmd *exec.Cmd
|
||||||
|
conn net.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *dovecotServer) Close() error {
|
||||||
|
if err := srv.conn.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return srv.cmd.Wait()
|
||||||
|
}
|
||||||
69
imapclient/enable.go
Normal file
69
imapclient/enable.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package imapclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Enable sends an ENABLE command.
|
||||||
|
//
|
||||||
|
// This command requires support for IMAP4rev2 or the ENABLE extension.
|
||||||
|
func (c *Client) Enable(caps ...imap.Cap) *EnableCommand {
|
||||||
|
// Enabling an extension may change the IMAP syntax, so only allow the
|
||||||
|
// extensions we support here
|
||||||
|
for _, name := range caps {
|
||||||
|
switch name {
|
||||||
|
case imap.CapIMAP4rev2, imap.CapUTF8Accept, imap.CapMetadata, imap.CapMetadataServer:
|
||||||
|
// ok
|
||||||
|
default:
|
||||||
|
done := make(chan error)
|
||||||
|
close(done)
|
||||||
|
err := fmt.Errorf("imapclient: cannot enable %q: not supported", name)
|
||||||
|
return &EnableCommand{commandBase: commandBase{done: done, err: err}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := &EnableCommand{}
|
||||||
|
enc := c.beginCommand("ENABLE", cmd)
|
||||||
|
for _, c := range caps {
|
||||||
|
enc.SP().Atom(string(c))
|
||||||
|
}
|
||||||
|
enc.end()
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) handleEnabled() error {
|
||||||
|
caps, err := readCapabilities(c.dec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mutex.Lock()
|
||||||
|
for name := range caps {
|
||||||
|
c.enabled[name] = struct{}{}
|
||||||
|
}
|
||||||
|
c.mutex.Unlock()
|
||||||
|
|
||||||
|
if cmd := findPendingCmdByType[*EnableCommand](c); cmd != nil {
|
||||||
|
cmd.data.Caps = caps
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnableCommand is an ENABLE command.
|
||||||
|
type EnableCommand struct {
|
||||||
|
commandBase
|
||||||
|
data EnableData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *EnableCommand) Wait() (*EnableData, error) {
|
||||||
|
return &cmd.data, cmd.wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnableData is the data returned by the ENABLE command.
|
||||||
|
type EnableData struct {
|
||||||
|
// Capabilities that were successfully enabled
|
||||||
|
Caps imap.CapSet
|
||||||
|
}
|
||||||
411
imapclient/example_test.go
Normal file
411
imapclient/example_test.go
Normal file
@@ -0,0 +1,411 @@
|
|||||||
|
package imapclient_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/emersion/go-message/mail"
|
||||||
|
"github.com/emersion/go-sasl"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/imapclient"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleClient() {
|
||||||
|
c, err := imapclient.DialTLS("mail.example.org:993", nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to dial IMAP server: %v", err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
if err := c.Login("root", "asdf").Wait(); err != nil {
|
||||||
|
log.Fatalf("failed to login: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mailboxes, err := c.List("", "%", nil).Collect()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to list mailboxes: %v", err)
|
||||||
|
}
|
||||||
|
log.Printf("Found %v mailboxes", len(mailboxes))
|
||||||
|
for _, mbox := range mailboxes {
|
||||||
|
log.Printf(" - %v", mbox.Mailbox)
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedMbox, err := c.Select("INBOX", nil).Wait()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to select INBOX: %v", err)
|
||||||
|
}
|
||||||
|
log.Printf("INBOX contains %v messages", selectedMbox.NumMessages)
|
||||||
|
|
||||||
|
if selectedMbox.NumMessages > 0 {
|
||||||
|
seqSet := imap.SeqSetNum(1)
|
||||||
|
fetchOptions := &imap.FetchOptions{Envelope: true}
|
||||||
|
messages, err := c.Fetch(seqSet, fetchOptions).Collect()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to fetch first message in INBOX: %v", err)
|
||||||
|
}
|
||||||
|
log.Printf("subject of first message in INBOX: %v", messages[0].Envelope.Subject)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.Logout().Wait(); err != nil {
|
||||||
|
log.Fatalf("failed to logout: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleClient_pipelining() {
|
||||||
|
var c *imapclient.Client
|
||||||
|
|
||||||
|
uid := imap.UID(42)
|
||||||
|
fetchOptions := &imap.FetchOptions{Envelope: true}
|
||||||
|
|
||||||
|
// Login, select and fetch a message in a single roundtrip
|
||||||
|
loginCmd := c.Login("root", "root")
|
||||||
|
selectCmd := c.Select("INBOX", nil)
|
||||||
|
fetchCmd := c.Fetch(imap.UIDSetNum(uid), fetchOptions)
|
||||||
|
|
||||||
|
if err := loginCmd.Wait(); err != nil {
|
||||||
|
log.Fatalf("failed to login: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := selectCmd.Wait(); err != nil {
|
||||||
|
log.Fatalf("failed to select INBOX: %v", err)
|
||||||
|
}
|
||||||
|
if messages, err := fetchCmd.Collect(); err != nil {
|
||||||
|
log.Fatalf("failed to fetch message: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("Subject: %v", messages[0].Envelope.Subject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleClient_Append() {
|
||||||
|
var c *imapclient.Client
|
||||||
|
|
||||||
|
buf := []byte("From: <root@nsa.gov>\r\n\r\nHi <3")
|
||||||
|
size := int64(len(buf))
|
||||||
|
appendCmd := c.Append("INBOX", size, nil)
|
||||||
|
if _, err := appendCmd.Write(buf); err != nil {
|
||||||
|
log.Fatalf("failed to write message: %v", err)
|
||||||
|
}
|
||||||
|
if err := appendCmd.Close(); err != nil {
|
||||||
|
log.Fatalf("failed to close message: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := appendCmd.Wait(); err != nil {
|
||||||
|
log.Fatalf("APPEND command failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleClient_Status() {
|
||||||
|
var c *imapclient.Client
|
||||||
|
|
||||||
|
options := imap.StatusOptions{NumMessages: true}
|
||||||
|
if data, err := c.Status("INBOX", &options).Wait(); err != nil {
|
||||||
|
log.Fatalf("STATUS command failed: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("INBOX contains %v messages", *data.NumMessages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleClient_List_stream() {
|
||||||
|
var c *imapclient.Client
|
||||||
|
|
||||||
|
// ReturnStatus requires server support for IMAP4rev2 or LIST-STATUS
|
||||||
|
listCmd := c.List("", "%", &imap.ListOptions{
|
||||||
|
ReturnStatus: &imap.StatusOptions{
|
||||||
|
NumMessages: true,
|
||||||
|
NumUnseen: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
for {
|
||||||
|
mbox := listCmd.Next()
|
||||||
|
if mbox == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
log.Printf("Mailbox %q contains %v messages (%v unseen)", mbox.Mailbox, mbox.Status.NumMessages, mbox.Status.NumUnseen)
|
||||||
|
}
|
||||||
|
if err := listCmd.Close(); err != nil {
|
||||||
|
log.Fatalf("LIST command failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleClient_Store() {
|
||||||
|
var c *imapclient.Client
|
||||||
|
|
||||||
|
seqSet := imap.SeqSetNum(1)
|
||||||
|
storeFlags := imap.StoreFlags{
|
||||||
|
Op: imap.StoreFlagsAdd,
|
||||||
|
Flags: []imap.Flag{imap.FlagFlagged},
|
||||||
|
Silent: true,
|
||||||
|
}
|
||||||
|
if err := c.Store(seqSet, &storeFlags, nil).Close(); err != nil {
|
||||||
|
log.Fatalf("STORE command failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleClient_Fetch() {
|
||||||
|
var c *imapclient.Client
|
||||||
|
|
||||||
|
seqSet := imap.SeqSetNum(1)
|
||||||
|
bodySection := &imap.FetchItemBodySection{Specifier: imap.PartSpecifierHeader}
|
||||||
|
fetchOptions := &imap.FetchOptions{
|
||||||
|
Flags: true,
|
||||||
|
Envelope: true,
|
||||||
|
BodySection: []*imap.FetchItemBodySection{bodySection},
|
||||||
|
}
|
||||||
|
messages, err := c.Fetch(seqSet, fetchOptions).Collect()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("FETCH command failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := messages[0]
|
||||||
|
header := msg.FindBodySection(bodySection)
|
||||||
|
|
||||||
|
log.Printf("Flags: %v", msg.Flags)
|
||||||
|
log.Printf("Subject: %v", msg.Envelope.Subject)
|
||||||
|
log.Printf("Header:\n%v", string(header))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleClient_Fetch_streamBody() {
|
||||||
|
var c *imapclient.Client
|
||||||
|
|
||||||
|
seqSet := imap.SeqSetNum(1)
|
||||||
|
bodySection := &imap.FetchItemBodySection{}
|
||||||
|
fetchOptions := &imap.FetchOptions{
|
||||||
|
UID: true,
|
||||||
|
BodySection: []*imap.FetchItemBodySection{bodySection},
|
||||||
|
}
|
||||||
|
fetchCmd := c.Fetch(seqSet, fetchOptions)
|
||||||
|
defer fetchCmd.Close()
|
||||||
|
|
||||||
|
for {
|
||||||
|
msg := fetchCmd.Next()
|
||||||
|
if msg == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
item := msg.Next()
|
||||||
|
if item == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
switch item := item.(type) {
|
||||||
|
case imapclient.FetchItemDataUID:
|
||||||
|
log.Printf("UID: %v", item.UID)
|
||||||
|
case imapclient.FetchItemDataBodySection:
|
||||||
|
b, err := io.ReadAll(item.Literal)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to read body section: %v", err)
|
||||||
|
}
|
||||||
|
log.Printf("Body:\n%v", string(b))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fetchCmd.Close(); err != nil {
|
||||||
|
log.Fatalf("FETCH command failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleClient_Fetch_parseBody() {
|
||||||
|
var c *imapclient.Client
|
||||||
|
|
||||||
|
// Send a FETCH command to fetch the message body
|
||||||
|
seqSet := imap.SeqSetNum(1)
|
||||||
|
bodySection := &imap.FetchItemBodySection{}
|
||||||
|
fetchOptions := &imap.FetchOptions{
|
||||||
|
BodySection: []*imap.FetchItemBodySection{bodySection},
|
||||||
|
}
|
||||||
|
fetchCmd := c.Fetch(seqSet, fetchOptions)
|
||||||
|
defer fetchCmd.Close()
|
||||||
|
|
||||||
|
msg := fetchCmd.Next()
|
||||||
|
if msg == nil {
|
||||||
|
log.Fatalf("FETCH command did not return any message")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the body section in the response
|
||||||
|
var bodySectionData imapclient.FetchItemDataBodySection
|
||||||
|
ok := false
|
||||||
|
for {
|
||||||
|
item := msg.Next()
|
||||||
|
if item == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
bodySectionData, ok = item.(imapclient.FetchItemDataBodySection)
|
||||||
|
if ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
log.Fatalf("FETCH command did not return body section")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the message via the go-message library
|
||||||
|
mr, err := mail.CreateReader(bodySectionData.Literal)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to create mail reader: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print a few header fields
|
||||||
|
h := mr.Header
|
||||||
|
if date, err := h.Date(); err != nil {
|
||||||
|
log.Printf("failed to parse Date header field: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("Date: %v", date)
|
||||||
|
}
|
||||||
|
if to, err := h.AddressList("To"); err != nil {
|
||||||
|
log.Printf("failed to parse To header field: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("To: %v", to)
|
||||||
|
}
|
||||||
|
if subject, err := h.Text("Subject"); err != nil {
|
||||||
|
log.Printf("failed to parse Subject header field: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("Subject: %v", subject)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the message's parts
|
||||||
|
for {
|
||||||
|
p, err := mr.NextPart()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
log.Fatalf("failed to read message part: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch h := p.Header.(type) {
|
||||||
|
case *mail.InlineHeader:
|
||||||
|
// This is the message's text (can be plain-text or HTML)
|
||||||
|
b, _ := io.ReadAll(p.Body)
|
||||||
|
log.Printf("Inline text: %v", string(b))
|
||||||
|
case *mail.AttachmentHeader:
|
||||||
|
// This is an attachment
|
||||||
|
filename, _ := h.Filename()
|
||||||
|
log.Printf("Attachment: %v", filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fetchCmd.Close(); err != nil {
|
||||||
|
log.Fatalf("FETCH command failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleClient_Search() {
|
||||||
|
var c *imapclient.Client
|
||||||
|
|
||||||
|
data, err := c.UIDSearch(&imap.SearchCriteria{
|
||||||
|
Body: []string{"Hello world"},
|
||||||
|
}, nil).Wait()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("UID SEARCH command failed: %v", err)
|
||||||
|
}
|
||||||
|
log.Fatalf("UIDs matching the search criteria: %v", data.AllUIDs())
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleClient_Idle() {
|
||||||
|
options := imapclient.Options{
|
||||||
|
UnilateralDataHandler: &imapclient.UnilateralDataHandler{
|
||||||
|
Expunge: func(seqNum uint32) {
|
||||||
|
log.Printf("message %v has been expunged", seqNum)
|
||||||
|
},
|
||||||
|
Mailbox: func(data *imapclient.UnilateralDataMailbox) {
|
||||||
|
if data.NumMessages != nil {
|
||||||
|
log.Printf("a new message has been received")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := imapclient.DialTLS("mail.example.org:993", &options)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to dial IMAP server: %v", err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
if err := c.Login("root", "asdf").Wait(); err != nil {
|
||||||
|
log.Fatalf("failed to login: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := c.Select("INBOX", nil).Wait(); err != nil {
|
||||||
|
log.Fatalf("failed to select INBOX: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start idling
|
||||||
|
idleCmd, err := c.Idle()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("IDLE command failed: %v", err)
|
||||||
|
}
|
||||||
|
defer idleCmd.Close()
|
||||||
|
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
done <- idleCmd.Wait()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for 30s to receive updates from the server, then stop idling
|
||||||
|
t := time.NewTimer(30 * time.Second)
|
||||||
|
defer t.Stop()
|
||||||
|
select {
|
||||||
|
case <-t.C:
|
||||||
|
if err := idleCmd.Close(); err != nil {
|
||||||
|
log.Fatalf("failed to stop idling: %v", err)
|
||||||
|
}
|
||||||
|
if err := <-done; err != nil {
|
||||||
|
log.Fatalf("IDLE command failed: %v", err)
|
||||||
|
}
|
||||||
|
case err := <-done:
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("IDLE command failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleClient_Authenticate_oauth() {
|
||||||
|
var (
|
||||||
|
c *imapclient.Client
|
||||||
|
username string
|
||||||
|
token string
|
||||||
|
)
|
||||||
|
|
||||||
|
if !c.Caps().Has(imap.AuthCap(sasl.OAuthBearer)) {
|
||||||
|
log.Fatal("OAUTHBEARER not supported by the server")
|
||||||
|
}
|
||||||
|
|
||||||
|
saslClient := sasl.NewOAuthBearerClient(&sasl.OAuthBearerOptions{
|
||||||
|
Username: username,
|
||||||
|
Token: token,
|
||||||
|
})
|
||||||
|
if err := c.Authenticate(saslClient); err != nil {
|
||||||
|
log.Fatalf("authentication failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleClient_Closed() {
|
||||||
|
c, err := imapclient.DialTLS("mail.example.org:993", nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to dial IMAP server: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
selected := false
|
||||||
|
|
||||||
|
go func(c *imapclient.Client) {
|
||||||
|
if err := c.Login("root", "asdf").Wait(); err != nil {
|
||||||
|
log.Fatalf("failed to login: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := c.Select("INBOX", nil).Wait(); err != nil {
|
||||||
|
log.Fatalf("failed to select INBOX: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
selected = true
|
||||||
|
|
||||||
|
c.Close()
|
||||||
|
}(c)
|
||||||
|
|
||||||
|
// This channel shall be closed when the connection is closed.
|
||||||
|
<-c.Closed()
|
||||||
|
log.Println("Connection has been closed")
|
||||||
|
|
||||||
|
if !selected {
|
||||||
|
log.Fatalf("Connection was closed before selecting mailbox")
|
||||||
|
}
|
||||||
|
}
|
||||||
84
imapclient/expunge.go
Normal file
84
imapclient/expunge.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package imapclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Expunge sends an EXPUNGE command.
|
||||||
|
func (c *Client) Expunge() *ExpungeCommand {
|
||||||
|
cmd := &ExpungeCommand{seqNums: make(chan uint32, 128)}
|
||||||
|
c.beginCommand("EXPUNGE", cmd).end()
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// UIDExpunge sends a UID EXPUNGE command.
|
||||||
|
//
|
||||||
|
// This command requires support for IMAP4rev2 or the UIDPLUS extension.
|
||||||
|
func (c *Client) UIDExpunge(uids imap.UIDSet) *ExpungeCommand {
|
||||||
|
cmd := &ExpungeCommand{seqNums: make(chan uint32, 128)}
|
||||||
|
enc := c.beginCommand("UID EXPUNGE", cmd)
|
||||||
|
enc.SP().NumSet(uids)
|
||||||
|
enc.end()
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) handleExpunge(seqNum uint32) error {
|
||||||
|
c.mutex.Lock()
|
||||||
|
if c.state == imap.ConnStateSelected && c.mailbox.NumMessages > 0 {
|
||||||
|
c.mailbox = c.mailbox.copy()
|
||||||
|
c.mailbox.NumMessages--
|
||||||
|
}
|
||||||
|
c.mutex.Unlock()
|
||||||
|
|
||||||
|
cmd := findPendingCmdByType[*ExpungeCommand](c)
|
||||||
|
if cmd != nil {
|
||||||
|
cmd.seqNums <- seqNum
|
||||||
|
} else if handler := c.options.unilateralDataHandler().Expunge; handler != nil {
|
||||||
|
handler(seqNum)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpungeCommand is an EXPUNGE command.
|
||||||
|
//
|
||||||
|
// The caller must fully consume the ExpungeCommand. A simple way to do so is
|
||||||
|
// to defer a call to FetchCommand.Close.
|
||||||
|
type ExpungeCommand struct {
|
||||||
|
commandBase
|
||||||
|
seqNums chan uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next advances to the next expunged message sequence number.
|
||||||
|
//
|
||||||
|
// On success, the message sequence number is returned. On error or if there
|
||||||
|
// are no more messages, 0 is returned. To check the error value, use Close.
|
||||||
|
func (cmd *ExpungeCommand) Next() uint32 {
|
||||||
|
return <-cmd.seqNums
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close releases the command.
|
||||||
|
//
|
||||||
|
// Calling Close unblocks the IMAP client decoder and lets it read the next
|
||||||
|
// responses. Next will always return nil after Close.
|
||||||
|
func (cmd *ExpungeCommand) Close() error {
|
||||||
|
for cmd.Next() != 0 {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return cmd.wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect accumulates expunged sequence numbers into a list.
|
||||||
|
//
|
||||||
|
// This is equivalent to calling Next repeatedly and then Close.
|
||||||
|
func (cmd *ExpungeCommand) Collect() ([]uint32, error) {
|
||||||
|
var l []uint32
|
||||||
|
for {
|
||||||
|
seqNum := cmd.Next()
|
||||||
|
if seqNum == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
l = append(l, seqNum)
|
||||||
|
}
|
||||||
|
return l, cmd.Close()
|
||||||
|
}
|
||||||
36
imapclient/expunge_test.go
Normal file
36
imapclient/expunge_test.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package imapclient_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExpunge(t *testing.T) {
|
||||||
|
client, server := newClientServerPair(t, imap.ConnStateSelected)
|
||||||
|
defer client.Close()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
seqNums, err := client.Expunge().Collect()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Expunge() = %v", err)
|
||||||
|
} else if len(seqNums) != 0 {
|
||||||
|
t.Errorf("Expunge().Collect() = %v, want []", seqNums)
|
||||||
|
}
|
||||||
|
|
||||||
|
seqSet := imap.SeqSetNum(1)
|
||||||
|
storeFlags := imap.StoreFlags{
|
||||||
|
Op: imap.StoreFlagsAdd,
|
||||||
|
Flags: []imap.Flag{imap.FlagDeleted},
|
||||||
|
}
|
||||||
|
if err := client.Store(seqSet, &storeFlags, nil).Close(); err != nil {
|
||||||
|
t.Fatalf("Store() = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
seqNums, err = client.Expunge().Collect()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Expunge() = %v", err)
|
||||||
|
} else if len(seqNums) != 1 || seqNums[0] != 1 {
|
||||||
|
t.Errorf("Expunge().Collect() = %v, want [1]", seqNums)
|
||||||
|
}
|
||||||
|
}
|
||||||
1326
imapclient/fetch.go
Normal file
1326
imapclient/fetch.go
Normal file
File diff suppressed because it is too large
Load Diff
39
imapclient/fetch_test.go
Normal file
39
imapclient/fetch_test.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package imapclient_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFetch(t *testing.T) {
|
||||||
|
client, server := newClientServerPair(t, imap.ConnStateSelected)
|
||||||
|
defer client.Close()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
seqSet := imap.SeqSetNum(1)
|
||||||
|
bodySection := &imap.FetchItemBodySection{}
|
||||||
|
fetchOptions := &imap.FetchOptions{
|
||||||
|
BodySection: []*imap.FetchItemBodySection{bodySection},
|
||||||
|
}
|
||||||
|
messages, err := client.Fetch(seqSet, fetchOptions).Collect()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to fetch first message: %v", err)
|
||||||
|
} else if len(messages) != 1 {
|
||||||
|
t.Fatalf("len(messages) = %v, want 1", len(messages))
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := messages[0]
|
||||||
|
if len(msg.BodySection) != 1 {
|
||||||
|
t.Fatalf("len(msg.BodySection) = %v, want 1", len(msg.BodySection))
|
||||||
|
}
|
||||||
|
b := msg.FindBodySection(bodySection)
|
||||||
|
if b == nil {
|
||||||
|
t.Fatalf("FindBodySection() = nil")
|
||||||
|
}
|
||||||
|
body := strings.ReplaceAll(string(b), "\r\n", "\n")
|
||||||
|
if body != simpleRawMessage {
|
||||||
|
t.Errorf("body mismatch: got \n%v\n but want \n%v", body, simpleRawMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
163
imapclient/id.go
Normal file
163
imapclient/id.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package imapclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ID sends an ID command.
|
||||||
|
//
|
||||||
|
// The ID command is introduced in RFC 2971. It requires support for the ID
|
||||||
|
// extension.
|
||||||
|
//
|
||||||
|
// An example ID command:
|
||||||
|
//
|
||||||
|
// ID ("name" "go-imap" "version" "1.0" "os" "Linux" "os-version" "7.9.4" "vendor" "Yahoo")
|
||||||
|
func (c *Client) ID(idData *imap.IDData) *IDCommand {
|
||||||
|
cmd := &IDCommand{}
|
||||||
|
enc := c.beginCommand("ID", cmd)
|
||||||
|
|
||||||
|
if idData == nil {
|
||||||
|
enc.SP().NIL()
|
||||||
|
enc.end()
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
enc.SP().Special('(')
|
||||||
|
isFirstKey := true
|
||||||
|
if idData.Name != "" {
|
||||||
|
addIDKeyValue(enc, &isFirstKey, "name", idData.Name)
|
||||||
|
}
|
||||||
|
if idData.Version != "" {
|
||||||
|
addIDKeyValue(enc, &isFirstKey, "version", idData.Version)
|
||||||
|
}
|
||||||
|
if idData.OS != "" {
|
||||||
|
addIDKeyValue(enc, &isFirstKey, "os", idData.OS)
|
||||||
|
}
|
||||||
|
if idData.OSVersion != "" {
|
||||||
|
addIDKeyValue(enc, &isFirstKey, "os-version", idData.OSVersion)
|
||||||
|
}
|
||||||
|
if idData.Vendor != "" {
|
||||||
|
addIDKeyValue(enc, &isFirstKey, "vendor", idData.Vendor)
|
||||||
|
}
|
||||||
|
if idData.SupportURL != "" {
|
||||||
|
addIDKeyValue(enc, &isFirstKey, "support-url", idData.SupportURL)
|
||||||
|
}
|
||||||
|
if idData.Address != "" {
|
||||||
|
addIDKeyValue(enc, &isFirstKey, "address", idData.Address)
|
||||||
|
}
|
||||||
|
if idData.Date != "" {
|
||||||
|
addIDKeyValue(enc, &isFirstKey, "date", idData.Date)
|
||||||
|
}
|
||||||
|
if idData.Command != "" {
|
||||||
|
addIDKeyValue(enc, &isFirstKey, "command", idData.Command)
|
||||||
|
}
|
||||||
|
if idData.Arguments != "" {
|
||||||
|
addIDKeyValue(enc, &isFirstKey, "arguments", idData.Arguments)
|
||||||
|
}
|
||||||
|
if idData.Environment != "" {
|
||||||
|
addIDKeyValue(enc, &isFirstKey, "environment", idData.Environment)
|
||||||
|
}
|
||||||
|
|
||||||
|
enc.Special(')')
|
||||||
|
enc.end()
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func addIDKeyValue(enc *commandEncoder, isFirstKey *bool, key, value string) {
|
||||||
|
if isFirstKey == nil {
|
||||||
|
panic("isFirstKey cannot be nil")
|
||||||
|
} else if !*isFirstKey {
|
||||||
|
enc.SP().Quoted(key).SP().Quoted(value)
|
||||||
|
} else {
|
||||||
|
enc.Quoted(key).SP().Quoted(value)
|
||||||
|
}
|
||||||
|
*isFirstKey = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) handleID() error {
|
||||||
|
data, err := c.readID(c.dec)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("in id: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd := findPendingCmdByType[*IDCommand](c); cmd != nil {
|
||||||
|
cmd.data = *data
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) readID(dec *imapwire.Decoder) (*imap.IDData, error) {
|
||||||
|
var data = imap.IDData{}
|
||||||
|
|
||||||
|
if !dec.ExpectSP() {
|
||||||
|
return nil, dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
if dec.ExpectNIL() {
|
||||||
|
return &data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
currKey := ""
|
||||||
|
err := dec.ExpectList(func() error {
|
||||||
|
var keyOrValue string
|
||||||
|
if !dec.String(&keyOrValue) {
|
||||||
|
return fmt.Errorf("in id key-val list: %v", dec.Err())
|
||||||
|
}
|
||||||
|
|
||||||
|
if currKey == "" {
|
||||||
|
currKey = keyOrValue
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch currKey {
|
||||||
|
case "name":
|
||||||
|
data.Name = keyOrValue
|
||||||
|
case "version":
|
||||||
|
data.Version = keyOrValue
|
||||||
|
case "os":
|
||||||
|
data.OS = keyOrValue
|
||||||
|
case "os-version":
|
||||||
|
data.OSVersion = keyOrValue
|
||||||
|
case "vendor":
|
||||||
|
data.Vendor = keyOrValue
|
||||||
|
case "support-url":
|
||||||
|
data.SupportURL = keyOrValue
|
||||||
|
case "address":
|
||||||
|
data.Address = keyOrValue
|
||||||
|
case "date":
|
||||||
|
data.Date = keyOrValue
|
||||||
|
case "command":
|
||||||
|
data.Command = keyOrValue
|
||||||
|
case "arguments":
|
||||||
|
data.Arguments = keyOrValue
|
||||||
|
case "environment":
|
||||||
|
data.Environment = keyOrValue
|
||||||
|
default:
|
||||||
|
// Ignore unknown key
|
||||||
|
// Yahoo server sends "host" and "remote-host" keys
|
||||||
|
// which are not defined in RFC 2971
|
||||||
|
}
|
||||||
|
currKey = ""
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type IDCommand struct {
|
||||||
|
commandBase
|
||||||
|
data imap.IDData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *IDCommand) Wait() (*imap.IDData, error) {
|
||||||
|
return &r.data, r.wait()
|
||||||
|
}
|
||||||
157
imapclient/idle.go
Normal file
157
imapclient/idle.go
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
package imapclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const idleRestartInterval = 28 * time.Minute
|
||||||
|
|
||||||
|
// Idle sends an IDLE command.
|
||||||
|
//
|
||||||
|
// Unlike other commands, this method blocks until the server acknowledges it.
|
||||||
|
// On success, the IDLE command is running and other commands cannot be sent.
|
||||||
|
// The caller must invoke IdleCommand.Close to stop IDLE and unblock the
|
||||||
|
// client.
|
||||||
|
//
|
||||||
|
// This command requires support for IMAP4rev2 or the IDLE extension. The IDLE
|
||||||
|
// command is restarted automatically to avoid getting disconnected due to
|
||||||
|
// inactivity timeouts.
|
||||||
|
func (c *Client) Idle() (*IdleCommand, error) {
|
||||||
|
child, err := c.idle()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := &IdleCommand{
|
||||||
|
stop: make(chan struct{}),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
go cmd.run(c, child)
|
||||||
|
return cmd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IdleCommand is an IDLE command.
|
||||||
|
//
|
||||||
|
// Initially, the IDLE command is running. The server may send unilateral
|
||||||
|
// data. The client cannot send any command while IDLE is running.
|
||||||
|
//
|
||||||
|
// Close must be called to stop the IDLE command.
|
||||||
|
type IdleCommand struct {
|
||||||
|
stopped atomic.Bool
|
||||||
|
stop chan struct{}
|
||||||
|
done chan struct{}
|
||||||
|
|
||||||
|
err error
|
||||||
|
lastChild *idleCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *IdleCommand) run(c *Client, child *idleCommand) {
|
||||||
|
defer close(cmd.done)
|
||||||
|
|
||||||
|
timer := time.NewTimer(idleRestartInterval)
|
||||||
|
defer timer.Stop()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if child != nil {
|
||||||
|
if err := child.Close(); err != nil && cmd.err == nil {
|
||||||
|
cmd.err = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-timer.C:
|
||||||
|
timer.Reset(idleRestartInterval)
|
||||||
|
|
||||||
|
if cmd.err = child.Close(); cmd.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if child, cmd.err = c.idle(); cmd.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-c.decCh:
|
||||||
|
cmd.lastChild = child
|
||||||
|
return
|
||||||
|
case <-cmd.stop:
|
||||||
|
cmd.lastChild = child
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close stops the IDLE command.
|
||||||
|
//
|
||||||
|
// This method blocks until the command to stop IDLE is written, but doesn't
|
||||||
|
// wait for the server to respond. Callers can use Wait for this purpose.
|
||||||
|
func (cmd *IdleCommand) Close() error {
|
||||||
|
if cmd.stopped.Swap(true) {
|
||||||
|
return fmt.Errorf("imapclient: IDLE already closed")
|
||||||
|
}
|
||||||
|
close(cmd.stop)
|
||||||
|
<-cmd.done
|
||||||
|
return cmd.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait blocks until the IDLE command has completed.
|
||||||
|
func (cmd *IdleCommand) Wait() error {
|
||||||
|
<-cmd.done
|
||||||
|
if cmd.err != nil {
|
||||||
|
return cmd.err
|
||||||
|
}
|
||||||
|
return cmd.lastChild.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) idle() (*idleCommand, error) {
|
||||||
|
cmd := &idleCommand{}
|
||||||
|
contReq := c.registerContReq(cmd)
|
||||||
|
cmd.enc = c.beginCommand("IDLE", cmd)
|
||||||
|
cmd.enc.flush()
|
||||||
|
|
||||||
|
_, err := contReq.Wait()
|
||||||
|
if err != nil {
|
||||||
|
cmd.enc.end()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// idleCommand represents a singular IDLE command, without the restart logic.
|
||||||
|
type idleCommand struct {
|
||||||
|
commandBase
|
||||||
|
enc *commandEncoder
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close stops the IDLE command.
|
||||||
|
//
|
||||||
|
// This method blocks until the command to stop IDLE is written, but doesn't
|
||||||
|
// wait for the server to respond. Callers can use Wait for this purpose.
|
||||||
|
func (cmd *idleCommand) Close() error {
|
||||||
|
if cmd.err != nil {
|
||||||
|
return cmd.err
|
||||||
|
}
|
||||||
|
if cmd.enc == nil {
|
||||||
|
return fmt.Errorf("imapclient: IDLE command closed twice")
|
||||||
|
}
|
||||||
|
cmd.enc.client.setWriteTimeout(cmdWriteTimeout)
|
||||||
|
_, err := cmd.enc.client.bw.WriteString("DONE\r\n")
|
||||||
|
if err == nil {
|
||||||
|
err = cmd.enc.client.bw.Flush()
|
||||||
|
}
|
||||||
|
cmd.enc.end()
|
||||||
|
cmd.enc = nil
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait blocks until the IDLE command has completed.
|
||||||
|
//
|
||||||
|
// Wait can only be called after Close.
|
||||||
|
func (cmd *idleCommand) Wait() error {
|
||||||
|
if cmd.enc != nil {
|
||||||
|
panic("imapclient: idleCommand.Close must be called before Wait")
|
||||||
|
}
|
||||||
|
return cmd.wait()
|
||||||
|
}
|
||||||
42
imapclient/idle_test.go
Normal file
42
imapclient/idle_test.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package imapclient_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIdle(t *testing.T) {
|
||||||
|
client, server := newClientServerPair(t, imap.ConnStateSelected)
|
||||||
|
defer client.Close()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
idleCmd, err := client.Idle()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Idle() = %v", err)
|
||||||
|
}
|
||||||
|
// TODO: test unilateral updates
|
||||||
|
if err := idleCmd.Close(); err != nil {
|
||||||
|
t.Errorf("Close() = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIdle_closedConn(t *testing.T) {
|
||||||
|
client, server := newClientServerPair(t, imap.ConnStateSelected)
|
||||||
|
defer client.Close()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
idleCmd, err := client.Idle()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Idle() = %v", err)
|
||||||
|
}
|
||||||
|
defer idleCmd.Close()
|
||||||
|
|
||||||
|
if err := client.Close(); err != nil {
|
||||||
|
t.Fatalf("client.Close() = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := idleCmd.Wait(); err == nil {
|
||||||
|
t.Errorf("IdleCommand.Wait() = nil, want an error")
|
||||||
|
}
|
||||||
|
}
|
||||||
259
imapclient/list.go
Normal file
259
imapclient/list.go
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
package imapclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getSelectOpts(options *imap.ListOptions) []string {
|
||||||
|
if options == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var l []string
|
||||||
|
if options.SelectSubscribed {
|
||||||
|
l = append(l, "SUBSCRIBED")
|
||||||
|
}
|
||||||
|
if options.SelectRemote {
|
||||||
|
l = append(l, "REMOTE")
|
||||||
|
}
|
||||||
|
if options.SelectRecursiveMatch {
|
||||||
|
l = append(l, "RECURSIVEMATCH")
|
||||||
|
}
|
||||||
|
if options.SelectSpecialUse {
|
||||||
|
l = append(l, "SPECIAL-USE")
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func getReturnOpts(options *imap.ListOptions) []string {
|
||||||
|
if options == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var l []string
|
||||||
|
if options.ReturnSubscribed {
|
||||||
|
l = append(l, "SUBSCRIBED")
|
||||||
|
}
|
||||||
|
if options.ReturnChildren {
|
||||||
|
l = append(l, "CHILDREN")
|
||||||
|
}
|
||||||
|
if options.ReturnStatus != nil {
|
||||||
|
l = append(l, "STATUS")
|
||||||
|
}
|
||||||
|
if options.ReturnSpecialUse {
|
||||||
|
l = append(l, "SPECIAL-USE")
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
// List sends a LIST command.
|
||||||
|
//
|
||||||
|
// The caller must fully consume the ListCommand. A simple way to do so is to
|
||||||
|
// defer a call to ListCommand.Close.
|
||||||
|
//
|
||||||
|
// A nil options pointer is equivalent to a zero options value.
|
||||||
|
//
|
||||||
|
// A non-zero options value requires support for IMAP4rev2 or the LIST-EXTENDED
|
||||||
|
// extension.
|
||||||
|
func (c *Client) List(ref, pattern string, options *imap.ListOptions) *ListCommand {
|
||||||
|
cmd := &ListCommand{
|
||||||
|
mailboxes: make(chan *imap.ListData, 64),
|
||||||
|
returnStatus: options != nil && options.ReturnStatus != nil,
|
||||||
|
}
|
||||||
|
enc := c.beginCommand("LIST", cmd)
|
||||||
|
if selectOpts := getSelectOpts(options); len(selectOpts) > 0 {
|
||||||
|
enc.SP().List(len(selectOpts), func(i int) {
|
||||||
|
enc.Atom(selectOpts[i])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
enc.SP().Mailbox(ref).SP().Mailbox(pattern)
|
||||||
|
if returnOpts := getReturnOpts(options); len(returnOpts) > 0 {
|
||||||
|
enc.SP().Atom("RETURN").SP().List(len(returnOpts), func(i int) {
|
||||||
|
opt := returnOpts[i]
|
||||||
|
enc.Atom(opt)
|
||||||
|
if opt == "STATUS" {
|
||||||
|
returnStatus := statusItems(options.ReturnStatus)
|
||||||
|
enc.SP().List(len(returnStatus), func(j int) {
|
||||||
|
enc.Atom(returnStatus[j])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
enc.end()
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) handleList() error {
|
||||||
|
data, err := readList(c.dec)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("in LIST: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := c.findPendingCmdFunc(func(cmd command) bool {
|
||||||
|
switch cmd := cmd.(type) {
|
||||||
|
case *ListCommand:
|
||||||
|
return true // TODO: match pattern, check if already handled
|
||||||
|
case *SelectCommand:
|
||||||
|
return cmd.mailbox == data.Mailbox && cmd.data.List == nil
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
switch cmd := cmd.(type) {
|
||||||
|
case *ListCommand:
|
||||||
|
if cmd.returnStatus {
|
||||||
|
if cmd.pendingData != nil {
|
||||||
|
cmd.mailboxes <- cmd.pendingData
|
||||||
|
}
|
||||||
|
cmd.pendingData = data
|
||||||
|
} else {
|
||||||
|
cmd.mailboxes <- data
|
||||||
|
}
|
||||||
|
case *SelectCommand:
|
||||||
|
cmd.data.List = data
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListCommand is a LIST command.
|
||||||
|
type ListCommand struct {
|
||||||
|
commandBase
|
||||||
|
mailboxes chan *imap.ListData
|
||||||
|
|
||||||
|
returnStatus bool
|
||||||
|
pendingData *imap.ListData
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next advances to the next mailbox.
|
||||||
|
//
|
||||||
|
// On success, the mailbox LIST data is returned. On error or if there are no
|
||||||
|
// more mailboxes, nil is returned.
|
||||||
|
func (cmd *ListCommand) Next() *imap.ListData {
|
||||||
|
return <-cmd.mailboxes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close releases the command.
|
||||||
|
//
|
||||||
|
// Calling Close unblocks the IMAP client decoder and lets it read the next
|
||||||
|
// responses. Next will always return nil after Close.
|
||||||
|
func (cmd *ListCommand) Close() error {
|
||||||
|
for cmd.Next() != nil {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return cmd.wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect accumulates mailboxes into a list.
|
||||||
|
//
|
||||||
|
// This is equivalent to calling Next repeatedly and then Close.
|
||||||
|
func (cmd *ListCommand) Collect() ([]*imap.ListData, error) {
|
||||||
|
var l []*imap.ListData
|
||||||
|
for {
|
||||||
|
data := cmd.Next()
|
||||||
|
if data == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
l = append(l, data)
|
||||||
|
}
|
||||||
|
return l, cmd.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func readList(dec *imapwire.Decoder) (*imap.ListData, error) {
|
||||||
|
var data imap.ListData
|
||||||
|
|
||||||
|
var err error
|
||||||
|
data.Attrs, err = internal.ExpectMailboxAttrList(dec)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("in mbx-list-flags: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dec.ExpectSP() {
|
||||||
|
return nil, dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Delim, err = readDelim(dec)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dec.ExpectSP() || !dec.ExpectMailbox(&data.Mailbox) {
|
||||||
|
return nil, dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
if dec.SP() {
|
||||||
|
err := dec.ExpectList(func() error {
|
||||||
|
var tag string
|
||||||
|
if !dec.ExpectAString(&tag) || !dec.ExpectSP() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
switch strings.ToUpper(tag) {
|
||||||
|
case "CHILDINFO":
|
||||||
|
data.ChildInfo, err = readChildInfoExtendedItem(dec)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("in childinfo-extended-item: %v", err)
|
||||||
|
}
|
||||||
|
case "OLDNAME":
|
||||||
|
data.OldName, err = readOldNameExtendedItem(dec)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("in oldname-extended-item: %v", err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if !dec.DiscardValue() {
|
||||||
|
return fmt.Errorf("in tagged-ext-val: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("in mbox-list-extended: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readChildInfoExtendedItem(dec *imapwire.Decoder) (*imap.ListDataChildInfo, error) {
|
||||||
|
var childInfo imap.ListDataChildInfo
|
||||||
|
err := dec.ExpectList(func() error {
|
||||||
|
var opt string
|
||||||
|
if !dec.ExpectAString(&opt) {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
if strings.ToUpper(opt) == "SUBSCRIBED" {
|
||||||
|
childInfo.Subscribed = true
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return &childInfo, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func readOldNameExtendedItem(dec *imapwire.Decoder) (string, error) {
|
||||||
|
var name string
|
||||||
|
if !dec.ExpectSpecial('(') || !dec.ExpectMailbox(&name) || !dec.ExpectSpecial(')') {
|
||||||
|
return "", dec.Err()
|
||||||
|
}
|
||||||
|
return name, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readDelim(dec *imapwire.Decoder) (rune, error) {
|
||||||
|
var delimStr string
|
||||||
|
if dec.Quoted(&delimStr) {
|
||||||
|
delim, size := utf8.DecodeRuneInString(delimStr)
|
||||||
|
if delim == utf8.RuneError || size != len(delimStr) {
|
||||||
|
return 0, fmt.Errorf("mailbox delimiter must be a single rune")
|
||||||
|
}
|
||||||
|
return delim, nil
|
||||||
|
} else if !dec.ExpectNIL() {
|
||||||
|
return 0, dec.Err()
|
||||||
|
} else {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
42
imapclient/list_test.go
Normal file
42
imapclient/list_test.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package imapclient_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestList(t *testing.T) {
|
||||||
|
client, server := newClientServerPair(t, imap.ConnStateAuthenticated)
|
||||||
|
defer client.Close()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
options := imap.ListOptions{
|
||||||
|
ReturnStatus: &imap.StatusOptions{
|
||||||
|
NumMessages: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mailboxes, err := client.List("", "%", &options).Collect()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("List() = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(mailboxes) != 1 {
|
||||||
|
t.Fatalf("List() returned %v mailboxes, want 1", len(mailboxes))
|
||||||
|
}
|
||||||
|
mbox := mailboxes[0]
|
||||||
|
|
||||||
|
wantNumMessages := uint32(1)
|
||||||
|
want := &imap.ListData{
|
||||||
|
Delim: '/',
|
||||||
|
Mailbox: "INBOX",
|
||||||
|
Status: &imap.StatusData{
|
||||||
|
Mailbox: "INBOX",
|
||||||
|
NumMessages: &wantNumMessages,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(mbox, want) {
|
||||||
|
t.Errorf("got %#v but want %#v", mbox, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
205
imapclient/metadata.go
Normal file
205
imapclient/metadata.go
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
package imapclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GetMetadataDepth int
|
||||||
|
|
||||||
|
const (
|
||||||
|
GetMetadataDepthZero GetMetadataDepth = 0
|
||||||
|
GetMetadataDepthOne GetMetadataDepth = 1
|
||||||
|
GetMetadataDepthInfinity GetMetadataDepth = -1
|
||||||
|
)
|
||||||
|
|
||||||
|
func (depth GetMetadataDepth) String() string {
|
||||||
|
switch depth {
|
||||||
|
case GetMetadataDepthZero:
|
||||||
|
return "0"
|
||||||
|
case GetMetadataDepthOne:
|
||||||
|
return "1"
|
||||||
|
case GetMetadataDepthInfinity:
|
||||||
|
return "infinity"
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("imapclient: unknown GETMETADATA depth %d", depth))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMetadataOptions contains options for the GETMETADATA command.
|
||||||
|
type GetMetadataOptions struct {
|
||||||
|
MaxSize *uint32
|
||||||
|
Depth GetMetadataDepth
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *GetMetadataOptions) names() []string {
|
||||||
|
if options == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var l []string
|
||||||
|
if options.MaxSize != nil {
|
||||||
|
l = append(l, "MAXSIZE")
|
||||||
|
}
|
||||||
|
if options.Depth != GetMetadataDepthZero {
|
||||||
|
l = append(l, "DEPTH")
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMetadata sends a GETMETADATA command.
|
||||||
|
//
|
||||||
|
// This command requires support for the METADATA or METADATA-SERVER extension.
|
||||||
|
func (c *Client) GetMetadata(mailbox string, entries []string, options *GetMetadataOptions) *GetMetadataCommand {
|
||||||
|
cmd := &GetMetadataCommand{mailbox: mailbox}
|
||||||
|
enc := c.beginCommand("GETMETADATA", cmd)
|
||||||
|
enc.SP().Mailbox(mailbox)
|
||||||
|
if opts := options.names(); len(opts) > 0 {
|
||||||
|
enc.SP().List(len(opts), func(i int) {
|
||||||
|
opt := opts[i]
|
||||||
|
enc.Atom(opt).SP()
|
||||||
|
switch opt {
|
||||||
|
case "MAXSIZE":
|
||||||
|
enc.Number(*options.MaxSize)
|
||||||
|
case "DEPTH":
|
||||||
|
enc.Atom(options.Depth.String())
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("imapclient: unknown GETMETADATA option %q", opt))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
enc.SP().List(len(entries), func(i int) {
|
||||||
|
enc.String(entries[i])
|
||||||
|
})
|
||||||
|
enc.end()
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMetadata sends a SETMETADATA command.
|
||||||
|
//
|
||||||
|
// To remove an entry, set it to nil.
|
||||||
|
//
|
||||||
|
// This command requires support for the METADATA or METADATA-SERVER extension.
|
||||||
|
func (c *Client) SetMetadata(mailbox string, entries map[string]*[]byte) *Command {
|
||||||
|
cmd := &Command{}
|
||||||
|
enc := c.beginCommand("SETMETADATA", cmd)
|
||||||
|
enc.SP().Mailbox(mailbox).SP().Special('(')
|
||||||
|
i := 0
|
||||||
|
for k, v := range entries {
|
||||||
|
if i > 0 {
|
||||||
|
enc.SP()
|
||||||
|
}
|
||||||
|
enc.String(k).SP()
|
||||||
|
if v == nil {
|
||||||
|
enc.NIL()
|
||||||
|
} else {
|
||||||
|
enc.String(string(*v)) // TODO: use literals if required
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
enc.Special(')')
|
||||||
|
enc.end()
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) handleMetadata() error {
|
||||||
|
data, err := readMetadataResp(c.dec)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("in metadata-resp: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := c.findPendingCmdFunc(func(anyCmd command) bool {
|
||||||
|
cmd, ok := anyCmd.(*GetMetadataCommand)
|
||||||
|
return ok && cmd.mailbox == data.Mailbox
|
||||||
|
})
|
||||||
|
if cmd != nil && len(data.EntryValues) > 0 {
|
||||||
|
cmd := cmd.(*GetMetadataCommand)
|
||||||
|
cmd.data.Mailbox = data.Mailbox
|
||||||
|
if cmd.data.Entries == nil {
|
||||||
|
cmd.data.Entries = make(map[string]*[]byte)
|
||||||
|
}
|
||||||
|
// The server might send multiple METADATA responses for a single
|
||||||
|
// METADATA command
|
||||||
|
for k, v := range data.EntryValues {
|
||||||
|
cmd.data.Entries[k] = v
|
||||||
|
}
|
||||||
|
} else if handler := c.options.unilateralDataHandler().Metadata; handler != nil && len(data.EntryList) > 0 {
|
||||||
|
handler(data.Mailbox, data.EntryList)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMetadataCommand is a GETMETADATA command.
|
||||||
|
type GetMetadataCommand struct {
|
||||||
|
commandBase
|
||||||
|
mailbox string
|
||||||
|
data GetMetadataData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *GetMetadataCommand) Wait() (*GetMetadataData, error) {
|
||||||
|
return &cmd.data, cmd.wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMetadataData is the data returned by the GETMETADATA command.
|
||||||
|
type GetMetadataData struct {
|
||||||
|
Mailbox string
|
||||||
|
Entries map[string]*[]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type metadataResp struct {
|
||||||
|
Mailbox string
|
||||||
|
EntryList []string
|
||||||
|
EntryValues map[string]*[]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func readMetadataResp(dec *imapwire.Decoder) (*metadataResp, error) {
|
||||||
|
var data metadataResp
|
||||||
|
|
||||||
|
if !dec.ExpectMailbox(&data.Mailbox) || !dec.ExpectSP() {
|
||||||
|
return nil, dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
isList, err := dec.List(func() error {
|
||||||
|
var name string
|
||||||
|
if !dec.ExpectAString(&name) || !dec.ExpectSP() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: decode as []byte
|
||||||
|
var (
|
||||||
|
value *[]byte
|
||||||
|
s string
|
||||||
|
)
|
||||||
|
if dec.String(&s) || dec.Literal(&s) {
|
||||||
|
b := []byte(s)
|
||||||
|
value = &b
|
||||||
|
} else if !dec.ExpectNIL() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.EntryValues == nil {
|
||||||
|
data.EntryValues = make(map[string]*[]byte)
|
||||||
|
}
|
||||||
|
data.EntryValues[name] = value
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if !isList {
|
||||||
|
var name string
|
||||||
|
if !dec.ExpectAString(&name) {
|
||||||
|
return nil, dec.Err()
|
||||||
|
}
|
||||||
|
data.EntryList = append(data.EntryList, name)
|
||||||
|
|
||||||
|
for dec.SP() {
|
||||||
|
if !dec.ExpectAString(&name) {
|
||||||
|
return nil, dec.Err()
|
||||||
|
}
|
||||||
|
data.EntryList = append(data.EntryList, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &data, nil
|
||||||
|
}
|
||||||
74
imapclient/move.go
Normal file
74
imapclient/move.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package imapclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Move sends a MOVE command.
|
||||||
|
//
|
||||||
|
// If the server doesn't support IMAP4rev2 nor the MOVE extension, a fallback
|
||||||
|
// with COPY + STORE + EXPUNGE commands is used.
|
||||||
|
func (c *Client) Move(numSet imap.NumSet, mailbox string) *MoveCommand {
|
||||||
|
// If the server doesn't support MOVE, fallback to [UID] COPY,
|
||||||
|
// [UID] STORE +FLAGS.SILENT \Deleted and [UID] EXPUNGE
|
||||||
|
cmdName := "MOVE"
|
||||||
|
if !c.Caps().Has(imap.CapMove) {
|
||||||
|
cmdName = "COPY"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := &MoveCommand{}
|
||||||
|
enc := c.beginCommand(uidCmdName(cmdName, imapwire.NumSetKind(numSet)), cmd)
|
||||||
|
enc.SP().NumSet(numSet).SP().Mailbox(mailbox)
|
||||||
|
enc.end()
|
||||||
|
|
||||||
|
if cmdName == "COPY" {
|
||||||
|
cmd.store = c.Store(numSet, &imap.StoreFlags{
|
||||||
|
Op: imap.StoreFlagsAdd,
|
||||||
|
Silent: true,
|
||||||
|
Flags: []imap.Flag{imap.FlagDeleted},
|
||||||
|
}, nil)
|
||||||
|
if uidSet, ok := numSet.(imap.UIDSet); ok && c.Caps().Has(imap.CapUIDPlus) {
|
||||||
|
cmd.expunge = c.UIDExpunge(uidSet)
|
||||||
|
} else {
|
||||||
|
cmd.expunge = c.Expunge()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveCommand is a MOVE command.
|
||||||
|
type MoveCommand struct {
|
||||||
|
commandBase
|
||||||
|
data MoveData
|
||||||
|
|
||||||
|
// Fallback
|
||||||
|
store *FetchCommand
|
||||||
|
expunge *ExpungeCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *MoveCommand) Wait() (*MoveData, error) {
|
||||||
|
if err := cmd.wait(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if cmd.store != nil {
|
||||||
|
if err := cmd.store.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cmd.expunge != nil {
|
||||||
|
if err := cmd.expunge.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &cmd.data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveData contains the data returned by a MOVE command.
|
||||||
|
type MoveData struct {
|
||||||
|
// requires UIDPLUS or IMAP4rev2
|
||||||
|
UIDValidity uint32
|
||||||
|
SourceUIDs imap.NumSet
|
||||||
|
DestUIDs imap.NumSet
|
||||||
|
}
|
||||||
110
imapclient/namespace.go
Normal file
110
imapclient/namespace.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package imapclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Namespace sends a NAMESPACE command.
|
||||||
|
//
|
||||||
|
// This command requires support for IMAP4rev2 or the NAMESPACE extension.
|
||||||
|
func (c *Client) Namespace() *NamespaceCommand {
|
||||||
|
cmd := &NamespaceCommand{}
|
||||||
|
c.beginCommand("NAMESPACE", cmd).end()
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) handleNamespace() error {
|
||||||
|
data, err := readNamespaceResponse(c.dec)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("in namespace-response: %v", err)
|
||||||
|
}
|
||||||
|
if cmd := findPendingCmdByType[*NamespaceCommand](c); cmd != nil {
|
||||||
|
cmd.data = *data
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NamespaceCommand is a NAMESPACE command.
|
||||||
|
type NamespaceCommand struct {
|
||||||
|
commandBase
|
||||||
|
data imap.NamespaceData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *NamespaceCommand) Wait() (*imap.NamespaceData, error) {
|
||||||
|
return &cmd.data, cmd.wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func readNamespaceResponse(dec *imapwire.Decoder) (*imap.NamespaceData, error) {
|
||||||
|
var (
|
||||||
|
data imap.NamespaceData
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
data.Personal, err = readNamespace(dec)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dec.ExpectSP() {
|
||||||
|
return nil, dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Other, err = readNamespace(dec)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dec.ExpectSP() {
|
||||||
|
return nil, dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Shared, err = readNamespace(dec)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readNamespace(dec *imapwire.Decoder) ([]imap.NamespaceDescriptor, error) {
|
||||||
|
var l []imap.NamespaceDescriptor
|
||||||
|
err := dec.ExpectNList(func() error {
|
||||||
|
descr, err := readNamespaceDescr(dec)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("in namespace-descr: %v", err)
|
||||||
|
}
|
||||||
|
l = append(l, *descr)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return l, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func readNamespaceDescr(dec *imapwire.Decoder) (*imap.NamespaceDescriptor, error) {
|
||||||
|
var descr imap.NamespaceDescriptor
|
||||||
|
|
||||||
|
if !dec.ExpectSpecial('(') || !dec.ExpectString(&descr.Prefix) || !dec.ExpectSP() {
|
||||||
|
return nil, dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
descr.Delim, err = readDelim(dec)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip namespace-response-extensions
|
||||||
|
for dec.SP() {
|
||||||
|
if !dec.DiscardValue() {
|
||||||
|
return nil, dec.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dec.ExpectSpecial(')') {
|
||||||
|
return nil, dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &descr, nil
|
||||||
|
}
|
||||||
176
imapclient/quota.go
Normal file
176
imapclient/quota.go
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
package imapclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetQuota sends a GETQUOTA command.
|
||||||
|
//
|
||||||
|
// This command requires support for the QUOTA extension.
|
||||||
|
func (c *Client) GetQuota(root string) *GetQuotaCommand {
|
||||||
|
cmd := &GetQuotaCommand{root: root}
|
||||||
|
enc := c.beginCommand("GETQUOTA", cmd)
|
||||||
|
enc.SP().String(root)
|
||||||
|
enc.end()
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetQuotaRoot sends a GETQUOTAROOT command.
|
||||||
|
//
|
||||||
|
// This command requires support for the QUOTA extension.
|
||||||
|
func (c *Client) GetQuotaRoot(mailbox string) *GetQuotaRootCommand {
|
||||||
|
cmd := &GetQuotaRootCommand{mailbox: mailbox}
|
||||||
|
enc := c.beginCommand("GETQUOTAROOT", cmd)
|
||||||
|
enc.SP().Mailbox(mailbox)
|
||||||
|
enc.end()
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetQuota sends a SETQUOTA command.
|
||||||
|
//
|
||||||
|
// This command requires support for the SETQUOTA extension.
|
||||||
|
func (c *Client) SetQuota(root string, limits map[imap.QuotaResourceType]int64) *Command {
|
||||||
|
// TODO: consider returning the QUOTA response data?
|
||||||
|
cmd := &Command{}
|
||||||
|
enc := c.beginCommand("SETQUOTA", cmd)
|
||||||
|
enc.SP().String(root).SP().Special('(')
|
||||||
|
i := 0
|
||||||
|
for typ, limit := range limits {
|
||||||
|
if i > 0 {
|
||||||
|
enc.SP()
|
||||||
|
}
|
||||||
|
enc.Atom(string(typ)).SP().Number64(limit)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
enc.Special(')')
|
||||||
|
enc.end()
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) handleQuota() error {
|
||||||
|
data, err := readQuotaResponse(c.dec)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("in quota-response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := c.findPendingCmdFunc(func(cmd command) bool {
|
||||||
|
switch cmd := cmd.(type) {
|
||||||
|
case *GetQuotaCommand:
|
||||||
|
return cmd.root == data.Root
|
||||||
|
case *GetQuotaRootCommand:
|
||||||
|
for _, root := range cmd.roots {
|
||||||
|
if root == data.Root {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
switch cmd := cmd.(type) {
|
||||||
|
case *GetQuotaCommand:
|
||||||
|
cmd.data = data
|
||||||
|
case *GetQuotaRootCommand:
|
||||||
|
cmd.data = append(cmd.data, *data)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) handleQuotaRoot() error {
|
||||||
|
mailbox, roots, err := readQuotaRoot(c.dec)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("in quotaroot-response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := c.findPendingCmdFunc(func(anyCmd command) bool {
|
||||||
|
cmd, ok := anyCmd.(*GetQuotaRootCommand)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return cmd.mailbox == mailbox
|
||||||
|
})
|
||||||
|
if cmd != nil {
|
||||||
|
cmd := cmd.(*GetQuotaRootCommand)
|
||||||
|
cmd.roots = roots
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetQuotaCommand is a GETQUOTA command.
|
||||||
|
type GetQuotaCommand struct {
|
||||||
|
commandBase
|
||||||
|
root string
|
||||||
|
data *QuotaData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *GetQuotaCommand) Wait() (*QuotaData, error) {
|
||||||
|
if err := cmd.wait(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cmd.data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetQuotaRootCommand is a GETQUOTAROOT command.
|
||||||
|
type GetQuotaRootCommand struct {
|
||||||
|
commandBase
|
||||||
|
mailbox string
|
||||||
|
roots []string
|
||||||
|
data []QuotaData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *GetQuotaRootCommand) Wait() ([]QuotaData, error) {
|
||||||
|
if err := cmd.wait(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cmd.data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuotaData is the data returned by a QUOTA response.
|
||||||
|
type QuotaData struct {
|
||||||
|
Root string
|
||||||
|
Resources map[imap.QuotaResourceType]QuotaResourceData
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuotaResourceData contains the usage and limit for a quota resource.
|
||||||
|
type QuotaResourceData struct {
|
||||||
|
Usage int64
|
||||||
|
Limit int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func readQuotaResponse(dec *imapwire.Decoder) (*QuotaData, error) {
|
||||||
|
var data QuotaData
|
||||||
|
if !dec.ExpectAString(&data.Root) || !dec.ExpectSP() {
|
||||||
|
return nil, dec.Err()
|
||||||
|
}
|
||||||
|
data.Resources = make(map[imap.QuotaResourceType]QuotaResourceData)
|
||||||
|
err := dec.ExpectList(func() error {
|
||||||
|
var (
|
||||||
|
name string
|
||||||
|
resData QuotaResourceData
|
||||||
|
)
|
||||||
|
if !dec.ExpectAtom(&name) || !dec.ExpectSP() || !dec.ExpectNumber64(&resData.Usage) || !dec.ExpectSP() || !dec.ExpectNumber64(&resData.Limit) {
|
||||||
|
return fmt.Errorf("in quota-resource: %v", dec.Err())
|
||||||
|
}
|
||||||
|
data.Resources[imap.QuotaResourceType(name)] = resData
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return &data, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func readQuotaRoot(dec *imapwire.Decoder) (mailbox string, roots []string, err error) {
|
||||||
|
if !dec.ExpectMailbox(&mailbox) {
|
||||||
|
return "", nil, dec.Err()
|
||||||
|
}
|
||||||
|
for dec.SP() {
|
||||||
|
var root string
|
||||||
|
if !dec.ExpectAString(&root) {
|
||||||
|
return "", nil, dec.Err()
|
||||||
|
}
|
||||||
|
roots = append(roots, root)
|
||||||
|
}
|
||||||
|
return mailbox, roots, nil
|
||||||
|
}
|
||||||
401
imapclient/search.go
Normal file
401
imapclient/search.go
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
package imapclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
func returnSearchOptions(options *imap.SearchOptions) []string {
|
||||||
|
if options == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m := map[string]bool{
|
||||||
|
"MIN": options.ReturnMin,
|
||||||
|
"MAX": options.ReturnMax,
|
||||||
|
"ALL": options.ReturnAll,
|
||||||
|
"COUNT": options.ReturnCount,
|
||||||
|
}
|
||||||
|
|
||||||
|
var l []string
|
||||||
|
for k, ret := range m {
|
||||||
|
if ret {
|
||||||
|
l = append(l, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) search(numKind imapwire.NumKind, criteria *imap.SearchCriteria, options *imap.SearchOptions) *SearchCommand {
|
||||||
|
// The IMAP4rev2 SEARCH charset defaults to UTF-8. When UTF8=ACCEPT is
|
||||||
|
// enabled, specifying any CHARSET is invalid. For IMAP4rev1 the default is
|
||||||
|
// undefined and only US-ASCII support is required. What's more, some
|
||||||
|
// servers completely reject the CHARSET keyword. So, let's check if we
|
||||||
|
// actually have UTF-8 strings in the search criteria before using that.
|
||||||
|
// TODO: there might be a benefit in specifying CHARSET UTF-8 for IMAP4rev1
|
||||||
|
// servers even if we only send ASCII characters: the server then must
|
||||||
|
// decode encoded headers and Content-Transfer-Encoding before matching the
|
||||||
|
// criteria.
|
||||||
|
var charset string
|
||||||
|
if !c.Caps().Has(imap.CapIMAP4rev2) && !c.enabled.Has(imap.CapUTF8Accept) && !searchCriteriaIsASCII(criteria) {
|
||||||
|
charset = "UTF-8"
|
||||||
|
}
|
||||||
|
|
||||||
|
var all imap.NumSet
|
||||||
|
switch numKind {
|
||||||
|
case imapwire.NumKindSeq:
|
||||||
|
all = imap.SeqSet(nil)
|
||||||
|
case imapwire.NumKindUID:
|
||||||
|
all = imap.UIDSet(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := &SearchCommand{}
|
||||||
|
cmd.data.All = all
|
||||||
|
enc := c.beginCommand(uidCmdName("SEARCH", numKind), cmd)
|
||||||
|
if returnOpts := returnSearchOptions(options); len(returnOpts) > 0 {
|
||||||
|
enc.SP().Atom("RETURN").SP().List(len(returnOpts), func(i int) {
|
||||||
|
enc.Atom(returnOpts[i])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
enc.SP()
|
||||||
|
if charset != "" {
|
||||||
|
enc.Atom("CHARSET").SP().Atom(charset).SP()
|
||||||
|
}
|
||||||
|
writeSearchKey(enc.Encoder, criteria)
|
||||||
|
enc.end()
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search sends a SEARCH command.
|
||||||
|
func (c *Client) Search(criteria *imap.SearchCriteria, options *imap.SearchOptions) *SearchCommand {
|
||||||
|
return c.search(imapwire.NumKindSeq, criteria, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UIDSearch sends a UID SEARCH command.
|
||||||
|
func (c *Client) UIDSearch(criteria *imap.SearchCriteria, options *imap.SearchOptions) *SearchCommand {
|
||||||
|
return c.search(imapwire.NumKindUID, criteria, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) handleSearch() error {
|
||||||
|
cmd := findPendingCmdByType[*SearchCommand](c)
|
||||||
|
for c.dec.SP() {
|
||||||
|
if c.dec.Special('(') {
|
||||||
|
var name string
|
||||||
|
if !c.dec.ExpectAtom(&name) || !c.dec.ExpectSP() {
|
||||||
|
return c.dec.Err()
|
||||||
|
} else if strings.ToUpper(name) != "MODSEQ" {
|
||||||
|
return fmt.Errorf("in search-sort-mod-seq: expected %q, got %q", "MODSEQ", name)
|
||||||
|
}
|
||||||
|
var modSeq uint64
|
||||||
|
if !c.dec.ExpectModSeq(&modSeq) || !c.dec.ExpectSpecial(')') {
|
||||||
|
return c.dec.Err()
|
||||||
|
}
|
||||||
|
if cmd != nil {
|
||||||
|
cmd.data.ModSeq = modSeq
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
var num uint32
|
||||||
|
if !c.dec.ExpectNumber(&num) {
|
||||||
|
return c.dec.Err()
|
||||||
|
}
|
||||||
|
if cmd != nil {
|
||||||
|
switch all := cmd.data.All.(type) {
|
||||||
|
case imap.SeqSet:
|
||||||
|
all.AddNum(num)
|
||||||
|
cmd.data.All = all
|
||||||
|
case imap.UIDSet:
|
||||||
|
all.AddNum(imap.UID(num))
|
||||||
|
cmd.data.All = all
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) handleESearch() error {
|
||||||
|
if !c.dec.ExpectSP() {
|
||||||
|
return c.dec.Err()
|
||||||
|
}
|
||||||
|
tag, data, err := readESearchResponse(c.dec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cmd := c.findPendingCmdFunc(func(anyCmd command) bool {
|
||||||
|
cmd, ok := anyCmd.(*SearchCommand)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if tag != "" {
|
||||||
|
return cmd.tag == tag
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if cmd != nil {
|
||||||
|
cmd := cmd.(*SearchCommand)
|
||||||
|
cmd.data = *data
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchCommand is a SEARCH command.
|
||||||
|
type SearchCommand struct {
|
||||||
|
commandBase
|
||||||
|
data imap.SearchData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *SearchCommand) Wait() (*imap.SearchData, error) {
|
||||||
|
return &cmd.data, cmd.wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeSearchKey(enc *imapwire.Encoder, criteria *imap.SearchCriteria) {
|
||||||
|
firstItem := true
|
||||||
|
encodeItem := func() *imapwire.Encoder {
|
||||||
|
if !firstItem {
|
||||||
|
enc.SP()
|
||||||
|
}
|
||||||
|
firstItem = false
|
||||||
|
return enc
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, seqSet := range criteria.SeqNum {
|
||||||
|
encodeItem().NumSet(seqSet)
|
||||||
|
}
|
||||||
|
for _, uidSet := range criteria.UID {
|
||||||
|
encodeItem().Atom("UID").SP().NumSet(uidSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !criteria.Since.IsZero() && !criteria.Before.IsZero() && criteria.Before.Sub(criteria.Since) == 24*time.Hour {
|
||||||
|
encodeItem().Atom("ON").SP().String(criteria.Since.Format(internal.DateLayout))
|
||||||
|
} else {
|
||||||
|
if !criteria.Since.IsZero() {
|
||||||
|
encodeItem().Atom("SINCE").SP().String(criteria.Since.Format(internal.DateLayout))
|
||||||
|
}
|
||||||
|
if !criteria.Before.IsZero() {
|
||||||
|
encodeItem().Atom("BEFORE").SP().String(criteria.Before.Format(internal.DateLayout))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !criteria.SentSince.IsZero() && !criteria.SentBefore.IsZero() && criteria.SentBefore.Sub(criteria.SentSince) == 24*time.Hour {
|
||||||
|
encodeItem().Atom("SENTON").SP().String(criteria.SentSince.Format(internal.DateLayout))
|
||||||
|
} else {
|
||||||
|
if !criteria.SentSince.IsZero() {
|
||||||
|
encodeItem().Atom("SENTSINCE").SP().String(criteria.SentSince.Format(internal.DateLayout))
|
||||||
|
}
|
||||||
|
if !criteria.SentBefore.IsZero() {
|
||||||
|
encodeItem().Atom("SENTBEFORE").SP().String(criteria.SentBefore.Format(internal.DateLayout))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, kv := range criteria.Header {
|
||||||
|
switch k := strings.ToUpper(kv.Key); k {
|
||||||
|
case "BCC", "CC", "FROM", "SUBJECT", "TO":
|
||||||
|
encodeItem().Atom(k)
|
||||||
|
default:
|
||||||
|
encodeItem().Atom("HEADER").SP().String(kv.Key)
|
||||||
|
}
|
||||||
|
enc.SP().String(kv.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range criteria.Body {
|
||||||
|
encodeItem().Atom("BODY").SP().String(s)
|
||||||
|
}
|
||||||
|
for _, s := range criteria.Text {
|
||||||
|
encodeItem().Atom("TEXT").SP().String(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, flag := range criteria.Flag {
|
||||||
|
if k := flagSearchKey(flag); k != "" {
|
||||||
|
encodeItem().Atom(k)
|
||||||
|
} else {
|
||||||
|
encodeItem().Atom("KEYWORD").SP().Flag(flag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, flag := range criteria.NotFlag {
|
||||||
|
if k := flagSearchKey(flag); k != "" {
|
||||||
|
encodeItem().Atom("UN" + k)
|
||||||
|
} else {
|
||||||
|
encodeItem().Atom("UNKEYWORD").SP().Flag(flag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if criteria.Larger > 0 {
|
||||||
|
encodeItem().Atom("LARGER").SP().Number64(criteria.Larger)
|
||||||
|
}
|
||||||
|
if criteria.Smaller > 0 {
|
||||||
|
encodeItem().Atom("SMALLER").SP().Number64(criteria.Smaller)
|
||||||
|
}
|
||||||
|
|
||||||
|
if modSeq := criteria.ModSeq; modSeq != nil {
|
||||||
|
encodeItem().Atom("MODSEQ")
|
||||||
|
if modSeq.MetadataName != "" && modSeq.MetadataType != "" {
|
||||||
|
enc.SP().Quoted(modSeq.MetadataName).SP().Atom(string(modSeq.MetadataType))
|
||||||
|
}
|
||||||
|
enc.SP()
|
||||||
|
if modSeq.ModSeq != 0 {
|
||||||
|
enc.ModSeq(modSeq.ModSeq)
|
||||||
|
} else {
|
||||||
|
enc.Atom("0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, not := range criteria.Not {
|
||||||
|
encodeItem().Atom("NOT").SP()
|
||||||
|
enc.Special('(')
|
||||||
|
writeSearchKey(enc, ¬)
|
||||||
|
enc.Special(')')
|
||||||
|
}
|
||||||
|
for _, or := range criteria.Or {
|
||||||
|
encodeItem().Atom("OR").SP()
|
||||||
|
enc.Special('(')
|
||||||
|
writeSearchKey(enc, &or[0])
|
||||||
|
enc.Special(')')
|
||||||
|
enc.SP()
|
||||||
|
enc.Special('(')
|
||||||
|
writeSearchKey(enc, &or[1])
|
||||||
|
enc.Special(')')
|
||||||
|
}
|
||||||
|
|
||||||
|
if firstItem {
|
||||||
|
enc.Atom("ALL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func flagSearchKey(flag imap.Flag) string {
|
||||||
|
switch flag {
|
||||||
|
case imap.FlagAnswered, imap.FlagDeleted, imap.FlagDraft, imap.FlagFlagged, imap.FlagSeen:
|
||||||
|
return strings.ToUpper(strings.TrimPrefix(string(flag), "\\"))
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readESearchResponse(dec *imapwire.Decoder) (tag string, data *imap.SearchData, err error) {
|
||||||
|
data = &imap.SearchData{}
|
||||||
|
if dec.Special('(') { // search-correlator
|
||||||
|
var correlator string
|
||||||
|
if !dec.ExpectAtom(&correlator) || !dec.ExpectSP() || !dec.ExpectAString(&tag) || !dec.ExpectSpecial(')') {
|
||||||
|
return "", nil, dec.Err()
|
||||||
|
}
|
||||||
|
if correlator != "TAG" {
|
||||||
|
return "", nil, fmt.Errorf("in search-correlator: name must be TAG, but got %q", correlator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var name string
|
||||||
|
if !dec.SP() {
|
||||||
|
return tag, data, nil
|
||||||
|
} else if !dec.ExpectAtom(&name) {
|
||||||
|
return "", nil, dec.Err()
|
||||||
|
}
|
||||||
|
isUID := name == "UID"
|
||||||
|
|
||||||
|
if isUID {
|
||||||
|
if !dec.SP() {
|
||||||
|
return tag, data, nil
|
||||||
|
} else if !dec.ExpectAtom(&name) {
|
||||||
|
return "", nil, dec.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
if !dec.ExpectSP() {
|
||||||
|
return "", nil, dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strings.ToUpper(name) {
|
||||||
|
case "MIN":
|
||||||
|
var num uint32
|
||||||
|
if !dec.ExpectNumber(&num) {
|
||||||
|
return "", nil, dec.Err()
|
||||||
|
}
|
||||||
|
data.Min = num
|
||||||
|
case "MAX":
|
||||||
|
var num uint32
|
||||||
|
if !dec.ExpectNumber(&num) {
|
||||||
|
return "", nil, dec.Err()
|
||||||
|
}
|
||||||
|
data.Max = num
|
||||||
|
case "ALL":
|
||||||
|
numKind := imapwire.NumKindSeq
|
||||||
|
if isUID {
|
||||||
|
numKind = imapwire.NumKindUID
|
||||||
|
}
|
||||||
|
if !dec.ExpectNumSet(numKind, &data.All) {
|
||||||
|
return "", nil, dec.Err()
|
||||||
|
}
|
||||||
|
if data.All.Dynamic() {
|
||||||
|
return "", nil, fmt.Errorf("imapclient: server returned a dynamic ALL number set in SEARCH response")
|
||||||
|
}
|
||||||
|
case "COUNT":
|
||||||
|
var num uint32
|
||||||
|
if !dec.ExpectNumber(&num) {
|
||||||
|
return "", nil, dec.Err()
|
||||||
|
}
|
||||||
|
data.Count = num
|
||||||
|
case "MODSEQ":
|
||||||
|
var modSeq uint64
|
||||||
|
if !dec.ExpectModSeq(&modSeq) {
|
||||||
|
return "", nil, dec.Err()
|
||||||
|
}
|
||||||
|
data.ModSeq = modSeq
|
||||||
|
default:
|
||||||
|
if !dec.DiscardValue() {
|
||||||
|
return "", nil, dec.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dec.SP() {
|
||||||
|
break
|
||||||
|
} else if !dec.ExpectAtom(&name) {
|
||||||
|
return "", nil, dec.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tag, data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchCriteriaIsASCII(criteria *imap.SearchCriteria) bool {
|
||||||
|
for _, kv := range criteria.Header {
|
||||||
|
if !isASCII(kv.Key) || !isASCII(kv.Value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, s := range criteria.Body {
|
||||||
|
if !isASCII(s) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, s := range criteria.Text {
|
||||||
|
if !isASCII(s) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, not := range criteria.Not {
|
||||||
|
if !searchCriteriaIsASCII(¬) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, or := range criteria.Or {
|
||||||
|
if !searchCriteriaIsASCII(&or[0]) || !searchCriteriaIsASCII(&or[1]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func isASCII(s string) bool {
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
if s[i] > unicode.MaxASCII {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
61
imapclient/search_test.go
Normal file
61
imapclient/search_test.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package imapclient_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSearch(t *testing.T) {
|
||||||
|
client, server := newClientServerPair(t, imap.ConnStateSelected)
|
||||||
|
defer client.Close()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
criteria := imap.SearchCriteria{
|
||||||
|
Header: []imap.SearchCriteriaHeaderField{{
|
||||||
|
Key: "Message-Id",
|
||||||
|
Value: "<191101702316132@example.com>",
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
data, err := client.Search(&criteria, nil).Wait()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Search().Wait() = %v", err)
|
||||||
|
}
|
||||||
|
seqSet, ok := data.All.(imap.SeqSet)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("SearchData.All = %T, want SeqSet", data.All)
|
||||||
|
}
|
||||||
|
nums, _ := seqSet.Nums()
|
||||||
|
want := []uint32{1}
|
||||||
|
if !reflect.DeepEqual(nums, want) {
|
||||||
|
t.Errorf("SearchData.All.Nums() = %v, want %v", nums, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestESearch(t *testing.T) {
|
||||||
|
client, server := newClientServerPair(t, imap.ConnStateSelected)
|
||||||
|
defer client.Close()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
if !client.Caps().Has(imap.CapESearch) {
|
||||||
|
t.Skip("server doesn't support ESEARCH")
|
||||||
|
}
|
||||||
|
|
||||||
|
criteria := imap.SearchCriteria{
|
||||||
|
Header: []imap.SearchCriteriaHeaderField{{
|
||||||
|
Key: "Message-Id",
|
||||||
|
Value: "<191101702316132@example.com>",
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
options := imap.SearchOptions{
|
||||||
|
ReturnCount: true,
|
||||||
|
}
|
||||||
|
data, err := client.Search(&criteria, &options).Wait()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Search().Wait() = %v", err)
|
||||||
|
}
|
||||||
|
if want := uint32(1); data.Count != want {
|
||||||
|
t.Errorf("Count = %v, want %v", data.Count, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
100
imapclient/select.go
Normal file
100
imapclient/select.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
package imapclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Select sends a SELECT or EXAMINE command.
|
||||||
|
//
|
||||||
|
// A nil options pointer is equivalent to a zero options value.
|
||||||
|
func (c *Client) Select(mailbox string, options *imap.SelectOptions) *SelectCommand {
|
||||||
|
cmdName := "SELECT"
|
||||||
|
if options != nil && options.ReadOnly {
|
||||||
|
cmdName = "EXAMINE"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := &SelectCommand{mailbox: mailbox}
|
||||||
|
enc := c.beginCommand(cmdName, cmd)
|
||||||
|
enc.SP().Mailbox(mailbox)
|
||||||
|
if options != nil && options.CondStore {
|
||||||
|
enc.SP().Special('(').Atom("CONDSTORE").Special(')')
|
||||||
|
}
|
||||||
|
enc.end()
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unselect sends an UNSELECT command.
|
||||||
|
//
|
||||||
|
// This command requires support for IMAP4rev2 or the UNSELECT extension.
|
||||||
|
func (c *Client) Unselect() *Command {
|
||||||
|
cmd := &unselectCommand{}
|
||||||
|
c.beginCommand("UNSELECT", cmd).end()
|
||||||
|
return &cmd.Command
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnselectAndExpunge sends a CLOSE command.
|
||||||
|
//
|
||||||
|
// CLOSE implicitly performs a silent EXPUNGE command.
|
||||||
|
func (c *Client) UnselectAndExpunge() *Command {
|
||||||
|
cmd := &unselectCommand{}
|
||||||
|
c.beginCommand("CLOSE", cmd).end()
|
||||||
|
return &cmd.Command
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) handleFlags() error {
|
||||||
|
flags, err := internal.ExpectFlagList(c.dec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mutex.Lock()
|
||||||
|
if c.state == imap.ConnStateSelected {
|
||||||
|
c.mailbox = c.mailbox.copy()
|
||||||
|
c.mailbox.Flags = flags
|
||||||
|
}
|
||||||
|
c.mutex.Unlock()
|
||||||
|
|
||||||
|
cmd := findPendingCmdByType[*SelectCommand](c)
|
||||||
|
if cmd != nil {
|
||||||
|
cmd.data.Flags = flags
|
||||||
|
} else if handler := c.options.unilateralDataHandler().Mailbox; handler != nil {
|
||||||
|
handler(&UnilateralDataMailbox{Flags: flags})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) handleExists(num uint32) error {
|
||||||
|
cmd := findPendingCmdByType[*SelectCommand](c)
|
||||||
|
if cmd != nil {
|
||||||
|
cmd.data.NumMessages = num
|
||||||
|
} else {
|
||||||
|
c.mutex.Lock()
|
||||||
|
if c.state == imap.ConnStateSelected {
|
||||||
|
c.mailbox = c.mailbox.copy()
|
||||||
|
c.mailbox.NumMessages = num
|
||||||
|
}
|
||||||
|
c.mutex.Unlock()
|
||||||
|
|
||||||
|
if handler := c.options.unilateralDataHandler().Mailbox; handler != nil {
|
||||||
|
handler(&UnilateralDataMailbox{NumMessages: &num})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectCommand is a SELECT command.
|
||||||
|
type SelectCommand struct {
|
||||||
|
commandBase
|
||||||
|
mailbox string
|
||||||
|
data imap.SelectData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *SelectCommand) Wait() (*imap.SelectData, error) {
|
||||||
|
return &cmd.data, cmd.wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
type unselectCommand struct {
|
||||||
|
Command
|
||||||
|
}
|
||||||
20
imapclient/select_test.go
Normal file
20
imapclient/select_test.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package imapclient_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSelect(t *testing.T) {
|
||||||
|
client, server := newClientServerPair(t, imap.ConnStateAuthenticated)
|
||||||
|
defer client.Close()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
data, err := client.Select("INBOX", nil).Wait()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Select() = %v", err)
|
||||||
|
} else if data.NumMessages != 1 {
|
||||||
|
t.Errorf("SelectData.NumMessages = %v, want %v", data.NumMessages, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
84
imapclient/sort.go
Normal file
84
imapclient/sort.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package imapclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SortKey string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SortKeyArrival SortKey = "ARRIVAL"
|
||||||
|
SortKeyCc SortKey = "CC"
|
||||||
|
SortKeyDate SortKey = "DATE"
|
||||||
|
SortKeyFrom SortKey = "FROM"
|
||||||
|
SortKeySize SortKey = "SIZE"
|
||||||
|
SortKeySubject SortKey = "SUBJECT"
|
||||||
|
SortKeyTo SortKey = "TO"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SortCriterion struct {
|
||||||
|
Key SortKey
|
||||||
|
Reverse bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// SortOptions contains options for the SORT command.
|
||||||
|
type SortOptions struct {
|
||||||
|
SearchCriteria *imap.SearchCriteria
|
||||||
|
SortCriteria []SortCriterion
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) sort(numKind imapwire.NumKind, options *SortOptions) *SortCommand {
|
||||||
|
cmd := &SortCommand{}
|
||||||
|
enc := c.beginCommand(uidCmdName("SORT", numKind), cmd)
|
||||||
|
enc.SP().List(len(options.SortCriteria), func(i int) {
|
||||||
|
criterion := options.SortCriteria[i]
|
||||||
|
if criterion.Reverse {
|
||||||
|
enc.Atom("REVERSE").SP()
|
||||||
|
}
|
||||||
|
enc.Atom(string(criterion.Key))
|
||||||
|
})
|
||||||
|
enc.SP().Atom("UTF-8").SP()
|
||||||
|
writeSearchKey(enc.Encoder, options.SearchCriteria)
|
||||||
|
enc.end()
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) handleSort() error {
|
||||||
|
cmd := findPendingCmdByType[*SortCommand](c)
|
||||||
|
for c.dec.SP() {
|
||||||
|
var num uint32
|
||||||
|
if !c.dec.ExpectNumber(&num) {
|
||||||
|
return c.dec.Err()
|
||||||
|
}
|
||||||
|
if cmd != nil {
|
||||||
|
cmd.nums = append(cmd.nums, num)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort sends a SORT command.
|
||||||
|
//
|
||||||
|
// This command requires support for the SORT extension.
|
||||||
|
func (c *Client) Sort(options *SortOptions) *SortCommand {
|
||||||
|
return c.sort(imapwire.NumKindSeq, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UIDSort sends a UID SORT command.
|
||||||
|
//
|
||||||
|
// See Sort.
|
||||||
|
func (c *Client) UIDSort(options *SortOptions) *SortCommand {
|
||||||
|
return c.sort(imapwire.NumKindUID, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SortCommand is a SORT command.
|
||||||
|
type SortCommand struct {
|
||||||
|
commandBase
|
||||||
|
nums []uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *SortCommand) Wait() ([]uint32, error) {
|
||||||
|
err := cmd.wait()
|
||||||
|
return cmd.nums, err
|
||||||
|
}
|
||||||
83
imapclient/starttls.go
Normal file
83
imapclient/starttls.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package imapclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// startTLS sends a STARTTLS command.
|
||||||
|
//
|
||||||
|
// Unlike other commands, this method blocks until the command completes.
|
||||||
|
func (c *Client) startTLS(config *tls.Config) error {
|
||||||
|
upgradeDone := make(chan struct{})
|
||||||
|
cmd := &startTLSCommand{
|
||||||
|
tlsConfig: config,
|
||||||
|
upgradeDone: upgradeDone,
|
||||||
|
}
|
||||||
|
enc := c.beginCommand("STARTTLS", cmd)
|
||||||
|
enc.flush()
|
||||||
|
defer enc.end()
|
||||||
|
|
||||||
|
// Once a client issues a STARTTLS command, it MUST NOT issue further
|
||||||
|
// commands until a server response is seen and the TLS negotiation is
|
||||||
|
// complete
|
||||||
|
|
||||||
|
if err := cmd.wait(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// The decoder goroutine will invoke Client.upgradeStartTLS
|
||||||
|
<-upgradeDone
|
||||||
|
|
||||||
|
return cmd.tlsConn.Handshake()
|
||||||
|
}
|
||||||
|
|
||||||
|
// upgradeStartTLS finishes the STARTTLS upgrade after the server has sent an
|
||||||
|
// OK response. It runs in the decoder goroutine.
|
||||||
|
func (c *Client) upgradeStartTLS(startTLS *startTLSCommand) {
|
||||||
|
defer close(startTLS.upgradeDone)
|
||||||
|
|
||||||
|
// Drain buffered data from our bufio.Reader
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if _, err := io.CopyN(&buf, c.br, int64(c.br.Buffered())); err != nil {
|
||||||
|
panic(err) // unreachable
|
||||||
|
}
|
||||||
|
|
||||||
|
var cleartextConn net.Conn
|
||||||
|
if buf.Len() > 0 {
|
||||||
|
r := io.MultiReader(&buf, c.conn)
|
||||||
|
cleartextConn = startTLSConn{c.conn, r}
|
||||||
|
} else {
|
||||||
|
cleartextConn = c.conn
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConn := tls.Client(cleartextConn, startTLS.tlsConfig)
|
||||||
|
rw := c.options.wrapReadWriter(tlsConn)
|
||||||
|
|
||||||
|
c.br.Reset(rw)
|
||||||
|
// Unfortunately we can't re-use the bufio.Writer here, it races with
|
||||||
|
// Client.StartTLS
|
||||||
|
c.bw = bufio.NewWriter(rw)
|
||||||
|
|
||||||
|
startTLS.tlsConn = tlsConn
|
||||||
|
}
|
||||||
|
|
||||||
|
type startTLSCommand struct {
|
||||||
|
commandBase
|
||||||
|
tlsConfig *tls.Config
|
||||||
|
|
||||||
|
upgradeDone chan<- struct{}
|
||||||
|
tlsConn *tls.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
type startTLSConn struct {
|
||||||
|
net.Conn
|
||||||
|
r io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conn startTLSConn) Read(b []byte) (int, error) {
|
||||||
|
return conn.r.Read(b)
|
||||||
|
}
|
||||||
27
imapclient/starttls_test.go
Normal file
27
imapclient/starttls_test.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package imapclient_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2/imapclient"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStartTLS(t *testing.T) {
|
||||||
|
conn, server := newMemClientServerPair(t)
|
||||||
|
defer conn.Close()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
options := imapclient.Options{
|
||||||
|
TLSConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
|
}
|
||||||
|
client, err := imapclient.NewStartTLS(conn, &options)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewStartTLS() = %v", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
if err := client.Noop().Wait(); err != nil {
|
||||||
|
t.Fatalf("Noop().Wait() = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
164
imapclient/status.go
Normal file
164
imapclient/status.go
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
package imapclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
func statusItems(options *imap.StatusOptions) []string {
|
||||||
|
m := map[string]bool{
|
||||||
|
"MESSAGES": options.NumMessages,
|
||||||
|
"UIDNEXT": options.UIDNext,
|
||||||
|
"UIDVALIDITY": options.UIDValidity,
|
||||||
|
"UNSEEN": options.NumUnseen,
|
||||||
|
"DELETED": options.NumDeleted,
|
||||||
|
"SIZE": options.Size,
|
||||||
|
"APPENDLIMIT": options.AppendLimit,
|
||||||
|
"DELETED-STORAGE": options.DeletedStorage,
|
||||||
|
"HIGHESTMODSEQ": options.HighestModSeq,
|
||||||
|
}
|
||||||
|
|
||||||
|
var l []string
|
||||||
|
for k, req := range m {
|
||||||
|
if req {
|
||||||
|
l = append(l, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status sends a STATUS command.
|
||||||
|
//
|
||||||
|
// A nil options pointer is equivalent to a zero options value.
|
||||||
|
func (c *Client) Status(mailbox string, options *imap.StatusOptions) *StatusCommand {
|
||||||
|
if options == nil {
|
||||||
|
options = new(imap.StatusOptions)
|
||||||
|
}
|
||||||
|
if options.NumRecent {
|
||||||
|
panic("StatusOptions.NumRecent is not supported in imapclient")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := &StatusCommand{mailbox: mailbox}
|
||||||
|
enc := c.beginCommand("STATUS", cmd)
|
||||||
|
enc.SP().Mailbox(mailbox).SP()
|
||||||
|
items := statusItems(options)
|
||||||
|
enc.List(len(items), func(i int) {
|
||||||
|
enc.Atom(items[i])
|
||||||
|
})
|
||||||
|
enc.end()
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) handleStatus() error {
|
||||||
|
data, err := readStatus(c.dec)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("in status: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := c.findPendingCmdFunc(func(cmd command) bool {
|
||||||
|
switch cmd := cmd.(type) {
|
||||||
|
case *StatusCommand:
|
||||||
|
return cmd.mailbox == data.Mailbox
|
||||||
|
case *ListCommand:
|
||||||
|
return cmd.returnStatus && cmd.pendingData != nil && cmd.pendingData.Mailbox == data.Mailbox
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
switch cmd := cmd.(type) {
|
||||||
|
case *StatusCommand:
|
||||||
|
cmd.data = *data
|
||||||
|
case *ListCommand:
|
||||||
|
cmd.pendingData.Status = data
|
||||||
|
cmd.mailboxes <- cmd.pendingData
|
||||||
|
cmd.pendingData = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatusCommand is a STATUS command.
|
||||||
|
type StatusCommand struct {
|
||||||
|
commandBase
|
||||||
|
mailbox string
|
||||||
|
data imap.StatusData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *StatusCommand) Wait() (*imap.StatusData, error) {
|
||||||
|
return &cmd.data, cmd.wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func readStatus(dec *imapwire.Decoder) (*imap.StatusData, error) {
|
||||||
|
var data imap.StatusData
|
||||||
|
|
||||||
|
if !dec.ExpectMailbox(&data.Mailbox) || !dec.ExpectSP() {
|
||||||
|
return nil, dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
err := dec.ExpectList(func() error {
|
||||||
|
if err := readStatusAttVal(dec, &data); err != nil {
|
||||||
|
return fmt.Errorf("in status-att-val: %v", dec.Err())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return &data, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func readStatusAttVal(dec *imapwire.Decoder, data *imap.StatusData) error {
|
||||||
|
var name string
|
||||||
|
if !dec.ExpectAtom(&name) || !dec.ExpectSP() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
var ok bool
|
||||||
|
switch strings.ToUpper(name) {
|
||||||
|
case "MESSAGES":
|
||||||
|
var num uint32
|
||||||
|
ok = dec.ExpectNumber(&num)
|
||||||
|
data.NumMessages = &num
|
||||||
|
case "UIDNEXT":
|
||||||
|
var uidNext imap.UID
|
||||||
|
ok = dec.ExpectUID(&uidNext)
|
||||||
|
data.UIDNext = uidNext
|
||||||
|
case "UIDVALIDITY":
|
||||||
|
ok = dec.ExpectNumber(&data.UIDValidity)
|
||||||
|
case "UNSEEN":
|
||||||
|
var num uint32
|
||||||
|
ok = dec.ExpectNumber(&num)
|
||||||
|
data.NumUnseen = &num
|
||||||
|
case "DELETED":
|
||||||
|
var num uint32
|
||||||
|
ok = dec.ExpectNumber(&num)
|
||||||
|
data.NumDeleted = &num
|
||||||
|
case "SIZE":
|
||||||
|
var size int64
|
||||||
|
ok = dec.ExpectNumber64(&size)
|
||||||
|
data.Size = &size
|
||||||
|
case "APPENDLIMIT":
|
||||||
|
var num uint32
|
||||||
|
if dec.Number(&num) {
|
||||||
|
ok = true
|
||||||
|
} else {
|
||||||
|
ok = dec.ExpectNIL()
|
||||||
|
num = ^uint32(0)
|
||||||
|
}
|
||||||
|
data.AppendLimit = &num
|
||||||
|
case "DELETED-STORAGE":
|
||||||
|
var storage int64
|
||||||
|
ok = dec.ExpectNumber64(&storage)
|
||||||
|
data.DeletedStorage = &storage
|
||||||
|
case "HIGHESTMODSEQ":
|
||||||
|
ok = dec.ExpectModSeq(&data.HighestModSeq)
|
||||||
|
default:
|
||||||
|
if !dec.DiscardValue() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
34
imapclient/status_test.go
Normal file
34
imapclient/status_test.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package imapclient_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStatus(t *testing.T) {
|
||||||
|
client, server := newClientServerPair(t, imap.ConnStateAuthenticated)
|
||||||
|
defer client.Close()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
options := imap.StatusOptions{
|
||||||
|
NumMessages: true,
|
||||||
|
NumUnseen: true,
|
||||||
|
}
|
||||||
|
data, err := client.Status("INBOX", &options).Wait()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Status() = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantNumMessages := uint32(1)
|
||||||
|
wantNumUnseen := uint32(1)
|
||||||
|
want := &imap.StatusData{
|
||||||
|
Mailbox: "INBOX",
|
||||||
|
NumMessages: &wantNumMessages,
|
||||||
|
NumUnseen: &wantNumUnseen,
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(data, want) {
|
||||||
|
t.Errorf("Status() = %#v but want %#v", data, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
44
imapclient/store.go
Normal file
44
imapclient/store.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package imapclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Store sends a STORE command.
|
||||||
|
//
|
||||||
|
// Unless StoreFlags.Silent is set, the server will return the updated values.
|
||||||
|
//
|
||||||
|
// A nil options pointer is equivalent to a zero options value.
|
||||||
|
func (c *Client) Store(numSet imap.NumSet, store *imap.StoreFlags, options *imap.StoreOptions) *FetchCommand {
|
||||||
|
cmd := &FetchCommand{
|
||||||
|
numSet: numSet,
|
||||||
|
msgs: make(chan *FetchMessageData, 128),
|
||||||
|
}
|
||||||
|
enc := c.beginCommand(uidCmdName("STORE", imapwire.NumSetKind(numSet)), cmd)
|
||||||
|
enc.SP().NumSet(numSet).SP()
|
||||||
|
if options != nil && options.UnchangedSince != 0 {
|
||||||
|
enc.Special('(').Atom("UNCHANGEDSINCE").SP().ModSeq(options.UnchangedSince).Special(')').SP()
|
||||||
|
}
|
||||||
|
switch store.Op {
|
||||||
|
case imap.StoreFlagsSet:
|
||||||
|
// nothing to do
|
||||||
|
case imap.StoreFlagsAdd:
|
||||||
|
enc.Special('+')
|
||||||
|
case imap.StoreFlagsDel:
|
||||||
|
enc.Special('-')
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("imapclient: unknown store flags op: %v", store.Op))
|
||||||
|
}
|
||||||
|
enc.Atom("FLAGS")
|
||||||
|
if store.Silent {
|
||||||
|
enc.Atom(".SILENT")
|
||||||
|
}
|
||||||
|
enc.SP().List(len(store.Flags), func(i int) {
|
||||||
|
enc.Flag(store.Flags[i])
|
||||||
|
})
|
||||||
|
enc.end()
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
40
imapclient/store_test.go
Normal file
40
imapclient/store_test.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package imapclient_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStore(t *testing.T) {
|
||||||
|
client, server := newClientServerPair(t, imap.ConnStateSelected)
|
||||||
|
defer client.Close()
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
seqSet := imap.SeqSetNum(1)
|
||||||
|
storeFlags := imap.StoreFlags{
|
||||||
|
Op: imap.StoreFlagsAdd,
|
||||||
|
Flags: []imap.Flag{imap.FlagDeleted},
|
||||||
|
}
|
||||||
|
msgs, err := client.Store(seqSet, &storeFlags, nil).Collect()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Store().Collect() = %v", err)
|
||||||
|
} else if len(msgs) != 1 {
|
||||||
|
t.Fatalf("len(msgs) = %v, want %v", len(msgs), 1)
|
||||||
|
}
|
||||||
|
msg := msgs[0]
|
||||||
|
if msg.SeqNum != 1 {
|
||||||
|
t.Errorf("msg.SeqNum = %v, want %v", msg.SeqNum, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for _, f := range msg.Flags {
|
||||||
|
if f == imap.FlagDeleted {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("msg.Flags is missing deleted flag: %v", msg.Flags)
|
||||||
|
}
|
||||||
|
}
|
||||||
85
imapclient/thread.go
Normal file
85
imapclient/thread.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package imapclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ThreadOptions contains options for the THREAD command.
|
||||||
|
type ThreadOptions struct {
|
||||||
|
Algorithm imap.ThreadAlgorithm
|
||||||
|
SearchCriteria *imap.SearchCriteria
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) thread(numKind imapwire.NumKind, options *ThreadOptions) *ThreadCommand {
|
||||||
|
cmd := &ThreadCommand{}
|
||||||
|
enc := c.beginCommand(uidCmdName("THREAD", numKind), cmd)
|
||||||
|
enc.SP().Atom(string(options.Algorithm)).SP().Atom("UTF-8").SP()
|
||||||
|
writeSearchKey(enc.Encoder, options.SearchCriteria)
|
||||||
|
enc.end()
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thread sends a THREAD command.
|
||||||
|
//
|
||||||
|
// This command requires support for the THREAD extension.
|
||||||
|
func (c *Client) Thread(options *ThreadOptions) *ThreadCommand {
|
||||||
|
return c.thread(imapwire.NumKindSeq, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UIDThread sends a UID THREAD command.
|
||||||
|
//
|
||||||
|
// See Thread.
|
||||||
|
func (c *Client) UIDThread(options *ThreadOptions) *ThreadCommand {
|
||||||
|
return c.thread(imapwire.NumKindUID, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) handleThread() error {
|
||||||
|
cmd := findPendingCmdByType[*ThreadCommand](c)
|
||||||
|
for c.dec.SP() {
|
||||||
|
data, err := readThreadList(c.dec)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("in thread-list: %v", err)
|
||||||
|
}
|
||||||
|
if cmd != nil {
|
||||||
|
cmd.data = append(cmd.data, *data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ThreadCommand is a THREAD command.
|
||||||
|
type ThreadCommand struct {
|
||||||
|
commandBase
|
||||||
|
data []ThreadData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *ThreadCommand) Wait() ([]ThreadData, error) {
|
||||||
|
err := cmd.wait()
|
||||||
|
return cmd.data, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type ThreadData struct {
|
||||||
|
Chain []uint32
|
||||||
|
SubThreads []ThreadData
|
||||||
|
}
|
||||||
|
|
||||||
|
func readThreadList(dec *imapwire.Decoder) (*ThreadData, error) {
|
||||||
|
var data ThreadData
|
||||||
|
err := dec.ExpectList(func() error {
|
||||||
|
var num uint32
|
||||||
|
if len(data.SubThreads) == 0 && dec.Number(&num) {
|
||||||
|
data.Chain = append(data.Chain, num)
|
||||||
|
} else {
|
||||||
|
sub, err := readThreadList(dec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data.SubThreads = append(data.SubThreads, *sub)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return &data, err
|
||||||
|
}
|
||||||
123
imapserver/append.go
Normal file
123
imapserver/append.go
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
package imapserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
// defaultAppendLimit is the default maximum size of an APPEND payload.
|
||||||
|
const defaultAppendLimit = 100 * 1024 * 1024 // 100MiB
|
||||||
|
|
||||||
|
func (c *Conn) handleAppend(tag string, dec *imapwire.Decoder) error {
|
||||||
|
var (
|
||||||
|
mailbox string
|
||||||
|
options imap.AppendOptions
|
||||||
|
)
|
||||||
|
if !dec.ExpectSP() || !dec.ExpectMailbox(&mailbox) || !dec.ExpectSP() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
hasFlagList, err := dec.List(func() error {
|
||||||
|
flag, err := internal.ExpectFlag(dec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
options.Flags = append(options.Flags, flag)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if hasFlagList && !dec.ExpectSP() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := internal.DecodeDateTime(dec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !t.IsZero() && !dec.ExpectSP() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
options.Time = t
|
||||||
|
|
||||||
|
var dataExt string
|
||||||
|
if !dec.Special('~') && dec.Atom(&dataExt) { // ignore literal8 prefix if any for BINARY
|
||||||
|
switch strings.ToUpper(dataExt) {
|
||||||
|
case "UTF8":
|
||||||
|
// '~' is the literal8 prefix
|
||||||
|
if !dec.ExpectSP() || !dec.ExpectSpecial('(') || !dec.ExpectSpecial('~') {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return newClientBugError("Unknown APPEND data extension")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lit, nonSync, err := dec.ExpectLiteralReader()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
appendLimit := int64(defaultAppendLimit)
|
||||||
|
if appendLimitSession, ok := c.session.(SessionAppendLimit); ok {
|
||||||
|
appendLimit = int64(appendLimitSession.AppendLimit())
|
||||||
|
}
|
||||||
|
|
||||||
|
if lit.Size() > appendLimit {
|
||||||
|
return &imap.Error{
|
||||||
|
Type: imap.StatusResponseTypeNo,
|
||||||
|
Code: imap.ResponseCodeTooBig,
|
||||||
|
Text: fmt.Sprintf("Literals are limited to %v bytes for this command", appendLimit),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := c.acceptLiteral(lit.Size(), nonSync); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.setReadTimeout(literalReadTimeout)
|
||||||
|
defer c.setReadTimeout(cmdReadTimeout)
|
||||||
|
|
||||||
|
if err := c.checkState(imap.ConnStateAuthenticated); err != nil {
|
||||||
|
io.Copy(io.Discard, lit)
|
||||||
|
dec.CRLF()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, appendErr := c.session.Append(mailbox, lit, &options)
|
||||||
|
if _, discardErr := io.Copy(io.Discard, lit); discardErr != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if dataExt != "" && !dec.ExpectSpecial(')') {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
if !dec.ExpectCRLF() {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if appendErr != nil {
|
||||||
|
return appendErr
|
||||||
|
}
|
||||||
|
if err := c.poll("APPEND"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.writeAppendOK(tag, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) writeAppendOK(tag string, data *imap.AppendData) error {
|
||||||
|
enc := newResponseEncoder(c)
|
||||||
|
defer enc.end()
|
||||||
|
|
||||||
|
enc.Atom(tag).SP().Atom("OK").SP()
|
||||||
|
if data != nil {
|
||||||
|
enc.Special('[')
|
||||||
|
enc.Atom("APPENDUID").SP().Number(data.UIDValidity).SP().UID(data.UID)
|
||||||
|
enc.Special(']').SP()
|
||||||
|
}
|
||||||
|
enc.Text("APPEND completed")
|
||||||
|
return enc.CRLF()
|
||||||
|
}
|
||||||
148
imapserver/authenticate.go
Normal file
148
imapserver/authenticate.go
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
package imapserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/emersion/go-sasl"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Conn) handleAuthenticate(tag string, dec *imapwire.Decoder) error {
|
||||||
|
var mech string
|
||||||
|
if !dec.ExpectSP() || !dec.ExpectAtom(&mech) {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
mech = strings.ToUpper(mech)
|
||||||
|
|
||||||
|
var initialResp []byte
|
||||||
|
if dec.SP() {
|
||||||
|
var initialRespStr string
|
||||||
|
if !dec.ExpectText(&initialRespStr) {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
initialResp, err = internal.DecodeSASL(initialRespStr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dec.ExpectCRLF() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.checkState(imap.ConnStateNotAuthenticated); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !c.canAuth() {
|
||||||
|
return &imap.Error{
|
||||||
|
Type: imap.StatusResponseTypeNo,
|
||||||
|
Code: imap.ResponseCodePrivacyRequired,
|
||||||
|
Text: "TLS is required to authenticate",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var saslServer sasl.Server
|
||||||
|
if authSess, ok := c.session.(SessionSASL); ok {
|
||||||
|
var err error
|
||||||
|
saslServer, err = authSess.Authenticate(mech)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if mech != "PLAIN" {
|
||||||
|
return &imap.Error{
|
||||||
|
Type: imap.StatusResponseTypeNo,
|
||||||
|
Text: "SASL mechanism not supported",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
saslServer = sasl.NewPlainServer(func(identity, username, password string) error {
|
||||||
|
if identity != "" && identity != username {
|
||||||
|
return &imap.Error{
|
||||||
|
Type: imap.StatusResponseTypeNo,
|
||||||
|
Code: imap.ResponseCodeAuthorizationFailed,
|
||||||
|
Text: "SASL identity not supported",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.session.Login(username, password)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
enc := newResponseEncoder(c)
|
||||||
|
defer enc.end()
|
||||||
|
|
||||||
|
resp := initialResp
|
||||||
|
for {
|
||||||
|
challenge, done, err := saslServer.Next(resp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if done {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
var challengeStr string
|
||||||
|
if challenge != nil {
|
||||||
|
challengeStr = internal.EncodeSASL(challenge)
|
||||||
|
}
|
||||||
|
if err := writeContReq(enc.Encoder, challengeStr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
encodedResp, isPrefix, err := c.br.ReadLine()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if isPrefix {
|
||||||
|
return fmt.Errorf("SASL response too long")
|
||||||
|
} else if string(encodedResp) == "*" {
|
||||||
|
return &imap.Error{
|
||||||
|
Type: imap.StatusResponseTypeBad,
|
||||||
|
Text: "AUTHENTICATE cancelled",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err = decodeSASL(string(encodedResp))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state = imap.ConnStateAuthenticated
|
||||||
|
text := fmt.Sprintf("%v authentication successful", mech)
|
||||||
|
return writeCapabilityOK(enc.Encoder, tag, c.availableCaps(), text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeSASL(s string) ([]byte, error) {
|
||||||
|
b, err := internal.DecodeSASL(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &imap.Error{
|
||||||
|
Type: imap.StatusResponseTypeBad,
|
||||||
|
Text: "Malformed SASL response",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) handleUnauthenticate(dec *imapwire.Decoder) error {
|
||||||
|
if !dec.ExpectCRLF() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
if err := c.checkState(imap.ConnStateAuthenticated); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
session, ok := c.session.(SessionUnauthenticate)
|
||||||
|
if !ok {
|
||||||
|
return newClientBugError("UNAUTHENTICATE is not supported")
|
||||||
|
}
|
||||||
|
if err := session.Unauthenticate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.state = imap.ConnStateNotAuthenticated
|
||||||
|
c.mutex.Lock()
|
||||||
|
c.enabled = make(imap.CapSet)
|
||||||
|
c.mutex.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
114
imapserver/capability.go
Normal file
114
imapserver/capability.go
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
package imapserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Conn) handleCapability(dec *imapwire.Decoder) error {
|
||||||
|
if !dec.ExpectCRLF() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
enc := newResponseEncoder(c)
|
||||||
|
defer enc.end()
|
||||||
|
enc.Atom("*").SP().Atom("CAPABILITY")
|
||||||
|
for _, c := range c.availableCaps() {
|
||||||
|
enc.SP().Atom(string(c))
|
||||||
|
}
|
||||||
|
return enc.CRLF()
|
||||||
|
}
|
||||||
|
|
||||||
|
// availableCaps returns the capabilities supported by the server.
|
||||||
|
//
|
||||||
|
// They depend on the connection state.
|
||||||
|
//
|
||||||
|
// Some extensions (e.g. SASL-IR, ENABLE) don't require backend support and
|
||||||
|
// thus are always enabled.
|
||||||
|
func (c *Conn) availableCaps() []imap.Cap {
|
||||||
|
available := c.server.options.caps()
|
||||||
|
|
||||||
|
var caps []imap.Cap
|
||||||
|
addAvailableCaps(&caps, available, []imap.Cap{
|
||||||
|
imap.CapIMAP4rev2,
|
||||||
|
imap.CapIMAP4rev1,
|
||||||
|
})
|
||||||
|
if len(caps) == 0 {
|
||||||
|
panic("imapserver: must support at least IMAP4rev1 or IMAP4rev2")
|
||||||
|
}
|
||||||
|
|
||||||
|
if available.Has(imap.CapIMAP4rev1) {
|
||||||
|
caps = append(caps, []imap.Cap{
|
||||||
|
imap.CapSASLIR,
|
||||||
|
imap.CapLiteralMinus,
|
||||||
|
}...)
|
||||||
|
}
|
||||||
|
if c.canStartTLS() {
|
||||||
|
caps = append(caps, imap.CapStartTLS)
|
||||||
|
}
|
||||||
|
if c.canAuth() {
|
||||||
|
mechs := []string{"PLAIN"}
|
||||||
|
if authSess, ok := c.session.(SessionSASL); ok {
|
||||||
|
mechs = authSess.AuthenticateMechanisms()
|
||||||
|
}
|
||||||
|
for _, mech := range mechs {
|
||||||
|
caps = append(caps, imap.Cap("AUTH="+mech))
|
||||||
|
}
|
||||||
|
} else if c.state == imap.ConnStateNotAuthenticated {
|
||||||
|
caps = append(caps, imap.CapLoginDisabled)
|
||||||
|
}
|
||||||
|
if c.state == imap.ConnStateAuthenticated || c.state == imap.ConnStateSelected {
|
||||||
|
if available.Has(imap.CapIMAP4rev1) {
|
||||||
|
// IMAP4rev1-specific capabilities that don't require backend
|
||||||
|
// support and are not applicable to IMAP4rev2
|
||||||
|
caps = append(caps, []imap.Cap{
|
||||||
|
imap.CapUnselect,
|
||||||
|
imap.CapEnable,
|
||||||
|
imap.CapIdle,
|
||||||
|
imap.CapUTF8Accept,
|
||||||
|
}...)
|
||||||
|
|
||||||
|
// IMAP4rev1-specific capabilities which require backend support
|
||||||
|
// and are not applicable to IMAP4rev2
|
||||||
|
addAvailableCaps(&caps, available, []imap.Cap{
|
||||||
|
imap.CapNamespace,
|
||||||
|
imap.CapUIDPlus,
|
||||||
|
imap.CapESearch,
|
||||||
|
imap.CapSearchRes,
|
||||||
|
imap.CapListExtended,
|
||||||
|
imap.CapListStatus,
|
||||||
|
imap.CapMove,
|
||||||
|
imap.CapStatusSize,
|
||||||
|
imap.CapBinary,
|
||||||
|
imap.CapChildren,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capabilities which require backend support and apply to both
|
||||||
|
// IMAP4rev1 and IMAP4rev2
|
||||||
|
addAvailableCaps(&caps, available, []imap.Cap{
|
||||||
|
imap.CapSpecialUse,
|
||||||
|
imap.CapCreateSpecialUse,
|
||||||
|
imap.CapLiteralPlus,
|
||||||
|
imap.CapUnauthenticate,
|
||||||
|
})
|
||||||
|
|
||||||
|
if appendLimitSession, ok := c.session.(SessionAppendLimit); ok {
|
||||||
|
limit := appendLimitSession.AppendLimit()
|
||||||
|
caps = append(caps, imap.Cap(fmt.Sprintf("APPENDLIMIT=%d", limit)))
|
||||||
|
} else {
|
||||||
|
addAvailableCaps(&caps, available, []imap.Cap{imap.CapAppendLimit})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return caps
|
||||||
|
}
|
||||||
|
|
||||||
|
func addAvailableCaps(caps *[]imap.Cap, available imap.CapSet, l []imap.Cap) {
|
||||||
|
for _, c := range l {
|
||||||
|
if available.Has(c) {
|
||||||
|
*caps = append(*caps, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
618
imapserver/conn.go
Normal file
618
imapserver/conn.go
Normal file
@@ -0,0 +1,618 @@
|
|||||||
|
package imapserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"runtime/debug"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
cmdReadTimeout = 30 * time.Second
|
||||||
|
idleReadTimeout = 35 * time.Minute // section 5.4 says 30min minimum
|
||||||
|
literalReadTimeout = 5 * time.Minute
|
||||||
|
|
||||||
|
respWriteTimeout = 30 * time.Second
|
||||||
|
literalWriteTimeout = 5 * time.Minute
|
||||||
|
|
||||||
|
maxCommandSize = 50 * 1024 // RFC 2683 section 3.2.1.5 says 8KiB minimum
|
||||||
|
)
|
||||||
|
|
||||||
|
var internalServerErrorResp = &imap.StatusResponse{
|
||||||
|
Type: imap.StatusResponseTypeNo,
|
||||||
|
Code: imap.ResponseCodeServerBug,
|
||||||
|
Text: "Internal server error",
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Conn represents an IMAP connection to the server.
|
||||||
|
type Conn struct {
|
||||||
|
server *Server
|
||||||
|
br *bufio.Reader
|
||||||
|
bw *bufio.Writer
|
||||||
|
encMutex sync.Mutex
|
||||||
|
|
||||||
|
mutex sync.Mutex
|
||||||
|
conn net.Conn
|
||||||
|
enabled imap.CapSet
|
||||||
|
|
||||||
|
state imap.ConnState
|
||||||
|
session Session
|
||||||
|
}
|
||||||
|
|
||||||
|
func newConn(c net.Conn, server *Server) *Conn {
|
||||||
|
rw := server.options.wrapReadWriter(c)
|
||||||
|
br := bufio.NewReader(rw)
|
||||||
|
bw := bufio.NewWriter(rw)
|
||||||
|
return &Conn{
|
||||||
|
conn: c,
|
||||||
|
server: server,
|
||||||
|
br: br,
|
||||||
|
bw: bw,
|
||||||
|
enabled: make(imap.CapSet),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetConn returns the underlying connection that is wrapped by the IMAP
|
||||||
|
// connection.
|
||||||
|
//
|
||||||
|
// Writing to or reading from this connection directly will corrupt the IMAP
|
||||||
|
// session.
|
||||||
|
func (c *Conn) NetConn() net.Conn {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
return c.conn
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bye terminates the IMAP connection.
|
||||||
|
func (c *Conn) Bye(text string) error {
|
||||||
|
respErr := c.writeStatusResp("", &imap.StatusResponse{
|
||||||
|
Type: imap.StatusResponseTypeBye,
|
||||||
|
Text: text,
|
||||||
|
})
|
||||||
|
closeErr := c.conn.Close()
|
||||||
|
if respErr != nil {
|
||||||
|
return respErr
|
||||||
|
}
|
||||||
|
return closeErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) EnabledCaps() imap.CapSet {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
return c.enabled.Copy()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) serve() {
|
||||||
|
defer func() {
|
||||||
|
if v := recover(); v != nil {
|
||||||
|
c.server.logger().Printf("panic handling command: %v\n%s", v, debug.Stack())
|
||||||
|
}
|
||||||
|
|
||||||
|
c.conn.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
c.server.mutex.Lock()
|
||||||
|
c.server.conns[c] = struct{}{}
|
||||||
|
c.server.mutex.Unlock()
|
||||||
|
defer func() {
|
||||||
|
c.server.mutex.Lock()
|
||||||
|
delete(c.server.conns, c)
|
||||||
|
c.server.mutex.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
var (
|
||||||
|
greetingData *GreetingData
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
c.session, greetingData, err = c.server.options.NewSession(c)
|
||||||
|
if err != nil {
|
||||||
|
var (
|
||||||
|
resp *imap.StatusResponse
|
||||||
|
imapErr *imap.Error
|
||||||
|
)
|
||||||
|
if errors.As(err, &imapErr) && imapErr.Type == imap.StatusResponseTypeBye {
|
||||||
|
resp = (*imap.StatusResponse)(imapErr)
|
||||||
|
} else {
|
||||||
|
c.server.logger().Printf("failed to create session: %v", err)
|
||||||
|
resp = internalServerErrorResp
|
||||||
|
}
|
||||||
|
if err := c.writeStatusResp("", resp); err != nil {
|
||||||
|
c.server.logger().Printf("failed to write greeting: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if c.session != nil {
|
||||||
|
if err := c.session.Close(); err != nil {
|
||||||
|
c.server.logger().Printf("failed to close session: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
caps := c.server.options.caps()
|
||||||
|
if _, ok := c.session.(SessionIMAP4rev2); !ok && caps.Has(imap.CapIMAP4rev2) {
|
||||||
|
panic("imapserver: server advertises IMAP4rev2 but session doesn't support it")
|
||||||
|
}
|
||||||
|
if _, ok := c.session.(SessionNamespace); !ok && caps.Has(imap.CapNamespace) {
|
||||||
|
panic("imapserver: server advertises NAMESPACE but session doesn't support it")
|
||||||
|
}
|
||||||
|
if _, ok := c.session.(SessionMove); !ok && caps.Has(imap.CapMove) {
|
||||||
|
panic("imapserver: server advertises MOVE but session doesn't support it")
|
||||||
|
}
|
||||||
|
if _, ok := c.session.(SessionUnauthenticate); !ok && caps.Has(imap.CapUnauthenticate) {
|
||||||
|
panic("imapserver: server advertises UNAUTHENTICATE but session doesn't support it")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state = imap.ConnStateNotAuthenticated
|
||||||
|
statusType := imap.StatusResponseTypeOK
|
||||||
|
if greetingData != nil && greetingData.PreAuth {
|
||||||
|
c.state = imap.ConnStateAuthenticated
|
||||||
|
statusType = imap.StatusResponseTypePreAuth
|
||||||
|
}
|
||||||
|
if err := c.writeCapabilityStatus("", statusType, "IMAP server ready"); err != nil {
|
||||||
|
c.server.logger().Printf("failed to write greeting: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
var readTimeout time.Duration
|
||||||
|
switch c.state {
|
||||||
|
case imap.ConnStateAuthenticated, imap.ConnStateSelected:
|
||||||
|
readTimeout = idleReadTimeout
|
||||||
|
default:
|
||||||
|
readTimeout = cmdReadTimeout
|
||||||
|
}
|
||||||
|
c.setReadTimeout(readTimeout)
|
||||||
|
|
||||||
|
dec := imapwire.NewDecoder(c.br, imapwire.ConnSideServer)
|
||||||
|
dec.MaxSize = maxCommandSize
|
||||||
|
dec.CheckBufferedLiteralFunc = c.checkBufferedLiteral
|
||||||
|
|
||||||
|
if c.state == imap.ConnStateLogout || dec.EOF() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
c.setReadTimeout(cmdReadTimeout)
|
||||||
|
if err := c.readCommand(dec); err != nil {
|
||||||
|
if !errors.Is(err, net.ErrClosed) {
|
||||||
|
c.server.logger().Printf("failed to read command: %v", err)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) readCommand(dec *imapwire.Decoder) error {
|
||||||
|
var tag, name string
|
||||||
|
if !dec.ExpectAtom(&tag) || !dec.ExpectSP() || !dec.ExpectAtom(&name) {
|
||||||
|
return fmt.Errorf("in command: %w", dec.Err())
|
||||||
|
}
|
||||||
|
name = strings.ToUpper(name)
|
||||||
|
|
||||||
|
numKind := NumKindSeq
|
||||||
|
if name == "UID" {
|
||||||
|
numKind = NumKindUID
|
||||||
|
var subName string
|
||||||
|
if !dec.ExpectSP() || !dec.ExpectAtom(&subName) {
|
||||||
|
return fmt.Errorf("in command: %w", dec.Err())
|
||||||
|
}
|
||||||
|
name = "UID " + strings.ToUpper(subName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: handle multiple commands concurrently
|
||||||
|
sendOK := true
|
||||||
|
var err error
|
||||||
|
switch name {
|
||||||
|
case "NOOP", "CHECK":
|
||||||
|
err = c.handleNoop(dec)
|
||||||
|
case "LOGOUT":
|
||||||
|
err = c.handleLogout(dec)
|
||||||
|
case "CAPABILITY":
|
||||||
|
err = c.handleCapability(dec)
|
||||||
|
case "STARTTLS":
|
||||||
|
err = c.handleStartTLS(tag, dec)
|
||||||
|
sendOK = false
|
||||||
|
case "AUTHENTICATE":
|
||||||
|
err = c.handleAuthenticate(tag, dec)
|
||||||
|
sendOK = false
|
||||||
|
case "UNAUTHENTICATE":
|
||||||
|
err = c.handleUnauthenticate(dec)
|
||||||
|
case "LOGIN":
|
||||||
|
err = c.handleLogin(tag, dec)
|
||||||
|
sendOK = false
|
||||||
|
case "ENABLE":
|
||||||
|
err = c.handleEnable(dec)
|
||||||
|
case "CREATE":
|
||||||
|
err = c.handleCreate(dec)
|
||||||
|
case "DELETE":
|
||||||
|
err = c.handleDelete(dec)
|
||||||
|
case "RENAME":
|
||||||
|
err = c.handleRename(dec)
|
||||||
|
case "SUBSCRIBE":
|
||||||
|
err = c.handleSubscribe(dec)
|
||||||
|
case "UNSUBSCRIBE":
|
||||||
|
err = c.handleUnsubscribe(dec)
|
||||||
|
case "STATUS":
|
||||||
|
err = c.handleStatus(dec)
|
||||||
|
case "LIST":
|
||||||
|
err = c.handleList(dec)
|
||||||
|
case "LSUB":
|
||||||
|
err = c.handleLSub(dec)
|
||||||
|
case "NAMESPACE":
|
||||||
|
err = c.handleNamespace(dec)
|
||||||
|
case "IDLE":
|
||||||
|
err = c.handleIdle(dec)
|
||||||
|
case "SELECT", "EXAMINE":
|
||||||
|
err = c.handleSelect(tag, dec, name == "EXAMINE")
|
||||||
|
sendOK = false
|
||||||
|
case "CLOSE", "UNSELECT":
|
||||||
|
err = c.handleUnselect(dec, name == "CLOSE")
|
||||||
|
case "APPEND":
|
||||||
|
err = c.handleAppend(tag, dec)
|
||||||
|
sendOK = false
|
||||||
|
case "FETCH", "UID FETCH":
|
||||||
|
err = c.handleFetch(dec, numKind)
|
||||||
|
case "EXPUNGE":
|
||||||
|
err = c.handleExpunge(dec)
|
||||||
|
case "UID EXPUNGE":
|
||||||
|
err = c.handleUIDExpunge(dec)
|
||||||
|
case "STORE", "UID STORE":
|
||||||
|
err = c.handleStore(dec, numKind)
|
||||||
|
case "COPY", "UID COPY":
|
||||||
|
err = c.handleCopy(tag, dec, numKind)
|
||||||
|
sendOK = false
|
||||||
|
case "MOVE", "UID MOVE":
|
||||||
|
err = c.handleMove(dec, numKind)
|
||||||
|
case "SEARCH", "UID SEARCH":
|
||||||
|
err = c.handleSearch(tag, dec, numKind)
|
||||||
|
default:
|
||||||
|
if c.state == imap.ConnStateNotAuthenticated {
|
||||||
|
// Don't allow a single unknown command before authentication to
|
||||||
|
// mitigate cross-protocol attacks:
|
||||||
|
// https://www-archive.mozilla.org/projects/netlib/portbanning
|
||||||
|
c.state = imap.ConnStateLogout
|
||||||
|
defer c.Bye("Unknown command")
|
||||||
|
}
|
||||||
|
err = &imap.Error{
|
||||||
|
Type: imap.StatusResponseTypeBad,
|
||||||
|
Text: "Unknown command",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dec.DiscardLine()
|
||||||
|
|
||||||
|
var (
|
||||||
|
resp *imap.StatusResponse
|
||||||
|
imapErr *imap.Error
|
||||||
|
decErr *imapwire.DecoderExpectError
|
||||||
|
)
|
||||||
|
if errors.As(err, &imapErr) {
|
||||||
|
resp = (*imap.StatusResponse)(imapErr)
|
||||||
|
} else if errors.As(err, &decErr) {
|
||||||
|
resp = &imap.StatusResponse{
|
||||||
|
Type: imap.StatusResponseTypeBad,
|
||||||
|
Code: imap.ResponseCodeClientBug,
|
||||||
|
Text: "Syntax error: " + decErr.Message,
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
c.server.logger().Printf("handling %v command: %v", name, err)
|
||||||
|
resp = internalServerErrorResp
|
||||||
|
} else {
|
||||||
|
if !sendOK {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := c.poll(name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resp = &imap.StatusResponse{
|
||||||
|
Type: imap.StatusResponseTypeOK,
|
||||||
|
Text: fmt.Sprintf("%v completed", name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.writeStatusResp(tag, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) handleNoop(dec *imapwire.Decoder) error {
|
||||||
|
if !dec.ExpectCRLF() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) handleLogout(dec *imapwire.Decoder) error {
|
||||||
|
if !dec.ExpectCRLF() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state = imap.ConnStateLogout
|
||||||
|
|
||||||
|
return c.writeStatusResp("", &imap.StatusResponse{
|
||||||
|
Type: imap.StatusResponseTypeBye,
|
||||||
|
Text: "Logging out",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) handleDelete(dec *imapwire.Decoder) error {
|
||||||
|
var name string
|
||||||
|
if !dec.ExpectSP() || !dec.ExpectMailbox(&name) || !dec.ExpectCRLF() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
if err := c.checkState(imap.ConnStateAuthenticated); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.session.Delete(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) handleRename(dec *imapwire.Decoder) error {
|
||||||
|
var oldName, newName string
|
||||||
|
if !dec.ExpectSP() || !dec.ExpectMailbox(&oldName) || !dec.ExpectSP() || !dec.ExpectMailbox(&newName) || !dec.ExpectCRLF() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
if err := c.checkState(imap.ConnStateAuthenticated); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var options imap.RenameOptions
|
||||||
|
return c.session.Rename(oldName, newName, &options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) handleSubscribe(dec *imapwire.Decoder) error {
|
||||||
|
var name string
|
||||||
|
if !dec.ExpectSP() || !dec.ExpectMailbox(&name) || !dec.ExpectCRLF() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
if err := c.checkState(imap.ConnStateAuthenticated); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.session.Subscribe(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) handleUnsubscribe(dec *imapwire.Decoder) error {
|
||||||
|
var name string
|
||||||
|
if !dec.ExpectSP() || !dec.ExpectMailbox(&name) || !dec.ExpectCRLF() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
if err := c.checkState(imap.ConnStateAuthenticated); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.session.Unsubscribe(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) checkBufferedLiteral(size int64, nonSync bool) error {
|
||||||
|
if size > 4096 {
|
||||||
|
return &imap.Error{
|
||||||
|
Type: imap.StatusResponseTypeNo,
|
||||||
|
Code: imap.ResponseCodeTooBig,
|
||||||
|
Text: "Literals are limited to 4096 bytes for this command",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.acceptLiteral(size, nonSync)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) acceptLiteral(size int64, nonSync bool) error {
|
||||||
|
if nonSync && size > 4096 && !c.server.options.caps().Has(imap.CapLiteralPlus) {
|
||||||
|
return &imap.Error{
|
||||||
|
Type: imap.StatusResponseTypeBad,
|
||||||
|
Text: "Non-synchronizing literals are limited to 4096 bytes",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if nonSync {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.writeContReq("Ready for literal data")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) canAuth() bool {
|
||||||
|
if c.state != imap.ConnStateNotAuthenticated {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, isTLS := c.conn.(*tls.Conn)
|
||||||
|
return isTLS || c.server.options.InsecureAuth
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) writeStatusResp(tag string, statusResp *imap.StatusResponse) error {
|
||||||
|
enc := newResponseEncoder(c)
|
||||||
|
defer enc.end()
|
||||||
|
return writeStatusResp(enc.Encoder, tag, statusResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) writeContReq(text string) error {
|
||||||
|
enc := newResponseEncoder(c)
|
||||||
|
defer enc.end()
|
||||||
|
return writeContReq(enc.Encoder, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) writeCapabilityStatus(tag string, typ imap.StatusResponseType, text string) error {
|
||||||
|
enc := newResponseEncoder(c)
|
||||||
|
defer enc.end()
|
||||||
|
return writeCapabilityStatus(enc.Encoder, tag, typ, c.availableCaps(), text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) checkState(state imap.ConnState) error {
|
||||||
|
if state == imap.ConnStateAuthenticated && c.state == imap.ConnStateSelected {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if c.state != state {
|
||||||
|
return newClientBugError(fmt.Sprintf("This command is only valid in the %s state", state))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) setReadTimeout(dur time.Duration) {
|
||||||
|
if dur > 0 {
|
||||||
|
c.conn.SetReadDeadline(time.Now().Add(dur))
|
||||||
|
} else {
|
||||||
|
c.conn.SetReadDeadline(time.Time{})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) setWriteTimeout(dur time.Duration) {
|
||||||
|
if dur > 0 {
|
||||||
|
c.conn.SetWriteDeadline(time.Now().Add(dur))
|
||||||
|
} else {
|
||||||
|
c.conn.SetWriteDeadline(time.Time{})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) poll(cmd string) error {
|
||||||
|
switch c.state {
|
||||||
|
case imap.ConnStateAuthenticated, imap.ConnStateSelected:
|
||||||
|
// nothing to do
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
allowExpunge := true
|
||||||
|
switch cmd {
|
||||||
|
case "FETCH", "STORE", "SEARCH":
|
||||||
|
allowExpunge = false
|
||||||
|
}
|
||||||
|
|
||||||
|
w := &UpdateWriter{conn: c, allowExpunge: allowExpunge}
|
||||||
|
return c.session.Poll(w, allowExpunge)
|
||||||
|
}
|
||||||
|
|
||||||
|
type responseEncoder struct {
|
||||||
|
*imapwire.Encoder
|
||||||
|
conn *Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func newResponseEncoder(conn *Conn) *responseEncoder {
|
||||||
|
conn.mutex.Lock()
|
||||||
|
quotedUTF8 := conn.enabled.Has(imap.CapIMAP4rev2) || conn.enabled.Has(imap.CapUTF8Accept)
|
||||||
|
conn.mutex.Unlock()
|
||||||
|
|
||||||
|
wireEnc := imapwire.NewEncoder(conn.bw, imapwire.ConnSideServer)
|
||||||
|
wireEnc.QuotedUTF8 = quotedUTF8
|
||||||
|
|
||||||
|
conn.encMutex.Lock() // released by responseEncoder.end
|
||||||
|
conn.setWriteTimeout(respWriteTimeout)
|
||||||
|
return &responseEncoder{
|
||||||
|
Encoder: wireEnc,
|
||||||
|
conn: conn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *responseEncoder) end() {
|
||||||
|
if enc.Encoder == nil {
|
||||||
|
panic("imapserver: responseEncoder.end called twice")
|
||||||
|
}
|
||||||
|
enc.Encoder = nil
|
||||||
|
enc.conn.setWriteTimeout(0)
|
||||||
|
enc.conn.encMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *responseEncoder) Literal(size int64) io.WriteCloser {
|
||||||
|
enc.conn.setWriteTimeout(literalWriteTimeout)
|
||||||
|
return literalWriter{
|
||||||
|
WriteCloser: enc.Encoder.Literal(size, nil),
|
||||||
|
conn: enc.conn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type literalWriter struct {
|
||||||
|
io.WriteCloser
|
||||||
|
conn *Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lw literalWriter) Close() error {
|
||||||
|
lw.conn.setWriteTimeout(respWriteTimeout)
|
||||||
|
return lw.WriteCloser.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeStatusResp(enc *imapwire.Encoder, tag string, statusResp *imap.StatusResponse) error {
|
||||||
|
if tag == "" {
|
||||||
|
tag = "*"
|
||||||
|
}
|
||||||
|
enc.Atom(tag).SP().Atom(string(statusResp.Type)).SP()
|
||||||
|
if statusResp.Code != "" {
|
||||||
|
enc.Atom(fmt.Sprintf("[%v]", statusResp.Code)).SP()
|
||||||
|
}
|
||||||
|
enc.Text(statusResp.Text)
|
||||||
|
return enc.CRLF()
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeCapabilityOK(enc *imapwire.Encoder, tag string, caps []imap.Cap, text string) error {
|
||||||
|
return writeCapabilityStatus(enc, tag, imap.StatusResponseTypeOK, caps, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeCapabilityStatus(enc *imapwire.Encoder, tag string, typ imap.StatusResponseType, caps []imap.Cap, text string) error {
|
||||||
|
if tag == "" {
|
||||||
|
tag = "*"
|
||||||
|
}
|
||||||
|
|
||||||
|
enc.Atom(tag).SP().Atom(string(typ)).SP().Special('[').Atom("CAPABILITY")
|
||||||
|
for _, c := range caps {
|
||||||
|
enc.SP().Atom(string(c))
|
||||||
|
}
|
||||||
|
enc.Special(']').SP().Text(text)
|
||||||
|
return enc.CRLF()
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeContReq(enc *imapwire.Encoder, text string) error {
|
||||||
|
return enc.Atom("+").SP().Text(text).CRLF()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newClientBugError(text string) error {
|
||||||
|
return &imap.Error{
|
||||||
|
Type: imap.StatusResponseTypeBad,
|
||||||
|
Code: imap.ResponseCodeClientBug,
|
||||||
|
Text: text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateWriter writes status updates.
|
||||||
|
type UpdateWriter struct {
|
||||||
|
conn *Conn
|
||||||
|
allowExpunge bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteExpunge writes an EXPUNGE response.
|
||||||
|
func (w *UpdateWriter) WriteExpunge(seqNum uint32) error {
|
||||||
|
if !w.allowExpunge {
|
||||||
|
return fmt.Errorf("imapserver: EXPUNGE updates are not allowed in this context")
|
||||||
|
}
|
||||||
|
return w.conn.writeExpunge(seqNum)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteNumMessages writes an EXISTS response.
|
||||||
|
func (w *UpdateWriter) WriteNumMessages(n uint32) error {
|
||||||
|
return w.conn.writeExists(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteNumRecent writes an RECENT response (not used in IMAP4rev2, will be ignored).
|
||||||
|
func (w *UpdateWriter) WriteNumRecent(n uint32) error {
|
||||||
|
if w.conn.enabled.Has(imap.CapIMAP4rev2) || !w.conn.server.options.caps().Has(imap.CapIMAP4rev1) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return w.conn.writeObsoleteRecent(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteMailboxFlags writes a FLAGS response.
|
||||||
|
func (w *UpdateWriter) WriteMailboxFlags(flags []imap.Flag) error {
|
||||||
|
return w.conn.writeFlags(flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteMessageFlags writes a FETCH response with FLAGS.
|
||||||
|
func (w *UpdateWriter) WriteMessageFlags(seqNum uint32, uid imap.UID, flags []imap.Flag) error {
|
||||||
|
fetchWriter := &FetchWriter{conn: w.conn}
|
||||||
|
respWriter := fetchWriter.CreateMessage(seqNum)
|
||||||
|
if uid != 0 {
|
||||||
|
respWriter.WriteUID(uid)
|
||||||
|
}
|
||||||
|
respWriter.WriteFlags(flags)
|
||||||
|
return respWriter.Close()
|
||||||
|
}
|
||||||
55
imapserver/copy.go
Normal file
55
imapserver/copy.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package imapserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Conn) handleCopy(tag string, dec *imapwire.Decoder, numKind NumKind) error {
|
||||||
|
numSet, dest, err := readCopy(numKind, dec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.checkState(imap.ConnStateSelected); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data, err := c.session.Copy(numSet, dest)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdName := "COPY"
|
||||||
|
if numKind == NumKindUID {
|
||||||
|
cmdName = "UID COPY"
|
||||||
|
}
|
||||||
|
if err := c.poll(cmdName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.writeCopyOK(tag, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) writeCopyOK(tag string, data *imap.CopyData) error {
|
||||||
|
enc := newResponseEncoder(c)
|
||||||
|
defer enc.end()
|
||||||
|
|
||||||
|
if tag == "" {
|
||||||
|
tag = "*"
|
||||||
|
}
|
||||||
|
|
||||||
|
enc.Atom(tag).SP().Atom("OK").SP()
|
||||||
|
if data != nil {
|
||||||
|
enc.Special('[')
|
||||||
|
enc.Atom("COPYUID").SP().Number(data.UIDValidity).SP().NumSet(data.SourceUIDs).SP().NumSet(data.DestUIDs)
|
||||||
|
enc.Special(']').SP()
|
||||||
|
}
|
||||||
|
enc.Text("COPY completed")
|
||||||
|
return enc.CRLF()
|
||||||
|
}
|
||||||
|
|
||||||
|
func readCopy(numKind NumKind, dec *imapwire.Decoder) (numSet imap.NumSet, dest string, err error) {
|
||||||
|
if !dec.ExpectSP() || !dec.ExpectNumSet(numKind.wire(), &numSet) || !dec.ExpectSP() || !dec.ExpectMailbox(&dest) || !dec.ExpectCRLF() {
|
||||||
|
return nil, "", dec.Err()
|
||||||
|
}
|
||||||
|
return numSet, dest, nil
|
||||||
|
}
|
||||||
45
imapserver/create.go
Normal file
45
imapserver/create.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package imapserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Conn) handleCreate(dec *imapwire.Decoder) error {
|
||||||
|
var (
|
||||||
|
name string
|
||||||
|
options imap.CreateOptions
|
||||||
|
)
|
||||||
|
if !dec.ExpectSP() || !dec.ExpectMailbox(&name) {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
if dec.SP() {
|
||||||
|
var name string
|
||||||
|
if !dec.ExpectSpecial('(') || !dec.ExpectAtom(&name) || !dec.ExpectSP() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
switch strings.ToUpper(name) {
|
||||||
|
case "USE":
|
||||||
|
var err error
|
||||||
|
options.SpecialUse, err = internal.ExpectMailboxAttrList(dec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return newClientBugError("unknown CREATE parameter")
|
||||||
|
}
|
||||||
|
if !dec.ExpectSpecial(')') {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !dec.ExpectCRLF() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
if err := c.checkState(imap.ConnStateAuthenticated); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.session.Create(name, &options)
|
||||||
|
}
|
||||||
47
imapserver/enable.go
Normal file
47
imapserver/enable.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package imapserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Conn) handleEnable(dec *imapwire.Decoder) error {
|
||||||
|
var requested []imap.Cap
|
||||||
|
for dec.SP() {
|
||||||
|
cap, err := internal.ExpectCap(dec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
requested = append(requested, cap)
|
||||||
|
}
|
||||||
|
if !dec.ExpectCRLF() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.checkState(imap.ConnStateAuthenticated); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var enabled []imap.Cap
|
||||||
|
for _, req := range requested {
|
||||||
|
switch req {
|
||||||
|
case imap.CapIMAP4rev2, imap.CapUTF8Accept:
|
||||||
|
enabled = append(enabled, req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mutex.Lock()
|
||||||
|
for _, e := range enabled {
|
||||||
|
c.enabled[e] = struct{}{}
|
||||||
|
}
|
||||||
|
c.mutex.Unlock()
|
||||||
|
|
||||||
|
enc := newResponseEncoder(c)
|
||||||
|
defer enc.end()
|
||||||
|
enc.Atom("*").SP().Atom("ENABLED")
|
||||||
|
for _, c := range enabled {
|
||||||
|
enc.SP().Atom(string(c))
|
||||||
|
}
|
||||||
|
return enc.CRLF()
|
||||||
|
}
|
||||||
50
imapserver/expunge.go
Normal file
50
imapserver/expunge.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package imapserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Conn) handleExpunge(dec *imapwire.Decoder) error {
|
||||||
|
if !dec.ExpectCRLF() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
return c.expunge(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) handleUIDExpunge(dec *imapwire.Decoder) error {
|
||||||
|
var uidSet imap.UIDSet
|
||||||
|
if !dec.ExpectSP() || !dec.ExpectUIDSet(&uidSet) || !dec.ExpectCRLF() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
return c.expunge(&uidSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) expunge(uids *imap.UIDSet) error {
|
||||||
|
if err := c.checkState(imap.ConnStateSelected); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
w := &ExpungeWriter{conn: c}
|
||||||
|
return c.session.Expunge(w, uids)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) writeExpunge(seqNum uint32) error {
|
||||||
|
enc := newResponseEncoder(c)
|
||||||
|
defer enc.end()
|
||||||
|
enc.Atom("*").SP().Number(seqNum).SP().Atom("EXPUNGE")
|
||||||
|
return enc.CRLF()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpungeWriter writes EXPUNGE updates.
|
||||||
|
type ExpungeWriter struct {
|
||||||
|
conn *Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteExpunge notifies the client that the message with the provided sequence
|
||||||
|
// number has been deleted.
|
||||||
|
func (w *ExpungeWriter) WriteExpunge(seqNum uint32) error {
|
||||||
|
if w.conn == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return w.conn.writeExpunge(seqNum)
|
||||||
|
}
|
||||||
715
imapserver/fetch.go
Normal file
715
imapserver/fetch.go
Normal file
@@ -0,0 +1,715 @@
|
|||||||
|
package imapserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
const envelopeDateLayout = "Mon, 02 Jan 2006 15:04:05 -0700"
|
||||||
|
|
||||||
|
type fetchWriterOptions struct {
|
||||||
|
bodyStructure struct {
|
||||||
|
extended bool // BODYSTRUCTURE
|
||||||
|
nonExtended bool // BODY
|
||||||
|
}
|
||||||
|
obsolete map[*imap.FetchItemBodySection]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) handleFetch(dec *imapwire.Decoder, numKind NumKind) error {
|
||||||
|
var numSet imap.NumSet
|
||||||
|
if !dec.ExpectSP() || !dec.ExpectNumSet(numKind.wire(), &numSet) || !dec.ExpectSP() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
var options imap.FetchOptions
|
||||||
|
writerOptions := fetchWriterOptions{obsolete: make(map[*imap.FetchItemBodySection]string)}
|
||||||
|
isList, err := dec.List(func() error {
|
||||||
|
name, err := readFetchAttName(dec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch name {
|
||||||
|
case "ALL", "FAST", "FULL":
|
||||||
|
return newClientBugError("FETCH macros are not allowed in a list")
|
||||||
|
}
|
||||||
|
return handleFetchAtt(dec, name, &options, &writerOptions)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !isList {
|
||||||
|
name, err := readFetchAttName(dec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle macros
|
||||||
|
switch name {
|
||||||
|
case "ALL":
|
||||||
|
options.Flags = true
|
||||||
|
options.InternalDate = true
|
||||||
|
options.RFC822Size = true
|
||||||
|
options.Envelope = true
|
||||||
|
case "FAST":
|
||||||
|
options.Flags = true
|
||||||
|
options.InternalDate = true
|
||||||
|
options.RFC822Size = true
|
||||||
|
case "FULL":
|
||||||
|
options.Flags = true
|
||||||
|
options.InternalDate = true
|
||||||
|
options.RFC822Size = true
|
||||||
|
options.Envelope = true
|
||||||
|
handleFetchBodyStructure(&options, &writerOptions, false)
|
||||||
|
default:
|
||||||
|
if err := handleFetchAtt(dec, name, &options, &writerOptions); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dec.ExpectCRLF() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.checkState(imap.ConnStateSelected); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if numKind == NumKindUID {
|
||||||
|
options.UID = true
|
||||||
|
}
|
||||||
|
|
||||||
|
w := &FetchWriter{conn: c, options: writerOptions}
|
||||||
|
if err := c.session.Fetch(w, numSet, &options); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleFetchAtt(dec *imapwire.Decoder, attName string, options *imap.FetchOptions, writerOptions *fetchWriterOptions) error {
|
||||||
|
switch attName {
|
||||||
|
case "BODYSTRUCTURE":
|
||||||
|
handleFetchBodyStructure(options, writerOptions, true)
|
||||||
|
case "ENVELOPE":
|
||||||
|
options.Envelope = true
|
||||||
|
case "FLAGS":
|
||||||
|
options.Flags = true
|
||||||
|
case "INTERNALDATE":
|
||||||
|
options.InternalDate = true
|
||||||
|
case "RFC822.SIZE":
|
||||||
|
options.RFC822Size = true
|
||||||
|
case "UID":
|
||||||
|
options.UID = true
|
||||||
|
case "RFC822": // equivalent to BODY[]
|
||||||
|
bs := &imap.FetchItemBodySection{}
|
||||||
|
writerOptions.obsolete[bs] = attName
|
||||||
|
options.BodySection = append(options.BodySection, bs)
|
||||||
|
case "RFC822.PEEK": // obsolete, equivalent to BODY.PEEK[], used by Outlook
|
||||||
|
bs := &imap.FetchItemBodySection{Peek: true}
|
||||||
|
writerOptions.obsolete[bs] = attName
|
||||||
|
options.BodySection = append(options.BodySection, bs)
|
||||||
|
case "RFC822.HEADER": // equivalent to BODY.PEEK[HEADER]
|
||||||
|
bs := &imap.FetchItemBodySection{
|
||||||
|
Specifier: imap.PartSpecifierHeader,
|
||||||
|
Peek: true,
|
||||||
|
}
|
||||||
|
writerOptions.obsolete[bs] = attName
|
||||||
|
options.BodySection = append(options.BodySection, bs)
|
||||||
|
case "RFC822.TEXT": // equivalent to BODY[TEXT]
|
||||||
|
bs := &imap.FetchItemBodySection{
|
||||||
|
Specifier: imap.PartSpecifierText,
|
||||||
|
}
|
||||||
|
writerOptions.obsolete[bs] = attName
|
||||||
|
options.BodySection = append(options.BodySection, bs)
|
||||||
|
case "BINARY", "BINARY.PEEK":
|
||||||
|
part, err := readSectionBinary(dec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
partial, err := maybeReadPartial(dec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bs := &imap.FetchItemBinarySection{
|
||||||
|
Part: part,
|
||||||
|
Partial: partial,
|
||||||
|
Peek: attName == "BINARY.PEEK",
|
||||||
|
}
|
||||||
|
options.BinarySection = append(options.BinarySection, bs)
|
||||||
|
case "BINARY.SIZE":
|
||||||
|
part, err := readSectionBinary(dec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bss := &imap.FetchItemBinarySectionSize{Part: part}
|
||||||
|
options.BinarySectionSize = append(options.BinarySectionSize, bss)
|
||||||
|
case "BODY":
|
||||||
|
if !dec.Special('[') {
|
||||||
|
handleFetchBodyStructure(options, writerOptions, false)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
section := imap.FetchItemBodySection{}
|
||||||
|
err := readSection(dec, §ion)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
section.Partial, err = maybeReadPartial(dec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
options.BodySection = append(options.BodySection, §ion)
|
||||||
|
case "BODY.PEEK":
|
||||||
|
if !dec.ExpectSpecial('[') {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
section := imap.FetchItemBodySection{Peek: true}
|
||||||
|
err := readSection(dec, §ion)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
section.Partial, err = maybeReadPartial(dec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
options.BodySection = append(options.BodySection, §ion)
|
||||||
|
default:
|
||||||
|
return newClientBugError("Unknown FETCH data item")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleFetchBodyStructure(options *imap.FetchOptions, writerOptions *fetchWriterOptions, extended bool) {
|
||||||
|
if options.BodyStructure == nil || extended {
|
||||||
|
options.BodyStructure = &imap.FetchItemBodyStructure{Extended: extended}
|
||||||
|
}
|
||||||
|
if extended {
|
||||||
|
writerOptions.bodyStructure.extended = true
|
||||||
|
} else {
|
||||||
|
writerOptions.bodyStructure.nonExtended = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readFetchAttName(dec *imapwire.Decoder) (string, error) {
|
||||||
|
var attName string
|
||||||
|
if !dec.Expect(dec.Func(&attName, isMsgAttNameChar), "msg-att name") {
|
||||||
|
return "", dec.Err()
|
||||||
|
}
|
||||||
|
return strings.ToUpper(attName), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isMsgAttNameChar(ch byte) bool {
|
||||||
|
return ch != '[' && imapwire.IsAtomChar(ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readSection(dec *imapwire.Decoder, section *imap.FetchItemBodySection) error {
|
||||||
|
if dec.Special(']') {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var dot bool
|
||||||
|
section.Part, dot = readSectionPart(dec)
|
||||||
|
if dot || len(section.Part) == 0 {
|
||||||
|
var specifier string
|
||||||
|
if dot {
|
||||||
|
if !dec.ExpectAtom(&specifier) {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dec.Atom(&specifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch specifier := imap.PartSpecifier(strings.ToUpper(specifier)); specifier {
|
||||||
|
case imap.PartSpecifierNone, imap.PartSpecifierHeader, imap.PartSpecifierMIME, imap.PartSpecifierText:
|
||||||
|
section.Specifier = specifier
|
||||||
|
case "HEADER.FIELDS", "HEADER.FIELDS.NOT":
|
||||||
|
if !dec.ExpectSP() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
headerList, err := readHeaderList(dec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
section.Specifier = imap.PartSpecifierHeader
|
||||||
|
if specifier == "HEADER.FIELDS" {
|
||||||
|
section.HeaderFields = headerList
|
||||||
|
} else {
|
||||||
|
section.HeaderFieldsNot = headerList
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return newClientBugError("unknown body section specifier")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dec.ExpectSpecial(']') {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readSectionPart(dec *imapwire.Decoder) (part []int, dot bool) {
|
||||||
|
for {
|
||||||
|
dot = len(part) > 0
|
||||||
|
if dot && !dec.Special('.') {
|
||||||
|
return part, false
|
||||||
|
}
|
||||||
|
|
||||||
|
var num uint32
|
||||||
|
if !dec.Number(&num) {
|
||||||
|
return part, dot
|
||||||
|
}
|
||||||
|
part = append(part, int(num))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readHeaderList(dec *imapwire.Decoder) ([]string, error) {
|
||||||
|
var l []string
|
||||||
|
err := dec.ExpectList(func() error {
|
||||||
|
var s string
|
||||||
|
if !dec.ExpectAString(&s) {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
l = append(l, s)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return l, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func readSectionBinary(dec *imapwire.Decoder) ([]int, error) {
|
||||||
|
if !dec.ExpectSpecial('[') {
|
||||||
|
return nil, dec.Err()
|
||||||
|
}
|
||||||
|
if dec.Special(']') {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var l []int
|
||||||
|
for {
|
||||||
|
var num uint32
|
||||||
|
if !dec.ExpectNumber(&num) {
|
||||||
|
return l, dec.Err()
|
||||||
|
}
|
||||||
|
l = append(l, int(num))
|
||||||
|
|
||||||
|
if !dec.Special('.') {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dec.ExpectSpecial(']') {
|
||||||
|
return l, dec.Err()
|
||||||
|
}
|
||||||
|
return l, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func maybeReadPartial(dec *imapwire.Decoder) (*imap.SectionPartial, error) {
|
||||||
|
if !dec.Special('<') {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
var partial imap.SectionPartial
|
||||||
|
if !dec.ExpectNumber64(&partial.Offset) || !dec.ExpectSpecial('.') || !dec.ExpectNumber64(&partial.Size) || !dec.ExpectSpecial('>') {
|
||||||
|
return nil, dec.Err()
|
||||||
|
}
|
||||||
|
return &partial, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchWriter writes FETCH responses.
|
||||||
|
type FetchWriter struct {
|
||||||
|
conn *Conn
|
||||||
|
options fetchWriterOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateMessage writes a FETCH response for a message.
|
||||||
|
//
|
||||||
|
// FetchResponseWriter.Close must be called.
|
||||||
|
func (cmd *FetchWriter) CreateMessage(seqNum uint32) *FetchResponseWriter {
|
||||||
|
enc := newResponseEncoder(cmd.conn)
|
||||||
|
enc.Atom("*").SP().Number(seqNum).SP().Atom("FETCH").SP().Special('(')
|
||||||
|
return &FetchResponseWriter{enc: enc, options: cmd.options}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchResponseWriter writes a single FETCH response for a message.
|
||||||
|
type FetchResponseWriter struct {
|
||||||
|
enc *responseEncoder
|
||||||
|
options fetchWriterOptions
|
||||||
|
|
||||||
|
hasItem bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *FetchResponseWriter) writeItemSep() {
|
||||||
|
if w.hasItem {
|
||||||
|
w.enc.SP()
|
||||||
|
}
|
||||||
|
w.hasItem = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteUID writes the message's UID.
|
||||||
|
func (w *FetchResponseWriter) WriteUID(uid imap.UID) {
|
||||||
|
w.writeItemSep()
|
||||||
|
w.enc.Atom("UID").SP().UID(uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteFlags writes the message's flags.
|
||||||
|
func (w *FetchResponseWriter) WriteFlags(flags []imap.Flag) {
|
||||||
|
w.writeItemSep()
|
||||||
|
w.enc.Atom("FLAGS").SP().List(len(flags), func(i int) {
|
||||||
|
w.enc.Flag(flags[i])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteRFC822Size writes the message's full size.
|
||||||
|
func (w *FetchResponseWriter) WriteRFC822Size(size int64) {
|
||||||
|
w.writeItemSep()
|
||||||
|
w.enc.Atom("RFC822.SIZE").SP().Number64(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteInternalDate writes the message's internal date.
|
||||||
|
func (w *FetchResponseWriter) WriteInternalDate(t time.Time) {
|
||||||
|
w.writeItemSep()
|
||||||
|
w.enc.Atom("INTERNALDATE").SP().String(t.Format(internal.DateTimeLayout))
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteBodySection writes a body section.
|
||||||
|
//
|
||||||
|
// The returned io.WriteCloser must be closed before writing any more message
|
||||||
|
// data items.
|
||||||
|
func (w *FetchResponseWriter) WriteBodySection(section *imap.FetchItemBodySection, size int64) io.WriteCloser {
|
||||||
|
w.writeItemSep()
|
||||||
|
enc := w.enc.Encoder
|
||||||
|
|
||||||
|
if obs, ok := w.options.obsolete[section]; ok {
|
||||||
|
enc.Atom(obs)
|
||||||
|
} else {
|
||||||
|
writeItemBodySection(enc, section)
|
||||||
|
}
|
||||||
|
|
||||||
|
enc.SP()
|
||||||
|
return w.enc.Literal(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeItemBodySection(enc *imapwire.Encoder, section *imap.FetchItemBodySection) {
|
||||||
|
enc.Atom("BODY")
|
||||||
|
enc.Special('[')
|
||||||
|
writeSectionPart(enc, section.Part)
|
||||||
|
if len(section.Part) > 0 && section.Specifier != imap.PartSpecifierNone {
|
||||||
|
enc.Special('.')
|
||||||
|
}
|
||||||
|
if section.Specifier != imap.PartSpecifierNone {
|
||||||
|
enc.Atom(string(section.Specifier))
|
||||||
|
|
||||||
|
var headerList []string
|
||||||
|
if len(section.HeaderFields) > 0 {
|
||||||
|
headerList = section.HeaderFields
|
||||||
|
enc.Atom(".FIELDS")
|
||||||
|
} else if len(section.HeaderFieldsNot) > 0 {
|
||||||
|
headerList = section.HeaderFieldsNot
|
||||||
|
enc.Atom(".FIELDS.NOT")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(headerList) > 0 {
|
||||||
|
enc.SP().List(len(headerList), func(i int) {
|
||||||
|
enc.String(headerList[i])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
enc.Special(']')
|
||||||
|
if partial := section.Partial; partial != nil {
|
||||||
|
enc.Special('<').Number(uint32(partial.Offset)).Special('>')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteBinarySection writes a binary section.
|
||||||
|
//
|
||||||
|
// The returned io.WriteCloser must be closed before writing any more message
|
||||||
|
// data items.
|
||||||
|
func (w *FetchResponseWriter) WriteBinarySection(section *imap.FetchItemBinarySection, size int64) io.WriteCloser {
|
||||||
|
w.writeItemSep()
|
||||||
|
enc := w.enc.Encoder
|
||||||
|
|
||||||
|
enc.Atom("BINARY").Special('[')
|
||||||
|
writeSectionPart(enc, section.Part)
|
||||||
|
enc.Special(']').SP()
|
||||||
|
enc.Special('~') // indicates literal8
|
||||||
|
return w.enc.Literal(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteBinarySectionSize writes a binary section size.
|
||||||
|
func (w *FetchResponseWriter) WriteBinarySectionSize(section *imap.FetchItemBinarySectionSize, size uint32) {
|
||||||
|
w.writeItemSep()
|
||||||
|
enc := w.enc.Encoder
|
||||||
|
|
||||||
|
enc.Atom("BINARY.SIZE").Special('[')
|
||||||
|
writeSectionPart(enc, section.Part)
|
||||||
|
enc.Special(']').SP().Number(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteEnvelope writes the message's envelope.
|
||||||
|
func (w *FetchResponseWriter) WriteEnvelope(envelope *imap.Envelope) {
|
||||||
|
w.writeItemSep()
|
||||||
|
enc := w.enc.Encoder
|
||||||
|
enc.Atom("ENVELOPE").SP()
|
||||||
|
writeEnvelope(enc, envelope)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteBodyStructure writes the message's body structure (either BODYSTRUCTURE
|
||||||
|
// or BODY).
|
||||||
|
func (w *FetchResponseWriter) WriteBodyStructure(bs imap.BodyStructure) {
|
||||||
|
if w.options.bodyStructure.nonExtended {
|
||||||
|
w.writeBodyStructure(bs, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.options.bodyStructure.extended {
|
||||||
|
var isExtended bool
|
||||||
|
switch bs := bs.(type) {
|
||||||
|
case *imap.BodyStructureSinglePart:
|
||||||
|
isExtended = bs.Extended != nil
|
||||||
|
case *imap.BodyStructureMultiPart:
|
||||||
|
isExtended = bs.Extended != nil
|
||||||
|
}
|
||||||
|
if !isExtended {
|
||||||
|
panic("imapserver: client requested extended body structure but a non-extended one is written back")
|
||||||
|
}
|
||||||
|
|
||||||
|
w.writeBodyStructure(bs, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *FetchResponseWriter) writeBodyStructure(bs imap.BodyStructure, extended bool) {
|
||||||
|
item := "BODY"
|
||||||
|
if extended {
|
||||||
|
item = "BODYSTRUCTURE"
|
||||||
|
}
|
||||||
|
|
||||||
|
w.writeItemSep()
|
||||||
|
enc := w.enc.Encoder
|
||||||
|
enc.Atom(item).SP()
|
||||||
|
writeBodyStructure(enc, bs, extended)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the FETCH message writer.
|
||||||
|
func (w *FetchResponseWriter) Close() error {
|
||||||
|
if w.enc == nil {
|
||||||
|
return fmt.Errorf("imapserver: FetchResponseWriter already closed")
|
||||||
|
}
|
||||||
|
err := w.enc.Special(')').CRLF()
|
||||||
|
w.enc.end()
|
||||||
|
w.enc = nil
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeEnvelope(enc *imapwire.Encoder, envelope *imap.Envelope) {
|
||||||
|
if envelope == nil {
|
||||||
|
envelope = new(imap.Envelope)
|
||||||
|
}
|
||||||
|
|
||||||
|
sender := envelope.Sender
|
||||||
|
if sender == nil {
|
||||||
|
sender = envelope.From
|
||||||
|
}
|
||||||
|
replyTo := envelope.ReplyTo
|
||||||
|
if replyTo == nil {
|
||||||
|
replyTo = envelope.From
|
||||||
|
}
|
||||||
|
|
||||||
|
enc.Special('(')
|
||||||
|
if envelope.Date.IsZero() {
|
||||||
|
enc.NIL()
|
||||||
|
} else {
|
||||||
|
enc.String(envelope.Date.Format(envelopeDateLayout))
|
||||||
|
}
|
||||||
|
enc.SP()
|
||||||
|
writeNString(enc, mime.QEncoding.Encode("utf-8", envelope.Subject))
|
||||||
|
addrs := [][]imap.Address{
|
||||||
|
envelope.From,
|
||||||
|
sender,
|
||||||
|
replyTo,
|
||||||
|
envelope.To,
|
||||||
|
envelope.Cc,
|
||||||
|
envelope.Bcc,
|
||||||
|
}
|
||||||
|
for _, l := range addrs {
|
||||||
|
enc.SP()
|
||||||
|
writeAddressList(enc, l)
|
||||||
|
}
|
||||||
|
enc.SP()
|
||||||
|
if len(envelope.InReplyTo) > 0 {
|
||||||
|
enc.String("<" + strings.Join(envelope.InReplyTo, "> <") + ">")
|
||||||
|
} else {
|
||||||
|
enc.NIL()
|
||||||
|
}
|
||||||
|
enc.SP()
|
||||||
|
if envelope.MessageID != "" {
|
||||||
|
enc.String("<" + envelope.MessageID + ">")
|
||||||
|
} else {
|
||||||
|
enc.NIL()
|
||||||
|
}
|
||||||
|
enc.Special(')')
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeAddressList(enc *imapwire.Encoder, l []imap.Address) {
|
||||||
|
if len(l) == 0 {
|
||||||
|
enc.NIL()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
enc.List(len(l), func(i int) {
|
||||||
|
addr := l[i]
|
||||||
|
enc.Special('(')
|
||||||
|
writeNString(enc, mime.QEncoding.Encode("utf-8", addr.Name))
|
||||||
|
enc.SP().NIL().SP()
|
||||||
|
writeNString(enc, addr.Mailbox)
|
||||||
|
enc.SP()
|
||||||
|
writeNString(enc, addr.Host)
|
||||||
|
enc.Special(')')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeNString(enc *imapwire.Encoder, s string) {
|
||||||
|
if s == "" {
|
||||||
|
enc.NIL()
|
||||||
|
} else {
|
||||||
|
enc.String(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeSectionPart(enc *imapwire.Encoder, part []int) {
|
||||||
|
if len(part) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var l []string
|
||||||
|
for _, num := range part {
|
||||||
|
l = append(l, fmt.Sprintf("%v", num))
|
||||||
|
}
|
||||||
|
enc.Atom(strings.Join(l, "."))
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeBodyStructure(enc *imapwire.Encoder, bs imap.BodyStructure, extended bool) {
|
||||||
|
enc.Special('(')
|
||||||
|
switch bs := bs.(type) {
|
||||||
|
case *imap.BodyStructureSinglePart:
|
||||||
|
writeBodyType1part(enc, bs, extended)
|
||||||
|
case *imap.BodyStructureMultiPart:
|
||||||
|
writeBodyTypeMpart(enc, bs, extended)
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("unknown body structure type %T", bs))
|
||||||
|
}
|
||||||
|
enc.Special(')')
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeBodyType1part(enc *imapwire.Encoder, bs *imap.BodyStructureSinglePart, extended bool) {
|
||||||
|
enc.String(bs.Type).SP().String(bs.Subtype).SP()
|
||||||
|
writeBodyFldParam(enc, bs.Params)
|
||||||
|
enc.SP()
|
||||||
|
writeNString(enc, bs.ID)
|
||||||
|
enc.SP()
|
||||||
|
writeNString(enc, bs.Description)
|
||||||
|
enc.SP()
|
||||||
|
if bs.Encoding == "" {
|
||||||
|
enc.String("7bit")
|
||||||
|
} else {
|
||||||
|
// Outlook for iOS chokes on upper-case encodings
|
||||||
|
enc.String(strings.ToLower(bs.Encoding))
|
||||||
|
}
|
||||||
|
enc.SP().Number(bs.Size)
|
||||||
|
|
||||||
|
if msg := bs.MessageRFC822; msg != nil {
|
||||||
|
enc.SP()
|
||||||
|
writeEnvelope(enc, msg.Envelope)
|
||||||
|
enc.SP()
|
||||||
|
writeBodyStructure(enc, msg.BodyStructure, extended)
|
||||||
|
enc.SP().Number64(msg.NumLines)
|
||||||
|
} else if text := bs.Text; text != nil {
|
||||||
|
enc.SP().Number64(text.NumLines)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !extended {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ext := bs.Extended
|
||||||
|
|
||||||
|
enc.SP()
|
||||||
|
enc.NIL() // MD5
|
||||||
|
enc.SP()
|
||||||
|
writeBodyFldDsp(enc, ext.Disposition)
|
||||||
|
enc.SP()
|
||||||
|
writeBodyFldLang(enc, ext.Language)
|
||||||
|
enc.SP()
|
||||||
|
writeNString(enc, ext.Location)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeBodyTypeMpart(enc *imapwire.Encoder, bs *imap.BodyStructureMultiPart, extended bool) {
|
||||||
|
if len(bs.Children) == 0 {
|
||||||
|
panic("imapserver: imap.BodyStructureMultiPart must have at least one child")
|
||||||
|
}
|
||||||
|
for _, child := range bs.Children {
|
||||||
|
// ABNF for body-type-mpart doesn't have SP between body entries, and
|
||||||
|
// Outlook for iOS chokes on SP
|
||||||
|
writeBodyStructure(enc, child, extended)
|
||||||
|
}
|
||||||
|
|
||||||
|
enc.SP().String(bs.Subtype)
|
||||||
|
|
||||||
|
if !extended {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ext := bs.Extended
|
||||||
|
|
||||||
|
enc.SP()
|
||||||
|
writeBodyFldParam(enc, ext.Params)
|
||||||
|
enc.SP()
|
||||||
|
writeBodyFldDsp(enc, ext.Disposition)
|
||||||
|
enc.SP()
|
||||||
|
writeBodyFldLang(enc, ext.Language)
|
||||||
|
enc.SP()
|
||||||
|
writeNString(enc, ext.Location)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeBodyFldParam(enc *imapwire.Encoder, params map[string]string) {
|
||||||
|
if len(params) == 0 {
|
||||||
|
enc.NIL()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var l []string
|
||||||
|
for k := range params {
|
||||||
|
l = append(l, k)
|
||||||
|
}
|
||||||
|
sort.Strings(l)
|
||||||
|
|
||||||
|
enc.List(len(l), func(i int) {
|
||||||
|
k := l[i]
|
||||||
|
v := params[k]
|
||||||
|
enc.String(k).SP().String(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeBodyFldDsp(enc *imapwire.Encoder, disp *imap.BodyStructureDisposition) {
|
||||||
|
if disp == nil {
|
||||||
|
enc.NIL()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
enc.Special('(').String(disp.Value).SP()
|
||||||
|
writeBodyFldParam(enc, disp.Params)
|
||||||
|
enc.Special(')')
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeBodyFldLang(enc *imapwire.Encoder, l []string) {
|
||||||
|
if len(l) == 0 {
|
||||||
|
enc.NIL()
|
||||||
|
} else {
|
||||||
|
enc.List(len(l), func(i int) {
|
||||||
|
enc.String(l[i])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
50
imapserver/idle.go
Normal file
50
imapserver/idle.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package imapserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"runtime/debug"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Conn) handleIdle(dec *imapwire.Decoder) error {
|
||||||
|
if !dec.ExpectCRLF() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.checkState(imap.ConnStateAuthenticated); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.writeContReq("idling"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
stop := make(chan struct{})
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if v := recover(); v != nil {
|
||||||
|
c.server.logger().Printf("panic idling: %v\n%s", v, debug.Stack())
|
||||||
|
done <- fmt.Errorf("imapserver: panic idling")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
w := &UpdateWriter{conn: c, allowExpunge: true}
|
||||||
|
done <- c.session.Idle(w, stop)
|
||||||
|
}()
|
||||||
|
|
||||||
|
c.setReadTimeout(idleReadTimeout)
|
||||||
|
line, isPrefix, err := c.br.ReadLine()
|
||||||
|
close(stop)
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
} else if isPrefix || string(line) != "DONE" {
|
||||||
|
return newClientBugError("Syntax error: expected DONE to end IDLE command")
|
||||||
|
}
|
||||||
|
|
||||||
|
return <-done
|
||||||
|
}
|
||||||
511
imapserver/imapmemserver/mailbox.go
Normal file
511
imapserver/imapmemserver/mailbox.go
Normal file
@@ -0,0 +1,511 @@
|
|||||||
|
package imapmemserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/imapserver"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mailbox is an in-memory mailbox.
|
||||||
|
//
|
||||||
|
// The same mailbox can be shared between multiple connections and multiple
|
||||||
|
// users.
|
||||||
|
type Mailbox struct {
|
||||||
|
tracker *imapserver.MailboxTracker
|
||||||
|
uidValidity uint32
|
||||||
|
|
||||||
|
mutex sync.Mutex
|
||||||
|
name string
|
||||||
|
subscribed bool
|
||||||
|
specialUse []imap.MailboxAttr
|
||||||
|
l []*message
|
||||||
|
uidNext imap.UID
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMailbox creates a new mailbox.
|
||||||
|
func NewMailbox(name string, uidValidity uint32) *Mailbox {
|
||||||
|
return &Mailbox{
|
||||||
|
tracker: imapserver.NewMailboxTracker(0),
|
||||||
|
uidValidity: uidValidity,
|
||||||
|
name: name,
|
||||||
|
uidNext: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mbox *Mailbox) list(options *imap.ListOptions) *imap.ListData {
|
||||||
|
mbox.mutex.Lock()
|
||||||
|
defer mbox.mutex.Unlock()
|
||||||
|
|
||||||
|
if options.SelectSubscribed && !mbox.subscribed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if options.SelectSpecialUse && len(mbox.specialUse) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data := imap.ListData{
|
||||||
|
Mailbox: mbox.name,
|
||||||
|
Delim: mailboxDelim,
|
||||||
|
}
|
||||||
|
if mbox.subscribed {
|
||||||
|
data.Attrs = append(data.Attrs, imap.MailboxAttrSubscribed)
|
||||||
|
}
|
||||||
|
if (options.ReturnSpecialUse || options.SelectSpecialUse) && len(mbox.specialUse) > 0 {
|
||||||
|
data.Attrs = append(data.Attrs, mbox.specialUse...)
|
||||||
|
}
|
||||||
|
if options.ReturnStatus != nil {
|
||||||
|
data.Status = mbox.statusDataLocked(options.ReturnStatus)
|
||||||
|
}
|
||||||
|
return &data
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatusData returns data for the STATUS command.
|
||||||
|
func (mbox *Mailbox) StatusData(options *imap.StatusOptions) *imap.StatusData {
|
||||||
|
mbox.mutex.Lock()
|
||||||
|
defer mbox.mutex.Unlock()
|
||||||
|
return mbox.statusDataLocked(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mbox *Mailbox) statusDataLocked(options *imap.StatusOptions) *imap.StatusData {
|
||||||
|
data := imap.StatusData{Mailbox: mbox.name}
|
||||||
|
if options.NumMessages {
|
||||||
|
num := uint32(len(mbox.l))
|
||||||
|
data.NumMessages = &num
|
||||||
|
}
|
||||||
|
if options.UIDNext {
|
||||||
|
data.UIDNext = mbox.uidNext
|
||||||
|
}
|
||||||
|
if options.UIDValidity {
|
||||||
|
data.UIDValidity = mbox.uidValidity
|
||||||
|
}
|
||||||
|
if options.NumUnseen {
|
||||||
|
num := uint32(len(mbox.l)) - mbox.countByFlagLocked(imap.FlagSeen)
|
||||||
|
data.NumUnseen = &num
|
||||||
|
}
|
||||||
|
if options.NumDeleted {
|
||||||
|
num := mbox.countByFlagLocked(imap.FlagDeleted)
|
||||||
|
data.NumDeleted = &num
|
||||||
|
}
|
||||||
|
if options.Size {
|
||||||
|
size := mbox.sizeLocked()
|
||||||
|
data.Size = &size
|
||||||
|
}
|
||||||
|
if options.NumRecent {
|
||||||
|
num := uint32(0)
|
||||||
|
data.NumRecent = &num
|
||||||
|
}
|
||||||
|
return &data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mbox *Mailbox) countByFlagLocked(flag imap.Flag) uint32 {
|
||||||
|
var n uint32
|
||||||
|
for _, msg := range mbox.l {
|
||||||
|
if _, ok := msg.flags[canonicalFlag(flag)]; ok {
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mbox *Mailbox) sizeLocked() int64 {
|
||||||
|
var size int64
|
||||||
|
for _, msg := range mbox.l {
|
||||||
|
size += int64(len(msg.buf))
|
||||||
|
}
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mbox *Mailbox) appendLiteral(r imap.LiteralReader, options *imap.AppendOptions) (*imap.AppendData, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if _, err := buf.ReadFrom(r); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return mbox.appendBytes(buf.Bytes(), options), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mbox *Mailbox) copyMsg(msg *message) *imap.AppendData {
|
||||||
|
return mbox.appendBytes(msg.buf, &imap.AppendOptions{
|
||||||
|
Time: msg.t,
|
||||||
|
Flags: msg.flagList(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mbox *Mailbox) appendBytes(buf []byte, options *imap.AppendOptions) *imap.AppendData {
|
||||||
|
msg := &message{
|
||||||
|
flags: make(map[imap.Flag]struct{}),
|
||||||
|
buf: buf,
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.Time.IsZero() {
|
||||||
|
msg.t = time.Now()
|
||||||
|
} else {
|
||||||
|
msg.t = options.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, flag := range options.Flags {
|
||||||
|
msg.flags[canonicalFlag(flag)] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
mbox.mutex.Lock()
|
||||||
|
defer mbox.mutex.Unlock()
|
||||||
|
|
||||||
|
msg.uid = mbox.uidNext
|
||||||
|
mbox.uidNext++
|
||||||
|
|
||||||
|
mbox.l = append(mbox.l, msg)
|
||||||
|
mbox.tracker.QueueNumMessages(uint32(len(mbox.l)))
|
||||||
|
|
||||||
|
return &imap.AppendData{
|
||||||
|
UIDValidity: mbox.uidValidity,
|
||||||
|
UID: msg.uid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mbox *Mailbox) rename(newName string) {
|
||||||
|
mbox.mutex.Lock()
|
||||||
|
mbox.name = newName
|
||||||
|
mbox.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSubscribed changes the subscription state of this mailbox.
|
||||||
|
func (mbox *Mailbox) SetSubscribed(subscribed bool) {
|
||||||
|
mbox.mutex.Lock()
|
||||||
|
mbox.subscribed = subscribed
|
||||||
|
mbox.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mbox *Mailbox) selectDataLocked() *imap.SelectData {
|
||||||
|
flags := mbox.flagsLocked()
|
||||||
|
|
||||||
|
permanentFlags := make([]imap.Flag, len(flags))
|
||||||
|
copy(permanentFlags, flags)
|
||||||
|
permanentFlags = append(permanentFlags, imap.FlagWildcard)
|
||||||
|
|
||||||
|
// TODO: skip if IMAP4rev1 is disabled by the server, or IMAP4rev2 is
|
||||||
|
// enabled by the client
|
||||||
|
firstUnseenSeqNum := mbox.firstUnseenSeqNumLocked()
|
||||||
|
|
||||||
|
return &imap.SelectData{
|
||||||
|
Flags: flags,
|
||||||
|
PermanentFlags: permanentFlags,
|
||||||
|
NumMessages: uint32(len(mbox.l)),
|
||||||
|
FirstUnseenSeqNum: firstUnseenSeqNum,
|
||||||
|
UIDNext: mbox.uidNext,
|
||||||
|
UIDValidity: mbox.uidValidity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mbox *Mailbox) firstUnseenSeqNumLocked() uint32 {
|
||||||
|
for i, msg := range mbox.l {
|
||||||
|
seqNum := uint32(i) + 1
|
||||||
|
if _, ok := msg.flags[canonicalFlag(imap.FlagSeen)]; !ok {
|
||||||
|
return seqNum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mbox *Mailbox) flagsLocked() []imap.Flag {
|
||||||
|
m := make(map[imap.Flag]struct{})
|
||||||
|
for _, msg := range mbox.l {
|
||||||
|
for flag := range msg.flags {
|
||||||
|
m[flag] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var l []imap.Flag
|
||||||
|
for flag := range m {
|
||||||
|
l = append(l, flag)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(l, func(i, j int) bool {
|
||||||
|
return l[i] < l[j]
|
||||||
|
})
|
||||||
|
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mbox *Mailbox) Expunge(w *imapserver.ExpungeWriter, uids *imap.UIDSet) error {
|
||||||
|
expunged := make(map[*message]struct{})
|
||||||
|
mbox.mutex.Lock()
|
||||||
|
for _, msg := range mbox.l {
|
||||||
|
if uids != nil && !uids.Contains(msg.uid) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := msg.flags[canonicalFlag(imap.FlagDeleted)]; ok {
|
||||||
|
expunged[msg] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mbox.mutex.Unlock()
|
||||||
|
|
||||||
|
if len(expunged) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mbox.mutex.Lock()
|
||||||
|
mbox.expungeLocked(expunged)
|
||||||
|
mbox.mutex.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mbox *Mailbox) expungeLocked(expunged map[*message]struct{}) (seqNums []uint32) {
|
||||||
|
// TODO: optimize
|
||||||
|
|
||||||
|
// Iterate in reverse order, to keep sequence numbers consistent
|
||||||
|
var filtered []*message
|
||||||
|
for i := len(mbox.l) - 1; i >= 0; i-- {
|
||||||
|
msg := mbox.l[i]
|
||||||
|
if _, ok := expunged[msg]; ok {
|
||||||
|
seqNum := uint32(i) + 1
|
||||||
|
seqNums = append(seqNums, seqNum)
|
||||||
|
mbox.tracker.QueueExpunge(seqNum)
|
||||||
|
} else {
|
||||||
|
filtered = append(filtered, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse filtered
|
||||||
|
for i := 0; i < len(filtered)/2; i++ {
|
||||||
|
j := len(filtered) - i - 1
|
||||||
|
filtered[i], filtered[j] = filtered[j], filtered[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
mbox.l = filtered
|
||||||
|
|
||||||
|
return seqNums
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewView creates a new view into this mailbox.
|
||||||
|
//
|
||||||
|
// Callers must call MailboxView.Close once they are done with the mailbox view.
|
||||||
|
func (mbox *Mailbox) NewView() *MailboxView {
|
||||||
|
return &MailboxView{
|
||||||
|
Mailbox: mbox,
|
||||||
|
tracker: mbox.tracker.NewSession(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A MailboxView is a view into a mailbox.
|
||||||
|
//
|
||||||
|
// Each view has its own queue of pending unilateral updates.
|
||||||
|
//
|
||||||
|
// Once the mailbox view is no longer used, Close must be called.
|
||||||
|
//
|
||||||
|
// Typically, a new MailboxView is created for each IMAP connection in the
|
||||||
|
// selected state.
|
||||||
|
type MailboxView struct {
|
||||||
|
*Mailbox
|
||||||
|
tracker *imapserver.SessionTracker
|
||||||
|
searchRes imap.UIDSet
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close releases the resources allocated for the mailbox view.
|
||||||
|
func (mbox *MailboxView) Close() {
|
||||||
|
mbox.tracker.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mbox *MailboxView) Fetch(w *imapserver.FetchWriter, numSet imap.NumSet, options *imap.FetchOptions) error {
|
||||||
|
markSeen := false
|
||||||
|
for _, bs := range options.BodySection {
|
||||||
|
if !bs.Peek {
|
||||||
|
markSeen = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
mbox.forEach(numSet, func(seqNum uint32, msg *message) {
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if markSeen {
|
||||||
|
msg.flags[canonicalFlag(imap.FlagSeen)] = struct{}{}
|
||||||
|
mbox.Mailbox.tracker.QueueMessageFlags(seqNum, msg.uid, msg.flagList(), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
respWriter := w.CreateMessage(mbox.tracker.EncodeSeqNum(seqNum))
|
||||||
|
err = msg.fetch(respWriter, options)
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mbox *MailboxView) Search(numKind imapserver.NumKind, criteria *imap.SearchCriteria, options *imap.SearchOptions) (*imap.SearchData, error) {
|
||||||
|
mbox.mutex.Lock()
|
||||||
|
defer mbox.mutex.Unlock()
|
||||||
|
|
||||||
|
mbox.staticSearchCriteria(criteria)
|
||||||
|
|
||||||
|
var (
|
||||||
|
data imap.SearchData
|
||||||
|
seqSet imap.SeqSet
|
||||||
|
uidSet imap.UIDSet
|
||||||
|
)
|
||||||
|
for i, msg := range mbox.l {
|
||||||
|
seqNum := mbox.tracker.EncodeSeqNum(uint32(i) + 1)
|
||||||
|
|
||||||
|
if !msg.search(seqNum, criteria) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always populate the UID set, since it may be saved later for SEARCHRES
|
||||||
|
uidSet.AddNum(msg.uid)
|
||||||
|
|
||||||
|
var num uint32
|
||||||
|
switch numKind {
|
||||||
|
case imapserver.NumKindSeq:
|
||||||
|
if seqNum == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seqSet.AddNum(seqNum)
|
||||||
|
num = seqNum
|
||||||
|
case imapserver.NumKindUID:
|
||||||
|
num = uint32(msg.uid)
|
||||||
|
}
|
||||||
|
if data.Min == 0 || num < data.Min {
|
||||||
|
data.Min = num
|
||||||
|
}
|
||||||
|
if data.Max == 0 || num > data.Max {
|
||||||
|
data.Max = num
|
||||||
|
}
|
||||||
|
data.Count++
|
||||||
|
}
|
||||||
|
|
||||||
|
switch numKind {
|
||||||
|
case imapserver.NumKindSeq:
|
||||||
|
data.All = seqSet
|
||||||
|
case imapserver.NumKindUID:
|
||||||
|
data.All = uidSet
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.ReturnSave {
|
||||||
|
mbox.searchRes = uidSet
|
||||||
|
}
|
||||||
|
|
||||||
|
return &data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mbox *MailboxView) staticSearchCriteria(criteria *imap.SearchCriteria) {
|
||||||
|
seqNums := make([]imap.SeqSet, 0, len(criteria.SeqNum))
|
||||||
|
for _, seqSet := range criteria.SeqNum {
|
||||||
|
numSet := mbox.staticNumSet(seqSet)
|
||||||
|
switch numSet := numSet.(type) {
|
||||||
|
case imap.SeqSet:
|
||||||
|
seqNums = append(seqNums, numSet)
|
||||||
|
case imap.UIDSet: // can happen with SEARCHRES
|
||||||
|
criteria.UID = append(criteria.UID, numSet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
criteria.SeqNum = seqNums
|
||||||
|
|
||||||
|
for i, uidSet := range criteria.UID {
|
||||||
|
criteria.UID[i] = mbox.staticNumSet(uidSet).(imap.UIDSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range criteria.Not {
|
||||||
|
mbox.staticSearchCriteria(&criteria.Not[i])
|
||||||
|
}
|
||||||
|
for i := range criteria.Or {
|
||||||
|
for j := range criteria.Or[i] {
|
||||||
|
mbox.staticSearchCriteria(&criteria.Or[i][j])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mbox *MailboxView) Store(w *imapserver.FetchWriter, numSet imap.NumSet, flags *imap.StoreFlags, options *imap.StoreOptions) error {
|
||||||
|
mbox.forEach(numSet, func(seqNum uint32, msg *message) {
|
||||||
|
msg.store(flags)
|
||||||
|
mbox.Mailbox.tracker.QueueMessageFlags(seqNum, msg.uid, msg.flagList(), mbox.tracker)
|
||||||
|
})
|
||||||
|
if !flags.Silent {
|
||||||
|
return mbox.Fetch(w, numSet, &imap.FetchOptions{Flags: true})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mbox *MailboxView) Poll(w *imapserver.UpdateWriter, allowExpunge bool) error {
|
||||||
|
return mbox.tracker.Poll(w, allowExpunge)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mbox *MailboxView) Idle(w *imapserver.UpdateWriter, stop <-chan struct{}) error {
|
||||||
|
return mbox.tracker.Idle(w, stop)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mbox *MailboxView) forEach(numSet imap.NumSet, f func(seqNum uint32, msg *message)) {
|
||||||
|
mbox.mutex.Lock()
|
||||||
|
defer mbox.mutex.Unlock()
|
||||||
|
mbox.forEachLocked(numSet, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mbox *MailboxView) forEachLocked(numSet imap.NumSet, f func(seqNum uint32, msg *message)) {
|
||||||
|
// TODO: optimize
|
||||||
|
|
||||||
|
numSet = mbox.staticNumSet(numSet)
|
||||||
|
|
||||||
|
for i, msg := range mbox.l {
|
||||||
|
seqNum := uint32(i) + 1
|
||||||
|
|
||||||
|
var contains bool
|
||||||
|
switch numSet := numSet.(type) {
|
||||||
|
case imap.SeqSet:
|
||||||
|
seqNum := mbox.tracker.EncodeSeqNum(seqNum)
|
||||||
|
contains = seqNum != 0 && numSet.Contains(seqNum)
|
||||||
|
case imap.UIDSet:
|
||||||
|
contains = numSet.Contains(msg.uid)
|
||||||
|
}
|
||||||
|
if !contains {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
f(seqNum, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// staticNumSet converts a dynamic sequence set into a static one.
|
||||||
|
//
|
||||||
|
// This is necessary to properly handle the special symbol "*", which
|
||||||
|
// represents the maximum sequence number or UID in the mailbox.
|
||||||
|
//
|
||||||
|
// This function also handles the special SEARCHRES marker "$".
|
||||||
|
func (mbox *MailboxView) staticNumSet(numSet imap.NumSet) imap.NumSet {
|
||||||
|
if imap.IsSearchRes(numSet) {
|
||||||
|
return mbox.searchRes
|
||||||
|
}
|
||||||
|
|
||||||
|
switch numSet := numSet.(type) {
|
||||||
|
case imap.SeqSet:
|
||||||
|
max := uint32(len(mbox.l))
|
||||||
|
for i := range numSet {
|
||||||
|
r := &numSet[i]
|
||||||
|
staticNumRange(&r.Start, &r.Stop, max)
|
||||||
|
}
|
||||||
|
case imap.UIDSet:
|
||||||
|
max := uint32(mbox.uidNext) - 1
|
||||||
|
for i := range numSet {
|
||||||
|
r := &numSet[i]
|
||||||
|
staticNumRange((*uint32)(&r.Start), (*uint32)(&r.Stop), max)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return numSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func staticNumRange(start, stop *uint32, max uint32) {
|
||||||
|
dyn := false
|
||||||
|
if *start == 0 {
|
||||||
|
*start = max
|
||||||
|
dyn = true
|
||||||
|
}
|
||||||
|
if *stop == 0 {
|
||||||
|
*stop = max
|
||||||
|
dyn = true
|
||||||
|
}
|
||||||
|
if dyn && *start > *stop {
|
||||||
|
*start, *stop = *stop, *start
|
||||||
|
}
|
||||||
|
}
|
||||||
273
imapserver/imapmemserver/message.go
Normal file
273
imapserver/imapmemserver/message.go
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
package imapmemserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/imapserver"
|
||||||
|
gomessage "github.com/emersion/go-message"
|
||||||
|
"github.com/emersion/go-message/mail"
|
||||||
|
"github.com/emersion/go-message/textproto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type message struct {
|
||||||
|
// immutable
|
||||||
|
uid imap.UID
|
||||||
|
buf []byte
|
||||||
|
t time.Time
|
||||||
|
|
||||||
|
// mutable, protected by Mailbox.mutex
|
||||||
|
flags map[imap.Flag]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *message) fetch(w *imapserver.FetchResponseWriter, options *imap.FetchOptions) error {
|
||||||
|
w.WriteUID(msg.uid)
|
||||||
|
|
||||||
|
if options.Flags {
|
||||||
|
w.WriteFlags(msg.flagList())
|
||||||
|
}
|
||||||
|
if options.InternalDate {
|
||||||
|
w.WriteInternalDate(msg.t)
|
||||||
|
}
|
||||||
|
if options.RFC822Size {
|
||||||
|
w.WriteRFC822Size(int64(len(msg.buf)))
|
||||||
|
}
|
||||||
|
if options.Envelope {
|
||||||
|
w.WriteEnvelope(msg.envelope())
|
||||||
|
}
|
||||||
|
if options.BodyStructure != nil {
|
||||||
|
w.WriteBodyStructure(imapserver.ExtractBodyStructure(bytes.NewReader(msg.buf)))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, bs := range options.BodySection {
|
||||||
|
buf := imapserver.ExtractBodySection(bytes.NewReader(msg.buf), bs)
|
||||||
|
wc := w.WriteBodySection(bs, int64(len(buf)))
|
||||||
|
_, writeErr := wc.Write(buf)
|
||||||
|
closeErr := wc.Close()
|
||||||
|
if writeErr != nil {
|
||||||
|
return writeErr
|
||||||
|
}
|
||||||
|
if closeErr != nil {
|
||||||
|
return closeErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, bs := range options.BinarySection {
|
||||||
|
buf := imapserver.ExtractBinarySection(bytes.NewReader(msg.buf), bs)
|
||||||
|
wc := w.WriteBinarySection(bs, int64(len(buf)))
|
||||||
|
_, writeErr := wc.Write(buf)
|
||||||
|
closeErr := wc.Close()
|
||||||
|
if writeErr != nil {
|
||||||
|
return writeErr
|
||||||
|
}
|
||||||
|
if closeErr != nil {
|
||||||
|
return closeErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, bss := range options.BinarySectionSize {
|
||||||
|
n := imapserver.ExtractBinarySectionSize(bytes.NewReader(msg.buf), bss)
|
||||||
|
w.WriteBinarySectionSize(bss, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
return w.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *message) envelope() *imap.Envelope {
|
||||||
|
br := bufio.NewReader(bytes.NewReader(msg.buf))
|
||||||
|
header, err := textproto.ReadHeader(br)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return imapserver.ExtractEnvelope(header)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *message) flagList() []imap.Flag {
|
||||||
|
var flags []imap.Flag
|
||||||
|
for flag := range msg.flags {
|
||||||
|
flags = append(flags, flag)
|
||||||
|
}
|
||||||
|
return flags
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *message) store(store *imap.StoreFlags) {
|
||||||
|
switch store.Op {
|
||||||
|
case imap.StoreFlagsSet:
|
||||||
|
msg.flags = make(map[imap.Flag]struct{})
|
||||||
|
fallthrough
|
||||||
|
case imap.StoreFlagsAdd:
|
||||||
|
for _, flag := range store.Flags {
|
||||||
|
msg.flags[canonicalFlag(flag)] = struct{}{}
|
||||||
|
}
|
||||||
|
case imap.StoreFlagsDel:
|
||||||
|
for _, flag := range store.Flags {
|
||||||
|
delete(msg.flags, canonicalFlag(flag))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("unknown STORE flag operation: %v", store.Op))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *message) reader() *gomessage.Entity {
|
||||||
|
r, _ := gomessage.Read(bytes.NewReader(msg.buf))
|
||||||
|
if r == nil {
|
||||||
|
r, _ = gomessage.New(gomessage.Header{}, bytes.NewReader(nil))
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *message) search(seqNum uint32, criteria *imap.SearchCriteria) bool {
|
||||||
|
for _, seqSet := range criteria.SeqNum {
|
||||||
|
if seqNum == 0 || !seqSet.Contains(seqNum) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, uidSet := range criteria.UID {
|
||||||
|
if !uidSet.Contains(msg.uid) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !matchDate(msg.t, criteria.Since, criteria.Before) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, flag := range criteria.Flag {
|
||||||
|
if _, ok := msg.flags[canonicalFlag(flag)]; !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, flag := range criteria.NotFlag {
|
||||||
|
if _, ok := msg.flags[canonicalFlag(flag)]; ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if criteria.Larger != 0 && int64(len(msg.buf)) <= criteria.Larger {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if criteria.Smaller != 0 && int64(len(msg.buf)) >= criteria.Smaller {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
header := mail.Header{msg.reader().Header}
|
||||||
|
|
||||||
|
for _, fieldCriteria := range criteria.Header {
|
||||||
|
if !matchHeaderFields(header.FieldsByKey(fieldCriteria.Key), fieldCriteria.Value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !criteria.SentSince.IsZero() || !criteria.SentBefore.IsZero() {
|
||||||
|
t, err := header.Date()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
} else if !matchDate(t, criteria.SentSince, criteria.SentBefore) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, text := range criteria.Text {
|
||||||
|
if !matchEntity(msg.reader(), text, true) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, body := range criteria.Body {
|
||||||
|
if !matchEntity(msg.reader(), body, false) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, not := range criteria.Not {
|
||||||
|
if msg.search(seqNum, ¬) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, or := range criteria.Or {
|
||||||
|
if !msg.search(seqNum, &or[0]) && !msg.search(seqNum, &or[1]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchDate(t, since, before time.Time) bool {
|
||||||
|
// We discard time zone information by setting it to UTC.
|
||||||
|
// RFC 3501 explicitly requires zone unaware date comparison.
|
||||||
|
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
if !since.IsZero() && t.Before(since) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !before.IsZero() && !t.Before(before) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchHeaderFields(fields gomessage.HeaderFields, pattern string) bool {
|
||||||
|
if pattern == "" {
|
||||||
|
return fields.Len() > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
pattern = strings.ToLower(pattern)
|
||||||
|
for fields.Next() {
|
||||||
|
v, _ := fields.Text()
|
||||||
|
if strings.Contains(strings.ToLower(v), pattern) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchEntity(e *gomessage.Entity, pattern string, includeHeader bool) bool {
|
||||||
|
if pattern == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if includeHeader && matchHeaderFields(e.Header.Fields(), pattern) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if mr := e.MultipartReader(); mr != nil {
|
||||||
|
for {
|
||||||
|
part, err := mr.NextPart()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if matchEntity(part, pattern, includeHeader) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
t, _, err := e.Header.ContentType()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(t, "text/") && !strings.HasPrefix(t, "message/") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := io.ReadAll(e.Body)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes.Contains(bytes.ToLower(buf), bytes.ToLower([]byte(pattern)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func canonicalFlag(flag imap.Flag) imap.Flag {
|
||||||
|
return imap.Flag(strings.ToLower(string(flag)))
|
||||||
|
}
|
||||||
61
imapserver/imapmemserver/server.go
Normal file
61
imapserver/imapmemserver/server.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
// Package imapmemserver implements an in-memory IMAP server.
|
||||||
|
package imapmemserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2/imapserver"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Server is a server instance.
|
||||||
|
//
|
||||||
|
// A server contains a list of users.
|
||||||
|
type Server struct {
|
||||||
|
mutex sync.Mutex
|
||||||
|
users map[string]*User
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new server.
|
||||||
|
func New() *Server {
|
||||||
|
return &Server{
|
||||||
|
users: make(map[string]*User),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSession creates a new IMAP session.
|
||||||
|
func (s *Server) NewSession() imapserver.Session {
|
||||||
|
return &serverSession{server: s}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) user(username string) *User {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
return s.users[username]
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddUser adds a user to the server.
|
||||||
|
func (s *Server) AddUser(user *User) {
|
||||||
|
s.mutex.Lock()
|
||||||
|
s.users[user.username] = user
|
||||||
|
s.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverSession struct {
|
||||||
|
*UserSession // may be nil
|
||||||
|
|
||||||
|
server *Server // immutable
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ imapserver.Session = (*serverSession)(nil)
|
||||||
|
|
||||||
|
func (sess *serverSession) Login(username, password string) error {
|
||||||
|
u := sess.server.user(username)
|
||||||
|
if u == nil {
|
||||||
|
return imapserver.ErrAuthFailed
|
||||||
|
}
|
||||||
|
if err := u.Login(username, password); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sess.UserSession = NewUserSession(u)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
140
imapserver/imapmemserver/session.go
Normal file
140
imapserver/imapmemserver/session.go
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
package imapmemserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/imapserver"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
user = User
|
||||||
|
mailbox = MailboxView
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserSession represents a session tied to a specific user.
|
||||||
|
//
|
||||||
|
// UserSession implements imapserver.Session. Typically, a UserSession pointer
|
||||||
|
// is embedded into a larger struct which overrides Login.
|
||||||
|
type UserSession struct {
|
||||||
|
*user // immutable
|
||||||
|
*mailbox // may be nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ imapserver.SessionIMAP4rev2 = (*UserSession)(nil)
|
||||||
|
|
||||||
|
// NewUserSession creates a new user session.
|
||||||
|
func NewUserSession(user *User) *UserSession {
|
||||||
|
return &UserSession{user: user}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sess *UserSession) Close() error {
|
||||||
|
if sess != nil && sess.mailbox != nil {
|
||||||
|
sess.mailbox.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sess *UserSession) Select(name string, options *imap.SelectOptions) (*imap.SelectData, error) {
|
||||||
|
mbox, err := sess.user.mailbox(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mbox.mutex.Lock()
|
||||||
|
defer mbox.mutex.Unlock()
|
||||||
|
sess.mailbox = mbox.NewView()
|
||||||
|
return mbox.selectDataLocked(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sess *UserSession) Unselect() error {
|
||||||
|
sess.mailbox.Close()
|
||||||
|
sess.mailbox = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sess *UserSession) Copy(numSet imap.NumSet, destName string) (*imap.CopyData, error) {
|
||||||
|
dest, err := sess.user.mailbox(destName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &imap.Error{
|
||||||
|
Type: imap.StatusResponseTypeNo,
|
||||||
|
Code: imap.ResponseCodeTryCreate,
|
||||||
|
Text: "No such mailbox",
|
||||||
|
}
|
||||||
|
} else if sess.mailbox != nil && dest == sess.mailbox.Mailbox {
|
||||||
|
return nil, &imap.Error{
|
||||||
|
Type: imap.StatusResponseTypeNo,
|
||||||
|
Text: "Source and destination mailboxes are identical",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourceUIDs, destUIDs imap.UIDSet
|
||||||
|
sess.mailbox.forEach(numSet, func(seqNum uint32, msg *message) {
|
||||||
|
appendData := dest.copyMsg(msg)
|
||||||
|
sourceUIDs.AddNum(msg.uid)
|
||||||
|
destUIDs.AddNum(appendData.UID)
|
||||||
|
})
|
||||||
|
|
||||||
|
return &imap.CopyData{
|
||||||
|
UIDValidity: dest.uidValidity,
|
||||||
|
SourceUIDs: sourceUIDs,
|
||||||
|
DestUIDs: destUIDs,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sess *UserSession) Move(w *imapserver.MoveWriter, numSet imap.NumSet, destName string) error {
|
||||||
|
dest, err := sess.user.mailbox(destName)
|
||||||
|
if err != nil {
|
||||||
|
return &imap.Error{
|
||||||
|
Type: imap.StatusResponseTypeNo,
|
||||||
|
Code: imap.ResponseCodeTryCreate,
|
||||||
|
Text: "No such mailbox",
|
||||||
|
}
|
||||||
|
} else if sess.mailbox != nil && dest == sess.mailbox.Mailbox {
|
||||||
|
return &imap.Error{
|
||||||
|
Type: imap.StatusResponseTypeNo,
|
||||||
|
Text: "Source and destination mailboxes are identical",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sess.mailbox.mutex.Lock()
|
||||||
|
defer sess.mailbox.mutex.Unlock()
|
||||||
|
|
||||||
|
var sourceUIDs, destUIDs imap.UIDSet
|
||||||
|
expunged := make(map[*message]struct{})
|
||||||
|
sess.mailbox.forEachLocked(numSet, func(seqNum uint32, msg *message) {
|
||||||
|
appendData := dest.copyMsg(msg)
|
||||||
|
sourceUIDs.AddNum(msg.uid)
|
||||||
|
destUIDs.AddNum(appendData.UID)
|
||||||
|
expunged[msg] = struct{}{}
|
||||||
|
})
|
||||||
|
seqNums := sess.mailbox.expungeLocked(expunged)
|
||||||
|
|
||||||
|
err = w.WriteCopyData(&imap.CopyData{
|
||||||
|
UIDValidity: dest.uidValidity,
|
||||||
|
SourceUIDs: sourceUIDs,
|
||||||
|
DestUIDs: destUIDs,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, seqNum := range seqNums {
|
||||||
|
if err := w.WriteExpunge(sess.mailbox.tracker.EncodeSeqNum(seqNum)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sess *UserSession) Poll(w *imapserver.UpdateWriter, allowExpunge bool) error {
|
||||||
|
if sess.mailbox == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return sess.mailbox.Poll(w, allowExpunge)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sess *UserSession) Idle(w *imapserver.UpdateWriter, stop <-chan struct{}) error {
|
||||||
|
if sess.mailbox == nil {
|
||||||
|
return nil // TODO
|
||||||
|
}
|
||||||
|
return sess.mailbox.Idle(w, stop)
|
||||||
|
}
|
||||||
204
imapserver/imapmemserver/user.go
Normal file
204
imapserver/imapmemserver/user.go
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
package imapmemserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/imapserver"
|
||||||
|
)
|
||||||
|
|
||||||
|
const mailboxDelim rune = '/'
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
username, password string
|
||||||
|
|
||||||
|
mutex sync.Mutex
|
||||||
|
mailboxes map[string]*Mailbox
|
||||||
|
prevUidValidity uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUser(username, password string) *User {
|
||||||
|
return &User{
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
mailboxes: make(map[string]*Mailbox),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) Login(username, password string) error {
|
||||||
|
if username != u.username {
|
||||||
|
return imapserver.ErrAuthFailed
|
||||||
|
}
|
||||||
|
if subtle.ConstantTimeCompare([]byte(password), []byte(u.password)) != 1 {
|
||||||
|
return imapserver.ErrAuthFailed
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) mailboxLocked(name string) (*Mailbox, error) {
|
||||||
|
mbox := u.mailboxes[name]
|
||||||
|
if mbox == nil {
|
||||||
|
return nil, &imap.Error{
|
||||||
|
Type: imap.StatusResponseTypeNo,
|
||||||
|
Code: imap.ResponseCodeNonExistent,
|
||||||
|
Text: "No such mailbox",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mbox, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) mailbox(name string) (*Mailbox, error) {
|
||||||
|
u.mutex.Lock()
|
||||||
|
defer u.mutex.Unlock()
|
||||||
|
return u.mailboxLocked(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) Status(name string, options *imap.StatusOptions) (*imap.StatusData, error) {
|
||||||
|
mbox, err := u.mailbox(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return mbox.StatusData(options), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) List(w *imapserver.ListWriter, ref string, patterns []string, options *imap.ListOptions) error {
|
||||||
|
u.mutex.Lock()
|
||||||
|
defer u.mutex.Unlock()
|
||||||
|
|
||||||
|
// TODO: fail if ref doesn't exist
|
||||||
|
|
||||||
|
if len(patterns) == 0 {
|
||||||
|
return w.WriteList(&imap.ListData{
|
||||||
|
Attrs: []imap.MailboxAttr{imap.MailboxAttrNoSelect},
|
||||||
|
Delim: mailboxDelim,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var l []imap.ListData
|
||||||
|
for name, mbox := range u.mailboxes {
|
||||||
|
match := false
|
||||||
|
for _, pattern := range patterns {
|
||||||
|
match = imapserver.MatchList(name, mailboxDelim, ref, pattern)
|
||||||
|
if match {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !match {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
data := mbox.list(options)
|
||||||
|
if data != nil {
|
||||||
|
l = append(l, *data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(l, func(i, j int) bool {
|
||||||
|
return l[i].Mailbox < l[j].Mailbox
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, data := range l {
|
||||||
|
if err := w.WriteList(&data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) Append(mailbox string, r imap.LiteralReader, options *imap.AppendOptions) (*imap.AppendData, error) {
|
||||||
|
mbox, err := u.mailbox(mailbox)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &imap.Error{
|
||||||
|
Type: imap.StatusResponseTypeNo,
|
||||||
|
Code: imap.ResponseCodeTryCreate,
|
||||||
|
Text: "No such mailbox",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mbox.appendLiteral(r, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) Create(name string, options *imap.CreateOptions) error {
|
||||||
|
u.mutex.Lock()
|
||||||
|
defer u.mutex.Unlock()
|
||||||
|
|
||||||
|
name = strings.TrimRight(name, string(mailboxDelim))
|
||||||
|
|
||||||
|
if u.mailboxes[name] != nil {
|
||||||
|
return &imap.Error{
|
||||||
|
Type: imap.StatusResponseTypeNo,
|
||||||
|
Code: imap.ResponseCodeAlreadyExists,
|
||||||
|
Text: "Mailbox already exists",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UIDVALIDITY must change if a mailbox is deleted and re-created with the
|
||||||
|
// same name.
|
||||||
|
u.prevUidValidity++
|
||||||
|
u.mailboxes[name] = NewMailbox(name, u.prevUidValidity)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) Delete(name string) error {
|
||||||
|
u.mutex.Lock()
|
||||||
|
defer u.mutex.Unlock()
|
||||||
|
|
||||||
|
if _, err := u.mailboxLocked(name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(u.mailboxes, name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) Rename(oldName, newName string, options *imap.RenameOptions) error {
|
||||||
|
u.mutex.Lock()
|
||||||
|
defer u.mutex.Unlock()
|
||||||
|
|
||||||
|
newName = strings.TrimRight(newName, string(mailboxDelim))
|
||||||
|
|
||||||
|
mbox, err := u.mailboxLocked(oldName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.mailboxes[newName] != nil {
|
||||||
|
return &imap.Error{
|
||||||
|
Type: imap.StatusResponseTypeNo,
|
||||||
|
Code: imap.ResponseCodeAlreadyExists,
|
||||||
|
Text: "Mailbox already exists",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mbox.rename(newName)
|
||||||
|
u.mailboxes[newName] = mbox
|
||||||
|
delete(u.mailboxes, oldName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) Subscribe(name string) error {
|
||||||
|
mbox, err := u.mailbox(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
mbox.SetSubscribed(true)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) Unsubscribe(name string) error {
|
||||||
|
mbox, err := u.mailbox(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
mbox.SetSubscribed(false)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) Namespace() (*imap.NamespaceData, error) {
|
||||||
|
return &imap.NamespaceData{
|
||||||
|
Personal: []imap.NamespaceDescriptor{{Delim: mailboxDelim}},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
329
imapserver/list.go
Normal file
329
imapserver/list.go
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
package imapserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/utf7"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Conn) handleList(dec *imapwire.Decoder) error {
|
||||||
|
ref, pattern, options, err := readListCmd(dec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.checkState(imap.ConnStateAuthenticated); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
w := &ListWriter{
|
||||||
|
conn: c,
|
||||||
|
options: options,
|
||||||
|
}
|
||||||
|
return c.session.List(w, ref, pattern, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) handleLSub(dec *imapwire.Decoder) error {
|
||||||
|
var ref string
|
||||||
|
if !dec.ExpectSP() || !dec.ExpectMailbox(&ref) || !dec.ExpectSP() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
pattern, err := readListMailbox(dec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !dec.ExpectCRLF() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.checkState(imap.ConnStateAuthenticated); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
options := &imap.ListOptions{SelectSubscribed: true}
|
||||||
|
w := &ListWriter{
|
||||||
|
conn: c,
|
||||||
|
lsub: true,
|
||||||
|
}
|
||||||
|
return c.session.List(w, ref, []string{pattern}, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) writeList(data *imap.ListData) error {
|
||||||
|
enc := newResponseEncoder(c)
|
||||||
|
defer enc.end()
|
||||||
|
|
||||||
|
enc.Atom("*").SP().Atom("LIST").SP()
|
||||||
|
enc.List(len(data.Attrs), func(i int) {
|
||||||
|
enc.MailboxAttr(data.Attrs[i])
|
||||||
|
})
|
||||||
|
enc.SP()
|
||||||
|
if data.Delim == 0 {
|
||||||
|
enc.NIL()
|
||||||
|
} else {
|
||||||
|
enc.Quoted(string(data.Delim))
|
||||||
|
}
|
||||||
|
enc.SP().Mailbox(data.Mailbox)
|
||||||
|
|
||||||
|
var ext []string
|
||||||
|
if data.ChildInfo != nil {
|
||||||
|
ext = append(ext, "CHILDINFO")
|
||||||
|
}
|
||||||
|
if data.OldName != "" {
|
||||||
|
ext = append(ext, "OLDNAME")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: omit extended data if the client didn't ask for it
|
||||||
|
if len(ext) > 0 {
|
||||||
|
enc.SP().List(len(ext), func(i int) {
|
||||||
|
name := ext[i]
|
||||||
|
enc.Atom(name).SP()
|
||||||
|
switch name {
|
||||||
|
case "CHILDINFO":
|
||||||
|
enc.Special('(')
|
||||||
|
if data.ChildInfo.Subscribed {
|
||||||
|
enc.Quoted("SUBSCRIBED")
|
||||||
|
}
|
||||||
|
enc.Special(')')
|
||||||
|
case "OLDNAME":
|
||||||
|
enc.Special('(').Mailbox(data.OldName).Special(')')
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("imapserver: unknown LIST extended-item %v", name))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return enc.CRLF()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) writeLSub(data *imap.ListData) error {
|
||||||
|
enc := newResponseEncoder(c)
|
||||||
|
defer enc.end()
|
||||||
|
|
||||||
|
enc.Atom("*").SP().Atom("LSUB").SP()
|
||||||
|
enc.List(len(data.Attrs), func(i int) {
|
||||||
|
enc.MailboxAttr(data.Attrs[i])
|
||||||
|
})
|
||||||
|
enc.SP()
|
||||||
|
if data.Delim == 0 {
|
||||||
|
enc.NIL()
|
||||||
|
} else {
|
||||||
|
enc.Quoted(string(data.Delim))
|
||||||
|
}
|
||||||
|
enc.SP().Mailbox(data.Mailbox)
|
||||||
|
return enc.CRLF()
|
||||||
|
}
|
||||||
|
|
||||||
|
func readListCmd(dec *imapwire.Decoder) (ref string, patterns []string, options *imap.ListOptions, err error) {
|
||||||
|
options = &imap.ListOptions{}
|
||||||
|
|
||||||
|
if !dec.ExpectSP() {
|
||||||
|
return "", nil, nil, dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
hasSelectOpts, err := dec.List(func() error {
|
||||||
|
var selectOpt string
|
||||||
|
if !dec.ExpectAString(&selectOpt) {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
switch strings.ToUpper(selectOpt) {
|
||||||
|
case "SUBSCRIBED":
|
||||||
|
options.SelectSubscribed = true
|
||||||
|
case "REMOTE":
|
||||||
|
options.SelectRemote = true
|
||||||
|
case "RECURSIVEMATCH":
|
||||||
|
options.SelectRecursiveMatch = true
|
||||||
|
case "SPECIAL-USE":
|
||||||
|
options.SelectSpecialUse = true
|
||||||
|
default:
|
||||||
|
return newClientBugError("Unknown LIST select option")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, nil, fmt.Errorf("in list-select-opts: %w", err)
|
||||||
|
}
|
||||||
|
if hasSelectOpts && !dec.ExpectSP() {
|
||||||
|
return "", nil, nil, dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dec.ExpectMailbox(&ref) || !dec.ExpectSP() {
|
||||||
|
return "", nil, nil, dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
hasPatterns, err := dec.List(func() error {
|
||||||
|
pattern, err := readListMailbox(dec)
|
||||||
|
if err == nil && pattern != "" {
|
||||||
|
patterns = append(patterns, pattern)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, nil, err
|
||||||
|
} else if hasPatterns && len(patterns) == 0 {
|
||||||
|
return "", nil, nil, newClientBugError("LIST-EXTENDED requires a non-empty parenthesized pattern list")
|
||||||
|
} else if !hasPatterns {
|
||||||
|
pattern, err := readListMailbox(dec)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, nil, err
|
||||||
|
}
|
||||||
|
if pattern != "" {
|
||||||
|
patterns = append(patterns, pattern)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if dec.SP() { // list-return-opts
|
||||||
|
var atom string
|
||||||
|
if !dec.ExpectAtom(&atom) || !dec.Expect(strings.EqualFold(atom, "RETURN"), "RETURN") || !dec.ExpectSP() {
|
||||||
|
return "", nil, nil, dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
err := dec.ExpectList(func() error {
|
||||||
|
return readReturnOption(dec, options)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, nil, fmt.Errorf("in list-return-opts: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dec.ExpectCRLF() {
|
||||||
|
return "", nil, nil, dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.SelectRecursiveMatch && !options.SelectSubscribed {
|
||||||
|
return "", nil, nil, newClientBugError("The LIST RECURSIVEMATCH select option requires SUBSCRIBED")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ref, patterns, options, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readListMailbox(dec *imapwire.Decoder) (string, error) {
|
||||||
|
var mailbox string
|
||||||
|
if !dec.String(&mailbox) {
|
||||||
|
if !dec.Expect(dec.Func(&mailbox, isListChar), "list-char") {
|
||||||
|
return "", dec.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return utf7.Decode(mailbox)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isListChar(ch byte) bool {
|
||||||
|
switch ch {
|
||||||
|
case '%', '*': // list-wildcards
|
||||||
|
return true
|
||||||
|
case ']': // resp-specials
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return imapwire.IsAtomChar(ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readReturnOption(dec *imapwire.Decoder, options *imap.ListOptions) error {
|
||||||
|
var name string
|
||||||
|
if !dec.ExpectAtom(&name) {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strings.ToUpper(name) {
|
||||||
|
case "SUBSCRIBED":
|
||||||
|
options.ReturnSubscribed = true
|
||||||
|
case "CHILDREN":
|
||||||
|
options.ReturnChildren = true
|
||||||
|
case "SPECIAL-USE":
|
||||||
|
options.ReturnSpecialUse = true
|
||||||
|
case "STATUS":
|
||||||
|
if !dec.ExpectSP() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
options.ReturnStatus = new(imap.StatusOptions)
|
||||||
|
return dec.ExpectList(func() error {
|
||||||
|
return readStatusItem(dec, options.ReturnStatus)
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
return newClientBugError("Unknown LIST RETURN options")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListWriter writes LIST responses.
|
||||||
|
type ListWriter struct {
|
||||||
|
conn *Conn
|
||||||
|
options *imap.ListOptions
|
||||||
|
lsub bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteList writes a single LIST response for a mailbox.
|
||||||
|
func (w *ListWriter) WriteList(data *imap.ListData) error {
|
||||||
|
if w.lsub {
|
||||||
|
return w.conn.writeLSub(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.conn.writeList(data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if w.options.ReturnStatus != nil && data.Status != nil {
|
||||||
|
if err := w.conn.writeStatus(data.Status, w.options.ReturnStatus); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchList checks whether a reference and a pattern matches a mailbox.
|
||||||
|
func MatchList(name string, delim rune, reference, pattern string) bool {
|
||||||
|
var delimStr string
|
||||||
|
if delim != 0 {
|
||||||
|
delimStr = string(delim)
|
||||||
|
}
|
||||||
|
|
||||||
|
if delimStr != "" && strings.HasPrefix(pattern, delimStr) {
|
||||||
|
reference = ""
|
||||||
|
pattern = strings.TrimPrefix(pattern, delimStr)
|
||||||
|
}
|
||||||
|
if reference != "" {
|
||||||
|
if delimStr != "" && !strings.HasSuffix(reference, delimStr) {
|
||||||
|
reference += delimStr
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(name, reference) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
name = strings.TrimPrefix(name, reference)
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchList(name, delimStr, pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchList(name, delim, pattern string) bool {
|
||||||
|
// TODO: optimize
|
||||||
|
|
||||||
|
i := strings.IndexAny(pattern, "*%")
|
||||||
|
if i == -1 {
|
||||||
|
// No more wildcards
|
||||||
|
return name == pattern
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get parts before and after wildcard
|
||||||
|
chunk, wildcard, rest := pattern[0:i], pattern[i], pattern[i+1:]
|
||||||
|
|
||||||
|
// Check that name begins with chunk
|
||||||
|
if len(chunk) > 0 && !strings.HasPrefix(name, chunk) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
name = strings.TrimPrefix(name, chunk)
|
||||||
|
|
||||||
|
// Expand wildcard
|
||||||
|
var j int
|
||||||
|
for j = 0; j < len(name); j++ {
|
||||||
|
if wildcard == '%' && string(name[j]) == delim {
|
||||||
|
break // Stop on delimiter if wildcard is %
|
||||||
|
}
|
||||||
|
// Try to match the rest from here
|
||||||
|
if matchList(name[j:], delim, rest) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchList(name[j:], delim, rest)
|
||||||
|
}
|
||||||
51
imapserver/list_test.go
Normal file
51
imapserver/list_test.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package imapserver_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2/imapserver"
|
||||||
|
)
|
||||||
|
|
||||||
|
var matchListTests = []struct {
|
||||||
|
name, ref, pattern string
|
||||||
|
result bool
|
||||||
|
}{
|
||||||
|
{name: "INBOX", pattern: "INBOX", result: true},
|
||||||
|
{name: "INBOX", pattern: "Asuka", result: false},
|
||||||
|
{name: "INBOX", pattern: "*", result: true},
|
||||||
|
{name: "INBOX", pattern: "%", result: true},
|
||||||
|
{name: "Neon Genesis Evangelion/Misato", pattern: "*", result: true},
|
||||||
|
{name: "Neon Genesis Evangelion/Misato", pattern: "%", result: false},
|
||||||
|
{name: "Neon Genesis Evangelion/Misato", pattern: "Neon Genesis Evangelion/*", result: true},
|
||||||
|
{name: "Neon Genesis Evangelion/Misato", pattern: "Neon Genesis Evangelion/%", result: true},
|
||||||
|
{name: "Neon Genesis Evangelion/Misato", pattern: "Neo* Evangelion/Misato", result: true},
|
||||||
|
{name: "Neon Genesis Evangelion/Misato", pattern: "Neo% Evangelion/Misato", result: true},
|
||||||
|
{name: "Neon Genesis Evangelion/Misato", pattern: "*Eva*/Misato", result: true},
|
||||||
|
{name: "Neon Genesis Evangelion/Misato", pattern: "%Eva%/Misato", result: true},
|
||||||
|
{name: "Neon Genesis Evangelion/Misato", pattern: "*X*/Misato", result: false},
|
||||||
|
{name: "Neon Genesis Evangelion/Misato", pattern: "%X%/Misato", result: false},
|
||||||
|
{name: "Neon Genesis Evangelion/Misato", pattern: "Neon Genesis Evangelion/Mi%o", result: true},
|
||||||
|
{name: "Neon Genesis Evangelion/Misato", pattern: "Neon Genesis Evangelion/Mi%too", result: false},
|
||||||
|
{name: "Misato/Misato", pattern: "Mis*to/Misato", result: true},
|
||||||
|
{name: "Misato/Misato", pattern: "Mis*to", result: true},
|
||||||
|
{name: "Misato/Misato/Misato", pattern: "Mis*to/Mis%to", result: true},
|
||||||
|
{name: "Misato/Misato", pattern: "Mis**to/Misato", result: true},
|
||||||
|
{name: "Misato/Misato", pattern: "Misat%/Misato", result: true},
|
||||||
|
{name: "Misato/Misato", pattern: "Misat%Misato", result: false},
|
||||||
|
{name: "Misato/Misato", ref: "Misato", pattern: "Misato", result: true},
|
||||||
|
{name: "Misato/Misato", ref: "Misato/", pattern: "Misato", result: true},
|
||||||
|
{name: "Misato/Misato", ref: "Shinji", pattern: "/Misato/*", result: true},
|
||||||
|
{name: "Misato/Misato", ref: "Misato", pattern: "/Misato", result: false},
|
||||||
|
{name: "Misato/Misato", ref: "Misato", pattern: "Shinji", result: false},
|
||||||
|
{name: "Misato/Misato", ref: "Shinji", pattern: "Misato", result: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMatchList(t *testing.T) {
|
||||||
|
delim := '/'
|
||||||
|
for _, test := range matchListTests {
|
||||||
|
result := imapserver.MatchList(test.name, delim, test.ref, test.pattern)
|
||||||
|
if result != test.result {
|
||||||
|
t.Errorf("matching name %q with pattern %q and reference %q returns %v, but expected %v", test.name, test.pattern, test.ref, result, test.result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
imapserver/login.go
Normal file
28
imapserver/login.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package imapserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Conn) handleLogin(tag string, dec *imapwire.Decoder) error {
|
||||||
|
var username, password string
|
||||||
|
if !dec.ExpectSP() || !dec.ExpectAString(&username) || !dec.ExpectSP() || !dec.ExpectAString(&password) || !dec.ExpectCRLF() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
if err := c.checkState(imap.ConnStateNotAuthenticated); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !c.canAuth() {
|
||||||
|
return &imap.Error{
|
||||||
|
Type: imap.StatusResponseTypeNo,
|
||||||
|
Code: imap.ResponseCodePrivacyRequired,
|
||||||
|
Text: "TLS is required to authenticate",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := c.session.Login(username, password); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.state = imap.ConnStateAuthenticated
|
||||||
|
return c.writeCapabilityStatus(tag, imap.StatusResponseTypeOK, "Logged in")
|
||||||
|
}
|
||||||
336
imapserver/message.go
Normal file
336
imapserver/message.go
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
package imapserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
gomessage "github.com/emersion/go-message"
|
||||||
|
"github.com/emersion/go-message/mail"
|
||||||
|
"github.com/emersion/go-message/textproto"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExtractBodySection extracts a section of a message body.
|
||||||
|
//
|
||||||
|
// It can be used by server backends to implement Session.Fetch.
|
||||||
|
func ExtractBodySection(r io.Reader, item *imap.FetchItemBodySection) []byte {
|
||||||
|
var (
|
||||||
|
header textproto.Header
|
||||||
|
body io.Reader
|
||||||
|
)
|
||||||
|
|
||||||
|
br := bufio.NewReader(r)
|
||||||
|
header, err := textproto.ReadHeader(br)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
body = br
|
||||||
|
|
||||||
|
parentMediaType, header, body := findMessagePart(header, body, item.Part)
|
||||||
|
if body == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(item.Part) > 0 {
|
||||||
|
switch item.Specifier {
|
||||||
|
case imap.PartSpecifierHeader, imap.PartSpecifierText:
|
||||||
|
header, body = openMessagePart(header, body, parentMediaType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter header fields
|
||||||
|
if len(item.HeaderFields) > 0 {
|
||||||
|
keep := make(map[string]struct{})
|
||||||
|
for _, k := range item.HeaderFields {
|
||||||
|
keep[strings.ToLower(k)] = struct{}{}
|
||||||
|
}
|
||||||
|
for field := header.Fields(); field.Next(); {
|
||||||
|
if _, ok := keep[strings.ToLower(field.Key())]; !ok {
|
||||||
|
field.Del()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, k := range item.HeaderFieldsNot {
|
||||||
|
header.Del(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the requested data to a buffer
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
writeHeader := true
|
||||||
|
switch item.Specifier {
|
||||||
|
case imap.PartSpecifierNone:
|
||||||
|
writeHeader = len(item.Part) == 0
|
||||||
|
case imap.PartSpecifierText:
|
||||||
|
writeHeader = false
|
||||||
|
}
|
||||||
|
if writeHeader {
|
||||||
|
if err := textproto.WriteHeader(&buf, header); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch item.Specifier {
|
||||||
|
case imap.PartSpecifierNone, imap.PartSpecifierText:
|
||||||
|
if _, err := io.Copy(&buf, body); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return extractPartial(buf.Bytes(), item.Partial)
|
||||||
|
}
|
||||||
|
|
||||||
|
func findMessagePart(header textproto.Header, body io.Reader, partPath []int) (string, textproto.Header, io.Reader) {
|
||||||
|
// First part of non-multipart message refers to the message itself
|
||||||
|
msgHeader := gomessage.Header{header}
|
||||||
|
mediaType, _, _ := msgHeader.ContentType()
|
||||||
|
if !strings.HasPrefix(mediaType, "multipart/") && len(partPath) > 0 && partPath[0] == 1 {
|
||||||
|
partPath = partPath[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
var parentMediaType string
|
||||||
|
for i := 0; i < len(partPath); i++ {
|
||||||
|
partNum := partPath[i]
|
||||||
|
|
||||||
|
header, body = openMessagePart(header, body, parentMediaType)
|
||||||
|
|
||||||
|
msgHeader := gomessage.Header{header}
|
||||||
|
mediaType, typeParams, _ := msgHeader.ContentType()
|
||||||
|
if !strings.HasPrefix(mediaType, "multipart/") {
|
||||||
|
if partNum != 1 {
|
||||||
|
return "", textproto.Header{}, nil
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
mr := textproto.NewMultipartReader(body, typeParams["boundary"])
|
||||||
|
found := false
|
||||||
|
for j := 1; j <= partNum; j++ {
|
||||||
|
p, err := mr.NextPart()
|
||||||
|
if err != nil {
|
||||||
|
return "", textproto.Header{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if j == partNum {
|
||||||
|
parentMediaType = mediaType
|
||||||
|
header = p.Header
|
||||||
|
body = p
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return "", textproto.Header{}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parentMediaType, header, body
|
||||||
|
}
|
||||||
|
|
||||||
|
func openMessagePart(header textproto.Header, body io.Reader, parentMediaType string) (textproto.Header, io.Reader) {
|
||||||
|
msgHeader := gomessage.Header{header}
|
||||||
|
mediaType, _, _ := msgHeader.ContentType()
|
||||||
|
if !msgHeader.Has("Content-Type") && parentMediaType == "multipart/digest" {
|
||||||
|
mediaType = "message/rfc822"
|
||||||
|
}
|
||||||
|
if mediaType == "message/rfc822" || mediaType == "message/global" {
|
||||||
|
br := bufio.NewReader(body)
|
||||||
|
header, _ = textproto.ReadHeader(br)
|
||||||
|
return header, br
|
||||||
|
}
|
||||||
|
return header, body
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractPartial(b []byte, partial *imap.SectionPartial) []byte {
|
||||||
|
if partial == nil {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
end := partial.Offset + partial.Size
|
||||||
|
if partial.Offset > int64(len(b)) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if end > int64(len(b)) {
|
||||||
|
end = int64(len(b))
|
||||||
|
}
|
||||||
|
return b[partial.Offset:end]
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExtractBinarySection(r io.Reader, item *imap.FetchItemBinarySection) []byte {
|
||||||
|
var (
|
||||||
|
header textproto.Header
|
||||||
|
body io.Reader
|
||||||
|
)
|
||||||
|
|
||||||
|
br := bufio.NewReader(r)
|
||||||
|
header, err := textproto.ReadHeader(br)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
body = br
|
||||||
|
|
||||||
|
_, header, body = findMessagePart(header, body, item.Part)
|
||||||
|
if body == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
part, err := gomessage.New(gomessage.Header{header}, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the requested data to a buffer
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
if len(item.Part) == 0 {
|
||||||
|
if err := textproto.WriteHeader(&buf, part.Header.Header); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := io.Copy(&buf, part.Body); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return extractPartial(buf.Bytes(), item.Partial)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExtractBinarySectionSize(r io.Reader, item *imap.FetchItemBinarySectionSize) uint32 {
|
||||||
|
// TODO: optimize
|
||||||
|
b := ExtractBinarySection(r, &imap.FetchItemBinarySection{Part: item.Part})
|
||||||
|
return uint32(len(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractEnvelope returns a message envelope from its header.
|
||||||
|
//
|
||||||
|
// It can be used by server backends to implement Session.Fetch.
|
||||||
|
func ExtractEnvelope(h textproto.Header) *imap.Envelope {
|
||||||
|
mh := mail.Header{gomessage.Header{h}}
|
||||||
|
date, _ := mh.Date()
|
||||||
|
subject, _ := mh.Subject()
|
||||||
|
inReplyTo, _ := mh.MsgIDList("In-Reply-To")
|
||||||
|
messageID, _ := mh.MessageID()
|
||||||
|
return &imap.Envelope{
|
||||||
|
Date: date,
|
||||||
|
Subject: subject,
|
||||||
|
From: parseAddressList(mh, "From"),
|
||||||
|
Sender: parseAddressList(mh, "Sender"),
|
||||||
|
ReplyTo: parseAddressList(mh, "Reply-To"),
|
||||||
|
To: parseAddressList(mh, "To"),
|
||||||
|
Cc: parseAddressList(mh, "Cc"),
|
||||||
|
Bcc: parseAddressList(mh, "Bcc"),
|
||||||
|
InReplyTo: inReplyTo,
|
||||||
|
MessageID: messageID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAddressList(mh mail.Header, k string) []imap.Address {
|
||||||
|
// TODO: handle groups
|
||||||
|
addrs, _ := mh.AddressList(k)
|
||||||
|
var l []imap.Address
|
||||||
|
for _, addr := range addrs {
|
||||||
|
mailbox, host, ok := strings.Cut(addr.Address, "@")
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
l = append(l, imap.Address{
|
||||||
|
Name: addr.Name,
|
||||||
|
Mailbox: mailbox,
|
||||||
|
Host: host,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractBodyStructure extracts the structure of a message body.
|
||||||
|
//
|
||||||
|
// It can be used by server backends to implement Session.Fetch.
|
||||||
|
func ExtractBodyStructure(r io.Reader) imap.BodyStructure {
|
||||||
|
br := bufio.NewReader(r)
|
||||||
|
header, _ := textproto.ReadHeader(br)
|
||||||
|
return extractBodyStructure(header, br)
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractBodyStructure(rawHeader textproto.Header, r io.Reader) imap.BodyStructure {
|
||||||
|
header := gomessage.Header{rawHeader}
|
||||||
|
|
||||||
|
mediaType, typeParams, _ := header.ContentType()
|
||||||
|
primaryType, subType, _ := strings.Cut(mediaType, "/")
|
||||||
|
|
||||||
|
if primaryType == "multipart" {
|
||||||
|
bs := &imap.BodyStructureMultiPart{Subtype: subType}
|
||||||
|
mr := textproto.NewMultipartReader(r, typeParams["boundary"])
|
||||||
|
for {
|
||||||
|
part, _ := mr.NextPart()
|
||||||
|
if part == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
bs.Children = append(bs.Children, extractBodyStructure(part.Header, part))
|
||||||
|
}
|
||||||
|
bs.Extended = &imap.BodyStructureMultiPartExt{
|
||||||
|
Params: typeParams,
|
||||||
|
Disposition: getContentDisposition(header),
|
||||||
|
Language: getContentLanguage(header),
|
||||||
|
Location: header.Get("Content-Location"),
|
||||||
|
}
|
||||||
|
return bs
|
||||||
|
} else {
|
||||||
|
body, _ := io.ReadAll(r) // TODO: optimize
|
||||||
|
bs := &imap.BodyStructureSinglePart{
|
||||||
|
Type: primaryType,
|
||||||
|
Subtype: subType,
|
||||||
|
Params: typeParams,
|
||||||
|
ID: header.Get("Content-Id"),
|
||||||
|
Description: header.Get("Content-Description"),
|
||||||
|
Encoding: header.Get("Content-Transfer-Encoding"),
|
||||||
|
Size: uint32(len(body)),
|
||||||
|
}
|
||||||
|
if mediaType == "message/rfc822" || mediaType == "message/global" {
|
||||||
|
br := bufio.NewReader(bytes.NewReader(body))
|
||||||
|
childHeader, _ := textproto.ReadHeader(br)
|
||||||
|
bs.MessageRFC822 = &imap.BodyStructureMessageRFC822{
|
||||||
|
Envelope: ExtractEnvelope(childHeader),
|
||||||
|
BodyStructure: extractBodyStructure(childHeader, br),
|
||||||
|
NumLines: int64(bytes.Count(body, []byte("\n"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if primaryType == "text" {
|
||||||
|
bs.Text = &imap.BodyStructureText{
|
||||||
|
NumLines: int64(bytes.Count(body, []byte("\n"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bs.Extended = &imap.BodyStructureSinglePartExt{
|
||||||
|
Disposition: getContentDisposition(header),
|
||||||
|
Language: getContentLanguage(header),
|
||||||
|
Location: header.Get("Content-Location"),
|
||||||
|
}
|
||||||
|
return bs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getContentDisposition(header gomessage.Header) *imap.BodyStructureDisposition {
|
||||||
|
disp, dispParams, _ := header.ContentDisposition()
|
||||||
|
if disp == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &imap.BodyStructureDisposition{
|
||||||
|
Value: disp,
|
||||||
|
Params: dispParams,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getContentLanguage(header gomessage.Header) []string {
|
||||||
|
v := header.Get("Content-Language")
|
||||||
|
if v == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// TODO: handle CFWS
|
||||||
|
l := strings.Split(v, ",")
|
||||||
|
for i, lang := range l {
|
||||||
|
l[i] = strings.TrimSpace(lang)
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
40
imapserver/move.go
Normal file
40
imapserver/move.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package imapserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Conn) handleMove(dec *imapwire.Decoder, numKind NumKind) error {
|
||||||
|
numSet, dest, err := readCopy(numKind, dec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.checkState(imap.ConnStateSelected); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
session, ok := c.session.(SessionMove)
|
||||||
|
if !ok {
|
||||||
|
return newClientBugError("MOVE is not supported")
|
||||||
|
}
|
||||||
|
w := &MoveWriter{conn: c}
|
||||||
|
return session.Move(w, numSet, dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveWriter writes responses for the MOVE command.
|
||||||
|
//
|
||||||
|
// Servers must first call WriteCopyData once, then call WriteExpunge any
|
||||||
|
// number of times.
|
||||||
|
type MoveWriter struct {
|
||||||
|
conn *Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteCopyData writes the untagged COPYUID response for a MOVE command.
|
||||||
|
func (w *MoveWriter) WriteCopyData(data *imap.CopyData) error {
|
||||||
|
return w.conn.writeCopyOK("", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteExpunge writes an EXPUNGE response for a MOVE command.
|
||||||
|
func (w *MoveWriter) WriteExpunge(seqNum uint32) error {
|
||||||
|
return w.conn.writeExpunge(seqNum)
|
||||||
|
}
|
||||||
54
imapserver/namespace.go
Normal file
54
imapserver/namespace.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package imapserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Conn) handleNamespace(dec *imapwire.Decoder) error {
|
||||||
|
if !dec.ExpectCRLF() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.checkState(imap.ConnStateAuthenticated); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
session, ok := c.session.(SessionNamespace)
|
||||||
|
if !ok {
|
||||||
|
return newClientBugError("NAMESPACE is not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := session.Namespace()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
enc := newResponseEncoder(c)
|
||||||
|
defer enc.end()
|
||||||
|
enc.Atom("*").SP().Atom("NAMESPACE").SP()
|
||||||
|
writeNamespace(enc.Encoder, data.Personal)
|
||||||
|
enc.SP()
|
||||||
|
writeNamespace(enc.Encoder, data.Other)
|
||||||
|
enc.SP()
|
||||||
|
writeNamespace(enc.Encoder, data.Shared)
|
||||||
|
return enc.CRLF()
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeNamespace(enc *imapwire.Encoder, l []imap.NamespaceDescriptor) {
|
||||||
|
if l == nil {
|
||||||
|
enc.NIL()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
enc.List(len(l), func(i int) {
|
||||||
|
descr := l[i]
|
||||||
|
enc.Special('(').String(descr.Prefix).SP()
|
||||||
|
if descr.Delim == 0 {
|
||||||
|
enc.NIL()
|
||||||
|
} else {
|
||||||
|
enc.Quoted(string(descr.Delim))
|
||||||
|
}
|
||||||
|
enc.Special(')')
|
||||||
|
})
|
||||||
|
}
|
||||||
343
imapserver/search.go
Normal file
343
imapserver/search.go
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
package imapserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Conn) handleSearch(tag string, dec *imapwire.Decoder, numKind NumKind) error {
|
||||||
|
if !dec.ExpectSP() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
atom string
|
||||||
|
options imap.SearchOptions
|
||||||
|
extended bool
|
||||||
|
)
|
||||||
|
if maybeReadSearchKeyAtom(dec, &atom) && strings.EqualFold(atom, "RETURN") {
|
||||||
|
if err := readSearchReturnOpts(dec, &options); err != nil {
|
||||||
|
return fmt.Errorf("in search-return-opts: %w", err)
|
||||||
|
}
|
||||||
|
if !dec.ExpectSP() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
extended = true
|
||||||
|
atom = ""
|
||||||
|
maybeReadSearchKeyAtom(dec, &atom)
|
||||||
|
}
|
||||||
|
if strings.EqualFold(atom, "CHARSET") {
|
||||||
|
var charset string
|
||||||
|
if !dec.ExpectSP() || !dec.ExpectAString(&charset) || !dec.ExpectSP() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
switch strings.ToUpper(charset) {
|
||||||
|
case "US-ASCII", "UTF-8":
|
||||||
|
// nothing to do
|
||||||
|
default:
|
||||||
|
return &imap.Error{
|
||||||
|
Type: imap.StatusResponseTypeNo,
|
||||||
|
Code: imap.ResponseCodeBadCharset, // TODO: return list of supported charsets
|
||||||
|
Text: "Only US-ASCII and UTF-8 are supported SEARCH charsets",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
atom = ""
|
||||||
|
maybeReadSearchKeyAtom(dec, &atom)
|
||||||
|
}
|
||||||
|
|
||||||
|
var criteria imap.SearchCriteria
|
||||||
|
for {
|
||||||
|
var err error
|
||||||
|
if atom != "" {
|
||||||
|
err = readSearchKeyWithAtom(&criteria, dec, atom)
|
||||||
|
atom = ""
|
||||||
|
} else {
|
||||||
|
err = readSearchKey(&criteria, dec)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("in search-key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dec.SP() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dec.ExpectCRLF() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.checkState(imap.ConnStateSelected); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no return option is specified, ALL is assumed
|
||||||
|
if !options.ReturnMin && !options.ReturnMax && !options.ReturnAll && !options.ReturnCount {
|
||||||
|
options.ReturnAll = true
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := c.session.Search(numKind, &criteria, &options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.enabled.Has(imap.CapIMAP4rev2) || extended {
|
||||||
|
return c.writeESearch(tag, data, &options, numKind)
|
||||||
|
} else {
|
||||||
|
return c.writeSearch(data.All)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) writeESearch(tag string, data *imap.SearchData, options *imap.SearchOptions, numKind NumKind) error {
|
||||||
|
enc := newResponseEncoder(c)
|
||||||
|
defer enc.end()
|
||||||
|
|
||||||
|
enc.Atom("*").SP().Atom("ESEARCH")
|
||||||
|
if tag != "" {
|
||||||
|
enc.SP().Special('(').Atom("TAG").SP().String(tag).Special(')')
|
||||||
|
}
|
||||||
|
if numKind == NumKindUID {
|
||||||
|
enc.SP().Atom("UID")
|
||||||
|
}
|
||||||
|
// When there is no result, we need to send an ESEARCH response with no ALL
|
||||||
|
// keyword
|
||||||
|
if options.ReturnAll && !isNumSetEmpty(data.All) {
|
||||||
|
enc.SP().Atom("ALL").SP().NumSet(data.All)
|
||||||
|
}
|
||||||
|
if options.ReturnMin && data.Min > 0 {
|
||||||
|
enc.SP().Atom("MIN").SP().Number(data.Min)
|
||||||
|
}
|
||||||
|
if options.ReturnMax && data.Max > 0 {
|
||||||
|
enc.SP().Atom("MAX").SP().Number(data.Max)
|
||||||
|
}
|
||||||
|
if options.ReturnCount {
|
||||||
|
enc.SP().Atom("COUNT").SP().Number(data.Count)
|
||||||
|
}
|
||||||
|
return enc.CRLF()
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNumSetEmpty(numSet imap.NumSet) bool {
|
||||||
|
switch numSet := numSet.(type) {
|
||||||
|
case imap.SeqSet:
|
||||||
|
return len(numSet) == 0
|
||||||
|
case imap.UIDSet:
|
||||||
|
return len(numSet) == 0
|
||||||
|
default:
|
||||||
|
panic("unknown imap.NumSet type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) writeSearch(numSet imap.NumSet) error {
|
||||||
|
enc := newResponseEncoder(c)
|
||||||
|
defer enc.end()
|
||||||
|
|
||||||
|
enc.Atom("*").SP().Atom("SEARCH")
|
||||||
|
var ok bool
|
||||||
|
switch numSet := numSet.(type) {
|
||||||
|
case imap.SeqSet:
|
||||||
|
var nums []uint32
|
||||||
|
nums, ok = numSet.Nums()
|
||||||
|
for _, num := range nums {
|
||||||
|
enc.SP().Number(num)
|
||||||
|
}
|
||||||
|
case imap.UIDSet:
|
||||||
|
var uids []imap.UID
|
||||||
|
uids, ok = numSet.Nums()
|
||||||
|
for _, uid := range uids {
|
||||||
|
enc.SP().UID(uid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("imapserver: failed to enumerate message numbers in SEARCH response")
|
||||||
|
}
|
||||||
|
return enc.CRLF()
|
||||||
|
}
|
||||||
|
|
||||||
|
func readSearchReturnOpts(dec *imapwire.Decoder, options *imap.SearchOptions) error {
|
||||||
|
if !dec.ExpectSP() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
return dec.ExpectList(func() error {
|
||||||
|
var name string
|
||||||
|
if !dec.ExpectAtom(&name) {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
switch strings.ToUpper(name) {
|
||||||
|
case "MIN":
|
||||||
|
options.ReturnMin = true
|
||||||
|
case "MAX":
|
||||||
|
options.ReturnMax = true
|
||||||
|
case "ALL":
|
||||||
|
options.ReturnAll = true
|
||||||
|
case "COUNT":
|
||||||
|
options.ReturnCount = true
|
||||||
|
case "SAVE":
|
||||||
|
options.ReturnSave = true
|
||||||
|
default:
|
||||||
|
return newClientBugError("unknown SEARCH RETURN option")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func maybeReadSearchKeyAtom(dec *imapwire.Decoder, ptr *string) bool {
|
||||||
|
return dec.Func(ptr, func(ch byte) bool {
|
||||||
|
return ch == '*' || imapwire.IsAtomChar(ch)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func readSearchKey(criteria *imap.SearchCriteria, dec *imapwire.Decoder) error {
|
||||||
|
var key string
|
||||||
|
if maybeReadSearchKeyAtom(dec, &key) {
|
||||||
|
return readSearchKeyWithAtom(criteria, dec, key)
|
||||||
|
}
|
||||||
|
return dec.ExpectList(func() error {
|
||||||
|
return readSearchKey(criteria, dec)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func readSearchKeyWithAtom(criteria *imap.SearchCriteria, dec *imapwire.Decoder, key string) error {
|
||||||
|
key = strings.ToUpper(key)
|
||||||
|
switch key {
|
||||||
|
case "ALL":
|
||||||
|
// nothing to do
|
||||||
|
case "UID":
|
||||||
|
var uidSet imap.UIDSet
|
||||||
|
if !dec.ExpectSP() || !dec.ExpectUIDSet(&uidSet) {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
criteria.UID = append(criteria.UID, uidSet)
|
||||||
|
case "ANSWERED", "DELETED", "DRAFT", "FLAGGED", "RECENT", "SEEN":
|
||||||
|
criteria.Flag = append(criteria.Flag, searchKeyFlag(key))
|
||||||
|
case "UNANSWERED", "UNDELETED", "UNDRAFT", "UNFLAGGED", "UNSEEN":
|
||||||
|
notKey := strings.TrimPrefix(key, "UN")
|
||||||
|
criteria.NotFlag = append(criteria.NotFlag, searchKeyFlag(notKey))
|
||||||
|
case "NEW":
|
||||||
|
criteria.Flag = append(criteria.Flag, internal.FlagRecent)
|
||||||
|
criteria.NotFlag = append(criteria.NotFlag, imap.FlagSeen)
|
||||||
|
case "OLD":
|
||||||
|
criteria.NotFlag = append(criteria.NotFlag, internal.FlagRecent)
|
||||||
|
case "KEYWORD", "UNKEYWORD":
|
||||||
|
if !dec.ExpectSP() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
flag, err := internal.ExpectFlag(dec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch key {
|
||||||
|
case "KEYWORD":
|
||||||
|
criteria.Flag = append(criteria.Flag, flag)
|
||||||
|
case "UNKEYWORD":
|
||||||
|
criteria.NotFlag = append(criteria.NotFlag, flag)
|
||||||
|
}
|
||||||
|
case "BCC", "CC", "FROM", "SUBJECT", "TO":
|
||||||
|
var value string
|
||||||
|
if !dec.ExpectSP() || !dec.ExpectAString(&value) {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
criteria.Header = append(criteria.Header, imap.SearchCriteriaHeaderField{
|
||||||
|
Key: strings.Title(strings.ToLower(key)),
|
||||||
|
Value: value,
|
||||||
|
})
|
||||||
|
case "HEADER":
|
||||||
|
var key, value string
|
||||||
|
if !dec.ExpectSP() || !dec.ExpectAString(&key) || !dec.ExpectSP() || !dec.ExpectAString(&value) {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
criteria.Header = append(criteria.Header, imap.SearchCriteriaHeaderField{
|
||||||
|
Key: key,
|
||||||
|
Value: value,
|
||||||
|
})
|
||||||
|
case "SINCE", "BEFORE", "ON", "SENTSINCE", "SENTBEFORE", "SENTON":
|
||||||
|
if !dec.ExpectSP() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
t, err := internal.ExpectDate(dec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var dateCriteria imap.SearchCriteria
|
||||||
|
switch key {
|
||||||
|
case "SINCE":
|
||||||
|
dateCriteria.Since = t
|
||||||
|
case "BEFORE":
|
||||||
|
dateCriteria.Before = t
|
||||||
|
case "ON":
|
||||||
|
dateCriteria.Since = t
|
||||||
|
dateCriteria.Before = t.Add(24 * time.Hour)
|
||||||
|
case "SENTSINCE":
|
||||||
|
dateCriteria.SentSince = t
|
||||||
|
case "SENTBEFORE":
|
||||||
|
dateCriteria.SentBefore = t
|
||||||
|
case "SENTON":
|
||||||
|
dateCriteria.SentSince = t
|
||||||
|
dateCriteria.SentBefore = t.Add(24 * time.Hour)
|
||||||
|
}
|
||||||
|
criteria.And(&dateCriteria)
|
||||||
|
case "BODY":
|
||||||
|
var body string
|
||||||
|
if !dec.ExpectSP() || !dec.ExpectAString(&body) {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
criteria.Body = append(criteria.Body, body)
|
||||||
|
case "TEXT":
|
||||||
|
var text string
|
||||||
|
if !dec.ExpectSP() || !dec.ExpectAString(&text) {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
criteria.Text = append(criteria.Text, text)
|
||||||
|
case "LARGER", "SMALLER":
|
||||||
|
var n int64
|
||||||
|
if !dec.ExpectSP() || !dec.ExpectNumber64(&n) {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
switch key {
|
||||||
|
case "LARGER":
|
||||||
|
criteria.And(&imap.SearchCriteria{Larger: n})
|
||||||
|
case "SMALLER":
|
||||||
|
criteria.And(&imap.SearchCriteria{Smaller: n})
|
||||||
|
}
|
||||||
|
case "NOT":
|
||||||
|
if !dec.ExpectSP() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
var not imap.SearchCriteria
|
||||||
|
if err := readSearchKey(¬, dec); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
criteria.Not = append(criteria.Not, not)
|
||||||
|
case "OR":
|
||||||
|
if !dec.ExpectSP() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
var or [2]imap.SearchCriteria
|
||||||
|
if err := readSearchKey(&or[0], dec); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !dec.ExpectSP() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
if err := readSearchKey(&or[1], dec); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
criteria.Or = append(criteria.Or, or)
|
||||||
|
case "$":
|
||||||
|
criteria.UID = append(criteria.UID, imap.SearchRes())
|
||||||
|
default:
|
||||||
|
seqSet, err := imapwire.ParseSeqSet(key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
criteria.SeqNum = append(criteria.SeqNum, seqSet)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchKeyFlag(key string) imap.Flag {
|
||||||
|
return imap.Flag("\\" + strings.Title(strings.ToLower(key)))
|
||||||
|
}
|
||||||
174
imapserver/select.go
Normal file
174
imapserver/select.go
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
package imapserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Conn) handleSelect(tag string, dec *imapwire.Decoder, readOnly bool) error {
|
||||||
|
var mailbox string
|
||||||
|
if !dec.ExpectSP() || !dec.ExpectMailbox(&mailbox) || !dec.ExpectCRLF() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.checkState(imap.ConnStateAuthenticated); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.state == imap.ConnStateSelected {
|
||||||
|
if err := c.session.Unselect(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.state = imap.ConnStateAuthenticated
|
||||||
|
err := c.writeStatusResp("", &imap.StatusResponse{
|
||||||
|
Type: imap.StatusResponseTypeOK,
|
||||||
|
Code: "CLOSED",
|
||||||
|
Text: "Previous mailbox is now closed",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
options := imap.SelectOptions{ReadOnly: readOnly}
|
||||||
|
data, err := c.session.Select(mailbox, &options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.writeExists(data.NumMessages); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !c.enabled.Has(imap.CapIMAP4rev2) && c.server.options.caps().Has(imap.CapIMAP4rev1) {
|
||||||
|
if err := c.writeObsoleteRecent(data.NumRecent); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if data.FirstUnseenSeqNum != 0 {
|
||||||
|
if err := c.writeObsoleteUnseen(data.FirstUnseenSeqNum); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := c.writeUIDValidity(data.UIDValidity); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.writeUIDNext(data.UIDNext); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.writeFlags(data.Flags); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.writePermanentFlags(data.PermanentFlags); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if data.List != nil {
|
||||||
|
if err := c.writeList(data.List); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state = imap.ConnStateSelected
|
||||||
|
// TODO: forbid write commands in read-only mode
|
||||||
|
|
||||||
|
var (
|
||||||
|
cmdName string
|
||||||
|
code imap.ResponseCode
|
||||||
|
)
|
||||||
|
if readOnly {
|
||||||
|
cmdName = "EXAMINE"
|
||||||
|
code = "READ-ONLY"
|
||||||
|
} else {
|
||||||
|
cmdName = "SELECT"
|
||||||
|
code = "READ-WRITE"
|
||||||
|
}
|
||||||
|
return c.writeStatusResp(tag, &imap.StatusResponse{
|
||||||
|
Type: imap.StatusResponseTypeOK,
|
||||||
|
Code: code,
|
||||||
|
Text: fmt.Sprintf("%v completed", cmdName),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) handleUnselect(dec *imapwire.Decoder, expunge bool) error {
|
||||||
|
if !dec.ExpectCRLF() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.checkState(imap.ConnStateSelected); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if expunge {
|
||||||
|
w := &ExpungeWriter{}
|
||||||
|
if err := c.session.Expunge(w, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.session.Unselect(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state = imap.ConnStateAuthenticated
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) writeExists(numMessages uint32) error {
|
||||||
|
enc := newResponseEncoder(c)
|
||||||
|
defer enc.end()
|
||||||
|
return enc.Atom("*").SP().Number(numMessages).SP().Atom("EXISTS").CRLF()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) writeObsoleteRecent(n uint32) error {
|
||||||
|
enc := newResponseEncoder(c)
|
||||||
|
defer enc.end()
|
||||||
|
return enc.Atom("*").SP().Number(n).SP().Atom("RECENT").CRLF()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) writeObsoleteUnseen(n uint32) error {
|
||||||
|
enc := newResponseEncoder(c)
|
||||||
|
defer enc.end()
|
||||||
|
enc.Atom("*").SP().Atom("OK").SP()
|
||||||
|
enc.Special('[').Atom("UNSEEN").SP().Number(n).Special(']')
|
||||||
|
enc.SP().Text("First unseen message")
|
||||||
|
return enc.CRLF()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) writeUIDValidity(uidValidity uint32) error {
|
||||||
|
enc := newResponseEncoder(c)
|
||||||
|
defer enc.end()
|
||||||
|
enc.Atom("*").SP().Atom("OK").SP()
|
||||||
|
enc.Special('[').Atom("UIDVALIDITY").SP().Number(uidValidity).Special(']')
|
||||||
|
enc.SP().Text("UIDs valid")
|
||||||
|
return enc.CRLF()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) writeUIDNext(uidNext imap.UID) error {
|
||||||
|
enc := newResponseEncoder(c)
|
||||||
|
defer enc.end()
|
||||||
|
enc.Atom("*").SP().Atom("OK").SP()
|
||||||
|
enc.Special('[').Atom("UIDNEXT").SP().UID(uidNext).Special(']')
|
||||||
|
enc.SP().Text("Predicted next UID")
|
||||||
|
return enc.CRLF()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) writeFlags(flags []imap.Flag) error {
|
||||||
|
enc := newResponseEncoder(c)
|
||||||
|
defer enc.end()
|
||||||
|
enc.Atom("*").SP().Atom("FLAGS").SP().List(len(flags), func(i int) {
|
||||||
|
enc.Flag(flags[i])
|
||||||
|
})
|
||||||
|
return enc.CRLF()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) writePermanentFlags(flags []imap.Flag) error {
|
||||||
|
enc := newResponseEncoder(c)
|
||||||
|
defer enc.end()
|
||||||
|
enc.Atom("*").SP().Atom("OK").SP()
|
||||||
|
enc.Special('[').Atom("PERMANENTFLAGS").SP().List(len(flags), func(i int) {
|
||||||
|
enc.Flag(flags[i])
|
||||||
|
}).Special(']')
|
||||||
|
enc.SP().Text("Permanent flags")
|
||||||
|
return enc.CRLF()
|
||||||
|
}
|
||||||
222
imapserver/server.go
Normal file
222
imapserver/server.go
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
// Package imapserver implements an IMAP server.
|
||||||
|
package imapserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errClosed = errors.New("imapserver: server closed")
|
||||||
|
|
||||||
|
// Logger is a facility to log error messages.
|
||||||
|
type Logger interface {
|
||||||
|
Printf(format string, args ...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options contains server options.
|
||||||
|
//
|
||||||
|
// The only required field is NewSession.
|
||||||
|
type Options struct {
|
||||||
|
// NewSession is called when a client connects.
|
||||||
|
NewSession func(*Conn) (Session, *GreetingData, error)
|
||||||
|
// Supported capabilities. If nil, only IMAP4rev1 is advertised. This set
|
||||||
|
// must contain at least IMAP4rev1 or IMAP4rev2.
|
||||||
|
//
|
||||||
|
// The following capabilities are part of IMAP4rev2 and need to be
|
||||||
|
// explicitly enabled by IMAP4rev1-only servers:
|
||||||
|
//
|
||||||
|
// - NAMESPACE
|
||||||
|
// - UIDPLUS
|
||||||
|
// - ESEARCH
|
||||||
|
// - LIST-EXTENDED
|
||||||
|
// - LIST-STATUS
|
||||||
|
// - MOVE
|
||||||
|
// - STATUS=SIZE
|
||||||
|
Caps imap.CapSet
|
||||||
|
// Logger is a logger to print error messages. If nil, log.Default is used.
|
||||||
|
Logger Logger
|
||||||
|
// TLSConfig is a TLS configuration for STARTTLS. If nil, STARTTLS is
|
||||||
|
// disabled.
|
||||||
|
TLSConfig *tls.Config
|
||||||
|
// InsecureAuth allows clients to authenticate without TLS. In this mode,
|
||||||
|
// the server is susceptible to man-in-the-middle attacks.
|
||||||
|
InsecureAuth bool
|
||||||
|
// Raw ingress and egress data will be written to this writer, if any.
|
||||||
|
// Note, this may include sensitive information such as credentials used
|
||||||
|
// during authentication.
|
||||||
|
DebugWriter io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Options) wrapReadWriter(rw io.ReadWriter) io.ReadWriter {
|
||||||
|
if options.DebugWriter == nil {
|
||||||
|
return rw
|
||||||
|
}
|
||||||
|
return struct {
|
||||||
|
io.Reader
|
||||||
|
io.Writer
|
||||||
|
}{
|
||||||
|
Reader: io.TeeReader(rw, options.DebugWriter),
|
||||||
|
Writer: io.MultiWriter(rw, options.DebugWriter),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (options *Options) caps() imap.CapSet {
|
||||||
|
if options.Caps != nil {
|
||||||
|
return options.Caps
|
||||||
|
}
|
||||||
|
return imap.CapSet{imap.CapIMAP4rev1: {}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server is an IMAP server.
|
||||||
|
type Server struct {
|
||||||
|
options Options
|
||||||
|
|
||||||
|
listenerWaitGroup sync.WaitGroup
|
||||||
|
|
||||||
|
mutex sync.Mutex
|
||||||
|
listeners map[net.Listener]struct{}
|
||||||
|
conns map[*Conn]struct{}
|
||||||
|
closed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new server.
|
||||||
|
func New(options *Options) *Server {
|
||||||
|
if caps := options.caps(); !caps.Has(imap.CapIMAP4rev2) && !caps.Has(imap.CapIMAP4rev1) {
|
||||||
|
panic("imapserver: at least IMAP4rev1 must be supported")
|
||||||
|
}
|
||||||
|
return &Server{
|
||||||
|
options: *options,
|
||||||
|
listeners: make(map[net.Listener]struct{}),
|
||||||
|
conns: make(map[*Conn]struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) logger() Logger {
|
||||||
|
if s.options.Logger == nil {
|
||||||
|
return log.Default()
|
||||||
|
}
|
||||||
|
return s.options.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve accepts incoming connections on the listener ln.
|
||||||
|
func (s *Server) Serve(ln net.Listener) error {
|
||||||
|
s.mutex.Lock()
|
||||||
|
ok := !s.closed
|
||||||
|
if ok {
|
||||||
|
s.listeners[ln] = struct{}{}
|
||||||
|
}
|
||||||
|
s.mutex.Unlock()
|
||||||
|
if !ok {
|
||||||
|
return errClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
s.mutex.Lock()
|
||||||
|
delete(s.listeners, ln)
|
||||||
|
s.mutex.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
s.listenerWaitGroup.Add(1)
|
||||||
|
defer s.listenerWaitGroup.Done()
|
||||||
|
|
||||||
|
var delay time.Duration
|
||||||
|
for {
|
||||||
|
conn, err := ln.Accept()
|
||||||
|
if ne, ok := err.(net.Error); ok && ne.Temporary() {
|
||||||
|
if delay == 0 {
|
||||||
|
delay = 5 * time.Millisecond
|
||||||
|
} else {
|
||||||
|
delay *= 2
|
||||||
|
}
|
||||||
|
if max := 1 * time.Second; delay > max {
|
||||||
|
delay = max
|
||||||
|
}
|
||||||
|
s.logger().Printf("accept error (retrying in %v): %v", delay, err)
|
||||||
|
time.Sleep(delay)
|
||||||
|
continue
|
||||||
|
} else if errors.Is(err, net.ErrClosed) {
|
||||||
|
return nil
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("accept error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
delay = 0
|
||||||
|
go newConn(conn, s).serve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenAndServe listens on the TCP network address addr and then calls Serve.
|
||||||
|
//
|
||||||
|
// If addr is empty, ":143" is used.
|
||||||
|
func (s *Server) ListenAndServe(addr string) error {
|
||||||
|
if addr == "" {
|
||||||
|
addr = ":143"
|
||||||
|
}
|
||||||
|
ln, err := net.Listen("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.Serve(ln)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenAndServeTLS listens on the TCP network address addr and then calls
|
||||||
|
// Serve to handle incoming TLS connections.
|
||||||
|
//
|
||||||
|
// The TLS configuration set in Options.TLSConfig is used. If addr is empty,
|
||||||
|
// ":993" is used.
|
||||||
|
func (s *Server) ListenAndServeTLS(addr string) error {
|
||||||
|
if addr == "" {
|
||||||
|
addr = ":993"
|
||||||
|
}
|
||||||
|
ln, err := tls.Listen("tcp", addr, s.options.TLSConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.Serve(ln)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close immediately closes all active listeners and connections.
|
||||||
|
//
|
||||||
|
// Close returns any error returned from closing the server's underlying
|
||||||
|
// listeners.
|
||||||
|
//
|
||||||
|
// Once Close has been called on a server, it may not be reused; future calls
|
||||||
|
// to methods such as Serve will return an error.
|
||||||
|
func (s *Server) Close() error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
s.mutex.Lock()
|
||||||
|
ok := !s.closed
|
||||||
|
if ok {
|
||||||
|
s.closed = true
|
||||||
|
for l := range s.listeners {
|
||||||
|
if closeErr := l.Close(); closeErr != nil && err == nil {
|
||||||
|
err = closeErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.mutex.Unlock()
|
||||||
|
if !ok {
|
||||||
|
return errClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
s.listenerWaitGroup.Wait()
|
||||||
|
|
||||||
|
s.mutex.Lock()
|
||||||
|
for c := range s.conns {
|
||||||
|
c.mutex.Lock()
|
||||||
|
c.conn.Close()
|
||||||
|
c.mutex.Unlock()
|
||||||
|
}
|
||||||
|
s.mutex.Unlock()
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
126
imapserver/session.go
Normal file
126
imapserver/session.go
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
package imapserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
"github.com/emersion/go-sasl"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errAuthFailed = &imap.Error{
|
||||||
|
Type: imap.StatusResponseTypeNo,
|
||||||
|
Code: imap.ResponseCodeAuthenticationFailed,
|
||||||
|
Text: "Authentication failed",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrAuthFailed is returned by Session.Login on authentication failure.
|
||||||
|
var ErrAuthFailed = errAuthFailed
|
||||||
|
|
||||||
|
// GreetingData is the data associated with an IMAP greeting.
|
||||||
|
type GreetingData struct {
|
||||||
|
PreAuth bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NumKind describes how a number should be interpreted: either as a sequence
|
||||||
|
// number, either as a UID.
|
||||||
|
type NumKind int
|
||||||
|
|
||||||
|
const (
|
||||||
|
NumKindSeq = NumKind(imapwire.NumKindSeq)
|
||||||
|
NumKindUID = NumKind(imapwire.NumKindUID)
|
||||||
|
)
|
||||||
|
|
||||||
|
// String implements fmt.Stringer.
|
||||||
|
func (kind NumKind) String() string {
|
||||||
|
switch kind {
|
||||||
|
case NumKindSeq:
|
||||||
|
return "seq"
|
||||||
|
case NumKindUID:
|
||||||
|
return "uid"
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("imapserver: unknown NumKind %d", kind))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kind NumKind) wire() imapwire.NumKind {
|
||||||
|
return imapwire.NumKind(kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session is an IMAP session.
|
||||||
|
type Session interface {
|
||||||
|
Close() error
|
||||||
|
|
||||||
|
// Not authenticated state
|
||||||
|
Login(username, password string) error
|
||||||
|
|
||||||
|
// Authenticated state
|
||||||
|
Select(mailbox string, options *imap.SelectOptions) (*imap.SelectData, error)
|
||||||
|
Create(mailbox string, options *imap.CreateOptions) error
|
||||||
|
Delete(mailbox string) error
|
||||||
|
Rename(mailbox, newName string, options *imap.RenameOptions) error
|
||||||
|
Subscribe(mailbox string) error
|
||||||
|
Unsubscribe(mailbox string) error
|
||||||
|
List(w *ListWriter, ref string, patterns []string, options *imap.ListOptions) error
|
||||||
|
Status(mailbox string, options *imap.StatusOptions) (*imap.StatusData, error)
|
||||||
|
Append(mailbox string, r imap.LiteralReader, options *imap.AppendOptions) (*imap.AppendData, error)
|
||||||
|
Poll(w *UpdateWriter, allowExpunge bool) error
|
||||||
|
Idle(w *UpdateWriter, stop <-chan struct{}) error
|
||||||
|
|
||||||
|
// Selected state
|
||||||
|
Unselect() error
|
||||||
|
Expunge(w *ExpungeWriter, uids *imap.UIDSet) error
|
||||||
|
Search(kind NumKind, criteria *imap.SearchCriteria, options *imap.SearchOptions) (*imap.SearchData, error)
|
||||||
|
Fetch(w *FetchWriter, numSet imap.NumSet, options *imap.FetchOptions) error
|
||||||
|
Store(w *FetchWriter, numSet imap.NumSet, flags *imap.StoreFlags, options *imap.StoreOptions) error
|
||||||
|
Copy(numSet imap.NumSet, dest string) (*imap.CopyData, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionNamespace is an IMAP session which supports NAMESPACE.
|
||||||
|
type SessionNamespace interface {
|
||||||
|
Session
|
||||||
|
|
||||||
|
// Authenticated state
|
||||||
|
Namespace() (*imap.NamespaceData, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionMove is an IMAP session which supports MOVE.
|
||||||
|
type SessionMove interface {
|
||||||
|
Session
|
||||||
|
|
||||||
|
// Selected state
|
||||||
|
Move(w *MoveWriter, numSet imap.NumSet, dest string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionIMAP4rev2 is an IMAP session which supports IMAP4rev2.
|
||||||
|
type SessionIMAP4rev2 interface {
|
||||||
|
Session
|
||||||
|
SessionNamespace
|
||||||
|
SessionMove
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionSASL is an IMAP session which supports its own set of SASL
|
||||||
|
// authentication mechanisms.
|
||||||
|
type SessionSASL interface {
|
||||||
|
Session
|
||||||
|
AuthenticateMechanisms() []string
|
||||||
|
Authenticate(mech string) (sasl.Server, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionUnauthenticate is an IMAP session which supports UNAUTHENTICATE.
|
||||||
|
type SessionUnauthenticate interface {
|
||||||
|
Session
|
||||||
|
|
||||||
|
// Authenticated state
|
||||||
|
Unauthenticate() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionAppendLimit is an IMAP session which has the same APPEND limit for
|
||||||
|
// all mailboxes.
|
||||||
|
type SessionAppendLimit interface {
|
||||||
|
Session
|
||||||
|
|
||||||
|
// AppendLimit returns the maximum size in bytes that can be uploaded to
|
||||||
|
// this server in an APPEND command.
|
||||||
|
AppendLimit() uint32
|
||||||
|
}
|
||||||
83
imapserver/starttls.go
Normal file
83
imapserver/starttls.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package imapserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Conn) canStartTLS() bool {
|
||||||
|
_, isTLS := c.conn.(*tls.Conn)
|
||||||
|
return c.server.options.TLSConfig != nil && c.state == imap.ConnStateNotAuthenticated && !isTLS
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) handleStartTLS(tag string, dec *imapwire.Decoder) error {
|
||||||
|
if !dec.ExpectCRLF() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.server.options.TLSConfig == nil {
|
||||||
|
return &imap.Error{
|
||||||
|
Type: imap.StatusResponseTypeNo,
|
||||||
|
Text: "STARTTLS not supported",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !c.canStartTLS() {
|
||||||
|
return &imap.Error{
|
||||||
|
Type: imap.StatusResponseTypeBad,
|
||||||
|
Text: "STARTTLS not available",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not allow to write cleartext data past this point: keep c.encMutex
|
||||||
|
// locked until the end
|
||||||
|
enc := newResponseEncoder(c)
|
||||||
|
defer enc.end()
|
||||||
|
|
||||||
|
err := writeStatusResp(enc.Encoder, tag, &imap.StatusResponse{
|
||||||
|
Type: imap.StatusResponseTypeOK,
|
||||||
|
Text: "Begin TLS negotiation now",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drain buffered data from our bufio.Reader
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if _, err := io.CopyN(&buf, c.br, int64(c.br.Buffered())); err != nil {
|
||||||
|
panic(err) // unreachable
|
||||||
|
}
|
||||||
|
|
||||||
|
var cleartextConn net.Conn
|
||||||
|
if buf.Len() > 0 {
|
||||||
|
r := io.MultiReader(&buf, c.conn)
|
||||||
|
cleartextConn = startTLSConn{c.conn, r}
|
||||||
|
} else {
|
||||||
|
cleartextConn = c.conn
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConn := tls.Server(cleartextConn, c.server.options.TLSConfig)
|
||||||
|
|
||||||
|
c.mutex.Lock()
|
||||||
|
c.conn = tlsConn
|
||||||
|
c.mutex.Unlock()
|
||||||
|
|
||||||
|
rw := c.server.options.wrapReadWriter(tlsConn)
|
||||||
|
c.br.Reset(rw)
|
||||||
|
c.bw.Reset(rw)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type startTLSConn struct {
|
||||||
|
net.Conn
|
||||||
|
r io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conn startTLSConn) Read(b []byte) (int, error) {
|
||||||
|
return conn.r.Read(b)
|
||||||
|
}
|
||||||
125
imapserver/status.go
Normal file
125
imapserver/status.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
package imapserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Conn) handleStatus(dec *imapwire.Decoder) error {
|
||||||
|
var mailbox string
|
||||||
|
if !dec.ExpectSP() || !dec.ExpectMailbox(&mailbox) || !dec.ExpectSP() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
var options imap.StatusOptions
|
||||||
|
err := dec.ExpectList(func() error {
|
||||||
|
err := readStatusItem(dec, &options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dec.ExpectCRLF() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.NumRecent && !c.server.options.caps().Has(imap.CapIMAP4rev1) {
|
||||||
|
return &imap.Error{
|
||||||
|
Type: imap.StatusResponseTypeBad,
|
||||||
|
Text: "Unknown STATUS data item",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.checkState(imap.ConnStateAuthenticated); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := c.session.Status(mailbox, &options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.writeStatus(data, &options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) writeStatus(data *imap.StatusData, options *imap.StatusOptions) error {
|
||||||
|
enc := newResponseEncoder(c)
|
||||||
|
defer enc.end()
|
||||||
|
|
||||||
|
enc.Atom("*").SP().Atom("STATUS").SP().Mailbox(data.Mailbox).SP()
|
||||||
|
listEnc := enc.BeginList()
|
||||||
|
if options.NumMessages {
|
||||||
|
listEnc.Item().Atom("MESSAGES").SP().Number(*data.NumMessages)
|
||||||
|
}
|
||||||
|
if options.UIDNext {
|
||||||
|
listEnc.Item().Atom("UIDNEXT").SP().UID(data.UIDNext)
|
||||||
|
}
|
||||||
|
if options.UIDValidity {
|
||||||
|
listEnc.Item().Atom("UIDVALIDITY").SP().Number(data.UIDValidity)
|
||||||
|
}
|
||||||
|
if options.NumUnseen {
|
||||||
|
listEnc.Item().Atom("UNSEEN").SP().Number(*data.NumUnseen)
|
||||||
|
}
|
||||||
|
if options.NumDeleted {
|
||||||
|
listEnc.Item().Atom("DELETED").SP().Number(*data.NumDeleted)
|
||||||
|
}
|
||||||
|
if options.Size {
|
||||||
|
listEnc.Item().Atom("SIZE").SP().Number64(*data.Size)
|
||||||
|
}
|
||||||
|
if options.AppendLimit {
|
||||||
|
listEnc.Item().Atom("APPENDLIMIT").SP()
|
||||||
|
if data.AppendLimit != nil {
|
||||||
|
enc.Number(*data.AppendLimit)
|
||||||
|
} else {
|
||||||
|
enc.NIL()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if options.DeletedStorage {
|
||||||
|
listEnc.Item().Atom("DELETED-STORAGE").SP().Number64(*data.DeletedStorage)
|
||||||
|
}
|
||||||
|
if options.NumRecent {
|
||||||
|
listEnc.Item().Atom("RECENT").SP().Number(*data.NumRecent)
|
||||||
|
}
|
||||||
|
listEnc.End()
|
||||||
|
|
||||||
|
return enc.CRLF()
|
||||||
|
}
|
||||||
|
|
||||||
|
func readStatusItem(dec *imapwire.Decoder, options *imap.StatusOptions) error {
|
||||||
|
var name string
|
||||||
|
if !dec.ExpectAtom(&name) {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
switch strings.ToUpper(name) {
|
||||||
|
case "MESSAGES":
|
||||||
|
options.NumMessages = true
|
||||||
|
case "UIDNEXT":
|
||||||
|
options.UIDNext = true
|
||||||
|
case "UIDVALIDITY":
|
||||||
|
options.UIDValidity = true
|
||||||
|
case "UNSEEN":
|
||||||
|
options.NumUnseen = true
|
||||||
|
case "DELETED":
|
||||||
|
options.NumDeleted = true
|
||||||
|
case "SIZE":
|
||||||
|
options.Size = true
|
||||||
|
case "APPENDLIMIT":
|
||||||
|
options.AppendLimit = true
|
||||||
|
case "DELETED-STORAGE":
|
||||||
|
options.DeletedStorage = true
|
||||||
|
case "RECENT":
|
||||||
|
options.NumRecent = true
|
||||||
|
default:
|
||||||
|
return &imap.Error{
|
||||||
|
Type: imap.StatusResponseTypeBad,
|
||||||
|
Text: "Unknown STATUS data item",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
78
imapserver/store.go
Normal file
78
imapserver/store.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package imapserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Conn) handleStore(dec *imapwire.Decoder, numKind NumKind) error {
|
||||||
|
var (
|
||||||
|
numSet imap.NumSet
|
||||||
|
item string
|
||||||
|
)
|
||||||
|
if !dec.ExpectSP() || !dec.ExpectNumSet(numKind.wire(), &numSet) || !dec.ExpectSP() || !dec.ExpectAtom(&item) || !dec.ExpectSP() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
var flags []imap.Flag
|
||||||
|
isList, err := dec.List(func() error {
|
||||||
|
flag, err := internal.ExpectFlag(dec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
flags = append(flags, flag)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if !isList {
|
||||||
|
for {
|
||||||
|
flag, err := internal.ExpectFlag(dec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
flags = append(flags, flag)
|
||||||
|
|
||||||
|
if !dec.SP() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !dec.ExpectCRLF() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
item = strings.ToUpper(item)
|
||||||
|
silent := strings.HasSuffix(item, ".SILENT")
|
||||||
|
item = strings.TrimSuffix(item, ".SILENT")
|
||||||
|
|
||||||
|
var op imap.StoreFlagsOp
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(item, "+"):
|
||||||
|
op = imap.StoreFlagsAdd
|
||||||
|
item = strings.TrimPrefix(item, "+")
|
||||||
|
case strings.HasPrefix(item, "-"):
|
||||||
|
op = imap.StoreFlagsDel
|
||||||
|
item = strings.TrimPrefix(item, "-")
|
||||||
|
default:
|
||||||
|
op = imap.StoreFlagsSet
|
||||||
|
}
|
||||||
|
|
||||||
|
if item != "FLAGS" {
|
||||||
|
return newClientBugError("STORE can only change FLAGS")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.checkState(imap.ConnStateSelected); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
w := &FetchWriter{conn: c}
|
||||||
|
options := imap.StoreOptions{}
|
||||||
|
return c.session.Store(w, numSet, &imap.StoreFlags{
|
||||||
|
Op: op,
|
||||||
|
Silent: silent,
|
||||||
|
Flags: flags,
|
||||||
|
}, &options)
|
||||||
|
}
|
||||||
284
imapserver/tracker.go
Normal file
284
imapserver/tracker.go
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
package imapserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MailboxTracker tracks the state of a mailbox.
|
||||||
|
//
|
||||||
|
// A mailbox can have multiple sessions listening for updates. Each session has
|
||||||
|
// its own view of the mailbox, because IMAP clients asynchronously receive
|
||||||
|
// mailbox updates.
|
||||||
|
type MailboxTracker struct {
|
||||||
|
mutex sync.Mutex
|
||||||
|
numMessages uint32
|
||||||
|
sessions map[*SessionTracker]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMailboxTracker creates a new mailbox tracker.
|
||||||
|
func NewMailboxTracker(numMessages uint32) *MailboxTracker {
|
||||||
|
return &MailboxTracker{
|
||||||
|
numMessages: numMessages,
|
||||||
|
sessions: make(map[*SessionTracker]struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSession creates a new session tracker for the mailbox.
|
||||||
|
//
|
||||||
|
// The caller must call SessionTracker.Close once they are done with the
|
||||||
|
// session.
|
||||||
|
func (t *MailboxTracker) NewSession() *SessionTracker {
|
||||||
|
st := &SessionTracker{mailbox: t}
|
||||||
|
t.mutex.Lock()
|
||||||
|
t.sessions[st] = struct{}{}
|
||||||
|
t.mutex.Unlock()
|
||||||
|
return st
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *MailboxTracker) queueUpdate(update *trackerUpdate, source *SessionTracker) {
|
||||||
|
t.mutex.Lock()
|
||||||
|
defer t.mutex.Unlock()
|
||||||
|
|
||||||
|
if update.expunge != 0 && update.expunge > t.numMessages {
|
||||||
|
panic(fmt.Errorf("imapserver: expunge sequence number (%v) out of range (%v messages in mailbox)", update.expunge, t.numMessages))
|
||||||
|
}
|
||||||
|
if update.numMessages != 0 && update.numMessages < t.numMessages {
|
||||||
|
panic(fmt.Errorf("imapserver: cannot decrease mailbox number of messages from %v to %v", t.numMessages, update.numMessages))
|
||||||
|
}
|
||||||
|
|
||||||
|
for st := range t.sessions {
|
||||||
|
if source != nil && st == source {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
st.queueUpdate(update)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case update.expunge != 0:
|
||||||
|
t.numMessages--
|
||||||
|
case update.numMessages != 0:
|
||||||
|
t.numMessages = update.numMessages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueueExpunge queues a new EXPUNGE update.
|
||||||
|
func (t *MailboxTracker) QueueExpunge(seqNum uint32) {
|
||||||
|
if seqNum == 0 {
|
||||||
|
panic("imapserver: invalid expunge message sequence number")
|
||||||
|
}
|
||||||
|
t.queueUpdate(&trackerUpdate{expunge: seqNum}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueueNumMessages queues a new EXISTS update.
|
||||||
|
func (t *MailboxTracker) QueueNumMessages(n uint32) {
|
||||||
|
// TODO: merge consecutive NumMessages updates
|
||||||
|
t.queueUpdate(&trackerUpdate{numMessages: n}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueueMailboxFlags queues a new FLAGS update.
|
||||||
|
func (t *MailboxTracker) QueueMailboxFlags(flags []imap.Flag) {
|
||||||
|
if flags == nil {
|
||||||
|
flags = []imap.Flag{}
|
||||||
|
}
|
||||||
|
t.queueUpdate(&trackerUpdate{mailboxFlags: flags}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueueMessageFlags queues a new FETCH FLAGS update.
|
||||||
|
//
|
||||||
|
// If source is not nil, the update won't be dispatched to it.
|
||||||
|
func (t *MailboxTracker) QueueMessageFlags(seqNum uint32, uid imap.UID, flags []imap.Flag, source *SessionTracker) {
|
||||||
|
t.queueUpdate(&trackerUpdate{fetch: &trackerUpdateFetch{
|
||||||
|
seqNum: seqNum,
|
||||||
|
uid: uid,
|
||||||
|
flags: flags,
|
||||||
|
}}, source)
|
||||||
|
}
|
||||||
|
|
||||||
|
type trackerUpdate struct {
|
||||||
|
expunge uint32
|
||||||
|
numMessages uint32
|
||||||
|
mailboxFlags []imap.Flag
|
||||||
|
fetch *trackerUpdateFetch
|
||||||
|
}
|
||||||
|
|
||||||
|
type trackerUpdateFetch struct {
|
||||||
|
seqNum uint32
|
||||||
|
uid imap.UID
|
||||||
|
flags []imap.Flag
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionTracker tracks the state of a mailbox for an IMAP client.
|
||||||
|
type SessionTracker struct {
|
||||||
|
mailbox *MailboxTracker
|
||||||
|
|
||||||
|
mutex sync.Mutex
|
||||||
|
queue []trackerUpdate
|
||||||
|
updates chan<- struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close unregisters the session.
|
||||||
|
func (t *SessionTracker) Close() {
|
||||||
|
t.mailbox.mutex.Lock()
|
||||||
|
delete(t.mailbox.sessions, t)
|
||||||
|
t.mailbox.mutex.Unlock()
|
||||||
|
t.mailbox = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *SessionTracker) queueUpdate(update *trackerUpdate) {
|
||||||
|
var updates chan<- struct{}
|
||||||
|
t.mutex.Lock()
|
||||||
|
t.queue = append(t.queue, *update)
|
||||||
|
updates = t.updates
|
||||||
|
t.mutex.Unlock()
|
||||||
|
|
||||||
|
if updates != nil {
|
||||||
|
select {
|
||||||
|
case updates <- struct{}{}:
|
||||||
|
// we notified SessionTracker.Idle about the update
|
||||||
|
default:
|
||||||
|
// skip the update
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poll dequeues pending mailbox updates for this session.
|
||||||
|
func (t *SessionTracker) Poll(w *UpdateWriter, allowExpunge bool) error {
|
||||||
|
var updates []trackerUpdate
|
||||||
|
t.mutex.Lock()
|
||||||
|
if allowExpunge {
|
||||||
|
updates = t.queue
|
||||||
|
t.queue = nil
|
||||||
|
} else {
|
||||||
|
stopIndex := -1
|
||||||
|
for i, update := range t.queue {
|
||||||
|
if update.expunge != 0 {
|
||||||
|
stopIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
updates = append(updates, update)
|
||||||
|
}
|
||||||
|
if stopIndex >= 0 {
|
||||||
|
t.queue = t.queue[stopIndex:]
|
||||||
|
} else {
|
||||||
|
t.queue = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.mutex.Unlock()
|
||||||
|
|
||||||
|
for _, update := range updates {
|
||||||
|
var err error
|
||||||
|
switch {
|
||||||
|
case update.expunge != 0:
|
||||||
|
err = w.WriteExpunge(update.expunge)
|
||||||
|
case update.numMessages != 0:
|
||||||
|
err = w.WriteNumMessages(update.numMessages)
|
||||||
|
case update.mailboxFlags != nil:
|
||||||
|
err = w.WriteMailboxFlags(update.mailboxFlags)
|
||||||
|
case update.fetch != nil:
|
||||||
|
err = w.WriteMessageFlags(update.fetch.seqNum, update.fetch.uid, update.fetch.flags)
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("imapserver: unknown tracker update %#v", update))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Idle continuously writes mailbox updates.
|
||||||
|
//
|
||||||
|
// When the stop channel is closed, it returns.
|
||||||
|
//
|
||||||
|
// Idle cannot be invoked concurrently from two separate goroutines.
|
||||||
|
func (t *SessionTracker) Idle(w *UpdateWriter, stop <-chan struct{}) error {
|
||||||
|
updates := make(chan struct{}, 64)
|
||||||
|
t.mutex.Lock()
|
||||||
|
ok := t.updates == nil
|
||||||
|
if ok {
|
||||||
|
t.updates = updates
|
||||||
|
}
|
||||||
|
t.mutex.Unlock()
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("imapserver: only a single SessionTracker.Idle call is allowed at a time")
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
t.mutex.Lock()
|
||||||
|
t.updates = nil
|
||||||
|
t.mutex.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-updates:
|
||||||
|
if err := t.Poll(w, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case <-stop:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeSeqNum converts a message sequence number from the client view to the
|
||||||
|
// server view.
|
||||||
|
//
|
||||||
|
// Zero is returned if the message doesn't exist from the server point-of-view.
|
||||||
|
func (t *SessionTracker) DecodeSeqNum(seqNum uint32) uint32 {
|
||||||
|
if seqNum == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
t.mutex.Lock()
|
||||||
|
defer t.mutex.Unlock()
|
||||||
|
|
||||||
|
for _, update := range t.queue {
|
||||||
|
if update.expunge == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if seqNum == update.expunge {
|
||||||
|
return 0
|
||||||
|
} else if seqNum > update.expunge {
|
||||||
|
seqNum--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if seqNum > t.mailbox.numMessages {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return seqNum
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeSeqNum converts a message sequence number from the server view to the
|
||||||
|
// client view.
|
||||||
|
//
|
||||||
|
// Zero is returned if the message doesn't exist from the client point-of-view.
|
||||||
|
func (t *SessionTracker) EncodeSeqNum(seqNum uint32) uint32 {
|
||||||
|
if seqNum == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
t.mutex.Lock()
|
||||||
|
defer t.mutex.Unlock()
|
||||||
|
|
||||||
|
if seqNum > t.mailbox.numMessages {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := len(t.queue) - 1; i >= 0; i-- {
|
||||||
|
update := t.queue[i]
|
||||||
|
// TODO: this doesn't handle increments > 1
|
||||||
|
if update.numMessages != 0 && seqNum == update.numMessages {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if update.expunge != 0 && seqNum >= update.expunge {
|
||||||
|
seqNum++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return seqNum
|
||||||
|
}
|
||||||
155
imapserver/tracker_test.go
Normal file
155
imapserver/tracker_test.go
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
package imapserver_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2/imapserver"
|
||||||
|
)
|
||||||
|
|
||||||
|
type trackerUpdate struct {
|
||||||
|
expunge uint32
|
||||||
|
numMessages uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
var sessionTrackerSeqNumTests = []struct {
|
||||||
|
name string
|
||||||
|
pending []trackerUpdate
|
||||||
|
clientSeqNum, serverSeqNum uint32
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "noop",
|
||||||
|
pending: nil,
|
||||||
|
clientSeqNum: 20,
|
||||||
|
serverSeqNum: 20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "noop_last",
|
||||||
|
pending: nil,
|
||||||
|
clientSeqNum: 42,
|
||||||
|
serverSeqNum: 42,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "noop_client_oob",
|
||||||
|
pending: nil,
|
||||||
|
clientSeqNum: 43,
|
||||||
|
serverSeqNum: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "noop_server_oob",
|
||||||
|
pending: nil,
|
||||||
|
clientSeqNum: 0,
|
||||||
|
serverSeqNum: 43,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "expunge_eq",
|
||||||
|
pending: []trackerUpdate{{expunge: 20}},
|
||||||
|
clientSeqNum: 20,
|
||||||
|
serverSeqNum: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "expunge_lt",
|
||||||
|
pending: []trackerUpdate{{expunge: 20}},
|
||||||
|
clientSeqNum: 10,
|
||||||
|
serverSeqNum: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "expunge_gt",
|
||||||
|
pending: []trackerUpdate{{expunge: 10}},
|
||||||
|
clientSeqNum: 20,
|
||||||
|
serverSeqNum: 19,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "append_eq",
|
||||||
|
pending: []trackerUpdate{{numMessages: 43}},
|
||||||
|
clientSeqNum: 0,
|
||||||
|
serverSeqNum: 43,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "append_lt",
|
||||||
|
pending: []trackerUpdate{{numMessages: 43}},
|
||||||
|
clientSeqNum: 42,
|
||||||
|
serverSeqNum: 42,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "expunge_append",
|
||||||
|
pending: []trackerUpdate{
|
||||||
|
{expunge: 42},
|
||||||
|
{numMessages: 42},
|
||||||
|
},
|
||||||
|
clientSeqNum: 42,
|
||||||
|
serverSeqNum: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "expunge_append",
|
||||||
|
pending: []trackerUpdate{
|
||||||
|
{expunge: 42},
|
||||||
|
{numMessages: 42},
|
||||||
|
},
|
||||||
|
clientSeqNum: 0,
|
||||||
|
serverSeqNum: 42,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "append_expunge",
|
||||||
|
pending: []trackerUpdate{
|
||||||
|
{numMessages: 43},
|
||||||
|
{expunge: 42},
|
||||||
|
},
|
||||||
|
clientSeqNum: 42,
|
||||||
|
serverSeqNum: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "append_expunge",
|
||||||
|
pending: []trackerUpdate{
|
||||||
|
{numMessages: 43},
|
||||||
|
{expunge: 42},
|
||||||
|
},
|
||||||
|
clientSeqNum: 0,
|
||||||
|
serverSeqNum: 42,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multi_expunge_middle",
|
||||||
|
pending: []trackerUpdate{
|
||||||
|
{expunge: 3},
|
||||||
|
{expunge: 1},
|
||||||
|
},
|
||||||
|
clientSeqNum: 2,
|
||||||
|
serverSeqNum: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multi_expunge_after",
|
||||||
|
pending: []trackerUpdate{
|
||||||
|
{expunge: 3},
|
||||||
|
{expunge: 1},
|
||||||
|
},
|
||||||
|
clientSeqNum: 4,
|
||||||
|
serverSeqNum: 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionTracker(t *testing.T) {
|
||||||
|
for _, tc := range sessionTrackerSeqNumTests {
|
||||||
|
tc := tc // capture range variable
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
mboxTracker := imapserver.NewMailboxTracker(42)
|
||||||
|
sessTracker := mboxTracker.NewSession()
|
||||||
|
for _, update := range tc.pending {
|
||||||
|
switch {
|
||||||
|
case update.expunge != 0:
|
||||||
|
mboxTracker.QueueExpunge(update.expunge)
|
||||||
|
case update.numMessages != 0:
|
||||||
|
mboxTracker.QueueNumMessages(update.numMessages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serverSeqNum := sessTracker.DecodeSeqNum(tc.clientSeqNum)
|
||||||
|
if tc.clientSeqNum != 0 && serverSeqNum != tc.serverSeqNum {
|
||||||
|
t.Errorf("DecodeSeqNum(%v): got %v, want %v", tc.clientSeqNum, serverSeqNum, tc.serverSeqNum)
|
||||||
|
}
|
||||||
|
|
||||||
|
clientSeqNum := sessTracker.EncodeSeqNum(tc.serverSeqNum)
|
||||||
|
if tc.serverSeqNum != 0 && clientSeqNum != tc.clientSeqNum {
|
||||||
|
t.Errorf("EncodeSeqNum(%v): got %v, want %v", tc.serverSeqNum, clientSeqNum, tc.clientSeqNum)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
13
internal/acl.go
Normal file
13
internal/acl.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FormatRights(rm imap.RightModification, rs imap.RightSet) string {
|
||||||
|
s := ""
|
||||||
|
if rm != imap.RightModificationReplace {
|
||||||
|
s = string(rm)
|
||||||
|
}
|
||||||
|
return s + string(rs)
|
||||||
|
}
|
||||||
306
internal/imapnum/numset.go
Normal file
306
internal/imapnum/numset.go
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
package imapnum
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Range represents a single seq-number or seq-range value (RFC 3501 ABNF). Values
|
||||||
|
// may be static (e.g. "1", "2:4") or dynamic (e.g. "*", "1:*"). A seq-number is
|
||||||
|
// represented by setting Start = Stop. Zero is used to represent "*", which is
|
||||||
|
// safe because seq-number uses nz-number rule. The order of values is always
|
||||||
|
// Start <= Stop, except when representing "n:*", where Start = n and Stop = 0.
|
||||||
|
type Range struct {
|
||||||
|
Start, Stop uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contains returns true if the seq-number q is contained in range value s.
|
||||||
|
// The dynamic value "*" contains only other "*" values, the dynamic range "n:*"
|
||||||
|
// contains "*" and all numbers >= n.
|
||||||
|
func (s Range) Contains(q uint32) bool {
|
||||||
|
if q == 0 {
|
||||||
|
return s.Stop == 0 // "*" is contained only in "*" and "n:*"
|
||||||
|
}
|
||||||
|
return s.Start != 0 && s.Start <= q && (q <= s.Stop || s.Stop == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Less returns true if s precedes and does not contain seq-number q.
|
||||||
|
func (s Range) Less(q uint32) bool {
|
||||||
|
return (s.Stop < q || q == 0) && s.Stop != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge combines range values s and t into a single union if the two
|
||||||
|
// intersect or one is a superset of the other. The order of s and t does not
|
||||||
|
// matter. If the values cannot be merged, s is returned unmodified and ok is
|
||||||
|
// set to false.
|
||||||
|
func (s Range) Merge(t Range) (union Range, ok bool) {
|
||||||
|
union = s
|
||||||
|
if s == t {
|
||||||
|
return s, true
|
||||||
|
}
|
||||||
|
if s.Start != 0 && t.Start != 0 {
|
||||||
|
// s and t are any combination of "n", "n:m", or "n:*"
|
||||||
|
if s.Start > t.Start {
|
||||||
|
s, t = t, s
|
||||||
|
}
|
||||||
|
// s starts at or before t, check where it ends
|
||||||
|
if (s.Stop >= t.Stop && t.Stop != 0) || s.Stop == 0 {
|
||||||
|
return s, true // s is a superset of t
|
||||||
|
}
|
||||||
|
// s is "n" or "n:m", if m == ^uint32(0) then t is "n:*"
|
||||||
|
if s.Stop+1 >= t.Start || s.Stop == ^uint32(0) {
|
||||||
|
return Range{s.Start, t.Stop}, true // s intersects or touches t
|
||||||
|
}
|
||||||
|
return union, false
|
||||||
|
}
|
||||||
|
// exactly one of s and t is "*"
|
||||||
|
if s.Start == 0 {
|
||||||
|
if t.Stop == 0 {
|
||||||
|
return t, true // s is "*", t is "n:*"
|
||||||
|
}
|
||||||
|
} else if s.Stop == 0 {
|
||||||
|
return s, true // s is "n:*", t is "*"
|
||||||
|
}
|
||||||
|
return union, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns range value s as a seq-number or seq-range string.
|
||||||
|
func (s Range) String() string {
|
||||||
|
if s.Start == s.Stop {
|
||||||
|
if s.Start == 0 {
|
||||||
|
return "*"
|
||||||
|
}
|
||||||
|
return strconv.FormatUint(uint64(s.Start), 10)
|
||||||
|
}
|
||||||
|
b := strconv.AppendUint(make([]byte, 0, 24), uint64(s.Start), 10)
|
||||||
|
if s.Stop == 0 {
|
||||||
|
return string(append(b, ':', '*'))
|
||||||
|
}
|
||||||
|
return string(strconv.AppendUint(append(b, ':'), uint64(s.Stop), 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Range) append(nums []uint32) (out []uint32, ok bool) {
|
||||||
|
if s.Start == 0 || s.Stop == 0 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
for n := s.Start; n <= s.Stop; n++ {
|
||||||
|
nums = append(nums, n)
|
||||||
|
}
|
||||||
|
return nums, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set is used to represent a set of message sequence numbers or UIDs (see
|
||||||
|
// sequence-set ABNF rule). The zero value is an empty set.
|
||||||
|
type Set []Range
|
||||||
|
|
||||||
|
// AddNum inserts new numbers into the set. The value 0 represents "*".
|
||||||
|
func (s *Set) AddNum(q ...uint32) {
|
||||||
|
for _, v := range q {
|
||||||
|
s.insert(Range{v, v})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddRange inserts a new range into the set.
|
||||||
|
func (s *Set) AddRange(start, stop uint32) {
|
||||||
|
if (stop < start && stop != 0) || start == 0 {
|
||||||
|
s.insert(Range{stop, start})
|
||||||
|
} else {
|
||||||
|
s.insert(Range{start, stop})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSet inserts all values from t into s.
|
||||||
|
func (s *Set) AddSet(t Set) {
|
||||||
|
for _, v := range t {
|
||||||
|
s.insert(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic returns true if the set contains "*" or "n:*" values.
|
||||||
|
func (s Set) Dynamic() bool {
|
||||||
|
return len(s) > 0 && s[len(s)-1].Stop == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contains returns true if the non-zero sequence number or UID q is contained
|
||||||
|
// in the set. The dynamic range "n:*" contains all q >= n. It is the caller's
|
||||||
|
// responsibility to handle the special case where q is the maximum UID in the
|
||||||
|
// mailbox and q < n (i.e. the set cannot match UIDs against "*:n" or "*" since
|
||||||
|
// it doesn't know what the maximum value is).
|
||||||
|
func (s Set) Contains(q uint32) bool {
|
||||||
|
if _, ok := s.search(q); ok {
|
||||||
|
return q != 0
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nums returns a slice of all numbers contained in the set.
|
||||||
|
func (s Set) Nums() (nums []uint32, ok bool) {
|
||||||
|
for _, v := range s {
|
||||||
|
nums, ok = v.append(nums)
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nums, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a sorted representation of all contained number values.
|
||||||
|
func (s Set) String() string {
|
||||||
|
if len(s) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
b := make([]byte, 0, 64)
|
||||||
|
for _, v := range s {
|
||||||
|
b = append(b, ',')
|
||||||
|
if v.Start == 0 {
|
||||||
|
b = append(b, '*')
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b = strconv.AppendUint(b, uint64(v.Start), 10)
|
||||||
|
if v.Start != v.Stop {
|
||||||
|
if v.Stop == 0 {
|
||||||
|
b = append(b, ':', '*')
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b = strconv.AppendUint(append(b, ':'), uint64(v.Stop), 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return string(b[1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert adds range value v to the set.
|
||||||
|
func (ptr *Set) insert(v Range) {
|
||||||
|
s := *ptr
|
||||||
|
defer func() {
|
||||||
|
*ptr = s
|
||||||
|
}()
|
||||||
|
|
||||||
|
i, _ := s.search(v.Start)
|
||||||
|
merged := false
|
||||||
|
if i > 0 {
|
||||||
|
// try merging with the preceding entry (e.g. "1,4".insert(2), i == 1)
|
||||||
|
s[i-1], merged = s[i-1].Merge(v)
|
||||||
|
}
|
||||||
|
if i == len(s) {
|
||||||
|
// v was either merged with the last entry or needs to be appended
|
||||||
|
if !merged {
|
||||||
|
s.insertAt(i, v)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} else if merged {
|
||||||
|
i--
|
||||||
|
} else if s[i], merged = s[i].Merge(v); !merged {
|
||||||
|
s.insertAt(i, v) // insert in the middle (e.g. "1,5".insert(3), i == 1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// v was merged with s[i], continue trying to merge until the end
|
||||||
|
for j := i + 1; j < len(s); j++ {
|
||||||
|
if s[i], merged = s[i].Merge(s[j]); !merged {
|
||||||
|
if j > i+1 {
|
||||||
|
// cut out all entries between i and j that were merged
|
||||||
|
s = append(s[:i+1], s[j:]...)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// everything after s[i] was merged
|
||||||
|
s = s[:i+1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// insertAt inserts a new range value v at index i, resizing s.Set as needed.
|
||||||
|
func (ptr *Set) insertAt(i int, v Range) {
|
||||||
|
s := *ptr
|
||||||
|
defer func() {
|
||||||
|
*ptr = s
|
||||||
|
}()
|
||||||
|
|
||||||
|
if n := len(s); i == n {
|
||||||
|
// insert at the end
|
||||||
|
s = append(s, v)
|
||||||
|
return
|
||||||
|
} else if n < cap(s) {
|
||||||
|
// enough space, shift everything at and after i to the right
|
||||||
|
s = s[:n+1]
|
||||||
|
copy(s[i+1:], s[i:])
|
||||||
|
} else {
|
||||||
|
// allocate new slice and copy everything, n is at least 1
|
||||||
|
set := make([]Range, n+1, n*2)
|
||||||
|
copy(set, s[:i])
|
||||||
|
copy(set[i+1:], s[i:])
|
||||||
|
s = set
|
||||||
|
}
|
||||||
|
s[i] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// search attempts to find the index of the range set value that contains q.
|
||||||
|
// If no values contain q, the returned index is the position where q should be
|
||||||
|
// inserted and ok is set to false.
|
||||||
|
func (s Set) search(q uint32) (i int, ok bool) {
|
||||||
|
min, max := 0, len(s)-1
|
||||||
|
for min < max {
|
||||||
|
if mid := (min + max) >> 1; s[mid].Less(q) {
|
||||||
|
min = mid + 1
|
||||||
|
} else {
|
||||||
|
max = mid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if max < 0 || s[min].Less(q) {
|
||||||
|
return len(s), false // q is the new largest value
|
||||||
|
}
|
||||||
|
return min, s[min].Contains(q)
|
||||||
|
}
|
||||||
|
|
||||||
|
// errBadNumSet is used to report problems with the format of a number set
|
||||||
|
// value.
|
||||||
|
type errBadNumSet string
|
||||||
|
|
||||||
|
func (err errBadNumSet) Error() string {
|
||||||
|
return fmt.Sprintf("imap: bad number set value %q", string(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseNum parses a single seq-number value (non-zero uint32 or "*").
|
||||||
|
func parseNum(v string) (uint32, error) {
|
||||||
|
if n, err := strconv.ParseUint(v, 10, 32); err == nil && v[0] != '0' {
|
||||||
|
return uint32(n), nil
|
||||||
|
} else if v == "*" {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
return 0, errBadNumSet(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseNumRange creates a new seq instance by parsing strings in the format
|
||||||
|
// "n" or "n:m", where n and/or m may be "*". An error is returned for invalid
|
||||||
|
// values.
|
||||||
|
func parseNumRange(v string) (Range, error) {
|
||||||
|
var (
|
||||||
|
r Range
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if sep := strings.IndexRune(v, ':'); sep < 0 {
|
||||||
|
r.Start, err = parseNum(v)
|
||||||
|
r.Stop = r.Start
|
||||||
|
return r, err
|
||||||
|
} else if r.Start, err = parseNum(v[:sep]); err == nil {
|
||||||
|
if r.Stop, err = parseNum(v[sep+1:]); err == nil {
|
||||||
|
if (r.Stop < r.Start && r.Stop != 0) || r.Start == 0 {
|
||||||
|
r.Start, r.Stop = r.Stop, r.Start
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r, errBadNumSet(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseSet returns a new Set after parsing the set string.
|
||||||
|
func ParseSet(set string) (Set, error) {
|
||||||
|
var s Set
|
||||||
|
for _, sv := range strings.Split(set, ",") {
|
||||||
|
r, err := parseNumRange(sv)
|
||||||
|
if err != nil {
|
||||||
|
return s, err
|
||||||
|
}
|
||||||
|
s.AddRange(r.Start, r.Stop)
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
724
internal/imapnum/numset_test.go
Normal file
724
internal/imapnum/numset_test.go
Normal file
@@ -0,0 +1,724 @@
|
|||||||
|
package imapnum
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const max = ^uint32(0)
|
||||||
|
|
||||||
|
func TestParseNumRange(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
in string
|
||||||
|
out Range
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
// Invalid number
|
||||||
|
{"", Range{}, false},
|
||||||
|
{" ", Range{}, false},
|
||||||
|
{"A", Range{}, false},
|
||||||
|
{"0", Range{}, false},
|
||||||
|
{" 1", Range{}, false},
|
||||||
|
{"1 ", Range{}, false},
|
||||||
|
{"*1", Range{}, false},
|
||||||
|
{"1*", Range{}, false},
|
||||||
|
{"-1", Range{}, false},
|
||||||
|
{"01", Range{}, false},
|
||||||
|
{"0x1", Range{}, false},
|
||||||
|
{"1 2", Range{}, false},
|
||||||
|
{"1,2", Range{}, false},
|
||||||
|
{"1.2", Range{}, false},
|
||||||
|
{"4294967296", Range{}, false},
|
||||||
|
|
||||||
|
// Valid number
|
||||||
|
{"*", Range{0, 0}, true},
|
||||||
|
{"1", Range{1, 1}, true},
|
||||||
|
{"42", Range{42, 42}, true},
|
||||||
|
{"1000", Range{1000, 1000}, true},
|
||||||
|
{"4294967295", Range{max, max}, true},
|
||||||
|
|
||||||
|
// Invalid range
|
||||||
|
{":", Range{}, false},
|
||||||
|
{"*:", Range{}, false},
|
||||||
|
{":*", Range{}, false},
|
||||||
|
{"1:", Range{}, false},
|
||||||
|
{":1", Range{}, false},
|
||||||
|
{"0:0", Range{}, false},
|
||||||
|
{"0:*", Range{}, false},
|
||||||
|
{"0:1", Range{}, false},
|
||||||
|
{"1:0", Range{}, false},
|
||||||
|
{"1:2 ", Range{}, false},
|
||||||
|
{"1: 2", Range{}, false},
|
||||||
|
{"1:2:", Range{}, false},
|
||||||
|
{"1:2,", Range{}, false},
|
||||||
|
{"1:2:3", Range{}, false},
|
||||||
|
{"1:2,3", Range{}, false},
|
||||||
|
{"*:4294967296", Range{}, false},
|
||||||
|
{"0:4294967295", Range{}, false},
|
||||||
|
{"1:4294967296", Range{}, false},
|
||||||
|
{"4294967296:*", Range{}, false},
|
||||||
|
{"4294967295:0", Range{}, false},
|
||||||
|
{"4294967296:1", Range{}, false},
|
||||||
|
{"4294967295:4294967296", Range{}, false},
|
||||||
|
|
||||||
|
// Valid range
|
||||||
|
{"*:*", Range{0, 0}, true},
|
||||||
|
{"1:*", Range{1, 0}, true},
|
||||||
|
{"*:1", Range{1, 0}, true},
|
||||||
|
{"2:2", Range{2, 2}, true},
|
||||||
|
{"2:42", Range{2, 42}, true},
|
||||||
|
{"42:2", Range{2, 42}, true},
|
||||||
|
{"*:4294967294", Range{max - 1, 0}, true},
|
||||||
|
{"*:4294967295", Range{max, 0}, true},
|
||||||
|
{"4294967294:*", Range{max - 1, 0}, true},
|
||||||
|
{"4294967295:*", Range{max, 0}, true},
|
||||||
|
{"1:4294967294", Range{1, max - 1}, true},
|
||||||
|
{"1:4294967295", Range{1, max}, true},
|
||||||
|
{"4294967295:1000", Range{1000, max}, true},
|
||||||
|
{"4294967294:4294967295", Range{max - 1, max}, true},
|
||||||
|
{"4294967295:4294967295", Range{max, max}, true},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
out, err := parseNumRange(test.in)
|
||||||
|
if !test.ok {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("parseSeq(%q) expected error; got %q", test.in, out)
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
t.Errorf("parseSeq(%q) expected %q; got %v", test.in, test.out, err)
|
||||||
|
} else if out != test.out {
|
||||||
|
t.Errorf("parseSeq(%q) expected %q; got %q", test.in, test.out, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNumRangeContainsLess(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
s string
|
||||||
|
q uint32
|
||||||
|
contains bool
|
||||||
|
less bool
|
||||||
|
}{
|
||||||
|
{"2", 0, false, true},
|
||||||
|
{"2", 1, false, false},
|
||||||
|
{"2", 2, true, false},
|
||||||
|
{"2", 3, false, true},
|
||||||
|
{"2", max, false, true},
|
||||||
|
|
||||||
|
{"*", 0, true, false},
|
||||||
|
{"*", 1, false, false},
|
||||||
|
{"*", 2, false, false},
|
||||||
|
{"*", 3, false, false},
|
||||||
|
{"*", max, false, false},
|
||||||
|
|
||||||
|
{"2:3", 0, false, true},
|
||||||
|
{"2:3", 1, false, false},
|
||||||
|
{"2:3", 2, true, false},
|
||||||
|
{"2:3", 3, true, false},
|
||||||
|
{"2:3", 4, false, true},
|
||||||
|
{"2:3", 5, false, true},
|
||||||
|
|
||||||
|
{"2:4", 0, false, true},
|
||||||
|
{"2:4", 1, false, false},
|
||||||
|
{"2:4", 2, true, false},
|
||||||
|
{"2:4", 3, true, false},
|
||||||
|
{"2:4", 4, true, false},
|
||||||
|
{"2:4", 5, false, true},
|
||||||
|
|
||||||
|
{"4:4294967295", 0, false, true},
|
||||||
|
{"4:4294967295", 1, false, false},
|
||||||
|
{"4:4294967295", 2, false, false},
|
||||||
|
{"4:4294967295", 3, false, false},
|
||||||
|
{"4:4294967295", 4, true, false},
|
||||||
|
{"4:4294967295", 5, true, false},
|
||||||
|
{"4:4294967295", max, true, false},
|
||||||
|
|
||||||
|
{"4:*", 0, true, false},
|
||||||
|
{"4:*", 1, false, false},
|
||||||
|
{"4:*", 2, false, false},
|
||||||
|
{"4:*", 3, false, false},
|
||||||
|
{"4:*", 4, true, false},
|
||||||
|
{"4:*", 5, true, false},
|
||||||
|
{"4:*", max, true, false},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
s, err := parseNumRange(test.s)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("parseSeq(%q) unexpected error; %v", test.s, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if s.Contains(test.q) != test.contains {
|
||||||
|
t.Errorf("%q.Contains(%d) expected %v", test.s, test.q, test.contains)
|
||||||
|
}
|
||||||
|
if s.Less(test.q) != test.less {
|
||||||
|
t.Errorf("%q.Less(%d) expected %v", test.s, test.q, test.less)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNumRangeMerge(T *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
s, t, out string
|
||||||
|
}{
|
||||||
|
// Number with number
|
||||||
|
{"1", "1", "1"},
|
||||||
|
{"1", "2", "1:2"},
|
||||||
|
{"1", "3", ""},
|
||||||
|
{"1", "4294967295", ""},
|
||||||
|
{"1", "*", ""},
|
||||||
|
|
||||||
|
{"4", "1", ""},
|
||||||
|
{"4", "2", ""},
|
||||||
|
{"4", "3", "3:4"},
|
||||||
|
{"4", "4", "4"},
|
||||||
|
{"4", "5", "4:5"},
|
||||||
|
{"4", "6", ""},
|
||||||
|
|
||||||
|
{"4294967295", "4294967293", ""},
|
||||||
|
{"4294967295", "4294967294", "4294967294:4294967295"},
|
||||||
|
{"4294967295", "4294967295", "4294967295"},
|
||||||
|
{"4294967295", "*", ""},
|
||||||
|
|
||||||
|
{"*", "1", ""},
|
||||||
|
{"*", "2", ""},
|
||||||
|
{"*", "4294967294", ""},
|
||||||
|
{"*", "4294967295", ""},
|
||||||
|
{"*", "*", "*"},
|
||||||
|
|
||||||
|
// Range with number
|
||||||
|
{"1:3", "1", "1:3"},
|
||||||
|
{"1:3", "2", "1:3"},
|
||||||
|
{"1:3", "3", "1:3"},
|
||||||
|
{"1:3", "4", "1:4"},
|
||||||
|
{"1:3", "5", ""},
|
||||||
|
{"1:3", "*", ""},
|
||||||
|
|
||||||
|
{"3:4", "1", ""},
|
||||||
|
{"3:4", "2", "2:4"},
|
||||||
|
{"3:4", "3", "3:4"},
|
||||||
|
{"3:4", "4", "3:4"},
|
||||||
|
{"3:4", "5", "3:5"},
|
||||||
|
{"3:4", "6", ""},
|
||||||
|
{"3:4", "*", ""},
|
||||||
|
|
||||||
|
{"2:3", "5", ""},
|
||||||
|
{"2:4", "5", "2:5"},
|
||||||
|
{"2:5", "5", "2:5"},
|
||||||
|
{"2:6", "5", "2:6"},
|
||||||
|
{"2:7", "5", "2:7"},
|
||||||
|
{"2:*", "5", "2:*"},
|
||||||
|
{"3:4", "5", "3:5"},
|
||||||
|
{"3:5", "5", "3:5"},
|
||||||
|
{"3:6", "5", "3:6"},
|
||||||
|
{"3:7", "5", "3:7"},
|
||||||
|
{"3:*", "5", "3:*"},
|
||||||
|
{"4:5", "5", "4:5"},
|
||||||
|
{"4:6", "5", "4:6"},
|
||||||
|
{"4:7", "5", "4:7"},
|
||||||
|
{"4:*", "5", "4:*"},
|
||||||
|
{"5:6", "5", "5:6"},
|
||||||
|
{"5:7", "5", "5:7"},
|
||||||
|
{"5:*", "5", "5:*"},
|
||||||
|
{"6:7", "5", "5:7"},
|
||||||
|
{"6:*", "5", "5:*"},
|
||||||
|
{"7:8", "5", ""},
|
||||||
|
{"7:*", "5", ""},
|
||||||
|
|
||||||
|
{"3:4294967294", "1", ""},
|
||||||
|
{"3:4294967294", "2", "2:4294967294"},
|
||||||
|
{"3:4294967294", "3", "3:4294967294"},
|
||||||
|
{"3:4294967294", "4", "3:4294967294"},
|
||||||
|
{"3:4294967294", "4294967293", "3:4294967294"},
|
||||||
|
{"3:4294967294", "4294967294", "3:4294967294"},
|
||||||
|
{"3:4294967294", "4294967295", "3:4294967295"},
|
||||||
|
{"3:4294967294", "*", ""},
|
||||||
|
|
||||||
|
{"3:4294967295", "1", ""},
|
||||||
|
{"3:4294967295", "2", "2:4294967295"},
|
||||||
|
{"3:4294967295", "3", "3:4294967295"},
|
||||||
|
{"3:4294967295", "4", "3:4294967295"},
|
||||||
|
{"3:4294967295", "4294967294", "3:4294967295"},
|
||||||
|
{"3:4294967295", "4294967295", "3:4294967295"},
|
||||||
|
{"3:4294967295", "*", ""},
|
||||||
|
|
||||||
|
{"1:4294967295", "1", "1:4294967295"},
|
||||||
|
{"1:4294967295", "4294967295", "1:4294967295"},
|
||||||
|
{"1:4294967295", "*", ""},
|
||||||
|
|
||||||
|
{"1:*", "1", "1:*"},
|
||||||
|
{"1:*", "2", "1:*"},
|
||||||
|
{"1:*", "4294967294", "1:*"},
|
||||||
|
{"1:*", "4294967295", "1:*"},
|
||||||
|
{"1:*", "*", "1:*"},
|
||||||
|
|
||||||
|
// Range with range
|
||||||
|
{"5:8", "1:2", ""},
|
||||||
|
{"5:8", "1:3", ""},
|
||||||
|
{"5:8", "1:4", "1:8"},
|
||||||
|
{"5:8", "1:5", "1:8"},
|
||||||
|
{"5:8", "1:6", "1:8"},
|
||||||
|
{"5:8", "1:7", "1:8"},
|
||||||
|
{"5:8", "1:8", "1:8"},
|
||||||
|
{"5:8", "1:9", "1:9"},
|
||||||
|
{"5:8", "1:10", "1:10"},
|
||||||
|
{"5:8", "1:11", "1:11"},
|
||||||
|
{"5:8", "1:*", "1:*"},
|
||||||
|
|
||||||
|
{"5:8", "2:3", ""},
|
||||||
|
{"5:8", "2:4", "2:8"},
|
||||||
|
{"5:8", "2:5", "2:8"},
|
||||||
|
{"5:8", "2:6", "2:8"},
|
||||||
|
{"5:8", "2:7", "2:8"},
|
||||||
|
{"5:8", "2:8", "2:8"},
|
||||||
|
{"5:8", "2:9", "2:9"},
|
||||||
|
{"5:8", "2:10", "2:10"},
|
||||||
|
{"5:8", "2:11", "2:11"},
|
||||||
|
{"5:8", "2:*", "2:*"},
|
||||||
|
|
||||||
|
{"5:8", "3:4", "3:8"},
|
||||||
|
{"5:8", "3:5", "3:8"},
|
||||||
|
{"5:8", "3:6", "3:8"},
|
||||||
|
{"5:8", "3:7", "3:8"},
|
||||||
|
{"5:8", "3:8", "3:8"},
|
||||||
|
{"5:8", "3:9", "3:9"},
|
||||||
|
{"5:8", "3:10", "3:10"},
|
||||||
|
{"5:8", "3:11", "3:11"},
|
||||||
|
{"5:8", "3:*", "3:*"},
|
||||||
|
|
||||||
|
{"5:8", "4:5", "4:8"},
|
||||||
|
{"5:8", "4:6", "4:8"},
|
||||||
|
{"5:8", "4:7", "4:8"},
|
||||||
|
{"5:8", "4:8", "4:8"},
|
||||||
|
{"5:8", "4:9", "4:9"},
|
||||||
|
{"5:8", "4:10", "4:10"},
|
||||||
|
{"5:8", "4:11", "4:11"},
|
||||||
|
{"5:8", "4:*", "4:*"},
|
||||||
|
|
||||||
|
{"5:8", "5:6", "5:8"},
|
||||||
|
{"5:8", "5:7", "5:8"},
|
||||||
|
{"5:8", "5:8", "5:8"},
|
||||||
|
{"5:8", "5:9", "5:9"},
|
||||||
|
{"5:8", "5:10", "5:10"},
|
||||||
|
{"5:8", "5:11", "5:11"},
|
||||||
|
{"5:8", "5:*", "5:*"},
|
||||||
|
|
||||||
|
{"5:8", "6:7", "5:8"},
|
||||||
|
{"5:8", "6:8", "5:8"},
|
||||||
|
{"5:8", "6:9", "5:9"},
|
||||||
|
{"5:8", "6:10", "5:10"},
|
||||||
|
{"5:8", "6:11", "5:11"},
|
||||||
|
{"5:8", "6:*", "5:*"},
|
||||||
|
|
||||||
|
{"5:8", "7:8", "5:8"},
|
||||||
|
{"5:8", "7:9", "5:9"},
|
||||||
|
{"5:8", "7:10", "5:10"},
|
||||||
|
{"5:8", "7:11", "5:11"},
|
||||||
|
{"5:8", "7:*", "5:*"},
|
||||||
|
|
||||||
|
{"5:8", "8:9", "5:9"},
|
||||||
|
{"5:8", "8:10", "5:10"},
|
||||||
|
{"5:8", "8:11", "5:11"},
|
||||||
|
{"5:8", "8:*", "5:*"},
|
||||||
|
|
||||||
|
{"5:8", "9:10", "5:10"},
|
||||||
|
{"5:8", "9:11", "5:11"},
|
||||||
|
{"5:8", "9:*", "5:*"},
|
||||||
|
|
||||||
|
{"5:8", "10:11", ""},
|
||||||
|
{"5:8", "10:*", ""},
|
||||||
|
|
||||||
|
{"1:*", "1:*", "1:*"},
|
||||||
|
{"1:*", "2:*", "1:*"},
|
||||||
|
{"1:*", "1:4294967294", "1:*"},
|
||||||
|
{"1:*", "1:4294967295", "1:*"},
|
||||||
|
{"1:*", "2:4294967295", "1:*"},
|
||||||
|
|
||||||
|
{"1:4294967295", "1:4294967294", "1:4294967295"},
|
||||||
|
{"1:4294967295", "1:4294967295", "1:4294967295"},
|
||||||
|
{"1:4294967295", "2:4294967295", "1:4294967295"},
|
||||||
|
{"1:4294967295", "2:*", "1:*"},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
s, err := parseNumRange(test.s)
|
||||||
|
if err != nil {
|
||||||
|
T.Errorf("parseSeq(%q) unexpected error; %v", test.s, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t, err := parseNumRange(test.t)
|
||||||
|
if err != nil {
|
||||||
|
T.Errorf("parseSeq(%q) unexpected error; %v", test.t, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
testOK := test.out != ""
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
if !testOK {
|
||||||
|
test.out = test.s
|
||||||
|
}
|
||||||
|
out, ok := s.Merge(t)
|
||||||
|
if out.String() != test.out || ok != testOK {
|
||||||
|
T.Errorf("%q.Merge(%q) expected %q; got %q", test.s, test.t, test.out, out)
|
||||||
|
}
|
||||||
|
// Swap s & t, result should be identical
|
||||||
|
test.s, test.t = test.t, test.s
|
||||||
|
s, t = t, s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkNumSet(s Set, t *testing.T) {
|
||||||
|
n := len(s)
|
||||||
|
for i, v := range s {
|
||||||
|
if v.Start == 0 {
|
||||||
|
if v.Stop != 0 {
|
||||||
|
t.Errorf(`NumSet(%q) index %d: "*:n" range`, s, i)
|
||||||
|
} else if i != n-1 {
|
||||||
|
t.Errorf(`NumSet(%q) index %d: "*" not at the end`, s, i)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if i > 0 && s[i-1].Stop >= v.Start-1 {
|
||||||
|
t.Errorf(`NumSet(%q) index %d: overlap`, s, i)
|
||||||
|
}
|
||||||
|
if v.Stop < v.Start {
|
||||||
|
if v.Stop != 0 {
|
||||||
|
t.Errorf(`NumSet(%q) index %d: reversed range`, s, i)
|
||||||
|
} else if i != n-1 {
|
||||||
|
t.Errorf(`NumSet(%q) index %d: "n:*" not at the end`, s, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNumSetInfo(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
s string
|
||||||
|
q uint32
|
||||||
|
contains bool
|
||||||
|
}{
|
||||||
|
{"", 0, false},
|
||||||
|
{"", 1, false},
|
||||||
|
{"", 2, false},
|
||||||
|
{"", 3, false},
|
||||||
|
{"", max, false},
|
||||||
|
|
||||||
|
{"2", 0, false},
|
||||||
|
{"2", 1, false},
|
||||||
|
{"2", 2, true},
|
||||||
|
{"2", 3, false},
|
||||||
|
{"2", max, false},
|
||||||
|
|
||||||
|
{"*", 0, false}, // Contains("*") is always false, use Dynamic() instead
|
||||||
|
{"*", 1, false},
|
||||||
|
{"*", 2, false},
|
||||||
|
{"*", 3, false},
|
||||||
|
{"*", max, false},
|
||||||
|
|
||||||
|
{"1:*", 0, false},
|
||||||
|
{"1:*", 1, true},
|
||||||
|
{"1:*", max, true},
|
||||||
|
|
||||||
|
{"2:4", 0, false},
|
||||||
|
{"2:4", 1, false},
|
||||||
|
{"2:4", 2, true},
|
||||||
|
{"2:4", 3, true},
|
||||||
|
{"2:4", 4, true},
|
||||||
|
{"2:4", 5, false},
|
||||||
|
{"2:4", max, false},
|
||||||
|
|
||||||
|
{"2,4", 0, false},
|
||||||
|
{"2,4", 1, false},
|
||||||
|
{"2,4", 2, true},
|
||||||
|
{"2,4", 3, false},
|
||||||
|
{"2,4", 4, true},
|
||||||
|
{"2,4", 5, false},
|
||||||
|
{"2,4", max, false},
|
||||||
|
|
||||||
|
{"2:4,6", 0, false},
|
||||||
|
{"2:4,6", 1, false},
|
||||||
|
{"2:4,6", 2, true},
|
||||||
|
{"2:4,6", 3, true},
|
||||||
|
{"2:4,6", 4, true},
|
||||||
|
{"2:4,6", 5, false},
|
||||||
|
{"2:4,6", 6, true},
|
||||||
|
{"2:4,6", 7, false},
|
||||||
|
|
||||||
|
{"2,4:6", 0, false},
|
||||||
|
{"2,4:6", 1, false},
|
||||||
|
{"2,4:6", 2, true},
|
||||||
|
{"2,4:6", 3, false},
|
||||||
|
{"2,4:6", 4, true},
|
||||||
|
{"2,4:6", 5, true},
|
||||||
|
{"2,4:6", 6, true},
|
||||||
|
{"2,4:6", 7, false},
|
||||||
|
|
||||||
|
{"2,4,6", 0, false},
|
||||||
|
{"2,4,6", 1, false},
|
||||||
|
{"2,4,6", 2, true},
|
||||||
|
{"2,4,6", 3, false},
|
||||||
|
{"2,4,6", 4, true},
|
||||||
|
{"2,4,6", 5, false},
|
||||||
|
{"2,4,6", 6, true},
|
||||||
|
{"2,4,6", 7, false},
|
||||||
|
|
||||||
|
{"1,3:5,7,9:*", 0, false},
|
||||||
|
{"1,3:5,7,9:*", 1, true},
|
||||||
|
{"1,3:5,7,9:*", 2, false},
|
||||||
|
{"1,3:5,7,9:*", 3, true},
|
||||||
|
{"1,3:5,7,9:*", 4, true},
|
||||||
|
{"1,3:5,7,9:*", 5, true},
|
||||||
|
{"1,3:5,7,9:*", 6, false},
|
||||||
|
{"1,3:5,7,9:*", 7, true},
|
||||||
|
{"1,3:5,7,9:*", 8, false},
|
||||||
|
{"1,3:5,7,9:*", 9, true},
|
||||||
|
{"1,3:5,7,9:*", 10, true},
|
||||||
|
{"1,3:5,7,9:*", max, true},
|
||||||
|
|
||||||
|
{"1,3:5,7,9,42", 0, false},
|
||||||
|
{"1,3:5,7,9,42", 1, true},
|
||||||
|
{"1,3:5,7,9,42", 2, false},
|
||||||
|
{"1,3:5,7,9,42", 3, true},
|
||||||
|
{"1,3:5,7,9,42", 4, true},
|
||||||
|
{"1,3:5,7,9,42", 5, true},
|
||||||
|
{"1,3:5,7,9,42", 6, false},
|
||||||
|
{"1,3:5,7,9,42", 7, true},
|
||||||
|
{"1,3:5,7,9,42", 8, false},
|
||||||
|
{"1,3:5,7,9,42", 9, true},
|
||||||
|
{"1,3:5,7,9,42", 10, false},
|
||||||
|
{"1,3:5,7,9,42", 41, false},
|
||||||
|
{"1,3:5,7,9,42", 42, true},
|
||||||
|
{"1,3:5,7,9,42", 43, false},
|
||||||
|
{"1,3:5,7,9,42", max, false},
|
||||||
|
|
||||||
|
{"1,3:5,7,9,42,*", 0, false},
|
||||||
|
{"1,3:5,7,9,42,*", 1, true},
|
||||||
|
{"1,3:5,7,9,42,*", 2, false},
|
||||||
|
{"1,3:5,7,9,42,*", 3, true},
|
||||||
|
{"1,3:5,7,9,42,*", 4, true},
|
||||||
|
{"1,3:5,7,9,42,*", 5, true},
|
||||||
|
{"1,3:5,7,9,42,*", 6, false},
|
||||||
|
{"1,3:5,7,9,42,*", 7, true},
|
||||||
|
{"1,3:5,7,9,42,*", 8, false},
|
||||||
|
{"1,3:5,7,9,42,*", 9, true},
|
||||||
|
{"1,3:5,7,9,42,*", 10, false},
|
||||||
|
{"1,3:5,7,9,42,*", 41, false},
|
||||||
|
{"1,3:5,7,9,42,*", 42, true},
|
||||||
|
{"1,3:5,7,9,42,*", 43, false},
|
||||||
|
{"1,3:5,7,9,42,*", max, false},
|
||||||
|
|
||||||
|
{"1,3:5,7,9,42,60:70,100:*", 0, false},
|
||||||
|
{"1,3:5,7,9,42,60:70,100:*", 1, true},
|
||||||
|
{"1,3:5,7,9,42,60:70,100:*", 2, false},
|
||||||
|
{"1,3:5,7,9,42,60:70,100:*", 3, true},
|
||||||
|
{"1,3:5,7,9,42,60:70,100:*", 4, true},
|
||||||
|
{"1,3:5,7,9,42,60:70,100:*", 5, true},
|
||||||
|
{"1,3:5,7,9,42,60:70,100:*", 6, false},
|
||||||
|
{"1,3:5,7,9,42,60:70,100:*", 7, true},
|
||||||
|
{"1,3:5,7,9,42,60:70,100:*", 8, false},
|
||||||
|
{"1,3:5,7,9,42,60:70,100:*", 9, true},
|
||||||
|
{"1,3:5,7,9,42,60:70,100:*", 10, false},
|
||||||
|
{"1,3:5,7,9,42,60:70,100:*", 41, false},
|
||||||
|
{"1,3:5,7,9,42,60:70,100:*", 42, true},
|
||||||
|
{"1,3:5,7,9,42,60:70,100:*", 43, false},
|
||||||
|
{"1,3:5,7,9,42,60:70,100:*", 59, false},
|
||||||
|
{"1,3:5,7,9,42,60:70,100:*", 60, true},
|
||||||
|
{"1,3:5,7,9,42,60:70,100:*", 65, true},
|
||||||
|
{"1,3:5,7,9,42,60:70,100:*", 70, true},
|
||||||
|
{"1,3:5,7,9,42,60:70,100:*", 71, false},
|
||||||
|
{"1,3:5,7,9,42,60:70,100:*", 99, false},
|
||||||
|
{"1,3:5,7,9,42,60:70,100:*", 100, true},
|
||||||
|
{"1,3:5,7,9,42,60:70,100:*", 1000, true},
|
||||||
|
{"1,3:5,7,9,42,60:70,100:*", max, true},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
s, _ := ParseSet(test.s)
|
||||||
|
checkNumSet(s, t)
|
||||||
|
if s.Contains(test.q) != test.contains {
|
||||||
|
t.Errorf("%q.Contains(%v) expected %v", test.s, test.q, test.contains)
|
||||||
|
}
|
||||||
|
if str := s.String(); str != test.s {
|
||||||
|
t.Errorf("%q.String() expected %q; got %q", test.s, test.s, str)
|
||||||
|
}
|
||||||
|
testEmpty := len(test.s) == 0
|
||||||
|
if (len(s) == 0) != testEmpty {
|
||||||
|
t.Errorf("%q.Empty() expected %v", test.s, testEmpty)
|
||||||
|
}
|
||||||
|
testDynamic := !testEmpty && test.s[len(test.s)-1] == '*'
|
||||||
|
if s.Dynamic() != testDynamic {
|
||||||
|
t.Errorf("%q.Dynamic() expected %v", test.s, testDynamic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseNumSet(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
in string
|
||||||
|
out string
|
||||||
|
}{
|
||||||
|
{"1,1", "1"},
|
||||||
|
{"1,2", "1:2"},
|
||||||
|
{"1,3", "1,3"},
|
||||||
|
{"1,*", "1,*"},
|
||||||
|
|
||||||
|
{"1,1,1", "1"},
|
||||||
|
{"1,1,2", "1:2"},
|
||||||
|
{"1,1:2", "1:2"},
|
||||||
|
{"1,1,3", "1,3"},
|
||||||
|
{"1,1:3", "1:3"},
|
||||||
|
{"1,2,2", "1:2"},
|
||||||
|
{"1,2,3", "1:3"},
|
||||||
|
{"1,2:3", "1:3"},
|
||||||
|
{"1,2,4", "1:2,4"},
|
||||||
|
{"1,3,3", "1,3"},
|
||||||
|
{"1,3,4", "1,3:4"},
|
||||||
|
{"1,3:4", "1,3:4"},
|
||||||
|
{"1,3,5", "1,3,5"},
|
||||||
|
{"1,3:5", "1,3:5"},
|
||||||
|
{"1:3,5", "1:3,5"},
|
||||||
|
{"1:5,3", "1:5"},
|
||||||
|
|
||||||
|
{"1,2,3,4", "1:4"},
|
||||||
|
{"1,2,4,5", "1:2,4:5"},
|
||||||
|
{"1,2,4:5", "1:2,4:5"},
|
||||||
|
{"1:2,4:5", "1:2,4:5"},
|
||||||
|
|
||||||
|
{"1,2,3,4,5", "1:5"},
|
||||||
|
{"1,2:3,4:5", "1:5"},
|
||||||
|
|
||||||
|
{"1,2,4,5,7,9", "1:2,4:5,7,9"},
|
||||||
|
{"1,2,4,5,7:9", "1:2,4:5,7:9"},
|
||||||
|
{"1:2,4:5,7:9", "1:2,4:5,7:9"},
|
||||||
|
{"1,2,4,5,7,8,9", "1:2,4:5,7:9"},
|
||||||
|
{"1:2,4:5,7,8,9", "1:2,4:5,7:9"},
|
||||||
|
|
||||||
|
{"3,5:10,15:20", "3,5:10,15:20"},
|
||||||
|
{"4,5:10,15:20", "4:10,15:20"},
|
||||||
|
{"5,5:10,15:20", "5:10,15:20"},
|
||||||
|
{"7,5:10,15:20", "5:10,15:20"},
|
||||||
|
{"10,5:10,15:20", "5:10,15:20"},
|
||||||
|
{"11,5:10,15:20", "5:11,15:20"},
|
||||||
|
{"12,5:10,15:20", "5:10,12,15:20"},
|
||||||
|
{"14,5:10,15:20", "5:10,14:20"},
|
||||||
|
{"17,5:10,15:20", "5:10,15:20"},
|
||||||
|
{"21,5:10,15:20", "5:10,15:21"},
|
||||||
|
{"22,5:10,15:20", "5:10,15:20,22"},
|
||||||
|
{"*,5:10,15:20", "5:10,15:20,*"},
|
||||||
|
|
||||||
|
{"1:3,5:10,15:20", "1:3,5:10,15:20"},
|
||||||
|
{"1:4,5:10,15:20", "1:10,15:20"},
|
||||||
|
{"1:8,5:10,15:20", "1:10,15:20"},
|
||||||
|
{"1:13,5:10,15:20", "1:13,15:20"},
|
||||||
|
{"1:14,5:10,15:20", "1:20"},
|
||||||
|
{"7:17,5:10,15:20", "5:20"},
|
||||||
|
{"11:14,5:10,15:20", "5:20"},
|
||||||
|
{"12,13,5:10,15:20", "5:10,12:13,15:20"},
|
||||||
|
{"12:13,5:10,15:20", "5:10,12:13,15:20"},
|
||||||
|
{"12:14,5:10,15:20", "5:10,12:20"},
|
||||||
|
{"11:13,5:10,15:20", "5:13,15:20"},
|
||||||
|
{"11,12,13,14,5:10,15:20", "5:20"},
|
||||||
|
|
||||||
|
{"1:*,5:10,15:20", "1:*"},
|
||||||
|
{"4:*,5:10,15:20", "4:*"},
|
||||||
|
{"6:*,5:10,15:20", "5:*"},
|
||||||
|
{"12:*,5:10,15:20", "5:10,12:*"},
|
||||||
|
{"19:*,5:10,15:20", "5:10,15:*"},
|
||||||
|
|
||||||
|
{"5:8,6,7:10,15,16,17,18:20,19,21:*", "5:10,15:*"},
|
||||||
|
|
||||||
|
{"4:13,1,5,10,15,20", "1,4:13,15,20"},
|
||||||
|
{"4:14,1,5,10,15,20", "1,4:15,20"},
|
||||||
|
{"4:15,1,5,10,15,20", "1,4:15,20"},
|
||||||
|
{"4:16,1,5,10,15,20", "1,4:16,20"},
|
||||||
|
{"4:17,1,5,10,15,20", "1,4:17,20"},
|
||||||
|
{"4:18,1,5,10,15,20", "1,4:18,20"},
|
||||||
|
{"4:19,1,5,10,15,20", "1,4:20"},
|
||||||
|
{"4:20,1,5,10,15,20", "1,4:20"},
|
||||||
|
{"4:21,1,5,10,15,20", "1,4:21"},
|
||||||
|
{"4:*,1,5,10,15,20", "1,4:*"},
|
||||||
|
|
||||||
|
{"1,3,5,7,9,11,13,15,17,19", "1,3,5,7,9,11,13,15,17,19"},
|
||||||
|
{"1,3,5,7,9,11:13,15,17,19", "1,3,5,7,9,11:13,15,17,19"},
|
||||||
|
{"1,3,5,7,9:11,13:15,17,19", "1,3,5,7,9:11,13:15,17,19"},
|
||||||
|
{"1,3,5,7:9,11:13,15:17,19", "1,3,5,7:9,11:13,15:17,19"},
|
||||||
|
{"1,3,5,7,9,11,13,15,17,19,*", "1,3,5,7,9,11,13,15,17,19,*"},
|
||||||
|
{"1,3,5,7,9,11,13,15,17,19:*", "1,3,5,7,9,11,13,15,17,19:*"},
|
||||||
|
{"1:20,3,5,7,9,11,13,15,17,19,*", "1:20,*"},
|
||||||
|
{"1:20,3,5,7,9,11,13,15,17,19:*", "1:*"},
|
||||||
|
|
||||||
|
{"4294967295,*", "4294967295,*"},
|
||||||
|
{"1,4294967295,*", "1,4294967295,*"},
|
||||||
|
{"1:4294967295,*", "1:4294967295,*"},
|
||||||
|
{"1,4294967295:*", "1,4294967295:*"},
|
||||||
|
{"1:*,4294967295", "1:*"},
|
||||||
|
{"1:*,4294967295:*", "1:*"},
|
||||||
|
{"1:4294967295,4294967295:*", "1:*"},
|
||||||
|
}
|
||||||
|
prng := rand.New(rand.NewSource(19860201))
|
||||||
|
done := make(map[string]bool)
|
||||||
|
permute := func(in string) string {
|
||||||
|
v := strings.Split(in, ",")
|
||||||
|
r := make([]string, len(v))
|
||||||
|
|
||||||
|
// Try to find a permutation that hasn't been checked already
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
for i, j := range prng.Perm(len(v)) {
|
||||||
|
r[i] = v[j]
|
||||||
|
}
|
||||||
|
if s := strings.Join(r, ","); !done[s] {
|
||||||
|
done[s] = true
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
for i := 0; i < 100 && test.in != ""; i++ {
|
||||||
|
s, err := ParseSet(test.in)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Add(%q) unexpected error; %v", test.in, err)
|
||||||
|
i = 100
|
||||||
|
}
|
||||||
|
checkNumSet(s, t)
|
||||||
|
if out := s.String(); out != test.out {
|
||||||
|
t.Errorf("%q.String() expected %q; got %q", test.in, test.out, out)
|
||||||
|
i = 100
|
||||||
|
}
|
||||||
|
test.in = permute(test.in)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNumSetAddNumRangeSet(t *testing.T) {
|
||||||
|
type num []uint32
|
||||||
|
tests := []struct {
|
||||||
|
num num
|
||||||
|
rng Range
|
||||||
|
set string
|
||||||
|
out string
|
||||||
|
}{
|
||||||
|
{num{5}, Range{1, 3}, "1:2,5,7:13,15,17:*", "1:3,5,7:13,15,17:*"},
|
||||||
|
{num{5}, Range{3, 1}, "2:3,7:13,15,17:*", "1:3,5,7:13,15,17:*"},
|
||||||
|
|
||||||
|
{num{15}, Range{17, 0}, "1:3,5,7:13", "1:3,5,7:13,15,17:*"},
|
||||||
|
{num{15}, Range{0, 17}, "1:3,5,7:13", "1:3,5,7:13,15,17:*"},
|
||||||
|
|
||||||
|
{num{1, 3, 5, 7, 9, 11, 0}, Range{8, 13}, "2,15,17:*", "1:3,5,7:13,15,17:*"},
|
||||||
|
{num{5, 1, 7, 3, 9, 0, 11}, Range{8, 13}, "2,15,17:*", "1:3,5,7:13,15,17:*"},
|
||||||
|
{num{5, 1, 7, 3, 9, 0, 11}, Range{13, 8}, "2,15,17:*", "1:3,5,7:13,15,17:*"},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
other, _ := ParseSet(test.set)
|
||||||
|
|
||||||
|
var s Set
|
||||||
|
s.AddNum(test.num...)
|
||||||
|
checkNumSet(s, t)
|
||||||
|
s.AddRange(test.rng.Start, test.rng.Stop)
|
||||||
|
checkNumSet(s, t)
|
||||||
|
s.AddSet(other)
|
||||||
|
checkNumSet(s, t)
|
||||||
|
|
||||||
|
if out := s.String(); out != test.out {
|
||||||
|
t.Errorf("(%v + %v + %q).String() expected %q; got %q", test.num, test.rng, test.set, test.out, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
654
internal/imapwire/decoder.go
Normal file
654
internal/imapwire/decoder.go
Normal file
@@ -0,0 +1,654 @@
|
|||||||
|
package imapwire
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapnum"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/utf7"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This limits the max list nesting depth to prevent stack overflow.
|
||||||
|
const maxListDepth = 1000
|
||||||
|
|
||||||
|
// IsAtomChar returns true if ch is an ATOM-CHAR.
|
||||||
|
func IsAtomChar(ch byte) bool {
|
||||||
|
switch ch {
|
||||||
|
case '(', ')', '{', ' ', '%', '*', '"', '\\', ']':
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return !unicode.IsControl(rune(ch))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is non-empty char
|
||||||
|
func isAStringChar(ch byte) bool {
|
||||||
|
return IsAtomChar(ch) || ch == ']'
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecoderExpectError is an error due to the Decoder.Expect family of methods.
|
||||||
|
type DecoderExpectError struct {
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err *DecoderExpectError) Error() string {
|
||||||
|
return fmt.Sprintf("imapwire: %v", err.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Decoder reads IMAP data.
|
||||||
|
//
|
||||||
|
// There are multiple families of methods:
|
||||||
|
//
|
||||||
|
// - Methods directly named after IMAP grammar elements attempt to decode
|
||||||
|
// said element, and return false if it's another element.
|
||||||
|
// - "Expect" methods do the same, but set the decoder error (see Err) on
|
||||||
|
// failure.
|
||||||
|
type Decoder struct {
|
||||||
|
// CheckBufferedLiteralFunc is called when a literal is about to be decoded
|
||||||
|
// and needs to be fully buffered in memory.
|
||||||
|
CheckBufferedLiteralFunc func(size int64, nonSync bool) error
|
||||||
|
// MaxSize defines a maximum number of bytes to be read from the input.
|
||||||
|
// Literals are ignored.
|
||||||
|
MaxSize int64
|
||||||
|
|
||||||
|
r *bufio.Reader
|
||||||
|
side ConnSide
|
||||||
|
err error
|
||||||
|
literal bool
|
||||||
|
crlf bool
|
||||||
|
listDepth int
|
||||||
|
readBytes int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDecoder creates a new decoder.
|
||||||
|
func NewDecoder(r *bufio.Reader, side ConnSide) *Decoder {
|
||||||
|
return &Decoder{r: r, side: side}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) mustUnreadByte() {
|
||||||
|
if err := dec.r.UnreadByte(); err != nil {
|
||||||
|
panic(fmt.Errorf("imapwire: failed to unread byte: %v", err))
|
||||||
|
}
|
||||||
|
dec.readBytes--
|
||||||
|
}
|
||||||
|
|
||||||
|
// Err returns the decoder error, if any.
|
||||||
|
func (dec *Decoder) Err() error {
|
||||||
|
return dec.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) returnErr(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if dec.err == nil {
|
||||||
|
dec.err = err
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) readByte() (byte, bool) {
|
||||||
|
if dec.MaxSize > 0 && dec.readBytes > dec.MaxSize {
|
||||||
|
return 0, dec.returnErr(fmt.Errorf("imapwire: max size exceeded"))
|
||||||
|
}
|
||||||
|
dec.crlf = false
|
||||||
|
if dec.literal {
|
||||||
|
return 0, dec.returnErr(fmt.Errorf("imapwire: cannot decode while a literal is open"))
|
||||||
|
}
|
||||||
|
b, err := dec.r.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
err = io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
return b, dec.returnErr(err)
|
||||||
|
}
|
||||||
|
dec.readBytes++
|
||||||
|
return b, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) acceptByte(want byte) bool {
|
||||||
|
got, ok := dec.readByte()
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
} else if got != want {
|
||||||
|
dec.mustUnreadByte()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// EOF returns true if end-of-file is reached.
|
||||||
|
func (dec *Decoder) EOF() bool {
|
||||||
|
_, err := dec.r.ReadByte()
|
||||||
|
if err == io.EOF {
|
||||||
|
return true
|
||||||
|
} else if err != nil {
|
||||||
|
return dec.returnErr(err)
|
||||||
|
}
|
||||||
|
dec.mustUnreadByte()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expect sets the decoder error if ok is false.
|
||||||
|
func (dec *Decoder) Expect(ok bool, name string) bool {
|
||||||
|
if !ok {
|
||||||
|
msg := fmt.Sprintf("expected %v", name)
|
||||||
|
if dec.r.Buffered() > 0 {
|
||||||
|
b, _ := dec.r.Peek(1)
|
||||||
|
msg += fmt.Sprintf(", got %q", b)
|
||||||
|
}
|
||||||
|
return dec.returnErr(&DecoderExpectError{Message: msg})
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) SP() bool {
|
||||||
|
if dec.acceptByte(' ') {
|
||||||
|
// https://github.com/emersion/go-imap/issues/571
|
||||||
|
b, ok := dec.readByte()
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
dec.mustUnreadByte()
|
||||||
|
return b != '\r' && b != '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special case: SP is optional if the next field is a parenthesized list
|
||||||
|
b, ok := dec.readByte()
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
dec.mustUnreadByte()
|
||||||
|
return b == '('
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) ExpectSP() bool {
|
||||||
|
return dec.Expect(dec.SP(), "SP")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) CRLF() bool {
|
||||||
|
dec.acceptByte(' ') // https://github.com/emersion/go-imap/issues/540
|
||||||
|
dec.acceptByte('\r') // be liberal in what we receive and accept lone LF
|
||||||
|
if !dec.acceptByte('\n') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
dec.crlf = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) ExpectCRLF() bool {
|
||||||
|
return dec.Expect(dec.CRLF(), "CRLF")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) Func(ptr *string, valid func(ch byte) bool) bool {
|
||||||
|
var sb strings.Builder
|
||||||
|
for {
|
||||||
|
b, ok := dec.readByte()
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !valid(b) {
|
||||||
|
dec.mustUnreadByte()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteByte(b)
|
||||||
|
}
|
||||||
|
if sb.Len() == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
*ptr = sb.String()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) Atom(ptr *string) bool {
|
||||||
|
return dec.Func(ptr, IsAtomChar)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) ExpectAtom(ptr *string) bool {
|
||||||
|
return dec.Expect(dec.Atom(ptr), "atom")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) ExpectNIL() bool {
|
||||||
|
var s string
|
||||||
|
return dec.ExpectAtom(&s) && dec.Expect(s == "NIL", "NIL")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) Special(b byte) bool {
|
||||||
|
return dec.acceptByte(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) ExpectSpecial(b byte) bool {
|
||||||
|
return dec.Expect(dec.Special(b), fmt.Sprintf("'%v'", string(b)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) Text(ptr *string) bool {
|
||||||
|
var sb strings.Builder
|
||||||
|
for {
|
||||||
|
b, ok := dec.readByte()
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
} else if b == '\r' || b == '\n' {
|
||||||
|
dec.mustUnreadByte()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
sb.WriteByte(b)
|
||||||
|
}
|
||||||
|
if sb.Len() == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
*ptr = sb.String()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) ExpectText(ptr *string) bool {
|
||||||
|
return dec.Expect(dec.Text(ptr), "text")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) DiscardUntilByte(untilCh byte) {
|
||||||
|
for {
|
||||||
|
ch, ok := dec.readByte()
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
} else if ch == untilCh {
|
||||||
|
dec.mustUnreadByte()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) DiscardLine() {
|
||||||
|
if dec.crlf {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var text string
|
||||||
|
dec.Text(&text)
|
||||||
|
dec.CRLF()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) DiscardValue() bool {
|
||||||
|
var s string
|
||||||
|
if dec.String(&s) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
isList, err := dec.List(func() error {
|
||||||
|
if !dec.DiscardValue() {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
} else if isList {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if dec.Atom(&s) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
dec.Expect(false, "value")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) numberStr() (s string, ok bool) {
|
||||||
|
var sb strings.Builder
|
||||||
|
for {
|
||||||
|
ch, ok := dec.readByte()
|
||||||
|
if !ok {
|
||||||
|
return "", false
|
||||||
|
} else if ch < '0' || ch > '9' {
|
||||||
|
dec.mustUnreadByte()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
sb.WriteByte(ch)
|
||||||
|
}
|
||||||
|
if sb.Len() == 0 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return sb.String(), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) Number(ptr *uint32) bool {
|
||||||
|
s, ok := dec.numberStr()
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
v64, err := strconv.ParseUint(s, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return false // can happen on overflow
|
||||||
|
}
|
||||||
|
*ptr = uint32(v64)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) ExpectNumber(ptr *uint32) bool {
|
||||||
|
return dec.Expect(dec.Number(ptr), "number")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) ExpectBodyFldOctets(ptr *uint32) bool {
|
||||||
|
// Workaround: some servers incorrectly return "-1" for the body structure
|
||||||
|
// size. See:
|
||||||
|
// https://github.com/emersion/go-imap/issues/534
|
||||||
|
if dec.acceptByte('-') {
|
||||||
|
*ptr = 0
|
||||||
|
return dec.Expect(dec.acceptByte('1'), "-1 (body-fld-octets workaround)")
|
||||||
|
}
|
||||||
|
return dec.ExpectNumber(ptr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) Number64(ptr *int64) bool {
|
||||||
|
s, ok := dec.numberStr()
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
v, err := strconv.ParseInt(s, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return false // can happen on overflow
|
||||||
|
}
|
||||||
|
*ptr = v
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) ExpectNumber64(ptr *int64) bool {
|
||||||
|
return dec.Expect(dec.Number64(ptr), "number64")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) ModSeq(ptr *uint64) bool {
|
||||||
|
s, ok := dec.numberStr()
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
v, err := strconv.ParseUint(s, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return false // can happen on overflow
|
||||||
|
}
|
||||||
|
*ptr = v
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) ExpectModSeq(ptr *uint64) bool {
|
||||||
|
return dec.Expect(dec.ModSeq(ptr), "mod-sequence-value")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) Quoted(ptr *string) bool {
|
||||||
|
if !dec.Special('"') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var sb strings.Builder
|
||||||
|
for {
|
||||||
|
ch, ok := dec.readByte()
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ch == '"' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if ch == '\\' {
|
||||||
|
ch, ok = dec.readByte()
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteByte(ch)
|
||||||
|
}
|
||||||
|
*ptr = sb.String()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) ExpectAString(ptr *string) bool {
|
||||||
|
if dec.Quoted(ptr) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if dec.Literal(ptr) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// We cannot do dec.Atom(ptr) here because sometimes mailbox names are unquoted,
|
||||||
|
// and they can contain special characters like `]`.
|
||||||
|
return dec.Expect(dec.Func(ptr, isAStringChar), "ASTRING-CHAR")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) String(ptr *string) bool {
|
||||||
|
return dec.Quoted(ptr) || dec.Literal(ptr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) ExpectString(ptr *string) bool {
|
||||||
|
return dec.Expect(dec.String(ptr), "string")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) ExpectNString(ptr *string) bool {
|
||||||
|
var s string
|
||||||
|
if dec.Atom(&s) {
|
||||||
|
if !dec.Expect(s == "NIL", "nstring") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
*ptr = ""
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return dec.ExpectString(ptr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) ExpectNStringReader() (lit *LiteralReader, nonSync, ok bool) {
|
||||||
|
var s string
|
||||||
|
if dec.Atom(&s) {
|
||||||
|
if !dec.Expect(s == "NIL", "nstring") {
|
||||||
|
return nil, false, false
|
||||||
|
}
|
||||||
|
return nil, true, true
|
||||||
|
}
|
||||||
|
// TODO: read quoted string as a string instead of buffering
|
||||||
|
if dec.Quoted(&s) {
|
||||||
|
return newLiteralReaderFromString(s), true, true
|
||||||
|
}
|
||||||
|
if lit, nonSync, ok = dec.LiteralReader(); ok {
|
||||||
|
return lit, nonSync, true
|
||||||
|
} else {
|
||||||
|
return nil, false, dec.Expect(false, "nstring")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) List(f func() error) (isList bool, err error) {
|
||||||
|
if !dec.Special('(') {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if dec.Special(')') {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dec.listDepth++
|
||||||
|
defer func() {
|
||||||
|
dec.listDepth--
|
||||||
|
}()
|
||||||
|
|
||||||
|
if dec.listDepth >= maxListDepth {
|
||||||
|
return false, fmt.Errorf("imapwire: exceeded max depth")
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
if err := f(); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if dec.Special(')') {
|
||||||
|
return true, nil
|
||||||
|
} else if !dec.ExpectSP() {
|
||||||
|
return true, dec.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) ExpectList(f func() error) error {
|
||||||
|
isList, err := dec.List(f)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if !dec.Expect(isList, "(") {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) ExpectNList(f func() error) error {
|
||||||
|
var s string
|
||||||
|
if dec.Atom(&s) {
|
||||||
|
if !dec.Expect(s == "NIL", "NIL") {
|
||||||
|
return dec.Err()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return dec.ExpectList(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) ExpectMailbox(ptr *string) bool {
|
||||||
|
var name string
|
||||||
|
if !dec.ExpectAString(&name) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if strings.EqualFold(name, "INBOX") {
|
||||||
|
*ptr = "INBOX"
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
name, err := utf7.Decode(name)
|
||||||
|
if err == nil {
|
||||||
|
*ptr = name
|
||||||
|
}
|
||||||
|
return dec.returnErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) ExpectUID(ptr *imap.UID) bool {
|
||||||
|
var num uint32
|
||||||
|
if !dec.ExpectNumber(&num) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
*ptr = imap.UID(num)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) ExpectNumSet(kind NumKind, ptr *imap.NumSet) bool {
|
||||||
|
if dec.Special('$') {
|
||||||
|
*ptr = imap.SearchRes()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
var s string
|
||||||
|
if !dec.Expect(dec.Func(&s, isNumSetChar), "sequence-set") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
numSet, err := imapnum.ParseSet(s)
|
||||||
|
if err != nil {
|
||||||
|
return dec.returnErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch kind {
|
||||||
|
case NumKindSeq:
|
||||||
|
*ptr = seqSetFromNumSet(numSet)
|
||||||
|
case NumKindUID:
|
||||||
|
*ptr = uidSetFromNumSet(numSet)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) ExpectUIDSet(ptr *imap.UIDSet) bool {
|
||||||
|
var numSet imap.NumSet
|
||||||
|
ok := dec.ExpectNumSet(NumKindUID, &numSet)
|
||||||
|
if ok {
|
||||||
|
*ptr = numSet.(imap.UIDSet)
|
||||||
|
}
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNumSetChar(ch byte) bool {
|
||||||
|
return ch == '*' || IsAtomChar(ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) Literal(ptr *string) bool {
|
||||||
|
lit, nonSync, ok := dec.LiteralReader()
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if dec.CheckBufferedLiteralFunc != nil {
|
||||||
|
if err := dec.CheckBufferedLiteralFunc(lit.Size(), nonSync); err != nil {
|
||||||
|
lit.cancel()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var sb strings.Builder
|
||||||
|
_, err := io.Copy(&sb, lit)
|
||||||
|
if err == nil {
|
||||||
|
*ptr = sb.String()
|
||||||
|
}
|
||||||
|
return dec.returnErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) LiteralReader() (lit *LiteralReader, nonSync, ok bool) {
|
||||||
|
if !dec.Special('{') {
|
||||||
|
return nil, false, false
|
||||||
|
}
|
||||||
|
var size int64
|
||||||
|
if !dec.ExpectNumber64(&size) {
|
||||||
|
return nil, false, false
|
||||||
|
}
|
||||||
|
if dec.side == ConnSideServer {
|
||||||
|
nonSync = dec.acceptByte('+')
|
||||||
|
}
|
||||||
|
if !dec.ExpectSpecial('}') || !dec.ExpectCRLF() {
|
||||||
|
return nil, false, false
|
||||||
|
}
|
||||||
|
dec.literal = true
|
||||||
|
lit = &LiteralReader{
|
||||||
|
dec: dec,
|
||||||
|
size: size,
|
||||||
|
r: io.LimitReader(dec.r, size),
|
||||||
|
}
|
||||||
|
return lit, nonSync, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dec *Decoder) ExpectLiteralReader() (lit *LiteralReader, nonSync bool, err error) {
|
||||||
|
lit, nonSync, ok := dec.LiteralReader()
|
||||||
|
if !dec.Expect(ok, "literal") {
|
||||||
|
return nil, false, dec.Err()
|
||||||
|
}
|
||||||
|
return lit, nonSync, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type LiteralReader struct {
|
||||||
|
dec *Decoder
|
||||||
|
size int64
|
||||||
|
r io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLiteralReaderFromString(s string) *LiteralReader {
|
||||||
|
return &LiteralReader{
|
||||||
|
size: int64(len(s)),
|
||||||
|
r: strings.NewReader(s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lit *LiteralReader) Size() int64 {
|
||||||
|
return lit.size
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lit *LiteralReader) Read(b []byte) (int, error) {
|
||||||
|
n, err := lit.r.Read(b)
|
||||||
|
if err == io.EOF {
|
||||||
|
lit.cancel()
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lit *LiteralReader) cancel() {
|
||||||
|
if lit.dec == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lit.dec.literal = false
|
||||||
|
lit.dec = nil
|
||||||
|
}
|
||||||
341
internal/imapwire/encoder.go
Normal file
341
internal/imapwire/encoder.go
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
package imapwire
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/utf7"
|
||||||
|
)
|
||||||
|
|
||||||
|
// An Encoder writes IMAP data.
|
||||||
|
//
|
||||||
|
// Most methods don't return an error, instead they defer error handling until
|
||||||
|
// CRLF is called. These methods return the Encoder so that calls can be
|
||||||
|
// chained.
|
||||||
|
type Encoder struct {
|
||||||
|
// QuotedUTF8 allows raw UTF-8 in quoted strings. This requires IMAP4rev2
|
||||||
|
// to be available, or UTF8=ACCEPT to be enabled.
|
||||||
|
QuotedUTF8 bool
|
||||||
|
// LiteralMinus enables non-synchronizing literals for short payloads.
|
||||||
|
// This requires IMAP4rev2 or LITERAL-. This is only meaningful for
|
||||||
|
// clients.
|
||||||
|
LiteralMinus bool
|
||||||
|
// LiteralPlus enables non-synchronizing literals for all payloads. This
|
||||||
|
// requires LITERAL+. This is only meaningful for clients.
|
||||||
|
LiteralPlus bool
|
||||||
|
// NewContinuationRequest creates a new continuation request. This is only
|
||||||
|
// meaningful for clients.
|
||||||
|
NewContinuationRequest func() *ContinuationRequest
|
||||||
|
|
||||||
|
w *bufio.Writer
|
||||||
|
side ConnSide
|
||||||
|
err error
|
||||||
|
literal bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEncoder creates a new encoder.
|
||||||
|
func NewEncoder(w *bufio.Writer, side ConnSide) *Encoder {
|
||||||
|
return &Encoder{w: w, side: side}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *Encoder) setErr(err error) {
|
||||||
|
if enc.err == nil {
|
||||||
|
enc.err = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *Encoder) writeString(s string) *Encoder {
|
||||||
|
if enc.err != nil {
|
||||||
|
return enc
|
||||||
|
}
|
||||||
|
if enc.literal {
|
||||||
|
enc.err = fmt.Errorf("imapwire: cannot encode while a literal is open")
|
||||||
|
return enc
|
||||||
|
}
|
||||||
|
if _, err := enc.w.WriteString(s); err != nil {
|
||||||
|
enc.err = err
|
||||||
|
}
|
||||||
|
return enc
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRLF writes a "\r\n" sequence and flushes the buffered writer.
|
||||||
|
func (enc *Encoder) CRLF() error {
|
||||||
|
enc.writeString("\r\n")
|
||||||
|
if enc.err != nil {
|
||||||
|
return enc.err
|
||||||
|
}
|
||||||
|
return enc.w.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *Encoder) Atom(s string) *Encoder {
|
||||||
|
return enc.writeString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *Encoder) SP() *Encoder {
|
||||||
|
return enc.writeString(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *Encoder) Special(ch byte) *Encoder {
|
||||||
|
return enc.writeString(string(ch))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *Encoder) Quoted(s string) *Encoder {
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.Grow(2 + len(s))
|
||||||
|
sb.WriteByte('"')
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
ch := s[i]
|
||||||
|
if ch == '"' || ch == '\\' {
|
||||||
|
sb.WriteByte('\\')
|
||||||
|
}
|
||||||
|
sb.WriteByte(ch)
|
||||||
|
}
|
||||||
|
sb.WriteByte('"')
|
||||||
|
return enc.writeString(sb.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *Encoder) String(s string) *Encoder {
|
||||||
|
if !enc.validQuoted(s) {
|
||||||
|
enc.stringLiteral(s)
|
||||||
|
return enc
|
||||||
|
}
|
||||||
|
return enc.Quoted(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *Encoder) validQuoted(s string) bool {
|
||||||
|
if len(s) > 4096 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
ch := s[i]
|
||||||
|
|
||||||
|
// NUL, CR and LF are never valid
|
||||||
|
switch ch {
|
||||||
|
case 0, '\r', '\n':
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !enc.QuotedUTF8 && ch > unicode.MaxASCII {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *Encoder) stringLiteral(s string) {
|
||||||
|
var sync *ContinuationRequest
|
||||||
|
if enc.side == ConnSideClient && (!enc.LiteralMinus || len(s) > 4096) && !enc.LiteralPlus {
|
||||||
|
if enc.NewContinuationRequest != nil {
|
||||||
|
sync = enc.NewContinuationRequest()
|
||||||
|
}
|
||||||
|
if sync == nil {
|
||||||
|
enc.setErr(fmt.Errorf("imapwire: cannot send synchronizing literal"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wc := enc.Literal(int64(len(s)), sync)
|
||||||
|
_, writeErr := io.WriteString(wc, s)
|
||||||
|
closeErr := wc.Close()
|
||||||
|
if writeErr != nil {
|
||||||
|
enc.setErr(writeErr)
|
||||||
|
} else if closeErr != nil {
|
||||||
|
enc.setErr(closeErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *Encoder) Mailbox(name string) *Encoder {
|
||||||
|
if strings.EqualFold(name, "INBOX") {
|
||||||
|
return enc.Atom("INBOX")
|
||||||
|
} else {
|
||||||
|
if enc.QuotedUTF8 {
|
||||||
|
name = utf7.Escape(name)
|
||||||
|
} else {
|
||||||
|
name = utf7.Encode(name)
|
||||||
|
}
|
||||||
|
return enc.String(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *Encoder) NumSet(numSet imap.NumSet) *Encoder {
|
||||||
|
s := numSet.String()
|
||||||
|
if s == "" {
|
||||||
|
enc.setErr(fmt.Errorf("imapwire: cannot encode empty sequence set"))
|
||||||
|
return enc
|
||||||
|
}
|
||||||
|
return enc.writeString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *Encoder) Flag(flag imap.Flag) *Encoder {
|
||||||
|
if flag != "\\*" && !isValidFlag(string(flag)) {
|
||||||
|
enc.setErr(fmt.Errorf("imapwire: invalid flag %q", flag))
|
||||||
|
return enc
|
||||||
|
}
|
||||||
|
return enc.writeString(string(flag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *Encoder) MailboxAttr(attr imap.MailboxAttr) *Encoder {
|
||||||
|
if !strings.HasPrefix(string(attr), "\\") || !isValidFlag(string(attr)) {
|
||||||
|
enc.setErr(fmt.Errorf("imapwire: invalid mailbox attribute %q", attr))
|
||||||
|
return enc
|
||||||
|
}
|
||||||
|
return enc.writeString(string(attr))
|
||||||
|
}
|
||||||
|
|
||||||
|
// isValidFlag checks whether the provided string satisfies
|
||||||
|
// flag-keyword / flag-extension.
|
||||||
|
func isValidFlag(s string) bool {
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
ch := s[i]
|
||||||
|
if ch == '\\' {
|
||||||
|
if i != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !IsAtomChar(ch) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(s) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *Encoder) Number(v uint32) *Encoder {
|
||||||
|
return enc.writeString(strconv.FormatUint(uint64(v), 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *Encoder) Number64(v int64) *Encoder {
|
||||||
|
// TODO: disallow negative values
|
||||||
|
return enc.writeString(strconv.FormatInt(v, 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *Encoder) ModSeq(v uint64) *Encoder {
|
||||||
|
// TODO: disallow zero values
|
||||||
|
return enc.writeString(strconv.FormatUint(v, 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
// List writes a parenthesized list.
|
||||||
|
func (enc *Encoder) List(n int, f func(i int)) *Encoder {
|
||||||
|
enc.Special('(')
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
if i > 0 {
|
||||||
|
enc.SP()
|
||||||
|
}
|
||||||
|
f(i)
|
||||||
|
}
|
||||||
|
enc.Special(')')
|
||||||
|
return enc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *Encoder) BeginList() *ListEncoder {
|
||||||
|
enc.Special('(')
|
||||||
|
return &ListEncoder{enc: enc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *Encoder) NIL() *Encoder {
|
||||||
|
return enc.Atom("NIL")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *Encoder) Text(s string) *Encoder {
|
||||||
|
return enc.writeString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *Encoder) UID(uid imap.UID) *Encoder {
|
||||||
|
return enc.Number(uint32(uid))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Literal writes a literal.
|
||||||
|
//
|
||||||
|
// The caller must write exactly size bytes to the returned writer.
|
||||||
|
//
|
||||||
|
// If sync is non-nil, the literal is synchronizing: the encoder will wait for
|
||||||
|
// nil to be sent to the channel before writing the literal data. If an error
|
||||||
|
// is sent to the channel, the literal will be cancelled.
|
||||||
|
func (enc *Encoder) Literal(size int64, sync *ContinuationRequest) io.WriteCloser {
|
||||||
|
if sync != nil && enc.side == ConnSideServer {
|
||||||
|
panic("imapwire: sync must be nil on a server-side Encoder.Literal")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: literal8
|
||||||
|
enc.writeString("{")
|
||||||
|
enc.Number64(size)
|
||||||
|
if sync == nil && enc.side == ConnSideClient {
|
||||||
|
enc.writeString("+")
|
||||||
|
}
|
||||||
|
enc.writeString("}")
|
||||||
|
|
||||||
|
if sync == nil {
|
||||||
|
enc.writeString("\r\n")
|
||||||
|
} else {
|
||||||
|
if err := enc.CRLF(); err != nil {
|
||||||
|
return errorWriter{err}
|
||||||
|
}
|
||||||
|
if _, err := sync.Wait(); err != nil {
|
||||||
|
enc.setErr(err)
|
||||||
|
return errorWriter{err}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enc.literal = true
|
||||||
|
return &literalWriter{
|
||||||
|
enc: enc,
|
||||||
|
n: size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type errorWriter struct {
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ew errorWriter) Write(b []byte) (int, error) {
|
||||||
|
return 0, ew.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ew errorWriter) Close() error {
|
||||||
|
return ew.err
|
||||||
|
}
|
||||||
|
|
||||||
|
type literalWriter struct {
|
||||||
|
enc *Encoder
|
||||||
|
n int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lw *literalWriter) Write(b []byte) (int, error) {
|
||||||
|
if lw.n-int64(len(b)) < 0 {
|
||||||
|
return 0, fmt.Errorf("wrote too many bytes in literal")
|
||||||
|
}
|
||||||
|
n, err := lw.enc.w.Write(b)
|
||||||
|
lw.n -= int64(n)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lw *literalWriter) Close() error {
|
||||||
|
lw.enc.literal = false
|
||||||
|
if lw.n != 0 {
|
||||||
|
return fmt.Errorf("wrote too few bytes in literal (%v remaining)", lw.n)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListEncoder struct {
|
||||||
|
enc *Encoder
|
||||||
|
n int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (le *ListEncoder) Item() *Encoder {
|
||||||
|
if le.n > 0 {
|
||||||
|
le.enc.SP()
|
||||||
|
}
|
||||||
|
le.n++
|
||||||
|
return le.enc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (le *ListEncoder) End() {
|
||||||
|
le.enc.Special(')')
|
||||||
|
le.enc = nil
|
||||||
|
}
|
||||||
47
internal/imapwire/imapwire.go
Normal file
47
internal/imapwire/imapwire.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// Package imapwire implements the IMAP wire protocol.
|
||||||
|
//
|
||||||
|
// The IMAP wire protocol is defined in RFC 9051 section 4.
|
||||||
|
package imapwire
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConnSide describes the side of a connection: client or server.
|
||||||
|
type ConnSide int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ConnSideClient ConnSide = 1 + iota
|
||||||
|
ConnSideServer
|
||||||
|
)
|
||||||
|
|
||||||
|
// ContinuationRequest is a continuation request.
|
||||||
|
//
|
||||||
|
// The sender must call either Done or Cancel. The receiver must call Wait.
|
||||||
|
type ContinuationRequest struct {
|
||||||
|
done chan struct{}
|
||||||
|
err error
|
||||||
|
text string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewContinuationRequest() *ContinuationRequest {
|
||||||
|
return &ContinuationRequest{done: make(chan struct{})}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cont *ContinuationRequest) Cancel(err error) {
|
||||||
|
if err == nil {
|
||||||
|
err = fmt.Errorf("imapwire: continuation request cancelled")
|
||||||
|
}
|
||||||
|
cont.err = err
|
||||||
|
close(cont.done)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cont *ContinuationRequest) Done(text string) {
|
||||||
|
cont.text = text
|
||||||
|
close(cont.done)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cont *ContinuationRequest) Wait() (string, error) {
|
||||||
|
<-cont.done
|
||||||
|
return cont.text, cont.err
|
||||||
|
}
|
||||||
39
internal/imapwire/num.go
Normal file
39
internal/imapwire/num.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package imapwire
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapnum"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NumKind int
|
||||||
|
|
||||||
|
const (
|
||||||
|
NumKindSeq NumKind = iota + 1
|
||||||
|
NumKindUID
|
||||||
|
)
|
||||||
|
|
||||||
|
func seqSetFromNumSet(s imapnum.Set) imap.SeqSet {
|
||||||
|
return *(*imap.SeqSet)(unsafe.Pointer(&s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func uidSetFromNumSet(s imapnum.Set) imap.UIDSet {
|
||||||
|
return *(*imap.UIDSet)(unsafe.Pointer(&s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func NumSetKind(numSet imap.NumSet) NumKind {
|
||||||
|
switch numSet.(type) {
|
||||||
|
case imap.SeqSet:
|
||||||
|
return NumKindSeq
|
||||||
|
case imap.UIDSet:
|
||||||
|
return NumKindUID
|
||||||
|
default:
|
||||||
|
panic("imap: invalid NumSet type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseSeqSet(s string) (imap.SeqSet, error) {
|
||||||
|
numSet, err := imapnum.ParseSet(s)
|
||||||
|
return seqSetFromNumSet(numSet), err
|
||||||
|
}
|
||||||
188
internal/internal.go
Normal file
188
internal/internal.go
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2"
|
||||||
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DateTimeLayout = "_2-Jan-2006 15:04:05 -0700"
|
||||||
|
DateLayout = "2-Jan-2006"
|
||||||
|
)
|
||||||
|
|
||||||
|
const FlagRecent imap.Flag = "\\Recent" // removed in IMAP4rev2
|
||||||
|
|
||||||
|
func DecodeDateTime(dec *imapwire.Decoder) (time.Time, error) {
|
||||||
|
var s string
|
||||||
|
if !dec.Quoted(&s) {
|
||||||
|
return time.Time{}, nil
|
||||||
|
}
|
||||||
|
t, err := time.Parse(DateTimeLayout, s)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, fmt.Errorf("in date-time: %v", err) // TODO: use imapwire.DecodeExpectError?
|
||||||
|
}
|
||||||
|
return t, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExpectDateTime(dec *imapwire.Decoder) (time.Time, error) {
|
||||||
|
t, err := DecodeDateTime(dec)
|
||||||
|
if err != nil {
|
||||||
|
return t, err
|
||||||
|
}
|
||||||
|
if !dec.Expect(!t.IsZero(), "date-time") {
|
||||||
|
return t, dec.Err()
|
||||||
|
}
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExpectDate(dec *imapwire.Decoder) (time.Time, error) {
|
||||||
|
var s string
|
||||||
|
if !dec.ExpectAString(&s) {
|
||||||
|
return time.Time{}, dec.Err()
|
||||||
|
}
|
||||||
|
t, err := time.Parse(DateLayout, s)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, fmt.Errorf("in date: %v", err) // use imapwire.DecodeExpectError?
|
||||||
|
}
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExpectFlagList(dec *imapwire.Decoder) ([]imap.Flag, error) {
|
||||||
|
var flags []imap.Flag
|
||||||
|
err := dec.ExpectList(func() error {
|
||||||
|
// Some servers start the list with a space, so we need to skip it
|
||||||
|
// https://github.com/emersion/go-imap/pull/633
|
||||||
|
dec.SP()
|
||||||
|
|
||||||
|
flag, err := ExpectFlag(dec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
flags = append(flags, flag)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return flags, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExpectCap(dec *imapwire.Decoder) (imap.Cap, error) {
|
||||||
|
var name string
|
||||||
|
if !dec.ExpectAtom(&name) {
|
||||||
|
return "", dec.Err()
|
||||||
|
}
|
||||||
|
return canonicalCap(name), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExpectFlag(dec *imapwire.Decoder) (imap.Flag, error) {
|
||||||
|
isSystem := dec.Special('\\')
|
||||||
|
if isSystem && dec.Special('*') {
|
||||||
|
return imap.FlagWildcard, nil // flag-perm
|
||||||
|
}
|
||||||
|
var name string
|
||||||
|
if !dec.ExpectAtom(&name) {
|
||||||
|
return "", fmt.Errorf("in flag: %w", dec.Err())
|
||||||
|
}
|
||||||
|
if isSystem {
|
||||||
|
name = "\\" + name
|
||||||
|
}
|
||||||
|
return canonicalFlag(name), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExpectMailboxAttrList(dec *imapwire.Decoder) ([]imap.MailboxAttr, error) {
|
||||||
|
var attrs []imap.MailboxAttr
|
||||||
|
err := dec.ExpectList(func() error {
|
||||||
|
attr, err := ExpectMailboxAttr(dec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
attrs = append(attrs, attr)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return attrs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExpectMailboxAttr(dec *imapwire.Decoder) (imap.MailboxAttr, error) {
|
||||||
|
flag, err := ExpectFlag(dec)
|
||||||
|
return canonicalMailboxAttr(string(flag)), err
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
canonOnce sync.Once
|
||||||
|
canonFlag map[string]imap.Flag
|
||||||
|
canonMailboxAttr map[string]imap.MailboxAttr
|
||||||
|
)
|
||||||
|
|
||||||
|
func canonInit() {
|
||||||
|
flags := []imap.Flag{
|
||||||
|
imap.FlagSeen,
|
||||||
|
imap.FlagAnswered,
|
||||||
|
imap.FlagFlagged,
|
||||||
|
imap.FlagDeleted,
|
||||||
|
imap.FlagDraft,
|
||||||
|
imap.FlagForwarded,
|
||||||
|
imap.FlagMDNSent,
|
||||||
|
imap.FlagJunk,
|
||||||
|
imap.FlagNotJunk,
|
||||||
|
imap.FlagPhishing,
|
||||||
|
imap.FlagImportant,
|
||||||
|
}
|
||||||
|
mailboxAttrs := []imap.MailboxAttr{
|
||||||
|
imap.MailboxAttrNonExistent,
|
||||||
|
imap.MailboxAttrNoInferiors,
|
||||||
|
imap.MailboxAttrNoSelect,
|
||||||
|
imap.MailboxAttrHasChildren,
|
||||||
|
imap.MailboxAttrHasNoChildren,
|
||||||
|
imap.MailboxAttrMarked,
|
||||||
|
imap.MailboxAttrUnmarked,
|
||||||
|
imap.MailboxAttrSubscribed,
|
||||||
|
imap.MailboxAttrRemote,
|
||||||
|
imap.MailboxAttrAll,
|
||||||
|
imap.MailboxAttrArchive,
|
||||||
|
imap.MailboxAttrDrafts,
|
||||||
|
imap.MailboxAttrFlagged,
|
||||||
|
imap.MailboxAttrJunk,
|
||||||
|
imap.MailboxAttrSent,
|
||||||
|
imap.MailboxAttrTrash,
|
||||||
|
imap.MailboxAttrImportant,
|
||||||
|
}
|
||||||
|
|
||||||
|
canonFlag = make(map[string]imap.Flag)
|
||||||
|
for _, flag := range flags {
|
||||||
|
canonFlag[strings.ToLower(string(flag))] = flag
|
||||||
|
}
|
||||||
|
|
||||||
|
canonMailboxAttr = make(map[string]imap.MailboxAttr)
|
||||||
|
for _, attr := range mailboxAttrs {
|
||||||
|
canonMailboxAttr[strings.ToLower(string(attr))] = attr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func canonicalFlag(s string) imap.Flag {
|
||||||
|
canonOnce.Do(canonInit)
|
||||||
|
if flag, ok := canonFlag[strings.ToLower(s)]; ok {
|
||||||
|
return flag
|
||||||
|
}
|
||||||
|
return imap.Flag(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func canonicalMailboxAttr(s string) imap.MailboxAttr {
|
||||||
|
canonOnce.Do(canonInit)
|
||||||
|
if attr, ok := canonMailboxAttr[strings.ToLower(s)]; ok {
|
||||||
|
return attr
|
||||||
|
}
|
||||||
|
return imap.MailboxAttr(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func canonicalCap(s string) imap.Cap {
|
||||||
|
// Only two caps are not fully uppercase
|
||||||
|
for _, cap := range []imap.Cap{imap.CapIMAP4rev1, imap.CapIMAP4rev2} {
|
||||||
|
if strings.EqualFold(s, string(cap)) {
|
||||||
|
return cap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return imap.Cap(strings.ToUpper(s))
|
||||||
|
}
|
||||||
23
internal/sasl.go
Normal file
23
internal/sasl.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
)
|
||||||
|
|
||||||
|
func EncodeSASL(b []byte) string {
|
||||||
|
if len(b) == 0 {
|
||||||
|
return "="
|
||||||
|
} else {
|
||||||
|
return base64.StdEncoding.EncodeToString(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeSASL(s string) ([]byte, error) {
|
||||||
|
if s == "=" {
|
||||||
|
// go-sasl treats nil as no challenge/response, so return a non-nil
|
||||||
|
// empty byte slice
|
||||||
|
return []byte{}, nil
|
||||||
|
} else {
|
||||||
|
return base64.StdEncoding.DecodeString(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
118
internal/utf7/decoder.go
Normal file
118
internal/utf7/decoder.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package utf7
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf16"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrInvalidUTF7 means that a decoder encountered invalid UTF-7.
|
||||||
|
var ErrInvalidUTF7 = errors.New("utf7: invalid UTF-7")
|
||||||
|
|
||||||
|
// Decode decodes a string encoded with modified UTF-7.
|
||||||
|
//
|
||||||
|
// Note, raw UTF-8 is accepted.
|
||||||
|
func Decode(src string) (string, error) {
|
||||||
|
if !utf8.ValidString(src) {
|
||||||
|
return "", errors.New("invalid UTF-8")
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.Grow(len(src))
|
||||||
|
|
||||||
|
ascii := true
|
||||||
|
for i := 0; i < len(src); i++ {
|
||||||
|
ch := src[i]
|
||||||
|
|
||||||
|
if ch < min || (ch > max && ch < utf8.RuneSelf) {
|
||||||
|
// Illegal code point in ASCII mode. Note, UTF-8 codepoints are
|
||||||
|
// always allowed.
|
||||||
|
return "", ErrInvalidUTF7
|
||||||
|
}
|
||||||
|
|
||||||
|
if ch != '&' {
|
||||||
|
sb.WriteByte(ch)
|
||||||
|
ascii = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the end of the Base64 or "&-" segment
|
||||||
|
start := i + 1
|
||||||
|
for i++; i < len(src) && src[i] != '-'; i++ {
|
||||||
|
if src[i] == '\r' || src[i] == '\n' { // base64 package ignores CR and LF
|
||||||
|
return "", ErrInvalidUTF7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == len(src) { // Implicit shift ("&...")
|
||||||
|
return "", ErrInvalidUTF7
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == start { // Escape sequence "&-"
|
||||||
|
sb.WriteByte('&')
|
||||||
|
ascii = true
|
||||||
|
} else { // Control or non-ASCII code points in base64
|
||||||
|
if !ascii { // Null shift ("&...-&...-")
|
||||||
|
return "", ErrInvalidUTF7
|
||||||
|
}
|
||||||
|
|
||||||
|
b := decode([]byte(src[start:i]))
|
||||||
|
if len(b) == 0 { // Bad encoding
|
||||||
|
return "", ErrInvalidUTF7
|
||||||
|
}
|
||||||
|
sb.Write(b)
|
||||||
|
|
||||||
|
ascii = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extracts UTF-16-BE bytes from base64 data and converts them to UTF-8.
|
||||||
|
// A nil slice is returned if the encoding is invalid.
|
||||||
|
func decode(b64 []byte) []byte {
|
||||||
|
var b []byte
|
||||||
|
|
||||||
|
// Allocate a single block of memory large enough to store the Base64 data
|
||||||
|
// (if padding is required), UTF-16-BE bytes, and decoded UTF-8 bytes.
|
||||||
|
// Since a 2-byte UTF-16 sequence may expand into a 3-byte UTF-8 sequence,
|
||||||
|
// double the space allocation for UTF-8.
|
||||||
|
if n := len(b64); b64[n-1] == '=' {
|
||||||
|
return nil
|
||||||
|
} else if n&3 == 0 {
|
||||||
|
b = make([]byte, b64Enc.DecodedLen(n)*3)
|
||||||
|
} else {
|
||||||
|
n += 4 - n&3
|
||||||
|
b = make([]byte, n+b64Enc.DecodedLen(n)*3)
|
||||||
|
copy(b[copy(b, b64):n], []byte("=="))
|
||||||
|
b64, b = b[:n], b[n:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode Base64 into the first 1/3rd of b
|
||||||
|
n, err := b64Enc.Decode(b, b64)
|
||||||
|
if err != nil || n&1 == 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode UTF-16-BE into the remaining 2/3rds of b
|
||||||
|
b, s := b[:n], b[n:]
|
||||||
|
j := 0
|
||||||
|
for i := 0; i < n; i += 2 {
|
||||||
|
r := rune(b[i])<<8 | rune(b[i+1])
|
||||||
|
if utf16.IsSurrogate(r) {
|
||||||
|
if i += 2; i == n {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
r2 := rune(b[i])<<8 | rune(b[i+1])
|
||||||
|
if r = utf16.DecodeRune(r, r2); r == utf8.RuneError {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else if min <= r && r <= max {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
j += utf8.EncodeRune(s[j:], r)
|
||||||
|
}
|
||||||
|
return s[:j]
|
||||||
|
}
|
||||||
115
internal/utf7/decoder_test.go
Normal file
115
internal/utf7/decoder_test.go
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package utf7_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2/internal/utf7"
|
||||||
|
)
|
||||||
|
|
||||||
|
var decode = []struct {
|
||||||
|
in string
|
||||||
|
out string
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
// Basics (the inverse test on encode checks other valid inputs)
|
||||||
|
{"", "", true},
|
||||||
|
{"abc", "abc", true},
|
||||||
|
{"&-abc", "&abc", true},
|
||||||
|
{"abc&-", "abc&", true},
|
||||||
|
{"a&-b&-c", "a&b&c", true},
|
||||||
|
{"&ABk-", "\x19", true},
|
||||||
|
{"&AB8-", "\x1F", true},
|
||||||
|
{"ABk-", "ABk-", true},
|
||||||
|
{"&-,&-&AP8-&-", "&,&\u00FF&", true},
|
||||||
|
{"&-&-,&AP8-&-", "&&,\u00FF&", true},
|
||||||
|
{"abc &- &AP8A,wD,- &- xyz", "abc & \u00FF\u00FF\u00FF & xyz", true},
|
||||||
|
|
||||||
|
// Illegal code point in ASCII
|
||||||
|
{"\x00", "", false},
|
||||||
|
{"\x1F", "", false},
|
||||||
|
{"abc\n", "", false},
|
||||||
|
{"abc\x7Fxyz", "", false},
|
||||||
|
|
||||||
|
// Invalid UTF-8
|
||||||
|
{"\xc3\x28", "", false},
|
||||||
|
{"\xe2\x82\x28", "", false},
|
||||||
|
|
||||||
|
// Invalid Base64 alphabet
|
||||||
|
{"&/+8-", "", false},
|
||||||
|
{"&*-", "", false},
|
||||||
|
{"&ZeVnLIqe -", "", false},
|
||||||
|
|
||||||
|
// CR and LF in Base64
|
||||||
|
{"&ZeVnLIqe\r\n-", "", false},
|
||||||
|
{"&ZeVnLIqe\r\n\r\n-", "", false},
|
||||||
|
{"&ZeVn\r\n\r\nLIqe-", "", false},
|
||||||
|
|
||||||
|
// Padding not stripped
|
||||||
|
{"&AAAAHw=-", "", false},
|
||||||
|
{"&AAAAHw==-", "", false},
|
||||||
|
{"&AAAAHwB,AIA=-", "", false},
|
||||||
|
{"&AAAAHwB,AIA==-", "", false},
|
||||||
|
|
||||||
|
// One byte short
|
||||||
|
{"&2A-", "", false},
|
||||||
|
{"&2ADc-", "", false},
|
||||||
|
{"&AAAAHwB,A-", "", false},
|
||||||
|
{"&AAAAHwB,A=-", "", false},
|
||||||
|
{"&AAAAHwB,A==-", "", false},
|
||||||
|
{"&AAAAHwB,A===-", "", false},
|
||||||
|
{"&AAAAHwB,AI-", "", false},
|
||||||
|
{"&AAAAHwB,AI=-", "", false},
|
||||||
|
{"&AAAAHwB,AI==-", "", false},
|
||||||
|
|
||||||
|
// Implicit shift
|
||||||
|
{"&", "", false},
|
||||||
|
{"&Jjo", "", false},
|
||||||
|
{"Jjo&", "", false},
|
||||||
|
{"&Jjo&", "", false},
|
||||||
|
{"&Jjo!", "", false},
|
||||||
|
{"&Jjo+", "", false},
|
||||||
|
{"abc&Jjo", "", false},
|
||||||
|
|
||||||
|
// Null shift
|
||||||
|
{"&AGE-&Jjo-", "", false},
|
||||||
|
{"&U,BTFw-&ZeVnLIqe-", "", false},
|
||||||
|
|
||||||
|
// Long input with Base64 at the end
|
||||||
|
{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa &2D3eCg- &2D3eCw- &2D3eDg-",
|
||||||
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \U0001f60a \U0001f60b \U0001f60e", true},
|
||||||
|
|
||||||
|
// Long input in Base64 between short ASCII
|
||||||
|
{"00000000000000000000 &MEIwQjBCMEIwQjBCMEIwQjBCMEIwQjBCMEIwQjBCMEIwQjBCMEIwQjBCMEIwQjBCMEIwQjBCMEIwQjBCMEIwQjBCMEIwQjBCMEI- 00000000000000000000",
|
||||||
|
"00000000000000000000 " + strings.Repeat("\U00003042", 37) + " 00000000000000000000", true},
|
||||||
|
|
||||||
|
// ASCII in Base64
|
||||||
|
{"&AGE-", "", false}, // "a"
|
||||||
|
{"&ACY-", "", false}, // "&"
|
||||||
|
{"&AGgAZQBsAGwAbw-", "", false}, // "hello"
|
||||||
|
{"&JjoAIQ-", "", false}, // "\u263a!"
|
||||||
|
|
||||||
|
// Bad surrogate
|
||||||
|
{"&2AA-", "", false}, // U+D800
|
||||||
|
{"&2AD-", "", false}, // U+D800
|
||||||
|
{"&3AA-", "", false}, // U+DC00
|
||||||
|
{"&2AAAQQ-", "", false}, // U+D800 'A'
|
||||||
|
{"&2AD,,w-", "", false}, // U+D800 U+FFFF
|
||||||
|
{"&3ADYAA-", "", false}, // U+DC00 U+D800
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecoder(t *testing.T) {
|
||||||
|
for _, test := range decode {
|
||||||
|
out, err := utf7.Decode(test.in)
|
||||||
|
if out != test.out {
|
||||||
|
t.Errorf("UTF7Decode(%+q) expected %+q; got %+q", test.in, test.out, out)
|
||||||
|
}
|
||||||
|
if test.ok {
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("UTF7Decode(%+q) unexpected error; %v", test.in, err)
|
||||||
|
}
|
||||||
|
} else if err == nil {
|
||||||
|
t.Errorf("UTF7Decode(%+q) expected error", test.in)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
88
internal/utf7/encoder.go
Normal file
88
internal/utf7/encoder.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package utf7
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"unicode/utf16"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Encode encodes a string with modified UTF-7.
|
||||||
|
func Encode(src string) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.Grow(len(src))
|
||||||
|
|
||||||
|
for i := 0; i < len(src); {
|
||||||
|
ch := src[i]
|
||||||
|
|
||||||
|
if min <= ch && ch <= max {
|
||||||
|
sb.WriteByte(ch)
|
||||||
|
if ch == '&' {
|
||||||
|
sb.WriteByte('-')
|
||||||
|
}
|
||||||
|
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
start := i
|
||||||
|
|
||||||
|
// Find the next printable ASCII code point
|
||||||
|
i++
|
||||||
|
for i < len(src) && (src[i] < min || src[i] > max) {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.Write(encode([]byte(src[start:i])))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts string s from UTF-8 to UTF-16-BE, encodes the result as base64,
|
||||||
|
// removes the padding, and adds UTF-7 shifts.
|
||||||
|
func encode(s []byte) []byte {
|
||||||
|
// len(s) is sufficient for UTF-8 to UTF-16 conversion if there are no
|
||||||
|
// control code points (see table below).
|
||||||
|
b := make([]byte, 0, len(s)+4)
|
||||||
|
for len(s) > 0 {
|
||||||
|
r, size := utf8.DecodeRune(s)
|
||||||
|
if r > utf8.MaxRune {
|
||||||
|
r, size = utf8.RuneError, 1 // Bug fix (issue 3785)
|
||||||
|
}
|
||||||
|
s = s[size:]
|
||||||
|
if r1, r2 := utf16.EncodeRune(r); r1 != utf8.RuneError {
|
||||||
|
b = append(b, byte(r1>>8), byte(r1))
|
||||||
|
r = r2
|
||||||
|
}
|
||||||
|
b = append(b, byte(r>>8), byte(r))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode as base64
|
||||||
|
n := b64Enc.EncodedLen(len(b)) + 2
|
||||||
|
b64 := make([]byte, n)
|
||||||
|
b64Enc.Encode(b64[1:], b)
|
||||||
|
|
||||||
|
// Strip padding
|
||||||
|
n -= 2 - (len(b)+2)%3
|
||||||
|
b64 = b64[:n]
|
||||||
|
|
||||||
|
// Add UTF-7 shifts
|
||||||
|
b64[0] = '&'
|
||||||
|
b64[n-1] = '-'
|
||||||
|
return b64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape passes through raw UTF-8 as-is and escapes the special UTF-7 marker
|
||||||
|
// (the ampersand character).
|
||||||
|
func Escape(src string) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.Grow(len(src))
|
||||||
|
|
||||||
|
for _, ch := range src {
|
||||||
|
sb.WriteRune(ch)
|
||||||
|
if ch == '&' {
|
||||||
|
sb.WriteByte('-')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
124
internal/utf7/encoder_test.go
Normal file
124
internal/utf7/encoder_test.go
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
package utf7_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap/v2/internal/utf7"
|
||||||
|
)
|
||||||
|
|
||||||
|
var encode = []struct {
|
||||||
|
in string
|
||||||
|
out string
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
// Printable ASCII
|
||||||
|
{"", "", true},
|
||||||
|
{"a", "a", true},
|
||||||
|
{"ab", "ab", true},
|
||||||
|
{"-", "-", true},
|
||||||
|
{"&", "&-", true},
|
||||||
|
{"&&", "&-&-", true},
|
||||||
|
{"&&&-&", "&-&-&--&-", true},
|
||||||
|
{"-&*&-", "-&-*&--", true},
|
||||||
|
{"a&b", "a&-b", true},
|
||||||
|
{"a&", "a&-", true},
|
||||||
|
{"&b", "&-b", true},
|
||||||
|
{"-a&", "-a&-", true},
|
||||||
|
{"&b-", "&-b-", true},
|
||||||
|
|
||||||
|
// Unicode range
|
||||||
|
{"\u0000", "&AAA-", true},
|
||||||
|
{"\n", "&AAo-", true},
|
||||||
|
{"\r", "&AA0-", true},
|
||||||
|
{"\u001F", "&AB8-", true},
|
||||||
|
{"\u0020", " ", true},
|
||||||
|
{"\u0025", "%", true},
|
||||||
|
{"\u0026", "&-", true},
|
||||||
|
{"\u0027", "'", true},
|
||||||
|
{"\u007E", "~", true},
|
||||||
|
{"\u007F", "&AH8-", true},
|
||||||
|
{"\u0080", "&AIA-", true},
|
||||||
|
{"\u00FF", "&AP8-", true},
|
||||||
|
{"\u07FF", "&B,8-", true},
|
||||||
|
{"\u0800", "&CAA-", true},
|
||||||
|
{"\uFFEF", "&,+8-", true},
|
||||||
|
{"\uFFFF", "&,,8-", true},
|
||||||
|
{"\U00010000", "&2ADcAA-", true},
|
||||||
|
{"\U0010FFFF", "&2,,f,w-", true},
|
||||||
|
|
||||||
|
// Padding
|
||||||
|
{"\x00\x1F", "&AAAAHw-", true}, // 2
|
||||||
|
{"\x00\x1F\x7F", "&AAAAHwB,-", true}, // 0
|
||||||
|
{"\x00\x1F\x7F\u0080", "&AAAAHwB,AIA-", true}, // 1
|
||||||
|
{"\x00\x1F\x7F\u0080\u00FF", "&AAAAHwB,AIAA,w-", true}, // 2
|
||||||
|
|
||||||
|
// Mix
|
||||||
|
{"a\x00", "a&AAA-", true},
|
||||||
|
{"\x00a", "&AAA-a", true},
|
||||||
|
{"&\x00", "&-&AAA-", true},
|
||||||
|
{"\x00&", "&AAA-&-", true},
|
||||||
|
{"a\x00&", "a&AAA-&-", true},
|
||||||
|
{"a&\x00", "a&-&AAA-", true},
|
||||||
|
{"&a\x00", "&-a&AAA-", true},
|
||||||
|
{"&\x00a", "&-&AAA-a", true},
|
||||||
|
{"\x00&a", "&AAA-&-a", true},
|
||||||
|
{"\x00a&", "&AAA-a&-", true},
|
||||||
|
{"ab&\uFFFF", "ab&-&,,8-", true},
|
||||||
|
{"a&b\uFFFF", "a&-b&,,8-", true},
|
||||||
|
{"&ab\uFFFF", "&-ab&,,8-", true},
|
||||||
|
{"ab\uFFFF&", "ab&,,8-&-", true},
|
||||||
|
{"a\uFFFFb&", "a&,,8-b&-", true},
|
||||||
|
{"\uFFFFab&", "&,,8-ab&-", true},
|
||||||
|
|
||||||
|
{"\x20\x25&\x27\x7E", " %&-'~", true},
|
||||||
|
{"\x1F\x20&\x7E\x7F", "&AB8- &-~&AH8-", true},
|
||||||
|
{"&\x00\x19\x7F\u0080", "&-&AAAAGQB,AIA-", true},
|
||||||
|
{"\x00&\x19\x7F\u0080", "&AAA-&-&ABkAfwCA-", true},
|
||||||
|
{"\x00\x19&\x7F\u0080", "&AAAAGQ-&-&AH8AgA-", true},
|
||||||
|
{"\x00\x19\x7F&\u0080", "&AAAAGQB,-&-&AIA-", true},
|
||||||
|
{"\x00\x19\x7F\u0080&", "&AAAAGQB,AIA-&-", true},
|
||||||
|
{"&\x00\x1F\x7F\u0080", "&-&AAAAHwB,AIA-", true},
|
||||||
|
{"\x00&\x1F\x7F\u0080", "&AAA-&-&AB8AfwCA-", true},
|
||||||
|
{"\x00\x1F&\x7F\u0080", "&AAAAHw-&-&AH8AgA-", true},
|
||||||
|
{"\x00\x1F\x7F&\u0080", "&AAAAHwB,-&-&AIA-", true},
|
||||||
|
{"\x00\x1F\x7F\u0080&", "&AAAAHwB,AIA-&-", true},
|
||||||
|
|
||||||
|
// Russian
|
||||||
|
{"\u041C\u0430\u043A\u0441\u0438\u043C \u0425\u0438\u0442\u0440\u043E\u0432",
|
||||||
|
"&BBwEMAQ6BEEEOAQ8- &BCUEOARCBEAEPgQy-", true},
|
||||||
|
|
||||||
|
// RFC 3501
|
||||||
|
{"~peter/mail/\u53F0\u5317/\u65E5\u672C\u8A9E", "~peter/mail/&U,BTFw-/&ZeVnLIqe-", true},
|
||||||
|
{"~peter/mail/\u53F0\u5317/\u65E5\u672C\u8A9E", "~peter/mail/&U,BTFw-/&ZeVnLIqe-", true},
|
||||||
|
{"\u263A!", "&Jjo-!", true},
|
||||||
|
{"\u53F0\u5317\u65E5\u672C\u8A9E", "&U,BTF2XlZyyKng-", true},
|
||||||
|
|
||||||
|
// RFC 2152 (modified)
|
||||||
|
{"\u0041\u2262\u0391\u002E", "A&ImIDkQ-.", true},
|
||||||
|
{"Hi Mom -\u263A-!", "Hi Mom -&Jjo--!", true},
|
||||||
|
{"\u65E5\u672C\u8A9E", "&ZeVnLIqe-", true},
|
||||||
|
|
||||||
|
// 8->16 and 24->16 byte UTF-8 to UTF-16 conversion
|
||||||
|
{"\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007", "&AAAAAQACAAMABAAFAAYABw-", true},
|
||||||
|
{"\u0800\u0801\u0802\u0803\u0804\u0805\u0806\u0807", "&CAAIAQgCCAMIBAgFCAYIBw-", true},
|
||||||
|
|
||||||
|
// Invalid UTF-8 (bad bytes are converted to U+FFFD)
|
||||||
|
{"\xC0\x80", "&,,3,,Q-", false}, // U+0000
|
||||||
|
{"\xF4\x90\x80\x80", "&,,3,,f,9,,0-", false}, // U+110000
|
||||||
|
{"\xF7\xBF\xBF\xBF", "&,,3,,f,9,,0-", false}, // U+1FFFFF
|
||||||
|
{"\xF8\x88\x80\x80\x80", "&,,3,,f,9,,3,,Q-", false}, // U+200000
|
||||||
|
{"\xF4\x8F\xBF\x3F", "&,,3,,f,9-?", false}, // U+10FFFF (bad byte)
|
||||||
|
{"\xF4\x8F\xBF", "&,,3,,f,9-", false}, // U+10FFFF (short)
|
||||||
|
{"\xF4\x8F", "&,,3,,Q-", false},
|
||||||
|
{"\xF4", "&,,0-", false},
|
||||||
|
{"\x00\xF4\x00", "&AAD,,QAA-", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncoder(t *testing.T) {
|
||||||
|
for _, test := range encode {
|
||||||
|
out := utf7.Encode(test.in)
|
||||||
|
if out != test.out {
|
||||||
|
t.Errorf("UTF7Encode(%+q) expected %+q; got %+q", test.in, test.out, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
internal/utf7/utf7.go
Normal file
13
internal/utf7/utf7.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// Package utf7 implements modified UTF-7 encoding defined in RFC 3501 section 5.1.3
|
||||||
|
package utf7
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
min = 0x20 // Minimum self-representing UTF-7 value
|
||||||
|
max = 0x7E // Maximum self-representing UTF-7 value
|
||||||
|
)
|
||||||
|
|
||||||
|
var b64Enc = base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,")
|
||||||
30
list.go
Normal file
30
list.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package imap
|
||||||
|
|
||||||
|
// ListOptions contains options for the LIST command.
|
||||||
|
type ListOptions struct {
|
||||||
|
SelectSubscribed bool
|
||||||
|
SelectRemote bool
|
||||||
|
SelectRecursiveMatch bool // requires SelectSubscribed to be set
|
||||||
|
SelectSpecialUse bool // requires SPECIAL-USE
|
||||||
|
|
||||||
|
ReturnSubscribed bool
|
||||||
|
ReturnChildren bool
|
||||||
|
ReturnStatus *StatusOptions // requires IMAP4rev2 or LIST-STATUS
|
||||||
|
ReturnSpecialUse bool // requires SPECIAL-USE
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListData is the mailbox data returned by a LIST command.
|
||||||
|
type ListData struct {
|
||||||
|
Attrs []MailboxAttr
|
||||||
|
Delim rune
|
||||||
|
Mailbox string
|
||||||
|
|
||||||
|
// Extended data
|
||||||
|
ChildInfo *ListDataChildInfo
|
||||||
|
OldName string
|
||||||
|
Status *StatusData
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListDataChildInfo struct {
|
||||||
|
Subscribed bool
|
||||||
|
}
|
||||||
14
namespace.go
Normal file
14
namespace.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package imap
|
||||||
|
|
||||||
|
// NamespaceData is the data returned by the NAMESPACE command.
|
||||||
|
type NamespaceData struct {
|
||||||
|
Personal []NamespaceDescriptor
|
||||||
|
Other []NamespaceDescriptor
|
||||||
|
Shared []NamespaceDescriptor
|
||||||
|
}
|
||||||
|
|
||||||
|
// NamespaceDescriptor describes a namespace.
|
||||||
|
type NamespaceDescriptor struct {
|
||||||
|
Prefix string
|
||||||
|
Delim rune
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user