mirror of
https://github.com/mjl-/mox.git
synced 2024-12-26 16:33:47 +03:00
4dea2de343
we only have a "storage" limit. for total disk usage. we don't have a limit on messages (count) or mailboxes (count). also not on total annotation size, but we don't have support annotations at all at the moment. we don't implement setquota. with rfc 9208 that's allowed. with the previous quota rfc 2087 it wasn't. the status command can now return "DELETED-STORAGE". which should be the disk space that can be reclaimed by removing messages with the \Deleted flags. however, it's not very likely clients set the \Deleted flag without expunging the message immediately. we don't want to go through all messages to calculate the sum of message sizes with the deleted flag. we also don't currently track that in MailboxCount. so we just respond with "0". not compliant, but let's wait until someone complains. when returning quota information, it is not possible to give the current usage when no limit is configured. clients implementing rfc 9208 should probably conclude from the presence of QUOTA=RES-* capabilities (only in rfc 9208, not in 2087) and the absence of those limits in quota responses (or the absence of an untagged quota response at all) that a resource type doesn't have a limit. thunderbird will claim there is no quota information when no limit was configured, so we can probably conclude that it implements rfc 2087, but not rfc 9208. we now also show the usage & limit on the account page. for issue #115 by pmarini
523 lines
12 KiB
Go
523 lines
12 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[string]int64 // Upper case status attributes. ../rfc/9051:7059
|
|
}
|
|
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
|
|
}
|
|
|
|
// BodyTypeBasic represents basic information about a part, used in a FETCH response.
|
|
type BodyTypeBasic struct {
|
|
// ../rfc/9051:6407
|
|
MediaType, MediaSubtype string
|
|
BodyFields BodyFields
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// "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" }
|