Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 72 additions & 19 deletions sender/sender.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,31 @@ import (
"go.mozilla.org/pkcs7"
)

// loginAuth implements the SMTP LOGIN authentication mechanism.
// Some SMTP servers (e.g. Mailo) only support LOGIN and not PLAIN.
type loginAuth struct {
username, password string
}

func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
return "LOGIN", nil, nil
}

func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
if !more {
return nil, nil
}
prompt := strings.TrimSpace(string(fromServer))
switch strings.ToLower(prompt) {
case "username:":
return []byte(a.username), nil
case "password:":
return []byte(a.password), nil
default:
return nil, fmt.Errorf("unexpected LOGIN prompt: %s", prompt)
}
}

// generateMessageID creates a unique Message-ID header.
func generateMessageID(from string) string {
buf := make([]byte, 16)
Expand All @@ -42,7 +67,8 @@ func SendEmail(account *config.Account, to, cc, bcc []string, subject, plainBody
return fmt.Errorf("unsupported or missing service_provider: %s", account.ServiceProvider)
}

auth := smtp.PlainAuth("", account.Email, account.Password, smtpServer)
plainAuth := smtp.PlainAuth("", account.Email, account.Password, smtpServer)
loginAuthFallback := &loginAuth{username: account.Email, password: account.Password}

fromHeader := account.FetchEmail
if account.Name != "" {
Expand Down Expand Up @@ -325,37 +351,64 @@ func SendEmail(account *config.Account, to, cc, bcc []string, subject, plainBody

addr := fmt.Sprintf("%s:%d", smtpServer, smtpPort)

// Custom SMTP dialer to support skipping TLS verification for Proton Bridge
c, err := smtp.Dial(addr)
if err != nil {
return err
tlsConfig := &tls.Config{
ServerName: smtpServer,
InsecureSkipVerify: account.Insecure,
}
defer c.Close()

if err = c.Hello("localhost"); err != nil {
return err
}
var c *smtp.Client

// Trigger STARTTLS if supported
if ok, _ := c.Extension("STARTTLS"); ok {
tlsConfig := &tls.Config{
ServerName: smtpServer,
InsecureSkipVerify: account.Insecure,
// Port 465 uses implicit TLS (the connection starts with TLS).
// All other ports use plain TCP with optional STARTTLS upgrade.
if smtpPort == 465 {
conn, err := tls.Dial("tcp", addr, tlsConfig)
if err != nil {
return err
}
if err = c.StartTLS(tlsConfig); err != nil {
c, err = smtp.NewClient(conn, smtpServer)
if err != nil {
conn.Close()
return err
}
} else {
var err error
c, err = smtp.Dial(addr)
if err != nil {
return err
}
}
defer c.Close()

if err = c.Hello("localhost"); err != nil {
return err
}

// Authenticate
if auth != nil {
if ok, _ := c.Extension("AUTH"); ok {
if err = c.Auth(auth); err != nil {
// Trigger STARTTLS if supported (not needed for implicit TLS on port 465)
if smtpPort != 465 {
if ok, _ := c.Extension("STARTTLS"); ok {
if err = c.StartTLS(tlsConfig); err != nil {
return err
}
}
}

// Authenticate using the best available mechanism.
// c.Extension("AUTH") returns the list of supported mechanisms.
if ok, mechs := c.Extension("AUTH"); ok {
mechList := strings.ToUpper(mechs)
if strings.Contains(mechList, "PLAIN") {
err = c.Auth(plainAuth)
} else if strings.Contains(mechList, "LOGIN") {
err = c.Auth(loginAuthFallback)
} else {
// Fall back to PLAIN and let the server decide
err = c.Auth(plainAuth)
}
if err != nil {
return err
}
}

// Send Envelope
if err = c.Mail(account.Email); err != nil {
return err
Expand Down