diff --git a/content/posts/laravel-dynamic-smtp-mail-configuration.md b/content/posts/laravel-dynamic-smtp-mail-configuration.md new file mode 100644 index 0000000..ff6d861 --- /dev/null +++ b/content/posts/laravel-dynamic-smtp-mail-configuration.md @@ -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. + +![](/img/laravel-mail-internals.png) + +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! \ No newline at end of file diff --git a/static/img/laravel-mail-internals.png b/static/img/laravel-mail-internals.png new file mode 100644 index 0000000..4be6dd7 Binary files /dev/null and b/static/img/laravel-mail-internals.png differ