diff --git a/fetcher/fetcher.go b/fetcher/fetcher.go index 0b956663..98919e14 100644 --- a/fetcher/fetcher.go +++ b/fetcher/fetcher.go @@ -416,17 +416,24 @@ func connectWithOptions(account *config.Account, extraOpts *imapclient.Options) var c *imapclient.Client var err error - // If using standard non-implicit ports (1143 or 143), use DialStartTLS - if imapPort == 1143 || imapPort == 143 { - c, err = imapclient.DialStartTLS(addr, options) + // Port 993 always uses implicit TLS. For all other ports, try STARTTLS + // first to support local bridges (e.g. Proton Mail Bridge) that serve + // IMAP on non-standard ports. If STARTTLS fails, fall back to implicit + // TLS for servers that use non-standard ports for implicit TLS + // (e.g. GreenMail on port 3993). + if imapPort == 993 { + c, err = imapclient.DialTLS(addr, options) if err != nil { return nil, err } } else { - // Otherwise default to implicit TLS (port 993) - c, err = imapclient.DialTLS(addr, options) + c, err = imapclient.DialStartTLS(addr, options) if err != nil { - return nil, err + // STARTTLS failed; try implicit TLS. + c, err = imapclient.DialTLS(addr, options) + if err != nil { + return nil, err + } } } diff --git a/fetcher/fetcher_chunk_test.go b/fetcher/fetcher_chunk_test.go index c5a0f72f..c1284b2e 100644 --- a/fetcher/fetcher_chunk_test.go +++ b/fetcher/fetcher_chunk_test.go @@ -71,13 +71,14 @@ func TestFetchMailboxEmailsUsesRequestedLimitForSmallFetchChunks(t *testing.T) { func startFetchRecorderIMAPServer(t *testing.T, messages uint32, fetchCommands chan<- string) (string, func()) { t.Helper() - listener, err := tls.Listen("tcp", "127.0.0.1:0", &tls.Config{ - Certificates: []tls.Certificate{newTestTLSCertificate(t)}, - }) + // Use plain TCP with STARTTLS support so the test matches the real-world + // connection flow used by connectWithOptions for non-993 ports. + listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("starting test IMAP server: %v", err) } + cert := newTestTLSCertificate(t) var closeOnce sync.Once var connMu sync.Mutex var conn net.Conn @@ -100,13 +101,13 @@ func startFetchRecorderIMAPServer(t *testing.T, messages uint32, fetchCommands c connMu.Lock() conn = accepted connMu.Unlock() - serveFetchRecorderIMAPConn(accepted, messages, fetchCommands) + serveFetchRecorderIMAPConn(accepted, messages, fetchCommands, cert) }() return listener.Addr().String(), closeServer } -func serveFetchRecorderIMAPConn(conn net.Conn, messages uint32, fetchCommands chan<- string) { +func serveFetchRecorderIMAPConn(conn net.Conn, messages uint32, fetchCommands chan<- string, cert tls.Certificate) { defer conn.Close() reader := bufio.NewReader(conn) @@ -136,12 +137,25 @@ func serveFetchRecorderIMAPConn(conn net.Conn, messages uint32, fetchCommands ch tag := fields[0] switch strings.ToUpper(fields[1]) { case "CAPABILITY": - if !writeIMAPLine("* CAPABILITY IMAP4rev1 AUTH=PLAIN") { + if !writeIMAPLine("* CAPABILITY IMAP4rev1 AUTH=PLAIN STARTTLS") { return } if !writeIMAPLine("%s OK CAPABILITY completed", tag) { return } + case "STARTTLS": + if !writeIMAPLine("%s OK BEGIN TLS negotiation", tag) { + return + } + writer.Flush() + tlsConn := tls.Server(conn, &tls.Config{ + Certificates: []tls.Certificate{cert}, + }) + if err := tlsConn.Handshake(); err != nil { + return + } + reader = bufio.NewReader(tlsConn) + writer = bufio.NewWriter(tlsConn) case "LOGIN": if !writeIMAPLine("%s OK LOGIN completed", tag) { return