mirror of
https://github.com/mjl-/mox.git
synced 2024-12-27 08:53:48 +03:00
5d97bf198a
not generating it yet from imapserver because we don't have content-md5 available. we could send "nil" instead of any actual content-md5 header (and probably no contemporary messages include a content-md5 header), but it would not be correct. if no known clients have problems in practice with absent extensible data, it's better to just leave the bodystructure as is, with extensible data. for issue #217 by danieleggert
580 lines
14 KiB
Go
580 lines
14 KiB
Go
package imapclient
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// 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
|
|
)
|
|
|
|
// 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
|
|
UID uint32
|
|
}
|
|
|
|
func (c CodeAppendUID) CodeString() string {
|
|
return fmt.Sprintf("APPENDUID %d %d", c.UIDValidity, c.UID)
|
|
}
|
|
|
|
// "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)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
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/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 {
|
|
// ../rfc/9051:6546
|
|
Correlator string
|
|
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{r: bufio.NewReader(strings.NewReader(s))}
|
|
defer c.recover(&rerr)
|
|
ns = c.xsequenceSet()
|
|
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 string // todo: parsed time
|
|
func (f FetchInternalDate) Attr() string { return "INTERNALDATE" }
|
|
|
|
// "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" }
|