mox/imapclient/protocol.go
Mechiel Lukkien 507ca73b96
imapserver: implement UIDONLY extension, RFC 9586
Once clients enable this extension, commands can no longer refer to "message
sequence numbers" (MSNs), but can only refer to messages with UIDs. This means
both sides no longer have to carefully keep their sequence numbers in sync
(error-prone), and don't have to keep track of a mapping of sequence numbers to
UIDs (saves resources).

With UIDONLY enabled, all FETCH responses are replaced with UIDFETCH response.
2025-04-11 11:45:49 +02:00

687 lines
17 KiB
Go

package imapclient
import (
"bufio"
"fmt"
"strings"
"time"
)
// Capability is a known string for with the ENABLED and CAPABILITY command.
type Capability string
const (
CapIMAP4rev1 Capability = "IMAP4rev1"
CapIMAP4rev2 Capability = "IMAP4rev2"
CapLoginDisabled Capability = "LOGINDISABLED"
CapStarttls Capability = "STARTTLS"
CapAuthPlain Capability = "AUTH=PLAIN"
CapLiteralPlus Capability = "LITERAL+"
CapLiteralMinus Capability = "LITERAL-"
CapIdle Capability = "IDLE"
CapNamespace Capability = "NAMESPACE"
CapBinary Capability = "BINARY"
CapUnselect Capability = "UNSELECT"
CapUidplus Capability = "UIDPLUS"
CapEsearch Capability = "ESEARCH"
CapEnable Capability = "ENABLE"
CapSave Capability = "SAVE"
CapListExtended Capability = "LIST-EXTENDED"
CapSpecialUse Capability = "SPECIAL-USE"
CapMove Capability = "MOVE"
CapUTF8Only Capability = "UTF8=ONLY"
CapUTF8Accept Capability = "UTF8=ACCEPT"
CapID Capability = "ID" // ../rfc/2971:80
CapMetadata Capability = "METADATA" // ../rfc/5464:124
CapMetadataServer Capability = "METADATA-SERVER" // ../rfc/5464:124
CapSaveDate Capability = "SAVEDATE" // ../rfc/8514
CapCreateSpecialUse Capability = "CREATE-SPECIAL-USE" // ../rfc/6154:296
CapCompressDeflate Capability = "COMPRESS=DEFLATE" // ../rfc/4978:65
CapListMetadata Capability = "LIST-METADTA" // ../rfc/9590:73
CapMultiAppend Capability = "MULTIAPPEND" // ../rfc/3502:33
CapReplace Capability = "REPLACE" // ../rfc/8508:155
CapPreview Capability = "PREVIEW" // ../rfc/8970:114
CapMultiSearch Capability = "MULTISEARCH" // ../rfc/7377:187
CapNotify Capability = "NOTIFY" // ../rfc/5465:195
CapUIDOnly Capability = "UIDONLY" // ../rfc/9586:129
)
// Status is the tagged final result of a command.
type Status string
const (
BAD Status = "BAD" // Syntax error.
NO Status = "NO" // Command failed.
OK Status = "OK" // Command succeeded.
)
// Result is the final response for a command, indicating success or failure.
type Result struct {
Status Status
RespText
}
// CodeArg represents a response code with arguments, i.e. the data between [] in the response line.
type CodeArg interface {
CodeString() string
}
// CodeOther is a valid but unrecognized response code.
type CodeOther struct {
Code string
Args []string
}
func (c CodeOther) CodeString() string {
return c.Code + " " + strings.Join(c.Args, " ")
}
// CodeWords is a code with space-separated string parameters. E.g. CAPABILITY.
type CodeWords struct {
Code string
Args []string
}
func (c CodeWords) CodeString() string {
s := c.Code
for _, w := range c.Args {
s += " " + w
}
return s
}
// CodeList is a code with a list with space-separated strings as parameters. E.g. BADCHARSET, PERMANENTFLAGS.
type CodeList struct {
Code string
Args []string // If nil, no list was present. List can also be empty.
}
func (c CodeList) CodeString() string {
s := c.Code
if c.Args == nil {
return s
}
return s + "(" + strings.Join(c.Args, " ") + ")"
}
// CodeUint is a code with a uint32 parameter, e.g. UIDNEXT and UIDVALIDITY.
type CodeUint struct {
Code string
Num uint32
}
func (c CodeUint) CodeString() string {
return fmt.Sprintf("%s %d", c.Code, c.Num)
}
// "APPENDUID" response code.
type CodeAppendUID struct {
UIDValidity uint32
UIDs NumRange
}
func (c CodeAppendUID) CodeString() string {
return fmt.Sprintf("APPENDUID %d %s", c.UIDValidity, c.UIDs.String())
}
// "COPYUID" response code.
type CodeCopyUID struct {
DestUIDValidity uint32
From []NumRange
To []NumRange
}
func (c CodeCopyUID) CodeString() string {
str := func(l []NumRange) string {
s := ""
for i, e := range l {
if i > 0 {
s += ","
}
s += fmt.Sprintf("%d", e.First)
if e.Last != nil {
s += fmt.Sprintf(":%d", *e.Last)
}
}
return s
}
return fmt.Sprintf("COPYUID %d %s %s", c.DestUIDValidity, str(c.From), str(c.To))
}
// For CONDSTORE.
type CodeModified NumSet
func (c CodeModified) CodeString() string {
return fmt.Sprintf("MODIFIED %s", NumSet(c).String())
}
// For CONDSTORE.
type CodeHighestModSeq int64
func (c CodeHighestModSeq) CodeString() string {
return fmt.Sprintf("HIGHESTMODSEQ %d", c)
}
// "INPROGRESS" response code.
type CodeInProgress struct {
Tag string // Nil is empty string.
Current *uint32
Goal *uint32
}
func (c CodeInProgress) CodeString() string {
// ABNF allows inprogress-tag/state with all nil values. Doesn't seem useful enough
// to keep track of.
if c.Tag == "" && c.Current == nil && c.Goal == nil {
return "INPROGRESS"
}
// todo: quote tag properly
current := "nil"
goal := "nil"
if c.Current != nil {
current = fmt.Sprintf("%d", *c.Current)
}
if c.Goal != nil {
goal = fmt.Sprintf("%d", *c.Goal)
}
return fmt.Sprintf("INPROGRESS (%q %s %s)", c.Tag, current, goal)
}
// "BADEVENT" response code, with the events that are supported, for the NOTIFY
// extension.
type CodeBadEvent []string
func (c CodeBadEvent) CodeString() string {
return fmt.Sprintf("BADEVENT (%s)", strings.Join([]string(c), " "))
}
// RespText represents a response line minus the leading tag.
type RespText struct {
Code string // The first word between [] after the status.
CodeArg CodeArg // Set if code has a parameter.
More string // Any remaining text.
}
// atom or string.
func astring(s string) string {
if len(s) == 0 {
return stringx(s)
}
for _, c := range s {
if c <= ' ' || c >= 0x7f || c == '(' || c == ')' || c == '{' || c == '%' || c == '*' || c == '"' || c == '\\' {
return stringx(s)
}
}
return s
}
// imap "string", i.e. double-quoted string or syncliteral.
func stringx(s string) string {
r := `"`
for _, c := range s {
if c == '\x00' || c == '\r' || c == '\n' {
return syncliteral(s)
}
if c == '\\' || c == '"' {
r += `\`
}
r += string(c)
}
r += `"`
return r
}
// sync literal, i.e. {<num>}\r\n<num bytes>.
func syncliteral(s string) string {
return fmt.Sprintf("{%d}\r\n", len(s)) + s
}
// Untagged is a parsed untagged response. See types starting with Untagged.
// todo: make an interface that the untagged responses implement?
type Untagged any
type UntaggedBye RespText
type UntaggedPreauth RespText
type UntaggedExpunge uint32
type UntaggedExists uint32
type UntaggedRecent uint32
type UntaggedCapability []string
type UntaggedEnabled []string
type UntaggedResult Result
type UntaggedFlags []string
type UntaggedList struct {
// ../rfc/9051:6690
Flags []string
Separator byte // 0 for NIL
Mailbox string
Extended []MboxListExtendedItem
OldName string // If present, taken out of Extended.
}
type UntaggedFetch struct {
Seq uint32
Attrs []FetchAttr
}
// UntaggedUIDFetch is like UntaggedFetch, but with UIDs instead of message
// sequence numbers, and returned instead of regular fetch responses when UIDONLY
// is enabled.
type UntaggedUIDFetch struct {
UID uint32
Attrs []FetchAttr
}
type UntaggedSearch []uint32
// ../rfc/7162:1101
type UntaggedSearchModSeq struct {
Nums []uint32
ModSeq int64
}
type UntaggedStatus struct {
Mailbox string
Attrs map[StatusAttr]int64 // Upper case status attributes.
}
// ../rfc/5464:716 Unsolicited response, indicating an annotation has changed.
type UntaggedMetadataKeys struct {
Mailbox string // Empty means not specific to mailbox.
// Keys that have changed. To get values (or determine absence), the server must be
// queried.
Keys []string
}
// Annotation is a metadata server of mailbox annotation.
type Annotation struct {
Key string
// Nil is represented by IsString false and a nil Value.
IsString bool
Value []byte
}
// ../rfc/5464:683
type UntaggedMetadataAnnotations struct {
Mailbox string // Empty means not specific to mailbox.
Annotations []Annotation
}
// ../rfc/9051:7059 ../9208:712
type StatusAttr string
const (
StatusMessages StatusAttr = "MESSAGES"
StatusUIDNext StatusAttr = "UIDNEXT"
StatusUIDValidity StatusAttr = "UIDVALIDITY"
StatusUnseen StatusAttr = "UNSEEN"
StatusDeleted StatusAttr = "DELETED"
StatusSize StatusAttr = "SIZE"
StatusRecent StatusAttr = "RECENT"
StatusAppendLimit StatusAttr = "APPENDLIMIT"
StatusHighestModSeq StatusAttr = "HIGHESTMODSEQ"
StatusDeletedStorage StatusAttr = "DELETED-STORAGE"
)
type UntaggedNamespace struct {
Personal, Other, Shared []NamespaceDescr
}
type UntaggedLsub struct {
// ../rfc/3501:4833
Flags []string
Separator byte
Mailbox string
}
// Fields are optional and zero if absent.
type UntaggedEsearch struct {
Tag string // ../rfc/9051:6546
Mailbox string // For MULTISEARCH. ../rfc/7377:437
UIDValidity uint32 // For MULTISEARCH, ../rfc/7377:438
UID bool
Min uint32
Max uint32
All NumSet
Count *uint32
ModSeq int64
Exts []EsearchDataExt
}
// UntaggedVanished is used in QRESYNC to send UIDs that have been removed.
type UntaggedVanished struct {
Earlier bool
UIDs NumSet
}
// UntaggedQuotaroot lists the roots for which quota can be present.
type UntaggedQuotaroot []string
// UntaggedQuota holds the quota for a quota root.
type UntaggedQuota struct {
Root string
// Always has at least one. Any QUOTA=RES-* capability not mentioned has no limit
// or this quota root.
Resources []QuotaResource
}
// Resource types ../rfc/9208:533
// QuotaResourceName is the name of a resource type. More can be defined in the
// future and encountered in the wild. Always in upper case.
type QuotaResourceName string
const (
QuotaResourceStorage = "STORAGE"
QuotaResourceMesssage = "MESSAGE"
QuotaResourceMailbox = "MAILBOX"
QuotaResourceAnnotationStorage = "ANNOTATION-STORAGE"
)
type QuotaResource struct {
Name QuotaResourceName
Usage int64 // Currently in use. Count or disk size in 1024 byte blocks.
Limit int64 // Maximum allowed usage.
}
// ../rfc/2971:184
type UntaggedID map[string]string
// Extended data in an ESEARCH response.
type EsearchDataExt struct {
Tag string
Value TaggedExtVal
}
type NamespaceDescr struct {
// ../rfc/9051:6769
Prefix string
Separator byte // If 0 then separator was absent.
Exts []NamespaceExtension
}
type NamespaceExtension struct {
// ../rfc/9051:6773
Key string
Values []string
}
// FetchAttr represents a FETCH response attribute.
type FetchAttr interface {
Attr() string // Name of attribute.
}
type NumSet struct {
SearchResult bool // True if "$", in which case Ranges is irrelevant.
Ranges []NumRange
}
func (ns NumSet) IsZero() bool {
return !ns.SearchResult && ns.Ranges == nil
}
func (ns NumSet) String() string {
if ns.SearchResult {
return "$"
}
var r string
for i, x := range ns.Ranges {
if i > 0 {
r += ","
}
r += x.String()
}
return r
}
func ParseNumSet(s string) (ns NumSet, rerr error) {
c := Conn{br: bufio.NewReader(strings.NewReader(s))}
defer c.recover(&rerr)
ns = c.xsequenceSet()
return
}
func ParseUIDRange(s string) (nr NumRange, rerr error) {
c := Conn{br: bufio.NewReader(strings.NewReader(s))}
defer c.recover(&rerr)
nr = c.xuidrange()
return
}
// NumRange is a single number or range.
type NumRange struct {
First uint32 // 0 for "*".
Last *uint32 // Nil if absent, 0 for "*".
}
func (nr NumRange) String() string {
var r string
if nr.First == 0 {
r += "*"
} else {
r += fmt.Sprintf("%d", nr.First)
}
if nr.Last == nil {
return r
}
r += ":"
v := *nr.Last
if v == 0 {
r += "*"
} else {
r += fmt.Sprintf("%d", v)
}
return r
}
type TaggedExtComp struct {
String string
Comps []TaggedExtComp // Used for both space-separated and ().
}
type TaggedExtVal struct {
// ../rfc/9051:7111
Number *int64
SeqSet *NumSet
Comp *TaggedExtComp // If SimpleNumber and SimpleSeqSet is nil, this is a Comp. But Comp is optional and can also be nil. Not great.
}
type MboxListExtendedItem struct {
// ../rfc/9051:6699
Tag string
Val TaggedExtVal
}
// "FLAGS" fetch response.
type FetchFlags []string
func (f FetchFlags) Attr() string { return "FLAGS" }
// "ENVELOPE" fetch response.
type FetchEnvelope Envelope
func (f FetchEnvelope) Attr() string { return "ENVELOPE" }
// Envelope holds the basic email message fields.
type Envelope struct {
Date string
Subject string
From, Sender, ReplyTo, To, CC, BCC []Address
InReplyTo, MessageID string
}
// Address is an address field in an email message, e.g. To.
type Address struct {
Name, Adl, Mailbox, Host string
}
// "INTERNALDATE" fetch response.
type FetchInternalDate struct {
Date time.Time
}
func (f FetchInternalDate) Attr() string { return "INTERNALDATE" }
// "SAVEDATE" fetch response. ../rfc/8514:265
type FetchSaveDate struct {
SaveDate *time.Time // nil means absent for message.
}
func (f FetchSaveDate) Attr() string { return "SAVEDATE" }
// "RFC822.SIZE" fetch response.
type FetchRFC822Size int64
func (f FetchRFC822Size) Attr() string { return "RFC822.SIZE" }
// "RFC822" fetch response.
type FetchRFC822 string
func (f FetchRFC822) Attr() string { return "RFC822" }
// "RFC822.HEADER" fetch response.
type FetchRFC822Header string
func (f FetchRFC822Header) Attr() string { return "RFC822.HEADER" }
// "RFC82.TEXT" fetch response.
type FetchRFC822Text string
func (f FetchRFC822Text) Attr() string { return "RFC822.TEXT" }
// "BODYSTRUCTURE" fetch response.
type FetchBodystructure struct {
// ../rfc/9051:6355
RespAttr string
Body any // BodyType*
}
func (f FetchBodystructure) Attr() string { return f.RespAttr }
// "BODY" fetch response.
type FetchBody struct {
// ../rfc/9051:6756 ../rfc/9051:6985
RespAttr string
Section string // todo: parse more ../rfc/9051:6985
Offset int32
Body string
}
func (f FetchBody) Attr() string { return f.RespAttr }
// BodyFields is part of a FETCH BODY[] response.
type BodyFields struct {
Params [][2]string
ContentID, ContentDescr, CTE string
Octets int32
}
// BodyTypeMpart represents the body structure a multipart message, with
// subparts and the multipart media subtype. Used in a FETCH response.
type BodyTypeMpart struct {
// ../rfc/9051:6411
Bodies []any // BodyTypeBasic, BodyTypeMsg, BodyTypeText
MediaSubtype string
Ext *BodyExtensionMpart
}
// BodyTypeBasic represents basic information about a part, used in a FETCH
// response.
type BodyTypeBasic struct {
// ../rfc/9051:6407
MediaType, MediaSubtype string
BodyFields BodyFields
Ext *BodyExtension1Part
}
// BodyTypeMsg represents an email message as a body structure, used in a FETCH
// response.
type BodyTypeMsg struct {
// ../rfc/9051:6415
MediaType, MediaSubtype string
BodyFields BodyFields
Envelope Envelope
Bodystructure any // One of the BodyType*
Lines int64
Ext *BodyExtension1Part
}
// BodyTypeText represents a text part as a body structure, used in a FETCH
// response.
type BodyTypeText struct {
// ../rfc/9051:6418
MediaType, MediaSubtype string
BodyFields BodyFields
Lines int64
Ext *BodyExtension1Part
}
// BodyExtension1Part has the extensible form fields of a BODYSTRUCTURE for
// multiparts.
type BodyExtensionMpart struct {
// ../rfc/9051:5986 ../rfc/3501:4161 ../rfc/9051:6371 ../rfc/3501:4599
Params [][2]string
Disposition string
DispositionParams [][2]string
Language []string
Location string
More []BodyExtension
}
// BodyExtension1Part has the extensible form fields of a BODYSTRUCTURE for
// non-multiparts.
type BodyExtension1Part struct {
// ../rfc/9051:6023 ../rfc/3501:4191 ../rfc/9051:6366 ../rfc/3501:4584
MD5 string
Disposition string
DispositionParams [][2]string
Language []string
Location string
More []BodyExtension
}
// BodyExtension has the additional extension fields for future expansion of
// extensions.
type BodyExtension struct {
String *string
Number *int64
More []BodyExtension
}
// "BINARY" fetch response.
type FetchBinary struct {
RespAttr string
Parts []uint32 // Can be nil.
Data string
}
func (f FetchBinary) Attr() string { return f.RespAttr }
// "BINARY.SIZE" fetch response.
type FetchBinarySize struct {
RespAttr string
Parts []uint32
Size int64
}
func (f FetchBinarySize) Attr() string { return f.RespAttr }
// "UID" fetch response.
type FetchUID uint32
func (f FetchUID) Attr() string { return "UID" }
// "MODSEQ" fetch response.
type FetchModSeq int64
func (f FetchModSeq) Attr() string { return "MODSEQ" }
// "PREVIEW" fetch response.
type FetchPreview struct {
Preview *string
}
// ../rfc/8970:146
func (f FetchPreview) Attr() string { return "PREVIEW" }