aboutsummaryrefslogtreecommitdiff
path: root/vendor/github.com/emersion/go-imap/client/client.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/emersion/go-imap/client/client.go')
-rw-r--r--vendor/github.com/emersion/go-imap/client/client.go689
1 files changed, 689 insertions, 0 deletions
diff --git a/vendor/github.com/emersion/go-imap/client/client.go b/vendor/github.com/emersion/go-imap/client/client.go
new file mode 100644
index 0000000..8b6fc84
--- /dev/null
+++ b/vendor/github.com/emersion/go-imap/client/client.go
@@ -0,0 +1,689 @@
+// Package client provides an IMAP client.
+//
+// It is not safe to use the same Client from multiple goroutines. In general,
+// the IMAP protocol doesn't make it possible to send multiple independent
+// IMAP commands on the same connection.
+package client
+
+import (
+ "crypto/tls"
+ "fmt"
+ "io"
+ "log"
+ "net"
+ "os"
+ "sync"
+ "time"
+
+ "github.com/emersion/go-imap"
+ "github.com/emersion/go-imap/commands"
+ "github.com/emersion/go-imap/responses"
+)
+
+// errClosed is used when a connection is closed while waiting for a command
+// response.
+var errClosed = fmt.Errorf("imap: connection closed")
+
+// errUnregisterHandler is returned by a response handler to unregister itself.
+var errUnregisterHandler = fmt.Errorf("imap: unregister handler")
+
+// Update is an unilateral server update.
+type Update interface {
+ update()
+}
+
+// StatusUpdate is delivered when a status update is received.
+type StatusUpdate struct {
+ Status *imap.StatusResp
+}
+
+func (u *StatusUpdate) update() {}
+
+// MailboxUpdate is delivered when a mailbox status changes.
+type MailboxUpdate struct {
+ Mailbox *imap.MailboxStatus
+}
+
+func (u *MailboxUpdate) update() {}
+
+// ExpungeUpdate is delivered when a message is deleted.
+type ExpungeUpdate struct {
+ SeqNum uint32
+}
+
+func (u *ExpungeUpdate) update() {}
+
+// MessageUpdate is delivered when a message attribute changes.
+type MessageUpdate struct {
+ Message *imap.Message
+}
+
+func (u *MessageUpdate) update() {}
+
+// Client is an IMAP client.
+type Client struct {
+ conn *imap.Conn
+ isTLS bool
+ serverName string
+
+ loggedOut chan struct{}
+ continues chan<- bool
+ upgrading bool
+
+ handlers []responses.Handler
+ handlersLocker sync.Mutex
+
+ // The current connection state.
+ state imap.ConnState
+ // The selected mailbox, if there is one.
+ mailbox *imap.MailboxStatus
+ // The cached server capabilities.
+ caps map[string]bool
+ // state, mailbox and caps may be accessed in different goroutines. Protect
+ // access.
+ locker sync.Mutex
+
+ // A channel to which unilateral updates from the server will be sent. An
+ // update can be one of: *StatusUpdate, *MailboxUpdate, *MessageUpdate,
+ // *ExpungeUpdate. Note that blocking this channel blocks the whole client,
+ // so it's recommended to use a separate goroutine and a buffered channel to
+ // prevent deadlocks.
+ Updates chan<- Update
+
+ // ErrorLog specifies an optional logger for errors accepting connections and
+ // unexpected behavior from handlers. By default, logging goes to os.Stderr
+ // via the log package's standard logger. The logger must be safe to use
+ // simultaneously from multiple goroutines.
+ ErrorLog imap.Logger
+
+ // Timeout specifies a maximum amount of time to wait on a command.
+ //
+ // A Timeout of zero means no timeout. This is the default.
+ Timeout time.Duration
+}
+
+func (c *Client) registerHandler(h responses.Handler) {
+ if h == nil {
+ return
+ }
+
+ c.handlersLocker.Lock()
+ c.handlers = append(c.handlers, h)
+ c.handlersLocker.Unlock()
+}
+
+func (c *Client) handle(resp imap.Resp) error {
+ c.handlersLocker.Lock()
+ for i := len(c.handlers) - 1; i >= 0; i-- {
+ if err := c.handlers[i].Handle(resp); err != responses.ErrUnhandled {
+ if err == errUnregisterHandler {
+ c.handlers = append(c.handlers[:i], c.handlers[i+1:]...)
+ err = nil
+ }
+ c.handlersLocker.Unlock()
+ return err
+ }
+ }
+ c.handlersLocker.Unlock()
+ return responses.ErrUnhandled
+}
+
+func (c *Client) reader() {
+ defer close(c.loggedOut)
+ // Loop while connected.
+ for {
+ connected, err := c.readOnce()
+ if err != nil {
+ c.ErrorLog.Println("error reading response:", err)
+ }
+ if !connected {
+ return
+ }
+ }
+}
+
+func (c *Client) readOnce() (bool, error) {
+ if c.State() == imap.LogoutState {
+ return false, nil
+ }
+
+ resp, err := imap.ReadResp(c.conn.Reader)
+ if err == io.EOF || c.State() == imap.LogoutState {
+ return false, nil
+ } else if err != nil {
+ if imap.IsParseError(err) {
+ return true, err
+ } else {
+ return false, err
+ }
+ }
+
+ if err := c.handle(resp); err != nil && err != responses.ErrUnhandled {
+ c.ErrorLog.Println("cannot handle response ", resp, err)
+ }
+ return true, nil
+}
+
+func (c *Client) writeReply(reply []byte) error {
+ if _, err := c.conn.Writer.Write(reply); err != nil {
+ return err
+ }
+ // Flush reply
+ return c.conn.Writer.Flush()
+}
+
+type handleResult struct {
+ status *imap.StatusResp
+ err error
+}
+
+func (c *Client) execute(cmdr imap.Commander, h responses.Handler) (*imap.StatusResp, error) {
+ cmd := cmdr.Command()
+ cmd.Tag = generateTag()
+
+ var replies <-chan []byte
+ if replier, ok := h.(responses.Replier); ok {
+ replies = replier.Replies()
+ }
+
+ if c.Timeout > 0 {
+ err := c.conn.SetDeadline(time.Now().Add(c.Timeout))
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ // It's possible the client had a timeout set from a previous command, but no
+ // longer does. Ensure we respect that. The zero time means no deadline.
+ if err := c.conn.SetDeadline(time.Time{}); err != nil {
+ return nil, err
+ }
+ }
+
+ // Check if we are upgrading.
+ upgrading := c.upgrading
+
+ // Add handler before sending command, to be sure to get the response in time
+ // (in tests, the response is sent right after our command is received, so
+ // sometimes the response was received before the setup of this handler)
+ doneHandle := make(chan handleResult, 1)
+ unregister := make(chan struct{})
+ c.registerHandler(responses.HandlerFunc(func(resp imap.Resp) error {
+ select {
+ case <-unregister:
+ // If an error occured while sending the command, abort
+ return errUnregisterHandler
+ default:
+ }
+
+ if s, ok := resp.(*imap.StatusResp); ok && s.Tag == cmd.Tag {
+ // This is the command's status response, we're done
+ doneHandle <- handleResult{s, nil}
+ // Special handling of connection upgrading.
+ if upgrading {
+ c.upgrading = false
+ // Wait for upgrade to finish.
+ c.conn.Wait()
+ }
+ // Cancel any pending literal write
+ select {
+ case c.continues <- false:
+ default:
+ }
+ return errUnregisterHandler
+ }
+
+ if h != nil {
+ // Pass the response to the response handler
+ if err := h.Handle(resp); err != nil && err != responses.ErrUnhandled {
+ // If the response handler returns an error, abort
+ doneHandle <- handleResult{nil, err}
+ return errUnregisterHandler
+ } else {
+ return err
+ }
+ }
+ return responses.ErrUnhandled
+ }))
+
+ // Send the command to the server
+ if err := cmd.WriteTo(c.conn.Writer); err != nil {
+ // Error while sending the command
+ close(unregister)
+
+ if err, ok := err.(imap.LiteralLengthErr); ok {
+ // Expected > Actual
+ // The server is waiting for us to write
+ // more bytes, we don't have them. Run.
+ // Expected < Actual
+ // We are about to send a potentially truncated message, we don't
+ // want this (ths terminating CRLF is not sent at this point).
+ c.conn.Close()
+ return nil, err
+ }
+
+ return nil, err
+ }
+ // Flush writer if we are upgrading
+ if upgrading {
+ if err := c.conn.Writer.Flush(); err != nil {
+ // Error while sending the command
+ close(unregister)
+ return nil, err
+ }
+ }
+
+ for {
+ select {
+ case reply := <-replies:
+ // Response handler needs to send a reply (Used for AUTHENTICATE)
+ if err := c.writeReply(reply); err != nil {
+ close(unregister)
+ return nil, err
+ }
+ case <-c.loggedOut:
+ // If the connection is closed (such as from an I/O error), ensure we
+ // realize this and don't block waiting on a response that will never
+ // come. loggedOut is a channel that closes when the reader goroutine
+ // ends.
+ close(unregister)
+ return nil, errClosed
+ case result := <-doneHandle:
+ return result.status, result.err
+ }
+ }
+}
+
+// State returns the current connection state.
+func (c *Client) State() imap.ConnState {
+ c.locker.Lock()
+ state := c.state
+ c.locker.Unlock()
+ return state
+}
+
+// Mailbox returns the selected mailbox. It returns nil if there isn't one.
+func (c *Client) Mailbox() *imap.MailboxStatus {
+ // c.Mailbox fields are not supposed to change, so we can return the pointer.
+ c.locker.Lock()
+ mbox := c.mailbox
+ c.locker.Unlock()
+ return mbox
+}
+
+// SetState sets this connection's internal state.
+//
+// This function should not be called directly, it must only be used by
+// libraries implementing extensions of the IMAP protocol.
+func (c *Client) SetState(state imap.ConnState, mailbox *imap.MailboxStatus) {
+ c.locker.Lock()
+ c.state = state
+ c.mailbox = mailbox
+ c.locker.Unlock()
+}
+
+// Execute executes a generic command. cmdr is a value that can be converted to
+// a raw command and h is a response handler. The function returns when the
+// command has completed or failed, in this case err is nil. A non-nil err value
+// indicates a network error.
+//
+// This function should not be called directly, it must only be used by
+// libraries implementing extensions of the IMAP protocol.
+func (c *Client) Execute(cmdr imap.Commander, h responses.Handler) (*imap.StatusResp, error) {
+ return c.execute(cmdr, h)
+}
+
+func (c *Client) handleContinuationReqs() {
+ c.registerHandler(responses.HandlerFunc(func(resp imap.Resp) error {
+ if _, ok := resp.(*imap.ContinuationReq); ok {
+ go func() {
+ c.continues <- true
+ }()
+ return nil
+ }
+ return responses.ErrUnhandled
+ }))
+}
+
+func (c *Client) gotStatusCaps(args []interface{}) {
+ c.locker.Lock()
+
+ c.caps = make(map[string]bool)
+ for _, cap := range args {
+ if cap, ok := cap.(string); ok {
+ c.caps[cap] = true
+ }
+ }
+
+ c.locker.Unlock()
+}
+
+// The server can send unilateral data. This function handles it.
+func (c *Client) handleUnilateral() {
+ c.registerHandler(responses.HandlerFunc(func(resp imap.Resp) error {
+ switch resp := resp.(type) {
+ case *imap.StatusResp:
+ if resp.Tag != "*" {
+ return responses.ErrUnhandled
+ }
+
+ switch resp.Type {
+ case imap.StatusRespOk, imap.StatusRespNo, imap.StatusRespBad:
+ if c.Updates != nil {
+ c.Updates <- &StatusUpdate{resp}
+ }
+ case imap.StatusRespBye:
+ c.locker.Lock()
+ c.state = imap.LogoutState
+ c.mailbox = nil
+ c.locker.Unlock()
+
+ c.conn.Close()
+
+ if c.Updates != nil {
+ c.Updates <- &StatusUpdate{resp}
+ }
+ default:
+ return responses.ErrUnhandled
+ }
+ case *imap.DataResp:
+ name, fields, ok := imap.ParseNamedResp(resp)
+ if !ok {
+ return responses.ErrUnhandled
+ }
+
+ switch name {
+ case "CAPABILITY":
+ c.gotStatusCaps(fields)
+ case "EXISTS":
+ if c.Mailbox() == nil {
+ break
+ }
+
+ if messages, err := imap.ParseNumber(fields[0]); err == nil {
+ c.locker.Lock()
+ c.mailbox.Messages = messages
+ c.locker.Unlock()
+
+ c.mailbox.ItemsLocker.Lock()
+ c.mailbox.Items[imap.StatusMessages] = nil
+ c.mailbox.ItemsLocker.Unlock()
+ }
+
+ if c.Updates != nil {
+ c.Updates <- &MailboxUpdate{c.Mailbox()}
+ }
+ case "RECENT":
+ if c.Mailbox() == nil {
+ break
+ }
+
+ if recent, err := imap.ParseNumber(fields[0]); err == nil {
+ c.locker.Lock()
+ c.mailbox.Recent = recent
+ c.locker.Unlock()
+
+ c.mailbox.ItemsLocker.Lock()
+ c.mailbox.Items[imap.StatusRecent] = nil
+ c.mailbox.ItemsLocker.Unlock()
+ }
+
+ if c.Updates != nil {
+ c.Updates <- &MailboxUpdate{c.Mailbox()}
+ }
+ case "EXPUNGE":
+ seqNum, _ := imap.ParseNumber(fields[0])
+
+ if c.Updates != nil {
+ c.Updates <- &ExpungeUpdate{seqNum}
+ }
+ case "FETCH":
+ seqNum, _ := imap.ParseNumber(fields[0])
+ fields, _ := fields[1].([]interface{})
+
+ msg := &imap.Message{SeqNum: seqNum}
+ if err := msg.Parse(fields); err != nil {
+ break
+ }
+
+ if c.Updates != nil {
+ c.Updates <- &MessageUpdate{msg}
+ }
+ default:
+ return responses.ErrUnhandled
+ }
+ default:
+ return responses.ErrUnhandled
+ }
+ return nil
+ }))
+}
+
+func (c *Client) handleGreetAndStartReading() error {
+ var greetErr error
+ gotGreet := false
+
+ c.registerHandler(responses.HandlerFunc(func(resp imap.Resp) error {
+ status, ok := resp.(*imap.StatusResp)
+ if !ok {
+ greetErr = fmt.Errorf("invalid greeting received from server: not a status response")
+ return errUnregisterHandler
+ }
+
+ c.locker.Lock()
+ switch status.Type {
+ case imap.StatusRespPreauth:
+ c.state = imap.AuthenticatedState
+ case imap.StatusRespBye:
+ c.state = imap.LogoutState
+ case imap.StatusRespOk:
+ c.state = imap.NotAuthenticatedState
+ default:
+ c.state = imap.LogoutState
+ c.locker.Unlock()
+ greetErr = fmt.Errorf("invalid greeting received from server: %v", status.Type)
+ return errUnregisterHandler
+ }
+ c.locker.Unlock()
+
+ if status.Code == imap.CodeCapability {
+ c.gotStatusCaps(status.Arguments)
+ }
+
+ gotGreet = true
+ return errUnregisterHandler
+ }))
+
+ // call `readOnce` until we get the greeting or an error
+ for !gotGreet {
+ connected, err := c.readOnce()
+ // Check for read errors
+ if err != nil {
+ // return read errors
+ return err
+ }
+ // Check for invalid greet
+ if greetErr != nil {
+ // return read errors
+ return greetErr
+ }
+ // Check if connection was closed.
+ if !connected {
+ // connection closed.
+ return io.EOF
+ }
+ }
+
+ // We got the greeting, now start the reader goroutine.
+ go c.reader()
+
+ return nil
+}
+
+// Upgrade a connection, e.g. wrap an unencrypted connection with an encrypted
+// tunnel.
+//
+// This function should not be called directly, it must only be used by
+// libraries implementing extensions of the IMAP protocol.
+func (c *Client) Upgrade(upgrader imap.ConnUpgrader) error {
+ return c.conn.Upgrade(upgrader)
+}
+
+// Writer returns the imap.Writer for this client's connection.
+//
+// This function should not be called directly, it must only be used by
+// libraries implementing extensions of the IMAP protocol.
+func (c *Client) Writer() *imap.Writer {
+ return c.conn.Writer
+}
+
+// IsTLS checks if this client's connection has TLS enabled.
+func (c *Client) IsTLS() bool {
+ return c.isTLS
+}
+
+// LoggedOut returns a channel which is closed when the connection to the server
+// is closed.
+func (c *Client) LoggedOut() <-chan struct{} {
+ return c.loggedOut
+}
+
+// SetDebug defines an io.Writer to which all network activity will be logged.
+// If nil is provided, network activity will not be logged.
+func (c *Client) SetDebug(w io.Writer) {
+ // Need to send a command to unblock the reader goroutine.
+ cmd := new(commands.Noop)
+ err := c.Upgrade(func(conn net.Conn) (net.Conn, error) {
+ // Flag connection as in upgrading
+ c.upgrading = true
+ if status, err := c.execute(cmd, nil); err != nil {
+ return nil, err
+ } else if err := status.Err(); err != nil {
+ return nil, err
+ }
+
+ // Wait for reader to block.
+ c.conn.WaitReady()
+
+ c.conn.SetDebug(w)
+ return conn, nil
+ })
+ if err != nil {
+ log.Println("SetDebug:", err)
+ }
+
+}
+
+// New creates a new client from an existing connection.
+func New(conn net.Conn) (*Client, error) {
+ continues := make(chan bool)
+ w := imap.NewClientWriter(nil, continues)
+ r := imap.NewReader(nil)
+
+ c := &Client{
+ conn: imap.NewConn(conn, r, w),
+ loggedOut: make(chan struct{}),
+ continues: continues,
+ state: imap.ConnectingState,
+ ErrorLog: log.New(os.Stderr, "imap/client: ", log.LstdFlags),
+ }
+
+ c.handleContinuationReqs()
+ c.handleUnilateral()
+ if err := c.handleGreetAndStartReading(); err != nil {
+ return c, err
+ }
+
+ plusOk, _ := c.Support("LITERAL+")
+ minusOk, _ := c.Support("LITERAL-")
+ // We don't use non-sync literal if it is bigger than 4096 bytes, so
+ // LITERAL- is fine too.
+ c.conn.AllowAsyncLiterals = plusOk || minusOk
+
+ return c, nil
+}
+
+// Dial connects to an IMAP server using an unencrypted connection.
+func Dial(addr string) (*Client, error) {
+ return DialWithDialer(new(net.Dialer), addr)
+}
+
+type Dialer interface {
+ // Dial connects to the given address.
+ Dial(network, addr string) (net.Conn, error)
+}
+
+// DialWithDialer connects to an IMAP server using an unencrypted connection
+// using dialer.Dial.
+//
+// Among other uses, this allows to apply a dial timeout.
+func DialWithDialer(dialer Dialer, addr string) (*Client, error) {
+ conn, err := dialer.Dial("tcp", addr)
+ if err != nil {
+ return nil, err
+ }
+
+ // We don't return to the caller until we try to receive a greeting. As such,
+ // there is no way to set the client's Timeout for that action. As a
+ // workaround, if the dialer has a timeout set, use that for the connection's
+ // deadline.
+ if netDialer, ok := dialer.(*net.Dialer); ok && netDialer.Timeout > 0 {
+ err := conn.SetDeadline(time.Now().Add(netDialer.Timeout))
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ c, err := New(conn)
+ if err != nil {
+ return nil, err
+ }
+
+ c.serverName, _, _ = net.SplitHostPort(addr)
+ return c, nil
+}
+
+// DialTLS connects to an IMAP server using an encrypted connection.
+func DialTLS(addr string, tlsConfig *tls.Config) (*Client, error) {
+ return DialWithDialerTLS(new(net.Dialer), addr, tlsConfig)
+}
+
+// DialWithDialerTLS connects to an IMAP server using an encrypted connection
+// using dialer.Dial.
+//
+// Among other uses, this allows to apply a dial timeout.
+func DialWithDialerTLS(dialer Dialer, addr string, tlsConfig *tls.Config) (*Client, error) {
+ conn, err := dialer.Dial("tcp", addr)
+ if err != nil {
+ return nil, err
+ }
+
+ serverName, _, _ := net.SplitHostPort(addr)
+ if tlsConfig == nil {
+ tlsConfig = &tls.Config{}
+ }
+ if tlsConfig.ServerName == "" {
+ tlsConfig = tlsConfig.Clone()
+ tlsConfig.ServerName = serverName
+ }
+ tlsConn := tls.Client(conn, tlsConfig)
+
+ // We don't return to the caller until we try to receive a greeting. As such,
+ // there is no way to set the client's Timeout for that action. As a
+ // workaround, if the dialer has a timeout set, use that for the connection's
+ // deadline.
+ if netDialer, ok := dialer.(*net.Dialer); ok && netDialer.Timeout > 0 {
+ err := tlsConn.SetDeadline(time.Now().Add(netDialer.Timeout))
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ c, err := New(tlsConn)
+ if err != nil {
+ return nil, err
+ }
+
+ c.isTLS = true
+ c.serverName = serverName
+ return c, nil
+}