How to streamline adding that unsubscribe link to your mail notifications in Laravel

The introduction of the notification system in Laravel 5.3 was truly ground-breaking. It’s a great system but one of the issues is that emails still have some teething issues even now in Laravel 5. This is totally expected of course, emails are a different beast to Push notifications. While Push notifications for say Android or iOS are visually handled by the operating system the other advantage is that these notification mechanisms don’t require you to manage the feedback of your users, namely unsubscribing. Unsubscribing is a really important feature for email because all domains have a “health” status. The more your email domain is marked as spam by those receiving the notifications the more you likely your domain will be damaged and it could even result in you being unable to send emails with that domain in the future.

This is a pretty extreme example but it is something to bare in mind, when you start out it’s unlikely to be an issue but as you scale there is more and more chance this issue will appear. At the very least it’s a requirement to not pester your customer unessecarily if they want to stop notification. In my opinion you should at a minimum be making sure users using your site have verified that they have access to that address before sending lots of emails to it.

How to get uniquely generated Unsubscribe links into notifications

This seems easier said than done. When we look at the notification class it’s simple to see how it works. The toMail method is simply there for generating the email content and while an unsubscribe link is content we don’t really want to do too much heavy lifting here. I know sending emails doesn’t really fit into the Model-View-Controller pattern but in a lot of ways, at the point we produce an email we shouldn’t be touching the database (think about it, emails are really just additional views and notification are an extension of that). Even then generating the URL here seems like it breaks the singlular purpose of a notification class.

At the same time we also want this to be fairly streamlined so we shouldn’t have to pass an unsubscribe link to each notification as a constructor argument. That would be a bit tedious or annoying as it doesn’t relate to the purpose of the notifications.

Instead how we’re going to solve this is actually by creating our own notification channel which extends and replaces the original one. Here’s what it looks like.

/**
*
@var SubscriptionTokenRepository
*/
private $tokens;

/**
* Create a new mail channel instance.
*
*
@param \Illuminate\Contracts\Mail\Mailer $mailer
*
@param \Illuminate\Mail\Markdown $markdown
*
@param SubscriptionTokenRepository $tokens
*/
public function __construct(
Mailer $mailer,
Markdown $markdown,
SubscriptionTokenRepository $tokens
) {
parent::__construct($mailer, $markdown);
$this->tokens = $tokens;
}

/**
* Send the given notification.
*
*
@param mixed $notifiable
*
@param \Illuminate\Notifications\Notification $notification
*
@return void
*/
public function send($notifiable, Notification $notification)
{
$token = $this->tokens->create($notifiable, $notification);

$unsubscribeUrl = route('email.unsubscribe', [$notifiable->routeNotificationFor('mail'), $token]);

$message = $notification->toMail($notifiable, $unsubscribeUrl);

if (! $notifiable->routeNotificationFor('mail') &&
! $message instanceof Mailable) {
return;
}

if ($message instanceof Mailable) {
return $message->send($this->mailer);
}

$this->mailer->send(
$this->buildView($message),
$message->data(),
$this->messageBuilder($notifiable, $notification, $message)
);
}

You see what’ve done here is override and modify the method of the MailChannel class which will call toMail on our notification classes and instead of only providing the notifiable object it will also a provide our unsubscribe link.

Thanks to how PHP works it doesn’t matter if the toMail method in our notifiable only has one parameter as it’ll simply be ignored but now we can create notifications with a toMail method such as:

/**
* Get the mail representation of the notification.
*
*
@param mixed $notifiable
*
@param string $unsubscribeUrl
*
@return MailMessage
*/
public function toMail($notifiable, $unsubscribeUrl)
{
return (new MailMessage($unsubscribeUrl))
->line('Something happened, good thing you can receive email!')
->action('Notification Action', route('home'))
->line('Thank you for using our application!')
->line("You can [unsubscribe here]({$unsubscribeUrl})");
}

This could still be improved further though to reduce having to add our line about unsubscribing so instead we create our own SubscriberMailMessage class that will handle adding the link to the template parameters.

class SubscriberMailMessage extends MailMessage
{
/**
*
@var string
*/
public $unsubscribeUrl;

public function __construct($unsubscribeUrl)
{
$this->unsubscribeUrl = $unsubscribeUrl;
}

/**
* Get the data array for the mail message.
*
*
@return array
*/
public function data()
{
return array_merge(parent::data(), ['unsubscribeUrl' => $this->unsubscribeUrl]);
}
}

So instead we only need to create a toMail method for our notifications that uses the new mail message class.

/**
* Get the mail representation of the notification.
*
*
@param mixed $notifiable
*
@param string $unsubscribeUrl
*
@return MailMessage
*/
public function toMail($notifiable, $unsubscribeUrl)
{
return (new SubscriberMailMessage($unsubscribeUrl))
->line('Something happened, good thing you can receive email!')
->action('Notification Action', route('home'))
->line('Thank you for using our application!');
}

So now if we generate the vendor email templates using the artisan command…

php artisan vendor:publish --tag=laravel-notifications

We can edit resources/views/vendor/notifications/email.blade.php to use our unsubscribeUrl variable if provided like say in our email’s sub copy.

@component('mail::subcopy')
If you’re having trouble clicking the "{{ $actionText }}" button, copy and paste the URL below
into your web browser: [{{ $actionUrl }}]({{ $actionUrl }})

@isset($unsubscribeUrl)
if you would like to unsubscribe from receiving further emails you can use the URL:
[{{ $unsubscribeUrl }}]({{ $unsubscribeUrl }})
@endisset
@endcomponent

To even improve things further because we’ve built a mail message class that keeps our unsubscribe URL we can further change the email in the MailChannel’s buildMessage method. By override this we can add an additional header called List-Unsubscribe which allows email clients a mechanism to unsubscribe for the user. Please bare in mind as this might not work as some email clients will only trust some emails with this feature.

/**
* Build the mail message.
*
*
@param \Illuminate\Mail\Message $mailMessage
*
@param mixed $notifiable
*
@param \Illuminate\Notifications\Notification $notification
*
@param SubscriberMailMessage $message
*
@return void
*/
protected function buildMessage($mailMessage, $notifiable, $notification, $message)
{
parent::buildMessage($mailMessage, $notifiable, $notification, $message);

$mailMessage
->getHeaders()
->addTextHeader('List-Unsubscribe', "<{$message->unsubscribeUrl}>");
}

All we need finally is to make sure our new Channel is used in place of the default one in the Laravel Framework. This can be done by the following code in the register method of the AppServiceProvider in your application.

/**
* Register any application services.
*
*
@return void
*/
public function register()
{
$this->app->bind(\Illuminate\Notifications\Channels\MailChannel::class, \App\Channels\MailChannel::class);
}

Conclusion

Now you’ve got this far you hopefully understand how and why this is one of the better ways to do it. While I don’t see any issue in maybe making the notifiable object itself carry the unsubscribe URL as a property to use it just feels a bit off to me. The important lesson here is that you should not be afraid to play around with the built in Laravel components and classes to find little tweaks that will make your life easier.

If you’re more of a learn by example developer and you wish to view the code in this tutorial in action you can get a copy of it from github.

I’m hoping to continue with these types of everyday challenges for Laravel. Please like, share and comment on this article if you found it helpful as it really helps to encourage me to take the time to make more content. Thank you for reading.