diff options
author | Jordan <me@jordan.im> | 2023-02-04 23:54:03 -0700 |
---|---|---|
committer | Jordan <me@jordan.im> | 2023-02-04 23:54:03 -0700 |
commit | c4159d895ac399ca55326f7b4ff8bfbf8402e654 (patch) | |
tree | 45340ca429c16f683b375695d01e03d65ebf22b0 /vendor/github.com/emersion/go-imap/client | |
download | pigeon-c4159d895ac399ca55326f7b4ff8bfbf8402e654.tar.gz pigeon-c4159d895ac399ca55326f7b4ff8bfbf8402e654.zip |
initial commit
Diffstat (limited to 'vendor/github.com/emersion/go-imap/client')
-rw-r--r-- | vendor/github.com/emersion/go-imap/client/client.go | 689 | ||||
-rw-r--r-- | vendor/github.com/emersion/go-imap/client/cmd_any.go | 88 | ||||
-rw-r--r-- | vendor/github.com/emersion/go-imap/client/cmd_auth.go | 380 | ||||
-rw-r--r-- | vendor/github.com/emersion/go-imap/client/cmd_noauth.go | 174 | ||||
-rw-r--r-- | vendor/github.com/emersion/go-imap/client/cmd_selected.go | 367 | ||||
-rw-r--r-- | vendor/github.com/emersion/go-imap/client/tag.go | 24 |
6 files changed, 1722 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 +} diff --git a/vendor/github.com/emersion/go-imap/client/cmd_any.go b/vendor/github.com/emersion/go-imap/client/cmd_any.go new file mode 100644 index 0000000..cb0d38a --- /dev/null +++ b/vendor/github.com/emersion/go-imap/client/cmd_any.go @@ -0,0 +1,88 @@ +package client + +import ( + "errors" + + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/commands" +) + +// ErrAlreadyLoggedOut is returned if Logout is called when the client is +// already logged out. +var ErrAlreadyLoggedOut = errors.New("Already logged out") + +// Capability requests a listing of capabilities that the server supports. +// Capabilities are often returned by the server with the greeting or with the +// STARTTLS and LOGIN responses, so usually explicitly requesting capabilities +// isn't needed. +// +// Most of the time, Support should be used instead. +func (c *Client) Capability() (map[string]bool, error) { + cmd := &commands.Capability{} + + if status, err := c.execute(cmd, nil); err != nil { + return nil, err + } else if err := status.Err(); err != nil { + return nil, err + } + + c.locker.Lock() + caps := c.caps + c.locker.Unlock() + return caps, nil +} + +// Support checks if cap is a capability supported by the server. If the server +// hasn't sent its capabilities yet, Support requests them. +func (c *Client) Support(cap string) (bool, error) { + c.locker.Lock() + ok := c.caps != nil + c.locker.Unlock() + + // If capabilities are not cached, request them + if !ok { + if _, err := c.Capability(); err != nil { + return false, err + } + } + + c.locker.Lock() + supported := c.caps[cap] + c.locker.Unlock() + + return supported, nil +} + +// Noop always succeeds and does nothing. +// +// It can be used as a periodic poll for new messages or message status updates +// during a period of inactivity. It can also be used to reset any inactivity +// autologout timer on the server. +func (c *Client) Noop() error { + cmd := new(commands.Noop) + + status, err := c.execute(cmd, nil) + if err != nil { + return err + } + return status.Err() +} + +// Logout gracefully closes the connection. +func (c *Client) Logout() error { + if c.State() == imap.LogoutState { + return ErrAlreadyLoggedOut + } + + cmd := new(commands.Logout) + + if status, err := c.execute(cmd, nil); err == errClosed { + // Server closed connection, that's what we want anyway + return nil + } else if err != nil { + return err + } else if status != nil { + return status.Err() + } + return nil +} diff --git a/vendor/github.com/emersion/go-imap/client/cmd_auth.go b/vendor/github.com/emersion/go-imap/client/cmd_auth.go new file mode 100644 index 0000000..a280017 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/client/cmd_auth.go @@ -0,0 +1,380 @@ +package client + +import ( + "errors" + "time" + + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/commands" + "github.com/emersion/go-imap/responses" +) + +// ErrNotLoggedIn is returned if a function that requires the client to be +// logged in is called then the client isn't. +var ErrNotLoggedIn = errors.New("Not logged in") + +func (c *Client) ensureAuthenticated() error { + state := c.State() + if state != imap.AuthenticatedState && state != imap.SelectedState { + return ErrNotLoggedIn + } + return nil +} + +// Select selects a mailbox so that messages in the mailbox can be accessed. Any +// currently selected mailbox is deselected before attempting the new selection. +// Even if the readOnly parameter is set to false, the server can decide to open +// the mailbox in read-only mode. +func (c *Client) Select(name string, readOnly bool) (*imap.MailboxStatus, error) { + if err := c.ensureAuthenticated(); err != nil { + return nil, err + } + + cmd := &commands.Select{ + Mailbox: name, + ReadOnly: readOnly, + } + + mbox := &imap.MailboxStatus{Name: name, Items: make(map[imap.StatusItem]interface{})} + res := &responses.Select{ + Mailbox: mbox, + } + c.locker.Lock() + c.mailbox = mbox + c.locker.Unlock() + + status, err := c.execute(cmd, res) + if err != nil { + c.locker.Lock() + c.mailbox = nil + c.locker.Unlock() + return nil, err + } + if err := status.Err(); err != nil { + c.locker.Lock() + c.mailbox = nil + c.locker.Unlock() + return nil, err + } + + c.locker.Lock() + mbox.ReadOnly = (status.Code == imap.CodeReadOnly) + c.state = imap.SelectedState + c.locker.Unlock() + return mbox, nil +} + +// Create creates a mailbox with the given name. +func (c *Client) Create(name string) error { + if err := c.ensureAuthenticated(); err != nil { + return err + } + + cmd := &commands.Create{ + Mailbox: name, + } + + status, err := c.execute(cmd, nil) + if err != nil { + return err + } + return status.Err() +} + +// Delete permanently removes the mailbox with the given name. +func (c *Client) Delete(name string) error { + if err := c.ensureAuthenticated(); err != nil { + return err + } + + cmd := &commands.Delete{ + Mailbox: name, + } + + status, err := c.execute(cmd, nil) + if err != nil { + return err + } + return status.Err() +} + +// Rename changes the name of a mailbox. +func (c *Client) Rename(existingName, newName string) error { + if err := c.ensureAuthenticated(); err != nil { + return err + } + + cmd := &commands.Rename{ + Existing: existingName, + New: newName, + } + + status, err := c.execute(cmd, nil) + if err != nil { + return err + } + return status.Err() +} + +// Subscribe adds the specified mailbox name to the server's set of "active" or +// "subscribed" mailboxes. +func (c *Client) Subscribe(name string) error { + if err := c.ensureAuthenticated(); err != nil { + return err + } + + cmd := &commands.Subscribe{ + Mailbox: name, + } + + status, err := c.execute(cmd, nil) + if err != nil { + return err + } + return status.Err() +} + +// Unsubscribe removes the specified mailbox name from the server's set of +// "active" or "subscribed" mailboxes. +func (c *Client) Unsubscribe(name string) error { + if err := c.ensureAuthenticated(); err != nil { + return err + } + + cmd := &commands.Unsubscribe{ + Mailbox: name, + } + + status, err := c.execute(cmd, nil) + if err != nil { + return err + } + return status.Err() +} + +// List returns a subset of names from the complete set of all names available +// to the client. +// +// An empty name argument is a special request to return the hierarchy delimiter +// and the root name of the name given in the reference. The character "*" is a +// wildcard, and matches zero or more characters at this position. The +// character "%" is similar to "*", but it does not match a hierarchy delimiter. +func (c *Client) List(ref, name string, ch chan *imap.MailboxInfo) error { + defer close(ch) + + if err := c.ensureAuthenticated(); err != nil { + return err + } + + cmd := &commands.List{ + Reference: ref, + Mailbox: name, + } + res := &responses.List{Mailboxes: ch} + + status, err := c.execute(cmd, res) + if err != nil { + return err + } + return status.Err() +} + +// Lsub returns a subset of names from the set of names that the user has +// declared as being "active" or "subscribed". +func (c *Client) Lsub(ref, name string, ch chan *imap.MailboxInfo) error { + defer close(ch) + + if err := c.ensureAuthenticated(); err != nil { + return err + } + + cmd := &commands.List{ + Reference: ref, + Mailbox: name, + Subscribed: true, + } + res := &responses.List{ + Mailboxes: ch, + Subscribed: true, + } + + status, err := c.execute(cmd, res) + if err != nil { + return err + } + return status.Err() +} + +// Status requests the status of the indicated mailbox. It does not change the +// currently selected mailbox, nor does it affect the state of any messages in +// the queried mailbox. +// +// See RFC 3501 section 6.3.10 for a list of items that can be requested. +func (c *Client) Status(name string, items []imap.StatusItem) (*imap.MailboxStatus, error) { + if err := c.ensureAuthenticated(); err != nil { + return nil, err + } + + cmd := &commands.Status{ + Mailbox: name, + Items: items, + } + res := &responses.Status{ + Mailbox: new(imap.MailboxStatus), + } + + status, err := c.execute(cmd, res) + if err != nil { + return nil, err + } + return res.Mailbox, status.Err() +} + +// Append appends the literal argument as a new message to the end of the +// specified destination mailbox. This argument SHOULD be in the format of an +// RFC 2822 message. flags and date are optional arguments and can be set to +// nil and the empty struct. +func (c *Client) Append(mbox string, flags []string, date time.Time, msg imap.Literal) error { + if err := c.ensureAuthenticated(); err != nil { + return err + } + + cmd := &commands.Append{ + Mailbox: mbox, + Flags: flags, + Date: date, + Message: msg, + } + + status, err := c.execute(cmd, nil) + if err != nil { + return err + } + return status.Err() +} + +// Enable requests the server to enable the named extensions. The extensions +// which were successfully enabled are returned. +// +// See RFC 5161 section 3.1. +func (c *Client) Enable(caps []string) ([]string, error) { + if ok, err := c.Support("ENABLE"); !ok || err != nil { + return nil, ErrExtensionUnsupported + } + + // ENABLE is invalid if a mailbox has been selected. + if c.State() != imap.AuthenticatedState { + return nil, ErrNotLoggedIn + } + + cmd := &commands.Enable{Caps: caps} + res := &responses.Enabled{} + + if status, err := c.Execute(cmd, res); err != nil { + return nil, err + } else { + return res.Caps, status.Err() + } +} + +func (c *Client) idle(stop <-chan struct{}) error { + cmd := &commands.Idle{} + + res := &responses.Idle{ + Stop: stop, + RepliesCh: make(chan []byte, 10), + } + + if status, err := c.Execute(cmd, res); err != nil { + return err + } else { + return status.Err() + } +} + +// IdleOptions holds options for Client.Idle. +type IdleOptions struct { + // LogoutTimeout is used to avoid being logged out by the server when + // idling. Each LogoutTimeout, the IDLE command is restarted. If set to + // zero, a default is used. If negative, this behavior is disabled. + LogoutTimeout time.Duration + // Poll interval when the server doesn't support IDLE. If zero, a default + // is used. If negative, polling is always disabled. + PollInterval time.Duration +} + +// Idle indicates to the server that the client is ready to receive unsolicited +// mailbox update messages. When the client wants to send commands again, it +// must first close stop. +// +// If the server doesn't support IDLE, go-imap falls back to polling. +func (c *Client) Idle(stop <-chan struct{}, opts *IdleOptions) error { + if ok, err := c.Support("IDLE"); err != nil { + return err + } else if !ok { + return c.idleFallback(stop, opts) + } + + logoutTimeout := 25 * time.Minute + if opts != nil { + if opts.LogoutTimeout > 0 { + logoutTimeout = opts.LogoutTimeout + } else if opts.LogoutTimeout < 0 { + return c.idle(stop) + } + } + + t := time.NewTicker(logoutTimeout) + defer t.Stop() + + for { + stopOrRestart := make(chan struct{}) + done := make(chan error, 1) + go func() { + done <- c.idle(stopOrRestart) + }() + + select { + case <-t.C: + close(stopOrRestart) + if err := <-done; err != nil { + return err + } + case <-stop: + close(stopOrRestart) + return <-done + case err := <-done: + close(stopOrRestart) + if err != nil { + return err + } + } + } +} + +func (c *Client) idleFallback(stop <-chan struct{}, opts *IdleOptions) error { + pollInterval := time.Minute + if opts != nil { + if opts.PollInterval > 0 { + pollInterval = opts.PollInterval + } else if opts.PollInterval < 0 { + return ErrExtensionUnsupported + } + } + + t := time.NewTicker(pollInterval) + defer t.Stop() + + for { + select { + case <-t.C: + if err := c.Noop(); err != nil { + return err + } + case <-stop: + return nil + case <-c.LoggedOut(): + return errors.New("disconnected while idling") + } + } +} diff --git a/vendor/github.com/emersion/go-imap/client/cmd_noauth.go b/vendor/github.com/emersion/go-imap/client/cmd_noauth.go new file mode 100644 index 0000000..f9b34d3 --- /dev/null +++ b/vendor/github.com/emersion/go-imap/client/cmd_noauth.go @@ -0,0 +1,174 @@ +package client + +import ( + "crypto/tls" + "errors" + "net" + + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/commands" + "github.com/emersion/go-imap/responses" + "github.com/emersion/go-sasl" +) + +var ( + // ErrAlreadyLoggedIn is returned if Login or Authenticate is called when the + // client is already logged in. + ErrAlreadyLoggedIn = errors.New("Already logged in") + // ErrTLSAlreadyEnabled is returned if StartTLS is called when TLS is already + // enabled. + ErrTLSAlreadyEnabled = errors.New("TLS is already enabled") + // ErrLoginDisabled is returned if Login or Authenticate is called when the + // server has disabled authentication. Most of the time, calling enabling TLS + // solves the problem. + ErrLoginDisabled = errors.New("Login is disabled in current state") +) + +// SupportStartTLS checks if the server supports STARTTLS. +func (c *Client) SupportStartTLS() (bool, error) { + return c.Support("STARTTLS") +} + +// StartTLS starts TLS negotiation. +func (c *Client) StartTLS(tlsConfig *tls.Config) error { + if c.isTLS { + return ErrTLSAlreadyEnabled + } + + if tlsConfig == nil { + tlsConfig = new(tls.Config) + } + if tlsConfig.ServerName == "" { + tlsConfig = tlsConfig.Clone() + tlsConfig.ServerName = c.serverName + } + + cmd := new(commands.StartTLS) + + 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() + tlsConn := tls.Client(conn, tlsConfig) + if err := tlsConn.Handshake(); err != nil { + return nil, err + } + + // Capabilities change when TLS is enabled + c.locker.Lock() + c.caps = nil + c.locker.Unlock() + + return tlsConn, nil + }) + if err != nil { + return err + } + + c.isTLS = true + return nil +} + +// SupportAuth checks if the server supports a given authentication mechanism. +func (c *Client) SupportAuth(mech string) (bool, error) { + return c.Support("AUTH=" + mech) +} + +// Authenticate indicates a SASL authentication mechanism to the server. If the +// server supports the requested authentication mechanism, it performs an +// authentication protocol exchange to authenticate and identify the client. +func (c *Client) Authenticate(auth sasl.Client) error { + if c.State() != imap.NotAuthenticatedState { + return ErrAlreadyLoggedIn + } + + mech, ir, err := auth.Start() + if err != nil { + return err + } + + cmd := &commands.Authenticate{ + Mechanism: mech, + } + + irOk, err := c.Support("SASL-IR") + if err != nil { + return err + } + if irOk { + cmd.InitialResponse = ir + } + + res := &responses.Authenticate{ + Mechanism: auth, + InitialResponse: ir, + RepliesCh: make(chan []byte, 10), + } + if irOk { + res.InitialResponse = nil + } + + status, err := c.execute(cmd, res) + if err != nil { + return err + } + if err = status.Err(); err != nil { + return err + } + + c.locker.Lock() + c.state = imap.AuthenticatedState + c.caps = nil // Capabilities change when user is logged in + c.locker.Unlock() + + if status.Code == "CAPABILITY" { + c.gotStatusCaps(status.Arguments) + } + + return nil +} + +// Login identifies the client to the server and carries the plaintext password +// authenticating this user. +func (c *Client) Login(username, password string) error { + if state := c.State(); state == imap.AuthenticatedState || state == imap.SelectedState { + return ErrAlreadyLoggedIn + } + + c.locker.Lock() + loginDisabled := c.caps != nil && c.caps["LOGINDISABLED"] + c.locker.Unlock() + if loginDisabled { + return ErrLoginDisabled + } + + cmd := &commands.Login{ + Username: username, + Password: password, + } + + status, err := c.execute(cmd, nil) + if err != nil { + return err + } + if err = status.Err(); err != nil { + return err + } + + c.locker.Lock() + c.state = imap.AuthenticatedState + c.caps = nil // Capabilities change when user is logged in + c.locker.Unlock() + + if status.Code == "CAPABILITY" { + c.gotStatusCaps(status.Arguments) + } + return nil +} diff --git a/vendor/github.com/emersion/go-imap/client/cmd_selected.go b/vendor/github.com/emersion/go-imap/client/cmd_selected.go new file mode 100644 index 0000000..0fb71ad --- /dev/null +++ b/vendor/github.com/emersion/go-imap/client/cmd_selected.go @@ -0,0 +1,367 @@ +package client + +import ( + "errors" + + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/commands" + "github.com/emersion/go-imap/responses" +) + +var ( + // ErrNoMailboxSelected is returned if a command that requires a mailbox to be + // selected is called when there isn't. + ErrNoMailboxSelected = errors.New("No mailbox selected") + + // ErrExtensionUnsupported is returned if a command uses a extension that + // is not supported by the server. + ErrExtensionUnsupported = errors.New("The required extension is not supported by the server") +) + +// Check requests a checkpoint of the currently selected mailbox. A checkpoint +// refers to any implementation-dependent housekeeping associated with the +// mailbox that is not normally executed as part of each command. +func (c *Client) Check() error { + if c.State() != imap.SelectedState { + return ErrNoMailboxSelected + } + + cmd := new(commands.Check) + + status, err := c.execute(cmd, nil) + if err != nil { + return err + } + + return status.Err() +} + +// Close permanently removes all messages that have the \Deleted flag set from +// the currently selected mailbox, and returns to the authenticated state from +// the selected state. +func (c *Client) Close() error { + if c.State() != imap.SelectedState { + return ErrNoMailboxSelected + } + + cmd := new(commands.Close) + + status, err := c.execute(cmd, nil) + if err != nil { + return err + } else if err := status.Err(); err != nil { + return err + } + + c.locker.Lock() + c.state = imap.AuthenticatedState + c.mailbox = nil + c.locker.Unlock() + return nil +} + +// Terminate closes the tcp connection +func (c *Client) Terminate() error { + return c.conn.Close() +} + +// Expunge permanently removes all messages that have the \Deleted flag set from +// the currently selected mailbox. If ch is not nil, sends sequence IDs of each +// deleted message to this channel. +func (c *Client) Expunge(ch chan uint32) error { + if ch != nil { + defer close(ch) + } + + if c.State() != imap.SelectedState { + return ErrNoMailboxSelected + } + + cmd := new(commands.Expunge) + + var h responses.Handler + if ch != nil { + h = &responses.Expunge{SeqNums: ch} + } + + status, err := c.execute(cmd, h) + if err != nil { + return err + } + return status.Err() +} + +func (c *Client) executeSearch(uid bool, criteria *imap.SearchCriteria, charset string) (ids []uint32, status *imap.StatusResp, err error) { + if c.State() != imap.SelectedState { + err = ErrNoMailboxSelected + return + } + + var cmd imap.Commander = &commands.Search{ + Charset: charset, + Criteria: criteria, + } + if uid { + cmd = &commands.Uid{Cmd: cmd} + } + + res := new(responses.Search) + + status, err = c.execute(cmd, res) + if err != nil { + return + } + + err, ids = status.Err(), res.Ids + return +} + +func (c *Client) search(uid bool, criteria *imap.SearchCriteria) (ids []uint32, err error) { + ids, status, err := c.executeSearch(uid, criteria, "UTF-8") + if status != nil && status.Code == imap.CodeBadCharset { + // Some servers don't support UTF-8 + ids, _, err = c.executeSearch(uid, criteria, "US-ASCII") + } + return +} + +// Search searches the mailbox for messages that match the given searching +// criteria. Searching criteria consist of one or more search keys. The response +// contains a list of message sequence IDs corresponding to those messages that +// match the searching criteria. When multiple keys are specified, the result is +// the intersection (AND function) of all the messages that match those keys. +// Criteria must be UTF-8 encoded. See RFC 3501 section 6.4.4 for a list of +// searching criteria. When no criteria has been set, all messages in the mailbox +// will be searched using ALL criteria. +func (c *Client) Search(criteria *imap.SearchCriteria) (seqNums []uint32, err error) { + return c.search(false, criteria) +} + +// UidSearch is identical to Search, but UIDs are returned instead of message +// sequence numbers. +func (c *Client) UidSearch(criteria *imap.SearchCriteria) (uids []uint32, err error) { + return c.search(true, criteria) +} + +func (c *Client) fetch(uid bool, seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error { + defer close(ch) + + if c.State() != imap.SelectedState { + return ErrNoMailboxSelected + } + + var cmd imap.Commander = &commands.Fetch{ + SeqSet: seqset, + Items: items, + } + if uid { + cmd = &commands.Uid{Cmd: cmd} + } + + res := &responses.Fetch{Messages: ch, SeqSet: seqset, Uid: uid} + + status, err := c.execute(cmd, res) + if err != nil { + return err + } + return status.Err() +} + +// Fetch retrieves data associated with a message in the mailbox. See RFC 3501 +// section 6.4.5 for a list of items that can be requested. +func (c *Client) Fetch(seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error { + return c.fetch(false, seqset, items, ch) +} + +// UidFetch is identical to Fetch, but seqset is interpreted as containing +// unique identifiers instead of message sequence numbers. +func (c *Client) UidFetch(seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error { + return c.fetch(true, seqset, items, ch) +} + +func (c *Client) store(uid bool, seqset *imap.SeqSet, item imap.StoreItem, value interface{}, ch chan *imap.Message) error { + if ch != nil { + defer close(ch) + } + + if c.State() != imap.SelectedState { + return ErrNoMailboxSelected + } + + // TODO: this could break extensions (this only works when item is FLAGS) + if fields, ok := value.([]interface{}); ok { + for i, field := range fields { + if s, ok := field.(string); ok { + fields[i] = imap.RawString(s) + } + } + } + + // If ch is nil, the updated values are data which will be lost, so don't + // retrieve it. + if ch == nil { + op, _, err := imap.ParseFlagsOp(item) + if err == nil { + item = imap.FormatFlagsOp(op, true) + } + } + + var cmd imap.Commander = &commands.Store{ + SeqSet: seqset, + Item: item, + Value: value, + } + if uid { + cmd = &commands.Uid{Cmd: cmd} + } + + var h responses.Handler + if ch != nil { + h = &responses.Fetch{Messages: ch, SeqSet: seqset, Uid: uid} + } + + status, err := c.execute(cmd, h) + if err != nil { + return err + } + return status.Err() +} + +// Store alters data associated with a message in the mailbox. If ch is not nil, +// the updated value of the data will be sent to this channel. See RFC 3501 +// section 6.4.6 for a list of items that can be updated. +func (c *Client) Store(seqset *imap.SeqSet, item imap.StoreItem, value interface{}, ch chan *imap.Message) error { + return c.store(false, seqset, item, value, ch) +} + +// UidStore is identical to Store, but seqset is interpreted as containing +// unique identifiers instead of message sequence numbers. +func (c *Client) UidStore(seqset *imap.SeqSet, item imap.StoreItem, value interface{}, ch chan *imap.Message) error { + return c.store(true, seqset, item, value, ch) +} + +func (c *Client) copy(uid bool, seqset *imap.SeqSet, dest string) error { + if c.State() != imap.SelectedState { + return ErrNoMailboxSelected + } + + var cmd imap.Commander = &commands.Copy{ + SeqSet: seqset, + Mailbox: dest, + } + if uid { + cmd = &commands.Uid{Cmd: cmd} + } + + status, err := c.execute(cmd, nil) + if err != nil { + return err + } + return status.Err() +} + +// Copy copies the specified message(s) to the end of the specified destination +// mailbox. +func (c *Client) Copy(seqset *imap.SeqSet, dest string) error { + return c.copy(false, seqset, dest) +} + +// UidCopy is identical to Copy, but seqset is interpreted as containing unique +// identifiers instead of message sequence numbers. +func (c *Client) UidCopy(seqset *imap.SeqSet, dest string) error { + return c.copy(true, seqset, dest) +} + +func (c *Client) move(uid bool, seqset *imap.SeqSet, dest string) error { + if c.State() != imap.SelectedState { + return ErrNoMailboxSelected + } + + if ok, err := c.Support("MOVE"); err != nil { + return err + } else if !ok { + return c.moveFallback(uid, seqset, dest) + } + + var cmd imap.Commander = &commands.Move{ + SeqSet: seqset, + Mailbox: dest, + } + if uid { + cmd = &commands.Uid{Cmd: cmd} + } + + if status, err := c.Execute(cmd, nil); err != nil { + return err + } else { + return status.Err() + } +} + +// moveFallback uses COPY, STORE and EXPUNGE for servers which don't support +// MOVE. +func (c *Client) moveFallback(uid bool, seqset *imap.SeqSet, dest string) error { + item := imap.FormatFlagsOp(imap.AddFlags, true) + flags := []interface{}{imap.DeletedFlag} + if uid { + if err := c.UidCopy(seqset, dest); err != nil { + return err + } + + if err := c.UidStore(seqset, item, flags, nil); err != nil { + return err + } + } else { + if err := c.Copy(seqset, dest); err != nil { + return err + } + + if err := c.Store(seqset, item, flags, nil); err != nil { + return err + } + } + + return c.Expunge(nil) +} + +// Move moves the specified message(s) to the end of the specified destination +// mailbox. +// +// If the server doesn't support the MOVE extension defined in RFC 6851, +// go-imap will fallback to copy, store and expunge. +func (c *Client) Move(seqset *imap.SeqSet, dest string) error { + return c.move(false, seqset, dest) +} + +// UidMove is identical to Move, but seqset is interpreted as containing unique +// identifiers instead of message sequence numbers. +func (c *Client) UidMove(seqset *imap.SeqSet, dest string) error { + return c.move(true, seqset, dest) +} + +// Unselect frees server's resources associated with the selected mailbox and +// returns the server to the authenticated state. This command performs the same +// actions as Close, except that no messages are permanently removed from the +// currently selected mailbox. +// +// If client does not support the UNSELECT extension, ErrExtensionUnsupported +// is returned. +func (c *Client) Unselect() error { + if ok, err := c.Support("UNSELECT"); !ok || err != nil { + return ErrExtensionUnsupported + } + + if c.State() != imap.SelectedState { + return ErrNoMailboxSelected + } + + cmd := &commands.Unselect{} + if status, err := c.Execute(cmd, nil); err != nil { + return err + } else if err := status.Err(); err != nil { + return err + } + + c.SetState(imap.AuthenticatedState, nil) + return nil +} diff --git a/vendor/github.com/emersion/go-imap/client/tag.go b/vendor/github.com/emersion/go-imap/client/tag.go new file mode 100644 index 0000000..01526ab --- /dev/null +++ b/vendor/github.com/emersion/go-imap/client/tag.go @@ -0,0 +1,24 @@ +package client + +import ( + "crypto/rand" + "encoding/base64" +) + +func randomString(n int) (string, error) { + b := make([]byte, n) + _, err := rand.Read(b) + if err != nil { + return "", err + } + + return base64.RawURLEncoding.EncodeToString(b), nil +} + +func generateTag() string { + tag, err := randomString(4) + if err != nil { + panic(err) + } + return tag +} |