Add IMAP connection timeout handling
Introduce idleConn wrapper to prevent wedged connections Add test for silent server timeout behavior Implement proper TLS handshake and greeting handling Set explicit dial and read timeouts
This commit is contained in:
+46
-9
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-imap/v2/imapclient"
|
||||
@@ -17,23 +18,59 @@ type Endpoint struct {
|
||||
|
||||
func (e Endpoint) addr() string { return fmt.Sprintf("%s:%d", e.Host, e.Port) }
|
||||
|
||||
func dialOnce(ep Endpoint) (*imapclient.Client, error) {
|
||||
// dialTimeout bounds establishing the TCP connection (matches go-imap's own
|
||||
// default). The subsequent idleReadTimeout governs reads once connected.
|
||||
const dialTimeout = 30 * time.Second
|
||||
|
||||
// dialOnce establishes one connection and returns a ready *Client whose reads
|
||||
// are guarded by idleReadTimeout. Unlike imapclient.Dial*, the underlying TCP
|
||||
// conn is wrapped in idleConn so a server that stops responding mid-command
|
||||
// unblocks the read instead of hanging forever. ctx bounds the TCP dial.
|
||||
func dialOnce(ctx context.Context, ep Endpoint) (*imapclient.Client, error) {
|
||||
d := &net.Dialer{Timeout: dialTimeout}
|
||||
raw, err := d.DialContext(ctx, "tcp", ep.addr())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn := &idleConn{Conn: raw, timeout: idleReadTimeout}
|
||||
|
||||
switch ep.TLSMode {
|
||||
case "ssl":
|
||||
return imapclient.DialTLS(ep.addr(), &imapclient.Options{
|
||||
TLSConfig: &tls.Config{ServerName: ep.Host},
|
||||
})
|
||||
// NextProtos mirrors imapclient.DialTLS's ALPN advertisement.
|
||||
tlsConn := tls.Client(conn, &tls.Config{ServerName: ep.Host, NextProtos: []string{"imap"}})
|
||||
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
c := imapclient.New(tlsConn, nil)
|
||||
return waitGreeting(c)
|
||||
case "starttls":
|
||||
return imapclient.DialStartTLS(ep.addr(), &imapclient.Options{
|
||||
TLSConfig: &tls.Config{ServerName: ep.Host},
|
||||
})
|
||||
opts := &imapclient.Options{TLSConfig: &tls.Config{ServerName: ep.Host}}
|
||||
c, err := imapclient.NewStartTLS(conn, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
case "plain":
|
||||
return imapclient.DialInsecure(ep.addr(), nil)
|
||||
c := imapclient.New(conn, nil)
|
||||
return waitGreeting(c)
|
||||
default:
|
||||
_ = conn.Close()
|
||||
return nil, fmt.Errorf("unknown tls_mode %q", ep.TLSMode)
|
||||
}
|
||||
}
|
||||
|
||||
// waitGreeting blocks for the server's initial greeting so a mute server is
|
||||
// caught at connect time (bounded by idleReadTimeout) rather than at the first
|
||||
// command. NewStartTLS already awaits the greeting during its STARTTLS upgrade.
|
||||
func waitGreeting(c *imapclient.Client) (*imapclient.Client, error) {
|
||||
if err := c.WaitGreeting(); err != nil {
|
||||
_ = c.Close()
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func Connect(ctx context.Context, ep Endpoint) (*imapclient.Client, error) {
|
||||
const attempts = 3
|
||||
var lastErr error
|
||||
@@ -41,7 +78,7 @@ func Connect(ctx context.Context, ep Endpoint) (*imapclient.Client, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c, err := dialOnce(ep)
|
||||
c, err := dialOnce(ctx, ep)
|
||||
if err == nil {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user