diff --git a/internal/mail/send.go b/internal/mail/send.go index f496f53..72ee450 100644 --- a/internal/mail/send.go +++ b/internal/mail/send.go @@ -46,6 +46,18 @@ func (m OutgoingMessage) Recipients() []string { return out } +// envelopeFrom returns the bare address for the SMTP envelope sender, stripping +// any display name. A display-name From (e.g. "Name ") is a valid header +// but an invalid envelope sender, so it must be reduced to the bare address. +// Unparseable input is passed through unchanged (preserves prior behaviour for +// plain addresses). +func envelopeFrom(from string) string { + if a, err := gomail.ParseAddress(from); err == nil { + return a.Address + } + return from +} + func addrList(addrs []string) []*gomail.Address { out := make([]*gomail.Address, 0, len(addrs)) for _, a := range addrs { @@ -163,7 +175,7 @@ func SendSMTP(cfg SMTPConfig, m OutgoingMessage) error { if err := c.Auth(auth); err != nil { return fmt.Errorf("smtp auth: %w", err) } - if err := c.SendMail(m.From, m.Recipients(), bytes.NewReader(raw)); err != nil { + if err := c.SendMail(envelopeFrom(m.From), m.Recipients(), bytes.NewReader(raw)); err != nil { return fmt.Errorf("smtp send: %w", err) } return c.Quit() diff --git a/internal/mail/send_test.go b/internal/mail/send_test.go index e8aeaff..8d686a9 100644 --- a/internal/mail/send_test.go +++ b/internal/mail/send_test.go @@ -100,6 +100,36 @@ func TestRecipientsCombinesAllFields(t *testing.T) { } } +func TestEnvelopeFromStripsDisplayName(t *testing.T) { + cases := map[string]string{ + "Steve Cliff ": "me@stevecliff.com", + "me@stevecliff.com": "me@stevecliff.com", + "": "me@stevecliff.com", + "not a valid address": "not a valid address", // unparseable ⇒ passthrough + } + for in, want := range cases { + if got := envelopeFrom(in); got != want { + t.Fatalf("envelopeFrom(%q) = %q, want %q", in, got, want) + } + } +} + +func TestBuildMIMEKeepsDisplayNameInHeader(t *testing.T) { + raw, err := BuildMIME(OutgoingMessage{ + From: "Steve Cliff ", + To: []string{"you@example.com"}, + Subject: "hi", + BodyText: "body", + Date: time.Date(2026, 6, 23, 12, 0, 0, 0, time.UTC), + }) + if err != nil { + t.Fatalf("BuildMIME: %v", err) + } + if !strings.Contains(string(raw), "Steve Cliff") { + t.Fatalf("From header lost display name:\n%s", raw) + } +} + func TestReadHeaderParsesReferences(t *testing.T) { raw := "From: a@x.com\r\n" + "To: b@x.com\r\n" +