From e7b562e3f2832835f5f050e26652b090a27a842e Mon Sep 17 00:00:00 2001 From: Mechiel Lukkien Date: Mon, 14 Apr 2025 21:53:18 +0200 Subject: [PATCH] imapclient: first step towards making package usable as imap client with other imap servers, and minor imapserver bug fix The imapclient needs more changes, like more strict parsing, before it can be a generally usable IMAP client, these are a few steps towards that. - Fix a bug in the imapserver METADATA responses for TOOMANY and MAXSIZE. - Split low-level IMAP protocol handling (new Proto type) from the higher-level client command handling (existing Conn type). The idea is that some simple uses of IMAP can get by with just using these commands, while more intricate uses of IMAP (like a synchronizing client that needs to talk to all kinds of servers with different behaviours and implemented extensions) can write custom commands and read untagged responses or command completion results explicitly. The lower-level method names have clearer names now, like ReadResponse instead of Response. - Merge the untagged responses and (command completion) "Result" into a new type Response. Makes function signatures simpler. And make Response implement the error interface, and change command methods to return the Response as error if the result is NO or BAD. Simplifies error handling, and still provides the option to continue after a NO or BAD. - Add UIDSearch/MSNSearch commands, with a custom "search program", so mostly to indicate these commands exist. - More complete coverage of types for response codes, for easier handling. - Automatically handle any ENABLED or CAPABILITY untagged response or response code for IMAP command methods on type Conn. - Make difference between MSN vs UID versions of FETCH/STORE/SEARCH/COPY/MOVE/REPLACE commands more clear. The original MSN commands now have MSN prefixed to their name, so they are grouped together in the documentation. - Document which capabilities are needed for a command. --- Makefile | 1 + imapclient/client.go | 532 +++++++--- imapclient/cmds.go | 545 ++++++---- imapclient/fuzz_test.go | 38 + imapclient/parse.go | 1612 +++++++++++++++--------------- imapclient/parse_test.go | 42 + imapclient/protocol.go | 352 +++++-- imapserver/append_test.go | 20 +- imapserver/authenticate_test.go | 61 +- imapserver/compress_test.go | 4 +- imapserver/condstore_test.go | 68 +- imapserver/copy_test.go | 10 +- imapserver/create_test.go | 2 +- imapserver/delete_test.go | 4 +- imapserver/fetch_test.go | 79 +- imapserver/fuzz_test.go | 8 +- imapserver/metadata.go | 4 +- imapserver/metadata_test.go | 10 +- imapserver/move_test.go | 10 +- imapserver/notify_test.go | 53 +- imapserver/rename_test.go | 12 +- imapserver/replace_test.go | 42 +- imapserver/search_test.go | 18 +- imapserver/selectexamine_test.go | 22 +- imapserver/server_test.go | 78 +- imapserver/starttls_test.go | 8 +- imapserver/store_test.go | 2 +- imapserver/uidonly_test.go | 18 +- imapserver/unsubscribe_test.go | 4 +- integration_test.go | 8 +- testdata/imapclient/fuzzseed.txt | 1056 +++++++++++++++++++ 31 files changed, 3198 insertions(+), 1525 deletions(-) create mode 100644 imapclient/fuzz_test.go create mode 100644 imapclient/parse_test.go create mode 100644 testdata/imapclient/fuzzseed.txt diff --git a/Makefile b/Makefile index 1aa11d7..0908b21 100644 --- a/Makefile +++ b/Makefile @@ -79,6 +79,7 @@ fuzz: go test -fullpath -fuzz . -fuzztime 5m ./dmarc go test -fullpath -fuzz . -fuzztime 5m ./dmarcrpt go test -fullpath -fuzz . -parallel 1 -fuzztime 5m ./imapserver + go test -fullpath -fuzz . -fuzztime 5m ./imapclient go test -fullpath -fuzz . -parallel 1 -fuzztime 5m ./junk go test -fullpath -fuzz FuzzParseRecord -fuzztime 5m ./mtasts go test -fullpath -fuzz FuzzParsePolicy -fuzztime 5m ./mtasts diff --git a/imapclient/client.go b/imapclient/client.go index e6a9bf9..180a780 100644 --- a/imapclient/client.go +++ b/imapclient/client.go @@ -1,34 +1,80 @@ /* -Package imapclient provides an IMAP4 client, primarily for testing the IMAP4 server. +Package imapclient provides an IMAP4 client implementing IMAP4rev1 (RFC 3501), +IMAP4rev2 (RFC 9051) and various extensions. -Commands can be sent to the server free-form, but responses are parsed strictly. -Behaviour that may not be required by the IMAP4 specification may be expected by -this client. +Warning: Currently primarily for testing the mox IMAP4 server. Behaviour that +may not be required by the IMAP4 specification may be expected by this client. + +See [Conn] for a high-level client for executing IMAP commands. Use its embedded +[Proto] for lower-level writing of commands and reading of responses. */ package imapclient -/* -- Try to keep the parsing method names and the types similar to the ABNF names in the RFCs. - -- todo: have mode for imap4rev1 vs imap4rev2, refusing what is not allowed. we are accepting too much now. -- todo: stricter parsing. xnonspace() and xword() should be replaced by proper parsers. -*/ - import ( "bufio" "crypto/tls" "fmt" + "io" "log/slog" "net" - "reflect" "strings" "github.com/mjl-/mox/mlog" "github.com/mjl-/mox/moxio" ) -// Conn is an IMAP connection to a server. +// Conn is an connection to an IMAP server. +// +// Method names on Conn are the names of IMAP commands. CloseMailbox, which +// executes the IMAP CLOSE command, is an exception. The Close method closes the +// connection. +// +// The methods starting with MSN are the original (old) IMAP commands. The variants +// starting with UID should almost always be used instead, if available. +// +// The methods on Conn typically return errors of type Error or Response. Error +// represents protocol and i/o level errors, including io.ErrDeadlineExceeded and +// various errors for closed connections. Response is returned as error if the IMAP +// result is NO or BAD instead of OK. The responses returned by the IMAP command +// methods can also be non-zero on errors. Callers may wish to process any untagged +// responses. +// +// The IMAP command methods defined on Conn don't interpret the untagged responses +// except for untagged CAPABILITY and untagged ENABLED responses, and the +// CAPABILITY response code. Fields CapAvailable and CapEnabled are updated when +// those untagged responses are received. +// +// Capabilities indicate which optional IMAP functionality is supported by a +// server. Capabilities are typically implicitly enabled when the client sends a +// command using syntax of an optional extension. Extensions without new syntax +// from client to server, but with new behaviour or syntax from server to client, +// the client needs to explicitly enable the capability with the ENABLE command, +// see the Enable method. type Conn struct { + // If true, server sent a PREAUTH tag and the connection is already authenticated, + // e.g. based on TLS certificate authentication. + Preauth bool + + // Capabilities available at server, from CAPABILITY command or response code. + CapAvailable []Capability + // Capabilities marked as enabled by the server, typically after an ENABLE command. + CapEnabled []Capability + + // Proto provides lower-level functions for interacting with the IMAP connection, + // such as reading and writing individual lines/commands/responses. + Proto +} + +// Proto provides low-level operations for writing requests and reading responses +// on an IMAP connection. +// +// To implement the IDLE command, write "IDLE" using [Proto.WriteCommandf], then +// read a line with [Proto.Readline]. If it starts with "+ ", the connection is in +// idle mode and untagged responses can be read using [Proto.ReadUntagged]. If the +// line doesn't start with "+ ", use [ParseResult] to interpret it as a response to +// IDLE, which should be a NO or BAD. To abort idle mode, write "DONE" using +// [Proto.Writelinef] and wait until a result line has been read. +type Proto struct { // Connection, may be original TCP or TLS connection. Reads go through c.br, and // writes through c.xbw. The "x" for the writes indicate that failed writes cause // an i/o panic, which is either turned into a returned error, or passed on (see @@ -50,10 +96,7 @@ type Conn struct { record bool // If true, bytes read are added to recordBuf. recorded() resets. recordBuf []byte - Preauth bool - LastTag string - CapAvailable map[Capability]struct{} // Capabilities available at server, from CAPABILITY command or response code. All uppercase. - CapEnabled map[Capability]struct{} // Capabilities enabled through ENABLE command. All uppercase. + lastTag string } // Error is a parse or other protocol error. @@ -71,26 +114,29 @@ func (e Error) Unwrap() error { type Opts struct { Logger *slog.Logger - // Error is called for both IMAP-level and connection-level errors. Is allowed to + // Error is called for IMAP-level and connection-level errors during the IMAP + // command methods on Conn, not for errors in calls on Proto. Error is allowed to // call panic. Error func(err error) } -// New creates a new client on conn. +// New initializes a new IMAP client on conn. +// +// Conn should normally be a TLS connection, typically connected to port 993 of an +// IMAP server. Alternatively, conn can be a plain TCP connection to port 143. TLS +// should be enabled on plain TCP connections with the [Conn.StartTLS] method. // // The initial untagged greeting response is read and must be "OK" or // "PREAUTH". If preauth, the connection is already in authenticated state, // typically through TLS client certificate. This is indicated in Conn.Preauth. // -// Logging is written to log, in particular IMAP protocol traces are written with -// prefixes "CR: " and "CW: " (client read/write) as quoted strings at levels -// Debug-4, with authentication messages at Debug-6 and (user) data at level +// Logging is written to opts.Logger. In particular, IMAP protocol traces are +// written with prefixes "CR: " and "CW: " (client read/write) as quoted strings at +// levels Debug-4, with authentication messages at Debug-6 and (user) data at level // Debug-8. func New(conn net.Conn, opts *Opts) (client *Conn, rerr error) { c := Conn{ - conn: conn, - CapAvailable: map[Capability]struct{}{}, - CapEnabled: map[Capability]struct{}{}, + Proto: Proto{conn: conn}, } var clog *slog.Logger @@ -109,7 +155,7 @@ func New(conn net.Conn, opts *Opts) (client *Conn, rerr error) { c.xtw = moxio.NewTraceWriter(c.log, "CW: ", &c) c.xbw = bufio.NewWriter(c.xtw) - defer c.recover(&rerr) + defer c.recoverErr(&rerr) tag := c.xnonspace() if tag != "*" { c.xerrorf("expected untagged *, got %q", tag) @@ -121,6 +167,11 @@ func New(conn net.Conn, opts *Opts) (client *Conn, rerr error) { if x.Status != OK { c.xerrorf("greeting, got status %q, expected OK", x.Status) } + if x.Code != nil { + if caps, ok := x.Code.(CodeCapability); ok { + c.CapAvailable = caps + } + } return &c, nil case UntaggedPreauth: c.Preauth = true @@ -133,13 +184,33 @@ func New(conn net.Conn, opts *Opts) (client *Conn, rerr error) { panic("not reached") } -func (c *Conn) recover(rerr *error) { +func (c *Conn) recoverErr(rerr *error) { + c.recover(rerr, nil) +} + +func (c *Conn) recover(rerr *error, resp *Response) { + if *rerr != nil { + if r, ok := (*rerr).(Response); ok && resp != nil { + *resp = r + } + c.errHandle(*rerr) + return + } + x := recover() if x == nil { return } - err, ok := x.(Error) - if !ok { + var err error + switch e := x.(type) { + case Error: + err = e + case Response: + err = e + if resp != nil { + *resp = e + } + default: panic(x) } if c.errHandle != nil { @@ -148,73 +219,110 @@ func (c *Conn) recover(rerr *error) { *rerr = err } -func (c *Conn) xerrorf(format string, args ...any) { - panic(Error{fmt.Errorf(format, args...)}) -} +func (p *Proto) recover(rerr *error) { + if *rerr != nil { + return + } -func (c *Conn) xcheckf(err error, format string, args ...any) { - if err != nil { - c.xerrorf("%s: %w", fmt.Sprintf(format, args...), err) + x := recover() + if x == nil { + return + } + switch e := x.(type) { + case Error: + *rerr = e + default: + panic(x) } } -func (c *Conn) xcheck(err error) { +func (p *Proto) xerrorf(format string, args ...any) { + panic(Error{fmt.Errorf(format, args...)}) +} + +func (p *Proto) xcheckf(err error, format string, args ...any) { + if err != nil { + p.xerrorf("%s: %w", fmt.Sprintf(format, args...), err) + } +} + +func (p *Proto) xcheck(err error) { if err != nil { panic(err) } } +// xresponse sets resp if err is a Response and resp is not nil. +func (p *Proto) xresponse(err error, resp *Response) { + if err == nil { + return + } + if r, ok := err.(Response); ok && resp != nil { + *resp = r + } + panic(err) +} + // Write writes directly to underlying connection (TCP, TLS). For internal use // only, to implement io.Writer. Write errors do take the connection's panic mode // into account, i.e. Write can panic. -func (c *Conn) Write(buf []byte) (n int, rerr error) { - defer c.recover(&rerr) +func (p *Proto) Write(buf []byte) (n int, rerr error) { + defer p.recover(&rerr) - n, rerr = c.conn.Write(buf) + n, rerr = p.conn.Write(buf) if rerr != nil { - c.connBroken = true + p.connBroken = true } - c.xcheckf(rerr, "write") + p.xcheckf(rerr, "write") return n, nil } // Read reads directly from the underlying connection (TCP, TLS). For internal use // only, to implement io.Reader. -func (c *Conn) Read(buf []byte) (n int, err error) { - return c.conn.Read(buf) +func (p *Proto) Read(buf []byte) (n int, err error) { + return p.conn.Read(buf) } -func (c *Conn) xflush() { +func (p *Proto) xflush() { // Not writing any more when connection is broken. - if c.connBroken { + if p.connBroken { return } - err := c.xbw.Flush() - c.xcheckf(err, "flush") + err := p.xbw.Flush() + p.xcheckf(err, "flush") // If compression is active, we need to flush the deflate stream. - if c.compress { - err := c.xflateWriter.Flush() - c.xcheckf(err, "flush deflate") - err = c.xflateBW.Flush() - c.xcheckf(err, "flush deflate buffer") + if p.compress { + err := p.xflateWriter.Flush() + p.xcheckf(err, "flush deflate") + err = p.xflateBW.Flush() + p.xcheckf(err, "flush deflate buffer") } } -func (c *Conn) xtraceread(level slog.Level) func() { - c.tr.SetTrace(level) +func (p *Proto) xtraceread(level slog.Level) func() { + if p.tr == nil { + // For ParseUntagged and other parse functions. + return func() {} + } + p.tr.SetTrace(level) return func() { - c.tr.SetTrace(mlog.LevelTrace) + p.tr.SetTrace(mlog.LevelTrace) } } -func (c *Conn) xtracewrite(level slog.Level) func() { - c.xflush() - c.xtw.SetTrace(level) +func (p *Proto) xtracewrite(level slog.Level) func() { + if p.xtw == nil { + // For ParseUntagged and other parse functions. + return func() {} + } + + p.xflush() + p.xtw.SetTrace(level) return func() { - c.xflush() - c.xtw.SetTrace(mlog.LevelTrace) + p.xflush() + p.xtw.SetTrace(mlog.LevelTrace) } } @@ -228,7 +336,7 @@ func (c *Conn) xtracewrite(level slog.Level) func() { // because the server may immediate close the underlying connection when it sees // the connection is being closed. func (c *Conn) Close() (rerr error) { - defer c.recover(&rerr) + defer c.recoverErr(&rerr) if c.conn == nil { return nil @@ -247,7 +355,9 @@ func (c *Conn) Close() (rerr error) { return } -// TLSConnectionState returns the TLS connection state if the connection uses TLS. +// TLSConnectionState returns the TLS connection state if the connection uses TLS, +// either because the conn passed to [New] was a TLS connection, or because +// [Conn.StartTLS] was called. func (c *Conn) TLSConnectionState() *tls.ConnectionState { if conn, ok := c.conn.(*tls.Conn); ok { cs := conn.ConnectionState() @@ -256,170 +366,266 @@ func (c *Conn) TLSConnectionState() *tls.ConnectionState { return nil } -// Commandf writes a free-form IMAP command to the server. An ending \r\n is +// WriteCommandf writes a free-form IMAP command to the server. An ending \r\n is // written too. +// // If tag is empty, a next unique tag is assigned. -func (c *Conn) Commandf(tag string, format string, args ...any) (rerr error) { - defer c.recover(&rerr) +func (p *Proto) WriteCommandf(tag string, format string, args ...any) (rerr error) { + defer p.recover(&rerr) if tag == "" { - tag = c.nextTag() + p.nextTag() + } else { + p.lastTag = tag } - c.LastTag = tag - fmt.Fprintf(c.xbw, "%s %s\r\n", tag, fmt.Sprintf(format, args...)) - c.xflush() + fmt.Fprintf(p.xbw, "%s %s\r\n", p.lastTag, fmt.Sprintf(format, args...)) + p.xflush() return } -func (c *Conn) nextTag() string { - c.tagGen++ - return fmt.Sprintf("x%03d", c.tagGen) +func (p *Proto) nextTag() string { + p.tagGen++ + p.lastTag = fmt.Sprintf("x%03d", p.tagGen) + return p.lastTag } -// Response reads from the IMAP server until a tagged response line is found. +// LastTag returns the tag last used for a command. For checking against a command +// completion result. +func (p *Proto) LastTag() string { + return p.lastTag +} + +// LastTagSet sets a new last tag, as used for checking against a command completion result. +func (p *Proto) LastTagSet(tag string) { + p.lastTag = tag +} + +// ReadResponse reads from the IMAP server until a tagged response line is found. // The tag must be the same as the tag for the last written command. -// Result holds the status of the command. The caller must check if this the status is OK. -func (c *Conn) Response() (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) +// +// If an error is returned, resp can still be non-empty, and a caller may wish to +// process resp.Untagged. +// +// Caller should check resp.Status for the result of the command too. +// +// Common types for the return error: +// - Error, for protocol errors +// - Various I/O errors from the underlying connection, including os.ErrDeadlineExceeded +func (p *Proto) ReadResponse() (resp Response, rerr error) { + defer p.recover(&rerr) for { - tag := c.xnonspace() - c.xspace() + tag := p.xnonspace() + p.xspace() if tag == "*" { - untagged = append(untagged, c.xuntagged()) + resp.Untagged = append(resp.Untagged, p.xuntagged()) continue } - if tag != c.LastTag { - c.xerrorf("got tag %q, expected %q", tag, c.LastTag) + if tag != p.lastTag { + p.xerrorf("got tag %q, expected %q", tag, p.lastTag) } - status := c.xstatus() - c.xspace() - result = c.xresult(status) - c.xcrlf() + status := p.xstatus() + p.xspace() + resp.Result = p.xresult(status) + p.xcrlf() return } } -// ReadUntagged reads a single untagged response line. -// Useful for reading lines from IDLE. -func (c *Conn) ReadUntagged() (untagged Untagged, rerr error) { - defer c.recover(&rerr) - - tag := c.xnonspace() - if tag != "*" { - c.xerrorf("got tag %q, expected untagged", tag) +// ParseCode parses a response code. The string must not have enclosing brackets. +// +// Example: +// +// "APPENDUID 123 10" +func ParseCode(s string) (code Code, rerr error) { + p := Proto{br: bufio.NewReader(strings.NewReader(s + "]"))} + defer p.recover(&rerr) + code = p.xrespCode() + p.xtake("]") + buf, err := io.ReadAll(p.br) + p.xcheckf(err, "read") + if len(buf) != 0 { + p.xerrorf("leftover data %q", buf) } - c.xspace() - ut := c.xuntagged() + return code, nil +} + +// ParseResult parses a line, including required crlf, as a command result line. +// +// Example: +// +// "tag1 OK [APPENDUID 123 10] message added\r\n" +func ParseResult(s string) (tag string, result Result, rerr error) { + p := Proto{br: bufio.NewReader(strings.NewReader(s))} + defer p.recover(&rerr) + tag = p.xnonspace() + p.xspace() + status := p.xstatus() + p.xspace() + result = p.xresult(status) + p.xcrlf() + return +} + +// ReadUntagged reads a single untagged response line. +func (p *Proto) ReadUntagged() (untagged Untagged, rerr error) { + defer p.recover(&rerr) + return p.readUntagged() +} + +// ParseUntagged parses a line, including required crlf, as untagged response. +// +// Example: +// +// "* BYE shutting down connection\r\n" +func ParseUntagged(s string) (untagged Untagged, rerr error) { + p := Proto{br: bufio.NewReader(strings.NewReader(s))} + defer p.recover(&rerr) + untagged, rerr = p.readUntagged() + return +} + +func (p *Proto) readUntagged() (untagged Untagged, rerr error) { + defer p.recover(&rerr) + tag := p.xnonspace() + if tag != "*" { + p.xerrorf("got tag %q, expected untagged", tag) + } + p.xspace() + ut := p.xuntagged() return ut, nil } // Readline reads a line, including CRLF. // Used with IDLE and synchronous literals. -func (c *Conn) Readline() (line string, rerr error) { - defer c.recover(&rerr) +func (p *Proto) Readline() (line string, rerr error) { + defer p.recover(&rerr) - line, err := c.br.ReadString('\n') - c.xcheckf(err, "read line") + line, err := p.br.ReadString('\n') + p.xcheckf(err, "read line") return line, nil } -// ReadContinuation reads a line. If it is a continuation, i.e. starts with a +, it -// is returned without leading "+ " and without trailing crlf. Otherwise, a command -// response is returned. A successfully read continuation can return an empty line. -// Callers should check rerr and result.Status being empty to check if a -// continuation was read. -func (c *Conn) ReadContinuation() (line string, untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) - - if !c.peek('+') { - untagged, result, rerr = c.Response() - if result.Status == OK { - c.xerrorf("unexpected OK instead of continuation") +func (c *Conn) readContinuation() (line string, rerr error) { + defer c.recover(&rerr, nil) + line, rerr = c.ReadContinuation() + if rerr != nil { + if resp, ok := rerr.(Response); ok { + c.processUntagged(resp.Untagged) + c.processResult(resp.Result) } - return } - c.xtake("+ ") - line, err := c.Readline() - c.xcheckf(err, "read line") + return +} + +// ReadContinuation reads a line. If it is a continuation, i.e. starts with "+", it +// is returned without leading "+ " and without trailing crlf. Otherwise, an error +// is returned, which can be a Response with Untagged that a caller may wish to +// process. A successfully read continuation can return an empty line. +func (p *Proto) ReadContinuation() (line string, rerr error) { + defer p.recover(&rerr) + + if !p.peek('+') { + var resp Response + resp, rerr = p.ReadResponse() + if rerr == nil { + rerr = resp + } + return "", rerr + } + p.xtake("+ ") + line, err := p.Readline() + p.xcheckf(err, "read line") line = strings.TrimSuffix(line, "\r\n") return } // Writelinef writes the formatted format and args as a single line, adding CRLF. // Used with IDLE and synchronous literals. -func (c *Conn) Writelinef(format string, args ...any) (rerr error) { - defer c.recover(&rerr) +func (p *Proto) Writelinef(format string, args ...any) (rerr error) { + defer p.recover(&rerr) s := fmt.Sprintf(format, args...) - fmt.Fprintf(c.xbw, "%s\r\n", s) - c.xflush() + fmt.Fprintf(p.xbw, "%s\r\n", s) + p.xflush() return nil } // WriteSyncLiteral first writes the synchronous literal size, then reads the -// continuation "+" and finally writes the data. -func (c *Conn) WriteSyncLiteral(s string) (untagged []Untagged, rerr error) { - defer c.recover(&rerr) +// continuation "+" and finally writes the data. If the literal is not accepted, an +// error is returned, which may be a Response. +func (p *Proto) WriteSyncLiteral(s string) (rerr error) { + defer p.recover(&rerr) - fmt.Fprintf(c.xbw, "{%d}\r\n", len(s)) - c.xflush() + fmt.Fprintf(p.xbw, "{%d}\r\n", len(s)) + p.xflush() - plus, err := c.br.Peek(1) - c.xcheckf(err, "read continuation") + plus, err := p.br.Peek(1) + p.xcheckf(err, "read continuation") if plus[0] == '+' { - _, err = c.Readline() - c.xcheckf(err, "read continuation line") + _, err = p.Readline() + p.xcheckf(err, "read continuation line") - defer c.xtracewrite(mlog.LevelTracedata)() - _, err = c.xbw.Write([]byte(s)) - c.xcheckf(err, "write literal data") - c.xtracewrite(mlog.LevelTrace) - return nil, nil + defer p.xtracewrite(mlog.LevelTracedata)() + _, err = p.xbw.Write([]byte(s)) + p.xcheckf(err, "write literal data") + p.xtracewrite(mlog.LevelTrace) + return nil } - untagged, result, err := c.Response() - if err == nil && result.Status == OK { - c.xerrorf("no continuation, but invalid ok response (%q)", result.More) + var resp Response + resp, rerr = p.ReadResponse() + if rerr == nil { + rerr = resp } - return untagged, fmt.Errorf("no continuation (%s)", result.Status) + return } -// Transactf writes format and args as an IMAP command, using Commandf with an +func (c *Conn) processUntagged(l []Untagged) { + for _, ut := range l { + switch e := ut.(type) { + case UntaggedCapability: + c.CapAvailable = []Capability(e) + case UntaggedEnabled: + c.CapEnabled = append(c.CapEnabled, e...) + } + } +} + +func (c *Conn) processResult(r Result) { + if r.Code == nil { + return + } + switch e := r.Code.(type) { + case CodeCapability: + c.CapAvailable = []Capability(e) + } +} + +// transactf writes format and args as an IMAP command, using Commandf with an // empty tag. I.e. format must not contain a tag. Transactf then reads a response // using ReadResponse and checks the result status is OK. -func (c *Conn) Transactf(format string, args ...any) (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) +func (c *Conn) transactf(format string, args ...any) (resp Response, rerr error) { + defer c.recover(&rerr, &resp) - err := c.Commandf("", format, args...) + err := c.WriteCommandf("", format, args...) if err != nil { - return nil, Result{}, err + return Response{}, err } - return c.ResponseOK() + + return c.responseOK() } -func (c *Conn) ResponseOK() (untagged []Untagged, result Result, rerr error) { - untagged, result, rerr = c.Response() - if rerr != nil { - return nil, Result{}, rerr - } - if result.Status != OK { - c.xerrorf("response status %q, expected OK", result.Status) - } - return untagged, result, rerr -} +func (c *Conn) responseOK() (resp Response, rerr error) { + defer c.recover(&rerr, &resp) -func (c *Conn) xgetUntagged(l []Untagged, dst any) { - if len(l) != 1 { - c.xerrorf("got %d untagged, expected 1: %v", len(l), l) + resp, rerr = c.ReadResponse() + c.processUntagged(resp.Untagged) + c.processResult(resp.Result) + if rerr == nil && resp.Status != OK { + rerr = resp } - got := l[0] - gotv := reflect.ValueOf(got) - dstv := reflect.ValueOf(dst) - if gotv.Type() != dstv.Type().Elem() { - c.xerrorf("got %v, expected %v", gotv.Type(), dstv.Type().Elem()) - } - dstv.Elem().Set(gotv) + return } diff --git a/imapclient/cmds.go b/imapclient/cmds.go index 64cfb4c..5fdd909 100644 --- a/imapclient/cmds.go +++ b/imapclient/cmds.go @@ -17,32 +17,34 @@ import ( "github.com/mjl-/mox/scram" ) -// Capability requests a list of capabilities from the server. They are returned in -// an UntaggedCapability response. The server also sends capabilities in initial -// server greeting, in the response code. -func (c *Conn) Capability() (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) - return c.Transactf("capability") +// Capability writes the IMAP4 "CAPABILITY" command, requesting a list of +// capabilities from the server. They are returned in an UntaggedCapability +// response. The server also sends capabilities in initial server greeting, in the +// response code. +func (c *Conn) Capability() (resp Response, rerr error) { + defer c.recover(&rerr, &resp) + return c.transactf("capability") } -// Noop does nothing on its own, but a server will return any pending untagged -// responses for new message delivery and changes to mailboxes. -func (c *Conn) Noop() (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) - return c.Transactf("noop") +// Noop writes the IMAP4 "NOOP" command, which does nothing on its own, but a +// server will return any pending untagged responses for new message delivery and +// changes to mailboxes. +func (c *Conn) Noop() (resp Response, rerr error) { + defer c.recover(&rerr, &resp) + return c.transactf("noop") } -// Logout ends the IMAP session by writing a LOGOUT command. Close must still be -// called on this client to close the socket. -func (c *Conn) Logout() (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) - return c.Transactf("logout") +// Logout ends the IMAP4 session by writing an IMAP "LOGOUT" command. [Conn.Close] +// must still be called on this client to close the socket. +func (c *Conn) Logout() (resp Response, rerr error) { + defer c.recover(&rerr, &resp) + return c.transactf("logout") } -// Starttls enables TLS on the connection with the STARTTLS command. -func (c *Conn) Starttls(config *tls.Config) (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) - untagged, result, rerr = c.Transactf("starttls") +// StartTLS enables TLS on the connection with the IMAP4 "STARTTLS" command. +func (c *Conn) StartTLS(config *tls.Config) (resp Response, rerr error) { + defer c.recover(&rerr, &resp) + resp, rerr = c.transactf("starttls") c.xcheckf(rerr, "starttls command") conn := c.xprefixConn() @@ -50,32 +52,43 @@ func (c *Conn) Starttls(config *tls.Config) (untagged []Untagged, result Result, err := tlsConn.Handshake() c.xcheckf(err, "tls handshake") c.conn = tlsConn - return untagged, result, nil + return } -// Login authenticates with username and password -func (c *Conn) Login(username, password string) (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) +// Login authenticates using the IMAP4 "LOGIN" command, sending the plain text +// password to the server. +// +// Authentication is not allowed while the "LOGINDISABLED" capability is announced. +// Call [Conn.StartTLS] first. +// +// See [Conn.AuthenticateSCRAM] for a better authentication mechanism. +func (c *Conn) Login(username, password string) (resp Response, rerr error) { + defer c.recover(&rerr, &resp) - c.LastTag = c.nextTag() - fmt.Fprintf(c.xbw, "%s login %s ", c.LastTag, astring(username)) + fmt.Fprintf(c.xbw, "%s login %s ", c.nextTag(), astring(username)) defer c.xtracewrite(mlog.LevelTraceauth)() fmt.Fprintf(c.xbw, "%s\r\n", astring(password)) c.xtracewrite(mlog.LevelTrace) // Restore. - return c.Response() + return c.responseOK() } -// Authenticate with plaintext password using AUTHENTICATE PLAIN. -func (c *Conn) AuthenticatePlain(username, password string) (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) +// AuthenticatePlain executes the AUTHENTICATE command with SASL mechanism "PLAIN", +// sending the password in plain text password to the server. +// +// Required capability: "AUTH=PLAIN" +// +// Authentication is not allowed while the "LOGINDISABLED" capability is announced. +// Call [Conn.StartTLS] first. +// +// See [Conn.AuthenticateSCRAM] for a better authentication mechanism. +func (c *Conn) AuthenticatePlain(username, password string) (resp Response, rerr error) { + defer c.recover(&rerr, &resp) - err := c.Commandf("", "authenticate plain") + err := c.WriteCommandf("", "authenticate plain") c.xcheckf(err, "writing authenticate command") - _, untagged, result, rerr = c.ReadContinuation() - c.xcheckf(rerr, "reading continuation") - if result.Status != "" { - c.xerrorf("got result status %q, expected continuation", result.Status) - } + _, rerr = c.readContinuation() + c.xresponse(rerr, &resp) + defer c.xtracewrite(mlog.LevelTraceauth)() xw := base64.NewEncoder(base64.StdEncoding, c.xbw) fmt.Fprintf(xw, "\u0000%s\u0000%s", username, password) @@ -83,23 +96,31 @@ func (c *Conn) AuthenticatePlain(username, password string) (untagged []Untagged c.xtracewrite(mlog.LevelTrace) // Restore. fmt.Fprintf(c.xbw, "\r\n") c.xflush() - return c.Response() + return c.responseOK() } // todo: implement cram-md5, write its credentials as traceauth. -// Authenticate with SCRAM-SHA-256(-PLUS) or SCRAM-SHA-1(-PLUS). With SCRAM, the -// password is not exchanged in plaintext form, but only derived hashes are -// exchanged by both parties as proof of knowledge of password. +// AuthenticateSCRAM executes the IMAP4 "AUTHENTICATE" command with one of the +// following SASL mechanisms: SCRAM-SHA-256(-PLUS) or SCRAM-SHA-1(-PLUS).// +// +// With SCRAM, the password is not sent to the server in plain text, but only +// derived hashes are exchanged by both parties as proof of knowledge of password. +// +// Authentication is not allowed while the "LOGINDISABLED" capability is announced. +// Call [Conn.StartTLS] first. +// +// Required capability: SCRAM-SHA-256-PLUS, SCRAM-SHA-256, SCRAM-SHA-1-PLUS, +// SCRAM-SHA-1. // // The PLUS variants bind the authentication exchange to the TLS connection, // detecting MitM attacks. -func (c *Conn) AuthenticateSCRAM(method string, h func() hash.Hash, username, password string) (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) +func (c *Conn) AuthenticateSCRAM(mechanism string, h func() hash.Hash, username, password string) (resp Response, rerr error) { + defer c.recover(&rerr, &resp) var cs *tls.ConnectionState - lmethod := strings.ToLower(method) - if strings.HasSuffix(lmethod, "-plus") { + lmech := strings.ToLower(mechanism) + if strings.HasSuffix(lmech, "-plus") { tlsConn, ok := c.conn.(*tls.Conn) if !ok { c.xerrorf("cannot use scram plus without tls") @@ -110,17 +131,14 @@ func (c *Conn) AuthenticateSCRAM(method string, h func() hash.Hash, username, pa sc := scram.NewClient(h, username, "", false, cs) clientFirst, err := sc.ClientFirst() c.xcheckf(err, "scram clientFirst") - c.LastTag = c.nextTag() - err = c.Writelinef("%s authenticate %s %s", c.LastTag, method, base64.StdEncoding.EncodeToString([]byte(clientFirst))) + // todo: only send clientFirst if server has announced SASL-IR + err = c.Writelinef("%s authenticate %s %s", c.nextTag(), mechanism, base64.StdEncoding.EncodeToString([]byte(clientFirst))) c.xcheckf(err, "writing command line") xreadContinuation := func() []byte { var line string - line, untagged, result, rerr = c.ReadContinuation() - c.xcheckf(err, "read continuation") - if result.Status != "" { - c.xerrorf("got result status %q, expected continuation", result.Status) - } + line, rerr = c.readContinuation() + c.xresponse(rerr, &resp) buf, err := base64.StdEncoding.DecodeString(line) c.xcheckf(err, "parsing base64 from remote") return buf @@ -140,18 +158,19 @@ func (c *Conn) AuthenticateSCRAM(method string, h func() hash.Hash, username, pa err = c.Writelinef("%s", base64.StdEncoding.EncodeToString(nil)) c.xcheckf(err, "scram client end") - return c.ResponseOK() + return c.responseOK() } -// CompressDeflate enables compression with deflate on the connection. +// CompressDeflate enables compression with deflate on the connection by executing +// the IMAP4 "COMPRESS=DEFAULT" command. // -// Only possible when server has announced the COMPRESS=DEFLATE capability. +// Required capability: "COMPRESS=DEFLATE". // // State: Authenticated or selected. -func (c *Conn) CompressDeflate() (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) +func (c *Conn) CompressDeflate() (resp Response, rerr error) { + defer c.recover(&rerr, &resp) - untagged, result, rerr = c.Transactf("compress deflate") + resp, rerr = c.transactf("compress deflate") c.xcheck(rerr) c.xflateBW = bufio.NewWriter(c) @@ -172,89 +191,98 @@ func (c *Conn) CompressDeflate() (untagged []Untagged, result Result, rerr error return } -// Enable enables capabilities for use with the connection, verifying the server has indeed enabled them. -func (c *Conn) Enable(capabilities ...string) (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) +// Enable enables capabilities for use with the connection by executing the IMAP4 "ENABLE" command. +// +// Required capability: "ENABLE" or "IMAP4rev2" +func (c *Conn) Enable(capabilities ...Capability) (resp Response, rerr error) { + defer c.recover(&rerr, &resp) - untagged, result, rerr = c.Transactf("enable %s", strings.Join(capabilities, " ")) - c.xcheck(rerr) - var enabled UntaggedEnabled - c.xgetUntagged(untagged, &enabled) - got := map[string]struct{}{} - for _, cap := range enabled { - got[cap] = struct{}{} + var caps strings.Builder + for _, c := range capabilities { + caps.WriteString(" " + string(c)) } - for _, cap := range capabilities { - if _, ok := got[cap]; !ok { - c.xerrorf("capability %q not enabled by server", cap) - } - } - return + return c.transactf("enable%s", caps.String()) } -// Select opens mailbox as active mailbox. -func (c *Conn) Select(mailbox string) (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) - return c.Transactf("select %s", astring(mailbox)) +// Select opens the mailbox with the IMAP4 "SELECT" command. +// +// If a mailbox is selected/active, it is automatically deselected before +// selecting the mailbox, without permanently removing ("expunging") messages +// marked \Deleted. +// +// If the mailbox cannot be opened, the connection is left in Authenticated state, +// not Selected. +func (c *Conn) Select(mailbox string) (resp Response, rerr error) { + defer c.recover(&rerr, &resp) + return c.transactf("select %s", astring(mailbox)) } -// Examine opens mailbox as active mailbox read-only. -func (c *Conn) Examine(mailbox string) (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) - return c.Transactf("examine %s", astring(mailbox)) +// Examine opens the mailbox like [Conn.Select], but read-only, with the IMAP4 +// "EXAMINE" command. +func (c *Conn) Examine(mailbox string) (resp Response, rerr error) { + defer c.recover(&rerr, &resp) + return c.transactf("examine %s", astring(mailbox)) } -// Create makes a new mailbox on the server. -// SpecialUse can only be used on servers that announced the CREATE-SPECIAL-USE +// Create makes a new mailbox on the server using the IMAP4 "CREATE" command. +// +// SpecialUse can only be used on servers that announced the "CREATE-SPECIAL-USE" // capability. Specify flags like \Archive, \Drafts, \Junk, \Sent, \Trash, \All. -func (c *Conn) Create(mailbox string, specialUse []string) (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) - if _, ok := c.CapAvailable[CapCreateSpecialUse]; !ok && len(specialUse) > 0 { - c.xerrorf("server does not implement create-special-use extension") - } +func (c *Conn) Create(mailbox string, specialUse []string) (resp Response, rerr error) { + defer c.recover(&rerr, &resp) var useStr string if len(specialUse) > 0 { useStr = fmt.Sprintf(" USE (%s)", strings.Join(specialUse, " ")) } - return c.Transactf("create %s%s", astring(mailbox), useStr) + return c.transactf("create %s%s", astring(mailbox), useStr) } -// Delete removes an entire mailbox and its messages. -func (c *Conn) Delete(mailbox string) (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) - return c.Transactf("delete %s", astring(mailbox)) +// Delete removes an entire mailbox and its messages using the IMAP4 "DELETE" +// command. +func (c *Conn) Delete(mailbox string) (resp Response, rerr error) { + defer c.recover(&rerr, &resp) + return c.transactf("delete %s", astring(mailbox)) } -// Rename changes the name of a mailbox and all its child mailboxes. -func (c *Conn) Rename(omailbox, nmailbox string) (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) - return c.Transactf("rename %s %s", astring(omailbox), astring(nmailbox)) +// Rename changes the name of a mailbox and all its child mailboxes +// using the IMAP4 "RENAME" command. +func (c *Conn) Rename(omailbox, nmailbox string) (resp Response, rerr error) { + defer c.recover(&rerr, &resp) + return c.transactf("rename %s %s", astring(omailbox), astring(nmailbox)) } -// Subscribe marks a mailbox as subscribed. The mailbox does not have to exist. It -// is not an error if the mailbox is already subscribed. -func (c *Conn) Subscribe(mailbox string) (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) - return c.Transactf("subscribe %s", astring(mailbox)) +// Subscribe marks a mailbox as subscribed using the IMAP4 "SUBSCRIBE" command. +// +// The mailbox does not have to exist. It is not an error if the mailbox is already +// subscribed. +func (c *Conn) Subscribe(mailbox string) (resp Response, rerr error) { + defer c.recover(&rerr, &resp) + return c.transactf("subscribe %s", astring(mailbox)) } -// Unsubscribe marks a mailbox as unsubscribed. -func (c *Conn) Unsubscribe(mailbox string) (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) - return c.Transactf("unsubscribe %s", astring(mailbox)) +// Unsubscribe marks a mailbox as unsubscribed using the IMAP4 "UNSUBSCRIBE" +// command. +func (c *Conn) Unsubscribe(mailbox string) (resp Response, rerr error) { + defer c.recover(&rerr, &resp) + return c.transactf("unsubscribe %s", astring(mailbox)) } -// List lists mailboxes with the basic LIST syntax. +// List lists mailboxes using the IMAP4 "LIST" command with the basic LIST syntax. // Pattern can contain * (match any) or % (match any except hierarchy delimiter). -func (c *Conn) List(pattern string) (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) - return c.Transactf(`list "" %s`, astring(pattern)) +func (c *Conn) List(pattern string) (resp Response, rerr error) { + defer c.recover(&rerr, &resp) + return c.transactf(`list "" %s`, astring(pattern)) } -// ListFull lists mailboxes with the extended LIST syntax requesting all supported data. +// ListFull lists mailboxes using the LIST command with the extended LIST +// syntax requesting all supported data. +// +// Required capability: "LIST-EXTENDED". If "IMAP4rev2" is announced, the command +// is also available but only with a single pattern. +// // Pattern can contain * (match any) or % (match any except hierarchy delimiter). -func (c *Conn) ListFull(subscribedOnly bool, patterns ...string) (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) +func (c *Conn) ListFull(subscribedOnly bool, patterns ...string) (resp Response, rerr error) { + defer c.recover(&rerr, &resp) var subscribedStr string if subscribedOnly { subscribedStr = "subscribed recursivematch" @@ -262,49 +290,54 @@ func (c *Conn) ListFull(subscribedOnly bool, patterns ...string) (untagged []Unt for i, s := range patterns { patterns[i] = astring(s) } - return c.Transactf(`list (%s) "" (%s) return (subscribed children special-use status (messages uidnext uidvalidity unseen deleted size recent appendlimit))`, subscribedStr, strings.Join(patterns, " ")) + return c.transactf(`list (%s) "" (%s) return (subscribed children special-use status (messages uidnext uidvalidity unseen deleted size recent appendlimit))`, subscribedStr, strings.Join(patterns, " ")) } -// Namespace returns the hiearchy separator in an UntaggedNamespace response with personal/shared/other namespaces if present. -func (c *Conn) Namespace() (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) - return c.Transactf("namespace") +// Namespace requests the hiearchy separator using the IMAP4 "NAMESPACE" command. +// +// Required capability: "NAMESPACE" or "IMAP4rev2". +// +// Server will return an UntaggedNamespace response with personal/shared/other +// namespaces if present. +func (c *Conn) Namespace() (resp Response, rerr error) { + defer c.recover(&rerr, &resp) + return c.transactf("namespace") } -// Status requests information about a mailbox, such as number of messages, size, -// etc. At least one attribute required. -func (c *Conn) Status(mailbox string, attrs ...StatusAttr) (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) +// Status requests information about a mailbox using the IMAP4 "STATUS" command. For +// example, number of messages, size, etc. At least one attribute required. +func (c *Conn) Status(mailbox string, attrs ...StatusAttr) (resp Response, rerr error) { + defer c.recover(&rerr, &resp) l := make([]string, len(attrs)) for i, a := range attrs { l[i] = string(a) } - return c.Transactf("status %s (%s)", astring(mailbox), strings.Join(l, " ")) + return c.transactf("status %s (%s)", astring(mailbox), strings.Join(l, " ")) } -// Append represents a parameter to the APPEND or REPLACE commands. +// Append represents a parameter to the IMAP4 "APPEND" or "REPLACE" commands, for +// adding a message to mailbox, or replacing a message with a new version in a +// mailbox. type Append struct { - Flags []string - Received *time.Time + Flags []string // Optional, flags for the new message. + Received *time.Time // Optional, the INTERNALDATE field, typically time at which a message was received. Size int64 - Data io.Reader // Must return Size bytes. + Data io.Reader // Required, must return Size bytes. } -// Append adds message to mailbox with flags and optional receive time. +// Append adds message to mailbox with flags and optional receive time using the +// IMAP4 "APPEND" command. +func (c *Conn) Append(mailbox string, message Append) (resp Response, rerr error) { + return c.MultiAppend(mailbox, message) +} + +// MultiAppend atomatically adds multiple messages to the mailbox. // -// Multiple messages are only possible when the server has announced the -// MULTIAPPEND capability. -func (c *Conn) Append(mailbox string, message Append, more ...Append) (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) +// Required capability: "MULTIAPPEND" +func (c *Conn) MultiAppend(mailbox string, message Append, more ...Append) (resp Response, rerr error) { + defer c.recover(&rerr, &resp) - if _, ok := c.CapAvailable[CapMultiAppend]; !ok && len(more) > 0 { - c.xerrorf("can only append multiple messages when server has announced MULTIAPPEND capability") - } - - tag := c.nextTag() - c.LastTag = tag - - fmt.Fprintf(c.xbw, "%s append %s", tag, astring(mailbox)) + fmt.Fprintf(c.xbw, "%s append %s", c.nextTag(), astring(mailbox)) msgs := append([]Append{message}, more...) for _, m := range msgs { @@ -325,150 +358,226 @@ func (c *Conn) Append(mailbox string, message Append, more ...Append) (untagged fmt.Fprintf(c.xbw, "\r\n") c.xflush() - return c.Response() + return c.responseOK() } // note: No Idle or Notify command. Idle/Notify is better implemented by // writing the request and reading and handling the responses as they come in. -// CloseMailbox closes the currently selected/active mailbox, permanently removing -// any messages marked with \Deleted. -func (c *Conn) CloseMailbox() (untagged []Untagged, result Result, rerr error) { - return c.Transactf("close") +// CloseMailbox closes the selected/active mailbox using the IMAP4 "CLOSE" command, +// permanently removing ("expunging") any messages marked with \Deleted. +// +// See [Conn.Unselect] for closing a mailbox without permanently removing messages. +func (c *Conn) CloseMailbox() (resp Response, rerr error) { + return c.transactf("close") } -// Unselect closes the currently selected/active mailbox, but unlike CloseMailbox -// does not permanently remove any messages marked with \Deleted. -func (c *Conn) Unselect() (untagged []Untagged, result Result, rerr error) { - return c.Transactf("unselect") +// Unselect closes the selected/active mailbox using the IMAP4 "UNSELECT" command, +// but unlike MailboxClose does not permanently remove ("expunge") any messages +// marked with \Deleted. +// +// Required capability: "UNSELECT" or "IMAP4rev2". +// +// If Unselect is not available, call [Conn.Select] with a non-existent mailbox for +// the same effect: Deselecting a mailbox without permanently removing messages +// marked \Deleted. +func (c *Conn) Unselect() (resp Response, rerr error) { + return c.transactf("unselect") } -// Expunge removes messages marked as deleted for the selected mailbox. -func (c *Conn) Expunge() (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) - return c.Transactf("expunge") +// Expunge removes all messages marked as deleted for the selected mailbox using +// the IMAP4 "EXPUNGE" command. If other sessions marked messages as deleted, even +// if they aren't visible in the session, they are removed as well. +// +// UIDExpunge gives more control over which the messages that are removed. +func (c *Conn) Expunge() (resp Response, rerr error) { + defer c.recover(&rerr, &resp) + return c.transactf("expunge") } -// UIDExpunge is like expunge, but only removes messages matching uidSet. -func (c *Conn) UIDExpunge(uidSet NumSet) (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) - return c.Transactf("uid expunge %s", uidSet.String()) +// UIDExpunge is like expunge, but only removes messages matching UID set, using +// the IMAP4 "UID EXPUNGE" command. +// +// Required capability: "UIDPLUS" or "IMAP4rev2". +func (c *Conn) UIDExpunge(uidSet NumSet) (resp Response, rerr error) { + defer c.recover(&rerr, &resp) + return c.transactf("uid expunge %s", uidSet.String()) } // Note: No search, fetch command yet due to its large syntax. -// StoreFlagsSet stores a new set of flags for messages from seqset with the STORE command. -// If silent, no untagged responses with the updated flags will be sent by the server. -func (c *Conn) StoreFlagsSet(seqset string, silent bool, flags ...string) (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) +// MSNStoreFlagsSet stores a new set of flags for messages matching message +// sequence numbers (MSNs) from sequence set with the IMAP4 "STORE" command. +// +// If silent, no untagged responses with the updated flags will be sent by the +// server. +// +// Method [Conn.UIDStoreFlagsSet], which operates on a uid set, should be +// preferred. +func (c *Conn) MSNStoreFlagsSet(seqset string, silent bool, flags ...string) (resp Response, rerr error) { + defer c.recover(&rerr, &resp) item := "flags" if silent { item += ".silent" } - return c.Transactf("store %s %s (%s)", seqset, item, strings.Join(flags, " ")) + return c.transactf("store %s %s (%s)", seqset, item, strings.Join(flags, " ")) } -// StoreFlagsAdd is like StoreFlagsSet, but only adds flags, leaving current flags on the message intact. -func (c *Conn) StoreFlagsAdd(seqset string, silent bool, flags ...string) (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) +// MSNStoreFlagsAdd is like [Conn.MSNStoreFlagsSet], but only adds flags, leaving +// current flags on the message intact. +// +// Method [Conn.UIDStoreFlagsAdd], which operates on a uid set, should be +// preferred. +func (c *Conn) MSNStoreFlagsAdd(seqset string, silent bool, flags ...string) (resp Response, rerr error) { + defer c.recover(&rerr, &resp) item := "+flags" if silent { item += ".silent" } - return c.Transactf("store %s %s (%s)", seqset, item, strings.Join(flags, " ")) + return c.transactf("store %s %s (%s)", seqset, item, strings.Join(flags, " ")) } -// StoreFlagsClear is like StoreFlagsSet, but only removes flags, leaving other flags on the message intact. -func (c *Conn) StoreFlagsClear(seqset string, silent bool, flags ...string) (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) +// MSNStoreFlagsClear is like [Conn.MSNStoreFlagsSet], but only removes flags, +// leaving other flags on the message intact. +// +// Method [Conn.UIDStoreFlagsClear], which operates on a uid set, should be +// preferred. +func (c *Conn) MSNStoreFlagsClear(seqset string, silent bool, flags ...string) (resp Response, rerr error) { + defer c.recover(&rerr, &resp) item := "-flags" if silent { item += ".silent" } - return c.Transactf("store %s %s (%s)", seqset, item, strings.Join(flags, " ")) + return c.transactf("store %s %s (%s)", seqset, item, strings.Join(flags, " ")) } -// UIDStoreFlagsSet stores a new set of flags for messages from uid set with -// the UID STORE command. +// UIDStoreFlagsSet stores a new set of flags for messages matching UIDs from +// uidSet with the IMAP4 "UID STORE" command. // // If silent, no untagged responses with the updated flags will be sent by the // server. -func (c *Conn) UIDStoreFlagsSet(seqset string, silent bool, flags ...string) (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) +// +// Required capability: "UIDPLUS" or "IMAP4rev2". +func (c *Conn) UIDStoreFlagsSet(uidSet string, silent bool, flags ...string) (resp Response, rerr error) { + defer c.recover(&rerr, &resp) item := "flags" if silent { item += ".silent" } - return c.Transactf("uid store %s %s (%s)", seqset, item, strings.Join(flags, " ")) + return c.transactf("uid store %s %s (%s)", uidSet, item, strings.Join(flags, " ")) } // UIDStoreFlagsAdd is like UIDStoreFlagsSet, but only adds flags, leaving // current flags on the message intact. -func (c *Conn) UIDStoreFlagsAdd(seqset string, silent bool, flags ...string) (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) +// +// Required capability: "UIDPLUS" or "IMAP4rev2". +func (c *Conn) UIDStoreFlagsAdd(uidSet string, silent bool, flags ...string) (resp Response, rerr error) { + defer c.recover(&rerr, &resp) item := "+flags" if silent { item += ".silent" } - return c.Transactf("uid store %s %s (%s)", seqset, item, strings.Join(flags, " ")) + return c.transactf("uid store %s %s (%s)", uidSet, item, strings.Join(flags, " ")) } // UIDStoreFlagsClear is like UIDStoreFlagsSet, but only removes flags, leaving // other flags on the message intact. -func (c *Conn) UIDStoreFlagsClear(seqset string, silent bool, flags ...string) (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) +// +// Required capability: "UIDPLUS" or "IMAP4rev2". +func (c *Conn) UIDStoreFlagsClear(uidSet string, silent bool, flags ...string) (resp Response, rerr error) { + defer c.recover(&rerr, &resp) item := "-flags" if silent { item += ".silent" } - return c.Transactf("uid store %s %s (%s)", seqset, item, strings.Join(flags, " ")) + return c.transactf("uid store %s %s (%s)", uidSet, item, strings.Join(flags, " ")) } -// Copy adds messages from the sequences in seqSet in the currently selected/active mailbox to dstMailbox. -func (c *Conn) Copy(seqSet NumSet, dstMailbox string) (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) - return c.Transactf("copy %s %s", seqSet.String(), astring(dstMailbox)) -} - -// UIDCopy is like copy, but operates on UIDs. -func (c *Conn) UIDCopy(uidSet NumSet, dstMailbox string) (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) - return c.Transactf("uid copy %s %s", uidSet.String(), astring(dstMailbox)) -} - -// Move moves messages from the sequences in seqSet in the currently selected/active mailbox to dstMailbox. -func (c *Conn) Move(seqSet NumSet, dstMailbox string) (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) - return c.Transactf("move %s %s", seqSet.String(), astring(dstMailbox)) -} - -// UIDMove is like move, but operates on UIDs. -func (c *Conn) UIDMove(uidSet NumSet, dstMailbox string) (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) - return c.Transactf("uid move %s %s", uidSet.String(), astring(dstMailbox)) -} - -// Replace replaces a message from the currently selected mailbox with a -// new/different version of the message in the named mailbox, which may be the -// same or different than the currently selected mailbox. +// MSNCopy adds messages from the sequences in the sequence set in the +// selected/active mailbox to destMailbox using the IMAP4 "COPY" command. // -// Num is a message sequence number. "*" references the last message. +// Method [Conn.UIDCopy], operating on UIDs instead of sequence numbers, should be +// preferred. +func (c *Conn) MSNCopy(seqSet string, destMailbox string) (resp Response, rerr error) { + defer c.recover(&rerr, &resp) + return c.transactf("copy %s %s", seqSet, astring(destMailbox)) +} + +// UIDCopy is like copy, but operates on UIDs, using the IMAP4 "UID COPY" command. // -// Servers must have announced the REPLACE capability. -func (c *Conn) Replace(msgseq string, mailbox string, msg Append) (untagged []Untagged, result Result, rerr error) { +// Required capability: "UIDPLUS" or "IMAP4rev2". +func (c *Conn) UIDCopy(uidSet string, destMailbox string) (resp Response, rerr error) { + defer c.recover(&rerr, &resp) + return c.transactf("uid copy %s %s", uidSet, astring(destMailbox)) +} + +// MSNSearch returns messages from the sequence set in the selected/active mailbox +// that match the search critera using the IMAP4 "SEARCH" command. +// +// Method [Conn.UIDSearch], operating on UIDs instead of sequence numbers, should be +// preferred. +func (c *Conn) MSNSearch(seqSet string, criteria string) (resp Response, rerr error) { + defer c.recover(&rerr, &resp) + return c.transactf("seach %s %s", seqSet, criteria) +} + +// UIDSearch returns messages from the uid set in the selected/active mailbox that +// match the search critera using the IMAP4 "SEARCH" command. +// +// Criteria is a search program, see RFC 9051 and RFC 3501 for details. +// +// Required capability: "UIDPLUS" or "IMAP4rev2". +func (c *Conn) UIDSearch(seqSet string, criteria string) (resp Response, rerr error) { + defer c.recover(&rerr, &resp) + return c.transactf("seach %s %s", seqSet, criteria) +} + +// MSNMove moves messages from the sequence set in the selected/active mailbox to +// destMailbox using the IMAP4 "MOVE" command. +// +// Required capability: "MOVE" or "IMAP4rev2". +// +// Method [Conn.UIDMove], operating on UIDs instead of sequence numbers, should be +// preferred. +func (c *Conn) MSNMove(seqSet string, destMailbox string) (resp Response, rerr error) { + defer c.recover(&rerr, &resp) + return c.transactf("move %s %s", seqSet, astring(destMailbox)) +} + +// UIDMove is like move, but operates on UIDs, using the IMAP4 "UID MOVE" command. +// +// Required capability: "MOVE" or "IMAP4rev2". +func (c *Conn) UIDMove(uidSet string, destMailbox string) (resp Response, rerr error) { + defer c.recover(&rerr, &resp) + return c.transactf("uid move %s %s", uidSet, astring(destMailbox)) +} + +// MSNReplace is like the preferred [Conn.UIDReplace], but operates on a message +// sequence number (MSN) instead of a UID. +// +// Required capability: "REPLACE". +// +// Method [Conn.UIDReplace], operating on UIDs instead of sequence numbers, should be +// preferred. +func (c *Conn) MSNReplace(msgseq string, mailbox string, msg Append) (resp Response, rerr error) { // todo: parse msgseq, must be nznumber, with a known msgseq. or "*" with at least one message. return c.replace("replace", msgseq, mailbox, msg) } -// UIDReplace is like Replace, but operates on a UID instead of message -// sequence number. -func (c *Conn) UIDReplace(uid string, mailbox string, msg Append) (untagged []Untagged, result Result, rerr error) { +// UIDReplace uses the IMAP4 "UID REPLACE" command to replace a message from the +// selected/active mailbox with a new/different version of the message in the named +// mailbox, which may be the same or different than the selected mailbox. +// +// The replaced message is indicated by uid. +// +// Required capability: "REPLACE". +func (c *Conn) UIDReplace(uid string, mailbox string, msg Append) (resp Response, rerr error) { // todo: parse uid, must be nznumber, with a known uid. or "*" with at least one message. return c.replace("uid replace", uid, mailbox, msg) } -func (c *Conn) replace(cmd string, num string, mailbox string, msg Append) (untagged []Untagged, result Result, rerr error) { - defer c.recover(&rerr) +func (c *Conn) replace(cmd string, num string, mailbox string, msg Append) (resp Response, rerr error) { + defer c.recover(&rerr, &resp) // todo: use synchronizing literal for larger messages. @@ -478,7 +587,7 @@ func (c *Conn) replace(cmd string, num string, mailbox string, msg Append) (unta } // todo: only use literal8 if needed, possibly with "UTF8()" // todo: encode mailbox - err := c.Commandf("", "%s %s %s (%s)%s ~{%d+}", cmd, num, astring(mailbox), strings.Join(msg.Flags, " "), date, msg.Size) + err := c.WriteCommandf("", "%s %s %s (%s)%s ~{%d+}", cmd, num, astring(mailbox), strings.Join(msg.Flags, " "), date, msg.Size) c.xcheckf(err, "writing replace command") defer c.xtracewrite(mlog.LevelTracedata)() @@ -489,5 +598,5 @@ func (c *Conn) replace(cmd string, num string, mailbox string, msg Append) (unta fmt.Fprintf(c.xbw, "\r\n") c.xflush() - return c.Response() + return c.responseOK() } diff --git a/imapclient/fuzz_test.go b/imapclient/fuzz_test.go new file mode 100644 index 0000000..5343d58 --- /dev/null +++ b/imapclient/fuzz_test.go @@ -0,0 +1,38 @@ +package imapclient + +import ( + "os" + "strings" + "testing" +) + +func FuzzParser(f *testing.F) { + /* + Gathering all untagged responses and command completion results from the RFCs: + + cd ../rfc + ( + grep ' S: \* [A-Z]' * | sed 's/^.*S: //g' + grep -E ' S: [^ *]+ (OK|NO|BAD) ' * | sed 's/^.*S: //g' + ) | grep -v '\.\.\/' | sort | uniq >../testdata/imapclient/fuzzseed.txt + */ + buf, err := os.ReadFile("../testdata/imapclient/fuzzseed.txt") + if err != nil { + f.Fatalf("reading seed: %v", err) + } + for _, s := range strings.Split(string(buf), "\n") { + f.Add(s + "\r\n") + } + f.Add("1:3") + f.Add("3:1") + f.Add("3,1") + f.Add("*") + + f.Fuzz(func(t *testing.T, data string) { + ParseUntagged(data) + ParseCode(data) + ParseResult(data) + ParseNumSet(data) + ParseUIDRange(data) + }) +} diff --git a/imapclient/parse.go b/imapclient/parse.go index 7fa46f7..300af57 100644 --- a/imapclient/parse.go +++ b/imapclient/parse.go @@ -10,87 +10,91 @@ import ( "github.com/mjl-/mox/mlog" ) -func (c *Conn) recorded() string { - s := string(c.recordBuf) - c.recordBuf = nil - c.record = false +// todo: stricter parsing. xnonspace() and xword() should be replaced by proper parsers + +// Keep the parsing method names and the types similar to the ABNF names in the RFCs. + +func (p *Proto) recorded() string { + s := string(p.recordBuf) + p.recordBuf = nil + p.record = false return s } -func (c *Conn) recordAdd(buf []byte) { - if c.record { - c.recordBuf = append(c.recordBuf, buf...) +func (p *Proto) recordAdd(buf []byte) { + if p.record { + p.recordBuf = append(p.recordBuf, buf...) } } -func (c *Conn) xtake(s string) { +func (p *Proto) xtake(s string) { buf := make([]byte, len(s)) - _, err := io.ReadFull(c.br, buf) - c.xcheckf(err, "taking %q", s) + _, err := io.ReadFull(p.br, buf) + p.xcheckf(err, "taking %q", s) if !strings.EqualFold(string(buf), s) { - c.xerrorf("got %q, expected %q", buf, s) + p.xerrorf("got %q, expected %q", buf, s) } - c.recordAdd(buf) + p.recordAdd(buf) } -func (c *Conn) readbyte() (byte, error) { - b, err := c.br.ReadByte() +func (p *Proto) readbyte() (byte, error) { + b, err := p.br.ReadByte() if err == nil { - c.recordAdd([]byte{b}) + p.recordAdd([]byte{b}) } return b, err } -func (c *Conn) xunreadbyte() { - if c.record { - c.recordBuf = c.recordBuf[:len(c.recordBuf)-1] +func (p *Proto) xunreadbyte() { + if p.record { + p.recordBuf = p.recordBuf[:len(p.recordBuf)-1] } - err := c.br.UnreadByte() - c.xcheckf(err, "unread byte") + err := p.br.UnreadByte() + p.xcheckf(err, "unread byte") } -func (c *Conn) readrune() (rune, error) { - x, _, err := c.br.ReadRune() +func (p *Proto) readrune() (rune, error) { + x, _, err := p.br.ReadRune() if err == nil { - c.recordAdd([]byte(string(x))) + p.recordAdd([]byte(string(x))) } return x, err } -func (c *Conn) space() bool { - return c.take(' ') +func (p *Proto) space() bool { + return p.take(' ') } -func (c *Conn) xspace() { - c.xtake(" ") +func (p *Proto) xspace() { + p.xtake(" ") } -func (c *Conn) xcrlf() { - c.xtake("\r\n") +func (p *Proto) xcrlf() { + p.xtake("\r\n") } -func (c *Conn) peek(exp byte) bool { - b, err := c.readbyte() +func (p *Proto) peek(exp byte) bool { + b, err := p.readbyte() if err == nil { - c.xunreadbyte() + p.xunreadbyte() } return err == nil && strings.EqualFold(string(rune(b)), string(rune(exp))) } -func (c *Conn) peekstring() bool { - return c.peek('"') || c.peek('{') +func (p *Proto) peekstring() bool { + return p.peek('"') || p.peek('{') } -func (c *Conn) take(exp byte) bool { - if c.peek(exp) { - _, _ = c.readbyte() +func (p *Proto) take(exp byte) bool { + if p.peek(exp) { + _, _ = p.readbyte() return true } return false } -func (c *Conn) xstatus() Status { - w := c.xword() +func (p *Proto) xstatus() Status { + w := p.xword() W := strings.ToUpper(w) switch W { case "OK": @@ -100,568 +104,600 @@ func (c *Conn) xstatus() Status { case "BAD": return BAD } - c.xerrorf("expected status, got %q", w) + p.xerrorf("expected status, got %q", w) panic("not reached") } // Already consumed: tag SP status SP -func (c *Conn) xresult(status Status) Result { - respText := c.xrespText() - return Result{status, respText} +func (p *Proto) xresult(status Status) Result { + code, text := p.xrespText() + return Result{status, code, text} } -func (c *Conn) xrespText() RespText { - var code string - var codeArg CodeArg - if c.take('[') { - code, codeArg = c.xrespCode() - c.xtake("]") - c.xspace() +func (p *Proto) xrespText() (code Code, text string) { + if p.take('[') { + code = p.xrespCode() + p.xtake("]") + p.xspace() } - more := "" - for !c.peek('\r') { - more += string(rune(c.xbyte())) + for !p.peek('\r') { + text += string(rune(p.xbyte())) } - return RespText{code, codeArg, more} -} - -var knownCodes = stringMap( - // Without parameters. - "ALERT", "PARSE", "READ-ONLY", "READ-WRITE", "TRYCREATE", "UIDNOTSTICKY", "UNAVAILABLE", "AUTHENTICATIONFAILED", "AUTHORIZATIONFAILED", "EXPIRED", "PRIVACYREQUIRED", "CONTACTADMIN", "NOPERM", "INUSE", "EXPUNGEISSUED", "CORRUPTION", "SERVERBUG", "CLIENTBUG", "CANNOT", "LIMIT", "OVERQUOTA", "ALREADYEXISTS", "NONEXISTENT", "NOTSAVED", "HASCHILDREN", "CLOSED", "UNKNOWN-CTE", - "OVERQUOTA", // ../rfc/9208:472 - "COMPRESSIONACTIVE", // ../rfc/4978:143 - // With parameters. - "BADCHARSET", "CAPABILITY", "PERMANENTFLAGS", "UIDNEXT", "UIDVALIDITY", "UNSEEN", "APPENDUID", "COPYUID", - "HIGHESTMODSEQ", "MODIFIED", - "INPROGRESS", // ../rfc/9585:104 - "BADEVENT", "NOTIFICATIONOVERFLOW", // ../rfc/5465:1023 - "SERVERBUG", - "UIDREQUIRED", // ../rfc/9586:136 -) - -func stringMap(l ...string) map[string]struct{} { - r := map[string]struct{}{} - for _, s := range l { - r[s] = struct{}{} - } - return r + return } // ../rfc/9051:6895 -func (c *Conn) xrespCode() (string, CodeArg) { +func (p *Proto) xrespCode() Code { w := "" - for !c.peek(' ') && !c.peek(']') { - w += string(rune(c.xbyte())) + for !p.peek(' ') && !p.peek(']') { + w += string(rune(p.xbyte())) } W := strings.ToUpper(w) - if _, ok := knownCodes[W]; !ok { - var args []string - for c.space() { - arg := "" - for !c.peek(' ') && !c.peek(']') { - arg += string(rune(c.xbyte())) - } - args = append(args, arg) - } - return W, CodeOther{W, args} - } - - var codeArg CodeArg switch W { case "BADCHARSET": var l []string // Must be nil initially. - if c.space() { - c.xtake("(") - l = []string{c.xcharset()} - for c.space() { - l = append(l, c.xcharset()) + if p.space() { + p.xtake("(") + l = []string{p.xcharset()} + for p.space() { + l = append(l, p.xcharset()) } - c.xtake(")") + p.xtake(")") } - codeArg = CodeList{W, l} + return CodeBadCharset(l) case "CAPABILITY": - c.xtake(" ") - caps := []string{c.xatom()} - for c.space() { - caps = append(caps, c.xatom()) + p.xtake(" ") + caps := []Capability{} + for { + s := p.xatom() + s = strings.ToUpper(s) + caps = append(caps, Capability(s)) + if !p.space() { + break + } } - c.CapAvailable = map[Capability]struct{}{} - for _, cap := range caps { - cap = strings.ToUpper(cap) - c.CapAvailable[Capability(cap)] = struct{}{} - } - codeArg = CodeWords{W, caps} - + return CodeCapability(caps) case "PERMANENTFLAGS": l := []string{} // Must be non-nil. - if c.space() { - c.xtake("(") - l = []string{c.xflagPerm()} - for c.space() { - l = append(l, c.xflagPerm()) + if p.space() { + p.xtake("(") + l = []string{p.xflagPerm()} + for p.space() { + l = append(l, p.xflagPerm()) } - c.xtake(")") + p.xtake(")") } - codeArg = CodeList{W, l} - case "UIDNEXT", "UIDVALIDITY", "UNSEEN": - c.xspace() - codeArg = CodeUint{W, c.xnzuint32()} + return CodePermanentFlags(l) + case "UIDNEXT": + p.xspace() + return CodeUIDNext(p.xnzuint32()) + case "UIDVALIDITY": + p.xspace() + return CodeUIDValidity(p.xnzuint32()) + case "UNSEEN": + p.xspace() + return CodeUnseen(p.xnzuint32()) case "APPENDUID": - c.xspace() - destUIDValidity := c.xnzuint32() - c.xspace() - uids := c.xuidrange() - codeArg = CodeAppendUID{destUIDValidity, uids} + p.xspace() + destUIDValidity := p.xnzuint32() + p.xspace() + uids := p.xuidrange() + return CodeAppendUID{destUIDValidity, uids} case "COPYUID": - c.xspace() - destUIDValidity := c.xnzuint32() - c.xspace() - from := c.xuidset() - c.xspace() - to := c.xuidset() - codeArg = CodeCopyUID{destUIDValidity, from, to} + p.xspace() + destUIDValidity := p.xnzuint32() + p.xspace() + from := p.xuidset() + p.xspace() + to := p.xuidset() + return CodeCopyUID{destUIDValidity, from, to} case "HIGHESTMODSEQ": - c.xspace() - codeArg = CodeHighestModSeq(c.xint64()) + p.xspace() + return CodeHighestModSeq(p.xint64()) case "MODIFIED": - c.xspace() - modified := c.xuidset() - codeArg = CodeModified(NumSet{Ranges: modified}) + p.xspace() + modified := p.xuidset() + return CodeModified(NumSet{Ranges: modified}) case "INPROGRESS": // ../rfc/9585:238 var tag string var current, goal *uint32 - if c.space() { - c.xtake("(") - tag = c.xquoted() - c.xspace() - if c.peek('n') || c.peek('N') { - c.xtake("nil") + if p.space() { + p.xtake("(") + tag = p.xquoted() + p.xspace() + if p.peek('n') || p.peek('N') { + p.xtake("nil") } else { - v := c.xuint32() + v := p.xuint32() current = &v } - c.xspace() - if c.peek('n') || c.peek('N') { - c.xtake("nil") + p.xspace() + if p.peek('n') || p.peek('N') { + p.xtake("nil") } else { - v := c.xnzuint32() + v := p.xnzuint32() goal = &v } - c.xtake(")") + p.xtake(")") } - codeArg = CodeInProgress{tag, current, goal} + return CodeInProgress{tag, current, goal} case "BADEVENT": // ../rfc/5465:1033 - c.xspace() - c.xtake("(") + p.xspace() + p.xtake("(") var l []string for { - s := c.xatom() + s := p.xatom() l = append(l, s) - if !c.space() { + if !p.space() { break } } - c.xtake(")") - codeArg = CodeBadEvent(l) + p.xtake(")") + return CodeBadEvent(l) + + case "METADATA": + p.xspace() + if !p.take('(') { + p.xtake("LONGENTRIES") + p.xspace() + num := p.xuint32() + return CodeMetadataLongEntries(num) + } + w := strings.ToUpper(p.xatom()) + switch w { + case "MAXSIZE": + p.xspace() + num := p.xuint32() + p.xtake(")") + return CodeMetadataMaxSize(num) + case "TOOMANY": + p.xtake(")") + return CodeMetadataTooMany{} + case "NOPRIVATE": + p.xtake(")") + return CodeMetadataNoPrivate{} + } + p.xerrorf("parsing METADATA response code, got %q, expected one of MAXSIZE, TOOMANY, NOPRIVATE", w) + panic("not reached") + + // Known codes without parameters. + case "ALERT", + "PARSE", + "READ-ONLY", + "READ-WRITE", + "TRYCREATE", + "UIDNOTSTICKY", + "UNAVAILABLE", + "AUTHENTICATIONFAILED", + "AUTHORIZATIONFAILED", + "EXPIRED", + "PRIVACYREQUIRED", + "CONTACTADMIN", + "NOPERM", + "INUSE", + "EXPUNGEISSUED", + "CORRUPTION", + "SERVERBUG", + "CLIENTBUG", + "CANNOT", + "LIMIT", + "ALREADYEXISTS", + "NONEXISTENT", + "NOTSAVED", + "HASCHILDREN", + "CLOSED", + "UNKNOWN-CTE", + "OVERQUOTA", // ../rfc/9208:472 + "COMPRESSIONACTIVE", // ../rfc/4978:143 + "NOTIFICATIONOVERFLOW", // ../rfc/5465:1023 + "UIDREQUIRED": // ../rfc/9586:136 + return CodeWord(W) + + default: + var args []string + for p.space() { + arg := "" + for !p.peek(' ') && !p.peek(']') { + arg += string(rune(p.xbyte())) + } + args = append(args, arg) + } + if len(args) == 0 { + return CodeWord(W) + } + return CodeParams{W, args} } - return W, codeArg } -func (c *Conn) xbyte() byte { - b, err := c.readbyte() - c.xcheckf(err, "read byte") +func (p *Proto) xbyte() byte { + b, err := p.readbyte() + p.xcheckf(err, "read byte") return b } // take until b is seen. don't take b itself. -func (c *Conn) xtakeuntil(b byte) string { +func (p *Proto) xtakeuntil(b byte) string { var s string for { - x, err := c.readbyte() - c.xcheckf(err, "read byte") + x, err := p.readbyte() + p.xcheckf(err, "read byte") if x == b { - c.xunreadbyte() + p.xunreadbyte() return s } s += string(rune(x)) } } -func (c *Conn) xdigits() string { +func (p *Proto) xdigits() string { var s string for { - b, err := c.readbyte() + b, err := p.readbyte() if err == nil && (b >= '0' && b <= '9') { s += string(rune(b)) continue } - c.xunreadbyte() + p.xunreadbyte() return s } } -func (c *Conn) peekdigit() bool { - if b, err := c.readbyte(); err == nil { - c.xunreadbyte() +func (p *Proto) peekdigit() bool { + if b, err := p.readbyte(); err == nil { + p.xunreadbyte() return b >= '0' && b <= '9' } return false } -func (c *Conn) xint32() int32 { - s := c.xdigits() +func (p *Proto) xint32() int32 { + s := p.xdigits() num, err := strconv.ParseInt(s, 10, 32) - c.xcheckf(err, "parsing int32") + p.xcheckf(err, "parsing int32") return int32(num) } -func (c *Conn) xint64() int64 { - s := c.xdigits() +func (p *Proto) xint64() int64 { + s := p.xdigits() num, err := strconv.ParseInt(s, 10, 63) - c.xcheckf(err, "parsing int64") + p.xcheckf(err, "parsing int64") return num } -func (c *Conn) xuint32() uint32 { - s := c.xdigits() +func (p *Proto) xuint32() uint32 { + s := p.xdigits() num, err := strconv.ParseUint(s, 10, 32) - c.xcheckf(err, "parsing uint32") + p.xcheckf(err, "parsing uint32") return uint32(num) } -func (c *Conn) xnzuint32() uint32 { - v := c.xuint32() +func (p *Proto) xnzuint32() uint32 { + v := p.xuint32() if v == 0 { - c.xerrorf("got 0, expected nonzero uint") + p.xerrorf("got 0, expected nonzero uint") } return v } // todo: replace with proper parsing. -func (c *Conn) xnonspace() string { +func (p *Proto) xnonspace() string { var s string - for !c.peek(' ') && !c.peek('\r') && !c.peek('\n') { - s += string(rune(c.xbyte())) + for !p.peek(' ') && !p.peek('\r') && !p.peek('\n') { + s += string(rune(p.xbyte())) } if s == "" { - c.xerrorf("expected non-space") + p.xerrorf("expected non-space") } return s } // todo: replace with proper parsing -func (c *Conn) xword() string { - return c.xatom() +func (p *Proto) xword() string { + return p.xatom() } // "*" SP is already consumed // ../rfc/9051:6868 -func (c *Conn) xuntagged() Untagged { - w := c.xnonspace() +func (p *Proto) xuntagged() Untagged { + w := p.xnonspace() W := strings.ToUpper(w) switch W { case "PREAUTH": - c.xspace() - r := UntaggedPreauth(c.xrespText()) - c.xcrlf() + p.xspace() + code, text := p.xrespText() + r := UntaggedPreauth{code, text} + p.xcrlf() return r case "BYE": - c.xspace() - r := UntaggedBye(c.xrespText()) - c.xcrlf() + p.xspace() + code, text := p.xrespText() + r := UntaggedBye{code, text} + p.xcrlf() return r case "OK", "NO", "BAD": - c.xspace() - r := UntaggedResult(c.xresult(Status(W))) - c.xcrlf() + p.xspace() + r := UntaggedResult(p.xresult(Status(W))) + p.xcrlf() return r case "CAPABILITY": // ../rfc/9051:6427 - var caps []string - for c.space() { - caps = append(caps, c.xnonspace()) + var caps []Capability + for p.space() { + s := p.xnonspace() + s = strings.ToUpper(s) + cc := Capability(s) + caps = append(caps, cc) } - c.CapAvailable = map[Capability]struct{}{} - for _, cap := range caps { - cap = strings.ToUpper(cap) - c.CapAvailable[Capability(cap)] = struct{}{} - } - r := UntaggedCapability(caps) - c.xcrlf() - return r + p.xcrlf() + return UntaggedCapability(caps) case "ENABLED": // ../rfc/9051:6520 - var caps []string - for c.space() { - caps = append(caps, c.xnonspace()) + var caps []Capability + for p.space() { + s := p.xnonspace() + s = strings.ToUpper(s) + cc := Capability(s) + caps = append(caps, cc) } - for _, cap := range caps { - cap = strings.ToUpper(cap) - c.CapEnabled[Capability(cap)] = struct{}{} - } - r := UntaggedEnabled(caps) - c.xcrlf() - return r + p.xcrlf() + return UntaggedEnabled(caps) case "FLAGS": - c.xspace() - r := UntaggedFlags(c.xflagList()) - c.xcrlf() + p.xspace() + r := UntaggedFlags(p.xflagList()) + p.xcrlf() return r case "LIST": - c.xspace() - r := c.xmailboxList() - c.xcrlf() + p.xspace() + r := p.xmailboxList() + p.xcrlf() return r case "STATUS": // ../rfc/9051:6681 - c.xspace() - mailbox := c.xastring() - c.xspace() - c.xtake("(") + p.xspace() + mailbox := p.xastring() + p.xspace() + p.xtake("(") attrs := map[StatusAttr]int64{} - for !c.take(')') { + for !p.take(')') { if len(attrs) > 0 { - c.xspace() + p.xspace() } - s := c.xatom() - c.xspace() + s := p.xatom() + p.xspace() S := StatusAttr(strings.ToUpper(s)) var num int64 // ../rfc/9051:7059 switch S { case "MESSAGES": - num = int64(c.xuint32()) + num = int64(p.xuint32()) case "UIDNEXT": - num = int64(c.xnzuint32()) + num = int64(p.xnzuint32()) case "UIDVALIDITY": - num = int64(c.xnzuint32()) + num = int64(p.xnzuint32()) case "UNSEEN": - num = int64(c.xuint32()) + num = int64(p.xuint32()) case "DELETED": - num = int64(c.xuint32()) + num = int64(p.xuint32()) case "SIZE": - num = c.xint64() + num = p.xint64() case "RECENT": - c.xneedDisabled("RECENT status flag", CapIMAP4rev2) - num = int64(c.xuint32()) + num = int64(p.xuint32()) case "APPENDLIMIT": - if c.peek('n') || c.peek('N') { - c.xtake("nil") + if p.peek('n') || p.peek('N') { + p.xtake("nil") } else { - num = c.xint64() + num = p.xint64() } case "HIGHESTMODSEQ": - num = c.xint64() + num = p.xint64() case "DELETED-STORAGE": - num = c.xint64() + num = p.xint64() default: - c.xerrorf("status: unknown attribute %q", s) + p.xerrorf("status: unknown attribute %q", s) } if _, ok := attrs[S]; ok { - c.xerrorf("status: duplicate attribute %q", s) + p.xerrorf("status: duplicate attribute %q", s) } attrs[S] = num } r := UntaggedStatus{mailbox, attrs} - c.xcrlf() + p.xcrlf() return r case "METADATA": // ../rfc/5464:807 - c.xspace() - mailbox := c.xastring() - c.xspace() - if !c.take('(') { + p.xspace() + mailbox := p.xastring() + p.xspace() + if !p.take('(') { // Unsolicited form, with only annotation keys, not values. var keys []string for { - key := c.xastring() + key := p.xastring() keys = append(keys, key) - if !c.space() { + if !p.space() { break } } - c.xcrlf() + p.xcrlf() return UntaggedMetadataKeys{mailbox, keys} } // Form with values, in response to GETMETADATA command. r := UntaggedMetadataAnnotations{Mailbox: mailbox} for { - key := c.xastring() - c.xspace() + key := p.xastring() + p.xspace() var value []byte var isString bool - if c.take('~') { - value = c.xliteral() - } else if c.peek('"') { - value = []byte(c.xstring()) + if p.take('~') { + value = p.xliteral() + } else if p.peek('"') { + value = []byte(p.xstring()) isString = true // note: the abnf also allows nstring, but that only makes sense when the // production rule is used in the setmetadata command. ../rfc/5464:831 } else { // For response to extended list. - c.xtake("nil") + p.xtake("nil") } r.Annotations = append(r.Annotations, Annotation{key, isString, value}) - if c.take(')') { + if p.take(')') { break } - c.xspace() + p.xspace() } - c.xcrlf() + p.xcrlf() return r case "NAMESPACE": // ../rfc/9051:6778 - c.xspace() - personal := c.xnamespace() - c.xspace() - other := c.xnamespace() - c.xspace() - shared := c.xnamespace() + p.xspace() + personal := p.xnamespace() + p.xspace() + other := p.xnamespace() + p.xspace() + shared := p.xnamespace() r := UntaggedNamespace{personal, other, shared} - c.xcrlf() + p.xcrlf() return r case "SEARCH": // ../rfc/9051:6809 - c.xneedDisabled("untagged SEARCH response", CapIMAP4rev2) var nums []uint32 - for c.space() { + for p.space() { // ../rfc/7162:2557 - if c.take('(') { - c.xtake("MODSEQ") - c.xspace() - modseq := c.xint64() - c.xtake(")") - c.xcrlf() + if p.take('(') { + p.xtake("MODSEQ") + p.xspace() + modseq := p.xint64() + p.xtake(")") + p.xcrlf() return UntaggedSearchModSeq{nums, modseq} } - nums = append(nums, c.xnzuint32()) + nums = append(nums, p.xnzuint32()) } r := UntaggedSearch(nums) - c.xcrlf() + p.xcrlf() return r case "ESEARCH": - r := c.xesearchResponse() - c.xcrlf() + r := p.xesearchResponse() + p.xcrlf() return r case "LSUB": - c.xneedDisabled("untagged LSUB response", CapIMAP4rev2) - r := c.xlsub() - c.xcrlf() + r := p.xlsub() + p.xcrlf() return r case "ID": // ../rfc/2971:243 - c.xspace() + p.xspace() var params map[string]string - if c.take('(') { + if p.take('(') { params = map[string]string{} - for !c.take(')') { + for !p.take(')') { if len(params) > 0 { - c.xspace() + p.xspace() } - k := c.xstring() - c.xspace() - v := c.xnilString() + k := p.xstring() + p.xspace() + v := p.xnilString() if _, ok := params[k]; ok { - c.xerrorf("duplicate key %q", k) + p.xerrorf("duplicate key %q", k) } params[k] = v } } else { - c.xtake("nil") + p.xtake("nil") } - c.xcrlf() + p.xcrlf() return UntaggedID(params) // ../rfc/7162:2623 case "VANISHED": - c.xspace() + p.xspace() var earlier bool - if c.take('(') { - c.xtake("EARLIER") - c.xtake(")") - c.xspace() + if p.take('(') { + p.xtake("EARLIER") + p.xtake(")") + p.xspace() earlier = true } - uids := c.xuidset() - c.xcrlf() + uids := p.xuidset() + p.xcrlf() return UntaggedVanished{earlier, NumSet{Ranges: uids}} // ../rfc/9208:668 ../2087:242 case "QUOTAROOT": - c.xspace() - c.xastring() + p.xspace() + p.xastring() var roots []string - for c.space() { - root := c.xastring() + for p.space() { + root := p.xastring() roots = append(roots, root) } - c.xcrlf() + p.xcrlf() return UntaggedQuotaroot(roots) // ../rfc/9208:666 ../rfc/2087:239 case "QUOTA": - c.xspace() - root := c.xastring() - c.xspace() - c.xtake("(") + p.xspace() + root := p.xastring() + p.xspace() + p.xtake("(") xresource := func() QuotaResource { - name := c.xatom() - c.xspace() - usage := c.xint64() - c.xspace() - limit := c.xint64() + name := p.xatom() + p.xspace() + usage := p.xint64() + p.xspace() + limit := p.xint64() return QuotaResource{QuotaResourceName(strings.ToUpper(name)), usage, limit} } seen := map[QuotaResourceName]bool{} l := []QuotaResource{xresource()} seen[l[0].Name] = true - for c.space() { + for p.space() { res := xresource() if seen[res.Name] { - c.xerrorf("duplicate resource name %q", res.Name) + p.xerrorf("duplicate resource name %q", res.Name) } seen[res.Name] = true l = append(l, res) } - c.xtake(")") - c.xcrlf() + p.xtake(")") + p.xcrlf() return UntaggedQuota{root, l} default: v, err := strconv.ParseUint(w, 10, 32) if err == nil { num := uint32(v) - c.xspace() - w = c.xword() + p.xspace() + w = p.xword() W = strings.ToUpper(w) switch W { case "FETCH", "UIDFETCH": if num == 0 { - c.xerrorf("invalid zero number for untagged fetch response") + p.xerrorf("invalid zero number for untagged fetch response") } - c.xspace() - attrs := c.xfetch() - c.xcrlf() + p.xspace() + attrs := p.xfetch() + p.xcrlf() if W == "UIDFETCH" { return UntaggedUIDFetch{num, attrs} } @@ -669,213 +705,227 @@ func (c *Conn) xuntagged() Untagged { case "EXPUNGE": if num == 0 { - c.xerrorf("invalid zero number for untagged expunge response") + p.xerrorf("invalid zero number for untagged expunge response") } - c.xcrlf() + p.xcrlf() return UntaggedExpunge(num) case "EXISTS": - c.xcrlf() + p.xcrlf() return UntaggedExists(num) case "RECENT": - c.xneedDisabled("should not send RECENT in IMAP4rev2", CapIMAP4rev2) - c.xcrlf() + p.xcrlf() return UntaggedRecent(num) default: - c.xerrorf("unknown untagged numbered response %q", w) + p.xerrorf("unknown untagged numbered response %q", w) panic("not reached") } } - c.xerrorf("unknown untagged response %q", w) + p.xerrorf("unknown untagged response %q", w) } panic("not reached") } // ../rfc/3501:4864 ../rfc/9051:6742 // Already parsed: "*" SP nznumber SP "FETCH" SP -func (c *Conn) xfetch() []FetchAttr { - c.xtake("(") - attrs := []FetchAttr{c.xmsgatt1()} - for c.space() { - attrs = append(attrs, c.xmsgatt1()) +func (p *Proto) xfetch() []FetchAttr { + p.xtake("(") + attrs := []FetchAttr{p.xmsgatt1()} + for p.space() { + attrs = append(attrs, p.xmsgatt1()) } - c.xtake(")") + p.xtake(")") return attrs } // ../rfc/9051:6746 -func (c *Conn) xmsgatt1() FetchAttr { +func (p *Proto) xmsgatt1() FetchAttr { f := "" for { - b := c.xbyte() + b := p.xbyte() if b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b >= '0' && b <= '9' || b == '.' { f += string(rune(b)) continue } - c.xunreadbyte() + p.xunreadbyte() break } F := strings.ToUpper(f) switch F { case "FLAGS": - c.xspace() - c.xtake("(") + p.xspace() + p.xtake("(") var flags []string - if !c.take(')') { - flags = []string{c.xflag()} - for c.space() { - flags = append(flags, c.xflag()) + if !p.take(')') { + flags = []string{p.xflag()} + for p.space() { + flags = append(flags, p.xflag()) } - c.xtake(")") + p.xtake(")") } return FetchFlags(flags) case "ENVELOPE": - c.xspace() - return FetchEnvelope(c.xenvelope()) + p.xspace() + return FetchEnvelope(p.xenvelope()) case "INTERNALDATE": - c.xspace() - s := c.xquoted() + p.xspace() + s := p.xquoted() v, err := time.Parse("_2-Jan-2006 15:04:05 -0700", s) - c.xcheckf(err, "parsing internaldate") + p.xcheckf(err, "parsing internaldate") return FetchInternalDate{v} case "SAVEDATE": - c.xspace() + p.xspace() var t *time.Time - if c.peek('"') { - s := c.xquoted() + if p.peek('"') { + s := p.xquoted() v, err := time.Parse("_2-Jan-2006 15:04:05 -0700", s) - c.xcheckf(err, "parsing savedate") + p.xcheckf(err, "parsing savedate") t = &v } else { - c.xtake("nil") + p.xtake("nil") } return FetchSaveDate{t} case "RFC822.SIZE": - c.xspace() - return FetchRFC822Size(c.xint64()) + p.xspace() + return FetchRFC822Size(p.xint64()) case "RFC822": - c.xspace() - s := c.xnilString() + p.xspace() + s := p.xnilString() return FetchRFC822(s) case "RFC822.HEADER": - c.xspace() - s := c.xnilString() + p.xspace() + s := p.xnilString() return FetchRFC822Header(s) case "RFC822.TEXT": - c.xspace() - s := c.xnilString() + p.xspace() + s := p.xnilString() return FetchRFC822Text(s) case "BODY": - if c.space() { - return FetchBodystructure{F, c.xbodystructure(false)} + if p.space() { + return FetchBodystructure{F, p.xbodystructure(false)} } - c.record = true - section := c.xsection() + p.record = true + section := p.xsection() var offset int32 - if c.take('<') { - offset = c.xint32() - c.xtake(">") + if p.take('<') { + offset = p.xint32() + p.xtake(">") } - F += c.recorded() - c.xspace() - body := c.xnilString() + F += p.recorded() + p.xspace() + body := p.xnilString() return FetchBody{F, section, offset, body} case "BODYSTRUCTURE": - c.xspace() - return FetchBodystructure{F, c.xbodystructure(true)} + p.xspace() + return FetchBodystructure{F, p.xbodystructure(true)} case "BINARY": - c.record = true - nums := c.xsectionBinary() - F += c.recorded() - c.xspace() - buf := c.xnilStringLiteral8() + p.record = true + nums := p.xsectionBinary() + F += p.recorded() + p.xspace() + buf := p.xnilStringLiteral8() return FetchBinary{F, nums, string(buf)} case "BINARY.SIZE": - c.record = true - nums := c.xsectionBinary() - F += c.recorded() - c.xspace() - size := c.xint64() + p.record = true + nums := p.xsectionBinary() + F += p.recorded() + p.xspace() + size := p.xint64() return FetchBinarySize{F, nums, size} case "UID": - c.xspace() - return FetchUID(c.xuint32()) + p.xspace() + return FetchUID(p.xuint32()) case "MODSEQ": // ../rfc/7162:2488 - c.xspace() - c.xtake("(") - modseq := c.xint64() - c.xtake(")") + p.xspace() + p.xtake("(") + modseq := p.xint64() + p.xtake(")") return FetchModSeq(modseq) case "PREVIEW": // ../rfc/8970:348 - c.xspace() + p.xspace() var preview *string - if c.peek('n') || c.peek('N') { - c.xtake("nil") + if p.peek('n') || p.peek('N') { + p.xtake("nil") } else { - s := c.xstring() + s := p.xstring() preview = &s } return FetchPreview{preview} } - c.xerrorf("unknown fetch attribute %q", f) + p.xerrorf("unknown fetch attribute %q", f) panic("not reached") } -func (c *Conn) xnilString() string { - if c.peek('"') { - return c.xquoted() - } else if c.peek('{') { - return string(c.xliteral()) +func (p *Proto) xnilString() string { + if p.peek('"') { + return p.xquoted() + } else if p.peek('{') { + return string(p.xliteral()) } else { - c.xtake("nil") + p.xtake("nil") return "" } } -func (c *Conn) xstring() string { - if c.peek('"') { - return c.xquoted() - } - return string(c.xliteral()) +func ptr[T any](v T) *T { + return &v } -func (c *Conn) xastring() string { - if c.peek('"') { - return c.xquoted() - } else if c.peek('{') { - return string(c.xliteral()) +func (p *Proto) xnilptrString() *string { + if p.peek('"') { + return ptr(p.xquoted()) + } else if p.peek('{') { + return ptr(string(p.xliteral())) + } else { + p.xtake("nil") + return nil } - return c.xatom() } -func (c *Conn) xatom() string { +func (p *Proto) xstring() string { + if p.peek('"') { + return p.xquoted() + } + return string(p.xliteral()) +} + +func (p *Proto) xastring() string { + if p.peek('"') { + return p.xquoted() + } else if p.peek('{') { + return string(p.xliteral()) + } + return p.xatom() +} + +func (p *Proto) xatom() string { var s string for { - b, err := c.readbyte() - c.xcheckf(err, "read byte for atom") + b, err := p.readbyte() + p.xcheckf(err, "read byte for atom") if b <= ' ' || strings.IndexByte("(){%*\"\\]", b) >= 0 { - c.xunreadbyte() + p.xunreadbyte() if s == "" { - c.xerrorf("expected atom") + p.xerrorf("expected atom") } return s } @@ -884,17 +934,17 @@ func (c *Conn) xatom() string { } // ../rfc/9051:6856 ../rfc/6855:153 -func (c *Conn) xquoted() string { - c.xtake(`"`) +func (p *Proto) xquoted() string { + p.xtake(`"`) s := "" - for !c.take('"') { - r, err := c.readrune() - c.xcheckf(err, "reading rune in quoted string") + for !p.take('"') { + r, err := p.readrune() + p.xcheckf(err, "reading rune in quoted string") if r == '\\' { - r, err = c.readrune() - c.xcheckf(err, "reading escaped char in quoted string") + r, err = p.readrune() + p.xcheckf(err, "reading escaped char in quoted string") if r != '\\' && r != '"' { - c.xerrorf("quoted char not backslash or dquote: %c", r) + p.xerrorf("quoted char not backslash or dquote: %c", r) } } // todo: probably refuse some more chars. like \0 and all ctl and backspace. @@ -903,459 +953,464 @@ func (c *Conn) xquoted() string { return s } -func (c *Conn) xliteral() []byte { - c.xtake("{") - size := c.xint64() - sync := c.take('+') - c.xtake("}") - c.xcrlf() +func (p *Proto) xliteral() []byte { + p.xtake("{") + size := p.xint64() + sync := p.take('+') + p.xtake("}") + p.xcrlf() // todo: for some literals, read as tracedata if size > 1<<20 { - c.xerrorf("refusing to read more than 1MB: %d", size) + p.xerrorf("refusing to read more than 1MB: %d", size) } if sync { - fmt.Fprintf(c.xbw, "+ ok\r\n") - c.xflush() + if p.xbw == nil { + p.xerrorf("cannot parse literals without connection") + } + fmt.Fprintf(p.xbw, "+ ok\r\n") + p.xflush() } buf := make([]byte, int(size)) - defer c.xtraceread(mlog.LevelTracedata)() - _, err := io.ReadFull(c.br, buf) - c.xcheckf(err, "reading data for literal") - c.xtraceread(mlog.LevelTrace) + defer p.xtraceread(mlog.LevelTracedata)() + _, err := io.ReadFull(p.br, buf) + p.xcheckf(err, "reading data for literal") + p.xtraceread(mlog.LevelTrace) return buf } // ../rfc/9051:6565 // todo: stricter -func (c *Conn) xflag0(allowPerm bool) string { +func (p *Proto) xflag0(allowPerm bool) string { s := "" - if c.take('\\') { + if p.take('\\') { s = `\` - if allowPerm && c.take('*') { + if allowPerm && p.take('*') { return `\*` } - } else if c.take('$') { + } else if p.take('$') { s = "$" } - s += c.xatom() + s += p.xatom() return s } -func (c *Conn) xflag() string { - return c.xflag0(false) +func (p *Proto) xflag() string { + return p.xflag0(false) } -func (c *Conn) xflagPerm() string { - return c.xflag0(true) +func (p *Proto) xflagPerm() string { + return p.xflag0(true) } -func (c *Conn) xsection() string { - c.xtake("[") - s := c.xtakeuntil(']') - c.xtake("]") +func (p *Proto) xsection() string { + p.xtake("[") + s := p.xtakeuntil(']') + p.xtake("]") return s } -func (c *Conn) xsectionBinary() []uint32 { - c.xtake("[") +func (p *Proto) xsectionBinary() []uint32 { + p.xtake("[") var nums []uint32 - for !c.take(']') { + for !p.take(']') { if len(nums) > 0 { - c.xtake(".") + p.xtake(".") } - nums = append(nums, c.xnzuint32()) + nums = append(nums, p.xnzuint32()) } return nums } -func (c *Conn) xnilStringLiteral8() []byte { +func (p *Proto) xnilStringLiteral8() []byte { // todo: should make difference for literal8 and literal from string, which bytes are allowed - if c.take('~') || c.peek('{') { - return c.xliteral() + if p.take('~') || p.peek('{') { + return p.xliteral() } - return []byte(c.xnilString()) + return []byte(p.xnilString()) } // ../rfc/9051:6355 -func (c *Conn) xbodystructure(extensibleForm bool) any { - c.xtake("(") - if c.peek('(') { +func (p *Proto) xbodystructure(extensibleForm bool) any { + p.xtake("(") + if p.peek('(') { // ../rfc/9051:6411 - parts := []any{c.xbodystructure(extensibleForm)} - for c.peek('(') { - parts = append(parts, c.xbodystructure(extensibleForm)) + parts := []any{p.xbodystructure(extensibleForm)} + for p.peek('(') { + parts = append(parts, p.xbodystructure(extensibleForm)) } - c.xspace() - mediaSubtype := c.xstring() + p.xspace() + mediaSubtype := p.xstring() var ext *BodyExtensionMpart - if extensibleForm && c.space() { - ext = c.xbodyExtMpart() + if extensibleForm && p.space() { + ext = p.xbodyExtMpart() } - c.xtake(")") + p.xtake(")") return BodyTypeMpart{parts, mediaSubtype, ext} } // todo: verify the media(sub)type is valid for returned data. var ext *BodyExtension1Part - mediaType := c.xstring() - c.xspace() - mediaSubtype := c.xstring() - c.xspace() - bodyFields := c.xbodyFields() - if !c.space() { + mediaType := p.xstring() + p.xspace() + mediaSubtype := p.xstring() + p.xspace() + bodyFields := p.xbodyFields() + if !p.space() { // Basic type without extension. - c.xtake(")") + p.xtake(")") return BodyTypeBasic{mediaType, mediaSubtype, bodyFields, nil} } - if c.peek('(') { + if p.peek('(') { // ../rfc/9051:6415 - envelope := c.xenvelope() - c.xspace() - bodyStructure := c.xbodystructure(extensibleForm) - c.xspace() - lines := c.xint64() - if extensibleForm && c.space() { - ext = c.xbodyExt1Part() + envelope := p.xenvelope() + p.xspace() + bodyStructure := p.xbodystructure(extensibleForm) + p.xspace() + lines := p.xint64() + if extensibleForm && p.space() { + ext = p.xbodyExt1Part() } - c.xtake(")") + p.xtake(")") return BodyTypeMsg{mediaType, mediaSubtype, bodyFields, envelope, bodyStructure, lines, ext} } if !strings.EqualFold(mediaType, "text") { if !extensibleForm { - c.xerrorf("body result, basic type, with disallowed extensible form") + p.xerrorf("body result, basic type, with disallowed extensible form") } - ext = c.xbodyExt1Part() + ext = p.xbodyExt1Part() // ../rfc/9051:6407 - c.xtake(")") + p.xtake(")") return BodyTypeBasic{mediaType, mediaSubtype, bodyFields, ext} } // ../rfc/9051:6418 - lines := c.xint64() - if extensibleForm && c.space() { - ext = c.xbodyExt1Part() + lines := p.xint64() + if extensibleForm && p.space() { + ext = p.xbodyExt1Part() } - c.xtake(")") + p.xtake(")") return BodyTypeText{mediaType, mediaSubtype, bodyFields, lines, ext} } // ../rfc/9051:6376 ../rfc/3501:4604 -func (c *Conn) xbodyFields() BodyFields { - params := c.xbodyFldParam() - c.xspace() - contentID := c.xnilString() - c.xspace() - contentDescr := c.xnilString() - c.xspace() - cte := c.xnilString() - c.xspace() - octets := c.xint32() +func (p *Proto) xbodyFields() BodyFields { + params := p.xbodyFldParam() + p.xspace() + contentID := p.xnilString() + p.xspace() + contentDescr := p.xnilString() + p.xspace() + cte := p.xnilString() + p.xspace() + octets := p.xint32() return BodyFields{params, contentID, contentDescr, cte, octets} } // ../rfc/9051:6371 ../rfc/3501:4599 -func (c *Conn) xbodyExtMpart() (ext *BodyExtensionMpart) { +func (p *Proto) xbodyExtMpart() (ext *BodyExtensionMpart) { ext = &BodyExtensionMpart{} - ext.Params = c.xbodyFldParam() - if !c.space() { + ext.Params = p.xbodyFldParam() + if !p.space() { return } - ext.Disposition, ext.DispositionParams = c.xbodyFldDsp() - if !c.space() { + disp, dispParams := p.xbodyFldDsp() + ext.Disposition, ext.DispositionParams = &disp, &dispParams + if !p.space() { return } - ext.Language = c.xbodyFldLang() - if !c.space() { + ext.Language = ptr(p.xbodyFldLang()) + if !p.space() { return } - ext.Location = c.xbodyFldLoc() - for c.space() { - ext.More = append(ext.More, c.xbodyExtension()) + ext.Location = ptr(p.xbodyFldLoc()) + for p.space() { + ext.More = append(ext.More, p.xbodyExtension()) } return } // ../rfc/9051:6366 ../rfc/3501:4584 -func (c *Conn) xbodyExt1Part() (ext *BodyExtension1Part) { +func (p *Proto) xbodyExt1Part() (ext *BodyExtension1Part) { ext = &BodyExtension1Part{} - ext.MD5 = c.xnilString() - if !c.space() { + ext.MD5 = p.xnilptrString() + if !p.space() { return } - ext.Disposition, ext.DispositionParams = c.xbodyFldDsp() - if !c.space() { + disp, dispParams := p.xbodyFldDsp() + ext.Disposition, ext.DispositionParams = &disp, &dispParams + if !p.space() { return } - ext.Language = c.xbodyFldLang() - if !c.space() { + ext.Language = ptr(p.xbodyFldLang()) + if !p.space() { return } - ext.Location = c.xbodyFldLoc() - for c.space() { - ext.More = append(ext.More, c.xbodyExtension()) + ext.Location = ptr(p.xbodyFldLoc()) + for p.space() { + ext.More = append(ext.More, p.xbodyExtension()) } return } // ../rfc/9051:6401 ../rfc/3501:4626 -func (c *Conn) xbodyFldParam() [][2]string { - if c.take('(') { - k := c.xstring() - c.xspace() - v := c.xstring() +func (p *Proto) xbodyFldParam() [][2]string { + if p.take('(') { + k := p.xstring() + p.xspace() + v := p.xstring() l := [][2]string{{k, v}} - for c.space() { - k = c.xstring() - c.xspace() - v = c.xstring() + for p.space() { + k = p.xstring() + p.xspace() + v = p.xstring() l = append(l, [2]string{k, v}) } - c.xtake(")") + p.xtake(")") return l } - c.xtake("nil") + p.xtake("nil") return nil } // ../rfc/9051:6381 ../rfc/3501:4609 -func (c *Conn) xbodyFldDsp() (string, [][2]string) { - if !c.take('(') { - c.xtake("nil") - return "", nil +func (p *Proto) xbodyFldDsp() (*string, [][2]string) { + if !p.take('(') { + p.xtake("nil") + return nil, nil } - disposition := c.xstring() - c.xspace() - param := c.xbodyFldParam() - c.xtake(")") - return disposition, param + disposition := p.xstring() + p.xspace() + param := p.xbodyFldParam() + p.xtake(")") + return ptr(disposition), param } // ../rfc/9051:6391 ../rfc/3501:4616 -func (c *Conn) xbodyFldLang() (lang []string) { - if c.take('(') { - lang = []string{c.xstring()} - for c.space() { - lang = append(lang, c.xstring()) +func (p *Proto) xbodyFldLang() (lang []string) { + if p.take('(') { + lang = []string{p.xstring()} + for p.space() { + lang = append(lang, p.xstring()) } - c.xtake(")") + p.xtake(")") return lang } - if c.peekstring() { - return []string{c.xstring()} + if p.peekstring() { + return []string{p.xstring()} } - c.xtake("nil") + p.xtake("nil") return nil } // ../rfc/9051:6393 ../rfc/3501:4618 -func (c *Conn) xbodyFldLoc() string { - return c.xnilString() +func (p *Proto) xbodyFldLoc() *string { + return p.xnilptrString() } // ../rfc/9051:6357 ../rfc/3501:4575 -func (c *Conn) xbodyExtension() (ext BodyExtension) { - if c.take('(') { +func (p *Proto) xbodyExtension() (ext BodyExtension) { + if p.take('(') { for { - ext.More = append(ext.More, c.xbodyExtension()) - if !c.space() { + ext.More = append(ext.More, p.xbodyExtension()) + if !p.space() { break } } - c.xtake(")") - } else if c.peekdigit() { - num := c.xint64() + p.xtake(")") + } else if p.peekdigit() { + num := p.xint64() ext.Number = &num - } else if c.peekstring() { - str := c.xstring() + } else if p.peekstring() { + str := p.xstring() ext.String = &str } else { - c.xtake("nil") + p.xtake("nil") } return ext } // ../rfc/9051:6522 -func (c *Conn) xenvelope() Envelope { - c.xtake("(") - date := c.xnilString() - c.xspace() - subject := c.xnilString() - c.xspace() - from := c.xaddresses() - c.xspace() - sender := c.xaddresses() - c.xspace() - replyTo := c.xaddresses() - c.xspace() - to := c.xaddresses() - c.xspace() - cc := c.xaddresses() - c.xspace() - bcc := c.xaddresses() - c.xspace() - inReplyTo := c.xnilString() - c.xspace() - messageID := c.xnilString() - c.xtake(")") +func (p *Proto) xenvelope() Envelope { + p.xtake("(") + date := p.xnilString() + p.xspace() + subject := p.xnilString() + p.xspace() + from := p.xaddresses() + p.xspace() + sender := p.xaddresses() + p.xspace() + replyTo := p.xaddresses() + p.xspace() + to := p.xaddresses() + p.xspace() + cc := p.xaddresses() + p.xspace() + bcc := p.xaddresses() + p.xspace() + inReplyTo := p.xnilString() + p.xspace() + messageID := p.xnilString() + p.xtake(")") return Envelope{date, subject, from, sender, replyTo, to, cc, bcc, inReplyTo, messageID} } // ../rfc/9051:6526 -func (c *Conn) xaddresses() []Address { - if !c.take('(') { - c.xtake("nil") +func (p *Proto) xaddresses() []Address { + if !p.take('(') { + p.xtake("nil") return nil } - l := []Address{c.xaddress()} - for !c.take(')') { - l = append(l, c.xaddress()) + l := []Address{p.xaddress()} + for !p.take(')') { + l = append(l, p.xaddress()) } return l } // ../rfc/9051:6303 -func (c *Conn) xaddress() Address { - c.xtake("(") - name := c.xnilString() - c.xspace() - adl := c.xnilString() - c.xspace() - mailbox := c.xnilString() - c.xspace() - host := c.xnilString() - c.xtake(")") +func (p *Proto) xaddress() Address { + p.xtake("(") + name := p.xnilString() + p.xspace() + adl := p.xnilString() + p.xspace() + mailbox := p.xnilString() + p.xspace() + host := p.xnilString() + p.xtake(")") return Address{name, adl, mailbox, host} } // ../rfc/9051:6584 -func (c *Conn) xflagList() []string { - c.xtake("(") +func (p *Proto) xflagList() []string { + p.xtake("(") var l []string - if !c.take(')') { - l = []string{c.xflag()} - for c.space() { - l = append(l, c.xflag()) + if !p.take(')') { + l = []string{p.xflag()} + for p.space() { + l = append(l, p.xflag()) } - c.xtake(")") + p.xtake(")") } return l } // ../rfc/9051:6690 -func (c *Conn) xmailboxList() UntaggedList { - c.xtake("(") +func (p *Proto) xmailboxList() UntaggedList { + p.xtake("(") var flags []string - if !c.peek(')') { - flags = append(flags, c.xflag()) - for c.space() { - flags = append(flags, c.xflag()) + if !p.peek(')') { + flags = append(flags, p.xflag()) + for p.space() { + flags = append(flags, p.xflag()) } } - c.xtake(")") - c.xspace() + p.xtake(")") + p.xspace() var quoted string var b byte - if c.peek('"') { - quoted = c.xquoted() + if p.peek('"') { + quoted = p.xquoted() if len(quoted) != 1 { - c.xerrorf("mailbox-list has multichar quoted part: %q", quoted) + p.xerrorf("mailbox-list has multichar quoted part: %q", quoted) } b = byte(quoted[0]) - } else if !c.peek(' ') { - c.xtake("nil") + } else if !p.peek(' ') { + p.xtake("nil") } - c.xspace() - mailbox := c.xastring() + p.xspace() + mailbox := p.xastring() ul := UntaggedList{flags, b, mailbox, nil, ""} - if c.space() { - c.xtake("(") - if !c.peek(')') { - c.xmboxListExtendedItem(&ul) - for c.space() { - c.xmboxListExtendedItem(&ul) + if p.space() { + p.xtake("(") + if !p.peek(')') { + p.xmboxListExtendedItem(&ul) + for p.space() { + p.xmboxListExtendedItem(&ul) } } - c.xtake(")") + p.xtake(")") } return ul } // ../rfc/9051:6699 -func (c *Conn) xmboxListExtendedItem(ul *UntaggedList) { - tag := c.xastring() - c.xspace() +func (p *Proto) xmboxListExtendedItem(ul *UntaggedList) { + tag := p.xastring() + p.xspace() if strings.ToUpper(tag) == "OLDNAME" { // ../rfc/9051:6811 - c.xtake("(") - name := c.xastring() - c.xtake(")") + p.xtake("(") + name := p.xastring() + p.xtake(")") ul.OldName = name return } - val := c.xtaggedExtVal() + val := p.xtaggedExtVal() ul.Extended = append(ul.Extended, MboxListExtendedItem{tag, val}) } // ../rfc/9051:7111 -func (c *Conn) xtaggedExtVal() TaggedExtVal { - if c.take('(') { +func (p *Proto) xtaggedExtVal() TaggedExtVal { + if p.take('(') { var r TaggedExtVal - if !c.take(')') { - comp := c.xtaggedExtComp() + if !p.take(')') { + comp := p.xtaggedExtComp() r.Comp = &comp - c.xtake(")") + p.xtake(")") } return r } // We cannot just parse sequence-set, because we also have to accept number/number64. So first look for a number. If it is not, we continue parsing the rest of the sequence set. - b, err := c.readbyte() - c.xcheckf(err, "read byte for tagged-ext-val") + b, err := p.readbyte() + p.xcheckf(err, "read byte for tagged-ext-val") if b < '0' || b > '9' { - c.xunreadbyte() - ss := c.xsequenceSet() + p.xunreadbyte() + ss := p.xsequenceSet() return TaggedExtVal{SeqSet: &ss} } - s := c.xdigits() + s := p.xdigits() num, err := strconv.ParseInt(s, 10, 63) - c.xcheckf(err, "parsing int") - if !c.peek(':') && !c.peek(',') { + p.xcheckf(err, "parsing int") + if !p.peek(':') && !p.peek(',') { // not a larger sequence-set return TaggedExtVal{Number: &num} } var sr NumRange sr.First = uint32(num) - if c.take(':') { + if p.take(':') { var num uint32 - if !c.take('*') { - num = c.xnzuint32() + if !p.take('*') { + num = p.xnzuint32() } sr.Last = &num } - ss := c.xsequenceSet() + ss := p.xsequenceSet() ss.Ranges = append([]NumRange{sr}, ss.Ranges...) return TaggedExtVal{SeqSet: &ss} } // ../rfc/9051:7034 -func (c *Conn) xsequenceSet() NumSet { - if c.take('$') { +func (p *Proto) xsequenceSet() NumSet { + if p.take('$') { return NumSet{SearchResult: true} } var ss NumSet for { var sr NumRange - if !c.take('*') { - sr.First = c.xnzuint32() + if !p.take('*') { + sr.First = p.xnzuint32() } - if c.take(':') { + if p.take(':') { var num uint32 - if !c.take('*') { - num = c.xnzuint32() + if !p.take('*') { + num = p.xnzuint32() } sr.Last = &num } ss.Ranges = append(ss.Ranges, sr) - if !c.take(',') { + if !p.take(',') { break } } @@ -1363,141 +1418,132 @@ func (c *Conn) xsequenceSet() NumSet { } // ../rfc/9051:7097 -func (c *Conn) xtaggedExtComp() TaggedExtComp { - if c.take('(') { - r := c.xtaggedExtComp() - c.xtake(")") +func (p *Proto) xtaggedExtComp() TaggedExtComp { + if p.take('(') { + r := p.xtaggedExtComp() + p.xtake(")") return TaggedExtComp{Comps: []TaggedExtComp{r}} } - s := c.xastring() - if !c.peek(' ') { + s := p.xastring() + if !p.peek(' ') { return TaggedExtComp{String: s} } l := []TaggedExtComp{{String: s}} - for c.space() { - l = append(l, c.xtaggedExtComp()) + for p.space() { + l = append(l, p.xtaggedExtComp()) } return TaggedExtComp{Comps: l} } // ../rfc/9051:6765 -func (c *Conn) xnamespace() []NamespaceDescr { - if !c.take('(') { - c.xtake("nil") +func (p *Proto) xnamespace() []NamespaceDescr { + if !p.take('(') { + p.xtake("nil") return nil } - l := []NamespaceDescr{c.xnamespaceDescr()} - for !c.take(')') { - l = append(l, c.xnamespaceDescr()) + l := []NamespaceDescr{p.xnamespaceDescr()} + for !p.take(')') { + l = append(l, p.xnamespaceDescr()) } return l } // ../rfc/9051:6769 -func (c *Conn) xnamespaceDescr() NamespaceDescr { - c.xtake("(") - prefix := c.xstring() - c.xspace() +func (p *Proto) xnamespaceDescr() NamespaceDescr { + p.xtake("(") + prefix := p.xstring() + p.xspace() var b byte - if c.peek('"') { - s := c.xquoted() + if p.peek('"') { + s := p.xquoted() if len(s) != 1 { - c.xerrorf("namespace-descr: expected single char, got %q", s) + p.xerrorf("namespace-descr: expected single char, got %q", s) } b = byte(s[0]) } else { - c.xtake("nil") + p.xtake("nil") } var exts []NamespaceExtension - for !c.take(')') { - c.xspace() - key := c.xstring() - c.xspace() - c.xtake("(") - values := []string{c.xstring()} - for c.space() { - values = append(values, c.xstring()) + for !p.take(')') { + p.xspace() + key := p.xstring() + p.xspace() + p.xtake("(") + values := []string{p.xstring()} + for p.space() { + values = append(values, p.xstring()) } - c.xtake(")") + p.xtake(")") exts = append(exts, NamespaceExtension{key, values}) } return NamespaceDescr{prefix, b, exts} } -// require all of caps to be disabled. -func (c *Conn) xneedDisabled(msg string, caps ...Capability) { - for _, cap := range caps { - if _, ok := c.CapEnabled[cap]; ok { - c.xerrorf("%s: invalid because of enabled capability %q", msg, cap) - } - } -} - // ../rfc/9051:6546 // Already consumed: "ESEARCH" -func (c *Conn) xesearchResponse() (r UntaggedEsearch) { - if !c.space() { +func (p *Proto) xesearchResponse() (r UntaggedEsearch) { + if !p.space() { return } - if c.take('(') { + if p.take('(') { // ../rfc/9051:6921 ../rfc/7377:465 seen := map[string]bool{} for { var kind string - if c.peek('t') || c.peek('T') { + if p.peek('t') || p.peek('T') { kind = "TAG" - c.xtake(kind) - c.xspace() - r.Tag = c.xastring() - } else if c.peek('m') || c.peek('M') { + p.xtake(kind) + p.xspace() + r.Tag = p.xastring() + } else if p.peek('m') || p.peek('M') { kind = "MAILBOX" - c.xtake(kind) - c.xspace() - r.Mailbox = c.xastring() + p.xtake(kind) + p.xspace() + r.Mailbox = p.xastring() if r.Mailbox == "" { - c.xerrorf("invalid empty mailbox in search correlator") + p.xerrorf("invalid empty mailbox in search correlator") } - } else if c.peek('u') || c.peek('U') { + } else if p.peek('u') || p.peek('U') { kind = "UIDVALIDITY" - c.xtake(kind) - c.xspace() - r.UIDValidity = c.xnzuint32() + p.xtake(kind) + p.xspace() + r.UIDValidity = p.xnzuint32() } else { - c.xerrorf("expected tag/correlator, mailbox or uidvalidity") + p.xerrorf("expected tag/correlator, mailbox or uidvalidity") } if seen[kind] { - c.xerrorf("duplicate search correlator %q", kind) + p.xerrorf("duplicate search correlator %q", kind) } seen[kind] = true - if !c.take(' ') { + if !p.take(' ') { break } } if r.Tag == "" { - c.xerrorf("missing tag search correlator") + p.xerrorf("missing tag search correlator") } if (r.Mailbox != "") != (r.UIDValidity != 0) { - c.xerrorf("mailbox and uidvalidity correlators must both be absent or both be present") + p.xerrorf("mailbox and uidvalidity correlators must both be absent or both be present") } - c.xtake(")") + p.xtake(")") } - if !c.space() { + if !p.space() { return } - w := c.xnonspace() + w := p.xnonspace() W := strings.ToUpper(w) if W == "UID" { r.UID = true - if !c.space() { + if !p.space() { return } - w = c.xnonspace() + w = p.xnonspace() W = strings.ToUpper(w) } for { @@ -1505,117 +1551,117 @@ func (c *Conn) xesearchResponse() (r UntaggedEsearch) { switch W { case "MIN": if r.Min != 0 { - c.xerrorf("duplicate MIN in ESEARCH") + p.xerrorf("duplicate MIN in ESEARCH") } - c.xspace() - num := c.xnzuint32() + p.xspace() + num := p.xnzuint32() r.Min = num case "MAX": if r.Max != 0 { - c.xerrorf("duplicate MAX in ESEARCH") + p.xerrorf("duplicate MAX in ESEARCH") } - c.xspace() - num := c.xnzuint32() + p.xspace() + num := p.xnzuint32() r.Max = num case "ALL": if !r.All.IsZero() { - c.xerrorf("duplicate ALL in ESEARCH") + p.xerrorf("duplicate ALL in ESEARCH") } - c.xspace() - ss := c.xsequenceSet() + p.xspace() + ss := p.xsequenceSet() if ss.SearchResult { - c.xerrorf("$ for last not valid in ESEARCH") + p.xerrorf("$ for last not valid in ESEARCH") } r.All = ss case "COUNT": if r.Count != nil { - c.xerrorf("duplicate COUNT in ESEARCH") + p.xerrorf("duplicate COUNT in ESEARCH") } - c.xspace() - num := c.xuint32() + p.xspace() + num := p.xuint32() r.Count = &num // ../rfc/7162:1211 ../rfc/4731:273 case "MODSEQ": - c.xspace() - r.ModSeq = c.xint64() + p.xspace() + r.ModSeq = p.xint64() default: // Validate ../rfc/9051:7090 for i, b := range []byte(w) { if !(b >= 'A' && b <= 'Z' || strings.IndexByte("-_.", b) >= 0 || i > 0 && strings.IndexByte("0123456789:", b) >= 0) { - c.xerrorf("invalid tag %q", w) + p.xerrorf("invalid tag %q", w) } } - c.xspace() - ext := EsearchDataExt{w, c.xtaggedExtVal()} + p.xspace() + ext := EsearchDataExt{w, p.xtaggedExtVal()} r.Exts = append(r.Exts, ext) } - if !c.space() { + if !p.space() { break } - w = c.xnonspace() // todo: this is too loose + w = p.xnonspace() // todo: this is too loose W = strings.ToUpper(w) } return } // ../rfc/9051:6441 -func (c *Conn) xcharset() string { - if c.peek('"') { - return c.xquoted() +func (p *Proto) xcharset() string { + if p.peek('"') { + return p.xquoted() } - return c.xatom() + return p.xatom() } // ../rfc/9051:7133 -func (c *Conn) xuidset() []NumRange { - ranges := []NumRange{c.xuidrange()} - for c.take(',') { - ranges = append(ranges, c.xuidrange()) +func (p *Proto) xuidset() []NumRange { + ranges := []NumRange{p.xuidrange()} + for p.take(',') { + ranges = append(ranges, p.xuidrange()) } return ranges } -func (c *Conn) xuidrange() NumRange { - uid := c.xnzuint32() +func (p *Proto) xuidrange() NumRange { + uid := p.xnzuint32() var end *uint32 - if c.take(':') { - x := c.xnzuint32() + if p.take(':') { + x := p.xnzuint32() end = &x } return NumRange{uid, end} } // ../rfc/3501:4833 -func (c *Conn) xlsub() UntaggedLsub { - c.xspace() - c.xtake("(") +func (p *Proto) xlsub() UntaggedLsub { + p.xspace() + p.xtake("(") r := UntaggedLsub{} - for !c.take(')') { + for !p.take(')') { if len(r.Flags) > 0 { - c.xspace() + p.xspace() } - r.Flags = append(r.Flags, c.xflag()) + r.Flags = append(r.Flags, p.xflag()) } - c.xspace() - if c.peek('"') { - s := c.xquoted() - if !c.peek(' ') { + p.xspace() + if p.peek('"') { + s := p.xquoted() + if !p.peek(' ') { r.Mailbox = s return r } if len(s) != 1 { // todo: check valid char - c.xerrorf("invalid separator %q", s) + p.xerrorf("invalid separator %q", s) } r.Separator = byte(s[0]) } - c.xspace() - r.Mailbox = c.xastring() + p.xspace() + r.Mailbox = p.xastring() return r } diff --git a/imapclient/parse_test.go b/imapclient/parse_test.go new file mode 100644 index 0000000..d0151bf --- /dev/null +++ b/imapclient/parse_test.go @@ -0,0 +1,42 @@ +package imapclient + +import ( + "fmt" + "reflect" + "testing" +) + +func tcheckf(t *testing.T, err error, format string, args ...any) { + if err != nil { + t.Fatalf("%s: %s", fmt.Sprintf(format, args...), err) + } +} + +func tcompare(t *testing.T, a, b any) { + if !reflect.DeepEqual(a, b) { + t.Fatalf("got:\n%#v\nexpected:\n%#v", a, b) + } +} + +func uint32ptr(v uint32) *uint32 { return &v } + +func TestParse(t *testing.T) { + code, err := ParseCode("COPYUID 1 1:3 2:4") + tcheckf(t, err, "parsing code") + tcompare(t, code, + CodeCopyUID{ + DestUIDValidity: 1, + From: []NumRange{{First: 1, Last: uint32ptr(3)}}, + To: []NumRange{{First: 2, Last: uint32ptr(4)}}, + }, + ) + + ut, err := ParseUntagged("* BYE done\r\n") + tcheckf(t, err, "parsing untagged") + tcompare(t, ut, UntaggedBye{Text: "done"}) + + tag, result, err := ParseResult("tag1 OK [ALERT] Hello\r\n") + tcheckf(t, err, "parsing result") + tcompare(t, tag, "tag1") + tcompare(t, result, Result{Status: OK, Code: CodeWord("ALERT"), Text: "Hello"}) +} diff --git a/imapclient/protocol.go b/imapclient/protocol.go index 3c0a23d..bbb52b8 100644 --- a/imapclient/protocol.go +++ b/imapclient/protocol.go @@ -2,48 +2,57 @@ package imapclient import ( "bufio" + "errors" "fmt" "strings" "time" ) -// Capability is a known string for with the ENABLED and CAPABILITY command. +// Capability is a known string for with the ENABLED command and response and +// CAPABILITY responses. Servers could send unknown values. Always in upper case. type Capability string const ( - CapIMAP4rev1 Capability = "IMAP4rev1" - CapIMAP4rev2 Capability = "IMAP4rev2" - CapLoginDisabled Capability = "LOGINDISABLED" - CapStarttls Capability = "STARTTLS" - CapAuthPlain Capability = "AUTH=PLAIN" - CapLiteralPlus Capability = "LITERAL+" - CapLiteralMinus Capability = "LITERAL-" - CapIdle Capability = "IDLE" - CapNamespace Capability = "NAMESPACE" - CapBinary Capability = "BINARY" - CapUnselect Capability = "UNSELECT" - CapUidplus Capability = "UIDPLUS" - CapEsearch Capability = "ESEARCH" - CapEnable Capability = "ENABLE" - CapSave Capability = "SAVE" - CapListExtended Capability = "LIST-EXTENDED" - CapSpecialUse Capability = "SPECIAL-USE" - CapMove Capability = "MOVE" - CapUTF8Only Capability = "UTF8=ONLY" - CapUTF8Accept Capability = "UTF8=ACCEPT" - CapID Capability = "ID" // ../rfc/2971:80 - CapMetadata Capability = "METADATA" // ../rfc/5464:124 - CapMetadataServer Capability = "METADATA-SERVER" // ../rfc/5464:124 - CapSaveDate Capability = "SAVEDATE" // ../rfc/8514 - CapCreateSpecialUse Capability = "CREATE-SPECIAL-USE" // ../rfc/6154:296 - CapCompressDeflate Capability = "COMPRESS=DEFLATE" // ../rfc/4978:65 - CapListMetadata Capability = "LIST-METADTA" // ../rfc/9590:73 - CapMultiAppend Capability = "MULTIAPPEND" // ../rfc/3502:33 - CapReplace Capability = "REPLACE" // ../rfc/8508:155 - CapPreview Capability = "PREVIEW" // ../rfc/8970:114 - CapMultiSearch Capability = "MULTISEARCH" // ../rfc/7377:187 - CapNotify Capability = "NOTIFY" // ../rfc/5465:195 - CapUIDOnly Capability = "UIDONLY" // ../rfc/9586:129 + CapIMAP4rev1 Capability = "IMAP4REV1" // ../rfc/3501:1310 + CapIMAP4rev2 Capability = "IMAP4REV2" // ../rfc/9051:1219 + CapLoginDisabled Capability = "LOGINDISABLED" // ../rfc/3501:3792 ../rfc/9051:5436 + CapStartTLS Capability = "STARTTLS" // ../rfc/3501:1327 ../rfc/9051:1238 + CapAuthPlain Capability = "AUTH=PLAIN" // ../rfc/3501:1327 ../rfc/9051:1238 + CapAuthExternal Capability = "AUTH=EXTERNAL" // ../rfc/4422:1575 + CapAuthSCRAMSHA256Plus Capability = "AUTH=SCRAM-SHA-256-PLUS" // ../rfc/7677:80 + CapAuthSCRAMSHA256 Capability = "AUTH=SCRAM-SHA-256" + CapAuthSCRAMSHA1Plus Capability = "AUTH=SCRAM-SHA-1-PLUS" // ../rfc/5802:465 + CapAuthSCRAMSHA1 Capability = "AUTH=SCRAM-SHA-1" + CapAuthCRAMMD5 Capability = "AUTH=CRAM-MD5" // ../rfc/2195:80 + CapLiteralPlus Capability = "LITERAL+" // ../rfc/2088:45 + CapLiteralMinus Capability = "LITERAL-" // ../rfc/7888:26 ../rfc/9051:847 Default since IMAP4rev2 + CapIdle Capability = "IDLE" // ../rfc/2177:69 ../rfc/9051:3542 Default since IMAP4rev2 + CapNamespace Capability = "NAMESPACE" // ../rfc/2342:130 ../rfc/9051:135 Default since IMAP4rev2 + CapBinary Capability = "BINARY" // ../rfc/3516:100 + CapUnselect Capability = "UNSELECT" // ../rfc/3691:78 ../rfc/9051:3667 Default since IMAP4rev2 + CapUidplus Capability = "UIDPLUS" // ../rfc/4315:36 ../rfc/9051:8015 Default since IMAP4rev2 + CapEsearch Capability = "ESEARCH" // ../rfc/4731:69 ../rfc/9051:8016 Default since IMAP4rev2 + CapEnable Capability = "ENABLE" // ../rfc/5161:52 ../rfc/9051:8016 Default since IMAP4rev2 + CapListExtended Capability = "LIST-EXTENDED" // ../rfc/5258:150 ../rfc/9051:7987 Syntax except multiple mailboxes default since IMAP4rev2 + CapSpecialUse Capability = "SPECIAL-USE" // ../rfc/6154:156 ../rfc/9051:8021 Special-use attributes in LIST responses by default since IMAP4rev2 + CapMove Capability = "MOVE" // ../rfc/6851:87 ../rfc/9051:8018 Default since IMAP4rev2 + CapUTF8Only Capability = "UTF8=ONLY" + CapUTF8Accept Capability = "UTF8=ACCEPT" + CapCondstore Capability = "CONDSTORE" // ../rfc/7162:411 + CapQresync Capability = "QRESYNC" // ../rfc/7162:1376 + CapID Capability = "ID" // ../rfc/2971:80 + CapMetadata Capability = "METADATA" // ../rfc/5464:124 + CapMetadataServer Capability = "METADATA-SERVER" // ../rfc/5464:124 + CapSaveDate Capability = "SAVEDATE" // ../rfc/8514 + CapCreateSpecialUse Capability = "CREATE-SPECIAL-USE" // ../rfc/6154:296 + CapCompressDeflate Capability = "COMPRESS=DEFLATE" // ../rfc/4978:65 + CapListMetadata Capability = "LIST-METADATA" // ../rfc/9590:73 + CapMultiAppend Capability = "MULTIAPPEND" // ../rfc/3502:33 + CapReplace Capability = "REPLACE" // ../rfc/8508:155 + CapPreview Capability = "PREVIEW" // ../rfc/8970:114 + CapMultiSearch Capability = "MULTISEARCH" // ../rfc/7377:187 + CapNotify Capability = "NOTIFY" // ../rfc/5465:195 + CapUIDOnly Capability = "UIDONLY" // ../rfc/9586:129 ) // Status is the tagged final result of a command. @@ -55,63 +64,134 @@ const ( OK Status = "OK" // Command succeeded. ) +// Response is a response to an IMAP command including any preceding untagged +// responses. Response implements the error interface through result. +// +// See [UntaggedResponseGet] and [UntaggedResponseList] to retrieve specific types +// of untagged responses. +type Response struct { + Untagged []Untagged + Result +} + +var ( + ErrMissing = errors.New("no response of type") // Returned by UntaggedResponseGet. + ErrMultiple = errors.New("multiple responses of type") // Idem. +) + +// UntaggedResponseGet returns the single untagged response of type T. Only +// [ErrMissing] or [ErrMultiple] can be returned as error. +func UntaggedResponseGet[T Untagged](resp Response) (T, error) { + var t T + var have bool + for _, e := range resp.Untagged { + if tt, ok := e.(T); ok { + if have { + return t, ErrMultiple + } + t = tt + } + } + if !have { + return t, ErrMissing + } + return t, nil +} + +// UntaggedResponseList returns all untagged responses of type T. +func UntaggedResponseList[T Untagged](resp Response) []T { + var l []T + for _, e := range resp.Untagged { + if tt, ok := e.(T); ok { + l = append(l, tt) + } + } + return l +} + // Result is the final response for a command, indicating success or failure. type Result struct { Status Status - RespText + Code Code // Set if response code is present. + Text string // Any remaining text. } -// CodeArg represents a response code with arguments, i.e. the data between [] in the response line. -type CodeArg interface { - CodeString() string -} - -// CodeOther is a valid but unrecognized response code. -type CodeOther struct { - Code string - Args []string -} - -func (c CodeOther) CodeString() string { - return c.Code + " " + strings.Join(c.Args, " ") -} - -// CodeWords is a code with space-separated string parameters. E.g. CAPABILITY. -type CodeWords struct { - Code string - Args []string -} - -func (c CodeWords) CodeString() string { - s := c.Code - for _, w := range c.Args { - s += " " + w +func (r Result) Error() string { + s := fmt.Sprintf("IMAP result %s", r.Status) + if r.Code != nil { + s += "[" + r.Code.CodeString() + "]" + } + if r.Text != "" { + s += " " + r.Text } return s } -// CodeList is a code with a list with space-separated strings as parameters. E.g. BADCHARSET, PERMANENTFLAGS. -type CodeList struct { - Code string - Args []string // If nil, no list was present. List can also be empty. +// Code represents a response code with optional arguments, i.e. the data between [] in the response line. +type Code interface { + CodeString() string } -func (c CodeList) CodeString() string { - s := c.Code - if c.Args == nil { +// CodeWord is a response code without parameters, always in upper case. +type CodeWord string + +func (c CodeWord) CodeString() string { + return string(c) +} + +// CodeOther is an unrecognized response code with parameters. +type CodeParams struct { + Code string // Always in upper case. + Args []string +} + +func (c CodeParams) CodeString() string { + return c.Code + " " + strings.Join(c.Args, " ") +} + +// CodeCapability is a CAPABILITY response code with the capabilities supported by the server. +type CodeCapability []Capability + +func (c CodeCapability) CodeString() string { + var s string + for _, c := range c { + s += " " + string(c) + } + return "CAPABILITY" + s +} + +type CodeBadCharset []string + +func (c CodeBadCharset) CodeString() string { + s := "BADCHARSET" + if len(c) == 0 { return s } - return s + "(" + strings.Join(c.Args, " ") + ")" + return s + " (" + strings.Join([]string(c), " ") + ")" } -// CodeUint is a code with a uint32 parameter, e.g. UIDNEXT and UIDVALIDITY. -type CodeUint struct { - Code string - Num uint32 +type CodePermanentFlags []string + +func (c CodePermanentFlags) CodeString() string { + return "PERMANENTFLAGS (" + strings.Join([]string(c), " ") + ")" } -func (c CodeUint) CodeString() string { - return fmt.Sprintf("%s %d", c.Code, c.Num) +type CodeUIDNext uint32 + +func (c CodeUIDNext) CodeString() string { + return fmt.Sprintf("UIDNEXT %d", c) +} + +type CodeUIDValidity uint32 + +func (c CodeUIDValidity) CodeString() string { + return fmt.Sprintf("UIDVALIDITY %d", c) +} + +type CodeUnseen uint32 + +func (c CodeUnseen) CodeString() string { + return fmt.Sprintf("UNSEEN %d", c) } // "APPENDUID" response code. @@ -196,11 +276,32 @@ func (c CodeBadEvent) CodeString() string { return fmt.Sprintf("BADEVENT (%s)", strings.Join([]string(c), " ")) } -// RespText represents a response line minus the leading tag. -type RespText struct { - Code string // The first word between [] after the status. - CodeArg CodeArg // Set if code has a parameter. - More string // Any remaining text. +// "METADATA LONGENTRIES number" response for GETMETADATA command. +type CodeMetadataLongEntries uint32 + +func (c CodeMetadataLongEntries) CodeString() string { + return fmt.Sprintf("METADATA LONGENTRIES %d", c) +} + +// "METADATA (MAXSIZE number)" response for SETMETADATA command. +type CodeMetadataMaxSize uint32 + +func (c CodeMetadataMaxSize) CodeString() string { + return fmt.Sprintf("METADATA (MAXSIZE %d)", c) +} + +// "METADATA (TOOMANY)" response for SETMETADATA command. +type CodeMetadataTooMany struct{} + +func (c CodeMetadataTooMany) CodeString() string { + return "METADATA (TOOMANY)" +} + +// "METADATA (NOPRIVATE)" response for SETMETADATA command. +type CodeMetadataNoPrivate struct{} + +func (c CodeMetadataNoPrivate) CodeString() string { + return "METADATA (NOPRIVATE)" } // atom or string. @@ -241,17 +342,30 @@ func syncliteral(s string) string { // todo: make an interface that the untagged responses implement? type Untagged any -type UntaggedBye RespText -type UntaggedPreauth RespText +type UntaggedBye struct { + Code Code // Set if response code is present. + Text string // Any remaining text. +} +type UntaggedPreauth struct { + Code Code // Set if response code is present. + Text string // Any remaining text. +} type UntaggedExpunge uint32 type UntaggedExists uint32 type UntaggedRecent uint32 -type UntaggedCapability []string -type UntaggedEnabled []string + +// UntaggedCapability lists all capabilities the server implements. +type UntaggedCapability []Capability + +// UntaggedEnabled indicates the capabilities that were enabled on the connection +// by the server, typically in response to an ENABLE command. +type UntaggedEnabled []Capability + type UntaggedResult Result type UntaggedFlags []string type UntaggedList struct { // ../rfc/9051:6690 + Flags []string Separator byte // 0 for NIL Mailbox string @@ -272,8 +386,9 @@ type UntaggedUIDFetch struct { } type UntaggedSearch []uint32 -// ../rfc/7162:1101 type UntaggedSearchModSeq struct { + // ../rfc/7162:1101 + Nums []uint32 ModSeq int64 } @@ -282,8 +397,10 @@ type UntaggedStatus struct { Attrs map[StatusAttr]int64 // Upper case status attributes. } -// ../rfc/5464:716 Unsolicited response, indicating an annotation has changed. +// Unsolicited response, indicating an annotation has changed. type UntaggedMetadataKeys struct { + // ../rfc/5464:716 + Mailbox string // Empty means not specific to mailbox. // Keys that have changed. To get values (or determine absence), the server must be @@ -299,15 +416,17 @@ type Annotation struct { Value []byte } -// ../rfc/5464:683 type UntaggedMetadataAnnotations struct { + // ../rfc/5464:683 + Mailbox string // Empty means not specific to mailbox. Annotations []Annotation } -// ../rfc/9051:7059 ../9208:712 type StatusAttr string +// ../rfc/9051:7059 ../9208:712 + const ( StatusMessages StatusAttr = "MESSAGES" StatusUIDNext StatusAttr = "UIDNEXT" @@ -326,6 +445,7 @@ type UntaggedNamespace struct { } type UntaggedLsub struct { // ../rfc/3501:4833 + Flags []string Separator byte Mailbox string @@ -395,6 +515,7 @@ type EsearchDataExt struct { type NamespaceDescr struct { // ../rfc/9051:6769 + Prefix string Separator byte // If 0 then separator was absent. Exts []NamespaceExtension @@ -402,13 +523,14 @@ type NamespaceDescr struct { type NamespaceExtension struct { // ../rfc/9051:6773 + Key string Values []string } // FetchAttr represents a FETCH response attribute. type FetchAttr interface { - Attr() string // Name of attribute. + Attr() string // Name of attribute in upper case, e.g. "UID". } type NumSet struct { @@ -435,14 +557,14 @@ func (ns NumSet) String() string { } func ParseNumSet(s string) (ns NumSet, rerr error) { - c := Conn{br: bufio.NewReader(strings.NewReader(s))} + c := Proto{br: bufio.NewReader(strings.NewReader(s))} defer c.recover(&rerr) ns = c.xsequenceSet() return } func ParseUIDRange(s string) (nr NumRange, rerr error) { - c := Conn{br: bufio.NewReader(strings.NewReader(s))} + c := Proto{br: bufio.NewReader(strings.NewReader(s))} defer c.recover(&rerr) nr = c.xuidrange() return @@ -481,6 +603,7 @@ type TaggedExtComp struct { type TaggedExtVal struct { // ../rfc/9051:7111 + Number *int64 SeqSet *NumSet Comp *TaggedExtComp // If SimpleNumber and SimpleSeqSet is nil, this is a Comp. But Comp is optional and can also be nil. Not great. @@ -488,6 +611,7 @@ type TaggedExtVal struct { type MboxListExtendedItem struct { // ../rfc/9051:6699 + Tag string Val TaggedExtVal } @@ -522,8 +646,10 @@ type FetchInternalDate struct { func (f FetchInternalDate) Attr() string { return "INTERNALDATE" } -// "SAVEDATE" fetch response. ../rfc/8514:265 +// "SAVEDATE" fetch response. type FetchSaveDate struct { + // ../rfc/8514:265 + SaveDate *time.Time // nil means absent for message. } @@ -552,6 +678,7 @@ func (f FetchRFC822Text) Attr() string { return "RFC822.TEXT" } // "BODYSTRUCTURE" fetch response. type FetchBodystructure struct { // ../rfc/9051:6355 + RespAttr string Body any // BodyType* } @@ -561,6 +688,7 @@ func (f FetchBodystructure) Attr() string { return f.RespAttr } // "BODY" fetch response. type FetchBody struct { // ../rfc/9051:6756 ../rfc/9051:6985 + RespAttr string Section string // todo: parse more ../rfc/9051:6985 Offset int32 @@ -580,6 +708,7 @@ type BodyFields struct { // subparts and the multipart media subtype. Used in a FETCH response. type BodyTypeMpart struct { // ../rfc/9051:6411 + Bodies []any // BodyTypeBasic, BodyTypeMsg, BodyTypeText MediaSubtype string Ext *BodyExtensionMpart @@ -589,6 +718,7 @@ type BodyTypeMpart struct { // response. type BodyTypeBasic struct { // ../rfc/9051:6407 + MediaType, MediaSubtype string BodyFields BodyFields Ext *BodyExtension1Part @@ -598,6 +728,7 @@ type BodyTypeBasic struct { // response. type BodyTypeMsg struct { // ../rfc/9051:6415 + MediaType, MediaSubtype string BodyFields BodyFields Envelope Envelope @@ -610,6 +741,7 @@ type BodyTypeMsg struct { // response. type BodyTypeText struct { // ../rfc/9051:6418 + MediaType, MediaSubtype string BodyFields BodyFields Lines int64 @@ -618,26 +750,42 @@ type BodyTypeText struct { // BodyExtension1Part has the extensible form fields of a BODYSTRUCTURE for // multiparts. +// +// Fields in this struct are optional in IMAP4, and can be NIL or contain a value. +// The first field is always present, otherwise the "parent" struct would have a +// nil *BodyExtensionMpart. The second and later fields are nil when absent. For +// non-reference types (e.g. strings), an IMAP4 NIL is represented as a pointer to +// (*T)(nil). For reference types (e.g. slices), an IMAP4 NIL is represented by a +// pointer to nil. type BodyExtensionMpart struct { // ../rfc/9051:5986 ../rfc/3501:4161 ../rfc/9051:6371 ../rfc/3501:4599 + Params [][2]string - Disposition string - DispositionParams [][2]string - Language []string - Location string - More []BodyExtension + Disposition **string + DispositionParams *[][2]string + Language *[]string + Location **string + More []BodyExtension // Nil if absent. } // BodyExtension1Part has the extensible form fields of a BODYSTRUCTURE for // non-multiparts. +// +// Fields in this struct are optional in IMAP4, and can be NIL or contain a value. +// The first field is always present, otherwise the "parent" struct would have a +// nil *BodyExtensionMpart. The second and later fields are nil when absent. For +// non-reference types (e.g. strings), an IMAP4 NIL is represented as a pointer to +// (*T)(nil). For reference types (e.g. slices), an IMAP4 NIL is represented by a +// pointer to nil. type BodyExtension1Part struct { // ../rfc/9051:6023 ../rfc/3501:4191 ../rfc/9051:6366 ../rfc/3501:4584 - MD5 string - Disposition string - DispositionParams [][2]string - Language []string - Location string - More []BodyExtension + + MD5 *string + Disposition **string + DispositionParams *[][2]string + Language *[]string + Location **string + More []BodyExtension // Nil means absent. } // BodyExtension has the additional extension fields for future expansion of diff --git a/imapserver/append_test.go b/imapserver/append_test.go index cadfda9..d9de2ce 100644 --- a/imapserver/append_test.go +++ b/imapserver/append_test.go @@ -50,14 +50,14 @@ func testAppend(t *testing.T, uidonly bool) { tc2.client.Select("inbox") tc2.transactf("no", "append nobox (\\Seen) \" 1-Jan-2022 10:10:00 +0100\" {1}") - tc2.xcode("TRYCREATE") + tc2.xcodeWord("TRYCREATE") tc2.transactf("no", "append expungebox (\\Seen) {1}") - tc2.xcode("TRYCREATE") + tc2.xcodeWord("TRYCREATE") tc2.transactf("ok", "append inbox (\\Seen Label1 $label2) \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx") tc2.xuntagged(imapclient.UntaggedExists(1)) - tc2.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("1")}) + tc2.xcode(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("1")}) tc.transactf("ok", "noop") flags := imapclient.FetchFlags{`\Seen`, "$label2", "label1"} @@ -67,11 +67,11 @@ func testAppend(t *testing.T, uidonly bool) { tc2.transactf("ok", "append inbox (\\Seen) \" 1-Jan-2022 10:10:00 +0100\" UTF8 (~{47+}\r\ncontent-type: just completely invalid;;\r\n\r\ntest)") tc2.xuntagged(imapclient.UntaggedExists(2)) - tc2.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("2")}) + tc2.xcode(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("2")}) tc2.transactf("ok", "append inbox (\\Seen) \" 1-Jan-2022 10:10:00 +0100\" UTF8 (~{31+}\r\ncontent-type: text/plain;\n\ntest)") tc2.xuntagged(imapclient.UntaggedExists(3)) - tc2.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("3")}) + tc2.xcode(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("3")}) // Messages that we cannot parse are marked as application/octet-stream. Perhaps // the imap client knows how to deal with them. @@ -92,7 +92,7 @@ func testAppend(t *testing.T, uidonly bool) { tc.transactf("ok", "noop") // Flush pending untagged responses. tc.transactf("ok", "append inbox {6+}\r\ntest\r\n ~{6+}\r\ntost\r\n") tc.xuntagged(imapclient.UntaggedExists(5)) - tc.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("4:5")}) + tc.xcode(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("4:5")}) // Cancelled with zero-length message. tc.transactf("no", "append inbox {6+}\r\ntest\r\n {0+}\r\n") @@ -106,7 +106,7 @@ func testAppend(t *testing.T, uidonly bool) { tclimit.xuntagged(imapclient.UntaggedExists(1)) // Second message would take account past limit. tclimit.transactf("no", "append inbox (\\Seen Label1 $label2) \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx") - tclimit.xcode("OVERQUOTA") + tclimit.xcodeWord("OVERQUOTA") // Empty mailbox. if uidonly { @@ -119,10 +119,10 @@ func testAppend(t *testing.T, uidonly bool) { // Multiappend with first message within quota, and second message with sync // literal causing quota error. Request should get error response immediately. tclimit.transactf("no", "append inbox {1+}\r\nx {100000}") - tclimit.xcode("OVERQUOTA") + tclimit.xcodeWord("OVERQUOTA") // Again, but second message now with non-sync literal, which is fully consumed by server. - tclimit.client.Commandf("", "append inbox {1+}\r\nx {4000+}") + tclimit.client.WriteCommandf("", "append inbox {1+}\r\nx {4000+}") buf := make([]byte, 4000, 4002) for i := range buf { buf[i] = 'x' @@ -131,5 +131,5 @@ func testAppend(t *testing.T, uidonly bool) { _, err := tclimit.client.Write(buf) tclimit.check(err, "write append message") tclimit.response("no") - tclimit.xcode("OVERQUOTA") + tclimit.xcodeWord("OVERQUOTA") } diff --git a/imapserver/authenticate_test.go b/imapserver/authenticate_test.go index 31e5c88..e73d504 100644 --- a/imapserver/authenticate_test.go +++ b/imapserver/authenticate_test.go @@ -20,6 +20,7 @@ import ( "golang.org/x/text/secure/precis" + "github.com/mjl-/mox/imapclient" "github.com/mjl-/mox/mox-" "github.com/mjl-/mox/scram" "github.com/mjl-/mox/store" @@ -38,19 +39,19 @@ func TestAuthenticatePlain(t *testing.T) { tc.transactf("no", "authenticate bogus ") tc.transactf("bad", "authenticate plain not base64...") tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000baduser\u0000badpass"))) - tc.xcode("AUTHENTICATIONFAILED") + tc.xcodeWord("AUTHENTICATIONFAILED") tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000badpass"))) - tc.xcode("AUTHENTICATIONFAILED") + tc.xcodeWord("AUTHENTICATIONFAILED") tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl\u0000badpass"))) // Need email, not account. - tc.xcode("AUTHENTICATIONFAILED") + tc.xcodeWord("AUTHENTICATIONFAILED") tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000test"))) - tc.xcode("AUTHENTICATIONFAILED") + tc.xcodeWord("AUTHENTICATIONFAILED") tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000test"+password0))) - tc.xcode("AUTHENTICATIONFAILED") + tc.xcodeWord("AUTHENTICATIONFAILED") tc.transactf("bad", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000"))) - tc.xcode("") + tc.xcode(nil) tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("other\u0000mjl@mox.example\u0000"+password0))) - tc.xcode("AUTHORIZATIONFAILED") + tc.xcodeWord("AUTHORIZATIONFAILED") tc.transactf("ok", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000"+password0))) tc.close() @@ -93,14 +94,14 @@ func TestLoginDisabled(t *testing.T) { tcheck(t, err, "close account") tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000disabled@mox.example\u0000test1234"))) - tc.xcode("") + tc.xcode(nil) tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000disabled@mox.example\u0000bogus"))) - tc.xcode("AUTHENTICATIONFAILED") + tc.xcodeWord("AUTHENTICATIONFAILED") tc.transactf("no", "login disabled@mox.example test1234") - tc.xcode("") + tc.xcode(nil) tc.transactf("no", "login disabled@mox.example bogus") - tc.xcode("AUTHENTICATIONFAILED") + tc.xcodeWord("AUTHENTICATIONFAILED") } func TestAuthenticateSCRAMSHA1(t *testing.T) { @@ -131,14 +132,11 @@ func testAuthenticateSCRAM(t *testing.T, tls bool, method string, h func() hash. sc := scram.NewClient(h, username, "", noServerPlus, tc.client.TLSConnectionState()) clientFirst, err := sc.ClientFirst() tc.check(err, "scram clientFirst") - tc.client.LastTag = "x001" - tc.writelinef("%s authenticate %s %s", tc.client.LastTag, method, base64.StdEncoding.EncodeToString([]byte(clientFirst))) + tc.client.WriteCommandf("", "authenticate %s %s", method, base64.StdEncoding.EncodeToString([]byte(clientFirst))) xreadContinuation := func() []byte { - line, _, result, _ := tc.client.ReadContinuation() - if result.Status != "" { - tc.t.Fatalf("expected continuation") - } + line, err := tc.client.ReadContinuation() + tcheck(t, err, "read continuation") buf, err := base64.StdEncoding.DecodeString(line) tc.check(err, "parsing base64 from remote") return buf @@ -161,10 +159,10 @@ func testAuthenticateSCRAM(t *testing.T, tls bool, method string, h func() hash. } else { tc.writelinef("") } - _, result, err := tc.client.Response() + resp, err := tc.client.ReadResponse() tc.check(err, "read response") - if string(result.Status) != strings.ToUpper(status) { - tc.t.Fatalf("got status %q, expected %q", result.Status, strings.ToUpper(status)) + if string(resp.Status) != strings.ToUpper(status) { + tc.t.Fatalf("got status %q, expected %q", resp.Status, strings.ToUpper(status)) } } @@ -195,14 +193,11 @@ func TestAuthenticateCRAMMD5(t *testing.T) { auth := func(status string, username, password string) { t.Helper() - tc.client.LastTag = "x001" - tc.writelinef("%s authenticate CRAM-MD5", tc.client.LastTag) + tc.client.WriteCommandf("", "authenticate CRAM-MD5") xreadContinuation := func() []byte { - line, _, result, _ := tc.client.ReadContinuation() - if result.Status != "" { - tc.t.Fatalf("expected continuation") - } + line, err := tc.client.ReadContinuation() + tcheck(t, err, "read continuation") buf, err := base64.StdEncoding.DecodeString(line) tc.check(err, "parsing base64 from remote") return buf @@ -215,13 +210,13 @@ func TestAuthenticateCRAMMD5(t *testing.T) { } h := hmac.New(md5.New, []byte(password)) h.Write([]byte(chal)) - resp := fmt.Sprintf("%s %x", username, h.Sum(nil)) - tc.writelinef("%s", base64.StdEncoding.EncodeToString([]byte(resp))) + data := fmt.Sprintf("%s %x", username, h.Sum(nil)) + tc.writelinef("%s", base64.StdEncoding.EncodeToString([]byte(data))) - _, result, err := tc.client.Response() + resp, err := tc.client.ReadResponse() tc.check(err, "read response") - if string(result.Status) != strings.ToUpper(status) { - tc.t.Fatalf("got status %q, expected %q", result.Status, strings.ToUpper(status)) + if string(resp.Status) != strings.ToUpper(status) { + tc.t.Fatalf("got status %q, expected %q", resp.Status, strings.ToUpper(status)) } } @@ -301,7 +296,7 @@ func TestAuthenticateTLSClientCert(t *testing.T) { // Starttls and external auth. tc = startArgsMore(t, false, true, false, nil, &clientConfig, false, true, "mjl", addClientCert) - tc.client.Starttls(&clientConfig) + tc.client.StartTLS(&clientConfig) tc.transactf("ok", "authenticate external =") tc.close() @@ -339,7 +334,7 @@ func TestAuthenticateTLSClientCert(t *testing.T) { t.Fatalf("tls connection was not resumed") } // Check that operations that require an account work. - tc.client.Enable("imap4rev2") + tc.client.Enable(imapclient.CapIMAP4rev2) received, err := time.Parse(time.RFC3339, "2022-11-16T10:01:00+01:00") tc.check(err, "parse time") tc.client.Append("inbox", makeAppendTime(exampleMsg, received)) diff --git a/imapserver/compress_test.go b/imapserver/compress_test.go index 1883f75..331fe0e 100644 --- a/imapserver/compress_test.go +++ b/imapserver/compress_test.go @@ -21,7 +21,7 @@ func TestCompress(t *testing.T) { tc.client.CompressDeflate() tc.transactf("no", "compress deflate") // Cannot have multiple. - tc.xcode("COMPRESSIONACTIVE") + tc.xcodeWord("COMPRESSIONACTIVE") tc.client.Select("inbox") tc.transactf("ok", "append inbox (\\seen) {%d+}\r\n%s", len(exampleMsg), exampleMsg) @@ -33,7 +33,7 @@ func TestCompressStartTLS(t *testing.T) { tc := start(t, false) defer tc.close() - tc.client.Starttls(&tls.Config{InsecureSkipVerify: true}) + tc.client.StartTLS(&tls.Config{InsecureSkipVerify: true}) tc.login("mjl@mox.example", password0) tc.client.CompressDeflate() tc.client.Select("inbox") diff --git a/imapserver/condstore_test.go b/imapserver/condstore_test.go index 1e5a59d..90b3f2c 100644 --- a/imapserver/condstore_test.go +++ b/imapserver/condstore_test.go @@ -38,15 +38,15 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) { // Check basic requirements of CONDSTORE. - capability := "Condstore" + capability := imapclient.CapCondstore if qresync { - capability = "Qresync" + capability = imapclient.CapQresync } tc.login("mjl@mox.example", password0) tc.client.Enable(capability) tc.transactf("ok", "Select inbox") - tc.xuntaggedOpt(false, imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(2), More: "x"}}) + tc.xuntaggedOpt(false, imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeHighestModSeq(2), Text: "x"}) // First some tests without any messages. @@ -133,19 +133,19 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) { // The ones we insert below will start with modseq 3. So we'll have messages with modseq 1 and 3-6. tc.transactf("ok", "Append inbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx") tc.xuntagged(imapclient.UntaggedExists(4)) - tc.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("4")}) + tc.xcode(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("4")}) tc.transactf("ok", "Append otherbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx") tc.xuntagged() - tc.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 3, UIDs: xparseUIDRange("1")}) + tc.xcode(imapclient.CodeAppendUID{UIDValidity: 3, UIDs: xparseUIDRange("1")}) tc.transactf("ok", "Append inbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx") tc.xuntagged(imapclient.UntaggedExists(5)) - tc.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("5")}) + tc.xcode(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("5")}) tc.transactf("ok", "Append inbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx") tc.xuntagged(imapclient.UntaggedExists(6)) - tc.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("6")}) + tc.xcode(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("6")}) tc2.transactf("ok", "Noop") noflags := imapclient.FetchFlags(nil) @@ -181,10 +181,10 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) { // Check highestmodseq when we select. tc.transactf("ok", "Examine otherbox") - tc.xuntaggedOpt(false, imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(clientModseq + 2), More: "x"}}) + tc.xuntaggedOpt(false, imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeHighestModSeq(clientModseq + 2), Text: "x"}) tc.transactf("ok", "Select inbox") - tc.xuntaggedOpt(false, imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(clientModseq + 4), More: "x"}}) + tc.xuntaggedOpt(false, imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeHighestModSeq(clientModseq + 4), Text: "x"}) clientModseq += 4 @@ -225,13 +225,13 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) { if !uidonly { // unchangedsince 0 never passes the check. ../rfc/7162:640 tc.transactf("ok", `Store 1 (Unchangedsince 0) +Flags ()`) - tc.xcodeArg(imapclient.CodeModified(xparseNumSet("1"))) + tc.xcode(imapclient.CodeModified(xparseNumSet("1"))) tc.xuntagged(tc.untaggedFetch(1, 1, noflags, imapclient.FetchModSeq(1))) } // Modseq is 2 for first condstore-aware-appended message, so also no match. tc.transactf("ok", `Uid Store 4 (Unchangedsince 1) +Flags ()`) - tc.xcodeArg(imapclient.CodeModified(xparseNumSet("4"))) + tc.xcode(imapclient.CodeModified(xparseNumSet("4"))) if uidonly { tc.transactf("ok", `Uid Store 1 (Unchangedsince 1) +Flags (label1)`) @@ -239,7 +239,7 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) { // Modseq is 1 for original message. tc.transactf("ok", `Store 1 (Unchangedsince 1) +Flags (label1)`) } - tc.xcode("") // No MODIFIED. + tc.xcode(nil) // No MODIFIED. clientModseq++ tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{"label1"}, imapclient.FetchModSeq(clientModseq))) tc2.transactf("ok", "Noop") @@ -255,7 +255,7 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) { // modseq change made in the first application. ../rfc/7162:823 tc.transactf("ok", `Uid Store 1,1 (Unchangedsince %d) -Flags (label1)`, clientModseq) clientModseq++ - tc.xcode("") // No MODIFIED. + tc.xcode(nil) // No MODIFIED. tc.xuntagged( tc.untaggedFetch(1, 1, imapclient.FetchFlags(nil), imapclient.FetchModSeq(clientModseq)), ) @@ -273,7 +273,7 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) { // Modify without actually changing flags, there will be no new modseq and no broadcast. tc.transactf("ok", `Store 1 (Unchangedsince %d) -Flags (label1)`, clientModseq) tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags(nil), imapclient.FetchModSeq(clientModseq))) - tc.xcode("") // No MODIFIED. + tc.xcode(nil) // No MODIFIED. tc2.transactf("ok", "Noop") tc2.xuntagged() tc3.transactf("ok", "Noop") @@ -318,7 +318,7 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) { } else { tc.xuntagged(imapclient.UntaggedExpunge(3), imapclient.UntaggedExpunge(3)) } - tc.xcodeArg(imapclient.CodeHighestModSeq(clientModseq)) + tc.xcode(imapclient.CodeHighestModSeq(clientModseq)) tc2.transactf("ok", "Noop") if uidonly { tc2.xuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("3:4")}) @@ -340,7 +340,7 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) { tc.transactf("ok", "Select inbox") tc.xuntaggedOpt(false, imapclient.UntaggedExists(4), - imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(clientModseq), More: "x"}}, + imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeHighestModSeq(clientModseq), Text: "x"}, ) if !uidonly { @@ -367,16 +367,16 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) { tc.transactf("ok", "Search Return (Min Max All) 1:* Modseq 8") tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 1, All: esearchall0("1"), ModSeq: 8}) tc.transactf("ok", "Search Return (Min Max All) 1:* Modseq 9") - tc.xuntagged(imapclient.UntaggedEsearch{Tag: tc.client.LastTag}) + tc.xuntagged(imapclient.UntaggedEsearch{Tag: tc.client.LastTag()}) } // store, cannot modify expunged messages. tc.transactf("ok", `Uid Store 3,4 (Unchangedsince %d) +Flags (label2)`, clientModseq) tc.xuntagged() - tc.xcode("") // Not MODIFIED. + tc.xcode(nil) // Not MODIFIED. tc.transactf("ok", `Uid Store 3,4 +Flags (label2)`) tc.xuntagged() - tc.xcode("") // Not MODIFIED. + tc.xcode(nil) // Not MODIFIED. // Check all condstore-enabling commands (and their syntax), ../rfc/7162:368 @@ -497,13 +497,13 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) { clientModseq++ if qresync { tc.xuntaggedOpt(false, imapclient.UntaggedVanished{UIDs: xparseNumSet("2")}) - tc.xcodeArg(imapclient.CodeHighestModSeq(clientModseq)) + tc.xcode(imapclient.CodeHighestModSeq(clientModseq)) } else if uidonly { tc.xuntaggedOpt(false, imapclient.UntaggedVanished{UIDs: xparseNumSet("2")}) - tc.xcode("") + tc.xcode(nil) } else { tc.xuntaggedOpt(false, imapclient.UntaggedExpunge(2)) - tc.xcode("") + tc.xcode(nil) } tc2.transactf("ok", "Noop") if uidonly { @@ -615,21 +615,21 @@ func testQresync(t *testing.T, tc *testconn, uidonly bool, clientModseq int64) { flags := strings.Split(`\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent l1 l2 l3 l4 l5 l6 l7 l8 label1`, " ") permflags := strings.Split(`\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent \*`, " ") uflags := imapclient.UntaggedFlags(flags) - upermflags := imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "PERMANENTFLAGS", CodeArg: imapclient.CodeList{Code: "PERMANENTFLAGS", Args: permflags}, More: "x"}} + upermflags := imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodePermanentFlags(permflags), Text: "x"} baseUntagged := []imapclient.Untagged{ uflags, upermflags, imapclient.UntaggedList{Separator: '/', Mailbox: "Inbox"}, - imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UIDNEXT", CodeArg: imapclient.CodeUint{Code: "UIDNEXT", Num: 7}, More: "x"}}, - imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UIDVALIDITY", CodeArg: imapclient.CodeUint{Code: "UIDVALIDITY", Num: 1}, More: "x"}}, + imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeUIDNext(7), Text: "x"}, + imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeUIDValidity(1), Text: "x"}, imapclient.UntaggedRecent(0), imapclient.UntaggedExists(4), - imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(clientModseq), More: "x"}}, + imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeHighestModSeq(clientModseq), Text: "x"}, } if !uidonly { baseUntagged = append(baseUntagged, - imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UNSEEN", CodeArg: imapclient.CodeUint{Code: "UNSEEN", Num: 1}, More: "x"}}, + imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeUnseen(1), Text: "x"}, ) } @@ -752,7 +752,7 @@ func testQresync(t *testing.T, tc *testconn, uidonly bool, clientModseq int64) { tc.transactf("ok", "Select inbox (Qresync (1 10 (1,3,6 1,3,6)))") tc.xuntagged( makeUntagged( - imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "ALERT", More: "Synchronization inconsistency in client detected. Client tried to sync with a UID that was removed at or after the MODSEQ it sent in the request. Sending all historic message removals for selected mailbox. Full synchronization recommended."}}, + imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeWord("ALERT"), Text: "Synchronization inconsistency in client detected. Client tried to sync with a UID that was removed at or after the MODSEQ it sent in the request. Sending all historic message removals for selected mailbox. Full synchronization recommended."}, imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")}, tc.untaggedFetch(4, 6, noflags, imapclient.FetchModSeq(clientModseq)), )..., @@ -765,7 +765,7 @@ func testQresync(t *testing.T, tc *testconn, uidonly bool, clientModseq int64) { tc.transactf("ok", "Select inbox (Qresync (1 18 (1,3,6 1,3,6)))") tc.xuntagged( makeUntagged( - imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "ALERT", More: "Synchronization inconsistency in client detected. Client tried to sync with a UID that was removed at or after the MODSEQ it sent in the request. Sending all historic message removals for selected mailbox. Full synchronization recommended."}}, + imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeWord("ALERT"), Text: "Synchronization inconsistency in client detected. Client tried to sync with a UID that was removed at or after the MODSEQ it sent in the request. Sending all historic message removals for selected mailbox. Full synchronization recommended."}, imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")}, tc.untaggedFetch(4, 6, noflags, imapclient.FetchModSeq(clientModseq)), )..., @@ -786,7 +786,7 @@ func testQresyncHistory(t *testing.T, uidonly bool) { defer tc.close() tc.login("mjl@mox.example", password0) - tc.client.Enable("Qresync") + tc.client.Enable(imapclient.CapQresync) tc.transactf("ok", "Append inbox {1+}\r\nx") tc.transactf("ok", "Append inbox {1+}\r\nx") // modseq 6 tc.transactf("ok", "Append inbox {1+}\r\nx") @@ -799,16 +799,16 @@ func testQresyncHistory(t *testing.T, uidonly bool) { flags := strings.Split(`\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent`, " ") permflags := strings.Split(`\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent \*`, " ") uflags := imapclient.UntaggedFlags(flags) - upermflags := imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "PERMANENTFLAGS", CodeArg: imapclient.CodeList{Code: "PERMANENTFLAGS", Args: permflags}, More: "x"}} + upermflags := imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodePermanentFlags(permflags), Text: "x"} baseUntagged := []imapclient.Untagged{ uflags, upermflags, imapclient.UntaggedList{Separator: '/', Mailbox: "Inbox"}, - imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UIDNEXT", CodeArg: imapclient.CodeUint{Code: "UIDNEXT", Num: 4}, More: "x"}}, - imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UIDVALIDITY", CodeArg: imapclient.CodeUint{Code: "UIDVALIDITY", Num: 1}, More: "x"}}, + imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeUIDNext(4), Text: "x"}, + imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeUIDValidity(1), Text: "x"}, imapclient.UntaggedRecent(0), imapclient.UntaggedExists(1), - imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(10), More: "x"}}, + imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeHighestModSeq(10), Text: "x"}, } makeUntagged := func(l ...imapclient.Untagged) []imapclient.Untagged { diff --git a/imapserver/copy_test.go b/imapserver/copy_test.go index 329ebb0..c145a01 100644 --- a/imapserver/copy_test.go +++ b/imapserver/copy_test.go @@ -44,16 +44,16 @@ func testCopy(t *testing.T, uidonly bool) { tc.transactf("ok", "uid copy 3:* Trash") } else { tc.transactf("no", "copy 1 nonexistent") - tc.xcode("TRYCREATE") + tc.xcodeWord("TRYCREATE") tc.transactf("no", "copy 1 expungebox") - tc.xcode("TRYCREATE") + tc.xcodeWord("TRYCREATE") tc.transactf("no", "copy 1 inbox") // Cannot copy to same mailbox. tc2.transactf("ok", "noop") // Drain. tc.transactf("ok", "copy 1:* Trash") - tc.xcodeArg(imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}, To: []imapclient.NumRange{{First: 1, Last: uint32ptr(2)}}}) + tc.xcode(mustParseCode("COPYUID 1 3:4 1:2")) } tc2.transactf("ok", "noop") tc2.xuntagged( @@ -64,7 +64,7 @@ func testCopy(t *testing.T, uidonly bool) { tc.transactf("no", "uid copy 1,2 Trash") // No match. tc.transactf("ok", "uid copy 4,3 Trash") - tc.xcodeArg(imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}, To: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}}) + tc.xcode(mustParseCode("COPYUID 1 3:4 3:4")) tc2.transactf("ok", "noop") tc2.xuntagged( imapclient.UntaggedExists(4), @@ -81,5 +81,5 @@ func testCopy(t *testing.T, uidonly bool) { tclimit.xuntagged(imapclient.UntaggedExists(1)) // Second message would take account past limit. tclimit.transactf("no", "uid copy 1:* Trash") - tclimit.xcode("OVERQUOTA") + tclimit.xcodeWord("OVERQUOTA") } diff --git a/imapserver/create_test.go b/imapserver/create_test.go index 85a97bc..ee9f5c7 100644 --- a/imapserver/create_test.go +++ b/imapserver/create_test.go @@ -57,7 +57,7 @@ func testCreate(t *testing.T, uidonly bool) { tc.xuntagged(imapclient.UntaggedList{Flags: []string{`\Subscribed`}, Separator: '/', Mailbox: "mailbox"}) // OldName is only set for IMAP4rev2 or NOTIFY. - tc.client.Enable("imap4rev2") + tc.client.Enable(imapclient.CapIMAP4rev2) tc.transactf("ok", "create mailbox2/") tc.xuntagged(imapclient.UntaggedList{Flags: []string{`\Subscribed`}, Separator: '/', Mailbox: "mailbox2", OldName: "mailbox2/"}) diff --git a/imapserver/delete_test.go b/imapserver/delete_test.go index e42621c..25f9ff4 100644 --- a/imapserver/delete_test.go +++ b/imapserver/delete_test.go @@ -43,9 +43,9 @@ func testDelete(t *testing.T, uidonly bool) { // ../rfc/9051:2000 tc.transactf("no", "delete a") // Still has child. - tc.xcode("HASCHILDREN") + tc.xcodeWord("HASCHILDREN") - tc3.client.Enable("IMAP4rev2") // For \NonExistent support. + tc3.client.Enable(imapclient.CapIMAP4rev2) // For \NonExistent support. tc.transactf("ok", "delete a/b") tc2.transactf("ok", "noop") tc2.xuntagged() // No IMAP4rev2, no \NonExistent. diff --git a/imapserver/fetch_test.go b/imapserver/fetch_test.go index edabac9..ece3e1e 100644 --- a/imapserver/fetch_test.go +++ b/imapserver/fetch_test.go @@ -24,7 +24,7 @@ func testFetch(t *testing.T, uidonly bool) { defer tc.close() tc.login("mjl@mox.example", password0) - tc.client.Enable("imap4rev2") + tc.client.Enable(imapclient.CapIMAP4rev2) received, err := time.Parse(time.RFC3339, "2022-11-16T10:01:00+01:00") tc.check(err, "parse time") tc.client.Append("inbox", makeAppendTime(exampleMsg, received)) @@ -58,7 +58,13 @@ func testFetch(t *testing.T, uidonly bool) { } bodystructure1 := bodyxstructure1 bodystructure1.RespAttr = "BODYSTRUCTURE" - bodystructbody1.Ext = &imapclient.BodyExtension1Part{} + bodyext1 := imapclient.BodyExtension1Part{ + Disposition: ptr((*string)(nil)), + DispositionParams: ptr([][2]string(nil)), + Language: ptr([]string(nil)), + Location: ptr((*string)(nil)), + } + bodystructbody1.Ext = &bodyext1 bodystructure1.Body = bodystructbody1 split := strings.SplitN(exampleMsg, "\r\n\r\n", 2) @@ -115,26 +121,26 @@ func testFetch(t *testing.T, uidonly bool) { tc.transactf("ok", "fetch 1 binary[1]") tc.xuntagged(tc.untaggedFetch(1, 1, binarypart1)) // Seen flag not changed. - tc.client.StoreFlagsClear("1", true, `\Seen`) + tc.client.MSNStoreFlagsClear("1", true, `\Seen`) tc.transactf("ok", "uid fetch 1 binary[]<1.1>") tc.xuntagged( tc.untaggedFetch(1, 1, binarypartial1, noflags), tc.untaggedFetch(1, 1, flagsSeen), // For UID FETCH, we get the flags during the command. ) - tc.client.StoreFlagsClear("1", true, `\Seen`) + tc.client.MSNStoreFlagsClear("1", true, `\Seen`) tc.transactf("ok", "fetch 1 binary[1]<1.1>") tc.xuntagged(tc.untaggedFetch(1, 1, binarypartpartial1, noflags)) tc.transactf("ok", "noop") tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen)) - tc.client.StoreFlagsClear("1", true, `\Seen`) + tc.client.MSNStoreFlagsClear("1", true, `\Seen`) tc.transactf("ok", "fetch 1 binary[]<10000.10001>") tc.xuntagged(tc.untaggedFetch(1, 1, binaryend1, noflags)) tc.transactf("ok", "noop") tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen)) - tc.client.StoreFlagsClear("1", true, `\Seen`) + tc.client.MSNStoreFlagsClear("1", true, `\Seen`) tc.transactf("ok", "fetch 1 binary[1]<10000.10001>") tc.xuntagged(tc.untaggedFetch(1, 1, binarypartend1, noflags)) tc.transactf("ok", "noop") @@ -146,7 +152,7 @@ func testFetch(t *testing.T, uidonly bool) { tc.transactf("ok", "fetch 1 binary.size[1]") tc.xuntagged(tc.untaggedFetch(1, 1, binarysizepart1)) - tc.client.StoreFlagsClear("1", true, `\Seen`) + tc.client.MSNStoreFlagsClear("1", true, `\Seen`) tc.transactf("ok", "fetch 1 body[]") tc.xuntagged(tc.untaggedFetch(1, 1, body1, noflags)) tc.transactf("ok", "noop") @@ -156,31 +162,31 @@ func testFetch(t *testing.T, uidonly bool) { tc.transactf("ok", "noop") tc.xuntagged() // Already seen. - tc.client.StoreFlagsClear("1", true, `\Seen`) + tc.client.MSNStoreFlagsClear("1", true, `\Seen`) tc.transactf("ok", "fetch 1 body[1]") tc.xuntagged(tc.untaggedFetch(1, 1, bodypart1, noflags)) tc.transactf("ok", "noop") tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen)) - tc.client.StoreFlagsClear("1", true, `\Seen`) + tc.client.MSNStoreFlagsClear("1", true, `\Seen`) tc.transactf("ok", "fetch 1 body[1]<1.2>") tc.xuntagged(tc.untaggedFetch(1, 1, body1off1, noflags)) tc.transactf("ok", "noop") tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen)) - tc.client.StoreFlagsClear("1", true, `\Seen`) + tc.client.MSNStoreFlagsClear("1", true, `\Seen`) tc.transactf("ok", "fetch 1 body[1]<100000.100000>") tc.xuntagged(tc.untaggedFetch(1, 1, bodyend1, noflags)) tc.transactf("ok", "noop") tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen)) - tc.client.StoreFlagsClear("1", true, `\Seen`) + tc.client.MSNStoreFlagsClear("1", true, `\Seen`) tc.transactf("ok", "fetch 1 body[header]") tc.xuntagged(tc.untaggedFetch(1, 1, bodyheader1, noflags)) tc.transactf("ok", "noop") tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen)) - tc.client.StoreFlagsClear("1", true, `\Seen`) + tc.client.MSNStoreFlagsClear("1", true, `\Seen`) tc.transactf("ok", "fetch 1 body[text]") tc.xuntagged(tc.untaggedFetch(1, 1, bodytext1, noflags)) tc.transactf("ok", "noop") @@ -191,21 +197,21 @@ func testFetch(t *testing.T, uidonly bool) { tc.xuntagged(tc.untaggedFetch(1, 1, rfcheader1)) // equivalent to body[text], ../rfc/3501:3199 - tc.client.StoreFlagsClear("1", true, `\Seen`) + tc.client.MSNStoreFlagsClear("1", true, `\Seen`) tc.transactf("ok", "fetch 1 rfc822.text") tc.xuntagged(tc.untaggedFetch(1, 1, rfctext1, noflags)) tc.transactf("ok", "noop") tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen)) // equivalent to body[], ../rfc/3501:3179 - tc.client.StoreFlagsClear("1", true, `\Seen`) + tc.client.MSNStoreFlagsClear("1", true, `\Seen`) tc.transactf("ok", "fetch 1 rfc822") tc.xuntagged(tc.untaggedFetch(1, 1, rfc1, noflags)) tc.transactf("ok", "noop") tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen)) // With PEEK, we should not get the \Seen flag. - tc.client.StoreFlagsClear("1", true, `\Seen`) + tc.client.MSNStoreFlagsClear("1", true, `\Seen`) tc.transactf("ok", "fetch 1 body.peek[]") tc.xuntagged(tc.untaggedFetch(1, 1, body1)) @@ -229,7 +235,7 @@ func testFetch(t *testing.T, uidonly bool) { // Missing sequence number. ../rfc/9051:7018 tc.transactf("bad", "fetch 2 body[]") - tc.client.StoreFlagsClear("1", true, `\Seen`) + tc.client.MSNStoreFlagsClear("1", true, `\Seen`) tc.transactf("ok", "fetch 1:1 body[]") tc.xuntagged(tc.untaggedFetch(1, 1, body1, noflags)) tc.transactf("ok", "noop") @@ -300,17 +306,28 @@ func testFetch(t *testing.T, uidonly bool) { RespAttr: "BODYSTRUCTURE", Body: imapclient.BodyTypeMpart{ Bodies: []any{ - imapclient.BodyTypeBasic{BodyFields: imapclient.BodyFields{Octets: 275}, Ext: &imapclient.BodyExtension1Part{}}, - imapclient.BodyTypeText{MediaType: "TEXT", MediaSubtype: "PLAIN", BodyFields: imapclient.BodyFields{Params: [][2]string{{"CHARSET", "US-ASCII"}}, Octets: 114}, Lines: 3, Ext: &imapclient.BodyExtension1Part{}}, + imapclient.BodyTypeBasic{BodyFields: imapclient.BodyFields{Octets: 275}, Ext: &bodyext1}, + imapclient.BodyTypeText{MediaType: "TEXT", MediaSubtype: "PLAIN", BodyFields: imapclient.BodyFields{Params: [][2]string{{"CHARSET", "US-ASCII"}}, Octets: 114}, Lines: 3, Ext: &bodyext1}, imapclient.BodyTypeMpart{ Bodies: []any{ - imapclient.BodyTypeBasic{MediaType: "AUDIO", MediaSubtype: "BASIC", BodyFields: imapclient.BodyFields{CTE: "BASE64", Octets: 22}, Ext: &imapclient.BodyExtension1Part{}}, - imapclient.BodyTypeBasic{MediaType: "IMAGE", MediaSubtype: "JPEG", BodyFields: imapclient.BodyFields{CTE: "BASE64"}, Ext: &imapclient.BodyExtension1Part{Disposition: "inline", DispositionParams: [][2]string{{"filename", "image.jpg"}}}}, + imapclient.BodyTypeBasic{MediaType: "AUDIO", MediaSubtype: "BASIC", BodyFields: imapclient.BodyFields{CTE: "BASE64", Octets: 22}, Ext: &bodyext1}, + imapclient.BodyTypeBasic{MediaType: "IMAGE", MediaSubtype: "JPEG", BodyFields: imapclient.BodyFields{CTE: "BASE64"}, Ext: &imapclient.BodyExtension1Part{ + Disposition: ptr(ptr("inline")), + DispositionParams: ptr([][2]string{{"filename", "image.jpg"}}), + Language: ptr([]string(nil)), + Location: ptr((*string)(nil)), + }}, }, MediaSubtype: "PARALLEL", - Ext: &imapclient.BodyExtensionMpart{Params: [][2]string{{"BOUNDARY", "unique-boundary-2"}}}, + Ext: &imapclient.BodyExtensionMpart{ + Params: [][2]string{{"BOUNDARY", "unique-boundary-2"}}, + Disposition: ptr((*string)(nil)), // Present but nil. + DispositionParams: ptr([][2]string(nil)), + Language: ptr([]string(nil)), + Location: ptr((*string)(nil)), + }, }, - imapclient.BodyTypeText{MediaType: "TEXT", MediaSubtype: "ENRICHED", BodyFields: imapclient.BodyFields{Octets: 145}, Lines: 5, Ext: &imapclient.BodyExtension1Part{}}, + imapclient.BodyTypeText{MediaType: "TEXT", MediaSubtype: "ENRICHED", BodyFields: imapclient.BodyFields{Octets: 145}, Lines: 5, Ext: &bodyext1}, imapclient.BodyTypeMsg{ MediaType: "MESSAGE", MediaSubtype: "RFC822", @@ -323,17 +340,25 @@ func testFetch(t *testing.T, uidonly bool) { To: []imapclient.Address{{Name: "mox", Adl: "", Mailbox: "info", Host: "mox.example"}}, }, Bodystructure: imapclient.BodyTypeText{ - MediaType: "TEXT", MediaSubtype: "PLAIN", BodyFields: imapclient.BodyFields{Params: [][2]string{{"CHARSET", "ISO-8859-1"}}, CTE: "QUOTED-PRINTABLE", Octets: 51}, Lines: 1, Ext: &imapclient.BodyExtension1Part{}}, + MediaType: "TEXT", MediaSubtype: "PLAIN", BodyFields: imapclient.BodyFields{Params: [][2]string{{"CHARSET", "ISO-8859-1"}}, CTE: "QUOTED-PRINTABLE", Octets: 51}, Lines: 1, Ext: &bodyext1}, Lines: 7, Ext: &imapclient.BodyExtension1Part{ - MD5: "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=", - Language: []string{"en", "de"}, - Location: "http://localhost", + MD5: ptr("MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY="), + Disposition: ptr((*string)(nil)), + DispositionParams: ptr([][2]string(nil)), + Language: ptr([]string{"en", "de"}), + Location: ptr(ptr("http://localhost")), }, }, }, MediaSubtype: "MIXED", - Ext: &imapclient.BodyExtensionMpart{Params: [][2]string{{"BOUNDARY", "unique-boundary-1"}}}, + Ext: &imapclient.BodyExtensionMpart{ + Params: [][2]string{{"BOUNDARY", "unique-boundary-1"}}, + Disposition: ptr((*string)(nil)), // Present but nil. + DispositionParams: ptr([][2]string(nil)), + Language: ptr([]string(nil)), + Location: ptr((*string)(nil)), + }, }, } tc.client.Append("inbox", makeAppendTime(nestedMessage, received)) diff --git a/imapserver/fuzz_test.go b/imapserver/fuzz_test.go index b31c8dc..74b2c53 100644 --- a/imapserver/fuzz_test.go +++ b/imapserver/fuzz_test.go @@ -134,11 +134,11 @@ func FuzzServer(f *testing.F) { client, _ := imapclient.New(clientConn, &opts) for _, cmd := range cmds { - client.Commandf("", "%s", cmd) - client.Response() + client.WriteCommandf("", "%s", cmd) + client.ReadResponse() } - client.Commandf("", "%s", s) - client.Response() + client.WriteCommandf("", "%s", s) + client.ReadResponse() }() err = serverConn.SetDeadline(time.Now().Add(time.Second)) diff --git a/imapserver/metadata.go b/imapserver/metadata.go index f64f28a..f00c43a 100644 --- a/imapserver/metadata.go +++ b/imapserver/metadata.go @@ -303,13 +303,13 @@ func (c *conn) xcheckMetadataSize(tx *bstore.Tx) { n++ if n > metadataMaxKeys { // ../rfc/5464:590 - xusercodeErrorf("METADATA TOOMANY", "too many metadata entries, 1000 allowed in total") + xusercodeErrorf("METADATA (TOOMANY)", "too many metadata entries, 1000 allowed in total") } size += len(a.Key) + len(a.Value) if size > metadataMaxSize { // ../rfc/5464:585 We only have a max total size limit, not per entry. We'll // mention the max total size. - xusercodeErrorf(fmt.Sprintf("METADATA MAXSIZE %d", metadataMaxSize), "metadata entry values too large, total maximum size is 1MB") + xusercodeErrorf(fmt.Sprintf("METADATA (MAXSIZE %d)", metadataMaxSize), "metadata entry values too large, total maximum size is 1MB") } return nil }) diff --git a/imapserver/metadata_test.go b/imapserver/metadata_test.go index 003c848..9877720 100644 --- a/imapserver/metadata_test.go +++ b/imapserver/metadata_test.go @@ -38,7 +38,7 @@ func testMetadata(t *testing.T, uidonly bool) { tc.transactf("ok", `delete metabox`) // Delete mailbox with live and expunged metadata. tc.transactf("no", `setmetadata expungebox (/private/comment "mailbox value")`) - tc.xcode("TRYCREATE") + tc.xcodeWord("TRYCREATE") tc.transactf("ok", `getmetadata "" ("/private/comment")`) tc.xuntagged(imapclient.UntaggedMetadataAnnotations{ @@ -114,7 +114,7 @@ func testMetadata(t *testing.T, uidonly bool) { // Request with a maximum size, we don't get anything larger. tc.transactf("ok", `setmetadata inbox (/private/another "longer")`) tc.transactf("ok", `getmetadata (maxsize 4) inbox (/private/comment /private/another)`) - tc.xcodeArg(imapclient.CodeOther{Code: "METADATA", Args: []string{"LONGENTRIES", "6"}}) + tc.xcode(imapclient.CodeMetadataLongEntries(6)) tc.xuntagged(imapclient.UntaggedMetadataAnnotations{ Mailbox: "Inbox", Annotations: []imapclient.Annotation{ @@ -230,7 +230,7 @@ func testMetadata(t *testing.T, uidonly bool) { } // Broadcast should happen when metadata capability is enabled. - tc2.client.Enable(string(imapclient.CapMetadata)) + tc2.client.Enable(imapclient.CapMetadata) tc2.cmdf("", "idle") tc2.readprefixline("+ ") done = make(chan error) @@ -285,12 +285,12 @@ func TestMetadataLimit(t *testing.T) { tc.client.Write(buf) tc.client.Writelinef(")") tc.response("no") - tc.xcodeArg(imapclient.CodeOther{Code: "METADATA", Args: []string{"MAXSIZE", fmt.Sprintf("%d", metadataMaxSize)}}) + tc.xcode(imapclient.CodeMetadataMaxSize(metadataMaxSize)) // Reach limit for max number. for i := 1; i <= metadataMaxKeys; i++ { tc.transactf("ok", `setmetadata inbox (/private/key%d "test")`, i) } tc.transactf("no", `setmetadata inbox (/private/toomany "test")`) - tc.xcodeArg(imapclient.CodeOther{Code: "METADATA", Args: []string{"TOOMANY"}}) + tc.xcode(imapclient.CodeMetadataTooMany{}) } diff --git a/imapserver/move_test.go b/imapserver/move_test.go index 85deef5..26485d4 100644 --- a/imapserver/move_test.go +++ b/imapserver/move_test.go @@ -56,10 +56,10 @@ func testMove(t *testing.T, uidonly bool) { tc.client.Select("inbox") tc.transactf("no", "move 1 nonexistent") - tc.xcode("TRYCREATE") + tc.xcodeWord("TRYCREATE") tc.transactf("no", "move 1 expungebox") - tc.xcode("TRYCREATE") + tc.xcodeWord("TRYCREATE") tc.transactf("no", "move 1 inbox") // Cannot move to same mailbox. @@ -68,7 +68,7 @@ func testMove(t *testing.T, uidonly bool) { tc.transactf("ok", "move 1:* Trash") tc.xuntagged( - imapclient.UntaggedResult{Status: "OK", RespText: imapclient.RespText{Code: "COPYUID", CodeArg: imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}, To: []imapclient.NumRange{{First: 1, Last: uint32ptr(2)}}}, More: "moved"}}, + imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}, To: []imapclient.NumRange{{First: 1, Last: uint32ptr(2)}}}, Text: "moved"}, imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1), ) @@ -92,12 +92,12 @@ func testMove(t *testing.T, uidonly bool) { tc.transactf("ok", "uid move 6:5 Trash") if uidonly { tc.xuntagged( - imapclient.UntaggedResult{Status: "OK", RespText: imapclient.RespText{Code: "COPYUID", CodeArg: imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 5, Last: uint32ptr(6)}}, To: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}}, More: "moved"}}, + imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 5, Last: uint32ptr(6)}}, To: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}}, Text: "moved"}, imapclient.UntaggedVanished{UIDs: xparseNumSet("5:6")}, ) } else { tc.xuntagged( - imapclient.UntaggedResult{Status: "OK", RespText: imapclient.RespText{Code: "COPYUID", CodeArg: imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 5, Last: uint32ptr(6)}}, To: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}}, More: "moved"}}, + imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 5, Last: uint32ptr(6)}}, To: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}}, Text: "moved"}, imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1), ) diff --git a/imapserver/notify_test.go b/imapserver/notify_test.go index 181e33f..326c35a 100644 --- a/imapserver/notify_test.go +++ b/imapserver/notify_test.go @@ -42,9 +42,9 @@ func testNotify(t *testing.T, uidonly bool) { tc.transactf("bad", "Notify Set Status (Selected (flagChange))") // flagChange must come with MessageNew and MessageExpunge. tc.transactf("bad", "Notify Set Status (Selected (mailboxName)) (Selected-Delayed (mailboxName))") // Duplicate selected. tc.transactf("no", "Notify Set Status (Selected (annotationChange))") // We don't implement annotation change. - tc.xcode("BADEVENT") + tc.xcode(imapclient.CodeBadEvent{"MessageNew", "MessageExpunge", "FlagChange", "MailboxName", "SubscriptionChange", "MailboxMetadataChange", "ServerMetadataChange"}) tc.transactf("no", "Notify Set Status (Personal (unknownEvent))") - tc.xcode("BADEVENT") + tc.xcode(imapclient.CodeBadEvent{"MessageNew", "MessageExpunge", "FlagChange", "MailboxName", "SubscriptionChange", "MailboxMetadataChange", "ServerMetadataChange"}) tc2 := startNoSwitchboard(t, uidonly) defer tc2.closeNoWait() @@ -76,7 +76,7 @@ func testNotify(t *testing.T, uidonly bool) { // Enable notify, will first result in a the pending changes, then status. tc.transactf("ok", "Notify Set Status (Selected (messageNew (Uid Modseq Bodystructure Preview) messageExpunge flagChange)) (personal (messageNew messageExpunge flagChange mailboxName subscriptionChange mailboxMetadataChange serverMetadataChange))") tc.xuntagged( - imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(modseq), More: "after condstore-enabling command"}}, + imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeHighestModSeq(modseq), Text: "after condstore-enabling command"}, // note: no status for Inbox since it is selected. imapclient.UntaggedStatus{Mailbox: "Drafts", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 0, imapclient.StatusUIDNext: 1, imapclient.StatusUIDValidity: 1, imapclient.StatusUnseen: 0, imapclient.StatusHighestModSeq: 2}}, imapclient.UntaggedStatus{Mailbox: "Sent", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 0, imapclient.StatusUIDNext: 1, imapclient.StatusUIDValidity: 1, imapclient.StatusUnseen: 0, imapclient.StatusHighestModSeq: 2}}, @@ -108,7 +108,12 @@ func testNotify(t *testing.T, uidonly bool) { Octets: 21, }, Lines: 1, - Ext: &imapclient.BodyExtension1Part{}, + Ext: &imapclient.BodyExtension1Part{ + Disposition: ptr((*string)(nil)), + DispositionParams: ptr([][2]string(nil)), + Language: ptr([]string(nil)), + Location: ptr((*string)(nil)), + }, }, imapclient.BodyTypeText{ MediaType: "TEXT", @@ -118,12 +123,21 @@ func testNotify(t *testing.T, uidonly bool) { Octets: 15, }, Lines: 1, - Ext: &imapclient.BodyExtension1Part{}, + Ext: &imapclient.BodyExtension1Part{ + Disposition: ptr((*string)(nil)), + DispositionParams: ptr([][2]string(nil)), + Language: ptr([]string(nil)), + Location: ptr((*string)(nil)), + }, }, }, MediaSubtype: "ALTERNATIVE", Ext: &imapclient.BodyExtensionMpart{ - Params: [][2]string{{"BOUNDARY", "x"}}, + Params: [][2]string{{"BOUNDARY", "x"}}, + Disposition: ptr((*string)(nil)), // Present but nil. + DispositionParams: ptr([][2]string(nil)), + Language: ptr([]string(nil)), + Location: ptr((*string)(nil)), }, }, }, @@ -413,12 +427,7 @@ func testNotify(t *testing.T, uidonly bool) { // modseq++ tc.readuntagged( imapclient.UntaggedExists(3), - imapclient.UntaggedResult{ - Status: "NO", - RespText: imapclient.RespText{ - More: "generating notify fetch response: requested part does not exist", - }, - }, + imapclient.UntaggedResult{Status: "NO", Text: "generating notify fetch response: requested part does not exist"}, tc.untaggedFetchUID(3, 4), ) @@ -457,15 +466,7 @@ func testNotifyOverflow(t *testing.T, uidonly bool) { tc2.client.Append("inbox", makeAppend(searchMsg)) tc.transactf("ok", "noop") - tc.xuntagged( - imapclient.UntaggedResult{ - Status: "OK", - RespText: imapclient.RespText{ - Code: "NOTIFICATIONOVERFLOW", - More: "out of sync after too many pending changes", - }, - }, - ) + tc.xuntagged(imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeWord("NOTIFICATIONOVERFLOW"), Text: "out of sync after too many pending changes"}) // Won't be getting any more notifications until we enable them again with NOTIFY. tc2.client.Append("inbox", makeAppend(searchMsg)) @@ -500,15 +501,7 @@ func testNotifyOverflow(t *testing.T, uidonly bool) { tc2.client.UIDStoreFlagsAdd("1", true, `\Seen`) tc2.client.UIDStoreFlagsClear("1", true, `\Seen`) tc.transactf("ok", "noop") - tc.xuntagged( - imapclient.UntaggedResult{ - Status: "OK", - RespText: imapclient.RespText{ - Code: "NOTIFICATIONOVERFLOW", - More: "out of sync after too many pending changes for selected mailbox", - }, - }, - ) + tc.xuntagged(imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeWord("NOTIFICATIONOVERFLOW"), Text: "out of sync after too many pending changes for selected mailbox"}) // Again, no new notifications until we select and enable again. tc2.client.UIDStoreFlagsAdd("1", true, `\Seen`) diff --git a/imapserver/rename_test.go b/imapserver/rename_test.go index 5ee7651..32f1b7b 100644 --- a/imapserver/rename_test.go +++ b/imapserver/rename_test.go @@ -30,11 +30,11 @@ func testRename(t *testing.T, uidonly bool) { tc.transactf("bad", "rename x y ") // Leftover data. tc.transactf("no", "rename doesnotexist newbox") // Does not exist. - tc.xcode("NONEXISTENT") // ../rfc/9051:5140 + tc.xcodeWord("NONEXISTENT") // ../rfc/9051:5140 tc.transactf("no", "rename expungebox newbox") // No longer exists. - tc.xcode("NONEXISTENT") + tc.xcodeWord("NONEXISTENT") tc.transactf("no", `rename "Sent" "Trash"`) // Already exists. - tc.xcode("ALREADYEXISTS") + tc.xcodeWord("ALREADYEXISTS") tc.client.Create("x", nil) tc.client.Subscribe("sub") @@ -47,7 +47,7 @@ func testRename(t *testing.T, uidonly bool) { tc2.xuntagged(imapclient.UntaggedList{Separator: '/', Mailbox: "z"}) // OldName is only set for IMAP4rev2 or NOTIFY. - tc2.client.Enable("IMAP4rev2") + tc2.client.Enable(imapclient.CapIMAP4rev2) tc.transactf("ok", "rename z y") tc2.transactf("ok", "noop") tc2.xuntagged(imapclient.UntaggedList{Separator: '/', Mailbox: "y", OldName: "z"}) @@ -59,9 +59,9 @@ func testRename(t *testing.T, uidonly bool) { // Cannot rename a child to a parent. It already exists. tc.transactf("no", "rename a/b/c a/b") - tc.xcode("ALREADYEXISTS") + tc.xcodeWord("ALREADYEXISTS") tc.transactf("no", "rename a/b a") - tc.xcode("ALREADYEXISTS") + tc.xcodeWord("ALREADYEXISTS") tc2.transactf("ok", "noop") // Drain. tc.transactf("ok", "rename a/b x/y") // This will cause new parent "x" to be created, and a/b and a/b/c to be renamed. diff --git a/imapserver/replace_test.go b/imapserver/replace_test.go index 5ef1186..f68c1aa 100644 --- a/imapserver/replace_test.go +++ b/imapserver/replace_test.go @@ -33,37 +33,37 @@ func testReplace(t *testing.T, uidonly bool) { } // Append 3 messages, remove first. Leaves msgseq 1,2 with uid 2,3. - tc.client.Append("inbox", makeAppend(exampleMsg), makeAppend(exampleMsg), makeAppend(exampleMsg)) + tc.client.MultiAppend("inbox", makeAppend(exampleMsg), makeAppend(exampleMsg), makeAppend(exampleMsg)) tc.client.UIDStoreFlagsSet("1", true, `\deleted`) tc.client.Expunge() tc.transactf("no", "uid replace 1 expungebox {1}") // Mailbox no longer exists. - tc.xcode("TRYCREATE") + tc.xcodeWord("TRYCREATE") tc2.login("mjl@mox.example", password0) tc2.client.Select("inbox") // Replace last message (msgseq 2, uid 3) in same mailbox. if uidonly { - tc.lastUntagged, tc.lastResult, tc.lastErr = tc.client.UIDReplace("3", "INBOX", makeAppend(searchMsg)) + tc.lastResponse, tc.lastErr = tc.client.UIDReplace("3", "INBOX", makeAppend(searchMsg)) } else { - tc.lastUntagged, tc.lastResult, tc.lastErr = tc.client.Replace("2", "INBOX", makeAppend(searchMsg)) + tc.lastResponse, tc.lastErr = tc.client.MSNReplace("2", "INBOX", makeAppend(searchMsg)) } tcheck(tc.t, tc.lastErr, "read imap response") if uidonly { tc.xuntagged( - imapclient.UntaggedResult{Status: "OK", RespText: imapclient.RespText{Code: "APPENDUID", CodeArg: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("4")}, More: ""}}, + imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("4")}, Text: ""}, imapclient.UntaggedExists(3), imapclient.UntaggedVanished{UIDs: xparseNumSet("3")}, ) } else { tc.xuntagged( - imapclient.UntaggedResult{Status: "OK", RespText: imapclient.RespText{Code: "APPENDUID", CodeArg: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("4")}, More: ""}}, + imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("4")}, Text: ""}, imapclient.UntaggedExists(3), imapclient.UntaggedExpunge(2), ) } - tc.xcodeArg(imapclient.CodeHighestModSeq(8)) + tc.xcode(imapclient.CodeHighestModSeq(8)) // Check that other client sees Exists and Expunge. tc2.transactf("ok", "noop") @@ -83,26 +83,26 @@ func testReplace(t *testing.T, uidonly bool) { // Enable qresync, replace uid 2 (msgseq 1) to different mailbox, see that we get vanished instead of expunged. tc.transactf("ok", "enable qresync") - tc.lastUntagged, tc.lastResult, tc.lastErr = tc.client.UIDReplace("2", "INBOX", makeAppend(searchMsg)) + tc.lastResponse, tc.lastErr = tc.client.UIDReplace("2", "INBOX", makeAppend(searchMsg)) tcheck(tc.t, tc.lastErr, "read imap response") tc.xuntagged( - imapclient.UntaggedResult{Status: "OK", RespText: imapclient.RespText{Code: "APPENDUID", CodeArg: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("5")}, More: ""}}, + imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("5")}, Text: ""}, imapclient.UntaggedExists(3), imapclient.UntaggedVanished{UIDs: xparseNumSet("2")}, ) - tc.xcodeArg(imapclient.CodeHighestModSeq(9)) + tc.xcode(imapclient.CodeHighestModSeq(9)) // Use "*" for replacing. tc.transactf("ok", "uid replace * inbox {1+}\r\nx") tc.xuntagged( - imapclient.UntaggedResult{Status: "OK", RespText: imapclient.RespText{Code: "APPENDUID", CodeArg: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("6")}, More: ""}}, + imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("6")}, Text: ""}, imapclient.UntaggedExists(3), imapclient.UntaggedVanished{UIDs: xparseNumSet("5")}, ) if !uidonly { tc.transactf("ok", "replace * inbox {1+}\r\ny") tc.xuntagged( - imapclient.UntaggedResult{Status: "OK", RespText: imapclient.RespText{Code: "APPENDUID", CodeArg: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("7")}, More: ""}}, + imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("7")}, Text: ""}, imapclient.UntaggedExists(3), imapclient.UntaggedVanished{UIDs: xparseNumSet("6")}, ) @@ -129,9 +129,9 @@ func TestReplaceBigNonsyncLit(t *testing.T) { // Adding a message >1mb with non-sync literal to non-existent mailbox should abort entire connection. tc.transactf("bad", "replace 12345 inbox {2000000+}") tc.xuntagged( - imapclient.UntaggedBye{Code: "ALERT", More: "error condition and non-synchronizing literal too big"}, + imapclient.UntaggedBye{Code: imapclient.CodeWord("ALERT"), Text: "error condition and non-synchronizing literal too big"}, ) - tc.xcode("TOOBIG") + tc.xcodeWord("TOOBIG") } func TestReplaceQuota(t *testing.T) { @@ -153,11 +153,11 @@ func testReplaceQuota(t *testing.T, uidonly bool) { // Synchronizing literal, we get failure immediately. tc.transactf("no", "uid replace 1 inbox {6}\r\n") - tc.xcode("OVERQUOTA") + tc.xcodeWord("OVERQUOTA") // Synchronizing literal to non-existent mailbox, we get failure immediately. tc.transactf("no", "uid replace 1 badbox {6}\r\n") - tc.xcode("TRYCREATE") + tc.xcodeWord("TRYCREATE") buf := make([]byte, 4000, 4002) for i := range buf { @@ -166,18 +166,18 @@ func testReplaceQuota(t *testing.T, uidonly bool) { buf = append(buf, "\r\n"...) // Non-synchronizing literal. We get to write our data. - tc.client.Commandf("", "uid replace 1 inbox ~{4000+}") + tc.client.WriteCommandf("", "uid replace 1 inbox ~{4000+}") _, err := tc.client.Write(buf) tc.check(err, "write replace message") tc.response("no") - tc.xcode("OVERQUOTA") + tc.xcodeWord("OVERQUOTA") // Non-synchronizing literal to bad mailbox. - tc.client.Commandf("", "uid replace 1 badbox {4000+}") + tc.client.WriteCommandf("", "uid replace 1 badbox {4000+}") _, err = tc.client.Write(buf) tc.check(err, "write replace message") tc.response("no") - tc.xcode("TRYCREATE") + tc.xcodeWord("TRYCREATE") } func TestReplaceExpunged(t *testing.T) { @@ -197,7 +197,7 @@ func testReplaceExpunged(t *testing.T, uidonly bool) { tc.client.Append("inbox", makeAppend(exampleMsg)) // We start the command, but don't write data yet. - tc.client.Commandf("", "uid replace 1 inbox {4000}") + tc.client.WriteCommandf("", "uid replace 1 inbox {4000}") // Get in with second client and remove the message we are replacing. tc2 := startNoSwitchboard(t, uidonly) diff --git a/imapserver/search_test.go b/imapserver/search_test.go index d5ebe02..b9e5bd6 100644 --- a/imapserver/search_test.go +++ b/imapserver/search_test.go @@ -57,7 +57,7 @@ func (tc *testconn) xsearchmodseq(modseq int64, nums ...uint32) { func (tc *testconn) xesearch(exp imapclient.UntaggedEsearch) { tc.t.Helper() - exp.Tag = tc.client.LastTag + exp.Tag = tc.client.LastTag() tc.xuntagged(exp) } @@ -298,11 +298,8 @@ func testSearch(t *testing.T, uidonly bool) { inprogress := func(cur, goal uint32) imapclient.UntaggedResult { return imapclient.UntaggedResult{ Status: "OK", - RespText: imapclient.RespText{ - Code: "INPROGRESS", - CodeArg: imapclient.CodeInProgress{Tag: "tag1", Current: &cur, Goal: &goal}, - More: "still searching", - }, + Code: imapclient.CodeInProgress{Tag: "tag1", Current: &cur, Goal: &goal}, + Text: "still searching", } } tc.xuntagged( @@ -408,7 +405,7 @@ func testSearch(t *testing.T, uidonly bool) { } // Do a seemingly old-style search command with IMAP4rev2 enabled. We'll still get ESEARCH responses. - tc.client.Enable("IMAP4rev2") + tc.client.Enable(imapclient.CapIMAP4rev2) if !uidonly { tc.transactf("ok", `search undraft`) @@ -566,11 +563,8 @@ func testSearchMulti(t *testing.T, selected, uidonly bool) { inprogress := func(cur, goal uint32) imapclient.UntaggedResult { return imapclient.UntaggedResult{ Status: "OK", - RespText: imapclient.RespText{ - Code: "INPROGRESS", - CodeArg: imapclient.CodeInProgress{Tag: "Tag1", Current: &cur, Goal: &goal}, - More: "still searching", - }, + Code: imapclient.CodeInProgress{Tag: "Tag1", Current: &cur, Goal: &goal}, + Text: "still searching", } } tc.cmdf("Tag1", `Esearch In (Personal) Return () All`) diff --git a/imapserver/selectexamine_test.go b/imapserver/selectexamine_test.go index afc24b7..610ee64 100644 --- a/imapserver/selectexamine_test.go +++ b/imapserver/selectexamine_test.go @@ -38,19 +38,19 @@ func testSelectExamine(t *testing.T, examine, uidonly bool) { okcode = "READ-ONLY" } - uclosed := imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "CLOSED", More: "x"}} + uclosed := imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeWord("CLOSED"), Text: "x"} flags := strings.Split(`\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent`, " ") permflags := strings.Split(`\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent \*`, " ") uflags := imapclient.UntaggedFlags(flags) - upermflags := imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "PERMANENTFLAGS", CodeArg: imapclient.CodeList{Code: "PERMANENTFLAGS", Args: permflags}, More: "x"}} + upermflags := imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodePermanentFlags(permflags), Text: "x"} urecent := imapclient.UntaggedRecent(0) uexists0 := imapclient.UntaggedExists(0) uexists1 := imapclient.UntaggedExists(1) - uuidval1 := imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UIDVALIDITY", CodeArg: imapclient.CodeUint{Code: "UIDVALIDITY", Num: 1}, More: "x"}} - uuidnext1 := imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UIDNEXT", CodeArg: imapclient.CodeUint{Code: "UIDNEXT", Num: 1}, More: "x"}} + uuidval1 := imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeUIDValidity(1), Text: "x"} + uuidnext1 := imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeUIDNext(1), Text: "x"} ulist := imapclient.UntaggedList{Separator: '/', Mailbox: "Inbox"} - uunseen := imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UNSEEN", CodeArg: imapclient.CodeUint{Code: "UNSEEN", Num: 1}, More: "x"}} - uuidnext2 := imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UIDNEXT", CodeArg: imapclient.CodeUint{Code: "UIDNEXT", Num: 2}, More: "x"}} + uunseen := imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeUnseen(1), Text: "x"} + uuidnext2 := imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeUIDNext(2), Text: "x"} // Parameter required. tc.transactf("bad", "%s", cmd) @@ -61,11 +61,11 @@ func testSelectExamine(t *testing.T, examine, uidonly bool) { tc.transactf("ok", "%s inbox", cmd) tc.xuntagged(uflags, upermflags, urecent, uexists0, uuidval1, uuidnext1, ulist) - tc.xcode(okcode) + tc.xcodeWord(okcode) tc.transactf("ok", `%s "inbox"`, cmd) tc.xuntagged(uclosed, uflags, upermflags, urecent, uexists0, uuidval1, uuidnext1, ulist) - tc.xcode(okcode) + tc.xcodeWord(okcode) // Append a message. It will be reported as UNSEEN. tc.client.Append("inbox", makeAppend(exampleMsg)) @@ -75,11 +75,11 @@ func testSelectExamine(t *testing.T, examine, uidonly bool) { } else { tc.xuntagged(uclosed, uflags, upermflags, urecent, uunseen, uexists1, uuidval1, uuidnext2, ulist) } - tc.xcode(okcode) + tc.xcodeWord(okcode) // With imap4rev2, we no longer get untagged RECENT or untagged UNSEEN. - tc.client.Enable("imap4rev2") + tc.client.Enable(imapclient.CapIMAP4rev2) tc.transactf("ok", "%s inbox", cmd) tc.xuntagged(uclosed, uflags, upermflags, uexists1, uuidval1, uuidnext2, ulist) - tc.xcode(okcode) + tc.xcodeWord(okcode) } diff --git a/imapserver/server_test.go b/imapserver/server_test.go index 4963a88..8c4b51f 100644 --- a/imapserver/server_test.go +++ b/imapserver/server_test.go @@ -161,6 +161,22 @@ func tcheck(t *testing.T, err error, msg string) { } } +func mustParseUntagged(s string) imapclient.Untagged { + r, err := imapclient.ParseUntagged(s + "\r\n") + if err != nil { + panic(err) + } + return r +} + +func mustParseCode(s string) imapclient.Code { + r, err := imapclient.ParseCode(s) + if err != nil { + panic(err) + } + return r +} + func mockUIDValidity() func() { orig := store.InitialUIDValidity store.InitialUIDValidity = func() uint32 { @@ -182,8 +198,7 @@ type testconn struct { switchStop func() // Result of last command. - lastUntagged []imapclient.Untagged - lastResult imapclient.Result + lastResponse imapclient.Response lastErr error } @@ -194,24 +209,21 @@ func (tc *testconn) check(err error, msg string) { } } -func (tc *testconn) last(l []imapclient.Untagged, r imapclient.Result, err error) { - tc.lastUntagged = l - tc.lastResult = r +func (tc *testconn) last(resp imapclient.Response, err error) { + tc.lastResponse = resp tc.lastErr = err } -func (tc *testconn) xcode(s string) { +func (tc *testconn) xcode(c imapclient.Code) { tc.t.Helper() - if tc.lastResult.Code != s { - tc.t.Fatalf("got last code %q, expected %q", tc.lastResult.Code, s) + if !reflect.DeepEqual(tc.lastResponse.Code, c) { + tc.t.Fatalf("got last code %#v, expected %#v", tc.lastResponse.Code, c) } } -func (tc *testconn) xcodeArg(v any) { +func (tc *testconn) xcodeWord(s string) { tc.t.Helper() - if !reflect.DeepEqual(tc.lastResult.CodeArg, v) { - tc.t.Fatalf("got last code argument %v, expected %v", tc.lastResult.CodeArg, v) - } + tc.xcode(imapclient.CodeWord(s)) } func (tc *testconn) xuntagged(exps ...imapclient.Untagged) { @@ -221,7 +233,7 @@ func (tc *testconn) xuntagged(exps ...imapclient.Untagged) { func (tc *testconn) xuntaggedOpt(all bool, exps ...imapclient.Untagged) { tc.t.Helper() - last := slices.Clone(tc.lastUntagged) + last := slices.Clone(tc.lastResponse.Untagged) var mismatch any next: for ei, exp := range exps { @@ -241,10 +253,10 @@ next: tc.t.Fatalf("untagged data mismatch, got:\n\t%T %#v\nexpected:\n\t%T %#v", mismatch, mismatch, exp, exp) } var next string - if len(tc.lastUntagged) > 0 { - next = fmt.Sprintf(", next:\n%#v", tc.lastUntagged[0]) + if len(tc.lastResponse.Untagged) > 0 { + next = fmt.Sprintf(", next:\n%#v", tc.lastResponse.Untagged[0]) } - tc.t.Fatalf("did not find untagged response:\n%#v %T (%d)\nin %v%s", exp, exp, ei, tc.lastUntagged, next) + tc.t.Fatalf("did not find untagged response:\n%#v %T (%d)\nin %v%s", exp, exp, ei, tc.lastResponse.Untagged, next) } if len(last) > 0 && all { tc.t.Fatalf("leftover untagged responses %v", last) @@ -263,8 +275,8 @@ func tuntagged(t *testing.T, got imapclient.Untagged, dst any) { func (tc *testconn) xnountagged() { tc.t.Helper() - if len(tc.lastUntagged) != 0 { - tc.t.Fatalf("got %v untagged, expected 0", tc.lastUntagged) + if len(tc.lastResponse.Untagged) != 0 { + tc.t.Fatalf("got %v untagged, expected 0", tc.lastResponse.Untagged) } } @@ -288,16 +300,24 @@ func (tc *testconn) transactf(status, format string, args ...any) { func (tc *testconn) response(status string) { tc.t.Helper() - tc.lastUntagged, tc.lastResult, tc.lastErr = tc.client.Response() - tcheck(tc.t, tc.lastErr, "read imap response") - if strings.ToUpper(status) != string(tc.lastResult.Status) { - tc.t.Fatalf("got status %q, expected %q", tc.lastResult.Status, status) + tc.lastResponse, tc.lastErr = tc.client.ReadResponse() + if tc.lastErr != nil { + if resp, ok := tc.lastErr.(imapclient.Response); ok { + if !reflect.DeepEqual(resp, tc.lastResponse) { + tc.t.Fatalf("response error %#v != returned response %#v", tc.lastErr, tc.lastResponse) + } + } else { + tcheck(tc.t, tc.lastErr, "read imap response") + } + } + if strings.ToUpper(status) != string(tc.lastResponse.Status) { + tc.t.Fatalf("got status %q, expected %q", tc.lastResponse.Status, status) } } func (tc *testconn) cmdf(tag, format string, args ...any) { tc.t.Helper() - err := tc.client.Commandf(tag, format, args...) + err := tc.client.WriteCommandf(tag, format, args...) tcheck(tc.t, err, "writing imap command") } @@ -658,15 +678,15 @@ func TestLiterals(t *testing.T) { from := "ntmpbox" to := "tmpbox" + tc.client.LastTagSet("xtag") fmt.Fprint(tc.client, "xtag rename ") tc.client.WriteSyncLiteral(from) fmt.Fprint(tc.client, " ") tc.client.WriteSyncLiteral(to) fmt.Fprint(tc.client, "\r\n") - tc.client.LastTag = "xtag" - tc.last(tc.client.Response()) - if tc.lastResult.Status != "OK" { - tc.t.Fatalf(`got %q, expected "OK"`, tc.lastResult.Status) + tc.lastResponse, tc.lastErr = tc.client.ReadResponse() + if tc.lastResponse.Status != "OK" { + tc.t.Fatalf(`got %q, expected "OK"`, tc.lastResponse.Status) } } @@ -937,7 +957,7 @@ func TestReference(t *testing.T) { tc2.login("mjl@mox.example", password0) tc2.client.Select("inbox") - tc.client.StoreFlagsSet("1", true, `\Deleted`) + tc.client.MSNStoreFlagsSet("1", true, `\Deleted`) tc.client.Expunge() tc3 := startNoSwitchboard(t, false) @@ -945,7 +965,7 @@ func TestReference(t *testing.T) { tc3.login("mjl@mox.example", password0) tc3.transactf("ok", `list "" "inbox" return (status (messages))`) tc3.xuntagged( - imapclient.UntaggedList{Separator: '/', Mailbox: "Inbox"}, + mustParseUntagged(`* LIST () "/" Inbox`), imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 0}}, ) diff --git a/imapserver/starttls_test.go b/imapserver/starttls_test.go index 167fe74..418fc49 100644 --- a/imapserver/starttls_test.go +++ b/imapserver/starttls_test.go @@ -8,7 +8,7 @@ import ( func TestStarttls(t *testing.T) { tc := start(t, false) - tc.client.Starttls(&tls.Config{InsecureSkipVerify: true}) + tc.client.StartTLS(&tls.Config{InsecureSkipVerify: true}) tc.transactf("bad", "starttls") // TLS already active. tc.login("mjl@mox.example", password0) tc.close() @@ -19,10 +19,10 @@ func TestStarttls(t *testing.T) { tc = startArgs(t, false, true, false, false, true, "mjl") tc.transactf("no", `login "mjl@mox.example" "%s"`, password0) - tc.xcode("PRIVACYREQUIRED") + tc.xcodeWord("PRIVACYREQUIRED") tc.transactf("no", "authenticate PLAIN %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000"+password0))) - tc.xcode("PRIVACYREQUIRED") - tc.client.Starttls(&tls.Config{InsecureSkipVerify: true}) + tc.xcodeWord("PRIVACYREQUIRED") + tc.client.StartTLS(&tls.Config{InsecureSkipVerify: true}) tc.login("mjl@mox.example", password0) tc.close() } diff --git a/imapserver/store_test.go b/imapserver/store_test.go index acc4a13..9360bf1 100644 --- a/imapserver/store_test.go +++ b/imapserver/store_test.go @@ -20,7 +20,7 @@ func testStore(t *testing.T, uidonly bool) { defer tc.close() tc.login("mjl@mox.example", password0) - tc.client.Enable("imap4rev2") + tc.client.Enable(imapclient.CapIMAP4rev2) tc.client.Append("inbox", makeAppend(exampleMsg)) tc.client.Select("inbox") diff --git a/imapserver/uidonly_test.go b/imapserver/uidonly_test.go index 29bd959..f1591e1 100644 --- a/imapserver/uidonly_test.go +++ b/imapserver/uidonly_test.go @@ -11,29 +11,29 @@ func TestUIDOnly(t *testing.T) { tc.client.Select("inbox") tc.transactf("bad", "Fetch 1") - tc.xcode("UIDREQUIRED") + tc.xcodeWord("UIDREQUIRED") tc.transactf("bad", "Fetch 1") - tc.xcode("UIDREQUIRED") + tc.xcodeWord("UIDREQUIRED") tc.transactf("bad", "Search 1") - tc.xcode("UIDREQUIRED") + tc.xcodeWord("UIDREQUIRED") tc.transactf("bad", "Store 1 Flags ()") - tc.xcode("UIDREQUIRED") + tc.xcodeWord("UIDREQUIRED") tc.transactf("bad", "Copy 1 Archive") - tc.xcode("UIDREQUIRED") + tc.xcodeWord("UIDREQUIRED") tc.transactf("bad", "Move 1 Archive") - tc.xcode("UIDREQUIRED") + tc.xcodeWord("UIDREQUIRED") // Sequence numbers in search program. tc.transactf("bad", "Uid Search 1") - tc.xcode("UIDREQUIRED") + tc.xcodeWord("UIDREQUIRED") // Sequence number in last qresync parameter. tc.transactf("ok", "Enable Qresync") tc.transactf("bad", "Select inbox (Qresync (1 5 (1,3,6 1,3,6)))") - tc.xcode("UIDREQUIRED") + tc.xcodeWord("UIDREQUIRED") tc.client.Select("inbox") // Select again. // Breaks connection. tc.transactf("bad", "replace 1 inbox {1+}\r\nx") - tc.xcode("UIDREQUIRED") + tc.xcodeWord("UIDREQUIRED") } diff --git a/imapserver/unsubscribe_test.go b/imapserver/unsubscribe_test.go index 51f6e01..f1ad73d 100644 --- a/imapserver/unsubscribe_test.go +++ b/imapserver/unsubscribe_test.go @@ -10,10 +10,10 @@ func TestUnsubscribe(t *testing.T) { tc := start(t, false) defer tc.close() - tc.login("mjl@mox.example", password0) - tc2 := startNoSwitchboard(t, false) defer tc2.closeNoWait() + + tc.login("mjl@mox.example", password0) tc2.login("mjl@mox.example", password0) tc.transactf("bad", "unsubscribe") // Missing param. diff --git a/integration_test.go b/integration_test.go index 18abca2..56ced77 100644 --- a/integration_test.go +++ b/integration_test.go @@ -70,16 +70,16 @@ func TestDeliver(t *testing.T) { imapc, err := imapclient.New(imapconn, &opts) tcheck(t, err, "new imapclient") - _, _, err = imapc.Login(imapuser, imappassword) + _, err = imapc.Login(imapuser, imappassword) tcheck(t, err, "imap login") - _, _, err = imapc.Select("Inbox") + _, err = imapc.Select("Inbox") tcheck(t, err, "imap select inbox") - err = imapc.Commandf("", "idle") + err = imapc.WriteCommandf("", "idle") tcheck(t, err, "write imap idle command") - _, _, _, err = imapc.ReadContinuation() + _, err = imapc.ReadContinuation() tcheck(t, err, "read imap continuation") idle := make(chan idleResponse) diff --git a/testdata/imapclient/fuzzseed.txt b/testdata/imapclient/fuzzseed.txt new file mode 100644 index 0000000..3f16e36 --- /dev/null +++ b/testdata/imapclient/fuzzseed.txt @@ -0,0 +1,1056 @@ +001 OK done +001 OK LIST complete +002 OK done +003 OK done +003 OK [READ-WRITE] SELECT completed +004 OK done +005 OK done +008 OK done +009 NO Delete failed; mailbox is not empty. +010 OK done +011 OK done +012 OK done +013 OK done +01 OK ENABLE completed +022 OK done +023 OK done +032 NO UID command rejected because UIDVALIDITY changed! +03 OK [MESSAGELIMIT 1000 23221] FETCH completed with 1000 +04 OK [MESSAGELIMIT 1000 23221] SEARCH completed with 1000 +05 NO [MESSAGELIMIT 1000 20001] Too many messages to copy, +06 OK [MESSAGELIMIT 1000 20001] MOVE completed for the last +07 BAD [UIDREQUIRED] Message numbers are not allowed +07 OK [MESSAGELIMIT 1000 19578] STORE completed for the last +08 OK [MESSAGELIMIT 1000 11627] UID EXPUNGE completed for +09 OK EXPUNGE completed +101 OK FETCH completed +10 NO [MESSAGELIMIT 1000] FETCH exceeds the maximum 1000- +10 OK Completed +10 OK FETCH completed +10 OK [MESSAGELIMIT 1000 23007] FETCH exceeds the maximum +11 OK [APPENDUID 1521475658 2] Completed +11 OK Completed (0.001 secs 3 calls) +11 OK [EXPUNGEISSUED] Some messages were also expunged +11 OK FETCH completed +12 OK expunged +12 OK SEARCH completed +15 OK UID MOVE Completed +17 OK [APPENDUID 1521475658 3] Completed +1b OK done +1 OK [CAPABILITY IMAP4rev1 JMAPACCESS] done +1 OK completed +22 OK Completed (0.000 sec) +23 OK Completed +24 OK Completed (0.000 sec) +25 OK [READ-WRITE] Completed +26 OK Completed (0.000 sec) +27 OK Completed (1 msgs in 0.000 secs) +27 OK [READ-WRITE] Completed +28 OK Completed (2 msgs in 0.000 secs) +2b OK done +2 OK [CAPABILITY IMAP4REV1 IDLE NAMESPACE MAILBOX-REFERRALS SCAN +2 OK done +3 OK done +3 OK [MAILBOXID (F2212ea87-6097-4256-9d51-71338625)] Completed +4 NO done +4 OK [MAILBOXID (F6352ae03-b7f5-463c-896f-d8b48ee3)] Completed +5 NO Mailbox already exists +5 OK [APPENDUID 1521475658 1] Completed +6 OK Completed +7 OK Completed +8 OK Completed +9 OK Completed +A0001 OK CRAM authentication successful +a000 OK CAPABILITY completed +A001 NO COPY rejected, because some of the requested +A001 NO [REFERRAL IMAP://user;AUTH=*@SERVER1/FOO +A001 NO [REFERRAL IMAP://user;AUTH=*@SERVER2/FOO +A001 NO [REFERRAL IMAP://user;AUTH=*@SERVER2/REMOTE +A001 NO [REFERRAL IMAP://user;AUTH=*@SERVER2/SHARED/FOO] +A001 NO [REFERRAL IMAP://user;AUTH=*@SERVER2/SHARED/STUFF] +a001 OK "Begin TLS negotiation now" +a001 OK CAPABILITY completed +a001 OK CONVERT COMPLETED +A001 OK COPY completed +A001 OK [COPYUID 1022843275 414,567 5:6] Completed +A001 OK CREATE succeeded +A001 OK done +A001 OK Getacl complete +A001 OK GSSAPI authentication successful +A001 OK Kerberos V4 authentication successful +A001 OK LANGUAGE-Befehl ausgefuehrt +A001 OK LIST Completed +a001 OK Listrights completed +a001 OK LOGIN completed +A001 OK LOGIN completed +A001 OK LOGIN Completed +A001 OK NAMESPACE command completed +A001 OK [READ-WRITE] SELECT completed +A001 OK Search completed (23.387 + 0.004 + 0.017 secs). +A001 OK SELECT completed +A001 OK Setacl complete +A001 OK Setquota completed +a001 OK submitserver logged in +A001 OK URLFETCH completed +A001 OK Welcome sheridan@babylon5.example.org +A001 OK Will use i;basic for collation +a002 NO "Cannot assume requested authorization identity" +a002 NO "Not authorized to requested authorization identity" +A002 NO [REFERRAL IMAP://user;AUTH=*@SERVER2/SHARED/FOO] +A002 NO The requested item could not be found. +A002 NO Unsupported language MUL +a002 OK "Authenticated" +a002 OK Begin TLS negotiation now +A002 OK [COPYUID 1022843345 111 94] Copy completed +A002 OK CREATE command completed +A002 OK done +A002 OK FIND ALL.MAILBOXES completed +A002 OK FIND MAILBOXES completed +A002 OK Getacl complete +A002 OK IDLE terminated +A002 OK LIST command completed +A002 OK LIST completed +A002 OK LIST Completed +A002 OK LSUB completed +a002 OK NOOP completed +A002 OK [READ-WRITE] Selected completed +A002 OK Setacl complete +A002 OK SUBSCRIBE completed +A002 OK SUBSCRIBE MAILBOX to #news.comp.mail.mime +A002 OK UNSUBSCRIBE completed +A002 OK UNSUBSCRIBE MAILBOX from #news.comp.mail.mime +a002 OK URLFETCH completed +A002 OK user john authenticated +A002 OK user lennier authenticated +A003 NO [BADURL "/INBOX;UIDVALIDITY=785799047/;UID=113330; +A003 NO [OVERQUOTA] APPEND Failed +A003 NO Sorry, message is too big +A003 OK append Completed +A003 OK APPEND completed +A003 OK APPEND Completed +A003 OK [APPENDUID 1022843275 77712,77713] completed +A003 OK [APPENDUID 1022843275 77712] APPEND completed +A003 OK [APPENDUID 1022843275 77712] completed +A003 OK [APPENDUID 38505 3955] APPEND completed +A003 OK [APPENDUID 385759045 45] append Completed +a003 OK CAPABILITY completed +A003 OK catenate append completed +A003 OK Copy completed +A003 OK COPY completed +A003 OK [COPYUID 38505 304,319:320 3956:3958] COPY completed +A003 OK CREATE command completed +A003 OK CREATE completed +A003 OK FETCH completed +A003 OK Getacl complete +A003 OK Getquota completed +A003 OK LIST command completed. +A003 OK LIST Completed +a003 OK Logout successful +A003 OK LSUB completed +A003 OK Myrights complete +A003 OK Replace completed +A003 OK STORE completed +A003 OK SUBSCRIBE to MAILBOX completed +A003 OK Supported languages have been enumerated +A003 OK UID EXPUNGE completed +A003 OK UID FETCH completed +A003 OK UNSUBSCRIBE from MAILBOX completed +A003 OK Welcome, trace information has been logged. +A004 NO [TRYCREATE] No such mailbox as bogusname +A004 OK [COPYUID 38505 304,319:320 3956:3958] COPY +A004 OK [COPYUID 38505 304,319:320 3956:3958] Done +A004 OK CREATE command completed +A004 OK CREATE completed +A004 OK Fetch Completed +A004 OK IDLE terminated +a004 OK LOGIN completed +A004 OK Replace completed +A004 OK SEARCH completed +A004 OK STORE completed +a004 OK Success (tls protection) +A005 NO APPEND failed: Disk quota exceeded +A005 OK Copy completed +A005 OK CREATE command completed +A005 OK CREATE completed +A005 OK FETCH completed +A005 OK No matching messages, so nothing copied +A005 OK PARTIAL completed +A006 OK CREATE command completed +A006 OK Done +A007 OK Fetch Completed +A007 OK [READ-WRITE] SELECT completed +a007 OK SETMETADATA complete +A00 OK Completed. +A010 OK [APPENDUID 1 3002] APPEND complete +A011 OK REPLACE completed +A015 OK Completed +A016 OK Mailbox deletion completed successfully +A01 OK Completed. +A01 OK done +A01 OK List completed. +A01 OK NAMESPACE command completed +A01 OK Search completed. +A01 OK STARTTLS completed +A01 OK STATUS completed +A01 OK Success (tls protection) +a023 OK ID completed +A023 OK LOGOUT completed +A02 OK CAPABILITY completed +A02 OK Completed. +A02 OK done +A02 OK LIST command completed +A02 OK List completed. +A02 OK [READ-WRITE] Select completed. +A02 OK [READ-WRITE] Sorry, UIDVALIDITY mismatch +A035 BAD Uppercase rights are not allowed +A036 BAD The q right is not supported +A03 OK Completed. +A03 OK done +A03 OK Fetch completed. +A03 OK Myrights completed +A03 OK [READ-WRITE] mailbox selected +A03 OK Success (tls protection) +a042 OK ID command completed +A042 OK STATUS completed +A044 BAD No such command as "BLURDYBLOOP" +a047 OK NOOP completed +A04 NO [TRYCREATE] Mailbox does not exist: INBOX/Archive +A04 OK Completed. +A04 OK done +A04 OK List completed. +A04 OK [READ-WRITE] mailbox selected +A05 OK Create completed +A05 OK done +A06 OK [APPENDUID 1533375901 2312] Append completed. +A06 OK done +a100 OK [READ-WRITE] Completed +A101 OK Fetch completed. +A101 OK LIST Completed +A102 OK LIST Completed +a102 OK [MODIFIED 12] Conditional STORE failed +A102 OK Search completed. +a103 OK Conditional Store completed +A103 OK LIST Completed +A103 OK Search completed. +a104 OK Store (conditional) completed +a106 OK Conditional STORE completed +a106 OK [MODIFIED 101] Conditional STORE failed +A142 OK [READ-WRITE] SELECT completed +A142 OK [READ-WRITE] SELECT completed, CONDSTORE is now enabled +A143 OK completed +A143 OK FETCH completed +A143 OK [READ-WRITE] SELECT completed +A143 OK Search complete +A144 OK FETCH completed +A150 OK [MODIFIED 3] done +A151 OK [HIGHESTMODSEQ 99] Still delaying VANISHED +A153 OK [HIGHESTMODSEQ 104] done +A160 OK Store completed +A1 OK Capability command completed. +a1 OK Conditional Store enabled +a1 OK done +A1 OK FETCH complete. +A1 OK [MODIFIED 3] done +a1 OK Search complete +A1 OK Search completed. +a200 NO STORE failed : no space left to store $MDNSent keyword +a200 OK STORE completed +A202 OK done +A202 OK EXPUNGE completed +A202 OK [HIGHESTMODSEQ 20010715194045319] done +A202 OK [HIGHESTMODSEQ 20010715194045319] expunged +A202 OK LIST completed +A210 OK EXPUNGE completed +A222 NO COPY failed: disk is full +A222 OK COPY completed +A223 NO COPY failed: disk is full +A240 OK Store completed +A282 OK SEARCH completed +A282 OK SEARCH completed, result saved +A282 OK SORT completed +A283 OK completed +A283 OK SEARCH completed +A283 OK SORT completed +A283 OK THREAD completed +A284 OK SEARCH completed +A284 OK SORT completed +A284 OK THREAD completed +A285 OK SEARCH completed +A285 OK THREAD completed +A286 OK SEARCH completed +a2 OK done +A2 OK FETCH complete. +A2 OK [HIGHESTMODSEQ 99] Still delaying VANISHED +a2 OK Search complete +A2 OK Search completed. +a300 OK FETCH completed +A300 OK SEARCH completed +A301 OK completed +a31 OK All keys removed +a3.1 OK done +a32 OK [URLMECH INTERNAL] mechs +A330 OK Store completed +a33 OK [URLMECH INTERNAL XSAMPLE=P34OKhO7VEkCbsiYY8rGEg==] done +A341 OK CLOSE completed +A341 OK Unselect completed +A342 OK Unselect completed +a3 OK done +A3 OK [HIGHESTMODSEQ 104] done +a3 OK Search complete +a400 OK SEARCH completed +a441 OK CAPABILITY completed +A442 OK XPIG-LATIN ompleted-cay +A443 OK Expunge completed +a4 OK Search complete +A654 OK FETCH completed +A682 OK LIST completed +A683 OK DELETE completed +A683 OK RENAME completed +A684 NO Name "foo" has inferior hierarchical names +A684 OK RENAME Completed +A685 OK DELETE Completed +A685 OK LIST completed +A686 OK LIST completed +A687 OK DELETE Completed +a775 BAD missing access identifier in supplied URL +a776 BAD missing owner username in supplied URL +a777 OK GENURLAUTH completed +A777 OK LIST completed +A82 OK LIST completed +A83 OK DELETE completed +A84 OK DELETE Completed +A85 OK LIST completed +A86 OK LIST completed +A932 OK [READ-ONLY] EXAMINE completed +A999 OK UID FETCH completed +a BAD Unknown parameter in SELECT command +abcd OK CAPABILITY completed +* ACAP (SASL "CRAM-MD5" "PLAIN") +* ACAP (SASL "CRAM-MD5") (STARTTLS) +* ACAP (SASL "DIGEST-MD5") +* ACAP (SASL "DIGEST-MD5" "EXTERNAL") +* ACL INBOX/Drafts Fred rwipslxetad Chris lrswi +* ACL INBOX/Drafts Fred rwipslxetad Chris lrswicdakxet +* ACL INBOX Fred rwipsldexta +* ACL INBOX Fred rwipslxcetda Byron lrswikcdeta +* ACL INBOX Fred rwipslxcetda David lrswideta +* ACL INBOX Fred rwipslxetad -Fred wetd $team w +* ACL INBOX -Fred wetd $team w +a NO [METADATA TOOMANY] SETMETADATA failed +a NO [UNAVAILABLE] User's backend down for maintenance +a OK APPEND completed +a OK CONVERSIONS completed +a OK Done +a OK [DOWNGRADED 70,105,108,109] Done +a OK EXAMINE complete +a OK Extended SEARCH completed +a OK Extended SORT completed +a OK Fetch complete +a OK GETMETADATA complete +a OK [METADATA LONGENTRIES 2199] GETMETADATA complete +a OK NOOP complete +a OK Password matched +a OK [READ-WRITE] Completed +a OK Search complete +a OK SELECT complete +a OK SELECT completed +a OK SETMETADATA complete +a OK Sort complete +a OK Store complete +a OK STORE complete +a OK TLS active +b001 NO All conversions failed +B001 NO [MODIFIED 2] Some of the messages no longer exist. +B001 NO Server is unable to enumerate supported languages +b001 OK CONVERT COMPLETED +B001 OK Getacl complete +B001 OK user is authenticated +b002 NO All conversions failed +b002 NO [TEMPFAIL] All conversions failed +B002 OK Deleteacl complete +B002 OK DELETE completed +B002 OK NOOP Completed. +B002 OK [READ-WRITE] Selected completed +b002 OK Some conversions failed +b003 NO All conversions failed +b003 OK Conditional Store completed +B003 OK FETCH Completed +B003 OK Getacl complete +B004 OK LOGOUT Completed +b005 NO All conversions failed +B01 BAD Tag reuse +B01 OK Search completed, will notify. +B02 OK Search completed, will not notify. +B03 OK Nothing done. +B04 OK No further updates. +B04 OK [READ-WRITE] mailbox selected +b108 OK Conditional Store completed +B1 OK FETCH completed. +B1 OK Search completed. +B202 OK [HIGHESTMODSEQ 20010715194045319] expunged +B282 OK SEARCH completed +B283 NO [BADCHARSET UTF-8] KOI8-R is not supported +B2 OK Fetch completed. +* BAD Command line too long +* BAD Disk crash, attempting salvage to a new disk! +* BAD Empty command line +BBB OK done +b NO [AUTHENTICATIONFAILED] Authentication failed +b OK CONVERSIONS completed +b OK done +b OK FETCH completed +b OK Logged in as arnt +* BYE Autologout; idle for too long +* BYE IMAP4rev1 server logging out +* BYE IMAP4rev1 Server logging out +* BYE IMAP4rev2 Server logging out +* BYE IMAP4 Server logging out +* BYE See you later +c000 OK CONVERT COMPLETED +c001 OK CONVERT COMPLETED +C001 OK Sprachwechsel durch LANGUAGE-Befehl ausgefuehrt +C01 OK Completed +C01 OK done +C021 OK fetch completed +C02 OK done +C035 OK Completed +C037 OK completed +C04 OK done +c101 OK Store (conditional) completed +C180 OK Noop completed +c1 NO [AUTHORIZATIONFAILED] No such authorization-ID +C1 OK Capability command completed. +C1 OK Sort completed. +C270 OK Noop completed +C282 OK SEARCH completed +C283 OK SEARCH completed +C284 OK SEARCH completed +C285 OK SEARCH completed +C286 OK SEARCH completed +c2 NO [AUTHORIZATIONFAILED] Authenticator is not an admin +C2 OK SEARCH completed. +C2 OK Sort completed. +C360 OK Noop completed +C3 OK FETCH completed. +C3 OK Sort completed. +C4 OK FETCH completed. +CA3 OK done +* CAPABILITY IMAP4 +* CAPABILITY IMAP4 IMAP4rev1 AUTH=DIGEST-MD5 AUTH=ANONYMOUS +* CAPABILITY IMAP4rev1 AUTH=DIGEST-MD5 +* CAPABILITY IMAP4rev1 AUTH=GSSAPI AUTH=PLAIN +* CAPABILITY IMAP4rev1 AUTH=KERBEROS_V4 +* CAPABILITY IMAP4rev1 AUTH=KERBEROS_V4 XPIG-LATIN +* CAPABILITY IMAP4rev1 AUTH=PLAIN +* CAPABILITY IMAP4rev1 CONVERT BINARY [...] +* CAPABILITY IMAP4rev1 CREATE-SPECIAL-USE +* CAPABILITY IMAP4rev1 ID APPENDLIMIT +* CAPABILITY IMAP4rev1 ID APPENDLIMIT=257890 +* CAPABILITY IMAP4rev1 ID LITERAL+ ENABLE X-GOOD-IDEA +* CAPABILITY IMAP4rev1 IMAP4rev2 \ +* CAPABILITY IMAP4rev1 LITERAL+ AUTH=DIGEST-MD5 +* CAPABILITY IMAP4rev1 PREVIEW +* CAPABILITY IMAP4rev1 PREVIEW SEARCHRES +* CAPABILITY IMAP4rev1 SASL-IR AUTH=PLAIN +* CAPABILITY IMAP4rev1 SASL-IR AUTH=PLAIN AUTH=EXTERNAL +* CAPABILITY IMAP4rev1 SPECIAL-USE +* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI +* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI XPIG-LATIN +* CAPABILITY IMAP4REV1 STARTTLS LITERAL+ +* CAPABILITY IMAP4rev1 STARTTLS LOGINDISABLED +* CAPABILITY IMAP4rev1 XPIG-LATIN +* CAPABILITY IMAP4rev2 AUTH=GSSAPI AUTH=PLAIN +* CAPABILITY IMAP4rev2 AUTH=PLAIN +* CAPABILITY IMAP4rev2 AUTH=PLAIN AUTH=EXTERNAL +* CAPABILITY IMAP4rev2 ID LITERAL+ X-GOOD-IDEA +* CAPABILITY IMAP4rev2 STARTTLS AUTH=GSSAPI +* CAPABILITY IMAP4rev2 STARTTLS LOGINDISABLED +* CAPABILITY IMAP4rev2 XPIG-LATIN +* CAPABILITY IMAP4 XPIG-LATIN +* CAPABILITY [...] QUOTA QUOTA=RES-STORAGE [...] +* CAPABILITY [...] QUOTA QUOTA=RES-STORAGE QUOTA=RES- +* CAPABILITY [...] QUOTA QUOTA=RES-STORAGE QUOTA=RES-MESSAGE +* CAPABILITY [...] QUOTA QUOTASET QUOTA=RES-STORAGE QUOTA=RES- +* CAPABILITY STARTTLS AUTH=GSSAPI IMAP4rev2 LOGINDISABLED +c NO [COMPRESSIONACTIVE] DEFLATE active via TLS +c NO [NOT-UTF-8] Mailbox does not support UTF-8 access +c OK CONVERSIONS completed +c OK INBOX selected +* COMPARATOR i;basic +* CONVERSION "image/gif" "image/jpeg" ("pix-y" "pix-x" +* CONVERSION "image/gif" "image/png" ([...]) +* CONVERSION "image/png" "image/jpeg" (...) +D0004 OK [MESSAGELIMIT 1000 1179] SEARCH completed with 1000 +D001 NO Diese Sprache ist nicht unterstuetzt +D002 OK Sprachwechsel durch LANGUAGE-Befehl ausgefuehrt +D003 OK Sprachwechsel durch LANGUAGE-Befehl ausgefuehrt +D01 OK done +D02 OK done +D03 OK done +d105 OK [MODIFIED 7,9] Conditional STORE failed +D210 OK Noop completed +D300 OK Noop completed +D390 OK Noop completed +d NO [EXPIRED] That password isn't valid any more +d NO [PRIVACYREQUIRED] Connection offers no privacy +d NO [UTF-8-ONLY] Mailbox requires UTF-8 client +d OK DEFLATE active +E001 OK Now speaking English +e001 OK UID CONVERT COMPLETED +e002 OK CONVERT COMPLETED +E01 OK Sort completed +E282 OK SEARCH completed +E283 OK COPY completed, nothing copied +efgh OK STARTLS completed +efgh OK STARTTLS completed +* ENABLED CONDSTORE +* ENABLED CONDSTORE QRESYNC +* ENABLED UIDONLY +* ENABLED X-GOOD-IDEA +e NO [CONTACTADMIN] +e OK [CONTACTADMIN] +e OK done +* ESEARCH COUNT 18 ALL 1:17,21 +* ESEARCH COUNT 5 ALL 1:17,21 +* ESEARCH (TAG "a00") ADDTO (0 93) +* ESEARCH (TAG "a00") (COUNT 17) +* ESEARCH (TAG "A01") COUNT 23765 +* ESEARCH (TAG "A01") UID PARTIAL (-1:-100 ...) +* ESEARCH (TAG "A02") UID PARTIAL (23500:24000 ...) +* ESEARCH (TAG "A03") UID PARTIAL (1:500 ...) +* ESEARCH (TAG "A04") UID PARTIAL (24000:24500 NIL) +* ESEARCH (TAG "a1") MIN 2 MODSEQ 917162488 +* ESEARCH (TAG "A282") MIN 2 COUNT 3 +* ESEARCH (TAG "A283") ALL 2,10:11 +* ESEARCH (TAG "A284") +* ESEARCH (TAG "A284") MIN 4 +* ESEARCH (TAG "A285") ALL 43 +* ESEARCH (TAG "A285") UID MIN 7 MAX 3800 +* ESEARCH (TAG "A286") COUNT 15 +* ESEARCH (TAG "a2") MAX 23 MODSEQ 907162321 +* ESEARCH (TAG "A301") UID ALL 17,900,901 +* ESEARCH (TAG "a3") MIN 2 MAX 23 MODSEQ 917162488 +* ESEARCH (TAG "a4") MIN 2 COUNT 10 MODSEQ 917162500 +* ESEARCH (TAG "a567") UID COUNT 17 ALL 4:18,21,28 +* ESEARCH (TAG "a567") UID COUNT 5 ALL 4:19,21,28 +* ESEARCH (TAG "a") ALL 1:3,5 MODSEQ 1236 +* ESEARCH (TAG "a") ALL 5,3,2,1 MODSEQ 1236 +* ESEARCH (TAG "B01") COUNT 74 +* ESEARCH (TAG "B01") UID ADDTO (0 32768:32769) +* ESEARCH (TAG "B01") UID REMOVEFROM (0 32768) +* ESEARCH (TAG "B02") COUNT 74 +* ESEARCH (TAG "B1") ALL 1,5,10 RELEVANCY (4 99 42) +* ESEARCH (TAG "C01") UID ADDTO (1 2731:2733) +* ESEARCH (TAG "C01") UID ADDTO (1 2733 1 2732 1 2731) +* ESEARCH (TAG "C01") UID ADDTO (1 2733) ADDTO (1 2731:2732) +* ESEARCH (TAG "C283") ALL 2,10:15,21 +* ESEARCH (TAG "C284") MIN 2 +* ESEARCH (TAG "C285") MIN 2 MAX 21 +* ESEARCH (TAG "C286") MIN 2 ALL 2,10:15,21 +* ESEARCH (TAG "C286") MIN 2 MAX 21 COUNT 8 +* ESEARCH (TAG "C2") ALL 5,10,1 RELEVANCY (99 42 4) +* ESEARCH (TAG "C3") PARTIAL (1:10 42,9,34,13,15,4,2,7,23,82) +* ESEARCH (TAG "E01") UID ALL 23765,23764,23763,23761,[...] +* ESEARCH (TAG "G283") ALL 3:15,27,29:103 +* ESEARCH (TAG "h") ALL 1:3,5,8,13,21,42 +* ESEARCH (TAG "P283") ALL 882,1102,3003,3005:3006 +* ESEARCH (TAG "tag1" MAILBOX "folder1" UIDVALIDITY 1) UID ALL +* ESEARCH (TAG "tag1" MAILBOX "folder2/banana" UIDVALIDITY 503) +* ESEARCH (TAG "tag1" MAILBOX "folder2/peach" UIDVALIDITY 3) UID +* ESEARCH (TAG "tag2" MAILBOX "folder1" UIDVALIDITY 1) UID ALL +* ESEARCH (TAG "tag2" MAILBOX "folder2/salmon" UIDVALIDITY +* ESEARCH UID COUNT 17 ALL 4:18,21,28 +* ESEARCH UID COUNT 5 ALL 4:19,21,28 +* EXISTS 93 +f001 OK CONVERT COMPLETED +F282 OK SEARCH completed +F283 OK COPY completed +F284 OK STORE completed +* FLAGS ($MDNSent \Flagged \Deleted \Draft \Seen) +* FLAGS (\Answered \Flagged \Deleted \Draft \Seen) +* FLAGS (\Answered \Flagged \Deleted \Seen \Draft) +* FLAGS (Answered Flagged Deleted Seen Draft) +* FLAGS (\Answered \Flagged \Draft \Deleted \Seen) +* FLAGS (Deleted Seen) +* FLAGS (\Deleted \Seen \Flagged) +* FLAGS (\Flagged \Draft \Deleted \Seen) +* FLAGS (\Flagged \Draft \Deleted \Seen $MDNSent) +f NO [NOPERM] Access denied +FXXZ OK CHECK Completed +G0001 OK Getquota complete +G0002 OK Getquotaroot complete +g001 NO [MAXCONVERTMESSAGES 1] +g001 NO [MAXCONVERTPARTS 1] You can only request 1 body part at +G282 OK SEARCH completed +G283 OK SEARCH completed +* GENURLAUTH "imap://joe@example.com/INBOX/;uid=20/;section=1.2 +g NO [INUSE] Mailbox in use +H282 OK SEARCH completed +H283 OK SEARCH completed +h OK [EXPUNGEISSUED] Search completed +* ID ("name" "Cyrus" "version" "1.5" "os" "sunos" +* ID NIL +ijkl OK CAPABILITY completed +i NO [CORRUPTION] Cannot open mailbox +* JMAPACCESS "https://example.com/.well-known/jmap" +j NO [SERVERBUG] This should not happen +k1 OK [READ-ONLY] Done +k2 OK [CLIENTBUG] Done +* LANGUAGE (DE) +* LANGUAGE (DE-IT) +* LANGUAGE (EN) +* LANGUAGE (EN DE IT i-default) +* LIST () "\\" {12} +* LIST () "\\" {17} +* LIST () "\\" {27} +* LIST () "/" ABC/DEF +* LIST () "/" ABC//DEF <=== must not do this +* LIST () "/" ABC/DEF <=== should do this +* LIST (\Archive) "/" Archive/Default +* LIST () "/" Banana +* LIST () "/" Banana/Apple +* LIST () "/" "baz2" +* LIST () "/" "baz2/bar2" +* LIST () "/" "baz2/bar22" +* LIST () "/" "baz2/bar222" +* LIST () "/" "baz2" ("CHILDINFO" ("SUBSCRIBED")) +* LIST () "." blurdybloop +* LIST () "/" blurdybloop +* LIST () "/" "C" +* LIST () "/" /DEF +* LIST () "/" Drafts +* LIST (\Drafts) "/" INBOX/Drafts +* LIST (\Drafts) "/" MyDrafts +* LIST (\Drafts) "/" SavedDrafts +* LIST () "/" "eps2" +* LIST () "/" "eps2/mamba" +* LIST ...etc... +* LIST () "." "foo" +* LIST () "." foo +* LIST () "/" foo +* LIST () "/" "Foo" +* LIST () "/" "foo2" +* LIST () "/" "foo2/bar1" +* LIST () "/" "foo2/bar2" +* LIST () "/" "foo2" ("CHILDINFO" ("SUBSCRIBED")) +* LIST () "." foo.bar +* LIST () "/" foo/bar +* LIST () "/" "Foo/Bar" +* LIST () "/" "Foo/Baz" +* LIST () "." "foo" (CHILDINFO ("SUBSCRIBED")) +* LIST () "/" "Foo" ("CHILDINFO" ("SUBSCRIBED")) +* LIST () "." "frop" +* LIST () "/" "Fruit" +* LIST () "/" "Fruit/Apple" +* LIST () "/" "Fruit/Banana" +* LIST () "." funny +* LIST (\HasChildren) "/" "Foo" +* LIST (\HasChildren) "/" "Fruit" +* LIST (\HasChildren) "/" ITEM_1 +* LIST (\HasChildren) "/" ITEM_2 +* LIST (\HasChildren) "/" Projects +* LIST (\HasChildren \Remote) "/" "Meat" +* LIST (\HasChildren \Subscribed) "/" "Foo" +* LIST (\HasChildren) "/" "Vegetable" +* LIST (\HasNoChildren) "." bar +* LIST (\HasNoChildren) "/" foo ("CHILDINFO" ("SUBSCRIBED")) +* LIST (\HasNoChildren \Important) "/" "Important Messages" +* LIST (\HasNoChildren) "/" "Imported Wine" +* LIST (\HasNoChildren) "." INBOX +* LIST (\HasNoChildren) "/" INBOX +* LIST (\HasNoChildren) "/" ITEM_1/ITEM_1A +* LIST (\HasNoChildren) "/" "Moo" +* LIST (\HasNoChildren) "." renamed +* LIST (\HasNoChildren \Subscribed) "/" "Moo" +* LIST (\HasNoChildren) "/" ToDo +* LIST (\HasNoChildren) "/" "Tofu" +* LIST () "" INBOX +* LIST () "." "INBOX" +* LIST () "." "INBOX" +* LIST () "." INBOX +* LIST () "/" "INBOX" +* LIST () "/" INBOX +* LIST () "." INBOX.bar +* LIST () "/" ~/Mail/meetings +* LIST (\Marked \Drafts \HasNoChildren) "/" MyDrafts +* LIST (\Marked \Drafts) "/" MyDrafts +* LIST (\Marked \HasNoChildren) "/" Inbox +* LIST (\Marked) "/" Inbox +* LIST (\Marked \NoInferiors) "/" "inbox" +* LIST (\Marked \NoInferiors \Subscribed) "/" "inbox" +* LIST (\Marked) "/" "Sent/December2003" +* LIST (\Marked) ":" Tables (tablecloth (("edge" "lacy") +* LIST () "/" "~mark/foo" +* LIST () "/" "~mark/INBOX" +* LIST () "/" "Moo" +* LIST () "/" music/rock +* LIST () "/" MyDrafts +* LIST () "/" "NewMailbox" +* LIST () "/" "NewMailbox" ("OLDNAME" ("OldMailbox")) +* LIST (\NoInferiors) "/" "A/B" +* LIST (\NoInferiors) "/" "C/D" +* LIST (\NoInferiors) "/" "Drafts" +* LIST (\NonExistent \Archive) "/" Archives +* LIST (\NonExistent \Archive) "/" "#mh/Archives" +* LIST (\NonExistent) "." "bar" +* LIST (\NonExistent \Drafts) "/" Drafts +* LIST (\NonExistent \Drafts) "/" "#mh/Drafts" +* LIST (\NonExistent) "/" "Foo" ("CHILDINFO" +* LIST (\NonExistent) "/" "Foo" ("CHILDINFO" ("SUBSCRIBED")) +* LIST (\NonExistent \HasChildren) "/" also +* LIST (\NonExistent \HasChildren) "/" music +* LIST (\NonExistent) "." "INBOX.DeletedMailbox" +* LIST (\NonExistent \Junk) "/" Junk +* LIST (\NonExistent \Junk) "/" "#mh/Junk" +* LIST (\NonExistent) "/" "qux2" ("CHILDINFO" ("SUBSCRIBED")) +* LIST (\NonExistent \Sent) "/" "#mh/Sent Mail" +* LIST (\NonExistent \Sent) "/" "Sent Mail" +* LIST (\NonExistent \Trash) "/" "Deleted Items" +* LIST (\NonExistent \Trash) "/" "#mh/Deleted Items" +* LIST () "/" "Normalized" ("OLDNAME" ("NonNormalized")) +* LIST (\Noselect) "/" "" +* LIST (\Noselect) "/" / +* LIST (\NoSelect) "." "bar" +* LIST (\Noselect) "." foo +* LIST (\Noselect) "/" foo +* LIST (\Noselect) "/" ~/Mail/foo +* LIST (\Noselect) "." #news. +* LIST (\Noselect) "/" zowie +* LIST () "." old-mail +* LIST () "/" "Other Users/Karen" +* LIST () "/" "Other Users/Matthew" +* LIST () "/" "Other Users/Mike" +* LIST () "/" "Other Users/Tesa" +* LIST () "/" Projects +* LIST () "/" "qux2/bar2" +* LIST (\Remote) "/" also/jazz +* LIST (\Remote) "/" "Bread" +* LIST (\Remote \HasNoChildren) "/" "Bread" +* LIST (\Remote) "/" "Meat" +* LIST (\Remote \Subscribed) "/" "Bread" +* LISTRIGHTS archive.imap anyone "" +* LISTRIGHTS archive/imap anyone "" +* LISTRIGHTS ~/Mail/saved smith la r swicdkxte +* LIST () "/" sarasoop +* LIST () "/" SavedDrafts +* LIST () "/" "Sent/August2004" +* LIST (\Sent \HasNoChildren) "/" SentMail +* LIST (\Sent) "/" INBOX/Sent +* LIST () "/" SentMail +* LIST () "/" "Sent/March2004" +* LIST (\Sent) "/" SentMail +* LIST (\Subscribed) "/" "baz2/bar2" +* LIST (\Subscribed) "/" "baz2/bar22" +* LIST (\Subscribed) "/" "baz2/bar222" +* LIST (\Subscribed) "/" "eps2" +* LIST (\Subscribed) "/" "eps2" ("CHILDINFO" ("SUBSCRIBED")) +* LIST (\Subscribed) "/" "eps2/mamba" +* LIST (\Subscribed) "/" "foo2/bar1" +* LIST (\Subscribed) "/" "foo2/bar2" +* LIST (\Subscribed) "/" "Foo/Baz" +* LIST (\Subscribed) "/" "Foo" ("CHILDINFO" +* LIST (\Subscribed) "/" "Foo" ("CHILDINFO" ("SUBSCRIBED")) +* LIST (\Subscribed) "/" "Fruit/Banana" +* LIST (\Subscribed \HasChildren) "/" "Foo" +* LIST (\Subscribed \HasChildren) "/" "Foo" ("CHILDINFO" +* LIST (\Subscribed) "." "INBOX" +* LIST (\Subscribed) "." "INBOX" +* LIST (\Subscribed \NonExistent) "/" foo/bar +* LIST (\Subscribed \NonExistent) "/" "Fruit/Peach" +* LIST (\Subscribed) "/" "qux2/bar2" +* LIST (\Subscribed) "/" "SubscribedMailbox" +* LIST (\Subscribed) "/" "Vegetable" +* LIST (\Subscribed) "/" "Vegetable/Broccoli" +* LIST () ":" Tables:new (tablecloth ("edge" "lacy") +* LIST () "\\" TEST +* LIST () "/" ToDo +* LIST () "/" "Tofu" +* LIST (\Trash \HasNoChildren) "/" Trash +* LIST (\Trash) "/" INBOX/Trash +* LIST (\Trash) "/" Trash +* LIST () "/" "#Users/Mike/Foo" +* LIST () "/" "#Users/Mike/INBOX" +* LIST () "/" "Vegetable" +* LIST () "/" "Vegetable/Broccoli" +* LIST () "/" "Vegetable/Corn" +* LIST () "/" zowie/bar +l NO [CANNOT] Adjacent slashes are not supported +* LSUB () "." #news.comp.mail.mime +* LSUB () "." #news.comp.mail.misc +* LSUB (\NoSelect) "." #news.comp.mail +* MAILBOX blurdybloop +* MAILBOX INBOX +* METADATA "foo" ("/shared/vendor/cmu/cyrus-imapd/color" NIL) +* METADATA "INBOX" (/private/comment "My comment" +* METADATA "INBOX" (/private/comment "My comment") +* METADATA "INBOX" (/private/comment "My own comment") +* METADATA "INBOX" (/private/filters/values/small +* METADATA "INBOX" /shared/comment +* METADATA "INBOX" (/shared/comment "Its sunny outside!") +* METADATA "INBOX" /shared/comment /private/comment +* METADATA "INBOX" (/shared/comment "Shared comment" +* METADATA INBOX ("/shared/vendor/cmu/cyrus-imapd/color" "#b71c1c") +* METADATA "MyDrafts" (/private/specialuse "\\Drafts") +* METADATA "MyDrafts" (/private/specialuse NIL) +* METADATA "" /shared/comment +* METADATA "" (/shared/comment "My comment") +* METADATA "" (/shared/comment "Shared comment") +m NO [LIMIT] At most 32 flags in one mailbox supported +* MYRIGHTS Archive/Default lrwsip +* MYRIGHTS "foo" lrs +* MYRIGHTS "INBOX" lrswipkxtecda +* MYRIGHTS INBOX lrwis +* MYRIGHTS INBOX rwiptsldaex +* MYRIGHTS TargetMailbox rsti +* MYRIGHTS TargetMailbox rwis +n1 NO [OVERQUOTA] Sorry +n2 OK [OVERQUOTA] You are now over your soft quota +* NAMESPACE (("INBOX/" "/")("Archive/" "/")) NIL (("Public/" "/")) +* NAMESPACE (("INBOX." ".")) NIL NIL +* NAMESPACE (("" "/")("#mh/" "/" "X-PARAM" +* NAMESPACE (("" "/")("#mh/" "/" "X-PARAM" ("FLAG1" "FLAG2"))) +* NAMESPACE (("" "/")) (("~" "/")) NIL +* NAMESPACE (("" "/")) NIL NIL +* NAMESPACE NIL NIL (("" ".")) +* NAMESPACE (("" "/")) NIL (("Public Folders/" "/")) +* NAMESPACE (("" "/")) (("Other Users/" "/")) NIL +* NAMESPACE (("" "/")) (("Other Users/" "/" "TRANSLATION" +* NAMESPACE (("" "/"))(("Other Users/" "/" "TRANSLATION" +* NAMESPACE (("" "/")) (("~" "/")) (("#shared/" "/") +* NAMESPACE (("" "/")) (("#Users/" "/")) NIL +* NO Disk is 98% full, please delete unnecessary data +* NO Disk is 99% full, please delete unnecessary data +* NO [MESSAGELIMIT 1000 23221] FETCH completed with 1000 partial +* NO [NOUPDATE "B02"] Too many contexts +* NO [OVERQUOTA] Soft quota has been exceeded +* NO [UIDNOTSTICKY] Non-persistent UIDs +o356 NO [ALREADYEXISTS] Mailbox "that" already exists +o356 NO [HASCHILDREN] Mailbox "Notes" has children +* OK [ALERT] System shutdown in 10 minutes +* OK [ANNOTATIONS 20480 NOPRIVATE] +* OK [APPENDUID 1 2000] Replacement Message ready +* OK [APPENDUID 1 2001] Replacement Message ready +* OK [APPENDUID 1 3003] Replacement Message ready +* OK [CAPABILITY IMAP4rev1 AUTH=OAUTHBEARER SASL-IR] example1 +* OK [CAPABILITY IMAP4rev1 ENABLE CONDSTORE QRESYNC UIDONLY +* OK [CAPABILITY IMAP4rev1 IMAP4rev2] example3 +* OK [CAPABILITY IMAP4REV1 LOGIN-REFERRALS STARTTLS +* OK [CAPABILITY IMAP4REV1 NOTIFY] +* OK [CAPABILITY IMAP4rev1 STARTTLS AUTH=ANONYMOUS] Welcome +* OK [CAPABILITY IMAP4rev1 STARTTLS AUTH=CRAM-MD5] Welcome +* OK [CAPABILITY IMAP4REV1 STARTTLS COMPRESS=DEFLATE] +* OK [CAPABILITY IMAP4REV1 URLAUTH] example.com IMAP server +* OK [CAPABILITY IMAP4rev2 AUTH=GSS] example4 +* OK [CAPABILITY IMAP4rev2] example2 +* OK [CAPABILITY IMAP4rev2 JMAPACCESS] done +* OK [CAPABILITY IMAP4rev2 STARTTLS AUTH=GSSAPI +* OK [CAPABILITY IMAP4rev2 STARTTLS AUTH=GSSAPI] +* OK [CLOSED] +* OK [CLOSED] Previous mailbox is now closed +* OK [COPYUID 1397597919 20001:21000 22363:23362] Some +* OK [COPYUID 1521475659 2 1] Completed +* OK [COPYUID 1685977201 597 2] UID MOVE +* OK [COPYUID 432432 42:69 1202:1229] +* OK Greetings +* OK [HIGHESTMODSEQ 12111230047] +* OK [HIGHESTMODSEQ 20010715194045007] +* OK [HIGHESTMODSEQ 20010715194045007] Highest +. OK [HIGHESTMODSEQ 20010715194045319] Ok +* OK [HIGHESTMODSEQ 3914] Highest +* OK [HIGHESTMODSEQ 715194045007] +* OK [HIGHESTMODSEQ 715194045007] Ok +* OK [HIGHESTMODSEQ 90060115205545359] +* OK [HIGHESTMODSEQ 90060115205545359] Highest +* OK [HIGHESTMODSEQ 90060115205545359] Highest mailbox +* OK [HIGHESTMODSEQ 90060115205545359] Highest mailbox mod- +* OK [HIGHESTMODSEQ 90060128194045007] +* OK [HIGHESTMODSEQ 90060128194045007] Highest mailbox +* OK [HIGHESTMODSEQ 99] VANISHED reply with +* OK [HIGHESTMODSEQ 99] VANISHED reply with MODSEQ 100 is delayed +* OK Hi there +* OK IMAP4rev1 server ready +* OK IMAP4rev2 server ready +* OK IMAP4 Server +* OK IMAP4 server ready +* OK [INPROGRESS ("A001" 454 1000)] Processed 45% of the items +* OK [INPROGRESS ("A001" 999 1000)] Processed 99% of the items +* OK [INPROGRESS ("A003" 1388 2001)] Still working on this... +* OK [INPROGRESS ("A003" 175 2001)] Still working on this... +* OK [INPROGRESS ("A003" 1876 2001)] Still working on this... +* OK [INPROGRESS ("A003" 440 2001)] Still working on this... +* OK [INPROGRESS ("A003" 987 2001)] Still working on this... +* OK KerberosV4 IMAP4rev1 Server +* OK KerberosV4 IMAP4 Server +* OK [MAILBOXID (F2212ea87-6097-4256-9d51-71338625)] Ok +* OK [NOMODSEQ] Sorry, this mailbox format doesn't support +* OK [NOTIFICATIONOVERFLOW] ...A comment can go here... +* OK [PERMANENTFLAGS ($MDNSent \Flagged \Deleted \Draft \Seen \*)] +* OK [PERMANENTFLAGS (Answered Deleted Seen ] +* OK [PERMANENTFLAGS (\Answered \Deleted \Seen \*)] Limited +* OK [PERMANENTFLAGS (\Answered \Flagged \Draft +* OK [PERMANENTFLAGS (\Deleted \Seen \Answered +* OK [PERMANENTFLAGS (\Deleted \Seen \Flagged)] Limited +* OK [PERMANENTFLAGS (\Deleted \Seen \*)] Limited +* OK [PERMANENTFLAGS (\Deleted \Seen)] Limited +* OK [PERMANENTFLAGS (\Flagged \Draft \Deleted \Seen \*)] +* OK [PERMANENTFLAGS (\Flagged \Draft \Deleted \Seen $MDNSent)] +* OK [PERMANENTFLAGS ()] No permanent flags permitted +* OK [PERMANENTFLAGS (\Seen \Answered \Flagged \*)] L +* OK rename foo renamed +* OK Salvage successful, no data lost +* OK [UIDNEXT 1] +* OK [UIDNEXT 102] Predicted next UID +* OK [UIDNEXT 201] Predicted next UID +* OK [UIDNEXT 242344] Predicted next UID +* OK [UIDNEXT 2] Predicted next UID +* OK [UIDNEXT 30013] Predicted next UID +* OK [UIDNEXT 34643] +* OK [UIDNEXT 4392] Predicted next UID +* OK [UIDNEXT 550] Predicted next UID +* OK [UIDNEXT 567] Predicted next UID +* OK [UIDNEXT 600] Predicted next UID +* OK [UIDVALIDITY 1] +* OK [UIDVALIDITY 123456789] +* OK [UIDVALIDITY 12345] New UIDVALIDITY value! +* OK [UIDVALIDITY 1364851417] UIDs valid +* OK [UIDVALIDITY 3857529045] UIDs valid +* OK [UIDVALIDITY 3857529045] UIDVALIDITY +* OK [UIDVALIDITY 3857529045] Validity session-only +* OK [UIDVALIDITY 67890007] UIDVALIDITY +* OK [UIDVALIDITY 824708485] UID validity status +* OK [UIDVALIDITY 890061587] +* OK [UIDVALIDITY 894294713] +* OK [UIDVALIDITY 9877410381] UIDs valid +* OK [UNSEEN 10268] +* OK [UNSEEN 10] Message 10 is first unseen +* OK [UNSEEN 12] Message 12 is first unseen +* OK [UNSEEN 1] Message 1 is first unseen +* OK [UNSEEN 7] First unseen. +* OK [UNSEEN 7] There are some unseen +* OK [UNSEEN 7] There are some unseen messages in the mailbox +* OK [UNSEEN 8] Message 8 is first unseen +* OK [UNSEEN 9921] First unseen message +* OK Welcome +o NO [ALREADYEXISTS] Mailbox "that" already exists +P282 OK SEARCH completed +P283 OK completed +p NO [NONEXISTENT] No such mailbox +* PREAUTH IMAP4rev1 server logged in as Smith +* PREAUTH IMAP4rev2 server logged in as Smith +* PREAUTH IMAP4 server ready and logged in as Smith +* QUOTA "!partition/sda4" (STORAGE 104 10923847) +* QUOTAROOT comp.mail.mime +* QUOTAROOT INBOX "" +* QUOTAROOT INBOX "#user/alice" "!partition/sda4" +* QUOTA "" (STORAGE 10 512) +* QUOTA "#user/alice" (MESSAGE 42 1000) +* QUOTA "#user/alice" (STORAGE 54 111 MESSAGE 42 1000) +* QUOTA "#user/alice" (STORAGE 58 512) +S0000 OK Getquota completed +S0001 OK Rounded quota +S0002 NO Cannot change system limit +S0003 OK Status complete. +s100 OK FETCH completed +s101 OK search completed +* SEARCH +* SEARCH 1 +* SEARCH 1 2 +* SEARCH 1 2 3 5 8 13 21 34 +* SEARCH 1 2 3 5 8 13 21 42 +* SEARCH 1 4 +* SEARCH 1 5 10 +* SEARCH 17 900 901 +* SEARCH 2 3 5 7 11 13 17 19 23 +* SEARCH 2 3 6 +* SEARCH 2 4 5 +* SEARCH 25000 24998 (... UIDs ...) 23221 +* SEARCH 2 5 6 7 11 12 18 19 20 23 (MODSEQ 917162500) +* SEARCH 27800 27798 (... 250 UIDs ...) 25001 +* SEARCH 2 84 882 +* SEARCH 34 +* SEARCH 404 406 407 408 410 412 +* SEARCH 43 +* SEARCH 447 735 +* SEARCH 65 34 27 7 +* SEARCH 6 9 ... +* SEARCH 6 9 11 12 18 19 20 23 (MODSEQ 20010917162500) +* SEARCH 7 10 12 +* SEARCH 8 10 13 14 15 16 +* SEARCH 882 1102 3003 3005 3006 +* SEARCH 993 994 +* SEARCH 993 994 995 996 997 998 999 1000 1001 +* SORT +* SORT 2 3 4 5 1 11 10 6 7 9 8 +* SORT 2 3 6 +* SORT 2 84 882 +* SORT 5 10 1 +* SORT 5 3 4 1 2 +* STATUS bar (MAILBOXID (F6352ae03-b7f5-463c-896f-d8b48ee3)) +* STATUS blurdybloop (MESSAGES 231 UIDNEXT 44292 +* STATUS blurdybloop (MESSAGES 231 UIDNEXT 44292) +* STATUS foo (MAILBOXID (F2212ea87-6097-4256-9d51-71338625)) +* STATUS "foo" (MESSAGES 30 UNSEEN 29) +* STATUS "frop" (MESSAGES 8 SIZE 44421) +* STATUS frop (MESSAGES 8 SIZE 44421 UIDNEXT 242344) +* STATUS "INBOX" (APPENDLIMIT 257890) +* STATUS INBOX (APPENDLIMIT 257890) +* STATUS INBOX (MAILBOXID (Ff8e3ead4-9389-4aff-adb1-d8d89efd8cbf)) +* STATUS INBOX (MESSAGES 12 DELETED 4 DELETED-STORAGE 8) +* STATUS "INBOX" (MESSAGES 17) +* STATUS "INBOX" (MESSAGES 17 SIZE 16234) +* STATUS "INBOX" (MESSAGES 17 UNSEEN 16) +* STATUS Lists/Im2000 (UIDVALIDITY 901 UIDNEXT 1 MESSAGES 0) +* STATUS Lists/Lemonade (HIGHESTMODSEQ 65666665 UIDVALIDITY +* STATUS Lists/Lemonade (UIDNEXT 10002 MESSAGES 503) +* STATUS Lists/Lemonade (UIDVALIDITY 4 UIDNEXT 10000 +* STATUS Lists/Lemonade (UIDVALIDITY 4 UIDNEXT 9999 MESSAGES +* STATUS misc (UIDNEXT 999 MESSAGES 554) +* STATUS misc (UIDVALIDITY 1 UIDNEXT 999) +* STATUS renamed (MAILBOXID (F2212ea87-6097-4256-9d51-71338625)) +t1 NO [USEATTR] An \Important mailbox already exists +t1 NO [USEATTR] Mailbox not created; unsupported use \Important +t1 OK done +t1 OK foo +t1 OK List completed. +t1 OK Mailbox created +t1 OK STATUS completed +t1 OK Success +t2 OK done +t2 OK foo +t2 OK MySpecial created +t3 NO [USEATTR] \All not supported +t3 OK done +t3 OK foo again +t3 OK SETMETADATA complete +t4 OK SETMETADATA complete +t5 OK done +tag1 OK done +tag2 OK done +* THREAD +* THREAD (166)(167)(168)(169)(172)(170)(171) +* THREAD (166)(167)(168)(169)(172)((170)(179)) +* THREAD (2)(3 6 (4 23)(44 7 96)) +* THREAD ((3)(5)) +t OK Search complete, nothing found +* URLFETCH "imap://joe@example.com/INBOX/;uid=20/; +* URLFETCH "imap://joe@example.com/INBOX/;uid=20/;section=1.2 +* VANISHED 1 +* VANISHED 3 +* VANISHED 405,407,410,425 +* VANISHED 5 +* VANISHED 504,508 +* VANISHED 505,507,510,625 +* VANISHED 5444 +* VANISHED 597 +* VANISHED 8 +* VANISHED (EARLIER) 1:2,4:5,7:8,10:11,13:14 [...] +* VANISHED (EARLIER) 1:2,4:5,7:8,10:11,13:14,[...], +* VANISHED (EARLIER) 1:5,7:8,10:15 +* VANISHED (EARLIER) 29998:29999,30001:30002,30004:30005,30007: +* VANISHED (EARLIER) 300:310,405,411 +* VANISHED (EARLIER) 41,43:116,118,120:211,214:540 +* XPIG-LATIN ow-nay eaking-spay ig-pay atin-lay +Z432 OK LIST completed +Z433 OK RENAME completed +Z434 OK LIST completed +Z4S9 OK RENAME completed