Skip to content

Commit

Permalink
add global recipients
Browse files Browse the repository at this point in the history
  • Loading branch information
TobMoeller committed Sep 29, 2024
1 parent ffea7ba commit 0a1813c
Show file tree
Hide file tree
Showing 10 changed files with 310 additions and 4 deletions.
31 changes: 29 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,28 @@
[![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/tobmoeller/laravel-mail-allowlist/fix-php-code-style-issues.yml?branch=main&label=code%20style&style=flat-square)](https://github.com/tobmoeller/laravel-mail-allowlist/actions?query=workflow%3A"Fix+PHP+code+style+issues"+branch%3Amain)
[![Total Downloads](https://img.shields.io/packagist/dt/tobmoeller/laravel-mail-allowlist.svg?style=flat-square)](https://packagist.org/packages/tobmoeller/laravel-mail-allowlist)

This package enables your Laravel application to filter recipients of outgoing emails by domain or specific email addresses through a configurable allowlist. Ideal for staging environments, it ensures that only approved recipients receive emails. Recipients not matching the allowlist are removed from the email, and if no valid "to" recipients remain, the email is stopped altogether, preventing unintended email delivery.
This package provides a customizable middleware pipeline for email messages, allowing you to filter, modify, and inspect emails before they are sent.

Additionally, the package now supports a customizable middleware pipeline, allowing you to control the existing functionality and implement additional logic for outgoing emails. You can add your own middleware to modify, inspect, or control email messages.
**Key Features:**

- **Recipient Allowlist Filtering:**
- Filter outgoing email recipients based on a configurable allowlist of domains and specific email addresses.
- Ideal for staging and testing environments to prevent unintended emails from reaching unintended recipients.
- Automatically removes recipients not matching the allowlist from the "To", "Cc", and "Bcc" fields.
- If no valid recipients remain after filtering, the email is canceled to prevent unintended delivery.

- **Add Global Recipients:**
- Set default or global "To", "Cc", and "Bcc" recipients via configuration.
- Ensure certain recipients always receive emails, such as administrators, audit logs, or monitoring addresses.

- **Customizable Middleware Pipeline:**
- Utilize a middleware pipeline similar to Laravel's HTTP middleware, but for outgoing emails.
- Add, remove, or reorder middleware to control the processing of emails.

- **Custom Middleware Support:**
- Create your own middleware to implement custom logic for outgoing emails.
- Modify email content, set headers, add attachments, or perform any email transformation needed.
- Middleware can inspect emails, log information, or integrate with other services.

> **Important Note:**
>
Expand Down Expand Up @@ -43,6 +62,11 @@ MAIL_ALLOWLIST_ALLOWED_DOMAINS="foo.com;bar.com"
# Define a semicolon separated list of allowed emails
MAIL_ALLOWLIST_ALLOWED_EMAILS="[email protected];[email protected]"
# Define a semicolon separated list of globally added emails
MAIL_ALLOWLIST_GLOBAL_TO="[email protected];[email protected]"
MAIL_ALLOWLIST_GLOBAL_CC="[email protected];[email protected]"
MAIL_ALLOWLIST_GLOBAL_BCC="[email protected];[email protected]"
```

### Customizing the Middleware Pipeline
Expand All @@ -54,6 +78,9 @@ The package processes outgoing emails through a middleware pipeline, allowing yo
ToFilter::class;
CcFilter::class;
BccFilter::class;
AddGlobalTo::class,
AddGlobalCc::class,
AddGlobalBcc::class,
EnsureRecipients::class;
],
```
Expand Down
37 changes: 35 additions & 2 deletions config/mail-allowlist.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<?php

use TobMoeller\LaravelMailAllowlist\MailMiddleware\Addresses\AddGlobalBcc;
use TobMoeller\LaravelMailAllowlist\MailMiddleware\Addresses\AddGlobalCc;
use TobMoeller\LaravelMailAllowlist\MailMiddleware\Addresses\AddGlobalTo;
use TobMoeller\LaravelMailAllowlist\MailMiddleware\Addresses\BccFilter;
use TobMoeller\LaravelMailAllowlist\MailMiddleware\Addresses\CcFilter;
use TobMoeller\LaravelMailAllowlist\MailMiddleware\Addresses\EnsureRecipients;
Expand All @@ -16,12 +19,15 @@
* Can be either a class-string or an instance. Class-strings will
* be instantiated through Laravel's service container
*
* All middleware has to implement the MailMiddlewareContract
* All middleware must implement the MailMiddlewareContract
*/
'middleware' => [
ToFilter::class,
CcFilter::class,
BccFilter::class,
AddGlobalTo::class,
AddGlobalCc::class,
AddGlobalBcc::class,
EnsureRecipients::class,
],

Expand All @@ -36,14 +42,41 @@
* Can either be a singular domain string,
* a semicolon separated list of domains or
* an array of domain strings
*
* e.g.
* 'bar.com'
* 'foo.com;bar.com;...'
* ['foo.com', 'bar.com']
*/
'domains' => env('MAIL_ALLOWLIST_ALLOWED_DOMAINS'),

/**
* Can either be a singular email address string,
* a semicolon separated list of email addresses or
* an array of email address strings
* an array of email address strings (only in config).
*
* e.g.
* '[email protected]'
* '[email protected];[email protected];...'
* ['foo.com', 'bar.com']
*/
'emails' => env('MAIL_ALLOWLIST_ALLOWED_EMAILS'),
],

/**
* Define global recipients to be added to every mail sent.
* Each one can either be a singular email address string,
* a semicolon separated list of email addresses or
* an array of email address strings (only in config)
*
* e.g.
* '[email protected]'
* '[email protected];[email protected];...'
* ['foo.com', 'bar.com']
*/
'global' => [
'to' => env('MAIL_ALLOWLIST_GLOBAL_TO'),
'cc' => env('MAIL_ALLOWLIST_GLOBAL_CC'),
'bcc' => env('MAIL_ALLOWLIST_GLOBAL_BCC'),
],
];
30 changes: 30 additions & 0 deletions src/LaravelMailAllowlist.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,36 @@ public function allowedEmailList(): array
return $this->extractArrayFromConfig($allowedEmails);
}

