8.9 KiB
+++ title = "Laravel dynamic SMTP mail configuration" date = "2021-11-09" author = "Aloïs Micard" authorTwitter = "" #do not include @ cover = "" tags = ["Laravel", "PHP", "Tutorial"] keywords = ["Laravel", "SMTP", "Dynamic", "PHP", "Tutorial"] description = "How to use dynamic SMTP credentials with Laravel." showFullContent = false +++
Hello friend...
It has been a while.
I have been very busy lately with work, open source and life that I didn't find the energy to write a blog post. Despite having some good ideas, I wasn't really in the mood.
Hopefully, I now have the energy and the subject to make a good blog post: let's talk about Laravel and emails!
1. Laravel and SMTP
1.1. Configuration
Laravel SMTP Mail support is truly awesome and work out-of-the-box without requiring anything more than a few env variables:
MAIL_MAILER=smtp
MAIL_HOST=mail1.example.org
MAIL_PORT=587
MAIL_USERNAME=demo@example.org
MAIL_PASSWORD=foo
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=no-reply@example.org
MAIL_FROM_NAME=Demo App
Note: The initial setup only requires these environment variables because the smtp mailer is already configured by default in
config/mail.php
.
1.2. Creating an email
Once everything is configured, creating an email is as simple as running this command:
php artisan make:mail Greetings
This command will generate a sample email in app/Mail/Greetings
and you'll just need to design it afterwards.
1.3. Sending an email
Sending an email to a user with Laravel can be either done
- using
\Illuminate\Notifications\RoutesNotifications::notify
:
$user->notify(new Greetings());
- or using
\Illuminate\Support\Facades\Notification::send
:
Notification::send($users, new Greetings());
Note: the later syntax is especially useful when bulk sending emails.
See how simple it is? I wonder what would be the limitations.
Eh! What if we need to 'whitelabelize' our application. :-)
2. Dynamic SMTP configuration
In our scenario, we have the need to whitelabelize our application:
each User
will belongs to a Provider
that will have custom SMTP settings. So when sending email to a user we need to
configure dynamically the mailer to use the SMTP credentials of his provider ($user->provider
).
Can Laravel help us doing so?
After a bit of googling and reading the official documentation, there's no out-of-the-box support for dynamic SMTP configuration, certainly because there would be 100x way of doing it, each way depending on your exact use-case.
So, we're screwed?
Not yet, because Laravel allows us to tweak almost anything, so we just need to find our way.
2.1. Designing the models
Here's a quick visualization of our models:
namespace App\Models;
/**
* @property Provider $provider
*/
class User extends Model
{
public function provider(): BelongsTo
{
return $this->belongsTo(Provider::class);
}
}
For the User
model there's nothing special: we only link the user to a Provider
.
namespace App\Models;
/**
* @property array $mail_configuration
* @property Provider $provider
*/
class Provider extends Model
{
protected $casts = [
'mail_configuration' => 'encrypted:array'
];
public function users(): HasMany
{
return $this->hasMany(User::class);
}
}
The Provider
model has many Users
and has a mail_configuration
field which is encrypted and that will contain the
SMTP credentials.
The email configuration will be stored as an encrypted JSON configuration. It will look like this:
{
"host": "smtp.example.org",
"port": 587,
"username": "foo",
"password": "bar",
"encryption": "tls",
"from_address": "no-reply@example.org",
"from_name": "Example"
}
2.2. Digging down the internals
Now that our models are ready, we must find a way to use the provider configuration to send the email. Let's dig down in Laravel source code to understand how emails works:
Remember the two-ways of sending emails?
\Illuminate\Notifications\RoutesNotifications::notify
\Illuminate\Support\Facades\Notification::send
What we need to do is find the common path between these two methods, and see if we can override some behavior in there.
As you can see here, the methods share the same execution path that end up
calling \Illuminate\Notifications\Channels\MailChannel::send
. So how can we hook up into this path?
The answer lies in \Illuminate\Notifications\NotificationSender::sendToNotifiable
:
protected function sendToNotifiable($notifiable, $id, $notification, $channel)
{
if (! $notification->id) {
$notification->id = $id;
}
if (! $this->shouldSendNotification($notifiable, $notification, $channel)) {
return;
}
$response = $this->manager->driver($channel)->send($notifiable, $notification);
$this->events->dispatch(
new NotificationSent($notifiable, $notification, $channel, $response)
);
}
As you can see this method is looking for a driver to use with the $channel
and finally calls the send()
method. So what we can do is registering a special SMTP driver that will use dynamic SMTP settings.
Fortunately registering a custom driver is a common use-case and there's a straightforward way.
2.3. Creating a custom MailChannel
namespace App\Notifications\Channels;
class ProviderMailChannel extends MailChannel
{
public function send($notifiable, Notification $notification)
{
// TODO: override the SMTP configuration
parent::send($notifiable, $notification);
}
}
2.4. Registering the ProviderMailChannel
All we need to know is extend (i.e: register a custom driver creator) for the mail
channel.
This way when an email is sent it will be sent using our ProviderMailChannel
.
namespace App\Providers;
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
/** @var ChannelManager $channelManager */
$channelManager = $this->app->get(ChannelManager::class);
$channelManager->extend('mail', function (Application $application) {
return new ProviderMailChannel($application->get('mail.manager'), $application->get(Markdown::class));
});
}
}
2.5. Creating a custom Mailer
Now that we are hooked up into the mail sending flow, we need to actually send the email. For doing so we need to instantiate
custom \Illuminate\Mail\Mailer
instance that will be configured using the provider settings. To register such dynamic
configurable service we will use the power of
the Service container.
namespace App\Providers;
class AppServiceProvider extends ServiceProvider
{
public function register()
{
// Register a custom mailer named `custom.mailer` that will receive his configuration dynamically
$this->app->bind('custom.mailer', function ($app, $parameters) {
$transport = new Swift_SmtpTransport($parameters['host'], $parameters['port']);
$transport->setUsername($parameters['username']);
$transport->setPassword($parameters['password']);
$transport->setEncryption($parameters['encryption']);
$mailer = new Mailer('', $app->get('view'), new Swift_Mailer($transport), $app->get('events'));
$mailer->alwaysFrom($parameters['from_address'], $parameters['from_name']);
return $mailer;
});
}
public function boot()
{
/** @var ChannelManager $channelManager */
$channelManager = $this->app->get(ChannelManager::class);
$channelManager->extend('mail', function (Application $application) {
return new ProviderMailChannel($application->get('mail.manager'), $application->get(Markdown::class));
});
}
}
Now we can instantiate this custom Mailer by doing the following:
$mailer = app()->make('custom.mailer', $configuration);
// do something with $mailer
Where $configuration
is the custom SMTP configuration.
2.6. Plug the custom Mailer into the ProviderMailChannel
Finally, all we need to do is to read the Provider
mail configuration
and use it to instantiate our custom.mailer
and then, use it to send the actual email.
namespace App\Notifications\Channels;
class ProviderMailChannel extends MailChannel
{
public function send($notifiable, Notification $notification)
{
$message = $notification->toMail($notifiable);
$mailer = app()->make('custom.mailer', $notifiable->provider->mail_configuration);
$message->send($mailer);
}
}
3. Conclusion
That all folks. You are now capable of sending email using dynamic SMTP credentials based on the use-case.
Happy hacking!