Events & Overriding "Recipients"

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

I want to propose two cool ideas.

First, while we're developing, if we decide to use Mailtrap, great: all of our emails will go there. But if we decide that we want to use SendGrid to send real emails while developing... it's a little trickier. For example, whenever you register, you would need to use a real email address. Otherwise, the email would never make it to your inbox.

So here's idea number 1: what if, in the dev environment only, we globally override the "to" of every email and send to ourselves. So even if we registered as space_cadet@example.com - the email would actually be delivered to our real address: ryan@symfonycasts.com for me. That would be cool!

My second idea is similar: instead of manually setting the from() on every email object... what if we hook into mailer and set this globally. That's less duplication and more consistency.

Hooking into Mailer: MessageEvent

The way to accomplish both of these is by leveraging an event. Whenever an email is sent through Mailer, internally, it dispatches one event called MessageEvent. Mailer itself comes with a couple of classes that can "listen" to this event. The most interesting one is called EnvelopeListener.

Built-in Listener: EnvelopeListener

I'll hit Shift+Shift and look for EnvelopeListener so we can see inside. Start by looking for getSubscribedEvents(). Yep! This is listening on MessageEvent. Here's the idea: if you used this class, you could instantiate it and pass a custom sender or a custom array of recipients. Then, whenever an email is sent, the onMessage() method would be called and it would override that stuff on the email.

I love it! Even though this class lives inside Mailer, Symfony doesn't activate it by default: it's not currently being used. In Symfony 4.4, some new config options were been added so you can activate & configure it easily:

# config/packages/mailer.yaml
# or config/packages/dev/mailer.yaml for only the dev environment
framework:
  mailer:
    envelope:
      sender: 'sender@example.org'
      recipients: ['redirected@example.org']

But in Symfony 4.3, if we want to use this class, we need to activate it manually... which is kinda fun anyways.

So here's the plan: to start, in the development environment only, I want all emails to actually be sent to ryan@symfonycasts.com, regardless of the to() address on the email.

Setting up the Dev Email

To do this, in .env, let's create a brand new, shiny environment variable: DEV_MAIL_RECIPIENT set to, how about, someone@example.com.

55 lines .env
... lines 1 - 53
DEV_MAIL_RECIPIENT=someone@example.com

That's not a real email, because each developer should need to copy this variable, open their own .env.local file, and customize it to whatever they want.

Registering EnvelopeListener in dev Only

Next, we need to register EnvelopeListener as a service... but only in the dev environment: I don't want to change the recipients on production. To do that, in the config/ directory, create a new file called services_dev.yaml. Thanks to that _dev part, this will only be loaded in the dev environment. At the top, start with the same _defaults code that we have on top of our main services file: services:, then the magic _defaults: to set up some default options that we want to apply to every service registered in this file. The default config we want is autowire: true and autoconfigure: true.

services:
_defaults:
autowire: true
autoconfigure: true
... lines 5 - 10

Now, let's register EnvelopeListener as a service. Copy its namespace, paste, add a \ then go copy the class name and put that here too.

services:
... lines 2 - 5
Symfony\Component\Mailer\EventListener\EnvelopeListener:
... lines 7 - 10

For arguments, the class has two: $sender and an array of $recipients. We'll focus on setting the "sender" globally in a few minutes... but for right now, I don't want to use that feature... so we can set the argument to null. Under arguments, use - null for sender and, for recipients, - [] with one email inside. To reference the environment variable we created, say %env()%, then copy the variable name - DEV_MAIL_RECIPIENT - and paste it in the middle.

services:
... lines 2 - 5
Symfony\Component\Mailer\EventListener\EnvelopeListener:
arguments:
- null
- ['%env(DEV_MAIL_RECIPIENT)%']

That should be it! This will register the service and, thanks to autoconfigure, Symfony will configure it as an event subscriber.

Testing time! Move over, refresh and... ah! I have a typo! The key should be _defaults with an "s". Try it again. This time register with a fake email: thetruthisoutthere13@example.com, any password, agree to the terms and register!

