Forked the emersion/go-imap v1 project.
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
|
||||
}
|
||||
213
capability.go
Normal file
213
capability.go
Normal file
@@ -0,0 +1,213 @@
|
||||
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
|
||||
|
||||
CapAuthPlain Cap = "AUTH=PLAIN"
|
||||
|
||||
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
|
||||
|
||||
CapACL Cap = "ACL" // RFC 4314
|
||||
CapAppendLimit Cap = "APPENDLIMIT" // RFC 7889
|
||||
CapBinary Cap = "BINARY" // RFC 3516
|
||||
CapCatenate Cap = "CATENATE" // RFC 4469
|
||||
CapChildren Cap = "CHILDREN" // RFC 3348
|
||||
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: {},
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
81
cmd/imapmemserver/main.go
Normal file
81
cmd/imapmemserver/main.go
Normal file
@@ -0,0 +1,81 @@
|
||||
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)
|
||||
user.Create("INBOX", nil)
|
||||
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.1
|
||||
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43
|
||||
)
|
||||
35
go.sum
Normal file
35
go.sum
Normal file
@@ -0,0 +1,35 @@
|
||||
github.com/emersion/go-message v0.18.1 h1:tfTxIoXFSFRwWaZsgnqS1DSZuGpYGzSmCZD8SK3QA2E=
|
||||
github.com/emersion/go-message v0.18.1/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA=
|
||||
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 h1:hH4PQfOndHDlpzYfLAAfl63E8Le6F2+EL/cdhlkyRJY=
|
||||
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43/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.Errorf("SetACL().Wait() error: %v", err)
|
||||
}
|
||||
|
||||
// execute GETACL command to reset cache on server
|
||||
getACLData, err := client.GetACL(tc.mailbox).Wait()
|
||||
if err != nil {
|
||||
t.Errorf("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)
|
||||
}
|
||||
}
|
||||
55
imapclient/capability.go
Normal file
55
imapclient/capability.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package imapclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"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() {
|
||||
}
|
||||
|
||||
var name string
|
||||
if !dec.ExpectAtom(&name) {
|
||||
return caps, fmt.Errorf("in capability-data: %v", dec.Err())
|
||||
}
|
||||
caps[imap.Cap(name)] = struct{}{}
|
||||
}
|
||||
return caps, nil
|
||||
}
|
||||
1215
imapclient/client.go
Normal file
1215
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/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)
|
||||
})
|
||||
}
|
||||
64
imapclient/dovecot_test.go
Normal file
64
imapclient/dovecot_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
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 := `log_path = "` + tempDir + `/dovecot.log"
|
||||
ssl = no
|
||||
mail_home = "` + tempDir + `/%u"
|
||||
mail_location = maildir:~/Mail
|
||||
|
||||
namespace inbox {
|
||||
separator = /
|
||||
prefix =
|
||||
inbox = yes
|
||||
}
|
||||
|
||||
mail_plugins = $mail_plugins acl
|
||||
protocol imap {
|
||||
mail_plugins = $mail_plugins imap_acl
|
||||
}
|
||||
plugin {
|
||||
acl = 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
|
||||
}
|
||||
365
imapclient/example_test.go
Normal file
365
imapclient/example_test.go
Normal file
@@ -0,0 +1,365 @@
|
||||
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)
|
||||
}
|
||||
|
||||
// Wait for 30s to receive updates from the server
|
||||
time.Sleep(30 * time.Second)
|
||||
|
||||
// Stop idling
|
||||
if err := idleCmd.Close(); err != nil {
|
||||
log.Fatalf("failed to stop idling: %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)
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
data.UID = name == "UID"
|
||||
|
||||
if data.UID {
|
||||
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 data.UID {
|
||||
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.PermanentFlags = 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
|
||||
}
|
||||
120
imapserver/append.go
Normal file
120
imapserver/append.go
Normal file
@@ -0,0 +1,120 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// appendLimit is the maximum size of an APPEND payload.
|
||||
//
|
||||
// TODO: make configurable
|
||||
const appendLimit = 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
95
imapserver/capability.go
Normal file
95
imapserver/capability.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package imapserver
|
||||
|
||||
import (
|
||||
"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) {
|
||||
caps = append(caps, []imap.Cap{
|
||||
imap.CapUnselect,
|
||||
imap.CapEnable,
|
||||
imap.CapIdle,
|
||||
imap.CapUTF8Accept,
|
||||
}...)
|
||||
addAvailableCaps(&caps, available, []imap.Cap{
|
||||
imap.CapNamespace,
|
||||
imap.CapUIDPlus,
|
||||
imap.CapESearch,
|
||||
imap.CapSearchRes,
|
||||
imap.CapListExtended,
|
||||
imap.CapListStatus,
|
||||
imap.CapMove,
|
||||
imap.CapStatusSize,
|
||||
imap.CapBinary,
|
||||
})
|
||||
}
|
||||
addAvailableCaps(&caps, available, []imap.Cap{
|
||||
imap.CapCreateSpecialUse,
|
||||
imap.CapLiteralPlus,
|
||||
imap.CapUnauthenticate,
|
||||
})
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
617
imapserver/conn.go
Normal file
617
imapserver/conn.go
Normal file
@@ -0,0 +1,617 @@
|
||||
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
|
||||
}
|
||||
return c.session.Rename(oldName, newName)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
46
imapserver/enable.go
Normal file
46
imapserver/enable.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package imapserver
|
||||
|
||||
import (
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/internal/imapwire"
|
||||
)
|
||||
|
||||
func (c *Conn) handleEnable(dec *imapwire.Decoder) error {
|
||||
var requested []imap.Cap
|
||||
for dec.SP() {
|
||||
var c string
|
||||
if !dec.ExpectAtom(&c) {
|
||||
return dec.Err()
|
||||
}
|
||||
requested = append(requested, imap.Cap(c))
|
||||
}
|
||||
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)
|
||||
}
|
||||
711
imapserver/fetch.go
Normal file
711
imapserver/fetch.go
Normal file
@@ -0,0 +1,711 @@
|
||||
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.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 {
|
||||
enc.String(strings.ToUpper(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 i, child := range bs.Children {
|
||||
if i > 0 {
|
||||
enc.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
|
||||
}
|
||||
486
imapserver/imapmemserver/mailbox.go
Normal file
486
imapserver/imapmemserver/mailbox.go
Normal file
@@ -0,0 +1,486 @@
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
data := imap.ListData{
|
||||
Mailbox: mbox.name,
|
||||
Delim: mailboxDelim,
|
||||
}
|
||||
if mbox.subscribed {
|
||||
data.Attrs = append(data.Attrs, imap.MailboxAttrSubscribed)
|
||||
}
|
||||
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
|
||||
}
|
||||
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)
|
||||
|
||||
return &imap.SelectData{
|
||||
Flags: flags,
|
||||
PermanentFlags: permanentFlags,
|
||||
NumMessages: uint32(len(mbox.l)),
|
||||
UIDNext: mbox.uidNext,
|
||||
UIDValidity: mbox.uidValidity,
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
data := imap.SearchData{UID: numKind == imapserver.NumKindUID}
|
||||
|
||||
var (
|
||||
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) 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
|
||||
}
|
||||
325
imapserver/list.go
Normal file
325
imapserver/list.go
Normal file
@@ -0,0 +1,325 @@
|
||||
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
|
||||
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 "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)
|
||||
} else {
|
||||
return c.writeSearch(data.All)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) writeESearch(tag string, data *imap.SearchData, options *imap.SearchOptions) error {
|
||||
enc := newResponseEncoder(c)
|
||||
defer enc.end()
|
||||
|
||||
enc.Atom("*").SP().Atom("ESEARCH")
|
||||
if tag != "" {
|
||||
enc.SP().Special('(').Atom("TAG").SP().Atom(tag).Special(')')
|
||||
}
|
||||
if data.UID {
|
||||
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.Flag, 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 nil
|
||||
}
|
||||
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 nil
|
||||
}
|
||||
if !dec.ExpectSP() {
|
||||
return dec.Err()
|
||||
}
|
||||
if err := readSearchKey(&or[1], dec); err != nil {
|
||||
return nil
|
||||
}
|
||||
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)))
|
||||
}
|
||||
160
imapserver/select.go
Normal file
160
imapserver/select.go
Normal file
@@ -0,0 +1,160 @@
|
||||
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) {
|
||||
if err := c.writeObsoleteRecent(data.NumRecent); 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) 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
|
||||
}
|
||||
116
imapserver/session.go
Normal file
116
imapserver/session.go
Normal file
@@ -0,0 +1,116 @@
|
||||
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) 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
|
||||
}
|
||||
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
|
||||
}
|
||||
170
internal/internal.go
Normal file
170
internal/internal.go
Normal file
@@ -0,0 +1,170 @@
|
||||
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 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)
|
||||
}
|
||||
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
|
||||
}
|
||||
149
numset.go
Normal file
149
numset.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package imap
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/emersion/go-imap/v2/internal/imapnum"
|
||||
)
|
||||
|
||||
// NumSet is a set of numbers identifying messages. NumSet is either a SeqSet
|
||||
// or a UIDSet.
|
||||
type NumSet interface {
|
||||
// String returns the IMAP representation of the message number set.
|
||||
String() string
|
||||
// Dynamic returns true if the set contains "*" or "n:*" ranges or if the
|
||||
// set represents the special SEARCHRES marker.
|
||||
Dynamic() bool
|
||||
|
||||
numSet() imapnum.Set
|
||||
}
|
||||
|
||||
var (
|
||||
_ NumSet = SeqSet(nil)
|
||||
_ NumSet = UIDSet(nil)
|
||||
)
|
||||
|
||||
// SeqSet is a set of message sequence numbers.
|
||||
type SeqSet []SeqRange
|
||||
|
||||
// SeqSetNum returns a new SeqSet containing the specified sequence numbers.
|
||||
func SeqSetNum(nums ...uint32) SeqSet {
|
||||
var s SeqSet
|
||||
s.AddNum(nums...)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *SeqSet) numSetPtr() *imapnum.Set {
|
||||
return (*imapnum.Set)(unsafe.Pointer(s))
|
||||
}
|
||||
|
||||
func (s SeqSet) numSet() imapnum.Set {
|
||||
return *s.numSetPtr()
|
||||
}
|
||||
|
||||
func (s SeqSet) String() string {
|
||||
return s.numSet().String()
|
||||
}
|
||||
|
||||
func (s SeqSet) Dynamic() bool {
|
||||
return s.numSet().Dynamic()
|
||||
}
|
||||
|
||||
// Contains returns true if the non-zero sequence number num is contained in
|
||||
// the set.
|
||||
func (s *SeqSet) Contains(num uint32) bool {
|
||||
return s.numSet().Contains(num)
|
||||
}
|
||||
|
||||
// Nums returns a slice of all sequence numbers contained in the set.
|
||||
func (s *SeqSet) Nums() ([]uint32, bool) {
|
||||
return s.numSet().Nums()
|
||||
}
|
||||
|
||||
// AddNum inserts new sequence numbers into the set. The value 0 represents "*".
|
||||
func (s *SeqSet) AddNum(nums ...uint32) {
|
||||
s.numSetPtr().AddNum(nums...)
|
||||
}
|
||||
|
||||
// AddRange inserts a new range into the set.
|
||||
func (s *SeqSet) AddRange(start, stop uint32) {
|
||||
s.numSetPtr().AddRange(start, stop)
|
||||
}
|
||||
|
||||
// AddSet inserts all sequence numbers from other into s.
|
||||
func (s *SeqSet) AddSet(other SeqSet) {
|
||||
s.numSetPtr().AddSet(other.numSet())
|
||||
}
|
||||
|
||||
// SeqRange is a range of message sequence numbers.
|
||||
type SeqRange struct {
|
||||
Start, Stop uint32
|
||||
}
|
||||
|
||||
// UIDSet is a set of message UIDs.
|
||||
type UIDSet []UIDRange
|
||||
|
||||
// UIDSetNum returns a new UIDSet containing the specified UIDs.
|
||||
func UIDSetNum(uids ...UID) UIDSet {
|
||||
var s UIDSet
|
||||
s.AddNum(uids...)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *UIDSet) numSetPtr() *imapnum.Set {
|
||||
return (*imapnum.Set)(unsafe.Pointer(s))
|
||||
}
|
||||
|
||||
func (s UIDSet) numSet() imapnum.Set {
|
||||
return *s.numSetPtr()
|
||||
}
|
||||
|
||||
func (s UIDSet) String() string {
|
||||
if IsSearchRes(s) {
|
||||
return "$"
|
||||
}
|
||||
return s.numSet().String()
|
||||
}
|
||||
|
||||
func (s UIDSet) Dynamic() bool {
|
||||
return s.numSet().Dynamic() || IsSearchRes(s)
|
||||
}
|
||||
|
||||
// Contains returns true if the non-zero UID uid is contained in the set.
|
||||
func (s UIDSet) Contains(uid UID) bool {
|
||||
return s.numSet().Contains(uint32(uid))
|
||||
}
|
||||
|
||||
// Nums returns a slice of all UIDs contained in the set.
|
||||
func (s UIDSet) Nums() ([]UID, bool) {
|
||||
nums, ok := s.numSet().Nums()
|
||||
return uidListFromNumList(nums), ok
|
||||
}
|
||||
|
||||
// AddNum inserts new UIDs into the set. The value 0 represents "*".
|
||||
func (s *UIDSet) AddNum(uids ...UID) {
|
||||
s.numSetPtr().AddNum(numListFromUIDList(uids)...)
|
||||
}
|
||||
|
||||
// AddRange inserts a new range into the set.
|
||||
func (s *UIDSet) AddRange(start, stop UID) {
|
||||
s.numSetPtr().AddRange(uint32(start), uint32(stop))
|
||||
}
|
||||
|
||||
// AddSet inserts all UIDs from other into s.
|
||||
func (s *UIDSet) AddSet(other UIDSet) {
|
||||
s.numSetPtr().AddSet(other.numSet())
|
||||
}
|
||||
|
||||
// UIDRange is a range of message UIDs.
|
||||
type UIDRange struct {
|
||||
Start, Stop UID
|
||||
}
|
||||
|
||||
func numListFromUIDList(uids []UID) []uint32 {
|
||||
return *(*[]uint32)(unsafe.Pointer(&uids))
|
||||
}
|
||||
|
||||
func uidListFromNumList(nums []uint32) []UID {
|
||||
return *(*[]UID)(unsafe.Pointer(&nums))
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user