API returns quoted local-parts unquoted, so the address it reports cannot be parsed back
Follow-up to #731, and thanks for the quick fix there — v1.31.1 accepts "odd user"@example.com at RCPT TO again. What it reports afterwards is still lossy, though, and for one case ambiguous.
Version: v1.31.1 (docker run axllent/mailpit:v1.31.1)
Reproduce
Send three messages, each to a different quoted local-part, then read GET /api/v1/messages:
| sent to | API To[].Address and Bcc[].Address |
|---|---|
"odd user"@example.com |
odd [email protected] |
"a@b"@example.com |
a@[email protected] |
"simple"@example.com |
[email protected] |
The third one is correct — RFC 5321 §4.1.2 says the quoted form SHOULD NOT be used when the local-part is a valid Dot-string, so unquoting it is a fair normalisation. The first two are not: the space and the @ are exactly what the quotes were carrying, and without them the result is no longer an address. Go's own parser, which Mailpit is built on, rejects both strings it just produced:
mail.ParseAddress("odd [email protected]") -> mail: no angle-addr
mail.ParseAddress("a@[email protected]") -> mail: expected single address, got "@example.com"a@[email protected] is the one I would call a real hazard rather than a cosmetic loss: it is not just unparseable, it reads as a different address to anything that splits on the last @.
Everything else Mailpit stores is right. The raw message keeps the quotes, and so does the trace it writes itself:
Bcc: "odd user"@example.com
Received: from [127.0.0.1] (unknown [172.17.0.1])
by e5a03462838f (Mailpit) with SMTP
for <"odd user"@example.com>; Sat, 5 Sep 2026 19:38:56 +0000 (UTC)So this is only the parsed view — To, Cc, Bcc and From in the JSON.
Where it comes from
addressToSlice (internal/storage/utils.go:38) returns enmime's []*mail.Address, and those structs are what the API marshals. mail.Address.Address holds the decoded local-part by design, which is why the quotes are gone by the time it reaches JSON.
mail.Address.String() already does the right thing on the same values — it quotes only when the local-part needs it:
"odd user"@example.com -> .Address="odd [email protected]" .String()=<"odd user"@example.com>
"a@b"@example.com -> .Address="a@[email protected]" .String()=<"a@b"@example.com>
"simple"@example.com -> .Address="[email protected]" .String()=<[email protected]>So re-encoding the local-part when it is not a dot-atom before serialising would fix both cases and leave the third exactly as it is today.
I have only checked the API; I have not looked at whether the web UI renders these same values.
Why I care
A Postfix relay test suite reads relayed mail back out of Mailpit to check that the relay hands addresses over unchanged. It cannot use the reported address for a quoted local-part, so that assertion had to move to the relay's own log instead.
Generated by Claude Code
Source: axllent/mailpit