From 91d10a7650160b94c08b1e140d11b0d9b7ea5fdb Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Fri, 11 Sep 2026 11:00:40 +0000 Subject: [PATCH] fix: send the bare address as SMTP envelope sender so mailcow DKIM-signs --- mail/sender.go | 25 +++++++++++++++++++------ mail/sender_test.go | 17 ++++++++++++++++- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/mail/sender.go b/mail/sender.go index 5d4d506..c021a4d 100644 --- a/mail/sender.go +++ b/mail/sender.go @@ -16,6 +16,7 @@ import ( "mime/multipart" "mime/quotedprintable" "net" + netmail "net/mail" "net/smtp" "net/textproto" "os" @@ -136,11 +137,11 @@ func (s Sender) send(m message) error { } } - if err := client.Mail(s.From); err != nil { + if err := client.Mail(addrSpec(s.From)); err != nil { return fmt.Errorf("smtp: mail from: %w", err) } for _, rcpt := range rcpts { - if err := client.Rcpt(rcpt); err != nil { + if err := client.Rcpt(addrSpec(rcpt)); err != nil { return fmt.Errorf("smtp: rcpt %s: %w", rcpt, err) } } @@ -163,6 +164,20 @@ func (s Sender) send(m message) error { return client.Quit() } +// addrSpec is the bare address for the SMTP envelope. SMTP_FROM is usually +// "Vantage ", which belongs in the From header only: +// sent as MAIL FROM it became ">". Postfix +// salvaged the address, so delivery and Return-Path looked fine, but rspamd +// saw no envelope sender and mailcow skipped DKIM signing, so Gmail filed +// every Vantage email as spam. Anything that does not parse is passed through +// for the server to judge. +func addrSpec(v string) string { + if a, err := netmail.ParseAddress(v); err == nil { + return a.Address + } + return strings.TrimSpace(v) +} + func recipients(to string) []string { parts := strings.Split(to, ",") out := make([]string, 0, len(parts)) @@ -184,10 +199,8 @@ func recipients(to string) []string { // // Both parts are quoted-printable. They were raw UTF-8 with no // Content-Transfer-Encoding, which means 7bit, and every template carries -// non-ASCII (the middot in the masthead at least). rspamd scored that -// R_BAD_CTE_7BIT, the message went out without a DKIM signature, and Gmail -// filed it as spam, while mail from the same mailbox via SOGo, sent -// quoted-printable, was signed and delivered. +// non-ASCII (the middot in the masthead at least), which rspamd scored as +// R_BAD_CTE_7BIT. func (s Sender) envelope(m message) ([]byte, error) { var parts strings.Builder w := multipart.NewWriter(&parts) diff --git a/mail/sender_test.go b/mail/sender_test.go index e291d36..6ddb906 100644 --- a/mail/sender_test.go +++ b/mail/sender_test.go @@ -11,9 +11,24 @@ import ( "testing" ) +// The envelope carries the bare address. "Vantage " as MAIL FROM left +// rspamd with no envelope sender, and mailcow skipped DKIM signing. +func TestAddrSpecStripsDisplayName(t *testing.T) { + for in, want := range map[string]string{ + "Vantage ": "support@example.com", + "support@example.com": "support@example.com", + " a@example.com ": "a@example.com", + "not an address": "not an address", + } { + if got := addrSpec(in); got != want { + t.Errorf("addrSpec(%q) = %q, want %q", in, got, want) + } + } +} + // Every part must declare quoted-printable and put only 7-bit bytes on the // wire. Raw UTF-8 under an implied 7bit encoding scored R_BAD_CTE_7BIT in -// rspamd, went out without a DKIM signature, and landed in Gmail's spam. +// rspamd. func TestEnvelopePartsAreQuotedPrintable(t *testing.T) { s := Sender{From: "Vantage "} m := message{