Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/internals.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ Sub-parts are serialized recursively; a `boundary=` is appended to `Content-Type
`Content-Disposition` filenames (quoted, RFC 2231 value), and plain values.
Folding at `LineLength` (76) with tab continuation is volatile line-level
mechanics — do not treat its exact offsets as contract.
- **Quoting a display name applies only to the literal path.** A `phrase` holding
characters outside `atext` (a dot, a comma, ...) is wrapped in a `quoted-string`
when it is emitted as-is. It must *not* be wrapped when it goes through
`iconv_mime_encode`: an encoded-word is built from atext-safe characters, so
nothing inside it needs protecting, and per RFC 2047 §5 an encoded-word must not
appear within a `quoted-string`. Quoting first would put the quotes inside the
base64 payload, where they decode as part of the name the recipient sees.
- `Message::$defaultHeaders` is a **mutable static** (`MIME-Version`,
`X-Mailer`), applied in the constructor — changing it affects every
subsequently created message, and since `X-Mailer` is in DKIM's default
Expand Down
6 changes: 2 additions & 4 deletions src/Mail/MimePart.php
Original file line number Diff line number Diff line change
Expand Up @@ -324,10 +324,8 @@ private static function encodeSequence(string $s, int &$offset = 0, ?int $type =
$offset = 1;
}

if ($type === self::SequenceWord) {
$s = $escape($s);
}

// no quoting here: an encoded-word is atext-safe, and per RFC 2047 it must not appear
// inside a quoted-string, the quotes would decode as part of the name
$s = iconv_mime_encode(str_repeat(' ', $old = $offset), $s, [
'scheme' => 'B', // Q is broken
'input-charset' => 'UTF-8',
Expand Down
61 changes: 61 additions & 0 deletions tests/Mail/Mail.email.encodedName.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php declare(strict_types=1);

/**
* Test: Nette\Mail\Message display name is not quoted inside an encoded-word.
*/

use Nette\Mail\Message;
use Tester\Assert;


require __DIR__ . '/../bootstrap.php';


test('ASCII name outside atext is a real quoted-string', function () {
$mail = new Message;
$mail->setFrom('doe@example.com', 'John.Doe');

Assert::same('"John.Doe" <doe@example.com>', $mail->getEncodedHeader('From'));
});


test('ASCII name within atext is left alone', function () {
$mail = new Message;
$mail->setFrom('doe@example.com', 'John Doe');

Assert::same('John Doe <doe@example.com>', $mail->getEncodedHeader('From'));
});


test('encoded name carries no quotes', function () {
$mail = new Message;
$mail->setFrom('doe@example.com', 'Jan Nováček');

$header = $mail->getEncodedHeader('From');
Assert::same('=?UTF-8?B?SmFuIE5vdsOhxI1law==?= <doe@example.com>', $header);
Assert::same('Jan Nováček <doe@example.com>', iconv_mime_decode($header, 0, 'UTF-8'));
});


test('quotes in an encoded name are neither escaped nor doubled', function () {
$mail = new Message;
$mail->setFrom('doe@example.com', 'Jan "Honza" Nováček');

Assert::same(
'Jan "Honza" Nováček <doe@example.com>',
iconv_mime_decode($mail->getEncodedHeader('From'), 0, 'UTF-8'),
);
});


test('a comma in an encoded name cannot split the address list', function () {
$mail = new Message;
$mail->addTo('jane@example.com', 'Doe, Jane');
$mail->addTo('john@example.com', 'Nováček, Jan');

$header = $mail->getEncodedHeader('To');

// the comma sits inside the base64 payload, so it cannot be read as an address separator
Assert::contains('=?UTF-8?B?Tm92w6HEjWVrLCBKYW4=?=', $header);
Assert::contains('"Doe, Jane" <jane@example.com>', $header);
});
6 changes: 3 additions & 3 deletions tests/Mail/Mail.email.multiple.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ Assert::match(<<<'EOD'
X-Mailer: Nette Framework
Date: %a%
From: John Doe <doe@example.com>
To: Lady Jane <jane@example.foo>,williams@example.foo,=?UTF-8?B?Ig==?=
=?UTF-8?B?xZhlaG/FmSDFmMOtemVrIg==?= <rizek@example.foo>,
=?UTF-8?B?Ikx1Ym/FoSBTbWHFvsOhayI=?= <smazak@example.foo>
To: Lady Jane <jane@example.foo>,williams@example.foo,=?UTF-8?B?xZg=?=
=?UTF-8?B?ZWhvxZkgxZjDrXplaw==?= <rizek@example.foo>,
=?UTF-8?B?THVib8WhIFNtYcW+w6Fr?= <smazak@example.foo>
Subject: Hello Jane!
Message-ID: <%S%@%S%>
Content-Type: text/plain; charset=UTF-8
Expand Down
11 changes: 7 additions & 4 deletions tests/Mail/Mail.email.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,20 @@ Assert::match(<<<'EOD'
MIME-Version: 1.0
X-Mailer: Nette Framework
Date: %a%
From: =?UTF-8?B?IsW9bHXFpW91xI1rw70ga8WvxYgi?= <kun1@example.com>
Reply-To: =?UTF-8?B?IsW9bHXFpW91xI1rw70ga8WvxYgi?= <kun2@example.com>,
From: =?UTF-8?B?xb1sdcWlb3XEjWvDvSBrxa/FiA==?= <kun1@example.com>
Reply-To: =?UTF-8?B?xb1sdcWlb3XEjWvDvSBrxa/FiA==?= <kun2@example.com>,
John Doe <doe2@example.com>
To: =?UTF-8?B?IsW9bHXFpW91xI1rw70gXCJrxa/FiFwiIg==?= <kun3@example.com>,
To: =?UTF-8?B?xb1sdcWlb3XEjWvDvSAia8WvxYgi?= <kun3@example.com>,
John 'jd' Doe <doe3@example.com>
Cc: TheMail <nette@example.com>,John Doe <doe4@example.com>,"The.Mail"
<nette2@example.com>
Bcc: =?UTF-8?B?IsW9bHXFpW91xI1rw70ga8WvxYgi?= <kun5@example.com>,
Bcc: =?UTF-8?B?xb1sdcWlb3XEjWvDvSBrxa/FiA==?= <kun5@example.com>,
doe5@example.com
Return-Path: doe@example.com
Message-ID: <%a%@%a%>
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 7bit
EOD, TestMailer::$output);

// the encoded-word decodes to the name as it was passed in, quotes included
Assert::match('Žluťoučký "kůň"', iconv_mime_decode('=?UTF-8?B?xb1sdcWlb3XEjWvDvSAia8WvxYgi?='));
5 changes: 3 additions & 2 deletions tests/Mail/Mail.headers.002.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Assert::match(<<<'EOD'
MIME-Version: 1.0
X-Mailer: Nette Framework
Date: %a%
From: =?UTF-8?B?IktkbyB1dGXEjWUsIG9ixJtkdsOhIg==?= <doe@example.com>
From: =?UTF-8?B?S2RvIHV0ZcSNZSwgb2LEm2R2w6E=?= <doe@example.com>
To: Lady Jane <jane@example.com>
Cc: jane@example.info
Bcc: bcc@example.com
Expand All @@ -49,4 +49,5 @@ Assert::match(<<<'EOD'
Content-Transfer-Encoding: 7bit
EOD, TestMailer::$output);

Assert::match('"Kdo uteče, obědvá"', iconv_mime_decode('=?UTF-8?B?IktkbyB1dGXEjWUsIG9ixJtkdsOhIg==?='));
// the comma is safe inside the encoded-word, so it cannot split the address list
Assert::match('Kdo uteče, obědvá', iconv_mime_decode('=?UTF-8?B?S2RvIHV0ZcSNZSwgb2LEm2R2w6E=?='));
6 changes: 3 additions & 3 deletions tests/Mail/Mail.longLines.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ Assert::match(<<<'EOD'
X-Mailer: Nette Framework
Date: %a%
From: John Doe <doe@example.com>
To: Lady Jane <jane@example.foo>,williams@example.foo,=?UTF-8?B?Ig==?=
=?UTF-8?B?xZhlaG/FmSDFmMOtemVrIg==?= <rizek@example.foo>,
=?UTF-8?B?Ikx1Ym/FoSBTbWHFvsOhayI=?= <smazak@example.foo>
To: Lady Jane <jane@example.foo>,williams@example.foo,=?UTF-8?B?xZg=?=
=?UTF-8?B?ZWhvxZkgxZjDrXplaw==?= <rizek@example.foo>,
=?UTF-8?B?THVib8WhIFNtYcW+w6Fr?= <smazak@example.foo>
Subject: Hello Jane!
Message-ID: <%S%@%S%>
Content-Type: text/plain; charset=UTF-8
Expand Down