/**
* @return array<int, string>
*/
public function globalToEmailList(): array
{
$toEmails = Config::get('mail-allowlist.global.to');

return $this->extractArrayFromConfig($toEmails);
}

/**
* @return array<int, string>
*/
public function globalCcEmailList(): array
{
$ccEmails = Config::get('mail-allowlist.global.cc');

return $this->extractArrayFromConfig($ccEmails);
}

/**
* @return array<int, string>
*/
public function globalBccEmailList(): array
{
$bccEmails = Config::get('mail-allowlist.global.bcc');

return $this->extractArrayFromConfig($bccEmails);
}

/**
* Extracts the array from a config value that can be
* either a semicolon separated string or an array
Expand Down
20 changes: 20 additions & 0 deletions src/MailMiddleware/Addresses/AddGlobalBcc.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace TobMoeller\LaravelMailAllowlist\MailMiddleware\Addresses;

use Closure;
use TobMoeller\LaravelMailAllowlist\Facades\LaravelMailAllowlist;
use TobMoeller\LaravelMailAllowlist\MailMiddleware\MailMiddlewareContract;
use TobMoeller\LaravelMailAllowlist\MailMiddleware\MessageContext;

class AddGlobalBcc implements MailMiddlewareContract
{
public function handle(MessageContext $messageContext, Closure $next): mixed
{
if (! empty($bcc = LaravelMailAllowlist::globalBccEmailList())) {
$messageContext->getMessage()->addBcc(...$bcc);
}

return $next($messageContext);
}
}
20 changes: 20 additions & 0 deletions src/MailMiddleware/Addresses/AddGlobalCc.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace TobMoeller\LaravelMailAllowlist\MailMiddleware\Addresses;

use Closure;
use TobMoeller\LaravelMailAllowlist\Facades\LaravelMailAllowlist;
use TobMoeller\LaravelMailAllowlist\MailMiddleware\MailMiddlewareContract;
use TobMoeller\LaravelMailAllowlist\MailMiddleware\MessageContext;

class AddGlobalCc implements MailMiddlewareContract
{
public function handle(MessageContext $messageContext, Closure $next): mixed
{
if (! empty($cc = LaravelMailAllowlist::globalCcEmailList())) {
$messageContext->getMessage()->addCc(...$cc);
}

return $next($messageContext);
}
}
20 changes: 20 additions & 0 deletions src/MailMiddleware/Addresses/AddGlobalTo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace TobMoeller\LaravelMailAllowlist\MailMiddleware\Addresses;

use Closure;
use TobMoeller\LaravelMailAllowlist\Facades\LaravelMailAllowlist;
use TobMoeller\LaravelMailAllowlist\MailMiddleware\MailMiddlewareContract;
use TobMoeller\LaravelMailAllowlist\MailMiddleware\MessageContext;

class AddGlobalTo implements MailMiddlewareContract
{
public function handle(MessageContext $messageContext, Closure $next): mixed
{
if (! empty($to = LaravelMailAllowlist::globalToEmailList())) {
$messageContext->getMessage()->addTo(...$to);
}

return $next($messageContext);
}
}
36 changes: 36 additions & 0 deletions tests/LaravelMailAllowListFacadeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
use Illuminate\Support\Facades\Event;
use TobMoeller\LaravelMailAllowlist\Facades\LaravelMailAllowlist;
use TobMoeller\LaravelMailAllowlist\Listeners\MessageSendingListener;
use TobMoeller\LaravelMailAllowlist\MailMiddleware\Addresses\AddGlobalBcc;
use TobMoeller\LaravelMailAllowlist\MailMiddleware\Addresses\AddGlobalCc;
use TobMoeller\LaravelMailAllowlist\MailMiddleware\Addresses\AddGlobalTo;
use TobMoeller\LaravelMailAllowlist\MailMiddleware\Addresses\BccFilter;
use TobMoeller\LaravelMailAllowlist\MailMiddleware\Addresses\CcFilter;
use TobMoeller\LaravelMailAllowlist\MailMiddleware\Addresses\EnsureRecipients;
Expand All @@ -26,6 +29,9 @@
ToFilter::class,
CcFilter::class,
BccFilter::class,
AddGlobalTo::class,
AddGlobalCc::class,
AddGlobalBcc::class,
EnsureRecipients::class,
]);
});
Expand Down Expand Up @@ -100,3 +106,33 @@
'expected' => [],
],
]);

it('returns the global to/cc/bcc email lists', function (mixed $value, mixed $expected) {
Config::set('mail-allowlist.global.to', $value);
Config::set('mail-allowlist.global.cc', $value);
Config::set('mail-allowlist.global.bcc', $value);

expect(LaravelMailAllowlist::globalToEmailList())
->toBe($expected)
->and(LaravelMailAllowlist::globalCcEmailList())
->toBe($expected)
->and(LaravelMailAllowlist::globalBccEmailList())
->toBe($expected);
})->with([
[
'value' => ['[email protected]', '[email protected]'],
'expected' => ['[email protected]', '[email protected]'],
],
[
'value' => '[email protected];[email protected]',
'expected' => ['[email protected]', '[email protected]'],
],
[
'value' => '[email protected]',
'expected' => ['[email protected]'],
],
[
'value' => null,
'expected' => [],
],
]);
40 changes: 40 additions & 0 deletions tests/MailMiddleware/Addresses/AddGlobalBccTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

use Illuminate\Support\Facades\Config;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
use TobMoeller\LaravelMailAllowlist\MailMiddleware\Addresses\AddGlobalBcc;
use TobMoeller\LaravelMailAllowlist\MailMiddleware\MessageContext;

it('adds global bcc addresses and continues the pipeline', function () {
Config::set('mail-allowlist.global.bcc', ['[email protected]', '[email protected]']);
$mail = new Email;
$context = new MessageContext($mail);

$middlewareReturn = (new AddGlobalBcc)->handle($context, fn () => '::next_response::');

$addresses = $mail->getBcc();
expect($addresses)
->toHaveCount(2)
->each->toBeInstanceOf(Address::class)
->and($addresses[0])
->getAddress()->toBe('[email protected]')
->and($addresses[1])
->getAddress()->toBe('[email protected]')
->and($middlewareReturn)
->toBe('::next_response::');
});

it('does not add an address if config is empty and continues the pipeline', function () {
Config::set('mail-allowlist.global.bcc', []);
$mail = new Email;
$context = new MessageContext($mail);

$middlewareReturn = (new AddGlobalBcc)->handle($context, fn () => '::next_response::');

$addresses = $mail->getBcc();
expect($addresses)
->toBeEmpty()
->and($middlewareReturn)
->toBe('::next_response::');
});
40 changes: 40 additions & 0 deletions tests/MailMiddleware/Addresses/AddGlobalCcTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

use Illuminate\Support\Facades\Config;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
use TobMoeller\LaravelMailAllowlist\MailMiddleware\Addresses\AddGlobalCc;
use TobMoeller\LaravelMailAllowlist\MailMiddleware\MessageContext;

it('adds global cc addresses and continues the pipeline', function () {
Config::set('mail-allowlist.global.cc', ['[email protected]', '[email protected]']);
$mail = new Email;
$context = new MessageContext($mail);

$middlewareReturn = (new AddGlobalCc)->handle($context, fn () => '::next_response::');

$addresses = $mail->getCc();
expect($addresses)
->toHaveCount(2)
->each->toBeInstanceOf(Address::class)
->and($addresses[0])
->getAddress()->toBe('[email protected]')
->and($addresses[1])
->getAddress()->toBe('[email protected]')
->and($middlewareReturn)
->toBe('::next_response::');
});

it('does not add an address if config is empty and continues the pipeline', function () {
Config::set('mail-allowlist.global.cc', []);
$mail = new Email;
$context = new MessageContext($mail);

$middlewareReturn = (new AddGlobalCc)->handle($context, fn () => '::next_response::');

$addresses = $mail->getCc();
expect($addresses)
->toBeEmpty()
->and($middlewareReturn)
->toBe('::next_response::');
});
40 changes: 40 additions & 0 deletions tests/MailMiddleware/Addresses/AddGlobalToTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

use Illuminate\Support\Facades\Config;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
use TobMoeller\LaravelMailAllowlist\MailMiddleware\Addresses\AddGlobalTo;
use TobMoeller\LaravelMailAllowlist\MailMiddleware\MessageContext;

it('adds global to addresses and continues the pipeline', function () {
Config::set('mail-allowlist.global.to', ['[email protected]', '[email protected]']);
$mail = new Email;
$context = new MessageContext($mail);

$middlewareReturn = (new AddGlobalTo)->handle($context, fn () => '::next_response::');

$to = $mail->getTo();
expect($to)
->toHaveCount(2)
->each->toBeInstanceOf(Address::class)
->and($to[0])
->getAddress()->toBe('[email protected]')
->and($to[1])
->getAddress()->toBe('[email protected]')
->and($middlewareReturn)
->toBe('::next_response::');
});

it('does not add an address if config is empty and continues the pipeline', function () {
Config::set('mail-allowlist.global.to', []);
$mail = new Email;
$context = new MessageContext($mail);

$middlewareReturn = (new AddGlobalTo)->handle($context, fn () => '::next_response::');

$to = $mail->getTo();
expect($to)
->toBeEmpty()
->and($middlewareReturn)
->toBe('::next_response::');
});

0 comments on commit 0a1813c

Please sign in to comment.