Because our app is configured to use SendGrid... that should have sent a real email. Check the inbox - we have a new one! That's the original email from a minute ago on top... and here's the new one.

Recipients Versus To

But! This is even cooler. If you were watching really closely, you may have noticed that, in EnvelopeListener, what we're setting is something called "recipients". But when we create an email... we use a method call ->to(). It turns out, those are two different concepts. Gasp!

Back over in gmail, I'll click to view the "original" message. Check this out: this email is to thetruthisoutthere13@example.com. Search for ryan@symfonycasts. Hmm, it says Delivered-To: ryan@symfonycasts.com.

Envelope Versus Message

Here's what's going on. Just like how, in the real world, you put a "message" into an "envelope" and then send it through the real-world mail, an email is also these same two parts: the message itself and an envelope that goes around it. The To of an email is what's written on top of the "message". But the envelope around that message could have a totally different address on it. That is known as the "recipient". The envelope is how the email is delivered. And the message is basically what you're looking at inside your inbox.

So by setting the recipients, we changed the address on the envelope, which caused the email to be delivered to ryan@symfonycasts.com. But the To on the message inside is still thetruthisoutthere13@example.com.

This... for the most part... is just fun mail trivia. Most of the time, the "To" and the "recipients" will be the same. And... that's exactly what happens if you set the To but don't set the recipients: mailer sets the recipients for you... to match the To.

This idea becomes even more important when we talk about setting the from address globally so we don't need to set it on every email. Because... yep, from is different than "sender". That's next.

Leave a comment!

This tutorial is built on Symfony 4.3, but will work well with Symfony 4.4 or 5.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "aws/aws-sdk-php": "^3.87", // 3.110.11
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.1
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-snappy-bundle": "^1.6", // v1.6.0
        "knplabs/knp-time-bundle": "^1.8", // v1.9.1
        "league/flysystem-aws-s3-v3": "^1.0", // 1.0.23
        "league/flysystem-cached-adapter": "^1.0", // 1.0.9
        "league/html-to-markdown": "^4.8", // 4.8.2
        "liip/imagine-bundle": "^2.1", // 2.1.0
        "nexylan/slack-bundle": "^2.1,<2.2.0", // v2.1.0
        "oneup/flysystem-bundle": "^3.0", // 3.1.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.4.1
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.3.4
        "symfony/console": "^4.0", // v4.3.4
        "symfony/flex": "^1.9", // v1.9.10
        "symfony/form": "^4.0", // v4.3.4
        "symfony/framework-bundle": "^4.0", // v4.3.4
        "symfony/mailer": "4.3.*", // v4.3.4
        "symfony/messenger": "4.3.*", // v4.3.4
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.3.4
        "symfony/sendgrid-mailer": "4.3.*", // v4.3.4
        "symfony/serializer-pack": "^1.0", // v1.0.2
        "symfony/twig-bundle": "^4.0", // v4.3.4
        "symfony/twig-pack": "^1.0", // v1.0.0
        "symfony/validator": "^4.0", // v4.3.4
        "symfony/web-server-bundle": "^4.0", // v4.3.4
        "symfony/webpack-encore-bundle": "^1.4", // v1.6.2
        "symfony/yaml": "^4.0", // v4.3.4
        "twig/cssinliner-extra": "^2.12", // v2.12.0
        "twig/extensions": "^1.5", // v1.5.4
        "twig/inky-extra": "^2.12" // v2.12.0
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.2.2
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/browser-kit": "4.3.*", // v4.3.5
        "symfony/debug-bundle": "^3.3|^4.0", // v4.3.4
        "symfony/dotenv": "^4.0", // v4.3.4
        "symfony/maker-bundle": "^1.0", // v1.13.0
        "symfony/monolog-bundle": "^3.0", // v3.4.0
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.3.4
        "symfony/profiler-pack": "^1.0", // v1.0.4
        "symfony/var-dumper": "^3.3|^4.0" // v4.3.4
    }
}