aboutsummaryrefslogtreecommitdiff
path: root/vendor/github.com/emersion/go-imap/client
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/emersion/go-imap/client')
-rw-r--r--vendor/github.com/emersion/go-imap/client/client.go689
-rw-r--r--vendor/github.com/emersion/go-imap/client/cmd_any.go88
-rw-r--r--vendor/github.com/emersion/go-imap/client/cmd_auth.go380
-rw-r--r--vendor/github.com/emersion/go-imap/client/cmd_noauth.go174
-rw-r--r--vendor/github.com/emersion/go-imap/client/cmd_selected.go367
-rw-r--r--vendor/github.com/emersion/go-imap/client/tag.go24
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
+}