Remove error handling for closed connections Add progress watchdog to detect stalled accounts Improve error modal styling and pagination
103 lines
2.7 KiB
Go
103 lines
2.7 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.
|
|
const dialTimeout = 30 * time.Second
|
|
|
|
// 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
|
|
}
|
|
|
|
switch ep.TLSMode {
|
|
case "ssl":
|
|
// NextProtos mirrors imapclient.DialTLS's ALPN advertisement.
|
|
tlsConn := tls.Client(raw, &tls.Config{ServerName: ep.Host, NextProtos: []string{"imap"}})
|
|
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
|
_ = raw.Close()
|
|
return nil, err
|
|
}
|
|
return waitGreeting(imapclient.New(tlsConn, nil))
|
|
case "starttls":
|
|
opts := &imapclient.Options{TLSConfig: &tls.Config{ServerName: ep.Host}}
|
|
c, err := imapclient.NewStartTLS(raw, opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return c, nil
|
|
case "plain":
|
|
return waitGreeting(imapclient.New(raw, nil))
|
|
default:
|
|
_ = 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 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()
|
|
}
|