From 0a1813cfa51fff0299635cffdd05da0c22a2c387 Mon Sep 17 00:00:00 2001 From: TobMoeller Date: Mon, 30 Sep 2024 00:57:02 +0200 Subject: [PATCH] add global recipients --- README.md | 31 +++++++++++++- config/mail-allowlist.php | 37 ++++++++++++++++- src/LaravelMailAllowlist.php | 30 ++++++++++++++ src/MailMiddleware/Addresses/AddGlobalBcc.php | 20 ++++++++++ src/MailMiddleware/Addresses/AddGlobalCc.php | 20 ++++++++++ src/MailMiddleware/Addresses/AddGlobalTo.php | 20 ++++++++++ tests/LaravelMailAllowListFacadeTest.php | 36 +++++++++++++++++ .../Addresses/AddGlobalBccTest.php | 40 +++++++++++++++++++ .../Addresses/AddGlobalCcTest.php | 40 +++++++++++++++++++ .../Addresses/AddGlobalToTest.php | 40 +++++++++++++++++++ 10 files changed, 310 insertions(+), 4 deletions(-) create mode 100644 src/MailMiddleware/Addresses/AddGlobalBcc.php create mode 100644 src/MailMiddleware/Addresses/AddGlobalCc.php create mode 100644 src/MailMiddleware/Addresses/AddGlobalTo.php create mode 100644 tests/MailMiddleware/Addresses/AddGlobalBccTest.php create mode 100644 tests/MailMiddleware/Addresses/AddGlobalCcTest.php create mode 100644 tests/MailMiddleware/Addresses/AddGlobalToTest.php diff --git a/README.md b/README.md index 94c485b..1913dce 100644 --- a/README.md +++ b/README.md @@ -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:** > @@ -43,6 +62,11 @@ MAIL_ALLOWLIST_ALLOWED_DOMAINS="foo.com;bar.com" # Define a semicolon separated list of allowed emails MAIL_ALLOWLIST_ALLOWED_EMAILS="mail@foo.com;mail@bar.com" + +# Define a semicolon separated list of globally added emails +MAIL_ALLOWLIST_GLOBAL_TO="mail@foo.com;mail@bar.com" +MAIL_ALLOWLIST_GLOBAL_CC="mail@foo.com;mail@bar.com" +MAIL_ALLOWLIST_GLOBAL_BCC="mail@foo.com;mail@bar.com" ``` ### Customizing the Middleware Pipeline @@ -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; ], ``` diff --git a/config/mail-allowlist.php b/config/mail-allowlist.php index e4c0536..d1f4fdd 100644 --- a/config/mail-allowlist.php +++ b/config/mail-allowlist.php @@ -1,5 +1,8 @@ [ ToFilter::class, CcFilter::class, BccFilter::class, + AddGlobalTo::class, + AddGlobalCc::class, + AddGlobalBcc::class, EnsureRecipients::class, ], @@ -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. + * 'foo@bar.com' + * 'foo@bar.com;bar@foo.com;...' + * ['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. + * 'foo@bar.com' + * 'foo@bar.com;bar@foo.com;...' + * ['foo.com', 'bar.com'] + */ + 'global' => [ + 'to' => env('MAIL_ALLOWLIST_GLOBAL_TO'), + 'cc' => env('MAIL_ALLOWLIST_GLOBAL_CC'), + 'bcc' => env('MAIL_ALLOWLIST_GLOBAL_BCC'), + ], ]; diff --git a/src/LaravelMailAllowlist.php b/src/LaravelMailAllowlist.php index 2455f9a..c1d3d47 100755 --- a/src/LaravelMailAllowlist.php +++ b/src/LaravelMailAllowlist.php @@ -42,6 +42,36 @@ public function allowedEmailList(): array return $this->extractArrayFromConfig($allowedEmails); } + /** + * @return array + */ + public function globalToEmailList(): array + { + $toEmails = Config::get('mail-allowlist.global.to'); + + return $this->extractArrayFromConfig($toEmails); + } + + /** + * @return array + */ + public function globalCcEmailList(): array + { + $ccEmails = Config::get('mail-allowlist.global.cc'); + + return $this->extractArrayFromConfig($ccEmails); + } + + /** + * @return array + */ + 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 diff --git a/src/MailMiddleware/Addresses/AddGlobalBcc.php b/src/MailMiddleware/Addresses/AddGlobalBcc.php new file mode 100644 index 0000000..7ab95fb --- /dev/null +++ b/src/MailMiddleware/Addresses/AddGlobalBcc.php @@ -0,0 +1,20 @@ +getMessage()->addBcc(...$bcc); + } + + return $next($messageContext); + } +} diff --git a/src/MailMiddleware/Addresses/AddGlobalCc.php b/src/MailMiddleware/Addresses/AddGlobalCc.php new file mode 100644 index 0000000..906635f --- /dev/null +++ b/src/MailMiddleware/Addresses/AddGlobalCc.php @@ -0,0 +1,20 @@ +getMessage()->addCc(...$cc); + } + + return $next($messageContext); + } +} diff --git a/src/MailMiddleware/Addresses/AddGlobalTo.php b/src/MailMiddleware/Addresses/AddGlobalTo.php new file mode 100644 index 0000000..b21020b --- /dev/null +++ b/src/MailMiddleware/Addresses/AddGlobalTo.php @@ -0,0 +1,20 @@ +getMessage()->addTo(...$to); + } + + return $next($messageContext); + } +} diff --git a/tests/LaravelMailAllowListFacadeTest.php b/tests/LaravelMailAllowListFacadeTest.php index a0d5a59..fb95cc9 100644 --- a/tests/LaravelMailAllowListFacadeTest.php +++ b/tests/LaravelMailAllowListFacadeTest.php @@ -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; @@ -26,6 +29,9 @@ ToFilter::class, CcFilter::class, BccFilter::class, + AddGlobalTo::class, + AddGlobalCc::class, + AddGlobalBcc::class, EnsureRecipients::class, ]); }); @@ -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' => ['bar@foo.de', 'foo@bar.de'], + 'expected' => ['bar@foo.de', 'foo@bar.de'], + ], + [ + 'value' => 'bar@foo.de;foo@bar.de', + 'expected' => ['bar@foo.de', 'foo@bar.de'], + ], + [ + 'value' => 'foo@bar.de', + 'expected' => ['foo@bar.de'], + ], + [ + 'value' => null, + 'expected' => [], + ], +]); diff --git a/tests/MailMiddleware/Addresses/AddGlobalBccTest.php b/tests/MailMiddleware/Addresses/AddGlobalBccTest.php new file mode 100644 index 0000000..3591c17 --- /dev/null +++ b/tests/MailMiddleware/Addresses/AddGlobalBccTest.php @@ -0,0 +1,40 @@ +handle($context, fn () => '::next_response::'); + + $addresses = $mail->getBcc(); + expect($addresses) + ->toHaveCount(2) + ->each->toBeInstanceOf(Address::class) + ->and($addresses[0]) + ->getAddress()->toBe('foo@bar.com') + ->and($addresses[1]) + ->getAddress()->toBe('bar@foo.com') + ->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::'); +}); diff --git a/tests/MailMiddleware/Addresses/AddGlobalCcTest.php b/tests/MailMiddleware/Addresses/AddGlobalCcTest.php new file mode 100644 index 0000000..d9d5f42 --- /dev/null +++ b/tests/MailMiddleware/Addresses/AddGlobalCcTest.php @@ -0,0 +1,40 @@ +handle($context, fn () => '::next_response::'); + + $addresses = $mail->getCc(); + expect($addresses) + ->toHaveCount(2) + ->each->toBeInstanceOf(Address::class) + ->and($addresses[0]) + ->getAddress()->toBe('foo@bar.com') + ->and($addresses[1]) + ->getAddress()->toBe('bar@foo.com') + ->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::'); +}); diff --git a/tests/MailMiddleware/Addresses/AddGlobalToTest.php b/tests/MailMiddleware/Addresses/AddGlobalToTest.php new file mode 100644 index 0000000..f0a2cae --- /dev/null +++ b/tests/MailMiddleware/Addresses/AddGlobalToTest.php @@ -0,0 +1,40 @@ +handle($context, fn () => '::next_response::'); + + $to = $mail->getTo(); + expect($to) + ->toHaveCount(2) + ->each->toBeInstanceOf(Address::class) + ->and($to[0]) + ->getAddress()->toBe('foo@bar.com') + ->and($to[1]) + ->getAddress()->toBe('bar@foo.com') + ->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::'); +});