1package imapclient
2
3import (
4 "bufio"
5 "fmt"
6 "strings"
7)
8
9// Capability is a known string for with the ENABLED and CAPABILITY command.
10type Capability string
11
12const (
13 CapIMAP4rev1 Capability = "IMAP4rev1"
14 CapIMAP4rev2 Capability = "IMAP4rev2"
15 CapLoginDisabled Capability = "LOGINDISABLED"
16 CapStarttls Capability = "STARTTLS"
17 CapAuthPlain Capability = "AUTH=PLAIN"
18 CapLiteralPlus Capability = "LITERAL+"
19 CapLiteralMinus Capability = "LITERAL-"
20 CapIdle Capability = "IDLE"
21 CapNamespace Capability = "NAMESPACE"
22 CapBinary Capability = "BINARY"
23 CapUnselect Capability = "UNSELECT"
24 CapUidplus Capability = "UIDPLUS"
25 CapEsearch Capability = "ESEARCH"
26 CapEnable Capability = "ENABLE"
27 CapSave Capability = "SAVE"
28 CapListExtended Capability = "LIST-EXTENDED"
29 CapSpecialUse Capability = "SPECIAL-USE"
30 CapMove Capability = "MOVE"
31 CapUTF8Only Capability = "UTF8=ONLY"
32 CapUTF8Accept Capability = "UTF8=ACCEPT"
33 CapID Capability = "ID" // ../rfc/2971:80
34)
35
36// Status is the tagged final result of a command.
37type Status string
38
39const (
40 BAD Status = "BAD" // Syntax error.
41 NO Status = "NO" // Command failed.
42 OK Status = "OK" // Command succeeded.
43)
44
45// Result is the final response for a command, indicating success or failure.
46type Result struct {
47 Status Status
48 RespText
49}
50
51// CodeArg represents a response code with arguments, i.e. the data between [] in the response line.
52type CodeArg interface {
53 CodeString() string
54}
55
56// CodeOther is a valid but unrecognized response code.
57type CodeOther struct {
58 Code string
59 Args []string
60}
61
62func (c CodeOther) CodeString() string {
63 return c.Code + " " + strings.Join(c.Args, " ")
64}
65
66// CodeWords is a code with space-separated string parameters. E.g. CAPABILITY.
67type CodeWords struct {
68 Code string
69 Args []string
70}
71
72func (c CodeWords) CodeString() string {
73 s := c.Code
74 for _, w := range c.Args {
75 s += " " + w
76 }
77 return s
78}
79
80// CodeList is a code with a list with space-separated strings as parameters. E.g. BADCHARSET, PERMANENTFLAGS.
81type CodeList struct {
82 Code string
83 Args []string // If nil, no list was present. List can also be empty.
84}
85
86func (c CodeList) CodeString() string {
87 s := c.Code
88 if c.Args == nil {
89 return s
90 }
91 return s + "(" + strings.Join(c.Args, " ") + ")"
92}
93
94// CodeUint is a code with a uint32 parameter, e.g. UIDNEXT and UIDVALIDITY.
95type CodeUint struct {
96 Code string
97 Num uint32
98}
99
100func (c CodeUint) CodeString() string {
101 return fmt.Sprintf("%s %d", c.Code, c.Num)
102}
103
104// "APPENDUID" response code.
105type CodeAppendUID struct {
106 UIDValidity uint32
107 UID uint32
108}
109
110func (c CodeAppendUID) CodeString() string {
111 return fmt.Sprintf("APPENDUID %d %d", c.UIDValidity, c.UID)
112}
113
114// "COPYUID" response code.
115type CodeCopyUID struct {
116 DestUIDValidity uint32
117 From []NumRange
118 To []NumRange
119}
120
121func (c CodeCopyUID) CodeString() string {
122 str := func(l []NumRange) string {
123 s := ""
124 for i, e := range l {
125 if i > 0 {
126 s += ","
127 }
128 s += fmt.Sprintf("%d", e.First)
129 if e.Last != nil {
130 s += fmt.Sprintf(":%d", *e.Last)
131 }
132 }
133 return s
134 }
135 return fmt.Sprintf("COPYUID %d %s %s", c.DestUIDValidity, str(c.From), str(c.To))
136}
137
138// For CONDSTORE.
139type CodeModified NumSet
140
141func (c CodeModified) CodeString() string {
142 return fmt.Sprintf("MODIFIED %s", NumSet(c).String())
143}
144
145// For CONDSTORE.
146type CodeHighestModSeq int64
147
148func (c CodeHighestModSeq) CodeString() string {
149 return fmt.Sprintf("HIGHESTMODSEQ %d", c)
150}
151
152// RespText represents a response line minus the leading tag.
153type RespText struct {
154 Code string // The first word between [] after the status.
155 CodeArg CodeArg // Set if code has a parameter.
156 More string // Any remaining text.
157}
158
159// atom or string.
160func astring(s string) string {
161 if len(s) == 0 {
162 return stringx(s)
163 }
164 for _, c := range s {
165 if c <= ' ' || c >= 0x7f || c == '(' || c == ')' || c == '{' || c == '%' || c == '*' || c == '"' || c == '\\' {
166 return stringx(s)
167 }
168 }
169 return s
170}
171
172// imap "string", i.e. double-quoted string or syncliteral.
173func stringx(s string) string {
174 r := `"`
175 for _, c := range s {
176 if c == '\x00' || c == '\r' || c == '\n' {
177 return syncliteral(s)
178 }
179 if c == '\\' || c == '"' {
180 r += `\`
181 }
182 r += string(c)
183 }
184 r += `"`
185 return r
186}
187
188// sync literal, i.e. {<num>}\r\n<num bytes>.
189func syncliteral(s string) string {
190 return fmt.Sprintf("{%d}\r\n", len(s)) + s
191}
192
193// Untagged is a parsed untagged response. See types starting with Untagged.
194// todo: make an interface that the untagged responses implement?
195type Untagged any
196
197type UntaggedBye RespText
198type UntaggedPreauth RespText
199type UntaggedExpunge uint32
200type UntaggedExists uint32
201type UntaggedRecent uint32
202type UntaggedCapability []string
203type UntaggedEnabled []string
204type UntaggedResult Result
205type UntaggedFlags []string
206type UntaggedList struct {
207 // ../rfc/9051:6690
208 Flags []string
209 Separator byte // 0 for NIL
210 Mailbox string
211 Extended []MboxListExtendedItem
212 OldName string // If present, taken out of Extended.
213}
214type UntaggedFetch struct {
215 Seq uint32
216 Attrs []FetchAttr
217}
218type UntaggedSearch []uint32
219
220// ../rfc/7162:1101
221type UntaggedSearchModSeq struct {
222 Nums []uint32
223 ModSeq int64
224}
225type UntaggedStatus struct {
226 Mailbox string
227 Attrs map[string]int64 // Upper case status attributes. ../rfc/9051:7059
228}
229type UntaggedNamespace struct {
230 Personal, Other, Shared []NamespaceDescr
231}
232type UntaggedLsub struct {
233 // ../rfc/3501:4833
234 Flags []string
235 Separator byte
236 Mailbox string
237}
238
239// Fields are optional and zero if absent.
240type UntaggedEsearch struct {
241 // ../rfc/9051:6546
242 Correlator string
243 UID bool
244 Min uint32
245 Max uint32
246 All NumSet
247 Count *uint32
248 ModSeq int64
249 Exts []EsearchDataExt
250}
251
252// UntaggedVanished is used in QRESYNC to send UIDs that have been removed.
253type UntaggedVanished struct {
254 Earlier bool
255 UIDs NumSet
256}
257
258// ../rfc/2971:184
259
260type UntaggedID map[string]string
261
262// Extended data in an ESEARCH response.
263type EsearchDataExt struct {
264 Tag string
265 Value TaggedExtVal
266}
267
268type NamespaceDescr struct {
269 // ../rfc/9051:6769
270 Prefix string
271 Separator byte // If 0 then separator was absent.
272 Exts []NamespaceExtension
273}
274
275type NamespaceExtension struct {
276 // ../rfc/9051:6773
277 Key string
278 Values []string
279}
280
281// FetchAttr represents a FETCH response attribute.
282type FetchAttr interface {
283 Attr() string // Name of attribute.
284}
285
286type NumSet struct {
287 SearchResult bool // True if "$", in which case Ranges is irrelevant.
288 Ranges []NumRange
289}
290
291func (ns NumSet) IsZero() bool {
292 return !ns.SearchResult && ns.Ranges == nil
293}
294
295func (ns NumSet) String() string {
296 if ns.SearchResult {
297 return "$"
298 }
299 var r string
300 for i, x := range ns.Ranges {
301 if i > 0 {
302 r += ","
303 }
304 r += x.String()
305 }
306 return r
307}
308
309func ParseNumSet(s string) (ns NumSet, rerr error) {
310 c := Conn{r: bufio.NewReader(strings.NewReader(s))}
311 defer c.recover(&rerr)
312 ns = c.xsequenceSet()
313 return
314}
315
316// NumRange is a single number or range.
317type NumRange struct {
318 First uint32 // 0 for "*".
319 Last *uint32 // Nil if absent, 0 for "*".
320}
321
322func (nr NumRange) String() string {
323 var r string
324 if nr.First == 0 {
325 r += "*"
326 } else {
327 r += fmt.Sprintf("%d", nr.First)
328 }
329 if nr.Last == nil {
330 return r
331 }
332 r += ":"
333 v := *nr.Last
334 if v == 0 {
335 r += "*"
336 } else {
337 r += fmt.Sprintf("%d", v)
338 }
339 return r
340}
341
342type TaggedExtComp struct {
343 String string
344 Comps []TaggedExtComp // Used for both space-separated and ().
345}
346
347type TaggedExtVal struct {
348 // ../rfc/9051:7111
349 Number *int64
350 SeqSet *NumSet
351 Comp *TaggedExtComp // If SimpleNumber and SimpleSeqSet is nil, this is a Comp. But Comp is optional and can also be nil. Not great.
352}
353
354type MboxListExtendedItem struct {
355 // ../rfc/9051:6699
356 Tag string
357 Val TaggedExtVal
358}
359
360// "FLAGS" fetch response.
361type FetchFlags []string
362
363func (f FetchFlags) Attr() string { return "FLAGS" }
364
365// "ENVELOPE" fetch response.
366type FetchEnvelope Envelope
367
368func (f FetchEnvelope) Attr() string { return "ENVELOPE" }
369
370// Envelope holds the basic email message fields.
371type Envelope struct {
372 Date string
373 Subject string
374 From, Sender, ReplyTo, To, CC, BCC []Address
375 InReplyTo, MessageID string
376}
377
378// Address is an address field in an email message, e.g. To.
379type Address struct {
380 Name, Adl, Mailbox, Host string
381}
382
383// "INTERNALDATE" fetch response.
384type FetchInternalDate string // todo: parsed time
385func (f FetchInternalDate) Attr() string { return "INTERNALDATE" }
386
387// "RFC822.SIZE" fetch response.
388type FetchRFC822Size int64
389
390func (f FetchRFC822Size) Attr() string { return "RFC822.SIZE" }
391
392// "RFC822" fetch response.
393type FetchRFC822 string
394
395func (f FetchRFC822) Attr() string { return "RFC822" }
396
397// "RFC822.HEADER" fetch response.
398type FetchRFC822Header string
399
400func (f FetchRFC822Header) Attr() string { return "RFC822.HEADER" }
401
402// "RFC82.TEXT" fetch response.
403type FetchRFC822Text string
404
405func (f FetchRFC822Text) Attr() string { return "RFC822.TEXT" }
406
407// "BODYSTRUCTURE" fetch response.
408type FetchBodystructure struct {
409 // ../rfc/9051:6355
410 RespAttr string
411 Body any // BodyType*
412}
413
414func (f FetchBodystructure) Attr() string { return f.RespAttr }
415
416// "BODY" fetch response.
417type FetchBody struct {
418 // ../rfc/9051:6756 ../rfc/9051:6985
419 RespAttr string
420 Section string // todo: parse more ../rfc/9051:6985
421 Offset int32
422 Body string
423}
424
425func (f FetchBody) Attr() string { return f.RespAttr }
426
427// BodyFields is part of a FETCH BODY[] response.
428type BodyFields struct {
429 Params [][2]string
430 ContentID, ContentDescr, CTE string
431 Octets int32
432}
433
434// BodyTypeMpart represents the body structure a multipart message, with subparts and the multipart media subtype. Used in a FETCH response.
435type BodyTypeMpart struct {
436 // ../rfc/9051:6411
437 Bodies []any // BodyTypeBasic, BodyTypeMsg, BodyTypeText
438 MediaSubtype string
439}
440
441// BodyTypeBasic represents basic information about a part, used in a FETCH response.
442type BodyTypeBasic struct {
443 // ../rfc/9051:6407
444 MediaType, MediaSubtype string
445 BodyFields BodyFields
446}
447
448// BodyTypeMsg represents an email message as a body structure, used in a FETCH response.
449type BodyTypeMsg struct {
450 // ../rfc/9051:6415
451 MediaType, MediaSubtype string
452 BodyFields BodyFields
453 Envelope Envelope
454 Bodystructure any // One of the BodyType*
455 Lines int64
456}
457
458// BodyTypeText represents a text part as a body structure, used in a FETCH response.
459type BodyTypeText struct {
460 // ../rfc/9051:6418
461 MediaType, MediaSubtype string
462 BodyFields BodyFields
463 Lines int64
464}
465
466// "BINARY" fetch response.
467type FetchBinary struct {
468 RespAttr string
469 Parts []uint32 // Can be nil.
470 Data string
471}
472
473func (f FetchBinary) Attr() string { return f.RespAttr }
474
475// "BINARY.SIZE" fetch response.
476type FetchBinarySize struct {
477 RespAttr string
478 Parts []uint32
479 Size int64
480}
481
482func (f FetchBinarySize) Attr() string { return f.RespAttr }
483
484// "UID" fetch response.
485type FetchUID uint32
486
487func (f FetchUID) Attr() string { return "UID" }
488
489// "MODSEQ" fetch response.
490type FetchModSeq int64
491
492func (f FetchModSeq) Attr() string { return "MODSEQ" }
493