diff --git a/README.md b/README.md index 1a8419a..99e9429 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![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 provides a customizable middleware pipeline for email messages, allowing you to filter, modify, and inspect emails before they are sent. +This package provides a customizable middleware pipeline for email messages, allowing you to filter, modify, and inspect emails before they are sent as well as inspecting just sent emails. **Key Features:** @@ -27,16 +27,18 @@ This package provides a customizable middleware pipeline for email messages, all - 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. + - You can define custom middleware that runs before or after an email is sent. - **Advanced Logging Options:** - Configure logging to use custom channels. - Set custom log levels (e.g., 'debug', 'info', 'error', etc.). - Enable mail middleware to add individual log messages during email processing. - - Choose whether to include middleware logs, email message headers or message bodies in the logs. + - Choose whether to include middleware logs, email message headers, message meta data or message bodies in the logs. + - Write logs for messages that are about to be sent and for messages that have just been sent. > **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. +> This package utilizes Laravel's `MessageSending` and `MessageSent` events 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 @@ -52,7 +54,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 to just keep the edited configuration values. Additionally this package provides the ability to configure most of the required values through your environment variables. ## Usage @@ -77,10 +79,18 @@ 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" -# Configure logging +# Configure the logging of emails before they are sent MAIL_ALLOWLIST_LOG_ENABLED=true MAIL_ALLOWLIST_LOG_CHANNEL=stack # optional, defaults to Laravel's logging.default MAIL_ALLOWLIST_LOG_LEVEL=error # optional, defaults to info + +# Enable mail middleware that runs after the email is sent +MAIL_ALLOWLIST_SENT_MIDDLEWARE_ENABLED=true + +# Configure the logging of emails after they are sent +MAIL_ALLOWLIST_SENT_LOG_ENABLED=true # optional, defaults to sending log +MAIL_ALLOWLIST_SENT_LOG_CHANNEL=stack # optional, defaults to sending log +MAIL_ALLOWLIST_SENT_LOG_LEVEL=info # optional, defaults to sending log ``` ### Customizing the Middleware Pipeline @@ -88,7 +98,7 @@ MAIL_ALLOWLIST_LOG_LEVEL=error # optional, defaults to info 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' => [ +'pipeline' => [ ToFilter::class; CcFilter::class; BccFilter::class; @@ -105,7 +115,7 @@ The order of middleware in the pipeline matters. Each middleware can modify the 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' => [ +'pipeline' => [ ToFilter::class; EnsureRecipients::class; // stops further execution when no recipients remain CcFilter::class; @@ -141,7 +151,7 @@ class CancelMessageMiddleware implements MailMiddlewareContract 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' => [ +'pipeline' => [ // Upstream middleware \App\Mail\Middleware\CancelMessageMiddleware::class, // As a class-string. new \App\Mail\Middleware\CancelMessageMiddleware(), // As an instance @@ -149,6 +159,26 @@ Then add it to your middleware pipeline. This can be done as a class-string whic ], ``` +#### Custom Middleware for already sent emails + +You can add custom middleware to the sent pipeline to run custom logic on the sent emails like creating database records. The process is similar to the sending middleware but requires you to implement the `MailSentMiddlewareContract` interface in your middleware class. + +```php +use Closure; +use TobMoeller\LaravelMailAllowlist\MailSentMiddleware\MailSentMiddlewareContract; +use TobMoeller\LaravelMailAllowlist\MailSentMiddleware\SentMessageContext; + +class CustomSentMessageMiddleware implements MailSentMiddlewareContract +{ + public function handle(SentMessageContext $messageContext, Closure $next): mixed + { + // custom code + + return $next($messageContext); + } +} +``` + ### Customizing the Logging Behavior You can control most of the logging behavior from environment variables or the configuration file. For more advanced use cases, you might want to have full control over how log messages are generated and where they are sent. You can achieve this by binding your own implementations of the log content generation action and/or the logging action itself. @@ -171,7 +201,7 @@ class CustomLogMessage implements GenerateLogMessageContract } ``` -#### Customizing the log message content +#### Customizing the log creation Create a new class that implements `LogMessageContract` to define how log messages are handled: @@ -219,6 +249,27 @@ class AppServiceProvider extends ServiceProvider } ``` +### Customize the behavior after an email is sent + +Customizing the logging for already sent mails is similar to customizing the logging of outgoing mails. You have to bind your custom implementations of the `SentLogMessageContract` and `GenerateSentLogMessageContract` interfaces in a service provider. + +```php +use TobMoeller\LaravelMailAllowlist\Actions\Logs\GenerateSentLogMessageContract; +use TobMoeller\LaravelMailAllowlist\Actions\Logs\SentLogMessageContract; + +class AppServiceProvider extends ServiceProvider +{ + public function register() + { + // Bind the custom log message generator + $this->app->bind(GenerateSentLogMessageContract::class, CustomSentLogMessage::class); + + // Bind the custom log handler + $this->app->bind(SentLogMessageContract::class, CustomSentMessageLogging::class); + } +} +``` + ## Testing ```bash diff --git a/UPGRADE.md b/UPGRADE.md new file mode 100644 index 0000000..471f103 --- /dev/null +++ b/UPGRADE.md @@ -0,0 +1,6 @@ +# Upgrade Guide + +## Upgrading from 0.4 to 0.5 + +The `mail-allowlist.php` configuration structure has changed. You should compare and incorporate any of these changes into your existing file if you have previously published it. (It may be easiest to make a backup copy of your existing file, re-publish it from this package, and then re-make your customizations to it.) + diff --git a/config/mail-allowlist.php b/config/mail-allowlist.php index b55f0b9..6c519f1 100644 --- a/config/mail-allowlist.php +++ b/config/mail-allowlist.php @@ -16,122 +16,210 @@ 'enabled' => env('MAIL_ALLOWLIST_ENABLED', false), /* - * Enables the mail middleware. + * ------------------------------------------------------------------------------ + * Configuration that applies to the message before it is sent. + * ------------------------------------------------------------------------------ */ - 'middleware_enabled' => env('MAIL_ALLOWLIST_MIDDLEWARE_ENABLED', true), + 'sending' => [ + /* + * Enables the mail middleware. + */ + 'middleware' => [ + 'enabled' => env('MAIL_ALLOWLIST_MIDDLEWARE_ENABLED', true), - /* - * 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 must implement the MailMiddlewareContract - */ - 'middleware' => [ - ToFilter::class, - CcFilter::class, - BccFilter::class, - AddGlobalTo::class, - AddGlobalCc::class, - AddGlobalBcc::class, - EnsureRecipients::class, - ], + /* + * 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 must implement the MailMiddlewareContract + */ + 'pipeline' => [ + ToFilter::class, + CcFilter::class, + BccFilter::class, + AddGlobalTo::class, + AddGlobalCc::class, + AddGlobalBcc::class, + EnsureRecipients::class, + ], - /* - * Define the domains and email addresses that are allowed - * to receive mails from your application. - * All other recipients will be filtered out. - */ - 'allowed' => [ + /* + * Define the domains and email addresses that are allowed + * to receive mails from your application. + * All other recipients will be filtered out. + */ + 'allowed' => [ - /* - * 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 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 (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'), + ], + ], /* - * 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'] + * Configure the logging of filtered mails. */ - 'emails' => env('MAIL_ALLOWLIST_ALLOWED_EMAILS'), - ], + 'log' => [ + /* + * Enables the log. + */ + 'enabled' => env('MAIL_ALLOWLIST_LOG_ENABLED', false), - /* - * 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'), + /* + * Define a custom logging channel for your filtered message + * logs. Leave empty (null) to default to Laravel's default + * logging channel (config: logging.default). If this is + * undefined, it will fall back to the 'stack' channel. + */ + 'channel' => env('MAIL_ALLOWLIST_LOG_CHANNEL'), + + /* + * Define the log level to log your filtered messages in. + */ + 'level' => env('MAIL_ALLOWLIST_LOG_LEVEL', LogLevel::INFO), + + /* + * Define, what parts of the message should be logged. + */ + 'include' => [ + /* + * Each middleware can add messages to the log through the + * message context that is passed through the pipeline. + */ + 'middleware' => true, + + /* + * Log the final message headers. + */ + 'headers' => true, + + /* + * Log the message data. + */ + 'message_data' => false, + + /* + * Log the final message body. + */ + 'body' => false, + ], + ], ], /* - * Configure the logging of filtered mails. + * ------------------------------------------------------------------------------ + * Configuration that applies to the message after it is sent. + * ------------------------------------------------------------------------------ */ - 'log' => [ + 'sent' => [ /* - * Enables the log. + * Enables the mail sent middleware. */ - 'enabled' => env('MAIL_ALLOWLIST_LOG_ENABLED', false), + 'middleware' => [ + 'enabled' => env('MAIL_ALLOWLIST_SENT_MIDDLEWARE_ENABLED', true), - /* - * Define a custom logging channel for your filtered message - * logs. Leave empty (null) to default to Laravel's default - * logging channel (config: logging.default). If this is - * undefined, it will fall back to the 'stack' channel. - */ - 'channel' => env('MAIL_ALLOWLIST_LOG_CHANNEL'), - - /* - * Define the log level to log your filtered messages in. - */ - 'level' => env('MAIL_ALLOWLIST_LOG_LEVEL', LogLevel::INFO), + /* + * Define the mail sent middleware every sent 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 must implement the MailSentMiddlewareContract + */ + 'pipeline' => [ + // + ], + ], /* - * Define, what parts of the message should be logged. + * Configure the logging of filtered mails. */ - 'include' => [ + 'log' => [ /* - * Each middleware can add messages to the log through the - * message context that is passed through the pipeline. + * Enables the logging of sent messages. + * Defaults to sending log if set to null. */ - 'middleware' => true, + 'enabled' => env('MAIL_ALLOWLIST_SENT_LOG_ENABLED'), /* - * Log the final message headers. + * Define a custom logging channel for your sent messages. + * Defaults to sending log channel if set to null. */ - 'headers' => true, + 'channel' => env('MAIL_ALLOWLIST_SENT_LOG_CHANNEL'), /* - * Log the message data. + * Define the log level to log your sent messages. + * Defaults to sending log level if set to null. */ - 'message_data' => false, + 'level' => env('MAIL_ALLOWLIST_SENT_LOG_LEVEL'), /* - * Log the final message body. + * Define, what parts of the sent message should be logged. */ - 'body' => false, + 'include' => [ + /* + * Each middleware can add messages to the log through the + * message context that is passed through the pipeline. + */ + 'middleware' => false, + + /* + * Log the sent message headers. + */ + 'headers' => true, + + /* + * Log the sent message data. + */ + 'message_data' => false, + + /* + * Log the sent message debug information. + */ + 'debug' => false, + + /* + * Log the sent message body. + */ + 'body' => false, + ], ], ], ]; diff --git a/src/Actions/Logs/GenerateLogMessage.php b/src/Actions/Logs/GenerateLogMessage.php index 99abf82..70d5e60 100644 --- a/src/Actions/Logs/GenerateLogMessage.php +++ b/src/Actions/Logs/GenerateLogMessage.php @@ -72,7 +72,7 @@ protected function generateMessageDataMessage(MessageContext $messageContext): s return <<getMessage()) instanceof Email; + $logMessage = 'LaravelMailAllowlist.MessageSent:'; + + if ($className = $messageContext->getOriginatingClassName()) { + $logMessage .= PHP_EOL.'ClassName: '.$className; + } + + if (LaravelMailAllowlist::sentLogMiddleware()) { + $logMessage .= $this->generateMiddlewareMessage($messageContext); + } + + if ($isEmail && LaravelMailAllowlist::sentLogHeaders()) { + $logMessage .= $this->generateHeadersMessage($message); + } + + if (LaravelMailAllowlist::sentLogMessageData()) { + $logMessage .= $this->generateMessageDataMessage($messageContext); + } + + if (LaravelMailAllowlist::sentLogDebugInformation()) { + $logMessage .= $this->generateDebugMessage($messageContext); + } + + if ($isEmail && LaravelMailAllowlist::sentLogBody()) { + $logMessage .= $this->generateBodyMessage($message); + } + + // Handle RawMessage + if (! $isEmail && + LaravelMailAllowlist::sentLogHeaders() && + LaravelMailAllowlist::sentLogBody() + ) { + $logMessage .= $this->generateRawMessageMessage($message); + } elseif (! $isEmail && + ( + LaravelMailAllowlist::sentLogHeaders() || + LaravelMailAllowlist::sentLogBody() + ) + ) { + $logMessage .= PHP_EOL.__('RawMessages can only be logged including headers and body'); + } + + return $logMessage; + } + + protected function generateMiddlewareMessage(SentMessageContext $messageContext): string + { + $logMessage = <<<'LOG_MIDDLEWARE' + + ---------- + MIDDLEWARE + ---------- + LOG_MIDDLEWARE; + + foreach ($messageContext->getLog() as $logEntry) { + $logMessage .= PHP_EOL.$logEntry; + } + + return $logMessage; + } + + protected function generateHeadersMessage(Email $message): string + { + return <<getHeaders()->toString()} + LOG_HEADERS; + } + + protected function generateMessageDataMessage(SentMessageContext $messageContext): string + { + $data = json_encode($messageContext->getMessageData()) ?: ''; + + return <<getBody()->toString()} + LOG_BODY; + } + + protected function generateRawMessageMessage(RawMessage $message): string + { + return <<toString()} + LOG_RAW_MESSAGE; + } + + protected function generateDebugMessage(SentMessageContext $messageContext): string + { + return <<getDebugInformation()} + LOG_DEBUG_INFORMATION; + } +} diff --git a/src/Actions/Logs/GenerateSentLogMessageContract.php b/src/Actions/Logs/GenerateSentLogMessageContract.php new file mode 100644 index 0000000..983296f --- /dev/null +++ b/src/Actions/Logs/GenerateSentLogMessageContract.php @@ -0,0 +1,10 @@ +generateLogMessage->generate($messageContext); + + Log::channel(LaravelMailAllowlist::sentLogChannel()) + ->log(LaravelMailAllowlist::sentLogLevel(), $message); + } +} diff --git a/src/Actions/Logs/SentLogMessageContract.php b/src/Actions/Logs/SentLogMessageContract.php new file mode 100644 index 0000000..7e4006f --- /dev/null +++ b/src/Actions/Logs/SentLogMessageContract.php @@ -0,0 +1,10 @@ +extractArrayFromConfig($allowedDomains); } @@ -46,7 +50,7 @@ public function allowedDomainList(): array */ public function allowedEmailList(): array { - $allowedEmails = Config::get('mail-allowlist.allowed.emails'); + $allowedEmails = Config::get('mail-allowlist.sending.middleware.allowed.emails'); return $this->extractArrayFromConfig($allowedEmails); } @@ -56,7 +60,7 @@ public function allowedEmailList(): array */ public function globalToEmailList(): array { - $toEmails = Config::get('mail-allowlist.global.to'); + $toEmails = Config::get('mail-allowlist.sending.middleware.global.to'); return $this->extractArrayFromConfig($toEmails); } @@ -66,7 +70,7 @@ public function globalToEmailList(): array */ public function globalCcEmailList(): array { - $ccEmails = Config::get('mail-allowlist.global.cc'); + $ccEmails = Config::get('mail-allowlist.sending.middleware.global.cc'); return $this->extractArrayFromConfig($ccEmails); } @@ -76,7 +80,7 @@ public function globalCcEmailList(): array */ public function globalBccEmailList(): array { - $bccEmails = Config::get('mail-allowlist.global.bcc'); + $bccEmails = Config::get('mail-allowlist.sending.middleware.global.bcc'); return $this->extractArrayFromConfig($bccEmails); } @@ -100,30 +104,61 @@ protected function extractArrayFromConfig(mixed $configValue): array return []; } + // -------------------------------------------------------------------------------- + // Sent Middleware Configuration + // -------------------------------------------------------------------------------- + + public function sentMailMiddlewareEnabled(): bool + { + return (bool) Config::get('mail-allowlist.sent.middleware.enabled', true); + } + + /** + * @TODO change contract + * + * @return array> + */ + public function sentMailMiddleware(): array + { + $middleware = Config::get('mail-allowlist.sent.middleware.pipeline'); + + return is_array($middleware) ? $middleware : []; + } + + // -------------------------------------------------------------------------------- + // Log Configuration + // -------------------------------------------------------------------------------- + public function logEnabled(): bool { - return (bool) Config::get('mail-allowlist.log.enabled', false); + return (bool) Config::get('mail-allowlist.sending.log.enabled', false); } public function logChannel(): string { - $channel = Config::get('mail-allowlist.log.channel'); + $channel = Config::get('mail-allowlist.sending.log.channel'); $channel = is_string($channel) ? $channel : Config::get('logging.default'); return is_string($channel) ? $channel : 'stack'; } + protected function validateLogLevel(string $level): bool + { + $allowedLevels = array_values((new ReflectionClass(LogLevel::class))->getConstants()); + + return in_array($level, $allowedLevels); + } + /** * @throws Throwable */ public function logLevel(): string { - $level = Config::get('mail-allowlist.log.level'); - $allowedLevels = array_values((new ReflectionClass(LogLevel::class))->getConstants()); + $level = Config::get('mail-allowlist.sending.log.level'); throw_unless( is_string($level) && - in_array($level = mb_strtolower($level), $allowedLevels), + $this->validateLogLevel($level = mb_strtolower($level)), InvalidArgumentException::class, 'Invalid log level provided' ); @@ -133,21 +168,83 @@ public function logLevel(): string public function logMiddleware(): bool { - return (bool) Config::get('mail-allowlist.log.include.middleware'); + return (bool) Config::get('mail-allowlist.sending.log.include.middleware'); } public function logHeaders(): bool { - return (bool) Config::get('mail-allowlist.log.include.headers'); + return (bool) Config::get('mail-allowlist.sending.log.include.headers'); } public function logMessageData(): bool { - return (bool) Config::get('mail-allowlist.log.include.message_data'); + return (bool) Config::get('mail-allowlist.sending.log.include.message_data'); } public function logBody(): bool { - return (bool) Config::get('mail-allowlist.log.include.body'); + return (bool) Config::get('mail-allowlist.sending.log.include.body'); + } + + // -------------------------------------------------------------------------------- + // Sent Log Configuration + // -------------------------------------------------------------------------------- + + public function sentLogEnabled(): bool + { + return (bool) (Config::get('mail-allowlist.sent.log.enabled') ?? $this->logEnabled()); + } + + public function sentLogChannel(): string + { + $channel = Config::get('mail-allowlist.sent.log.channel'); + + return is_string($channel) ? $channel : $this->logChannel(); + } + + /** + * @throws Throwable + */ + public function sentLogLevel(): string + { + $level = Config::get('mail-allowlist.sent.log.level'); + + if ($level === null) { + $level = $this->logLevel(); + } else { + throw_unless( + is_string($level) && + $this->validateLogLevel($level = mb_strtolower($level)), + InvalidArgumentException::class, + 'Invalid log level provided' + ); + } + + return $level; + } + + public function sentLogMiddleware(): bool + { + return (bool) (Config::get('mail-allowlist.sent.log.include.middleware') ?? $this->logMiddleware()); + } + + public function sentLogHeaders(): bool + { + return (bool) (Config::get('mail-allowlist.sent.log.include.headers') ?? $this->logHeaders()); + } + + public function sentLogMessageData(): bool + { + return (bool) (Config::get('mail-allowlist.sent.log.include.message_data') ?? $this->logMessageData()); + } + + public function sentLogDebugInformation(): bool + { + return (bool) Config::get('mail-allowlist.sent.log.include.debug', false); + } + + public function sentLogBody(): bool + { + return (bool) (Config::get('mail-allowlist.sent.log.include.body') ?? $this->logBody()); } } diff --git a/src/LaravelMailAllowlistServiceProvider.php b/src/LaravelMailAllowlistServiceProvider.php index 2ae3ad5..e149321 100644 --- a/src/LaravelMailAllowlistServiceProvider.php +++ b/src/LaravelMailAllowlistServiceProvider.php @@ -3,16 +3,22 @@ namespace TobMoeller\LaravelMailAllowlist; use Illuminate\Mail\Events\MessageSending; +use Illuminate\Mail\Events\MessageSent; use Illuminate\Support\Facades\Event; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; use TobMoeller\LaravelMailAllowlist\Actions\Addresses\IsAllowedRecipient; use TobMoeller\LaravelMailAllowlist\Actions\Logs\GenerateLogMessage; use TobMoeller\LaravelMailAllowlist\Actions\Logs\GenerateLogMessageContract; +use TobMoeller\LaravelMailAllowlist\Actions\Logs\GenerateSentLogMessage; +use TobMoeller\LaravelMailAllowlist\Actions\Logs\GenerateSentLogMessageContract; use TobMoeller\LaravelMailAllowlist\Actions\Logs\LogMessage; use TobMoeller\LaravelMailAllowlist\Actions\Logs\LogMessageContract; +use TobMoeller\LaravelMailAllowlist\Actions\Logs\SentLogMessage; +use TobMoeller\LaravelMailAllowlist\Actions\Logs\SentLogMessageContract; use TobMoeller\LaravelMailAllowlist\Facades\LaravelMailAllowlist; use TobMoeller\LaravelMailAllowlist\Listeners\MessageSendingListener; +use TobMoeller\LaravelMailAllowlist\Listeners\MessageSentListener; class LaravelMailAllowlistServiceProvider extends PackageServiceProvider { @@ -27,6 +33,10 @@ public function packageRegistered(): void { $this->app->bind(LogMessageContract::class, LogMessage::class); $this->app->bind(GenerateLogMessageContract::class, GenerateLogMessage::class); + + $this->app->bind(SentLogMessageContract::class, SentLogMessage::class); + $this->app->bind(GenerateSentLogMessageContract::class, GenerateSentLogMessage::class); + $this->app->singleton(IsAllowedRecipient::class, function () { return new IsAllowedRecipient( LaravelMailAllowlist::allowedDomainList(), @@ -39,6 +49,7 @@ public function packageBooted(): void { if (LaravelMailAllowlist::enabled()) { Event::listen(MessageSending::class, MessageSendingListener::class); + Event::listen(MessageSent::class, MessageSentListener::class); } } } diff --git a/src/Listeners/MessageSentListener.php b/src/Listeners/MessageSentListener.php new file mode 100644 index 0000000..cbe35fe --- /dev/null +++ b/src/Listeners/MessageSentListener.php @@ -0,0 +1,39 @@ + $messageSent->sent, + 'messageData' => $messageSent->data, + ]); + + if (LaravelMailAllowlist::sentMailMiddlewareEnabled()) { + Pipeline::send($messageContext) + ->through(LaravelMailAllowlist::sentMailMiddleware()) + ->thenReturn(); + } + + if (LaravelMailAllowlist::sentLogEnabled()) { + $this->messageLogger->log($messageContext); + } + } +} diff --git a/src/MailSentMiddleware/MailSentMiddlewareContract.php b/src/MailSentMiddleware/MailSentMiddlewareContract.php new file mode 100644 index 0000000..2772191 --- /dev/null +++ b/src/MailSentMiddleware/MailSentMiddlewareContract.php @@ -0,0 +1,10 @@ + + */ + public Collection $sharedData; + + protected SentMessage $sentMessage; + + /** + * @param array $messageData + */ + public function __construct(SentMessage $sentMessage, array $messageData = []) + { + $this->sentMessage = $sentMessage; + $this->messageData = $messageData; + $this->sharedData = new Collection; + } + + public function getMessage(): Email|RawMessage + { + return $this->sentMessage->getOriginalMessage(); + } + + public function getSentMessage(): SentMessage + { + return $this->sentMessage; + } + + public function getDebugInformation(): string + { + return $this->getSentMessage()->getDebug(); + } +} diff --git a/tests/Actions/IsAllowedRecipientTest.php b/tests/Actions/IsAllowedRecipientTest.php index 2cd025c..6be36dd 100644 --- a/tests/Actions/IsAllowedRecipientTest.php +++ b/tests/Actions/IsAllowedRecipientTest.php @@ -5,8 +5,8 @@ use TobMoeller\LaravelMailAllowlist\Actions\Addresses\IsAllowedRecipient; it('gets initialized with allowed domain and email lists', function () { - Config::set('mail-allowlist.allowed.domains', $allowedDomains = ['foo.de', 'bar.de']); - Config::set('mail-allowlist.allowed.emails', $allowedEmails = ['bar@foo.de', 'foo@bar.de']); + Config::set('mail-allowlist.sending.middleware.allowed.domains', $allowedDomains = ['foo.de', 'bar.de']); + Config::set('mail-allowlist.sending.middleware.allowed.emails', $allowedEmails = ['bar@foo.de', 'foo@bar.de']); $action = app(IsAllowedRecipient::class); @@ -16,7 +16,7 @@ }); it('checks if the recipient has an allowed domain', function () { - Config::set('mail-allowlist.allowed.domains', ['bar.de']); + Config::set('mail-allowlist.sending.middleware.allowed.domains', ['bar.de']); $allowedAddress = new Address('foo@bar.de'); $deniedAddress = new Address('bar@foo.de'); @@ -28,7 +28,7 @@ }); it('checks if the recipient has an allowed email', function () { - Config::set('mail-allowlist.allowed.emails', ['foo@bar.de']); + Config::set('mail-allowlist.sending.middleware.allowed.emails', ['foo@bar.de']); $allowedAddress = new Address('foo@bar.de'); $deniedAddress = new Address('bar@foo.de'); diff --git a/tests/Actions/Logs/GenerateLogMessageTest.php b/tests/Actions/Logs/GenerateLogMessageTest.php index 8ce4cb3..af245f1 100644 --- a/tests/Actions/Logs/GenerateLogMessageTest.php +++ b/tests/Actions/Logs/GenerateLogMessageTest.php @@ -29,10 +29,10 @@ }); it('generates a log message', function (bool $hasClassName, bool $canceled) { - Config::set('mail-allowlist.log.include.middleware', false); - Config::set('mail-allowlist.log.include.headers', false); - Config::set('mail-allowlist.log.include.message_data', false); - Config::set('mail-allowlist.log.include.body', false); + Config::set('mail-allowlist.sending.log.include.middleware', false); + Config::set('mail-allowlist.sending.log.include.headers', false); + Config::set('mail-allowlist.sending.log.include.message_data', false); + Config::set('mail-allowlist.sending.log.include.body', false); $expectation = 'LaravelMailAllowlist.MessageSending:'; $expectation .= PHP_EOL.'ClassName: ::notification_name::'; @@ -47,10 +47,10 @@ })->with([true, false], [true, false]); it('generates a log message with middleware', function () { - Config::set('mail-allowlist.log.include.middleware', true); - Config::set('mail-allowlist.log.include.headers', false); - Config::set('mail-allowlist.log.include.message_data', false); - Config::set('mail-allowlist.log.include.body', false); + Config::set('mail-allowlist.sending.log.include.middleware', true); + Config::set('mail-allowlist.sending.log.include.headers', false); + Config::set('mail-allowlist.sending.log.include.message_data', false); + Config::set('mail-allowlist.sending.log.include.body', false); $this->context->cancelSendingMessage('::reason::'); @@ -71,10 +71,10 @@ }); it('generates a log message with headers', function () { - Config::set('mail-allowlist.log.include.middleware', false); - Config::set('mail-allowlist.log.include.headers', true); - Config::set('mail-allowlist.log.include.message_data', false); - Config::set('mail-allowlist.log.include.body', false); + Config::set('mail-allowlist.sending.log.include.middleware', false); + Config::set('mail-allowlist.sending.log.include.headers', true); + Config::set('mail-allowlist.sending.log.include.message_data', false); + Config::set('mail-allowlist.sending.log.include.body', false); $expectation = <<<'LOG_MESSAGE' LaravelMailAllowlist.MessageSending: @@ -90,10 +90,10 @@ }); it('generates a log message with body', function () { - Config::set('mail-allowlist.log.include.middleware', false); - Config::set('mail-allowlist.log.include.headers', false); - Config::set('mail-allowlist.log.include.message_data', false); - Config::set('mail-allowlist.log.include.body', true); + Config::set('mail-allowlist.sending.log.include.middleware', false); + Config::set('mail-allowlist.sending.log.include.headers', false); + Config::set('mail-allowlist.sending.log.include.message_data', false); + Config::set('mail-allowlist.sending.log.include.body', true); $expectation = <<<'LOG_MESSAGE' LaravelMailAllowlist.MessageSending: @@ -109,16 +109,16 @@ }); it('generates a log message with message data', function () { - Config::set('mail-allowlist.log.include.middleware', false); - Config::set('mail-allowlist.log.include.headers', false); - Config::set('mail-allowlist.log.include.message_data', true); - Config::set('mail-allowlist.log.include.body', false); + Config::set('mail-allowlist.sending.log.include.middleware', false); + Config::set('mail-allowlist.sending.log.include.headers', false); + Config::set('mail-allowlist.sending.log.include.message_data', true); + Config::set('mail-allowlist.sending.log.include.body', false); $expectation = <<<'LOG_MESSAGE' LaravelMailAllowlist.MessageSending: ClassName: ::notification_name:: ---------- - MESSAGE DATA + DATA ---------- LOG_MESSAGE; $expectation .= PHP_EOL.json_encode($this->messageData); @@ -128,10 +128,10 @@ }); it('generates a log message with all options enabled', function () { - Config::set('mail-allowlist.log.include.middleware', true); - Config::set('mail-allowlist.log.include.headers', true); - Config::set('mail-allowlist.log.include.message_data', true); - Config::set('mail-allowlist.log.include.body', true); + Config::set('mail-allowlist.sending.log.include.middleware', true); + Config::set('mail-allowlist.sending.log.include.headers', true); + Config::set('mail-allowlist.sending.log.include.message_data', true); + Config::set('mail-allowlist.sending.log.include.body', true); $headers = $this->mail->getHeaders()->toString(); $body = $this->mail->getBody()->toString(); @@ -150,7 +150,7 @@ ---------- {$headers} ---------- - MESSAGE DATA + DATA ---------- {$data} ---------- diff --git a/tests/Actions/Logs/GenerateSentLogMessageTest.php b/tests/Actions/Logs/GenerateSentLogMessageTest.php new file mode 100644 index 0000000..1b2e301 --- /dev/null +++ b/tests/Actions/Logs/GenerateSentLogMessageTest.php @@ -0,0 +1,226 @@ +mail = new Email; + $this->sentMail = generateSentMessage($this->mail); + + $this->messageData = [ + 'test_meta' => '::test_meta::', + '__laravel_notification' => '::notification_name::', + ]; + + $this->context = new SentMessageContext($this->sentMail, $this->messageData); + $this->context->addLog('::middleware_log::'); + $this->context->addLog('::middleware_log2::'); + + $this->logger = new GenerateSentLogMessage; +}); + +it('is bound to interface', function () { + expect(app(GenerateSentLogMessageContract::class)) + ->toBeInstanceOf(GenerateSentLogMessage::class); +}); + +it('generates a log message', function () { + Config::set('mail-allowlist.sent.log.include.middleware', false); + Config::set('mail-allowlist.sent.log.include.headers', false); + Config::set('mail-allowlist.sent.log.include.message_data', false); + Config::set('mail-allowlist.sent.log.include.body', false); + Config::set('mail-allowlist.sent.log.include.debug', false); + + $expectation = 'LaravelMailAllowlist.MessageSent:'; + $expectation .= PHP_EOL.'ClassName: ::notification_name::'; + + expect($this->logger->generate($this->context)) + ->toBe($expectation); +}); + +it('generates a log message with middleware', function () { + Config::set('mail-allowlist.sent.log.include.middleware', true); + Config::set('mail-allowlist.sent.log.include.headers', false); + Config::set('mail-allowlist.sent.log.include.message_data', false); + Config::set('mail-allowlist.sent.log.include.body', false); + Config::set('mail-allowlist.sent.log.include.debug', false); + + $expectation = <<<'LOG_MESSAGE' + LaravelMailAllowlist.MessageSent: + ClassName: ::notification_name:: + ---------- + MIDDLEWARE + ---------- + ::middleware_log:: + ::middleware_log2:: + LOG_MESSAGE; + + expect($this->logger->generate($this->context)) + ->toBe($expectation); +}); + +it('generates a log message with headers', function () { + Config::set('mail-allowlist.sent.log.include.middleware', false); + Config::set('mail-allowlist.sent.log.include.headers', true); + Config::set('mail-allowlist.sent.log.include.message_data', false); + Config::set('mail-allowlist.sent.log.include.body', false); + Config::set('mail-allowlist.sent.log.include.debug', false); + + $expectation = <<<'LOG_MESSAGE' + LaravelMailAllowlist.MessageSent: + ClassName: ::notification_name:: + ---------- + HEADERS + ---------- + LOG_MESSAGE; + $expectation .= PHP_EOL.$this->mail->getHeaders()->toString(); + + expect($this->logger->generate($this->context)) + ->toBe($expectation); +}); + +it('generates a log message with body', function () { + Config::set('mail-allowlist.sent.log.include.middleware', false); + Config::set('mail-allowlist.sent.log.include.headers', false); + Config::set('mail-allowlist.sent.log.include.message_data', false); + Config::set('mail-allowlist.sent.log.include.body', true); + Config::set('mail-allowlist.sent.log.include.debug', false); + + $expectation = <<<'LOG_MESSAGE' + LaravelMailAllowlist.MessageSent: + ClassName: ::notification_name:: + ---------- + BODY + ---------- + LOG_MESSAGE; + $expectation .= PHP_EOL.$this->mail->getBody()->toString(); + + expect($this->logger->generate($this->context)) + ->toBe($expectation); +}); + +it('generates a log message with message data', function () { + Config::set('mail-allowlist.sent.log.include.middleware', false); + Config::set('mail-allowlist.sent.log.include.headers', false); + Config::set('mail-allowlist.sent.log.include.message_data', true); + Config::set('mail-allowlist.sent.log.include.body', false); + Config::set('mail-allowlist.sent.log.include.debug', false); + + $expectation = <<<'LOG_MESSAGE' + LaravelMailAllowlist.MessageSent: + ClassName: ::notification_name:: + ---------- + DATA + ---------- + LOG_MESSAGE; + $expectation .= PHP_EOL.json_encode($this->messageData); + + expect($this->logger->generate($this->context)) + ->toBe($expectation); +}); + +it('generates a log message with all options enabled', function () { + Config::set('mail-allowlist.sent.log.include.middleware', true); + Config::set('mail-allowlist.sent.log.include.headers', true); + Config::set('mail-allowlist.sent.log.include.message_data', true); + Config::set('mail-allowlist.sent.log.include.body', true); + Config::set('mail-allowlist.sent.log.include.debug', true); + + $headers = $this->mail->getHeaders()->toString(); + $body = $this->mail->getBody()->toString(); + $data = json_encode($this->messageData); + + $expectation = <<logger->generate($this->context)) + ->toBe($expectation); +}); + +it('generates a log message for raw messages', function () { + Config::set('mail-allowlist.sent.log.include.middleware', false); + Config::set('mail-allowlist.sent.log.include.headers', true); + Config::set('mail-allowlist.sent.log.include.message_data', false); + Config::set('mail-allowlist.sent.log.include.body', true); + Config::set('mail-allowlist.sent.log.include.debug', false); + + $rawMessage = new RawMessage($this->mail->toString()); + $context = new SentMessageContext(generateSentMessage($rawMessage)); + + $expectation = <<toString()} + LOG_MESSAGE; + + expect($this->logger->generate($context)) + ->toBe($expectation); +}); + +it('does not generate a log message for raw messages if only headers or body is checked', function (bool $body) { + Config::set('mail-allowlist.sent.log.include.middleware', false); + Config::set('mail-allowlist.sent.log.include.headers', ! $body); + Config::set('mail-allowlist.sent.log.include.message_data', false); + Config::set('mail-allowlist.sent.log.include.body', $body); + Config::set('mail-allowlist.sent.log.include.debug', false); + + $rawMessage = new RawMessage($this->mail->toString()); + $context = new SentMessageContext(generateSentMessage($rawMessage)); + + $expectation = <<<'LOG_MESSAGE' + LaravelMailAllowlist.MessageSent: + RawMessages can only be logged including headers and body + LOG_MESSAGE; + + expect($this->logger->generate($context)) + ->toBe($expectation); +})->with([true, false]); + +it('logs debug information', function () { + Config::set('mail-allowlist.sent.log.include.middleware', false); + Config::set('mail-allowlist.sent.log.include.headers', false); + Config::set('mail-allowlist.sent.log.include.message_data', false); + Config::set('mail-allowlist.sent.log.include.body', false); + Config::set('mail-allowlist.sent.log.include.debug', true); + + $expectation = <<<'LOG_MESSAGE' + LaravelMailAllowlist.MessageSent: + ClassName: ::notification_name:: + ---------- + DEBUG + ---------- + ::debug:: + LOG_MESSAGE; + + expect($this->logger->generate($this->context)) + ->toBe($expectation); +}); diff --git a/tests/Actions/Logs/LogMessageTest.php b/tests/Actions/Logs/LogMessageTest.php index a2641c2..55cd295 100644 --- a/tests/Actions/Logs/LogMessageTest.php +++ b/tests/Actions/Logs/LogMessageTest.php @@ -15,8 +15,8 @@ }); it('logs the message context', function () { - Config::set('mail-allowlist.log.channel', '::channel::'); - Config::set('mail-allowlist.log.level', LogLevel::INFO); + Config::set('mail-allowlist.sending.log.channel', '::channel::'); + Config::set('mail-allowlist.sending.log.level', LogLevel::INFO); $mail = new Email; $context = new MessageContext($mail); diff --git a/tests/Actions/Logs/SentLogMessageTest.php b/tests/Actions/Logs/SentLogMessageTest.php new file mode 100644 index 0000000..ab7c6bb --- /dev/null +++ b/tests/Actions/Logs/SentLogMessageTest.php @@ -0,0 +1,38 @@ +toBeInstanceOf(SentLogMessage::class); +}); + +it('logs the message context', function () { + Config::set('mail-allowlist.sent.log.channel', '::channel::'); + Config::set('mail-allowlist.sent.log.level', LogLevel::INFO); + + $context = new SentMessageContext(generateSentMessage()); + + $messageGeneratorMock = Mockery::mock(GenerateSentLogMessageContract::class); + $messageGeneratorMock->shouldReceive('generate') + ->once() + ->with(Mockery::on(fn (SentMessageContext $contextArgument) => $contextArgument === $context)) + ->andReturn('::log_message::'); + + Log::shouldReceive('channel') + ->once() + ->with('::channel::') + ->andReturnSelf() + ->shouldReceive('log') + ->once() + ->with(LogLevel::INFO, '::log_message::'); + + $logger = new SentLogMessage($messageGeneratorMock); + $logger->log($context); +}); diff --git a/tests/LaravelMailAllowListFacadeTest.php b/tests/LaravelMailAllowListFacadeTest.php index 80dc564..69698a2 100644 --- a/tests/LaravelMailAllowListFacadeTest.php +++ b/tests/LaravelMailAllowListFacadeTest.php @@ -1,11 +1,13 @@ toBe($enabled); Event::assertListening(MessageSending::class, MessageSendingListener::class); + Event::assertListening(MessageSent::class, MessageSentListener::class); })->with([true, false]); it('returns default mail middleware', function () { @@ -36,12 +39,19 @@ EnsureRecipients::class, ]); }); -it('returns mail middleware', function (mixed $value, mixed $expected) { - Config::set('mail-allowlist.middleware', $value); - expect(LaravelMailAllowlist::mailMiddleware()) +it('returns mail middleware', function (string $event, mixed $value, mixed $expected) { + Config::set('mail-allowlist.'.$event.'.middleware.pipeline', $value); + + $middleware = $event === 'sending' ? + LaravelMailAllowlist::mailMiddleware() : + LaravelMailAllowlist::sentMailMiddleware(); + expect($middleware) ->toBe($expected); })->with([ + 'sending', + 'sent', +], [ [ 'value' => ['::class-string::', $class = new class {}], 'expected' => ['::class-string::', $class], @@ -61,7 +71,7 @@ ]); it('returns the allowed domain list', function (mixed $value, mixed $expected) { - Config::set('mail-allowlist.allowed.domains', $value); + Config::set('mail-allowlist.sending.middleware.allowed.domains', $value); expect(LaravelMailAllowlist::allowedDomainList()) ->toBe($expected); @@ -85,7 +95,7 @@ ]); it('returns the allowed email list', function (mixed $value, mixed $expected) { - Config::set('mail-allowlist.allowed.emails', $value); + Config::set('mail-allowlist.sending.middleware.allowed.emails', $value); expect(LaravelMailAllowlist::allowedEmailList()) ->toBe($expected); @@ -109,9 +119,9 @@ ]); 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); + Config::set('mail-allowlist.sending.middleware.global.to', $value); + Config::set('mail-allowlist.sending.middleware.global.cc', $value); + Config::set('mail-allowlist.sending.middleware.global.bcc', $value); expect(LaravelMailAllowlist::globalToEmailList()) ->toBe($expected) @@ -139,15 +149,23 @@ ]); it('returns if logging is enabled', function (bool $enabled) { - Config::set('mail-allowlist.log.enabled', $enabled); + Config::set('mail-allowlist.sending.log.enabled', $enabled); expect(LaravelMailAllowlist::logEnabled()) ->toBe($enabled); })->with([true, false]); +it('returns if sent logging is enabled', function (?bool $enabled) { + Config::set('mail-allowlist.sending.log.enabled', true); + Config::set('mail-allowlist.sent.log.enabled', $enabled); + + expect(LaravelMailAllowlist::sentLogEnabled()) + ->toBe($enabled === null ? true : $enabled); +})->with([true, false, null]); + it('returns the log channel', function (mixed $value, mixed $default, string $expected) { Config::set('logging.default', $default); - Config::set('mail-allowlist.log.channel', $value); + Config::set('mail-allowlist.sending.log.channel', $value); expect(LaravelMailAllowlist::logChannel()) ->toBe($expected); @@ -179,15 +197,57 @@ ], ]); +it('returns the sent log channel', function (mixed $value, mixed $default, string $expected) { + Config::set('mail-allowlist.sending.log.channel', $default); + Config::set('mail-allowlist.sent.log.channel', $value); + + expect(LaravelMailAllowlist::sentLogChannel()) + ->toBe($expected); +})->with([ + [ + 'value' => '::channel::', + 'default' => null, + 'expected' => '::channel::', + ], + [ + 'value' => null, + 'default' => '::default::', + 'expected' => '::default::', + ], + [ + 'value' => null, + 'default' => null, + 'expected' => 'stack', + ], + [ + 'value' => false, + 'default' => '::default::', + 'expected' => '::default::', + ], + [ + 'value' => false, + 'default' => false, + 'expected' => 'stack', + ], +]); + it('returns the log level', function (string $level) { - Config::set('mail-allowlist.log.level', $level); + Config::set('mail-allowlist.sending.log.level', $level); expect(LaravelMailAllowlist::logLevel()) ->toBe($level); })->with(fn () => array_values((new ReflectionClass(LogLevel::class))->getConstants())); +it('returns the sent log level', function (?string $level) { + Config::set('mail-allowlist.sending.log.level', LogLevel::INFO); + Config::set('mail-allowlist.sent.log.level', $level); + + expect(LaravelMailAllowlist::sentLogLevel()) + ->toBe($level === null ? LogLevel::INFO : $level); +})->with(fn () => array_merge(array_values((new ReflectionClass(LogLevel::class))->getConstants()), [null])); + it('throws on invalid log levels', function (mixed $level) { - Config::set('mail-allowlist.log.level', $level); + Config::set('mail-allowlist.sending.log.level', $level); expect(fn () => LaravelMailAllowlist::logLevel()) ->toThrow(InvalidArgumentException::class, 'Invalid log level provided'); @@ -196,23 +256,51 @@ null, ]); +it('throws on invalid sent log levels', function () { + Config::set('mail-allowlist.sent.log.level', '::invalid_level::'); + + expect(fn () => LaravelMailAllowlist::sentLogLevel()) + ->toThrow(InvalidArgumentException::class, 'Invalid log level provided'); +}); + it('returns if middleware should be logged', function (bool $enabled) { - Config::set('mail-allowlist.log.include.middleware', $enabled); + Config::set('mail-allowlist.sending.log.include.middleware', $enabled); expect(LaravelMailAllowlist::logMiddleware()) ->toBe($enabled); })->with([true, false]); it('returns if headers should be logged', function (bool $enabled) { - Config::set('mail-allowlist.log.include.headers', $enabled); + Config::set('mail-allowlist.sending.log.include.headers', $enabled); expect(LaravelMailAllowlist::logHeaders()) ->toBe($enabled); })->with([true, false]); it('returns if body should be logged', function (bool $enabled) { - Config::set('mail-allowlist.log.include.body', $enabled); + Config::set('mail-allowlist.sending.log.include.body', $enabled); expect(LaravelMailAllowlist::logBody()) ->toBe($enabled); })->with([true, false]); + +it('returns if sent middleware should be logged', function (bool $enabled) { + Config::set('mail-allowlist.sent.log.include.middleware', $enabled); + + expect(LaravelMailAllowlist::sentLogMiddleware()) + ->toBe($enabled); +})->with([true, false]); + +it('returns if sent headers should be logged', function (bool $enabled) { + Config::set('mail-allowlist.sent.log.include.headers', $enabled); + + expect(LaravelMailAllowlist::sentLogHeaders()) + ->toBe($enabled); +})->with([true, false]); + +it('returns if sent body should be logged', function (bool $enabled) { + Config::set('mail-allowlist.sent.log.include.body', $enabled); + + expect(LaravelMailAllowlist::sentLogBody()) + ->toBe($enabled); +})->with([true, false]); diff --git a/tests/Listeners/MessageSendingListenerTest.php b/tests/Listeners/MessageSendingListenerTest.php index 3c517f6..5d572a3 100644 --- a/tests/Listeners/MessageSendingListenerTest.php +++ b/tests/Listeners/MessageSendingListenerTest.php @@ -28,9 +28,9 @@ it('runs the middleware pipelines and returns if the message should be sent', function (bool $shouldSendMessage, bool $shouldLog) { Config::set('mail-allowlist.enabled', true); - Config::set('mail-allowlist.log.enabled', $shouldLog); - Config::set('mail-allowlist.middleware_enabled', true); - Config::set('mail-allowlist.middleware', $middleware = ['::middleware::']); + Config::set('mail-allowlist.sending.log.enabled', $shouldLog); + Config::set('mail-allowlist.sending.middleware.enabled', true); + Config::set('mail-allowlist.sending.middleware.pipeline', $middleware = ['::middleware::']); $message = new Email; @@ -74,8 +74,8 @@ it('does not run the middleware if disabled', function () { Config::set('mail-allowlist.enabled', true); - Config::set('mail-allowlist.middleware_enabled', false); - Config::set('mail-allowlist.middleware', ['::middleware::']); + Config::set('mail-allowlist.sending.middleware.enabled', false); + Config::set('mail-allowlist.sending.middleware.pipeline', ['::middleware::']); $loggerMock = Mockery::mock(LogMessage::class); $loggerMock->shouldNotReceive('log'); diff --git a/tests/Listeners/MessageSentListenerTest.php b/tests/Listeners/MessageSentListenerTest.php new file mode 100644 index 0000000..abe898e --- /dev/null +++ b/tests/Listeners/MessageSentListenerTest.php @@ -0,0 +1,93 @@ +message = new Email; + $this->sentMessage = generateSentMessage($this->message); +}); + +it('return null without running middleware if disabled', function () { + Config::set('mail-allowlist.enabled', false); + + $loggerMock = Mockery::mock(SentLogMessage::class); + $loggerMock->shouldNotReceive('log'); + + $event = new MessageSent($this->sentMessage); + $listener = new MessageSentListener($loggerMock); + + $mock = Mockery::mock(Pipeline::class); + $mock->shouldNotReceive('send'); + $this->instance('pipeline', $mock); + + $listener->handle($event); +}); + +it('runs the middleware pipelines and returns if the message should be sent', function (bool $shouldLog) { + Config::set('mail-allowlist.enabled', true); + Config::set('mail-allowlist.sent.log.enabled', $shouldLog); + Config::set('mail-allowlist.sent.middleware.enabled', true); + Config::set('mail-allowlist.sent.middleware.pipeline', $middleware = ['::middleware::']); + + $message = $this->message; + $sentMessage = $this->sentMessage; + + $loggerMock = Mockery::mock(SentLogMessage::class); + if ($shouldLog) { + $loggerMock->shouldReceive('log') + ->once() + ->with(Mockery::on(fn (SentMessageContext $context) => $context->getMessage() === $message)); + } else { + $loggerMock->shouldNotReceive('log'); + } + + $messageData = ['test_meta' => '::test_meta::']; + $event = new MessageSent($this->sentMessage, $messageData); + $listener = new MessageSentListener($loggerMock); + + $mock = Mockery::mock(Pipeline::class); + $mock->shouldReceive('send') + ->with(Mockery::on(function (SentMessageContext $messageContext) use ($message, $sentMessage, $messageData) { + return $message === $messageContext->getMessage() && + $sentMessage === $messageContext->getSentMessage() && + $messageData === $messageContext->getMessageData(); + })) + ->once() + ->andReturnSelf() + ->shouldReceive('through') + ->with($middleware) + ->once() + ->andReturnSelf() + ->shouldReceive('thenReturn') + ->once() + ->andReturnSelf(); + + $this->instance('pipeline', $mock); + + $listener->handle($event); +})->with([true, false]); + +it('does not run the middleware if disabled', function () { + Config::set('mail-allowlist.enabled', true); + Config::set('mail-allowlist.sent.middleware.enabled', false); + Config::set('mail-allowlist.sent.middleware.pipeline', ['::middleware::']); + + $loggerMock = Mockery::mock(SentLogMessage::class); + $loggerMock->shouldNotReceive('log'); + + $event = new MessageSent($this->sentMessage); + $listener = new MessageSentListener($loggerMock); + + $mock = Mockery::mock(Pipeline::class); + $mock->shouldNotReceive('send', 'through', 'andReturn'); + + $this->instance('pipeline', $mock); + + $listener->handle($event); +}); diff --git a/tests/MailMiddleware/Addresses/AddGlobalBccTest.php b/tests/MailMiddleware/Addresses/AddGlobalBccTest.php index 82a4e66..551ae02 100644 --- a/tests/MailMiddleware/Addresses/AddGlobalBccTest.php +++ b/tests/MailMiddleware/Addresses/AddGlobalBccTest.php @@ -7,7 +7,7 @@ use TobMoeller\LaravelMailAllowlist\MailMiddleware\MessageContext; it('adds global bcc addresses and continues the pipeline', function () { - Config::set('mail-allowlist.global.bcc', ['foo@bar.com', 'bar@foo.com']); + Config::set('mail-allowlist.sending.middleware.global.bcc', ['foo@bar.com', 'bar@foo.com']); $mail = new Email; $context = new MessageContext($mail); @@ -28,7 +28,7 @@ }); it('does not add an address if config is empty and continues the pipeline', function () { - Config::set('mail-allowlist.global.bcc', []); + Config::set('mail-allowlist.sending.middleware.global.bcc', []); $mail = new Email; $context = new MessageContext($mail); diff --git a/tests/MailMiddleware/Addresses/AddGlobalCcTest.php b/tests/MailMiddleware/Addresses/AddGlobalCcTest.php index d376ee8..c7981c4 100644 --- a/tests/MailMiddleware/Addresses/AddGlobalCcTest.php +++ b/tests/MailMiddleware/Addresses/AddGlobalCcTest.php @@ -7,7 +7,7 @@ use TobMoeller\LaravelMailAllowlist\MailMiddleware\MessageContext; it('adds global cc addresses and continues the pipeline', function () { - Config::set('mail-allowlist.global.cc', ['foo@bar.com', 'bar@foo.com']); + Config::set('mail-allowlist.sending.middleware.global.cc', ['foo@bar.com', 'bar@foo.com']); $mail = new Email; $context = new MessageContext($mail); @@ -28,7 +28,7 @@ }); it('does not add an address if config is empty and continues the pipeline', function () { - Config::set('mail-allowlist.global.cc', []); + Config::set('mail-allowlist.sending.middleware.global.cc', []); $mail = new Email; $context = new MessageContext($mail); diff --git a/tests/MailMiddleware/Addresses/AddGlobalToTest.php b/tests/MailMiddleware/Addresses/AddGlobalToTest.php index 71fc5bb..5a17466 100644 --- a/tests/MailMiddleware/Addresses/AddGlobalToTest.php +++ b/tests/MailMiddleware/Addresses/AddGlobalToTest.php @@ -7,7 +7,7 @@ use TobMoeller\LaravelMailAllowlist\MailMiddleware\MessageContext; it('adds global to addresses and continues the pipeline', function () { - Config::set('mail-allowlist.global.to', ['foo@bar.com', 'bar@foo.com']); + Config::set('mail-allowlist.sending.middleware.global.to', ['foo@bar.com', 'bar@foo.com']); $mail = new Email; $context = new MessageContext($mail); @@ -28,7 +28,7 @@ }); it('does not add an address if config is empty and continues the pipeline', function () { - Config::set('mail-allowlist.global.to', []); + Config::set('mail-allowlist.sending.middleware.global.to', []); $mail = new Email; $context = new MessageContext($mail); diff --git a/tests/MailMiddleware/Addresses/AddressFilterTest.php b/tests/MailMiddleware/Addresses/AddressFilterTest.php index 4c42505..2d3cdef 100644 --- a/tests/MailMiddleware/Addresses/AddressFilterTest.php +++ b/tests/MailMiddleware/Addresses/AddressFilterTest.php @@ -10,8 +10,8 @@ use TobMoeller\LaravelMailAllowlist\MailMiddleware\MessageContext; it('filters a mail with address list headers', function (Header $header) { - Config::set('mail-allowlist.allowed.domains', ['foo.de', 'bar.de']); - Config::set('mail-allowlist.allowed.emails', ['bar@foo.com', 'foo@bar.com']); + Config::set('mail-allowlist.sending.middleware.allowed.domains', ['foo.de', 'bar.de']); + Config::set('mail-allowlist.sending.middleware.allowed.emails', ['bar@foo.com', 'foo@bar.com']); $allowed = [ new Address('allowed@foo.de'), @@ -45,8 +45,8 @@ })->with(Header::addressListHeaders()); it('filters a mail with address headers', function (Header $header) { - Config::set('mail-allowlist.allowed.domains', ['foo.de']); - Config::set('mail-allowlist.allowed.emails', ['bar@foo.com']); + Config::set('mail-allowlist.sending.middleware.allowed.domains', ['foo.de']); + Config::set('mail-allowlist.sending.middleware.allowed.emails', ['bar@foo.com']); $allowedDomainAddress = new Address('allowed@foo.de'); $allowedEmailAddress = new Address('bar@foo.com'); @@ -87,8 +87,8 @@ })->with(Header::addressHeaders()); it('removes address list headers with no allowed lists', function (Header $header) { - Config::set('mail-allowlist.allowed.domains', []); - Config::set('mail-allowlist.allowed.emails', []); + Config::set('mail-allowlist.sending.middleware.allowed.domains', []); + Config::set('mail-allowlist.sending.middleware.allowed.emails', []); $mail = new Email; $mail->getHeaders()->addMailboxListHeader($header->value, [new Address('foo@bar.com')]); @@ -101,8 +101,8 @@ })->with(Header::addressListHeaders()); it('removes address headers with no allowed lists', function (Header $header) { - Config::set('mail-allowlist.allowed.domains', []); - Config::set('mail-allowlist.allowed.emails', []); + Config::set('mail-allowlist.sending.middleware.allowed.domains', []); + Config::set('mail-allowlist.sending.middleware.allowed.emails', []); $mail = new Email; $mail->getHeaders()->addMailboxHeader($header->value, new Address('foo@bar.com')); diff --git a/tests/MailSentMiddleware/SentMessageContextTest.php b/tests/MailSentMiddleware/SentMessageContextTest.php new file mode 100644 index 0000000..44a846b --- /dev/null +++ b/tests/MailSentMiddleware/SentMessageContextTest.php @@ -0,0 +1,69 @@ +message = new Email; + $this->sentMessage = generateSentMessage($this->message); + $this->context = new SentMessageContext($this->sentMessage); +}); + +it('holds message information', function () { + + expect($this->context->getMessage()) + ->toBe($this->message) + ->and($this->context->getSentMessage()) + ->toBe($this->sentMessage) + ->and($this->context->getMessageData()) + ->toBeEmpty(); + + $this->context->addLog('::log1::'); + $this->context->addLog('::log2::'); + + expect($this->context->getDebugInformation()) + ->toBe('::debug::') + ->and($this->context->getLog()) + ->toMatchArray([ + '::log1::', + '::log2::', + ]); +}); + +it('holds message data and returns the originating class name', function (array $className, mixed $expectation) { + $messageData = array_merge([ + 'test_meta' => '::test_meta::', + ], $className); + $this->context = new SentMessageContext($this->sentMessage, $messageData); + + expect($this->context->getMessageData()) + ->toBe($messageData) + ->and($this->context->getOriginatingClassName()) + ->toBe($expectation); +})->with([ + [ + 'className' => ['__laravel_notification' => '::notification_name::'], + 'expectation' => '::notification_name::', + ], + [ + 'className' => ['__laravel_mailable' => '::mailable_name::'], + 'expectation' => '::mailable_name::', + ], + [ + 'className' => ['__laravel_notification' => false], + 'expectation' => null, + ], + [ + 'className' => [], + 'expectation' => null, + ], +]); + +it('shares data for middleware', function () { + $this->context->sharedData->put('foo', 'bar'); + + expect($this->context->sharedData) + ->toBeInstanceOf(Collection::class) + ->get('foo')->toBe('bar'); +}); diff --git a/tests/Pest.php b/tests/Pest.php index c3a7507..61af143 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,5 +1,32 @@ in(__DIR__); + +function generateSentMessage( + Email|RawMessage|null $message = null, + Address|string|null $sender = null, + Address|string|null $to = null, + ?string $text = null, + ?string $debug = null, +): SentMessage { + $sender ??= new Address('sender@test.de'); + $to ??= new Address('to@test.de'); + + $message ??= new Email; + if ($message instanceof Email) { + $message->text($text ?? '::text::')->to($to)->sender($sender); + } + $envelope = new Envelope($sender, [$to]); + $symfonySentMessage = new SymfonySentMessage($message, $envelope); + $symfonySentMessage->appendDebug($debug ?? '::debug::'); + + return new SentMessage($symfonySentMessage); +};