Description
I extend my heartfelt gratitude to the Symfony community for your unwavering dedication and perseverance.
Background
I was using the Laravel 10 mailer along with Symfony Mailer and encountered a bug related to failover functionality when the first mailer failed to send an email.
Failover Configuration
My failover setup is as follows:
Primary Mailer Driver: Mailgun
Secondary Mailer Driver: Amazon SES
Issue Description
When an email fails to send using Mailgun, the system attempts to send the email via Amazon SES. However, I discovered that emails sent through Amazon SES have broken image sources (src), causing the images to not display correctly in the emails.
Package version
symfony/mailer 6.4.8
symfony/mailgun-mailer 7.1.1
Try to Fix
I tried to fix the issue in the file located at vendor/symfony/mailer/Transport/RoundRobinTransport.php.
It worked when I made the following changes:
It seems that the $message attachments are a reference. The clone operation can't properly clone the attachments objects. Therefore, I changed it to use deep_copy to perform a deep clone for each send operation.
I also found that the Mailgun driver changes the attachments' CIDs.
Here is the modified section of the code:
public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage
{
$exception = null;
while ($transport = $this->getNextTransport()) {
try {
$currentMessage = deep_copy($message); //here i change
return $transport->send($currentMessage, Envelope::create($currentMessage)); //here i change
} catch (TransportExceptionInterface $e) {
$exception ??= new TransportException('All transports failed.');
$exception->appendDebug(sprintf("Transport \"%s\": %s\n", $transport, $e->getDebug()));
$this->deadTransports[$transport] = microtime(true);
}
}
throw $exception ?? new TransportException('No transports found.');
}
My Local debug test
When I change the first driver to SMTP, it works well.
I do this before the FailoverTransport class, prior to the first send.
$s = new Symfony\Component\Mailer\SentMessage($message,$envelope);
$s->toString();
The attachment will have the wrong src path.
When I deep copy the message, everything worked well !!!
$current = deep_copy($message)
$s = new Symfony\Component\Mailer\SentMessage($current,$envelope);
$s->toString();
The Next Info for Reference
Here is the first email source.
It's normal and works well.
This will be used to compare with the second email source.
The image source is cid:V9J4rKBvmB and it will be replaced with cid:d3a788f4cbf3a6aab32dcacde69a2e34@symfony, resulting in the following output:
Content-Type: multipart/related;
...
\<img id=3D"logo" width=3D"190" height=3D"40" style=3D"width:=
190px; height: 40px; -ms-interpolation-mode: bicubic;" src=3D"cid:d3a788f4cbf3a6aab32dcacde69a2e34@symfony" alt=3D""\>
....
\--KGCwwzRU
Content-ID: d3a788f4cbf3a6aab32dcacde69a2e34@symfony
Content-Type: image/png; name="d3a788f4cbf3a6aab32dcacde69a2e34@symfony"
Content-Transfer-Encoding: base64
Content-Disposition: inline;
name="d3a788f4cbf3a6aab32dcacde69a2e34@symfony"; filename=V9J4rKBvmB
Here is the second image. The source is incorrect, so it can't be displayed.
The problematic image email source is here:
The image source is cid:L7HALxfBwi.
It should be updated to Content-ID cid:2eabc9240b3557c72ad65e92a60f7f57@symfony, but the result is still cid:L7HALxfBwi.
Content-Type: multipart/mixed;
\<img id=3D"logo" width=3D"190" height=3D"40" style=3D"width:=
190px; height: 40px; -ms-interpolation-mode: bicubic;" src=3D"cid:L7HALxfBwi" alt=3D""\>
...
\--TG5g10Dt
Content-ID: 2eabc9240b3557c72ad65e92a60f7f57@symfony
Content-Type: image/png; name="2eabc9240b3557c72ad65e92a60f7f57@symfony"
Content-Transfer-Encoding: base64
Content-Disposition: inline;
name="2eabc9240b3557c72ad65e92a60f7f57@symfony"; filename=L7HALxfBwi
I found that this issue might be related to the Symfony\Component\Mailer\Transport\AbstractTransport class, specifically in the send function.
$message = clone $message; // this clone haven't clone the inner object var
fake code for my test
I use Symfony Mailer in Laravel 10. Here is some example code (sorry for the demo code):
// Set the mailgun always fail
config()->set('services.mailgun.endpoint', '127.0.0.1');
// Set the mail mailers configuration for failover
config()->set('mail.mailers.failover.mailers', ['mailgun', 'ses']);
// Get the mail manager instance and set the Symfony transporter
$mail_manager = app()->get('mail.manager');
$mail_manager->setSymfonyTransport($mail_manager->createSymfonyTransport(config('mail.mailers.failover')));
// Send the email using the specified driver
Mail::mailer($driver)->send(new MailableView(1, "The $driver mailable view test email"));
// Use Illuminate\Mail\Mailable to define your Mailable class
use Illuminate\Mail\Mailable;
class Mailable extends IlluminateMailable
{
public function build()
{
// Compose the email view
return $this->view('emails.mailable_view_test.email_view')
->text('emails.mailable_view_test.email_view_text')
->sender('xxx', $name)
->from('xxx', $name)
->replyTo('xxx', $name)
->to('xxx', 'xxx')
->with([
'name' => 'xxx',
'url' => 'xxx',
])
->subject($this->title);
}
}
in the blade template of view
<!-- resources/views/emails/mailable_view_test/email_view.blade.php -->
<!DOCTYPE html>
<html>
<head>
<title>{{ $subject }}</title>
</head>
<body>
<h1>Hello, {{ $name }}</h1>
<p>Welcome to our service. Please visit <a href="{{ $url }}">this link</a> for more information.</p>
<img src="{{ $message->embedData($logo, 'logo.png', 'image/png') }}" alt="Logo">
</body>
</html>
Additional Context
here is the dump of the $message by laravel dd() function, this content to show clone will not clone the #1826 object of $message->attachments
first email var $message dump, this is normal for compare the second wrong one
-attachments: array:1 [
0 => Symfony\Component\Mime\Part\DataPart^ {#1826
-headers: Symfony\Component\Mime\Header\Headers^ {#1829
-headers: []
-lineLength: 76
}
#_headers: ? Symfony\Component\Mime\Header\Headers
-body: Symfony\Component\Mime\Part\File^ {#1818
-path: "/User/rick/xxxx/img/logo-negative.png"
-filename: null
}
-charset: null
-subtype: "png"
-disposition: "inline"
-name: "4nF4PcToTi"
-encoding: "base64"
-seekable: null
#_parent: ? array
-filename: "4nF4PcToTi"
-mediaType: "image"
-cid: null
}
]
-cachedBody: null
with broken image src of second email $message dump.
-attachments: array:1 [
0 => Symfony\Component\Mime\Part\DataPart^ {#1826 //object is same
-headers: Symfony\Component\Mime\Header\Headers^ {#1829 //object is same
-headers: array:1 [
"content-id" => array:1 [
0 => Symfony\Component\Mime\Header\IdentificationHeader^ {#1932
-name: "Content-ID"
-lineLength: 76
-lang: null
-charset: "utf-8"
-ids: array:1 [
0 => "9b83b7043798ef148b6b46ac184ca696@symfony"
]
-idsAsAddresses: array:1 [
0 => Symfony\Component\Mime\Address^ {#1933
-address: "9b83b7043798ef148b6b46ac184ca696@symfony"
-name: ""
}
]
}
]
]
-lineLength: 76
}
#_headers: ? Symfony\Component\Mime\Header\Headers
-body: Symfony\Component\Mime\Part\File^ {#1818
-path: "/User/rick/xxxx/img/logo-negative.png"
-filename: null
}
-charset: null
-subtype: "png"
-disposition: "inline"
-name: "7a7a484a463c3667a9e55d610c2f0641@symfony"
-encoding: "base64"
-seekable: null
#_parent: ? array
-filename: "4nF4PcToTi"
-mediaType: "image"
-cid: "7a7a484a463c3667a9e55d610c2f0641@symfony"
}
]
-cachedBody: null