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.
26// Conn is an IMAP connection to a server.
32 record bool // If true, bytes read are added to recordBuf. recorded() resets.
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.
41// Error is a parse or other protocol error.
42type Error struct{ err error }
44func (e Error) Error() string {
48func (e Error) Unwrap() error {
52// New creates a new client on conn.
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.
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) {
63 r: bufio.NewReader(conn),
65 CapAvailable: map[Capability]struct{}{},
66 CapEnabled: map[Capability]struct{}{},
69 defer c.recover(&rerr)
72 c.xerrorf("expected untagged *, got %q", tag)
76 switch x := ut.(type) {
79 c.xerrorf("greeting, got status %q, expected OK", x.Status)
86 c.xerrorf("greeting: server sent bye")
88 c.xerrorf("unexpected untagged %v", ut)
93func (c *Conn) recover(rerr *error) {
109func (c *Conn) xerrorf(format string, args ...any) {
110 panic(Error{fmt.Errorf(format, args...)})
113func (c *Conn) xcheckf(err error, format string, args ...any) {
115 c.xerrorf("%s: %w", fmt.Sprintf(format, args...), err)
119func (c *Conn) xcheck(err error) {
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()
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)
144 _, err := fmt.Fprintf(c.conn, "%s %s\r\n", tag, fmt.Sprintf(format, args...))
145 c.xcheckf(err, "write command")
149func (c *Conn) nextTag() string {
151 return fmt.Sprintf("x%03d", c.tagGen)
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)
164 untagged = append(untagged, c.xuntagged())
168 if tag != c.LastTag {
169 c.xerrorf("got tag %q, expected %q", tag, c.LastTag)
172 status := c.xstatus()
174 result = c.xresult(status)
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)
187 c.xerrorf("got tag %q, expected untagged", tag)
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)
199 line, err := c.r.ReadString('\n')
200 c.xcheckf(err, "read line")
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) {
211 untagged, result, rerr = c.Response()
212 c.xcheckf(rerr, "reading non-continuation response")
213 c.xerrorf("response status %q, expected OK", result.Status)
216 line, err := c.Readline()
217 c.xcheckf(err, "read line")
218 line = strings.TrimSuffix(line, "\r\n")
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)
227 s := fmt.Sprintf(format, args...)
228 _, err := fmt.Fprintf(c.conn, "%s\r\n", s)
229 c.xcheckf(err, "writeline")
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)
238 n, rerr = c.conn.Write(buf)
239 c.xcheckf(rerr, "write")
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)
248 _, err := fmt.Fprintf(c.conn, "{%d}\r\n", len(s))
249 c.xcheckf(err, "write sync literal size")
251 plus, err := c.r.Peek(1)
252 c.xcheckf(err, "read continuation")
254 _, err = c.Readline()
255 c.xcheckf(err, "read continuation line")
257 _, err = c.conn.Write([]byte(s))
258 c.xcheckf(err, "write literal data")
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)
265 return untagged, fmt.Errorf("no continuation (%s)", result.Status)
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)
274 err := c.Commandf("", format, args...)
276 return nil, Result{}, err
278 return c.ResponseOK()
281func (c *Conn) ResponseOK() (untagged []Untagged, result Result, rerr error) {
282 untagged, result, rerr = c.Response()
284 return nil, Result{}, rerr
286 if result.Status != OK {
287 c.xerrorf("response status %q, expected OK", result.Status)
289 return untagged, result, rerr
292func (c *Conn) xgetUntagged(l []Untagged, dst any) {
294 c.xerrorf("got %d untagged, expected 1: %v", len(l), l)
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())
302 dstv.Elem().Set(gotv)
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 {