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