1package imapclient
2
3import (
4 "bufio"
5 "fmt"
6 "strings"
7 "time"
8)
9
10// Capability is a known string for with the ENABLED and CAPABILITY command.
11type Capability string
12
13const (
14 CapIMAP4rev1 Capability = "IMAP4rev1"
15 CapIMAP4rev2 Capability = "IMAP4rev2"
16 CapLoginDisabled Capability = "LOGINDISABLED"
17 CapStarttls Capability = "STARTTLS"
18 CapAuthPlain Capability = "AUTH=PLAIN"
19 CapLiteralPlus Capability = "LITERAL+"
20 CapLiteralMinus Capability = "LITERAL-"
21 CapIdle Capability = "IDLE"
22 CapNamespace Capability = "NAMESPACE"
23 CapBinary Capability = "BINARY"
24 CapUnselect Capability = "UNSELECT"
25 CapUidplus Capability = "UIDPLUS"
26 CapEsearch Capability = "ESEARCH"
27 CapEnable Capability = "ENABLE"
28 CapSave Capability = "SAVE"
29 CapListExtended Capability = "LIST-EXTENDED"
30 CapSpecialUse Capability = "SPECIAL-USE"
31 CapMove Capability = "MOVE"
32 CapUTF8Only Capability = "UTF8=ONLY"
33 CapUTF8Accept Capability = "UTF8=ACCEPT"
34 CapID Capability = "ID" // ../rfc/2971:80
35 CapMetadata Capability = "METADATA" // ../rfc/5464:124
36 CapMetadataServer Capability = "METADATA-SERVER" // ../rfc/5464:124
37 CapSaveDate Capability = "SAVEDATE" // ../rfc/8514
38 CapCreateSpecialUse Capability = "CREATE-SPECIAL-USE" // ../rfc/6154:296
39 CapCompressDeflate Capability = "COMPRESS=DEFLATE" // ../rfc/4978:65
40)
41
42// Status is the tagged final result of a command.
43type Status string
44
45const (
46 BAD Status = "BAD" // Syntax error.
47 NO Status = "NO" // Command failed.
48 OK Status = "OK" // Command succeeded.
49)
50
51// Result is the final response for a command, indicating success or failure.
52type Result struct {
53 Status Status
54 RespText
55}
56
57// CodeArg represents a response code with arguments, i.e. the data between [] in the response line.
58type CodeArg interface {
59 CodeString() string
60}
61
62// CodeOther is a valid but unrecognized response code.
63type CodeOther struct {
64 Code string
65 Args []string
66}
67
68func (c CodeOther) CodeString() string {
69 return c.Code + " " + strings.Join(c.Args, " ")
70}
71
72// CodeWords is a code with space-separated string parameters. E.g. CAPABILITY.
73type CodeWords struct {
74 Code string
75 Args []string
76}
77
78func (c CodeWords) CodeString() string {
79 s := c.Code
80 for _, w := range c.Args {
81 s += " " + w
82 }
83 return s
84}
85
86// CodeList is a code with a list with space-separated strings as parameters. E.g. BADCHARSET, PERMANENTFLAGS.
87type CodeList struct {
88 Code string
89 Args []string // If nil, no list was present. List can also be empty.
90}
91
92func (c CodeList) CodeString() string {
93 s := c.Code
94 if c.Args == nil {
95 return s
96 }
97 return s + "(" + strings.Join(c.Args, " ") + ")"
98}
99
100// CodeUint is a code with a uint32 parameter, e.g. UIDNEXT and UIDVALIDITY.
101type CodeUint struct {
102 Code string
103 Num uint32
104}
105
106func (c CodeUint) CodeString() string {
107 return fmt.Sprintf("%s %d", c.Code, c.Num)
108}
109
110// "APPENDUID" response code.
111type CodeAppendUID struct {
112 UIDValidity uint32
113 UID uint32
114}
115
116func (c CodeAppendUID) CodeString() string {
117 return fmt.Sprintf("APPENDUID %d %d", c.UIDValidity, c.UID)
118}
119
120// "COPYUID" response code.
121type CodeCopyUID struct {
122 DestUIDValidity uint32
123 From []NumRange
124 To []NumRange
125}
126
127func (c CodeCopyUID) CodeString() string {
128 str := func(l []NumRange) string {
129 s := ""
130 for i, e := range l {
131 if i > 0 {
132 s += ","
133 }
134 s += fmt.Sprintf("%d", e.First)
135 if e.Last != nil {
136 s += fmt.Sprintf(":%d", *e.Last)
137 }
138 }
139 return s
140 }
141 return fmt.Sprintf("COPYUID %d %s %s", c.DestUIDValidity, str(c.From), str(c.To))
142}
143
144// For CONDSTORE.
145type CodeModified NumSet
146
147func (c CodeModified) CodeString() string {
148 return fmt.Sprintf("MODIFIED %s", NumSet(c).String())
149}
150
151// For CONDSTORE.
152type CodeHighestModSeq int64
153
154func (c CodeHighestModSeq) CodeString() string {
155 return fmt.Sprintf("HIGHESTMODSEQ %d", c)
156}
157
158// RespText represents a response line minus the leading tag.
159type RespText struct {
160 Code string // The first word between [] after the status.
161 CodeArg CodeArg // Set if code has a parameter.
162 More string // Any remaining text.
163}
164
165// atom or string.
166func astring(s string) string {
167 if len(s) == 0 {
168 return stringx(s)
169 }
170 for _, c := range s {
171 if c <= ' ' || c >= 0x7f || c == '(' || c == ')' || c == '{' || c == '%' || c == '*' || c == '"' || c == '\\' {
172 return stringx(s)
173 }
174 }
175 return s
176}
177
178// imap "string", i.e. double-quoted string or syncliteral.
179func stringx(s string) string {
180 r := `"`
181 for _, c := range s {
182 if c == '\x00' || c == '\r' || c == '\n' {
183 return syncliteral(s)
184 }
185 if c == '\\' || c == '"' {
186 r += `\`
187 }
188 r += string(c)
189 }
190 r += `"`
191 return r
192}
193
194// sync literal, i.e. {<num>}\r\n<num bytes>.
195func syncliteral(s string) string {
196 return fmt.Sprintf("{%d}\r\n", len(s)) + s
197}
198
199// Untagged is a parsed untagged response. See types starting with Untagged.
200// todo: make an interface that the untagged responses implement?
201type Untagged any
202
203type UntaggedBye RespText
204type UntaggedPreauth RespText
205type UntaggedExpunge uint32
206type UntaggedExists uint32
207type UntaggedRecent uint32
208type UntaggedCapability []string
209type UntaggedEnabled []string
210type UntaggedResult Result
211type UntaggedFlags []string
212type UntaggedList struct {
213 // ../rfc/9051:6690
214 Flags []string
215 Separator byte // 0 for NIL
216 Mailbox string
217 Extended []MboxListExtendedItem
218 OldName string // If present, taken out of Extended.
219}
220type UntaggedFetch struct {
221 Seq uint32
222 Attrs []FetchAttr
223}
224type UntaggedSearch []uint32
225
226// ../rfc/7162:1101
227type UntaggedSearchModSeq struct {
228 Nums []uint32
229 ModSeq int64
230}
231type UntaggedStatus struct {
232 Mailbox string
233 Attrs map[StatusAttr]int64 // Upper case status attributes.
234}
235
236// ../rfc/5464:716 Unsolicited response, indicating an annotation has changed.
237type UntaggedMetadataKeys struct {
238 Mailbox string // Empty means not specific to mailbox.
239
240 // Keys that have changed. To get values (or determine absence), the server must be
241 // queried.
242 Keys []string
243}
244
245type Annotation struct {
246 Key string
247 IsString bool
248 Value []byte
249}
250
251// ../rfc/5464:683
252type UntaggedMetadataAnnotations struct {
253 Mailbox string // Empty means not specific to mailbox.
254 Annotations []Annotation
255}
256
257// ../rfc/9051:7059 ../9208:712
258type StatusAttr string
259
260const (
261 StatusMessages StatusAttr = "MESSAGES"
262 StatusUIDNext StatusAttr = "UIDNEXT"
263 StatusUIDValidity StatusAttr = "UIDVALIDITY"
264 StatusUnseen StatusAttr = "UNSEEN"
265 StatusDeleted StatusAttr = "DELETED"
266 StatusSize StatusAttr = "SIZE"
267 StatusRecent StatusAttr = "RECENT"
268 StatusAppendLimit StatusAttr = "APPENDLIMIT"
269 StatusHighestModSeq StatusAttr = "HIGHESTMODSEQ"
270 StatusDeletedStorage StatusAttr = "DELETED-STORAGE"
271)
272
273type UntaggedNamespace struct {
274 Personal, Other, Shared []NamespaceDescr
275}
276type UntaggedLsub struct {
277 // ../rfc/3501:4833
278 Flags []string
279 Separator byte
280 Mailbox string
281}
282
283// Fields are optional and zero if absent.
284type UntaggedEsearch struct {
285 // ../rfc/9051:6546
286 Correlator string
287 UID bool
288 Min uint32
289 Max uint32
290 All NumSet
291 Count *uint32
292 ModSeq int64
293 Exts []EsearchDataExt
294}
295
296// UntaggedVanished is used in QRESYNC to send UIDs that have been removed.
297type UntaggedVanished struct {
298 Earlier bool
299 UIDs NumSet
300}
301
302// UntaggedQuotaroot lists the roots for which quota can be present.
303type UntaggedQuotaroot []string
304
305// UntaggedQuota holds the quota for a quota root.
306type UntaggedQuota struct {
307 Root string
308
309 // Always has at least one. Any QUOTA=RES-* capability not mentioned has no limit
310 // or this quota root.
311 Resources []QuotaResource
312}
313
314// Resource types ../rfc/9208:533
315
316// QuotaResourceName is the name of a resource type. More can be defined in the
317// future and encountered in the wild. Always in upper case.
318type QuotaResourceName string
319
320const (
321 QuotaResourceStorage = "STORAGE"
322 QuotaResourceMesssage = "MESSAGE"
323 QuotaResourceMailbox = "MAILBOX"
324 QuotaResourceAnnotationStorage = "ANNOTATION-STORAGE"
325)
326
327type QuotaResource struct {
328 Name QuotaResourceName
329 Usage int64 // Currently in use. Count or disk size in 1024 byte blocks.
330 Limit int64 // Maximum allowed usage.
331}
332
333// ../rfc/2971:184
334
335type UntaggedID map[string]string
336
337// Extended data in an ESEARCH response.
338type EsearchDataExt struct {
339 Tag string
340 Value TaggedExtVal
341}
342
343type NamespaceDescr struct {
344 // ../rfc/9051:6769
345 Prefix string
346 Separator byte // If 0 then separator was absent.
347 Exts []NamespaceExtension
348}
349
350type NamespaceExtension struct {
351 // ../rfc/9051:6773
352 Key string
353 Values []string
354}
355
356// FetchAttr represents a FETCH response attribute.
357type FetchAttr interface {
358 Attr() string // Name of attribute.
359}
360
361type NumSet struct {
362 SearchResult bool // True if "$", in which case Ranges is irrelevant.
363 Ranges []NumRange
364}
365
366func (ns NumSet) IsZero() bool {
367 return !ns.SearchResult && ns.Ranges == nil
368}
369
370func (ns NumSet) String() string {
371 if ns.SearchResult {
372 return "$"
373 }
374 var r string
375 for i, x := range ns.Ranges {
376 if i > 0 {
377 r += ","
378 }
379 r += x.String()
380 }
381 return r
382}
383
384func ParseNumSet(s string) (ns NumSet, rerr error) {
385 c := Conn{br: bufio.NewReader(strings.NewReader(s))}
386 defer c.recover(&rerr)
387 ns = c.xsequenceSet()
388 return
389}
390
391// NumRange is a single number or range.
392type NumRange struct {
393 First uint32 // 0 for "*".
394 Last *uint32 // Nil if absent, 0 for "*".
395}
396
397func (nr NumRange) String() string {
398 var r string
399 if nr.First == 0 {
400 r += "*"
401 } else {
402 r += fmt.Sprintf("%d", nr.First)
403 }
404 if nr.Last == nil {
405 return r
406 }
407 r += ":"
408 v := *nr.Last
409 if v == 0 {
410 r += "*"
411 } else {
412 r += fmt.Sprintf("%d", v)
413 }
414 return r
415}
416
417type TaggedExtComp struct {
418 String string
419 Comps []TaggedExtComp // Used for both space-separated and ().
420}
421
422type TaggedExtVal struct {
423 // ../rfc/9051:7111
424 Number *int64
425 SeqSet *NumSet
426 Comp *TaggedExtComp // If SimpleNumber and SimpleSeqSet is nil, this is a Comp. But Comp is optional and can also be nil. Not great.
427}
428
429type MboxListExtendedItem struct {
430 // ../rfc/9051:6699
431 Tag string
432 Val TaggedExtVal
433}
434
435// "FLAGS" fetch response.
436type FetchFlags []string
437
438func (f FetchFlags) Attr() string { return "FLAGS" }
439
440// "ENVELOPE" fetch response.
441type FetchEnvelope Envelope
442
443func (f FetchEnvelope) Attr() string { return "ENVELOPE" }
444
445// Envelope holds the basic email message fields.
446type Envelope struct {
447 Date string
448 Subject string
449 From, Sender, ReplyTo, To, CC, BCC []Address
450 InReplyTo, MessageID string
451}
452
453// Address is an address field in an email message, e.g. To.
454type Address struct {
455 Name, Adl, Mailbox, Host string
456}
457
458// "INTERNALDATE" fetch response.
459type FetchInternalDate struct {
460 Date time.Time
461}
462
463func (f FetchInternalDate) Attr() string { return "INTERNALDATE" }
464
465// "SAVEDATE" fetch response. ../rfc/8514:265
466type FetchSaveDate struct {
467 SaveDate *time.Time // nil means absent for message.
468}
469
470func (f FetchSaveDate) Attr() string { return "SAVEDATE" }
471
472// "RFC822.SIZE" fetch response.
473type FetchRFC822Size int64
474
475func (f FetchRFC822Size) Attr() string { return "RFC822.SIZE" }
476
477// "RFC822" fetch response.
478type FetchRFC822 string
479
480func (f FetchRFC822) Attr() string { return "RFC822" }
481
482// "RFC822.HEADER" fetch response.
483type FetchRFC822Header string
484
485func (f FetchRFC822Header) Attr() string { return "RFC822.HEADER" }
486
487// "RFC82.TEXT" fetch response.
488type FetchRFC822Text string
489
490func (f FetchRFC822Text) Attr() string { return "RFC822.TEXT" }
491
492// "BODYSTRUCTURE" fetch response.
493type FetchBodystructure struct {
494 // ../rfc/9051:6355
495 RespAttr string
496 Body any // BodyType*
497}
498
499func (f FetchBodystructure) Attr() string { return f.RespAttr }
500
501// "BODY" fetch response.
502type FetchBody struct {
503 // ../rfc/9051:6756 ../rfc/9051:6985
504 RespAttr string
505 Section string // todo: parse more ../rfc/9051:6985
506 Offset int32
507 Body string
508}
509
510func (f FetchBody) Attr() string { return f.RespAttr }
511
512// BodyFields is part of a FETCH BODY[] response.
513type BodyFields struct {
514 Params [][2]string
515 ContentID, ContentDescr, CTE string
516 Octets int32
517}
518
519// BodyTypeMpart represents the body structure a multipart message, with
520// subparts and the multipart media subtype. Used in a FETCH response.
521type BodyTypeMpart struct {
522 // ../rfc/9051:6411
523 Bodies []any // BodyTypeBasic, BodyTypeMsg, BodyTypeText
524 MediaSubtype string
525 Ext *BodyExtensionMpart
526}
527
528// BodyTypeBasic represents basic information about a part, used in a FETCH
529// response.
530type BodyTypeBasic struct {
531 // ../rfc/9051:6407
532 MediaType, MediaSubtype string
533 BodyFields BodyFields
534 Ext *BodyExtension1Part
535}
536
537// BodyTypeMsg represents an email message as a body structure, used in a FETCH
538// response.
539type BodyTypeMsg struct {
540 // ../rfc/9051:6415
541 MediaType, MediaSubtype string
542 BodyFields BodyFields
543 Envelope Envelope
544 Bodystructure any // One of the BodyType*
545 Lines int64
546 Ext *BodyExtension1Part
547}
548
549// BodyTypeText represents a text part as a body structure, used in a FETCH
550// response.
551type BodyTypeText struct {
552 // ../rfc/9051:6418
553 MediaType, MediaSubtype string
554 BodyFields BodyFields
555 Lines int64
556 Ext *BodyExtension1Part
557}
558
559// BodyExtension1Part has the extensible form fields of a BODYSTRUCTURE for
560// multiparts.
561type BodyExtensionMpart struct {
562 // ../rfc/9051:5986 ../rfc/3501:4161 ../rfc/9051:6371 ../rfc/3501:4599
563 Params [][2]string
564 Disposition string
565 DispositionParams [][2]string
566 Language []string
567 Location string
568 More []BodyExtension
569}
570
571// BodyExtension1Part has the extensible form fields of a BODYSTRUCTURE for
572// non-multiparts.
573type BodyExtension1Part struct {
574 // ../rfc/9051:6023 ../rfc/3501:4191 ../rfc/9051:6366 ../rfc/3501:4584
575 MD5 string
576 Disposition string
577 DispositionParams [][2]string
578 Language []string
579 Location string
580 More []BodyExtension
581}
582
583// BodyExtension has the additional extension fields for future expansion of
584// extensions.
585type BodyExtension struct {
586 String *string
587 Number *int64
588 More []BodyExtension
589}
590
591// "BINARY" fetch response.
592type FetchBinary struct {
593 RespAttr string
594 Parts []uint32 // Can be nil.
595 Data string
596}
597
598func (f FetchBinary) Attr() string { return f.RespAttr }
599
600// "BINARY.SIZE" fetch response.
601type FetchBinarySize struct {
602 RespAttr string
603 Parts []uint32
604 Size int64
605}
606
607func (f FetchBinarySize) Attr() string { return f.RespAttr }
608
609// "UID" fetch response.
610type FetchUID uint32
611
612func (f FetchUID) Attr() string { return "UID" }
613
614// "MODSEQ" fetch response.
615type FetchModSeq int64
616
617func (f FetchModSeq) Attr() string { return "MODSEQ" }
618