Skip to content

Commit 6467fa4

Browse files
committed
bug symfony#46863 [Mime] Fix invalid DKIM signature with multiple parts (BrokenSourceCode)
This PR was squashed before being merged into the 4.4 branch. Discussion ---------- [Mime] Fix invalid DKIM signature with multiple parts | Q | A | ------------- | --- | Branch? | 4.4 | Bug fix? | yes | New feature? | no | Deprecations? | no | Tickets | Fix symfony#42407, symfony#40131, symfony#39354 | License | MIT After many, many hours of investigations, I finally isolated the issue. When sending an email with a DKIM signature, **2** boundaries are generated because **2** instances of [`AbstractMultipartPart`](https://github.com/symfony/symfony/blob/04ae33a1f92bd76e17c7f0e7bbaacb5248cde79e/src/Symfony/Component/Mime/Part/AbstractMultipartPart.php) ([`AlternativePart`](https://github.com/symfony/symfony/blob/04ae33a1f92bd76e17c7f0e7bbaacb5248cde79e/src/Symfony/Component/Mime/Part/Multipart/AlternativePart.php) in my case) are created for **1** email. Backtrace of the first boundary: ``` Symfony\Component\Mime\Part\AbstractMultipartPart->getBoundary() Symfony\Component\Mime\Part\AbstractMultipartPart->bodyToIterable() Symfony\Component\Mime\Crypto\DkimSigner->hashBody() Symfony\Component\Mime\Crypto\DkimSigner->sign() ``` Backtrace of the second boundary: ``` Symfony\Component\Mime\Part\AbstractMultipartPart->getBoundary() Symfony\Component\Mime\Part\AbstractMultipartPart->getPreparedHeaders() Symfony\Component\Mime\Part\AbstractPart->toIterable() Symfony\Component\Mime\Message->toIterable() Symfony\Component\Mime\RawMessage->toIterable() Symfony\Component\Mailer\Transport\Smtp\Stream\AbstractStream::replace() Symfony\Component\Mailer\Transport\Smtp\SmtpTransport->doSend() Symfony\Component\Mailer\Transport\AbstractTransport->send() Symfony\Component\Mailer\Transport\Smtp\SmtpTransport->send() Symfony\Component\Mailer\Mailer->send() ``` The fix is to use a single instance of [`AbstractMultipartPart`](https://github.com/symfony/symfony/blob/04ae33a1f92bd76e17c7f0e7bbaacb5248cde79e/src/Symfony/Component/Mime/Part/AbstractMultipartPart.php). I suggest to use a cache system for the method [`Email->getBody()`](https://github.com/symfony/symfony/blob/04ae33a1f92bd76e17c7f0e7bbaacb5248cde79e/src/Symfony/Component/Mime/Email.php#L409-L416) with a property named `$cachedBody`. In this way, only one instance will be created. The property `$cachedBody` will be reset whenever necessary. Thanks to @calebsolano for putting me on the right way with his comment symfony#39354 (comment). > My instincts tell me it has something to do with the multipart boundary, in particular with it being random...but I can't determine where in the code it instantiates a second one that would have a different value between creating the body hash and the actual body I hope this solution will suit you, I couldn't think of an easier one. Commits ------- f09f960 [Mime] Fix invalid DKIM signature with multiple parts
2 parents 2670125 + f09f960 commit 6467fa4

File tree

2 files changed

+34
-0
lines changed

2 files changed

+34
-0
lines changed

src/Symfony/Component/Mime/Email.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class Email extends Message
4343
private $html;
4444
private $htmlCharset;
4545
private $attachments = [];
46+
private ?AbstractPart $cachedBody = null; // Used to avoid wrong body hash in DKIM signatures with multiple parts (e.g. HTML + TEXT) due to multiple boundaries.
4647

