Send Email From Multiple Domains In Laravel 10

Last updated 8th September, 2023

Out of the box Laravel provides a fluent system for sending emails. For the sake of getting to the answer quickly I'll assume that you know how to send email using Laravel's mail facade. If you want to look at the basics, there's nothing better than the documentation.

Altering The Laravel Mailer Config

Typically in Laravel, you would set up your config to include the domain you want to send from in your config/services.php (or config/mail.php if you're on Laravel 8 or below). Which will likely look something like this:

'mailgun' => [
    'domain' => env('MAILGUN_DOMAIN'),
    'secret' => env('MAILGUN_SECRET'),
],

I'll use Mailgun in all examples here to keep things simple, but the functionality between providers will likely be the same.

"Can't you just switch the config out?"

It stands to reason that you would be able to simply switch the domain in config at runtime, and the underlying system would simply switch to that domain when sending.

Spoiler, you can't. I learned this the hard way.

The reason this happens is because the underlying mail driver is a singleton which is only instantiated once at the start of the process.

How Laravel Manages Mail Behind The Scenes

We'll have to replace the transport driver behind Laravel's mailer facade to change how emails are sent on the fly. We'll get into that, but lets break down what Laravel is doing behind the scenes first so we can understand what's going on.

When the application boots, config/app.php will register all of the application service providers required to run Laravel. This includes the Illuminate\Mail\MailServiceProvider. When this provider is registered, it binds the mail.manager singleton, which is responsible managing the configuration of the drivers in Laravel's mail system.

class MailServiceProvider extends ServiceProvider
{
    protected function registerIlluminateMailer()
    {
        $this->app->singleton('mail.manager', function ($app) {
            return new MailManager($app);
        });
		
        $this->app->bind('mailer', function ($app) {
            return $app->make('mail.manager')->mailer();
        });
    }
}

Next, it uses that instance to configure the mailer for the application. This part is important because although the mailer instance is not a singleton, and will be re-created every time its resolved; that instance itself is cached on the mail.manager instance, which is cached in the container. This is why changing the config doesn't work - its already bound into the container by this point.

You can see this happening in Illuminate/Mail/MailManager.php:

protected function get($name)
{
    return $this->mailers[$name] ?? $this->resolve($name);
}

P.s. If you're unsure on how the container works in Laravel, the wonderful people at beyondCode have a great article that explains all the complexity (and benefits!) of Laravel's service container.

Swapping Out The Mail Driver

Fortunately, Laravel allows us to access the transport driver that SymfonyMailer (or SwiftMailer if you're on Laravel 8 or below) uses to send mail to various different services. Using the the mail.manager interface that I talked about earlier you can swap in a new transport driver with our new domain:

// If you're on Laravel 8 or below, this config will be
// in config/mail.php
$config = config('services.mailgun');

Mailer::setSymfonyTransport(app('mail.manager')->createSymfonyTransport([
    'transport' => 'mailgun',
    'secret' => $config['secret'],
    'domain' => 'my-other-domain',
]));

Mail::to('...')->send(new ExampleMailable);

Creating A Mail Facade Macro To DRY It Up

I know what you're thinking... its a little verbose to run this at runtime.

What I've done in projects that have required this kind of functionality is create a macro on the Mailer instance to abstract away this functionality so I don't have to worry about the internals.

Macros are a way of decorating classes to extend their functionality. Here's an article I like if you're interested in learning about them.

class AppServiceProvider extends ServiceProvider
{
    Mailer::macro('domain', function (string $domain) {
        $config = config('services.mailgun');

        Mailer::setSymfonyTransport(app('mail.manager')->createSymfonyTransport([
            'transport' => 'mailgun',
            'secret' => $config['secret'],
            'domain' => $domain,
        ]));

        return $this;
    });
}

By adding the domain macro, you can now call it when accessing the Mail facade, which delegates calls to the Mailer class to which the method is bound.

Mail::domain('my-fancy-domain.com')
    ->to('...')
    ->send(new ExampleMailable);

Tidy right?

Resetting The Driver After Each Send

There's one thing to remember here. Once you've swapped out that transport driver to give the mailer the domain you want to send from - it won't get refreshed until the end of the current process. If you happen try to send a mail with your default settings after this has run in the current process - it will continue to use this driver to do so.

If you need to make sure that the driver is reset each time, you can call the purge and forgetDrivers methods to clear these bound instances. That way they will get recreated again next time the class is called.

// To clear a single mailer driver...
app('mail.manager')->purge('mailgun');

// Forget all mailer drivers...
app('mail.manager')->forgetDrivers();

You can check this works by running it in tinker and looking at the hashes of the resolved classes.

// The {#number} represents the instance of the object returned
// a matching number means that the same instance was resolved
Mail::domain('test.com'); // Illuminate\Mail\Mailer {#5978}
Mail::domain('test.com'); // Illuminate\Mail\Mailer {#5978}
Mail::domain('test.com'); // Illuminate\Mail\Mailer {#5978}

app('mail.manager')->forgetMailers();

// A different instance of the mailer is resolved!
Mail::domain('test.com'); // Illuminate\Mail\Mailer {#5862}

Mail::mailer('mailgun')->domain('test.com'); // Illuminate\Mail\Mailer {#5978}
Mail::mailer('mailgun')->domain('test.com'); // Illuminate\Mail\Mailer {#5978}
Mail::mailer('mailgun')->domain('test.com'); // Illuminate\Mail\Mailer {#5978}

// This time only the mailgun driver will be cleared
app('mail.manager')->purge('mailgun');

Mail::mailer('mailgun')->domain('test'); // Illuminate\Mail\Mailer {#6063}

Hope This Helped 🤙

No comments yet…

DevInTheWild.

Login To Add Comments.

Want More Like This?

Subscribe to occasional updates.

Related Articles