1/*
2Package imapclient provides an IMAP4 client, primarily for testing the IMAP4 server.
3
4Commands can be sent to the server free-form, but responses are parsed strictly.
5Behaviour that may not be required by the IMAP4 specification may be expected by
6this client.
7*/
8package imapclient
9
10/*
11- Try to keep the parsing method names and the types similar to the ABNF names in the RFCs.
12
13- todo: have mode for imap4rev1 vs imap4rev2, refusing what is not allowed. we are accepting too much now.
14- todo: stricter parsing. xnonspace() and xword() should be replaced by proper parsers.
15*/
16
17import (
18 "bufio"
19 "crypto/tls"
20 "fmt"
21 "log/slog"
22 "net"
23 "reflect"
24 "strings"
25
26 "github.com/mjl-/mox/mlog"
27 "github.com/mjl-/mox/moxio"
28)
29
30// Conn is an IMAP connection to a server.
31type Conn struct {
32 // Connection, may be original TCP or TLS connection. Reads go through c.br, and
33 // writes through c.xbw. The "x" for the writes indicate that failed writes cause
34 // an i/o panic, which is either turned into a returned error, or passed on (see
35 // boolean panic). The reader and writer wrap a tracing reading/writer and may wrap
36 // flate compression.
37 conn net.Conn
38 connBroken bool // If connection is broken, we won't flush (and write) again.
39 br *bufio.Reader
40 tr *moxio.TraceReader
41 xbw *bufio.Writer
42 compress bool // If compression is enabled, we must flush flateWriter and its target original bufio writer.
43 xflateWriter *moxio.FlateWriter
44 xflateBW *bufio.Writer
45 xtw *moxio.TraceWriter
46
47 log mlog.Log
48 panic bool
49 tagGen int
50 record bool // If true, bytes read are added to recordBuf. recorded() resets.
51 recordBuf []byte
52
53 Preauth bool
54 LastTag string
55 CapAvailable map[Capability]struct{} // Capabilities available at server, from CAPABILITY command or response code. All uppercase.
56 CapEnabled map[Capability]struct{} // Capabilities enabled through ENABLE command. All uppercase.
57}
58
59// Error is a parse or other protocol error.
60type Error struct{ err error }
61
62func (e Error) Error() string {
63 return e.err.Error()
64}
65
66func (e Error) Unwrap() error {
67 return e.err
68}
69
70// New creates a new client on conn.
71//
72// If xpanic is true, functions that would return an error instead panic. For parse
73// errors, the resulting stack traces show typically show what was being parsed.
74//
75// The initial untagged greeting response is read and must be "OK" or
76// "PREAUTH". If preauth, the connection is already in authenticated state,
77// typically through TLS client certificate. This is indicated in Conn.Preauth.
78func New(cid int64, conn net.Conn, xpanic bool) (client *Conn, rerr error) {
79 log := mlog.New("imapclient", nil).WithCid(cid)
80 c := Conn{
81 conn: conn,
82 log: log,
83 panic: xpanic,
84 CapAvailable: map[Capability]struct{}{},
85 CapEnabled: map[Capability]struct{}{},
86 }
87 c.tr = moxio.NewTraceReader(log, "CR: ", &c)
88 c.br = bufio.NewReader(c.tr)
89
90 // Writes are buffered and write to Conn, which may panic.
91 c.xtw = moxio.NewTraceWriter(log, "CW: ", &c)
92 c.xbw = bufio.NewWriter(c.xtw)
93
94 defer c.recover(&rerr)
95 tag := c.xnonspace()
96 if tag != "*" {
97 c.xerrorf("expected untagged *, got %q", tag)
98 }
99 c.xspace()
100 ut := c.xuntagged()
101 switch x := ut.(type) {
102 case UntaggedResult:
103 if x.Status != OK {
104 c.xerrorf("greeting, got status %q, expected OK", x.Status)
105 }
106 return &c, nil
107 case UntaggedPreauth:
108 c.Preauth = true
109 return &c, nil
110 case UntaggedBye:
111 c.xerrorf("greeting: server sent bye")
112 default:
113 c.xerrorf("unexpected untagged %v", ut)
114 }
115 panic("not reached")
116}
117
118func (c *Conn) recover(rerr *error) {
119 if c.panic {
120 return
121 }
122
123 x := recover()
124 if x == nil {
125 return
126 }
127 err, ok := x.(Error)
128 if !ok {
129 panic(x)
130 }
131 *rerr = err
132}
133
134func (c *Conn) xerrorf(format string, args ...any) {
135 panic(Error{fmt.Errorf(format, args...)})
136}
137
138func (c *Conn) xcheckf(err error, format string, args ...any) {
139 if err != nil {
140 c.xerrorf("%s: %w", fmt.Sprintf(format, args...), err)
141 }
142}
143
144func (c *Conn) xcheck(err error) {
145 if err != nil {
146 panic(err)
147 }
148}
149
150// Write writes directly to underlying connection (TCP, TLS). For internal use
151// only, to implement io.Writer. Write errors do take the connection's panic mode
152// into account, i.e. Write can panic.
153func (c *Conn) Write(buf []byte) (n int, rerr error) {
154 defer c.recover(&rerr)
155
156 n, rerr = c.conn.Write(buf)
157 if rerr != nil {
158 c.connBroken = true
159 }
160 c.xcheckf(rerr, "write")
161 return n, nil
162}
163
164// Read reads directly from the underlying connection (TCP, TLS). For internal use
165// only, to implement io.Reader.
166func (c *Conn) Read(buf []byte) (n int, err error) {
167 return c.conn.Read(buf)
168}
169
170func (c *Conn) xflush() {
171 // Not writing any more when connection is broken.
172 if c.connBroken {
173 return
174 }
175
176 err := c.xbw.Flush()
177 c.xcheckf(err, "flush")
178
179 // If compression is active, we need to flush the deflate stream.
180 if c.compress {
181 err := c.xflateWriter.Flush()
182 c.xcheckf(err, "flush deflate")
183 err = c.xflateBW.Flush()
184 c.xcheckf(err, "flush deflate buffer")
185 }
186}
187
188func (c *Conn) xtrace(level slog.Level) func() {
189 c.xflush()
190 c.tr.SetTrace(level)
191 c.xtw.SetTrace(level)
192 return func() {
193 c.xflush()
194 c.tr.SetTrace(mlog.LevelTrace)
195 c.xtw.SetTrace(mlog.LevelTrace)
196 }
197}
198
199// SetPanic sets whether errors cause a panic instead of returning errors.
200func (c *Conn) SetPanic(panic bool) {
201 c.panic = panic
202}
203
204// Close closes the connection, flushing and closing any compression and TLS layer.
205//
206// You may want to call Logout first. Closing a connection with a mailbox with
207// deleted messages not yet expunged will not expunge those messages.
208//
209// Closing a TLS connection that is logged out, or closing a TLS connection with
210// compression enabled (i.e. two layered streams), may cause spurious errors
211// because the server may immediate close the underlying connection when it sees
212// the connection is being closed.
213func (c *Conn) Close() (rerr error) {
214 defer c.recover(&rerr)
215
216 if c.conn == nil {
217 return nil
218 }
219 if !c.connBroken && c.xflateWriter != nil {
220 err := c.xflateWriter.Close()
221 c.xcheckf(err, "close deflate writer")
222 err = c.xflateBW.Flush()
223 c.xcheckf(err, "flush deflate buffer")
224 c.xflateWriter = nil
225 c.xflateBW = nil
226 }
227 err := c.conn.Close()
228 c.xcheckf(err, "close connection")
229 c.conn = nil
230 return
231}
232
233// TLSConnectionState returns the TLS connection state if the connection uses TLS.
234func (c *Conn) TLSConnectionState() *tls.ConnectionState {
235 if conn, ok := c.conn.(*tls.Conn); ok {
236 cs := conn.ConnectionState()
237 return &cs
238 }
239 return nil
240}
241
242// Commandf writes a free-form IMAP command to the server. An ending \r\n is
243// written too.
244// If tag is empty, a next unique tag is assigned.
245func (c *Conn) Commandf(tag string, format string, args ...any) (rerr error) {
246 defer c.recover(&rerr)
247
248 if tag == "" {
249 tag = c.nextTag()
250 }
251 c.LastTag = tag
252
253 fmt.Fprintf(c.xbw, "%s %s\r\n", tag, fmt.Sprintf(format, args...))
254 c.xflush()
255 return
256}
257
258func (c *Conn) nextTag() string {
259 c.tagGen++
260 return fmt.Sprintf("x%03d", c.tagGen)
261}
262
263// Response reads from the IMAP server until a tagged response line is found.
264// The tag must be the same as the tag for the last written command.
265// Result holds the status of the command. The caller must check if this the status is OK.
266func (c *Conn) Response() (untagged []Untagged, result Result, rerr error) {
267 defer c.recover(&rerr)
268
269 for {
270 tag := c.xnonspace()
271 c.xspace()
272 if tag == "*" {
273 untagged = append(untagged, c.xuntagged())
274 continue
275 }
276
277 if tag != c.LastTag {
278 c.xerrorf("got tag %q, expected %q", tag, c.LastTag)
279 }
280
281 status := c.xstatus()
282 c.xspace()
283 result = c.xresult(status)
284 c.xcrlf()
285 return
286 }
287}
288
289// ReadUntagged reads a single untagged response line.
290// Useful for reading lines from IDLE.
291func (c *Conn) ReadUntagged() (untagged Untagged, rerr error) {
292 defer c.recover(&rerr)
293
294 tag := c.xnonspace()
295 if tag != "*" {
296 c.xerrorf("got tag %q, expected untagged", tag)
297 }
298 c.xspace()
299 ut := c.xuntagged()
300 return ut, nil
301}
302
303// Readline reads a line, including CRLF.
304// Used with IDLE and synchronous literals.
305func (c *Conn) Readline() (line string, rerr error) {
306 defer c.recover(&rerr)
307
308 line, err := c.br.ReadString('\n')
309 c.xcheckf(err, "read line")
310 return line, nil
311}
312
313// ReadContinuation reads a line. If it is a continuation, i.e. starts with a +, it
314// is returned without leading "+ " and without trailing crlf. Otherwise, a command
315// response is returned. A successfully read continuation can return an empty line.
316// Callers should check rerr and result.Status being empty to check if a
317// continuation was read.
318func (c *Conn) ReadContinuation() (line string, untagged []Untagged, result Result, rerr error) {
319 defer c.recover(&rerr)
320
321 if !c.peek('+') {
322 untagged, result, rerr = c.Response()
323 if result.Status == OK {
324 c.xerrorf("unexpected OK instead of continuation")
325 }
326 return
327 }
328 c.xtake("+ ")
329 line, err := c.Readline()
330 c.xcheckf(err, "read line")
331 line = strings.TrimSuffix(line, "\r\n")
332 return
333}
334
335// Writelinef writes the formatted format and args as a single line, adding CRLF.
336// Used with IDLE and synchronous literals.
337func (c *Conn) Writelinef(format string, args ...any) (rerr error) {
338 defer c.recover(&rerr)
339
340 s := fmt.Sprintf(format, args...)
341 fmt.Fprintf(c.xbw, "%s\r\n", s)
342 c.xflush()
343 return nil
344}
345
346// WriteSyncLiteral first writes the synchronous literal size, then reads the
347// continuation "+" and finally writes the data.
348func (c *Conn) WriteSyncLiteral(s string) (untagged []Untagged, rerr error) {
349 defer c.recover(&rerr)
350
351 fmt.Fprintf(c.xbw, "{%d}\r\n", len(s))
352 c.xflush()
353
354 plus, err := c.br.Peek(1)
355 c.xcheckf(err, "read continuation")
356 if plus[0] == '+' {
357 _, err = c.Readline()
358 c.xcheckf(err, "read continuation line")
359
360 _, err = c.xbw.Write([]byte(s))
361 c.xcheckf(err, "write literal data")
362 c.xflush()
363 return nil, nil
364 }
365 untagged, result, err := c.Response()
366 if err == nil && result.Status == OK {
367 c.xerrorf("no continuation, but invalid ok response (%q)", result.More)
368 }
369 return untagged, fmt.Errorf("no continuation (%s)", result.Status)
370}
371
372// Transactf writes format and args as an IMAP command, using Commandf with an
373// empty tag. I.e. format must not contain a tag. Transactf then reads a response
374// using ReadResponse and checks the result status is OK.
375func (c *Conn) Transactf(format string, args ...any) (untagged []Untagged, result Result, rerr error) {
376 defer c.recover(&rerr)
377
378 err := c.Commandf("", format, args...)
379 if err != nil {
380 return nil, Result{}, err
381 }
382 return c.ResponseOK()
383}
384
385func (c *Conn) ResponseOK() (untagged []Untagged, result Result, rerr error) {
386 untagged, result, rerr = c.Response()
387 if rerr != nil {
388 return nil, Result{}, rerr
389 }
390 if result.Status != OK {
391 c.xerrorf("response status %q, expected OK", result.Status)
392 }
393 return untagged, result, rerr
394}
395
396func (c *Conn) xgetUntagged(l []Untagged, dst any) {
397 if len(l) != 1 {
398 c.xerrorf("got %d untagged, expected 1: %v", len(l), l)
399 }
400 got := l[0]
401 gotv := reflect.ValueOf(got)
402 dstv := reflect.ValueOf(dst)
403 if gotv.Type() != dstv.Type().Elem() {
404 c.xerrorf("got %v, expected %v", gotv.Type(), dstv.Type().Elem())
405 }
406 dstv.Elem().Set(gotv)
407}
408