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