diff --git a/worker/imap/connect.go b/worker/imap/connect.go new file mode 100644 index 0000000..9cc394b --- /dev/null +++ b/worker/imap/connect.go @@ -0,0 +1,178 @@ +package imap + +import ( + "crypto/tls" + "fmt" + "net" + "time" + + "git.sr.ht/~rjarry/aerc/lib" + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/client" +) + +// connect establishes a new tcp connection to the imap server, logs in and +// selects the default inbox. If no error is returned, the imap client will be +// in the imap.SelectedState. +func (w *IMAPWorker) connect() (*client.Client, error) { + + var ( + conn *net.TCPConn + err error + c *client.Client + ) + + conn, err = newTCPConn(w.config.addr, w.config.connection_timeout) + if conn == nil || err != nil { + return nil, err + } + + if w.config.connection_timeout > 0 { + end := time.Now().Add(w.config.connection_timeout) + err = conn.SetDeadline(end) + if err != nil { + return nil, err + } + } + + if w.config.keepalive_period > 0 { + err = w.setKeepaliveParameters(conn) + if err != nil { + return nil, err + } + } + + serverName, _, _ := net.SplitHostPort(w.config.addr) + tlsConfig := &tls.Config{ServerName: serverName} + + switch w.config.scheme { + case "imap": + c, err = client.New(conn) + if err != nil { + return nil, err + } + if !w.config.insecure { + if err = c.StartTLS(tlsConfig); err != nil { + return nil, err + } + } + case "imaps": + tlsConn := tls.Client(conn, tlsConfig) + c, err = client.New(tlsConn) + if err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("Unknown IMAP scheme %s", w.config.scheme) + } + + c.ErrorLog = w.worker.Logger + + if w.config.user != nil { + username := w.config.user.Username() + password, hasPassword := w.config.user.Password() + if !hasPassword { + // TODO: ask password + } + + if w.config.oauthBearer.Enabled { + if err := w.config.oauthBearer.Authenticate( + username, password, c); err != nil { + return nil, err + } + } else if err := c.Login(username, password); err != nil { + return nil, err + } + } + + c.SetDebug(w.worker.Logger.Writer()) + + if _, err := c.Select(imap.InboxName, false); err != nil { + return nil, err + } + + return c, nil +} + +// newTCPConn establishes a new tcp connection. Timeout will ensure that the +// function does not hang when there is no connection. If there is a timeout, +// but a valid connection is eventually returned, ensure that it is properly +// closed. +func newTCPConn(addr string, timeout time.Duration) (*net.TCPConn, error) { + + var errTCPTimeout = fmt.Errorf("tcp connection timeout") + + type tcpConn struct { + conn *net.TCPConn + err error + } + + done := make(chan tcpConn) + go func() { + addr, err := net.ResolveTCPAddr("tcp", addr) + if err != nil { + done <- tcpConn{nil, err} + return + } + + newConn, err := net.DialTCP("tcp", nil, addr) + if err != nil { + done <- tcpConn{nil, err} + return + } + + done <- tcpConn{newConn, nil} + return + }() + + select { + case <-time.After(timeout): + go func() { + if tcpResult := <-done; tcpResult.conn != nil { + tcpResult.conn.Close() + } + }() + return nil, errTCPTimeout + case tcpResult := <-done: + if tcpResult.conn == nil || tcpResult.err != nil { + return nil, tcpResult.err + } + return tcpResult.conn, nil + } +} + +// Set additional keepalive parameters. +// Uses new interfaces introduced in Go1.11, which let us get connection's file +// descriptor, without blocking, and therefore without uncontrolled spawning of +// threads (not goroutines, actual threads). +func (w *IMAPWorker) setKeepaliveParameters(conn *net.TCPConn) error { + err := conn.SetKeepAlive(true) + if err != nil { + return err + } + // Idle time before sending a keepalive probe + err = conn.SetKeepAlivePeriod(w.config.keepalive_period) + if err != nil { + return err + } + rawConn, e := conn.SyscallConn() + if e != nil { + return e + } + err = rawConn.Control(func(fdPtr uintptr) { + fd := int(fdPtr) + // Max number of probes before failure + err := lib.SetTcpKeepaliveProbes(fd, w.config.keepalive_probes) + if err != nil { + w.worker.Logger.Printf( + "cannot set tcp keepalive probes: %v\n", err) + } + // Wait time after an unsuccessful probe + err = lib.SetTcpKeepaliveInterval(fd, w.config.keepalive_interval) + if err != nil { + w.worker.Logger.Printf( + "cannot set tcp keepalive interval: %v\n", err) + } + }) + return err +} diff --git a/worker/imap/worker.go b/worker/imap/worker.go index 1ff6341..eabaae0 100644 --- a/worker/imap/worker.go +++ b/worker/imap/worker.go @@ -1,9 +1,7 @@ package imap import ( - "crypto/tls" "fmt" - "net" "net/url" "time" @@ -25,6 +23,7 @@ func init() { var ( errUnsupported = fmt.Errorf("unsupported command") + errClientNotReady = fmt.Errorf("client not ready") errNotConnected = fmt.Errorf("not connected") errAlreadyConnected = fmt.Errorf("already connected") ) @@ -93,6 +92,15 @@ func (w *IMAPWorker) handleMessage(msg types.WorkerMessage) error { var reterr error // will be returned at the end, needed to support idle + // when client is nil allow only certain messages to be handled + if w.client == nil { + switch msg.(type) { + case *types.Connect, *types.Reconnect, *types.Disconnect, *types.Configure: + default: + return errClientNotReady + } + } + // set connection timeout for calls to imap server if w.client != nil { w.client.Timeout = w.config.connection_timeout @@ -251,124 +259,6 @@ func (w *IMAPWorker) handleImapUpdate(update client.Update) { } } -func (w *IMAPWorker) connect() (*client.Client, error) { - var ( - conn *net.TCPConn - c *client.Client - ) - - addr, err := net.ResolveTCPAddr("tcp", w.config.addr) - if err != nil { - return nil, err - } - - conn, err = net.DialTCP("tcp", nil, addr) - if err != nil { - return nil, err - } - - if w.config.connection_timeout > 0 { - end := time.Now().Add(w.config.connection_timeout) - err = conn.SetDeadline(end) - if err != nil { - return nil, err - } - } - if w.config.keepalive_period > 0 { - err = w.setKeepaliveParameters(conn) - if err != nil { - return nil, err - } - } - - serverName, _, _ := net.SplitHostPort(w.config.addr) - tlsConfig := &tls.Config{ServerName: serverName} - - switch w.config.scheme { - case "imap": - c, err = client.New(conn) - if err != nil { - return nil, err - } - if !w.config.insecure { - if err = c.StartTLS(tlsConfig); err != nil { - return nil, err - } - } - case "imaps": - tlsConn := tls.Client(conn, tlsConfig) - c, err = client.New(tlsConn) - if err != nil { - return nil, err - } - default: - return nil, fmt.Errorf("Unknown IMAP scheme %s", w.config.scheme) - } - - c.ErrorLog = w.worker.Logger - - if w.config.user != nil { - username := w.config.user.Username() - password, hasPassword := w.config.user.Password() - if !hasPassword { - // TODO: ask password - } - - if w.config.oauthBearer.Enabled { - if err := w.config.oauthBearer.Authenticate( - username, password, c); err != nil { - return nil, err - } - } else if err := c.Login(username, password); err != nil { - return nil, err - } - } - - c.SetDebug(w.worker.Logger.Writer()) - - if _, err := c.Select(imap.InboxName, false); err != nil { - return nil, err - } - - return c, nil -} - -// Set additional keepalive parameters. -// Uses new interfaces introduced in Go1.11, which let us get connection's file -// descriptor, without blocking, and therefore without uncontrolled spawning of -// threads (not goroutines, actual threads). -func (w *IMAPWorker) setKeepaliveParameters(conn *net.TCPConn) error { - err := conn.SetKeepAlive(true) - if err != nil { - return err - } - // Idle time before sending a keepalive probe - err = conn.SetKeepAlivePeriod(w.config.keepalive_period) - if err != nil { - return err - } - rawConn, e := conn.SyscallConn() - if e != nil { - return e - } - err = rawConn.Control(func(fdPtr uintptr) { - fd := int(fdPtr) - // Max number of probes before failure - err := lib.SetTcpKeepaliveProbes(fd, w.config.keepalive_probes) - if err != nil { - w.worker.Logger.Printf( - "cannot set tcp keepalive probes: %v\n", err) - } - // Wait time after an unsuccessful probe - err = lib.SetTcpKeepaliveInterval(fd, w.config.keepalive_interval) - if err != nil { - w.worker.Logger.Printf( - "cannot set tcp keepalive interval: %v\n", err) - } - }) - return err -} - func (w *IMAPWorker) Run() { for { select {