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