1/*
2Package imapclient provides an IMAP4 client implementing IMAP4rev1 (RFC 3501),
3IMAP4rev2 (RFC 9051) and various extensions.
4
5Warning: Currently primarily for testing the mox IMAP4 server. Behaviour that
6may not be required by the IMAP4 specification may be expected by this client.
7
8See [Conn] for a high-level client for executing IMAP commands. Use its embedded
9[Proto] for lower-level writing of commands and reading of responses.
10*/
11package imapclient
12
13import (
14 "bufio"
15 "crypto/tls"
16 "fmt"
17 "io"
18 "log/slog"
19 "net"
20 "strings"
21
22 "github.com/mjl-/mox/mlog"
23 "github.com/mjl-/mox/moxio"
24)
25
26// Conn is an connection to an IMAP server.
27//
28// Method names on Conn are the names of IMAP commands. CloseMailbox, which
29// executes the IMAP CLOSE command, is an exception. The Close method closes the
30// connection.
31//
32// The methods starting with MSN are the original (old) IMAP commands. The variants
33// starting with UID should almost always be used instead, if available.
34//
35// The methods on Conn typically return errors of type Error or Response. Error
36// represents protocol and i/o level errors, including io.ErrDeadlineExceeded and
37// various errors for closed connections. Response is returned as error if the IMAP
38// result is NO or BAD instead of OK. The responses returned by the IMAP command
39// methods can also be non-zero on errors. Callers may wish to process any untagged
40// responses.
41//
42// The IMAP command methods defined on Conn don't interpret the untagged responses
43// except for untagged CAPABILITY and untagged ENABLED responses, and the
44// CAPABILITY response code. Fields CapAvailable and CapEnabled are updated when
45// those untagged responses are received.
46//
47// Capabilities indicate which optional IMAP functionality is supported by a
48// server. Capabilities are typically implicitly enabled when the client sends a
49// command using syntax of an optional extension. Extensions without new syntax
50// from client to server, but with new behaviour or syntax from server to client,
51// the client needs to explicitly enable the capability with the ENABLE command,
52// see the Enable method.
53type Conn struct {
54 // If true, server sent a PREAUTH tag and the connection is already authenticated,
55 // e.g. based on TLS certificate authentication.
56 Preauth bool
57
58 // Capabilities available at server, from CAPABILITY command or response code.
59 CapAvailable []Capability
60 // Capabilities marked as enabled by the server, typically after an ENABLE command.
61 CapEnabled []Capability
62
63 // Proto provides lower-level functions for interacting with the IMAP connection,
64 // such as reading and writing individual lines/commands/responses.
65 Proto
66}
67
68// Proto provides low-level operations for writing requests and reading responses
69// on an IMAP connection.
70//
71// To implement the IDLE command, write "IDLE" using [Proto.WriteCommandf], then
72// read a line with [Proto.Readline]. If it starts with "+ ", the connection is in
73// idle mode and untagged responses can be read using [Proto.ReadUntagged]. If the
74// line doesn't start with "+ ", use [ParseResult] to interpret it as a response to
75// IDLE, which should be a NO or BAD. To abort idle mode, write "DONE" using
76// [Proto.Writelinef] and wait until a result line has been read.
77type Proto struct {
78 // Connection, may be original TCP or TLS connection. Reads go through c.br, and
79 // writes through c.xbw. The "x" for the writes indicate that failed writes cause
80 // an i/o panic, which is either turned into a returned error, or passed on (see
81 // boolean panic). The reader and writer wrap a tracing reading/writer and may wrap
82 // flate compression.
83 conn net.Conn
84 connBroken bool // If connection is broken, we won't flush (and write) again.
85 br *bufio.Reader
86 tr *moxio.TraceReader
87 xbw *bufio.Writer
88 compress bool // If compression is enabled, we must flush flateWriter and its target original bufio writer.
89 xflateWriter *moxio.FlateWriter
90 xflateBW *bufio.Writer
91 xtw *moxio.TraceWriter
92
93 log mlog.Log
94 errHandle func(err error) // If set, called for all errors. Can panic. Used for imapserver tests.
95 tagGen int
96 record bool // If true, bytes read are added to recordBuf. recorded() resets.
97 recordBuf []byte
98
99 lastTag string
100}
101
102// Error is a parse or other protocol error.
103type Error struct{ err error }
104
105func (e Error) Error() string {
106 return e.err.Error()
107}
108
109func (e Error) Unwrap() error {
110 return e.err
111}
112
113// Opts has optional fields that influence behaviour of a Conn.
114type Opts struct {
115 Logger *slog.Logger
116
117 // Error is called for IMAP-level and connection-level errors during the IMAP
118 // command methods on Conn, not for errors in calls on Proto. Error is allowed to
119 // call panic.
120 Error func(err error)
121}
122
123// New initializes a new IMAP client on conn.
124//
125// Conn should normally be a TLS connection, typically connected to port 993 of an
126// IMAP server. Alternatively, conn can be a plain TCP connection to port 143. TLS
127// should be enabled on plain TCP connections with the [Conn.StartTLS] method.
128//
129// The initial untagged greeting response is read and must be "OK" or
130// "PREAUTH". If preauth, the connection is already in authenticated state,
131// typically through TLS client certificate. This is indicated in Conn.Preauth.
132//
133// Logging is written to opts.Logger. In particular, IMAP protocol traces are
134// written with prefixes "CR: " and "CW: " (client read/write) as quoted strings at
135// levels Debug-4, with authentication messages at Debug-6 and (user) data at level
136// Debug-8.
137func New(conn net.Conn, opts *Opts) (client *Conn, rerr error) {
138 c := Conn{
139 Proto: Proto{conn: conn},
140 }
141
142 var clog *slog.Logger
143 if opts != nil {
144 c.errHandle = opts.Error
145 clog = opts.Logger
146 } else {
147 clog = slog.Default()
148 }
149 c.log = mlog.New("imapclient", clog)
150
151 c.tr = moxio.NewTraceReader(c.log, "CR: ", &c)
152 c.br = bufio.NewReader(c.tr)
153
154 // Writes are buffered and write to Conn, which may panic.
155 c.xtw = moxio.NewTraceWriter(c.log, "CW: ", &c)
156 c.xbw = bufio.NewWriter(c.xtw)
157
158 defer c.recoverErr(&rerr)
159 tag := c.xnonspace()
160 if tag != "*" {
161 c.xerrorf("expected untagged *, got %q", tag)
162 }
163 c.xspace()
164 ut := c.xuntagged()
165 switch x := ut.(type) {
166 case UntaggedResult:
167 if x.Status != OK {
168 c.xerrorf("greeting, got status %q, expected OK", x.Status)
169 }
170 if x.Code != nil {
171 if caps, ok := x.Code.(CodeCapability); ok {
172 c.CapAvailable = caps
173 }
174 }
175 return &c, nil
176 case UntaggedPreauth:
177 c.Preauth = true
178 return &c, nil
179 case UntaggedBye:
180 c.xerrorf("greeting: server sent bye")
181 default:
182 c.xerrorf("unexpected untagged %v", ut)
183 }
184 panic("not reached")
185}
186
187func (c *Conn) recoverErr(rerr *error) {
188 c.recover(rerr, nil)
189}
190
191func (c *Conn) recover(rerr *error, resp *Response) {
192 if *rerr != nil {
193 if r, ok := (*rerr).(Response); ok && resp != nil {
194 *resp = r
195 }
196 c.errHandle(*rerr)
197 return
198 }
199
200 x := recover()
201 if x == nil {
202 return
203 }
204 var err error
205 switch e := x.(type) {
206 case Error:
207 err = e
208 case Response:
209 err = e
210 if resp != nil {
211 *resp = e
212 }
213 default:
214 panic(x)
215 }
216 if c.errHandle != nil {
217 c.errHandle(err)
218 }
219 *rerr = err
220}
221
222func (p *Proto) recover(rerr *error) {
223 if *rerr != nil {
224 return
225 }
226
227 x := recover()
228 if x == nil {
229 return
230 }
231 switch e := x.(type) {
232 case Error:
233 *rerr = e
234 default:
235 panic(x)
236 }
237}
238
239func (p *Proto) xerrorf(format string, args ...any) {
240 panic(Error{fmt.Errorf(format, args...)})
241}
242
243func (p *Proto) xcheckf(err error, format string, args ...any) {
244 if err != nil {
245 p.xerrorf("%s: %w", fmt.Sprintf(format, args...), err)
246 }
247}
248
249func (p *Proto) xcheck(err error) {
250 if err != nil {
251 panic(err)
252 }
253}
254
255// xresponse sets resp if err is a Response and resp is not nil.
256func (p *Proto) xresponse(err error, resp *Response) {
257 if err == nil {
258 return
259 }
260 if r, ok := err.(Response); ok && resp != nil {
261 *resp = r
262 }
263 panic(err)
264}
265
266// Write writes directly to underlying connection (TCP, TLS). For internal use
267// only, to implement io.Writer. Write errors do take the connection's panic mode
268// into account, i.e. Write can panic.
269func (p *Proto) Write(buf []byte) (n int, rerr error) {
270 defer p.recover(&rerr)
271
272 n, rerr = p.conn.Write(buf)
273 if rerr != nil {
274 p.connBroken = true
275 }
276 p.xcheckf(rerr, "write")
277 return n, nil
278}
279
280// Read reads directly from the underlying connection (TCP, TLS). For internal use
281// only, to implement io.Reader.
282func (p *Proto) Read(buf []byte) (n int, err error) {
283 return p.conn.Read(buf)
284}
285
286func (p *Proto) xflush() {
287 // Not writing any more when connection is broken.
288 if p.connBroken {
289 return
290 }
291
292 err := p.xbw.Flush()
293 p.xcheckf(err, "flush")
294
295 // If compression is active, we need to flush the deflate stream.
296 if p.compress {
297 err := p.xflateWriter.Flush()
298 p.xcheckf(err, "flush deflate")
299 err = p.xflateBW.Flush()
300 p.xcheckf(err, "flush deflate buffer")
301 }
302}
303
304func (p *Proto) xtraceread(level slog.Level) func() {
305 if p.tr == nil {
306 // For ParseUntagged and other parse functions.
307 return func() {}
308 }
309 p.tr.SetTrace(level)
310 return func() {
311 p.tr.SetTrace(mlog.LevelTrace)
312 }
313}
314
315func (p *Proto) xtracewrite(level slog.Level) func() {
316 if p.xtw == nil {
317 // For ParseUntagged and other parse functions.
318 return func() {}
319 }
320
321 p.xflush()
322 p.xtw.SetTrace(level)
323 return func() {
324 p.xflush()
325 p.xtw.SetTrace(mlog.LevelTrace)
326 }
327}
328
329// Close closes the connection, flushing and closing any compression and TLS layer.
330//
331// You may want to call Logout first. Closing a connection with a mailbox with
332// deleted messages not yet expunged will not expunge those messages.
333//
334// Closing a TLS connection that is logged out, or closing a TLS connection with
335// compression enabled (i.e. two layered streams), may cause spurious errors
336// because the server may immediate close the underlying connection when it sees
337// the connection is being closed.
338func (c *Conn) Close() (rerr error) {
339 defer c.recoverErr(&rerr)
340
341 if c.conn == nil {
342 return nil
343 }
344 if !c.connBroken && c.xflateWriter != nil {
345 err := c.xflateWriter.Close()
346 c.xcheckf(err, "close deflate writer")
347 err = c.xflateBW.Flush()
348 c.xcheckf(err, "flush deflate buffer")
349 c.xflateWriter = nil
350 c.xflateBW = nil
351 }
352 err := c.conn.Close()
353 c.xcheckf(err, "close connection")
354 c.conn = nil
355 return
356}
357
358// TLSConnectionState returns the TLS connection state if the connection uses TLS,
359// either because the conn passed to [New] was a TLS connection, or because
360// [Conn.StartTLS] was called.
361func (c *Conn) TLSConnectionState() *tls.ConnectionState {
362 if conn, ok := c.conn.(*tls.Conn); ok {
363 cs := conn.ConnectionState()
364 return &cs
365 }
366 return nil
367}
368
369// WriteCommandf writes a free-form IMAP command to the server. An ending \r\n is
370// written too.
371//
372// If tag is empty, a next unique tag is assigned.
373func (p *Proto) WriteCommandf(tag string, format string, args ...any) (rerr error) {
374 defer p.recover(&rerr)
375
376 if tag == "" {
377 p.nextTag()
378 } else {
379 p.lastTag = tag
380 }
381
382 fmt.Fprintf(p.xbw, "%s %s\r\n", p.lastTag, fmt.Sprintf(format, args...))
383 p.xflush()
384 return
385}
386
387func (p *Proto) nextTag() string {
388 p.tagGen++
389 p.lastTag = fmt.Sprintf("x%03d", p.tagGen)
390 return p.lastTag
391}
392
393// LastTag returns the tag last used for a command. For checking against a command
394// completion result.
395func (p *Proto) LastTag() string {
396 return p.lastTag
397}
398
399// LastTagSet sets a new last tag, as used for checking against a command completion result.
400func (p *Proto) LastTagSet(tag string) {
401 p.lastTag = tag
402}
403
404// ReadResponse reads from the IMAP server until a tagged response line is found.
405// The tag must be the same as the tag for the last written command.
406//
407// If an error is returned, resp can still be non-empty, and a caller may wish to
408// process resp.Untagged.
409//
410// Caller should check resp.Status for the result of the command too.
411//
412// Common types for the return error:
413// - Error, for protocol errors
414// - Various I/O errors from the underlying connection, including os.ErrDeadlineExceeded
415func (p *Proto) ReadResponse() (resp Response, rerr error) {
416 defer p.recover(&rerr)
417
418 for {
419 tag := p.xnonspace()
420 p.xspace()
421 if tag == "*" {
422 resp.Untagged = append(resp.Untagged, p.xuntagged())
423 continue
424 }
425
426 if tag != p.lastTag {
427 p.xerrorf("got tag %q, expected %q", tag, p.lastTag)
428 }
429
430 status := p.xstatus()
431 p.xspace()
432 resp.Result = p.xresult(status)
433 p.xcrlf()
434 return
435 }
436}
437
438// ParseCode parses a response code. The string must not have enclosing brackets.
439//
440// Example:
441//
442// "APPENDUID 123 10"
443func ParseCode(s string) (code Code, rerr error) {
444 p := Proto{br: bufio.NewReader(strings.NewReader(s + "]"))}
445 defer p.recover(&rerr)
446 code = p.xrespCode()
447 p.xtake("]")
448 buf, err := io.ReadAll(p.br)
449 p.xcheckf(err, "read")
450 if len(buf) != 0 {
451 p.xerrorf("leftover data %q", buf)
452 }
453 return code, nil
454}
455
456// ParseResult parses a line, including required crlf, as a command result line.
457//
458// Example:
459//
460// "tag1 OK [APPENDUID 123 10] message added\r\n"
461func ParseResult(s string) (tag string, result Result, rerr error) {
462 p := Proto{br: bufio.NewReader(strings.NewReader(s))}
463 defer p.recover(&rerr)
464 tag = p.xnonspace()
465 p.xspace()
466 status := p.xstatus()
467 p.xspace()
468 result = p.xresult(status)
469 p.xcrlf()
470 return
471}
472
473// ReadUntagged reads a single untagged response line.
474func (p *Proto) ReadUntagged() (untagged Untagged, rerr error) {
475 defer p.recover(&rerr)
476 return p.readUntagged()
477}
478
479// ParseUntagged parses a line, including required crlf, as untagged response.
480//
481// Example:
482//
483// "* BYE shutting down connection\r\n"
484func ParseUntagged(s string) (untagged Untagged, rerr error) {
485 p := Proto{br: bufio.NewReader(strings.NewReader(s))}
486 defer p.recover(&rerr)
487 untagged, rerr = p.readUntagged()
488 return
489}
490
491func (p *Proto) readUntagged() (untagged Untagged, rerr error) {
492 defer p.recover(&rerr)
493 tag := p.xnonspace()
494 if tag != "*" {
495 p.xerrorf("got tag %q, expected untagged", tag)
496 }
497 p.xspace()
498 ut := p.xuntagged()
499 return ut, nil
500}
501
502// Readline reads a line, including CRLF.
503// Used with IDLE and synchronous literals.
504func (p *Proto) Readline() (line string, rerr error) {
505 defer p.recover(&rerr)
506
507 line, err := p.br.ReadString('\n')
508 p.xcheckf(err, "read line")
509 return line, nil
510}
511
512func (c *Conn) readContinuation() (line string, rerr error) {
513 defer c.recover(&rerr, nil)
514 line, rerr = c.ReadContinuation()
515 if rerr != nil {
516 if resp, ok := rerr.(Response); ok {
517 c.processUntagged(resp.Untagged)
518 c.processResult(resp.Result)
519 }
520 }
521 return
522}
523
524// ReadContinuation reads a line. If it is a continuation, i.e. starts with "+", it
525// is returned without leading "+ " and without trailing crlf. Otherwise, an error
526// is returned, which can be a Response with Untagged that a caller may wish to
527// process. A successfully read continuation can return an empty line.
528func (p *Proto) ReadContinuation() (line string, rerr error) {
529 defer p.recover(&rerr)
530
531 if !p.peek('+') {
532 var resp Response
533 resp, rerr = p.ReadResponse()
534 if rerr == nil {
535 rerr = resp
536 }
537 return "", rerr
538 }
539 p.xtake("+ ")
540 line, err := p.Readline()
541 p.xcheckf(err, "read line")
542 line = strings.TrimSuffix(line, "\r\n")
543 return
544}
545
546// Writelinef writes the formatted format and args as a single line, adding CRLF.
547// Used with IDLE and synchronous literals.
548func (p *Proto) Writelinef(format string, args ...any) (rerr error) {
549 defer p.recover(&rerr)
550
551 s := fmt.Sprintf(format, args...)
552 fmt.Fprintf(p.xbw, "%s\r\n", s)
553 p.xflush()
554 return nil
555}
556
557// WriteSyncLiteral first writes the synchronous literal size, then reads the
558// continuation "+" and finally writes the data. If the literal is not accepted, an
559// error is returned, which may be a Response.
560func (p *Proto) WriteSyncLiteral(s string) (rerr error) {
561 defer p.recover(&rerr)
562
563 fmt.Fprintf(p.xbw, "{%d}\r\n", len(s))
564 p.xflush()
565
566 plus, err := p.br.Peek(1)
567 p.xcheckf(err, "read continuation")
568 if plus[0] == '+' {
569 _, err = p.Readline()
570 p.xcheckf(err, "read continuation line")
571
572 defer p.xtracewrite(mlog.LevelTracedata)()
573 _, err = p.xbw.Write([]byte(s))
574 p.xcheckf(err, "write literal data")
575 p.xtracewrite(mlog.LevelTrace)
576 return nil
577 }
578 var resp Response
579 resp, rerr = p.ReadResponse()
580 if rerr == nil {
581 rerr = resp
582 }
583 return
584}
585
586func (c *Conn) processUntagged(l []Untagged) {
587 for _, ut := range l {
588 switch e := ut.(type) {
589 case UntaggedCapability:
590 c.CapAvailable = []Capability(e)
591 case UntaggedEnabled:
592 c.CapEnabled = append(c.CapEnabled, e...)
593 }
594 }
595}
596
597func (c *Conn) processResult(r Result) {
598 if r.Code == nil {
599 return
600 }
601 switch e := r.Code.(type) {
602 case CodeCapability:
603 c.CapAvailable = []Capability(e)
604 }
605}
606
607// transactf writes format and args as an IMAP command, using Commandf with an
608// empty tag. I.e. format must not contain a tag. Transactf then reads a response
609// using ReadResponse and checks the result status is OK.
610func (c *Conn) transactf(format string, args ...any) (resp Response, rerr error) {
611 defer c.recover(&rerr, &resp)
612
613 err := c.WriteCommandf("", format, args...)
614 if err != nil {
615 return Response{}, err
616 }
617
618 return c.responseOK()
619}
620
621func (c *Conn) responseOK() (resp Response, rerr error) {
622 defer c.recover(&rerr, &resp)
623
624 resp, rerr = c.ReadResponse()
625 c.processUntagged(resp.Untagged)
626 c.processResult(resp.Result)
627 if rerr == nil && resp.Status != OK {
628 rerr = resp
629 }
630 return
631}
632