1244 lines
29 KiB
Go
1244 lines
29 KiB
Go
// Package imapclient implements an IMAP client.
|
|
//
|
|
// # Charset decoding
|
|
//
|
|
// By default, only basic charset decoding is performed. For non-UTF-8 decoding
|
|
// of message subjects and e-mail address names, users can set
|
|
// Options.WordDecoder. For instance, to use go-message's collection of
|
|
// charsets:
|
|
//
|
|
// import (
|
|
// "mime"
|
|
//
|
|
// "github.com/emersion/go-message/charset"
|
|
// )
|
|
//
|
|
// options := &imapclient.Options{
|
|
// WordDecoder: &mime.WordDecoder{CharsetReader: charset.Reader},
|
|
// }
|
|
// client, err := imapclient.DialTLS("imap.example.org:993", options)
|
|
package imapclient
|
|
|
|
import (
|
|
"bufio"
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"mime"
|
|
"net"
|
|
"runtime/debug"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/emersion/go-imap/v2"
|
|
"github.com/emersion/go-imap/v2/internal"
|
|
"github.com/emersion/go-imap/v2/internal/imapwire"
|
|
)
|
|
|
|
const (
|
|
idleReadTimeout = time.Duration(0)
|
|
respReadTimeout = 30 * time.Second
|
|
literalReadTimeout = 5 * time.Minute
|
|
|
|
cmdWriteTimeout = 30 * time.Second
|
|
literalWriteTimeout = 5 * time.Minute
|
|
|
|
defaultDialTimeout = 30 * time.Second
|
|
)
|
|
|
|
// SelectedMailbox contains metadata for the currently selected mailbox.
|
|
type SelectedMailbox struct {
|
|
Name string
|
|
NumMessages uint32
|
|
Flags []imap.Flag
|
|
PermanentFlags []imap.Flag
|
|
}
|
|
|
|
func (mbox *SelectedMailbox) copy() *SelectedMailbox {
|
|
copy := *mbox
|
|
return ©
|
|
}
|
|
|
|
// Options contains options for Client.
|
|
type Options struct {
|
|
// TLS configuration for use by DialTLS and DialStartTLS. If nil, the
|
|
// default configuration is used.
|
|
TLSConfig *tls.Config
|
|
// Raw ingress and egress data will be written to this writer, if any.
|
|
// Note, this may include sensitive information such as credentials used
|
|
// during authentication.
|
|
DebugWriter io.Writer
|
|
// Unilateral data handler.
|
|
UnilateralDataHandler *UnilateralDataHandler
|
|
// Decoder for RFC 2047 words.
|
|
WordDecoder *mime.WordDecoder
|
|
// Dialer to use when establishing connections with the Dial* functions.
|
|
// If nil, a default dialer with a 30 second timeout is used.
|
|
Dialer *net.Dialer
|
|
}
|
|
|
|
func (options *Options) wrapReadWriter(rw io.ReadWriter) io.ReadWriter {
|
|
if options.DebugWriter == nil {
|
|
return rw
|
|
}
|
|
return struct {
|
|
io.Reader
|
|
io.Writer
|
|
}{
|
|
Reader: io.TeeReader(rw, options.DebugWriter),
|
|
Writer: io.MultiWriter(rw, options.DebugWriter),
|
|
}
|
|
}
|
|
|
|
func (options *Options) decodeText(s string) (string, error) {
|
|
wordDecoder := options.WordDecoder
|
|
if wordDecoder == nil {
|
|
wordDecoder = &mime.WordDecoder{}
|
|
}
|
|
out, err := wordDecoder.DecodeHeader(s)
|
|
if err != nil {
|
|
return s, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (options *Options) unilateralDataHandler() *UnilateralDataHandler {
|
|
if options.UnilateralDataHandler == nil {
|
|
return &UnilateralDataHandler{}
|
|
}
|
|
return options.UnilateralDataHandler
|
|
}
|
|
|
|
func (options *Options) tlsConfig() *tls.Config {
|
|
if options.TLSConfig != nil {
|
|
return options.TLSConfig.Clone()
|
|
} else {
|
|
return new(tls.Config)
|
|
}
|
|
}
|
|
|
|
func (options *Options) dialer() *net.Dialer {
|
|
if options.Dialer == nil {
|
|
return &net.Dialer{Timeout: defaultDialTimeout}
|
|
}
|
|
return options.Dialer
|
|
}
|
|
|
|
// Client is an IMAP client.
|
|
//
|
|
// IMAP commands are exposed as methods. These methods will block until the
|
|
// command has been sent to the server, but won't block until the server sends
|
|
// a response. They return a command struct which can be used to wait for the
|
|
// server response. This can be used to execute multiple commands concurrently,
|
|
// however care must be taken to avoid ambiguities. See RFC 9051 section 5.5.
|
|
//
|
|
// A client can be safely used from multiple goroutines, however this doesn't
|
|
// guarantee any command ordering and is subject to the same caveats as command
|
|
// pipelining (see above). Additionally, some commands (e.g. StartTLS,
|
|
// Authenticate, Idle) block the client during their execution.
|
|
type Client struct {
|
|
conn net.Conn
|
|
options Options
|
|
br *bufio.Reader
|
|
bw *bufio.Writer
|
|
dec *imapwire.Decoder
|
|
encMutex sync.Mutex
|
|
|
|
greetingCh chan struct{}
|
|
greetingRecv bool
|
|
greetingErr error
|
|
|
|
decCh chan struct{}
|
|
decErr error
|
|
|
|
mutex sync.Mutex
|
|
state imap.ConnState
|
|
caps imap.CapSet
|
|
enabled imap.CapSet
|
|
pendingCapCh chan struct{}
|
|
mailbox *SelectedMailbox
|
|
cmdTag uint64
|
|
pendingCmds []command
|
|
contReqs []continuationRequest
|
|
closed bool
|
|
}
|
|
|
|
// New creates a new IMAP client.
|
|
//
|
|
// This function doesn't perform I/O.
|
|
//
|
|
// A nil options pointer is equivalent to a zero options value.
|
|
func New(conn net.Conn, options *Options) *Client {
|
|
if options == nil {
|
|
options = &Options{}
|
|
}
|
|
|
|
rw := options.wrapReadWriter(conn)
|
|
br := bufio.NewReader(rw)
|
|
bw := bufio.NewWriter(rw)
|
|
|
|
client := &Client{
|
|
conn: conn,
|
|
options: *options,
|
|
br: br,
|
|
bw: bw,
|
|
dec: imapwire.NewDecoder(br, imapwire.ConnSideClient),
|
|
greetingCh: make(chan struct{}),
|
|
decCh: make(chan struct{}),
|
|
state: imap.ConnStateNone,
|
|
enabled: make(imap.CapSet),
|
|
}
|
|
go client.read()
|
|
return client
|
|
}
|
|
|
|
// NewStartTLS creates a new IMAP client with STARTTLS.
|
|
//
|
|
// A nil options pointer is equivalent to a zero options value.
|
|
func NewStartTLS(conn net.Conn, options *Options) (*Client, error) {
|
|
if options == nil {
|
|
options = &Options{}
|
|
}
|
|
|
|
client := New(conn, options)
|
|
if err := client.startTLS(options.TLSConfig); err != nil {
|
|
conn.Close()
|
|
return nil, err
|
|
}
|
|
|
|
// Per section 7.1.4, refuse PREAUTH when using STARTTLS
|
|
if client.State() != imap.ConnStateNotAuthenticated {
|
|
client.Close()
|
|
return nil, fmt.Errorf("imapclient: server sent PREAUTH on unencrypted connection")
|
|
}
|
|
|
|
return client, nil
|
|
}
|
|
|
|
// DialInsecure connects to an IMAP server without any encryption at all.
|
|
func DialInsecure(address string, options *Options) (*Client, error) {
|
|
if options == nil {
|
|
options = &Options{}
|
|
}
|
|
|
|
conn, err := options.dialer().Dial("tcp", address)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return New(conn, options), nil
|
|
}
|
|
|
|
// DialTLS connects to an IMAP server with implicit TLS.
|
|
func DialTLS(address string, options *Options) (*Client, error) {
|
|
if options == nil {
|
|
options = &Options{}
|
|
}
|
|
|
|
tlsConfig := options.tlsConfig()
|
|
if tlsConfig.NextProtos == nil {
|
|
tlsConfig.NextProtos = []string{"imap"}
|
|
}
|
|
|
|
dialer := options.dialer()
|
|
conn, err := tls.DialWithDialer(dialer, "tcp", address, tlsConfig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return New(conn, options), nil
|
|
}
|
|
|
|
// DialStartTLS connects to an IMAP server with STARTTLS.
|
|
func DialStartTLS(address string, options *Options) (*Client, error) {
|
|
if options == nil {
|
|
options = &Options{}
|
|
}
|
|
|
|
host, _, err := net.SplitHostPort(address)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
conn, err := options.dialer().Dial("tcp", address)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tlsConfig := options.tlsConfig()
|
|
if tlsConfig.ServerName == "" {
|
|
tlsConfig.ServerName = host
|
|
}
|
|
newOptions := *options
|
|
newOptions.TLSConfig = tlsConfig
|
|
return NewStartTLS(conn, &newOptions)
|
|
}
|
|
|
|
func (c *Client) setReadTimeout(dur time.Duration) {
|
|
if dur > 0 {
|
|
c.conn.SetReadDeadline(time.Now().Add(dur))
|
|
} else {
|
|
c.conn.SetReadDeadline(time.Time{})
|
|
}
|
|
}
|
|
|
|
func (c *Client) setWriteTimeout(dur time.Duration) {
|
|
if dur > 0 {
|
|
c.conn.SetWriteDeadline(time.Now().Add(dur))
|
|
} else {
|
|
c.conn.SetWriteDeadline(time.Time{})
|
|
}
|
|
}
|
|
|
|
// State returns the current connection state of the client.
|
|
func (c *Client) State() imap.ConnState {
|
|
c.mutex.Lock()
|
|
defer c.mutex.Unlock()
|
|
return c.state
|
|
}
|
|
|
|
func (c *Client) setState(state imap.ConnState) {
|
|
c.mutex.Lock()
|
|
c.state = state
|
|
if c.state != imap.ConnStateSelected {
|
|
c.mailbox = nil
|
|
}
|
|
c.mutex.Unlock()
|
|
}
|
|
|
|
// Caps returns the capabilities advertised by the server.
|
|
//
|
|
// When the server hasn't sent the capability list, this method will request it
|
|
// and block until it's received. If the capabilities cannot be fetched, nil is
|
|
// returned.
|
|
func (c *Client) Caps() imap.CapSet {
|
|
if err := c.WaitGreeting(); err != nil {
|
|
return nil
|
|
}
|
|
|
|
c.mutex.Lock()
|
|
caps := c.caps
|
|
capCh := c.pendingCapCh
|
|
c.mutex.Unlock()
|
|
|
|
if caps != nil {
|
|
return caps
|
|
}
|
|
|
|
if capCh == nil {
|
|
capCmd := c.Capability()
|
|
capCh := make(chan struct{})
|
|
go func() {
|
|
capCmd.Wait()
|
|
close(capCh)
|
|
}()
|
|
c.mutex.Lock()
|
|
c.pendingCapCh = capCh
|
|
c.mutex.Unlock()
|
|
}
|
|
|
|
timer := time.NewTimer(respReadTimeout)
|
|
defer timer.Stop()
|
|
select {
|
|
case <-timer.C:
|
|
return nil
|
|
case <-capCh:
|
|
// ok
|
|
}
|
|
|
|
// TODO: this is racy if caps are reset before we get the reply
|
|
c.mutex.Lock()
|
|
defer c.mutex.Unlock()
|
|
return c.caps
|
|
}
|
|
|
|
func (c *Client) setCaps(caps imap.CapSet) {
|
|
// If the capabilities are being reset, request the updated capabilities
|
|
// from the server
|
|
var capCh chan struct{}
|
|
if caps == nil {
|
|
capCh = make(chan struct{})
|
|
|
|
// We need to send the CAPABILITY command in a separate goroutine:
|
|
// setCaps might be called with Client.encMutex locked
|
|
go func() {
|
|
c.Capability().Wait()
|
|
close(capCh)
|
|
}()
|
|
}
|
|
|
|
c.mutex.Lock()
|
|
c.caps = caps
|
|
c.pendingCapCh = capCh
|
|
c.mutex.Unlock()
|
|
}
|
|
|
|
// Mailbox returns the state of the currently selected mailbox.
|
|
//
|
|
// If there is no currently selected mailbox, nil is returned.
|
|
//
|
|
// The returned struct must not be mutated.
|
|
func (c *Client) Mailbox() *SelectedMailbox {
|
|
c.mutex.Lock()
|
|
defer c.mutex.Unlock()
|
|
return c.mailbox
|
|
}
|
|
|
|
// Closed returns a channel that is closed when the connection is closed.
|
|
//
|
|
// This channel cannot be used to reliably determine whether a connection is healthy. If
|
|
// the underlying connection times out, the channel will be closed eventually, but not
|
|
// immediately. To check whether the connection is healthy, send a command (such as Noop).
|
|
func (c *Client) Closed() <-chan struct{} {
|
|
return c.decCh
|
|
}
|
|
|
|
// Close immediately closes the connection.
|
|
func (c *Client) Close() error {
|
|
c.mutex.Lock()
|
|
alreadyClosed := c.closed
|
|
c.closed = true
|
|
c.mutex.Unlock()
|
|
|
|
// Ignore net.ErrClosed here, because we also call conn.Close in c.read
|
|
if err := c.conn.Close(); err != nil && !errors.Is(err, net.ErrClosed) && !errors.Is(err, io.ErrClosedPipe) {
|
|
return err
|
|
}
|
|
|
|
<-c.decCh
|
|
if err := c.decErr; err != nil {
|
|
return err
|
|
}
|
|
|
|
if alreadyClosed {
|
|
return net.ErrClosed
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// beginCommand starts sending a command to the server.
|
|
//
|
|
// The command name and a space are written.
|
|
//
|
|
// The caller must call commandEncoder.end.
|
|
func (c *Client) beginCommand(name string, cmd command) *commandEncoder {
|
|
c.encMutex.Lock() // unlocked by commandEncoder.end
|
|
|
|
c.mutex.Lock()
|
|
|
|
c.cmdTag++
|
|
tag := fmt.Sprintf("T%v", c.cmdTag)
|
|
|
|
baseCmd := cmd.base()
|
|
*baseCmd = commandBase{
|
|
tag: tag,
|
|
done: make(chan error, 1),
|
|
}
|
|
|
|
c.pendingCmds = append(c.pendingCmds, cmd)
|
|
quotedUTF8 := c.caps.Has(imap.CapIMAP4rev2) || c.enabled.Has(imap.CapUTF8Accept)
|
|
literalMinus := c.caps.Has(imap.CapLiteralMinus)
|
|
literalPlus := c.caps.Has(imap.CapLiteralPlus)
|
|
|
|
c.mutex.Unlock()
|
|
|
|
c.setWriteTimeout(cmdWriteTimeout)
|
|
|
|
wireEnc := imapwire.NewEncoder(c.bw, imapwire.ConnSideClient)
|
|
wireEnc.QuotedUTF8 = quotedUTF8
|
|
wireEnc.LiteralMinus = literalMinus
|
|
wireEnc.LiteralPlus = literalPlus
|
|
wireEnc.NewContinuationRequest = func() *imapwire.ContinuationRequest {
|
|
return c.registerContReq(cmd)
|
|
}
|
|
|
|
enc := &commandEncoder{
|
|
Encoder: wireEnc,
|
|
client: c,
|
|
cmd: baseCmd,
|
|
}
|
|
enc.Atom(tag).SP().Atom(name)
|
|
return enc
|
|
}
|
|
|
|
func (c *Client) deletePendingCmdByTag(tag string) command {
|
|
c.mutex.Lock()
|
|
defer c.mutex.Unlock()
|
|
|
|
for i, cmd := range c.pendingCmds {
|
|
if cmd.base().tag == tag {
|
|
c.pendingCmds = append(c.pendingCmds[:i], c.pendingCmds[i+1:]...)
|
|
return cmd
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) findPendingCmdFunc(f func(cmd command) bool) command {
|
|
c.mutex.Lock()
|
|
defer c.mutex.Unlock()
|
|
|
|
for _, cmd := range c.pendingCmds {
|
|
if f(cmd) {
|
|
return cmd
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func findPendingCmdByType[T command](c *Client) T {
|
|
c.mutex.Lock()
|
|
defer c.mutex.Unlock()
|
|
|
|
for _, cmd := range c.pendingCmds {
|
|
if cmd, ok := cmd.(T); ok {
|
|
return cmd
|
|
}
|
|
}
|
|
|
|
var cmd T
|
|
return cmd
|
|
}
|
|
|
|
func (c *Client) completeCommand(cmd command, err error) {
|
|
done := cmd.base().done
|
|
done <- err
|
|
close(done)
|
|
|
|
// Ensure the command is not blocked waiting on continuation requests
|
|
c.mutex.Lock()
|
|
var filtered []continuationRequest
|
|
for _, contReq := range c.contReqs {
|
|
if contReq.cmd != cmd.base() {
|
|
filtered = append(filtered, contReq)
|
|
} else {
|
|
contReq.Cancel(err)
|
|
}
|
|
}
|
|
c.contReqs = filtered
|
|
c.mutex.Unlock()
|
|
|
|
switch cmd := cmd.(type) {
|
|
case *authenticateCommand, *loginCommand:
|
|
if err == nil {
|
|
c.setState(imap.ConnStateAuthenticated)
|
|
}
|
|
case *unauthenticateCommand:
|
|
if err == nil {
|
|
c.mutex.Lock()
|
|
c.state = imap.ConnStateNotAuthenticated
|
|
c.mailbox = nil
|
|
c.enabled = make(imap.CapSet)
|
|
c.mutex.Unlock()
|
|
}
|
|
case *SelectCommand:
|
|
if err == nil {
|
|
c.mutex.Lock()
|
|
c.state = imap.ConnStateSelected
|
|
c.mailbox = &SelectedMailbox{
|
|
Name: cmd.mailbox,
|
|
NumMessages: cmd.data.NumMessages,
|
|
Flags: cmd.data.Flags,
|
|
PermanentFlags: cmd.data.PermanentFlags,
|
|
}
|
|
c.mutex.Unlock()
|
|
}
|
|
case *unselectCommand:
|
|
if err == nil {
|
|
c.setState(imap.ConnStateAuthenticated)
|
|
}
|
|
case *logoutCommand:
|
|
if err == nil {
|
|
c.setState(imap.ConnStateLogout)
|
|
}
|
|
case *ListCommand:
|
|
if cmd.pendingData != nil {
|
|
cmd.mailboxes <- cmd.pendingData
|
|
}
|
|
close(cmd.mailboxes)
|
|
case *FetchCommand:
|
|
close(cmd.msgs)
|
|
case *ExpungeCommand:
|
|
close(cmd.seqNums)
|
|
}
|
|
}
|
|
|
|
func (c *Client) registerContReq(cmd command) *imapwire.ContinuationRequest {
|
|
contReq := imapwire.NewContinuationRequest()
|
|
|
|
c.mutex.Lock()
|
|
c.contReqs = append(c.contReqs, continuationRequest{
|
|
ContinuationRequest: contReq,
|
|
cmd: cmd.base(),
|
|
})
|
|
c.mutex.Unlock()
|
|
|
|
return contReq
|
|
}
|
|
|
|
func (c *Client) closeWithError(err error) {
|
|
c.conn.Close()
|
|
|
|
c.mutex.Lock()
|
|
c.state = imap.ConnStateLogout
|
|
pendingCmds := c.pendingCmds
|
|
c.pendingCmds = nil
|
|
c.mutex.Unlock()
|
|
|
|
for _, cmd := range pendingCmds {
|
|
c.completeCommand(cmd, err)
|
|
}
|
|
}
|
|
|
|
// read continuously reads data coming from the server.
|
|
//
|
|
// All the data is decoded in the read goroutine, then dispatched via channels
|
|
// to pending commands.
|
|
func (c *Client) read() {
|
|
defer close(c.decCh)
|
|
defer func() {
|
|
if v := recover(); v != nil {
|
|
c.decErr = fmt.Errorf("imapclient: panic reading response: %v\n%s", v, debug.Stack())
|
|
}
|
|
|
|
cmdErr := c.decErr
|
|
if cmdErr == nil {
|
|
cmdErr = io.ErrUnexpectedEOF
|
|
}
|
|
c.closeWithError(cmdErr)
|
|
}()
|
|
|
|
c.setReadTimeout(respReadTimeout) // We're waiting for the greeting
|
|
for {
|
|
// Ignore net.ErrClosed here, because we also call conn.Close in c.Close
|
|
if c.dec.EOF() || errors.Is(c.dec.Err(), net.ErrClosed) || errors.Is(c.dec.Err(), io.ErrClosedPipe) {
|
|
break
|
|
}
|
|
if err := c.readResponse(); err != nil {
|
|
c.decErr = err
|
|
break
|
|
}
|
|
if c.greetingErr != nil {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Client) readResponse() error {
|
|
c.setReadTimeout(respReadTimeout)
|
|
defer c.setReadTimeout(idleReadTimeout)
|
|
|
|
if c.dec.Special('+') {
|
|
if err := c.readContinueReq(); err != nil {
|
|
return fmt.Errorf("in continue-req: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var tag, typ string
|
|
if !c.dec.Expect(c.dec.Special('*') || c.dec.Atom(&tag), "'*' or atom") {
|
|
return fmt.Errorf("in response: cannot read tag: %v", c.dec.Err())
|
|
}
|
|
if !c.dec.ExpectSP() {
|
|
return fmt.Errorf("in response: %v", c.dec.Err())
|
|
}
|
|
if !c.dec.ExpectAtom(&typ) {
|
|
return fmt.Errorf("in response: cannot read type: %v", c.dec.Err())
|
|
}
|
|
|
|
// Change typ to uppercase, as it's case-insensitive
|
|
typ = strings.ToUpper(typ)
|
|
|
|
var (
|
|
token string
|
|
err error
|
|
startTLS *startTLSCommand
|
|
)
|
|
if tag != "" {
|
|
token = "response-tagged"
|
|
startTLS, err = c.readResponseTagged(tag, typ)
|
|
} else {
|
|
token = "response-data"
|
|
err = c.readResponseData(typ)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("in %v: %v", token, err)
|
|
}
|
|
|
|
if !c.dec.ExpectCRLF() {
|
|
return fmt.Errorf("in response: %v", c.dec.Err())
|
|
}
|
|
|
|
if startTLS != nil {
|
|
c.upgradeStartTLS(startTLS)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) readContinueReq() error {
|
|
var text string
|
|
if c.dec.SP() {
|
|
c.dec.Text(&text)
|
|
}
|
|
if !c.dec.ExpectCRLF() {
|
|
return c.dec.Err()
|
|
}
|
|
|
|
var contReq *imapwire.ContinuationRequest
|
|
c.mutex.Lock()
|
|
if len(c.contReqs) > 0 {
|
|
contReq = c.contReqs[0].ContinuationRequest
|
|
c.contReqs = append(c.contReqs[:0], c.contReqs[1:]...)
|
|
}
|
|
c.mutex.Unlock()
|
|
|
|
if contReq == nil {
|
|
return fmt.Errorf("received unmatched continuation request")
|
|
}
|
|
|
|
contReq.Done(text)
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) readResponseTagged(tag, typ string) (startTLS *startTLSCommand, err error) {
|
|
cmd := c.deletePendingCmdByTag(tag)
|
|
if cmd == nil {
|
|
return nil, fmt.Errorf("received tagged response with unknown tag %q", tag)
|
|
}
|
|
|
|
// We've removed the command from the pending queue above. Make sure we
|
|
// don't stall it on error.
|
|
defer func() {
|
|
if err != nil {
|
|
c.completeCommand(cmd, err)
|
|
}
|
|
}()
|
|
|
|
// Some servers don't provide a text even if the RFC requires it,
|
|
// see #500 and #502
|
|
hasSP := c.dec.SP()
|
|
|
|
var code string
|
|
if hasSP && c.dec.Special('[') { // resp-text-code
|
|
if !c.dec.ExpectAtom(&code) {
|
|
return nil, fmt.Errorf("in resp-text-code: %v", c.dec.Err())
|
|
}
|
|
// TODO: LONGENTRIES and MAXSIZE from METADATA
|
|
switch code {
|
|
case "CAPABILITY": // capability-data
|
|
caps, err := readCapabilities(c.dec)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("in capability-data: %v", err)
|
|
}
|
|
c.setCaps(caps)
|
|
case "APPENDUID":
|
|
var (
|
|
uidValidity uint32
|
|
uid imap.UID
|
|
)
|
|
if !c.dec.ExpectSP() || !c.dec.ExpectNumber(&uidValidity) || !c.dec.ExpectSP() || !c.dec.ExpectUID(&uid) {
|
|
return nil, fmt.Errorf("in resp-code-apnd: %v", c.dec.Err())
|
|
}
|
|
if cmd, ok := cmd.(*AppendCommand); ok {
|
|
cmd.data.UID = uid
|
|
cmd.data.UIDValidity = uidValidity
|
|
}
|
|
case "COPYUID":
|
|
if !c.dec.ExpectSP() {
|
|
return nil, c.dec.Err()
|
|
}
|
|
uidValidity, srcUIDs, dstUIDs, err := readRespCodeCopyUID(c.dec)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("in resp-code-copy: %v", err)
|
|
}
|
|
switch cmd := cmd.(type) {
|
|
case *CopyCommand:
|
|
cmd.data.UIDValidity = uidValidity
|
|
cmd.data.SourceUIDs = srcUIDs
|
|
cmd.data.DestUIDs = dstUIDs
|
|
case *MoveCommand:
|
|
// This can happen when Client.Move falls back to COPY +
|
|
// STORE + EXPUNGE
|
|
cmd.data.UIDValidity = uidValidity
|
|
cmd.data.SourceUIDs = srcUIDs
|
|
cmd.data.DestUIDs = dstUIDs
|
|
}
|
|
default: // [SP 1*<any TEXT-CHAR except "]">]
|
|
if c.dec.SP() {
|
|
c.dec.DiscardUntilByte(']')
|
|
}
|
|
}
|
|
if !c.dec.ExpectSpecial(']') {
|
|
return nil, fmt.Errorf("in resp-text: %v", c.dec.Err())
|
|
}
|
|
hasSP = c.dec.SP()
|
|
}
|
|
var text string
|
|
if hasSP && !c.dec.ExpectText(&text) {
|
|
return nil, fmt.Errorf("in resp-text: %v", c.dec.Err())
|
|
}
|
|
|
|
var cmdErr error
|
|
switch typ {
|
|
case "OK":
|
|
// nothing to do
|
|
case "NO", "BAD":
|
|
cmdErr = &imap.Error{
|
|
Type: imap.StatusResponseType(typ),
|
|
Code: imap.ResponseCode(code),
|
|
Text: text,
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("in resp-cond-state: expected OK, NO or BAD status condition, but got %v", typ)
|
|
}
|
|
|
|
c.completeCommand(cmd, cmdErr)
|
|
|
|
if cmd, ok := cmd.(*startTLSCommand); ok && cmdErr == nil {
|
|
startTLS = cmd
|
|
}
|
|
|
|
if cmdErr == nil && code != "CAPABILITY" {
|
|
switch cmd.(type) {
|
|
case *startTLSCommand, *loginCommand, *authenticateCommand, *unauthenticateCommand:
|
|
// These commands invalidate the capabilities
|
|
c.setCaps(nil)
|
|
}
|
|
}
|
|
|
|
return startTLS, nil
|
|
}
|
|
|
|
func (c *Client) readResponseData(typ string) error {
|
|
// number SP ("EXISTS" / "RECENT" / "FETCH" / "EXPUNGE")
|
|
var num uint32
|
|
if typ[0] >= '0' && typ[0] <= '9' {
|
|
v, err := strconv.ParseUint(typ, 10, 32)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
num = uint32(v)
|
|
if !c.dec.ExpectSP() || !c.dec.ExpectAtom(&typ) {
|
|
return c.dec.Err()
|
|
}
|
|
}
|
|
|
|
// All response type are case insensitive
|
|
switch strings.ToUpper(typ) {
|
|
case "OK", "PREAUTH", "NO", "BAD", "BYE": // resp-cond-state / resp-cond-bye / resp-cond-auth
|
|
// Some servers don't provide a text even if the RFC requires it,
|
|
// see #500 and #502
|
|
hasSP := c.dec.SP()
|
|
|
|
var code string
|
|
if hasSP && c.dec.Special('[') { // resp-text-code
|
|
if !c.dec.ExpectAtom(&code) {
|
|
return fmt.Errorf("in resp-text-code: %v", c.dec.Err())
|
|
}
|
|
switch code {
|
|
case "CAPABILITY": // capability-data
|
|
caps, err := readCapabilities(c.dec)
|
|
if err != nil {
|
|
return fmt.Errorf("in capability-data: %v", err)
|
|
}
|
|
c.setCaps(caps)
|
|
case "PERMANENTFLAGS":
|
|
if !c.dec.ExpectSP() {
|
|
return c.dec.Err()
|
|
}
|
|
flags, err := internal.ExpectFlagList(c.dec)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
c.mutex.Lock()
|
|
if c.state == imap.ConnStateSelected {
|
|
c.mailbox = c.mailbox.copy()
|
|
c.mailbox.PermanentFlags = flags
|
|
}
|
|
c.mutex.Unlock()
|
|
|
|
if cmd := findPendingCmdByType[*SelectCommand](c); cmd != nil {
|
|
cmd.data.PermanentFlags = flags
|
|
} else if handler := c.options.unilateralDataHandler().Mailbox; handler != nil {
|
|
handler(&UnilateralDataMailbox{PermanentFlags: flags})
|
|
}
|
|
case "UIDNEXT":
|
|
var uidNext imap.UID
|
|
if !c.dec.ExpectSP() || !c.dec.ExpectUID(&uidNext) {
|
|
return c.dec.Err()
|
|
}
|
|
if cmd := findPendingCmdByType[*SelectCommand](c); cmd != nil {
|
|
cmd.data.UIDNext = uidNext
|
|
}
|
|
case "UIDVALIDITY":
|
|
var uidValidity uint32
|
|
if !c.dec.ExpectSP() || !c.dec.ExpectNumber(&uidValidity) {
|
|
return c.dec.Err()
|
|
}
|
|
if cmd := findPendingCmdByType[*SelectCommand](c); cmd != nil {
|
|
cmd.data.UIDValidity = uidValidity
|
|
}
|
|
case "COPYUID":
|
|
if !c.dec.ExpectSP() {
|
|
return c.dec.Err()
|
|
}
|
|
uidValidity, srcUIDs, dstUIDs, err := readRespCodeCopyUID(c.dec)
|
|
if err != nil {
|
|
return fmt.Errorf("in resp-code-copy: %v", err)
|
|
}
|
|
if cmd := findPendingCmdByType[*MoveCommand](c); cmd != nil {
|
|
cmd.data.UIDValidity = uidValidity
|
|
cmd.data.SourceUIDs = srcUIDs
|
|
cmd.data.DestUIDs = dstUIDs
|
|
}
|
|
case "HIGHESTMODSEQ":
|
|
var modSeq uint64
|
|
if !c.dec.ExpectSP() || !c.dec.ExpectModSeq(&modSeq) {
|
|
return c.dec.Err()
|
|
}
|
|
if cmd := findPendingCmdByType[*SelectCommand](c); cmd != nil {
|
|
cmd.data.HighestModSeq = modSeq
|
|
}
|
|
case "NOMODSEQ":
|
|
// ignore
|
|
default: // [SP 1*<any TEXT-CHAR except "]">]
|
|
if c.dec.SP() {
|
|
c.dec.DiscardUntilByte(']')
|
|
}
|
|
}
|
|
if !c.dec.ExpectSpecial(']') {
|
|
return fmt.Errorf("in resp-text: %v", c.dec.Err())
|
|
}
|
|
hasSP = c.dec.SP()
|
|
}
|
|
|
|
var text string
|
|
if hasSP && !c.dec.ExpectText(&text) {
|
|
return fmt.Errorf("in resp-text: %v", c.dec.Err())
|
|
}
|
|
|
|
if code == "CLOSED" {
|
|
c.setState(imap.ConnStateAuthenticated)
|
|
}
|
|
|
|
if !c.greetingRecv {
|
|
switch typ {
|
|
case "OK":
|
|
c.setState(imap.ConnStateNotAuthenticated)
|
|
case "PREAUTH":
|
|
c.setState(imap.ConnStateAuthenticated)
|
|
default:
|
|
c.setState(imap.ConnStateLogout)
|
|
c.greetingErr = &imap.Error{
|
|
Type: imap.StatusResponseType(typ),
|
|
Code: imap.ResponseCode(code),
|
|
Text: text,
|
|
}
|
|
}
|
|
c.greetingRecv = true
|
|
if c.greetingErr == nil && code != "CAPABILITY" {
|
|
c.setCaps(nil) // request initial capabilities
|
|
}
|
|
close(c.greetingCh)
|
|
}
|
|
case "ID":
|
|
return c.handleID()
|
|
case "CAPABILITY":
|
|
return c.handleCapability()
|
|
case "ENABLED":
|
|
return c.handleEnabled()
|
|
case "NAMESPACE":
|
|
if !c.dec.ExpectSP() {
|
|
return c.dec.Err()
|
|
}
|
|
return c.handleNamespace()
|
|
case "FLAGS":
|
|
if !c.dec.ExpectSP() {
|
|
return c.dec.Err()
|
|
}
|
|
return c.handleFlags()
|
|
case "EXISTS":
|
|
return c.handleExists(num)
|
|
case "RECENT":
|
|
// ignore
|
|
case "LIST":
|
|
if !c.dec.ExpectSP() {
|
|
return c.dec.Err()
|
|
}
|
|
return c.handleList()
|
|
case "STATUS":
|
|
if !c.dec.ExpectSP() {
|
|
return c.dec.Err()
|
|
}
|
|
return c.handleStatus()
|
|
case "FETCH":
|
|
if !c.dec.ExpectSP() {
|
|
return c.dec.Err()
|
|
}
|
|
return c.handleFetch(num)
|
|
case "EXPUNGE":
|
|
return c.handleExpunge(num)
|
|
case "SEARCH":
|
|
return c.handleSearch()
|
|
case "ESEARCH":
|
|
return c.handleESearch()
|
|
case "SORT":
|
|
return c.handleSort()
|
|
case "THREAD":
|
|
return c.handleThread()
|
|
case "METADATA":
|
|
if !c.dec.ExpectSP() {
|
|
return c.dec.Err()
|
|
}
|
|
return c.handleMetadata()
|
|
case "QUOTA":
|
|
if !c.dec.ExpectSP() {
|
|
return c.dec.Err()
|
|
}
|
|
return c.handleQuota()
|
|
case "QUOTAROOT":
|
|
if !c.dec.ExpectSP() {
|
|
return c.dec.Err()
|
|
}
|
|
return c.handleQuotaRoot()
|
|
case "MYRIGHTS":
|
|
if !c.dec.ExpectSP() {
|
|
return c.dec.Err()
|
|
}
|
|
return c.handleMyRights()
|
|
case "ACL":
|
|
if !c.dec.ExpectSP() {
|
|
return c.dec.Err()
|
|
}
|
|
return c.handleGetACL()
|
|
default:
|
|
return fmt.Errorf("unsupported response type %q", typ)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// WaitGreeting waits for the server's initial greeting.
|
|
func (c *Client) WaitGreeting() error {
|
|
select {
|
|
case <-c.greetingCh:
|
|
return c.greetingErr
|
|
case <-c.decCh:
|
|
if c.decErr != nil {
|
|
return fmt.Errorf("got error before greeting: %v", c.decErr)
|
|
}
|
|
return fmt.Errorf("connection closed before greeting")
|
|
}
|
|
}
|
|
|
|
// Noop sends a NOOP command.
|
|
func (c *Client) Noop() *Command {
|
|
cmd := &Command{}
|
|
c.beginCommand("NOOP", cmd).end()
|
|
return cmd
|
|
}
|
|
|
|
// Logout sends a LOGOUT command.
|
|
//
|
|
// This command informs the server that the client is done with the connection.
|
|
func (c *Client) Logout() *Command {
|
|
cmd := &logoutCommand{}
|
|
c.beginCommand("LOGOUT", cmd).end()
|
|
return &cmd.Command
|
|
}
|
|
|
|
// Login sends a LOGIN command.
|
|
func (c *Client) Login(username, password string) *Command {
|
|
cmd := &loginCommand{}
|
|
enc := c.beginCommand("LOGIN", cmd)
|
|
enc.SP().String(username).SP().String(password)
|
|
enc.end()
|
|
return &cmd.Command
|
|
}
|
|
|
|
// Delete sends a DELETE command.
|
|
func (c *Client) Delete(mailbox string) *Command {
|
|
cmd := &Command{}
|
|
enc := c.beginCommand("DELETE", cmd)
|
|
enc.SP().Mailbox(mailbox)
|
|
enc.end()
|
|
return cmd
|
|
}
|
|
|
|
// Rename sends a RENAME command.
|
|
//
|
|
// A nil options pointer is equivalent to a zero options value.
|
|
func (c *Client) Rename(mailbox, newName string, options *imap.RenameOptions) *Command {
|
|
cmd := &Command{}
|
|
enc := c.beginCommand("RENAME", cmd)
|
|
enc.SP().Mailbox(mailbox).SP().Mailbox(newName)
|
|
enc.end()
|
|
return cmd
|
|
}
|
|
|
|
// Subscribe sends a SUBSCRIBE command.
|
|
func (c *Client) Subscribe(mailbox string) *Command {
|
|
cmd := &Command{}
|
|
enc := c.beginCommand("SUBSCRIBE", cmd)
|
|
enc.SP().Mailbox(mailbox)
|
|
enc.end()
|
|
return cmd
|
|
}
|
|
|
|
// Unsubscribe sends an UNSUBSCRIBE command.
|
|
func (c *Client) Unsubscribe(mailbox string) *Command {
|
|
cmd := &Command{}
|
|
enc := c.beginCommand("UNSUBSCRIBE", cmd)
|
|
enc.SP().Mailbox(mailbox)
|
|
enc.end()
|
|
return cmd
|
|
}
|
|
|
|
func uidCmdName(name string, kind imapwire.NumKind) string {
|
|
switch kind {
|
|
case imapwire.NumKindSeq:
|
|
return name
|
|
case imapwire.NumKindUID:
|
|
return "UID " + name
|
|
default:
|
|
panic("imapclient: invalid imapwire.NumKind")
|
|
}
|
|
}
|
|
|
|
type commandEncoder struct {
|
|
*imapwire.Encoder
|
|
client *Client
|
|
cmd *commandBase
|
|
}
|
|
|
|
// end ends an outgoing command.
|
|
//
|
|
// A CRLF is written, the encoder is flushed and its lock is released.
|
|
func (ce *commandEncoder) end() {
|
|
if ce.Encoder != nil {
|
|
ce.flush()
|
|
}
|
|
ce.client.setWriteTimeout(0)
|
|
ce.client.encMutex.Unlock()
|
|
}
|
|
|
|
// flush sends an outgoing command, but keeps the encoder lock.
|
|
//
|
|
// A CRLF is written and the encoder is flushed. Callers must call
|
|
// commandEncoder.end to release the lock.
|
|
func (ce *commandEncoder) flush() {
|
|
if err := ce.Encoder.CRLF(); err != nil {
|
|
// TODO: consider stashing the error in Client to return it in future
|
|
// calls
|
|
ce.client.closeWithError(err)
|
|
}
|
|
ce.Encoder = nil
|
|
}
|
|
|
|
// Literal encodes a literal.
|
|
func (ce *commandEncoder) Literal(size int64) io.WriteCloser {
|
|
var contReq *imapwire.ContinuationRequest
|
|
ce.client.mutex.Lock()
|
|
hasCapLiteralMinus := ce.client.caps.Has(imap.CapLiteralMinus)
|
|
ce.client.mutex.Unlock()
|
|
if size > 4096 || !hasCapLiteralMinus {
|
|
contReq = ce.client.registerContReq(ce.cmd)
|
|
}
|
|
ce.client.setWriteTimeout(literalWriteTimeout)
|
|
return literalWriter{
|
|
WriteCloser: ce.Encoder.Literal(size, contReq),
|
|
client: ce.client,
|
|
}
|
|
}
|
|
|
|
type literalWriter struct {
|
|
io.WriteCloser
|
|
client *Client
|
|
}
|
|
|
|
func (lw literalWriter) Close() error {
|
|
lw.client.setWriteTimeout(cmdWriteTimeout)
|
|
return lw.WriteCloser.Close()
|
|
}
|
|
|
|
// continuationRequest is a pending continuation request.
|
|
type continuationRequest struct {
|
|
*imapwire.ContinuationRequest
|
|
cmd *commandBase
|
|
}
|
|
|
|
// UnilateralDataMailbox describes a mailbox status update.
|
|
//
|
|
// If a field is nil, it hasn't changed.
|
|
type UnilateralDataMailbox struct {
|
|
NumMessages *uint32
|
|
Flags []imap.Flag
|
|
PermanentFlags []imap.Flag
|
|
}
|
|
|
|
// UnilateralDataHandler handles unilateral data.
|
|
//
|
|
// The handler will block the client while running. If the caller intends to
|
|
// perform slow operations, a buffered channel and a separate goroutine should
|
|
// be used.
|
|
//
|
|
// The handler will be invoked in an arbitrary goroutine.
|
|
//
|
|
// See Options.UnilateralDataHandler.
|
|
type UnilateralDataHandler struct {
|
|
Expunge func(seqNum uint32)
|
|
Mailbox func(data *UnilateralDataMailbox)
|
|
Fetch func(msg *FetchMessageData)
|
|
|
|
// requires ENABLE METADATA or ENABLE SERVER-METADATA
|
|
Metadata func(mailbox string, entries []string)
|
|
}
|
|
|
|
// command is an interface for IMAP commands.
|
|
//
|
|
// Commands are represented by the Command type, but can be extended by other
|
|
// types (e.g. CapabilityCommand).
|
|
type command interface {
|
|
base() *commandBase
|
|
}
|
|
|
|
type commandBase struct {
|
|
tag string
|
|
done chan error
|
|
err error
|
|
}
|
|
|
|
func (cmd *commandBase) base() *commandBase {
|
|
return cmd
|
|
}
|
|
|
|
func (cmd *commandBase) wait() error {
|
|
if cmd.err == nil {
|
|
cmd.err = <-cmd.done
|
|
}
|
|
return cmd.err
|
|
}
|
|
|
|
// Command is a basic IMAP command.
|
|
type Command struct {
|
|
commandBase
|
|
}
|
|
|
|
// Wait blocks until the command has completed.
|
|
func (cmd *Command) Wait() error {
|
|
return cmd.wait()
|
|
}
|
|
|
|
type loginCommand struct {
|
|
Command
|
|
}
|
|
|
|
// logoutCommand is a LOGOUT command.
|
|
type logoutCommand struct {
|
|
Command
|
|
}
|