This course is still being released! Check back later for more chapters.
Global From (and Fun) with Email Events
I bet that most, if not every email your app sends will be from the same email address, something clever like hal9000@universal-travel.com
or the tried-and-true but sleepier info@universal-travel.com
.
Because every email will have the same from address, there's no point to set it in every email. Instead, let's set it globally. Oddly, there isn't any tiny config option for this. But that's great for us: it gives us a chance to learn about events! Very powerful, very nerdy.
The MessageEvent
Before an email is sent, Mailer dispatches a MessageEvent
.
To listen to this, find your terminal and run:
symfony console make:listener
Call it GlobalFromEmailListener
. The gives us a list of events we can listen to. We want the first one: MessageEvent
. Start typing Symfony
and it's autocompleted for us. Hit enter.
Listener created!
To be extra cool, let's set our global from address as a parameter. In config/services.yaml
, under parameters
, add a new one: global_from_email
.
Special Email Address String
This will be a string, but check this out: set it to Universal Travel
, then in angle brackets, put the email: <info@universal-travel.com>
:
// ... lines 1 - 5 | |
parameters: | |
global_from_email: 'Universal Travel <info@universal-travel.com>' | |
// ... lines 8 - 26 |
When Symfony Mailer sees a string that looks like this as an email address, it'll create the proper Address
object with both a name and email set. Sweet!
MessageEvent
Listener
Open the new class src/EventListener/GlobalFromEmailListener.php
. Add a constructor with a private string $fromEmail
argument and an #[Autowire]
attribute with our parameter name: %global_from_email%
:
// ... lines 1 - 8 | |
final class GlobalFromEmailListener | |
{ | |
public function __construct( | |
#[Autowire('%global_from_email%')] | |
private string $fromEmail, | |
) { | |
} | |
// ... lines 16 - 21 | |
} |
Down here, the #[AsEventListener]
attribute is what marks this method as an event listener. We can actually remove this event
argument - it'll be inferred from the method argument's type-hint: MessageEvent
:
// ... lines 1 - 9 | |
final class GlobalFromEmailListener | |
{ | |
// ... lines 12 - 17 | |
public function onMessageEvent(MessageEvent $event): void | |
{ | |
// ... lines 21 - 31 | |
} | |
} |
Inside, first grab the message from the event: $message = $event->getMessage()
:
// ... lines 1 - 9 | |
final class GlobalFromEmailListener | |
{ | |
// ... lines 12 - 18 | |
public function onMessageEvent(MessageEvent $event): void | |
{ | |
$message = $event->getMessage(); | |
// ... lines 22 - 31 | |
} | |
} |
Jump into the getMessage()
method to see what it returns. RawMessage
... jump into this and look at what classes extend it. TemplatedEmail
! Perfect!
Back in our listener, write if (!$message instanceof TemplatedEmail)
, and inside, return;
:
// ... lines 1 - 9 | |
final class GlobalFromEmailListener | |
{ | |
// ... lines 12 - 18 | |
public function onMessageEvent(MessageEvent $event): void | |
{ | |
// ... lines 21 - 22 | |
if (!$message instanceof TemplatedEmail) { | |
return; | |
} | |
// ... lines 26 - 31 | |
} | |
} |
This will likely never be the case, but it's good practice to double-check. Plus, it helps our IDE know that $message
is a TemplatedEmail
now.
It's possible that an email might still set its own from
address. In this case, we don't want to override it. So, add a guard clause: if ($message->getFrom())
, return;
:
// ... lines 1 - 9 | |
final class GlobalFromEmailListener | |
{ | |
// ... lines 12 - 18 | |
public function onMessageEvent(MessageEvent $event): void | |
{ | |
// ... lines 21 - 26 | |
if ($message->getFrom()) { | |
return; | |
} | |
// ... lines 30 - 31 | |
} | |
} |
Now, we can set the global from
: $message->from($this->fromEmail)
:
// ... lines 1 - 9 | |
final class GlobalFromEmailListener | |
{ | |
// ... lines 12 - 18 | |
public function onMessageEvent(MessageEvent $event): void | |
{ | |
// ... lines 21 - 30 | |
$message->from($this->fromEmail); | |
} | |
} |
Perfect!
Back in TripController::show()
, remove the ->from()
for the email.
Time to test this! In our app, book a trip and check Mailtrap for the email. Drumroll... the from
is set correctly! Our listener works! I never doubted us.
Reply-To
One more detail to make this completely airtight (like most of our ships).
Imagine a contact form where the user fills their name, email, and a message. This fires off an email with these details to your support team. In their email clients, it'd be nice if, when they hit reply, it goes to the email from the form - not your "global from".
You might think that you should set the from
address to the user's email. But that won't work, as we're not authorized to send emails on behalf of that user. More on email security soon.
Fortunately, there's a special email header called Reply-To
for just this scenario. When building your email, set it with ->replyTo()
and pass the user's email address.
Strap in because the booster tanks are full and ready for launch! Time to send real emails in production! That's next.
Although using the event listener is a lot more fun, you can also use the
framework.mailer.headers
config to set the from address globally: https://symfony.com/doc/current/mailer.html#configuring-emails-globally