// 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 }