imapclient: first step towards making package usable as imap client with other imap servers, and minor imapserver bug fix

The imapclient needs more changes, like more strict parsing, before it can be a
generally usable IMAP client, these are a few steps towards that.

- Fix a bug in the imapserver METADATA responses for TOOMANY and MAXSIZE.
- Split low-level IMAP protocol handling (new Proto type) from the higher-level
  client command handling (existing Conn type). The idea is that some simple
  uses of IMAP can get by with just using these commands, while more intricate
  uses of IMAP (like a synchronizing client that needs to talk to all kinds of
  servers with different behaviours and implemented extensions) can write custom
  commands and read untagged responses or command completion results
  explicitly. The lower-level method names have clearer names now, like
  ReadResponse instead of Response.
- Merge the untagged responses and (command completion) "Result" into a new
  type Response. Makes function signatures simpler. And make Response implement
  the error interface, and change command methods to return the Response as error
  if the result is NO or BAD. Simplifies error handling, and still provides the
  option to continue after a NO or BAD.
- Add UIDSearch/MSNSearch commands, with a custom "search program", so mostly
  to indicate these commands exist.
- More complete coverage of types for response codes, for easier handling.
- Automatically handle any ENABLED or CAPABILITY untagged response or response
  code for IMAP command methods on type Conn.
- Make difference between MSN vs UID versions of
  FETCH/STORE/SEARCH/COPY/MOVE/REPLACE commands more clear. The original MSN
  commands now have MSN prefixed to their name, so they are grouped together in
  the documentation.
- Document which capabilities are needed for a command.
This commit is contained in:
Mechiel Lukkien 2025-04-14 21:53:18 +02:00
parent 2c1283f032
commit e7b562e3f2
No known key found for this signature in database
31 changed files with 3198 additions and 1525 deletions

View File

@ -79,6 +79,7 @@ fuzz:
go test -fullpath -fuzz . -fuzztime 5m ./dmarc go test -fullpath -fuzz . -fuzztime 5m ./dmarc
go test -fullpath -fuzz . -fuzztime 5m ./dmarcrpt go test -fullpath -fuzz . -fuzztime 5m ./dmarcrpt
go test -fullpath -fuzz . -parallel 1 -fuzztime 5m ./imapserver 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 . -parallel 1 -fuzztime 5m ./junk
go test -fullpath -fuzz FuzzParseRecord -fuzztime 5m ./mtasts go test -fullpath -fuzz FuzzParseRecord -fuzztime 5m ./mtasts
go test -fullpath -fuzz FuzzParsePolicy -fuzztime 5m ./mtasts go test -fullpath -fuzz FuzzParsePolicy -fuzztime 5m ./mtasts

View File

