parent
c0c642c2cf
commit
713e7d276d
2 changed files with 280 additions and 0 deletions
280
content/posts/laravel-dynamic-smtp-mail-configuration.md
Normal file
280
content/posts/laravel-dynamic-smtp-mail-configuration.md
Normal file
|
@ -0,0 +1,280 @@
|
|||
+++
|
||||
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 SMTP credentials to use depending on the `$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
|
||||
/**
|
||||
* @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
|
||||
/**
|
||||
* @property array $mail_configuration
|
||||
* @property Provider $provider
|
||||
*/
|
||||
class Provider
|
||||
{
|
||||
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 all
|
||||
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
|
||||
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 the `ProviderMailChannel`.
|
||||
|
||||
```php
|
||||
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
|
||||
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
|
||||
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!
|
BIN
static/img/laravel-mail-internals.png
Normal file
BIN
static/img/laravel-mail-internals.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
Loading…
Add table
Add a link
Reference in a new issue