Log emails as *.eml in Laravel Mailer
How to log full email messages into *.eml
files on filesystem in your Laravel project? We would like to log full message bodies for all messages, no matter if they are sent out directly using Laravel's Mail
facade or as Mail Notifications. That can actually be implemented in a super easy way, but it was so hard to find any good documentation or tutorial about this.
NOTE: Swift Mailer is no longer maintained since Nov 2021. In Laravel 9 LTS, which is going to be released on Jan 25th 2022, Swift Mailer is going to be swapped out with Symfony Mailer, see PR #38481. Below presented solution seems to be compatible with the new Mailer, though. I'll keep this post updated, if any changes are needed for Laravel 9.
Simple log channel implementation (DON'T)
In this Stackoverflow post, I have presented a simple way of logging email messages.
Create logdir logs/emails
Add an extra channel configuration to config/logging.php
:
'channels' => [
// ...
'emails' => [
'driver' => 'single',
'path' => storage_path('logs/emails/' . \Illuminate\Support\Str::uuid() . '.eml'),
'level' => 'debug',
],
And then simply use the following in your .env
:
MAIL_MAILER=log
MAIL_LOG_CHANNEL=emails
That way, you get wonderful logs/emails/<UUID>.eml
files. But this solution has several drawbacks:
- There is no easy way of sending out emails both through
MAIL_MAILER=log
and some other mailer, e.g. the standardMAIL_MAILER=smtp
- Writing more than one message to the
email
log channel during the same run would not trigger an updated UUID, so the second message would get appended to the same logfile. This happens e.g. if you trigger a notification in Tinker. - The first line (before
Message-ID: ...
) of the email header is prefixed with log formatting prefix[_TIMESTAMP_] local.DEBUG:
. As a non-standard email header is ignored by all/most email clients, this should be no issue in development. But definitely not cool! - The
*.eml
message body is scrambled, somehow. The MIME multipart content type boundary gets lost, so the email client has no idea where the HTML part of the message body starts and which part to treat as plaintext.
So, this solution is rejected! We need some slightly more sophisticated logging!
LogSentMessage
event listener
Let's forget about the emails
log channel from above solution and create the following disk configuration instead, config/filesystems.php
:
return [
// ...
'disks' => [
// ...
'emails' => [
'driver' => 'local',
'root' => storage_path('logs/emails'),
],
Instead of changing the Mailer in .env
(keep whatever MAIL_MAILER
you are used to!), we build our LogSentMessage
event listener and use Laravel's MessageSent
event.
Create app/Listeners/LogSentMessage.php
, which is going to write the full message:
<?php
namespace App\Listeners;
use Illuminate\Mail\Events\MessageSent;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class LogSentMessage
{
public function handle(MessageSent $event)
{
$messageId = $event->data['__laravel_notification_id'] ?? Str::uuid();
Storage::disk('emails')->put(
sprintf('%s_%s.eml', now()->format('YmdHis'), $messageId),
$event->message->toString()
);
}
}
The LogSentMessage
event listener can now be hooked into app/Providers/EventServiceProvider.php
<?php
namespace App\Providers;
use App\Listeners\LogSentMessage;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Mail\Events\MessageSent;
class EventServiceProvider extends ServiceProvider
{
/**
* The event listener mappings for the application.
*
* @var array
*/
protected $listen = [
MessageSent::class => [
LogSentMessage::class,
],
];
}
To generate the listener class, you could simply add these lines to
EventServiceProvider.php
:protected $listen = [ + MessageSent::class => [ + 'App\Listeners\LogSentMessage', + ], ];
... and then fire:
$ php artisan event:generate
This works great! Email notifications/messages are now getting sent out normally and at the same time a copy of the full message is stored here:
storage/logs/emails
├── 20211203131816_385ce6e8-437c-427e-828c-7464a4a48ca2.eml
└── 20211203132656_757c62b6-b6fa-45d0-99e3-6caf5be6a9f3.eml
You can open or even import them with any mail client and they are just 100% valid emails, including all headers (also Bcc). Just make sure this storage path is never publicly exposed.
Thanks @davewood for this simple trick.