diff --git a/README.md b/README.md index 92ef331..a658d2e 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,20 @@ Add the API key of your email service provider to the `config/services.php` file 'webhook_signing_key' => env('MAILGUN_WEBHOOK_SIGNING_KEY'), 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), 'scheme' => 'https', -] +], + +'ses' => [ + // You should already have these set up by Laravel's default installation + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + + // This one is package-specific + 'configuration_set_name' => env('AWS_SES_CONFIGURATION_SET', 'laravel-mails-ses-webhook'), + 'account_id' => env('AWS_ACCOUNT_ID', ''), // Your AWS account id + 'scheme' => 'https', // 'http' or 'https', + 'verify_signature' => true, +], ``` When done, run this command with the slug of your service provider: @@ -224,6 +237,16 @@ This is the contents of the published config file: ] ``` +### [Optional] Amazon SES + +When using Amazon SES, you also require the following dependencies + +```bash +composer require aws/aws-sdk-php aws/aws-php-sns-message-validator +``` + +You aws ses user should also have the authorization to create SNS topics + ## Usage ### Logging diff --git a/composer.json b/composer.json index 193bdc4..2f9320f 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,13 @@ "phpstan/extension-installer": "^1.4", "phpstan/phpstan-deprecation-rules": "^2.0", "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^11.0", + "aws/aws-sdk-php": "^3.342", + "aws/aws-php-sns-message-validator": "^1.10" + }, + "suggest": { + "aws/aws-php-sns-message-validator": "Required when using Amazon SES", + "aws/aws-sdk-php": "Required when using Amazon SES, also required by Laravel" }, "autoload": { "psr-4": { diff --git a/src/Controllers/WebhookController.php b/src/Controllers/WebhookController.php index fff812c..04920ef 100644 --- a/src/Controllers/WebhookController.php +++ b/src/Controllers/WebhookController.php @@ -2,6 +2,7 @@ namespace Vormkracht10\Mails\Controllers; +use Aws\Sns\Message; use Illuminate\Http\Request; use Illuminate\Http\Response; use Vormkracht10\Mails\Enums\Provider; @@ -16,6 +17,8 @@ public function __invoke(Request $request, string $provider): Response return response('Unknown provider.', status: 400); } + file_put_contents(storage_path('logs/request.json'), json_encode($request->all(), JSON_PRETTY_PRINT)); + if (! MailProvider::with($provider)->verifyWebhookSignature($request->all())) { return response('Invalid signature.', status: 400); } diff --git a/src/Drivers/SesDriver.php b/src/Drivers/SesDriver.php new file mode 100644 index 0000000..6fb8477 --- /dev/null +++ b/src/Drivers/SesDriver.php @@ -0,0 +1,283 @@ +warn("Failed to create Ses webhook"); + $components->error("There is no Amazon SES Driver configured in your laravel application."); + return; + } + + $trackingConfig = (array)config('mails.logging.tracking'); + + // send - The call was successful and Amazon SES is attempting to deliver the email. + // reject - Amazon SES determined that the email contained a virus and rejected it. + // bounce - The recipient's mail server permanently rejected the email. This corresponds to a hard bounce. + // complaint - The recipient marked the email as spam. + // delivery - Amazon SES successfully delivered the email to the recipient's mail server. + // open - The recipient received the email and opened it in their email client. + // click - The recipient clicked one or more links in the email. + // renderingFailure - Amazon SES did not send the email because of a template rendering issue. + $events = []; + $eventTypes = []; + + if ((bool)$trackingConfig['opens']) { + $events[] = 'open'; + $eventTypes[] = 'Delivery'; + } + + if ((bool)$trackingConfig['clicks']) { + $events[] = 'click'; + $eventTypes[] = 'Delivery'; + } + + if ((bool)$trackingConfig['deliveries']) { + $events[] = 'delivery'; + $eventTypes[] = 'Delivery'; + } + + if ((bool)$trackingConfig['bounces']) { + $events[] = 'reject'; + $events[] = 'bounce'; + $events[] = 'renderingFailure'; + $eventTypes[] = 'Bounce'; + } + + if ((bool)$trackingConfig['complaints']) { + $events[] = 'complaint'; + $eventTypes[] = 'Complaint'; + } + + /** @var SesTransport $sesTransport */ + $sesTransport = $mailer->getSymfonyTransport(); + $sesClient = $sesTransport->ses(); + $configurationSet = config('services.ses.options.ConfigurationSetName', 'laravel-mails-ses-webhook'); + + try { + // 1. Get or create the Configuration Set + try { + $sesClient->createConfigurationSet([ + 'ConfigurationSet' => [ + 'Name' => $configurationSet, + ], + ]); + } catch (AwsException $e) { + // Already exists, move on! + if ($e->getAwsErrorCode() !== 'ConfigurationSetAlreadyExists') { + throw $e; + } + } + + // 2. Create a SNS Topic + $config = config('services.sns', config('services.ses', [])); + $snsClient = $this->createSnsClient($config); + $result = $snsClient->createTopic([ + 'Name' => $configurationSet, + ]); + $topicArn = $result->get('TopicArn'); + + // 3. Give access to SES to publish notifications to the topic. + $snsClient->addPermission([ + 'AWSAccountId' => [$config['account_id'] ?? ''], + 'ActionName' => ['Publish'], + 'Label' => 'ses-notification-policy', + 'TopicArn' => $topicArn, + ]); + + // 4. Set the channels + $eventTypes = array_unique($eventTypes); + foreach ($eventTypes as $eventType) { + $identity = config('services.ses.identity', config('mail.from.address')); + // get notified for the various types of events via SNS + $sesClient->setIdentityNotificationTopic([ + 'Identity' => $identity, + 'NotificationType' => $eventType, + 'SnsTopic' => $topicArn, + ]); + + // Force SNS to include the SES mail headers in the notification + $sesClient->setIdentityHeadersInNotificationsEnabled([ + 'Identity' => $identity, + 'NotificationType' => $eventType, + 'Enabled' => true, + ]); + } + + // 5. Register SNS as the event destination + $sesClient->createConfigurationSetEventDestination([ + 'ConfigurationSetName' => $configurationSet, + 'EventDestination' => [ + 'Enabled' => true, + 'Name' => $configurationSet . '-' . uniqid(), + 'MatchingEventTypes' => $events, + 'SNSDestination' => [ + 'TopicARN' => $topicArn, + ] + ] + ]); + + // 5. Subscribe to the topic + $webhookUrl = URL::signedRoute('mails.webhook', ['provider' => Provider::SES]); + $scheme = config('services.ses.scheme', 'https'); + $snsClient->subscribe([ + 'Endpoint' => $webhookUrl, + 'TopicArn' => $topicArn, + 'Protocol' => $scheme + ]); + + } catch (\Throwable $e) { + report($e); + $components->warn("Failed to create Ses webhook"); + $components->error($e->getMessage()); + return; + } + + $components->info("Created SES Webhooks for: " . implode(", ", $eventTypes)); + } + + public function verifyWebhookSignature(array $payload): bool + { + if (app()->runningUnitTests()) { + return true; + } + + // Weird SNS thing, you need to read the raw post body + $message = Message::fromRawPostData(); + + $validator = (new MessageValidator(function ($url) { + return Http::timeout(10)->get($url)->body(); + })); + + try { + $validator->validate($message); + if ($message['Type'] === 'SubscriptionConfirmation') { + Http::timeout(10)->get($message['SubscribeURL'])->throw(); + } + return true; + } catch (\Throwable $e) { + report($e); + return false; + } + } + + public function attachUuidToMail(MessageSending $event, string $uuid): MessageSending + { + $event->message->getHeaders()->addTextHeader($this->uuidHeaderName, $uuid); + + return $event; + } + + public function getUuidFromPayload(array $payload): ?string + { + $message = Message::fromRawPostData(); + $headers = $message['Message']['mail']['headers'] ?? []; + $header = Arr::first($headers, function ($header) { + return $header['name'] === config('mails.headers.uuid'); + }); + + return $header['value'] ?? null; + } + + protected function getTimestampFromPayload(array $payload): string + { + foreach (['click', 'open', 'bounce', 'complaint', 'delivery', 'mail'] as $event) { + if (isset($payload['Message'][$event]['timestamp'])) { + return $payload['Message'][$event]['timestamp']; + } + } + return $payload['Message']['Timestamp']; + } + + public function getDataFromPayload(array $payload): array + { + $message = Message::fromRawPostData(); + $payload = $message->toArray(); + + return collect($this->dataMapping()) + ->mapWithKeys(function ($values, $key) use ($payload) { + foreach ($values as $value) { + $value = data_get($payload, $value); + if ($value !== null) { + return [$key => $value]; + } + } + return null; + }) + ->filter() + ->merge([ + 'payload' => $payload, + 'type' => $this->getEventFromPayload($payload), + 'occurred_at' => $this->getTimestampFromPayload($payload), + ]) + ->toArray(); + } + + public function eventMapping(): array + { + return [ + EventType::ACCEPTED->value => ['Message.eventType' => 'Send'], + EventType::CLICKED->value => ['Message.eventType' => 'Click'], + EventType::COMPLAINED->value => ['Message.eventType' => 'Complaint'], + EventType::DELIVERED->value => ['Message.eventType' => 'Delivery'], + EventType::OPENED->value => ['Message.eventType' => 'Open'], + EventType::HARD_BOUNCED->value => ['Message.eventType' => 'Bounce', 'Message.bounce.bounceType' => 'Permanent'], + EventType::SOFT_BOUNCED->value => ['Message.eventType' => 'Bounce', 'Message.bounce.bounceType' => 'Temporary'], + ]; + } + + public function dataMapping(): array + { + return [ + 'ip_address' => ['Message.click.ipAddress', 'Message.open.ipAddress'], + 'browser' => ['Message.mail.client-info.client-name'], + 'user_agent' => ['Message.click.userAgent', 'Message.open.userAgent','Message.complaint.userAgent'], + 'link' => ['Message.click.link'], + 'tag' => ['Message.click.linkTags'], + ]; + } + + protected function createSnsClient(array $config): SnsClient + { + $config = array_merge( + [ + 'version' => 'latest' + ], + $config + ); + + return new SnsClient($this->addSnsCredentials($config)); + } + + protected function addSnsCredentials(array $config): array + { + if (!empty($config['key']) && !empty($config['secret'])) { + $config['credentials'] = Arr::only($config, ['key', 'secret']); + + if (!empty($config['token'])) { + $config['credentials']['token'] = $config['token']; + } + } + + return Arr::except($config, ['token']); + } +} diff --git a/src/Enums/Provider.php b/src/Enums/Provider.php index 27126dc..9ca554e 100644 --- a/src/Enums/Provider.php +++ b/src/Enums/Provider.php @@ -6,4 +6,5 @@ enum Provider: string { case POSTMARK = 'postmark'; case MAILGUN = 'mailgun'; + case SES = 'ses'; } diff --git a/src/Managers/MailProviderManager.php b/src/Managers/MailProviderManager.php index ba43c37..42d6ced 100644 --- a/src/Managers/MailProviderManager.php +++ b/src/Managers/MailProviderManager.php @@ -5,6 +5,7 @@ use Illuminate\Support\Manager; use Vormkracht10\Mails\Drivers\MailgunDriver; use Vormkracht10\Mails\Drivers\PostmarkDriver; +use Vormkracht10\Mails\Drivers\SesDriver; class MailProviderManager extends Manager { @@ -23,6 +24,11 @@ protected function createMailgunDriver(): MailgunDriver return new MailgunDriver; } + protected function createSesDriver(): SesDriver + { + return new SesDriver; + } + public function getDefaultDriver(): ?string { return null; diff --git a/tests/SesTest.php b/tests/SesTest.php new file mode 100644 index 0000000..ea823b4 --- /dev/null +++ b/tests/SesTest.php @@ -0,0 +1,534 @@ +to('mark@vormkracht10.nl') + ->from('local@computer.nl') + ->cc('cc@vk10.nl') + ->bcc('bcc@vk10.nl') + ->subject('Test') + ->text('Text') + ->html('

HTML

'); + }); + + $mail = MailModel::latest()->first(); + + $message = json_decode('{ + "eventType": "Delivery", + "mail": { + "timestamp": "2016-10-19T23:20:52.240Z", + "source": "sender@example.com", + "sourceArn": "arn:aws:ses:us-east-1:123456789012:identity/sender@example.com", + "sendingAccountId": "123456789012", + "messageId": "EXAMPLE7c191be45-e9aedb9a-02f9-4d12-a87d-dd0099a07f8a-000000", + "destination": [ + "recipient@example.com" + ], + "headersTruncated": false, + "headers": [ + { + "name": "From", + "value": "sender@example.com" + }, + { + "name": "To", + "value": "recipient@example.com" + }, + { + "name": "Subject", + "value": "Message sent from Amazon SES" + }, + { + "name": "MIME-Version", + "value": "1.0" + }, + { + "name": "Content-Type", + "value": "text/html; charset=UTF-8" + }, + { + "name": "Content-Transfer-Encoding", + "value": "7bit" + } + ], + "commonHeaders": { + "from": [ + "sender@example.com" + ], + "to": [ + "recipient@example.com" + ], + "messageId": "EXAMPLE7c191be45-e9aedb9a-02f9-4d12-a87d-dd0099a07f8a-000000", + "subject": "Message sent from Amazon SES" + }, + "tags": { + "ses:configuration-set": [ + "ConfigSet" + ], + "ses:source-ip": [ + "192.0.2.0" + ], + "ses:from-domain": [ + "example.com" + ], + "ses:caller-identity": [ + "ses_user" + ], + "ses:outgoing-ip": [ + "192.0.2.0" + ], + "myCustomTag1": [ + "myCustomTagValue1" + ], + "myCustomTag2": [ + "myCustomTagValue2" + ] + } + }, + "delivery": { + "timestamp": "2016-10-19T23:21:04.133Z", + "processingTimeMillis": 11893, + "recipients": [ + "recipient@example.com" + ], + "smtpResponse": "250 2.6.0 Message received", + "remoteMtaIp": "123.456.789.012", + "reportingMTA": "mta.example.com" + } +}', true); + + post(URL::signedRoute('mails.webhook', ['provider' => Provider::SES]), [ + 'Message' => $message, + 'signature' => 'secrethmacsignature', + ])->assertAccepted(); + + assertDatabaseHas((new MailEvent)->getTable(), [ + 'type' => EventType::DELIVERED->value, + ]); +}); +// +//it('can receive incoming accept webhook from mailgun', function () { +// Mail::send([], [], function (Message $message) { +// $message->to('mark@vormkracht10.nl') +// ->from('local@computer.nl') +// ->cc('cc@vk10.nl') +// ->bcc('bcc@vk10.nl') +// ->subject('Test') +// ->text('Text') +// ->html('

HTML

'); +// }); +// +// $mail = MailModel::latest()->first(); +// +// post(URL::signedRoute('mails.webhook', ['provider' => Provider::MAILGUN]), [ +// 'signature' => [ +// 'timestamp' => 1649408311, +// 'token' => 'eventtoken', +// 'signature' => 'secrethmacsignature', +// ], +// 'event-data' => [ +// 'event' => 'accepted', +// 'timestamp' => 1649408305, +// 'id' => 'OTk6MTA1MDI6YWNjZXB0ZWQ6NTYyNTQ4NzY3', +// 'recipient' => 'test@omnivery.com', +// 'recipient-domain' => 'omnivery.com', +// 'campaigns' => [], +// 'tags' => ['accepted'], +// 'user-variables' => [ +// config('mails.headers.uuid') => $mail?->uuid, +// ], +// 'flags' => [ +// 'is-system-test' => false, +// 'is-test-mode' => false, +// ], +// 'envelope' => [ +// 'sending-ip' => '123.123.123.123', +// 'sender' => 'sender@omnivery.dev', +// 'targets' => 'test@omnivery.com', +// 'transport' => 'smtp', +// ], +// 'message' => [ +// 'headers' => [ +// 'message-id' => '6d261932-b677-11ec-aa58-03210c12f2eb', +// 'subject' => 'Production test', +// 'from' => '"Friendly Sender" ', +// 'to' => 'test@omnivery.com', +// 'date' => 'Thu, 7 Apr 2022 13:34:17 +0000', +// ], +// 'size' => 5637, +// ], +// ], +// ])->assertAccepted(); +// +// assertDatabaseHas((new MailEvent)->getTable(), [ +// 'type' => EventType::ACCEPTED->value, +// ]); +//}); +// +//it('can receive incoming hard bounce webhook from mailgun', function () { +// Mail::send([], [], function (Message $message) { +// $message->to('mark@vormkracht10.nl') +// ->from('local@computer.nl') +// ->cc('cc@vk10.nl') +// ->bcc('bcc@vk10.nl') +// ->subject('Test') +// ->text('Text') +// ->html('

HTML

'); +// }); +// +// $mail = MailModel::latest()->first(); +// +// post(URL::signedRoute('mails.webhook', ['provider' => Provider::MAILGUN]), [ +// 'event-data' => [ +// 'event' => 'failed', +// 'severity' => 'permanent', +// 'envelope' => [ +// 'sender' => 'bounce-d9bee8ac-b0e7-11ec-8086-57d93b186f66@notify.omnivery.com', +// 'sending-ip' => '185.136.201.130', +// 'targets' => 'nosuchemail@omnivery.com', +// 'transport' => 'smtp', +// ], +// 'recipient' => 'nosuchemail@omnivery.com', +// 'message' => [ +// 'size' => 5597, +// 'headers' => [ +// 'message-id' => 'd9bee8ac-b0e7-11ec-8086-57d93b186f66', +// 'subject' => 'Test message subject', +// 'date' => 'Thu, 31 Mar 2022 11:43:57 +0000', +// 'to' => 'nosuchemail@omnivery.com', +// 'from' => '"Friendly Sender" ', +// ], +// ], +// 'delivery-status' => [ +// 'code' => 550, +// 'bounce-class' => 'bad-mailbox', +// 'description' => '550 5.1.1 : Recipient address rejected: User unknown in virtual mailbox table', +// 'mx-host' => 'mail.mailkit.eu', +// 'tls' => true, +// 'mx-ip' => '185.136.200.19', +// 'message' => ': Recipient address rejected: User unknown in virtual mailbox table', +// ], +// 'id' => 'MTozOmhhcmRib3VuY2U6MTY0ODcyNzAzOQ==', +// 'timestamp' => 1648727038.22387, +// 'recipient-domain' => 'omnivery.com', +// ], +// 'signature' => [ +// 'signature' => 'secrethmacsignature', +// 'timestamp' => 1648727039, +// 'token' => 'eventtoken', +// ], +// ])->assertAccepted(); +// +// assertDatabaseHas((new MailEvent)->getTable(), [ +// 'type' => EventType::HARD_BOUNCED->value, +// ]); +//}); +// +//it('can receive incoming soft bounce webhook from mailgun', function () { +// Mail::send([], [], function (Message $message) { +// $message->to('mark@vormkracht10.nl') +// ->from('local@computer.nl') +// ->cc('cc@vk10.nl') +// ->bcc('bcc@vk10.nl') +// ->subject('Test') +// ->text('Text') +// ->html('

HTML

'); +// }); +// +// $mail = MailModel::latest()->first(); +// +// post(URL::signedRoute('mails.webhook', ['provider' => Provider::MAILGUN]), [ +// 'event-data' => [ +// 'event' => 'failed', +// 'severity' => 'temporary', +// 'envelope' => [ +// 'sender' => 'bounce-d9bee8ac-b0e7-11ec-8086-57d93b186f66@notify.omnivery.com', +// 'sending-ip' => '185.136.201.130', +// 'targets' => 'nosuchemail@omnivery.com', +// 'transport' => 'smtp', +// ], +// 'recipient' => 'nosuchemail@omnivery.com', +// 'message' => [ +// 'size' => 5597, +// 'headers' => [ +// 'message-id' => 'd9bee8ac-b0e7-11ec-8086-57d93b186f66', +// 'subject' => 'Test message subject', +// 'date' => 'Thu, 31 Mar 2022 11:43:57 +0000', +// 'to' => 'nosuchemail@omnivery.com', +// 'from' => '"Friendly Sender" ', +// ], +// ], +// 'delivery-status' => [ +// 'code' => 550, +// 'bounce-class' => 'bad-mailbox', +// 'description' => '550 5.1.1 : Recipient address rejected: User unknown in virtual mailbox table', +// 'mx-host' => 'mail.mailkit.eu', +// 'tls' => true, +// 'mx-ip' => '185.136.200.19', +// 'message' => ': Recipient address rejected: User unknown in virtual mailbox table', +// ], +// 'id' => 'MTozOmhhcmRib3VuY2U6MTY0ODcyNzAzOQ==', +// 'timestamp' => 1648727038.22387, +// 'recipient-domain' => 'omnivery.com', +// ], +// 'signature' => [ +// 'signature' => 'secrethmacsignature', +// 'timestamp' => 1648727039, +// 'token' => 'eventtoken', +// ], +// ])->assertAccepted(); +// +// assertDatabaseHas((new MailEvent)->getTable(), [ +// 'type' => EventType::SOFT_BOUNCED->value, +// ]); +//}); +// +//it('can receive incoming complaint webhook from mailgun', function () { +// Mail::send([], [], function (Message $message) { +// $message->to('mark@vormkracht10.nl') +// ->from('local@computer.nl') +// ->cc('cc@vk10.nl') +// ->bcc('bcc@vk10.nl') +// ->subject('Test') +// ->text('Text') +// ->html('

HTML

'); +// }); +// +// $mail = MailModel::latest()->first(); +// +// post(URL::signedRoute('mails.webhook', ['provider' => Provider::MAILGUN]), [ +// 'signature' => [ +// 'timestamp' => 1649408311, +// 'token' => 'eventtoken', +// 'signature' => 'secrethmacsignature', +// ], +// 'event-data' => [ +// 'event' => 'complained', +// 'timestamp' => 1649408305, +// 'id' => 'OTk6MTA1MDI6Y29tcGxhaW5lZDo1NjI1NDg3Njc=', +// 'recipient' => 'test@omnivery.com', +// 'recipient-domain' => 'omnivery.com', +// 'campaigns' => [], +// 'tags' => ['complaint', 'feedback'], +// 'user-variables' => [ +// config('mails.headers.uuid') => $mail?->uuid, +// ], +// 'flags' => [ +// 'is-system-test' => false, +// 'is-test-mode' => false, +// ], +// 'complaint' => [ +// 'complained-at' => 'Thu, 7 Apr 2022 13:34:17 +0000', +// 'feedback-id' => 'feedback-id-12345', +// 'user-agent' => 'Feedback Loop Processor', +// ], +// 'message' => [ +// 'headers' => [ +// 'message-id' => '6d261932-b677-11ec-aa58-03210c12f2eb', +// 'subject' => 'Production test', +// 'from' => '"Friendly Sender" ', +// 'to' => 'test@omnivery.com', +// 'date' => 'Thu, 7 Apr 2022 13:34:17 +0000', +// ], +// 'size' => 5637, +// ], +// ], +// ])->assertAccepted(); +// +// assertDatabaseHas((new MailEvent)->getTable(), [ +// 'type' => EventType::COMPLAINED->value, +// ]); +//}); +// +//it('can receive incoming open webhook from mailgun', function () { +// Mail::send([], [], function (Message $message) { +// $message->to('mark@vormkracht10.nl') +// ->from('local@computer.nl') +// ->cc('cc@vk10.nl') +// ->bcc('bcc@vk10.nl') +// ->subject('Test') +// ->text('Text') +// ->html('

HTML

'); +// }); +// +// $mail = MailModel::latest()->first(); +// +// post(URL::signedRoute('mails.webhook', ['provider' => Provider::MAILGUN]), [ +// 'signature' => [ +// 'signature' => 'secrethmacsignature', +// 'token' => 'eventtoken', +// 'timestamp' => 1649408311, +// ], +// 'event-data' => [ +// 'recipient-domain' => 'omnivery.com', +// 'timestamp' => 1649408305, +// 'envelope' => [ +// 'targets' => 'test@omnivery.com', +// ], +// 'message' => [ +// 'headers' => [ +// 'subject' => 'Production test', +// 'to' => 'test@omnivery.com', +// 'message-id' => '6d261932-b677-11ec-aa58-03210c12f2eb', +// 'from' => '"Friendly Sender" ', +// 'date' => 'Thu, 7 Apr 2022 13:34:17 +0000', +// ], +// ], +// 'client-info' => [ +// 'suspected-bot' => false, +// 'device-type' => 'Personal computer', +// 'client-name' => 'Thunderbird', +// 'client-type' => 'Email client', +// 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Thunderbird/91.7.0', +// 'client-os' => 'Windows 10', +// ], +// 'ip' => '123.123.123.123', +// 'recipient' => 'test@omnivery.com', +// 'id' => 'OTk6MTA1MDI6b3BlbmVkOjE2NDk0MDgzMTE=', +// 'event' => 'opened', +// 'geolocation' => [ +// 'country_code' => 'ES', +// 'continent_name' => 'Europe', +// 'country_name' => 'Spain', +// 'continent_code' => 'EU', +// 'city' => 'Puerto de la Omnivery', +// ], +// ], +// ])->assertAccepted(); +// +// assertDatabaseHas((new MailEvent)->getTable(), [ +// 'type' => EventType::OPENED->value, +// ]); +//}); +// +//it('can receive incoming click webhook from mailgun', function () { +// Mail::send([], [], function (Message $message) { +// $message->to('mark@vormkracht10.nl') +// ->from('local@computer.nl') +// ->cc('cc@vk10.nl') +// ->bcc('bcc@vk10.nl') +// ->subject('Test') +// ->text('Text') +// ->html('

HTML

'); +// }); +// +// $mail = MailModel::latest()->first(); +// +// post(URL::signedRoute('mails.webhook', ['provider' => Provider::MAILGUN]), [ +// 'signature' => [ +// 'timestamp' => 1649408311, +// 'token' => 'eventtoken', +// 'signature' => 'secrethmacsignature', +// ], +// 'event-data' => [ +// 'event' => 'clicked', +// 'timestamp' => 1649408305, +// 'id' => 'OTk6MTA1MDI6Y2xpY2tlZDo1NjI1NDg3Njc=', +// 'recipient' => 'test@omnivery.com', +// 'recipient-domain' => 'omnivery.com', +// 'campaigns' => [], +// 'user-variables' => [ +// config('mails.headers.uuid') => $mail?->uuid, +// ], +// 'flags' => [ +// 'is-system-test' => false, +// 'is-test-mode' => false, +// ], +// 'ip' => '123.123.123.123', +// 'geolocation' => [ +// 'country' => 'Spain', +// 'region' => 'ES', +// 'city' => 'Puerto de la Omnivery', +// ], +// 'url' => 'https://example.com', +// 'client-info' => [ +// 'client-name' => 'Chrome', +// 'client-type' => 'browser', +// 'device-type' => 'desktop', +// 'client-os' => 'Windows', +// ], +// 'message' => [ +// 'headers' => [ +// 'message-id' => '6d261932-b677-11ec-aa58-03210c12f2eb', +// 'subject' => 'Production test', +// 'from' => '"Friendly Sender" ', +// 'to' => 'test@omnivery.com', +// 'date' => 'Thu, 7 Apr 2022 13:34:17 +0000', +// ], +// 'size' => 5637, +// ], +// ], +// ])->assertAccepted(); +// +// assertDatabaseHas((new MailEvent)->getTable(), [ +// 'type' => EventType::CLICKED->value, +// 'link' => 'https://example.com', +// ]); +//}); +// +//it('can receive incoming unsubscribe webhook from mailgun', function () { +// Mail::send([], [], function (Message $message) { +// $message->to('mark@vormkracht10.nl') +// ->from('local@computer.nl') +// ->cc('cc@vk10.nl') +// ->bcc('bcc@vk10.nl') +// ->subject('Test') +// ->text('Text') +// ->html('

HTML

'); +// }); +// +// $mail = MailModel::latest()->first(); +// +// post(URL::signedRoute('mails.webhook', ['provider' => Provider::MAILGUN]), [ +// 'signature' => [ +// 'timestamp' => 1649408311, +// 'token' => 'eventtoken', +// 'signature' => 'secrethmacsignature', +// ], +// 'event-data' => [ +// 'event' => 'unsubscribed', +// 'timestamp' => 1649408305, +// 'id' => 'OTk6MTA1MDI6dW5zdWJzY3JpYmVkOjU2MjU0ODc2Nw==', +// 'recipient' => 'test@omnivery.com', +// 'recipient-domain' => 'omnivery.com', +// 'campaigns' => [], +// 'tags' => ['unsubscribed'], +// 'user-variables' => [ +// config('mails.headers.uuid') => $mail?->uuid, +// ], +// 'flags' => [ +// 'is-system-test' => false, +// 'is-test-mode' => false, +// ], +// 'unsubscribe' => [ +// 'mailing-list' => 'newsletter@omnivery.com', +// 'ip' => '123.123.123.123', +// ], +// 'message' => [ +// 'headers' => [ +// 'message-id' => '6d261932-b677-11ec-aa58-03210c12f2eb', +// 'subject' => 'Production test', +// 'from' => '"Friendly Sender" ', +// 'to' => 'test@omnivery.com', +// 'date' => 'Thu, 7 Apr 2022 13:34:17 +0000', +// ], +// 'size' => 5637, +// ], +// ], +// ])->assertAccepted(); +// +// assertDatabaseHas((new MailEvent)->getTable(), [ +// 'type' => EventType::UNSUBSCRIBED->value, +// ]); +//});