Skip to content

Commit 7d7e072

Browse files
committed
feat: support custom encoders in mime parts
1 parent 232d6d0 commit 7d7e072

File tree

2 files changed

+72
-3
lines changed

2 files changed

+72
-3
lines changed

src/Symfony/Component/Mime/Part/TextPart.php

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
*/
2424
class TextPart extends AbstractPart
2525
{
26+
private const DEFAULT_ENCODERS = ['quoted-printable', 'base64', '8bit'];
27+
2628
/** @internal */
2729
protected Headers $_headers;
2830

@@ -63,8 +65,8 @@ public function __construct($body, ?string $charset = 'utf-8', string $subtype =
6365
if (null === $encoding) {
6466
$this->encoding = $this->chooseEncoding();
6567
} else {
66-
if ('quoted-printable' !== $encoding && 'base64' !== $encoding && '8bit' !== $encoding) {
67-
throw new InvalidArgumentException(sprintf('The encoding must be one of "quoted-printable", "base64", or "8bit" ("%s" given).', $encoding));
68+
if (!\in_array($encoding, self::DEFAULT_ENCODERS, true) && !\array_key_exists($encoding, self::$encoders)) {
69+
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));
6870
}
6971
$this->encoding = $encoding;
7072
}
@@ -207,7 +209,20 @@ private function getEncoder(): ContentEncoderInterface
207209
return self::$encoders[$this->encoding] ??= new QpContentEncoder();
208210
}
209211

210-
return self::$encoders[$this->encoding] ??= new Base64ContentEncoder();
212+
if ('base64' === $this->encoding) {
213+
return self::$encoders[$this->encoding] ??= new Base64ContentEncoder();
214+
}
215+
216+
return self::$encoders[$this->encoding];
217+
}
218+
219+
public static function addEncoder(string $name, ContentEncoderInterface $encoder): void
220+
{
221+
if (\in_array($name, self::DEFAULT_ENCODERS, true)) {
222+
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.');
223+
}
224+
225+
self::$encoders[$name] = $encoder;
211226
}
212227

213228
private function chooseEncoding(): string

src/Symfony/Component/Mime/Tests/Part/TextPartTest.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
namespace Symfony\Component\Mime\Tests\Part;
1313

1414
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Mime\Encoder\ContentEncoderInterface;
16+
use Symfony\Component\Mime\Exception\InvalidArgumentException;
17+
use Symfony\Component\Mime\Exception\RuntimeException;
1518
use Symfony\Component\Mime\Header\Headers;
1619
use Symfony\Component\Mime\Header\ParameterizedHeader;
1720
use Symfony\Component\Mime\Header\UnstructuredHeader;
@@ -87,6 +90,57 @@ public function testEncoding()
8790
), $p->getPreparedHeaders());
8891
}
8992

93+
public function testCustomEncoderNeedsToRegisterFirst()
94+
{
95+
$this->expectException(InvalidArgumentException::class);
96+
$this->expectExceptionMessage('The encoding must be one of "quoted-printable", "base64", "8bit", "exception_test" ("upper_encoder" given).');
97+
TextPart::addEncoder('exception_test', $this->createMock(ContentEncoderInterface::class));
98+
new TextPart('content', 'utf-8', 'plain', 'upper_encoder');
99+
}
100+
101+
public function testOverwriteDefaultEncoder()
102+
{
103+
$this->expectException(InvalidArgumentException::class);
104+
$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.');
105+
TextPart::addEncoder('8bit', $this->createMock(ContentEncoderInterface::class));
106+
}
107+
108+
public function testCustomEncoding()
109+
{
110+
TextPart::addEncoder('upper_encoder', new class() implements ContentEncoderInterface {
111+
public function encodeByteStream($stream, int $maxLineLength = 0): iterable
112+
{
113+
$filter = stream_filter_append($stream, 'string.toupper', \STREAM_FILTER_READ);
114+
if (!\is_resource($filter)) {
115+
throw new RuntimeException('Unable to set the upper content encoder to the filter.');
116+
}
117+
118+
while (!feof($stream)) {
119+
yield fread($stream, 16372);
120+
}
121+
stream_filter_remove($filter);
122+
}
123+
124+
public function getName(): string
125+
{
126+
return 'upper_encoder';
127+
}
128+
129+
public function encodeString(string $string, ?string $charset = 'utf-8', int $firstLineOffset = 0, int $maxLineLength = 0): string
130+
{
131+
return strtoupper($string);
132+
}
133+
});
134+
135+
$p = new TextPart('content', 'utf-8', 'plain', 'upper_encoder');
136+
$this->assertEquals('CONTENT', $p->bodyToString());
137+
$this->assertEquals('CONTENT', implode('', iterator_to_array($p->bodyToIterable())));
138+
$this->assertEquals(new Headers(
139+
new ParameterizedHeader('Content-Type', 'text/plain', ['charset' => 'utf-8']),
140+
new UnstructuredHeader('Content-Transfer-Encoding', 'upper_encoder')
141+
), $p->getPreparedHeaders());
142+
}
143+
90144
public function testSerialize()
91145
{
92146
$r = fopen('php://memory', 'r+', false);

0 commit comments

Comments
 (0)