From 7d7e072960171be5d5cc86c46a7e1e5091abee6f Mon Sep 17 00:00:00 2001 From: Kai Dederichs Date: Fri, 17 May 2024 17:06:36 +0200 Subject: [PATCH] feat: support custom encoders in mime parts --- src/Symfony/Component/Mime/Part/TextPart.php | 21 ++++++-- .../Mime/Tests/Part/TextPartTest.php | 54 +++++++++++++++++++ 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/Mime/Part/TextPart.php b/src/Symfony/Component/Mime/Part/TextPart.php index a13300955e36c..82a5cd3095468 100644 --- a/src/Symfony/Component/Mime/Part/TextPart.php +++ b/src/Symfony/Component/Mime/Part/TextPart.php @@ -23,6 +23,8 @@ */ class TextPart extends AbstractPart { + private const DEFAULT_ENCODERS = ['quoted-printable', 'base64', '8bit']; + /** @internal */ protected Headers $_headers; @@ -63,8 +65,8 @@ public function __construct($body, ?string $charset = 'utf-8', string $subtype = if (null === $encoding) { $this->encoding = $this->chooseEncoding(); } else { - if ('quoted-printable' !== $encoding && 'base64' !== $encoding && '8bit' !== $encoding) { - throw new InvalidArgumentException(sprintf('The encoding must be one of "quoted-printable", "base64", or "8bit" ("%s" given).', $encoding)); + if (!\in_array($encoding, self::DEFAULT_ENCODERS, true) && !\array_key_exists($encoding, self::$encoders)) { + throw new InvalidArgumentException(sprintf('The encoding must be one of "%s" ("%s" given).', implode('", "', array_unique(array_merge(self::DEFAULT_ENCODERS, array_keys(self::$encoders)))), $encoding)); } $this->encoding = $encoding; } @@ -207,7 +209,20 @@ private function getEncoder(): ContentEncoderInterface return self::$encoders[$this->encoding] ??= new QpContentEncoder(); } - return self::$encoders[$this->encoding] ??= new Base64ContentEncoder(); + if ('base64' === $this->encoding) { + return self::$encoders[$this->encoding] ??= new Base64ContentEncoder(); + } + + return self::$encoders[$this->encoding]; + } + + public static function addEncoder(string $name, ContentEncoderInterface $encoder): void + { + if (\in_array($name, self::DEFAULT_ENCODERS, true)) { + throw new InvalidArgumentException('You are not allowed to change the default encoders ("quoted-printable","base64","8bit"). If you want to modify their behaviour please register and use a new encoder.'); + } + + self::$encoders[$name] = $encoder; } private function chooseEncoding(): string diff --git a/src/Symfony/Component/Mime/Tests/Part/TextPartTest.php b/src/Symfony/Component/Mime/Tests/Part/TextPartTest.php index 905349e670048..a3155ac12d9db 100644 --- a/src/Symfony/Component/Mime/Tests/Part/TextPartTest.php +++ b/src/Symfony/Component/Mime/Tests/Part/TextPartTest.php @@ -12,6 +12,9 @@ namespace Symfony\Component\Mime\Tests\Part; use PHPUnit\Framework\TestCase; +use Symfony\Component\Mime\Encoder\ContentEncoderInterface; +use Symfony\Component\Mime\Exception\InvalidArgumentException; +use Symfony\Component\Mime\Exception\RuntimeException; use Symfony\Component\Mime\Header\Headers; use Symfony\Component\Mime\Header\ParameterizedHeader; use Symfony\Component\Mime\Header\UnstructuredHeader; @@ -87,6 +90,57 @@ public function testEncoding() ), $p->getPreparedHeaders()); } + public function testCustomEncoderNeedsToRegisterFirst() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The encoding must be one of "quoted-printable", "base64", "8bit", "exception_test" ("upper_encoder" given).'); + TextPart::addEncoder('exception_test', $this->createMock(ContentEncoderInterface::class)); + new TextPart('content', 'utf-8', 'plain', 'upper_encoder'); + } + + public function testOverwriteDefaultEncoder() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('You are not allowed to change the default encoders ("quoted-printable","base64","8bit"). If you want to modify their behaviour please register and use a new encoder.'); + TextPart::addEncoder('8bit', $this->createMock(ContentEncoderInterface::class)); + } + + public function testCustomEncoding() + { + TextPart::addEncoder('upper_encoder', new class() implements ContentEncoderInterface { + public function encodeByteStream($stream, int $maxLineLength = 0): iterable + { + $filter = stream_filter_append($stream, 'string.toupper', \STREAM_FILTER_READ); + if (!\is_resource($filter)) { + throw new RuntimeException('Unable to set the upper content encoder to the filter.'); + } + + while (!feof($stream)) { + yield fread($stream, 16372); + } + stream_filter_remove($filter); + } + + public function getName(): string + { + return 'upper_encoder'; + } + + public function encodeString(string $string, ?string $charset = 'utf-8', int $firstLineOffset = 0, int $maxLineLength = 0): string + { + return strtoupper($string); + } + }); + + $p = new TextPart('content', 'utf-8', 'plain', 'upper_encoder'); + $this->assertEquals('CONTENT', $p->bodyToString()); + $this->assertEquals('CONTENT', implode('', iterator_to_array($p->bodyToIterable()))); + $this->assertEquals(new Headers( + new ParameterizedHeader('Content-Type', 'text/plain', ['charset' => 'utf-8']), + new UnstructuredHeader('Content-Transfer-Encoding', 'upper_encoder') + ), $p->getPreparedHeaders()); + } + public function testSerialize() { $r = fopen('php://memory', 'r+', false);