@ -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. Warning: Currently primarily for testing the mox IMAP4 server. Behaviour that
Behaviour that may not be required by the IMAP4 specification may be expected by may not be required by the IMAP4 specification may be expected by this client.
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 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 ( import (
"bufio" "bufio"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"io"
"log/slog" "log/slog"
"net" "net"
"reflect"
"strings" "strings"
"github.com/mjl-/mox/mlog" "github.com/mjl-/mox/mlog"
"github.com/mjl-/mox/moxio" "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 { 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 // 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 // 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 // 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. record bool // If true, bytes read are added to recordBuf. recorded() resets.
recordBuf []byte recordBuf []byte
Preauth bool lastTag string
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.
} }
// Error is a parse or other protocol error. // Error is a parse or other protocol error.
@ -71,26 +114,29 @@ func (e Error) Unwrap() error {
type Opts struct { type Opts struct {
Logger *slog.Logger 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. // call panic.
Error func(err error) 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 // The initial untagged greeting response is read and must be "OK" or
// "PREAUTH". If preauth, the connection is already in authenticated state, // "PREAUTH". If preauth, the connection is already in authenticated state,
// typically through TLS client certificate. This is indicated in Conn.Preauth. // typically through TLS client certificate. This is indicated in Conn.Preauth.
// //
// Logging is written to log, in particular IMAP protocol traces are written with // Logging is written to opts.Logger. In particular, IMAP protocol traces are
// prefixes "CR: " and "CW: " (client read/write) as quoted strings at levels // written with prefixes "CR: " and "CW: " (client read/write) as quoted strings at
// Debug-4, with authentication messages at Debug-6 and (user) data at level // levels Debug-4, with authentication messages at Debug-6 and (user) data at level
// Debug-8. // Debug-8.
func New(conn net.Conn, opts *Opts) (client *Conn, rerr error) { func New(conn net.Conn, opts *Opts) (client *Conn, rerr error) {
c := Conn{ c := Conn{
conn: conn, Proto: Proto{conn: conn},
CapAvailable: map[Capability]struct{}{},
CapEnabled: map[Capability]struct{}{},
} }
var clog *slog.Logger 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.xtw = moxio.NewTraceWriter(c.log, "CW: ", &c)
c.xbw = bufio.NewWriter(c.xtw) c.xbw = bufio.NewWriter(c.xtw)
defer c.recover(&rerr) defer c.recoverErr(&rerr)
tag := c.xnonspace() tag := c.xnonspace()
if tag != "*" { if tag != "*" {
c.xerrorf("expected untagged *, got %q", 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 { if x.Status != OK {
c.xerrorf("greeting, got status %q, expected OK", x.Status) 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 return &c, nil
case UntaggedPreauth: case UntaggedPreauth:
c.Preauth = true c.Preauth = true
@ -133,13 +184,33 @@ func New(conn net.Conn, opts *Opts) (client *Conn, rerr error) {
panic("not reached") 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() x := recover()
if x == nil { if x == nil {
return return
} }
err, ok := x.(Error) var err error
if !ok { switch e := x.(type) {
case Error:
err = e
case Response:
err = e
if resp != nil {
*resp = e
}
default:
panic(x) panic(x)
} }
if c.errHandle != nil { if c.errHandle != nil {
@ -148,73 +219,110 @@ func (c *Conn) recover(rerr *error) {
*rerr = err *rerr = err
} }
func (c *Conn) xerrorf(format string, args ...any) { func (p *Proto) recover(rerr *error) {
panic(Error{fmt.Errorf(format, args...)}) if *rerr != nil {
} return
}
func (c *Conn) xcheckf(err error, format string, args ...any) { x := recover()
if err != nil { if x == nil {
c.xerrorf("%s: %w", fmt.Sprintf(format, args...), err) 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 { if err != nil {
panic(err) 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 // 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 // only, to implement io.Writer. Write errors do take the connection's panic mode
// into account, i.e. Write can panic. // into account, i.e. Write can panic.
func (c *Conn) Write(buf []byte) (n int, rerr error) { func (p *Proto) Write(buf []byte) (n int, rerr error) {
defer c.recover(&rerr) defer p.recover(&rerr)
n, rerr = c.conn.Write(buf) n, rerr = p.conn.Write(buf)
if rerr != nil { if rerr != nil {
c.connBroken = true p.connBroken = true
} }
c.xcheckf(rerr, "write") p.xcheckf(rerr, "write")
return n, nil return n, nil
} }
// Read reads directly from the underlying connection (TCP, TLS). For internal use // Read reads directly from the underlying connection (TCP, TLS). For internal use
// only, to implement io.Reader. // only, to implement io.Reader.
func (c *Conn) Read(buf []byte) (n int, err error) { func (p *Proto) Read(buf []byte) (n int, err error) {
return c.conn.Read(buf) return p.conn.Read(buf)
} }
func (c *Conn) xflush() { func (p *Proto) xflush() {
// Not writing any more when connection is broken. // Not writing any more when connection is broken.
if c.connBroken { if p.connBroken {
return return
} }
err := c.xbw.Flush() err := p.xbw.Flush()
c.xcheckf(err, "flush") p.xcheckf(err, "flush")
// If compression is active, we need to flush the deflate stream. // If compression is active, we need to flush the deflate stream.
if c.compress { if p.compress {
err := c.xflateWriter.Flush() err := p.xflateWriter.Flush()
c.xcheckf(err, "flush deflate") p.xcheckf(err, "flush deflate")
err = c.xflateBW.Flush() err = p.xflateBW.Flush()
c.xcheckf(err, "flush deflate buffer") p.xcheckf(err, "flush deflate buffer")
} }
} }
func (c *Conn) xtraceread(level slog.Level) func() { func (p *Proto) xtraceread(level slog.Level) func() {
c.tr.SetTrace(level) if p.tr == nil {
// For ParseUntagged and other parse functions.
return func() {}
}
p.tr.SetTrace(level)
return func() { return func() {
c.tr.SetTrace(mlog.LevelTrace) p.tr.SetTrace(mlog.LevelTrace)
} }
} }
func (c *Conn) xtracewrite(level slog.Level) func() { func (p *Proto) xtracewrite(level slog.Level) func() {
c.xflush() if p.xtw == nil {
c.xtw.SetTrace(level) // For ParseUntagged and other parse functions.
return func() {}
}
p.xflush()
p.xtw.SetTrace(level)
return func() { return func() {
c.xflush() p.xflush()
c.xtw.SetTrace(mlog.LevelTrace) 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 // because the server may immediate close the underlying connection when it sees
// the connection is being closed. // the connection is being closed.
func (c *Conn) Close() (rerr error) { func (c *Conn) Close() (rerr error) {
defer c.recover(&rerr) defer c.recoverErr(&rerr)
if c.conn == nil { if c.conn == nil {
return nil return nil
@ -247,7 +355,9 @@ func (c *Conn) Close() (rerr error) {
return 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 { func (c *Conn) TLSConnectionState() *tls.ConnectionState {
if conn, ok := c.conn.(*tls.Conn); ok { if conn, ok := c.conn.(*tls.Conn); ok {
cs := conn.ConnectionState() cs := conn.ConnectionState()
@ -256,170 +366,266 @@ func (c *Conn) TLSConnectionState() *tls.ConnectionState {
return nil 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. // written too.
//
// If tag is empty, a next unique tag is assigned. // If tag is empty, a next unique tag is assigned.
func (c *Conn) Commandf(tag string, format string, args ...any) (rerr error) { func (p *Proto) WriteCommandf(tag string, format string, args ...any) (rerr error) {
defer c.recover(&rerr) defer p.recover(&rerr)
if tag == "" { 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...)) fmt.Fprintf(p.xbw, "%s %s\r\n", p.lastTag, fmt.Sprintf(format, args...))
c.xflush() p.xflush()
return return
} }
func (c *Conn) nextTag() string { func (p *Proto) nextTag() string {
c.tagGen++ p.tagGen++
return fmt.Sprintf("x%03d", c.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. // 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) { // If an error is returned, resp can still be non-empty, and a caller may wish to
defer c.recover(&rerr) // 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 { for {
tag := c.xnonspace() tag := p.xnonspace()
c.xspace() p.xspace()
if tag == "*" { if tag == "*" {
untagged = append(untagged, c.xuntagged()) resp.Untagged = append(resp.Untagged, p.xuntagged())
continue continue
} }
if tag != c.LastTag { if tag != p.lastTag {
c.xerrorf("got tag %q, expected %q", tag, c.LastTag) p.xerrorf("got tag %q, expected %q", tag, p.lastTag)
} }
status := c.xstatus() status := p.xstatus()
c.xspace() p.xspace()
result = c.xresult(status) resp.Result = p.xresult(status)
c.xcrlf() p.xcrlf()
return return
} }
} }
// ReadUntagged reads a single untagged response line. // ParseCode parses a response code. The string must not have enclosing brackets.
// Useful for reading lines from IDLE. //
func (c *Conn) ReadUntagged() (untagged Untagged, rerr error) { // Example:
defer c.recover(&rerr) //
// "APPENDUID 123 10"
tag := c.xnonspace() func ParseCode(s string) (code Code, rerr error) {
if tag != "*" { p := Proto{br: bufio.NewReader(strings.NewReader(s + "]"))}
c.xerrorf("got tag %q, expected untagged", tag) 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() return code, nil
ut := c.xuntagged() }
// 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 return ut, nil
} }
// Readline reads a line, including CRLF. // Readline reads a line, including CRLF.
// Used with IDLE and synchronous literals. // Used with IDLE and synchronous literals.
func (c *Conn) Readline() (line string, rerr error) { func (p *Proto) Readline() (line string, rerr error) {
defer c.recover(&rerr) defer p.recover(&rerr)
line, err := c.br.ReadString('\n') line, err := p.br.ReadString('\n')
c.xcheckf(err, "read line") p.xcheckf(err, "read line")
return line, nil return line, nil
} }
// ReadContinuation reads a line. If it is a continuation, i.e. starts with a +, it func (c *Conn) readContinuation() (line string, rerr error) {
// is returned without leading "+ " and without trailing crlf. Otherwise, a command defer c.recover(&rerr, nil)
// response is returned. A successfully read continuation can return an empty line. line, rerr = c.ReadContinuation()
// Callers should check rerr and result.Status being empty to check if a if rerr != nil {
// continuation was read. if resp, ok := rerr.(Response); ok {
func (c *Conn) ReadContinuation() (line string, untagged []Untagged, result Result, rerr error) { c.processUntagged(resp.Untagged)
defer c.recover(&rerr) c.processResult(resp.Result)
}
if !c.peek('+') {
untagged, result, rerr = c.Response()
if result.Status == OK {
c.xerrorf("unexpected OK instead of continuation")
} }
return 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
} }
c.xtake("+ ") return "", rerr
line, err := c.Readline() }
c.xcheckf(err, "read line") p.xtake("+ ")
line, err := p.Readline()
p.xcheckf(err, "read line")
line = strings.TrimSuffix(line, "\r\n") line = strings.TrimSuffix(line, "\r\n")
return return
} }
// Writelinef writes the formatted format and args as a single line, adding CRLF. // Writelinef writes the formatted format and args as a single line, adding CRLF.
// Used with IDLE and synchronous literals. // Used with IDLE and synchronous literals.
func (c *Conn) Writelinef(format string, args ...any) (rerr error) { func (p *Proto) Writelinef(format string, args ...any) (rerr error) {
defer c.recover(&rerr) defer p.recover(&rerr)
s := fmt.Sprintf(format, args...) s := fmt.Sprintf(format, args...)
fmt.Fprintf(c.xbw, "%s\r\n", s) fmt.Fprintf(p.xbw, "%s\r\n", s)
c.xflush() p.xflush()
return nil return nil
} }
// WriteSyncLiteral first writes the synchronous literal size, then reads the // WriteSyncLiteral first writes the synchronous literal size, then reads the
// continuation "+" and finally writes the data. // continuation "+" and finally writes the data. If the literal is not accepted, an
func (c *Conn) WriteSyncLiteral(s string) (untagged []Untagged, rerr error) { // error is returned, which may be a Response.
defer c.recover(&rerr) func (p *Proto) WriteSyncLiteral(s string) (rerr error) {
defer p.recover(&rerr)
fmt.Fprintf(c.xbw, "{%d}\r\n", len(s)) fmt.Fprintf(p.xbw, "{%d}\r\n", len(s))
c.xflush() p.xflush()
plus, err := c.br.Peek(1) plus, err := p.br.Peek(1)
c.xcheckf(err, "read continuation") p.xcheckf(err, "read continuation")
if plus[0] == '+' { if plus[0] == '+' {
_, err = c.Readline() _, err = p.Readline()
c.xcheckf(err, "read continuation line") p.xcheckf(err, "read continuation line")
defer c.xtracewrite(mlog.LevelTracedata)() defer p.xtracewrite(mlog.LevelTracedata)()
_, err = c.xbw.Write([]byte(s)) _, err = p.xbw.Write([]byte(s))
c.xcheckf(err, "write literal data") p.xcheckf(err, "write literal data")
c.xtracewrite(mlog.LevelTrace) p.xtracewrite(mlog.LevelTrace)
return nil, nil return nil
} }
untagged, result, err := c.Response() var resp Response
if err == nil && result.Status == OK { resp, rerr = p.ReadResponse()
c.xerrorf("no continuation, but invalid ok response (%q)", result.More) 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 // empty tag. I.e. format must not contain a tag. Transactf then reads a response
// using ReadResponse and checks the result status is OK. // using ReadResponse and checks the result status is OK.
func (c *Conn) Transactf(format string, args ...any) (untagged []Untagged, result Result, rerr error) { func (c *Conn) transactf(format string, args ...any) (resp Response, rerr error) {
defer c.recover(&rerr) defer c.recover(&rerr, &resp)
err := c.Commandf("", format, args...) err := c.WriteCommandf("", format, args...)
if err != nil { 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) { func (c *Conn) responseOK() (resp Response, rerr error) {
untagged, result, rerr = c.Response() defer c.recover(&rerr, &resp)
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) xgetUntagged(l []Untagged, dst any) { resp, rerr = c.ReadResponse()
if len(l) != 1 { c.processUntagged(resp.Untagged)
c.xerrorf("got %d untagged, expected 1: %v", len(l), l) c.processResult(resp.Result)
if rerr == nil && resp.Status != OK {
rerr = resp
} }
got := l[0] return
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)
} }

View File

@ -17,32 +17,34 @@ import (
"github.com/mjl-/mox/scram" "github.com/mjl-/mox/scram"
) )
// Capability requests a list of capabilities from the server. They are returned in // Capability writes the IMAP4 "CAPABILITY" command, requesting a list of
// an UntaggedCapability response. The server also sends capabilities in initial // capabilities from the server. They are returned in an UntaggedCapability
// server greeting, in the response code. // response. The server also sends capabilities in initial server greeting, in the
func (c *Conn) Capability() (untagged []Untagged, result Result, rerr error) { // response code.
defer c.recover(&rerr) func (c *Conn) Capability() (resp Response, rerr error) {
return c.Transactf("capability") defer c.recover(&rerr, &resp)
return c.transactf("capability")
} }
// Noop does nothing on its own, but a server will return any pending untagged // Noop writes the IMAP4 "NOOP" command, which does nothing on its own, but a
// responses for new message delivery and changes to mailboxes. // server will return any pending untagged responses for new message delivery and
func (c *Conn) Noop() (untagged []Untagged, result Result, rerr error) { // changes to mailboxes.
defer c.recover(&rerr) func (c *Conn) Noop() (resp Response, rerr error) {
return c.Transactf("noop") defer c.recover(&rerr, &resp)
return c.transactf("noop")
} }
// Logout ends the IMAP session by writing a LOGOUT command. Close must still be // Logout ends the IMAP4 session by writing an IMAP "LOGOUT" command. [Conn.Close]
// called on this client to close the socket. // must still be called on this client to close the socket.
func (c *Conn) Logout() (untagged []Untagged, result Result, rerr error) { func (c *Conn) Logout() (resp Response, rerr error) {
defer c.recover(&rerr) defer c.recover(&rerr, &resp)
return c.Transactf("logout") return c.transactf("logout")
} }
// Starttls enables TLS on the connection with the STARTTLS command. // StartTLS enables TLS on the connection with the IMAP4 "STARTTLS" command.
func (c *Conn) Starttls(config *tls.Config) (untagged []Untagged, result Result, rerr error) { func (c *Conn) StartTLS(config *tls.Config) (resp Response, rerr error) {
defer c.recover(&rerr) defer c.recover(&rerr, &resp)
untagged, result, rerr = c.Transactf("starttls") resp, rerr = c.transactf("starttls")
c.xcheckf(rerr, "starttls command") c.xcheckf(rerr, "starttls command")
conn := c.xprefixConn() conn := c.xprefixConn()
@ -50,32 +52,43 @@ func (c *Conn) Starttls(config *tls.Config) (untagged []Untagged, result Result,
err := tlsConn.Handshake() err := tlsConn.Handshake()
c.xcheckf(err, "tls handshake") c.xcheckf(err, "tls handshake")
c.conn = tlsConn c.conn = tlsConn
return untagged, result, nil return
} }
// Login authenticates with username and password // Login authenticates using the IMAP4 "LOGIN" command, sending the plain text
func (c *Conn) Login(username, password string) (untagged []Untagged, result Result, rerr error) { // password to the server.
defer c.recover(&rerr) //
// 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.nextTag(), astring(username))
fmt.Fprintf(c.xbw, "%s login %s ", c.LastTag, astring(username))
defer c.xtracewrite(mlog.LevelTraceauth)() defer c.xtracewrite(mlog.LevelTraceauth)()
fmt.Fprintf(c.xbw, "%s\r\n", astring(password)) fmt.Fprintf(c.xbw, "%s\r\n", astring(password))
c.xtracewrite(mlog.LevelTrace) // Restore. c.xtracewrite(mlog.LevelTrace) // Restore.
return c.Response() return c.responseOK()
} }
// Authenticate with plaintext password using AUTHENTICATE PLAIN. // AuthenticatePlain executes the AUTHENTICATE command with SASL mechanism "PLAIN",
func (c *Conn) AuthenticatePlain(username, password string) (untagged []Untagged, result Result, rerr error) { // sending the password in plain text password to the server.
defer c.recover(&rerr) //
// 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") c.xcheckf(err, "writing authenticate command")
_, untagged, result, rerr = c.ReadContinuation() _, rerr = c.readContinuation()
c.xcheckf(rerr, "reading continuation") c.xresponse(rerr, &resp)
if result.Status != "" {
c.xerrorf("got result status %q, expected continuation", result.Status)
}
defer c.xtracewrite(mlog.LevelTraceauth)() defer c.xtracewrite(mlog.LevelTraceauth)()
xw := base64.NewEncoder(base64.StdEncoding, c.xbw) xw := base64.NewEncoder(base64.StdEncoding, c.xbw)
fmt.Fprintf(xw, "\u0000%s\u0000%s", username, password) 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. c.xtracewrite(mlog.LevelTrace) // Restore.
fmt.Fprintf(c.xbw, "\r\n") fmt.Fprintf(c.xbw, "\r\n")
c.xflush() c.xflush()
return c.Response() return c.responseOK()
} }
// todo: implement cram-md5, write its credentials as traceauth. // todo: implement cram-md5, write its credentials as traceauth.
// Authenticate with SCRAM-SHA-256(-PLUS) or SCRAM-SHA-1(-PLUS). With SCRAM, the // AuthenticateSCRAM executes the IMAP4 "AUTHENTICATE" command with one of the
// password is not exchanged in plaintext form, but only derived hashes are // following SASL mechanisms: SCRAM-SHA-256(-PLUS) or SCRAM-SHA-1(-PLUS).//
// exchanged by both parties as proof of knowledge of password. //
// 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, // The PLUS variants bind the authentication exchange to the TLS connection,
// detecting MitM attacks. // detecting MitM attacks.
func (c *Conn) AuthenticateSCRAM(method string, h func() hash.Hash, username, password string) (untagged []Untagged, result Result, rerr error) { func (c *Conn) AuthenticateSCRAM(mechanism string, h func() hash.Hash, username, password string) (resp Response, rerr error) {
defer c.recover(&rerr) defer c.recover(&rerr, &resp)
var cs *tls.ConnectionState var cs *tls.ConnectionState
lmethod := strings.ToLower(method) lmech := strings.ToLower(mechanism)
if strings.HasSuffix(lmethod, "-plus") { if strings.HasSuffix(lmech, "-plus") {
tlsConn, ok := c.conn.(*tls.Conn) tlsConn, ok := c.conn.(*tls.Conn)
if !ok { if !ok {
c.xerrorf("cannot use scram plus without tls") 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) sc := scram.NewClient(h, username, "", false, cs)
clientFirst, err := sc.ClientFirst() clientFirst, err := sc.ClientFirst()
c.xcheckf(err, "scram clientFirst") c.xcheckf(err, "scram clientFirst")
c.LastTag = c.nextTag() // todo: only send clientFirst if server has announced SASL-IR
err = c.Writelinef("%s authenticate %s %s", c.LastTag, method, base64.StdEncoding.EncodeToString([]byte(clientFirst))) err = c.Writelinef("%s authenticate %s %s", c.nextTag(), mechanism, base64.StdEncoding.EncodeToString([]byte(clientFirst)))
c.xcheckf(err, "writing command line") c.xcheckf(err, "writing command line")
xreadContinuation := func() []byte { xreadContinuation := func() []byte {
var line string var line string
line, untagged, result, rerr = c.ReadContinuation() line, rerr = c.readContinuation()
c.xcheckf(err, "read continuation") c.xresponse(rerr, &resp)
if result.Status != "" {
c.xerrorf("got result status %q, expected continuation", result.Status)
}
buf, err := base64.StdEncoding.DecodeString(line) buf, err := base64.StdEncoding.DecodeString(line)
c.xcheckf(err, "parsing base64 from remote") c.xcheckf(err, "parsing base64 from remote")
return buf 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)) err = c.Writelinef("%s", base64.StdEncoding.EncodeToString(nil))
c.xcheckf(err, "scram client end") 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. // State: Authenticated or selected.
func (c *Conn) CompressDeflate() (untagged []Untagged, result Result, rerr error) { func (c *Conn) CompressDeflate() (resp Response, rerr error) {
defer c.recover(&rerr) defer c.recover(&rerr, &resp)
untagged, result, rerr = c.Transactf("compress deflate") resp, rerr = c.transactf("compress deflate")
c.xcheck(rerr) c.xcheck(rerr)
c.xflateBW = bufio.NewWriter(c) c.xflateBW = bufio.NewWriter(c)
@ -172,89 +191,98 @@ func (c *Conn) CompressDeflate() (untagged []Untagged, result Result, rerr error
return return
} }
// Enable enables capabilities for use with the connection, verifying the server has indeed enabled them. // Enable enables capabilities for use with the connection by executing the IMAP4 "ENABLE" command.
func (c *Conn) Enable(capabilities ...string) (untagged []Untagged, result Result, rerr error) { //
defer c.recover(&rerr) // 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, " ")) var caps strings.Builder
c.xcheck(rerr) for _, c := range capabilities {
var enabled UntaggedEnabled caps.WriteString(" " + string(c))
c.xgetUntagged(untagged, &enabled)
got := map[string]struct{}{}
for _, cap := range enabled {
got[cap] = struct{}{}
} }
for _, cap := range capabilities { return c.transactf("enable%s", caps.String())
if _, ok := got[cap]; !ok {
c.xerrorf("capability %q not enabled by server", cap)
}
}
return
} }
// Select opens mailbox as active mailbox. // Select opens the mailbox with the IMAP4 "SELECT" command.
func (c *Conn) Select(mailbox string) (untagged []Untagged, result Result, rerr error) { //
defer c.recover(&rerr) // If a mailbox is selected/active, it is automatically deselected before
return c.Transactf("select %s", astring(mailbox)) // 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. // Examine opens the mailbox like [Conn.Select], but read-only, with the IMAP4
func (c *Conn) Examine(mailbox string) (untagged []Untagged, result Result, rerr error) { // "EXAMINE" command.
defer c.recover(&rerr) func (c *Conn) Examine(mailbox string) (resp Response, rerr error) {
return c.Transactf("examine %s", astring(mailbox)) defer c.recover(&rerr, &resp)
return c.transactf("examine %s", astring(mailbox))
} }
// Create makes a new mailbox on the server. // 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 //
// SpecialUse can only be used on servers that announced the "CREATE-SPECIAL-USE"
// capability. Specify flags like \Archive, \Drafts, \Junk, \Sent, \Trash, \All. // capability. Specify flags like \Archive, \Drafts, \Junk, \Sent, \Trash, \All.
func (c *Conn) Create(mailbox string, specialUse []string) (untagged []Untagged, result Result, rerr error) { func (c *Conn) Create(mailbox string, specialUse []string) (resp Response, rerr error) {
defer c.recover(&rerr) defer c.recover(&rerr, &resp)
if _, ok := c.CapAvailable[CapCreateSpecialUse]; !ok && len(specialUse) > 0 {
c.xerrorf("server does not implement create-special-use extension")
}
var useStr string var useStr string
if len(specialUse) > 0 { if len(specialUse) > 0 {
useStr = fmt.Sprintf(" USE (%s)", strings.Join(specialUse, " ")) 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. // Delete removes an entire mailbox and its messages using the IMAP4 "DELETE"
func (c *Conn) Delete(mailbox string) (untagged []Untagged, result Result, rerr error) { // command.
defer c.recover(&rerr) func (c *Conn) Delete(mailbox string) (resp Response, rerr error) {
return c.Transactf("delete %s", astring(mailbox)) defer c.recover(&rerr, &resp)
return c.transactf("delete %s", astring(mailbox))
} }
// Rename changes the name of a mailbox and all its child mailboxes. // 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) { // using the IMAP4 "RENAME" command.
defer c.recover(&rerr) func (c *Conn) Rename(omailbox, nmailbox string) (resp Response, rerr error) {
return c.Transactf("rename %s %s", astring(omailbox), astring(nmailbox)) 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 // Subscribe marks a mailbox as subscribed using the IMAP4 "SUBSCRIBE" command.
// is not an error if the mailbox is already subscribed. //
func (c *Conn) Subscribe(mailbox string) (untagged []Untagged, result Result, rerr error) { // The mailbox does not have to exist. It is not an error if the mailbox is already
defer c.recover(&rerr) // subscribed.
return c.Transactf("subscribe %s", astring(mailbox)) 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. // Unsubscribe marks a mailbox as unsubscribed using the IMAP4 "UNSUBSCRIBE"
func (c *Conn) Unsubscribe(mailbox string) (untagged []Untagged, result Result, rerr error) { // command.
defer c.recover(&rerr) func (c *Conn) Unsubscribe(mailbox string) (resp Response, rerr error) {
return c.Transactf("unsubscribe %s", astring(mailbox)) 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). // Pattern can contain * (match any) or % (match any except hierarchy delimiter).
func (c *Conn) List(pattern string) (untagged []Untagged, result Result, rerr error) { func (c *Conn) List(pattern string) (resp Response, rerr error) {
defer c.recover(&rerr) defer c.recover(&rerr, &resp)
return c.Transactf(`list "" %s`, astring(pattern)) 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). // 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) { func (c *Conn) ListFull(subscribedOnly bool, patterns ...string) (resp Response, rerr error) {
defer c.recover(&rerr) defer c.recover(&rerr, &resp)
var subscribedStr string var subscribedStr string
if subscribedOnly { if subscribedOnly {
subscribedStr = "subscribed recursivematch" subscribedStr = "subscribed recursivematch"
@ -262,49 +290,54 @@ func (c *Conn) ListFull(subscribedOnly bool, patterns ...string) (untagged []Unt
for i, s := range patterns { for i, s := range patterns {
patterns[i] = astring(s) 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. // Namespace requests the hiearchy separator using the IMAP4 "NAMESPACE" command.
func (c *Conn) Namespace() (untagged []Untagged, result Result, rerr error) { //
defer c.recover(&rerr) // Required capability: "NAMESPACE" or "IMAP4rev2".
return c.Transactf("namespace") //
// 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, // Status requests information about a mailbox using the IMAP4 "STATUS" command. For
// etc. At least one attribute required. // example, number of messages, size, etc. At least one attribute required.
func (c *Conn) Status(mailbox string, attrs ...StatusAttr) (untagged []Untagged, result Result, rerr error) { func (c *Conn) Status(mailbox string, attrs ...StatusAttr) (resp Response, rerr error) {
defer c.recover(&rerr) defer c.recover(&rerr, &resp)
l := make([]string, len(attrs)) l := make([]string, len(attrs))
for i, a := range attrs { for i, a := range attrs {
l[i] = string(a) 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 { type Append struct {
Flags []string Flags []string // Optional, flags for the new message.
Received *time.Time Received *time.Time // Optional, the INTERNALDATE field, typically time at which a message was received.
Size int64 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 // Required capability: "MULTIAPPEND"
// MULTIAPPEND capability. func (c *Conn) MultiAppend(mailbox string, message Append, more ...Append) (resp Response, rerr error) {
func (c *Conn) Append(mailbox string, message Append, more ...Append) (untagged []Untagged, result Result, rerr error) { defer c.recover(&rerr, &resp)
defer c.recover(&rerr)
if _, ok := c.CapAvailable[CapMultiAppend]; !ok && len(more) > 0 { fmt.Fprintf(c.xbw, "%s append %s", c.nextTag(), astring(mailbox))
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))
msgs := append([]Append{message}, more...) msgs := append([]Append{message}, more...)
for _, m := range msgs { 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") fmt.Fprintf(c.xbw, "\r\n")
c.xflush() c.xflush()
return c.Response() return c.responseOK()
} }
// note: No Idle or Notify command. Idle/Notify is better implemented by // 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. // writing the request and reading and handling the responses as they come in.
// CloseMailbox closes the currently selected/active mailbox, permanently removing // CloseMailbox closes the selected/active mailbox using the IMAP4 "CLOSE" command,
// any messages marked with \Deleted. // permanently removing ("expunging") any messages marked with \Deleted.
func (c *Conn) CloseMailbox() (untagged []Untagged, result Result, rerr error) { //
return c.Transactf("close") // 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 // Unselect closes the selected/active mailbox using the IMAP4 "UNSELECT" command,
// does not permanently remove any messages marked with \Deleted. // but unlike MailboxClose does not permanently remove ("expunge") any messages
func (c *Conn) Unselect() (untagged []Untagged, result Result, rerr error) { // marked with \Deleted.
return c.Transactf("unselect") //
// 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. // Expunge removes all messages marked as deleted for the selected mailbox using
func (c *Conn) Expunge() (untagged []Untagged, result Result, rerr error) { // the IMAP4 "EXPUNGE" command. If other sessions marked messages as deleted, even
defer c.recover(&rerr) // if they aren't visible in the session, they are removed as well.
return c.Transactf("expunge") //
// 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. // UIDExpunge is like expunge, but only removes messages matching UID set, using
func (c *Conn) UIDExpunge(uidSet NumSet) (untagged []Untagged, result Result, rerr error) { // the IMAP4 "UID EXPUNGE" command.
defer c.recover(&rerr) //
return c.Transactf("uid expunge %s", uidSet.String()) // 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. // 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. // MSNStoreFlagsSet stores a new set of flags for messages matching message
// If silent, no untagged responses with the updated flags will be sent by the server. // sequence numbers (MSNs) from sequence set with the IMAP4 "STORE" command.
func (c *Conn) StoreFlagsSet(seqset string, silent bool, flags ...string) (untagged []Untagged, result Result, rerr error) { //
defer c.recover(&rerr) // 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" item := "flags"
if silent { if silent {
item += ".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. // MSNStoreFlagsAdd is like [Conn.MSNStoreFlagsSet], but only adds flags, leaving
func (c *Conn) StoreFlagsAdd(seqset string, silent bool, flags ...string) (untagged []Untagged, result Result, rerr error) { // current flags on the message intact.
defer c.recover(&rerr) //
// 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" item := "+flags"
if silent { if silent {
item += ".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. // MSNStoreFlagsClear is like [Conn.MSNStoreFlagsSet], but only removes flags,
func (c *Conn) StoreFlagsClear(seqset string, silent bool, flags ...string) (untagged []Untagged, result Result, rerr error) { // leaving other flags on the message intact.
defer c.recover(&rerr) //
// 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" item := "-flags"
if silent { if silent {
item += ".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 // UIDStoreFlagsSet stores a new set of flags for messages matching UIDs from
// the UID STORE command. // uidSet with the IMAP4 "UID STORE" command.
// //
// If silent, no untagged responses with the updated flags will be sent by the // If silent, no untagged responses with the updated flags will be sent by the
// server. // 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" item := "flags"
if silent { if silent {
item += ".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 // UIDStoreFlagsAdd is like UIDStoreFlagsSet, but only adds flags, leaving
// current flags on the message intact. // 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" item := "+flags"
if silent { if silent {
item += ".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 // UIDStoreFlagsClear is like UIDStoreFlagsSet, but only removes flags, leaving
// other flags on the message intact. // 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" item := "-flags"
if silent { if silent {
item += ".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. // MSNCopy adds messages from the sequences in the sequence set in the
func (c *Conn) Copy(seqSet NumSet, dstMailbox string) (untagged []Untagged, result Result, rerr error) { // selected/active mailbox to destMailbox using the IMAP4 "COPY" command.
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.
// //
// 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. // Required capability: "UIDPLUS" or "IMAP4rev2".
func (c *Conn) Replace(msgseq string, mailbox string, msg Append) (untagged []Untagged, result Result, rerr error) { 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. // todo: parse msgseq, must be nznumber, with a known msgseq. or "*" with at least one message.
return c.replace("replace", msgseq, mailbox, msg) return c.replace("replace", msgseq, mailbox, msg)
} }
// UIDReplace is like Replace, but operates on a UID instead of message // UIDReplace uses the IMAP4 "UID REPLACE" command to replace a message from the
// sequence number. // selected/active mailbox with a new/different version of the message in the named
func (c *Conn) UIDReplace(uid string, mailbox string, msg Append) (untagged []Untagged, result Result, rerr error) { // 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. // todo: parse uid, must be nznumber, with a known uid. or "*" with at least one message.
return c.replace("uid replace", uid, mailbox, msg) 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) { func (c *Conn) replace(cmd string, num string, mailbox string, msg Append) (resp Response, rerr error) {
defer c.recover(&rerr) defer c.recover(&rerr, &resp)
// todo: use synchronizing literal for larger messages. // 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: only use literal8 if needed, possibly with "UTF8()"
// todo: encode mailbox // 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") c.xcheckf(err, "writing replace command")
defer c.xtracewrite(mlog.LevelTracedata)() 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") fmt.Fprintf(c.xbw, "\r\n")
c.xflush() c.xflush()
return c.Response() return c.responseOK()
} }

38
imapclient/fuzz_test.go Normal file
View File

@ -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)
})
}

File diff suppressed because it is too large Load Diff

42
imapclient/parse_test.go Normal file
View File

@ -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"})
}

View File

@ -2,42 +2,51 @@ package imapclient
import ( import (
"bufio" "bufio"
"errors"
"fmt" "fmt"
"strings" "strings"
"time" "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 type Capability string
const ( const (
CapIMAP4rev1 Capability = "IMAP4rev1" CapIMAP4rev1 Capability = "IMAP4REV1" // ../rfc/3501:1310
CapIMAP4rev2 Capability = "IMAP4rev2" CapIMAP4rev2 Capability = "IMAP4REV2" // ../rfc/9051:1219
CapLoginDisabled Capability = "LOGINDISABLED" CapLoginDisabled Capability = "LOGINDISABLED" // ../rfc/3501:3792 ../rfc/9051:5436
CapStarttls Capability = "STARTTLS" CapStartTLS Capability = "STARTTLS" // ../rfc/3501:1327 ../rfc/9051:1238
CapAuthPlain Capability = "AUTH=PLAIN" CapAuthPlain Capability = "AUTH=PLAIN" // ../rfc/3501:1327 ../rfc/9051:1238
CapLiteralPlus Capability = "LITERAL+" CapAuthExternal Capability = "AUTH=EXTERNAL" // ../rfc/4422:1575
CapLiteralMinus Capability = "LITERAL-" CapAuthSCRAMSHA256Plus Capability = "AUTH=SCRAM-SHA-256-PLUS" // ../rfc/7677:80
CapIdle Capability = "IDLE" CapAuthSCRAMSHA256 Capability = "AUTH=SCRAM-SHA-256"
CapNamespace Capability = "NAMESPACE" CapAuthSCRAMSHA1Plus Capability = "AUTH=SCRAM-SHA-1-PLUS" // ../rfc/5802:465
CapBinary Capability = "BINARY" CapAuthSCRAMSHA1 Capability = "AUTH=SCRAM-SHA-1"
CapUnselect Capability = "UNSELECT" CapAuthCRAMMD5 Capability = "AUTH=CRAM-MD5" // ../rfc/2195:80
CapUidplus Capability = "UIDPLUS" CapLiteralPlus Capability = "LITERAL+" // ../rfc/2088:45
CapEsearch Capability = "ESEARCH" CapLiteralMinus Capability = "LITERAL-" // ../rfc/7888:26 ../rfc/9051:847 Default since IMAP4rev2
CapEnable Capability = "ENABLE" CapIdle Capability = "IDLE" // ../rfc/2177:69 ../rfc/9051:3542 Default since IMAP4rev2
CapSave Capability = "SAVE" CapNamespace Capability = "NAMESPACE" // ../rfc/2342:130 ../rfc/9051:135 Default since IMAP4rev2
CapListExtended Capability = "LIST-EXTENDED" CapBinary Capability = "BINARY" // ../rfc/3516:100
CapSpecialUse Capability = "SPECIAL-USE" CapUnselect Capability = "UNSELECT" // ../rfc/3691:78 ../rfc/9051:3667 Default since IMAP4rev2
CapMove Capability = "MOVE" 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" CapUTF8Only Capability = "UTF8=ONLY"
CapUTF8Accept Capability = "UTF8=ACCEPT" CapUTF8Accept Capability = "UTF8=ACCEPT"
CapCondstore Capability = "CONDSTORE" // ../rfc/7162:411
CapQresync Capability = "QRESYNC" // ../rfc/7162:1376
CapID Capability = "ID" // ../rfc/2971:80 CapID Capability = "ID" // ../rfc/2971:80
CapMetadata Capability = "METADATA" // ../rfc/5464:124 CapMetadata Capability = "METADATA" // ../rfc/5464:124
CapMetadataServer Capability = "METADATA-SERVER" // ../rfc/5464:124 CapMetadataServer Capability = "METADATA-SERVER" // ../rfc/5464:124
CapSaveDate Capability = "SAVEDATE" // ../rfc/8514 CapSaveDate Capability = "SAVEDATE" // ../rfc/8514
CapCreateSpecialUse Capability = "CREATE-SPECIAL-USE" // ../rfc/6154:296 CapCreateSpecialUse Capability = "CREATE-SPECIAL-USE" // ../rfc/6154:296
CapCompressDeflate Capability = "COMPRESS=DEFLATE" // ../rfc/4978:65 CapCompressDeflate Capability = "COMPRESS=DEFLATE" // ../rfc/4978:65
CapListMetadata Capability = "LIST-METADTA" // ../rfc/9590:73 CapListMetadata Capability = "LIST-METADATA" // ../rfc/9590:73
CapMultiAppend Capability = "MULTIAPPEND" // ../rfc/3502:33 CapMultiAppend Capability = "MULTIAPPEND" // ../rfc/3502:33
CapReplace Capability = "REPLACE" // ../rfc/8508:155 CapReplace Capability = "REPLACE" // ../rfc/8508:155
CapPreview Capability = "PREVIEW" // ../rfc/8970:114 CapPreview Capability = "PREVIEW" // ../rfc/8970:114
@ -55,63 +64,134 @@ const (
OK Status = "OK" // Command succeeded. 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. // Result is the final response for a command, indicating success or failure.
type Result struct { type Result struct {
Status Status 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. func (r Result) Error() string {
type CodeArg interface { s := fmt.Sprintf("IMAP result %s", r.Status)
if r.Code != nil {
s += "[" + r.Code.CodeString() + "]"
}
if r.Text != "" {
s += " " + r.Text
}
return s
}
// Code represents a response code with optional arguments, i.e. the data between [] in the response line.
type Code interface {
CodeString() string CodeString() string
} }
// CodeOther is a valid but unrecognized response code. // CodeWord is a response code without parameters, always in upper case.
type CodeOther struct { type CodeWord string
Code 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 Args []string
} }
func (c CodeOther) CodeString() string { func (c CodeParams) CodeString() string {
return c.Code + " " + strings.Join(c.Args, " ") return c.Code + " " + strings.Join(c.Args, " ")
} }
// CodeWords is a code with space-separated string parameters. E.g. CAPABILITY. // CodeCapability is a CAPABILITY response code with the capabilities supported by the server.
type CodeWords struct { type CodeCapability []Capability
Code string
Args []string
}
func (c CodeWords) CodeString() string { func (c CodeCapability) CodeString() string {
s := c.Code var s string
for _, w := range c.Args { for _, c := range c {
s += " " + w s += " " + string(c)
} }
return s return "CAPABILITY" + s
} }
// CodeList is a code with a list with space-separated strings as parameters. E.g. BADCHARSET, PERMANENTFLAGS. type CodeBadCharset []string
type CodeList struct {
Code string
Args []string // If nil, no list was present. List can also be empty.
}
func (c CodeList) CodeString() string { func (c CodeBadCharset) CodeString() string {
s := c.Code s := "BADCHARSET"
if c.Args == nil { if len(c) == 0 {
return s 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 CodePermanentFlags []string
type CodeUint struct {
Code string func (c CodePermanentFlags) CodeString() string {
Num uint32 return "PERMANENTFLAGS (" + strings.Join([]string(c), " ") + ")"
} }
func (c CodeUint) CodeString() string { type CodeUIDNext uint32
return fmt.Sprintf("%s %d", c.Code, c.Num)
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. // "APPENDUID" response code.
@ -196,11 +276,32 @@ func (c CodeBadEvent) CodeString() string {
return fmt.Sprintf("BADEVENT (%s)", strings.Join([]string(c), " ")) return fmt.Sprintf("BADEVENT (%s)", strings.Join([]string(c), " "))
} }
// RespText represents a response line minus the leading tag. // "METADATA LONGENTRIES number" response for GETMETADATA command.
type RespText struct { type CodeMetadataLongEntries uint32
Code string // The first word between [] after the status.
CodeArg CodeArg // Set if code has a parameter. func (c CodeMetadataLongEntries) CodeString() string {
More string // Any remaining text. 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. // atom or string.
@ -241,17 +342,30 @@ func syncliteral(s string) string {
// todo: make an interface that the untagged responses implement? // todo: make an interface that the untagged responses implement?
type Untagged any type Untagged any
type UntaggedBye RespText type UntaggedBye struct {
type UntaggedPreauth RespText 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 UntaggedExpunge uint32
type UntaggedExists uint32 type UntaggedExists uint32
type UntaggedRecent 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 UntaggedResult Result
type UntaggedFlags []string type UntaggedFlags []string
type UntaggedList struct { type UntaggedList struct {
// ../rfc/9051:6690 // ../rfc/9051:6690
Flags []string Flags []string
Separator byte // 0 for NIL Separator byte // 0 for NIL
Mailbox string Mailbox string
@ -272,8 +386,9 @@ type UntaggedUIDFetch struct {
} }
type UntaggedSearch []uint32 type UntaggedSearch []uint32
// ../rfc/7162:1101
type UntaggedSearchModSeq struct { type UntaggedSearchModSeq struct {
// ../rfc/7162:1101
Nums []uint32 Nums []uint32
ModSeq int64 ModSeq int64
} }
@ -282,8 +397,10 @@ type UntaggedStatus struct {
Attrs map[StatusAttr]int64 // Upper case status attributes. 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 { type UntaggedMetadataKeys struct {
// ../rfc/5464:716
Mailbox string // Empty means not specific to mailbox. Mailbox string // Empty means not specific to mailbox.
// Keys that have changed. To get values (or determine absence), the server must be // Keys that have changed. To get values (or determine absence), the server must be
@ -299,15 +416,17 @@ type Annotation struct {
Value []byte Value []byte
} }
// ../rfc/5464:683
type UntaggedMetadataAnnotations struct { type UntaggedMetadataAnnotations struct {
// ../rfc/5464:683
Mailbox string // Empty means not specific to mailbox. Mailbox string // Empty means not specific to mailbox.
Annotations []Annotation Annotations []Annotation
} }
// ../rfc/9051:7059 ../9208:712
type StatusAttr string type StatusAttr string
// ../rfc/9051:7059 ../9208:712
const ( const (
StatusMessages StatusAttr = "MESSAGES" StatusMessages StatusAttr = "MESSAGES"
StatusUIDNext StatusAttr = "UIDNEXT" StatusUIDNext StatusAttr = "UIDNEXT"
@ -326,6 +445,7 @@ type UntaggedNamespace struct {
} }
type UntaggedLsub struct { type UntaggedLsub struct {
// ../rfc/3501:4833 // ../rfc/3501:4833
Flags []string Flags []string
Separator byte Separator byte
Mailbox string Mailbox string
@ -395,6 +515,7 @@ type EsearchDataExt struct {
type NamespaceDescr struct { type NamespaceDescr struct {
// ../rfc/9051:6769 // ../rfc/9051:6769
Prefix string Prefix string
Separator byte // If 0 then separator was absent. Separator byte // If 0 then separator was absent.
Exts []NamespaceExtension Exts []NamespaceExtension
@ -402,13 +523,14 @@ type NamespaceDescr struct {
type NamespaceExtension struct { type NamespaceExtension struct {
// ../rfc/9051:6773 // ../rfc/9051:6773
Key string Key string
Values []string Values []string
} }
// FetchAttr represents a FETCH response attribute. // FetchAttr represents a FETCH response attribute.
type FetchAttr interface { type FetchAttr interface {
Attr() string // Name of attribute. Attr() string // Name of attribute in upper case, e.g. "UID".
} }
type NumSet struct { type NumSet struct {
@ -435,14 +557,14 @@ func (ns NumSet) String() string {
} }
func ParseNumSet(s string) (ns NumSet, rerr error) { 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) defer c.recover(&rerr)
ns = c.xsequenceSet() ns = c.xsequenceSet()
return return
} }
func ParseUIDRange(s string) (nr NumRange, rerr error) { 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) defer c.recover(&rerr)
nr = c.xuidrange() nr = c.xuidrange()
return return
@ -481,6 +603,7 @@ type TaggedExtComp struct {
type TaggedExtVal struct { type TaggedExtVal struct {
// ../rfc/9051:7111 // ../rfc/9051:7111
Number *int64 Number *int64
SeqSet *NumSet 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. 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 { type MboxListExtendedItem struct {
// ../rfc/9051:6699 // ../rfc/9051:6699
Tag string Tag string
Val TaggedExtVal Val TaggedExtVal
} }
@ -522,8 +646,10 @@ type FetchInternalDate struct {
func (f FetchInternalDate) Attr() string { return "INTERNALDATE" } func (f FetchInternalDate) Attr() string { return "INTERNALDATE" }
// "SAVEDATE" fetch response. ../rfc/8514:265 // "SAVEDATE" fetch response.
type FetchSaveDate struct { type FetchSaveDate struct {
// ../rfc/8514:265
SaveDate *time.Time // nil means absent for message. SaveDate *time.Time // nil means absent for message.
} }
@ -552,6 +678,7 @@ func (f FetchRFC822Text) Attr() string { return "RFC822.TEXT" }
// "BODYSTRUCTURE" fetch response. // "BODYSTRUCTURE" fetch response.
type FetchBodystructure struct { type FetchBodystructure struct {
// ../rfc/9051:6355 // ../rfc/9051:6355
RespAttr string RespAttr string
Body any // BodyType* Body any // BodyType*
} }
@ -561,6 +688,7 @@ func (f FetchBodystructure) Attr() string { return f.RespAttr }
// "BODY" fetch response. // "BODY" fetch response.
type FetchBody struct { type FetchBody struct {
// ../rfc/9051:6756 ../rfc/9051:6985 // ../rfc/9051:6756 ../rfc/9051:6985
RespAttr string RespAttr string
Section string // todo: parse more ../rfc/9051:6985 Section string // todo: parse more ../rfc/9051:6985
Offset int32 Offset int32
@ -580,6 +708,7 @@ type BodyFields struct {
// subparts and the multipart media subtype. Used in a FETCH response. // subparts and the multipart media subtype. Used in a FETCH response.
type BodyTypeMpart struct { type BodyTypeMpart struct {
// ../rfc/9051:6411 // ../rfc/9051:6411
Bodies []any // BodyTypeBasic, BodyTypeMsg, BodyTypeText Bodies []any // BodyTypeBasic, BodyTypeMsg, BodyTypeText
MediaSubtype string MediaSubtype string
Ext *BodyExtensionMpart Ext *BodyExtensionMpart
@ -589,6 +718,7 @@ type BodyTypeMpart struct {
// response. // response.
type BodyTypeBasic struct { type BodyTypeBasic struct {
// ../rfc/9051:6407 // ../rfc/9051:6407
MediaType, MediaSubtype string MediaType, MediaSubtype string
BodyFields BodyFields BodyFields BodyFields
Ext *BodyExtension1Part Ext *BodyExtension1Part
@ -598,6 +728,7 @@ type BodyTypeBasic struct {
// response. // response.
type BodyTypeMsg struct { type BodyTypeMsg struct {
// ../rfc/9051:6415 // ../rfc/9051:6415
MediaType, MediaSubtype string MediaType, MediaSubtype string
BodyFields BodyFields BodyFields BodyFields
Envelope Envelope Envelope Envelope
@ -610,6 +741,7 @@ type BodyTypeMsg struct {
// response. // response.
type BodyTypeText struct { type BodyTypeText struct {
// ../rfc/9051:6418 // ../rfc/9051:6418
MediaType, MediaSubtype string MediaType, MediaSubtype string
BodyFields BodyFields BodyFields BodyFields
Lines int64 Lines int64
@ -618,26 +750,42 @@ type BodyTypeText struct {
// BodyExtension1Part has the extensible form fields of a BODYSTRUCTURE for // BodyExtension1Part has the extensible form fields of a BODYSTRUCTURE for
// multiparts. // 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 { type BodyExtensionMpart struct {
// ../rfc/9051:5986 ../rfc/3501:4161 ../rfc/9051:6371 ../rfc/3501:4599 // ../rfc/9051:5986 ../rfc/3501:4161 ../rfc/9051:6371 ../rfc/3501:4599
Params [][2]string Params [][2]string
Disposition string Disposition **string
DispositionParams [][2]string DispositionParams *[][2]string
Language []string Language *[]string
Location string Location **string
More []BodyExtension More []BodyExtension // Nil if absent.
} }
// BodyExtension1Part has the extensible form fields of a BODYSTRUCTURE for // BodyExtension1Part has the extensible form fields of a BODYSTRUCTURE for
// non-multiparts. // 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 { type BodyExtension1Part struct {
// ../rfc/9051:6023 ../rfc/3501:4191 ../rfc/9051:6366 ../rfc/3501:4584 // ../rfc/9051:6023 ../rfc/3501:4191 ../rfc/9051:6366 ../rfc/3501:4584
MD5 string
Disposition string MD5 *string
DispositionParams [][2]string Disposition **string
Language []string DispositionParams *[][2]string
Location string Language *[]string
More []BodyExtension Location **string
More []BodyExtension // Nil means absent.
} }
// BodyExtension has the additional extension fields for future expansion of // BodyExtension has the additional extension fields for future expansion of

View File

@ -50,14 +50,14 @@ func testAppend(t *testing.T, uidonly bool) {
tc2.client.Select("inbox") tc2.client.Select("inbox")
tc2.transactf("no", "append nobox (\\Seen) \" 1-Jan-2022 10:10:00 +0100\" {1}") 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.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.transactf("ok", "append inbox (\\Seen Label1 $label2) \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
tc2.xuntagged(imapclient.UntaggedExists(1)) 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") tc.transactf("ok", "noop")
flags := imapclient.FetchFlags{`\Seen`, "$label2", "label1"} 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.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.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.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.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 // Messages that we cannot parse are marked as application/octet-stream. Perhaps
// the imap client knows how to deal with them. // 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", "noop") // Flush pending untagged responses.
tc.transactf("ok", "append inbox {6+}\r\ntest\r\n ~{6+}\r\ntost\r\n") tc.transactf("ok", "append inbox {6+}\r\ntest\r\n ~{6+}\r\ntost\r\n")
tc.xuntagged(imapclient.UntaggedExists(5)) 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. // Cancelled with zero-length message.
tc.transactf("no", "append inbox {6+}\r\ntest\r\n {0+}\r\n") 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)) tclimit.xuntagged(imapclient.UntaggedExists(1))
// Second message would take account past limit. // 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.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. // Empty mailbox.
if uidonly { if uidonly {
@ -119,10 +119,10 @@ func testAppend(t *testing.T, uidonly bool) {
// Multiappend with first message within quota, and second message with sync // Multiappend with first message within quota, and second message with sync
// literal causing quota error. Request should get error response immediately. // literal causing quota error. Request should get error response immediately.
tclimit.transactf("no", "append inbox {1+}\r\nx {100000}") 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. // 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) buf := make([]byte, 4000, 4002)
for i := range buf { for i := range buf {
buf[i] = 'x' buf[i] = 'x'
@ -131,5 +131,5 @@ func testAppend(t *testing.T, uidonly bool) {
_, err := tclimit.client.Write(buf) _, err := tclimit.client.Write(buf)
tclimit.check(err, "write append message") tclimit.check(err, "write append message")
tclimit.response("no") tclimit.response("no")
tclimit.xcode("OVERQUOTA") tclimit.xcodeWord("OVERQUOTA")
} }

View File

@ -20,6 +20,7 @@ import (
"golang.org/x/text/secure/precis" "golang.org/x/text/secure/precis"
"github.com/mjl-/mox/imapclient"
"github.com/mjl-/mox/mox-" "github.com/mjl-/mox/mox-"
"github.com/mjl-/mox/scram" "github.com/mjl-/mox/scram"
"github.com/mjl-/mox/store" "github.com/mjl-/mox/store"
@ -38,19 +39,19 @@ func TestAuthenticatePlain(t *testing.T) {
tc.transactf("no", "authenticate bogus ") tc.transactf("no", "authenticate bogus ")
tc.transactf("bad", "authenticate plain not base64...") tc.transactf("bad", "authenticate plain not base64...")
tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000baduser\u0000badpass"))) 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.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.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.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.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.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.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.transactf("ok", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000"+password0)))
tc.close() tc.close()
@ -93,14 +94,14 @@ func TestLoginDisabled(t *testing.T) {
tcheck(t, err, "close account") tcheck(t, err, "close account")
tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000disabled@mox.example\u0000test1234"))) 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.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.transactf("no", "login disabled@mox.example test1234")
tc.xcode("") tc.xcode(nil)
tc.transactf("no", "login disabled@mox.example bogus") tc.transactf("no", "login disabled@mox.example bogus")
tc.xcode("AUTHENTICATIONFAILED") tc.xcodeWord("AUTHENTICATIONFAILED")
} }
func TestAuthenticateSCRAMSHA1(t *testing.T) { 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()) sc := scram.NewClient(h, username, "", noServerPlus, tc.client.TLSConnectionState())
clientFirst, err := sc.ClientFirst() clientFirst, err := sc.ClientFirst()
tc.check(err, "scram clientFirst") tc.check(err, "scram clientFirst")
tc.client.LastTag = "x001" tc.client.WriteCommandf("", "authenticate %s %s", method, base64.StdEncoding.EncodeToString([]byte(clientFirst)))
tc.writelinef("%s authenticate %s %s", tc.client.LastTag, method, base64.StdEncoding.EncodeToString([]byte(clientFirst)))
xreadContinuation := func() []byte { xreadContinuation := func() []byte {
line, _, result, _ := tc.client.ReadContinuation() line, err := tc.client.ReadContinuation()
if result.Status != "" { tcheck(t, err, "read continuation")
tc.t.Fatalf("expected continuation")
}
buf, err := base64.StdEncoding.DecodeString(line) buf, err := base64.StdEncoding.DecodeString(line)
tc.check(err, "parsing base64 from remote") tc.check(err, "parsing base64 from remote")
return buf return buf
@ -161,10 +159,10 @@ func testAuthenticateSCRAM(t *testing.T, tls bool, method string, h func() hash.
} else { } else {
tc.writelinef("") tc.writelinef("")
} }
_, result, err := tc.client.Response() resp, err := tc.client.ReadResponse()
tc.check(err, "read response") tc.check(err, "read response")
if string(result.Status) != strings.ToUpper(status) { if string(resp.Status) != strings.ToUpper(status) {
tc.t.Fatalf("got status %q, expected %q", result.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) { auth := func(status string, username, password string) {
t.Helper() t.Helper()
tc.client.LastTag = "x001" tc.client.WriteCommandf("", "authenticate CRAM-MD5")
tc.writelinef("%s authenticate CRAM-MD5", tc.client.LastTag)
xreadContinuation := func() []byte { xreadContinuation := func() []byte {
line, _, result, _ := tc.client.ReadContinuation() line, err := tc.client.ReadContinuation()
if result.Status != "" { tcheck(t, err, "read continuation")
tc.t.Fatalf("expected continuation")
}
buf, err := base64.StdEncoding.DecodeString(line) buf, err := base64.StdEncoding.DecodeString(line)
tc.check(err, "parsing base64 from remote") tc.check(err, "parsing base64 from remote")
return buf return buf
@ -215,13 +210,13 @@ func TestAuthenticateCRAMMD5(t *testing.T) {
} }
h := hmac.New(md5.New, []byte(password)) h := hmac.New(md5.New, []byte(password))
h.Write([]byte(chal)) h.Write([]byte(chal))
resp := fmt.Sprintf("%s %x", username, h.Sum(nil)) data := fmt.Sprintf("%s %x", username, h.Sum(nil))
tc.writelinef("%s", base64.StdEncoding.EncodeToString([]byte(resp))) tc.writelinef("%s", base64.StdEncoding.EncodeToString([]byte(data)))
_, result, err := tc.client.Response() resp, err := tc.client.ReadResponse()
tc.check(err, "read response") tc.check(err, "read response")
if string(result.Status) != strings.ToUpper(status) { if string(resp.Status) != strings.ToUpper(status) {
tc.t.Fatalf("got status %q, expected %q", result.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. // Starttls and external auth.
tc = startArgsMore(t, false, true, false, nil, &clientConfig, false, true, "mjl", addClientCert) 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.transactf("ok", "authenticate external =")
tc.close() tc.close()
@ -339,7 +334,7 @@ func TestAuthenticateTLSClientCert(t *testing.T) {
t.Fatalf("tls connection was not resumed") t.Fatalf("tls connection was not resumed")
} }
// Check that operations that require an account work. // 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") received, err := time.Parse(time.RFC3339, "2022-11-16T10:01:00+01:00")
tc.check(err, "parse time") tc.check(err, "parse time")
tc.client.Append("inbox", makeAppendTime(exampleMsg, received)) tc.client.Append("inbox", makeAppendTime(exampleMsg, received))

View File

@ -21,7 +21,7 @@ func TestCompress(t *testing.T) {
tc.client.CompressDeflate() tc.client.CompressDeflate()
tc.transactf("no", "compress deflate") // Cannot have multiple. tc.transactf("no", "compress deflate") // Cannot have multiple.
tc.xcode("COMPRESSIONACTIVE") tc.xcodeWord("COMPRESSIONACTIVE")
tc.client.Select("inbox") tc.client.Select("inbox")
tc.transactf("ok", "append inbox (\\seen) {%d+}\r\n%s", len(exampleMsg), exampleMsg) 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) tc := start(t, false)
defer tc.close() defer tc.close()
tc.client.Starttls(&tls.Config{InsecureSkipVerify: true}) tc.client.StartTLS(&tls.Config{InsecureSkipVerify: true})
tc.login("mjl@mox.example", password0) tc.login("mjl@mox.example", password0)
tc.client.CompressDeflate() tc.client.CompressDeflate()
tc.client.Select("inbox") tc.client.Select("inbox")

View File

@ -38,15 +38,15 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) {
// Check basic requirements of CONDSTORE. // Check basic requirements of CONDSTORE.
capability := "Condstore" capability := imapclient.CapCondstore
if qresync { if qresync {
capability = "Qresync" capability = imapclient.CapQresync
} }
tc.login("mjl@mox.example", password0) tc.login("mjl@mox.example", password0)
tc.client.Enable(capability) tc.client.Enable(capability)
tc.transactf("ok", "Select inbox") 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. // 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. // 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.transactf("ok", "Append inbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
tc.xuntagged(imapclient.UntaggedExists(4)) 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.transactf("ok", "Append otherbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
tc.xuntagged() 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.transactf("ok", "Append inbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
tc.xuntagged(imapclient.UntaggedExists(5)) 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.transactf("ok", "Append inbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
tc.xuntagged(imapclient.UntaggedExists(6)) 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") tc2.transactf("ok", "Noop")
noflags := imapclient.FetchFlags(nil) noflags := imapclient.FetchFlags(nil)
@ -181,10 +181,10 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) {
// Check highestmodseq when we select. // Check highestmodseq when we select.
tc.transactf("ok", "Examine otherbox") 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.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 clientModseq += 4
@ -225,13 +225,13 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) {
if !uidonly { if !uidonly {
// unchangedsince 0 never passes the check. ../rfc/7162:640 // unchangedsince 0 never passes the check. ../rfc/7162:640
tc.transactf("ok", `Store 1 (Unchangedsince 0) +Flags ()`) 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))) tc.xuntagged(tc.untaggedFetch(1, 1, noflags, imapclient.FetchModSeq(1)))
} }
// Modseq is 2 for first condstore-aware-appended message, so also no match. // Modseq is 2 for first condstore-aware-appended message, so also no match.
tc.transactf("ok", `Uid Store 4 (Unchangedsince 1) +Flags ()`) tc.transactf("ok", `Uid Store 4 (Unchangedsince 1) +Flags ()`)
tc.xcodeArg(imapclient.CodeModified(xparseNumSet("4"))) tc.xcode(imapclient.CodeModified(xparseNumSet("4")))
if uidonly { if uidonly {
tc.transactf("ok", `Uid Store 1 (Unchangedsince 1) +Flags (label1)`) 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. // Modseq is 1 for original message.
tc.transactf("ok", `Store 1 (Unchangedsince 1) +Flags (label1)`) tc.transactf("ok", `Store 1 (Unchangedsince 1) +Flags (label1)`)
} }
tc.xcode("") // No MODIFIED. tc.xcode(nil) // No MODIFIED.
clientModseq++ clientModseq++
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{"label1"}, imapclient.FetchModSeq(clientModseq))) tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{"label1"}, imapclient.FetchModSeq(clientModseq)))
tc2.transactf("ok", "Noop") 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 // modseq change made in the first application. ../rfc/7162:823
tc.transactf("ok", `Uid Store 1,1 (Unchangedsince %d) -Flags (label1)`, clientModseq) tc.transactf("ok", `Uid Store 1,1 (Unchangedsince %d) -Flags (label1)`, clientModseq)
clientModseq++ clientModseq++
tc.xcode("") // No MODIFIED. tc.xcode(nil) // No MODIFIED.
tc.xuntagged( tc.xuntagged(
tc.untaggedFetch(1, 1, imapclient.FetchFlags(nil), imapclient.FetchModSeq(clientModseq)), 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. // 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.transactf("ok", `Store 1 (Unchangedsince %d) -Flags (label1)`, clientModseq)
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags(nil), imapclient.FetchModSeq(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.transactf("ok", "Noop")
tc2.xuntagged() tc2.xuntagged()
tc3.transactf("ok", "Noop") tc3.transactf("ok", "Noop")
@ -318,7 +318,7 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) {
} else { } else {
tc.xuntagged(imapclient.UntaggedExpunge(3), imapclient.UntaggedExpunge(3)) tc.xuntagged(imapclient.UntaggedExpunge(3), imapclient.UntaggedExpunge(3))
} }
tc.xcodeArg(imapclient.CodeHighestModSeq(clientModseq)) tc.xcode(imapclient.CodeHighestModSeq(clientModseq))
tc2.transactf("ok", "Noop") tc2.transactf("ok", "Noop")
if uidonly { if uidonly {
tc2.xuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("3:4")}) 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.transactf("ok", "Select inbox")
tc.xuntaggedOpt(false, tc.xuntaggedOpt(false,
imapclient.UntaggedExists(4), 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 { 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.transactf("ok", "Search Return (Min Max All) 1:* Modseq 8")
tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 1, All: esearchall0("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.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. // store, cannot modify expunged messages.
tc.transactf("ok", `Uid Store 3,4 (Unchangedsince %d) +Flags (label2)`, clientModseq) tc.transactf("ok", `Uid Store 3,4 (Unchangedsince %d) +Flags (label2)`, clientModseq)
tc.xuntagged() tc.xuntagged()
tc.xcode("") // Not MODIFIED. tc.xcode(nil) // Not MODIFIED.
tc.transactf("ok", `Uid Store 3,4 +Flags (label2)`) tc.transactf("ok", `Uid Store 3,4 +Flags (label2)`)
tc.xuntagged() tc.xuntagged()
tc.xcode("") // Not MODIFIED. tc.xcode(nil) // Not MODIFIED.
// Check all condstore-enabling commands (and their syntax), ../rfc/7162:368 // Check all condstore-enabling commands (and their syntax), ../rfc/7162:368
@ -497,13 +497,13 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) {
clientModseq++ clientModseq++
if qresync { if qresync {
tc.xuntaggedOpt(false, imapclient.UntaggedVanished{UIDs: xparseNumSet("2")}) tc.xuntaggedOpt(false, imapclient.UntaggedVanished{UIDs: xparseNumSet("2")})
tc.xcodeArg(imapclient.CodeHighestModSeq(clientModseq)) tc.xcode(imapclient.CodeHighestModSeq(clientModseq))
} else if uidonly { } else if uidonly {
tc.xuntaggedOpt(false, imapclient.UntaggedVanished{UIDs: xparseNumSet("2")}) tc.xuntaggedOpt(false, imapclient.UntaggedVanished{UIDs: xparseNumSet("2")})
tc.xcode("") tc.xcode(nil)
} else { } else {
tc.xuntaggedOpt(false, imapclient.UntaggedExpunge(2)) tc.xuntaggedOpt(false, imapclient.UntaggedExpunge(2))
tc.xcode("") tc.xcode(nil)
} }
tc2.transactf("ok", "Noop") tc2.transactf("ok", "Noop")
if uidonly { 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`, " ") 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 \*`, " ") permflags := strings.Split(`\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent \*`, " ")
uflags := imapclient.UntaggedFlags(flags) 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{ baseUntagged := []imapclient.Untagged{
uflags, uflags,
upermflags, upermflags,
imapclient.UntaggedList{Separator: '/', Mailbox: "Inbox"}, 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, Code: imapclient.CodeUIDNext(7), Text: "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.CodeUIDValidity(1), Text: "x"},
imapclient.UntaggedRecent(0), imapclient.UntaggedRecent(0),
imapclient.UntaggedExists(4), 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 { if !uidonly {
baseUntagged = append(baseUntagged, 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.transactf("ok", "Select inbox (Qresync (1 10 (1,3,6 1,3,6)))")
tc.xuntagged( tc.xuntagged(
makeUntagged( 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")}, imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
tc.untaggedFetch(4, 6, noflags, imapclient.FetchModSeq(clientModseq)), 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.transactf("ok", "Select inbox (Qresync (1 18 (1,3,6 1,3,6)))")
tc.xuntagged( tc.xuntagged(
makeUntagged( 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")}, imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
tc.untaggedFetch(4, 6, noflags, imapclient.FetchModSeq(clientModseq)), tc.untaggedFetch(4, 6, noflags, imapclient.FetchModSeq(clientModseq)),
)..., )...,
@ -786,7 +786,7 @@ func testQresyncHistory(t *testing.T, uidonly bool) {
defer tc.close() defer tc.close()
tc.login("mjl@mox.example", password0) 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")
tc.transactf("ok", "Append inbox {1+}\r\nx") // modseq 6 tc.transactf("ok", "Append inbox {1+}\r\nx") // modseq 6
tc.transactf("ok", "Append inbox {1+}\r\nx") 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`, " ") 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 \*`, " ") permflags := strings.Split(`\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent \*`, " ")
uflags := imapclient.UntaggedFlags(flags) 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{ baseUntagged := []imapclient.Untagged{
uflags, uflags,
upermflags, upermflags,
imapclient.UntaggedList{Separator: '/', Mailbox: "Inbox"}, 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, Code: imapclient.CodeUIDNext(4), Text: "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.CodeUIDValidity(1), Text: "x"},
imapclient.UntaggedRecent(0), imapclient.UntaggedRecent(0),
imapclient.UntaggedExists(1), 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 { makeUntagged := func(l ...imapclient.Untagged) []imapclient.Untagged {

View File

@ -44,16 +44,16 @@ func testCopy(t *testing.T, uidonly bool) {
tc.transactf("ok", "uid copy 3:* Trash") tc.transactf("ok", "uid copy 3:* Trash")
} else { } else {
tc.transactf("no", "copy 1 nonexistent") tc.transactf("no", "copy 1 nonexistent")
tc.xcode("TRYCREATE") tc.xcodeWord("TRYCREATE")
tc.transactf("no", "copy 1 expungebox") tc.transactf("no", "copy 1 expungebox")
tc.xcode("TRYCREATE") tc.xcodeWord("TRYCREATE")
tc.transactf("no", "copy 1 inbox") // Cannot copy to same mailbox. tc.transactf("no", "copy 1 inbox") // Cannot copy to same mailbox.
tc2.transactf("ok", "noop") // Drain. tc2.transactf("ok", "noop") // Drain.
tc.transactf("ok", "copy 1:* Trash") 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.transactf("ok", "noop")
tc2.xuntagged( 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("no", "uid copy 1,2 Trash") // No match.
tc.transactf("ok", "uid copy 4,3 Trash") 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.transactf("ok", "noop")
tc2.xuntagged( tc2.xuntagged(
imapclient.UntaggedExists(4), imapclient.UntaggedExists(4),
@ -81,5 +81,5 @@ func testCopy(t *testing.T, uidonly bool) {
tclimit.xuntagged(imapclient.UntaggedExists(1)) tclimit.xuntagged(imapclient.UntaggedExists(1))
// Second message would take account past limit. // Second message would take account past limit.
tclimit.transactf("no", "uid copy 1:* Trash") tclimit.transactf("no", "uid copy 1:* Trash")
tclimit.xcode("OVERQUOTA") tclimit.xcodeWord("OVERQUOTA")
} }

View File

@ -57,7 +57,7 @@ func testCreate(t *testing.T, uidonly bool) {
tc.xuntagged(imapclient.UntaggedList{Flags: []string{`\Subscribed`}, Separator: '/', Mailbox: "mailbox"}) tc.xuntagged(imapclient.UntaggedList{Flags: []string{`\Subscribed`}, Separator: '/', Mailbox: "mailbox"})
// OldName is only set for IMAP4rev2 or NOTIFY. // OldName is only set for IMAP4rev2 or NOTIFY.
tc.client.Enable("imap4rev2") tc.client.Enable(imapclient.CapIMAP4rev2)
tc.transactf("ok", "create mailbox2/") tc.transactf("ok", "create mailbox2/")
tc.xuntagged(imapclient.UntaggedList{Flags: []string{`\Subscribed`}, Separator: '/', Mailbox: "mailbox2", OldName: "mailbox2/"}) tc.xuntagged(imapclient.UntaggedList{Flags: []string{`\Subscribed`}, Separator: '/', Mailbox: "mailbox2", OldName: "mailbox2/"})

View File

@ -43,9 +43,9 @@ func testDelete(t *testing.T, uidonly bool) {
// ../rfc/9051:2000 // ../rfc/9051:2000
tc.transactf("no", "delete a") // Still has child. 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") tc.transactf("ok", "delete a/b")
tc2.transactf("ok", "noop") tc2.transactf("ok", "noop")
tc2.xuntagged() // No IMAP4rev2, no \NonExistent. tc2.xuntagged() // No IMAP4rev2, no \NonExistent.

View File

@ -24,7 +24,7 @@ func testFetch(t *testing.T, uidonly bool) {
defer tc.close() defer tc.close()
tc.login("mjl@mox.example", password0) 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") received, err := time.Parse(time.RFC3339, "2022-11-16T10:01:00+01:00")
tc.check(err, "parse time") tc.check(err, "parse time")
tc.client.Append("inbox", makeAppendTime(exampleMsg, received)) tc.client.Append("inbox", makeAppendTime(exampleMsg, received))
@ -58,7 +58,13 @@ func testFetch(t *testing.T, uidonly bool) {
} }
bodystructure1 := bodyxstructure1 bodystructure1 := bodyxstructure1
bodystructure1.RespAttr = "BODYSTRUCTURE" 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 bodystructure1.Body = bodystructbody1
split := strings.SplitN(exampleMsg, "\r\n\r\n", 2) 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.transactf("ok", "fetch 1 binary[1]")
tc.xuntagged(tc.untaggedFetch(1, 1, binarypart1)) // Seen flag not changed. 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.transactf("ok", "uid fetch 1 binary[]<1.1>")
tc.xuntagged( tc.xuntagged(
tc.untaggedFetch(1, 1, binarypartial1, noflags), tc.untaggedFetch(1, 1, binarypartial1, noflags),
tc.untaggedFetch(1, 1, flagsSeen), // For UID FETCH, we get the flags during the command. 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.transactf("ok", "fetch 1 binary[1]<1.1>")
tc.xuntagged(tc.untaggedFetch(1, 1, binarypartpartial1, noflags)) tc.xuntagged(tc.untaggedFetch(1, 1, binarypartpartial1, noflags))
tc.transactf("ok", "noop") tc.transactf("ok", "noop")
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen)) 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.transactf("ok", "fetch 1 binary[]<10000.10001>")
tc.xuntagged(tc.untaggedFetch(1, 1, binaryend1, noflags)) tc.xuntagged(tc.untaggedFetch(1, 1, binaryend1, noflags))
tc.transactf("ok", "noop") tc.transactf("ok", "noop")
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen)) 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.transactf("ok", "fetch 1 binary[1]<10000.10001>")
tc.xuntagged(tc.untaggedFetch(1, 1, binarypartend1, noflags)) tc.xuntagged(tc.untaggedFetch(1, 1, binarypartend1, noflags))
tc.transactf("ok", "noop") tc.transactf("ok", "noop")
@ -146,7 +152,7 @@ func testFetch(t *testing.T, uidonly bool) {
tc.transactf("ok", "fetch 1 binary.size[1]") tc.transactf("ok", "fetch 1 binary.size[1]")
tc.xuntagged(tc.untaggedFetch(1, 1, binarysizepart1)) 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.transactf("ok", "fetch 1 body[]")
tc.xuntagged(tc.untaggedFetch(1, 1, body1, noflags)) tc.xuntagged(tc.untaggedFetch(1, 1, body1, noflags))
tc.transactf("ok", "noop") tc.transactf("ok", "noop")
@ -156,31 +162,31 @@ func testFetch(t *testing.T, uidonly bool) {
tc.transactf("ok", "noop") tc.transactf("ok", "noop")
tc.xuntagged() // Already seen. tc.xuntagged() // Already seen.
tc.client.StoreFlagsClear("1", true, `\Seen`) tc.client.MSNStoreFlagsClear("1", true, `\Seen`)
tc.transactf("ok", "fetch 1 body[1]") tc.transactf("ok", "fetch 1 body[1]")
tc.xuntagged(tc.untaggedFetch(1, 1, bodypart1, noflags)) tc.xuntagged(tc.untaggedFetch(1, 1, bodypart1, noflags))
tc.transactf("ok", "noop") tc.transactf("ok", "noop")
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen)) 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.transactf("ok", "fetch 1 body[1]<1.2>")
tc.xuntagged(tc.untaggedFetch(1, 1, body1off1, noflags)) tc.xuntagged(tc.untaggedFetch(1, 1, body1off1, noflags))
tc.transactf("ok", "noop") tc.transactf("ok", "noop")
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen)) 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.transactf("ok", "fetch 1 body[1]<100000.100000>")
tc.xuntagged(tc.untaggedFetch(1, 1, bodyend1, noflags)) tc.xuntagged(tc.untaggedFetch(1, 1, bodyend1, noflags))
tc.transactf("ok", "noop") tc.transactf("ok", "noop")
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen)) 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.transactf("ok", "fetch 1 body[header]")
tc.xuntagged(tc.untaggedFetch(1, 1, bodyheader1, noflags)) tc.xuntagged(tc.untaggedFetch(1, 1, bodyheader1, noflags))
tc.transactf("ok", "noop") tc.transactf("ok", "noop")
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen)) 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.transactf("ok", "fetch 1 body[text]")
tc.xuntagged(tc.untaggedFetch(1, 1, bodytext1, noflags)) tc.xuntagged(tc.untaggedFetch(1, 1, bodytext1, noflags))
tc.transactf("ok", "noop") tc.transactf("ok", "noop")
@ -191,21 +197,21 @@ func testFetch(t *testing.T, uidonly bool) {
tc.xuntagged(tc.untaggedFetch(1, 1, rfcheader1)) tc.xuntagged(tc.untaggedFetch(1, 1, rfcheader1))
// equivalent to body[text], ../rfc/3501:3199 // 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.transactf("ok", "fetch 1 rfc822.text")
tc.xuntagged(tc.untaggedFetch(1, 1, rfctext1, noflags)) tc.xuntagged(tc.untaggedFetch(1, 1, rfctext1, noflags))
tc.transactf("ok", "noop") tc.transactf("ok", "noop")
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen)) tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
// equivalent to body[], ../rfc/3501:3179 // 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.transactf("ok", "fetch 1 rfc822")
tc.xuntagged(tc.untaggedFetch(1, 1, rfc1, noflags)) tc.xuntagged(tc.untaggedFetch(1, 1, rfc1, noflags))
tc.transactf("ok", "noop") tc.transactf("ok", "noop")
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen)) tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
// With PEEK, we should not get the \Seen flag. // 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.transactf("ok", "fetch 1 body.peek[]")
tc.xuntagged(tc.untaggedFetch(1, 1, body1)) tc.xuntagged(tc.untaggedFetch(1, 1, body1))
@ -229,7 +235,7 @@ func testFetch(t *testing.T, uidonly bool) {
// Missing sequence number. ../rfc/9051:7018 // Missing sequence number. ../rfc/9051:7018
tc.transactf("bad", "fetch 2 body[]") 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.transactf("ok", "fetch 1:1 body[]")
tc.xuntagged(tc.untaggedFetch(1, 1, body1, noflags)) tc.xuntagged(tc.untaggedFetch(1, 1, body1, noflags))
tc.transactf("ok", "noop") tc.transactf("ok", "noop")
@ -300,17 +306,28 @@ func testFetch(t *testing.T, uidonly bool) {
RespAttr: "BODYSTRUCTURE", RespAttr: "BODYSTRUCTURE",
Body: imapclient.BodyTypeMpart{ Body: imapclient.BodyTypeMpart{
Bodies: []any{ Bodies: []any{
imapclient.BodyTypeBasic{BodyFields: imapclient.BodyFields{Octets: 275}, 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: &imapclient.BodyExtension1Part{}}, imapclient.BodyTypeText{MediaType: "TEXT", MediaSubtype: "PLAIN", BodyFields: imapclient.BodyFields{Params: [][2]string{{"CHARSET", "US-ASCII"}}, Octets: 114}, Lines: 3, Ext: &bodyext1},
imapclient.BodyTypeMpart{ imapclient.BodyTypeMpart{
Bodies: []any{ Bodies: []any{
imapclient.BodyTypeBasic{MediaType: "AUDIO", MediaSubtype: "BASIC", BodyFields: imapclient.BodyFields{CTE: "BASE64", Octets: 22}, Ext: &imapclient.BodyExtension1Part{}}, 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: "inline", DispositionParams: [][2]string{{"filename", "image.jpg"}}}}, 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", 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{ imapclient.BodyTypeMsg{
MediaType: "MESSAGE", MediaType: "MESSAGE",
MediaSubtype: "RFC822", MediaSubtype: "RFC822",
@ -323,17 +340,25 @@ func testFetch(t *testing.T, uidonly bool) {
To: []imapclient.Address{{Name: "mox", Adl: "", Mailbox: "info", Host: "mox.example"}}, To: []imapclient.Address{{Name: "mox", Adl: "", Mailbox: "info", Host: "mox.example"}},
}, },
Bodystructure: imapclient.BodyTypeText{ 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, Lines: 7,
Ext: &imapclient.BodyExtension1Part{ Ext: &imapclient.BodyExtension1Part{
MD5: "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=", MD5: ptr("MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY="),
Language: []string{"en", "de"}, Disposition: ptr((*string)(nil)),
Location: "http://localhost", DispositionParams: ptr([][2]string(nil)),
Language: ptr([]string{"en", "de"}),
Location: ptr(ptr("http://localhost")),
}, },
}, },
}, },
MediaSubtype: "MIXED", 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)) tc.client.Append("inbox", makeAppendTime(nestedMessage, received))

View File

@ -134,11 +134,11 @@ func FuzzServer(f *testing.F) {
client, _ := imapclient.New(clientConn, &opts) client, _ := imapclient.New(clientConn, &opts)
for _, cmd := range cmds { for _, cmd := range cmds {
client.Commandf("", "%s", cmd) client.WriteCommandf("", "%s", cmd)
client.Response() client.ReadResponse()
} }
client.Commandf("", "%s", s) client.WriteCommandf("", "%s", s)
client.Response() client.ReadResponse()
}() }()
err = serverConn.SetDeadline(time.Now().Add(time.Second)) err = serverConn.SetDeadline(time.Now().Add(time.Second))

View File

@ -303,13 +303,13 @@ func (c *conn) xcheckMetadataSize(tx *bstore.Tx) {
n++ n++
if n > metadataMaxKeys { if n > metadataMaxKeys {
// ../rfc/5464:590 // ../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) size += len(a.Key) + len(a.Value)
if size > metadataMaxSize { if size > metadataMaxSize {
// ../rfc/5464:585 We only have a max total size limit, not per entry. We'll // ../rfc/5464:585 We only have a max total size limit, not per entry. We'll
// mention the max total size. // 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 return nil
}) })

View File

@ -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("ok", `delete metabox`) // Delete mailbox with live and expunged metadata.
tc.transactf("no", `setmetadata expungebox (/private/comment "mailbox value")`) tc.transactf("no", `setmetadata expungebox (/private/comment "mailbox value")`)
tc.xcode("TRYCREATE") tc.xcodeWord("TRYCREATE")
tc.transactf("ok", `getmetadata "" ("/private/comment")`) tc.transactf("ok", `getmetadata "" ("/private/comment")`)
tc.xuntagged(imapclient.UntaggedMetadataAnnotations{ 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. // Request with a maximum size, we don't get anything larger.
tc.transactf("ok", `setmetadata inbox (/private/another "longer")`) tc.transactf("ok", `setmetadata inbox (/private/another "longer")`)
tc.transactf("ok", `getmetadata (maxsize 4) inbox (/private/comment /private/another)`) 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{ tc.xuntagged(imapclient.UntaggedMetadataAnnotations{
Mailbox: "Inbox", Mailbox: "Inbox",
Annotations: []imapclient.Annotation{ Annotations: []imapclient.Annotation{
@ -230,7 +230,7 @@ func testMetadata(t *testing.T, uidonly bool) {
} }
// Broadcast should happen when metadata capability is enabled. // Broadcast should happen when metadata capability is enabled.
tc2.client.Enable(string(imapclient.CapMetadata)) tc2.client.Enable(imapclient.CapMetadata)
tc2.cmdf("", "idle") tc2.cmdf("", "idle")
tc2.readprefixline("+ ") tc2.readprefixline("+ ")
done = make(chan error) done = make(chan error)
@ -285,12 +285,12 @@ func TestMetadataLimit(t *testing.T) {
tc.client.Write(buf) tc.client.Write(buf)
tc.client.Writelinef(")") tc.client.Writelinef(")")
tc.response("no") 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. // Reach limit for max number.
for i := 1; i <= metadataMaxKeys; i++ { for i := 1; i <= metadataMaxKeys; i++ {
tc.transactf("ok", `setmetadata inbox (/private/key%d "test")`, i) tc.transactf("ok", `setmetadata inbox (/private/key%d "test")`, i)
} }
tc.transactf("no", `setmetadata inbox (/private/toomany "test")`) tc.transactf("no", `setmetadata inbox (/private/toomany "test")`)
tc.xcodeArg(imapclient.CodeOther{Code: "METADATA", Args: []string{"TOOMANY"}}) tc.xcode(imapclient.CodeMetadataTooMany{})
} }

View File

@ -56,10 +56,10 @@ func testMove(t *testing.T, uidonly bool) {
tc.client.Select("inbox") tc.client.Select("inbox")
tc.transactf("no", "move 1 nonexistent") tc.transactf("no", "move 1 nonexistent")
tc.xcode("TRYCREATE") tc.xcodeWord("TRYCREATE")
tc.transactf("no", "move 1 expungebox") tc.transactf("no", "move 1 expungebox")
tc.xcode("TRYCREATE") tc.xcodeWord("TRYCREATE")
tc.transactf("no", "move 1 inbox") // Cannot move to same mailbox. 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.transactf("ok", "move 1:* Trash")
tc.xuntagged( 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),
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") tc.transactf("ok", "uid move 6:5 Trash")
if uidonly { if uidonly {
tc.xuntagged( 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")}, imapclient.UntaggedVanished{UIDs: xparseNumSet("5:6")},
) )
} else { } else {
tc.xuntagged( 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),
imapclient.UntaggedExpunge(1), imapclient.UntaggedExpunge(1),
) )

View File

@ -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 (flagChange))") // flagChange must come with MessageNew and MessageExpunge.
tc.transactf("bad", "Notify Set Status (Selected (mailboxName)) (Selected-Delayed (mailboxName))") // Duplicate selected. 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.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.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) tc2 := startNoSwitchboard(t, uidonly)
defer tc2.closeNoWait() 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. // 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.transactf("ok", "Notify Set Status (Selected (messageNew (Uid Modseq Bodystructure Preview) messageExpunge flagChange)) (personal (messageNew messageExpunge flagChange mailboxName subscriptionChange mailboxMetadataChange serverMetadataChange))")
tc.xuntagged( 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. // 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: "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}}, 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, Octets: 21,
}, },
Lines: 1, 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{ imapclient.BodyTypeText{
MediaType: "TEXT", MediaType: "TEXT",
@ -118,12 +123,21 @@ func testNotify(t *testing.T, uidonly bool) {
Octets: 15, Octets: 15,
}, },
Lines: 1, 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", MediaSubtype: "ALTERNATIVE",
Ext: &imapclient.BodyExtensionMpart{ 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++ // modseq++
tc.readuntagged( tc.readuntagged(
imapclient.UntaggedExists(3), imapclient.UntaggedExists(3),
imapclient.UntaggedResult{ imapclient.UntaggedResult{Status: "NO", Text: "generating notify fetch response: requested part does not exist"},
Status: "NO",
RespText: imapclient.RespText{
More: "generating notify fetch response: requested part does not exist",
},
},
tc.untaggedFetchUID(3, 4), tc.untaggedFetchUID(3, 4),
) )
@ -457,15 +466,7 @@ func testNotifyOverflow(t *testing.T, uidonly bool) {
tc2.client.Append("inbox", makeAppend(searchMsg)) tc2.client.Append("inbox", makeAppend(searchMsg))
tc.transactf("ok", "noop") tc.transactf("ok", "noop")
tc.xuntagged( tc.xuntagged(imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeWord("NOTIFICATIONOVERFLOW"), Text: "out of sync after too many pending changes"})
imapclient.UntaggedResult{
Status: "OK",
RespText: imapclient.RespText{
Code: "NOTIFICATIONOVERFLOW",
More: "out of sync after too many pending changes",
},
},
)
// Won't be getting any more notifications until we enable them again with NOTIFY. // Won't be getting any more notifications until we enable them again with NOTIFY.
tc2.client.Append("inbox", makeAppend(searchMsg)) 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.UIDStoreFlagsAdd("1", true, `\Seen`)
tc2.client.UIDStoreFlagsClear("1", true, `\Seen`) tc2.client.UIDStoreFlagsClear("1", true, `\Seen`)
tc.transactf("ok", "noop") tc.transactf("ok", "noop")
tc.xuntagged( tc.xuntagged(imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeWord("NOTIFICATIONOVERFLOW"), Text: "out of sync after too many pending changes for selected mailbox"})
imapclient.UntaggedResult{
Status: "OK",
RespText: imapclient.RespText{
Code: "NOTIFICATIONOVERFLOW",
More: "out of sync after too many pending changes for selected mailbox",
},
},
)
// Again, no new notifications until we select and enable again. // Again, no new notifications until we select and enable again.
tc2.client.UIDStoreFlagsAdd("1", true, `\Seen`) tc2.client.UIDStoreFlagsAdd("1", true, `\Seen`)

View File

@ -30,11 +30,11 @@ func testRename(t *testing.T, uidonly bool) {
tc.transactf("bad", "rename x y ") // Leftover data. tc.transactf("bad", "rename x y ") // Leftover data.
tc.transactf("no", "rename doesnotexist newbox") // Does not exist. 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.transactf("no", "rename expungebox newbox") // No longer exists.
tc.xcode("NONEXISTENT") tc.xcodeWord("NONEXISTENT")
tc.transactf("no", `rename "Sent" "Trash"`) // Already exists. tc.transactf("no", `rename "Sent" "Trash"`) // Already exists.
tc.xcode("ALREADYEXISTS") tc.xcodeWord("ALREADYEXISTS")
tc.client.Create("x", nil) tc.client.Create("x", nil)
tc.client.Subscribe("sub") tc.client.Subscribe("sub")
@ -47,7 +47,7 @@ func testRename(t *testing.T, uidonly bool) {
tc2.xuntagged(imapclient.UntaggedList{Separator: '/', Mailbox: "z"}) tc2.xuntagged(imapclient.UntaggedList{Separator: '/', Mailbox: "z"})
// OldName is only set for IMAP4rev2 or NOTIFY. // OldName is only set for IMAP4rev2 or NOTIFY.
tc2.client.Enable("IMAP4rev2") tc2.client.Enable(imapclient.CapIMAP4rev2)
tc.transactf("ok", "rename z y") tc.transactf("ok", "rename z y")
tc2.transactf("ok", "noop") tc2.transactf("ok", "noop")
tc2.xuntagged(imapclient.UntaggedList{Separator: '/', Mailbox: "y", OldName: "z"}) 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. // Cannot rename a child to a parent. It already exists.
tc.transactf("no", "rename a/b/c a/b") tc.transactf("no", "rename a/b/c a/b")
tc.xcode("ALREADYEXISTS") tc.xcodeWord("ALREADYEXISTS")
tc.transactf("no", "rename a/b a") tc.transactf("no", "rename a/b a")
tc.xcode("ALREADYEXISTS") tc.xcodeWord("ALREADYEXISTS")
tc2.transactf("ok", "noop") // Drain. 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. 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.

View File

@ -33,37 +33,37 @@ func testReplace(t *testing.T, uidonly bool) {
} }
// Append 3 messages, remove first. Leaves msgseq 1,2 with uid 2,3. // 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.UIDStoreFlagsSet("1", true, `\deleted`)
tc.client.Expunge() tc.client.Expunge()
tc.transactf("no", "uid replace 1 expungebox {1}") // Mailbox no longer exists. 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.login("mjl@mox.example", password0)
tc2.client.Select("inbox") tc2.client.Select("inbox")
// Replace last message (msgseq 2, uid 3) in same mailbox. // Replace last message (msgseq 2, uid 3) in same mailbox.
if uidonly { 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 { } 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") tcheck(tc.t, tc.lastErr, "read imap response")
if uidonly { if uidonly {
tc.xuntagged( 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.UntaggedExists(3),
imapclient.UntaggedVanished{UIDs: xparseNumSet("3")}, imapclient.UntaggedVanished{UIDs: xparseNumSet("3")},
) )
} else { } else {
tc.xuntagged( 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.UntaggedExists(3),
imapclient.UntaggedExpunge(2), imapclient.UntaggedExpunge(2),
) )
} }
tc.xcodeArg(imapclient.CodeHighestModSeq(8)) tc.xcode(imapclient.CodeHighestModSeq(8))
// Check that other client sees Exists and Expunge. // Check that other client sees Exists and Expunge.
tc2.transactf("ok", "noop") 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. // Enable qresync, replace uid 2 (msgseq 1) to different mailbox, see that we get vanished instead of expunged.
tc.transactf("ok", "enable qresync") 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") tcheck(tc.t, tc.lastErr, "read imap response")
tc.xuntagged( 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.UntaggedExists(3),
imapclient.UntaggedVanished{UIDs: xparseNumSet("2")}, imapclient.UntaggedVanished{UIDs: xparseNumSet("2")},
) )
tc.xcodeArg(imapclient.CodeHighestModSeq(9)) tc.xcode(imapclient.CodeHighestModSeq(9))
// Use "*" for replacing. // Use "*" for replacing.
tc.transactf("ok", "uid replace * inbox {1+}\r\nx") tc.transactf("ok", "uid replace * inbox {1+}\r\nx")
tc.xuntagged( 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.UntaggedExists(3),
imapclient.UntaggedVanished{UIDs: xparseNumSet("5")}, imapclient.UntaggedVanished{UIDs: xparseNumSet("5")},
) )
if !uidonly { if !uidonly {
tc.transactf("ok", "replace * inbox {1+}\r\ny") tc.transactf("ok", "replace * inbox {1+}\r\ny")
tc.xuntagged( 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.UntaggedExists(3),
imapclient.UntaggedVanished{UIDs: xparseNumSet("6")}, 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. // Adding a message >1mb with non-sync literal to non-existent mailbox should abort entire connection.
tc.transactf("bad", "replace 12345 inbox {2000000+}") tc.transactf("bad", "replace 12345 inbox {2000000+}")
tc.xuntagged( 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) { func TestReplaceQuota(t *testing.T) {
@ -153,11 +153,11 @@ func testReplaceQuota(t *testing.T, uidonly bool) {
// Synchronizing literal, we get failure immediately. // Synchronizing literal, we get failure immediately.
tc.transactf("no", "uid replace 1 inbox {6}\r\n") 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. // Synchronizing literal to non-existent mailbox, we get failure immediately.
tc.transactf("no", "uid replace 1 badbox {6}\r\n") tc.transactf("no", "uid replace 1 badbox {6}\r\n")
tc.xcode("TRYCREATE") tc.xcodeWord("TRYCREATE")
buf := make([]byte, 4000, 4002) buf := make([]byte, 4000, 4002)
for i := range buf { for i := range buf {
@ -166,18 +166,18 @@ func testReplaceQuota(t *testing.T, uidonly bool) {
buf = append(buf, "\r\n"...) buf = append(buf, "\r\n"...)
// Non-synchronizing literal. We get to write our data. // 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) _, err := tc.client.Write(buf)
tc.check(err, "write replace message") tc.check(err, "write replace message")
tc.response("no") tc.response("no")
tc.xcode("OVERQUOTA") tc.xcodeWord("OVERQUOTA")
// Non-synchronizing literal to bad mailbox. // 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) _, err = tc.client.Write(buf)
tc.check(err, "write replace message") tc.check(err, "write replace message")
tc.response("no") tc.response("no")
tc.xcode("TRYCREATE") tc.xcodeWord("TRYCREATE")
} }
func TestReplaceExpunged(t *testing.T) { func TestReplaceExpunged(t *testing.T) {
@ -197,7 +197,7 @@ func testReplaceExpunged(t *testing.T, uidonly bool) {
tc.client.Append("inbox", makeAppend(exampleMsg)) tc.client.Append("inbox", makeAppend(exampleMsg))
// We start the command, but don't write data yet. // 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. // Get in with second client and remove the message we are replacing.
tc2 := startNoSwitchboard(t, uidonly) tc2 := startNoSwitchboard(t, uidonly)

View File

@ -57,7 +57,7 @@ func (tc *testconn) xsearchmodseq(modseq int64, nums ...uint32) {
func (tc *testconn) xesearch(exp imapclient.UntaggedEsearch) { func (tc *testconn) xesearch(exp imapclient.UntaggedEsearch) {
tc.t.Helper() tc.t.Helper()
exp.Tag = tc.client.LastTag exp.Tag = tc.client.LastTag()
tc.xuntagged(exp) tc.xuntagged(exp)
} }
@ -298,11 +298,8 @@ func testSearch(t *testing.T, uidonly bool) {
inprogress := func(cur, goal uint32) imapclient.UntaggedResult { inprogress := func(cur, goal uint32) imapclient.UntaggedResult {
return imapclient.UntaggedResult{ return imapclient.UntaggedResult{
Status: "OK", Status: "OK",
RespText: imapclient.RespText{ Code: imapclient.CodeInProgress{Tag: "tag1", Current: &cur, Goal: &goal},
Code: "INPROGRESS", Text: "still searching",
CodeArg: imapclient.CodeInProgress{Tag: "tag1", Current: &cur, Goal: &goal},
More: "still searching",
},
} }
} }
tc.xuntagged( 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. // 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 { if !uidonly {
tc.transactf("ok", `search undraft`) tc.transactf("ok", `search undraft`)
@ -566,11 +563,8 @@ func testSearchMulti(t *testing.T, selected, uidonly bool) {
inprogress := func(cur, goal uint32) imapclient.UntaggedResult { inprogress := func(cur, goal uint32) imapclient.UntaggedResult {
return imapclient.UntaggedResult{ return imapclient.UntaggedResult{
Status: "OK", Status: "OK",
RespText: imapclient.RespText{ Code: imapclient.CodeInProgress{Tag: "Tag1", Current: &cur, Goal: &goal},
Code: "INPROGRESS", Text: "still searching",
CodeArg: imapclient.CodeInProgress{Tag: "Tag1", Current: &cur, Goal: &goal},
More: "still searching",
},
} }
} }
tc.cmdf("Tag1", `Esearch In (Personal) Return () All`) tc.cmdf("Tag1", `Esearch In (Personal) Return () All`)

View File

@ -38,19 +38,19 @@ func testSelectExamine(t *testing.T, examine, uidonly bool) {
okcode = "READ-ONLY" 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`, " ") 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 \*`, " ") permflags := strings.Split(`\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent \*`, " ")
uflags := imapclient.UntaggedFlags(flags) 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) urecent := imapclient.UntaggedRecent(0)
uexists0 := imapclient.UntaggedExists(0) uexists0 := imapclient.UntaggedExists(0)
uexists1 := imapclient.UntaggedExists(1) uexists1 := imapclient.UntaggedExists(1)
uuidval1 := imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UIDVALIDITY", CodeArg: imapclient.CodeUint{Code: "UIDVALIDITY", Num: 1}, More: "x"}} uuidval1 := imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeUIDValidity(1), Text: "x"}
uuidnext1 := imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UIDNEXT", CodeArg: imapclient.CodeUint{Code: "UIDNEXT", Num: 1}, More: "x"}} uuidnext1 := imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeUIDNext(1), Text: "x"}
ulist := imapclient.UntaggedList{Separator: '/', Mailbox: "Inbox"} 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"}} uunseen := imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeUnseen(1), Text: "x"}
uuidnext2 := imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UIDNEXT", CodeArg: imapclient.CodeUint{Code: "UIDNEXT", Num: 2}, More: "x"}} uuidnext2 := imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeUIDNext(2), Text: "x"}
// Parameter required. // Parameter required.
tc.transactf("bad", "%s", cmd) tc.transactf("bad", "%s", cmd)
@ -61,11 +61,11 @@ func testSelectExamine(t *testing.T, examine, uidonly bool) {
tc.transactf("ok", "%s inbox", cmd) tc.transactf("ok", "%s inbox", cmd)
tc.xuntagged(uflags, upermflags, urecent, uexists0, uuidval1, uuidnext1, ulist) tc.xuntagged(uflags, upermflags, urecent, uexists0, uuidval1, uuidnext1, ulist)
tc.xcode(okcode) tc.xcodeWord(okcode)
tc.transactf("ok", `%s "inbox"`, cmd) tc.transactf("ok", `%s "inbox"`, cmd)
tc.xuntagged(uclosed, uflags, upermflags, urecent, uexists0, uuidval1, uuidnext1, ulist) 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. // Append a message. It will be reported as UNSEEN.
tc.client.Append("inbox", makeAppend(exampleMsg)) tc.client.Append("inbox", makeAppend(exampleMsg))
@ -75,11 +75,11 @@ func testSelectExamine(t *testing.T, examine, uidonly bool) {
} else { } else {
tc.xuntagged(uclosed, uflags, upermflags, urecent, uunseen, uexists1, uuidval1, uuidnext2, ulist) 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. // 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.transactf("ok", "%s inbox", cmd)
tc.xuntagged(uclosed, uflags, upermflags, uexists1, uuidval1, uuidnext2, ulist) tc.xuntagged(uclosed, uflags, upermflags, uexists1, uuidval1, uuidnext2, ulist)
tc.xcode(okcode) tc.xcodeWord(okcode)
} }

View File

@ -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() { func mockUIDValidity() func() {
orig := store.InitialUIDValidity orig := store.InitialUIDValidity
store.InitialUIDValidity = func() uint32 { store.InitialUIDValidity = func() uint32 {
@ -182,8 +198,7 @@ type testconn struct {
switchStop func() switchStop func()
// Result of last command. // Result of last command.
lastUntagged []imapclient.Untagged lastResponse imapclient.Response
lastResult imapclient.Result
lastErr error 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) { func (tc *testconn) last(resp imapclient.Response, err error) {
tc.lastUntagged = l tc.lastResponse = resp
tc.lastResult = r
tc.lastErr = err tc.lastErr = err
} }
func (tc *testconn) xcode(s string) { func (tc *testconn) xcode(c imapclient.Code) {
tc.t.Helper() tc.t.Helper()
if tc.lastResult.Code != s { if !reflect.DeepEqual(tc.lastResponse.Code, c) {
tc.t.Fatalf("got last code %q, expected %q", tc.lastResult.Code, s) 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() tc.t.Helper()
if !reflect.DeepEqual(tc.lastResult.CodeArg, v) { tc.xcode(imapclient.CodeWord(s))
tc.t.Fatalf("got last code argument %v, expected %v", tc.lastResult.CodeArg, v)
}
} }
func (tc *testconn) xuntagged(exps ...imapclient.Untagged) { 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) { func (tc *testconn) xuntaggedOpt(all bool, exps ...imapclient.Untagged) {
tc.t.Helper() tc.t.Helper()
last := slices.Clone(tc.lastUntagged) last := slices.Clone(tc.lastResponse.Untagged)
var mismatch any var mismatch any
next: next:
for ei, exp := range exps { 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) tc.t.Fatalf("untagged data mismatch, got:\n\t%T %#v\nexpected:\n\t%T %#v", mismatch, mismatch, exp, exp)
} }
var next string var next string
if len(tc.lastUntagged) > 0 { if len(tc.lastResponse.Untagged) > 0 {
next = fmt.Sprintf(", next:\n%#v", tc.lastUntagged[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 { if len(last) > 0 && all {
tc.t.Fatalf("leftover untagged responses %v", last) 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() { func (tc *testconn) xnountagged() {
tc.t.Helper() tc.t.Helper()
if len(tc.lastUntagged) != 0 { if len(tc.lastResponse.Untagged) != 0 {
tc.t.Fatalf("got %v untagged, expected 0", tc.lastUntagged) 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) { func (tc *testconn) response(status string) {
tc.t.Helper() tc.t.Helper()
tc.lastUntagged, tc.lastResult, tc.lastErr = tc.client.Response() 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") 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) }
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) { func (tc *testconn) cmdf(tag, format string, args ...any) {
tc.t.Helper() tc.t.Helper()
err := tc.client.Commandf(tag, format, args...) err := tc.client.WriteCommandf(tag, format, args...)
tcheck(tc.t, err, "writing imap command") tcheck(tc.t, err, "writing imap command")
} }
@ -658,15 +678,15 @@ func TestLiterals(t *testing.T) {
from := "ntmpbox" from := "ntmpbox"
to := "tmpbox" to := "tmpbox"
tc.client.LastTagSet("xtag")
fmt.Fprint(tc.client, "xtag rename ") fmt.Fprint(tc.client, "xtag rename ")
tc.client.WriteSyncLiteral(from) tc.client.WriteSyncLiteral(from)
fmt.Fprint(tc.client, " ") fmt.Fprint(tc.client, " ")
tc.client.WriteSyncLiteral(to) tc.client.WriteSyncLiteral(to)
fmt.Fprint(tc.client, "\r\n") fmt.Fprint(tc.client, "\r\n")
tc.client.LastTag = "xtag" tc.lastResponse, tc.lastErr = tc.client.ReadResponse()
tc.last(tc.client.Response()) if tc.lastResponse.Status != "OK" {
if tc.lastResult.Status != "OK" { tc.t.Fatalf(`got %q, expected "OK"`, tc.lastResponse.Status)
tc.t.Fatalf(`got %q, expected "OK"`, tc.lastResult.Status)
} }
} }
@ -937,7 +957,7 @@ func TestReference(t *testing.T) {
tc2.login("mjl@mox.example", password0) tc2.login("mjl@mox.example", password0)
tc2.client.Select("inbox") tc2.client.Select("inbox")
tc.client.StoreFlagsSet("1", true, `\Deleted`) tc.client.MSNStoreFlagsSet("1", true, `\Deleted`)
tc.client.Expunge() tc.client.Expunge()
tc3 := startNoSwitchboard(t, false) tc3 := startNoSwitchboard(t, false)
@ -945,7 +965,7 @@ func TestReference(t *testing.T) {
tc3.login("mjl@mox.example", password0) tc3.login("mjl@mox.example", password0)
tc3.transactf("ok", `list "" "inbox" return (status (messages))`) tc3.transactf("ok", `list "" "inbox" return (status (messages))`)
tc3.xuntagged( tc3.xuntagged(
imapclient.UntaggedList{Separator: '/', Mailbox: "Inbox"}, mustParseUntagged(`* LIST () "/" Inbox`),
imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 0}}, imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 0}},
) )

View File

@ -8,7 +8,7 @@ import (
func TestStarttls(t *testing.T) { func TestStarttls(t *testing.T) {
tc := start(t, false) 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.transactf("bad", "starttls") // TLS already active.
tc.login("mjl@mox.example", password0) tc.login("mjl@mox.example", password0)
tc.close() tc.close()
@ -19,10 +19,10 @@ func TestStarttls(t *testing.T) {
tc = startArgs(t, false, true, false, false, true, "mjl") tc = startArgs(t, false, true, false, false, true, "mjl")
tc.transactf("no", `login "mjl@mox.example" "%s"`, password0) 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.transactf("no", "authenticate PLAIN %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000"+password0)))
tc.xcode("PRIVACYREQUIRED") tc.xcodeWord("PRIVACYREQUIRED")
tc.client.Starttls(&tls.Config{InsecureSkipVerify: true}) tc.client.StartTLS(&tls.Config{InsecureSkipVerify: true})
tc.login("mjl@mox.example", password0) tc.login("mjl@mox.example", password0)
tc.close() tc.close()
} }

View File

@ -20,7 +20,7 @@ func testStore(t *testing.T, uidonly bool) {
defer tc.close() defer tc.close()
tc.login("mjl@mox.example", password0) tc.login("mjl@mox.example", password0)
tc.client.Enable("imap4rev2") tc.client.Enable(imapclient.CapIMAP4rev2)
tc.client.Append("inbox", makeAppend(exampleMsg)) tc.client.Append("inbox", makeAppend(exampleMsg))
tc.client.Select("inbox") tc.client.Select("inbox")

View File

@ -11,29 +11,29 @@ func TestUIDOnly(t *testing.T) {
tc.client.Select("inbox") tc.client.Select("inbox")
tc.transactf("bad", "Fetch 1") tc.transactf("bad", "Fetch 1")
tc.xcode("UIDREQUIRED") tc.xcodeWord("UIDREQUIRED")
tc.transactf("bad", "Fetch 1") tc.transactf("bad", "Fetch 1")
tc.xcode("UIDREQUIRED") tc.xcodeWord("UIDREQUIRED")
tc.transactf("bad", "Search 1") tc.transactf("bad", "Search 1")
tc.xcode("UIDREQUIRED") tc.xcodeWord("UIDREQUIRED")
tc.transactf("bad", "Store 1 Flags ()") tc.transactf("bad", "Store 1 Flags ()")
tc.xcode("UIDREQUIRED") tc.xcodeWord("UIDREQUIRED")
tc.transactf("bad", "Copy 1 Archive") tc.transactf("bad", "Copy 1 Archive")
tc.xcode("UIDREQUIRED") tc.xcodeWord("UIDREQUIRED")
tc.transactf("bad", "Move 1 Archive") tc.transactf("bad", "Move 1 Archive")
tc.xcode("UIDREQUIRED") tc.xcodeWord("UIDREQUIRED")
// Sequence numbers in search program. // Sequence numbers in search program.
tc.transactf("bad", "Uid Search 1") tc.transactf("bad", "Uid Search 1")
tc.xcode("UIDREQUIRED") tc.xcodeWord("UIDREQUIRED")
// Sequence number in last qresync parameter. // Sequence number in last qresync parameter.
tc.transactf("ok", "Enable Qresync") tc.transactf("ok", "Enable Qresync")
tc.transactf("bad", "Select inbox (Qresync (1 5 (1,3,6 1,3,6)))") 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. tc.client.Select("inbox") // Select again.
// Breaks connection. // Breaks connection.
tc.transactf("bad", "replace 1 inbox {1+}\r\nx") tc.transactf("bad", "replace 1 inbox {1+}\r\nx")
tc.xcode("UIDREQUIRED") tc.xcodeWord("UIDREQUIRED")
} }

View File

@ -10,10 +10,10 @@ func TestUnsubscribe(t *testing.T) {
tc := start(t, false) tc := start(t, false)
defer tc.close() defer tc.close()
tc.login("mjl@mox.example", password0)
tc2 := startNoSwitchboard(t, false) tc2 := startNoSwitchboard(t, false)
defer tc2.closeNoWait() defer tc2.closeNoWait()
tc.login("mjl@mox.example", password0)
tc2.login("mjl@mox.example", password0) tc2.login("mjl@mox.example", password0)
tc.transactf("bad", "unsubscribe") // Missing param. tc.transactf("bad", "unsubscribe") // Missing param.

View File

@ -70,16 +70,16 @@ func TestDeliver(t *testing.T) {
imapc, err := imapclient.New(imapconn, &opts) imapc, err := imapclient.New(imapconn, &opts)
tcheck(t, err, "new imapclient") tcheck(t, err, "new imapclient")
_, _, err = imapc.Login(imapuser, imappassword) _, err = imapc.Login(imapuser, imappassword)
tcheck(t, err, "imap login") tcheck(t, err, "imap login")
_, _, err = imapc.Select("Inbox") _, err = imapc.Select("Inbox")
tcheck(t, err, "imap select inbox") tcheck(t, err, "imap select inbox")
err = imapc.Commandf("", "idle") err = imapc.WriteCommandf("", "idle")
tcheck(t, err, "write imap idle command") tcheck(t, err, "write imap idle command")
_, _, _, err = imapc.ReadContinuation() _, err = imapc.ReadContinuation()
tcheck(t, err, "read imap continuation") tcheck(t, err, "read imap continuation")
idle := make(chan idleResponse) idle := make(chan idleResponse)

1056
testdata/imapclient/fuzzseed.txt vendored Normal file

File diff suppressed because it is too large Load Diff