Files
go-imap/imapclient/client.go
T
2023-03-17 20:12:52 +01:00

1202 lines
26 KiB
Go

package imapclient
import (
"bufio"
"crypto/tls"
"fmt"
"io"
"mime"
"net"
"runtime/debug"
"strconv"
"sync"
"time"
"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/internal/imapwire"
)
const dateTimeLayout = "_2-Jan-2006 15:04:05 -0700"
const (
idleReadTimeout = time.Duration(0)
respReadTimeout = 30 * time.Second
literalReadTimeout = 5 * time.Minute
respWriteTimeout = 30 * time.Second
literalWriteTimeout = 5 * time.Minute
)
// State describes the client state.
//
// See RFC 9051 section 3.
type State int
const (
StateNone State = iota
StateNotAuthenticated
StateAuthenticated
StateSelected
StateLogout
)
// String implements fmt.Stringer.
func (state State) String() string {
switch state {
case StateNone:
return "none"
case StateNotAuthenticated:
return "not authenticated"
case StateAuthenticated:
return "authenticated"
case StateSelected:
return "selected"
case StateLogout:
return "logout"
default:
panic(fmt.Errorf("imapclient: unknown state %v", int(state)))
}
}
// 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 {
// Raw ingress and egress data will be written to this writer, if any
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
}
// 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.
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 State
caps imap.CapSet
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),
greetingCh: make(chan struct{}),
decCh: make(chan struct{}),
state: StateNone,
}
go client.read()
return client
}
// DialTLS connects to an IMAP server with implicit TLS.
func DialTLS(address string, options *Options) (*Client, error) {
conn, err := tls.Dial("tcp", address, &tls.Config{
NextProtos: []string{"imap"},
})
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) {
host, _, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
conn, err := net.Dial("tcp", address)
if err != nil {
return nil, err
}
client := New(conn, options)
if err := client.StartTLS(&tls.Config{ServerName: host}); err != nil {
conn.Close()
return nil, err
}
return client, err
}
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 state of the client.
func (c *Client) State() State {
c.mutex.Lock()
defer c.mutex.Unlock()
return c.state
}
func (c *Client) setState(state State) {
c.mutex.Lock()
c.state = state
if c.state != StateSelected {
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
c.mutex.Unlock()
if caps != nil {
return caps
}
caps, _ = c.Capability().Wait()
return caps
}
func (c *Client) setCaps(caps imap.CapSet) {
c.mutex.Lock()
c.caps = caps
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 && err != net.ErrClosed {
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)
c.pendingCmds = append(c.pendingCmds, cmd)
c.mutex.Unlock()
c.setWriteTimeout(respWriteTimeout)
baseCmd := cmd.base()
*baseCmd = Command{
tag: tag,
done: make(chan error, 1),
}
enc := &commandEncoder{
Encoder: imapwire.NewEncoder(c.bw),
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(StateAuthenticated)
}
case *SelectCommand:
if err == nil {
c.mutex.Lock()
c.state = StateSelected
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(StateAuthenticated)
}
case *logoutCommand:
if err == nil {
c.setState(StateLogout)
}
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) unregisterContReq(contReq *imapwire.ContinuationRequest) {
c.mutex.Lock()
for i := range c.contReqs {
if c.contReqs[i].ContinuationRequest == contReq {
c.contReqs = append(c.contReqs[:i], c.contReqs[i+1:]...)
break
}
}
c.mutex.Unlock()
}
// 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())
}
c.conn.Close()
c.mutex.Lock()
c.state = StateLogout
pendingCmds := c.pendingCmds
c.pendingCmds = nil
c.mutex.Unlock()
cmdErr := c.decErr
if cmdErr == nil {
cmdErr = io.ErrUnexpectedEOF
}
for _, cmd := range pendingCmds {
c.completeCommand(cmd, cmdErr)
}
}()
c.setReadTimeout(idleReadTimeout)
for {
if c.dec.EOF() {
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())
}
var (
token string
err error
startTLS *startTLSCommand
)
if tag != "" {
token = "response-tagged"
startTLS, err = c.readResponseTagged(tag, typ)
} else if typ == "BYE" {
token = "resp-cond-bye"
var text string
if !c.dec.ExpectText(&text) {
return fmt.Errorf("in resp-text: %v", c.dec.Err())
}
} 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.tlsConfig)
close(startTLS.upgradeDone)
}
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) (*startTLSCommand, error) {
cmd := c.deletePendingCmdByTag(tag)
if cmd == nil {
return nil, fmt.Errorf("received tagged response with unknown tag %q", tag)
}
if !c.dec.ExpectSP() {
return nil, c.dec.Err()
}
var code string
if 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, uid uint32
if !c.dec.ExpectSP() || !c.dec.ExpectNumber(&uidValidity) || !c.dec.ExpectSP() || !c.dec.ExpectNumber(&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 := readRespCodeCopy(c.dec)
if err != nil {
return nil, fmt.Errorf("in resp-code-copy: %v", err)
}
if cmd, ok := cmd.(*CopyCommand); ok {
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(']') || !c.dec.ExpectSP() {
return nil, fmt.Errorf("in resp-text: %v", c.dec.Err())
}
}
var text string
if !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)
var startTLS *startTLSCommand
if cmd, ok := cmd.(*startTLSCommand); ok && cmdErr == nil {
startTLS = cmd
}
if cmdErr == nil && code != "CAPABILITY" {
switch cmd.(type) {
case *startTLSCommand, *loginCommand, *authenticateCommand:
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()
}
}
switch typ {
case "OK", "PREAUTH", "NO", "BAD", "BYE": // resp-cond-state / resp-cond-bye / resp-cond-auth
if !c.dec.ExpectSP() {
return c.dec.Err()
}
var code string
if 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 := readFlagList(c.dec)
if err != nil {
return err
}
c.mutex.Lock()
if c.state == StateSelected {
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":
if !c.dec.ExpectSP() {
return c.dec.Err()
}
var uidNext uint32
if !c.dec.ExpectNumber(&uidNext) {
return c.dec.Err()
}
if cmd := findPendingCmdByType[*SelectCommand](c); cmd != nil {
cmd.data.UIDNext = uidNext
}
case "UIDVALIDITY":
if !c.dec.ExpectSP() {
return c.dec.Err()
}
var uidValidity uint32
if !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 := readRespCodeCopy(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
}
default: // [SP 1*<any TEXT-CHAR except "]">]
if c.dec.SP() {
c.dec.DiscardUntilByte(']')
}
}
if !c.dec.ExpectSpecial(']') || !c.dec.ExpectSP() {
return fmt.Errorf("in resp-text: %v", c.dec.Err())
}
}
var text string
if !c.dec.ExpectText(&text) {
return fmt.Errorf("in resp-text: %v", c.dec.Err())
}
if code == "CLOSED" {
c.setState(StateAuthenticated)
}
if !c.greetingRecv {
switch typ {
case "OK":
c.setState(StateNotAuthenticated)
case "PREAUTH":
c.setState(StateAuthenticated)
default:
c.setState(StateLogout)
c.greetingErr = &imap.Error{
Type: imap.StatusResponseType(typ),
Code: imap.ResponseCode(code),
Text: text,
}
}
c.greetingRecv = true
close(c.greetingCh)
}
case "CAPABILITY": // capability-data
caps, err := readCapabilities(c.dec)
if err != nil {
return err
}
c.setCaps(caps)
if cmd := findPendingCmdByType[*CapabilityCommand](c); cmd != nil {
cmd.caps = caps
}
case "ENABLED":
caps, err := readCapabilities(c.dec)
if err != nil {
return err
}
if cmd := findPendingCmdByType[*EnableCommand](c); cmd != nil {
cmd.data.Caps = caps
}
case "NAMESPACE":
if !c.dec.ExpectSP() {
return c.dec.Err()
}
data, err := readNamespaceResponse(c.dec)
if err != nil {
return fmt.Errorf("in namespace-response: %v", err)
}
if cmd := findPendingCmdByType[*NamespaceCommand](c); cmd != nil {
cmd.data = *data
}
case "FLAGS":
if !c.dec.ExpectSP() {
return c.dec.Err()
}
flags, err := readFlagList(c.dec)
if err != nil {
return err
}
c.mutex.Lock()
if c.state == StateSelected {
c.mailbox = c.mailbox.copy()
c.mailbox.PermanentFlags = flags
}
c.mutex.Unlock()
cmd := findPendingCmdByType[*SelectCommand](c)
if cmd != nil {
cmd.data.Flags = flags
} else if handler := c.options.unilateralDataHandler().Mailbox; handler != nil {
handler(&UnilateralDataMailbox{Flags: flags})
}
case "EXISTS":
cmd := findPendingCmdByType[*SelectCommand](c)
if cmd != nil {
cmd.data.NumMessages = num
} else {
c.mutex.Lock()
if c.state == StateSelected {
c.mailbox = c.mailbox.copy()
c.mailbox.NumMessages = num
}
c.mutex.Unlock()
if handler := c.options.unilateralDataHandler().Mailbox; handler != nil {
handler(&UnilateralDataMailbox{NumMessages: &num})
}
}
case "RECENT":
// ignore
case "LIST":
if !c.dec.ExpectSP() {
return c.dec.Err()
}
data, err := readList(c.dec)
if err != nil {
return fmt.Errorf("in LIST: %v", err)
}
cmd := c.findPendingCmdFunc(func(cmd command) bool {
switch cmd := cmd.(type) {
case *ListCommand:
return true // TODO: match pattern, check if already handled
case *SelectCommand:
return cmd.mailbox == data.Mailbox && cmd.data.List == nil
default:
return false
}
})
switch cmd := cmd.(type) {
case *ListCommand:
if cmd.returnStatus {
if cmd.pendingData != nil {
cmd.mailboxes <- cmd.pendingData
}
cmd.pendingData = data
} else {
cmd.mailboxes <- data
}
case *SelectCommand:
cmd.data.List = data
}
case "STATUS":
if !c.dec.ExpectSP() {
return c.dec.Err()
}
data, err := readStatus(c.dec)
if err != nil {
return fmt.Errorf("in status: %v", err)
}
cmd := c.findPendingCmdFunc(func(cmd command) bool {
switch cmd := cmd.(type) {
case *StatusCommand:
return cmd.mailbox == data.Mailbox
case *ListCommand:
return cmd.returnStatus && cmd.pendingData != nil && cmd.pendingData.Mailbox == data.Mailbox
default:
return false
}
})
switch cmd := cmd.(type) {
case *StatusCommand:
cmd.data = *data
case *ListCommand:
cmd.pendingData.Status = data
cmd.mailboxes <- cmd.pendingData
cmd.pendingData = nil
}
case "FETCH":
if !c.dec.ExpectSP() {
return c.dec.Err()
}
if err := readMsgAtt(c, num); err != nil {
return fmt.Errorf("in msg-att: %v", err)
}
case "EXPUNGE":
c.mutex.Lock()
if c.state == StateSelected && c.mailbox.NumMessages > 0 {
c.mailbox = c.mailbox.copy()
c.mailbox.NumMessages--
}
c.mutex.Unlock()
cmd := findPendingCmdByType[*ExpungeCommand](c)
if cmd != nil {
cmd.seqNums <- num
} else if handler := c.options.unilateralDataHandler().Expunge; handler != nil {
handler(num)
}
case "SEARCH":
// TODO: handle ESEARCH
cmd := findPendingCmdByType[*SearchCommand](c)
for c.dec.SP() {
var num uint32
if !c.dec.ExpectNumber(&num) {
return c.dec.Err()
}
if cmd != nil {
cmd.data.All.AddNum(num)
}
}
case "METADATA":
if !c.dec.ExpectSP() {
return c.dec.Err()
}
data, err := readMetadataResp(c.dec)
if err != nil {
return fmt.Errorf("in metadata-resp: %v", err)
}
cmd := c.findPendingCmdFunc(func(anyCmd command) bool {
cmd, ok := anyCmd.(*GetMetadataCommand)
return ok && cmd.mailbox == data.Mailbox
})
if cmd != nil {
cmd := cmd.(*GetMetadataCommand)
cmd.data = *data
}
default:
return fmt.Errorf("unsupported response type %q", typ)
}
return nil
}
// WaitGreeting waits for the server's initial greeting.
func (c *Client) WaitGreeting() error {
<-c.greetingCh
return c.greetingErr
}
// 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.cmd
}
// 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.cmd
}
// Create sends a CREATE command.
func (c *Client) Create(mailbox string) *Command {
cmd := &Command{}
enc := c.beginCommand("CREATE", cmd)
enc.SP().Mailbox(mailbox)
enc.end()
return cmd
}
// 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, uid bool) string {
if uid {
return "UID " + name
} else {
return name
}
}
type commandEncoder struct {
*imapwire.Encoder
client *Client
cmd *Command
}
// 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 {
ce.cmd.err = err
}
ce.Encoder = nil
}
// Literal encodes a literal.
func (ce *commandEncoder) Literal(size int64) io.WriteCloser {
var contReq *imapwire.ContinuationRequest
if size > 4096 || !ce.client.Caps().Has(imap.CapLiteralMinus) {
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(respWriteTimeout)
return lw.WriteCloser.Close()
}
// continuationRequest is a pending continuation request.
type continuationRequest struct {
*imapwire.ContinuationRequest
cmd *Command
}
// 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)
}
// 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() *Command
}
// Command is a basic IMAP command.
type Command struct {
tag string
done chan error
err error
}
func (cmd *Command) base() *Command {
return cmd
}
// Wait blocks until the command has completed.
func (cmd *Command) Wait() error {
if cmd.err == nil {
cmd.err = <-cmd.done
}
return cmd.err
}
type cmd = Command // type alias to avoid exporting anonymous struct fields
type loginCommand struct {
cmd
}
// logoutCommand is a LOGOUT command.
type logoutCommand struct {
cmd
}
func readFlagList(dec *imapwire.Decoder) ([]imap.Flag, error) {
var flags []imap.Flag
err := dec.ExpectList(func() error {
flag, err := readFlag(dec)
if err != nil {
return err
}
flags = append(flags, imap.Flag(flag))
return nil
})
return flags, err
}
func readFlag(dec *imapwire.Decoder) (string, error) {
isSystem := dec.Special('\\')
if isSystem && dec.Special('*') {
return "\\*", nil // flag-perm
}
var name string
if !dec.ExpectAtom(&name) {
return "", fmt.Errorf("in flag: %v", dec.Err())
}
if isSystem {
name = "\\" + name
}
return name, nil
}