Remove idle connection timeout handling

Remove error handling for closed connections

Add progress watchdog to detect stalled accounts

Improve error modal styling and pagination
This commit is contained in:
2026-07-05 14:50:25 +07:00
parent 95ddbf5619
commit e84366eb0c
7 changed files with 191 additions and 145 deletions
+15 -17
View File
@@ -18,51 +18,49 @@ type Endpoint struct {
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.
// dialTimeout bounds establishing the TCP connection.
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.
// dialOnce establishes one connection and returns a ready *Client. ctx bounds
// the TCP dial. We deliberately do NOT impose a socket-level read deadline:
// a blanket read deadline can't tell an idle connection (e.g. dst sitting idle
// 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) {
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"}})
tlsConn := tls.Client(raw, &tls.Config{ServerName: ep.Host, NextProtos: []string{"imap"}})
if err := tlsConn.HandshakeContext(ctx); err != nil {
_ = conn.Close()
_ = raw.Close()
return nil, err
}
c := imapclient.New(tlsConn, nil)
return waitGreeting(c)
return waitGreeting(imapclient.New(tlsConn, nil))
case "starttls":
opts := &imapclient.Options{TLSConfig: &tls.Config{ServerName: ep.Host}}
c, err := imapclient.NewStartTLS(conn, opts)
c, err := imapclient.NewStartTLS(raw, opts)
if err != nil {
return nil, err
}
return c, nil
case "plain":
c := imapclient.New(conn, nil)
return waitGreeting(c)
return waitGreeting(imapclient.New(raw, nil))
default:
_ = conn.Close()
_ = raw.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.
// caught at connect time 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()