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 ./dmarcrpt
go test -fullpath -fuzz . -parallel 1 -fuzztime 5m ./imapserver
go test -fullpath -fuzz . -fuzztime 5m ./imapclient
go test -fullpath -fuzz . -parallel 1 -fuzztime 5m ./junk
go test -fullpath -fuzz FuzzParseRecord -fuzztime 5m ./mtasts
go test -fullpath -fuzz FuzzParsePolicy -fuzztime 5m ./mtasts

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.
Behaviour that may not be required by the IMAP4 specification may be expected by
this client.
Warning: Currently primarily for testing the mox IMAP4 server. Behaviour that
may not be required by the IMAP4 specification may be expected by this client.
See [Conn] for a high-level client for executing IMAP commands. Use its embedded
[Proto] for lower-level writing of commands and reading of responses.
*/
package imapclient
/*
- Try to keep the parsing method names and the types similar to the ABNF names in the RFCs.
- todo: have mode for imap4rev1 vs imap4rev2, refusing what is not allowed. we are accepting too much now.
- todo: stricter parsing. xnonspace() and xword() should be replaced by proper parsers.
*/
import (
"bufio"
"crypto/tls"
"fmt"
"io"
"log/slog"
"net"
"reflect"
"strings"
"github.com/mjl-/mox/mlog"
"github.com/mjl-/mox/moxio"
)
// Conn is an IMAP connection to a server.
// Conn is an connection to an IMAP server.
//
// Method names on Conn are the names of IMAP commands. CloseMailbox, which
// executes the IMAP CLOSE command, is an exception. The Close method closes the
// connection.
//
// The methods starting with MSN are the original (old) IMAP commands. The variants
// starting with UID should almost always be used instead, if available.
//
// The methods on Conn typically return errors of type Error or Response. Error
// represents protocol and i/o level errors, including io.ErrDeadlineExceeded and
// various errors for closed connections. Response is returned as error if the IMAP
// result is NO or BAD instead of OK. The responses returned by the IMAP command
// methods can also be non-zero on errors. Callers may wish to process any untagged
// responses.
//
// The IMAP command methods defined on Conn don't interpret the untagged responses
// except for untagged CAPABILITY and untagged ENABLED responses, and the
// CAPABILITY response code. Fields CapAvailable and CapEnabled are updated when
// those untagged responses are received.
//
// Capabilities indicate which optional IMAP functionality is supported by a
// server. Capabilities are typically implicitly enabled when the client sends a
// command using syntax of an optional extension. Extensions without new syntax
// from client to server, but with new behaviour or syntax from server to client,
// the client needs to explicitly enable the capability with the ENABLE command,
// see the Enable method.
type Conn struct {
// If true, server sent a PREAUTH tag and the connection is already authenticated,
// e.g. based on TLS certificate authentication.
Preauth bool
// Capabilities available at server, from CAPABILITY command or response code.
CapAvailable []Capability
// Capabilities marked as enabled by the server, typically after an ENABLE command.
CapEnabled []Capability
// Proto provides lower-level functions for interacting with the IMAP connection,
// such as reading and writing individual lines/commands/responses.
Proto
}
// Proto provides low-level operations for writing requests and reading responses
// on an IMAP connection.
//
// To implement the IDLE command, write "IDLE" using [Proto.WriteCommandf], then
// read a line with [Proto.Readline]. If it starts with "+ ", the connection is in
// idle mode and untagged responses can be read using [Proto.ReadUntagged]. If the
// line doesn't start with "+ ", use [ParseResult] to interpret it as a response to
// IDLE, which should be a NO or BAD. To abort idle mode, write "DONE" using
// [Proto.Writelinef] and wait until a result line has been read.
type Proto struct {
// Connection, may be original TCP or TLS connection. Reads go through c.br, and
// writes through c.xbw. The "x" for the writes indicate that failed writes cause
// an i/o panic, which is either turned into a returned error, or passed on (see
@ -50,10 +96,7 @@ type Conn struct {
record bool // If true, bytes read are added to recordBuf. recorded() resets.
recordBuf []byte
Preauth bool
LastTag string
CapAvailable map[Capability]struct{} // Capabilities available at server, from CAPABILITY command or response code. All uppercase.
CapEnabled map[Capability]struct{} // Capabilities enabled through ENABLE command. All uppercase.
lastTag string
}
// Error is a parse or other protocol error.
@ -71,26 +114,29 @@ func (e Error) Unwrap() error {
type Opts struct {
Logger *slog.Logger
// Error is called for both IMAP-level and connection-level errors. Is allowed to
// Error is called for IMAP-level and connection-level errors during the IMAP
// command methods on Conn, not for errors in calls on Proto. Error is allowed to
// call panic.
Error func(err error)
}
// New creates a new client on conn.
// New initializes a new IMAP client on conn.
//
// Conn should normally be a TLS connection, typically connected to port 993 of an
// IMAP server. Alternatively, conn can be a plain TCP connection to port 143. TLS
// should be enabled on plain TCP connections with the [Conn.StartTLS] method.
//
// The initial untagged greeting response is read and must be "OK" or
// "PREAUTH". If preauth, the connection is already in authenticated state,
// typically through TLS client certificate. This is indicated in Conn.Preauth.
//
// Logging is written to log, in particular IMAP protocol traces are written with
// prefixes "CR: " and "CW: " (client read/write) as quoted strings at levels
// Debug-4, with authentication messages at Debug-6 and (user) data at level
// Logging is written to opts.Logger. In particular, IMAP protocol traces are
// written with prefixes "CR: " and "CW: " (client read/write) as quoted strings at
// levels Debug-4, with authentication messages at Debug-6 and (user) data at level
// Debug-8.
func New(conn net.Conn, opts *Opts) (client *Conn, rerr error) {
c := Conn{
conn: conn,
CapAvailable: map[Capability]struct{}{},
CapEnabled: map[Capability]struct{}{},
Proto: Proto{conn: conn},
}
var clog *slog.Logger
@ -109,7 +155,7 @@ func New(conn net.Conn, opts *Opts) (client *Conn, rerr error) {
c.xtw = moxio.NewTraceWriter(c.log, "CW: ", &c)
c.xbw = bufio.NewWriter(c.xtw)
defer c.recover(&rerr)
defer c.recoverErr(&rerr)
tag := c.xnonspace()
if tag != "*" {
c.xerrorf("expected untagged *, got %q", tag)
@ -121,6 +167,11 @@ func New(conn net.Conn, opts *Opts) (client *Conn, rerr error) {
if x.Status != OK {
c.xerrorf("greeting, got status %q, expected OK", x.Status)
}
if x.Code != nil {
if caps, ok := x.Code.(CodeCapability); ok {
c.CapAvailable = caps
}
}
return &c, nil
case UntaggedPreauth:
c.Preauth = true
@ -133,13 +184,33 @@ func New(conn net.Conn, opts *Opts) (client *Conn, rerr error) {
panic("not reached")
}
func (c *Conn) recover(rerr *error) {
func (c *Conn) recoverErr(rerr *error) {
c.recover(rerr, nil)
}
func (c *Conn) recover(rerr *error, resp *Response) {
if *rerr != nil {
if r, ok := (*rerr).(Response); ok && resp != nil {
*resp = r
}
c.errHandle(*rerr)
return
}
x := recover()
if x == nil {
return
}
err, ok := x.(Error)
if !ok {
var err error
switch e := x.(type) {
case Error:
err = e
case Response:
err = e
if resp != nil {
*resp = e
}
default:
panic(x)
}
if c.errHandle != nil {
@ -148,73 +219,110 @@ func (c *Conn) recover(rerr *error) {
*rerr = err
}
func (c *Conn) xerrorf(format string, args ...any) {
panic(Error{fmt.Errorf(format, args...)})
}
func (p *Proto) recover(rerr *error) {
if *rerr != nil {
return
}
func (c *Conn) xcheckf(err error, format string, args ...any) {
if err != nil {
c.xerrorf("%s: %w", fmt.Sprintf(format, args...), err)
x := recover()
if x == nil {
return
}
switch e := x.(type) {
case Error:
*rerr = e
default:
panic(x)
}
}
func (c *Conn) xcheck(err error) {
func (p *Proto) xerrorf(format string, args ...any) {
panic(Error{fmt.Errorf(format, args...)})
}
func (p *Proto) xcheckf(err error, format string, args ...any) {
if err != nil {
p.xerrorf("%s: %w", fmt.Sprintf(format, args...), err)
}
}
func (p *Proto) xcheck(err error) {
if err != nil {
panic(err)
}
}
// xresponse sets resp if err is a Response and resp is not nil.
func (p *Proto) xresponse(err error, resp *Response) {
if err == nil {
return
}
if r, ok := err.(Response); ok && resp != nil {
*resp = r
}
panic(err)
}
// Write writes directly to underlying connection (TCP, TLS). For internal use
// only, to implement io.Writer. Write errors do take the connection's panic mode
// into account, i.e. Write can panic.
func (c *Conn) Write(buf []byte) (n int, rerr error) {
defer c.recover(&rerr)
func (p *Proto) Write(buf []byte) (n int, rerr error) {
defer p.recover(&rerr)
n, rerr = c.conn.Write(buf)
n, rerr = p.conn.Write(buf)
if rerr != nil {
c.connBroken = true
p.connBroken = true
}
c.xcheckf(rerr, "write")
p.xcheckf(rerr, "write")
return n, nil
}
// Read reads directly from the underlying connection (TCP, TLS). For internal use
// only, to implement io.Reader.
func (c *Conn) Read(buf []byte) (n int, err error) {
return c.conn.Read(buf)
func (p *Proto) Read(buf []byte) (n int, err error) {
return p.conn.Read(buf)
}
func (c *Conn) xflush() {
func (p *Proto) xflush() {
// Not writing any more when connection is broken.
if c.connBroken {
if p.connBroken {
return
}
err := c.xbw.Flush()
c.xcheckf(err, "flush")
err := p.xbw.Flush()
p.xcheckf(err, "flush")
// If compression is active, we need to flush the deflate stream.
if c.compress {
err := c.xflateWriter.Flush()
c.xcheckf(err, "flush deflate")
err = c.xflateBW.Flush()
c.xcheckf(err, "flush deflate buffer")
if p.compress {
err := p.xflateWriter.Flush()
p.xcheckf(err, "flush deflate")
err = p.xflateBW.Flush()
p.xcheckf(err, "flush deflate buffer")
}
}
func (c *Conn) xtraceread(level slog.Level) func() {
c.tr.SetTrace(level)
func (p *Proto) xtraceread(level slog.Level) func() {
if p.tr == nil {
// For ParseUntagged and other parse functions.
return func() {}
}
p.tr.SetTrace(level)
return func() {
c.tr.SetTrace(mlog.LevelTrace)
p.tr.SetTrace(mlog.LevelTrace)
}
}
func (c *Conn) xtracewrite(level slog.Level) func() {
c.xflush()
c.xtw.SetTrace(level)
func (p *Proto) xtracewrite(level slog.Level) func() {
if p.xtw == nil {
// For ParseUntagged and other parse functions.
return func() {}
}
p.xflush()
p.xtw.SetTrace(level)
return func() {
c.xflush()
c.xtw.SetTrace(mlog.LevelTrace)
p.xflush()
p.xtw.SetTrace(mlog.LevelTrace)
}
}
@ -228,7 +336,7 @@ func (c *Conn) xtracewrite(level slog.Level) func() {
// because the server may immediate close the underlying connection when it sees
// the connection is being closed.
func (c *Conn) Close() (rerr error) {
defer c.recover(&rerr)
defer c.recoverErr(&rerr)
if c.conn == nil {
return nil
@ -247,7 +355,9 @@ func (c *Conn) Close() (rerr error) {
return
}
// TLSConnectionState returns the TLS connection state if the connection uses TLS.
// TLSConnectionState returns the TLS connection state if the connection uses TLS,
// either because the conn passed to [New] was a TLS connection, or because
// [Conn.StartTLS] was called.
func (c *Conn) TLSConnectionState() *tls.ConnectionState {
if conn, ok := c.conn.(*tls.Conn); ok {
cs := conn.ConnectionState()
@ -256,170 +366,266 @@ func (c *Conn) TLSConnectionState() *tls.ConnectionState {
return nil
}
// Commandf writes a free-form IMAP command to the server. An ending \r\n is
// WriteCommandf writes a free-form IMAP command to the server. An ending \r\n is
// written too.
//
// If tag is empty, a next unique tag is assigned.
func (c *Conn) Commandf(tag string, format string, args ...any) (rerr error) {
defer c.recover(&rerr)
func (p *Proto) WriteCommandf(tag string, format string, args ...any) (rerr error) {
defer p.recover(&rerr)
if tag == "" {
tag = c.nextTag()
p.nextTag()
} else {
p.lastTag = tag
}
c.LastTag = tag
fmt.Fprintf(c.xbw, "%s %s\r\n", tag, fmt.Sprintf(format, args...))
c.xflush()
fmt.Fprintf(p.xbw, "%s %s\r\n", p.lastTag, fmt.Sprintf(format, args...))
p.xflush()
return
}
func (c *Conn) nextTag() string {
c.tagGen++
return fmt.Sprintf("x%03d", c.tagGen)
func (p *Proto) nextTag() string {
p.tagGen++
p.lastTag = fmt.Sprintf("x%03d", p.tagGen)
return p.lastTag
}
// Response reads from the IMAP server until a tagged response line is found.
// LastTag returns the tag last used for a command. For checking against a command
// completion result.
func (p *Proto) LastTag() string {
return p.lastTag
}
// LastTagSet sets a new last tag, as used for checking against a command completion result.
func (p *Proto) LastTagSet(tag string) {
p.lastTag = tag
}
// ReadResponse reads from the IMAP server until a tagged response line is found.
// The tag must be the same as the tag for the last written command.
// Result holds the status of the command. The caller must check if this the status is OK.
func (c *Conn) Response() (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
//
// If an error is returned, resp can still be non-empty, and a caller may wish to
// process resp.Untagged.
//
// Caller should check resp.Status for the result of the command too.
//
// Common types for the return error:
// - Error, for protocol errors
// - Various I/O errors from the underlying connection, including os.ErrDeadlineExceeded
func (p *Proto) ReadResponse() (resp Response, rerr error) {
defer p.recover(&rerr)
for {
tag := c.xnonspace()
c.xspace()
tag := p.xnonspace()
p.xspace()
if tag == "*" {
untagged = append(untagged, c.xuntagged())
resp.Untagged = append(resp.Untagged, p.xuntagged())
continue
}
if tag != c.LastTag {
c.xerrorf("got tag %q, expected %q", tag, c.LastTag)
if tag != p.lastTag {
p.xerrorf("got tag %q, expected %q", tag, p.lastTag)
}
status := c.xstatus()
c.xspace()
result = c.xresult(status)
c.xcrlf()
status := p.xstatus()
p.xspace()
resp.Result = p.xresult(status)
p.xcrlf()
return
}
}
// ReadUntagged reads a single untagged response line.
// Useful for reading lines from IDLE.
func (c *Conn) ReadUntagged() (untagged Untagged, rerr error) {
defer c.recover(&rerr)
tag := c.xnonspace()
if tag != "*" {
c.xerrorf("got tag %q, expected untagged", tag)
// ParseCode parses a response code. The string must not have enclosing brackets.
//
// Example:
//
// "APPENDUID 123 10"
func ParseCode(s string) (code Code, rerr error) {
p := Proto{br: bufio.NewReader(strings.NewReader(s + "]"))}
defer p.recover(&rerr)
code = p.xrespCode()
p.xtake("]")
buf, err := io.ReadAll(p.br)
p.xcheckf(err, "read")
if len(buf) != 0 {
p.xerrorf("leftover data %q", buf)
}
c.xspace()
ut := c.xuntagged()
return code, nil
}
// ParseResult parses a line, including required crlf, as a command result line.
//
// Example:
//
// "tag1 OK [APPENDUID 123 10] message added\r\n"
func ParseResult(s string) (tag string, result Result, rerr error) {
p := Proto{br: bufio.NewReader(strings.NewReader(s))}
defer p.recover(&rerr)
tag = p.xnonspace()
p.xspace()
status := p.xstatus()
p.xspace()
result = p.xresult(status)
p.xcrlf()
return
}
// ReadUntagged reads a single untagged response line.
func (p *Proto) ReadUntagged() (untagged Untagged, rerr error) {
defer p.recover(&rerr)
return p.readUntagged()
}
// ParseUntagged parses a line, including required crlf, as untagged response.
//
// Example:
//
// "* BYE shutting down connection\r\n"
func ParseUntagged(s string) (untagged Untagged, rerr error) {
p := Proto{br: bufio.NewReader(strings.NewReader(s))}
defer p.recover(&rerr)
untagged, rerr = p.readUntagged()
return
}
func (p *Proto) readUntagged() (untagged Untagged, rerr error) {
defer p.recover(&rerr)
tag := p.xnonspace()
if tag != "*" {
p.xerrorf("got tag %q, expected untagged", tag)
}
p.xspace()
ut := p.xuntagged()
return ut, nil
}
// Readline reads a line, including CRLF.
// Used with IDLE and synchronous literals.
func (c *Conn) Readline() (line string, rerr error) {
defer c.recover(&rerr)
func (p *Proto) Readline() (line string, rerr error) {
defer p.recover(&rerr)
line, err := c.br.ReadString('\n')
c.xcheckf(err, "read line")
line, err := p.br.ReadString('\n')
p.xcheckf(err, "read line")
return line, nil
}
// ReadContinuation reads a line. If it is a continuation, i.e. starts with a +, it
// is returned without leading "+ " and without trailing crlf. Otherwise, a command
// response is returned. A successfully read continuation can return an empty line.
// Callers should check rerr and result.Status being empty to check if a
// continuation was read.
func (c *Conn) ReadContinuation() (line string, untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
if !c.peek('+') {
untagged, result, rerr = c.Response()
if result.Status == OK {
c.xerrorf("unexpected OK instead of continuation")
func (c *Conn) readContinuation() (line string, rerr error) {
defer c.recover(&rerr, nil)
line, rerr = c.ReadContinuation()
if rerr != nil {
if resp, ok := rerr.(Response); ok {
c.processUntagged(resp.Untagged)
c.processResult(resp.Result)
}
}
return
}
// 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("+ ")
line, err := c.Readline()
c.xcheckf(err, "read line")
return "", rerr
}
p.xtake("+ ")
line, err := p.Readline()
p.xcheckf(err, "read line")
line = strings.TrimSuffix(line, "\r\n")
return
}
// Writelinef writes the formatted format and args as a single line, adding CRLF.
// Used with IDLE and synchronous literals.
func (c *Conn) Writelinef(format string, args ...any) (rerr error) {
defer c.recover(&rerr)
func (p *Proto) Writelinef(format string, args ...any) (rerr error) {
defer p.recover(&rerr)
s := fmt.Sprintf(format, args...)
fmt.Fprintf(c.xbw, "%s\r\n", s)
c.xflush()
fmt.Fprintf(p.xbw, "%s\r\n", s)
p.xflush()
return nil
}
// WriteSyncLiteral first writes the synchronous literal size, then reads the
// continuation "+" and finally writes the data.
func (c *Conn) WriteSyncLiteral(s string) (untagged []Untagged, rerr error) {
defer c.recover(&rerr)
// continuation "+" and finally writes the data. If the literal is not accepted, an
// error is returned, which may be a Response.
func (p *Proto) WriteSyncLiteral(s string) (rerr error) {
defer p.recover(&rerr)
fmt.Fprintf(c.xbw, "{%d}\r\n", len(s))
c.xflush()
fmt.Fprintf(p.xbw, "{%d}\r\n", len(s))
p.xflush()
plus, err := c.br.Peek(1)
c.xcheckf(err, "read continuation")
plus, err := p.br.Peek(1)
p.xcheckf(err, "read continuation")
if plus[0] == '+' {
_, err = c.Readline()
c.xcheckf(err, "read continuation line")
_, err = p.Readline()
p.xcheckf(err, "read continuation line")
defer c.xtracewrite(mlog.LevelTracedata)()
_, err = c.xbw.Write([]byte(s))
c.xcheckf(err, "write literal data")
c.xtracewrite(mlog.LevelTrace)
return nil, nil
defer p.xtracewrite(mlog.LevelTracedata)()
_, err = p.xbw.Write([]byte(s))
p.xcheckf(err, "write literal data")
p.xtracewrite(mlog.LevelTrace)
return nil
}
untagged, result, err := c.Response()
if err == nil && result.Status == OK {
c.xerrorf("no continuation, but invalid ok response (%q)", result.More)
var resp Response
resp, rerr = p.ReadResponse()
if rerr == nil {
rerr = resp
}
return untagged, fmt.Errorf("no continuation (%s)", result.Status)
return
}
// Transactf writes format and args as an IMAP command, using Commandf with an
func (c *Conn) processUntagged(l []Untagged) {
for _, ut := range l {
switch e := ut.(type) {
case UntaggedCapability:
c.CapAvailable = []Capability(e)
case UntaggedEnabled:
c.CapEnabled = append(c.CapEnabled, e...)
}
}
}
func (c *Conn) processResult(r Result) {
if r.Code == nil {
return
}
switch e := r.Code.(type) {
case CodeCapability:
c.CapAvailable = []Capability(e)
}
}
// transactf writes format and args as an IMAP command, using Commandf with an
// empty tag. I.e. format must not contain a tag. Transactf then reads a response
// using ReadResponse and checks the result status is OK.
func (c *Conn) Transactf(format string, args ...any) (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
func (c *Conn) transactf(format string, args ...any) (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
err := c.Commandf("", format, args...)
err := c.WriteCommandf("", format, args...)
if err != nil {
return nil, Result{}, err
return Response{}, err
}
return c.ResponseOK()
return c.responseOK()
}
func (c *Conn) ResponseOK() (untagged []Untagged, result Result, rerr error) {
untagged, result, rerr = c.Response()
if rerr != nil {
return nil, Result{}, rerr
}
if result.Status != OK {
c.xerrorf("response status %q, expected OK", result.Status)
}
return untagged, result, rerr
}
func (c *Conn) responseOK() (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
func (c *Conn) xgetUntagged(l []Untagged, dst any) {
if len(l) != 1 {
c.xerrorf("got %d untagged, expected 1: %v", len(l), l)
resp, rerr = c.ReadResponse()
c.processUntagged(resp.Untagged)
c.processResult(resp.Result)
if rerr == nil && resp.Status != OK {
rerr = resp
}
got := l[0]
gotv := reflect.ValueOf(got)
dstv := reflect.ValueOf(dst)
if gotv.Type() != dstv.Type().Elem() {
c.xerrorf("got %v, expected %v", gotv.Type(), dstv.Type().Elem())
}
dstv.Elem().Set(gotv)
return
}

View File

@ -17,32 +17,34 @@ import (
"github.com/mjl-/mox/scram"
)
// Capability requests a list of capabilities from the server. They are returned in
// an UntaggedCapability response. The server also sends capabilities in initial
// server greeting, in the response code.
func (c *Conn) Capability() (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
return c.Transactf("capability")
// Capability writes the IMAP4 "CAPABILITY" command, requesting a list of
// capabilities from the server. They are returned in an UntaggedCapability
// response. The server also sends capabilities in initial server greeting, in the
// response code.
func (c *Conn) Capability() (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
return c.transactf("capability")
}
// Noop does nothing on its own, but a server will return any pending untagged
// responses for new message delivery and changes to mailboxes.
func (c *Conn) Noop() (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
return c.Transactf("noop")
// Noop writes the IMAP4 "NOOP" command, which does nothing on its own, but a
// server will return any pending untagged responses for new message delivery and
// changes to mailboxes.
func (c *Conn) Noop() (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
return c.transactf("noop")
}
// Logout ends the IMAP session by writing a LOGOUT command. Close must still be
// called on this client to close the socket.
func (c *Conn) Logout() (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
return c.Transactf("logout")
// Logout ends the IMAP4 session by writing an IMAP "LOGOUT" command. [Conn.Close]
// must still be called on this client to close the socket.
func (c *Conn) Logout() (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
return c.transactf("logout")
}
// Starttls enables TLS on the connection with the STARTTLS command.
func (c *Conn) Starttls(config *tls.Config) (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
untagged, result, rerr = c.Transactf("starttls")
// StartTLS enables TLS on the connection with the IMAP4 "STARTTLS" command.
func (c *Conn) StartTLS(config *tls.Config) (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
resp, rerr = c.transactf("starttls")
c.xcheckf(rerr, "starttls command")
conn := c.xprefixConn()
@ -50,32 +52,43 @@ func (c *Conn) Starttls(config *tls.Config) (untagged []Untagged, result Result,
err := tlsConn.Handshake()
c.xcheckf(err, "tls handshake")
c.conn = tlsConn
return untagged, result, nil
return
}
// Login authenticates with username and password
func (c *Conn) Login(username, password string) (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
// Login authenticates using the IMAP4 "LOGIN" command, sending the plain text
// password to the server.
//
// Authentication is not allowed while the "LOGINDISABLED" capability is announced.
// Call [Conn.StartTLS] first.
//
// See [Conn.AuthenticateSCRAM] for a better authentication mechanism.
func (c *Conn) Login(username, password string) (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
c.LastTag = c.nextTag()
fmt.Fprintf(c.xbw, "%s login %s ", c.LastTag, astring(username))
fmt.Fprintf(c.xbw, "%s login %s ", c.nextTag(), astring(username))
defer c.xtracewrite(mlog.LevelTraceauth)()
fmt.Fprintf(c.xbw, "%s\r\n", astring(password))
c.xtracewrite(mlog.LevelTrace) // Restore.
return c.Response()
return c.responseOK()
}
// Authenticate with plaintext password using AUTHENTICATE PLAIN.
func (c *Conn) AuthenticatePlain(username, password string) (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
// AuthenticatePlain executes the AUTHENTICATE command with SASL mechanism "PLAIN",
// sending the password in plain text password to the server.
//
// Required capability: "AUTH=PLAIN"
//
// Authentication is not allowed while the "LOGINDISABLED" capability is announced.
// Call [Conn.StartTLS] first.
//
// See [Conn.AuthenticateSCRAM] for a better authentication mechanism.
func (c *Conn) AuthenticatePlain(username, password string) (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
err := c.Commandf("", "authenticate plain")
err := c.WriteCommandf("", "authenticate plain")
c.xcheckf(err, "writing authenticate command")
_, untagged, result, rerr = c.ReadContinuation()
c.xcheckf(rerr, "reading continuation")
if result.Status != "" {
c.xerrorf("got result status %q, expected continuation", result.Status)
}
_, rerr = c.readContinuation()
c.xresponse(rerr, &resp)
defer c.xtracewrite(mlog.LevelTraceauth)()
xw := base64.NewEncoder(base64.StdEncoding, c.xbw)
fmt.Fprintf(xw, "\u0000%s\u0000%s", username, password)
@ -83,23 +96,31 @@ func (c *Conn) AuthenticatePlain(username, password string) (untagged []Untagged
c.xtracewrite(mlog.LevelTrace) // Restore.
fmt.Fprintf(c.xbw, "\r\n")
c.xflush()
return c.Response()
return c.responseOK()
}
// todo: implement cram-md5, write its credentials as traceauth.
// Authenticate with SCRAM-SHA-256(-PLUS) or SCRAM-SHA-1(-PLUS). With SCRAM, the
// password is not exchanged in plaintext form, but only derived hashes are
// exchanged by both parties as proof of knowledge of password.
// AuthenticateSCRAM executes the IMAP4 "AUTHENTICATE" command with one of the
// following SASL mechanisms: SCRAM-SHA-256(-PLUS) or SCRAM-SHA-1(-PLUS).//
//
// With SCRAM, the password is not sent to the server in plain text, but only
// derived hashes are exchanged by both parties as proof of knowledge of password.
//
// Authentication is not allowed while the "LOGINDISABLED" capability is announced.
// Call [Conn.StartTLS] first.
//
// Required capability: SCRAM-SHA-256-PLUS, SCRAM-SHA-256, SCRAM-SHA-1-PLUS,
// SCRAM-SHA-1.
//
// The PLUS variants bind the authentication exchange to the TLS connection,
// detecting MitM attacks.
func (c *Conn) AuthenticateSCRAM(method string, h func() hash.Hash, username, password string) (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
func (c *Conn) AuthenticateSCRAM(mechanism string, h func() hash.Hash, username, password string) (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
var cs *tls.ConnectionState
lmethod := strings.ToLower(method)
if strings.HasSuffix(lmethod, "-plus") {
lmech := strings.ToLower(mechanism)
if strings.HasSuffix(lmech, "-plus") {
tlsConn, ok := c.conn.(*tls.Conn)
if !ok {
c.xerrorf("cannot use scram plus without tls")
@ -110,17 +131,14 @@ func (c *Conn) AuthenticateSCRAM(method string, h func() hash.Hash, username, pa
sc := scram.NewClient(h, username, "", false, cs)
clientFirst, err := sc.ClientFirst()
c.xcheckf(err, "scram clientFirst")
c.LastTag = c.nextTag()
err = c.Writelinef("%s authenticate %s %s", c.LastTag, method, base64.StdEncoding.EncodeToString([]byte(clientFirst)))
// todo: only send clientFirst if server has announced SASL-IR
err = c.Writelinef("%s authenticate %s %s", c.nextTag(), mechanism, base64.StdEncoding.EncodeToString([]byte(clientFirst)))
c.xcheckf(err, "writing command line")
xreadContinuation := func() []byte {
var line string
line, untagged, result, rerr = c.ReadContinuation()
c.xcheckf(err, "read continuation")
if result.Status != "" {
c.xerrorf("got result status %q, expected continuation", result.Status)
}
line, rerr = c.readContinuation()
c.xresponse(rerr, &resp)
buf, err := base64.StdEncoding.DecodeString(line)
c.xcheckf(err, "parsing base64 from remote")
return buf
@ -140,18 +158,19 @@ func (c *Conn) AuthenticateSCRAM(method string, h func() hash.Hash, username, pa
err = c.Writelinef("%s", base64.StdEncoding.EncodeToString(nil))
c.xcheckf(err, "scram client end")
return c.ResponseOK()
return c.responseOK()
}
// CompressDeflate enables compression with deflate on the connection.
// CompressDeflate enables compression with deflate on the connection by executing
// the IMAP4 "COMPRESS=DEFAULT" command.
//
// Only possible when server has announced the COMPRESS=DEFLATE capability.
// Required capability: "COMPRESS=DEFLATE".
//
// State: Authenticated or selected.
func (c *Conn) CompressDeflate() (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
func (c *Conn) CompressDeflate() (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
untagged, result, rerr = c.Transactf("compress deflate")
resp, rerr = c.transactf("compress deflate")
c.xcheck(rerr)
c.xflateBW = bufio.NewWriter(c)
@ -172,89 +191,98 @@ func (c *Conn) CompressDeflate() (untagged []Untagged, result Result, rerr error
return
}
// Enable enables capabilities for use with the connection, verifying the server has indeed enabled them.
func (c *Conn) Enable(capabilities ...string) (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
// Enable enables capabilities for use with the connection by executing the IMAP4 "ENABLE" command.
//
// Required capability: "ENABLE" or "IMAP4rev2"
func (c *Conn) Enable(capabilities ...Capability) (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
untagged, result, rerr = c.Transactf("enable %s", strings.Join(capabilities, " "))
c.xcheck(rerr)
var enabled UntaggedEnabled
c.xgetUntagged(untagged, &enabled)
got := map[string]struct{}{}
for _, cap := range enabled {
got[cap] = struct{}{}
var caps strings.Builder
for _, c := range capabilities {
caps.WriteString(" " + string(c))
}
for _, cap := range capabilities {
if _, ok := got[cap]; !ok {
c.xerrorf("capability %q not enabled by server", cap)
}
}
return
return c.transactf("enable%s", caps.String())
}
// Select opens mailbox as active mailbox.
func (c *Conn) Select(mailbox string) (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
return c.Transactf("select %s", astring(mailbox))
// Select opens the mailbox with the IMAP4 "SELECT" command.
//
// If a mailbox is selected/active, it is automatically deselected before
// selecting the mailbox, without permanently removing ("expunging") messages
// marked \Deleted.
//
// If the mailbox cannot be opened, the connection is left in Authenticated state,
// not Selected.
func (c *Conn) Select(mailbox string) (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
return c.transactf("select %s", astring(mailbox))
}
// Examine opens mailbox as active mailbox read-only.
func (c *Conn) Examine(mailbox string) (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
return c.Transactf("examine %s", astring(mailbox))
// Examine opens the mailbox like [Conn.Select], but read-only, with the IMAP4
// "EXAMINE" command.
func (c *Conn) Examine(mailbox string) (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
return c.transactf("examine %s", astring(mailbox))
}
// Create makes a new mailbox on the server.
// SpecialUse can only be used on servers that announced the CREATE-SPECIAL-USE
// Create makes a new mailbox on the server using the IMAP4 "CREATE" command.
//
// SpecialUse can only be used on servers that announced the "CREATE-SPECIAL-USE"
// capability. Specify flags like \Archive, \Drafts, \Junk, \Sent, \Trash, \All.
func (c *Conn) Create(mailbox string, specialUse []string) (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
if _, ok := c.CapAvailable[CapCreateSpecialUse]; !ok && len(specialUse) > 0 {
c.xerrorf("server does not implement create-special-use extension")
}
func (c *Conn) Create(mailbox string, specialUse []string) (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
var useStr string
if len(specialUse) > 0 {
useStr = fmt.Sprintf(" USE (%s)", strings.Join(specialUse, " "))
}
return c.Transactf("create %s%s", astring(mailbox), useStr)
return c.transactf("create %s%s", astring(mailbox), useStr)
}
// Delete removes an entire mailbox and its messages.
func (c *Conn) Delete(mailbox string) (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
return c.Transactf("delete %s", astring(mailbox))
// Delete removes an entire mailbox and its messages using the IMAP4 "DELETE"
// command.
func (c *Conn) Delete(mailbox string) (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
return c.transactf("delete %s", astring(mailbox))
}
// Rename changes the name of a mailbox and all its child mailboxes.
func (c *Conn) Rename(omailbox, nmailbox string) (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
return c.Transactf("rename %s %s", astring(omailbox), astring(nmailbox))
// Rename changes the name of a mailbox and all its child mailboxes
// using the IMAP4 "RENAME" command.
func (c *Conn) Rename(omailbox, nmailbox string) (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
return c.transactf("rename %s %s", astring(omailbox), astring(nmailbox))
}
// Subscribe marks a mailbox as subscribed. The mailbox does not have to exist. It
// is not an error if the mailbox is already subscribed.
func (c *Conn) Subscribe(mailbox string) (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
return c.Transactf("subscribe %s", astring(mailbox))
// Subscribe marks a mailbox as subscribed using the IMAP4 "SUBSCRIBE" command.
//
// The mailbox does not have to exist. It is not an error if the mailbox is already
// subscribed.
func (c *Conn) Subscribe(mailbox string) (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
return c.transactf("subscribe %s", astring(mailbox))
}
// Unsubscribe marks a mailbox as unsubscribed.
func (c *Conn) Unsubscribe(mailbox string) (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
return c.Transactf("unsubscribe %s", astring(mailbox))
// Unsubscribe marks a mailbox as unsubscribed using the IMAP4 "UNSUBSCRIBE"
// command.
func (c *Conn) Unsubscribe(mailbox string) (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
return c.transactf("unsubscribe %s", astring(mailbox))
}
// List lists mailboxes with the basic LIST syntax.
// List lists mailboxes using the IMAP4 "LIST" command with the basic LIST syntax.
// Pattern can contain * (match any) or % (match any except hierarchy delimiter).
func (c *Conn) List(pattern string) (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
return c.Transactf(`list "" %s`, astring(pattern))
func (c *Conn) List(pattern string) (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
return c.transactf(`list "" %s`, astring(pattern))
}
// ListFull lists mailboxes with the extended LIST syntax requesting all supported data.
// ListFull lists mailboxes using the LIST command with the extended LIST
// syntax requesting all supported data.
//
// Required capability: "LIST-EXTENDED". If "IMAP4rev2" is announced, the command
// is also available but only with a single pattern.
//
// Pattern can contain * (match any) or % (match any except hierarchy delimiter).
func (c *Conn) ListFull(subscribedOnly bool, patterns ...string) (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
func (c *Conn) ListFull(subscribedOnly bool, patterns ...string) (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
var subscribedStr string
if subscribedOnly {
subscribedStr = "subscribed recursivematch"
@ -262,49 +290,54 @@ func (c *Conn) ListFull(subscribedOnly bool, patterns ...string) (untagged []Unt
for i, s := range patterns {
patterns[i] = astring(s)
}
return c.Transactf(`list (%s) "" (%s) return (subscribed children special-use status (messages uidnext uidvalidity unseen deleted size recent appendlimit))`, subscribedStr, strings.Join(patterns, " "))
return c.transactf(`list (%s) "" (%s) return (subscribed children special-use status (messages uidnext uidvalidity unseen deleted size recent appendlimit))`, subscribedStr, strings.Join(patterns, " "))
}
// Namespace returns the hiearchy separator in an UntaggedNamespace response with personal/shared/other namespaces if present.
func (c *Conn) Namespace() (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
return c.Transactf("namespace")
// Namespace requests the hiearchy separator using the IMAP4 "NAMESPACE" command.
//
// Required capability: "NAMESPACE" or "IMAP4rev2".
//
// Server will return an UntaggedNamespace response with personal/shared/other
// namespaces if present.
func (c *Conn) Namespace() (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
return c.transactf("namespace")
}
// Status requests information about a mailbox, such as number of messages, size,
// etc. At least one attribute required.
func (c *Conn) Status(mailbox string, attrs ...StatusAttr) (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
// Status requests information about a mailbox using the IMAP4 "STATUS" command. For
// example, number of messages, size, etc. At least one attribute required.
func (c *Conn) Status(mailbox string, attrs ...StatusAttr) (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
l := make([]string, len(attrs))
for i, a := range attrs {
l[i] = string(a)
}
return c.Transactf("status %s (%s)", astring(mailbox), strings.Join(l, " "))
return c.transactf("status %s (%s)", astring(mailbox), strings.Join(l, " "))
}
// Append represents a parameter to the APPEND or REPLACE commands.
// Append represents a parameter to the IMAP4 "APPEND" or "REPLACE" commands, for
// adding a message to mailbox, or replacing a message with a new version in a
// mailbox.
type Append struct {
Flags []string
Received *time.Time
Flags []string // Optional, flags for the new message.
Received *time.Time // Optional, the INTERNALDATE field, typically time at which a message was received.
Size int64
Data io.Reader // Must return Size bytes.
Data io.Reader // Required, must return Size bytes.
}
// Append adds message to mailbox with flags and optional receive time.
// Append adds message to mailbox with flags and optional receive time using the
// IMAP4 "APPEND" command.
func (c *Conn) Append(mailbox string, message Append) (resp Response, rerr error) {
return c.MultiAppend(mailbox, message)
}
// MultiAppend atomatically adds multiple messages to the mailbox.
//
// Multiple messages are only possible when the server has announced the
// MULTIAPPEND capability.
func (c *Conn) Append(mailbox string, message Append, more ...Append) (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
// Required capability: "MULTIAPPEND"
func (c *Conn) MultiAppend(mailbox string, message Append, more ...Append) (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
if _, ok := c.CapAvailable[CapMultiAppend]; !ok && len(more) > 0 {
c.xerrorf("can only append multiple messages when server has announced MULTIAPPEND capability")
}
tag := c.nextTag()
c.LastTag = tag
fmt.Fprintf(c.xbw, "%s append %s", tag, astring(mailbox))
fmt.Fprintf(c.xbw, "%s append %s", c.nextTag(), astring(mailbox))
msgs := append([]Append{message}, more...)
for _, m := range msgs {
@ -325,150 +358,226 @@ func (c *Conn) Append(mailbox string, message Append, more ...Append) (untagged
fmt.Fprintf(c.xbw, "\r\n")
c.xflush()
return c.Response()
return c.responseOK()
}
// note: No Idle or Notify command. Idle/Notify is better implemented by
// writing the request and reading and handling the responses as they come in.
// CloseMailbox closes the currently selected/active mailbox, permanently removing
// any messages marked with \Deleted.
func (c *Conn) CloseMailbox() (untagged []Untagged, result Result, rerr error) {
return c.Transactf("close")
// CloseMailbox closes the selected/active mailbox using the IMAP4 "CLOSE" command,
// permanently removing ("expunging") any messages marked with \Deleted.
//
// See [Conn.Unselect] for closing a mailbox without permanently removing messages.
func (c *Conn) CloseMailbox() (resp Response, rerr error) {
return c.transactf("close")
}
// Unselect closes the currently selected/active mailbox, but unlike CloseMailbox
// does not permanently remove any messages marked with \Deleted.
func (c *Conn) Unselect() (untagged []Untagged, result Result, rerr error) {
return c.Transactf("unselect")
// Unselect closes the selected/active mailbox using the IMAP4 "UNSELECT" command,
// but unlike MailboxClose does not permanently remove ("expunge") any messages
// marked with \Deleted.
//
// Required capability: "UNSELECT" or "IMAP4rev2".
//
// If Unselect is not available, call [Conn.Select] with a non-existent mailbox for
// the same effect: Deselecting a mailbox without permanently removing messages
// marked \Deleted.
func (c *Conn) Unselect() (resp Response, rerr error) {
return c.transactf("unselect")
}
// Expunge removes messages marked as deleted for the selected mailbox.
func (c *Conn) Expunge() (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
return c.Transactf("expunge")
// Expunge removes all messages marked as deleted for the selected mailbox using
// the IMAP4 "EXPUNGE" command. If other sessions marked messages as deleted, even
// if they aren't visible in the session, they are removed as well.
//
// UIDExpunge gives more control over which the messages that are removed.
func (c *Conn) Expunge() (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
return c.transactf("expunge")
}
// UIDExpunge is like expunge, but only removes messages matching uidSet.
func (c *Conn) UIDExpunge(uidSet NumSet) (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
return c.Transactf("uid expunge %s", uidSet.String())
// UIDExpunge is like expunge, but only removes messages matching UID set, using
// the IMAP4 "UID EXPUNGE" command.
//
// Required capability: "UIDPLUS" or "IMAP4rev2".
func (c *Conn) UIDExpunge(uidSet NumSet) (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
return c.transactf("uid expunge %s", uidSet.String())
}
// Note: No search, fetch command yet due to its large syntax.
// StoreFlagsSet stores a new set of flags for messages from seqset with the STORE command.
// If silent, no untagged responses with the updated flags will be sent by the server.
func (c *Conn) StoreFlagsSet(seqset string, silent bool, flags ...string) (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
// MSNStoreFlagsSet stores a new set of flags for messages matching message
// sequence numbers (MSNs) from sequence set with the IMAP4 "STORE" command.
//
// If silent, no untagged responses with the updated flags will be sent by the
// server.
//
// Method [Conn.UIDStoreFlagsSet], which operates on a uid set, should be
// preferred.
func (c *Conn) MSNStoreFlagsSet(seqset string, silent bool, flags ...string) (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
item := "flags"
if silent {
item += ".silent"
}
return c.Transactf("store %s %s (%s)", seqset, item, strings.Join(flags, " "))
return c.transactf("store %s %s (%s)", seqset, item, strings.Join(flags, " "))
}
// StoreFlagsAdd is like StoreFlagsSet, but only adds flags, leaving current flags on the message intact.
func (c *Conn) StoreFlagsAdd(seqset string, silent bool, flags ...string) (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
// MSNStoreFlagsAdd is like [Conn.MSNStoreFlagsSet], but only adds flags, leaving
// current flags on the message intact.
//
// Method [Conn.UIDStoreFlagsAdd], which operates on a uid set, should be
// preferred.
func (c *Conn) MSNStoreFlagsAdd(seqset string, silent bool, flags ...string) (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
item := "+flags"
if silent {
item += ".silent"
}
return c.Transactf("store %s %s (%s)", seqset, item, strings.Join(flags, " "))
return c.transactf("store %s %s (%s)", seqset, item, strings.Join(flags, " "))
}
// StoreFlagsClear is like StoreFlagsSet, but only removes flags, leaving other flags on the message intact.
func (c *Conn) StoreFlagsClear(seqset string, silent bool, flags ...string) (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
// MSNStoreFlagsClear is like [Conn.MSNStoreFlagsSet], but only removes flags,
// leaving other flags on the message intact.
//
// Method [Conn.UIDStoreFlagsClear], which operates on a uid set, should be
// preferred.
func (c *Conn) MSNStoreFlagsClear(seqset string, silent bool, flags ...string) (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
item := "-flags"
if silent {
item += ".silent"
}
return c.Transactf("store %s %s (%s)", seqset, item, strings.Join(flags, " "))
return c.transactf("store %s %s (%s)", seqset, item, strings.Join(flags, " "))
}
// UIDStoreFlagsSet stores a new set of flags for messages from uid set with
// the UID STORE command.
// UIDStoreFlagsSet stores a new set of flags for messages matching UIDs from
// uidSet with the IMAP4 "UID STORE" command.
//
// If silent, no untagged responses with the updated flags will be sent by the
// server.
func (c *Conn) UIDStoreFlagsSet(seqset string, silent bool, flags ...string) (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
//
// Required capability: "UIDPLUS" or "IMAP4rev2".
func (c *Conn) UIDStoreFlagsSet(uidSet string, silent bool, flags ...string) (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
item := "flags"
if silent {
item += ".silent"
}
return c.Transactf("uid store %s %s (%s)", seqset, item, strings.Join(flags, " "))
return c.transactf("uid store %s %s (%s)", uidSet, item, strings.Join(flags, " "))
}
// UIDStoreFlagsAdd is like UIDStoreFlagsSet, but only adds flags, leaving
// current flags on the message intact.
func (c *Conn) UIDStoreFlagsAdd(seqset string, silent bool, flags ...string) (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
//
// Required capability: "UIDPLUS" or "IMAP4rev2".
func (c *Conn) UIDStoreFlagsAdd(uidSet string, silent bool, flags ...string) (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
item := "+flags"
if silent {
item += ".silent"
}
return c.Transactf("uid store %s %s (%s)", seqset, item, strings.Join(flags, " "))
return c.transactf("uid store %s %s (%s)", uidSet, item, strings.Join(flags, " "))
}
// UIDStoreFlagsClear is like UIDStoreFlagsSet, but only removes flags, leaving
// other flags on the message intact.
func (c *Conn) UIDStoreFlagsClear(seqset string, silent bool, flags ...string) (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
//
// Required capability: "UIDPLUS" or "IMAP4rev2".
func (c *Conn) UIDStoreFlagsClear(uidSet string, silent bool, flags ...string) (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
item := "-flags"
if silent {
item += ".silent"
}
return c.Transactf("uid store %s %s (%s)", seqset, item, strings.Join(flags, " "))
return c.transactf("uid store %s %s (%s)", uidSet, item, strings.Join(flags, " "))
}
// Copy adds messages from the sequences in seqSet in the currently selected/active mailbox to dstMailbox.
func (c *Conn) Copy(seqSet NumSet, dstMailbox string) (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
return c.Transactf("copy %s %s", seqSet.String(), astring(dstMailbox))
}
// UIDCopy is like copy, but operates on UIDs.
func (c *Conn) UIDCopy(uidSet NumSet, dstMailbox string) (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
return c.Transactf("uid copy %s %s", uidSet.String(), astring(dstMailbox))
}
// Move moves messages from the sequences in seqSet in the currently selected/active mailbox to dstMailbox.
func (c *Conn) Move(seqSet NumSet, dstMailbox string) (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
return c.Transactf("move %s %s", seqSet.String(), astring(dstMailbox))
}
// UIDMove is like move, but operates on UIDs.
func (c *Conn) UIDMove(uidSet NumSet, dstMailbox string) (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
return c.Transactf("uid move %s %s", uidSet.String(), astring(dstMailbox))
}
// Replace replaces a message from the currently selected mailbox with a
// new/different version of the message in the named mailbox, which may be the
// same or different than the currently selected mailbox.
// MSNCopy adds messages from the sequences in the sequence set in the
// selected/active mailbox to destMailbox using the IMAP4 "COPY" command.
//
// Num is a message sequence number. "*" references the last message.
// Method [Conn.UIDCopy], operating on UIDs instead of sequence numbers, should be
// preferred.
func (c *Conn) MSNCopy(seqSet string, destMailbox string) (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
return c.transactf("copy %s %s", seqSet, astring(destMailbox))
}
// UIDCopy is like copy, but operates on UIDs, using the IMAP4 "UID COPY" command.
//
// Servers must have announced the REPLACE capability.
func (c *Conn) Replace(msgseq string, mailbox string, msg Append) (untagged []Untagged, result Result, rerr error) {
// Required capability: "UIDPLUS" or "IMAP4rev2".
func (c *Conn) UIDCopy(uidSet string, destMailbox string) (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
return c.transactf("uid copy %s %s", uidSet, astring(destMailbox))
}
// MSNSearch returns messages from the sequence set in the selected/active mailbox
// that match the search critera using the IMAP4 "SEARCH" command.
//
// Method [Conn.UIDSearch], operating on UIDs instead of sequence numbers, should be
// preferred.
func (c *Conn) MSNSearch(seqSet string, criteria string) (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
return c.transactf("seach %s %s", seqSet, criteria)
}
// UIDSearch returns messages from the uid set in the selected/active mailbox that
// match the search critera using the IMAP4 "SEARCH" command.
//
// Criteria is a search program, see RFC 9051 and RFC 3501 for details.
//
// Required capability: "UIDPLUS" or "IMAP4rev2".
func (c *Conn) UIDSearch(seqSet string, criteria string) (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
return c.transactf("seach %s %s", seqSet, criteria)
}
// MSNMove moves messages from the sequence set in the selected/active mailbox to
// destMailbox using the IMAP4 "MOVE" command.
//
// Required capability: "MOVE" or "IMAP4rev2".
//
// Method [Conn.UIDMove], operating on UIDs instead of sequence numbers, should be
// preferred.
func (c *Conn) MSNMove(seqSet string, destMailbox string) (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
return c.transactf("move %s %s", seqSet, astring(destMailbox))
}
// UIDMove is like move, but operates on UIDs, using the IMAP4 "UID MOVE" command.
//
// Required capability: "MOVE" or "IMAP4rev2".
func (c *Conn) UIDMove(uidSet string, destMailbox string) (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
return c.transactf("uid move %s %s", uidSet, astring(destMailbox))
}
// MSNReplace is like the preferred [Conn.UIDReplace], but operates on a message
// sequence number (MSN) instead of a UID.
//
// Required capability: "REPLACE".
//
// Method [Conn.UIDReplace], operating on UIDs instead of sequence numbers, should be
// preferred.
func (c *Conn) MSNReplace(msgseq string, mailbox string, msg Append) (resp Response, rerr error) {
// todo: parse msgseq, must be nznumber, with a known msgseq. or "*" with at least one message.
return c.replace("replace", msgseq, mailbox, msg)
}
// UIDReplace is like Replace, but operates on a UID instead of message
// sequence number.
func (c *Conn) UIDReplace(uid string, mailbox string, msg Append) (untagged []Untagged, result Result, rerr error) {
// UIDReplace uses the IMAP4 "UID REPLACE" command to replace a message from the
// selected/active mailbox with a new/different version of the message in the named
// mailbox, which may be the same or different than the selected mailbox.
//
// The replaced message is indicated by uid.
//
// Required capability: "REPLACE".
func (c *Conn) UIDReplace(uid string, mailbox string, msg Append) (resp Response, rerr error) {
// todo: parse uid, must be nznumber, with a known uid. or "*" with at least one message.
return c.replace("uid replace", uid, mailbox, msg)
}
func (c *Conn) replace(cmd string, num string, mailbox string, msg Append) (untagged []Untagged, result Result, rerr error) {
defer c.recover(&rerr)
func (c *Conn) replace(cmd string, num string, mailbox string, msg Append) (resp Response, rerr error) {
defer c.recover(&rerr, &resp)
// todo: use synchronizing literal for larger messages.
@ -478,7 +587,7 @@ func (c *Conn) replace(cmd string, num string, mailbox string, msg Append) (unta
}
// todo: only use literal8 if needed, possibly with "UTF8()"
// todo: encode mailbox
err := c.Commandf("", "%s %s %s (%s)%s ~{%d+}", cmd, num, astring(mailbox), strings.Join(msg.Flags, " "), date, msg.Size)
err := c.WriteCommandf("", "%s %s %s (%s)%s ~{%d+}", cmd, num, astring(mailbox), strings.Join(msg.Flags, " "), date, msg.Size)
c.xcheckf(err, "writing replace command")
defer c.xtracewrite(mlog.LevelTracedata)()
@ -489,5 +598,5 @@ func (c *Conn) replace(cmd string, num string, mailbox string, msg Append) (unta
fmt.Fprintf(c.xbw, "\r\n")
c.xflush()
return c.Response()
return c.responseOK()
}

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

View File

@ -50,14 +50,14 @@ func testAppend(t *testing.T, uidonly bool) {
tc2.client.Select("inbox")
tc2.transactf("no", "append nobox (\\Seen) \" 1-Jan-2022 10:10:00 +0100\" {1}")
tc2.xcode("TRYCREATE")
tc2.xcodeWord("TRYCREATE")
tc2.transactf("no", "append expungebox (\\Seen) {1}")
tc2.xcode("TRYCREATE")
tc2.xcodeWord("TRYCREATE")
tc2.transactf("ok", "append inbox (\\Seen Label1 $label2) \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
tc2.xuntagged(imapclient.UntaggedExists(1))
tc2.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("1")})
tc2.xcode(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("1")})
tc.transactf("ok", "noop")
flags := imapclient.FetchFlags{`\Seen`, "$label2", "label1"}
@ -67,11 +67,11 @@ func testAppend(t *testing.T, uidonly bool) {
tc2.transactf("ok", "append inbox (\\Seen) \" 1-Jan-2022 10:10:00 +0100\" UTF8 (~{47+}\r\ncontent-type: just completely invalid;;\r\n\r\ntest)")
tc2.xuntagged(imapclient.UntaggedExists(2))
tc2.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("2")})
tc2.xcode(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("2")})
tc2.transactf("ok", "append inbox (\\Seen) \" 1-Jan-2022 10:10:00 +0100\" UTF8 (~{31+}\r\ncontent-type: text/plain;\n\ntest)")
tc2.xuntagged(imapclient.UntaggedExists(3))
tc2.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("3")})
tc2.xcode(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("3")})
// Messages that we cannot parse are marked as application/octet-stream. Perhaps
// the imap client knows how to deal with them.
@ -92,7 +92,7 @@ func testAppend(t *testing.T, uidonly bool) {
tc.transactf("ok", "noop") // Flush pending untagged responses.
tc.transactf("ok", "append inbox {6+}\r\ntest\r\n ~{6+}\r\ntost\r\n")
tc.xuntagged(imapclient.UntaggedExists(5))
tc.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("4:5")})
tc.xcode(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("4:5")})
// Cancelled with zero-length message.
tc.transactf("no", "append inbox {6+}\r\ntest\r\n {0+}\r\n")
@ -106,7 +106,7 @@ func testAppend(t *testing.T, uidonly bool) {
tclimit.xuntagged(imapclient.UntaggedExists(1))
// Second message would take account past limit.
tclimit.transactf("no", "append inbox (\\Seen Label1 $label2) \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
tclimit.xcode("OVERQUOTA")
tclimit.xcodeWord("OVERQUOTA")
// Empty mailbox.
if uidonly {
@ -119,10 +119,10 @@ func testAppend(t *testing.T, uidonly bool) {
// Multiappend with first message within quota, and second message with sync
// literal causing quota error. Request should get error response immediately.
tclimit.transactf("no", "append inbox {1+}\r\nx {100000}")
tclimit.xcode("OVERQUOTA")
tclimit.xcodeWord("OVERQUOTA")
// Again, but second message now with non-sync literal, which is fully consumed by server.
tclimit.client.Commandf("", "append inbox {1+}\r\nx {4000+}")
tclimit.client.WriteCommandf("", "append inbox {1+}\r\nx {4000+}")
buf := make([]byte, 4000, 4002)
for i := range buf {
buf[i] = 'x'
@ -131,5 +131,5 @@ func testAppend(t *testing.T, uidonly bool) {
_, err := tclimit.client.Write(buf)
tclimit.check(err, "write append message")
tclimit.response("no")
tclimit.xcode("OVERQUOTA")
tclimit.xcodeWord("OVERQUOTA")
}

View File

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

View File

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

View File

@ -38,15 +38,15 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) {
// Check basic requirements of CONDSTORE.
capability := "Condstore"
capability := imapclient.CapCondstore
if qresync {
capability = "Qresync"
capability = imapclient.CapQresync
}
tc.login("mjl@mox.example", password0)
tc.client.Enable(capability)
tc.transactf("ok", "Select inbox")
tc.xuntaggedOpt(false, imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(2), More: "x"}})
tc.xuntaggedOpt(false, imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeHighestModSeq(2), Text: "x"})
// First some tests without any messages.
@ -133,19 +133,19 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) {
// The ones we insert below will start with modseq 3. So we'll have messages with modseq 1 and 3-6.
tc.transactf("ok", "Append inbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
tc.xuntagged(imapclient.UntaggedExists(4))
tc.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("4")})
tc.xcode(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("4")})
tc.transactf("ok", "Append otherbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
tc.xuntagged()
tc.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 3, UIDs: xparseUIDRange("1")})
tc.xcode(imapclient.CodeAppendUID{UIDValidity: 3, UIDs: xparseUIDRange("1")})
tc.transactf("ok", "Append inbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
tc.xuntagged(imapclient.UntaggedExists(5))
tc.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("5")})
tc.xcode(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("5")})
tc.transactf("ok", "Append inbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
tc.xuntagged(imapclient.UntaggedExists(6))
tc.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("6")})
tc.xcode(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("6")})
tc2.transactf("ok", "Noop")
noflags := imapclient.FetchFlags(nil)
@ -181,10 +181,10 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) {
// Check highestmodseq when we select.
tc.transactf("ok", "Examine otherbox")
tc.xuntaggedOpt(false, imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(clientModseq + 2), More: "x"}})
tc.xuntaggedOpt(false, imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeHighestModSeq(clientModseq + 2), Text: "x"})
tc.transactf("ok", "Select inbox")
tc.xuntaggedOpt(false, imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(clientModseq + 4), More: "x"}})
tc.xuntaggedOpt(false, imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeHighestModSeq(clientModseq + 4), Text: "x"})
clientModseq += 4
@ -225,13 +225,13 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) {
if !uidonly {
// unchangedsince 0 never passes the check. ../rfc/7162:640
tc.transactf("ok", `Store 1 (Unchangedsince 0) +Flags ()`)
tc.xcodeArg(imapclient.CodeModified(xparseNumSet("1")))
tc.xcode(imapclient.CodeModified(xparseNumSet("1")))
tc.xuntagged(tc.untaggedFetch(1, 1, noflags, imapclient.FetchModSeq(1)))
}
// Modseq is 2 for first condstore-aware-appended message, so also no match.
tc.transactf("ok", `Uid Store 4 (Unchangedsince 1) +Flags ()`)
tc.xcodeArg(imapclient.CodeModified(xparseNumSet("4")))
tc.xcode(imapclient.CodeModified(xparseNumSet("4")))
if uidonly {
tc.transactf("ok", `Uid Store 1 (Unchangedsince 1) +Flags (label1)`)
@ -239,7 +239,7 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) {
// Modseq is 1 for original message.
tc.transactf("ok", `Store 1 (Unchangedsince 1) +Flags (label1)`)
}
tc.xcode("") // No MODIFIED.
tc.xcode(nil) // No MODIFIED.
clientModseq++
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{"label1"}, imapclient.FetchModSeq(clientModseq)))
tc2.transactf("ok", "Noop")
@ -255,7 +255,7 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) {
// modseq change made in the first application. ../rfc/7162:823
tc.transactf("ok", `Uid Store 1,1 (Unchangedsince %d) -Flags (label1)`, clientModseq)
clientModseq++
tc.xcode("") // No MODIFIED.
tc.xcode(nil) // No MODIFIED.
tc.xuntagged(
tc.untaggedFetch(1, 1, imapclient.FetchFlags(nil), imapclient.FetchModSeq(clientModseq)),
)
@ -273,7 +273,7 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) {
// Modify without actually changing flags, there will be no new modseq and no broadcast.
tc.transactf("ok", `Store 1 (Unchangedsince %d) -Flags (label1)`, clientModseq)
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags(nil), imapclient.FetchModSeq(clientModseq)))
tc.xcode("") // No MODIFIED.
tc.xcode(nil) // No MODIFIED.
tc2.transactf("ok", "Noop")
tc2.xuntagged()
tc3.transactf("ok", "Noop")
@ -318,7 +318,7 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) {
} else {
tc.xuntagged(imapclient.UntaggedExpunge(3), imapclient.UntaggedExpunge(3))
}
tc.xcodeArg(imapclient.CodeHighestModSeq(clientModseq))
tc.xcode(imapclient.CodeHighestModSeq(clientModseq))
tc2.transactf("ok", "Noop")
if uidonly {
tc2.xuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("3:4")})
@ -340,7 +340,7 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) {
tc.transactf("ok", "Select inbox")
tc.xuntaggedOpt(false,
imapclient.UntaggedExists(4),
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(clientModseq), More: "x"}},
imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeHighestModSeq(clientModseq), Text: "x"},
)
if !uidonly {
@ -367,16 +367,16 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) {
tc.transactf("ok", "Search Return (Min Max All) 1:* Modseq 8")
tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 1, All: esearchall0("1"), ModSeq: 8})
tc.transactf("ok", "Search Return (Min Max All) 1:* Modseq 9")
tc.xuntagged(imapclient.UntaggedEsearch{Tag: tc.client.LastTag})
tc.xuntagged(imapclient.UntaggedEsearch{Tag: tc.client.LastTag()})
}
// store, cannot modify expunged messages.
tc.transactf("ok", `Uid Store 3,4 (Unchangedsince %d) +Flags (label2)`, clientModseq)
tc.xuntagged()
tc.xcode("") // Not MODIFIED.
tc.xcode(nil) // Not MODIFIED.
tc.transactf("ok", `Uid Store 3,4 +Flags (label2)`)
tc.xuntagged()
tc.xcode("") // Not MODIFIED.
tc.xcode(nil) // Not MODIFIED.
// Check all condstore-enabling commands (and their syntax), ../rfc/7162:368
@ -497,13 +497,13 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) {
clientModseq++
if qresync {
tc.xuntaggedOpt(false, imapclient.UntaggedVanished{UIDs: xparseNumSet("2")})
tc.xcodeArg(imapclient.CodeHighestModSeq(clientModseq))
tc.xcode(imapclient.CodeHighestModSeq(clientModseq))
} else if uidonly {
tc.xuntaggedOpt(false, imapclient.UntaggedVanished{UIDs: xparseNumSet("2")})
tc.xcode("")
tc.xcode(nil)
} else {
tc.xuntaggedOpt(false, imapclient.UntaggedExpunge(2))
tc.xcode("")
tc.xcode(nil)
}
tc2.transactf("ok", "Noop")
if uidonly {
@ -615,21 +615,21 @@ func testQresync(t *testing.T, tc *testconn, uidonly bool, clientModseq int64) {
flags := strings.Split(`\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent l1 l2 l3 l4 l5 l6 l7 l8 label1`, " ")
permflags := strings.Split(`\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent \*`, " ")
uflags := imapclient.UntaggedFlags(flags)
upermflags := imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "PERMANENTFLAGS", CodeArg: imapclient.CodeList{Code: "PERMANENTFLAGS", Args: permflags}, More: "x"}}
upermflags := imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodePermanentFlags(permflags), Text: "x"}
baseUntagged := []imapclient.Untagged{
uflags,
upermflags,
imapclient.UntaggedList{Separator: '/', Mailbox: "Inbox"},
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UIDNEXT", CodeArg: imapclient.CodeUint{Code: "UIDNEXT", Num: 7}, More: "x"}},
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UIDVALIDITY", CodeArg: imapclient.CodeUint{Code: "UIDVALIDITY", Num: 1}, More: "x"}},
imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeUIDNext(7), Text: "x"},
imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeUIDValidity(1), Text: "x"},
imapclient.UntaggedRecent(0),
imapclient.UntaggedExists(4),
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(clientModseq), More: "x"}},
imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeHighestModSeq(clientModseq), Text: "x"},
}
if !uidonly {
baseUntagged = append(baseUntagged,
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UNSEEN", CodeArg: imapclient.CodeUint{Code: "UNSEEN", Num: 1}, More: "x"}},
imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeUnseen(1), Text: "x"},
)
}
@ -752,7 +752,7 @@ func testQresync(t *testing.T, tc *testconn, uidonly bool, clientModseq int64) {
tc.transactf("ok", "Select inbox (Qresync (1 10 (1,3,6 1,3,6)))")
tc.xuntagged(
makeUntagged(
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "ALERT", More: "Synchronization inconsistency in client detected. Client tried to sync with a UID that was removed at or after the MODSEQ it sent in the request. Sending all historic message removals for selected mailbox. Full synchronization recommended."}},
imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeWord("ALERT"), Text: "Synchronization inconsistency in client detected. Client tried to sync with a UID that was removed at or after the MODSEQ it sent in the request. Sending all historic message removals for selected mailbox. Full synchronization recommended."},
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
tc.untaggedFetch(4, 6, noflags, imapclient.FetchModSeq(clientModseq)),
)...,
@ -765,7 +765,7 @@ func testQresync(t *testing.T, tc *testconn, uidonly bool, clientModseq int64) {
tc.transactf("ok", "Select inbox (Qresync (1 18 (1,3,6 1,3,6)))")
tc.xuntagged(
makeUntagged(
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "ALERT", More: "Synchronization inconsistency in client detected. Client tried to sync with a UID that was removed at or after the MODSEQ it sent in the request. Sending all historic message removals for selected mailbox. Full synchronization recommended."}},
imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeWord("ALERT"), Text: "Synchronization inconsistency in client detected. Client tried to sync with a UID that was removed at or after the MODSEQ it sent in the request. Sending all historic message removals for selected mailbox. Full synchronization recommended."},
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
tc.untaggedFetch(4, 6, noflags, imapclient.FetchModSeq(clientModseq)),
)...,
@ -786,7 +786,7 @@ func testQresyncHistory(t *testing.T, uidonly bool) {
defer tc.close()
tc.login("mjl@mox.example", password0)
tc.client.Enable("Qresync")
tc.client.Enable(imapclient.CapQresync)
tc.transactf("ok", "Append inbox {1+}\r\nx")
tc.transactf("ok", "Append inbox {1+}\r\nx") // modseq 6
tc.transactf("ok", "Append inbox {1+}\r\nx")
@ -799,16 +799,16 @@ func testQresyncHistory(t *testing.T, uidonly bool) {
flags := strings.Split(`\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent`, " ")
permflags := strings.Split(`\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent \*`, " ")
uflags := imapclient.UntaggedFlags(flags)
upermflags := imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "PERMANENTFLAGS", CodeArg: imapclient.CodeList{Code: "PERMANENTFLAGS", Args: permflags}, More: "x"}}
upermflags := imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodePermanentFlags(permflags), Text: "x"}
baseUntagged := []imapclient.Untagged{
uflags,
upermflags,
imapclient.UntaggedList{Separator: '/', Mailbox: "Inbox"},
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UIDNEXT", CodeArg: imapclient.CodeUint{Code: "UIDNEXT", Num: 4}, More: "x"}},
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UIDVALIDITY", CodeArg: imapclient.CodeUint{Code: "UIDVALIDITY", Num: 1}, More: "x"}},
imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeUIDNext(4), Text: "x"},
imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeUIDValidity(1), Text: "x"},
imapclient.UntaggedRecent(0),
imapclient.UntaggedExists(1),
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(10), More: "x"}},
imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeHighestModSeq(10), Text: "x"},
}
makeUntagged := func(l ...imapclient.Untagged) []imapclient.Untagged {

View File

@ -44,16 +44,16 @@ func testCopy(t *testing.T, uidonly bool) {
tc.transactf("ok", "uid copy 3:* Trash")
} else {
tc.transactf("no", "copy 1 nonexistent")
tc.xcode("TRYCREATE")
tc.xcodeWord("TRYCREATE")
tc.transactf("no", "copy 1 expungebox")
tc.xcode("TRYCREATE")
tc.xcodeWord("TRYCREATE")
tc.transactf("no", "copy 1 inbox") // Cannot copy to same mailbox.
tc2.transactf("ok", "noop") // Drain.
tc.transactf("ok", "copy 1:* Trash")
tc.xcodeArg(imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}, To: []imapclient.NumRange{{First: 1, Last: uint32ptr(2)}}})
tc.xcode(mustParseCode("COPYUID 1 3:4 1:2"))
}
tc2.transactf("ok", "noop")
tc2.xuntagged(
@ -64,7 +64,7 @@ func testCopy(t *testing.T, uidonly bool) {
tc.transactf("no", "uid copy 1,2 Trash") // No match.
tc.transactf("ok", "uid copy 4,3 Trash")
tc.xcodeArg(imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}, To: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}})
tc.xcode(mustParseCode("COPYUID 1 3:4 3:4"))
tc2.transactf("ok", "noop")
tc2.xuntagged(
imapclient.UntaggedExists(4),
@ -81,5 +81,5 @@ func testCopy(t *testing.T, uidonly bool) {
tclimit.xuntagged(imapclient.UntaggedExists(1))
// Second message would take account past limit.
tclimit.transactf("no", "uid copy 1:* Trash")
tclimit.xcode("OVERQUOTA")
tclimit.xcodeWord("OVERQUOTA")
}

View File

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

View File

@ -43,9 +43,9 @@ func testDelete(t *testing.T, uidonly bool) {
// ../rfc/9051:2000
tc.transactf("no", "delete a") // Still has child.
tc.xcode("HASCHILDREN")
tc.xcodeWord("HASCHILDREN")
tc3.client.Enable("IMAP4rev2") // For \NonExistent support.
tc3.client.Enable(imapclient.CapIMAP4rev2) // For \NonExistent support.
tc.transactf("ok", "delete a/b")
tc2.transactf("ok", "noop")
tc2.xuntagged() // No IMAP4rev2, no \NonExistent.

View File

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

View File

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

View File

@ -303,13 +303,13 @@ func (c *conn) xcheckMetadataSize(tx *bstore.Tx) {
n++
if n > metadataMaxKeys {
// ../rfc/5464:590
xusercodeErrorf("METADATA TOOMANY", "too many metadata entries, 1000 allowed in total")
xusercodeErrorf("METADATA (TOOMANY)", "too many metadata entries, 1000 allowed in total")
}
size += len(a.Key) + len(a.Value)
if size > metadataMaxSize {
// ../rfc/5464:585 We only have a max total size limit, not per entry. We'll
// mention the max total size.
xusercodeErrorf(fmt.Sprintf("METADATA MAXSIZE %d", metadataMaxSize), "metadata entry values too large, total maximum size is 1MB")
xusercodeErrorf(fmt.Sprintf("METADATA (MAXSIZE %d)", metadataMaxSize), "metadata entry values too large, total maximum size is 1MB")
}
return nil
})

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

View File

@ -56,10 +56,10 @@ func testMove(t *testing.T, uidonly bool) {
tc.client.Select("inbox")
tc.transactf("no", "move 1 nonexistent")
tc.xcode("TRYCREATE")
tc.xcodeWord("TRYCREATE")
tc.transactf("no", "move 1 expungebox")
tc.xcode("TRYCREATE")
tc.xcodeWord("TRYCREATE")
tc.transactf("no", "move 1 inbox") // Cannot move to same mailbox.
@ -68,7 +68,7 @@ func testMove(t *testing.T, uidonly bool) {
tc.transactf("ok", "move 1:* Trash")
tc.xuntagged(
imapclient.UntaggedResult{Status: "OK", RespText: imapclient.RespText{Code: "COPYUID", CodeArg: imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}, To: []imapclient.NumRange{{First: 1, Last: uint32ptr(2)}}}, More: "moved"}},
imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}, To: []imapclient.NumRange{{First: 1, Last: uint32ptr(2)}}}, Text: "moved"},
imapclient.UntaggedExpunge(1),
imapclient.UntaggedExpunge(1),
)
@ -92,12 +92,12 @@ func testMove(t *testing.T, uidonly bool) {
tc.transactf("ok", "uid move 6:5 Trash")
if uidonly {
tc.xuntagged(
imapclient.UntaggedResult{Status: "OK", RespText: imapclient.RespText{Code: "COPYUID", CodeArg: imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 5, Last: uint32ptr(6)}}, To: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}}, More: "moved"}},
imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 5, Last: uint32ptr(6)}}, To: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}}, Text: "moved"},
imapclient.UntaggedVanished{UIDs: xparseNumSet("5:6")},
)
} else {
tc.xuntagged(
imapclient.UntaggedResult{Status: "OK", RespText: imapclient.RespText{Code: "COPYUID", CodeArg: imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 5, Last: uint32ptr(6)}}, To: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}}, More: "moved"}},
imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 5, Last: uint32ptr(6)}}, To: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}}, Text: "moved"},
imapclient.UntaggedExpunge(1),
imapclient.UntaggedExpunge(1),
)

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 (mailboxName)) (Selected-Delayed (mailboxName))") // Duplicate selected.
tc.transactf("no", "Notify Set Status (Selected (annotationChange))") // We don't implement annotation change.
tc.xcode("BADEVENT")
tc.xcode(imapclient.CodeBadEvent{"MessageNew", "MessageExpunge", "FlagChange", "MailboxName", "SubscriptionChange", "MailboxMetadataChange", "ServerMetadataChange"})
tc.transactf("no", "Notify Set Status (Personal (unknownEvent))")
tc.xcode("BADEVENT")
tc.xcode(imapclient.CodeBadEvent{"MessageNew", "MessageExpunge", "FlagChange", "MailboxName", "SubscriptionChange", "MailboxMetadataChange", "ServerMetadataChange"})
tc2 := startNoSwitchboard(t, uidonly)
defer tc2.closeNoWait()
@ -76,7 +76,7 @@ func testNotify(t *testing.T, uidonly bool) {
// Enable notify, will first result in a the pending changes, then status.
tc.transactf("ok", "Notify Set Status (Selected (messageNew (Uid Modseq Bodystructure Preview) messageExpunge flagChange)) (personal (messageNew messageExpunge flagChange mailboxName subscriptionChange mailboxMetadataChange serverMetadataChange))")
tc.xuntagged(
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(modseq), More: "after condstore-enabling command"}},
imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeHighestModSeq(modseq), Text: "after condstore-enabling command"},
// note: no status for Inbox since it is selected.
imapclient.UntaggedStatus{Mailbox: "Drafts", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 0, imapclient.StatusUIDNext: 1, imapclient.StatusUIDValidity: 1, imapclient.StatusUnseen: 0, imapclient.StatusHighestModSeq: 2}},
imapclient.UntaggedStatus{Mailbox: "Sent", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 0, imapclient.StatusUIDNext: 1, imapclient.StatusUIDValidity: 1, imapclient.StatusUnseen: 0, imapclient.StatusHighestModSeq: 2}},
@ -108,7 +108,12 @@ func testNotify(t *testing.T, uidonly bool) {
Octets: 21,
},
Lines: 1,
Ext: &imapclient.BodyExtension1Part{},
Ext: &imapclient.BodyExtension1Part{
Disposition: ptr((*string)(nil)),
DispositionParams: ptr([][2]string(nil)),
Language: ptr([]string(nil)),
Location: ptr((*string)(nil)),
},
},
imapclient.BodyTypeText{
MediaType: "TEXT",
@ -118,12 +123,21 @@ func testNotify(t *testing.T, uidonly bool) {
Octets: 15,
},
Lines: 1,
Ext: &imapclient.BodyExtension1Part{},
Ext: &imapclient.BodyExtension1Part{
Disposition: ptr((*string)(nil)),
DispositionParams: ptr([][2]string(nil)),
Language: ptr([]string(nil)),
Location: ptr((*string)(nil)),
},
},
},
MediaSubtype: "ALTERNATIVE",
Ext: &imapclient.BodyExtensionMpart{
Params: [][2]string{{"BOUNDARY", "x"}},
Disposition: ptr((*string)(nil)), // Present but nil.
DispositionParams: ptr([][2]string(nil)),
Language: ptr([]string(nil)),
Location: ptr((*string)(nil)),
},
},
},
@ -413,12 +427,7 @@ func testNotify(t *testing.T, uidonly bool) {
// modseq++
tc.readuntagged(
imapclient.UntaggedExists(3),
imapclient.UntaggedResult{
Status: "NO",
RespText: imapclient.RespText{
More: "generating notify fetch response: requested part does not exist",
},
},
imapclient.UntaggedResult{Status: "NO", Text: "generating notify fetch response: requested part does not exist"},
tc.untaggedFetchUID(3, 4),
)
@ -457,15 +466,7 @@ func testNotifyOverflow(t *testing.T, uidonly bool) {
tc2.client.Append("inbox", makeAppend(searchMsg))
tc.transactf("ok", "noop")
tc.xuntagged(
imapclient.UntaggedResult{
Status: "OK",
RespText: imapclient.RespText{
Code: "NOTIFICATIONOVERFLOW",
More: "out of sync after too many pending changes",
},
},
)
tc.xuntagged(imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeWord("NOTIFICATIONOVERFLOW"), Text: "out of sync after too many pending changes"})
// Won't be getting any more notifications until we enable them again with NOTIFY.
tc2.client.Append("inbox", makeAppend(searchMsg))
@ -500,15 +501,7 @@ func testNotifyOverflow(t *testing.T, uidonly bool) {
tc2.client.UIDStoreFlagsAdd("1", true, `\Seen`)
tc2.client.UIDStoreFlagsClear("1", true, `\Seen`)
tc.transactf("ok", "noop")
tc.xuntagged(
imapclient.UntaggedResult{
Status: "OK",
RespText: imapclient.RespText{
Code: "NOTIFICATIONOVERFLOW",
More: "out of sync after too many pending changes for selected mailbox",
},
},
)
tc.xuntagged(imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeWord("NOTIFICATIONOVERFLOW"), Text: "out of sync after too many pending changes for selected mailbox"})
// Again, no new notifications until we select and enable again.
tc2.client.UIDStoreFlagsAdd("1", true, `\Seen`)

View File

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

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

View File

@ -57,7 +57,7 @@ func (tc *testconn) xsearchmodseq(modseq int64, nums ...uint32) {
func (tc *testconn) xesearch(exp imapclient.UntaggedEsearch) {
tc.t.Helper()
exp.Tag = tc.client.LastTag
exp.Tag = tc.client.LastTag()
tc.xuntagged(exp)
}
@ -298,11 +298,8 @@ func testSearch(t *testing.T, uidonly bool) {
inprogress := func(cur, goal uint32) imapclient.UntaggedResult {
return imapclient.UntaggedResult{
Status: "OK",
RespText: imapclient.RespText{
Code: "INPROGRESS",
CodeArg: imapclient.CodeInProgress{Tag: "tag1", Current: &cur, Goal: &goal},
More: "still searching",
},
Code: imapclient.CodeInProgress{Tag: "tag1", Current: &cur, Goal: &goal},
Text: "still searching",
}
}
tc.xuntagged(
@ -408,7 +405,7 @@ func testSearch(t *testing.T, uidonly bool) {
}
// Do a seemingly old-style search command with IMAP4rev2 enabled. We'll still get ESEARCH responses.
tc.client.Enable("IMAP4rev2")
tc.client.Enable(imapclient.CapIMAP4rev2)
if !uidonly {
tc.transactf("ok", `search undraft`)
@ -566,11 +563,8 @@ func testSearchMulti(t *testing.T, selected, uidonly bool) {
inprogress := func(cur, goal uint32) imapclient.UntaggedResult {
return imapclient.UntaggedResult{
Status: "OK",
RespText: imapclient.RespText{
Code: "INPROGRESS",
CodeArg: imapclient.CodeInProgress{Tag: "Tag1", Current: &cur, Goal: &goal},
More: "still searching",
},
Code: imapclient.CodeInProgress{Tag: "Tag1", Current: &cur, Goal: &goal},
Text: "still searching",
}
}
tc.cmdf("Tag1", `Esearch In (Personal) Return () All`)

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

1056
testdata/imapclient/fuzzseed.txt vendored Normal file

File diff suppressed because it is too large Load Diff