Skip to content

[Mime] Support custom encoders in mime parts #54975

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 31, 2024
Merged
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
21 changes: 18 additions & 3 deletions src/Symfony/Component/Mime/Part/TextPart.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
*/
class TextPart extends AbstractPart
{
private const DEFAULT_ENCODERS = ['quoted-printable', 'base64', '8bit'];

/** @internal */
protected Headers $_headers;

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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
Expand Down
54 changes: 54 additions & 0 deletions src/Symfony/Component/Mime/Tests/Part/TextPartTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Loading