From 01e776498fba9365abe2d8ebba11e0fbfdd43ec9 Mon Sep 17 00:00:00 2001 From: Drew Smirnoff Date: Thu, 25 Jun 2026 02:36:56 +0400 Subject: [PATCH] fix: gmail 555 error when sending (#1496) ## 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 --- sender/sender.go | 20 ++++++++++++++++++-- sender/sender_test.go | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/sender/sender.go b/sender/sender.go index db87055c4a5feb4f0f09022c06ff15255bd22662..0a1ac1d6dc711491bd13851cd16724262f4dc3b5 100644 --- a/sender/sender.go +++ b/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 " 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 { diff --git a/sender/sender_test.go b/sender/sender_test.go index d6a0efb8db48baca5d323d489bd502495fb8aefe..a323db2474a044274bbf23e110a417381835b12c 100644 --- a/sender/sender_test.go +++ b/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 ", + expected: "user@example.com", + }, + { + name: "formatted with quoted name", + input: "\"John Doe\" ", + 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) + } + }) + } +}