From b75615ff7ab484edb9757b430964d29767bb4c39 Mon Sep 17 00:00:00 2001 From: johan Vlaar Date: Mon, 7 Oct 2024 09:47:58 +0200 Subject: [PATCH] [Webhook] Add Mailchimp webhook (fixes #50285) --- .../FrameworkExtension.php | 1 + .../Resources/config/mailer_webhook.php | 7 ++ .../Mailer/Bridge/Mailchimp/CHANGELOG.md | 5 + .../RemoteEvent/MailchimpPayloadConverter.php | 79 +++++++++++++ .../Tests/Webhook/Fixtures/batch.json | 70 ++++++++++++ .../Tests/Webhook/Fixtures/batch.php | 31 +++++ .../Tests/Webhook/Fixtures/click.json | 42 +++++++ .../Tests/Webhook/Fixtures/click.php | 11 ++ .../Tests/Webhook/Fixtures/deferral.json | 33 ++++++ .../Tests/Webhook/Fixtures/deferral.php | 12 ++ .../Tests/Webhook/Fixtures/delivered.json | 33 ++++++ .../Tests/Webhook/Fixtures/delivered.php | 11 ++ .../Tests/Webhook/Fixtures/hard_bounce.json | 33 ++++++ .../Tests/Webhook/Fixtures/hard_bounce.php | 12 ++ .../Tests/Webhook/Fixtures/opens.json | 37 ++++++ .../Tests/Webhook/Fixtures/opens.php | 11 ++ .../Tests/Webhook/Fixtures/reject.json | 33 ++++++ .../Tests/Webhook/Fixtures/reject.php | 12 ++ .../Tests/Webhook/Fixtures/send.json | 33 ++++++ .../Mailchimp/Tests/Webhook/Fixtures/send.php | 11 ++ .../Tests/Webhook/Fixtures/soft_bounce.json | 33 ++++++ .../Tests/Webhook/Fixtures/soft_bounce.php | 12 ++ .../Tests/Webhook/Fixtures/spam.json | 33 ++++++ .../Mailchimp/Tests/Webhook/Fixtures/spam.php | 11 ++ .../Tests/Webhook/Fixtures/unsub.json | 33 ++++++ .../Tests/Webhook/Fixtures/unsub.php | 11 ++ .../Webhook/MailchimpRequestParserTest.php | 42 +++++++ .../Webhook/MailchimpRequestParser.php | 107 ++++++++++++++++++ .../Mailer/Bridge/Mailchimp/composer.json | 6 +- 29 files changed, 804 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailchimp/RemoteEvent/MailchimpPayloadConverter.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/batch.json create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/batch.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/click.json create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/click.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/deferral.json create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/deferral.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/delivered.json create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/delivered.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/hard_bounce.json create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/hard_bounce.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/opens.json create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/opens.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/reject.json create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/reject.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/send.json create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/send.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/soft_bounce.json create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/soft_bounce.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/spam.json create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/spam.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/unsub.json create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/unsub.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/MailchimpRequestParserTest.php create mode 100644 src/Symfony/Component/Mailer/Bridge/Mailchimp/Webhook/MailchimpRequestParser.php diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 87a173faca291..826e8fb0f31f2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -2677,6 +2677,7 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co $webhookRequestParsers = [ MailerBridge\Brevo\Webhook\BrevoRequestParser::class => 'mailer.webhook.request_parser.brevo', MailerBridge\MailerSend\Webhook\MailerSendRequestParser::class => 'mailer.webhook.request_parser.mailersend', + MailerBridge\Mailchimp\Webhook\MailchimpRequestParser::class => 'mailer.webhook.request_parser.mailchimp', MailerBridge\Mailgun\Webhook\MailgunRequestParser::class => 'mailer.webhook.request_parser.mailgun', MailerBridge\Mailjet\Webhook\MailjetRequestParser::class => 'mailer.webhook.request_parser.mailjet', MailerBridge\Mailomat\Webhook\MailomatRequestParser::class => 'mailer.webhook.request_parser.mailomat', diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_webhook.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_webhook.php index e6f6c425b0232..c574324db0b9f 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\Mailchimp\RemoteEvent\MailchimpPayloadConverter; +use Symfony\Component\Mailer\Bridge\Mailchimp\Webhook\MailchimpRequestParser; use Symfony\Component\Mailer\Bridge\MailerSend\RemoteEvent\MailerSendPayloadConverter; use Symfony\Component\Mailer\Bridge\MailerSend\Webhook\MailerSendRequestParser; use Symfony\Component\Mailer\Bridge\Mailgun\RemoteEvent\MailgunPayloadConverter; @@ -83,5 +85,10 @@ ->set('mailer.webhook.request_parser.sweego', SweegoRequestParser::class) ->args([service('mailer.payload_converter.sweego')]) ->alias(SweegoRequestParser::class, 'mailer.webhook.request_parser.sweego') + + ->set('mailer.payload_converter.mailchimp', MailchimpPayloadConverter::class) + ->set('mailer.webhook.request_parser.mailchimp', MailchimpRequestParser::class) + ->args([service('mailer.payload_converter.mailchimp')]) + ->alias(MailchimpRequestParser::class, 'mailer.webhook.request_parser.mailchimp') ; }; diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/CHANGELOG.md b/src/Symfony/Component/Mailer/Bridge/Mailchimp/CHANGELOG.md index 332571da66647..e817193a6e3f7 100644 --- a/src/Symfony/Component/Mailer/Bridge/Mailchimp/CHANGELOG.md +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.2 +--- + +* Add support for webhook + 4.4.0 ----- diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/RemoteEvent/MailchimpPayloadConverter.php b/src/Symfony/Component/Mailer/Bridge/Mailchimp/RemoteEvent/MailchimpPayloadConverter.php new file mode 100644 index 0000000000000..c108b9ecb70f4 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/RemoteEvent/MailchimpPayloadConverter.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Mailchimp\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; + +final class MailchimpPayloadConverter implements PayloadConverterInterface +{ + public function convert(array $payload): AbstractMailerEvent + { + if (\in_array($payload['event'], ['send', 'deferral', 'soft_bounce', 'hard_bounce', 'delivered', 'reject'], true)) { + $name = match ($payload['event']) { + 'send' => MailerDeliveryEvent::RECEIVED, + 'deferral', => MailerDeliveryEvent::DEFERRED, + 'soft_bounce', 'hard_bounce' => MailerDeliveryEvent::BOUNCE, + 'delivered' => MailerDeliveryEvent::DELIVERED, + 'reject' => MailerDeliveryEvent::DROPPED, + }; + + $event = new MailerDeliveryEvent($name, $payload['msg']['_id'], $payload); + // reason is only available on failed messages + $event->setReason($this->getReason($payload)); + } else { + $name = match ($payload['event']) { + 'click' => MailerEngagementEvent::CLICK, + 'open' => MailerEngagementEvent::OPEN, + 'spam' => MailerEngagementEvent::SPAM, + 'unsub' => MailerEngagementEvent::UNSUBSCRIBE, + default => throw new ParseException(\sprintf('Unsupported event "%s".', $payload['event'])), + }; + $event = new MailerEngagementEvent($name, $payload['msg']['_id'], $payload); + } + + if (!$date = \DateTimeImmutable::createFromFormat('U', $payload['msg']['ts'])) { + throw new ParseException(\sprintf('Invalid date "%s".', $payload['msg']['ts'])); + } + $event->setDate($date); + $event->setRecipientEmail($payload['msg']['email']); + $event->setMetadata($payload['msg']['metadata']); + $event->setTags($payload['msg']['tags']); + + return $event; + } + + private function getReason(array $payload): string + { + if (null !== $payload['msg']['diag']) { + return $payload['msg']['diag']; + } + if (null !== $payload['msg']['bounce_description']) { + return $payload['msg']['bounce_description']; + } + + if (null !== $payload['msg']['smtp_events'] && [] !== $payload['msg']['smtp_events']) { + $reasons = []; + foreach ($payload['msg']['smtp_events'] as $event) { + $reasons[] = \sprintf('type: %s diag: %s', $event['type'], $event['diag']); + } + + // Return concatenated reasons or an empty string if no reasons found + return implode(' ', $reasons); + } + + return ''; + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/batch.json b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/batch.json new file mode 100644 index 0000000000000..4618be9901984 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/batch.json @@ -0,0 +1,70 @@ +{ + "mandrill_events": [ + { + "ts": 1365109999, + "event": "click", + "msg": { + "ts": 1365109999, + "subject": "Foo bar", + "email": "foo@example.com", + "sender": "bar@example.com", + "tags": [ + "my_tag_1", + "my_tag_2" + ], + "smtp_events": null, + "opens": [ + { + "ts": 1365110000 + } + ], + "clicks": [ + { + "ts": 1365110000, + "url": "https://www.example.com" + } + ], + "state": "sent", + "metadata": { + "mandrill-var-1": "foo", + "mandrill-var-2": "bar" + }, + "_id": "7761630", + "_version": "123", + "subaccount": null, + "diag": null, + "bounce_description": null, + "template": null + } + }, + { + "ts": 1365109999, + "event": "deferral", + "msg": { + "ts": 1365109999, + "subject": "Foo bar", + "email": "foo@example.com", + "sender": "bar@example.com", + "tags": [ + "my_tag_1", + "my_tag_2" + ], + "smtp_events": null, + "opens": null, + "clicks": null, + "state": "sent", + "metadata": { + "mandrill-var-1": "foo", + "mandrill-var-2": "bar" + }, + "_id": "7761631", + "_version": "123", + "subaccount": null, + "diag": null, + "bounce_description": null, + "template": null + } + } + ], + "X-Mandrill-Signature": "Mjask0i0giC/nEAYZrt6sX6JF2M=" +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/batch.php b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/batch.php new file mode 100644 index 0000000000000..0801de2f2c715 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/batch.php @@ -0,0 +1,31 @@ +setRecipientEmail('foo@example.com'); +$wh1->setTags(['my_tag_1', 'my_tag_2']); +$wh1->setMetadata(['mandrill-var-1' => 'foo', 'mandrill-var-2' => 'bar']); +$wh1->setDate(\DateTimeImmutable::createFromFormat('U', 1365109999)); + +$wh2 = new MailerDeliveryEvent( + MailerDeliveryEvent::DEFERRED, '7761631', json_decode( + file_get_contents( + str_replace('.php', '.json', __FILE__) + ), true, flags: JSON_THROW_ON_ERROR + )['mandrill_events'][1] +); +$wh2->setRecipientEmail('foo@example.com'); +$wh2->setTags(['my_tag_1', 'my_tag_2']); +$wh2->setMetadata(['mandrill-var-1' => 'foo', 'mandrill-var-2' => 'bar']); +$wh2->setDate(\DateTimeImmutable::createFromFormat('U', 1365109999)); +$wh2->setReason(''); + +return [$wh1, $wh2]; diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/click.json b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/click.json new file mode 100644 index 0000000000000..fccf649a6a979 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/click.json @@ -0,0 +1,42 @@ +{ + "mandrill_events": [ + { + "ts": 1365109999, + "event": "click", + "msg": { + "ts": 1365109999, + "subject": "Foo bar", + "email": "foo@example.com", + "sender": "bar@example.com", + "tags": [ + "my_tag_1", + "my_tag_2" + ], + "smtp_events": null, + "opens": [ + { + "ts": 1365110000 + } + ], + "clicks": [ + { + "ts": 1365110000, + "url": "https://www.example.com" + } + ], + "state": "sent", + "metadata": { + "mandrill-var-1": "foo", + "mandrill-var-2": "bar" + }, + "_id": "7761630", + "_version": "123", + "subaccount": null, + "diag": null, + "bounce_description": null, + "template": null + } + } + ], + "X-Mandrill-Signature": "5mcP4EaDf9cd1tFLAIY+E13Eqmw=" +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/click.php b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/click.php new file mode 100644 index 0000000000000..e11305980d23a --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/click.php @@ -0,0 +1,11 @@ +setRecipientEmail('foo@example.com'); +$wh->setTags(['my_tag_1', 'my_tag_2']); +$wh->setMetadata(['mandrill-var-1' => 'foo', 'mandrill-var-2' => 'bar']); +$wh->setDate(\DateTimeImmutable::createFromFormat('U', 1365109999)); + +return [$wh]; diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/deferral.json b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/deferral.json new file mode 100644 index 0000000000000..df89a721c952a --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/deferral.json @@ -0,0 +1,33 @@ +{ + "mandrill_events": [ + { + "ts": 1365109999, + "event": "deferral", + "msg": { + "ts": 1365109999, + "subject": "Foo bar", + "email": "foo@example.com", + "sender": "bar@example.com", + "tags": [ + "my_tag_1", + "my_tag_2" + ], + "smtp_events": null, + "opens": null, + "clicks": null, + "state": "sent", + "metadata": { + "mandrill-var-1": "foo", + "mandrill-var-2": "bar" + }, + "_id": "7761631", + "_version": "123", + "subaccount": null, + "diag": null, + "bounce_description": null, + "template": null + } + } + ], + "X-Mandrill-Signature": "VuT14QGTsui1T7B+FqjV6SqACDA=" +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/deferral.php b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/deferral.php new file mode 100644 index 0000000000000..196e54c5e39a2 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/deferral.php @@ -0,0 +1,12 @@ +setRecipientEmail('foo@example.com'); +$wh->setTags(['my_tag_1', 'my_tag_2']); +$wh->setMetadata(['mandrill-var-1' => 'foo', 'mandrill-var-2' => 'bar']); +$wh->setDate(\DateTimeImmutable::createFromFormat('U', 1365109999)); +$wh->setReason(''); + +return [$wh]; diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/delivered.json b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/delivered.json new file mode 100644 index 0000000000000..fedd6bc89408e --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/delivered.json @@ -0,0 +1,33 @@ +{ + "mandrill_events": [ + { + "ts": 1365109999, + "event": "delivered", + "msg": { + "ts": 1365109999, + "subject": "Foo bar", + "email": "foo@example.com", + "sender": "bar@example.com", + "tags": [ + "my_tag_1", + "my_tag_2" + ], + "smtp_events": null, + "opens": null, + "clicks": null, + "state": "sent", + "metadata": { + "mandrill-var-1": "foo", + "mandrill-var-2": "bar" + }, + "_id": "7761632", + "_version": "123", + "subaccount": null, + "diag": null, + "bounce_description": null, + "template": null + } + } + ], + "X-Mandrill-Signature": "Q287c9JdfPkKnVE9vRJw5jd+XcE=" +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/delivered.php b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/delivered.php new file mode 100644 index 0000000000000..d8654e9a49559 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/delivered.php @@ -0,0 +1,11 @@ +setRecipientEmail('foo@example.com'); +$wh->setTags(['my_tag_1', 'my_tag_2']); +$wh->setMetadata(['mandrill-var-1' => 'foo', 'mandrill-var-2' => 'bar']); +$wh->setDate(\DateTimeImmutable::createFromFormat('U', 1365109999)); + +return [$wh]; diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/hard_bounce.json b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/hard_bounce.json new file mode 100644 index 0000000000000..fe9b9e63e452a --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/hard_bounce.json @@ -0,0 +1,33 @@ +{ + "mandrill_events": [ + { + "ts": 1365109999, + "event": "hard_bounce", + "msg": { + "ts": 1365109999, + "subject": "Foo bar", + "email": "foo@example.com", + "sender": "bar@example.com", + "tags": [ + "my_tag_1", + "my_tag_2" + ], + "smtp_events": null, + "opens": null, + "clicks": null, + "state": "sent", + "metadata": { + "mandrill-var-1": "foo", + "mandrill-var-2": "bar" + }, + "_id": "7761633", + "_version": "123", + "subaccount": null, + "diag": null, + "bounce_description": "hard bounce", + "template": null + } + } + ], + "X-Mandrill-Signature": "ip7K8yTxSFakEhIGiJz8tydjVsQ=" +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/hard_bounce.php b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/hard_bounce.php new file mode 100644 index 0000000000000..dd11d59e30dc8 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/hard_bounce.php @@ -0,0 +1,12 @@ +setRecipientEmail('foo@example.com'); +$wh->setTags(['my_tag_1', 'my_tag_2']); +$wh->setMetadata(['mandrill-var-1' => 'foo', 'mandrill-var-2' => 'bar']); +$wh->setDate(\DateTimeImmutable::createFromFormat('U', 1365109999)); +$wh->setReason('hard bounce'); + +return [$wh]; diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/opens.json b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/opens.json new file mode 100644 index 0000000000000..7fa0fd15296fc --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/opens.json @@ -0,0 +1,37 @@ +{ + "mandrill_events": [ + { + "ts": 1365109999, + "event": "open", + "msg": { + "ts": 1365109999, + "subject": "Foo bar", + "email": "foo@example.com", + "sender": "bar@example.com", + "tags": [ + "my_tag_1", + "my_tag_2" + ], + "smtp_events": null, + "opens": [ + { + "ts": 1365110000 + } + ], + "clicks": null, + "state": "sent", + "metadata": { + "mandrill-var-1": "foo", + "mandrill-var-2": "bar" + }, + "_id": "7761634", + "_version": "123", + "subaccount": null, + "diag": null, + "bounce_description": null, + "template": null + } + } + ], + "X-Mandrill-Signature": "9dI0KIewRZJQ/ZabxPXcjhWh4J0=" +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/opens.php b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/opens.php new file mode 100644 index 0000000000000..c400ea3ad4afb --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/opens.php @@ -0,0 +1,11 @@ +setRecipientEmail('foo@example.com'); +$wh->setTags(['my_tag_1', 'my_tag_2']); +$wh->setMetadata(['mandrill-var-1' => 'foo', 'mandrill-var-2' => 'bar']); +$wh->setDate(\DateTimeImmutable::createFromFormat('U', 1365109999)); + +return [$wh]; diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/reject.json b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/reject.json new file mode 100644 index 0000000000000..6a514685feab8 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/reject.json @@ -0,0 +1,33 @@ +{ + "mandrill_events": [ + { + "ts": 1365109999, + "event": "reject", + "msg": { + "ts": 1365109999, + "subject": "Foo bar", + "email": "foo@example.com", + "sender": "bar@example.com", + "tags": [ + "my_tag_1", + "my_tag_2" + ], + "smtp_events": null, + "opens": null, + "clicks": null, + "state": "sent", + "metadata": { + "mandrill-var-1": "foo", + "mandrill-var-2": "bar" + }, + "_id": "7761635", + "_version": "123", + "subaccount": null, + "diag": null, + "bounce_description": null, + "template": null + } + } + ], + "X-Mandrill-Signature": "8ExrBIUlnVaanrjQrvL8Sh/M9pg=" +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/reject.php b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/reject.php new file mode 100644 index 0000000000000..458afb55f5966 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/reject.php @@ -0,0 +1,12 @@ +setRecipientEmail('foo@example.com'); +$wh->setTags(['my_tag_1', 'my_tag_2']); +$wh->setMetadata(['mandrill-var-1' => 'foo', 'mandrill-var-2' => 'bar']); +$wh->setDate(\DateTimeImmutable::createFromFormat('U', 1365109999)); +$wh->setReason(''); + +return [$wh]; diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/send.json b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/send.json new file mode 100644 index 0000000000000..4d76ef1f98cd9 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/send.json @@ -0,0 +1,33 @@ +{ + "mandrill_events": [ + { + "ts": 1365109999, + "event": "send", + "msg": { + "ts": 1365109999, + "subject": "Foo bar", + "email": "foo@example.com", + "sender": "bar@example.com", + "tags": [ + "my_tag_1", + "my_tag_2" + ], + "smtp_events": null, + "opens": null, + "clicks": null, + "state": "sent", + "metadata": { + "mandrill-var-1": "foo", + "mandrill-var-2": "bar" + }, + "_id": "7761636", + "_version": "123", + "subaccount": null, + "diag": null, + "bounce_description": null, + "template": null + } + } + ], + "X-Mandrill-Signature": "mCOMq35TsmWlTHjqDzj3n2eqVg8=" +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/send.php b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/send.php new file mode 100644 index 0000000000000..713dd85b763c0 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/send.php @@ -0,0 +1,11 @@ +setRecipientEmail('foo@example.com'); +$wh->setTags(['my_tag_1', 'my_tag_2']); +$wh->setMetadata(['mandrill-var-1' => 'foo', 'mandrill-var-2' => 'bar']); +$wh->setDate(\DateTimeImmutable::createFromFormat('U', 1365109999)); + +return [$wh]; diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/soft_bounce.json b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/soft_bounce.json new file mode 100644 index 0000000000000..cf4dff9d84ff7 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/soft_bounce.json @@ -0,0 +1,33 @@ +{ + "mandrill_events": [ + { + "ts": 1365109999, + "event": "soft_bounce", + "msg": { + "ts": 1365109999, + "subject": "Foo bar", + "email": "foo@example.com", + "sender": "bar@example.com", + "tags": [ + "my_tag_1", + "my_tag_2" + ], + "smtp_events": null, + "opens": null, + "clicks": null, + "state": "sent", + "metadata": { + "mandrill-var-1": "foo", + "mandrill-var-2": "bar" + }, + "_id": "7761637", + "_version": "123", + "subaccount": null, + "diag": null, + "bounce_description": "soft_bounce", + "template": null + } + } + ], + "X-Mandrill-Signature": "0BdaIQOp5HuT+cs3769ughZdoK0=" +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/soft_bounce.php b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/soft_bounce.php new file mode 100644 index 0000000000000..be507f9cdb53c --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/soft_bounce.php @@ -0,0 +1,12 @@ +setRecipientEmail('foo@example.com'); +$wh->setTags(['my_tag_1', 'my_tag_2']); +$wh->setMetadata(['mandrill-var-1' => 'foo', 'mandrill-var-2' => 'bar']); +$wh->setDate(\DateTimeImmutable::createFromFormat('U', 1365109999)); +$wh->setReason('soft_bounce'); + +return [$wh]; diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/spam.json b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/spam.json new file mode 100644 index 0000000000000..679add8222f8c --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/spam.json @@ -0,0 +1,33 @@ +{ + "mandrill_events": [ + { + "ts": 1365109999, + "event": "spam", + "msg": { + "ts": 1365109999, + "subject": "Foo bar", + "email": "foo@example.com", + "sender": "bar@example.com", + "tags": [ + "my_tag_1", + "my_tag_2" + ], + "smtp_events": null, + "opens": null, + "clicks": null, + "state": "sent", + "metadata": { + "mandrill-var-1": "foo", + "mandrill-var-2": "bar" + }, + "_id": "7761638", + "_version": "123", + "subaccount": null, + "diag": null, + "bounce_description": null, + "template": null + } + } + ], + "X-Mandrill-Signature": "5mOstZe7kgzwJdBbXtlakc4W87w=" +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/spam.php b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/spam.php new file mode 100644 index 0000000000000..8021dd9fed83e --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/spam.php @@ -0,0 +1,11 @@ +setRecipientEmail('foo@example.com'); +$wh->setTags(['my_tag_1', 'my_tag_2']); +$wh->setMetadata(['mandrill-var-1' => 'foo', 'mandrill-var-2' => 'bar']); +$wh->setDate(\DateTimeImmutable::createFromFormat('U', 1365109999)); + +return [$wh]; diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/unsub.json b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/unsub.json new file mode 100644 index 0000000000000..b723eb69422cd --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/unsub.json @@ -0,0 +1,33 @@ +{ + "mandrill_events": [ + { + "ts": 1365109999, + "event": "unsub", + "msg": { + "ts": 1365109999, + "subject": "Foo bar", + "email": "foo@example.com", + "sender": "bar@example.com", + "tags": [ + "my_tag_1", + "my_tag_2" + ], + "smtp_events": null, + "opens": null, + "clicks": null, + "state": "sent", + "metadata": { + "mandrill-var-1": "foo", + "mandrill-var-2": "bar" + }, + "_id": "7761639", + "_version": "123", + "subaccount": null, + "diag": null, + "bounce_description": null, + "template": null + } + } + ], + "X-Mandrill-Signature": "LIs4Mw5kGDw8VQVaX8P0Ibnejyo=" +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/unsub.php b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/unsub.php new file mode 100644 index 0000000000000..28b404ed7a723 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/Fixtures/unsub.php @@ -0,0 +1,11 @@ +setRecipientEmail('foo@example.com'); +$wh->setTags(['my_tag_1', 'my_tag_2']); +$wh->setMetadata(['mandrill-var-1' => 'foo', 'mandrill-var-2' => 'bar']); +$wh->setDate(\DateTimeImmutable::createFromFormat('U', 1365109999)); + +return [$wh]; diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/MailchimpRequestParserTest.php b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/MailchimpRequestParserTest.php new file mode 100644 index 0000000000000..4f7c704dea9e1 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Webhook/MailchimpRequestParserTest.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Mailchimp\Tests\Webhook; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Mailer\Bridge\Mailchimp\RemoteEvent\MailchimpPayloadConverter; +use Symfony\Component\Mailer\Bridge\Mailchimp\Webhook\MailchimpRequestParser; +use Symfony\Component\Webhook\Client\RequestParserInterface; +use Symfony\Component\Webhook\Test\AbstractRequestParserTestCase; + +class MailchimpRequestParserTest extends AbstractRequestParserTestCase +{ + protected function createRequestParser(): RequestParserInterface + { + return new MailchimpRequestParser(new MailchimpPayloadConverter()); + } + + protected function createRequest(string $payload): Request + { + $decodedPayload = json_decode($payload, true, 512, \JSON_THROW_ON_ERROR); + $mandrillSignature = $decodedPayload['X-Mandrill-Signature'] ?? ''; + unset($decodedPayload['X-Mandrill-Signature']); + $request = parent::createRequest(json_encode($decodedPayload, \JSON_THROW_ON_ERROR)); + $request->headers->set('X-Mandrill-Signature', $mandrillSignature); + + return $request; + } + + protected function getSecret(): string + { + return 'key-0p6mqbf74lb20gzq9f4dhpn9rg3zyk26'; + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Webhook/MailchimpRequestParser.php b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Webhook/MailchimpRequestParser.php new file mode 100644 index 0000000000000..f631d2661b442 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Webhook/MailchimpRequestParser.php @@ -0,0 +1,107 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Mailchimp\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\Mailchimp\RemoteEvent\MailchimpPayloadConverter; +use Symfony\Component\RemoteEvent\Exception\ParseException; +use Symfony\Component\RemoteEvent\RemoteEvent; +use Symfony\Component\Webhook\Client\AbstractRequestParser; +use Symfony\Component\Webhook\Exception\RejectWebhookException; + +final class MailchimpRequestParser extends AbstractRequestParser +{ + public function __construct( + private readonly MailchimpPayloadConverter $converter, + ) { + } + + protected function getRequestMatcher(): RequestMatcherInterface + { + return new ChainRequestMatcher([ + new MethodRequestMatcher('POST'), + new IsJsonRequestMatcher(), + ]); + } + + protected function doParse(Request $request, #[\SensitiveParameter] string $secret): RemoteEvent|array|null + { + $content = $request->toArray(); + if (!isset($content['mandrill_events'][0]['event']) + || !isset($content['mandrill_events'][0]['msg']) + ) { + throw new RejectWebhookException(400, 'Payload malformed.'); + } + + $this->validateSignature($content, $secret, $request->getUri(), $request->headers->get('X-Mandrill-Signature')); + + try { + return array_map($this->converter->convert(...), $content['mandrill_events']); + } catch (ParseException $e) { + throw new RejectWebhookException(406, $e->getMessage(), $e); + } + } + + /** + * @see https://mailchimp.com/developer/transactional/guides/track-respond-activity-webhooks/#authenticating-webhook-requests + */ + private function validateSignature(array $content, string $secret, string $webhookUrl, ?string $mandrillHeaderSignature): void + { + if (null === $mandrillHeaderSignature || false === isset($content['mandrill_events'])) { + throw new RejectWebhookException(400, 'Signature is wrong.'); + } + // First add url to signedData. + $signedData = $webhookUrl; + + // When no params is set we know its a test and we set the key to test. + if ('[]' === $content['mandrill_events']) { + $secret = 'test-webhook'; + } + + // Sort params and add to signed data. + ksort($content); + foreach ($content as $key => $value) { + // Add keys and values. + $signedData .= $key; + $signedData .= \is_array($value) ? $this->stringifyArray($value) : $value; + } + + if ($mandrillHeaderSignature !== base64_encode(hash_hmac('sha1', $signedData, $secret, true))) { + throw new RejectWebhookException(400, 'Signature is wrong.'); + } + } + + /** + * Recursively converts an array to a string representation. + * + * @param array $array the array to be converted + */ + private function stringifyArray(array $array): string + { + ksort($array); + $result = ''; + foreach ($array as $key => $value) { + $result .= $key; + if (\is_array($value)) { + $result .= $this->stringifyArray($value); + } else { + $result .= $value; + } + } + + return $result; + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/composer.json b/src/Symfony/Component/Mailer/Bridge/Mailchimp/composer.json index 5f51051650c99..081b5998e6206 100644 --- a/src/Symfony/Component/Mailer/Bridge/Mailchimp/composer.json +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/composer.json @@ -20,7 +20,11 @@ "symfony/mailer": "^7.2" }, "require-dev": { - "symfony/http-client": "^6.4|^7.0" + "symfony/http-client": "^6.4|^7.0", + "symfony/webhook": "^7.2" + }, + "conflict": { + "symfony/webhook": "<7.2" }, "autoload": { "psr-4": { "Symfony\\Component\\Mailer\\Bridge\\Mailchimp\\": "" },