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
105 lines
2.8 KiB
Go
105 lines
2.8 KiB
Go
package imapx
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"net"
|
|
"time"
|
|
|
|
"github.com/emersion/go-imap/v2/imapclient"
|
|
)
|
|
|
|
type Endpoint struct {
|
|
Host string
|
|
Port int
|
|
TLSMode string // ssl | starttls | plain
|
|
}
|
|
|
|
func (e Endpoint) addr() string { return fmt.Sprintf("%s:%d", e.Host, e.Port) }
|
|
|
|
// 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":
|
|
// 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":
|
|
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":
|
|
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
|
|
for i := 0; i < attempts; i++ {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
c, err := dialOnce(ctx, ep)
|
|
if err == nil {
|
|
return c, nil
|
|
}
|
|
lastErr = err
|
|
if i < attempts-1 {
|
|
backoff := time.Duration(200*(i+1)) * time.Millisecond
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
case <-time.After(backoff):
|
|
}
|
|
}
|
|
}
|
|
return nil, lastErr
|
|
}
|
|
|
|
func TestEndpoint(ctx context.Context, ep Endpoint) error {
|
|
c, err := Connect(ctx, ep)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.Logout().Wait()
|
|
}
|