fix: add support for STARTTLS (#219)

Drew Smirnoff created

Signed-off-by: drew <me@andrinoff.com>

Change summary

config/config.go   |  3 ++
fetcher/fetcher.go | 28 ++++++++++++++++++++--
sender/sender.go   | 59 +++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 86 insertions(+), 4 deletions(-)

Detailed changes

config/config.go 🔗

@@ -27,6 +27,7 @@ type Account struct {
 	IMAPPort   int    `json:"imap_port,omitempty"`
 	SMTPServer string `json:"smtp_server,omitempty"`
 	SMTPPort   int    `json:"smtp_port,omitempty"`
+	Insecure   bool   `json:"insecure,omitempty"`
 }
 
 // MailingList represents a named group of email addresses.
@@ -170,6 +171,7 @@ func LoadConfig() (*Config, error) {
 		IMAPPort        int    `json:"imap_port,omitempty"`
 		SMTPServer      string `json:"smtp_server,omitempty"`
 		SMTPPort        int    `json:"smtp_port,omitempty"`
+		Insecure        bool   `json:"insecure,omitempty"`
 	}
 	type diskConfig struct {
 		Accounts      []rawAccount  `json:"accounts"`
@@ -215,6 +217,7 @@ func LoadConfig() (*Config, error) {
 			IMAPPort:        rawAcc.IMAPPort,
 			SMTPServer:      rawAcc.SMTPServer,
 			SMTPPort:        rawAcc.SMTPPort,
+			Insecure:        rawAcc.Insecure,
 		}
 
 		if rawAcc.Password != "" {

fetcher/fetcher.go 🔗

@@ -2,6 +2,7 @@ package fetcher
 
 import (
 	"bytes"
+	"crypto/tls"
 	"encoding/base64"
 	"fmt"
 	"io"
@@ -111,9 +112,30 @@ func connect(account *config.Account) (*client.Client, error) {
 	}
 
 	addr := fmt.Sprintf("%s:%d", imapServer, imapPort)
-	c, err := client.DialTLS(addr, nil)
-	if err != nil {
-		return nil, err
+
+	tlsConfig := &tls.Config{
+		ServerName:         imapServer,
+		InsecureSkipVerify: account.Insecure,
+	}
+
+	var c *client.Client
+	var err error
+
+	// If using standard non-implicit ports (1143 or 143), use Dial + STARTTLS
+	if imapPort == 1143 || imapPort == 143 {
+		c, err = client.Dial(addr)
+		if err != nil {
+			return nil, err
+		}
+		if err := c.StartTLS(tlsConfig); err != nil {
+			return nil, err
+		}
+	} else {
+		// Otherwise default to implicit TLS (port 993)
+		c, err = client.DialTLS(addr, tlsConfig)
+		if err != nil {
+			return nil, err
+		}
 	}
 
 	if err := c.Login(account.Email, account.Password); err != nil {

sender/sender.go 🔗

@@ -3,6 +3,7 @@ package sender
 import (
 	"bytes"
 	"crypto/rand"
+	"crypto/tls"
 	"encoding/base64"
 	"fmt"
 	"mime"
@@ -166,7 +167,63 @@ func SendEmail(account *config.Account, to, cc, bcc []string, subject, plainBody
 	allRecipients = append(allRecipients, bcc...)
 
 	addr := fmt.Sprintf("%s:%d", smtpServer, smtpPort)
-	return smtp.SendMail(addr, auth, account.Email, allRecipients, msg.Bytes())
+
+	// Custom SMTP dialer to support skipping TLS verification for Proton Bridge
+	c, err := smtp.Dial(addr)
+	if err != nil {
+		return err
+	}
+	defer c.Close()
+
+	if err = c.Hello("localhost"); err != nil {
+		return err
+	}
+
+	// Trigger STARTTLS if supported
+	if ok, _ := c.Extension("STARTTLS"); ok {
+		tlsConfig := &tls.Config{
+			ServerName:         smtpServer,
+			InsecureSkipVerify: account.Insecure,
+		}
+		if err = c.StartTLS(tlsConfig); err != nil {
+			return err
+		}
+	}
+
+	// Authenticate
+	if auth != nil {
+		if ok, _ := c.Extension("AUTH"); ok {
+			if err = c.Auth(auth); err != nil {
+				return err
+			}
+		}
+	}
+
+	// Send Envelope
+	if err = c.Mail(account.Email); err != nil {
+		return err
+	}
+	for _, r := range allRecipients {
+		if err = c.Rcpt(r); err != nil {
+			return err
+		}
+	}
+
+	// Write Data
+	w, err := c.Data()
+	if err != nil {
+		return err
+	}
+	_, err = w.Write(msg.Bytes())
+	if err != nil {
+		return err
+	}
+	err = w.Close()
+	if err != nil {
+		return err
+	}
+
+	return c.Quit()
 }
 
 // wrapBase64 wraps base64-encoded data at 76 characters per line as required by MIME.