diff --git a/src/Symfony/Component/Mailer/Bridge/Mailjet/CHANGELOG.md b/src/Symfony/Component/Mailer/Bridge/Mailjet/CHANGELOG.md index 8795f1e1008da..bd1d2d82779bd 100644 --- a/src/Symfony/Component/Mailer/Bridge/Mailjet/CHANGELOG.md +++ b/src/Symfony/Component/Mailer/Bridge/Mailjet/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +6.4 +--- + + * Add `RemoteEvent` and `Webhook` support + 6.3 --- diff --git a/src/Symfony/Component/Mailer/Bridge/Mailjet/README.md b/src/Symfony/Component/Mailer/Bridge/Mailjet/README.md index 3c56a3d7d5b94..afa1d9fcdfa37 100644 --- a/src/Symfony/Component/Mailer/Bridge/Mailjet/README.md +++ b/src/Symfony/Component/Mailer/Bridge/Mailjet/README.md @@ -18,6 +18,11 @@ where: - `ACCESS_KEY` is your Mailjet access key - `SECRET_KEY` is your Mailjet secret key +Webhook +------- + +When you [setup your webhook URL](https://app.mailjet.com/account/triggers) on Mailjet you must not group events by unchecking the checkboxes. + Resources --------- diff --git a/src/Symfony/Component/Mailer/Bridge/Mailjet/RemoteEvent/MailjetPayloadConverter.php b/src/Symfony/Component/Mailer/Bridge/Mailjet/RemoteEvent/MailjetPayloadConverter.php new file mode 100644 index 0000000000000..c940e1ba4b097 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailjet/RemoteEvent/MailjetPayloadConverter.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Mailjet\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 MailjetPayloadConverter implements PayloadConverterInterface +{ + public function convert(array $payload): AbstractMailerEvent + { + if (\in_array($payload['event'], ['bounce', 'sent', 'blocked'], true)) { + $name = match ($payload['event']) { + 'bounce' => MailerDeliveryEvent::BOUNCE, + 'sent' => MailerDeliveryEvent::DELIVERED, + 'blocked' => MailerDeliveryEvent::DROPPED, + }; + + $event = new MailerDeliveryEvent($name, $payload['MessageID'], $payload); + $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['MessageID'], $payload); + } + + if (!$date = \DateTimeImmutable::createFromFormat('U', $payload['time'])) { + throw new ParseException(sprintf('Invalid date "%s".', $payload['time'])); + } + + $event->setDate($date); + $event->setRecipientEmail($payload['email']); + + if (isset($payload['CustomID'])) { + $event->setTags([$payload['CustomID']]); + } + + if (isset($payload['Payload'])) { + $event->setMetadata(['Payload' => $payload['Payload']]); + } + + return $event; + } + + private function getReason(array $payload): string + { + return $payload['smtp_reply'] ?? $payload['error_related_to'] ?? ''; + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/blocked.json b/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/blocked.json new file mode 100644 index 0000000000000..b632620f96516 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/blocked.json @@ -0,0 +1,14 @@ +{ + "event": "blocked", + "time": 1430812195, + "MessageID": 13792286917004336, + "Message_GUID": "1ab23cd4-e567-8901-2345-6789f0gh1i2j", + "email": "bounce@mailjet.com", + "mj_campaign_id": 0, + "mj_contact_id": 1000, + "customcampaign": "", + "CustomID": "helloworld", + "Payload": "", + "error_related_to": "mailjet", + "error": "preblocked" +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/blocked.php b/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/blocked.php new file mode 100644 index 0000000000000..f8afa7dad0835 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/blocked.php @@ -0,0 +1,12 @@ +setRecipientEmail('bounce@mailjet.com'); +$wh->setDate(\DateTimeImmutable::createFromFormat('U', 1430812195)); +$wh->setReason('mailjet'); +$wh->setTags(['helloworld']); +$wh->setMetadata(['Payload' => '']); + +return $wh; diff --git a/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/bounce.json b/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/bounce.json new file mode 100644 index 0000000000000..d40b22d08655a --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/bounce.json @@ -0,0 +1,17 @@ +{ + "event": "bounce", + "time": 1685525050, + "MessageID": 104427216766056450, + "Message_GUID": "5577705c-024a-472d-8918-29ef81a64738", + "email": "event-bounce@yahoo.fr", + "mj_campaign_id": 0, + "mj_contact_id": 1000, + "customcampaign": "", + "blocked": false, + "hard_bounce": false, + "error_related_to": "policy issue", + "error": "", + "comment": "421 4.7.0 [TSS04] Messages from 87.253.233.123 temporarily deferred due to unexpected volume or user complaints - 4.16.55.1; see https://postmaster.yahooinc.com/error-codes", + "CustomID": "helloworld", + "Payload": "" +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/bounce.php b/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/bounce.php new file mode 100644 index 0000000000000..d9b21e0e45b70 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/bounce.php @@ -0,0 +1,12 @@ +setRecipientEmail('event-bounce@yahoo.fr'); +$wh->setDate(\DateTimeImmutable::createFromFormat('U', 1685525050)); +$wh->setReason('policy issue'); +$wh->setTags(['helloworld']); +$wh->setMetadata(['Payload' => '']); + +return $wh; diff --git a/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/click.json b/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/click.json new file mode 100644 index 0000000000000..20c01b0c0a5bd --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/click.json @@ -0,0 +1,16 @@ +{ + "event": "click", + "time": 1685519224, + "MessageID": 93449692684977140, + "Message_GUID": "245e4120-9d53-41b7-91f5-9aac8fda3cb0", + "email": "event-click@hotmail.com", + "mj_campaign_id": 0, + "mj_contact_id": 1000, + "customcampaign": "", + "url": "https://mailjet.com", + "ip": "127.0.0.1", + "geo": "FR", + "agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 CCleaner/113.0.21244.129", + "CustomID": "helloworld", + "Payload": "" +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/click.php b/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/click.php new file mode 100644 index 0000000000000..3872b4980dad6 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/click.php @@ -0,0 +1,11 @@ +setRecipientEmail('event-click@hotmail.com'); +$wh->setDate(\DateTimeImmutable::createFromFormat('U', 1685519224)); +$wh->setTags(['helloworld']); +$wh->setMetadata(['Payload' => '']); + +return $wh; diff --git a/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/open.json b/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/open.json new file mode 100644 index 0000000000000..de34d63f72a89 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/open.json @@ -0,0 +1,15 @@ +{ + "event": "open", + "time": 1685519055, + "MessageID": 102175416994919440, + "Message_GUID": "982f91f1-4417-4ab1-9777-a2410e1bde36", + "email": "event-open@gmail.com", + "mj_campaign_id": 0, + "mj_contact_id": 1000, + "customcampaign": "", + "ip": "127.0.0.1", + "geo": "EU", + "agent": "Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko Firefox/11.0 (via ggpht.com GoogleImageProxy)", + "CustomID": "helloworld", + "Payload": "" +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/open.php b/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/open.php new file mode 100644 index 0000000000000..ea41b2be202b5 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/open.php @@ -0,0 +1,11 @@ +setRecipientEmail('event-open@gmail.com'); +$wh->setDate(\DateTimeImmutable::createFromFormat('U', 1685519055)); +$wh->setTags(['helloworld']); +$wh->setMetadata(['Payload' => '']); + +return $wh; diff --git a/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/sent.json b/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/sent.json new file mode 100644 index 0000000000000..9937c466cfb52 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/sent.json @@ -0,0 +1,13 @@ +{ + "event": "sent", + "time": 1685518742, + "MessageID": 92042317804662640, + "Message_GUID": "5b4de5f5-63d0-44f3-b4bd-a34f222cb8af", + "email": "event-sent@gmail.com", + "mj_campaign_id": 0, + "mj_contact_id": 1000, + "customcampaign": "", + "CustomID": "helloworld", + "Payload": "", + "smtp_reply": "250 2.0.0 OK 1685518742 k22-20020a05600c0b5600b003f6020d9976si8376621wmr.181 - gsmtp" +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/sent.php b/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/sent.php new file mode 100644 index 0000000000000..76eb06b79032d --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/sent.php @@ -0,0 +1,12 @@ +setRecipientEmail('event-sent@gmail.com'); +$wh->setDate(\DateTimeImmutable::createFromFormat('U', 1685518742)); +$wh->setReason('250 2.0.0 OK 1685518742 k22-20020a05600c0b5600b003f6020d9976si8376621wmr.181 - gsmtp'); +$wh->setTags(['helloworld']); +$wh->setMetadata(['Payload' => '']); + +return $wh; diff --git a/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/spam.json b/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/spam.json new file mode 100644 index 0000000000000..944877eafb690 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/spam.json @@ -0,0 +1,13 @@ +{ + "event": "spam", + "time": 1430812195, + "MessageID": 13792286917004336, + "Message_GUID": "1ab23cd4-e567-8901-2345-6789f0gh1i2j", + "email": "bounce@mailjet.com", + "mj_campaign_id": 0, + "mj_contact_id": 1000, + "customcampaign": "", + "CustomID": "helloworld", + "Payload": "", + "source": "JMRPP" +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/spam.php b/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/spam.php new file mode 100644 index 0000000000000..23221094a3d88 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/spam.php @@ -0,0 +1,11 @@ +setRecipientEmail('bounce@mailjet.com'); +$wh->setDate(\DateTimeImmutable::createFromFormat('U', 1430812195)); +$wh->setTags(['helloworld']); +$wh->setMetadata(['Payload' => '']); + +return $wh; diff --git a/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/unsub.json b/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/unsub.json new file mode 100644 index 0000000000000..7b632d62ee905 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/unsub.json @@ -0,0 +1,17 @@ +{ + "event": "unsub", + "time": 1433334941, + "MessageID": 20547674933128000, + "Message_GUID": "1ab23cd4-e567-8901-2345-6789f0gh1i2j", + "email": "api@mailjet.com", + "mj_campaign_id": 0, + "mj_contact_id": 1000, + "customcampaign": "", + "CustomID": "helloworld", + "Payload": "", + "mj_list_id": 1, + "ip": "127.0.0.1", + "geo": "FR", + "agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36" +} + diff --git a/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/unsub.php b/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/unsub.php new file mode 100644 index 0000000000000..bba2b27cface8 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/Fixtures/unsub.php @@ -0,0 +1,11 @@ +setRecipientEmail('api@mailjet.com'); +$wh->setDate(\DateTimeImmutable::createFromFormat('U', 1433334941)); +$wh->setTags(['helloworld']); +$wh->setMetadata(['Payload' => '']); + +return $wh; diff --git a/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/MailjetRequestParserTest.php b/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/MailjetRequestParserTest.php new file mode 100644 index 0000000000000..6001db4e90e9e --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailjet/Tests/Webhook/MailjetRequestParserTest.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\Mailjet\Tests\Webhook; + +use Symfony\Component\Mailer\Bridge\Mailjet\RemoteEvent\MailjetPayloadConverter; +use Symfony\Component\Mailer\Bridge\Mailjet\Webhook\MailjetRequestParser; +use Symfony\Component\Webhook\Client\RequestParserInterface; +use Symfony\Component\Webhook\Test\AbstractRequestParserTestCase; + +class MailjetRequestParserTest extends AbstractRequestParserTestCase +{ + protected function createRequestParser(): RequestParserInterface + { + return new MailjetRequestParser(new MailjetPayloadConverter()); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailjet/Webhook/MailjetRequestParser.php b/src/Symfony/Component/Mailer/Bridge/Mailjet/Webhook/MailjetRequestParser.php new file mode 100644 index 0000000000000..d3f28ea461104 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailjet/Webhook/MailjetRequestParser.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Mailjet\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\Mailjet\RemoteEvent\MailjetPayloadConverter; +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 MailjetRequestParser extends AbstractRequestParser +{ + public function __construct( + private readonly MailjetPayloadConverter $converter, + ) { + } + + protected function getRequestMatcher(): RequestMatcherInterface + { + return new ChainRequestMatcher([ + new MethodRequestMatcher('POST'), + new IsJsonRequestMatcher(), + ]); + } + + protected function doParse(Request $request, string $secret): ?AbstractMailerEvent + { + try { + return $this->converter->convert($request->toArray()); + } catch (ParseException $e) { + throw new RejectWebhookException(406, $e->getMessage(), $e); + } + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailjet/composer.json b/src/Symfony/Component/Mailer/Bridge/Mailjet/composer.json index 245d62810f8da..66cd40f9b58f5 100644 --- a/src/Symfony/Component/Mailer/Bridge/Mailjet/composer.json +++ b/src/Symfony/Component/Mailer/Bridge/Mailjet/composer.json @@ -20,7 +20,8 @@ "symfony/mailer": "^5.4.21|^6.2.7|^7.0" }, "require-dev": { - "symfony/http-client": "^5.4|^6.0|^7.0" + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/webhook": "^6.4" }, "autoload": { "psr-4": { "Symfony\\Component\\Mailer\\Bridge\\Mailjet\\": "" },