diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index cf8bb8dfc5701..095d782aa12a3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -2589,6 +2589,7 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co if ($webhookEnabled) { $webhookRequestParsers = [ MailerBridge\Brevo\Webhook\BrevoRequestParser::class => 'mailer.webhook.request_parser.brevo', + MailerBridge\MailerSend\Webhook\MailerSendRequestParser::class => 'mailer.webhook.request_parser.mailersend', MailerBridge\Mailgun\Webhook\MailgunRequestParser::class => 'mailer.webhook.request_parser.mailgun', MailerBridge\Mailjet\Webhook\MailjetRequestParser::class => 'mailer.webhook.request_parser.mailjet', MailerBridge\Postmark\Webhook\PostmarkRequestParser::class => 'mailer.webhook.request_parser.postmark', diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_webhook.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_webhook.php index 5c1014713112b..f9d2b9686ff03 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_webhook.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_webhook.php @@ -13,6 +13,8 @@ use Symfony\Component\Mailer\Bridge\Brevo\RemoteEvent\BrevoPayloadConverter; use Symfony\Component\Mailer\Bridge\Brevo\Webhook\BrevoRequestParser; +use Symfony\Component\Mailer\Bridge\MailerSend\RemoteEvent\MailerSendPayloadConverter; +use Symfony\Component\Mailer\Bridge\MailerSend\Webhook\MailerSendRequestParser; use Symfony\Component\Mailer\Bridge\Mailgun\RemoteEvent\MailgunPayloadConverter; use Symfony\Component\Mailer\Bridge\Mailgun\Webhook\MailgunRequestParser; use Symfony\Component\Mailer\Bridge\Mailjet\RemoteEvent\MailjetPayloadConverter; @@ -31,6 +33,11 @@ ->args([service('mailer.payload_converter.brevo')]) ->alias(BrevoRequestParser::class, 'mailer.webhook.request_parser.brevo') + ->set('mailer.payload_converter.mailersend', MailerSendPayloadConverter::class) + ->set('mailer.webhook.request_parser.mailersend', MailerSendRequestParser::class) + ->args([service('mailer.payload_converter.mailersend')]) + ->alias(MailerSendRequestParser::class, 'mailer.webhook.request_parser.mailersend') + ->set('mailer.payload_converter.mailgun', MailgunPayloadConverter::class) ->set('mailer.webhook.request_parser.mailgun', MailgunRequestParser::class) ->args([service('mailer.payload_converter.mailgun')]) diff --git a/src/Symfony/Component/Mailer/Bridge/MailerSend/CHANGELOG.md b/src/Symfony/Component/Mailer/Bridge/MailerSend/CHANGELOG.md index 1f2c8f86cde72..45273cc83c340 100644 --- a/src/Symfony/Component/Mailer/Bridge/MailerSend/CHANGELOG.md +++ b/src/Symfony/Component/Mailer/Bridge/MailerSend/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + + * Add support for `RemoteEvent` and `Webhook` + 6.3 --- diff --git a/src/Symfony/Component/Mailer/Bridge/MailerSend/RemoteEvent/MailerSendPayloadConverter.php b/src/Symfony/Component/Mailer/Bridge/MailerSend/RemoteEvent/MailerSendPayloadConverter.php new file mode 100644 index 0000000000000..7ef67b1d5dacd --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MailerSend/RemoteEvent/MailerSendPayloadConverter.php @@ -0,0 +1,105 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\MailerSend\RemoteEvent; + +use Symfony\Component\RemoteEvent\Event\Mailer\AbstractMailerEvent; +use Symfony\Component\RemoteEvent\Event\Mailer\MailerDeliveryEvent; +use Symfony\Component\RemoteEvent\Event\Mailer\MailerEngagementEvent; +use Symfony\Component\RemoteEvent\Exception\ParseException; +use Symfony\Component\RemoteEvent\PayloadConverterInterface; + +/** + * @author WoutervanderLoop.nl + */ +final class MailerSendPayloadConverter implements PayloadConverterInterface +{ + public function convert(array $payload): AbstractMailerEvent + { + if (\in_array($payload['type'], ['activity.sent', 'activity.delivered', 'activity.soft_bounced', 'activity.hard_bounced'], true)) { + $name = match ($payload['type']) { + 'activity.sent' => MailerDeliveryEvent::RECEIVED, + 'activity.delivered' => MailerDeliveryEvent::DELIVERED, + 'activity.soft_bounced', 'activity.hard_bounced' => MailerDeliveryEvent::BOUNCE, + }; + $event = new MailerDeliveryEvent($name, $this->getMessageId($payload), $payload); + $event->setReason($this->getReason($payload)); + } else { + $name = match ($payload['type']) { + 'activity.clicked', 'activity.clicked_unique' => MailerEngagementEvent::CLICK, + 'activity.unsubscribed' => MailerEngagementEvent::UNSUBSCRIBE, + 'activity.opened', 'activity.opened_unique' => MailerEngagementEvent::OPEN, + 'activity.spam_complaint' => MailerEngagementEvent::SPAM, + default => throw new ParseException(sprintf('Unsupported event "%s".', $payload['type'])), + }; + $event = new MailerEngagementEvent($name, $this->getMessageId($payload), $payload); + } + + if (!$date = \DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.u\Z', $payload['created_at'])) { + throw new ParseException(sprintf('Invalid date "%s".', $payload['created_at'])); + } + + $event->setDate($date); + $event->setRecipientEmail($this->getRecipientEmail($payload)); + $event->setMetadata($this->getMetadata($payload)); + $event->setTags($this->getTags($payload)); + + return $event; + } + + private function getMessageId(array $payload): string + { + return $payload['data']['email']['message']['id']; + } + + private function getRecipientEmail(array $payload): string + { + return $payload['data']['email']['recipient']['email']; + } + + private function getReason(array $payload): string + { + if (isset($payload['data']['morph']['readable_reason'])) { + return $payload['data']['morph']['readable_reason']; + } + + if (isset($payload['data']['morph']['reason'])) { + return $payload['data']['morph']['reason']; + } + + return ''; + } + + private function getTags(array $payload): array + { + return $payload['data']['email']['tags'] ?? []; + } + + private function getMetadata(array $payload): array + { + $morphObject = $payload['data']['morph']['object'] ?? null; + + return match ($morphObject) { + 'open' => [ + 'ip' => $payload['data']['morph']['ip'] ?? null + ], + 'click' => [ + 'ip' => $payload['data']['morph']['ip'] ?? null, + 'url' => $payload['data']['morph']['url'] ?? null, + ], + 'recipient_unsubscribe' => [ + 'reason' => $payload['data']['morph']['reason'] ?? null, + 'readable_reason' => $payload['data']['morph']['readable_reason'] ?? null, + ], + default => [], + }; + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/clicked.json b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/clicked.json new file mode 100644 index 0000000000000..2857de9c0b64d --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/clicked.json @@ -0,0 +1,42 @@ +{ + "type": "activity.clicked", + "domain_id": "7z3m5jgrogdpyo6n", + "created_at": "2024-01-01T12:00:00.000000Z", + "webhook_id": "7z3m5jgrogdpyo6n", + "url": "https://www.mailersend.com/webhook", + "data": { + "object": "activity", + "id": "62f114f8165fe0d8db0288e5", + "type": "clicked", + "created_at": "2024-01-01T12:00:00.000000Z", + "email": { + "object": "email", + "id": "62f114f7165fe0d8db0288e2", + "created_at": "2024-01-01T12:00:00.000000Z", + "from": "test@mailersend.com", + "subject": "Test subject", + "status": "delivered", + "tags": ["test-tag"], + "headers": null, + "message": { + "object": "message", + "id": "62fb66bef54a112e920b5493", + "created_at": "2024-01-01T12:00:00.000000Z" + }, + "recipient": { + "object": "recipient", + "id": "62c69be104270ee9c0074d32", + "email": "test@example.com", + "created_at": "2024-01-01T12:00:00.000000Z" + } + }, + "morph": { + "object": "click", + "id": "62fb9215f2481f74e3085356", + "created_at": "2024-01-01T12:00:00.000000Z", + "ip": "127.0.0.1", + "url": "https://www.mailersend.com" + }, + "template_id": "0z76k5jg0o3yeg2d" + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/clicked.php b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/clicked.php new file mode 100644 index 0000000000000..5ca285288014a --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/clicked.php @@ -0,0 +1,14 @@ +setRecipientEmail('test@example.com'); +$wh->setTags(["test-tag"]); +$wh->setMetadata([ + 'ip' => '127.0.0.1', + 'url' => 'https://www.mailersend.com' +]); +$wh->setDate(\DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.u\Z', '2024-01-01T12:00:00.000000Z')); + +return $wh; diff --git a/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/clicked_unique.json b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/clicked_unique.json new file mode 100644 index 0000000000000..9cb5ed17af3b8 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/clicked_unique.json @@ -0,0 +1,42 @@ +{ + "type": "activity.clicked_unique", + "domain_id": "7z3m5jgrogdpyo6n", + "created_at": "2024-01-01T12:00:00.000000Z", + "webhook_id": "7z3m5jgrogdpyo6n", + "url": "https://www.mailersend.com/webhook", + "data": { + "object": "activity", + "id": "62f114f8165fe0d8db0288e5", + "type": "clicked_unique", + "created_at": "2024-01-01T12:00:00.000000Z", + "email": { + "object": "email", + "id": "62f114f7165fe0d8db0288e2", + "created_at": "2024-01-01T12:00:00.000000Z", + "from": "test@mailersend.com", + "subject": "Test subject", + "status": "delivered", + "tags": ["test-tag"], + "headers": null, + "message": { + "object": "message", + "id": "62fb66bef54a112e920b5493", + "created_at": "2024-01-01T12:00:00.000000Z" + }, + "recipient": { + "object": "recipient", + "id": "62c69be104270ee9c0074d32", + "email": "test@example.com", + "created_at": "2024-01-01T12:00:00.000000Z" + } + }, + "morph": { + "object": "click", + "id": "62fb9215f2481f74e3085356", + "created_at": "2024-01-01T12:00:00.000000Z", + "ip": "127.0.0.1", + "url": "https://www.mailersend.com" + }, + "template_id": "0z76k5jg0o3yeg2d" + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/clicked_unique.php b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/clicked_unique.php new file mode 100644 index 0000000000000..5ca285288014a --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/clicked_unique.php @@ -0,0 +1,14 @@ +setRecipientEmail('test@example.com'); +$wh->setTags(["test-tag"]); +$wh->setMetadata([ + 'ip' => '127.0.0.1', + 'url' => 'https://www.mailersend.com' +]); +$wh->setDate(\DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.u\Z', '2024-01-01T12:00:00.000000Z')); + +return $wh; diff --git a/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/delivered.json b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/delivered.json new file mode 100644 index 0000000000000..eb924779ee895 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/delivered.json @@ -0,0 +1,36 @@ +{ + "type": "activity.delivered", + "domain_id": "7z3m5jgrogdpyo6n", + "created_at": "2024-01-01T12:00:00.000000Z", + "webhook_id": "7z3m5jgrogdpyo6n", + "url": "https://www.mailersend.com/webhook", + "data": { + "object": "activity", + "id": "62f114f8165fe0d8db0288e5", + "type": "delivered", + "created_at": "2024-01-01T12:00:00.000000Z", + "email": { + "object": "email", + "id": "62f114f7165fe0d8db0288e2", + "created_at": "2024-01-01T12:00:00.000000Z", + "from": "test@mailersend.com", + "subject": "Test subject", + "status": "delivered", + "tags": ["test-tag"], + "headers": null, + "message": { + "object": "message", + "id": "62fb66bef54a112e920b5493", + "created_at": "2024-01-01T12:00:00.000000Z" + }, + "recipient": { + "object": "recipient", + "id": "62c69be104270ee9c0074d32", + "email": "test@example.com", + "created_at": "2024-01-01T12:00:00.000000Z" + } + }, + "morph": null, + "template_id": "0z76k5jg0o3yeg2d" + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/delivered.php b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/delivered.php new file mode 100644 index 0000000000000..62558144c06aa --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/delivered.php @@ -0,0 +1,12 @@ +setRecipientEmail('test@example.com'); +$wh->setTags(["test-tag"]); +$wh->setMetadata([]); +$wh->setReason(''); +$wh->setDate(\DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.u\Z', '2024-01-01T12:00:00.000000Z')); + +return $wh; diff --git a/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/hard_bounced.json b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/hard_bounced.json new file mode 100644 index 0000000000000..d59a3bcefee38 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/hard_bounced.json @@ -0,0 +1,39 @@ +{ + "type": "activity.hard_bounced", + "domain_id": "7z3m5jgrogdpyo6n", + "created_at": "2024-01-01T12:00:00.000000Z", + "webhook_id": "7z3m5jgrogdpyo6n", + "url": "https://www.mailersend.com/webhook", + "data": { + "object": "activity", + "id": "62f114f8165fe0d8db0288e5", + "type": "hard_bounced", + "created_at": "2024-01-01T12:00:00.000000Z", + "email": { + "object": "email", + "id": "62f114f7165fe0d8db0288e2", + "created_at": "2024-01-01T12:00:00.000000Z", + "from": "test@mailersend.com", + "subject": "Test subject", + "status": "rejected", + "tags": ["test-tag"], + "headers": null, + "message": { + "object": "message", + "id": "62fb66bef54a112e920b5493", + "created_at": "2024-01-01T12:00:00.000000Z" + }, + "recipient": { + "object": "recipient", + "id": "62c69be104270ee9c0074d32", + "email": "test@example.com", + "created_at": "2024-01-01T12:00:00.000000Z" + } + }, + "morph": { + "object": "recipient_bounce", + "reason": "Host or domain name not found" + }, + "template_id": "0z76k5jg0o3yeg2d" + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/hard_bounced.php b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/hard_bounced.php new file mode 100644 index 0000000000000..06dd15ff42b70 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/hard_bounced.php @@ -0,0 +1,12 @@ +setRecipientEmail('test@example.com'); +$wh->setTags(["test-tag"]); +$wh->setMetadata([]); +$wh->setReason('Host or domain name not found'); +$wh->setDate(\DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.u\Z', '2024-01-01T12:00:00.000000Z')); + +return $wh; diff --git a/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/opened.json b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/opened.json new file mode 100644 index 0000000000000..9b5aac7905bca --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/opened.json @@ -0,0 +1,41 @@ +{ + "type": "activity.opened", + "domain_id": "7z3m5jgrogdpyo6n", + "created_at": "2024-01-01T12:00:00.000000Z", + "webhook_id": "7z3m5jgrogdpyo6n", + "url": "https://www.mailersend.com/webhook", + "data": { + "object": "activity", + "id": "62f114f8165fe0d8db0288e5", + "type": "opened", + "created_at": "2024-01-01T12:00:00.000000Z", + "email": { + "object": "email", + "id": "62f114f7165fe0d8db0288e2", + "created_at": "2024-01-01T12:00:00.000000Z", + "from": "test@mailersend.com", + "subject": "Test subject", + "status": "delivered", + "tags": ["test-tag"], + "headers": null, + "message": { + "object": "message", + "id": "62fb66bef54a112e920b5493", + "created_at": "2024-01-01T12:00:00.000000Z" + }, + "recipient": { + "object": "recipient", + "id": "62c69be104270ee9c0074d32", + "email": "test@example.com", + "created_at": "2024-01-01T12:00:00.000000Z" + } + }, + "morph": { + "object": "open", + "id": "62fb9151f2481f74e3085352", + "created_at": "2024-01-01T12:00:00.000000Z", + "ip": "127.0.0.1" + }, + "template_id": "0z76k5jg0o3yeg2d" + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/opened.php b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/opened.php new file mode 100644 index 0000000000000..8deb1ca7ceb03 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/opened.php @@ -0,0 +1,13 @@ +setRecipientEmail('test@example.com'); +$wh->setTags(["test-tag"]); +$wh->setMetadata([ + 'ip' => '127.0.0.1' +]); +$wh->setDate(\DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.u\Z', '2024-01-01T12:00:00.000000Z')); + +return $wh; diff --git a/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/opened_unique.json b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/opened_unique.json new file mode 100644 index 0000000000000..7323512a83927 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/opened_unique.json @@ -0,0 +1,41 @@ +{ + "type": "activity.opened_unique", + "domain_id": "7z3m5jgrogdpyo6n", + "created_at": "2024-01-01T12:00:00.000000Z", + "webhook_id": "7z3m5jgrogdpyo6n", + "url": "https://www.mailersend.com/webhook", + "data": { + "object": "activity", + "id": "62f114f8165fe0d8db0288e5", + "type": "opened_unique", + "created_at": "2024-01-01T12:00:00.000000Z", + "email": { + "object": "email", + "id": "62f114f7165fe0d8db0288e2", + "created_at": "2024-01-01T12:00:00.000000Z", + "from": "test@mailersend.com", + "subject": "Test subject", + "status": "delivered", + "tags": ["test-tag"], + "headers": null, + "message": { + "object": "message", + "id": "62fb66bef54a112e920b5493", + "created_at": "2024-01-01T12:00:00.000000Z" + }, + "recipient": { + "object": "recipient", + "id": "62c69be104270ee9c0074d32", + "email": "test@example.com", + "created_at": "2024-01-01T12:00:00.000000Z" + } + }, + "morph": { + "object": "open", + "id": "62fb9151f2481f74e3085352", + "created_at": "2024-01-01T12:00:00.000000Z", + "ip": "127.0.0.1" + }, + "template_id": "0z76k5jg0o3yeg2d" + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/opened_unique.php b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/opened_unique.php new file mode 100644 index 0000000000000..8deb1ca7ceb03 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/opened_unique.php @@ -0,0 +1,13 @@ +setRecipientEmail('test@example.com'); +$wh->setTags(["test-tag"]); +$wh->setMetadata([ + 'ip' => '127.0.0.1' +]); +$wh->setDate(\DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.u\Z', '2024-01-01T12:00:00.000000Z')); + +return $wh; diff --git a/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/sent.json b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/sent.json new file mode 100644 index 0000000000000..3d846979eeb94 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/sent.json @@ -0,0 +1,36 @@ +{ + "type": "activity.sent", + "domain_id": "7z3m5jgrogdpyo6n", + "created_at": "2024-01-01T12:00:00.000000Z", + "webhook_id": "7z3m5jgrogdpyo6n", + "url": "https://www.mailersend.com/webhook", + "data": { + "object": "activity", + "id": "62f114f8165fe0d8db0288e5", + "type": "sent", + "created_at": "2024-01-01T12:00:00.000000Z", + "email": { + "object": "email", + "id": "62f114f7165fe0d8db0288e2", + "created_at": "2024-01-01T12:00:00.000000Z", + "from": "test@mailersend.com", + "subject": "Test subject", + "status": "sent", + "tags": ["test-tag"], + "headers": null, + "message": { + "object": "message", + "id": "62fb66bef54a112e920b5493", + "created_at": "2024-01-01T12:00:00.000000Z" + }, + "recipient": { + "object": "recipient", + "id": "62c69be104270ee9c0074d32", + "email": "test@example.com", + "created_at": "2024-01-01T12:00:00.000000Z" + } + }, + "morph": null, + "template_id": "0z76k5jg0o3yeg2d" + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/sent.php b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/sent.php new file mode 100644 index 0000000000000..519813c9a39c5 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/sent.php @@ -0,0 +1,12 @@ +setRecipientEmail('test@example.com'); +$wh->setTags(["test-tag"]); +$wh->setMetadata([]); +$wh->setReason(''); +$wh->setDate(\DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.u\Z', '2024-01-01T12:00:00.000000Z')); + +return $wh; diff --git a/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/soft_bounced.json b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/soft_bounced.json new file mode 100644 index 0000000000000..27a3facf73c56 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/soft_bounced.json @@ -0,0 +1,39 @@ +{ + "type": "activity.soft_bounced", + "domain_id": "7z3m5jgrogdpyo6n", + "created_at": "2024-01-01T12:00:00.000000Z", + "webhook_id": "7z3m5jgrogdpyo6n", + "url": "https://www.mailersend.com/webhook", + "data": { + "object": "activity", + "id": "62f114f8165fe0d8db0288e5", + "type": "soft_bounced", + "created_at": "2024-01-01T12:00:00.000000Z", + "email": { + "object": "email", + "id": "62f114f7165fe0d8db0288e2", + "created_at": "2024-01-01T12:00:00.000000Z", + "from": "test@mailersend.com", + "subject": "Test subject", + "status": "rejected", + "tags": ["test-tag"], + "headers": null, + "message": { + "object": "message", + "id": "62fb66bef54a112e920b5493", + "created_at": "2024-01-01T12:00:00.000000Z" + }, + "recipient": { + "object": "recipient", + "id": "62c69be104270ee9c0074d32", + "email": "test@example.com", + "created_at": "2024-01-01T12:00:00.000000Z" + } + }, + "morph": { + "object": "recipient_bounce", + "reason": "Unknown reason" + }, + "template_id": "0z76k5jg0o3yeg2d" + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/soft_bounced.php b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/soft_bounced.php new file mode 100644 index 0000000000000..0973e3cd637f6 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/soft_bounced.php @@ -0,0 +1,12 @@ +setRecipientEmail('test@example.com'); +$wh->setTags(["test-tag"]); +$wh->setMetadata([]); +$wh->setReason('Unknown reason'); +$wh->setDate(\DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.u\Z', '2024-01-01T12:00:00.000000Z')); + +return $wh; diff --git a/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/spam_complaint.json b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/spam_complaint.json new file mode 100644 index 0000000000000..a63d796ecd2bc --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/spam_complaint.json @@ -0,0 +1,38 @@ +{ + "type": "activity.spam_complaint", + "domain_id": "7z3m5jgrogdpyo6n", + "created_at": "2024-01-01T12:00:00.000000Z", + "webhook_id": "7z3m5jgrogdpyo6n", + "url": "https://www.mailersend.com/webhook", + "data": { + "object": "activity", + "id": "62f114f8165fe0d8db0288e5", + "type": "spam_complaint", + "created_at": "2024-01-01T12:00:00.000000Z", + "email": { + "object": "email", + "id": "62f114f7165fe0d8db0288e2", + "created_at": "2024-01-01T12:00:00.000000Z", + "from": "test@mailersend.com", + "subject": "Test subject", + "status": "delivered", + "tags": ["test-tag"], + "headers": null, + "message": { + "object": "message", + "id": "62fb66bef54a112e920b5493", + "created_at": "2024-01-01T12:00:00.000000Z" + }, + "recipient": { + "object": "recipient", + "id": "62c69be104270ee9c0074d32", + "email": "test@example.com", + "created_at": "2024-01-01T12:00:00.000000Z" + } + }, + "morph": { + "object": "spam_complaint" + }, + "template_id": "0z76k5jg0o3yeg2d" + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/spam_complaint.php b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/spam_complaint.php new file mode 100644 index 0000000000000..7bd73b9fa97d1 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/spam_complaint.php @@ -0,0 +1,11 @@ +setRecipientEmail('test@example.com'); +$wh->setTags(["test-tag"]); +$wh->setMetadata([]); +$wh->setDate(\DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.u\Z', '2024-01-01T12:00:00.000000Z')); + +return $wh; diff --git a/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/unsubscribed.json b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/unsubscribed.json new file mode 100644 index 0000000000000..4c7c7ee22cb68 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/unsubscribed.json @@ -0,0 +1,40 @@ +{ + "type": "activity.unsubscribed", + "domain_id": "7z3m5jgrogdpyo6n", + "created_at": "2024-01-01T12:00:00.000000Z", + "webhook_id": "7z3m5jgrogdpyo6n", + "url": "https://www.mailersend.com/webhook", + "data": { + "object": "activity", + "id": "62f114f8165fe0d8db0288e5", + "type": "unsubscribed", + "created_at": "2024-01-01T12:00:00.000000Z", + "email": { + "object": "email", + "id": "62f114f7165fe0d8db0288e2", + "created_at": "2024-01-01T12:00:00.000000Z", + "from": "test@mailersend.com", + "subject": "Test subject", + "status": "delivered", + "tags": ["test-tag"], + "headers": null, + "message": { + "object": "message", + "id": "62fb66bef54a112e920b5493", + "created_at": "2024-01-01T12:00:00.000000Z" + }, + "recipient": { + "object": "recipient", + "id": "62c69be104270ee9c0074d32", + "email": "test@example.com", + "created_at": "2024-01-01T12:00:00.000000Z" + } + }, + "morph": { + "object": "recipient_unsubscribe", + "reason": "NO_LONGER_WANT", + "readable_reason": "I no longer want to receive these emails" + }, + "template_id": "0z76k5jg0o3yeg2d" + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/unsubscribed.php b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/unsubscribed.php new file mode 100644 index 0000000000000..45e7a63beb16b --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/Fixtures/unsubscribed.php @@ -0,0 +1,14 @@ +setRecipientEmail('test@example.com'); +$wh->setTags(["test-tag"]); +$wh->setMetadata([ + 'reason' => 'NO_LONGER_WANT', + 'readable_reason' => 'I no longer want to receive these emails' +]); +$wh->setDate(\DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.u\Z', '2024-01-01T12:00:00.000000Z')); + +return $wh; diff --git a/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/MailerSendMissingSignatureRequestParserTest.php b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/MailerSendMissingSignatureRequestParserTest.php new file mode 100644 index 0000000000000..010b451a1462e --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/MailerSendMissingSignatureRequestParserTest.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\MailerSend\Tests\Webhook; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Mailer\Bridge\MailerSend\RemoteEvent\MailerSendPayloadConverter; +use Symfony\Component\Mailer\Bridge\MailerSend\Webhook\MailerSendRequestParser; +use Symfony\Component\Webhook\Client\RequestParserInterface; +use Symfony\Component\Webhook\Exception\RejectWebhookException; +use Symfony\Component\Webhook\Test\AbstractRequestParserTestCase; + +class MailerSendMissingSignatureRequestParserTest extends AbstractRequestParserTestCase +{ + protected function createRequestParser(): RequestParserInterface + { + $this->expectException(RejectWebhookException::class); + $this->expectExceptionMessage('Signature is required.'); + + return new MailerSendRequestParser(new MailerSendPayloadConverter()); + } + + public static function getPayloads(): iterable + { + $filename = 'sent.json'; + $currentDir = \dirname((new \ReflectionClass(static::class))->getFileName()); + + yield $filename => [ + file_get_contents($currentDir . '/Fixtures/sent.json'), + include($currentDir . '/Fixtures/sent.php'), + ]; + } + + protected function getSecret(): string + { + return 'GvLY88Uyj70jQm3fUwYyWmAaiz98wWim'; + } + + protected function createRequest(string $payload): Request + { + return Request::create('/', 'POST', [], [], [], [ + 'Content-Type' => 'application/json', + ], $payload); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/MailerSendRequestParserTest.php b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/MailerSendRequestParserTest.php new file mode 100644 index 0000000000000..1f8230b97ce64 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/MailerSendRequestParserTest.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\MailerSend\Tests\Webhook; + +use Symfony\Component\Mailer\Bridge\MailerSend\RemoteEvent\MailerSendPayloadConverter; +use Symfony\Component\Mailer\Bridge\MailerSend\Webhook\MailerSendRequestParser; +use Symfony\Component\Webhook\Client\RequestParserInterface; +use Symfony\Component\Webhook\Test\AbstractRequestParserTestCase; + +class MailerSendRequestParserTest extends AbstractRequestParserTestCase +{ + protected function createRequestParser(): RequestParserInterface + { + return new MailerSendRequestParser(new MailerSendPayloadConverter()); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/MailerSendSignedRequestParserTest.php b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/MailerSendSignedRequestParserTest.php new file mode 100644 index 0000000000000..9553e989752e1 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/MailerSendSignedRequestParserTest.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\MailerSend\Tests\Webhook; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Mailer\Bridge\MailerSend\RemoteEvent\MailerSendPayloadConverter; +use Symfony\Component\Mailer\Bridge\MailerSend\Webhook\MailerSendRequestParser; +use Symfony\Component\Webhook\Client\RequestParserInterface; +use Symfony\Component\Webhook\Exception\RejectWebhookException; +use Symfony\Component\Webhook\Test\AbstractRequestParserTestCase; + +class MailerSendSignedRequestParserTest extends AbstractRequestParserTestCase +{ + protected function createRequestParser(): RequestParserInterface + { + return new MailerSendRequestParser(new MailerSendPayloadConverter()); + } + + public static function getPayloads(): iterable + { + $filename = 'sent.json'; + $currentDir = \dirname((new \ReflectionClass(static::class))->getFileName()); + + yield $filename => [ + file_get_contents($currentDir . '/Fixtures/sent.json'), + include($currentDir . '/Fixtures/sent.php'), + ]; + } + + protected function getSecret(): string + { + return 'GvLY88Uyj70jQm3fUwYyWmAaiz98wWim'; + } + + protected function createRequest(string $payload): Request + { + return Request::create( + uri: '/', + method: 'POST', + server: [ + 'Content-Type' => 'application/json', + 'HTTP_Signature' => 'e60f87b019f0aaae29042b14762991765ebb0cd6f6d42884af9fccca4cbd16e7' + ], + content: str_replace("\n", "", $payload) + ); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/MailerSendWrongSecretRequestParserTest.php b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/MailerSendWrongSecretRequestParserTest.php new file mode 100644 index 0000000000000..fe64118cba141 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MailerSend/Tests/Webhook/MailerSendWrongSecretRequestParserTest.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\MailerSend\Tests\Webhook; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Mailer\Bridge\MailerSend\RemoteEvent\MailerSendPayloadConverter; +use Symfony\Component\Mailer\Bridge\MailerSend\Webhook\MailerSendRequestParser; +use Symfony\Component\Webhook\Client\RequestParserInterface; +use Symfony\Component\Webhook\Exception\RejectWebhookException; +use Symfony\Component\Webhook\Test\AbstractRequestParserTestCase; + +class MailerSendWrongSecretRequestParserTest extends AbstractRequestParserTestCase +{ + protected function createRequestParser(): RequestParserInterface + { + $this->expectException(RejectWebhookException::class); + $this->expectExceptionMessage('Signature is wrong.'); + + return new MailerSendRequestParser(new MailerSendPayloadConverter()); + } + + public static function getPayloads(): iterable + { + $filename = 'sent.json'; + $currentDir = \dirname((new \ReflectionClass(static::class))->getFileName()); + + yield $filename => [ + file_get_contents($currentDir . '/Fixtures/sent.json'), + include($currentDir . '/Fixtures/sent.php'), + ]; + } + + protected function getSecret(): string + { + return 'wrong_secret'; + } + + protected function createRequest(string $payload): Request + { + return Request::create( + uri: '/', + method: 'POST', + server: [ + 'Content-Type' => 'application/json', + 'HTTP_Signature' => 'e60f87b019f0aaae29042b14762991765ebb0cd6f6d42884af9fccca4cbd16e7' + ], + content: str_replace("\n", "", $payload) + ); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/MailerSend/Webhook/MailerSendRequestParser.php b/src/Symfony/Component/Mailer/Bridge/MailerSend/Webhook/MailerSendRequestParser.php new file mode 100644 index 0000000000000..959b321da2da7 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/MailerSend/Webhook/MailerSendRequestParser.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\MailerSend\Webhook; + +use Symfony\Component\HttpFoundation\ChainRequestMatcher; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestMatcher\IsJsonRequestMatcher; +use Symfony\Component\HttpFoundation\RequestMatcher\MethodRequestMatcher; +use Symfony\Component\HttpFoundation\RequestMatcherInterface; +use Symfony\Component\Mailer\Bridge\MailerSend\RemoteEvent\MailerSendPayloadConverter; +use Symfony\Component\RemoteEvent\Event\Mailer\AbstractMailerEvent; +use Symfony\Component\RemoteEvent\Exception\ParseException; +use Symfony\Component\Webhook\Client\AbstractRequestParser; +use Symfony\Component\Webhook\Exception\RejectWebhookException; + +final class MailerSendRequestParser extends AbstractRequestParser +{ + public function __construct( + private readonly MailerSendPayloadConverter $converter, + ) { + } + + protected function getRequestMatcher(): RequestMatcherInterface + { + return new ChainRequestMatcher([ + new MethodRequestMatcher('POST'), + new IsJsonRequestMatcher(), + ]); + } + + protected function doParse(Request $request, #[\SensitiveParameter] string $secret): ?AbstractMailerEvent + { + if ($secret) { + if (!$request->headers->get('Signature')) { + throw new RejectWebhookException(406, 'Signature is required.'); + } + + $this->validateSignature( + $request->headers->get('Signature'), + $request->getContent(), + $secret, + ); + } + + $content = $request->toArray(); + if (!isset($content['type'], $content['data']['email']['message']['id'], $content['data']['email']['recipient']['email'])) { + throw new RejectWebhookException(406, 'Payload is malformed.'); + } + + try { + return $this->converter->convert($content); + } catch (ParseException $e) { + throw new RejectWebhookException(406, $e->getMessage(), $e); + } + } + + private function validateSignature(string $signature, string $payload, #[\SensitiveParameter] string $secret): void + { + // see https://developers.mailersend.com/api/v1/webhooks.html#security + $computedSignature = hash_hmac('sha256', $payload, $secret); + + if (!hash_equals($signature, $computedSignature)) { + throw new RejectWebhookException(406, 'Signature is wrong.'); + } + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/MailerSend/composer.json b/src/Symfony/Component/Mailer/Bridge/MailerSend/composer.json index 144c50ac44149..4a9b3946f2723 100644 --- a/src/Symfony/Component/Mailer/Bridge/MailerSend/composer.json +++ b/src/Symfony/Component/Mailer/Bridge/MailerSend/composer.json @@ -24,7 +24,8 @@ "symfony/mailer": "^6.4|^7.0" }, "require-dev": { - "symfony/http-client": "^6.4|^7.0" + "symfony/http-client": "^6.4|^7.0", + "symfony/webhook": "^6.3|^7.0" }, "autoload": { "psr-4": { "Symfony\\Component\\Mailer\\Bridge\\MailerSend\\": "" },