From cbbea892bce41f3a42f6b9c48f86c140b133fe2d Mon Sep 17 00:00:00 2001 From: Frederik Bosch Date: Tue, 26 Sep 2017 18:12:15 +0200 Subject: [PATCH] Use base64 encoding when phrase headers contain specials from RFC 822 (#28) * use base64 encoding when phrase headers contain specials from RFC 822 --- src/Address.php | 2 +- src/Header/OptimalEncodedHeaderValue.php | 27 ++- .../OptimalTransferEncodedPhraseStream.php | 207 ++++++++++++++++++ test/Unit/AddressTest.php | 1 + ...OptimalTransferEncodedPhraseStreamTest.php | 39 ++++ .../OptimalTransferEncodedTextStreamTest.php | 1 + 6 files changed, 274 insertions(+), 3 deletions(-) create mode 100644 src/Stream/OptimalTransferEncodedPhraseStream.php create mode 100644 test/Unit/Stream/OptimalTransferEncodedPhraseStreamTest.php diff --git a/src/Address.php b/src/Address.php index 53f33766..64ca7e71 100644 --- a/src/Address.php +++ b/src/Address.php @@ -87,7 +87,7 @@ public function __toString(): string return (string)$this->address; } - $encodedName = (string) (new OptimalEncodedHeaderValue($this->name)); + $encodedName = (string) OptimalEncodedHeaderValue::forPhrase($this->name); if ($encodedName === $this->name) { $encodedName = addcslashes($encodedName, "\0..\37\177\\\""); diff --git a/src/Header/OptimalEncodedHeaderValue.php b/src/Header/OptimalEncodedHeaderValue.php index 47ed09e2..38dcb80e 100644 --- a/src/Header/OptimalEncodedHeaderValue.php +++ b/src/Header/OptimalEncodedHeaderValue.php @@ -3,6 +3,7 @@ namespace Genkgo\Mail\Header; +use Genkgo\Mail\Stream\OptimalTransferEncodedPhraseStream; use Genkgo\Mail\Stream\OptimalTransferEncodedTextStream; /** @@ -19,6 +20,10 @@ final class OptimalEncodedHeaderValue * @var string */ private $value; + /** + * @var array + */ + private $phrase = false; /** * OptimalEncodedHeaderValue constructor. @@ -34,9 +39,16 @@ public function __construct(string $value) */ public function __toString(): string { - $encoded = new OptimalTransferEncodedTextStream($this->value, 68, self::FOLDING); + if ($this->phrase === true) { + $encoded = new OptimalTransferEncodedPhraseStream($this->value, 68, self::FOLDING); + + $encoding = $encoded->getMetadata(['transfer-encoding'])['transfer-encoding']; + } else { + $encoded = new OptimalTransferEncodedTextStream($this->value, 68, self::FOLDING); + + $encoding = $encoded->getMetadata(['transfer-encoding'])['transfer-encoding']; + } - $encoding = $encoded->getMetadata(['transfer-encoding'])['transfer-encoding']; if ($encoding === '7bit' || $encoding === '8bit') { return (string) $encoded; } @@ -48,4 +60,15 @@ public function __toString(): string return sprintf('=?%s?Q?%s?=', 'UTF-8', (string) $encoded); } + /** + * @param string $value + * @return OptimalEncodedHeaderValue + */ + public static function forPhrase(string $value): self + { + $encoded = new self($value); + $encoded->value = $value; + $encoded->phrase = true; + return $encoded; + } } \ No newline at end of file diff --git a/src/Stream/OptimalTransferEncodedPhraseStream.php b/src/Stream/OptimalTransferEncodedPhraseStream.php new file mode 100644 index 00000000..23b73e84 --- /dev/null +++ b/src/Stream/OptimalTransferEncodedPhraseStream.php @@ -0,0 +1,207 @@ +lineLength = $lineLength; + $this->lineBreak = $lineBreak; + $this->decoratedStream = $this->calculateOptimalStream($text); + } + + /** + * @param string $text + * @return StreamInterface + */ + private function calculateOptimalStream(string $text): StreamInterface + { + if (strcspn($text, self::NON_7BIT_CHARS) === strlen($text)) { + $this->encoding = '7bit'; + return new AsciiEncodedStream($text, $this->lineLength, $this->lineBreak); + } + + if (strcspn($text, HeaderValueParameter::RFC_822_T_SPECIAL) !== strlen($text)) { + $this->encoding = 'base64'; + return Base64EncodedStream::fromString($text, $this->lineLength, $this->lineBreak); + } + + if (preg_match_all('/[\000-\010\013\014\016-\037\177-\377]/', $text) > (strlen($text) / 3)) { + $this->encoding = 'base64'; + return Base64EncodedStream::fromString($text, $this->lineLength, $this->lineBreak); + } + + $this->encoding = 'quoted-printable'; + return QuotedPrintableStream::fromString($text, $this->lineLength, $this->lineBreak); + } + + /** + * @return string + */ + public function __toString(): string + { + return $this->decoratedStream->__toString(); + } + + /** + * + */ + public function close(): void + { + $this->decoratedStream->close(); + } + + /** + * @return mixed + */ + public function detach() + { + return $this->decoratedStream->detach(); + } + + /** + * @return int|null + */ + public function getSize(): ?int + { + return $this->decoratedStream->getSize(); + } + + /** + * @return int + * @throws \RuntimeException + */ + public function tell(): int + { + return $this->decoratedStream->tell(); + } + + /** + * @return bool + */ + public function eof(): bool + { + return $this->decoratedStream->eof(); + } + + /** + * @return bool + */ + public function isSeekable(): bool + { + return $this->decoratedStream->isSeekable(); + } + + /** + * @param int $offset + * @param int $whence + * @return int + */ + public function seek(int $offset, int $whence = SEEK_SET): int + { + return $this->decoratedStream->seek($offset, $whence); + } + + /** + * @return bool + */ + public function rewind(): bool + { + return $this->decoratedStream->rewind(); + } + + /** + * @return bool + */ + public function isWritable(): bool + { + return $this->decoratedStream->isWritable(); + } + + /** + * @param $string + * @return int + */ + public function write($string): int + { + return $this->decoratedStream->write($string); + } + + /** + * @return bool + */ + public function isReadable(): bool + { + return $this->decoratedStream->isReadable(); + } + + /** + * @param int $length + * @return string + */ + public function read(int $length): string + { + return $this->decoratedStream->read($length); + } + + /** + * @return string + */ + public function getContents(): string + { + return $this->decoratedStream->getContents(); + } + + /** + * @param array $keys + * @return array + */ + public function getMetadata(array $keys = []): array + { + $metaData = $this->decoratedStream->getMetadata($keys); + $metaData['transfer-encoding'] = $this->encoding; + + $keys = array_map('strtolower', $keys); + + return array_filter( + $metaData, + function ($key) use ($keys) { + return in_array(strtolower($key), $keys); + }, + ARRAY_FILTER_USE_KEY + ); + } + +} \ No newline at end of file diff --git a/test/Unit/AddressTest.php b/test/Unit/AddressTest.php index ec12f593..1823631e 100644 --- a/test/Unit/AddressTest.php +++ b/test/Unit/AddressTest.php @@ -38,6 +38,7 @@ public function provideAddresses() ['local-part@domain.com', "test\r\ntest", false, 'local-part@domain.com'], ['local-part@domain.com', "tëst", true, '=?UTF-8?B?dMOrc3Q=?= '], ['a."local-part"@domain.com', "test", true, 'test '], + ['h.sprode@domain.com', "sprode, henriëtte", true, '=?UTF-8?B?c3Byb2RlLCBoZW5yacOrdHRl?= '], ]; } diff --git a/test/Unit/Stream/OptimalTransferEncodedPhraseStreamTest.php b/test/Unit/Stream/OptimalTransferEncodedPhraseStreamTest.php new file mode 100644 index 00000000..1f9fc229 --- /dev/null +++ b/test/Unit/Stream/OptimalTransferEncodedPhraseStreamTest.php @@ -0,0 +1,39 @@ +assertEquals(['transfer-encoding' => $expectedEncoding], $stream->getMetadata(['transfer-encoding'])); + } + + /** + * @return array + */ + public function provideText() + { + return [ + [str_repeat('test1 test2', 50), '7bit'], + [str_repeat('tëst, test2', 50), 'base64'], + [str_repeat('tëst1 test2', 50), 'quoted-printable'], + [str_repeat('ëëëëë ëëëëë', 50), 'base64'], + ["\x00", 'base64'], + ["\x80", 'base64'], + ["\u{aa}", 'base64'], + ["\u{1F600}", 'base64'], + ]; + } + +} \ No newline at end of file diff --git a/test/Unit/Stream/OptimalTransferEncodedTextStreamTest.php b/test/Unit/Stream/OptimalTransferEncodedTextStreamTest.php index b63826e0..37e0e7e0 100644 --- a/test/Unit/Stream/OptimalTransferEncodedTextStreamTest.php +++ b/test/Unit/Stream/OptimalTransferEncodedTextStreamTest.php @@ -26,6 +26,7 @@ public function provideText() { return [ [str_repeat('test1 test2', 50), '7bit'], + [str_repeat('tëst, test2', 50), 'quoted-printable'], [str_repeat('tëst1 test2', 50), 'quoted-printable'], [str_repeat('ëëëëë ëëëëë', 50), 'base64'], ["\x00", 'base64'],