From b20ca3b58109c25a6428004d028f11a234b9c78e Mon Sep 17 00:00:00 2001 From: Mark van Eijk Date: Fri, 6 Sep 2024 23:33:54 +0200 Subject: [PATCH] wip --- src/Contracts/MailDriverContract.php | 2 ++ src/Controllers/WebhookController.php | 5 ++++ src/Drivers/MailgunDriver.php | 15 ++++++++++ src/Drivers/PostmarkDriver.php | 9 ++++-- tests/PostmarkTest.php | 43 +++++++++++++++++++++++++++ 5 files changed, 72 insertions(+), 2 deletions(-) diff --git a/src/Contracts/MailDriverContract.php b/src/Contracts/MailDriverContract.php index be6d40e..56a98a9 100644 --- a/src/Contracts/MailDriverContract.php +++ b/src/Contracts/MailDriverContract.php @@ -8,6 +8,8 @@ interface MailDriverContract { public function registerWebhooks($components): void; + public function verifyWebhookSignature(array $payload): bool; + public function getUuidFromPayload(array $payload): ?string; public function getMailFromPayload(array $payload): ?Mail; diff --git a/src/Controllers/WebhookController.php b/src/Controllers/WebhookController.php index 9b10208..3a0c09c 100644 --- a/src/Controllers/WebhookController.php +++ b/src/Controllers/WebhookController.php @@ -6,6 +6,7 @@ use Illuminate\Http\Response; use Vormkracht10\Mails\Enums\Provider; use Vormkracht10\Mails\Events\MailEvent; +use Vormkracht10\Mails\Facades\MailProvider; class WebhookController { @@ -15,6 +16,10 @@ public function __invoke(Request $request, string $driver): Response return response('Unknown provider.', status: 400); } + if (MailProvider::with($driver)->verifyWebhookSignature($request->all())) { + return response('Invalid signature.', status: 400); + } + MailEvent::dispatch( $driver, $request->except('signature') ); diff --git a/src/Drivers/MailgunDriver.php b/src/Drivers/MailgunDriver.php index 8ee0502..4914405 100644 --- a/src/Drivers/MailgunDriver.php +++ b/src/Drivers/MailgunDriver.php @@ -9,6 +9,21 @@ class MailgunDriver extends MailDriver implements MailDriverContract { public function registerWebhooks($components): void {} + public function verifyWebhookSignature(array $payload): bool + { + if (empty($payload['signature']['timestamp']) || empty($payload['signature']['token']) || empty($payload['signature']['signature'])) { + return false; + } + + $hmac = hash_hmac('sha256', $payload['signature']['timestamp'].$payload['signature']['token'], config('services.mailgun.api_key')); + + if (function_exists('hash_equals')) { + return hash_equals($hmac, $payload['signature']['signature']); + } + + return $hmac === $payload['signature']['signature']; + } + public function getUuidFromPayload(array $payload): ?string { return $payload['event-data']['message']['headers'][$this->uuidHeaderName] ?? diff --git a/src/Drivers/PostmarkDriver.php b/src/Drivers/PostmarkDriver.php index 3d8b007..447e39a 100644 --- a/src/Drivers/PostmarkDriver.php +++ b/src/Drivers/PostmarkDriver.php @@ -48,6 +48,11 @@ public function registerWebhooks($components): void } } + public function verifyWebhookSignature(array $payload): bool + { + return true; + } + public function getUuidFromPayload(array $payload): ?string { return $payload['Metadata'][$this->uuidHeaderName] ?? @@ -65,9 +70,9 @@ public function eventMapping(): array { return [ EventType::CLICKED->value => ['RecordType' => 'Click'], - EventType::COMPLAINED->value => ['RecordType' => 'Complaint'], + EventType::COMPLAINED->value => ['RecordType' => 'SpamComplaint'], EventType::DELIVERED->value => ['RecordType' => 'Delivery'], - EventType::HARD_BOUNCED->value => ['Type' => 'Bounce', 'RecordType' => 'HardBounce'], + EventType::HARD_BOUNCED->value => ['RecordType' => 'Bounce', 'Type' => 'HardBounce'], EventType::OPENED->value => ['RecordType' => 'Open'], EventType::SOFT_BOUNCED->value => ['RecordType' => 'Bounce', 'Type' => 'SoftBounce'], ]; diff --git a/tests/PostmarkTest.php b/tests/PostmarkTest.php index 4db5e76..3c77feb 100644 --- a/tests/PostmarkTest.php +++ b/tests/PostmarkTest.php @@ -85,6 +85,49 @@ ]); }); +it('can receive incoming soft bounce webhook from postmark', 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' => 'postmark']), [ + 'Metadata' => [ + config('mails.headers.uuid') => $mail?->uuid, + ], + 'RecordType' => 'Bounce', + 'ID' => 42, + 'Type' => 'SoftBounce', + 'TypeCode' => 1, + 'Name' => 'Soft bounce', + 'Tag' => 'Test', + 'MessageID' => '00000000-0000-0000-0000-000000000000', + 'ServerID' => 1234, + 'MessageStream' => 'outbound', + 'Description' => 'The server was unable to deliver your message (ex => unknown user, mailbox not found).', + 'Details' => 'Test bounce details', + 'Email' => 'john@example.com', + 'From' => 'sender@example.com', + 'BouncedAt' => '2023-05-21T02:51:39Z', + 'DumpAvailable' => true, + 'Inactive' => true, + 'CanActivate' => true, + 'Subject' => 'Test subject', + 'Content' => 'Test content', + ])->assertAccepted(); + + assertDatabaseHas((new MailEvent)->getTable(), [ + 'type' => EventType::HARD_BOUNCED->value, + ]); +}); + it('can receive incoming complaint webhook from postmark', function () { Mail::send([], [], function (Message $message) { $message->to('mark@vormkracht10.nl')