Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mail middleware #1

Merged
merged 7 commits into from
Sep 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 74 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
# Prevent stray mails from your laravel application
# Prevent stray mails from your Laravel application

[![Latest Version on Packagist](https://img.shields.io/packagist/v/tobmoeller/laravel-mail-allowlist.svg?style=flat-square)](https://packagist.org/packages/tobmoeller/laravel-mail-allowlist)
[![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/tobmoeller/laravel-mail-allowlist/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/tobmoeller/laravel-mail-allowlist/actions?query=workflow%3Arun-tests+branch%3Amain)
[![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 and testing 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 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.

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.

> **Important Note:**
>
> This package utilizes Laravel's `MessageSending` event to inspect and modify outgoing emails. If your application has custom listeners or modifications affecting this event, please thoroughly test the package to ensure it integrates seamlessly and maintains the correct filtering functionality.

## Installation

Expand All @@ -21,7 +27,7 @@ You can publish the config file with:
php artisan vendor:publish --tag="mail-allowlist-config"
```

Your laravel application will merge your local config file with the package config file. This enables you just to keep the edited config values.
Your Laravel application will merge your local config file with the package config file. This enables you just to keep the edited config values.
Additionally this package provides the ability to configure most of the required values through your environment variables.

## Usage
Expand All @@ -32,13 +38,76 @@ You can configure the package through environment variables:
# Enable the package
MAIL_ALLOWLIST_ENABLED=true

# Define a semicolon separated list a allowed domains
# Define a semicolon separated list of allowed domains
MAIL_ALLOWLIST_ALLOWED_DOMAINS="foo.com;bar.com"

# Define a semicolon separated list a allowed emails
# Define a semicolon separated list of allowed emails
MAIL_ALLOWLIST_ALLOWED_EMAILS="[email protected];[email protected]"
```

### Customizing the Middleware Pipeline

The package processes outgoing emails through a middleware pipeline, allowing you to customize or extend the email handling logic. By default, the pipeline includes the following middleware:

```php
'middleware' => [
ToFilter::class;
CcFilter::class;
BccFilter::class;
EnsureRecipients::class;
],
```

#### Reordering or Removing Middleware

The order of middleware in the pipeline matters. Each middleware can modify the email before passing it to the next middleware.
You can also reorder or remove middleware from the pipeline to suit your requirements. For example, if you want to disable the `BccFilter` and want the pipeline to stop right after no recipients remain in the `ToFilter`, you can adjust the pipeline:

```php
'middleware' => [
ToFilter::class;
EnsureRecipients::class; // stops further execution when no recipients remain
CcFilter::class;
// BccFilter::class; // disabled
],
```

#### Creating Custom Middleware

You can add your own middleware to the pipeline to modify, inspect, or control outgoing emails according to your application's needs. For example, to prevent a mail from being sent on a custom condition, you might create a middleware like this:

```php
use Closure;
use TobMoeller\LaravelMailAllowlist\MailMiddleware\MailMiddlewareContract;
use TobMoeller\LaravelMailAllowlist\MailMiddleware\MessageContext;

class CancelMessageMiddleware implements MailMiddlewareContract
{
public function handle(MessageContext $messageContext, Closure $next): mixed
{
if ($customCondition) {
// Indicate that the message should be canceled
$messageContext->cancelSendingMessage('Custom reason');
// Prevent execution of following middleware
return null;
}

return $next($messageContext);
}
}
```

Then add it to your middleware pipeline. This can be done as a class-string which will be instantiated by Laravel's service container or as a concrete instance.

```php
'middleware' => [
// Upstream middleware
\App\Mail\Middleware\CancelMessageMiddleware::class, // As a class-string.
new \App\Mail\Middleware\CancelMessageMiddleware(), // As an instance
// Downstream middleware
],
```

## Testing

```bash
Expand Down
19 changes: 19 additions & 0 deletions config/mail-allowlist.php
Original file line number Diff line number Diff line change
@@ -1,11 +1,30 @@
<?php

use TobMoeller\LaravelMailAllowlist\MailMiddleware\Addresses\BccFilter;
use TobMoeller\LaravelMailAllowlist\MailMiddleware\Addresses\CcFilter;
use TobMoeller\LaravelMailAllowlist\MailMiddleware\Addresses\EnsureRecipients;
use TobMoeller\LaravelMailAllowlist\MailMiddleware\Addresses\ToFilter;

return [
/**
* Enables the mail allowlist
*/
'enabled' => env('MAIL_ALLOWLIST_ENABLED', false),

/**
* Define the mail middleware every message should be passed through.
* 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
*/
'middleware' => [
ToFilter::class,
CcFilter::class,
BccFilter::class,
EnsureRecipients::class,
],

/**
* Define the domains and email addresses that are allowed
* to receive mails from your application.
Expand Down
3 changes: 3 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,7 @@
<directory suffix=".php">./src</directory>
</include>
</source>
<php>
<env name="MAIL_ALLOWLIST_ENABLED" value="true"/>
</php>
</phpunit>
10 changes: 10 additions & 0 deletions src/Actions/Addresses/CheckAddressContract.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace TobMoeller\LaravelMailAllowlist\Actions\Addresses;

use Symfony\Component\Mime\Address;

interface CheckAddressContract
{
public function check(Address $address): bool;
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<?php

namespace TobMoeller\LaravelMailAllowlist\Actions;
namespace TobMoeller\LaravelMailAllowlist\Actions\Addresses;

use Symfony\Component\Mime\Address;

class IsAllowedRecipient
class IsAllowedRecipient implements CheckAddressContract
{
/**
* @param array<int, string> $allowedDomains
Expand Down
33 changes: 0 additions & 33 deletions src/Actions/FilterMessageRecipients.php

This file was deleted.

55 changes: 55 additions & 0 deletions src/Enums/Header.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace TobMoeller\LaravelMailAllowlist\Enums;

/**
* Corresponds to Symfony\Component\Mime\Header\Headers::HEADER_CLASS_MAP
*/
enum Header: string
{
case DATE = 'date';
case FROM = 'from';
case SENDER = 'sender';
case REPLY_TO = 'reply-to';
case TO = 'to';
case CC = 'cc';
case BCC = 'bcc';
case MESSAGE_ID = 'message-id';
case IN_REPLY_TO = 'in-reply-to';
case REFERENCES = 'references';
case SUBJECT = 'subject';

/**
* @return array<int, self>
*/
public static function addressHeaders(): array
{
return [
self::SENDER,
];
}

public function isAddressHeader(): bool
{
return in_array($this, self::addressHeaders());
}

/**
* @return array<int, self>
*/
public static function addressListHeaders(): array
{
return [
self::FROM,
self::REPLY_TO,
self::TO,
self::CC,
self::BCC,
];
}

public function isAddressListHeader(): bool
{
return in_array($this, self::addressListHeaders());
}
}
11 changes: 11 additions & 0 deletions src/LaravelMailAllowlist.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace TobMoeller\LaravelMailAllowlist;

use Illuminate\Support\Facades\Config;
use TobMoeller\LaravelMailAllowlist\MailMiddleware\MailMiddlewareContract;

class LaravelMailAllowlist
{
Expand All @@ -11,6 +12,16 @@ public function enabled(): bool
return (bool) Config::get('mail-allowlist.enabled', false);
}

/**
* @return array<int, MailMiddlewareContract|class-string<MailMiddlewareContract>>
*/
public function mailMiddleware(): array
{
$middleware = Config::get('mail-allowlist.middleware');

return is_array($middleware) ? $middleware : [];
}

/**
* @return array<int, string>
*/
Expand Down
8 changes: 5 additions & 3 deletions src/LaravelMailAllowlistServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
use Illuminate\Support\Facades\Event;
use Spatie\LaravelPackageTools\Package;
use Spatie\LaravelPackageTools\PackageServiceProvider;
use TobMoeller\LaravelMailAllowlist\Actions\IsAllowedRecipient;
use TobMoeller\LaravelMailAllowlist\Actions\Addresses\IsAllowedRecipient;
use TobMoeller\LaravelMailAllowlist\Facades\LaravelMailAllowlist;
use TobMoeller\LaravelMailAllowlist\Listeners\MessageSendingListener;

Expand All @@ -19,16 +19,18 @@ public function configurePackage(Package $package): void
->hasConfigFile();
}

public function packageBooted(): void
public function packageRegistered(): void
{
$this->app->bind(RecipientFilter::class);
$this->app->singleton(IsAllowedRecipient::class, function () {
return new IsAllowedRecipient(
LaravelMailAllowlist::allowedDomainList(),
LaravelMailAllowlist::allowedEmailList(),
);
});
}

public function packageBooted(): void
{
if (LaravelMailAllowlist::enabled()) {
Event::listen(MessageSending::class, MessageSendingListener::class);
}
Expand Down
15 changes: 7 additions & 8 deletions src/Listeners/MessageSendingListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
namespace TobMoeller\LaravelMailAllowlist\Listeners;

use Illuminate\Mail\Events\MessageSending;
use TobMoeller\LaravelMailAllowlist\Actions\FilterMessageRecipients;
use Illuminate\Support\Facades\Pipeline;
use TobMoeller\LaravelMailAllowlist\Facades\LaravelMailAllowlist;
use TobMoeller\LaravelMailAllowlist\MailMiddleware\MessageContext;

class MessageSendingListener
{
Expand All @@ -14,14 +15,12 @@ public function handle(MessageSending $messageSendingEvent): bool
return true;
}

$message = $messageSendingEvent->message;
$messageContext = app(MessageContext::class, ['message' => $messageSendingEvent->message]);

app(FilterMessageRecipients::class)->filter($message);
Pipeline::send($messageContext)
->through(LaravelMailAllowlist::mailMiddleware())
->thenReturn();

if (empty($message->getTo())) {
return false;
}

return true;
return $messageContext->shouldSendMessage();
}
}
Loading