Files
go-imap/client/client.go
T

528 lines
13 KiB
Go

// Package client provides an IMAP client.
package client
import (
"crypto/tls"
"fmt"
"io"
"log"
"net"
"os"
"sync"
"time"
"github.com/emersion/go-imap"
)
// errClosed is used when a connection is closed while waiting for a command
// response.
var errClosed = fmt.Errorf("imap: connection closed")
// Client is an IMAP client.
type Client struct {
conn *imap.Conn
isTLS bool
handles imap.RespHandler
handler *imap.MultiRespHandler
greeted chan struct{}
loggedOut chan struct{}
// The cached server capabilities.
caps map[string]bool
// The caps map may be accessed in different goroutines. Protect access.
capsLocker sync.Mutex
// The current connection state.
State imap.ConnState
// The selected mailbox, if there is one.
Mailbox *imap.MailboxStatus
// A channel where info messages from the server will be sent.
Infos chan *imap.StatusResp
// A channel where warning messages from the server will be sent.
Warnings chan *imap.StatusResp
// A channel where error messages from the server will be sent.
Errors chan *imap.StatusResp
// A channel where bye messages from the server will be sent.
Byes chan *imap.StatusResp
// A channel where mailbox updates from the server will be sent.
MailboxUpdates chan *imap.MailboxStatus
// A channel where deleted message IDs will be sent.
Expunges chan uint32
// A channel where messages updates from the server will be sent.
MessageUpdates chan *imap.Message
// ErrorLog specifies an optional logger for errors accepting
// connections and unexpected behavior from handlers.
// If nil, logging goes to os.Stderr via the log package's
// standard logger.
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) read(greeted chan struct{}) error {
defer func() {
// Ensure we close the greeted channel. New may be waiting on an indication
// that we've seen the greeting.
if c.greeted != nil {
close(c.greeted)
c.greeted = nil
}
close(c.handles)
close(c.loggedOut)
}()
first := true
for {
if c.State == imap.LogoutState {
return nil
}
c.conn.Wait()
if first {
first = false
} else {
<-greeted
if c.greeted != nil {
close(c.greeted)
c.greeted = nil
}
}
res, err := imap.ReadResp(c.conn.Reader)
if err == io.EOF || c.State == imap.LogoutState {
return nil
}
if err != nil {
c.ErrorLog.Println("error reading response:", err)
if imap.IsParseError(err) {
continue
} else {
return err
}
}
rh := &imap.RespHandle{
Resp: res,
Accepts: make(chan bool),
}
c.handles <- rh
if accepted := <-rh.Accepts; !accepted {
c.ErrorLog.Println("response has not been handled:", res)
}
}
}
func (c *Client) execute(cmdr imap.Commander, res imap.RespHandlerFrom) (status *imap.StatusResp, err error) {
cmd := cmdr.Command()
cmd.Tag = generateTag()
if c.Timeout > 0 {
err = c.conn.SetDeadline(time.Now().Add(c.Timeout))
if err != nil {
return
}
} 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.
err = c.conn.SetDeadline(time.Time{})
if err != nil {
return
}
}
// 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)
statusHdlr := make(imap.RespHandler)
c.handler.Add(statusHdlr)
// Send the command to the server
doneWrite := make(chan error, 1)
go func() {
doneWrite <- cmd.WriteTo(c.conn.Writer)
}()
// If a response handler is provided, start it
var hdlr imap.RespHandler
var doneHandle chan error
if res != nil {
hdlr = make(imap.RespHandler)
doneHandle = make(chan error, 1)
go func() {
doneHandle <- res.HandleFrom(hdlr)
}()
}
for {
select {
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.
err = errClosed
return
case err = <-doneWrite:
// Error while sending the command
if err != nil {
c.handler.Del(statusHdlr)
}
case err = <-doneHandle:
// Error while handling responses
if err != nil {
c.handler.Del(statusHdlr)
}
case h, more := <-statusHdlr:
if !more {
// statusHdlr has been closed, stop here
return
}
// If the status tag matches the command tag, the response is completed
if s, ok := h.Resp.(*imap.StatusResp); ok && s.Tag == cmd.Tag {
h.Accept()
status = s
// Stop the response handler, if it's running
if hdlr != nil {
close(hdlr)
}
// Do not listen for responses anymore
c.handler.Del(statusHdlr)
} else if hdlr != nil {
hdlr <- h
} else {
h.Reject()
}
}
}
}
// Execute executes a generic command. cmdr is a value that can be converted to
// a raw command and res is a value that can handle responses. 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, res imap.RespHandlerFrom) (status *imap.StatusResp, err error) {
return c.execute(cmdr, res)
}
func (c *Client) handleContinuationReqs(continues chan<- bool) {
hdlr := make(imap.RespHandler)
c.handler.Add(hdlr)
defer c.handler.Del(hdlr)
defer close(continues)
for h := range hdlr {
if _, ok := h.Resp.(*imap.ContinuationResp); ok {
h.Accept()
continues <- true
} else {
h.Reject()
}
}
}
func (c *Client) gotStatusCaps(args []interface{}) {
c.capsLocker.Lock()
c.caps = make(map[string]bool)
for _, cap := range args {
if cap, ok := cap.(string); ok {
c.caps[cap] = true
}
}
c.capsLocker.Unlock()
}
// The server can send unilateral data. This function handles it.
func (c *Client) handleUnilateral() {
hdlr := make(imap.RespHandler)
c.handler.Add(hdlr)
defer c.handler.Del(hdlr)
greeted := make(chan struct{})
// Make sure to start reading after we have set up the base handlers,
// otherwise some messages will be lost.
go c.read(greeted)
for h := range hdlr {
switch res := h.Resp.(type) {
case *imap.StatusResp:
if res.Tag != "*" ||
(res.Type != imap.StatusOk && res.Type != imap.StatusNo && res.Type != imap.StatusBad && res.Type != imap.StatusBye) ||
(res.Code != "" && res.Code != imap.CodeAlert && res.Code != imap.CodeCapability) {
h.Reject()
break
}
h.Accept()
if greeted != nil {
switch res.Type {
case imap.StatusPreauth:
c.State = imap.AuthenticatedState
case imap.StatusBye:
c.State = imap.LogoutState
case imap.StatusOk:
c.State = imap.NotAuthenticatedState
default:
c.ErrorLog.Println("invalid greeting:", res.Type)
c.State = imap.LogoutState
}
if res.Code == imap.CodeCapability {
c.gotStatusCaps(res.Arguments)
}
close(greeted)
greeted = nil
}
switch res.Type {
case imap.StatusOk:
if c.Infos != nil {
c.Infos <- res
}
case imap.StatusNo:
if c.Warnings != nil {
c.Warnings <- res
}
case imap.StatusBad:
if c.Errors != nil {
c.Errors <- res
}
case imap.StatusBye:
c.State = imap.LogoutState
c.Mailbox = nil
c.conn.Close()
if c.Byes != nil {
c.Byes <- res
}
}
case *imap.Resp:
if len(res.Fields) < 2 {
h.Reject()
break
}
// A CAPABILITY response
if name, ok := res.Fields[0].(string); ok && name == imap.Capability {
h.Accept()
c.gotStatusCaps(res.Fields[1:len(res.Fields)])
break
}
// An unilateral EXISTS, RECENT, EXPUNGE or FETCH response
name, ok := res.Fields[1].(string)
if !ok || (name != "EXISTS" && name != "RECENT" && name != "EXPUNGE" && name != "FETCH") {
h.Reject()
break
}
h.Accept()
switch name {
case "EXISTS":
if c.Mailbox == nil {
break
}
if messages, err := imap.ParseNumber(res.Fields[0]); err == nil {
c.Mailbox.Messages = messages
c.Mailbox.ItemsLocker.Lock()
c.Mailbox.Items[imap.MailboxMessages] = nil
c.Mailbox.ItemsLocker.Unlock()
}
if c.MailboxUpdates != nil {
c.MailboxUpdates <- c.Mailbox
}
case "RECENT":
if c.Mailbox == nil {
break
}
if recent, err := imap.ParseNumber(res.Fields[0]); err == nil {
c.Mailbox.Recent = recent
c.Mailbox.ItemsLocker.Lock()
c.Mailbox.Items[imap.MailboxRecent] = nil
c.Mailbox.ItemsLocker.Unlock()
}
if c.MailboxUpdates != nil {
c.MailboxUpdates <- c.Mailbox
}
case "EXPUNGE":
seqNum, _ := imap.ParseNumber(res.Fields[0])
if c.Expunges != nil {
c.Expunges <- seqNum
}
case "FETCH":
seqNum, _ := imap.ParseNumber(res.Fields[0])
fields, _ := res.Fields[2].([]interface{})
msg := &imap.Message{
SeqNum: seqNum,
}
if err := msg.Parse(fields); err != nil {
break
}
if c.MessageUpdates != nil {
c.MessageUpdates <- msg
}
}
default:
h.Reject()
}
}
}
// 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) {
c.conn.SetDebug(w)
}
// New creates a new client from an existing connection.
func New(conn net.Conn) (c *Client, err error) {
continues := make(chan bool)
w := imap.NewClientWriter(nil, continues)
r := imap.NewReader(nil)
c = &Client{
conn: imap.NewConn(conn, r, w),
handles: make(imap.RespHandler),
handler: imap.NewMultiRespHandler(),
greeted: make(chan struct{}),
loggedOut: make(chan struct{}),
State: imap.ConnectingState,
ErrorLog: log.New(os.Stderr, "imap/client: ", log.LstdFlags),
}
go c.handleContinuationReqs(continues)
go c.handleUnilateral()
go c.handler.HandleFrom(c.handles)
<-c.greeted
return
}
// Dial connects to an IMAP server using an unencrypted connection.
func Dial(addr string) (c *Client, err error) {
conn, err := net.Dial("tcp", addr)
if err != nil {
return
}
c, err = New(conn)
return
}
// DialWithDialer connects to an IMAP server using an unencrypted connection
// using dialer.Dial.
//
// Among other uses, this allows us to apply a connection timeout.
func DialWithDialer(dialer *net.Dialer, address string) (c *Client, err error) {
conn, err := dialer.Dial("tcp", address)
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 dialer.Timeout > 0 {
err = conn.SetDeadline(time.Now().Add(dialer.Timeout))
if err != nil {
return
}
}
c, err = New(conn)
return
}
// DialTLS connects to an IMAP server using an encrypted connection.
func DialTLS(addr string, tlsConfig *tls.Config) (c *Client, err error) {
conn, err := tls.Dial("tcp", addr, tlsConfig)
if err != nil {
return
}
c, err = New(conn)
c.isTLS = true
return
}
// DialWithDialerTLS connects to an IMAP server using an encrypted connection
// using dialer.Dial.
//
// Among other uses, this allows us to apply a connection timeout.
func DialWithDialerTLS(dialer *net.Dialer, addr string,
tlsConfig *tls.Config) (c *Client, err error) {
conn, err := tls.DialWithDialer(dialer, "tcp", addr, tlsConfig)
if err != nil {
return
}
// 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 dialer.Timeout > 0 {
err = conn.SetDeadline(time.Now().Add(dialer.Timeout))
if err != nil {
return
}
}
c, err = New(conn)
c.isTLS = true
return
}