fix: gmail 555 error when sending (#1496)

Drew Smirnoff created

## What?

Fixed the SMTP "555 5.5.2 Syntax error" that occurred when sending
emails with CatchAll
enabled by formatting the bare email

## Why?

When CatchAll is enabled, the `From` field displays a formatted address
like
"`Name email@example.com`". This formatted value was being passed
directly to Gmail's SMTP
MAIL FROM  command, which expects only the bare email address.

According to RFC 5321 (SMTP protocol), the MAIL FROM command must
contain only the
mailbox (email address), not a formatted display name. The display name
belongs in the
message headers (the From: header), which is separate from the SMTP
envelope.

Fixes #1449

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

Change summary

sender/sender.go      | 20 ++++++++++++++++++--
sender/sender_test.go | 43 +++++++++++++++++++++++++++++++++++++++++++
2 files changed, 61 insertions(+), 2 deletions(-)

Detailed changes

sender/sender.go 🔗

@@ -13,6 +13,7 @@ import (
 	"mime"
 	"mime/multipart"
 	"mime/quotedprintable"
+	"net/mail"
 	"net/smtp"
 	"net/textproto"
 	"os"
@@ -106,6 +107,21 @@ func smtpHelloHostname() string {
 	return hostname
 }
 
+// extractBareEmail extracts just the email address from a formatted address
+// like "Name <email@example.com>" or returns the input if it's already bare.
+// This is needed for SMTP MAIL FROM command which requires only the email address.
+func extractBareEmail(addr string) string {
+	if addr == "" {
+		return ""
+	}
+	parsed, err := mail.ParseAddress(addr)
+	if err != nil {
+		// If parsing fails, return as-is (it might already be bare)
+		return addr
+	}
+	return parsed.Address
+}
+
 // generateMessageID creates a unique Message-ID header.
 func generateMessageID(from string) string {
 	buf := make([]byte, 16)
@@ -742,7 +758,7 @@ func SendEmail(account *config.Account, to, cc, bcc []string, subject, plainBody
 	}
 
 	// Send Envelope
-	if err = c.Mail(account.GetSendAsEmail()); err != nil {
+	if err = c.Mail(extractBareEmail(account.GetSendAsEmail())); err != nil {
 		return nil, err
 	}
 	for _, r := range allRecipients {
@@ -954,7 +970,7 @@ func SendCalendarReply(account *config.Account, to []string, subject, plainBody
 		}
 	}
 
-	if err = c.Mail(account.GetSendAsEmail()); err != nil {
+	if err = c.Mail(extractBareEmail(account.GetSendAsEmail())); err != nil {
 		return nil, err
 	}
 	for _, r := range to {

sender/sender_test.go 🔗

@@ -130,3 +130,46 @@ func TestGenerateMessageID(t *testing.T) {
 		t.Errorf("Message-ID has an empty random part, got %s", msgID)
 	}
 }
+
+func TestExtractBareEmail(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    string
+		expected string
+	}{
+		{
+			name:     "bare email",
+			input:    "user@example.com",
+			expected: "user@example.com",
+		},
+		{
+			name:     "formatted with name",
+			input:    "John Doe <user@example.com>",
+			expected: "user@example.com",
+		},
+		{
+			name:     "formatted with quoted name",
+			input:    "\"John Doe\" <user@example.com>",
+			expected: "user@example.com",
+		},
+		{
+			name:     "empty string",
+			input:    "",
+			expected: "",
+		},
+		{
+			name:     "invalid format returns as-is",
+			input:    "not-an-email",
+			expected: "not-an-email",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := extractBareEmail(tt.input)
+			if got != tt.expected {
+				t.Errorf("extractBareEmail(%q) = %q, want %q", tt.input, got, tt.expected)
+			}
+		})
+	}
+}