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