Skip to content

Commit

Permalink
Use base64 encoding when phrase headers contain specials from RFC 822 (
Browse files Browse the repository at this point in the history
…#28)

* use base64 encoding when phrase headers contain specials from RFC 822
  • Loading branch information
frederikbosch authored Sep 26, 2017
1 parent 050efe9 commit cbbea89
Show file tree
Hide file tree
Showing 6 changed files with 274 additions and 3 deletions.
2 changes: 1 addition & 1 deletion src/Address.php
Original file line number Diff line number Diff line change
Expand Up @@ -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\\\"");

Expand Down
27 changes: 25 additions & 2 deletions src/Header/OptimalEncodedHeaderValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

namespace Genkgo\Mail\Header;

use Genkgo\Mail\Stream\OptimalTransferEncodedPhraseStream;
use Genkgo\Mail\Stream\OptimalTransferEncodedTextStream;

/**
Expand All @@ -19,6 +20,10 @@ final class OptimalEncodedHeaderValue
* @var string
*/
private $value;
/**
* @var array
*/
private $phrase = false;

/**
* OptimalEncodedHeaderValue constructor.
Expand All @@ -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;
}
Expand All @@ -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;
}
}
207 changes: 207 additions & 0 deletions src/Stream/OptimalTransferEncodedPhraseStream.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
<?php
declare(strict_types=1);

namespace Genkgo\Mail\Stream;

use Genkgo\Mail\Header\HeaderValueParameter;
use Genkgo\Mail\StreamInterface;

final class OptimalTransferEncodedPhraseStream implements StreamInterface
{
/**
* @var StreamInterface
*/
private $decoratedStream;
/**
* @var string
*/
private $encoding = '7bit';
/**
* @var int
*/
private $lineLength = 78;
/**
*
*/
private CONST NON_7BIT_CHARS = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F\x7F\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D\x8E\x8F\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C\x9D\x9E\x9F\xA0\xA1\xA2\xA3\xA4\xA5\xA6\xA7\xA8\xA9\xAA\xAB\xAC\xAD\xAE\xAF\xB0\xB1\xB2\xB3\xB4\xB5\xB6\xB7\xB8\xB9\xBA\xBB\xBC\xBD\xBE\xBF\xC0\xC1\xC2\xC3\xC4\xC5\xC6\xC7\xC8\xC9\xCA\xCB\xCC\xCD\xCE\xCF\xD0\xD1\xD2\xD3\xD4\xD5\xD6\xD7\xD8\xD9\xDA\xDB\xDC\xDD\xDE\xDF\xE0\xE1\xE2\xE3\xE4\xE5\xE6\xE7\xE8\xE9\xEA\xEB\xEC\xED\xEE\xEF\xF0\xF1\xF2\xF3\xF4\xF5\xF6\xF7\xF8\xF9\xFA\xFB\xFC\xFD\xFE\xFF";
/**
* @var string
*/
private $lineBreak;

/**
* OptimalEncodedTextStream constructor.
* @param string $text
* @param int $lineLength
* @param string $lineBreak
*/
public function __construct(string $text, int $lineLength = 78, string $lineBreak = "\r\n")
{
$this->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
);
}

}
1 change: 1 addition & 0 deletions test/Unit/AddressTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public function provideAddresses()
['[email protected]', "test\r\ntest", false, '[email protected]'],
['[email protected]', "tëst", true, '=?UTF-8?B?dMOrc3Q=?= <[email protected]>'],
['a."local-part"@domain.com', "test", true, 'test <a."local-part"@domain.com>'],
['[email protected]', "sprode, henriëtte", true, '=?UTF-8?B?c3Byb2RlLCBoZW5yacOrdHRl?= <[email protected]>'],
];
}

Expand Down
39 changes: 39 additions & 0 deletions test/Unit/Stream/OptimalTransferEncodedPhraseStreamTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

namespace Genkgo\TestMail\Unit\Stream;

use Genkgo\Mail\Stream\OptimalTransferEncodedPhraseStream;
use Genkgo\TestMail\AbstractTestCase;;

final class OptimalTransferEncodedPhraseStreamTest extends AbstractTestCase
{

/**
* @test
* @dataProvider provideText
*/
public function it_uses_correct_transfer_encoding($text, $expectedEncoding)
{
$stream = new OptimalTransferEncodedPhraseStream($text);

$this->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'],
];
}

}
1 change: 1 addition & 0 deletions test/Unit/Stream/OptimalTransferEncodedTextStreamTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down

0 comments on commit cbbea89

Please sign in to comment.