4748
/**
4849
* @return $this
@@ -282,6 +283,7 @@ public function text($body, string $charset = 'utf-8')
282283
throw new \TypeError(sprintf('The body must be a string, a resource or null (got "%s").', get_debug_type($body)));
283284
}
284285

286+
$this->cachedBody = null;
285287
$this->text = $body;
286288
$this->textCharset = $charset;
287289

@@ -312,6 +314,7 @@ public function html($body, string $charset = 'utf-8')
312314
throw new \TypeError(sprintf('The body must be a string, a resource or null (got "%s").', get_debug_type($body)));
313315
}
314316

317+
$this->cachedBody = null;
315318
$this->html = $body;
316319
$this->htmlCharset = $charset;
317320

@@ -342,6 +345,7 @@ public function attach($body, string $name = null, string $contentType = null)
342345
throw new \TypeError(sprintf('The body must be a string or a resource (got "%s").', get_debug_type($body)));
343346
}
344347

348+
$this->cachedBody = null;
345349
$this->attachments[] = ['body' => $body, 'name' => $name, 'content-type' => $contentType, 'inline' => false];
346350

347351
return $this;
@@ -352,6 +356,7 @@ public function attach($body, string $name = null, string $contentType = null)
352356
*/
353357
public function attachFromPath(string $path, string $name = null, string $contentType = null)
354358
{
359+
$this->cachedBody = null;
355360
$this->attachments[] = ['path' => $path, 'name' => $name, 'content-type' => $contentType, 'inline' => false];
356361

357362
return $this;
@@ -368,6 +373,7 @@ public function embed($body, string $name = null, string $contentType = null)
368373
throw new \TypeError(sprintf('The body must be a string or a resource (got "%s").', get_debug_type($body)));
369374
}
370375

376+
$this->cachedBody = null;
371377
$this->attachments[] = ['body' => $body, 'name' => $name, 'content-type' => $contentType, 'inline' => true];
372378

373379
return $this;
@@ -378,6 +384,7 @@ public function embed($body, string $name = null, string $contentType = null)
378384
*/
379385
public function embedFromPath(string $path, string $name = null, string $contentType = null)
380386
{
387+
$this->cachedBody = null;
381388
$this->attachments[] = ['path' => $path, 'name' => $name, 'content-type' => $contentType, 'inline' => true];
382389

383390
return $this;
@@ -388,6 +395,7 @@ public function embedFromPath(string $path, string $name = null, string $content
388395
*/
389396
public function attachPart(DataPart $part)
390397
{
398+
$this->cachedBody = null;
391399
$this->attachments[] = ['part' => $part];
392400

393401
return $this;
@@ -446,6 +454,10 @@ public function ensureValidity()
446454
*/
447455
private function generateBody(): AbstractPart
448456
{
457+
if (null !== $this->cachedBody) {
458+
return $this->cachedBody;
459+
}
460+
449461
$this->ensureValidity();
450462

451463
[$htmlPart, $attachmentParts, $inlineParts] = $this->prepareParts();
@@ -471,6 +483,8 @@ private function generateBody(): AbstractPart
471483
}
472484
}
473485

486+
$this->cachedBody = $part;
487+
474488
return $part;
475489
}
476490

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,4 +458,24 @@ public function testTextBodyAcceptedTypes()
458458
$email->text($contents);
459459
$this->assertSame($contents, $email->getTextBody());
460460
}
461+
462+
public function testBodyCache()
463+
{
464+
$email = new Email();
465+
$email->from('[email protected]');
466+
$email->to('[email protected]');
467+
$email->text('foo');
468+
$body1 = $email->getBody();
469+
$body2 = $email->getBody();
470+
$this->assertSame($body1, $body2, 'The two bodies must reference the same object, so the body cache ensures that the hash for the DKIM signature is unique.');
471+
472+
$email = new Email();
473+
$email->from('[email protected]');
474+
$email->to('[email protected]');
475+
$email->text('foo');
476+
$body1 = $email->getBody();
477+
$email->html('<b>bar</b>'); // We change a part to reset the body cache.
478+
$body2 = $email->getBody();
479+
$this->assertNotSame($body1, $body2, 'The two bodies must not reference the same object, so the body cache does not ensure that the hash for the DKIM signature is unique.');
480+
}
461481
}

0 commit comments

Comments
 (0)