Add body read timeout recovery to IMAP client
Introduce Client wrapper with socket deadline support Add reconnection logic for body read timeouts Implement test cases for underflow scenarios Update orchestrator to handle reconnections
This commit is contained in:
+39
-5
@@ -18,6 +18,29 @@ type Endpoint struct {
|
||||
|
||||
func (e Endpoint) addr() string { return fmt.Sprintf("%s:%d", e.Host, e.Port) }
|
||||
|
||||
// Client wraps an imapclient.Client together with the raw network connection it
|
||||
// runs over. The embedded *imapclient.Client provides the full IMAP API; the
|
||||
// retained conn lets callers impose a read deadline on the socket for the
|
||||
// duration of a body transfer.
|
||||
//
|
||||
// This defends against servers that announce a BODY[] literal LARGER than the
|
||||
// bytes they actually send (a protocol violation observed on some webmail
|
||||
// servers). go-imap reads a literal strictly by its announced size, so a short
|
||||
// literal makes it block forever waiting for bytes that never arrive. A
|
||||
// deadline around the body read turns that infinite hang into a timeout the
|
||||
// copier can recover from.
|
||||
type Client struct {
|
||||
*imapclient.Client
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
// SetReadDeadline sets (or, with the zero time, clears) a deadline on the
|
||||
// underlying socket. Used to bound a single body read; always cleared again
|
||||
// once the read completes so it never affects idle periods.
|
||||
func (c *Client) SetReadDeadline(t time.Time) error {
|
||||
return c.conn.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
// dialTimeout bounds establishing the TCP connection.
|
||||
const dialTimeout = 30 * time.Second
|
||||
|
||||
@@ -27,7 +50,7 @@ const dialTimeout = 30 * time.Second
|
||||
// during a long src scan) from one stuck mid-response, and would wrongly close
|
||||
// idle connections. Stall detection is done at the orchestrator level via a
|
||||
// progress watchdog; go-imap's own per-command timeouts bound active commands.
|
||||
func dialOnce(ctx context.Context, ep Endpoint) (*imapclient.Client, error) {
|
||||
func dialOnce(ctx context.Context, ep Endpoint) (*Client, error) {
|
||||
d := &net.Dialer{Timeout: dialTimeout}
|
||||
raw, err := d.DialContext(ctx, "tcp", ep.addr())
|
||||
if err != nil {
|
||||
@@ -42,16 +65,27 @@ func dialOnce(ctx context.Context, ep Endpoint) (*imapclient.Client, error) {
|
||||
_ = raw.Close()
|
||||
return nil, err
|
||||
}
|
||||
return waitGreeting(imapclient.New(tlsConn, nil))
|
||||
c, err := waitGreeting(imapclient.New(tlsConn, nil))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Client{Client: c, conn: tlsConn}, nil
|
||||
case "starttls":
|
||||
// Deadline goes on the raw TCP conn: it sits beneath the TLS layer that
|
||||
// NewStartTLS negotiates, and a TCP read deadline still interrupts the
|
||||
// TLS read above it.
|
||||
opts := &imapclient.Options{TLSConfig: &tls.Config{ServerName: ep.Host}}
|
||||
c, err := imapclient.NewStartTLS(raw, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
return &Client{Client: c, conn: raw}, nil
|
||||
case "plain":
|
||||
return waitGreeting(imapclient.New(raw, nil))
|
||||
c, err := waitGreeting(imapclient.New(raw, nil))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Client{Client: c, conn: raw}, nil
|
||||
default:
|
||||
_ = raw.Close()
|
||||
return nil, fmt.Errorf("unknown tls_mode %q", ep.TLSMode)
|
||||
@@ -69,7 +103,7 @@ func waitGreeting(c *imapclient.Client) (*imapclient.Client, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func Connect(ctx context.Context, ep Endpoint) (*imapclient.Client, error) {
|
||||
func Connect(ctx context.Context, ep Endpoint) (*Client, error) {
|
||||
const attempts = 3
|
||||
var lastErr error
|
||||
for i := 0; i < attempts; i++ {
|
||||
|
||||
Reference in New Issue
Block a user