Files
go-imap/imapclient/client.go
T
Zhi QuandGitHub 1ae44445f4 Support IMAP server response token case-insensitively (#656)
I saw some IMAP server are returning tags in different cases. For
example:

```
a select INBOX
* 2 EXISTS
* 2 Recent
* FLAGS (\Deleted \Seen \Flagged \Answered $Forwarded)
* OK [UIDVALIDITY 1690369825] UIDs valid
* OK [PERMANENTFLAGS (\Deleted \Seen \Flagged \Answered $Forwarded)] Permanent flags
a OK [READ-WRITE] SELECT completed
```

Note the `* 2 Recent` line. The `Recent` keyword is not all upper-cased.

In RFC-9051 IMAP protocol, it seems like client should parse server tags
without case-sensitivity.

> Unless otherwise noted, all alphabetic characters are case
insensitive. The use of uppercase or lowercase characters to define
token strings is for editorial clarity only. Implementations MUST accept
these strings in a case-insensitive fashion.

https://www.rfc-editor.org/rfc/rfc9051.html#section-9

This PR will start accepting such tags.
2024-12-10 23:02:06 +01:00

1216 lines
28 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
)
var dialer = &net.Dialer{
Timeout: 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 &copy
}
// 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
}
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 != nil && options.TLSConfig != nil {
return options.TLSConfig.Clone()
} else {
return new(tls.Config)
}
}
// 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) {
conn, err := net.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) {
tlsConfig := options.tlsConfig()
if tlsConfig.NextProtos == nil {
tlsConfig.NextProtos = []string{"imap"}
}
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 := 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
}
// 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.
func (c *Client) Rename(mailbox, newName string) *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
}
// Subscribe 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
}