mirror of
https://github.com/mjl-/mox.git
synced 2025-06-28 01:48:15 +03:00
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:
parent
2c1283f032
commit
e7b562e3f2
1
Makefile
1
Makefile
@ -79,6 +79,7 @@ fuzz:
|
|||||||
go test -fullpath -fuzz . -fuzztime 5m ./dmarc
|
go test -fullpath -fuzz . -fuzztime 5m ./dmarc
|
||||||
go test -fullpath -fuzz . -fuzztime 5m ./dmarcrpt
|
go test -fullpath -fuzz . -fuzztime 5m ./dmarcrpt
|
||||||
go test -fullpath -fuzz . -parallel 1 -fuzztime 5m ./imapserver
|
go test -fullpath -fuzz . -parallel 1 -fuzztime 5m ./imapserver
|
||||||
|
go test -fullpath -fuzz . -fuzztime 5m ./imapclient
|
||||||
go test -fullpath -fuzz . -parallel 1 -fuzztime 5m ./junk
|
go test -fullpath -fuzz . -parallel 1 -fuzztime 5m ./junk
|
||||||
go test -fullpath -fuzz FuzzParseRecord -fuzztime 5m ./mtasts
|
go test -fullpath -fuzz FuzzParseRecord -fuzztime 5m ./mtasts
|
||||||
go test -fullpath -fuzz FuzzParsePolicy -fuzztime 5m ./mtasts
|
go test -fullpath -fuzz FuzzParsePolicy -fuzztime 5m ./mtasts
|
||||||
|
@ -1,34 +1,80 @@
|
|||||||
/*
|
/*
|
||||||
Package imapclient provides an IMAP4 client, primarily for testing the IMAP4 server.
|
Package imapclient provides an IMAP4 client implementing IMAP4rev1 (RFC 3501),
|
||||||
|
IMAP4rev2 (RFC 9051) and various extensions.
|
||||||
|
|
||||||
Commands can be sent to the server free-form, but responses are parsed strictly.
|
Warning: Currently primarily for testing the mox IMAP4 server. Behaviour that
|
||||||
Behaviour that may not be required by the IMAP4 specification may be expected by
|
may not be required by the IMAP4 specification may be expected by this client.
|
||||||
this client.
|
|
||||||
|
See [Conn] for a high-level client for executing IMAP commands. Use its embedded
|
||||||
|
[Proto] for lower-level writing of commands and reading of responses.
|
||||||
*/
|
*/
|
||||||
package imapclient
|
package imapclient
|
||||||
|
|
||||||
/*
|
|
||||||
- Try to keep the parsing method names and the types similar to the ABNF names in the RFCs.
|
|
||||||
|
|
||||||
- todo: have mode for imap4rev1 vs imap4rev2, refusing what is not allowed. we are accepting too much now.
|
|
||||||
- todo: stricter parsing. xnonspace() and xword() should be replaced by proper parsers.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"reflect"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/mjl-/mox/mlog"
|
"github.com/mjl-/mox/mlog"
|
||||||
"github.com/mjl-/mox/moxio"
|
"github.com/mjl-/mox/moxio"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Conn is an IMAP connection to a server.
|
// Conn is an connection to an IMAP server.
|
||||||
|
//
|
||||||
|
// Method names on Conn are the names of IMAP commands. CloseMailbox, which
|
||||||
|
// executes the IMAP CLOSE command, is an exception. The Close method closes the
|
||||||
|
// connection.
|
||||||
|
//
|
||||||
|
// The methods starting with MSN are the original (old) IMAP commands. The variants
|
||||||
|
// starting with UID should almost always be used instead, if available.
|
||||||
|
//
|
||||||
|
// The methods on Conn typically return errors of type Error or Response. Error
|
||||||
|
// represents protocol and i/o level errors, including io.ErrDeadlineExceeded and
|
||||||
|
// various errors for closed connections. Response is returned as error if the IMAP
|
||||||
|
// result is NO or BAD instead of OK. The responses returned by the IMAP command
|
||||||
|
// methods can also be non-zero on errors. Callers may wish to process any untagged
|
||||||
|
// responses.
|
||||||
|
//
|
||||||
|
// The IMAP command methods defined on Conn don't interpret the untagged responses
|
||||||
|
// except for untagged CAPABILITY and untagged ENABLED responses, and the
|
||||||
|
// CAPABILITY response code. Fields CapAvailable and CapEnabled are updated when
|
||||||
|
// those untagged responses are received.
|
||||||
|
//
|
||||||
|
// Capabilities indicate which optional IMAP functionality is supported by a
|
||||||
|
// server. Capabilities are typically implicitly enabled when the client sends a
|
||||||
|
// command using syntax of an optional extension. Extensions without new syntax
|
||||||
|
// from client to server, but with new behaviour or syntax from server to client,
|
||||||
|
// the client needs to explicitly enable the capability with the ENABLE command,
|
||||||
|
// see the Enable method.
|
||||||
type Conn struct {
|
type Conn struct {
|
||||||
|
// If true, server sent a PREAUTH tag and the connection is already authenticated,
|
||||||
|
// e.g. based on TLS certificate authentication.
|
||||||
|
Preauth bool
|
||||||
|
|
||||||
|
// Capabilities available at server, from CAPABILITY command or response code.
|
||||||
|
CapAvailable []Capability
|
||||||
|
// Capabilities marked as enabled by the server, typically after an ENABLE command.
|
||||||
|
CapEnabled []Capability
|
||||||
|
|
||||||
|
// Proto provides lower-level functions for interacting with the IMAP connection,
|
||||||
|
// such as reading and writing individual lines/commands/responses.
|
||||||
|
Proto
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proto provides low-level operations for writing requests and reading responses
|
||||||
|
// on an IMAP connection.
|
||||||
|
//
|
||||||
|
// To implement the IDLE command, write "IDLE" using [Proto.WriteCommandf], then
|
||||||
|
// read a line with [Proto.Readline]. If it starts with "+ ", the connection is in
|
||||||
|
// idle mode and untagged responses can be read using [Proto.ReadUntagged]. If the
|
||||||
|
// line doesn't start with "+ ", use [ParseResult] to interpret it as a response to
|
||||||
|
// IDLE, which should be a NO or BAD. To abort idle mode, write "DONE" using
|
||||||
|
// [Proto.Writelinef] and wait until a result line has been read.
|
||||||
|
type Proto struct {
|
||||||
// Connection, may be original TCP or TLS connection. Reads go through c.br, and
|
// Connection, may be original TCP or TLS connection. Reads go through c.br, and
|
||||||
// writes through c.xbw. The "x" for the writes indicate that failed writes cause
|
// writes through c.xbw. The "x" for the writes indicate that failed writes cause
|
||||||
// an i/o panic, which is either turned into a returned error, or passed on (see
|
// an i/o panic, which is either turned into a returned error, or passed on (see
|
||||||
@ -50,10 +96,7 @@ type Conn struct {
|
|||||||
record bool // If true, bytes read are added to recordBuf. recorded() resets.
|
record bool // If true, bytes read are added to recordBuf. recorded() resets.
|
||||||
recordBuf []byte
|
recordBuf []byte
|
||||||
|
|
||||||
Preauth bool
|
lastTag string
|
||||||
LastTag string
|
|
||||||
CapAvailable map[Capability]struct{} // Capabilities available at server, from CAPABILITY command or response code. All uppercase.
|
|
||||||
CapEnabled map[Capability]struct{} // Capabilities enabled through ENABLE command. All uppercase.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error is a parse or other protocol error.
|
// Error is a parse or other protocol error.
|
||||||
@ -71,26 +114,29 @@ func (e Error) Unwrap() error {
|
|||||||
type Opts struct {
|
type Opts struct {
|
||||||
Logger *slog.Logger
|
Logger *slog.Logger
|
||||||
|
|
||||||
// Error is called for both IMAP-level and connection-level errors. Is allowed to
|
// Error is called for IMAP-level and connection-level errors during the IMAP
|
||||||
|
// command methods on Conn, not for errors in calls on Proto. Error is allowed to
|
||||||
// call panic.
|
// call panic.
|
||||||
Error func(err error)
|
Error func(err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new client on conn.
|
// New initializes a new IMAP client on conn.
|
||||||
|
//
|
||||||
|
// Conn should normally be a TLS connection, typically connected to port 993 of an
|
||||||
|
// IMAP server. Alternatively, conn can be a plain TCP connection to port 143. TLS
|
||||||
|
// should be enabled on plain TCP connections with the [Conn.StartTLS] method.
|
||||||
//
|
//
|
||||||
// The initial untagged greeting response is read and must be "OK" or
|
// The initial untagged greeting response is read and must be "OK" or
|
||||||
// "PREAUTH". If preauth, the connection is already in authenticated state,
|
// "PREAUTH". If preauth, the connection is already in authenticated state,
|
||||||
// typically through TLS client certificate. This is indicated in Conn.Preauth.
|
// typically through TLS client certificate. This is indicated in Conn.Preauth.
|
||||||
//
|
//
|
||||||
// Logging is written to log, in particular IMAP protocol traces are written with
|
// Logging is written to opts.Logger. In particular, IMAP protocol traces are
|
||||||
// prefixes "CR: " and "CW: " (client read/write) as quoted strings at levels
|
// written with prefixes "CR: " and "CW: " (client read/write) as quoted strings at
|
||||||
// Debug-4, with authentication messages at Debug-6 and (user) data at level
|
// levels Debug-4, with authentication messages at Debug-6 and (user) data at level
|
||||||
// Debug-8.
|
// Debug-8.
|
||||||
func New(conn net.Conn, opts *Opts) (client *Conn, rerr error) {
|
func New(conn net.Conn, opts *Opts) (client *Conn, rerr error) {
|
||||||
c := Conn{
|
c := Conn{
|
||||||
conn: conn,
|
Proto: Proto{conn: conn},
|
||||||
CapAvailable: map[Capability]struct{}{},
|
|
||||||
CapEnabled: map[Capability]struct{}{},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var clog *slog.Logger
|
var clog *slog.Logger
|
||||||
@ -109,7 +155,7 @@ func New(conn net.Conn, opts *Opts) (client *Conn, rerr error) {
|
|||||||
c.xtw = moxio.NewTraceWriter(c.log, "CW: ", &c)
|
c.xtw = moxio.NewTraceWriter(c.log, "CW: ", &c)
|
||||||
c.xbw = bufio.NewWriter(c.xtw)
|
c.xbw = bufio.NewWriter(c.xtw)
|
||||||
|
|
||||||
defer c.recover(&rerr)
|
defer c.recoverErr(&rerr)
|
||||||
tag := c.xnonspace()
|
tag := c.xnonspace()
|
||||||
if tag != "*" {
|
if tag != "*" {
|
||||||
c.xerrorf("expected untagged *, got %q", tag)
|
c.xerrorf("expected untagged *, got %q", tag)
|
||||||
@ -121,6 +167,11 @@ func New(conn net.Conn, opts *Opts) (client *Conn, rerr error) {
|
|||||||
if x.Status != OK {
|
if x.Status != OK {
|
||||||
c.xerrorf("greeting, got status %q, expected OK", x.Status)
|
c.xerrorf("greeting, got status %q, expected OK", x.Status)
|
||||||
}
|
}
|
||||||
|
if x.Code != nil {
|
||||||
|
if caps, ok := x.Code.(CodeCapability); ok {
|
||||||
|
c.CapAvailable = caps
|
||||||
|
}
|
||||||
|
}
|
||||||
return &c, nil
|
return &c, nil
|
||||||
case UntaggedPreauth:
|
case UntaggedPreauth:
|
||||||
c.Preauth = true
|
c.Preauth = true
|
||||||
@ -133,13 +184,33 @@ func New(conn net.Conn, opts *Opts) (client *Conn, rerr error) {
|
|||||||
panic("not reached")
|
panic("not reached")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) recover(rerr *error) {
|
func (c *Conn) recoverErr(rerr *error) {
|
||||||
|
c.recover(rerr, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) recover(rerr *error, resp *Response) {
|
||||||
|
if *rerr != nil {
|
||||||
|
if r, ok := (*rerr).(Response); ok && resp != nil {
|
||||||
|
*resp = r
|
||||||
|
}
|
||||||
|
c.errHandle(*rerr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
x := recover()
|
x := recover()
|
||||||
if x == nil {
|
if x == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err, ok := x.(Error)
|
var err error
|
||||||
if !ok {
|
switch e := x.(type) {
|
||||||
|
case Error:
|
||||||
|
err = e
|
||||||
|
case Response:
|
||||||
|
err = e
|
||||||
|
if resp != nil {
|
||||||
|
*resp = e
|
||||||
|
}
|
||||||
|
default:
|
||||||
panic(x)
|
panic(x)
|
||||||
}
|
}
|
||||||
if c.errHandle != nil {
|
if c.errHandle != nil {
|
||||||
@ -148,73 +219,110 @@ func (c *Conn) recover(rerr *error) {
|
|||||||
*rerr = err
|
*rerr = err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) xerrorf(format string, args ...any) {
|
func (p *Proto) recover(rerr *error) {
|
||||||
|
if *rerr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
x := recover()
|
||||||
|
if x == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch e := x.(type) {
|
||||||
|
case Error:
|
||||||
|
*rerr = e
|
||||||
|
default:
|
||||||
|
panic(x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Proto) xerrorf(format string, args ...any) {
|
||||||
panic(Error{fmt.Errorf(format, args...)})
|
panic(Error{fmt.Errorf(format, args...)})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) xcheckf(err error, format string, args ...any) {
|
func (p *Proto) xcheckf(err error, format string, args ...any) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.xerrorf("%s: %w", fmt.Sprintf(format, args...), err)
|
p.xerrorf("%s: %w", fmt.Sprintf(format, args...), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) xcheck(err error) {
|
func (p *Proto) xcheck(err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// xresponse sets resp if err is a Response and resp is not nil.
|
||||||
|
func (p *Proto) xresponse(err error, resp *Response) {
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r, ok := err.(Response); ok && resp != nil {
|
||||||
|
*resp = r
|
||||||
|
}
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
// Write writes directly to underlying connection (TCP, TLS). For internal use
|
// Write writes directly to underlying connection (TCP, TLS). For internal use
|
||||||
// only, to implement io.Writer. Write errors do take the connection's panic mode
|
// only, to implement io.Writer. Write errors do take the connection's panic mode
|
||||||
// into account, i.e. Write can panic.
|
// into account, i.e. Write can panic.
|
||||||
func (c *Conn) Write(buf []byte) (n int, rerr error) {
|
func (p *Proto) Write(buf []byte) (n int, rerr error) {
|
||||||
defer c.recover(&rerr)
|
defer p.recover(&rerr)
|
||||||
|
|
||||||
n, rerr = c.conn.Write(buf)
|
n, rerr = p.conn.Write(buf)
|
||||||
if rerr != nil {
|
if rerr != nil {
|
||||||
c.connBroken = true
|
p.connBroken = true
|
||||||
}
|
}
|
||||||
c.xcheckf(rerr, "write")
|
p.xcheckf(rerr, "write")
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read reads directly from the underlying connection (TCP, TLS). For internal use
|
// Read reads directly from the underlying connection (TCP, TLS). For internal use
|
||||||
// only, to implement io.Reader.
|
// only, to implement io.Reader.
|
||||||
func (c *Conn) Read(buf []byte) (n int, err error) {
|
func (p *Proto) Read(buf []byte) (n int, err error) {
|
||||||
return c.conn.Read(buf)
|
return p.conn.Read(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) xflush() {
|
func (p *Proto) xflush() {
|
||||||
// Not writing any more when connection is broken.
|
// Not writing any more when connection is broken.
|
||||||
if c.connBroken {
|
if p.connBroken {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := c.xbw.Flush()
|
err := p.xbw.Flush()
|
||||||
c.xcheckf(err, "flush")
|
p.xcheckf(err, "flush")
|
||||||
|
|
||||||
// If compression is active, we need to flush the deflate stream.
|
// If compression is active, we need to flush the deflate stream.
|
||||||
if c.compress {
|
if p.compress {
|
||||||
err := c.xflateWriter.Flush()
|
err := p.xflateWriter.Flush()
|
||||||
c.xcheckf(err, "flush deflate")
|
p.xcheckf(err, "flush deflate")
|
||||||
err = c.xflateBW.Flush()
|
err = p.xflateBW.Flush()
|
||||||
c.xcheckf(err, "flush deflate buffer")
|
p.xcheckf(err, "flush deflate buffer")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) xtraceread(level slog.Level) func() {
|
func (p *Proto) xtraceread(level slog.Level) func() {
|
||||||
c.tr.SetTrace(level)
|
if p.tr == nil {
|
||||||
|
// For ParseUntagged and other parse functions.
|
||||||
|
return func() {}
|
||||||
|
}
|
||||||
|
p.tr.SetTrace(level)
|
||||||
return func() {
|
return func() {
|
||||||
c.tr.SetTrace(mlog.LevelTrace)
|
p.tr.SetTrace(mlog.LevelTrace)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) xtracewrite(level slog.Level) func() {
|
func (p *Proto) xtracewrite(level slog.Level) func() {
|
||||||
c.xflush()
|
if p.xtw == nil {
|
||||||
c.xtw.SetTrace(level)
|
// For ParseUntagged and other parse functions.
|
||||||
|
return func() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.xflush()
|
||||||
|
p.xtw.SetTrace(level)
|
||||||
return func() {
|
return func() {
|
||||||
c.xflush()
|
p.xflush()
|
||||||
c.xtw.SetTrace(mlog.LevelTrace)
|
p.xtw.SetTrace(mlog.LevelTrace)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,7 +336,7 @@ func (c *Conn) xtracewrite(level slog.Level) func() {
|
|||||||
// because the server may immediate close the underlying connection when it sees
|
// because the server may immediate close the underlying connection when it sees
|
||||||
// the connection is being closed.
|
// the connection is being closed.
|
||||||
func (c *Conn) Close() (rerr error) {
|
func (c *Conn) Close() (rerr error) {
|
||||||
defer c.recover(&rerr)
|
defer c.recoverErr(&rerr)
|
||||||
|
|
||||||
if c.conn == nil {
|
if c.conn == nil {
|
||||||
return nil
|
return nil
|
||||||
@ -247,7 +355,9 @@ func (c *Conn) Close() (rerr error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TLSConnectionState returns the TLS connection state if the connection uses TLS.
|
// TLSConnectionState returns the TLS connection state if the connection uses TLS,
|
||||||
|
// either because the conn passed to [New] was a TLS connection, or because
|
||||||
|
// [Conn.StartTLS] was called.
|
||||||
func (c *Conn) TLSConnectionState() *tls.ConnectionState {
|
func (c *Conn) TLSConnectionState() *tls.ConnectionState {
|
||||||
if conn, ok := c.conn.(*tls.Conn); ok {
|
if conn, ok := c.conn.(*tls.Conn); ok {
|
||||||
cs := conn.ConnectionState()
|
cs := conn.ConnectionState()
|
||||||
@ -256,170 +366,266 @@ func (c *Conn) TLSConnectionState() *tls.ConnectionState {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commandf writes a free-form IMAP command to the server. An ending \r\n is
|
// WriteCommandf writes a free-form IMAP command to the server. An ending \r\n is
|
||||||
// written too.
|
// written too.
|
||||||
|
//
|
||||||
// If tag is empty, a next unique tag is assigned.
|
// If tag is empty, a next unique tag is assigned.
|
||||||
func (c *Conn) Commandf(tag string, format string, args ...any) (rerr error) {
|
func (p *Proto) WriteCommandf(tag string, format string, args ...any) (rerr error) {
|
||||||
defer c.recover(&rerr)
|
defer p.recover(&rerr)
|
||||||
|
|
||||||
if tag == "" {
|
if tag == "" {
|
||||||
tag = c.nextTag()
|
p.nextTag()
|
||||||
|
} else {
|
||||||
|
p.lastTag = tag
|
||||||
}
|
}
|
||||||
c.LastTag = tag
|
|
||||||
|
|
||||||
fmt.Fprintf(c.xbw, "%s %s\r\n", tag, fmt.Sprintf(format, args...))
|
fmt.Fprintf(p.xbw, "%s %s\r\n", p.lastTag, fmt.Sprintf(format, args...))
|
||||||
c.xflush()
|
p.xflush()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) nextTag() string {
|
func (p *Proto) nextTag() string {
|
||||||
c.tagGen++
|
p.tagGen++
|
||||||
return fmt.Sprintf("x%03d", c.tagGen)
|
p.lastTag = fmt.Sprintf("x%03d", p.tagGen)
|
||||||
|
return p.lastTag
|
||||||
}
|
}
|
||||||
|
|
||||||
// Response reads from the IMAP server until a tagged response line is found.
|
// LastTag returns the tag last used for a command. For checking against a command
|
||||||
|
// completion result.
|
||||||
|
func (p *Proto) LastTag() string {
|
||||||
|
return p.lastTag
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastTagSet sets a new last tag, as used for checking against a command completion result.
|
||||||
|
func (p *Proto) LastTagSet(tag string) {
|
||||||
|
p.lastTag = tag
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadResponse reads from the IMAP server until a tagged response line is found.
|
||||||
// The tag must be the same as the tag for the last written command.
|
// The tag must be the same as the tag for the last written command.
|
||||||
// Result holds the status of the command. The caller must check if this the status is OK.
|
//
|
||||||
func (c *Conn) Response() (untagged []Untagged, result Result, rerr error) {
|
// If an error is returned, resp can still be non-empty, and a caller may wish to
|
||||||
defer c.recover(&rerr)
|
// process resp.Untagged.
|
||||||
|
//
|
||||||
|
// Caller should check resp.Status for the result of the command too.
|
||||||
|
//
|
||||||
|
// Common types for the return error:
|
||||||
|
// - Error, for protocol errors
|
||||||
|
// - Various I/O errors from the underlying connection, including os.ErrDeadlineExceeded
|
||||||
|
func (p *Proto) ReadResponse() (resp Response, rerr error) {
|
||||||
|
defer p.recover(&rerr)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
tag := c.xnonspace()
|
tag := p.xnonspace()
|
||||||
c.xspace()
|
p.xspace()
|
||||||
if tag == "*" {
|
if tag == "*" {
|
||||||
untagged = append(untagged, c.xuntagged())
|
resp.Untagged = append(resp.Untagged, p.xuntagged())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if tag != c.LastTag {
|
if tag != p.lastTag {
|
||||||
c.xerrorf("got tag %q, expected %q", tag, c.LastTag)
|
p.xerrorf("got tag %q, expected %q", tag, p.lastTag)
|
||||||
}
|
}
|
||||||
|
|
||||||
status := c.xstatus()
|
status := p.xstatus()
|
||||||
c.xspace()
|
p.xspace()
|
||||||
result = c.xresult(status)
|
resp.Result = p.xresult(status)
|
||||||
c.xcrlf()
|
p.xcrlf()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadUntagged reads a single untagged response line.
|
// ParseCode parses a response code. The string must not have enclosing brackets.
|
||||||
// Useful for reading lines from IDLE.
|
//
|
||||||
func (c *Conn) ReadUntagged() (untagged Untagged, rerr error) {
|
// Example:
|
||||||
defer c.recover(&rerr)
|
//
|
||||||
|
// "APPENDUID 123 10"
|
||||||
tag := c.xnonspace()
|
func ParseCode(s string) (code Code, rerr error) {
|
||||||
if tag != "*" {
|
p := Proto{br: bufio.NewReader(strings.NewReader(s + "]"))}
|
||||||
c.xerrorf("got tag %q, expected untagged", tag)
|
defer p.recover(&rerr)
|
||||||
|
code = p.xrespCode()
|
||||||
|
p.xtake("]")
|
||||||
|
buf, err := io.ReadAll(p.br)
|
||||||
|
p.xcheckf(err, "read")
|
||||||
|
if len(buf) != 0 {
|
||||||
|
p.xerrorf("leftover data %q", buf)
|
||||||
}
|
}
|
||||||
c.xspace()
|
return code, nil
|
||||||
ut := c.xuntagged()
|
}
|
||||||
|
|
||||||
|
// ParseResult parses a line, including required crlf, as a command result line.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// "tag1 OK [APPENDUID 123 10] message added\r\n"
|
||||||
|
func ParseResult(s string) (tag string, result Result, rerr error) {
|
||||||
|
p := Proto{br: bufio.NewReader(strings.NewReader(s))}
|
||||||
|
defer p.recover(&rerr)
|
||||||
|
tag = p.xnonspace()
|
||||||
|
p.xspace()
|
||||||
|
status := p.xstatus()
|
||||||
|
p.xspace()
|
||||||
|
result = p.xresult(status)
|
||||||
|
p.xcrlf()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadUntagged reads a single untagged response line.
|
||||||
|
func (p *Proto) ReadUntagged() (untagged Untagged, rerr error) {
|
||||||
|
defer p.recover(&rerr)
|
||||||
|
return p.readUntagged()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseUntagged parses a line, including required crlf, as untagged response.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// "* BYE shutting down connection\r\n"
|
||||||
|
func ParseUntagged(s string) (untagged Untagged, rerr error) {
|
||||||
|
p := Proto{br: bufio.NewReader(strings.NewReader(s))}
|
||||||
|
defer p.recover(&rerr)
|
||||||
|
untagged, rerr = p.readUntagged()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Proto) readUntagged() (untagged Untagged, rerr error) {
|
||||||
|
defer p.recover(&rerr)
|
||||||
|
tag := p.xnonspace()
|
||||||
|
if tag != "*" {
|
||||||
|
p.xerrorf("got tag %q, expected untagged", tag)
|
||||||
|
}
|
||||||
|
p.xspace()
|
||||||
|
ut := p.xuntagged()
|
||||||
return ut, nil
|
return ut, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Readline reads a line, including CRLF.
|
// Readline reads a line, including CRLF.
|
||||||
// Used with IDLE and synchronous literals.
|
// Used with IDLE and synchronous literals.
|
||||||
func (c *Conn) Readline() (line string, rerr error) {
|
func (p *Proto) Readline() (line string, rerr error) {
|
||||||
defer c.recover(&rerr)
|
defer p.recover(&rerr)
|
||||||
|
|
||||||
line, err := c.br.ReadString('\n')
|
line, err := p.br.ReadString('\n')
|
||||||
c.xcheckf(err, "read line")
|
p.xcheckf(err, "read line")
|
||||||
return line, nil
|
return line, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadContinuation reads a line. If it is a continuation, i.e. starts with a +, it
|
func (c *Conn) readContinuation() (line string, rerr error) {
|
||||||
// is returned without leading "+ " and without trailing crlf. Otherwise, a command
|
defer c.recover(&rerr, nil)
|
||||||
// response is returned. A successfully read continuation can return an empty line.
|
line, rerr = c.ReadContinuation()
|
||||||
// Callers should check rerr and result.Status being empty to check if a
|
if rerr != nil {
|
||||||
// continuation was read.
|
if resp, ok := rerr.(Response); ok {
|
||||||
func (c *Conn) ReadContinuation() (line string, untagged []Untagged, result Result, rerr error) {
|
c.processUntagged(resp.Untagged)
|
||||||
defer c.recover(&rerr)
|
c.processResult(resp.Result)
|
||||||
|
}
|
||||||
if !c.peek('+') {
|
|
||||||
untagged, result, rerr = c.Response()
|
|
||||||
if result.Status == OK {
|
|
||||||
c.xerrorf("unexpected OK instead of continuation")
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.xtake("+ ")
|
|
||||||
line, err := c.Readline()
|
// ReadContinuation reads a line. If it is a continuation, i.e. starts with "+", it
|
||||||
c.xcheckf(err, "read line")
|
// is returned without leading "+ " and without trailing crlf. Otherwise, an error
|
||||||
|
// is returned, which can be a Response with Untagged that a caller may wish to
|
||||||
|
// process. A successfully read continuation can return an empty line.
|
||||||
|
func (p *Proto) ReadContinuation() (line string, rerr error) {
|
||||||
|
defer p.recover(&rerr)
|
||||||
|
|
||||||
|
if !p.peek('+') {
|
||||||
|
var resp Response
|
||||||
|
resp, rerr = p.ReadResponse()
|
||||||
|
if rerr == nil {
|
||||||
|
rerr = resp
|
||||||
|
}
|
||||||
|
return "", rerr
|
||||||
|
}
|
||||||
|
p.xtake("+ ")
|
||||||
|
line, err := p.Readline()
|
||||||
|
p.xcheckf(err, "read line")
|
||||||
line = strings.TrimSuffix(line, "\r\n")
|
line = strings.TrimSuffix(line, "\r\n")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Writelinef writes the formatted format and args as a single line, adding CRLF.
|
// Writelinef writes the formatted format and args as a single line, adding CRLF.
|
||||||
// Used with IDLE and synchronous literals.
|
// Used with IDLE and synchronous literals.
|
||||||
func (c *Conn) Writelinef(format string, args ...any) (rerr error) {
|
func (p *Proto) Writelinef(format string, args ...any) (rerr error) {
|
||||||
defer c.recover(&rerr)
|
defer p.recover(&rerr)
|
||||||
|
|
||||||
s := fmt.Sprintf(format, args...)
|
s := fmt.Sprintf(format, args...)
|
||||||
fmt.Fprintf(c.xbw, "%s\r\n", s)
|
fmt.Fprintf(p.xbw, "%s\r\n", s)
|
||||||
c.xflush()
|
p.xflush()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteSyncLiteral first writes the synchronous literal size, then reads the
|
// WriteSyncLiteral first writes the synchronous literal size, then reads the
|
||||||
// continuation "+" and finally writes the data.
|
// continuation "+" and finally writes the data. If the literal is not accepted, an
|
||||||
func (c *Conn) WriteSyncLiteral(s string) (untagged []Untagged, rerr error) {
|
// error is returned, which may be a Response.
|
||||||
defer c.recover(&rerr)
|
func (p *Proto) WriteSyncLiteral(s string) (rerr error) {
|
||||||
|
defer p.recover(&rerr)
|
||||||
|
|
||||||
fmt.Fprintf(c.xbw, "{%d}\r\n", len(s))
|
fmt.Fprintf(p.xbw, "{%d}\r\n", len(s))
|
||||||
c.xflush()
|
p.xflush()
|
||||||
|
|
||||||
plus, err := c.br.Peek(1)
|
plus, err := p.br.Peek(1)
|
||||||
c.xcheckf(err, "read continuation")
|
p.xcheckf(err, "read continuation")
|
||||||
if plus[0] == '+' {
|
if plus[0] == '+' {
|
||||||
_, err = c.Readline()
|
_, err = p.Readline()
|
||||||
c.xcheckf(err, "read continuation line")
|
p.xcheckf(err, "read continuation line")
|
||||||
|
|
||||||
defer c.xtracewrite(mlog.LevelTracedata)()
|
defer p.xtracewrite(mlog.LevelTracedata)()
|
||||||
_, err = c.xbw.Write([]byte(s))
|
_, err = p.xbw.Write([]byte(s))
|
||||||
c.xcheckf(err, "write literal data")
|
p.xcheckf(err, "write literal data")
|
||||||
c.xtracewrite(mlog.LevelTrace)
|
p.xtracewrite(mlog.LevelTrace)
|
||||||
return nil, nil
|
return nil
|
||||||
}
|
}
|
||||||
untagged, result, err := c.Response()
|
var resp Response
|
||||||
if err == nil && result.Status == OK {
|
resp, rerr = p.ReadResponse()
|
||||||
c.xerrorf("no continuation, but invalid ok response (%q)", result.More)
|
if rerr == nil {
|
||||||
|
rerr = resp
|
||||||
}
|
}
|
||||||
return untagged, fmt.Errorf("no continuation (%s)", result.Status)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transactf writes format and args as an IMAP command, using Commandf with an
|
func (c *Conn) processUntagged(l []Untagged) {
|
||||||
|
for _, ut := range l {
|
||||||
|
switch e := ut.(type) {
|
||||||
|
case UntaggedCapability:
|
||||||
|
c.CapAvailable = []Capability(e)
|
||||||
|
case UntaggedEnabled:
|
||||||
|
c.CapEnabled = append(c.CapEnabled, e...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) processResult(r Result) {
|
||||||
|
if r.Code == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch e := r.Code.(type) {
|
||||||
|
case CodeCapability:
|
||||||
|
c.CapAvailable = []Capability(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// transactf writes format and args as an IMAP command, using Commandf with an
|
||||||
// empty tag. I.e. format must not contain a tag. Transactf then reads a response
|
// empty tag. I.e. format must not contain a tag. Transactf then reads a response
|
||||||
// using ReadResponse and checks the result status is OK.
|
// using ReadResponse and checks the result status is OK.
|
||||||
func (c *Conn) Transactf(format string, args ...any) (untagged []Untagged, result Result, rerr error) {
|
func (c *Conn) transactf(format string, args ...any) (resp Response, rerr error) {
|
||||||
defer c.recover(&rerr)
|
defer c.recover(&rerr, &resp)
|
||||||
|
|
||||||
err := c.Commandf("", format, args...)
|
err := c.WriteCommandf("", format, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, Result{}, err
|
return Response{}, err
|
||||||
}
|
|
||||||
return c.ResponseOK()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) ResponseOK() (untagged []Untagged, result Result, rerr error) {
|
return c.responseOK()
|
||||||
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) xgetUntagged(l []Untagged, dst any) {
|
func (c *Conn) responseOK() (resp Response, rerr error) {
|
||||||
if len(l) != 1 {
|
defer c.recover(&rerr, &resp)
|
||||||
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]
|
return
|
||||||
gotv := reflect.ValueOf(got)
|
|
||||||
dstv := reflect.ValueOf(dst)
|
|
||||||
if gotv.Type() != dstv.Type().Elem() {
|
|
||||||
c.xerrorf("got %v, expected %v", gotv.Type(), dstv.Type().Elem())
|
|
||||||
}
|
|
||||||
dstv.Elem().Set(gotv)
|
|
||||||
}
|
}
|
||||||
|
@ -17,32 +17,34 @@ import (
|
|||||||
"github.com/mjl-/mox/scram"
|
"github.com/mjl-/mox/scram"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Capability requests a list of capabilities from the server. They are returned in
|
// Capability writes the IMAP4 "CAPABILITY" command, requesting a list of
|
||||||
// an UntaggedCapability response. The server also sends capabilities in initial
|
// capabilities from the server. They are returned in an UntaggedCapability
|
||||||
// server greeting, in the response code.
|
// response. The server also sends capabilities in initial server greeting, in the
|
||||||
func (c *Conn) Capability() (untagged []Untagged, result Result, rerr error) {
|
// response code.
|
||||||
defer c.recover(&rerr)
|
func (c *Conn) Capability() (resp Response, rerr error) {
|
||||||
return c.Transactf("capability")
|
defer c.recover(&rerr, &resp)
|
||||||
|
return c.transactf("capability")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Noop does nothing on its own, but a server will return any pending untagged
|
// Noop writes the IMAP4 "NOOP" command, which does nothing on its own, but a
|
||||||
// responses for new message delivery and changes to mailboxes.
|
// server will return any pending untagged responses for new message delivery and
|
||||||
func (c *Conn) Noop() (untagged []Untagged, result Result, rerr error) {
|
// changes to mailboxes.
|
||||||
defer c.recover(&rerr)
|
func (c *Conn) Noop() (resp Response, rerr error) {
|
||||||
return c.Transactf("noop")
|
defer c.recover(&rerr, &resp)
|
||||||
|
return c.transactf("noop")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logout ends the IMAP session by writing a LOGOUT command. Close must still be
|
// Logout ends the IMAP4 session by writing an IMAP "LOGOUT" command. [Conn.Close]
|
||||||
// called on this client to close the socket.
|
// must still be called on this client to close the socket.
|
||||||
func (c *Conn) Logout() (untagged []Untagged, result Result, rerr error) {
|
func (c *Conn) Logout() (resp Response, rerr error) {
|
||||||
defer c.recover(&rerr)
|
defer c.recover(&rerr, &resp)
|
||||||
return c.Transactf("logout")
|
return c.transactf("logout")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Starttls enables TLS on the connection with the STARTTLS command.
|
// StartTLS enables TLS on the connection with the IMAP4 "STARTTLS" command.
|
||||||
func (c *Conn) Starttls(config *tls.Config) (untagged []Untagged, result Result, rerr error) {
|
func (c *Conn) StartTLS(config *tls.Config) (resp Response, rerr error) {
|
||||||
defer c.recover(&rerr)
|
defer c.recover(&rerr, &resp)
|
||||||
untagged, result, rerr = c.Transactf("starttls")
|
resp, rerr = c.transactf("starttls")
|
||||||
c.xcheckf(rerr, "starttls command")
|
c.xcheckf(rerr, "starttls command")
|
||||||
|
|
||||||
conn := c.xprefixConn()
|
conn := c.xprefixConn()
|
||||||
@ -50,32 +52,43 @@ func (c *Conn) Starttls(config *tls.Config) (untagged []Untagged, result Result,
|
|||||||
err := tlsConn.Handshake()
|
err := tlsConn.Handshake()
|
||||||
c.xcheckf(err, "tls handshake")
|
c.xcheckf(err, "tls handshake")
|
||||||
c.conn = tlsConn
|
c.conn = tlsConn
|
||||||
return untagged, result, nil
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login authenticates with username and password
|
// Login authenticates using the IMAP4 "LOGIN" command, sending the plain text
|
||||||
func (c *Conn) Login(username, password string) (untagged []Untagged, result Result, rerr error) {
|
// password to the server.
|
||||||
defer c.recover(&rerr)
|
//
|
||||||
|
// Authentication is not allowed while the "LOGINDISABLED" capability is announced.
|
||||||
|
// Call [Conn.StartTLS] first.
|
||||||
|
//
|
||||||
|
// See [Conn.AuthenticateSCRAM] for a better authentication mechanism.
|
||||||
|
func (c *Conn) Login(username, password string) (resp Response, rerr error) {
|
||||||
|
defer c.recover(&rerr, &resp)
|
||||||
|
|
||||||
c.LastTag = c.nextTag()
|
fmt.Fprintf(c.xbw, "%s login %s ", c.nextTag(), astring(username))
|
||||||
fmt.Fprintf(c.xbw, "%s login %s ", c.LastTag, astring(username))
|
|
||||||
defer c.xtracewrite(mlog.LevelTraceauth)()
|
defer c.xtracewrite(mlog.LevelTraceauth)()
|
||||||
fmt.Fprintf(c.xbw, "%s\r\n", astring(password))
|
fmt.Fprintf(c.xbw, "%s\r\n", astring(password))
|
||||||
c.xtracewrite(mlog.LevelTrace) // Restore.
|
c.xtracewrite(mlog.LevelTrace) // Restore.
|
||||||
return c.Response()
|
return c.responseOK()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authenticate with plaintext password using AUTHENTICATE PLAIN.
|
// AuthenticatePlain executes the AUTHENTICATE command with SASL mechanism "PLAIN",
|
||||||
func (c *Conn) AuthenticatePlain(username, password string) (untagged []Untagged, result Result, rerr error) {
|
// sending the password in plain text password to the server.
|
||||||
defer c.recover(&rerr)
|
//
|
||||||
|
// Required capability: "AUTH=PLAIN"
|
||||||
|
//
|
||||||
|
// Authentication is not allowed while the "LOGINDISABLED" capability is announced.
|
||||||
|
// Call [Conn.StartTLS] first.
|
||||||
|
//
|
||||||
|
// See [Conn.AuthenticateSCRAM] for a better authentication mechanism.
|
||||||
|
func (c *Conn) AuthenticatePlain(username, password string) (resp Response, rerr error) {
|
||||||
|
defer c.recover(&rerr, &resp)
|
||||||
|
|
||||||
err := c.Commandf("", "authenticate plain")
|
err := c.WriteCommandf("", "authenticate plain")
|
||||||
c.xcheckf(err, "writing authenticate command")
|
c.xcheckf(err, "writing authenticate command")
|
||||||
_, untagged, result, rerr = c.ReadContinuation()
|
_, rerr = c.readContinuation()
|
||||||
c.xcheckf(rerr, "reading continuation")
|
c.xresponse(rerr, &resp)
|
||||||
if result.Status != "" {
|
|
||||||
c.xerrorf("got result status %q, expected continuation", result.Status)
|
|
||||||
}
|
|
||||||
defer c.xtracewrite(mlog.LevelTraceauth)()
|
defer c.xtracewrite(mlog.LevelTraceauth)()
|
||||||
xw := base64.NewEncoder(base64.StdEncoding, c.xbw)
|
xw := base64.NewEncoder(base64.StdEncoding, c.xbw)
|
||||||
fmt.Fprintf(xw, "\u0000%s\u0000%s", username, password)
|
fmt.Fprintf(xw, "\u0000%s\u0000%s", username, password)
|
||||||
@ -83,23 +96,31 @@ func (c *Conn) AuthenticatePlain(username, password string) (untagged []Untagged
|
|||||||
c.xtracewrite(mlog.LevelTrace) // Restore.
|
c.xtracewrite(mlog.LevelTrace) // Restore.
|
||||||
fmt.Fprintf(c.xbw, "\r\n")
|
fmt.Fprintf(c.xbw, "\r\n")
|
||||||
c.xflush()
|
c.xflush()
|
||||||
return c.Response()
|
return c.responseOK()
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: implement cram-md5, write its credentials as traceauth.
|
// todo: implement cram-md5, write its credentials as traceauth.
|
||||||
|
|
||||||
// Authenticate with SCRAM-SHA-256(-PLUS) or SCRAM-SHA-1(-PLUS). With SCRAM, the
|
// AuthenticateSCRAM executes the IMAP4 "AUTHENTICATE" command with one of the
|
||||||
// password is not exchanged in plaintext form, but only derived hashes are
|
// following SASL mechanisms: SCRAM-SHA-256(-PLUS) or SCRAM-SHA-1(-PLUS).//
|
||||||
// exchanged by both parties as proof of knowledge of password.
|
//
|
||||||
|
// With SCRAM, the password is not sent to the server in plain text, but only
|
||||||
|
// derived hashes are exchanged by both parties as proof of knowledge of password.
|
||||||
|
//
|
||||||
|
// Authentication is not allowed while the "LOGINDISABLED" capability is announced.
|
||||||
|
// Call [Conn.StartTLS] first.
|
||||||
|
//
|
||||||
|
// Required capability: SCRAM-SHA-256-PLUS, SCRAM-SHA-256, SCRAM-SHA-1-PLUS,
|
||||||
|
// SCRAM-SHA-1.
|
||||||
//
|
//
|
||||||
// The PLUS variants bind the authentication exchange to the TLS connection,
|
// The PLUS variants bind the authentication exchange to the TLS connection,
|
||||||
// detecting MitM attacks.
|
// detecting MitM attacks.
|
||||||
func (c *Conn) AuthenticateSCRAM(method string, h func() hash.Hash, username, password string) (untagged []Untagged, result Result, rerr error) {
|
func (c *Conn) AuthenticateSCRAM(mechanism string, h func() hash.Hash, username, password string) (resp Response, rerr error) {
|
||||||
defer c.recover(&rerr)
|
defer c.recover(&rerr, &resp)
|
||||||
|
|
||||||
var cs *tls.ConnectionState
|
var cs *tls.ConnectionState
|
||||||
lmethod := strings.ToLower(method)
|
lmech := strings.ToLower(mechanism)
|
||||||
if strings.HasSuffix(lmethod, "-plus") {
|
if strings.HasSuffix(lmech, "-plus") {
|
||||||
tlsConn, ok := c.conn.(*tls.Conn)
|
tlsConn, ok := c.conn.(*tls.Conn)
|
||||||
if !ok {
|
if !ok {
|
||||||
c.xerrorf("cannot use scram plus without tls")
|
c.xerrorf("cannot use scram plus without tls")
|
||||||
@ -110,17 +131,14 @@ func (c *Conn) AuthenticateSCRAM(method string, h func() hash.Hash, username, pa
|
|||||||
sc := scram.NewClient(h, username, "", false, cs)
|
sc := scram.NewClient(h, username, "", false, cs)
|
||||||
clientFirst, err := sc.ClientFirst()
|
clientFirst, err := sc.ClientFirst()
|
||||||
c.xcheckf(err, "scram clientFirst")
|
c.xcheckf(err, "scram clientFirst")
|
||||||
c.LastTag = c.nextTag()
|
// todo: only send clientFirst if server has announced SASL-IR
|
||||||
err = c.Writelinef("%s authenticate %s %s", c.LastTag, method, base64.StdEncoding.EncodeToString([]byte(clientFirst)))
|
err = c.Writelinef("%s authenticate %s %s", c.nextTag(), mechanism, base64.StdEncoding.EncodeToString([]byte(clientFirst)))
|
||||||
c.xcheckf(err, "writing command line")
|
c.xcheckf(err, "writing command line")
|
||||||
|
|
||||||
xreadContinuation := func() []byte {
|
xreadContinuation := func() []byte {
|
||||||
var line string
|
var line string
|
||||||
line, untagged, result, rerr = c.ReadContinuation()
|
line, rerr = c.readContinuation()
|
||||||
c.xcheckf(err, "read continuation")
|
c.xresponse(rerr, &resp)
|
||||||
if result.Status != "" {
|
|
||||||
c.xerrorf("got result status %q, expected continuation", result.Status)
|
|
||||||
}
|
|
||||||
buf, err := base64.StdEncoding.DecodeString(line)
|
buf, err := base64.StdEncoding.DecodeString(line)
|
||||||
c.xcheckf(err, "parsing base64 from remote")
|
c.xcheckf(err, "parsing base64 from remote")
|
||||||
return buf
|
return buf
|
||||||
@ -140,18 +158,19 @@ func (c *Conn) AuthenticateSCRAM(method string, h func() hash.Hash, username, pa
|
|||||||
err = c.Writelinef("%s", base64.StdEncoding.EncodeToString(nil))
|
err = c.Writelinef("%s", base64.StdEncoding.EncodeToString(nil))
|
||||||
c.xcheckf(err, "scram client end")
|
c.xcheckf(err, "scram client end")
|
||||||
|
|
||||||
return c.ResponseOK()
|
return c.responseOK()
|
||||||
}
|
}
|
||||||
|
|
||||||
// CompressDeflate enables compression with deflate on the connection.
|
// CompressDeflate enables compression with deflate on the connection by executing
|
||||||
|
// the IMAP4 "COMPRESS=DEFAULT" command.
|
||||||
//
|
//
|
||||||
// Only possible when server has announced the COMPRESS=DEFLATE capability.
|
// Required capability: "COMPRESS=DEFLATE".
|
||||||
//
|
//
|
||||||
// State: Authenticated or selected.
|
// State: Authenticated or selected.
|
||||||
func (c *Conn) CompressDeflate() (untagged []Untagged, result Result, rerr error) {
|
func (c *Conn) CompressDeflate() (resp Response, rerr error) {
|
||||||
defer c.recover(&rerr)
|
defer c.recover(&rerr, &resp)
|
||||||
|
|
||||||
untagged, result, rerr = c.Transactf("compress deflate")
|
resp, rerr = c.transactf("compress deflate")
|
||||||
c.xcheck(rerr)
|
c.xcheck(rerr)
|
||||||
|
|
||||||
c.xflateBW = bufio.NewWriter(c)
|
c.xflateBW = bufio.NewWriter(c)
|
||||||
@ -172,89 +191,98 @@ func (c *Conn) CompressDeflate() (untagged []Untagged, result Result, rerr error
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable enables capabilities for use with the connection, verifying the server has indeed enabled them.
|
// Enable enables capabilities for use with the connection by executing the IMAP4 "ENABLE" command.
|
||||||
func (c *Conn) Enable(capabilities ...string) (untagged []Untagged, result Result, rerr error) {
|
//
|
||||||
defer c.recover(&rerr)
|
// Required capability: "ENABLE" or "IMAP4rev2"
|
||||||
|
func (c *Conn) Enable(capabilities ...Capability) (resp Response, rerr error) {
|
||||||
|
defer c.recover(&rerr, &resp)
|
||||||
|
|
||||||
untagged, result, rerr = c.Transactf("enable %s", strings.Join(capabilities, " "))
|
var caps strings.Builder
|
||||||
c.xcheck(rerr)
|
for _, c := range capabilities {
|
||||||
var enabled UntaggedEnabled
|
caps.WriteString(" " + string(c))
|
||||||
c.xgetUntagged(untagged, &enabled)
|
|
||||||
got := map[string]struct{}{}
|
|
||||||
for _, cap := range enabled {
|
|
||||||
got[cap] = struct{}{}
|
|
||||||
}
|
}
|
||||||
for _, cap := range capabilities {
|
return c.transactf("enable%s", caps.String())
|
||||||
if _, ok := got[cap]; !ok {
|
|
||||||
c.xerrorf("capability %q not enabled by server", cap)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Select opens mailbox as active mailbox.
|
// Select opens the mailbox with the IMAP4 "SELECT" command.
|
||||||
func (c *Conn) Select(mailbox string) (untagged []Untagged, result Result, rerr error) {
|
//
|
||||||
defer c.recover(&rerr)
|
// If a mailbox is selected/active, it is automatically deselected before
|
||||||
return c.Transactf("select %s", astring(mailbox))
|
// selecting the mailbox, without permanently removing ("expunging") messages
|
||||||
|
// marked \Deleted.
|
||||||
|
//
|
||||||
|
// If the mailbox cannot be opened, the connection is left in Authenticated state,
|
||||||
|
// not Selected.
|
||||||
|
func (c *Conn) Select(mailbox string) (resp Response, rerr error) {
|
||||||
|
defer c.recover(&rerr, &resp)
|
||||||
|
return c.transactf("select %s", astring(mailbox))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Examine opens mailbox as active mailbox read-only.
|
// Examine opens the mailbox like [Conn.Select], but read-only, with the IMAP4
|
||||||
func (c *Conn) Examine(mailbox string) (untagged []Untagged, result Result, rerr error) {
|
// "EXAMINE" command.
|
||||||
defer c.recover(&rerr)
|
func (c *Conn) Examine(mailbox string) (resp Response, rerr error) {
|
||||||
return c.Transactf("examine %s", astring(mailbox))
|
defer c.recover(&rerr, &resp)
|
||||||
|
return c.transactf("examine %s", astring(mailbox))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create makes a new mailbox on the server.
|
// Create makes a new mailbox on the server using the IMAP4 "CREATE" command.
|
||||||
// SpecialUse can only be used on servers that announced the CREATE-SPECIAL-USE
|
//
|
||||||
|
// SpecialUse can only be used on servers that announced the "CREATE-SPECIAL-USE"
|
||||||
// capability. Specify flags like \Archive, \Drafts, \Junk, \Sent, \Trash, \All.
|
// capability. Specify flags like \Archive, \Drafts, \Junk, \Sent, \Trash, \All.
|
||||||
func (c *Conn) Create(mailbox string, specialUse []string) (untagged []Untagged, result Result, rerr error) {
|
func (c *Conn) Create(mailbox string, specialUse []string) (resp Response, rerr error) {
|
||||||
defer c.recover(&rerr)
|
defer c.recover(&rerr, &resp)
|
||||||
if _, ok := c.CapAvailable[CapCreateSpecialUse]; !ok && len(specialUse) > 0 {
|
|
||||||
c.xerrorf("server does not implement create-special-use extension")
|
|
||||||
}
|
|
||||||
var useStr string
|
var useStr string
|
||||||
if len(specialUse) > 0 {
|
if len(specialUse) > 0 {
|
||||||
useStr = fmt.Sprintf(" USE (%s)", strings.Join(specialUse, " "))
|
useStr = fmt.Sprintf(" USE (%s)", strings.Join(specialUse, " "))
|
||||||
}
|
}
|
||||||
return c.Transactf("create %s%s", astring(mailbox), useStr)
|
return c.transactf("create %s%s", astring(mailbox), useStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes an entire mailbox and its messages.
|
// Delete removes an entire mailbox and its messages using the IMAP4 "DELETE"
|
||||||
func (c *Conn) Delete(mailbox string) (untagged []Untagged, result Result, rerr error) {
|
// command.
|
||||||
defer c.recover(&rerr)
|
func (c *Conn) Delete(mailbox string) (resp Response, rerr error) {
|
||||||
return c.Transactf("delete %s", astring(mailbox))
|
defer c.recover(&rerr, &resp)
|
||||||
|
return c.transactf("delete %s", astring(mailbox))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rename changes the name of a mailbox and all its child mailboxes.
|
// Rename changes the name of a mailbox and all its child mailboxes
|
||||||
func (c *Conn) Rename(omailbox, nmailbox string) (untagged []Untagged, result Result, rerr error) {
|
// using the IMAP4 "RENAME" command.
|
||||||
defer c.recover(&rerr)
|
func (c *Conn) Rename(omailbox, nmailbox string) (resp Response, rerr error) {
|
||||||
return c.Transactf("rename %s %s", astring(omailbox), astring(nmailbox))
|
defer c.recover(&rerr, &resp)
|
||||||
|
return c.transactf("rename %s %s", astring(omailbox), astring(nmailbox))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscribe marks a mailbox as subscribed. The mailbox does not have to exist. It
|
// Subscribe marks a mailbox as subscribed using the IMAP4 "SUBSCRIBE" command.
|
||||||
// is not an error if the mailbox is already subscribed.
|
//
|
||||||
func (c *Conn) Subscribe(mailbox string) (untagged []Untagged, result Result, rerr error) {
|
// The mailbox does not have to exist. It is not an error if the mailbox is already
|
||||||
defer c.recover(&rerr)
|
// subscribed.
|
||||||
return c.Transactf("subscribe %s", astring(mailbox))
|
func (c *Conn) Subscribe(mailbox string) (resp Response, rerr error) {
|
||||||
|
defer c.recover(&rerr, &resp)
|
||||||
|
return c.transactf("subscribe %s", astring(mailbox))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unsubscribe marks a mailbox as unsubscribed.
|
// Unsubscribe marks a mailbox as unsubscribed using the IMAP4 "UNSUBSCRIBE"
|
||||||
func (c *Conn) Unsubscribe(mailbox string) (untagged []Untagged, result Result, rerr error) {
|
// command.
|
||||||
defer c.recover(&rerr)
|
func (c *Conn) Unsubscribe(mailbox string) (resp Response, rerr error) {
|
||||||
return c.Transactf("unsubscribe %s", astring(mailbox))
|
defer c.recover(&rerr, &resp)
|
||||||
|
return c.transactf("unsubscribe %s", astring(mailbox))
|
||||||
}
|
}
|
||||||
|
|
||||||
// List lists mailboxes with the basic LIST syntax.
|
// List lists mailboxes using the IMAP4 "LIST" command with the basic LIST syntax.
|
||||||
// Pattern can contain * (match any) or % (match any except hierarchy delimiter).
|
// Pattern can contain * (match any) or % (match any except hierarchy delimiter).
|
||||||
func (c *Conn) List(pattern string) (untagged []Untagged, result Result, rerr error) {
|
func (c *Conn) List(pattern string) (resp Response, rerr error) {
|
||||||
defer c.recover(&rerr)
|
defer c.recover(&rerr, &resp)
|
||||||
return c.Transactf(`list "" %s`, astring(pattern))
|
return c.transactf(`list "" %s`, astring(pattern))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListFull lists mailboxes with the extended LIST syntax requesting all supported data.
|
// ListFull lists mailboxes using the LIST command with the extended LIST
|
||||||
|
// syntax requesting all supported data.
|
||||||
|
//
|
||||||
|
// Required capability: "LIST-EXTENDED". If "IMAP4rev2" is announced, the command
|
||||||
|
// is also available but only with a single pattern.
|
||||||
|
//
|
||||||
// Pattern can contain * (match any) or % (match any except hierarchy delimiter).
|
// Pattern can contain * (match any) or % (match any except hierarchy delimiter).
|
||||||
func (c *Conn) ListFull(subscribedOnly bool, patterns ...string) (untagged []Untagged, result Result, rerr error) {
|
func (c *Conn) ListFull(subscribedOnly bool, patterns ...string) (resp Response, rerr error) {
|
||||||
defer c.recover(&rerr)
|
defer c.recover(&rerr, &resp)
|
||||||
var subscribedStr string
|
var subscribedStr string
|
||||||
if subscribedOnly {
|
if subscribedOnly {
|
||||||
subscribedStr = "subscribed recursivematch"
|
subscribedStr = "subscribed recursivematch"
|
||||||
@ -262,49 +290,54 @@ func (c *Conn) ListFull(subscribedOnly bool, patterns ...string) (untagged []Unt
|
|||||||
for i, s := range patterns {
|
for i, s := range patterns {
|
||||||
patterns[i] = astring(s)
|
patterns[i] = astring(s)
|
||||||
}
|
}
|
||||||
return c.Transactf(`list (%s) "" (%s) return (subscribed children special-use status (messages uidnext uidvalidity unseen deleted size recent appendlimit))`, subscribedStr, strings.Join(patterns, " "))
|
return c.transactf(`list (%s) "" (%s) return (subscribed children special-use status (messages uidnext uidvalidity unseen deleted size recent appendlimit))`, subscribedStr, strings.Join(patterns, " "))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Namespace returns the hiearchy separator in an UntaggedNamespace response with personal/shared/other namespaces if present.
|
// Namespace requests the hiearchy separator using the IMAP4 "NAMESPACE" command.
|
||||||
func (c *Conn) Namespace() (untagged []Untagged, result Result, rerr error) {
|
//
|
||||||
defer c.recover(&rerr)
|
// Required capability: "NAMESPACE" or "IMAP4rev2".
|
||||||
return c.Transactf("namespace")
|
//
|
||||||
|
// Server will return an UntaggedNamespace response with personal/shared/other
|
||||||
|
// namespaces if present.
|
||||||
|
func (c *Conn) Namespace() (resp Response, rerr error) {
|
||||||
|
defer c.recover(&rerr, &resp)
|
||||||
|
return c.transactf("namespace")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status requests information about a mailbox, such as number of messages, size,
|
// Status requests information about a mailbox using the IMAP4 "STATUS" command. For
|
||||||
// etc. At least one attribute required.
|
// example, number of messages, size, etc. At least one attribute required.
|
||||||
func (c *Conn) Status(mailbox string, attrs ...StatusAttr) (untagged []Untagged, result Result, rerr error) {
|
func (c *Conn) Status(mailbox string, attrs ...StatusAttr) (resp Response, rerr error) {
|
||||||
defer c.recover(&rerr)
|
defer c.recover(&rerr, &resp)
|
||||||
l := make([]string, len(attrs))
|
l := make([]string, len(attrs))
|
||||||
for i, a := range attrs {
|
for i, a := range attrs {
|
||||||
l[i] = string(a)
|
l[i] = string(a)
|
||||||
}
|
}
|
||||||
return c.Transactf("status %s (%s)", astring(mailbox), strings.Join(l, " "))
|
return c.transactf("status %s (%s)", astring(mailbox), strings.Join(l, " "))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append represents a parameter to the APPEND or REPLACE commands.
|
// Append represents a parameter to the IMAP4 "APPEND" or "REPLACE" commands, for
|
||||||
|
// adding a message to mailbox, or replacing a message with a new version in a
|
||||||
|
// mailbox.
|
||||||
type Append struct {
|
type Append struct {
|
||||||
Flags []string
|
Flags []string // Optional, flags for the new message.
|
||||||
Received *time.Time
|
Received *time.Time // Optional, the INTERNALDATE field, typically time at which a message was received.
|
||||||
Size int64
|
Size int64
|
||||||
Data io.Reader // Must return Size bytes.
|
Data io.Reader // Required, must return Size bytes.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append adds message to mailbox with flags and optional receive time.
|
// Append adds message to mailbox with flags and optional receive time using the
|
||||||
|
// IMAP4 "APPEND" command.
|
||||||
|
func (c *Conn) Append(mailbox string, message Append) (resp Response, rerr error) {
|
||||||
|
return c.MultiAppend(mailbox, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiAppend atomatically adds multiple messages to the mailbox.
|
||||||
//
|
//
|
||||||
// Multiple messages are only possible when the server has announced the
|
// Required capability: "MULTIAPPEND"
|
||||||
// MULTIAPPEND capability.
|
func (c *Conn) MultiAppend(mailbox string, message Append, more ...Append) (resp Response, rerr error) {
|
||||||
func (c *Conn) Append(mailbox string, message Append, more ...Append) (untagged []Untagged, result Result, rerr error) {
|
defer c.recover(&rerr, &resp)
|
||||||
defer c.recover(&rerr)
|
|
||||||
|
|
||||||
if _, ok := c.CapAvailable[CapMultiAppend]; !ok && len(more) > 0 {
|
fmt.Fprintf(c.xbw, "%s append %s", c.nextTag(), astring(mailbox))
|
||||||
c.xerrorf("can only append multiple messages when server has announced MULTIAPPEND capability")
|
|
||||||
}
|
|
||||||
|
|
||||||
tag := c.nextTag()
|
|
||||||
c.LastTag = tag
|
|
||||||
|
|
||||||
fmt.Fprintf(c.xbw, "%s append %s", tag, astring(mailbox))
|
|
||||||
|
|
||||||
msgs := append([]Append{message}, more...)
|
msgs := append([]Append{message}, more...)
|
||||||
for _, m := range msgs {
|
for _, m := range msgs {
|
||||||
@ -325,150 +358,226 @@ func (c *Conn) Append(mailbox string, message Append, more ...Append) (untagged
|
|||||||
|
|
||||||
fmt.Fprintf(c.xbw, "\r\n")
|
fmt.Fprintf(c.xbw, "\r\n")
|
||||||
c.xflush()
|
c.xflush()
|
||||||
return c.Response()
|
return c.responseOK()
|
||||||
}
|
}
|
||||||
|
|
||||||
// note: No Idle or Notify command. Idle/Notify is better implemented by
|
// note: No Idle or Notify command. Idle/Notify is better implemented by
|
||||||
// writing the request and reading and handling the responses as they come in.
|
// writing the request and reading and handling the responses as they come in.
|
||||||
|
|
||||||
// CloseMailbox closes the currently selected/active mailbox, permanently removing
|
// CloseMailbox closes the selected/active mailbox using the IMAP4 "CLOSE" command,
|
||||||
// any messages marked with \Deleted.
|
// permanently removing ("expunging") any messages marked with \Deleted.
|
||||||
func (c *Conn) CloseMailbox() (untagged []Untagged, result Result, rerr error) {
|
//
|
||||||
return c.Transactf("close")
|
// See [Conn.Unselect] for closing a mailbox without permanently removing messages.
|
||||||
|
func (c *Conn) CloseMailbox() (resp Response, rerr error) {
|
||||||
|
return c.transactf("close")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unselect closes the currently selected/active mailbox, but unlike CloseMailbox
|
// Unselect closes the selected/active mailbox using the IMAP4 "UNSELECT" command,
|
||||||
// does not permanently remove any messages marked with \Deleted.
|
// but unlike MailboxClose does not permanently remove ("expunge") any messages
|
||||||
func (c *Conn) Unselect() (untagged []Untagged, result Result, rerr error) {
|
// marked with \Deleted.
|
||||||
return c.Transactf("unselect")
|
//
|
||||||
|
// Required capability: "UNSELECT" or "IMAP4rev2".
|
||||||
|
//
|
||||||
|
// If Unselect is not available, call [Conn.Select] with a non-existent mailbox for
|
||||||
|
// the same effect: Deselecting a mailbox without permanently removing messages
|
||||||
|
// marked \Deleted.
|
||||||
|
func (c *Conn) Unselect() (resp Response, rerr error) {
|
||||||
|
return c.transactf("unselect")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expunge removes messages marked as deleted for the selected mailbox.
|
// Expunge removes all messages marked as deleted for the selected mailbox using
|
||||||
func (c *Conn) Expunge() (untagged []Untagged, result Result, rerr error) {
|
// the IMAP4 "EXPUNGE" command. If other sessions marked messages as deleted, even
|
||||||
defer c.recover(&rerr)
|
// if they aren't visible in the session, they are removed as well.
|
||||||
return c.Transactf("expunge")
|
//
|
||||||
|
// UIDExpunge gives more control over which the messages that are removed.
|
||||||
|
func (c *Conn) Expunge() (resp Response, rerr error) {
|
||||||
|
defer c.recover(&rerr, &resp)
|
||||||
|
return c.transactf("expunge")
|
||||||
}
|
}
|
||||||
|
|
||||||
// UIDExpunge is like expunge, but only removes messages matching uidSet.
|
// UIDExpunge is like expunge, but only removes messages matching UID set, using
|
||||||
func (c *Conn) UIDExpunge(uidSet NumSet) (untagged []Untagged, result Result, rerr error) {
|
// the IMAP4 "UID EXPUNGE" command.
|
||||||
defer c.recover(&rerr)
|
//
|
||||||
return c.Transactf("uid expunge %s", uidSet.String())
|
// Required capability: "UIDPLUS" or "IMAP4rev2".
|
||||||
|
func (c *Conn) UIDExpunge(uidSet NumSet) (resp Response, rerr error) {
|
||||||
|
defer c.recover(&rerr, &resp)
|
||||||
|
return c.transactf("uid expunge %s", uidSet.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: No search, fetch command yet due to its large syntax.
|
// Note: No search, fetch command yet due to its large syntax.
|
||||||
|
|
||||||
// StoreFlagsSet stores a new set of flags for messages from seqset with the STORE command.
|
// MSNStoreFlagsSet stores a new set of flags for messages matching message
|
||||||
// If silent, no untagged responses with the updated flags will be sent by the server.
|
// sequence numbers (MSNs) from sequence set with the IMAP4 "STORE" command.
|
||||||
func (c *Conn) StoreFlagsSet(seqset string, silent bool, flags ...string) (untagged []Untagged, result Result, rerr error) {
|
//
|
||||||
defer c.recover(&rerr)
|
// If silent, no untagged responses with the updated flags will be sent by the
|
||||||
|
// server.
|
||||||
|
//
|
||||||
|
// Method [Conn.UIDStoreFlagsSet], which operates on a uid set, should be
|
||||||
|
// preferred.
|
||||||
|
func (c *Conn) MSNStoreFlagsSet(seqset string, silent bool, flags ...string) (resp Response, rerr error) {
|
||||||
|
defer c.recover(&rerr, &resp)
|
||||||
item := "flags"
|
item := "flags"
|
||||||
if silent {
|
if silent {
|
||||||
item += ".silent"
|
item += ".silent"
|
||||||
}
|
}
|
||||||
return c.Transactf("store %s %s (%s)", seqset, item, strings.Join(flags, " "))
|
return c.transactf("store %s %s (%s)", seqset, item, strings.Join(flags, " "))
|
||||||
}
|
}
|
||||||
|
|
||||||
// StoreFlagsAdd is like StoreFlagsSet, but only adds flags, leaving current flags on the message intact.
|
// MSNStoreFlagsAdd is like [Conn.MSNStoreFlagsSet], but only adds flags, leaving
|
||||||
func (c *Conn) StoreFlagsAdd(seqset string, silent bool, flags ...string) (untagged []Untagged, result Result, rerr error) {
|
// current flags on the message intact.
|
||||||
defer c.recover(&rerr)
|
//
|
||||||
|
// Method [Conn.UIDStoreFlagsAdd], which operates on a uid set, should be
|
||||||
|
// preferred.
|
||||||
|
func (c *Conn) MSNStoreFlagsAdd(seqset string, silent bool, flags ...string) (resp Response, rerr error) {
|
||||||
|
defer c.recover(&rerr, &resp)
|
||||||
item := "+flags"
|
item := "+flags"
|
||||||
if silent {
|
if silent {
|
||||||
item += ".silent"
|
item += ".silent"
|
||||||
}
|
}
|
||||||
return c.Transactf("store %s %s (%s)", seqset, item, strings.Join(flags, " "))
|
return c.transactf("store %s %s (%s)", seqset, item, strings.Join(flags, " "))
|
||||||
}
|
}
|
||||||
|
|
||||||
// StoreFlagsClear is like StoreFlagsSet, but only removes flags, leaving other flags on the message intact.
|
// MSNStoreFlagsClear is like [Conn.MSNStoreFlagsSet], but only removes flags,
|
||||||
func (c *Conn) StoreFlagsClear(seqset string, silent bool, flags ...string) (untagged []Untagged, result Result, rerr error) {
|
// leaving other flags on the message intact.
|
||||||
defer c.recover(&rerr)
|
//
|
||||||
|
// Method [Conn.UIDStoreFlagsClear], which operates on a uid set, should be
|
||||||
|
// preferred.
|
||||||
|
func (c *Conn) MSNStoreFlagsClear(seqset string, silent bool, flags ...string) (resp Response, rerr error) {
|
||||||
|
defer c.recover(&rerr, &resp)
|
||||||
item := "-flags"
|
item := "-flags"
|
||||||
if silent {
|
if silent {
|
||||||
item += ".silent"
|
item += ".silent"
|
||||||
}
|
}
|
||||||
return c.Transactf("store %s %s (%s)", seqset, item, strings.Join(flags, " "))
|
return c.transactf("store %s %s (%s)", seqset, item, strings.Join(flags, " "))
|
||||||
}
|
}
|
||||||
|
|
||||||
// UIDStoreFlagsSet stores a new set of flags for messages from uid set with
|
// UIDStoreFlagsSet stores a new set of flags for messages matching UIDs from
|
||||||
// the UID STORE command.
|
// uidSet with the IMAP4 "UID STORE" command.
|
||||||
//
|
//
|
||||||
// If silent, no untagged responses with the updated flags will be sent by the
|
// If silent, no untagged responses with the updated flags will be sent by the
|
||||||
// server.
|
// server.
|
||||||
func (c *Conn) UIDStoreFlagsSet(seqset string, silent bool, flags ...string) (untagged []Untagged, result Result, rerr error) {
|
//
|
||||||
defer c.recover(&rerr)
|
// Required capability: "UIDPLUS" or "IMAP4rev2".
|
||||||
|
func (c *Conn) UIDStoreFlagsSet(uidSet string, silent bool, flags ...string) (resp Response, rerr error) {
|
||||||
|
defer c.recover(&rerr, &resp)
|
||||||
item := "flags"
|
item := "flags"
|
||||||
if silent {
|
if silent {
|
||||||
item += ".silent"
|
item += ".silent"
|
||||||
}
|
}
|
||||||
return c.Transactf("uid store %s %s (%s)", seqset, item, strings.Join(flags, " "))
|
return c.transactf("uid store %s %s (%s)", uidSet, item, strings.Join(flags, " "))
|
||||||
}
|
}
|
||||||
|
|
||||||
// UIDStoreFlagsAdd is like UIDStoreFlagsSet, but only adds flags, leaving
|
// UIDStoreFlagsAdd is like UIDStoreFlagsSet, but only adds flags, leaving
|
||||||
// current flags on the message intact.
|
// current flags on the message intact.
|
||||||
func (c *Conn) UIDStoreFlagsAdd(seqset string, silent bool, flags ...string) (untagged []Untagged, result Result, rerr error) {
|
//
|
||||||
defer c.recover(&rerr)
|
// Required capability: "UIDPLUS" or "IMAP4rev2".
|
||||||
|
func (c *Conn) UIDStoreFlagsAdd(uidSet string, silent bool, flags ...string) (resp Response, rerr error) {
|
||||||
|
defer c.recover(&rerr, &resp)
|
||||||
item := "+flags"
|
item := "+flags"
|
||||||
if silent {
|
if silent {
|
||||||
item += ".silent"
|
item += ".silent"
|
||||||
}
|
}
|
||||||
return c.Transactf("uid store %s %s (%s)", seqset, item, strings.Join(flags, " "))
|
return c.transactf("uid store %s %s (%s)", uidSet, item, strings.Join(flags, " "))
|
||||||
}
|
}
|
||||||
|
|
||||||
// UIDStoreFlagsClear is like UIDStoreFlagsSet, but only removes flags, leaving
|
// UIDStoreFlagsClear is like UIDStoreFlagsSet, but only removes flags, leaving
|
||||||
// other flags on the message intact.
|
// other flags on the message intact.
|
||||||
func (c *Conn) UIDStoreFlagsClear(seqset string, silent bool, flags ...string) (untagged []Untagged, result Result, rerr error) {
|
//
|
||||||
defer c.recover(&rerr)
|
// Required capability: "UIDPLUS" or "IMAP4rev2".
|
||||||
|
func (c *Conn) UIDStoreFlagsClear(uidSet string, silent bool, flags ...string) (resp Response, rerr error) {
|
||||||
|
defer c.recover(&rerr, &resp)
|
||||||
item := "-flags"
|
item := "-flags"
|
||||||
if silent {
|
if silent {
|
||||||
item += ".silent"
|
item += ".silent"
|
||||||
}
|
}
|
||||||
return c.Transactf("uid store %s %s (%s)", seqset, item, strings.Join(flags, " "))
|
return c.transactf("uid store %s %s (%s)", uidSet, item, strings.Join(flags, " "))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy adds messages from the sequences in seqSet in the currently selected/active mailbox to dstMailbox.
|
// MSNCopy adds messages from the sequences in the sequence set in the
|
||||||
func (c *Conn) Copy(seqSet NumSet, dstMailbox string) (untagged []Untagged, result Result, rerr error) {
|
// selected/active mailbox to destMailbox using the IMAP4 "COPY" command.
|
||||||
defer c.recover(&rerr)
|
|
||||||
return c.Transactf("copy %s %s", seqSet.String(), astring(dstMailbox))
|
|
||||||
}
|
|
||||||
|
|
||||||
// UIDCopy is like copy, but operates on UIDs.
|
|
||||||
func (c *Conn) UIDCopy(uidSet NumSet, dstMailbox string) (untagged []Untagged, result Result, rerr error) {
|
|
||||||
defer c.recover(&rerr)
|
|
||||||
return c.Transactf("uid copy %s %s", uidSet.String(), astring(dstMailbox))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move moves messages from the sequences in seqSet in the currently selected/active mailbox to dstMailbox.
|
|
||||||
func (c *Conn) Move(seqSet NumSet, dstMailbox string) (untagged []Untagged, result Result, rerr error) {
|
|
||||||
defer c.recover(&rerr)
|
|
||||||
return c.Transactf("move %s %s", seqSet.String(), astring(dstMailbox))
|
|
||||||
}
|
|
||||||
|
|
||||||
// UIDMove is like move, but operates on UIDs.
|
|
||||||
func (c *Conn) UIDMove(uidSet NumSet, dstMailbox string) (untagged []Untagged, result Result, rerr error) {
|
|
||||||
defer c.recover(&rerr)
|
|
||||||
return c.Transactf("uid move %s %s", uidSet.String(), astring(dstMailbox))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace replaces a message from the currently selected mailbox with a
|
|
||||||
// new/different version of the message in the named mailbox, which may be the
|
|
||||||
// same or different than the currently selected mailbox.
|
|
||||||
//
|
//
|
||||||
// Num is a message sequence number. "*" references the last message.
|
// Method [Conn.UIDCopy], operating on UIDs instead of sequence numbers, should be
|
||||||
|
// preferred.
|
||||||
|
func (c *Conn) MSNCopy(seqSet string, destMailbox string) (resp Response, rerr error) {
|
||||||
|
defer c.recover(&rerr, &resp)
|
||||||
|
return c.transactf("copy %s %s", seqSet, astring(destMailbox))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UIDCopy is like copy, but operates on UIDs, using the IMAP4 "UID COPY" command.
|
||||||
//
|
//
|
||||||
// Servers must have announced the REPLACE capability.
|
// Required capability: "UIDPLUS" or "IMAP4rev2".
|
||||||
func (c *Conn) Replace(msgseq string, mailbox string, msg Append) (untagged []Untagged, result Result, rerr error) {
|
func (c *Conn) UIDCopy(uidSet string, destMailbox string) (resp Response, rerr error) {
|
||||||
|
defer c.recover(&rerr, &resp)
|
||||||
|
return c.transactf("uid copy %s %s", uidSet, astring(destMailbox))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MSNSearch returns messages from the sequence set in the selected/active mailbox
|
||||||
|
// that match the search critera using the IMAP4 "SEARCH" command.
|
||||||
|
//
|
||||||
|
// Method [Conn.UIDSearch], operating on UIDs instead of sequence numbers, should be
|
||||||
|
// preferred.
|
||||||
|
func (c *Conn) MSNSearch(seqSet string, criteria string) (resp Response, rerr error) {
|
||||||
|
defer c.recover(&rerr, &resp)
|
||||||
|
return c.transactf("seach %s %s", seqSet, criteria)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UIDSearch returns messages from the uid set in the selected/active mailbox that
|
||||||
|
// match the search critera using the IMAP4 "SEARCH" command.
|
||||||
|
//
|
||||||
|
// Criteria is a search program, see RFC 9051 and RFC 3501 for details.
|
||||||
|
//
|
||||||
|
// Required capability: "UIDPLUS" or "IMAP4rev2".
|
||||||
|
func (c *Conn) UIDSearch(seqSet string, criteria string) (resp Response, rerr error) {
|
||||||
|
defer c.recover(&rerr, &resp)
|
||||||
|
return c.transactf("seach %s %s", seqSet, criteria)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MSNMove moves messages from the sequence set in the selected/active mailbox to
|
||||||
|
// destMailbox using the IMAP4 "MOVE" command.
|
||||||
|
//
|
||||||
|
// Required capability: "MOVE" or "IMAP4rev2".
|
||||||
|
//
|
||||||
|
// Method [Conn.UIDMove], operating on UIDs instead of sequence numbers, should be
|
||||||
|
// preferred.
|
||||||
|
func (c *Conn) MSNMove(seqSet string, destMailbox string) (resp Response, rerr error) {
|
||||||
|
defer c.recover(&rerr, &resp)
|
||||||
|
return c.transactf("move %s %s", seqSet, astring(destMailbox))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UIDMove is like move, but operates on UIDs, using the IMAP4 "UID MOVE" command.
|
||||||
|
//
|
||||||
|
// Required capability: "MOVE" or "IMAP4rev2".
|
||||||
|
func (c *Conn) UIDMove(uidSet string, destMailbox string) (resp Response, rerr error) {
|
||||||
|
defer c.recover(&rerr, &resp)
|
||||||
|
return c.transactf("uid move %s %s", uidSet, astring(destMailbox))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MSNReplace is like the preferred [Conn.UIDReplace], but operates on a message
|
||||||
|
// sequence number (MSN) instead of a UID.
|
||||||
|
//
|
||||||
|
// Required capability: "REPLACE".
|
||||||
|
//
|
||||||
|
// Method [Conn.UIDReplace], operating on UIDs instead of sequence numbers, should be
|
||||||
|
// preferred.
|
||||||
|
func (c *Conn) MSNReplace(msgseq string, mailbox string, msg Append) (resp Response, rerr error) {
|
||||||
// todo: parse msgseq, must be nznumber, with a known msgseq. or "*" with at least one message.
|
// todo: parse msgseq, must be nznumber, with a known msgseq. or "*" with at least one message.
|
||||||
return c.replace("replace", msgseq, mailbox, msg)
|
return c.replace("replace", msgseq, mailbox, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UIDReplace is like Replace, but operates on a UID instead of message
|
// UIDReplace uses the IMAP4 "UID REPLACE" command to replace a message from the
|
||||||
// sequence number.
|
// selected/active mailbox with a new/different version of the message in the named
|
||||||
func (c *Conn) UIDReplace(uid string, mailbox string, msg Append) (untagged []Untagged, result Result, rerr error) {
|
// mailbox, which may be the same or different than the selected mailbox.
|
||||||
|
//
|
||||||
|
// The replaced message is indicated by uid.
|
||||||
|
//
|
||||||
|
// Required capability: "REPLACE".
|
||||||
|
func (c *Conn) UIDReplace(uid string, mailbox string, msg Append) (resp Response, rerr error) {
|
||||||
// todo: parse uid, must be nznumber, with a known uid. or "*" with at least one message.
|
// todo: parse uid, must be nznumber, with a known uid. or "*" with at least one message.
|
||||||
return c.replace("uid replace", uid, mailbox, msg)
|
return c.replace("uid replace", uid, mailbox, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) replace(cmd string, num string, mailbox string, msg Append) (untagged []Untagged, result Result, rerr error) {
|
func (c *Conn) replace(cmd string, num string, mailbox string, msg Append) (resp Response, rerr error) {
|
||||||
defer c.recover(&rerr)
|
defer c.recover(&rerr, &resp)
|
||||||
|
|
||||||
// todo: use synchronizing literal for larger messages.
|
// todo: use synchronizing literal for larger messages.
|
||||||
|
|
||||||
@ -478,7 +587,7 @@ func (c *Conn) replace(cmd string, num string, mailbox string, msg Append) (unta
|
|||||||
}
|
}
|
||||||
// todo: only use literal8 if needed, possibly with "UTF8()"
|
// todo: only use literal8 if needed, possibly with "UTF8()"
|
||||||
// todo: encode mailbox
|
// todo: encode mailbox
|
||||||
err := c.Commandf("", "%s %s %s (%s)%s ~{%d+}", cmd, num, astring(mailbox), strings.Join(msg.Flags, " "), date, msg.Size)
|
err := c.WriteCommandf("", "%s %s %s (%s)%s ~{%d+}", cmd, num, astring(mailbox), strings.Join(msg.Flags, " "), date, msg.Size)
|
||||||
c.xcheckf(err, "writing replace command")
|
c.xcheckf(err, "writing replace command")
|
||||||
|
|
||||||
defer c.xtracewrite(mlog.LevelTracedata)()
|
defer c.xtracewrite(mlog.LevelTracedata)()
|
||||||
@ -489,5 +598,5 @@ func (c *Conn) replace(cmd string, num string, mailbox string, msg Append) (unta
|
|||||||
fmt.Fprintf(c.xbw, "\r\n")
|
fmt.Fprintf(c.xbw, "\r\n")
|
||||||
c.xflush()
|
c.xflush()
|
||||||
|
|
||||||
return c.Response()
|
return c.responseOK()
|
||||||
}
|
}
|
||||||
|
38
imapclient/fuzz_test.go
Normal file
38
imapclient/fuzz_test.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
1610
imapclient/parse.go
1610
imapclient/parse.go
File diff suppressed because it is too large
Load Diff
42
imapclient/parse_test.go
Normal file
42
imapclient/parse_test.go
Normal 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"})
|
||||||
|
}
|
@ -2,42 +2,51 @@ package imapclient
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Capability is a known string for with the ENABLED and CAPABILITY command.
|
// Capability is a known string for with the ENABLED command and response and
|
||||||
|
// CAPABILITY responses. Servers could send unknown values. Always in upper case.
|
||||||
type Capability string
|
type Capability string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
CapIMAP4rev1 Capability = "IMAP4rev1"
|
CapIMAP4rev1 Capability = "IMAP4REV1" // ../rfc/3501:1310
|
||||||
CapIMAP4rev2 Capability = "IMAP4rev2"
|
CapIMAP4rev2 Capability = "IMAP4REV2" // ../rfc/9051:1219
|
||||||
CapLoginDisabled Capability = "LOGINDISABLED"
|
CapLoginDisabled Capability = "LOGINDISABLED" // ../rfc/3501:3792 ../rfc/9051:5436
|
||||||
CapStarttls Capability = "STARTTLS"
|
CapStartTLS Capability = "STARTTLS" // ../rfc/3501:1327 ../rfc/9051:1238
|
||||||
CapAuthPlain Capability = "AUTH=PLAIN"
|
CapAuthPlain Capability = "AUTH=PLAIN" // ../rfc/3501:1327 ../rfc/9051:1238
|
||||||
CapLiteralPlus Capability = "LITERAL+"
|
CapAuthExternal Capability = "AUTH=EXTERNAL" // ../rfc/4422:1575
|
||||||
CapLiteralMinus Capability = "LITERAL-"
|
CapAuthSCRAMSHA256Plus Capability = "AUTH=SCRAM-SHA-256-PLUS" // ../rfc/7677:80
|
||||||
CapIdle Capability = "IDLE"
|
CapAuthSCRAMSHA256 Capability = "AUTH=SCRAM-SHA-256"
|
||||||
CapNamespace Capability = "NAMESPACE"
|
CapAuthSCRAMSHA1Plus Capability = "AUTH=SCRAM-SHA-1-PLUS" // ../rfc/5802:465
|
||||||
CapBinary Capability = "BINARY"
|
CapAuthSCRAMSHA1 Capability = "AUTH=SCRAM-SHA-1"
|
||||||
CapUnselect Capability = "UNSELECT"
|
CapAuthCRAMMD5 Capability = "AUTH=CRAM-MD5" // ../rfc/2195:80
|
||||||
CapUidplus Capability = "UIDPLUS"
|
CapLiteralPlus Capability = "LITERAL+" // ../rfc/2088:45
|
||||||
CapEsearch Capability = "ESEARCH"
|
CapLiteralMinus Capability = "LITERAL-" // ../rfc/7888:26 ../rfc/9051:847 Default since IMAP4rev2
|
||||||
CapEnable Capability = "ENABLE"
|
CapIdle Capability = "IDLE" // ../rfc/2177:69 ../rfc/9051:3542 Default since IMAP4rev2
|
||||||
CapSave Capability = "SAVE"
|
CapNamespace Capability = "NAMESPACE" // ../rfc/2342:130 ../rfc/9051:135 Default since IMAP4rev2
|
||||||
CapListExtended Capability = "LIST-EXTENDED"
|
CapBinary Capability = "BINARY" // ../rfc/3516:100
|
||||||
CapSpecialUse Capability = "SPECIAL-USE"
|
CapUnselect Capability = "UNSELECT" // ../rfc/3691:78 ../rfc/9051:3667 Default since IMAP4rev2
|
||||||
CapMove Capability = "MOVE"
|
CapUidplus Capability = "UIDPLUS" // ../rfc/4315:36 ../rfc/9051:8015 Default since IMAP4rev2
|
||||||
|
CapEsearch Capability = "ESEARCH" // ../rfc/4731:69 ../rfc/9051:8016 Default since IMAP4rev2
|
||||||
|
CapEnable Capability = "ENABLE" // ../rfc/5161:52 ../rfc/9051:8016 Default since IMAP4rev2
|
||||||
|
CapListExtended Capability = "LIST-EXTENDED" // ../rfc/5258:150 ../rfc/9051:7987 Syntax except multiple mailboxes default since IMAP4rev2
|
||||||
|
CapSpecialUse Capability = "SPECIAL-USE" // ../rfc/6154:156 ../rfc/9051:8021 Special-use attributes in LIST responses by default since IMAP4rev2
|
||||||
|
CapMove Capability = "MOVE" // ../rfc/6851:87 ../rfc/9051:8018 Default since IMAP4rev2
|
||||||
CapUTF8Only Capability = "UTF8=ONLY"
|
CapUTF8Only Capability = "UTF8=ONLY"
|
||||||
CapUTF8Accept Capability = "UTF8=ACCEPT"
|
CapUTF8Accept Capability = "UTF8=ACCEPT"
|
||||||
|
CapCondstore Capability = "CONDSTORE" // ../rfc/7162:411
|
||||||
|
CapQresync Capability = "QRESYNC" // ../rfc/7162:1376
|
||||||
CapID Capability = "ID" // ../rfc/2971:80
|
CapID Capability = "ID" // ../rfc/2971:80
|
||||||
CapMetadata Capability = "METADATA" // ../rfc/5464:124
|
CapMetadata Capability = "METADATA" // ../rfc/5464:124
|
||||||
CapMetadataServer Capability = "METADATA-SERVER" // ../rfc/5464:124
|
CapMetadataServer Capability = "METADATA-SERVER" // ../rfc/5464:124
|
||||||
CapSaveDate Capability = "SAVEDATE" // ../rfc/8514
|
CapSaveDate Capability = "SAVEDATE" // ../rfc/8514
|
||||||
CapCreateSpecialUse Capability = "CREATE-SPECIAL-USE" // ../rfc/6154:296
|
CapCreateSpecialUse Capability = "CREATE-SPECIAL-USE" // ../rfc/6154:296
|
||||||
CapCompressDeflate Capability = "COMPRESS=DEFLATE" // ../rfc/4978:65
|
CapCompressDeflate Capability = "COMPRESS=DEFLATE" // ../rfc/4978:65
|
||||||
CapListMetadata Capability = "LIST-METADTA" // ../rfc/9590:73
|
CapListMetadata Capability = "LIST-METADATA" // ../rfc/9590:73
|
||||||
CapMultiAppend Capability = "MULTIAPPEND" // ../rfc/3502:33
|
CapMultiAppend Capability = "MULTIAPPEND" // ../rfc/3502:33
|
||||||
CapReplace Capability = "REPLACE" // ../rfc/8508:155
|
CapReplace Capability = "REPLACE" // ../rfc/8508:155
|
||||||
CapPreview Capability = "PREVIEW" // ../rfc/8970:114
|
CapPreview Capability = "PREVIEW" // ../rfc/8970:114
|
||||||
@ -55,63 +64,134 @@ const (
|
|||||||
OK Status = "OK" // Command succeeded.
|
OK Status = "OK" // Command succeeded.
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Response is a response to an IMAP command including any preceding untagged
|
||||||
|
// responses. Response implements the error interface through result.
|
||||||
|
//
|
||||||
|
// See [UntaggedResponseGet] and [UntaggedResponseList] to retrieve specific types
|
||||||
|
// of untagged responses.
|
||||||
|
type Response struct {
|
||||||
|
Untagged []Untagged
|
||||||
|
Result
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrMissing = errors.New("no response of type") // Returned by UntaggedResponseGet.
|
||||||
|
ErrMultiple = errors.New("multiple responses of type") // Idem.
|
||||||
|
)
|
||||||
|
|
||||||
|
// UntaggedResponseGet returns the single untagged response of type T. Only
|
||||||
|
// [ErrMissing] or [ErrMultiple] can be returned as error.
|
||||||
|
func UntaggedResponseGet[T Untagged](resp Response) (T, error) {
|
||||||
|
var t T
|
||||||
|
var have bool
|
||||||
|
for _, e := range resp.Untagged {
|
||||||
|
if tt, ok := e.(T); ok {
|
||||||
|
if have {
|
||||||
|
return t, ErrMultiple
|
||||||
|
}
|
||||||
|
t = tt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !have {
|
||||||
|
return t, ErrMissing
|
||||||
|
}
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UntaggedResponseList returns all untagged responses of type T.
|
||||||
|
func UntaggedResponseList[T Untagged](resp Response) []T {
|
||||||
|
var l []T
|
||||||
|
for _, e := range resp.Untagged {
|
||||||
|
if tt, ok := e.(T); ok {
|
||||||
|
l = append(l, tt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
// Result is the final response for a command, indicating success or failure.
|
// Result is the final response for a command, indicating success or failure.
|
||||||
type Result struct {
|
type Result struct {
|
||||||
Status Status
|
Status Status
|
||||||
RespText
|
Code Code // Set if response code is present.
|
||||||
|
Text string // Any remaining text.
|
||||||
}
|
}
|
||||||
|
|
||||||
// CodeArg represents a response code with arguments, i.e. the data between [] in the response line.
|
func (r Result) Error() string {
|
||||||
type CodeArg interface {
|
s := fmt.Sprintf("IMAP result %s", r.Status)
|
||||||
|
if r.Code != nil {
|
||||||
|
s += "[" + r.Code.CodeString() + "]"
|
||||||
|
}
|
||||||
|
if r.Text != "" {
|
||||||
|
s += " " + r.Text
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Code represents a response code with optional arguments, i.e. the data between [] in the response line.
|
||||||
|
type Code interface {
|
||||||
CodeString() string
|
CodeString() string
|
||||||
}
|
}
|
||||||
|
|
||||||
// CodeOther is a valid but unrecognized response code.
|
// CodeWord is a response code without parameters, always in upper case.
|
||||||
type CodeOther struct {
|
type CodeWord string
|
||||||
Code string
|
|
||||||
|
func (c CodeWord) CodeString() string {
|
||||||
|
return string(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CodeOther is an unrecognized response code with parameters.
|
||||||
|
type CodeParams struct {
|
||||||
|
Code string // Always in upper case.
|
||||||
Args []string
|
Args []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c CodeOther) CodeString() string {
|
func (c CodeParams) CodeString() string {
|
||||||
return c.Code + " " + strings.Join(c.Args, " ")
|
return c.Code + " " + strings.Join(c.Args, " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// CodeWords is a code with space-separated string parameters. E.g. CAPABILITY.
|
// CodeCapability is a CAPABILITY response code with the capabilities supported by the server.
|
||||||
type CodeWords struct {
|
type CodeCapability []Capability
|
||||||
Code string
|
|
||||||
Args []string
|
func (c CodeCapability) CodeString() string {
|
||||||
|
var s string
|
||||||
|
for _, c := range c {
|
||||||
|
s += " " + string(c)
|
||||||
|
}
|
||||||
|
return "CAPABILITY" + s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c CodeWords) CodeString() string {
|
type CodeBadCharset []string
|
||||||
s := c.Code
|
|
||||||
for _, w := range c.Args {
|
func (c CodeBadCharset) CodeString() string {
|
||||||
s += " " + w
|
s := "BADCHARSET"
|
||||||
}
|
if len(c) == 0 {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
return s + " (" + strings.Join([]string(c), " ") + ")"
|
||||||
// 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.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c CodeList) CodeString() string {
|
type CodePermanentFlags []string
|
||||||
s := c.Code
|
|
||||||
if c.Args == nil {
|
func (c CodePermanentFlags) CodeString() string {
|
||||||
return s
|
return "PERMANENTFLAGS (" + strings.Join([]string(c), " ") + ")"
|
||||||
}
|
|
||||||
return s + "(" + strings.Join(c.Args, " ") + ")"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CodeUint is a code with a uint32 parameter, e.g. UIDNEXT and UIDVALIDITY.
|
type CodeUIDNext uint32
|
||||||
type CodeUint struct {
|
|
||||||
Code string
|
func (c CodeUIDNext) CodeString() string {
|
||||||
Num uint32
|
return fmt.Sprintf("UIDNEXT %d", c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c CodeUint) CodeString() string {
|
type CodeUIDValidity uint32
|
||||||
return fmt.Sprintf("%s %d", c.Code, c.Num)
|
|
||||||
|
func (c CodeUIDValidity) CodeString() string {
|
||||||
|
return fmt.Sprintf("UIDVALIDITY %d", c)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CodeUnseen uint32
|
||||||
|
|
||||||
|
func (c CodeUnseen) CodeString() string {
|
||||||
|
return fmt.Sprintf("UNSEEN %d", c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// "APPENDUID" response code.
|
// "APPENDUID" response code.
|
||||||
@ -196,11 +276,32 @@ func (c CodeBadEvent) CodeString() string {
|
|||||||
return fmt.Sprintf("BADEVENT (%s)", strings.Join([]string(c), " "))
|
return fmt.Sprintf("BADEVENT (%s)", strings.Join([]string(c), " "))
|
||||||
}
|
}
|
||||||
|
|
||||||
// RespText represents a response line minus the leading tag.
|
// "METADATA LONGENTRIES number" response for GETMETADATA command.
|
||||||
type RespText struct {
|
type CodeMetadataLongEntries uint32
|
||||||
Code string // The first word between [] after the status.
|
|
||||||
CodeArg CodeArg // Set if code has a parameter.
|
func (c CodeMetadataLongEntries) CodeString() string {
|
||||||
More string // Any remaining text.
|
return fmt.Sprintf("METADATA LONGENTRIES %d", c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// "METADATA (MAXSIZE number)" response for SETMETADATA command.
|
||||||
|
type CodeMetadataMaxSize uint32
|
||||||
|
|
||||||
|
func (c CodeMetadataMaxSize) CodeString() string {
|
||||||
|
return fmt.Sprintf("METADATA (MAXSIZE %d)", c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// "METADATA (TOOMANY)" response for SETMETADATA command.
|
||||||
|
type CodeMetadataTooMany struct{}
|
||||||
|
|
||||||
|
func (c CodeMetadataTooMany) CodeString() string {
|
||||||
|
return "METADATA (TOOMANY)"
|
||||||
|
}
|
||||||
|
|
||||||
|
// "METADATA (NOPRIVATE)" response for SETMETADATA command.
|
||||||
|
type CodeMetadataNoPrivate struct{}
|
||||||
|
|
||||||
|
func (c CodeMetadataNoPrivate) CodeString() string {
|
||||||
|
return "METADATA (NOPRIVATE)"
|
||||||
}
|
}
|
||||||
|
|
||||||
// atom or string.
|
// atom or string.
|
||||||
@ -241,17 +342,30 @@ func syncliteral(s string) string {
|
|||||||
// todo: make an interface that the untagged responses implement?
|
// todo: make an interface that the untagged responses implement?
|
||||||
type Untagged any
|
type Untagged any
|
||||||
|
|
||||||
type UntaggedBye RespText
|
type UntaggedBye struct {
|
||||||
type UntaggedPreauth RespText
|
Code Code // Set if response code is present.
|
||||||
|
Text string // Any remaining text.
|
||||||
|
}
|
||||||
|
type UntaggedPreauth struct {
|
||||||
|
Code Code // Set if response code is present.
|
||||||
|
Text string // Any remaining text.
|
||||||
|
}
|
||||||
type UntaggedExpunge uint32
|
type UntaggedExpunge uint32
|
||||||
type UntaggedExists uint32
|
type UntaggedExists uint32
|
||||||
type UntaggedRecent uint32
|
type UntaggedRecent uint32
|
||||||
type UntaggedCapability []string
|
|
||||||
type UntaggedEnabled []string
|
// UntaggedCapability lists all capabilities the server implements.
|
||||||
|
type UntaggedCapability []Capability
|
||||||
|
|
||||||
|
// UntaggedEnabled indicates the capabilities that were enabled on the connection
|
||||||
|
// by the server, typically in response to an ENABLE command.
|
||||||
|
type UntaggedEnabled []Capability
|
||||||
|
|
||||||
type UntaggedResult Result
|
type UntaggedResult Result
|
||||||
type UntaggedFlags []string
|
type UntaggedFlags []string
|
||||||
type UntaggedList struct {
|
type UntaggedList struct {
|
||||||
// ../rfc/9051:6690
|
// ../rfc/9051:6690
|
||||||
|
|
||||||
Flags []string
|
Flags []string
|
||||||
Separator byte // 0 for NIL
|
Separator byte // 0 for NIL
|
||||||
Mailbox string
|
Mailbox string
|
||||||
@ -272,8 +386,9 @@ type UntaggedUIDFetch struct {
|
|||||||
}
|
}
|
||||||
type UntaggedSearch []uint32
|
type UntaggedSearch []uint32
|
||||||
|
|
||||||
// ../rfc/7162:1101
|
|
||||||
type UntaggedSearchModSeq struct {
|
type UntaggedSearchModSeq struct {
|
||||||
|
// ../rfc/7162:1101
|
||||||
|
|
||||||
Nums []uint32
|
Nums []uint32
|
||||||
ModSeq int64
|
ModSeq int64
|
||||||
}
|
}
|
||||||
@ -282,8 +397,10 @@ type UntaggedStatus struct {
|
|||||||
Attrs map[StatusAttr]int64 // Upper case status attributes.
|
Attrs map[StatusAttr]int64 // Upper case status attributes.
|
||||||
}
|
}
|
||||||
|
|
||||||
// ../rfc/5464:716 Unsolicited response, indicating an annotation has changed.
|
// Unsolicited response, indicating an annotation has changed.
|
||||||
type UntaggedMetadataKeys struct {
|
type UntaggedMetadataKeys struct {
|
||||||
|
// ../rfc/5464:716
|
||||||
|
|
||||||
Mailbox string // Empty means not specific to mailbox.
|
Mailbox string // Empty means not specific to mailbox.
|
||||||
|
|
||||||
// Keys that have changed. To get values (or determine absence), the server must be
|
// Keys that have changed. To get values (or determine absence), the server must be
|
||||||
@ -299,15 +416,17 @@ type Annotation struct {
|
|||||||
Value []byte
|
Value []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// ../rfc/5464:683
|
|
||||||
type UntaggedMetadataAnnotations struct {
|
type UntaggedMetadataAnnotations struct {
|
||||||
|
// ../rfc/5464:683
|
||||||
|
|
||||||
Mailbox string // Empty means not specific to mailbox.
|
Mailbox string // Empty means not specific to mailbox.
|
||||||
Annotations []Annotation
|
Annotations []Annotation
|
||||||
}
|
}
|
||||||
|
|
||||||
// ../rfc/9051:7059 ../9208:712
|
|
||||||
type StatusAttr string
|
type StatusAttr string
|
||||||
|
|
||||||
|
// ../rfc/9051:7059 ../9208:712
|
||||||
|
|
||||||
const (
|
const (
|
||||||
StatusMessages StatusAttr = "MESSAGES"
|
StatusMessages StatusAttr = "MESSAGES"
|
||||||
StatusUIDNext StatusAttr = "UIDNEXT"
|
StatusUIDNext StatusAttr = "UIDNEXT"
|
||||||
@ -326,6 +445,7 @@ type UntaggedNamespace struct {
|
|||||||
}
|
}
|
||||||
type UntaggedLsub struct {
|
type UntaggedLsub struct {
|
||||||
// ../rfc/3501:4833
|
// ../rfc/3501:4833
|
||||||
|
|
||||||
Flags []string
|
Flags []string
|
||||||
Separator byte
|
Separator byte
|
||||||
Mailbox string
|
Mailbox string
|
||||||
@ -395,6 +515,7 @@ type EsearchDataExt struct {
|
|||||||
|
|
||||||
type NamespaceDescr struct {
|
type NamespaceDescr struct {
|
||||||
// ../rfc/9051:6769
|
// ../rfc/9051:6769
|
||||||
|
|
||||||
Prefix string
|
Prefix string
|
||||||
Separator byte // If 0 then separator was absent.
|
Separator byte // If 0 then separator was absent.
|
||||||
Exts []NamespaceExtension
|
Exts []NamespaceExtension
|
||||||
@ -402,13 +523,14 @@ type NamespaceDescr struct {
|
|||||||
|
|
||||||
type NamespaceExtension struct {
|
type NamespaceExtension struct {
|
||||||
// ../rfc/9051:6773
|
// ../rfc/9051:6773
|
||||||
|
|
||||||
Key string
|
Key string
|
||||||
Values []string
|
Values []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchAttr represents a FETCH response attribute.
|
// FetchAttr represents a FETCH response attribute.
|
||||||
type FetchAttr interface {
|
type FetchAttr interface {
|
||||||
Attr() string // Name of attribute.
|
Attr() string // Name of attribute in upper case, e.g. "UID".
|
||||||
}
|
}
|
||||||
|
|
||||||
type NumSet struct {
|
type NumSet struct {
|
||||||
@ -435,14 +557,14 @@ func (ns NumSet) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ParseNumSet(s string) (ns NumSet, rerr error) {
|
func ParseNumSet(s string) (ns NumSet, rerr error) {
|
||||||
c := Conn{br: bufio.NewReader(strings.NewReader(s))}
|
c := Proto{br: bufio.NewReader(strings.NewReader(s))}
|
||||||
defer c.recover(&rerr)
|
defer c.recover(&rerr)
|
||||||
ns = c.xsequenceSet()
|
ns = c.xsequenceSet()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseUIDRange(s string) (nr NumRange, rerr error) {
|
func ParseUIDRange(s string) (nr NumRange, rerr error) {
|
||||||
c := Conn{br: bufio.NewReader(strings.NewReader(s))}
|
c := Proto{br: bufio.NewReader(strings.NewReader(s))}
|
||||||
defer c.recover(&rerr)
|
defer c.recover(&rerr)
|
||||||
nr = c.xuidrange()
|
nr = c.xuidrange()
|
||||||
return
|
return
|
||||||
@ -481,6 +603,7 @@ type TaggedExtComp struct {
|
|||||||
|
|
||||||
type TaggedExtVal struct {
|
type TaggedExtVal struct {
|
||||||
// ../rfc/9051:7111
|
// ../rfc/9051:7111
|
||||||
|
|
||||||
Number *int64
|
Number *int64
|
||||||
SeqSet *NumSet
|
SeqSet *NumSet
|
||||||
Comp *TaggedExtComp // If SimpleNumber and SimpleSeqSet is nil, this is a Comp. But Comp is optional and can also be nil. Not great.
|
Comp *TaggedExtComp // If SimpleNumber and SimpleSeqSet is nil, this is a Comp. But Comp is optional and can also be nil. Not great.
|
||||||
@ -488,6 +611,7 @@ type TaggedExtVal struct {
|
|||||||
|
|
||||||
type MboxListExtendedItem struct {
|
type MboxListExtendedItem struct {
|
||||||
// ../rfc/9051:6699
|
// ../rfc/9051:6699
|
||||||
|
|
||||||
Tag string
|
Tag string
|
||||||
Val TaggedExtVal
|
Val TaggedExtVal
|
||||||
}
|
}
|
||||||
@ -522,8 +646,10 @@ type FetchInternalDate struct {
|
|||||||
|
|
||||||
func (f FetchInternalDate) Attr() string { return "INTERNALDATE" }
|
func (f FetchInternalDate) Attr() string { return "INTERNALDATE" }
|
||||||
|
|
||||||
// "SAVEDATE" fetch response. ../rfc/8514:265
|
// "SAVEDATE" fetch response.
|
||||||
type FetchSaveDate struct {
|
type FetchSaveDate struct {
|
||||||
|
// ../rfc/8514:265
|
||||||
|
|
||||||
SaveDate *time.Time // nil means absent for message.
|
SaveDate *time.Time // nil means absent for message.
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -552,6 +678,7 @@ func (f FetchRFC822Text) Attr() string { return "RFC822.TEXT" }
|
|||||||
// "BODYSTRUCTURE" fetch response.
|
// "BODYSTRUCTURE" fetch response.
|
||||||
type FetchBodystructure struct {
|
type FetchBodystructure struct {
|
||||||
// ../rfc/9051:6355
|
// ../rfc/9051:6355
|
||||||
|
|
||||||
RespAttr string
|
RespAttr string
|
||||||
Body any // BodyType*
|
Body any // BodyType*
|
||||||
}
|
}
|
||||||
@ -561,6 +688,7 @@ func (f FetchBodystructure) Attr() string { return f.RespAttr }
|
|||||||
// "BODY" fetch response.
|
// "BODY" fetch response.
|
||||||
type FetchBody struct {
|
type FetchBody struct {
|
||||||
// ../rfc/9051:6756 ../rfc/9051:6985
|
// ../rfc/9051:6756 ../rfc/9051:6985
|
||||||
|
|
||||||
RespAttr string
|
RespAttr string
|
||||||
Section string // todo: parse more ../rfc/9051:6985
|
Section string // todo: parse more ../rfc/9051:6985
|
||||||
Offset int32
|
Offset int32
|
||||||
@ -580,6 +708,7 @@ type BodyFields struct {
|
|||||||
// subparts and the multipart media subtype. Used in a FETCH response.
|
// subparts and the multipart media subtype. Used in a FETCH response.
|
||||||
type BodyTypeMpart struct {
|
type BodyTypeMpart struct {
|
||||||
// ../rfc/9051:6411
|
// ../rfc/9051:6411
|
||||||
|
|
||||||
Bodies []any // BodyTypeBasic, BodyTypeMsg, BodyTypeText
|
Bodies []any // BodyTypeBasic, BodyTypeMsg, BodyTypeText
|
||||||
MediaSubtype string
|
MediaSubtype string
|
||||||
Ext *BodyExtensionMpart
|
Ext *BodyExtensionMpart
|
||||||
@ -589,6 +718,7 @@ type BodyTypeMpart struct {
|
|||||||
// response.
|
// response.
|
||||||
type BodyTypeBasic struct {
|
type BodyTypeBasic struct {
|
||||||
// ../rfc/9051:6407
|
// ../rfc/9051:6407
|
||||||
|
|
||||||
MediaType, MediaSubtype string
|
MediaType, MediaSubtype string
|
||||||
BodyFields BodyFields
|
BodyFields BodyFields
|
||||||
Ext *BodyExtension1Part
|
Ext *BodyExtension1Part
|
||||||
@ -598,6 +728,7 @@ type BodyTypeBasic struct {
|
|||||||
// response.
|
// response.
|
||||||
type BodyTypeMsg struct {
|
type BodyTypeMsg struct {
|
||||||
// ../rfc/9051:6415
|
// ../rfc/9051:6415
|
||||||
|
|
||||||
MediaType, MediaSubtype string
|
MediaType, MediaSubtype string
|
||||||
BodyFields BodyFields
|
BodyFields BodyFields
|
||||||
Envelope Envelope
|
Envelope Envelope
|
||||||
@ -610,6 +741,7 @@ type BodyTypeMsg struct {
|
|||||||
// response.
|
// response.
|
||||||
type BodyTypeText struct {
|
type BodyTypeText struct {
|
||||||
// ../rfc/9051:6418
|
// ../rfc/9051:6418
|
||||||
|
|
||||||
MediaType, MediaSubtype string
|
MediaType, MediaSubtype string
|
||||||
BodyFields BodyFields
|
BodyFields BodyFields
|
||||||
Lines int64
|
Lines int64
|
||||||
@ -618,26 +750,42 @@ type BodyTypeText struct {
|
|||||||
|
|
||||||
// BodyExtension1Part has the extensible form fields of a BODYSTRUCTURE for
|
// BodyExtension1Part has the extensible form fields of a BODYSTRUCTURE for
|
||||||
// multiparts.
|
// multiparts.
|
||||||
|
//
|
||||||
|
// Fields in this struct are optional in IMAP4, and can be NIL or contain a value.
|
||||||
|
// The first field is always present, otherwise the "parent" struct would have a
|
||||||
|
// nil *BodyExtensionMpart. The second and later fields are nil when absent. For
|
||||||
|
// non-reference types (e.g. strings), an IMAP4 NIL is represented as a pointer to
|
||||||
|
// (*T)(nil). For reference types (e.g. slices), an IMAP4 NIL is represented by a
|
||||||
|
// pointer to nil.
|
||||||
type BodyExtensionMpart struct {
|
type BodyExtensionMpart struct {
|
||||||
// ../rfc/9051:5986 ../rfc/3501:4161 ../rfc/9051:6371 ../rfc/3501:4599
|
// ../rfc/9051:5986 ../rfc/3501:4161 ../rfc/9051:6371 ../rfc/3501:4599
|
||||||
|
|
||||||
Params [][2]string
|
Params [][2]string
|
||||||
Disposition string
|
Disposition **string
|
||||||
DispositionParams [][2]string
|
DispositionParams *[][2]string
|
||||||
Language []string
|
Language *[]string
|
||||||
Location string
|
Location **string
|
||||||
More []BodyExtension
|
More []BodyExtension // Nil if absent.
|
||||||
}
|
}
|
||||||
|
|
||||||
// BodyExtension1Part has the extensible form fields of a BODYSTRUCTURE for
|
// BodyExtension1Part has the extensible form fields of a BODYSTRUCTURE for
|
||||||
// non-multiparts.
|
// non-multiparts.
|
||||||
|
//
|
||||||
|
// Fields in this struct are optional in IMAP4, and can be NIL or contain a value.
|
||||||
|
// The first field is always present, otherwise the "parent" struct would have a
|
||||||
|
// nil *BodyExtensionMpart. The second and later fields are nil when absent. For
|
||||||
|
// non-reference types (e.g. strings), an IMAP4 NIL is represented as a pointer to
|
||||||
|
// (*T)(nil). For reference types (e.g. slices), an IMAP4 NIL is represented by a
|
||||||
|
// pointer to nil.
|
||||||
type BodyExtension1Part struct {
|
type BodyExtension1Part struct {
|
||||||
// ../rfc/9051:6023 ../rfc/3501:4191 ../rfc/9051:6366 ../rfc/3501:4584
|
// ../rfc/9051:6023 ../rfc/3501:4191 ../rfc/9051:6366 ../rfc/3501:4584
|
||||||
MD5 string
|
|
||||||
Disposition string
|
MD5 *string
|
||||||
DispositionParams [][2]string
|
Disposition **string
|
||||||
Language []string
|
DispositionParams *[][2]string
|
||||||
Location string
|
Language *[]string
|
||||||
More []BodyExtension
|
Location **string
|
||||||
|
More []BodyExtension // Nil means absent.
|
||||||
}
|
}
|
||||||
|
|
||||||
// BodyExtension has the additional extension fields for future expansion of
|
// BodyExtension has the additional extension fields for future expansion of
|
||||||
|
@ -50,14 +50,14 @@ func testAppend(t *testing.T, uidonly bool) {
|
|||||||
tc2.client.Select("inbox")
|
tc2.client.Select("inbox")
|
||||||
|
|
||||||
tc2.transactf("no", "append nobox (\\Seen) \" 1-Jan-2022 10:10:00 +0100\" {1}")
|
tc2.transactf("no", "append nobox (\\Seen) \" 1-Jan-2022 10:10:00 +0100\" {1}")
|
||||||
tc2.xcode("TRYCREATE")
|
tc2.xcodeWord("TRYCREATE")
|
||||||
|
|
||||||
tc2.transactf("no", "append expungebox (\\Seen) {1}")
|
tc2.transactf("no", "append expungebox (\\Seen) {1}")
|
||||||
tc2.xcode("TRYCREATE")
|
tc2.xcodeWord("TRYCREATE")
|
||||||
|
|
||||||
tc2.transactf("ok", "append inbox (\\Seen Label1 $label2) \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
|
tc2.transactf("ok", "append inbox (\\Seen Label1 $label2) \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
|
||||||
tc2.xuntagged(imapclient.UntaggedExists(1))
|
tc2.xuntagged(imapclient.UntaggedExists(1))
|
||||||
tc2.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("1")})
|
tc2.xcode(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("1")})
|
||||||
|
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
flags := imapclient.FetchFlags{`\Seen`, "$label2", "label1"}
|
flags := imapclient.FetchFlags{`\Seen`, "$label2", "label1"}
|
||||||
@ -67,11 +67,11 @@ func testAppend(t *testing.T, uidonly bool) {
|
|||||||
|
|
||||||
tc2.transactf("ok", "append inbox (\\Seen) \" 1-Jan-2022 10:10:00 +0100\" UTF8 (~{47+}\r\ncontent-type: just completely invalid;;\r\n\r\ntest)")
|
tc2.transactf("ok", "append inbox (\\Seen) \" 1-Jan-2022 10:10:00 +0100\" UTF8 (~{47+}\r\ncontent-type: just completely invalid;;\r\n\r\ntest)")
|
||||||
tc2.xuntagged(imapclient.UntaggedExists(2))
|
tc2.xuntagged(imapclient.UntaggedExists(2))
|
||||||
tc2.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("2")})
|
tc2.xcode(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("2")})
|
||||||
|
|
||||||
tc2.transactf("ok", "append inbox (\\Seen) \" 1-Jan-2022 10:10:00 +0100\" UTF8 (~{31+}\r\ncontent-type: text/plain;\n\ntest)")
|
tc2.transactf("ok", "append inbox (\\Seen) \" 1-Jan-2022 10:10:00 +0100\" UTF8 (~{31+}\r\ncontent-type: text/plain;\n\ntest)")
|
||||||
tc2.xuntagged(imapclient.UntaggedExists(3))
|
tc2.xuntagged(imapclient.UntaggedExists(3))
|
||||||
tc2.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("3")})
|
tc2.xcode(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("3")})
|
||||||
|
|
||||||
// Messages that we cannot parse are marked as application/octet-stream. Perhaps
|
// Messages that we cannot parse are marked as application/octet-stream. Perhaps
|
||||||
// the imap client knows how to deal with them.
|
// the imap client knows how to deal with them.
|
||||||
@ -92,7 +92,7 @@ func testAppend(t *testing.T, uidonly bool) {
|
|||||||
tc.transactf("ok", "noop") // Flush pending untagged responses.
|
tc.transactf("ok", "noop") // Flush pending untagged responses.
|
||||||
tc.transactf("ok", "append inbox {6+}\r\ntest\r\n ~{6+}\r\ntost\r\n")
|
tc.transactf("ok", "append inbox {6+}\r\ntest\r\n ~{6+}\r\ntost\r\n")
|
||||||
tc.xuntagged(imapclient.UntaggedExists(5))
|
tc.xuntagged(imapclient.UntaggedExists(5))
|
||||||
tc.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("4:5")})
|
tc.xcode(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("4:5")})
|
||||||
|
|
||||||
// Cancelled with zero-length message.
|
// Cancelled with zero-length message.
|
||||||
tc.transactf("no", "append inbox {6+}\r\ntest\r\n {0+}\r\n")
|
tc.transactf("no", "append inbox {6+}\r\ntest\r\n {0+}\r\n")
|
||||||
@ -106,7 +106,7 @@ func testAppend(t *testing.T, uidonly bool) {
|
|||||||
tclimit.xuntagged(imapclient.UntaggedExists(1))
|
tclimit.xuntagged(imapclient.UntaggedExists(1))
|
||||||
// Second message would take account past limit.
|
// Second message would take account past limit.
|
||||||
tclimit.transactf("no", "append inbox (\\Seen Label1 $label2) \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
|
tclimit.transactf("no", "append inbox (\\Seen Label1 $label2) \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
|
||||||
tclimit.xcode("OVERQUOTA")
|
tclimit.xcodeWord("OVERQUOTA")
|
||||||
|
|
||||||
// Empty mailbox.
|
// Empty mailbox.
|
||||||
if uidonly {
|
if uidonly {
|
||||||
@ -119,10 +119,10 @@ func testAppend(t *testing.T, uidonly bool) {
|
|||||||
// Multiappend with first message within quota, and second message with sync
|
// Multiappend with first message within quota, and second message with sync
|
||||||
// literal causing quota error. Request should get error response immediately.
|
// literal causing quota error. Request should get error response immediately.
|
||||||
tclimit.transactf("no", "append inbox {1+}\r\nx {100000}")
|
tclimit.transactf("no", "append inbox {1+}\r\nx {100000}")
|
||||||
tclimit.xcode("OVERQUOTA")
|
tclimit.xcodeWord("OVERQUOTA")
|
||||||
|
|
||||||
// Again, but second message now with non-sync literal, which is fully consumed by server.
|
// Again, but second message now with non-sync literal, which is fully consumed by server.
|
||||||
tclimit.client.Commandf("", "append inbox {1+}\r\nx {4000+}")
|
tclimit.client.WriteCommandf("", "append inbox {1+}\r\nx {4000+}")
|
||||||
buf := make([]byte, 4000, 4002)
|
buf := make([]byte, 4000, 4002)
|
||||||
for i := range buf {
|
for i := range buf {
|
||||||
buf[i] = 'x'
|
buf[i] = 'x'
|
||||||
@ -131,5 +131,5 @@ func testAppend(t *testing.T, uidonly bool) {
|
|||||||
_, err := tclimit.client.Write(buf)
|
_, err := tclimit.client.Write(buf)
|
||||||
tclimit.check(err, "write append message")
|
tclimit.check(err, "write append message")
|
||||||
tclimit.response("no")
|
tclimit.response("no")
|
||||||
tclimit.xcode("OVERQUOTA")
|
tclimit.xcodeWord("OVERQUOTA")
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ import (
|
|||||||
|
|
||||||
"golang.org/x/text/secure/precis"
|
"golang.org/x/text/secure/precis"
|
||||||
|
|
||||||
|
"github.com/mjl-/mox/imapclient"
|
||||||
"github.com/mjl-/mox/mox-"
|
"github.com/mjl-/mox/mox-"
|
||||||
"github.com/mjl-/mox/scram"
|
"github.com/mjl-/mox/scram"
|
||||||
"github.com/mjl-/mox/store"
|
"github.com/mjl-/mox/store"
|
||||||
@ -38,19 +39,19 @@ func TestAuthenticatePlain(t *testing.T) {
|
|||||||
tc.transactf("no", "authenticate bogus ")
|
tc.transactf("no", "authenticate bogus ")
|
||||||
tc.transactf("bad", "authenticate plain not base64...")
|
tc.transactf("bad", "authenticate plain not base64...")
|
||||||
tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000baduser\u0000badpass")))
|
tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000baduser\u0000badpass")))
|
||||||
tc.xcode("AUTHENTICATIONFAILED")
|
tc.xcodeWord("AUTHENTICATIONFAILED")
|
||||||
tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000badpass")))
|
tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000badpass")))
|
||||||
tc.xcode("AUTHENTICATIONFAILED")
|
tc.xcodeWord("AUTHENTICATIONFAILED")
|
||||||
tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl\u0000badpass"))) // Need email, not account.
|
tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl\u0000badpass"))) // Need email, not account.
|
||||||
tc.xcode("AUTHENTICATIONFAILED")
|
tc.xcodeWord("AUTHENTICATIONFAILED")
|
||||||
tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000test")))
|
tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000test")))
|
||||||
tc.xcode("AUTHENTICATIONFAILED")
|
tc.xcodeWord("AUTHENTICATIONFAILED")
|
||||||
tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000test"+password0)))
|
tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000test"+password0)))
|
||||||
tc.xcode("AUTHENTICATIONFAILED")
|
tc.xcodeWord("AUTHENTICATIONFAILED")
|
||||||
tc.transactf("bad", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000")))
|
tc.transactf("bad", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000")))
|
||||||
tc.xcode("")
|
tc.xcode(nil)
|
||||||
tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("other\u0000mjl@mox.example\u0000"+password0)))
|
tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("other\u0000mjl@mox.example\u0000"+password0)))
|
||||||
tc.xcode("AUTHORIZATIONFAILED")
|
tc.xcodeWord("AUTHORIZATIONFAILED")
|
||||||
tc.transactf("ok", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000"+password0)))
|
tc.transactf("ok", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000"+password0)))
|
||||||
tc.close()
|
tc.close()
|
||||||
|
|
||||||
@ -93,14 +94,14 @@ func TestLoginDisabled(t *testing.T) {
|
|||||||
tcheck(t, err, "close account")
|
tcheck(t, err, "close account")
|
||||||
|
|
||||||
tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000disabled@mox.example\u0000test1234")))
|
tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000disabled@mox.example\u0000test1234")))
|
||||||
tc.xcode("")
|
tc.xcode(nil)
|
||||||
tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000disabled@mox.example\u0000bogus")))
|
tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000disabled@mox.example\u0000bogus")))
|
||||||
tc.xcode("AUTHENTICATIONFAILED")
|
tc.xcodeWord("AUTHENTICATIONFAILED")
|
||||||
|
|
||||||
tc.transactf("no", "login disabled@mox.example test1234")
|
tc.transactf("no", "login disabled@mox.example test1234")
|
||||||
tc.xcode("")
|
tc.xcode(nil)
|
||||||
tc.transactf("no", "login disabled@mox.example bogus")
|
tc.transactf("no", "login disabled@mox.example bogus")
|
||||||
tc.xcode("AUTHENTICATIONFAILED")
|
tc.xcodeWord("AUTHENTICATIONFAILED")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthenticateSCRAMSHA1(t *testing.T) {
|
func TestAuthenticateSCRAMSHA1(t *testing.T) {
|
||||||
@ -131,14 +132,11 @@ func testAuthenticateSCRAM(t *testing.T, tls bool, method string, h func() hash.
|
|||||||
sc := scram.NewClient(h, username, "", noServerPlus, tc.client.TLSConnectionState())
|
sc := scram.NewClient(h, username, "", noServerPlus, tc.client.TLSConnectionState())
|
||||||
clientFirst, err := sc.ClientFirst()
|
clientFirst, err := sc.ClientFirst()
|
||||||
tc.check(err, "scram clientFirst")
|
tc.check(err, "scram clientFirst")
|
||||||
tc.client.LastTag = "x001"
|
tc.client.WriteCommandf("", "authenticate %s %s", method, base64.StdEncoding.EncodeToString([]byte(clientFirst)))
|
||||||
tc.writelinef("%s authenticate %s %s", tc.client.LastTag, method, base64.StdEncoding.EncodeToString([]byte(clientFirst)))
|
|
||||||
|
|
||||||
xreadContinuation := func() []byte {
|
xreadContinuation := func() []byte {
|
||||||
line, _, result, _ := tc.client.ReadContinuation()
|
line, err := tc.client.ReadContinuation()
|
||||||
if result.Status != "" {
|
tcheck(t, err, "read continuation")
|
||||||
tc.t.Fatalf("expected continuation")
|
|
||||||
}
|
|
||||||
buf, err := base64.StdEncoding.DecodeString(line)
|
buf, err := base64.StdEncoding.DecodeString(line)
|
||||||
tc.check(err, "parsing base64 from remote")
|
tc.check(err, "parsing base64 from remote")
|
||||||
return buf
|
return buf
|
||||||
@ -161,10 +159,10 @@ func testAuthenticateSCRAM(t *testing.T, tls bool, method string, h func() hash.
|
|||||||
} else {
|
} else {
|
||||||
tc.writelinef("")
|
tc.writelinef("")
|
||||||
}
|
}
|
||||||
_, result, err := tc.client.Response()
|
resp, err := tc.client.ReadResponse()
|
||||||
tc.check(err, "read response")
|
tc.check(err, "read response")
|
||||||
if string(result.Status) != strings.ToUpper(status) {
|
if string(resp.Status) != strings.ToUpper(status) {
|
||||||
tc.t.Fatalf("got status %q, expected %q", result.Status, strings.ToUpper(status))
|
tc.t.Fatalf("got status %q, expected %q", resp.Status, strings.ToUpper(status))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,14 +193,11 @@ func TestAuthenticateCRAMMD5(t *testing.T) {
|
|||||||
auth := func(status string, username, password string) {
|
auth := func(status string, username, password string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
tc.client.LastTag = "x001"
|
tc.client.WriteCommandf("", "authenticate CRAM-MD5")
|
||||||
tc.writelinef("%s authenticate CRAM-MD5", tc.client.LastTag)
|
|
||||||
|
|
||||||
xreadContinuation := func() []byte {
|
xreadContinuation := func() []byte {
|
||||||
line, _, result, _ := tc.client.ReadContinuation()
|
line, err := tc.client.ReadContinuation()
|
||||||
if result.Status != "" {
|
tcheck(t, err, "read continuation")
|
||||||
tc.t.Fatalf("expected continuation")
|
|
||||||
}
|
|
||||||
buf, err := base64.StdEncoding.DecodeString(line)
|
buf, err := base64.StdEncoding.DecodeString(line)
|
||||||
tc.check(err, "parsing base64 from remote")
|
tc.check(err, "parsing base64 from remote")
|
||||||
return buf
|
return buf
|
||||||
@ -215,13 +210,13 @@ func TestAuthenticateCRAMMD5(t *testing.T) {
|
|||||||
}
|
}
|
||||||
h := hmac.New(md5.New, []byte(password))
|
h := hmac.New(md5.New, []byte(password))
|
||||||
h.Write([]byte(chal))
|
h.Write([]byte(chal))
|
||||||
resp := fmt.Sprintf("%s %x", username, h.Sum(nil))
|
data := fmt.Sprintf("%s %x", username, h.Sum(nil))
|
||||||
tc.writelinef("%s", base64.StdEncoding.EncodeToString([]byte(resp)))
|
tc.writelinef("%s", base64.StdEncoding.EncodeToString([]byte(data)))
|
||||||
|
|
||||||
_, result, err := tc.client.Response()
|
resp, err := tc.client.ReadResponse()
|
||||||
tc.check(err, "read response")
|
tc.check(err, "read response")
|
||||||
if string(result.Status) != strings.ToUpper(status) {
|
if string(resp.Status) != strings.ToUpper(status) {
|
||||||
tc.t.Fatalf("got status %q, expected %q", result.Status, strings.ToUpper(status))
|
tc.t.Fatalf("got status %q, expected %q", resp.Status, strings.ToUpper(status))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -301,7 +296,7 @@ func TestAuthenticateTLSClientCert(t *testing.T) {
|
|||||||
|
|
||||||
// Starttls and external auth.
|
// Starttls and external auth.
|
||||||
tc = startArgsMore(t, false, true, false, nil, &clientConfig, false, true, "mjl", addClientCert)
|
tc = startArgsMore(t, false, true, false, nil, &clientConfig, false, true, "mjl", addClientCert)
|
||||||
tc.client.Starttls(&clientConfig)
|
tc.client.StartTLS(&clientConfig)
|
||||||
tc.transactf("ok", "authenticate external =")
|
tc.transactf("ok", "authenticate external =")
|
||||||
tc.close()
|
tc.close()
|
||||||
|
|
||||||
@ -339,7 +334,7 @@ func TestAuthenticateTLSClientCert(t *testing.T) {
|
|||||||
t.Fatalf("tls connection was not resumed")
|
t.Fatalf("tls connection was not resumed")
|
||||||
}
|
}
|
||||||
// Check that operations that require an account work.
|
// Check that operations that require an account work.
|
||||||
tc.client.Enable("imap4rev2")
|
tc.client.Enable(imapclient.CapIMAP4rev2)
|
||||||
received, err := time.Parse(time.RFC3339, "2022-11-16T10:01:00+01:00")
|
received, err := time.Parse(time.RFC3339, "2022-11-16T10:01:00+01:00")
|
||||||
tc.check(err, "parse time")
|
tc.check(err, "parse time")
|
||||||
tc.client.Append("inbox", makeAppendTime(exampleMsg, received))
|
tc.client.Append("inbox", makeAppendTime(exampleMsg, received))
|
||||||
|
@ -21,7 +21,7 @@ func TestCompress(t *testing.T) {
|
|||||||
|
|
||||||
tc.client.CompressDeflate()
|
tc.client.CompressDeflate()
|
||||||
tc.transactf("no", "compress deflate") // Cannot have multiple.
|
tc.transactf("no", "compress deflate") // Cannot have multiple.
|
||||||
tc.xcode("COMPRESSIONACTIVE")
|
tc.xcodeWord("COMPRESSIONACTIVE")
|
||||||
|
|
||||||
tc.client.Select("inbox")
|
tc.client.Select("inbox")
|
||||||
tc.transactf("ok", "append inbox (\\seen) {%d+}\r\n%s", len(exampleMsg), exampleMsg)
|
tc.transactf("ok", "append inbox (\\seen) {%d+}\r\n%s", len(exampleMsg), exampleMsg)
|
||||||
@ -33,7 +33,7 @@ func TestCompressStartTLS(t *testing.T) {
|
|||||||
tc := start(t, false)
|
tc := start(t, false)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc.client.Starttls(&tls.Config{InsecureSkipVerify: true})
|
tc.client.StartTLS(&tls.Config{InsecureSkipVerify: true})
|
||||||
tc.login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc.client.CompressDeflate()
|
tc.client.CompressDeflate()
|
||||||
tc.client.Select("inbox")
|
tc.client.Select("inbox")
|
||||||
|
@ -38,15 +38,15 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) {
|
|||||||
|
|
||||||
// Check basic requirements of CONDSTORE.
|
// Check basic requirements of CONDSTORE.
|
||||||
|
|
||||||
capability := "Condstore"
|
capability := imapclient.CapCondstore
|
||||||
if qresync {
|
if qresync {
|
||||||
capability = "Qresync"
|
capability = imapclient.CapQresync
|
||||||
}
|
}
|
||||||
|
|
||||||
tc.login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc.client.Enable(capability)
|
tc.client.Enable(capability)
|
||||||
tc.transactf("ok", "Select inbox")
|
tc.transactf("ok", "Select inbox")
|
||||||
tc.xuntaggedOpt(false, imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(2), More: "x"}})
|
tc.xuntaggedOpt(false, imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeHighestModSeq(2), Text: "x"})
|
||||||
|
|
||||||
// First some tests without any messages.
|
// First some tests without any messages.
|
||||||
|
|
||||||
@ -133,19 +133,19 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) {
|
|||||||
// The ones we insert below will start with modseq 3. So we'll have messages with modseq 1 and 3-6.
|
// The ones we insert below will start with modseq 3. So we'll have messages with modseq 1 and 3-6.
|
||||||
tc.transactf("ok", "Append inbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
|
tc.transactf("ok", "Append inbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
|
||||||
tc.xuntagged(imapclient.UntaggedExists(4))
|
tc.xuntagged(imapclient.UntaggedExists(4))
|
||||||
tc.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("4")})
|
tc.xcode(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("4")})
|
||||||
|
|
||||||
tc.transactf("ok", "Append otherbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
|
tc.transactf("ok", "Append otherbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
|
||||||
tc.xuntagged()
|
tc.xuntagged()
|
||||||
tc.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 3, UIDs: xparseUIDRange("1")})
|
tc.xcode(imapclient.CodeAppendUID{UIDValidity: 3, UIDs: xparseUIDRange("1")})
|
||||||
|
|
||||||
tc.transactf("ok", "Append inbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
|
tc.transactf("ok", "Append inbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
|
||||||
tc.xuntagged(imapclient.UntaggedExists(5))
|
tc.xuntagged(imapclient.UntaggedExists(5))
|
||||||
tc.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("5")})
|
tc.xcode(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("5")})
|
||||||
|
|
||||||
tc.transactf("ok", "Append inbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
|
tc.transactf("ok", "Append inbox () \" 1-Jan-2022 10:10:00 +0100\" {1+}\r\nx")
|
||||||
tc.xuntagged(imapclient.UntaggedExists(6))
|
tc.xuntagged(imapclient.UntaggedExists(6))
|
||||||
tc.xcodeArg(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("6")})
|
tc.xcode(imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("6")})
|
||||||
|
|
||||||
tc2.transactf("ok", "Noop")
|
tc2.transactf("ok", "Noop")
|
||||||
noflags := imapclient.FetchFlags(nil)
|
noflags := imapclient.FetchFlags(nil)
|
||||||
@ -181,10 +181,10 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) {
|
|||||||
|
|
||||||
// Check highestmodseq when we select.
|
// Check highestmodseq when we select.
|
||||||
tc.transactf("ok", "Examine otherbox")
|
tc.transactf("ok", "Examine otherbox")
|
||||||
tc.xuntaggedOpt(false, imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(clientModseq + 2), More: "x"}})
|
tc.xuntaggedOpt(false, imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeHighestModSeq(clientModseq + 2), Text: "x"})
|
||||||
|
|
||||||
tc.transactf("ok", "Select inbox")
|
tc.transactf("ok", "Select inbox")
|
||||||
tc.xuntaggedOpt(false, imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(clientModseq + 4), More: "x"}})
|
tc.xuntaggedOpt(false, imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeHighestModSeq(clientModseq + 4), Text: "x"})
|
||||||
|
|
||||||
clientModseq += 4
|
clientModseq += 4
|
||||||
|
|
||||||
@ -225,13 +225,13 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) {
|
|||||||
if !uidonly {
|
if !uidonly {
|
||||||
// unchangedsince 0 never passes the check. ../rfc/7162:640
|
// unchangedsince 0 never passes the check. ../rfc/7162:640
|
||||||
tc.transactf("ok", `Store 1 (Unchangedsince 0) +Flags ()`)
|
tc.transactf("ok", `Store 1 (Unchangedsince 0) +Flags ()`)
|
||||||
tc.xcodeArg(imapclient.CodeModified(xparseNumSet("1")))
|
tc.xcode(imapclient.CodeModified(xparseNumSet("1")))
|
||||||
tc.xuntagged(tc.untaggedFetch(1, 1, noflags, imapclient.FetchModSeq(1)))
|
tc.xuntagged(tc.untaggedFetch(1, 1, noflags, imapclient.FetchModSeq(1)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modseq is 2 for first condstore-aware-appended message, so also no match.
|
// Modseq is 2 for first condstore-aware-appended message, so also no match.
|
||||||
tc.transactf("ok", `Uid Store 4 (Unchangedsince 1) +Flags ()`)
|
tc.transactf("ok", `Uid Store 4 (Unchangedsince 1) +Flags ()`)
|
||||||
tc.xcodeArg(imapclient.CodeModified(xparseNumSet("4")))
|
tc.xcode(imapclient.CodeModified(xparseNumSet("4")))
|
||||||
|
|
||||||
if uidonly {
|
if uidonly {
|
||||||
tc.transactf("ok", `Uid Store 1 (Unchangedsince 1) +Flags (label1)`)
|
tc.transactf("ok", `Uid Store 1 (Unchangedsince 1) +Flags (label1)`)
|
||||||
@ -239,7 +239,7 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) {
|
|||||||
// Modseq is 1 for original message.
|
// Modseq is 1 for original message.
|
||||||
tc.transactf("ok", `Store 1 (Unchangedsince 1) +Flags (label1)`)
|
tc.transactf("ok", `Store 1 (Unchangedsince 1) +Flags (label1)`)
|
||||||
}
|
}
|
||||||
tc.xcode("") // No MODIFIED.
|
tc.xcode(nil) // No MODIFIED.
|
||||||
clientModseq++
|
clientModseq++
|
||||||
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{"label1"}, imapclient.FetchModSeq(clientModseq)))
|
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags{"label1"}, imapclient.FetchModSeq(clientModseq)))
|
||||||
tc2.transactf("ok", "Noop")
|
tc2.transactf("ok", "Noop")
|
||||||
@ -255,7 +255,7 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) {
|
|||||||
// modseq change made in the first application. ../rfc/7162:823
|
// modseq change made in the first application. ../rfc/7162:823
|
||||||
tc.transactf("ok", `Uid Store 1,1 (Unchangedsince %d) -Flags (label1)`, clientModseq)
|
tc.transactf("ok", `Uid Store 1,1 (Unchangedsince %d) -Flags (label1)`, clientModseq)
|
||||||
clientModseq++
|
clientModseq++
|
||||||
tc.xcode("") // No MODIFIED.
|
tc.xcode(nil) // No MODIFIED.
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
tc.untaggedFetch(1, 1, imapclient.FetchFlags(nil), imapclient.FetchModSeq(clientModseq)),
|
tc.untaggedFetch(1, 1, imapclient.FetchFlags(nil), imapclient.FetchModSeq(clientModseq)),
|
||||||
)
|
)
|
||||||
@ -273,7 +273,7 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) {
|
|||||||
// Modify without actually changing flags, there will be no new modseq and no broadcast.
|
// Modify without actually changing flags, there will be no new modseq and no broadcast.
|
||||||
tc.transactf("ok", `Store 1 (Unchangedsince %d) -Flags (label1)`, clientModseq)
|
tc.transactf("ok", `Store 1 (Unchangedsince %d) -Flags (label1)`, clientModseq)
|
||||||
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags(nil), imapclient.FetchModSeq(clientModseq)))
|
tc.xuntagged(tc.untaggedFetch(1, 1, imapclient.FetchFlags(nil), imapclient.FetchModSeq(clientModseq)))
|
||||||
tc.xcode("") // No MODIFIED.
|
tc.xcode(nil) // No MODIFIED.
|
||||||
tc2.transactf("ok", "Noop")
|
tc2.transactf("ok", "Noop")
|
||||||
tc2.xuntagged()
|
tc2.xuntagged()
|
||||||
tc3.transactf("ok", "Noop")
|
tc3.transactf("ok", "Noop")
|
||||||
@ -318,7 +318,7 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) {
|
|||||||
} else {
|
} else {
|
||||||
tc.xuntagged(imapclient.UntaggedExpunge(3), imapclient.UntaggedExpunge(3))
|
tc.xuntagged(imapclient.UntaggedExpunge(3), imapclient.UntaggedExpunge(3))
|
||||||
}
|
}
|
||||||
tc.xcodeArg(imapclient.CodeHighestModSeq(clientModseq))
|
tc.xcode(imapclient.CodeHighestModSeq(clientModseq))
|
||||||
tc2.transactf("ok", "Noop")
|
tc2.transactf("ok", "Noop")
|
||||||
if uidonly {
|
if uidonly {
|
||||||
tc2.xuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("3:4")})
|
tc2.xuntagged(imapclient.UntaggedVanished{UIDs: xparseNumSet("3:4")})
|
||||||
@ -340,7 +340,7 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) {
|
|||||||
tc.transactf("ok", "Select inbox")
|
tc.transactf("ok", "Select inbox")
|
||||||
tc.xuntaggedOpt(false,
|
tc.xuntaggedOpt(false,
|
||||||
imapclient.UntaggedExists(4),
|
imapclient.UntaggedExists(4),
|
||||||
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(clientModseq), More: "x"}},
|
imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeHighestModSeq(clientModseq), Text: "x"},
|
||||||
)
|
)
|
||||||
|
|
||||||
if !uidonly {
|
if !uidonly {
|
||||||
@ -367,16 +367,16 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) {
|
|||||||
tc.transactf("ok", "Search Return (Min Max All) 1:* Modseq 8")
|
tc.transactf("ok", "Search Return (Min Max All) 1:* Modseq 8")
|
||||||
tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 1, All: esearchall0("1"), ModSeq: 8})
|
tc.xesearch(imapclient.UntaggedEsearch{Min: 1, Max: 1, All: esearchall0("1"), ModSeq: 8})
|
||||||
tc.transactf("ok", "Search Return (Min Max All) 1:* Modseq 9")
|
tc.transactf("ok", "Search Return (Min Max All) 1:* Modseq 9")
|
||||||
tc.xuntagged(imapclient.UntaggedEsearch{Tag: tc.client.LastTag})
|
tc.xuntagged(imapclient.UntaggedEsearch{Tag: tc.client.LastTag()})
|
||||||
}
|
}
|
||||||
|
|
||||||
// store, cannot modify expunged messages.
|
// store, cannot modify expunged messages.
|
||||||
tc.transactf("ok", `Uid Store 3,4 (Unchangedsince %d) +Flags (label2)`, clientModseq)
|
tc.transactf("ok", `Uid Store 3,4 (Unchangedsince %d) +Flags (label2)`, clientModseq)
|
||||||
tc.xuntagged()
|
tc.xuntagged()
|
||||||
tc.xcode("") // Not MODIFIED.
|
tc.xcode(nil) // Not MODIFIED.
|
||||||
tc.transactf("ok", `Uid Store 3,4 +Flags (label2)`)
|
tc.transactf("ok", `Uid Store 3,4 +Flags (label2)`)
|
||||||
tc.xuntagged()
|
tc.xuntagged()
|
||||||
tc.xcode("") // Not MODIFIED.
|
tc.xcode(nil) // Not MODIFIED.
|
||||||
|
|
||||||
// Check all condstore-enabling commands (and their syntax), ../rfc/7162:368
|
// Check all condstore-enabling commands (and their syntax), ../rfc/7162:368
|
||||||
|
|
||||||
@ -497,13 +497,13 @@ func testCondstoreQresync(t *testing.T, qresync, uidonly bool) {
|
|||||||
clientModseq++
|
clientModseq++
|
||||||
if qresync {
|
if qresync {
|
||||||
tc.xuntaggedOpt(false, imapclient.UntaggedVanished{UIDs: xparseNumSet("2")})
|
tc.xuntaggedOpt(false, imapclient.UntaggedVanished{UIDs: xparseNumSet("2")})
|
||||||
tc.xcodeArg(imapclient.CodeHighestModSeq(clientModseq))
|
tc.xcode(imapclient.CodeHighestModSeq(clientModseq))
|
||||||
} else if uidonly {
|
} else if uidonly {
|
||||||
tc.xuntaggedOpt(false, imapclient.UntaggedVanished{UIDs: xparseNumSet("2")})
|
tc.xuntaggedOpt(false, imapclient.UntaggedVanished{UIDs: xparseNumSet("2")})
|
||||||
tc.xcode("")
|
tc.xcode(nil)
|
||||||
} else {
|
} else {
|
||||||
tc.xuntaggedOpt(false, imapclient.UntaggedExpunge(2))
|
tc.xuntaggedOpt(false, imapclient.UntaggedExpunge(2))
|
||||||
tc.xcode("")
|
tc.xcode(nil)
|
||||||
}
|
}
|
||||||
tc2.transactf("ok", "Noop")
|
tc2.transactf("ok", "Noop")
|
||||||
if uidonly {
|
if uidonly {
|
||||||
@ -615,21 +615,21 @@ func testQresync(t *testing.T, tc *testconn, uidonly bool, clientModseq int64) {
|
|||||||
flags := strings.Split(`\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent l1 l2 l3 l4 l5 l6 l7 l8 label1`, " ")
|
flags := strings.Split(`\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent l1 l2 l3 l4 l5 l6 l7 l8 label1`, " ")
|
||||||
permflags := strings.Split(`\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent \*`, " ")
|
permflags := strings.Split(`\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent \*`, " ")
|
||||||
uflags := imapclient.UntaggedFlags(flags)
|
uflags := imapclient.UntaggedFlags(flags)
|
||||||
upermflags := imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "PERMANENTFLAGS", CodeArg: imapclient.CodeList{Code: "PERMANENTFLAGS", Args: permflags}, More: "x"}}
|
upermflags := imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodePermanentFlags(permflags), Text: "x"}
|
||||||
|
|
||||||
baseUntagged := []imapclient.Untagged{
|
baseUntagged := []imapclient.Untagged{
|
||||||
uflags,
|
uflags,
|
||||||
upermflags,
|
upermflags,
|
||||||
imapclient.UntaggedList{Separator: '/', Mailbox: "Inbox"},
|
imapclient.UntaggedList{Separator: '/', Mailbox: "Inbox"},
|
||||||
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UIDNEXT", CodeArg: imapclient.CodeUint{Code: "UIDNEXT", Num: 7}, More: "x"}},
|
imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeUIDNext(7), Text: "x"},
|
||||||
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UIDVALIDITY", CodeArg: imapclient.CodeUint{Code: "UIDVALIDITY", Num: 1}, More: "x"}},
|
imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeUIDValidity(1), Text: "x"},
|
||||||
imapclient.UntaggedRecent(0),
|
imapclient.UntaggedRecent(0),
|
||||||
imapclient.UntaggedExists(4),
|
imapclient.UntaggedExists(4),
|
||||||
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(clientModseq), More: "x"}},
|
imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeHighestModSeq(clientModseq), Text: "x"},
|
||||||
}
|
}
|
||||||
if !uidonly {
|
if !uidonly {
|
||||||
baseUntagged = append(baseUntagged,
|
baseUntagged = append(baseUntagged,
|
||||||
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UNSEEN", CodeArg: imapclient.CodeUint{Code: "UNSEEN", Num: 1}, More: "x"}},
|
imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeUnseen(1), Text: "x"},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -752,7 +752,7 @@ func testQresync(t *testing.T, tc *testconn, uidonly bool, clientModseq int64) {
|
|||||||
tc.transactf("ok", "Select inbox (Qresync (1 10 (1,3,6 1,3,6)))")
|
tc.transactf("ok", "Select inbox (Qresync (1 10 (1,3,6 1,3,6)))")
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
makeUntagged(
|
makeUntagged(
|
||||||
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "ALERT", More: "Synchronization inconsistency in client detected. Client tried to sync with a UID that was removed at or after the MODSEQ it sent in the request. Sending all historic message removals for selected mailbox. Full synchronization recommended."}},
|
imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeWord("ALERT"), Text: "Synchronization inconsistency in client detected. Client tried to sync with a UID that was removed at or after the MODSEQ it sent in the request. Sending all historic message removals for selected mailbox. Full synchronization recommended."},
|
||||||
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
|
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
|
||||||
tc.untaggedFetch(4, 6, noflags, imapclient.FetchModSeq(clientModseq)),
|
tc.untaggedFetch(4, 6, noflags, imapclient.FetchModSeq(clientModseq)),
|
||||||
)...,
|
)...,
|
||||||
@ -765,7 +765,7 @@ func testQresync(t *testing.T, tc *testconn, uidonly bool, clientModseq int64) {
|
|||||||
tc.transactf("ok", "Select inbox (Qresync (1 18 (1,3,6 1,3,6)))")
|
tc.transactf("ok", "Select inbox (Qresync (1 18 (1,3,6 1,3,6)))")
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
makeUntagged(
|
makeUntagged(
|
||||||
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "ALERT", More: "Synchronization inconsistency in client detected. Client tried to sync with a UID that was removed at or after the MODSEQ it sent in the request. Sending all historic message removals for selected mailbox. Full synchronization recommended."}},
|
imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeWord("ALERT"), Text: "Synchronization inconsistency in client detected. Client tried to sync with a UID that was removed at or after the MODSEQ it sent in the request. Sending all historic message removals for selected mailbox. Full synchronization recommended."},
|
||||||
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
|
imapclient.UntaggedVanished{Earlier: true, UIDs: xparseNumSet("3:4")},
|
||||||
tc.untaggedFetch(4, 6, noflags, imapclient.FetchModSeq(clientModseq)),
|
tc.untaggedFetch(4, 6, noflags, imapclient.FetchModSeq(clientModseq)),
|
||||||
)...,
|
)...,
|
||||||
@ -786,7 +786,7 @@ func testQresyncHistory(t *testing.T, uidonly bool) {
|
|||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc.login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc.client.Enable("Qresync")
|
tc.client.Enable(imapclient.CapQresync)
|
||||||
tc.transactf("ok", "Append inbox {1+}\r\nx")
|
tc.transactf("ok", "Append inbox {1+}\r\nx")
|
||||||
tc.transactf("ok", "Append inbox {1+}\r\nx") // modseq 6
|
tc.transactf("ok", "Append inbox {1+}\r\nx") // modseq 6
|
||||||
tc.transactf("ok", "Append inbox {1+}\r\nx")
|
tc.transactf("ok", "Append inbox {1+}\r\nx")
|
||||||
@ -799,16 +799,16 @@ func testQresyncHistory(t *testing.T, uidonly bool) {
|
|||||||
flags := strings.Split(`\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent`, " ")
|
flags := strings.Split(`\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent`, " ")
|
||||||
permflags := strings.Split(`\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent \*`, " ")
|
permflags := strings.Split(`\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent \*`, " ")
|
||||||
uflags := imapclient.UntaggedFlags(flags)
|
uflags := imapclient.UntaggedFlags(flags)
|
||||||
upermflags := imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "PERMANENTFLAGS", CodeArg: imapclient.CodeList{Code: "PERMANENTFLAGS", Args: permflags}, More: "x"}}
|
upermflags := imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodePermanentFlags(permflags), Text: "x"}
|
||||||
baseUntagged := []imapclient.Untagged{
|
baseUntagged := []imapclient.Untagged{
|
||||||
uflags,
|
uflags,
|
||||||
upermflags,
|
upermflags,
|
||||||
imapclient.UntaggedList{Separator: '/', Mailbox: "Inbox"},
|
imapclient.UntaggedList{Separator: '/', Mailbox: "Inbox"},
|
||||||
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UIDNEXT", CodeArg: imapclient.CodeUint{Code: "UIDNEXT", Num: 4}, More: "x"}},
|
imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeUIDNext(4), Text: "x"},
|
||||||
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UIDVALIDITY", CodeArg: imapclient.CodeUint{Code: "UIDVALIDITY", Num: 1}, More: "x"}},
|
imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeUIDValidity(1), Text: "x"},
|
||||||
imapclient.UntaggedRecent(0),
|
imapclient.UntaggedRecent(0),
|
||||||
imapclient.UntaggedExists(1),
|
imapclient.UntaggedExists(1),
|
||||||
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(10), More: "x"}},
|
imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeHighestModSeq(10), Text: "x"},
|
||||||
}
|
}
|
||||||
|
|
||||||
makeUntagged := func(l ...imapclient.Untagged) []imapclient.Untagged {
|
makeUntagged := func(l ...imapclient.Untagged) []imapclient.Untagged {
|
||||||
|
@ -44,16 +44,16 @@ func testCopy(t *testing.T, uidonly bool) {
|
|||||||
tc.transactf("ok", "uid copy 3:* Trash")
|
tc.transactf("ok", "uid copy 3:* Trash")
|
||||||
} else {
|
} else {
|
||||||
tc.transactf("no", "copy 1 nonexistent")
|
tc.transactf("no", "copy 1 nonexistent")
|
||||||
tc.xcode("TRYCREATE")
|
tc.xcodeWord("TRYCREATE")
|
||||||
tc.transactf("no", "copy 1 expungebox")
|
tc.transactf("no", "copy 1 expungebox")
|
||||||
tc.xcode("TRYCREATE")
|
tc.xcodeWord("TRYCREATE")
|
||||||
|
|
||||||
tc.transactf("no", "copy 1 inbox") // Cannot copy to same mailbox.
|
tc.transactf("no", "copy 1 inbox") // Cannot copy to same mailbox.
|
||||||
|
|
||||||
tc2.transactf("ok", "noop") // Drain.
|
tc2.transactf("ok", "noop") // Drain.
|
||||||
|
|
||||||
tc.transactf("ok", "copy 1:* Trash")
|
tc.transactf("ok", "copy 1:* Trash")
|
||||||
tc.xcodeArg(imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}, To: []imapclient.NumRange{{First: 1, Last: uint32ptr(2)}}})
|
tc.xcode(mustParseCode("COPYUID 1 3:4 1:2"))
|
||||||
}
|
}
|
||||||
tc2.transactf("ok", "noop")
|
tc2.transactf("ok", "noop")
|
||||||
tc2.xuntagged(
|
tc2.xuntagged(
|
||||||
@ -64,7 +64,7 @@ func testCopy(t *testing.T, uidonly bool) {
|
|||||||
|
|
||||||
tc.transactf("no", "uid copy 1,2 Trash") // No match.
|
tc.transactf("no", "uid copy 1,2 Trash") // No match.
|
||||||
tc.transactf("ok", "uid copy 4,3 Trash")
|
tc.transactf("ok", "uid copy 4,3 Trash")
|
||||||
tc.xcodeArg(imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}, To: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}})
|
tc.xcode(mustParseCode("COPYUID 1 3:4 3:4"))
|
||||||
tc2.transactf("ok", "noop")
|
tc2.transactf("ok", "noop")
|
||||||
tc2.xuntagged(
|
tc2.xuntagged(
|
||||||
imapclient.UntaggedExists(4),
|
imapclient.UntaggedExists(4),
|
||||||
@ -81,5 +81,5 @@ func testCopy(t *testing.T, uidonly bool) {
|
|||||||
tclimit.xuntagged(imapclient.UntaggedExists(1))
|
tclimit.xuntagged(imapclient.UntaggedExists(1))
|
||||||
// Second message would take account past limit.
|
// Second message would take account past limit.
|
||||||
tclimit.transactf("no", "uid copy 1:* Trash")
|
tclimit.transactf("no", "uid copy 1:* Trash")
|
||||||
tclimit.xcode("OVERQUOTA")
|
tclimit.xcodeWord("OVERQUOTA")
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,7 @@ func testCreate(t *testing.T, uidonly bool) {
|
|||||||
tc.xuntagged(imapclient.UntaggedList{Flags: []string{`\Subscribed`}, Separator: '/', Mailbox: "mailbox"})
|
tc.xuntagged(imapclient.UntaggedList{Flags: []string{`\Subscribed`}, Separator: '/', Mailbox: "mailbox"})
|
||||||
|
|
||||||
// OldName is only set for IMAP4rev2 or NOTIFY.
|
// OldName is only set for IMAP4rev2 or NOTIFY.
|
||||||
tc.client.Enable("imap4rev2")
|
tc.client.Enable(imapclient.CapIMAP4rev2)
|
||||||
tc.transactf("ok", "create mailbox2/")
|
tc.transactf("ok", "create mailbox2/")
|
||||||
tc.xuntagged(imapclient.UntaggedList{Flags: []string{`\Subscribed`}, Separator: '/', Mailbox: "mailbox2", OldName: "mailbox2/"})
|
tc.xuntagged(imapclient.UntaggedList{Flags: []string{`\Subscribed`}, Separator: '/', Mailbox: "mailbox2", OldName: "mailbox2/"})
|
||||||
|
|
||||||
|
@ -43,9 +43,9 @@ func testDelete(t *testing.T, uidonly bool) {
|
|||||||
|
|
||||||
// ../rfc/9051:2000
|
// ../rfc/9051:2000
|
||||||
tc.transactf("no", "delete a") // Still has child.
|
tc.transactf("no", "delete a") // Still has child.
|
||||||
tc.xcode("HASCHILDREN")
|
tc.xcodeWord("HASCHILDREN")
|
||||||
|
|
||||||
tc3.client.Enable("IMAP4rev2") // For \NonExistent support.
|
tc3.client.Enable(imapclient.CapIMAP4rev2) // For \NonExistent support.
|
||||||
tc.transactf("ok", "delete a/b")
|
tc.transactf("ok", "delete a/b")
|
||||||
tc2.transactf("ok", "noop")
|
tc2.transactf("ok", "noop")
|
||||||
tc2.xuntagged() // No IMAP4rev2, no \NonExistent.
|
tc2.xuntagged() // No IMAP4rev2, no \NonExistent.
|
||||||
|
@ -24,7 +24,7 @@ func testFetch(t *testing.T, uidonly bool) {
|
|||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc.login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc.client.Enable("imap4rev2")
|
tc.client.Enable(imapclient.CapIMAP4rev2)
|
||||||
received, err := time.Parse(time.RFC3339, "2022-11-16T10:01:00+01:00")
|
received, err := time.Parse(time.RFC3339, "2022-11-16T10:01:00+01:00")
|
||||||
tc.check(err, "parse time")
|
tc.check(err, "parse time")
|
||||||
tc.client.Append("inbox", makeAppendTime(exampleMsg, received))
|
tc.client.Append("inbox", makeAppendTime(exampleMsg, received))
|
||||||
@ -58,7 +58,13 @@ func testFetch(t *testing.T, uidonly bool) {
|
|||||||
}
|
}
|
||||||
bodystructure1 := bodyxstructure1
|
bodystructure1 := bodyxstructure1
|
||||||
bodystructure1.RespAttr = "BODYSTRUCTURE"
|
bodystructure1.RespAttr = "BODYSTRUCTURE"
|
||||||
bodystructbody1.Ext = &imapclient.BodyExtension1Part{}
|
bodyext1 := imapclient.BodyExtension1Part{
|
||||||
|
Disposition: ptr((*string)(nil)),
|
||||||
|
DispositionParams: ptr([][2]string(nil)),
|
||||||
|
Language: ptr([]string(nil)),
|
||||||
|
Location: ptr((*string)(nil)),
|
||||||
|
}
|
||||||
|
bodystructbody1.Ext = &bodyext1
|
||||||
bodystructure1.Body = bodystructbody1
|
bodystructure1.Body = bodystructbody1
|
||||||
|
|
||||||
split := strings.SplitN(exampleMsg, "\r\n\r\n", 2)
|
split := strings.SplitN(exampleMsg, "\r\n\r\n", 2)
|
||||||
@ -115,26 +121,26 @@ func testFetch(t *testing.T, uidonly bool) {
|
|||||||
tc.transactf("ok", "fetch 1 binary[1]")
|
tc.transactf("ok", "fetch 1 binary[1]")
|
||||||
tc.xuntagged(tc.untaggedFetch(1, 1, binarypart1)) // Seen flag not changed.
|
tc.xuntagged(tc.untaggedFetch(1, 1, binarypart1)) // Seen flag not changed.
|
||||||
|
|
||||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
tc.client.MSNStoreFlagsClear("1", true, `\Seen`)
|
||||||
tc.transactf("ok", "uid fetch 1 binary[]<1.1>")
|
tc.transactf("ok", "uid fetch 1 binary[]<1.1>")
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
tc.untaggedFetch(1, 1, binarypartial1, noflags),
|
tc.untaggedFetch(1, 1, binarypartial1, noflags),
|
||||||
tc.untaggedFetch(1, 1, flagsSeen), // For UID FETCH, we get the flags during the command.
|
tc.untaggedFetch(1, 1, flagsSeen), // For UID FETCH, we get the flags during the command.
|
||||||
)
|
)
|
||||||
|
|
||||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
tc.client.MSNStoreFlagsClear("1", true, `\Seen`)
|
||||||
tc.transactf("ok", "fetch 1 binary[1]<1.1>")
|
tc.transactf("ok", "fetch 1 binary[1]<1.1>")
|
||||||
tc.xuntagged(tc.untaggedFetch(1, 1, binarypartpartial1, noflags))
|
tc.xuntagged(tc.untaggedFetch(1, 1, binarypartpartial1, noflags))
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||||
|
|
||||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
tc.client.MSNStoreFlagsClear("1", true, `\Seen`)
|
||||||
tc.transactf("ok", "fetch 1 binary[]<10000.10001>")
|
tc.transactf("ok", "fetch 1 binary[]<10000.10001>")
|
||||||
tc.xuntagged(tc.untaggedFetch(1, 1, binaryend1, noflags))
|
tc.xuntagged(tc.untaggedFetch(1, 1, binaryend1, noflags))
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||||
|
|
||||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
tc.client.MSNStoreFlagsClear("1", true, `\Seen`)
|
||||||
tc.transactf("ok", "fetch 1 binary[1]<10000.10001>")
|
tc.transactf("ok", "fetch 1 binary[1]<10000.10001>")
|
||||||
tc.xuntagged(tc.untaggedFetch(1, 1, binarypartend1, noflags))
|
tc.xuntagged(tc.untaggedFetch(1, 1, binarypartend1, noflags))
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
@ -146,7 +152,7 @@ func testFetch(t *testing.T, uidonly bool) {
|
|||||||
tc.transactf("ok", "fetch 1 binary.size[1]")
|
tc.transactf("ok", "fetch 1 binary.size[1]")
|
||||||
tc.xuntagged(tc.untaggedFetch(1, 1, binarysizepart1))
|
tc.xuntagged(tc.untaggedFetch(1, 1, binarysizepart1))
|
||||||
|
|
||||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
tc.client.MSNStoreFlagsClear("1", true, `\Seen`)
|
||||||
tc.transactf("ok", "fetch 1 body[]")
|
tc.transactf("ok", "fetch 1 body[]")
|
||||||
tc.xuntagged(tc.untaggedFetch(1, 1, body1, noflags))
|
tc.xuntagged(tc.untaggedFetch(1, 1, body1, noflags))
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
@ -156,31 +162,31 @@ func testFetch(t *testing.T, uidonly bool) {
|
|||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.xuntagged() // Already seen.
|
tc.xuntagged() // Already seen.
|
||||||
|
|
||||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
tc.client.MSNStoreFlagsClear("1", true, `\Seen`)
|
||||||
tc.transactf("ok", "fetch 1 body[1]")
|
tc.transactf("ok", "fetch 1 body[1]")
|
||||||
tc.xuntagged(tc.untaggedFetch(1, 1, bodypart1, noflags))
|
tc.xuntagged(tc.untaggedFetch(1, 1, bodypart1, noflags))
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||||
|
|
||||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
tc.client.MSNStoreFlagsClear("1", true, `\Seen`)
|
||||||
tc.transactf("ok", "fetch 1 body[1]<1.2>")
|
tc.transactf("ok", "fetch 1 body[1]<1.2>")
|
||||||
tc.xuntagged(tc.untaggedFetch(1, 1, body1off1, noflags))
|
tc.xuntagged(tc.untaggedFetch(1, 1, body1off1, noflags))
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||||
|
|
||||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
tc.client.MSNStoreFlagsClear("1", true, `\Seen`)
|
||||||
tc.transactf("ok", "fetch 1 body[1]<100000.100000>")
|
tc.transactf("ok", "fetch 1 body[1]<100000.100000>")
|
||||||
tc.xuntagged(tc.untaggedFetch(1, 1, bodyend1, noflags))
|
tc.xuntagged(tc.untaggedFetch(1, 1, bodyend1, noflags))
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||||
|
|
||||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
tc.client.MSNStoreFlagsClear("1", true, `\Seen`)
|
||||||
tc.transactf("ok", "fetch 1 body[header]")
|
tc.transactf("ok", "fetch 1 body[header]")
|
||||||
tc.xuntagged(tc.untaggedFetch(1, 1, bodyheader1, noflags))
|
tc.xuntagged(tc.untaggedFetch(1, 1, bodyheader1, noflags))
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||||
|
|
||||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
tc.client.MSNStoreFlagsClear("1", true, `\Seen`)
|
||||||
tc.transactf("ok", "fetch 1 body[text]")
|
tc.transactf("ok", "fetch 1 body[text]")
|
||||||
tc.xuntagged(tc.untaggedFetch(1, 1, bodytext1, noflags))
|
tc.xuntagged(tc.untaggedFetch(1, 1, bodytext1, noflags))
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
@ -191,21 +197,21 @@ func testFetch(t *testing.T, uidonly bool) {
|
|||||||
tc.xuntagged(tc.untaggedFetch(1, 1, rfcheader1))
|
tc.xuntagged(tc.untaggedFetch(1, 1, rfcheader1))
|
||||||
|
|
||||||
// equivalent to body[text], ../rfc/3501:3199
|
// equivalent to body[text], ../rfc/3501:3199
|
||||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
tc.client.MSNStoreFlagsClear("1", true, `\Seen`)
|
||||||
tc.transactf("ok", "fetch 1 rfc822.text")
|
tc.transactf("ok", "fetch 1 rfc822.text")
|
||||||
tc.xuntagged(tc.untaggedFetch(1, 1, rfctext1, noflags))
|
tc.xuntagged(tc.untaggedFetch(1, 1, rfctext1, noflags))
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||||
|
|
||||||
// equivalent to body[], ../rfc/3501:3179
|
// equivalent to body[], ../rfc/3501:3179
|
||||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
tc.client.MSNStoreFlagsClear("1", true, `\Seen`)
|
||||||
tc.transactf("ok", "fetch 1 rfc822")
|
tc.transactf("ok", "fetch 1 rfc822")
|
||||||
tc.xuntagged(tc.untaggedFetch(1, 1, rfc1, noflags))
|
tc.xuntagged(tc.untaggedFetch(1, 1, rfc1, noflags))
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
tc.xuntagged(tc.untaggedFetch(1, 1, flagsSeen))
|
||||||
|
|
||||||
// With PEEK, we should not get the \Seen flag.
|
// With PEEK, we should not get the \Seen flag.
|
||||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
tc.client.MSNStoreFlagsClear("1", true, `\Seen`)
|
||||||
tc.transactf("ok", "fetch 1 body.peek[]")
|
tc.transactf("ok", "fetch 1 body.peek[]")
|
||||||
tc.xuntagged(tc.untaggedFetch(1, 1, body1))
|
tc.xuntagged(tc.untaggedFetch(1, 1, body1))
|
||||||
|
|
||||||
@ -229,7 +235,7 @@ func testFetch(t *testing.T, uidonly bool) {
|
|||||||
// Missing sequence number. ../rfc/9051:7018
|
// Missing sequence number. ../rfc/9051:7018
|
||||||
tc.transactf("bad", "fetch 2 body[]")
|
tc.transactf("bad", "fetch 2 body[]")
|
||||||
|
|
||||||
tc.client.StoreFlagsClear("1", true, `\Seen`)
|
tc.client.MSNStoreFlagsClear("1", true, `\Seen`)
|
||||||
tc.transactf("ok", "fetch 1:1 body[]")
|
tc.transactf("ok", "fetch 1:1 body[]")
|
||||||
tc.xuntagged(tc.untaggedFetch(1, 1, body1, noflags))
|
tc.xuntagged(tc.untaggedFetch(1, 1, body1, noflags))
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
@ -300,17 +306,28 @@ func testFetch(t *testing.T, uidonly bool) {
|
|||||||
RespAttr: "BODYSTRUCTURE",
|
RespAttr: "BODYSTRUCTURE",
|
||||||
Body: imapclient.BodyTypeMpart{
|
Body: imapclient.BodyTypeMpart{
|
||||||
Bodies: []any{
|
Bodies: []any{
|
||||||
imapclient.BodyTypeBasic{BodyFields: imapclient.BodyFields{Octets: 275}, Ext: &imapclient.BodyExtension1Part{}},
|
imapclient.BodyTypeBasic{BodyFields: imapclient.BodyFields{Octets: 275}, Ext: &bodyext1},
|
||||||
imapclient.BodyTypeText{MediaType: "TEXT", MediaSubtype: "PLAIN", BodyFields: imapclient.BodyFields{Params: [][2]string{{"CHARSET", "US-ASCII"}}, Octets: 114}, Lines: 3, Ext: &imapclient.BodyExtension1Part{}},
|
imapclient.BodyTypeText{MediaType: "TEXT", MediaSubtype: "PLAIN", BodyFields: imapclient.BodyFields{Params: [][2]string{{"CHARSET", "US-ASCII"}}, Octets: 114}, Lines: 3, Ext: &bodyext1},
|
||||||
imapclient.BodyTypeMpart{
|
imapclient.BodyTypeMpart{
|
||||||
Bodies: []any{
|
Bodies: []any{
|
||||||
imapclient.BodyTypeBasic{MediaType: "AUDIO", MediaSubtype: "BASIC", BodyFields: imapclient.BodyFields{CTE: "BASE64", Octets: 22}, Ext: &imapclient.BodyExtension1Part{}},
|
imapclient.BodyTypeBasic{MediaType: "AUDIO", MediaSubtype: "BASIC", BodyFields: imapclient.BodyFields{CTE: "BASE64", Octets: 22}, Ext: &bodyext1},
|
||||||
imapclient.BodyTypeBasic{MediaType: "IMAGE", MediaSubtype: "JPEG", BodyFields: imapclient.BodyFields{CTE: "BASE64"}, Ext: &imapclient.BodyExtension1Part{Disposition: "inline", DispositionParams: [][2]string{{"filename", "image.jpg"}}}},
|
imapclient.BodyTypeBasic{MediaType: "IMAGE", MediaSubtype: "JPEG", BodyFields: imapclient.BodyFields{CTE: "BASE64"}, Ext: &imapclient.BodyExtension1Part{
|
||||||
|
Disposition: ptr(ptr("inline")),
|
||||||
|
DispositionParams: ptr([][2]string{{"filename", "image.jpg"}}),
|
||||||
|
Language: ptr([]string(nil)),
|
||||||
|
Location: ptr((*string)(nil)),
|
||||||
|
}},
|
||||||
},
|
},
|
||||||
MediaSubtype: "PARALLEL",
|
MediaSubtype: "PARALLEL",
|
||||||
Ext: &imapclient.BodyExtensionMpart{Params: [][2]string{{"BOUNDARY", "unique-boundary-2"}}},
|
Ext: &imapclient.BodyExtensionMpart{
|
||||||
|
Params: [][2]string{{"BOUNDARY", "unique-boundary-2"}},
|
||||||
|
Disposition: ptr((*string)(nil)), // Present but nil.
|
||||||
|
DispositionParams: ptr([][2]string(nil)),
|
||||||
|
Language: ptr([]string(nil)),
|
||||||
|
Location: ptr((*string)(nil)),
|
||||||
},
|
},
|
||||||
imapclient.BodyTypeText{MediaType: "TEXT", MediaSubtype: "ENRICHED", BodyFields: imapclient.BodyFields{Octets: 145}, Lines: 5, Ext: &imapclient.BodyExtension1Part{}},
|
},
|
||||||
|
imapclient.BodyTypeText{MediaType: "TEXT", MediaSubtype: "ENRICHED", BodyFields: imapclient.BodyFields{Octets: 145}, Lines: 5, Ext: &bodyext1},
|
||||||
imapclient.BodyTypeMsg{
|
imapclient.BodyTypeMsg{
|
||||||
MediaType: "MESSAGE",
|
MediaType: "MESSAGE",
|
||||||
MediaSubtype: "RFC822",
|
MediaSubtype: "RFC822",
|
||||||
@ -323,17 +340,25 @@ func testFetch(t *testing.T, uidonly bool) {
|
|||||||
To: []imapclient.Address{{Name: "mox", Adl: "", Mailbox: "info", Host: "mox.example"}},
|
To: []imapclient.Address{{Name: "mox", Adl: "", Mailbox: "info", Host: "mox.example"}},
|
||||||
},
|
},
|
||||||
Bodystructure: imapclient.BodyTypeText{
|
Bodystructure: imapclient.BodyTypeText{
|
||||||
MediaType: "TEXT", MediaSubtype: "PLAIN", BodyFields: imapclient.BodyFields{Params: [][2]string{{"CHARSET", "ISO-8859-1"}}, CTE: "QUOTED-PRINTABLE", Octets: 51}, Lines: 1, Ext: &imapclient.BodyExtension1Part{}},
|
MediaType: "TEXT", MediaSubtype: "PLAIN", BodyFields: imapclient.BodyFields{Params: [][2]string{{"CHARSET", "ISO-8859-1"}}, CTE: "QUOTED-PRINTABLE", Octets: 51}, Lines: 1, Ext: &bodyext1},
|
||||||
Lines: 7,
|
Lines: 7,
|
||||||
Ext: &imapclient.BodyExtension1Part{
|
Ext: &imapclient.BodyExtension1Part{
|
||||||
MD5: "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=",
|
MD5: ptr("MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY="),
|
||||||
Language: []string{"en", "de"},
|
Disposition: ptr((*string)(nil)),
|
||||||
Location: "http://localhost",
|
DispositionParams: ptr([][2]string(nil)),
|
||||||
|
Language: ptr([]string{"en", "de"}),
|
||||||
|
Location: ptr(ptr("http://localhost")),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
MediaSubtype: "MIXED",
|
MediaSubtype: "MIXED",
|
||||||
Ext: &imapclient.BodyExtensionMpart{Params: [][2]string{{"BOUNDARY", "unique-boundary-1"}}},
|
Ext: &imapclient.BodyExtensionMpart{
|
||||||
|
Params: [][2]string{{"BOUNDARY", "unique-boundary-1"}},
|
||||||
|
Disposition: ptr((*string)(nil)), // Present but nil.
|
||||||
|
DispositionParams: ptr([][2]string(nil)),
|
||||||
|
Language: ptr([]string(nil)),
|
||||||
|
Location: ptr((*string)(nil)),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
tc.client.Append("inbox", makeAppendTime(nestedMessage, received))
|
tc.client.Append("inbox", makeAppendTime(nestedMessage, received))
|
||||||
|
@ -134,11 +134,11 @@ func FuzzServer(f *testing.F) {
|
|||||||
client, _ := imapclient.New(clientConn, &opts)
|
client, _ := imapclient.New(clientConn, &opts)
|
||||||
|
|
||||||
for _, cmd := range cmds {
|
for _, cmd := range cmds {
|
||||||
client.Commandf("", "%s", cmd)
|
client.WriteCommandf("", "%s", cmd)
|
||||||
client.Response()
|
client.ReadResponse()
|
||||||
}
|
}
|
||||||
client.Commandf("", "%s", s)
|
client.WriteCommandf("", "%s", s)
|
||||||
client.Response()
|
client.ReadResponse()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
err = serverConn.SetDeadline(time.Now().Add(time.Second))
|
err = serverConn.SetDeadline(time.Now().Add(time.Second))
|
||||||
|
@ -303,13 +303,13 @@ func (c *conn) xcheckMetadataSize(tx *bstore.Tx) {
|
|||||||
n++
|
n++
|
||||||
if n > metadataMaxKeys {
|
if n > metadataMaxKeys {
|
||||||
// ../rfc/5464:590
|
// ../rfc/5464:590
|
||||||
xusercodeErrorf("METADATA TOOMANY", "too many metadata entries, 1000 allowed in total")
|
xusercodeErrorf("METADATA (TOOMANY)", "too many metadata entries, 1000 allowed in total")
|
||||||
}
|
}
|
||||||
size += len(a.Key) + len(a.Value)
|
size += len(a.Key) + len(a.Value)
|
||||||
if size > metadataMaxSize {
|
if size > metadataMaxSize {
|
||||||
// ../rfc/5464:585 We only have a max total size limit, not per entry. We'll
|
// ../rfc/5464:585 We only have a max total size limit, not per entry. We'll
|
||||||
// mention the max total size.
|
// mention the max total size.
|
||||||
xusercodeErrorf(fmt.Sprintf("METADATA MAXSIZE %d", metadataMaxSize), "metadata entry values too large, total maximum size is 1MB")
|
xusercodeErrorf(fmt.Sprintf("METADATA (MAXSIZE %d)", metadataMaxSize), "metadata entry values too large, total maximum size is 1MB")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
@ -38,7 +38,7 @@ func testMetadata(t *testing.T, uidonly bool) {
|
|||||||
tc.transactf("ok", `delete metabox`) // Delete mailbox with live and expunged metadata.
|
tc.transactf("ok", `delete metabox`) // Delete mailbox with live and expunged metadata.
|
||||||
|
|
||||||
tc.transactf("no", `setmetadata expungebox (/private/comment "mailbox value")`)
|
tc.transactf("no", `setmetadata expungebox (/private/comment "mailbox value")`)
|
||||||
tc.xcode("TRYCREATE")
|
tc.xcodeWord("TRYCREATE")
|
||||||
|
|
||||||
tc.transactf("ok", `getmetadata "" ("/private/comment")`)
|
tc.transactf("ok", `getmetadata "" ("/private/comment")`)
|
||||||
tc.xuntagged(imapclient.UntaggedMetadataAnnotations{
|
tc.xuntagged(imapclient.UntaggedMetadataAnnotations{
|
||||||
@ -114,7 +114,7 @@ func testMetadata(t *testing.T, uidonly bool) {
|
|||||||
// Request with a maximum size, we don't get anything larger.
|
// Request with a maximum size, we don't get anything larger.
|
||||||
tc.transactf("ok", `setmetadata inbox (/private/another "longer")`)
|
tc.transactf("ok", `setmetadata inbox (/private/another "longer")`)
|
||||||
tc.transactf("ok", `getmetadata (maxsize 4) inbox (/private/comment /private/another)`)
|
tc.transactf("ok", `getmetadata (maxsize 4) inbox (/private/comment /private/another)`)
|
||||||
tc.xcodeArg(imapclient.CodeOther{Code: "METADATA", Args: []string{"LONGENTRIES", "6"}})
|
tc.xcode(imapclient.CodeMetadataLongEntries(6))
|
||||||
tc.xuntagged(imapclient.UntaggedMetadataAnnotations{
|
tc.xuntagged(imapclient.UntaggedMetadataAnnotations{
|
||||||
Mailbox: "Inbox",
|
Mailbox: "Inbox",
|
||||||
Annotations: []imapclient.Annotation{
|
Annotations: []imapclient.Annotation{
|
||||||
@ -230,7 +230,7 @@ func testMetadata(t *testing.T, uidonly bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Broadcast should happen when metadata capability is enabled.
|
// Broadcast should happen when metadata capability is enabled.
|
||||||
tc2.client.Enable(string(imapclient.CapMetadata))
|
tc2.client.Enable(imapclient.CapMetadata)
|
||||||
tc2.cmdf("", "idle")
|
tc2.cmdf("", "idle")
|
||||||
tc2.readprefixline("+ ")
|
tc2.readprefixline("+ ")
|
||||||
done = make(chan error)
|
done = make(chan error)
|
||||||
@ -285,12 +285,12 @@ func TestMetadataLimit(t *testing.T) {
|
|||||||
tc.client.Write(buf)
|
tc.client.Write(buf)
|
||||||
tc.client.Writelinef(")")
|
tc.client.Writelinef(")")
|
||||||
tc.response("no")
|
tc.response("no")
|
||||||
tc.xcodeArg(imapclient.CodeOther{Code: "METADATA", Args: []string{"MAXSIZE", fmt.Sprintf("%d", metadataMaxSize)}})
|
tc.xcode(imapclient.CodeMetadataMaxSize(metadataMaxSize))
|
||||||
|
|
||||||
// Reach limit for max number.
|
// Reach limit for max number.
|
||||||
for i := 1; i <= metadataMaxKeys; i++ {
|
for i := 1; i <= metadataMaxKeys; i++ {
|
||||||
tc.transactf("ok", `setmetadata inbox (/private/key%d "test")`, i)
|
tc.transactf("ok", `setmetadata inbox (/private/key%d "test")`, i)
|
||||||
}
|
}
|
||||||
tc.transactf("no", `setmetadata inbox (/private/toomany "test")`)
|
tc.transactf("no", `setmetadata inbox (/private/toomany "test")`)
|
||||||
tc.xcodeArg(imapclient.CodeOther{Code: "METADATA", Args: []string{"TOOMANY"}})
|
tc.xcode(imapclient.CodeMetadataTooMany{})
|
||||||
}
|
}
|
||||||
|
@ -56,10 +56,10 @@ func testMove(t *testing.T, uidonly bool) {
|
|||||||
tc.client.Select("inbox")
|
tc.client.Select("inbox")
|
||||||
|
|
||||||
tc.transactf("no", "move 1 nonexistent")
|
tc.transactf("no", "move 1 nonexistent")
|
||||||
tc.xcode("TRYCREATE")
|
tc.xcodeWord("TRYCREATE")
|
||||||
|
|
||||||
tc.transactf("no", "move 1 expungebox")
|
tc.transactf("no", "move 1 expungebox")
|
||||||
tc.xcode("TRYCREATE")
|
tc.xcodeWord("TRYCREATE")
|
||||||
|
|
||||||
tc.transactf("no", "move 1 inbox") // Cannot move to same mailbox.
|
tc.transactf("no", "move 1 inbox") // Cannot move to same mailbox.
|
||||||
|
|
||||||
@ -68,7 +68,7 @@ func testMove(t *testing.T, uidonly bool) {
|
|||||||
|
|
||||||
tc.transactf("ok", "move 1:* Trash")
|
tc.transactf("ok", "move 1:* Trash")
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
imapclient.UntaggedResult{Status: "OK", RespText: imapclient.RespText{Code: "COPYUID", CodeArg: imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}, To: []imapclient.NumRange{{First: 1, Last: uint32ptr(2)}}}, More: "moved"}},
|
imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}, To: []imapclient.NumRange{{First: 1, Last: uint32ptr(2)}}}, Text: "moved"},
|
||||||
imapclient.UntaggedExpunge(1),
|
imapclient.UntaggedExpunge(1),
|
||||||
imapclient.UntaggedExpunge(1),
|
imapclient.UntaggedExpunge(1),
|
||||||
)
|
)
|
||||||
@ -92,12 +92,12 @@ func testMove(t *testing.T, uidonly bool) {
|
|||||||
tc.transactf("ok", "uid move 6:5 Trash")
|
tc.transactf("ok", "uid move 6:5 Trash")
|
||||||
if uidonly {
|
if uidonly {
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
imapclient.UntaggedResult{Status: "OK", RespText: imapclient.RespText{Code: "COPYUID", CodeArg: imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 5, Last: uint32ptr(6)}}, To: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}}, More: "moved"}},
|
imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 5, Last: uint32ptr(6)}}, To: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}}, Text: "moved"},
|
||||||
imapclient.UntaggedVanished{UIDs: xparseNumSet("5:6")},
|
imapclient.UntaggedVanished{UIDs: xparseNumSet("5:6")},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
imapclient.UntaggedResult{Status: "OK", RespText: imapclient.RespText{Code: "COPYUID", CodeArg: imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 5, Last: uint32ptr(6)}}, To: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}}, More: "moved"}},
|
imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeCopyUID{DestUIDValidity: 1, From: []imapclient.NumRange{{First: 5, Last: uint32ptr(6)}}, To: []imapclient.NumRange{{First: 3, Last: uint32ptr(4)}}}, Text: "moved"},
|
||||||
imapclient.UntaggedExpunge(1),
|
imapclient.UntaggedExpunge(1),
|
||||||
imapclient.UntaggedExpunge(1),
|
imapclient.UntaggedExpunge(1),
|
||||||
)
|
)
|
||||||
|
@ -42,9 +42,9 @@ func testNotify(t *testing.T, uidonly bool) {
|
|||||||
tc.transactf("bad", "Notify Set Status (Selected (flagChange))") // flagChange must come with MessageNew and MessageExpunge.
|
tc.transactf("bad", "Notify Set Status (Selected (flagChange))") // flagChange must come with MessageNew and MessageExpunge.
|
||||||
tc.transactf("bad", "Notify Set Status (Selected (mailboxName)) (Selected-Delayed (mailboxName))") // Duplicate selected.
|
tc.transactf("bad", "Notify Set Status (Selected (mailboxName)) (Selected-Delayed (mailboxName))") // Duplicate selected.
|
||||||
tc.transactf("no", "Notify Set Status (Selected (annotationChange))") // We don't implement annotation change.
|
tc.transactf("no", "Notify Set Status (Selected (annotationChange))") // We don't implement annotation change.
|
||||||
tc.xcode("BADEVENT")
|
tc.xcode(imapclient.CodeBadEvent{"MessageNew", "MessageExpunge", "FlagChange", "MailboxName", "SubscriptionChange", "MailboxMetadataChange", "ServerMetadataChange"})
|
||||||
tc.transactf("no", "Notify Set Status (Personal (unknownEvent))")
|
tc.transactf("no", "Notify Set Status (Personal (unknownEvent))")
|
||||||
tc.xcode("BADEVENT")
|
tc.xcode(imapclient.CodeBadEvent{"MessageNew", "MessageExpunge", "FlagChange", "MailboxName", "SubscriptionChange", "MailboxMetadataChange", "ServerMetadataChange"})
|
||||||
|
|
||||||
tc2 := startNoSwitchboard(t, uidonly)
|
tc2 := startNoSwitchboard(t, uidonly)
|
||||||
defer tc2.closeNoWait()
|
defer tc2.closeNoWait()
|
||||||
@ -76,7 +76,7 @@ func testNotify(t *testing.T, uidonly bool) {
|
|||||||
// Enable notify, will first result in a the pending changes, then status.
|
// Enable notify, will first result in a the pending changes, then status.
|
||||||
tc.transactf("ok", "Notify Set Status (Selected (messageNew (Uid Modseq Bodystructure Preview) messageExpunge flagChange)) (personal (messageNew messageExpunge flagChange mailboxName subscriptionChange mailboxMetadataChange serverMetadataChange))")
|
tc.transactf("ok", "Notify Set Status (Selected (messageNew (Uid Modseq Bodystructure Preview) messageExpunge flagChange)) (personal (messageNew messageExpunge flagChange mailboxName subscriptionChange mailboxMetadataChange serverMetadataChange))")
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "HIGHESTMODSEQ", CodeArg: imapclient.CodeHighestModSeq(modseq), More: "after condstore-enabling command"}},
|
imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeHighestModSeq(modseq), Text: "after condstore-enabling command"},
|
||||||
// note: no status for Inbox since it is selected.
|
// note: no status for Inbox since it is selected.
|
||||||
imapclient.UntaggedStatus{Mailbox: "Drafts", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 0, imapclient.StatusUIDNext: 1, imapclient.StatusUIDValidity: 1, imapclient.StatusUnseen: 0, imapclient.StatusHighestModSeq: 2}},
|
imapclient.UntaggedStatus{Mailbox: "Drafts", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 0, imapclient.StatusUIDNext: 1, imapclient.StatusUIDValidity: 1, imapclient.StatusUnseen: 0, imapclient.StatusHighestModSeq: 2}},
|
||||||
imapclient.UntaggedStatus{Mailbox: "Sent", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 0, imapclient.StatusUIDNext: 1, imapclient.StatusUIDValidity: 1, imapclient.StatusUnseen: 0, imapclient.StatusHighestModSeq: 2}},
|
imapclient.UntaggedStatus{Mailbox: "Sent", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 0, imapclient.StatusUIDNext: 1, imapclient.StatusUIDValidity: 1, imapclient.StatusUnseen: 0, imapclient.StatusHighestModSeq: 2}},
|
||||||
@ -108,7 +108,12 @@ func testNotify(t *testing.T, uidonly bool) {
|
|||||||
Octets: 21,
|
Octets: 21,
|
||||||
},
|
},
|
||||||
Lines: 1,
|
Lines: 1,
|
||||||
Ext: &imapclient.BodyExtension1Part{},
|
Ext: &imapclient.BodyExtension1Part{
|
||||||
|
Disposition: ptr((*string)(nil)),
|
||||||
|
DispositionParams: ptr([][2]string(nil)),
|
||||||
|
Language: ptr([]string(nil)),
|
||||||
|
Location: ptr((*string)(nil)),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
imapclient.BodyTypeText{
|
imapclient.BodyTypeText{
|
||||||
MediaType: "TEXT",
|
MediaType: "TEXT",
|
||||||
@ -118,12 +123,21 @@ func testNotify(t *testing.T, uidonly bool) {
|
|||||||
Octets: 15,
|
Octets: 15,
|
||||||
},
|
},
|
||||||
Lines: 1,
|
Lines: 1,
|
||||||
Ext: &imapclient.BodyExtension1Part{},
|
Ext: &imapclient.BodyExtension1Part{
|
||||||
|
Disposition: ptr((*string)(nil)),
|
||||||
|
DispositionParams: ptr([][2]string(nil)),
|
||||||
|
Language: ptr([]string(nil)),
|
||||||
|
Location: ptr((*string)(nil)),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
MediaSubtype: "ALTERNATIVE",
|
MediaSubtype: "ALTERNATIVE",
|
||||||
Ext: &imapclient.BodyExtensionMpart{
|
Ext: &imapclient.BodyExtensionMpart{
|
||||||
Params: [][2]string{{"BOUNDARY", "x"}},
|
Params: [][2]string{{"BOUNDARY", "x"}},
|
||||||
|
Disposition: ptr((*string)(nil)), // Present but nil.
|
||||||
|
DispositionParams: ptr([][2]string(nil)),
|
||||||
|
Language: ptr([]string(nil)),
|
||||||
|
Location: ptr((*string)(nil)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -413,12 +427,7 @@ func testNotify(t *testing.T, uidonly bool) {
|
|||||||
// modseq++
|
// modseq++
|
||||||
tc.readuntagged(
|
tc.readuntagged(
|
||||||
imapclient.UntaggedExists(3),
|
imapclient.UntaggedExists(3),
|
||||||
imapclient.UntaggedResult{
|
imapclient.UntaggedResult{Status: "NO", Text: "generating notify fetch response: requested part does not exist"},
|
||||||
Status: "NO",
|
|
||||||
RespText: imapclient.RespText{
|
|
||||||
More: "generating notify fetch response: requested part does not exist",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
tc.untaggedFetchUID(3, 4),
|
tc.untaggedFetchUID(3, 4),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -457,15 +466,7 @@ func testNotifyOverflow(t *testing.T, uidonly bool) {
|
|||||||
tc2.client.Append("inbox", makeAppend(searchMsg))
|
tc2.client.Append("inbox", makeAppend(searchMsg))
|
||||||
|
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.xuntagged(
|
tc.xuntagged(imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeWord("NOTIFICATIONOVERFLOW"), Text: "out of sync after too many pending changes"})
|
||||||
imapclient.UntaggedResult{
|
|
||||||
Status: "OK",
|
|
||||||
RespText: imapclient.RespText{
|
|
||||||
Code: "NOTIFICATIONOVERFLOW",
|
|
||||||
More: "out of sync after too many pending changes",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Won't be getting any more notifications until we enable them again with NOTIFY.
|
// Won't be getting any more notifications until we enable them again with NOTIFY.
|
||||||
tc2.client.Append("inbox", makeAppend(searchMsg))
|
tc2.client.Append("inbox", makeAppend(searchMsg))
|
||||||
@ -500,15 +501,7 @@ func testNotifyOverflow(t *testing.T, uidonly bool) {
|
|||||||
tc2.client.UIDStoreFlagsAdd("1", true, `\Seen`)
|
tc2.client.UIDStoreFlagsAdd("1", true, `\Seen`)
|
||||||
tc2.client.UIDStoreFlagsClear("1", true, `\Seen`)
|
tc2.client.UIDStoreFlagsClear("1", true, `\Seen`)
|
||||||
tc.transactf("ok", "noop")
|
tc.transactf("ok", "noop")
|
||||||
tc.xuntagged(
|
tc.xuntagged(imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeWord("NOTIFICATIONOVERFLOW"), Text: "out of sync after too many pending changes for selected mailbox"})
|
||||||
imapclient.UntaggedResult{
|
|
||||||
Status: "OK",
|
|
||||||
RespText: imapclient.RespText{
|
|
||||||
Code: "NOTIFICATIONOVERFLOW",
|
|
||||||
More: "out of sync after too many pending changes for selected mailbox",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Again, no new notifications until we select and enable again.
|
// Again, no new notifications until we select and enable again.
|
||||||
tc2.client.UIDStoreFlagsAdd("1", true, `\Seen`)
|
tc2.client.UIDStoreFlagsAdd("1", true, `\Seen`)
|
||||||
|
@ -30,11 +30,11 @@ func testRename(t *testing.T, uidonly bool) {
|
|||||||
tc.transactf("bad", "rename x y ") // Leftover data.
|
tc.transactf("bad", "rename x y ") // Leftover data.
|
||||||
|
|
||||||
tc.transactf("no", "rename doesnotexist newbox") // Does not exist.
|
tc.transactf("no", "rename doesnotexist newbox") // Does not exist.
|
||||||
tc.xcode("NONEXISTENT") // ../rfc/9051:5140
|
tc.xcodeWord("NONEXISTENT") // ../rfc/9051:5140
|
||||||
tc.transactf("no", "rename expungebox newbox") // No longer exists.
|
tc.transactf("no", "rename expungebox newbox") // No longer exists.
|
||||||
tc.xcode("NONEXISTENT")
|
tc.xcodeWord("NONEXISTENT")
|
||||||
tc.transactf("no", `rename "Sent" "Trash"`) // Already exists.
|
tc.transactf("no", `rename "Sent" "Trash"`) // Already exists.
|
||||||
tc.xcode("ALREADYEXISTS")
|
tc.xcodeWord("ALREADYEXISTS")
|
||||||
|
|
||||||
tc.client.Create("x", nil)
|
tc.client.Create("x", nil)
|
||||||
tc.client.Subscribe("sub")
|
tc.client.Subscribe("sub")
|
||||||
@ -47,7 +47,7 @@ func testRename(t *testing.T, uidonly bool) {
|
|||||||
tc2.xuntagged(imapclient.UntaggedList{Separator: '/', Mailbox: "z"})
|
tc2.xuntagged(imapclient.UntaggedList{Separator: '/', Mailbox: "z"})
|
||||||
|
|
||||||
// OldName is only set for IMAP4rev2 or NOTIFY.
|
// OldName is only set for IMAP4rev2 or NOTIFY.
|
||||||
tc2.client.Enable("IMAP4rev2")
|
tc2.client.Enable(imapclient.CapIMAP4rev2)
|
||||||
tc.transactf("ok", "rename z y")
|
tc.transactf("ok", "rename z y")
|
||||||
tc2.transactf("ok", "noop")
|
tc2.transactf("ok", "noop")
|
||||||
tc2.xuntagged(imapclient.UntaggedList{Separator: '/', Mailbox: "y", OldName: "z"})
|
tc2.xuntagged(imapclient.UntaggedList{Separator: '/', Mailbox: "y", OldName: "z"})
|
||||||
@ -59,9 +59,9 @@ func testRename(t *testing.T, uidonly bool) {
|
|||||||
|
|
||||||
// Cannot rename a child to a parent. It already exists.
|
// Cannot rename a child to a parent. It already exists.
|
||||||
tc.transactf("no", "rename a/b/c a/b")
|
tc.transactf("no", "rename a/b/c a/b")
|
||||||
tc.xcode("ALREADYEXISTS")
|
tc.xcodeWord("ALREADYEXISTS")
|
||||||
tc.transactf("no", "rename a/b a")
|
tc.transactf("no", "rename a/b a")
|
||||||
tc.xcode("ALREADYEXISTS")
|
tc.xcodeWord("ALREADYEXISTS")
|
||||||
|
|
||||||
tc2.transactf("ok", "noop") // Drain.
|
tc2.transactf("ok", "noop") // Drain.
|
||||||
tc.transactf("ok", "rename a/b x/y") // This will cause new parent "x" to be created, and a/b and a/b/c to be renamed.
|
tc.transactf("ok", "rename a/b x/y") // This will cause new parent "x" to be created, and a/b and a/b/c to be renamed.
|
||||||
|
@ -33,37 +33,37 @@ func testReplace(t *testing.T, uidonly bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Append 3 messages, remove first. Leaves msgseq 1,2 with uid 2,3.
|
// Append 3 messages, remove first. Leaves msgseq 1,2 with uid 2,3.
|
||||||
tc.client.Append("inbox", makeAppend(exampleMsg), makeAppend(exampleMsg), makeAppend(exampleMsg))
|
tc.client.MultiAppend("inbox", makeAppend(exampleMsg), makeAppend(exampleMsg), makeAppend(exampleMsg))
|
||||||
tc.client.UIDStoreFlagsSet("1", true, `\deleted`)
|
tc.client.UIDStoreFlagsSet("1", true, `\deleted`)
|
||||||
tc.client.Expunge()
|
tc.client.Expunge()
|
||||||
|
|
||||||
tc.transactf("no", "uid replace 1 expungebox {1}") // Mailbox no longer exists.
|
tc.transactf("no", "uid replace 1 expungebox {1}") // Mailbox no longer exists.
|
||||||
tc.xcode("TRYCREATE")
|
tc.xcodeWord("TRYCREATE")
|
||||||
|
|
||||||
tc2.login("mjl@mox.example", password0)
|
tc2.login("mjl@mox.example", password0)
|
||||||
tc2.client.Select("inbox")
|
tc2.client.Select("inbox")
|
||||||
|
|
||||||
// Replace last message (msgseq 2, uid 3) in same mailbox.
|
// Replace last message (msgseq 2, uid 3) in same mailbox.
|
||||||
if uidonly {
|
if uidonly {
|
||||||
tc.lastUntagged, tc.lastResult, tc.lastErr = tc.client.UIDReplace("3", "INBOX", makeAppend(searchMsg))
|
tc.lastResponse, tc.lastErr = tc.client.UIDReplace("3", "INBOX", makeAppend(searchMsg))
|
||||||
} else {
|
} else {
|
||||||
tc.lastUntagged, tc.lastResult, tc.lastErr = tc.client.Replace("2", "INBOX", makeAppend(searchMsg))
|
tc.lastResponse, tc.lastErr = tc.client.MSNReplace("2", "INBOX", makeAppend(searchMsg))
|
||||||
}
|
}
|
||||||
tcheck(tc.t, tc.lastErr, "read imap response")
|
tcheck(tc.t, tc.lastErr, "read imap response")
|
||||||
if uidonly {
|
if uidonly {
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
imapclient.UntaggedResult{Status: "OK", RespText: imapclient.RespText{Code: "APPENDUID", CodeArg: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("4")}, More: ""}},
|
imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("4")}, Text: ""},
|
||||||
imapclient.UntaggedExists(3),
|
imapclient.UntaggedExists(3),
|
||||||
imapclient.UntaggedVanished{UIDs: xparseNumSet("3")},
|
imapclient.UntaggedVanished{UIDs: xparseNumSet("3")},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
imapclient.UntaggedResult{Status: "OK", RespText: imapclient.RespText{Code: "APPENDUID", CodeArg: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("4")}, More: ""}},
|
imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("4")}, Text: ""},
|
||||||
imapclient.UntaggedExists(3),
|
imapclient.UntaggedExists(3),
|
||||||
imapclient.UntaggedExpunge(2),
|
imapclient.UntaggedExpunge(2),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
tc.xcodeArg(imapclient.CodeHighestModSeq(8))
|
tc.xcode(imapclient.CodeHighestModSeq(8))
|
||||||
|
|
||||||
// Check that other client sees Exists and Expunge.
|
// Check that other client sees Exists and Expunge.
|
||||||
tc2.transactf("ok", "noop")
|
tc2.transactf("ok", "noop")
|
||||||
@ -83,26 +83,26 @@ func testReplace(t *testing.T, uidonly bool) {
|
|||||||
|
|
||||||
// Enable qresync, replace uid 2 (msgseq 1) to different mailbox, see that we get vanished instead of expunged.
|
// Enable qresync, replace uid 2 (msgseq 1) to different mailbox, see that we get vanished instead of expunged.
|
||||||
tc.transactf("ok", "enable qresync")
|
tc.transactf("ok", "enable qresync")
|
||||||
tc.lastUntagged, tc.lastResult, tc.lastErr = tc.client.UIDReplace("2", "INBOX", makeAppend(searchMsg))
|
tc.lastResponse, tc.lastErr = tc.client.UIDReplace("2", "INBOX", makeAppend(searchMsg))
|
||||||
tcheck(tc.t, tc.lastErr, "read imap response")
|
tcheck(tc.t, tc.lastErr, "read imap response")
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
imapclient.UntaggedResult{Status: "OK", RespText: imapclient.RespText{Code: "APPENDUID", CodeArg: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("5")}, More: ""}},
|
imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("5")}, Text: ""},
|
||||||
imapclient.UntaggedExists(3),
|
imapclient.UntaggedExists(3),
|
||||||
imapclient.UntaggedVanished{UIDs: xparseNumSet("2")},
|
imapclient.UntaggedVanished{UIDs: xparseNumSet("2")},
|
||||||
)
|
)
|
||||||
tc.xcodeArg(imapclient.CodeHighestModSeq(9))
|
tc.xcode(imapclient.CodeHighestModSeq(9))
|
||||||
|
|
||||||
// Use "*" for replacing.
|
// Use "*" for replacing.
|
||||||
tc.transactf("ok", "uid replace * inbox {1+}\r\nx")
|
tc.transactf("ok", "uid replace * inbox {1+}\r\nx")
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
imapclient.UntaggedResult{Status: "OK", RespText: imapclient.RespText{Code: "APPENDUID", CodeArg: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("6")}, More: ""}},
|
imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("6")}, Text: ""},
|
||||||
imapclient.UntaggedExists(3),
|
imapclient.UntaggedExists(3),
|
||||||
imapclient.UntaggedVanished{UIDs: xparseNumSet("5")},
|
imapclient.UntaggedVanished{UIDs: xparseNumSet("5")},
|
||||||
)
|
)
|
||||||
if !uidonly {
|
if !uidonly {
|
||||||
tc.transactf("ok", "replace * inbox {1+}\r\ny")
|
tc.transactf("ok", "replace * inbox {1+}\r\ny")
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
imapclient.UntaggedResult{Status: "OK", RespText: imapclient.RespText{Code: "APPENDUID", CodeArg: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("7")}, More: ""}},
|
imapclient.UntaggedResult{Status: "OK", Code: imapclient.CodeAppendUID{UIDValidity: 1, UIDs: xparseUIDRange("7")}, Text: ""},
|
||||||
imapclient.UntaggedExists(3),
|
imapclient.UntaggedExists(3),
|
||||||
imapclient.UntaggedVanished{UIDs: xparseNumSet("6")},
|
imapclient.UntaggedVanished{UIDs: xparseNumSet("6")},
|
||||||
)
|
)
|
||||||
@ -129,9 +129,9 @@ func TestReplaceBigNonsyncLit(t *testing.T) {
|
|||||||
// Adding a message >1mb with non-sync literal to non-existent mailbox should abort entire connection.
|
// Adding a message >1mb with non-sync literal to non-existent mailbox should abort entire connection.
|
||||||
tc.transactf("bad", "replace 12345 inbox {2000000+}")
|
tc.transactf("bad", "replace 12345 inbox {2000000+}")
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
imapclient.UntaggedBye{Code: "ALERT", More: "error condition and non-synchronizing literal too big"},
|
imapclient.UntaggedBye{Code: imapclient.CodeWord("ALERT"), Text: "error condition and non-synchronizing literal too big"},
|
||||||
)
|
)
|
||||||
tc.xcode("TOOBIG")
|
tc.xcodeWord("TOOBIG")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReplaceQuota(t *testing.T) {
|
func TestReplaceQuota(t *testing.T) {
|
||||||
@ -153,11 +153,11 @@ func testReplaceQuota(t *testing.T, uidonly bool) {
|
|||||||
|
|
||||||
// Synchronizing literal, we get failure immediately.
|
// Synchronizing literal, we get failure immediately.
|
||||||
tc.transactf("no", "uid replace 1 inbox {6}\r\n")
|
tc.transactf("no", "uid replace 1 inbox {6}\r\n")
|
||||||
tc.xcode("OVERQUOTA")
|
tc.xcodeWord("OVERQUOTA")
|
||||||
|
|
||||||
// Synchronizing literal to non-existent mailbox, we get failure immediately.
|
// Synchronizing literal to non-existent mailbox, we get failure immediately.
|
||||||
tc.transactf("no", "uid replace 1 badbox {6}\r\n")
|
tc.transactf("no", "uid replace 1 badbox {6}\r\n")
|
||||||
tc.xcode("TRYCREATE")
|
tc.xcodeWord("TRYCREATE")
|
||||||
|
|
||||||
buf := make([]byte, 4000, 4002)
|
buf := make([]byte, 4000, 4002)
|
||||||
for i := range buf {
|
for i := range buf {
|
||||||
@ -166,18 +166,18 @@ func testReplaceQuota(t *testing.T, uidonly bool) {
|
|||||||
buf = append(buf, "\r\n"...)
|
buf = append(buf, "\r\n"...)
|
||||||
|
|
||||||
// Non-synchronizing literal. We get to write our data.
|
// Non-synchronizing literal. We get to write our data.
|
||||||
tc.client.Commandf("", "uid replace 1 inbox ~{4000+}")
|
tc.client.WriteCommandf("", "uid replace 1 inbox ~{4000+}")
|
||||||
_, err := tc.client.Write(buf)
|
_, err := tc.client.Write(buf)
|
||||||
tc.check(err, "write replace message")
|
tc.check(err, "write replace message")
|
||||||
tc.response("no")
|
tc.response("no")
|
||||||
tc.xcode("OVERQUOTA")
|
tc.xcodeWord("OVERQUOTA")
|
||||||
|
|
||||||
// Non-synchronizing literal to bad mailbox.
|
// Non-synchronizing literal to bad mailbox.
|
||||||
tc.client.Commandf("", "uid replace 1 badbox {4000+}")
|
tc.client.WriteCommandf("", "uid replace 1 badbox {4000+}")
|
||||||
_, err = tc.client.Write(buf)
|
_, err = tc.client.Write(buf)
|
||||||
tc.check(err, "write replace message")
|
tc.check(err, "write replace message")
|
||||||
tc.response("no")
|
tc.response("no")
|
||||||
tc.xcode("TRYCREATE")
|
tc.xcodeWord("TRYCREATE")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReplaceExpunged(t *testing.T) {
|
func TestReplaceExpunged(t *testing.T) {
|
||||||
@ -197,7 +197,7 @@ func testReplaceExpunged(t *testing.T, uidonly bool) {
|
|||||||
tc.client.Append("inbox", makeAppend(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
|
|
||||||
// We start the command, but don't write data yet.
|
// We start the command, but don't write data yet.
|
||||||
tc.client.Commandf("", "uid replace 1 inbox {4000}")
|
tc.client.WriteCommandf("", "uid replace 1 inbox {4000}")
|
||||||
|
|
||||||
// Get in with second client and remove the message we are replacing.
|
// Get in with second client and remove the message we are replacing.
|
||||||
tc2 := startNoSwitchboard(t, uidonly)
|
tc2 := startNoSwitchboard(t, uidonly)
|
||||||
|
@ -57,7 +57,7 @@ func (tc *testconn) xsearchmodseq(modseq int64, nums ...uint32) {
|
|||||||
func (tc *testconn) xesearch(exp imapclient.UntaggedEsearch) {
|
func (tc *testconn) xesearch(exp imapclient.UntaggedEsearch) {
|
||||||
tc.t.Helper()
|
tc.t.Helper()
|
||||||
|
|
||||||
exp.Tag = tc.client.LastTag
|
exp.Tag = tc.client.LastTag()
|
||||||
tc.xuntagged(exp)
|
tc.xuntagged(exp)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -298,11 +298,8 @@ func testSearch(t *testing.T, uidonly bool) {
|
|||||||
inprogress := func(cur, goal uint32) imapclient.UntaggedResult {
|
inprogress := func(cur, goal uint32) imapclient.UntaggedResult {
|
||||||
return imapclient.UntaggedResult{
|
return imapclient.UntaggedResult{
|
||||||
Status: "OK",
|
Status: "OK",
|
||||||
RespText: imapclient.RespText{
|
Code: imapclient.CodeInProgress{Tag: "tag1", Current: &cur, Goal: &goal},
|
||||||
Code: "INPROGRESS",
|
Text: "still searching",
|
||||||
CodeArg: imapclient.CodeInProgress{Tag: "tag1", Current: &cur, Goal: &goal},
|
|
||||||
More: "still searching",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tc.xuntagged(
|
tc.xuntagged(
|
||||||
@ -408,7 +405,7 @@ func testSearch(t *testing.T, uidonly bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Do a seemingly old-style search command with IMAP4rev2 enabled. We'll still get ESEARCH responses.
|
// Do a seemingly old-style search command with IMAP4rev2 enabled. We'll still get ESEARCH responses.
|
||||||
tc.client.Enable("IMAP4rev2")
|
tc.client.Enable(imapclient.CapIMAP4rev2)
|
||||||
|
|
||||||
if !uidonly {
|
if !uidonly {
|
||||||
tc.transactf("ok", `search undraft`)
|
tc.transactf("ok", `search undraft`)
|
||||||
@ -566,11 +563,8 @@ func testSearchMulti(t *testing.T, selected, uidonly bool) {
|
|||||||
inprogress := func(cur, goal uint32) imapclient.UntaggedResult {
|
inprogress := func(cur, goal uint32) imapclient.UntaggedResult {
|
||||||
return imapclient.UntaggedResult{
|
return imapclient.UntaggedResult{
|
||||||
Status: "OK",
|
Status: "OK",
|
||||||
RespText: imapclient.RespText{
|
Code: imapclient.CodeInProgress{Tag: "Tag1", Current: &cur, Goal: &goal},
|
||||||
Code: "INPROGRESS",
|
Text: "still searching",
|
||||||
CodeArg: imapclient.CodeInProgress{Tag: "Tag1", Current: &cur, Goal: &goal},
|
|
||||||
More: "still searching",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tc.cmdf("Tag1", `Esearch In (Personal) Return () All`)
|
tc.cmdf("Tag1", `Esearch In (Personal) Return () All`)
|
||||||
|
@ -38,19 +38,19 @@ func testSelectExamine(t *testing.T, examine, uidonly bool) {
|
|||||||
okcode = "READ-ONLY"
|
okcode = "READ-ONLY"
|
||||||
}
|
}
|
||||||
|
|
||||||
uclosed := imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "CLOSED", More: "x"}}
|
uclosed := imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeWord("CLOSED"), Text: "x"}
|
||||||
flags := strings.Split(`\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent`, " ")
|
flags := strings.Split(`\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent`, " ")
|
||||||
permflags := strings.Split(`\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent \*`, " ")
|
permflags := strings.Split(`\Seen \Answered \Flagged \Deleted \Draft $Forwarded $Junk $NotJunk $Phishing $MDNSent \*`, " ")
|
||||||
uflags := imapclient.UntaggedFlags(flags)
|
uflags := imapclient.UntaggedFlags(flags)
|
||||||
upermflags := imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "PERMANENTFLAGS", CodeArg: imapclient.CodeList{Code: "PERMANENTFLAGS", Args: permflags}, More: "x"}}
|
upermflags := imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodePermanentFlags(permflags), Text: "x"}
|
||||||
urecent := imapclient.UntaggedRecent(0)
|
urecent := imapclient.UntaggedRecent(0)
|
||||||
uexists0 := imapclient.UntaggedExists(0)
|
uexists0 := imapclient.UntaggedExists(0)
|
||||||
uexists1 := imapclient.UntaggedExists(1)
|
uexists1 := imapclient.UntaggedExists(1)
|
||||||
uuidval1 := imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UIDVALIDITY", CodeArg: imapclient.CodeUint{Code: "UIDVALIDITY", Num: 1}, More: "x"}}
|
uuidval1 := imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeUIDValidity(1), Text: "x"}
|
||||||
uuidnext1 := imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UIDNEXT", CodeArg: imapclient.CodeUint{Code: "UIDNEXT", Num: 1}, More: "x"}}
|
uuidnext1 := imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeUIDNext(1), Text: "x"}
|
||||||
ulist := imapclient.UntaggedList{Separator: '/', Mailbox: "Inbox"}
|
ulist := imapclient.UntaggedList{Separator: '/', Mailbox: "Inbox"}
|
||||||
uunseen := imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UNSEEN", CodeArg: imapclient.CodeUint{Code: "UNSEEN", Num: 1}, More: "x"}}
|
uunseen := imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeUnseen(1), Text: "x"}
|
||||||
uuidnext2 := imapclient.UntaggedResult{Status: imapclient.OK, RespText: imapclient.RespText{Code: "UIDNEXT", CodeArg: imapclient.CodeUint{Code: "UIDNEXT", Num: 2}, More: "x"}}
|
uuidnext2 := imapclient.UntaggedResult{Status: imapclient.OK, Code: imapclient.CodeUIDNext(2), Text: "x"}
|
||||||
|
|
||||||
// Parameter required.
|
// Parameter required.
|
||||||
tc.transactf("bad", "%s", cmd)
|
tc.transactf("bad", "%s", cmd)
|
||||||
@ -61,11 +61,11 @@ func testSelectExamine(t *testing.T, examine, uidonly bool) {
|
|||||||
|
|
||||||
tc.transactf("ok", "%s inbox", cmd)
|
tc.transactf("ok", "%s inbox", cmd)
|
||||||
tc.xuntagged(uflags, upermflags, urecent, uexists0, uuidval1, uuidnext1, ulist)
|
tc.xuntagged(uflags, upermflags, urecent, uexists0, uuidval1, uuidnext1, ulist)
|
||||||
tc.xcode(okcode)
|
tc.xcodeWord(okcode)
|
||||||
|
|
||||||
tc.transactf("ok", `%s "inbox"`, cmd)
|
tc.transactf("ok", `%s "inbox"`, cmd)
|
||||||
tc.xuntagged(uclosed, uflags, upermflags, urecent, uexists0, uuidval1, uuidnext1, ulist)
|
tc.xuntagged(uclosed, uflags, upermflags, urecent, uexists0, uuidval1, uuidnext1, ulist)
|
||||||
tc.xcode(okcode)
|
tc.xcodeWord(okcode)
|
||||||
|
|
||||||
// Append a message. It will be reported as UNSEEN.
|
// Append a message. It will be reported as UNSEEN.
|
||||||
tc.client.Append("inbox", makeAppend(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
@ -75,11 +75,11 @@ func testSelectExamine(t *testing.T, examine, uidonly bool) {
|
|||||||
} else {
|
} else {
|
||||||
tc.xuntagged(uclosed, uflags, upermflags, urecent, uunseen, uexists1, uuidval1, uuidnext2, ulist)
|
tc.xuntagged(uclosed, uflags, upermflags, urecent, uunseen, uexists1, uuidval1, uuidnext2, ulist)
|
||||||
}
|
}
|
||||||
tc.xcode(okcode)
|
tc.xcodeWord(okcode)
|
||||||
|
|
||||||
// With imap4rev2, we no longer get untagged RECENT or untagged UNSEEN.
|
// With imap4rev2, we no longer get untagged RECENT or untagged UNSEEN.
|
||||||
tc.client.Enable("imap4rev2")
|
tc.client.Enable(imapclient.CapIMAP4rev2)
|
||||||
tc.transactf("ok", "%s inbox", cmd)
|
tc.transactf("ok", "%s inbox", cmd)
|
||||||
tc.xuntagged(uclosed, uflags, upermflags, uexists1, uuidval1, uuidnext2, ulist)
|
tc.xuntagged(uclosed, uflags, upermflags, uexists1, uuidval1, uuidnext2, ulist)
|
||||||
tc.xcode(okcode)
|
tc.xcodeWord(okcode)
|
||||||
}
|
}
|
||||||
|
@ -161,6 +161,22 @@ func tcheck(t *testing.T, err error, msg string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mustParseUntagged(s string) imapclient.Untagged {
|
||||||
|
r, err := imapclient.ParseUntagged(s + "\r\n")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustParseCode(s string) imapclient.Code {
|
||||||
|
r, err := imapclient.ParseCode(s)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
func mockUIDValidity() func() {
|
func mockUIDValidity() func() {
|
||||||
orig := store.InitialUIDValidity
|
orig := store.InitialUIDValidity
|
||||||
store.InitialUIDValidity = func() uint32 {
|
store.InitialUIDValidity = func() uint32 {
|
||||||
@ -182,8 +198,7 @@ type testconn struct {
|
|||||||
switchStop func()
|
switchStop func()
|
||||||
|
|
||||||
// Result of last command.
|
// Result of last command.
|
||||||
lastUntagged []imapclient.Untagged
|
lastResponse imapclient.Response
|
||||||
lastResult imapclient.Result
|
|
||||||
lastErr error
|
lastErr error
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,24 +209,21 @@ func (tc *testconn) check(err error, msg string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *testconn) last(l []imapclient.Untagged, r imapclient.Result, err error) {
|
func (tc *testconn) last(resp imapclient.Response, err error) {
|
||||||
tc.lastUntagged = l
|
tc.lastResponse = resp
|
||||||
tc.lastResult = r
|
|
||||||
tc.lastErr = err
|
tc.lastErr = err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *testconn) xcode(s string) {
|
func (tc *testconn) xcode(c imapclient.Code) {
|
||||||
tc.t.Helper()
|
tc.t.Helper()
|
||||||
if tc.lastResult.Code != s {
|
if !reflect.DeepEqual(tc.lastResponse.Code, c) {
|
||||||
tc.t.Fatalf("got last code %q, expected %q", tc.lastResult.Code, s)
|
tc.t.Fatalf("got last code %#v, expected %#v", tc.lastResponse.Code, c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *testconn) xcodeArg(v any) {
|
func (tc *testconn) xcodeWord(s string) {
|
||||||
tc.t.Helper()
|
tc.t.Helper()
|
||||||
if !reflect.DeepEqual(tc.lastResult.CodeArg, v) {
|
tc.xcode(imapclient.CodeWord(s))
|
||||||
tc.t.Fatalf("got last code argument %v, expected %v", tc.lastResult.CodeArg, v)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *testconn) xuntagged(exps ...imapclient.Untagged) {
|
func (tc *testconn) xuntagged(exps ...imapclient.Untagged) {
|
||||||
@ -221,7 +233,7 @@ func (tc *testconn) xuntagged(exps ...imapclient.Untagged) {
|
|||||||
|
|
||||||
func (tc *testconn) xuntaggedOpt(all bool, exps ...imapclient.Untagged) {
|
func (tc *testconn) xuntaggedOpt(all bool, exps ...imapclient.Untagged) {
|
||||||
tc.t.Helper()
|
tc.t.Helper()
|
||||||
last := slices.Clone(tc.lastUntagged)
|
last := slices.Clone(tc.lastResponse.Untagged)
|
||||||
var mismatch any
|
var mismatch any
|
||||||
next:
|
next:
|
||||||
for ei, exp := range exps {
|
for ei, exp := range exps {
|
||||||
@ -241,10 +253,10 @@ next:
|
|||||||
tc.t.Fatalf("untagged data mismatch, got:\n\t%T %#v\nexpected:\n\t%T %#v", mismatch, mismatch, exp, exp)
|
tc.t.Fatalf("untagged data mismatch, got:\n\t%T %#v\nexpected:\n\t%T %#v", mismatch, mismatch, exp, exp)
|
||||||
}
|
}
|
||||||
var next string
|
var next string
|
||||||
if len(tc.lastUntagged) > 0 {
|
if len(tc.lastResponse.Untagged) > 0 {
|
||||||
next = fmt.Sprintf(", next:\n%#v", tc.lastUntagged[0])
|
next = fmt.Sprintf(", next:\n%#v", tc.lastResponse.Untagged[0])
|
||||||
}
|
}
|
||||||
tc.t.Fatalf("did not find untagged response:\n%#v %T (%d)\nin %v%s", exp, exp, ei, tc.lastUntagged, next)
|
tc.t.Fatalf("did not find untagged response:\n%#v %T (%d)\nin %v%s", exp, exp, ei, tc.lastResponse.Untagged, next)
|
||||||
}
|
}
|
||||||
if len(last) > 0 && all {
|
if len(last) > 0 && all {
|
||||||
tc.t.Fatalf("leftover untagged responses %v", last)
|
tc.t.Fatalf("leftover untagged responses %v", last)
|
||||||
@ -263,8 +275,8 @@ func tuntagged(t *testing.T, got imapclient.Untagged, dst any) {
|
|||||||
|
|
||||||
func (tc *testconn) xnountagged() {
|
func (tc *testconn) xnountagged() {
|
||||||
tc.t.Helper()
|
tc.t.Helper()
|
||||||
if len(tc.lastUntagged) != 0 {
|
if len(tc.lastResponse.Untagged) != 0 {
|
||||||
tc.t.Fatalf("got %v untagged, expected 0", tc.lastUntagged)
|
tc.t.Fatalf("got %v untagged, expected 0", tc.lastResponse.Untagged)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -288,16 +300,24 @@ func (tc *testconn) transactf(status, format string, args ...any) {
|
|||||||
|
|
||||||
func (tc *testconn) response(status string) {
|
func (tc *testconn) response(status string) {
|
||||||
tc.t.Helper()
|
tc.t.Helper()
|
||||||
tc.lastUntagged, tc.lastResult, tc.lastErr = tc.client.Response()
|
tc.lastResponse, tc.lastErr = tc.client.ReadResponse()
|
||||||
|
if tc.lastErr != nil {
|
||||||
|
if resp, ok := tc.lastErr.(imapclient.Response); ok {
|
||||||
|
if !reflect.DeepEqual(resp, tc.lastResponse) {
|
||||||
|
tc.t.Fatalf("response error %#v != returned response %#v", tc.lastErr, tc.lastResponse)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
tcheck(tc.t, tc.lastErr, "read imap response")
|
tcheck(tc.t, tc.lastErr, "read imap response")
|
||||||
if strings.ToUpper(status) != string(tc.lastResult.Status) {
|
}
|
||||||
tc.t.Fatalf("got status %q, expected %q", tc.lastResult.Status, status)
|
}
|
||||||
|
if strings.ToUpper(status) != string(tc.lastResponse.Status) {
|
||||||
|
tc.t.Fatalf("got status %q, expected %q", tc.lastResponse.Status, status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *testconn) cmdf(tag, format string, args ...any) {
|
func (tc *testconn) cmdf(tag, format string, args ...any) {
|
||||||
tc.t.Helper()
|
tc.t.Helper()
|
||||||
err := tc.client.Commandf(tag, format, args...)
|
err := tc.client.WriteCommandf(tag, format, args...)
|
||||||
tcheck(tc.t, err, "writing imap command")
|
tcheck(tc.t, err, "writing imap command")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -658,15 +678,15 @@ func TestLiterals(t *testing.T) {
|
|||||||
|
|
||||||
from := "ntmpbox"
|
from := "ntmpbox"
|
||||||
to := "tmpbox"
|
to := "tmpbox"
|
||||||
|
tc.client.LastTagSet("xtag")
|
||||||
fmt.Fprint(tc.client, "xtag rename ")
|
fmt.Fprint(tc.client, "xtag rename ")
|
||||||
tc.client.WriteSyncLiteral(from)
|
tc.client.WriteSyncLiteral(from)
|
||||||
fmt.Fprint(tc.client, " ")
|
fmt.Fprint(tc.client, " ")
|
||||||
tc.client.WriteSyncLiteral(to)
|
tc.client.WriteSyncLiteral(to)
|
||||||
fmt.Fprint(tc.client, "\r\n")
|
fmt.Fprint(tc.client, "\r\n")
|
||||||
tc.client.LastTag = "xtag"
|
tc.lastResponse, tc.lastErr = tc.client.ReadResponse()
|
||||||
tc.last(tc.client.Response())
|
if tc.lastResponse.Status != "OK" {
|
||||||
if tc.lastResult.Status != "OK" {
|
tc.t.Fatalf(`got %q, expected "OK"`, tc.lastResponse.Status)
|
||||||
tc.t.Fatalf(`got %q, expected "OK"`, tc.lastResult.Status)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -937,7 +957,7 @@ func TestReference(t *testing.T) {
|
|||||||
tc2.login("mjl@mox.example", password0)
|
tc2.login("mjl@mox.example", password0)
|
||||||
tc2.client.Select("inbox")
|
tc2.client.Select("inbox")
|
||||||
|
|
||||||
tc.client.StoreFlagsSet("1", true, `\Deleted`)
|
tc.client.MSNStoreFlagsSet("1", true, `\Deleted`)
|
||||||
tc.client.Expunge()
|
tc.client.Expunge()
|
||||||
|
|
||||||
tc3 := startNoSwitchboard(t, false)
|
tc3 := startNoSwitchboard(t, false)
|
||||||
@ -945,7 +965,7 @@ func TestReference(t *testing.T) {
|
|||||||
tc3.login("mjl@mox.example", password0)
|
tc3.login("mjl@mox.example", password0)
|
||||||
tc3.transactf("ok", `list "" "inbox" return (status (messages))`)
|
tc3.transactf("ok", `list "" "inbox" return (status (messages))`)
|
||||||
tc3.xuntagged(
|
tc3.xuntagged(
|
||||||
imapclient.UntaggedList{Separator: '/', Mailbox: "Inbox"},
|
mustParseUntagged(`* LIST () "/" Inbox`),
|
||||||
imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 0}},
|
imapclient.UntaggedStatus{Mailbox: "Inbox", Attrs: map[imapclient.StatusAttr]int64{imapclient.StatusMessages: 0}},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
func TestStarttls(t *testing.T) {
|
func TestStarttls(t *testing.T) {
|
||||||
tc := start(t, false)
|
tc := start(t, false)
|
||||||
tc.client.Starttls(&tls.Config{InsecureSkipVerify: true})
|
tc.client.StartTLS(&tls.Config{InsecureSkipVerify: true})
|
||||||
tc.transactf("bad", "starttls") // TLS already active.
|
tc.transactf("bad", "starttls") // TLS already active.
|
||||||
tc.login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc.close()
|
tc.close()
|
||||||
@ -19,10 +19,10 @@ func TestStarttls(t *testing.T) {
|
|||||||
|
|
||||||
tc = startArgs(t, false, true, false, false, true, "mjl")
|
tc = startArgs(t, false, true, false, false, true, "mjl")
|
||||||
tc.transactf("no", `login "mjl@mox.example" "%s"`, password0)
|
tc.transactf("no", `login "mjl@mox.example" "%s"`, password0)
|
||||||
tc.xcode("PRIVACYREQUIRED")
|
tc.xcodeWord("PRIVACYREQUIRED")
|
||||||
tc.transactf("no", "authenticate PLAIN %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000"+password0)))
|
tc.transactf("no", "authenticate PLAIN %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000"+password0)))
|
||||||
tc.xcode("PRIVACYREQUIRED")
|
tc.xcodeWord("PRIVACYREQUIRED")
|
||||||
tc.client.Starttls(&tls.Config{InsecureSkipVerify: true})
|
tc.client.StartTLS(&tls.Config{InsecureSkipVerify: true})
|
||||||
tc.login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc.close()
|
tc.close()
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ func testStore(t *testing.T, uidonly bool) {
|
|||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc.login("mjl@mox.example", password0)
|
tc.login("mjl@mox.example", password0)
|
||||||
tc.client.Enable("imap4rev2")
|
tc.client.Enable(imapclient.CapIMAP4rev2)
|
||||||
|
|
||||||
tc.client.Append("inbox", makeAppend(exampleMsg))
|
tc.client.Append("inbox", makeAppend(exampleMsg))
|
||||||
tc.client.Select("inbox")
|
tc.client.Select("inbox")
|
||||||
|
@ -11,29 +11,29 @@ func TestUIDOnly(t *testing.T) {
|
|||||||
tc.client.Select("inbox")
|
tc.client.Select("inbox")
|
||||||
|
|
||||||
tc.transactf("bad", "Fetch 1")
|
tc.transactf("bad", "Fetch 1")
|
||||||
tc.xcode("UIDREQUIRED")
|
tc.xcodeWord("UIDREQUIRED")
|
||||||
tc.transactf("bad", "Fetch 1")
|
tc.transactf("bad", "Fetch 1")
|
||||||
tc.xcode("UIDREQUIRED")
|
tc.xcodeWord("UIDREQUIRED")
|
||||||
tc.transactf("bad", "Search 1")
|
tc.transactf("bad", "Search 1")
|
||||||
tc.xcode("UIDREQUIRED")
|
tc.xcodeWord("UIDREQUIRED")
|
||||||
tc.transactf("bad", "Store 1 Flags ()")
|
tc.transactf("bad", "Store 1 Flags ()")
|
||||||
tc.xcode("UIDREQUIRED")
|
tc.xcodeWord("UIDREQUIRED")
|
||||||
tc.transactf("bad", "Copy 1 Archive")
|
tc.transactf("bad", "Copy 1 Archive")
|
||||||
tc.xcode("UIDREQUIRED")
|
tc.xcodeWord("UIDREQUIRED")
|
||||||
tc.transactf("bad", "Move 1 Archive")
|
tc.transactf("bad", "Move 1 Archive")
|
||||||
tc.xcode("UIDREQUIRED")
|
tc.xcodeWord("UIDREQUIRED")
|
||||||
|
|
||||||
// Sequence numbers in search program.
|
// Sequence numbers in search program.
|
||||||
tc.transactf("bad", "Uid Search 1")
|
tc.transactf("bad", "Uid Search 1")
|
||||||
tc.xcode("UIDREQUIRED")
|
tc.xcodeWord("UIDREQUIRED")
|
||||||
|
|
||||||
// Sequence number in last qresync parameter.
|
// Sequence number in last qresync parameter.
|
||||||
tc.transactf("ok", "Enable Qresync")
|
tc.transactf("ok", "Enable Qresync")
|
||||||
tc.transactf("bad", "Select inbox (Qresync (1 5 (1,3,6 1,3,6)))")
|
tc.transactf("bad", "Select inbox (Qresync (1 5 (1,3,6 1,3,6)))")
|
||||||
tc.xcode("UIDREQUIRED")
|
tc.xcodeWord("UIDREQUIRED")
|
||||||
tc.client.Select("inbox") // Select again.
|
tc.client.Select("inbox") // Select again.
|
||||||
|
|
||||||
// Breaks connection.
|
// Breaks connection.
|
||||||
tc.transactf("bad", "replace 1 inbox {1+}\r\nx")
|
tc.transactf("bad", "replace 1 inbox {1+}\r\nx")
|
||||||
tc.xcode("UIDREQUIRED")
|
tc.xcodeWord("UIDREQUIRED")
|
||||||
}
|
}
|
||||||
|
@ -10,10 +10,10 @@ func TestUnsubscribe(t *testing.T) {
|
|||||||
tc := start(t, false)
|
tc := start(t, false)
|
||||||
defer tc.close()
|
defer tc.close()
|
||||||
|
|
||||||
tc.login("mjl@mox.example", password0)
|
|
||||||
|
|
||||||
tc2 := startNoSwitchboard(t, false)
|
tc2 := startNoSwitchboard(t, false)
|
||||||
defer tc2.closeNoWait()
|
defer tc2.closeNoWait()
|
||||||
|
|
||||||
|
tc.login("mjl@mox.example", password0)
|
||||||
tc2.login("mjl@mox.example", password0)
|
tc2.login("mjl@mox.example", password0)
|
||||||
|
|
||||||
tc.transactf("bad", "unsubscribe") // Missing param.
|
tc.transactf("bad", "unsubscribe") // Missing param.
|
||||||
|
@ -70,16 +70,16 @@ func TestDeliver(t *testing.T) {
|
|||||||
imapc, err := imapclient.New(imapconn, &opts)
|
imapc, err := imapclient.New(imapconn, &opts)
|
||||||
tcheck(t, err, "new imapclient")
|
tcheck(t, err, "new imapclient")
|
||||||
|
|
||||||
_, _, err = imapc.Login(imapuser, imappassword)
|
_, err = imapc.Login(imapuser, imappassword)
|
||||||
tcheck(t, err, "imap login")
|
tcheck(t, err, "imap login")
|
||||||
|
|
||||||
_, _, err = imapc.Select("Inbox")
|
_, err = imapc.Select("Inbox")
|
||||||
tcheck(t, err, "imap select inbox")
|
tcheck(t, err, "imap select inbox")
|
||||||
|
|
||||||
err = imapc.Commandf("", "idle")
|
err = imapc.WriteCommandf("", "idle")
|
||||||
tcheck(t, err, "write imap idle command")
|
tcheck(t, err, "write imap idle command")
|
||||||
|
|
||||||
_, _, _, err = imapc.ReadContinuation()
|
_, err = imapc.ReadContinuation()
|
||||||
tcheck(t, err, "read imap continuation")
|
tcheck(t, err, "read imap continuation")
|
||||||
|
|
||||||
idle := make(chan idleResponse)
|
idle := make(chan idleResponse)
|
||||||
|
1056
testdata/imapclient/fuzzseed.txt
vendored
Normal file
1056
testdata/imapclient/fuzzseed.txt
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user