fix: send the bare address as SMTP envelope sender so mailcow DKIM-signs

This commit is contained in:
2026-09-11 11:00:40 +00:00
parent 91841174dd
commit 91d10a7650
2 changed files with 35 additions and 7 deletions
+19 -6
View File
@@ -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 <support@example.com>", which belongs in the From header only:
// sent as MAIL FROM it became "<Vantage <support@example.com>>". 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)
+16 -1
View File
@@ -11,9 +11,24 @@ import (
"testing"
)
// The envelope carries the bare address. "Vantage <x@y>" 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": "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 <support@example.com>"}
m := message{