293 lines
8.7 KiB
Markdown
293 lines
8.7 KiB
Markdown
+++
|
|
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](https://laravel.com/) and emails!
|
|
|
|
# 1. Laravel and SMTP
|
|
|
|
## 1.1. Configuration
|
|
|
|
Laravel SMTP [Mail](https://laravel.com/docs/8.x/mail) support is truly awesome and work out-of-the-box without
|
|
requiring anything more than a few env variables:
|
|
|
|
```text
|
|
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 in `config/mail.php`.
|
|
|
|
## 1.2. Creating an email
|
|
|
|
Once everything is configured, creating an email is as simple as running this command:
|
|
|
|
```shell
|
|
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`:
|
|
|
|
```php
|
|
$user->notify(new Greetings());
|
|
```
|
|
|
|
- or using `\Illuminate\Support\Facades\Notification::send`:
|
|
|
|
```php
|
|
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](https://en.wikipedia.org/wiki/White-label_product) 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 `$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:
|
|
|
|
```php
|
|
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`.
|
|
|
|
```php
|
|
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.
|
|
|
|
## 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`:
|
|
|
|
```php
|
|
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
|
|
|
|
```php
|
|
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`.
|
|
|
|
```php
|
|
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](https://laravel.com/docs/8.x/container#binding-basics).
|
|
|
|
```php
|
|
namespace App\Providers;
|
|
|
|
class AppServiceProvider extends ServiceProvider
|
|
{
|
|
public function register()
|
|
{
|
|
// When running in testing environment then return the mocked mail implementation
|
|
if (env('APP_ENV') === 'testing') {
|
|
$this->app->singleton('custom.mailer', MailFake::class);
|
|
return;
|
|
}
|
|
|
|
// 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($from_address, $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:
|
|
|
|
```php
|
|
$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.
|
|
|
|
```php
|
|
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!
|