Skip to content

MimePart: quote an encoded display name only when it could be re-parsed - #107

Merged
dg merged 1 commit into
nette:masterfrom
hubipe:fix-encoded-word-quotes
Aug 18, 2026
Merged

MimePart: quote an encoded display name only when it could be re-parsed#107
dg merged 1 commit into
nette:masterfrom
hubipe:fix-encoded-word-quotes

Conversation

@hubipe

@hubipe hubipe commented Aug 18, 2026

Copy link
Copy Markdown
Contributor
Q A
Bug fix? yes
New feature? no
BC break? no API change; encoded display names holding no address-structuring character lose the quotes added in #102

Problem

#102 fixed a real failure – a comma in a display name broke DKIM verification at Gmail – by wrapping the phrase in a quoted-string before iconv_mime_encode(). That part works and this PR keeps it.

The side effect is the predicate: $escape fires on any character outside RFC 2822 atext, and every diacritic is outside atext. So a name that contains nothing dangerous at all is quoted too, and because the quoting happens before encoding, the quotes land inside the base64 payload and decode as part of the name:

$mail->setFrom('objednavky@domena.cz', 'Objednávky domena.cz');
// From: =?UTF-8?B?Ik9iamVkbsOhdmt5IGRvbWVuYS5jeiI=?= <objednavky@domena.cz>
// displayed as: "Objednávky domena.cz" <objednavky@domena.cz>

Every sender or recipient name with diacritics now reaches the recipient with visible quotation marks around it. $mail->getFrom() still returns the name unquoted, so the wire format and the API disagree.

Solution

Keep the quoting on the encoded path, but only for characters that can actually restructure an address list: , ; : < > @ " \. A diacritic or a dot cannot, so those names are encoded bare:

$mail->setFrom('objednavky@domena.cz', 'Objednávky domena.cz');
// From: =?UTF-8?B?T2JqZWRuw6F2a3kgZG9tZW5hLmN6?= <objednavky@domena.cz>
// displayed as: Objednávky domena.cz <objednavky@domena.cz>

$mail->setFrom('doe@example.com', 'Kdo uteče, obědvá');
// unchanged from #102 – the comma keeps the quoted-string

The literal (non-encoded) path keeps the original atext test unchanged: there the name really is emitted as-is, the quotes are RFC 5322 delimiters, and dropping them would be a syntax error.

Worth being explicit about the reasoning, because it is the part #102 got slightly wrong. Per RFC 2047 §6.2:

Decoding and display of encoded-words occurs after a structured field body is parsed into tokens.

So in a conforming parser a comma inside base64 cannot split an address – at parse time it is not a comma. The quoting in #102 is therefore not protection against a spec-compliant receiver; it is protection against one that decodes first and re-parses the result. That is a real class of receiver, worth defending against, but it only justifies quoting characters that such a receiver could misparse into a different address structure. Diacritics are not in that set, and paying for them with a visibly mangled display name in every conforming client is a bad trade.

Scope decisions worth a second opinion

  • The dot is excluded. RFC 5322 lists . among specials, so a strict reading would keep quoting domena.cz. It cannot restructure an address, and including it would leave the original complaint unfixed, so it is treated as harmless here. Say the word if you would rather have it in.
  • Parentheses and brackets are excluded for the same reason. A decode-then-reparse receiver could read (...) as a comment and hide part of the name, but it cannot end up with a different set of addresses. Happy to add them if you consider hidden text the greater evil.

Tests

tests/Mail/Mail.email.encodedName.phpt is new and pins both paths: an ASCII name outside atext still becomes a real quoted-string, a plain diacritic name and a dotted name encode bare, and a comma, a quote or an embedded <addr> keep the quoting.

Three existing expectations change, all of them names with nothing dangerous in them:

test decoded name: before → after
Mail.email.phpt "Žluťoučký kůň"Žluťoučký kůň
Mail.email.multiple.phpt, Mail.longLines.phpt "Řehoř Řízek"Řehoř Řízek, "Luboš Smažák"Luboš Smažák

The two fixtures that motivated #102 are asserted unchanged: Mail.headers.002.phpt's "Kdo uteče, obědvá" (comma) and Mail.email.phpt's "Žluťoučký \"kůň\"" (quote). Both now carry a comment saying why they keep the quotes.

docs/internals.md gains a bullet on the two quoting tests and why they differ.

Verification

All four CI jobs run locally on PHP 8.4:

vendor/bin/tester tests -s      # OK (56 tests)
vendor/bin/phpstan analyse      # [OK] No errors
code-checker --strict-types     # clean (only pre-existing HEREDOC tips in untouched files)
coding-standard/ecs check       # Code style checks passed

@dg

dg commented Aug 18, 2026

Copy link
Copy Markdown
Member

Thanks

@dg
dg merged commit eaea3ab into nette:master Aug 18, 2026
dg pushed a commit that referenced this pull request Aug 18, 2026
…ed (#107)

whenever it contained anything outside RFC 2822 atext. Every accented letter
is outside atext, so every non-ASCII name was quoted, and because the quotes
land inside the base64 payload, clients showed them as part of the name:
"Objednávky domena.cz" instead of Objednávky domena.cz.

Per RFC 2047 §6.2 an encoded-word is decoded only after the field is parsed,
so quotes there are not syntax; they only guard against a receiver that
decodes first and re-parses the result (the Gmail failure behind #102).
The encoded path therefore quotes only names holding a character such a
receiver could turn into another address (,;:<>@"\); diacritics and dots
are encoded bare. The literal path keeps the atext test, where the quotes
are real RFC 5322 delimiters.
dg pushed a commit that referenced this pull request Aug 18, 2026
…ed (#107)

whenever it contained anything outside RFC 2822 atext. Every accented letter
is outside atext, so every non-ASCII name was quoted, and because the quotes
land inside the base64 payload, clients showed them as part of the name:
"Objednávky domena.cz" instead of Objednávky domena.cz.

Per RFC 2047 §6.2 an encoded-word is decoded only after the field is parsed,
so quotes there are not syntax; they only guard against a receiver that
decodes first and re-parses the result (the Gmail failure behind #102).
The encoded path therefore quotes only names holding a character such a
receiver could turn into another address (,;:<>@"\); diacritics and dots
are encoded bare. The literal path keeps the atext test, where the quotes
are real RFC 5322 delimiters.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants