Skip to content

Commit cbdab0b

Browse files
committed
Fix Mime message serialization
1 parent 95eb341 commit cbdab0b

File tree

9 files changed

+424
-2
lines changed

9 files changed

+424
-2
lines changed

src/Symfony/Bridge/Twig/Tests/Mime/TemplatedEmailTest.php

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@
44

55
use PHPUnit\Framework\TestCase;
66
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
7+
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
8+
use Symfony\Component\Serializer\Encoder\JsonEncoder;
9+
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
10+
use Symfony\Component\Serializer\Normalizer\MimeMessageNormalizer;
11+
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
12+
use Symfony\Component\Serializer\Normalizer\PropertyNormalizer;
13+
use Symfony\Component\Serializer\Serializer;
714

815
class TemplatedEmailTest extends TestCase
916
{
@@ -33,4 +40,77 @@ public function testSerialize()
3340
$this->assertEquals('text.html.twig', $email->getHtmlTemplate());
3441
$this->assertEquals($context, $email->getContext());
3542
}
43+
44+
public function testSymfonySerialize()
45+
{
46+
// we don't add from/sender to check that validation is not triggered to serialize an email
47+
$e = new TemplatedEmail();
48+
$e->to('you@example.com');
49+
$e->textTemplate('email.txt.twig');
50+
$e->htmlTemplate('email.html.twig');
51+
$e->context(['foo' => 'bar']);
52+
$e->attach('Some Text file', 'test.txt');
53+
$expected = clone $e;
54+
55+
$expectedJson = <<<EOF
56+
{
57+
"htmlTemplate": "email.html.twig",
58+
"textTemplate": "email.txt.twig",
59+
"context": {
60+
"foo": "bar"
61+
},
62+
"text": null,
63+
"textCharset": null,
64+
"html": null,
65+
"htmlCharset": null,
66+
"attachments": [
67+
{
68+
"body": "Some Text file",
69+
"name": "test.txt",
70+
"content-type": null,
71+
"inline": false
72+
}
73+
],
74+
"headers": {
75+
"to": [
76+
{
77+
"addresses": [
78+
{
79+
"address": "you@example.com",
80+
"name": ""
81+
}
82+
],
83+
"name": "To",
84+
"lineLength": 76,
85+
"lang": null,
86+
"charset": "utf-8"
87+
}
88+
]
89+
},
90+
"body": null,
91+
"message": null
92+
}
93+
EOF;
94+
95+
$extractor = new PhpDocExtractor();
96+
$propertyNormalizer = new PropertyNormalizer(null, null, $extractor);
97+
$serializer = new Serializer([
98+
new ArrayDenormalizer(),
99+
new MimeMessageNormalizer($propertyNormalizer),
100+
new ObjectNormalizer(null, null, null, $extractor),
101+
$propertyNormalizer
102+
], [new JsonEncoder()]);
103+
104+
$serialized = $serializer->serialize($e, 'json');
105+
$this->assertSame($expectedJson, json_encode(json_decode($serialized), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
106+
107+
$n = $serializer->deserialize($serialized, TemplatedEmail::class, 'json');
108+
$serialized = $serializer->serialize($e, 'json');
109+
$this->assertSame($expectedJson, json_encode(json_decode($serialized), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
110+
111+
$n->from('fabien@symfony.com');
112+
$expected->from('fabien@symfony.com');
113+
$this->assertEquals($expected->getHeaders(), $n->getHeaders());
114+
$this->assertEquals($expected->getBody(), $n->getBody());
115+
}
36116
}

src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,11 @@
3939
use Symfony\Component\Serializer\Normalizer\DateTimeZoneNormalizer;
4040
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
4141
use Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer;
42+
use Symfony\Component\Serializer\Normalizer\MimeMessageNormalizer;
4243
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
4344
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
4445
use Symfony\Component\Serializer\Normalizer\ProblemNormalizer;
46+
use Symfony\Component\Serializer\Normalizer\PropertyNormalizer;
4547
use Symfony\Component\Serializer\Normalizer\UnwrappingDenormalizer;
4648
use Symfony\Component\Serializer\Serializer;
4749
use Symfony\Component\Serializer\SerializerInterface;
@@ -76,6 +78,10 @@
7678
->args([[], service('serializer.name_converter.metadata_aware')])
7779
->tag('serializer.normalizer', ['priority' => -915])
7880

81+
->set('serializer.normalizer.mime_message', MimeMessageNormalizer::class)
82+
->args([service('serializer.normalizer.property')])
83+
->tag('serializer.normalizer', ['priority' => -915])
84+
7985
->set('serializer.normalizer.datetimezone', DateTimeZoneNormalizer::class)
8086
->tag('serializer.normalizer', ['priority' => -915])
8187

@@ -114,6 +120,18 @@
114120

115121
->alias(ObjectNormalizer::class, 'serializer.normalizer.object')
116122

123+
->set('serializer.normalizer.property', PropertyNormalizer::class)
124+
->args([
125+
service('serializer.mapping.class_metadata_factory'),
126+
service('serializer.name_converter.metadata_aware'),
127+
service('property_info')->ignoreOnInvalid(),
128+
service('serializer.mapping.class_discriminator_resolver')->ignoreOnInvalid(),
129+
null,
130+
[],
131+
])
132+
133+
->alias(PropertyNormalizer::class, 'serializer.normalizer.property')
134+
117135
->set('serializer.denormalizer.array', ArrayDenormalizer::class)
118136
->tag('serializer.normalizer', ['priority' => -990])
119137

src/Symfony/Component/Mime/Email.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ public function attachPart(DataPart $part)
378378
}
379379

380380
/**
381-
* @return DataPart[]
381+
* @return array|DataPart[]
382382
*/
383383
public function getAttachments(): array
384384
{

src/Symfony/Component/Mime/Header/Headers.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ final class Headers
3939
'return-path' => PathHeader::class,
4040
];
4141

42+
/**
43+
* @var HeaderInterface[][]
44+
*/
4245
private $headers = [];
4346
private $lineLength = 76;
4447

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ class TextPart extends AbstractPart
2828
private $body;
2929
private $charset;
3030
private $subtype;
31+
/**
32+
* @var ?string
33+
*/
3134
private $disposition;
3235
private $name;
3336
private $encoding;

src/Symfony/Component/Mime/Tests/EmailTest.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@
1919
use Symfony\Component\Mime\Part\Multipart\MixedPart;
2020
use Symfony\Component\Mime\Part\Multipart\RelatedPart;
2121
use Symfony\Component\Mime\Part\TextPart;
22+
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
23+
use Symfony\Component\Serializer\Encoder\JsonEncoder;
24+
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
25+
use Symfony\Component\Serializer\Normalizer\MimeMessageNormalizer;
26+
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
27+
use Symfony\Component\Serializer\Normalizer\PropertyNormalizer;
28+
use Symfony\Component\Serializer\Serializer;
2229

2330
class EmailTest extends TestCase
2431
{
@@ -384,4 +391,71 @@ public function testSerialize()
384391
$this->assertEquals($expected->getHeaders(), $n->getHeaders());
385392
$this->assertEquals($e->getBody(), $n->getBody());
386393
}
394+
395+
public function testSymfonySerialize()
396+
{
397+
// we don't add from/sender to check that validation is not triggered to serialize an email
398+
$e = new Email();
399+
$e->to('you@example.com');
400+
$e->text('Text content');
401+
$e->html('HTML <b>content</b>');
402+
$e->attach('Some Text file', 'test.txt');
403+
$expected = clone $e;
404+
405+
$expectedJson = <<<EOF
406+
{
407+
"text": "Text content",
408+
"textCharset": "utf-8",
409+
"html": "HTML <b>content</b>",
410+
"htmlCharset": "utf-8",
411+
"attachments": [
412+
{
413+
"body": "Some Text file",
414+
"name": "test.txt",
415+
"content-type": null,
416+
"inline": false
417+
}
418+
],
419+
"headers": {
420+
"to": [
421+
{
422+
"addresses": [
423+
{
424+
"address": "you@example.com",
425+
"name": ""
426+
}
427+
],
428+
"name": "To",
429+
"lineLength": 76,
430+
"lang": null,
431+
"charset": "utf-8"
432+
}
433+
]
434+
},
435+
"body": null,
436+
"message": null
437+
}
438+
EOF;
439+
440+
$extractor = new PhpDocExtractor();
441+
$propertyNormalizer = new PropertyNormalizer(null, null, $extractor);
442+
$serializer = new Serializer([
443+
new ArrayDenormalizer(),
444+
new MimeMessageNormalizer($propertyNormalizer),
445+
new ObjectNormalizer(null, null, null, $extractor),
446+
$propertyNormalizer
447+
], [new JsonEncoder()]);
448+
449+
$serialized = $serializer->serialize($e, 'json');
450+
$this->assertSame($expectedJson, json_encode(json_decode($serialized), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
451+
452+
$n = $serializer->deserialize($serialized, Email::class, 'json');
453+
$serialized = $serializer->serialize($e, 'json');
454+
$this->assertSame($expectedJson, json_encode(json_decode($serialized), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
455+
456+
$n->from('fabien@symfony.com');
457+
$expected->from('fabien@symfony.com');
458+
$this->assertEquals($expected->getHeaders(), $n->getHeaders());
459+
$this->assertEquals($expected->getBody(), $n->getBody());
460+
}
387461
}

src/Symfony/Component/Mime/Tests/MessageTest.php

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,17 @@
1717
use Symfony\Component\Mime\Header\MailboxListHeader;
1818
use Symfony\Component\Mime\Header\UnstructuredHeader;
1919
use Symfony\Component\Mime\Message;
20+
use Symfony\Component\Mime\Part\DataPart;
21+
use Symfony\Component\Mime\Part\Multipart\AlternativePart;
22+
use Symfony\Component\Mime\Part\Multipart\MixedPart;
2023
use Symfony\Component\Mime\Part\TextPart;
24+
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
25+
use Symfony\Component\Serializer\Encoder\JsonEncoder;
26+
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
27+
use Symfony\Component\Serializer\Normalizer\MimeMessageNormalizer;
28+
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
29+
use Symfony\Component\Serializer\Normalizer\PropertyNormalizer;
30+
use Symfony\Component\Serializer\Serializer;
2131

2232
class MessageTest extends TestCase
2333
{
@@ -147,4 +157,109 @@ public function testToString()
147157
$this->assertStringMatchesFormat($expected, str_replace("\r\n", "\n", $message->toString()));
148158
$this->assertStringMatchesFormat($expected, str_replace("\r\n", "\n", implode('', iterator_to_array($message->toIterable(), false))));
149159
}
160+
161+
public function testSymfonySerialize()
162+
{
163+
// we don't add from/sender to check that it's not needed to serialize an email
164+
$body = new MixedPart(
165+
new AlternativePart(
166+
new TextPart('Text content'),
167+
new TextPart('HTML content', 'utf-8', 'html')
168+
),
169+
new DataPart('text data', 'text.txt')
170+
);
171+
$e = new Message((new Headers())->addMailboxListHeader('To', ['you@example.com']), $body);
172+
$expected = clone $e;
173+
174+
$expectedJson = <<<EOF
175+
{
176+
"headers": {
177+
"to": [
178+
{
179+
"addresses": [
180+
{
181+
"address": "you@example.com",
182+
"name": ""
183+
}
184+
],
185+
"name": "To",
186+
"lineLength": 76,
187+
"lang": null,
188+
"charset": "utf-8"
189+
}
190+
]
191+
},
192+
"body": {
193+
"boundary": null,
194+
"parts": [
195+
{
196+
"boundary": null,
197+
"parts": [
198+
{
199+
"body": "Text content",
200+
"charset": "utf-8",
201+
"subtype": "plain",
202+
"disposition": null,
203+
"name": null,
204+
"encoding": "quoted-printable",
205+
"seekable": null,
206+
"headers": [],
207+
"class": "Symfony\\\\Component\\\\Mime\\\\Part\\\TextPart"
208+
},
209+
{
210+
"body": "HTML content",
211+
"charset": "utf-8",
212+
"subtype": "html",
213+
"disposition": null,
214+
"name": null,
215+
"encoding": "quoted-printable",
216+
"seekable": null,
217+
"headers": [],
218+
"class": "Symfony\\\\Component\\\\Mime\\\\Part\\\\TextPart"
219+
}
220+
],
221+
"headers": [],
222+
"class": "Symfony\\\\Component\\\\Mime\\\\Part\\\\Multipart\\\\AlternativePart"
223+
},
224+
{
225+
"filename": "text.txt",
226+
"mediaType": "application",
227+
"cid": null,
228+
"handle": null,
229+
"body": "text data",
230+
"charset": null,
231+
"subtype": "octet-stream",
232+
"disposition": "attachment",
233+
"name": "text.txt",
234+
"encoding": "base64",
235+
"seekable": null,
236+
"headers": [],
237+
"class": "Symfony\\\\Component\\\\Mime\\\\Part\\\\DataPart"
238+
}
239+
],
240+
"headers": [],
241+
"class": "Symfony\\\\Component\\\\Mime\\\\Part\\\\Multipart\\\\MixedPart"
242+
},
243+
"message": null
244+
}
245+
EOF;
246+
247+
$extractor = new PhpDocExtractor();
248+
$propertyNormalizer = new PropertyNormalizer(null, null, $extractor);
249+
$serializer = new Serializer([
250+
new ArrayDenormalizer(),
251+
new MimeMessageNormalizer($propertyNormalizer),
252+
new ObjectNormalizer(null, null, null, $extractor),
253+
$propertyNormalizer
254+
], [new JsonEncoder()]);
255+
256+
$serialized = $serializer->serialize($e, 'json');
257+
$this->assertSame($expectedJson, json_encode(json_decode($serialized), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
258+
259+
$n = $serializer->deserialize($serialized, Message::class, 'json');
260+
$this->assertEquals($expected->getHeaders(), $n->getHeaders());
261+
262+
$serialized = $serializer->serialize($e, 'json');
263+
$this->assertSame($expectedJson, json_encode(json_decode($serialized), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
264+
}
150265
}

src/Symfony/Component/Mime/composer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
"symfony/deprecation-contracts": "^2.1",
2121
"symfony/polyfill-intl-idn": "^1.10",
2222
"symfony/polyfill-mbstring": "^1.0",
23-
"symfony/polyfill-php80": "^1.15"
23+
"symfony/polyfill-php80": "^1.15",
24+
"symfony/property-info": "^4.4|^5.1",
25+
"symfony/serializer": "^4.4|^5.1"
2426
},
2527
"require-dev": {
2628
"egulias/email-validator": "^2.1.10",

0 commit comments

Comments
 (0)