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