2Package imapclient provides an IMAP4 client, primarily for testing the IMAP4 server.
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
11- Try to keep the parsing method names and the types similar to the ABNF names in the RFCs.
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.
25 "github.com/mjl-/flate"
27 "github.com/mjl-/mox/mlog"
28 "github.com/mjl-/mox/moxio"
31// Conn is an IMAP connection to a server.
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
39 compress bool // If compression is enabled, we must flush flateWriter and its target original bufio writer.
40 flateWriter *flate.Writer
46 record bool // If true, bytes read are added to recordBuf. recorded() resets.
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.
55// Error is a parse or other protocol error.
56type Error struct{ err error }
58func (e Error) Error() string {
62func (e Error) Unwrap() error {
66// New creates a new client on conn.
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.
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)
78 br: bufio.NewReader(moxio.NewTraceReader(log, "CR: ", conn)),
81 CapAvailable: map[Capability]struct{}{},
82 CapEnabled: map[Capability]struct{}{},
84 // Writes are buffered and write to Conn, which may panic.
85 c.bw = bufio.NewWriter(moxio.NewTraceWriter(log, "CW: ", &c))
87 defer c.recover(&rerr)
90 c.xerrorf("expected untagged *, got %q", tag)
94 switch x := ut.(type) {
97 c.xerrorf("greeting, got status %q, expected OK", x.Status)
100 case UntaggedPreauth:
104 c.xerrorf("greeting: server sent bye")
106 c.xerrorf("unexpected untagged %v", ut)
111func (c *Conn) recover(rerr *error) {
127func (c *Conn) xerrorf(format string, args ...any) {
128 panic(Error{fmt.Errorf(format, args...)})
131func (c *Conn) xcheckf(err error, format string, args ...any) {
133 c.xerrorf("%s: %w", fmt.Sprintf(format, args...), err)
137func (c *Conn) xcheck(err error) {
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)
148 n, rerr = c.conn.Write(buf)
149 c.xcheckf(rerr, "write")
153func (c *Conn) xflush() {
155 c.xcheckf(err, "flush")
157 // If compression is active, we need to flush the deflate stream.
159 err := c.flateWriter.Flush()
160 c.xcheckf(err, "flush deflate")
161 err = c.flateBW.Flush()
162 c.xcheckf(err, "flush deflate buffer")
166// Close closes the connection, flushing and closing any compression and TLS layer.
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)
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")
184 err := c.conn.Close()
185 c.xcheckf(err, "close connection")
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()
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)
209 _, err := fmt.Fprintf(c.bw, "%s %s\r\n", tag, fmt.Sprintf(format, args...))
210 c.xcheckf(err, "write command")
215func (c *Conn) nextTag() string {
217 return fmt.Sprintf("x%03d", c.tagGen)
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)
230 untagged = append(untagged, c.xuntagged())
234 if tag != c.LastTag {
235 c.xerrorf("got tag %q, expected %q", tag, c.LastTag)
238 status := c.xstatus()
240 result = c.xresult(status)
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)
253 c.xerrorf("got tag %q, expected untagged", tag)
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)
265 line, err := c.br.ReadString('\n')
266 c.xcheckf(err, "read line")
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) {
277 untagged, result, rerr = c.Response()
278 c.xcheckf(rerr, "reading non-continuation response")
279 c.xerrorf("response status %q, expected OK", result.Status)
282 line, err := c.Readline()
283 c.xcheckf(err, "read line")
284 line = strings.TrimSuffix(line, "\r\n")
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)
293 s := fmt.Sprintf(format, args...)
294 _, err := fmt.Fprintf(c.bw, "%s\r\n", s)
295 c.xcheckf(err, "writeline")
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)
305 _, err := fmt.Fprintf(c.bw, "{%d}\r\n", len(s))
306 c.xcheckf(err, "write sync literal size")
309 plus, err := c.br.Peek(1)
310 c.xcheckf(err, "read continuation")
312 _, err = c.Readline()
313 c.xcheckf(err, "read continuation line")
315 _, err = c.bw.Write([]byte(s))
316 c.xcheckf(err, "write literal data")
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)
324 return untagged, fmt.Errorf("no continuation (%s)", result.Status)
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)
333 err := c.Commandf("", format, args...)
335 return nil, Result{}, err
337 return c.ResponseOK()
340func (c *Conn) ResponseOK() (untagged []Untagged, result Result, rerr error) {
341 untagged, result, rerr = c.Response()
343 return nil, Result{}, rerr
345 if result.Status != OK {
346 c.xerrorf("response status %q, expected OK", result.Status)
348 return untagged, result, rerr
351func (c *Conn) xgetUntagged(l []Untagged, dst any) {
353 c.xerrorf("got %d untagged, expected 1: %v", len(l), l)
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())
361 dstv.Elem().Set(gotv)