mirror of
https://github.com/emersion/go-imap
synced 2026-07-02 15:02:28 +00:00
528 lines
13 KiB
Go
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
|
|
}
|