Skip to content

Commit

Permalink
[Webhook][RemoteEvent] Add Sendgrid #50704
Browse files Browse the repository at this point in the history
  • Loading branch information
WoutervanderLoopNL committed Aug 18, 2023
1 parent 52a9292 commit bb57ad0
Show file tree
Hide file tree
Showing 14 changed files with 453 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2615,6 +2615,7 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co
$webhookRequestParsers = [
MailerBridge\Mailgun\Webhook\MailgunRequestParser::class => 'mailer.webhook.request_parser.mailgun',
MailerBridge\Postmark\Webhook\PostmarkRequestParser::class => 'mailer.webhook.request_parser.postmark',
MailerBridge\Sendgrid\Webhook\SendgridRequestParser::class => 'mailer.webhook.request_parser.sendgrid',
];

foreach ($webhookRequestParsers as $class => $service) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
use Symfony\Component\Mailer\Bridge\Mailgun\Webhook\MailgunRequestParser;
use Symfony\Component\Mailer\Bridge\Postmark\RemoteEvent\PostmarkPayloadConverter;
use Symfony\Component\Mailer\Bridge\Postmark\Webhook\PostmarkRequestParser;
use Symfony\Component\Mailer\Bridge\Sendgrid\RemoteEvent\SendgridPayloadConverter;
use Symfony\Component\Mailer\Bridge\Sendgrid\Webhook\SendgridRequestParser;

return static function (ContainerConfigurator $container) {
$container->services()
Expand All @@ -27,5 +29,10 @@
->set('mailer.webhook.request_parser.postmark', PostmarkRequestParser::class)
->args([service('mailer.payload_converter.postmark')])
->alias(PostmarkRequestParser::class, 'mailer.webhook.request_parser.postmark')

->set('mailer.payload_converter.sendgrid', SendgridPayloadConverter::class)
->set('mailer.webhook.request_parser.sendgrid', SendgridRequestParser::class)
->args([service('mailer.payload_converter.sendgrid')])
->alias(SendgridRequestParser::class, 'mailer.webhook.request_parser.sendgrid')
;
};
5 changes: 5 additions & 0 deletions src/Symfony/Component/Mailer/Bridge/Sendgrid/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

6.4
---

* Add support for webhooks

5.4
---

Expand Down
24 changes: 24 additions & 0 deletions src/Symfony/Component/Mailer/Bridge/Sendgrid/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,30 @@ MAILER_DSN=sendgrid+api://KEY@default
where:
- `KEY` is your Sendgrid API Key


Webhook:
--------
Create route:
```yaml
framework:
webhook:
routing:
sendgrid:
service: mailer.webhook.request_parser.sendgrid
secret: '!SENDGRID_VALIDATION_SECRET!' #Leave blank if you dont want to use the signature validation
```
Create consumer:
```php
#[\Symfony\Component\RemoteEvent\Attribute\AsRemoteEventConsumer(name: 'sendgrid')]
class SendGridConsumer implements ConsumerInterface
{
public function consume(RemoteEvent|MailerDeliveryEvent $event): void
{
//your code
}
}
```

Resources
---------

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Mailer\Bridge\Sendgrid\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 <info@woutervanderloop.nl>
*/
final class SendgridPayloadConverter implements PayloadConverterInterface
{
public function convert(array $payload): AbstractMailerEvent
{
if (\in_array($payload['event'], ['processed', 'delivered', 'bounce', 'dropped', 'deferred'], true)) {
$name = match ($payload['event']) {
'processed', 'delivered' => MailerDeliveryEvent::DELIVERED,
'dropped' => MailerDeliveryEvent::DROPPED,
'deferred' => MailerDeliveryEvent::DEFERRED,
'bounce' => MailerDeliveryEvent::BOUNCE,
};
$event = new MailerDeliveryEvent($name, $payload['sg_message_id'], $payload);
$event->setReason($payload['reason'] ?? '');
} else {
$name = match ($payload['event']) {
'click' => MailerEngagementEvent::CLICK,
'unsubscribe' => MailerEngagementEvent::UNSUBSCRIBE,
'open' => MailerEngagementEvent::OPEN,
'spamreport' => MailerEngagementEvent::SPAM,
default => throw new ParseException(sprintf('Unsupported event "%s".', $payload['unsubscribe'])),
};
$event = new MailerEngagementEvent($name, $payload['sg_message_id'], $payload);
}

if (!$date = \DateTimeImmutable::createFromFormat('U', $payload['timestamp'])) {
throw new ParseException(sprintf('Invalid date "%s".', $payload['timestamp']));
}

$event->setDate($date);
$event->setRecipientEmail($payload['email']);
$event->setMetadata([]);
$event->setTags($payload['category'] ?? []);

return $event;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"email":"hello@world.com","event":"dropped","reason":"Bounced Address","sg_event_id":"ZHJvcC0xMDk5NDkxOS1MUnpYbF9OSFN0T0doUTRrb2ZTbV9BLTA","sg_message_id":"LRzXl_NHStOGhQ4kofSm_A.filterdrecv-p3mdw1-756b745b58-kmzbl-18-5F5FC76C-9.0","smtp-id":"<LRzXl_NHStOGhQ4kofSm_A@ismtpd0039p1iad1.sendgrid.net>","timestamp":1600112492}]
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

use Symfony\Component\RemoteEvent\Event\Mailer\MailerDeliveryEvent;

$wh = new MailerDeliveryEvent(MailerDeliveryEvent::DROPPED, 'LRzXl_NHStOGhQ4kofSm_A.filterdrecv-p3mdw1-756b745b58-kmzbl-18-5F5FC76C-9.0', json_decode(file_get_contents(str_replace('.php', '.json', __FILE__)), true)[0]);
$wh->setRecipientEmail('hello@world.com');
$wh->setTags([]);
$wh->setMetadata([]);
$wh->setReason('Bounced Address');
$wh->setDate(\DateTimeImmutable::createFromFormat('U', 1600112492));

return $wh;
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Mailer\Bridge\Sendgrid\Tests\Webhook;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Mailer\Bridge\Sendgrid\RemoteEvent\SendgridPayloadConverter;
use Symfony\Component\Mailer\Bridge\Sendgrid\Webhook\SendgridRequestParser;
use Symfony\Component\Webhook\Client\RequestParserInterface;
use Symfony\Component\Webhook\Exception\RejectWebhookException;
use Symfony\Component\Webhook\Test\AbstractRequestParserTestCase;

/**
* @author WoutervanderLoop.nl <info@woutervanderloop.nl>
*/
class SendgridMissingSignedRequestParserTest extends AbstractRequestParserTestCase
{
protected function createRequestParser(): RequestParserInterface
{
$this->expectException(RejectWebhookException::class);
$this->expectExceptionMessage('Signature is required.');

return new SendgridRequestParser(new SendgridPayloadConverter());
}

/**
* @see https://github.com/sendgrid/sendgrid-php/blob/9335dca98bc64456a72db73469d1dd67db72f6ea/test/unit/EventWebhookTest.php#L20
*/
protected function createRequest(string $payload): Request
{
return Request::create('/', 'POST', [], [], [], [
'Content-Type' => 'application/json',
], str_replace("\n", "\r\n", $payload));
}

protected function getSecret(): string
{
return 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE83T4O/n84iotIvIW4mdBgQ/7dAfSmpqIM8kF9mN1flpVKS3GRqe62gw+2fNNRaINXvVpiglSI8eNEc6wEA3F+g==';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Mailer\Bridge\Sendgrid\Tests\Webhook;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Mailer\Bridge\Sendgrid\RemoteEvent\SendgridPayloadConverter;
use Symfony\Component\Mailer\Bridge\Sendgrid\Webhook\SendgridRequestParser;
use Symfony\Component\Webhook\Client\RequestParserInterface;
use Symfony\Component\Webhook\Test\AbstractRequestParserTestCase;

/**
* @author WoutervanderLoop.nl <info@woutervanderloop.nl>
*/
class SendgridSignedRequestParserTest extends AbstractRequestParserTestCase
{
protected function createRequestParser(): RequestParserInterface
{
return new SendgridRequestParser(new SendgridPayloadConverter(), true);
}

/**
* @see https://github.com/sendgrid/sendgrid-php/blob/9335dca98bc64456a72db73469d1dd67db72f6ea/test/unit/EventWebhookTest.php#L20
*/
protected function createRequest(string $payload): Request
{
return Request::create('/', 'POST', [], [], [], [
'Content-Type' => 'application/json',
'HTTP_X-Twilio-Email-Event-Webhook-Signature' => 'MEUCIGHQVtGj+Y3LkG9fLcxf3qfI10QysgDWmMOVmxG0u6ZUAiEAyBiXDWzM+uOe5W0JuG+luQAbPIqHh89M15TluLtEZtM=',
'HTTP_X-Twilio-Email-Event-Webhook-Timestamp' => '1600112502',
], str_replace("\n", "\r\n", $payload));
}

protected function getSecret(): string
{
return 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE83T4O/n84iotIvIW4mdBgQ/7dAfSmpqIM8kF9mN1flpVKS3GRqe62gw+2fNNRaINXvVpiglSI8eNEc6wEA3F+g==';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Mailer\Bridge\Sendgrid\Tests\Webhook;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Mailer\Bridge\Sendgrid\RemoteEvent\SendgridPayloadConverter;
use Symfony\Component\Mailer\Bridge\Sendgrid\Webhook\SendgridRequestParser;
use Symfony\Component\Webhook\Client\RequestParserInterface;
use Symfony\Component\Webhook\Test\AbstractRequestParserTestCase;

/**
* @author WoutervanderLoop.nl <info@woutervanderloop.nl>
*/
class SendgridUnsignedRequestParserTest extends AbstractRequestParserTestCase
{
protected function createRequestParser(): RequestParserInterface
{
return new SendgridRequestParser(new SendgridPayloadConverter());
}

/**
* @see https://github.com/sendgrid/sendgrid-php/blob/9335dca98bc64456a72db73469d1dd67db72f6ea/test/unit/EventWebhookTest.php#L20
*/
protected function createRequest(string $payload): Request
{
return Request::create('/', 'POST', [], [], [], [
'Content-Type' => 'application/json',
], str_replace("\n", "\r\n", $payload));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Mailer\Bridge\Sendgrid\Tests\Webhook;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Mailer\Bridge\Sendgrid\RemoteEvent\SendgridPayloadConverter;
use Symfony\Component\Mailer\Bridge\Sendgrid\Webhook\SendgridRequestParser;
use Symfony\Component\Webhook\Client\RequestParserInterface;
use Symfony\Component\Webhook\Exception\RejectWebhookException;
use Symfony\Component\Webhook\Test\AbstractRequestParserTestCase;

/**
* @author WoutervanderLoop.nl <info@woutervanderloop.nl>
*/
class SendgridWrongSecretRequestParserTest extends AbstractRequestParserTestCase
{
protected function createRequestParser(): RequestParserInterface
{
$this->expectException(RejectWebhookException::class);
$this->expectExceptionMessage('Public key is wrong.');

return new SendgridRequestParser(new SendgridPayloadConverter());
}

/**
* @see https://github.com/sendgrid/sendgrid-php/blob/9335dca98bc64456a72db73469d1dd67db72f6ea/test/unit/EventWebhookTest.php#L20
*/
protected function createRequest(string $payload): Request
{
return Request::create('/', 'POST', [], [], [], [
'Content-Type' => 'application/json',
'HTTP_X-Twilio-Email-Event-Webhook-Signature' => 'MEUCIGHQVtGj+Y3LkG9fLcxf3qfI10QysgDWmMOVmxG0u6ZUAiEAyBiXDWzM+uOe5W0JuG+luQAbPIqHh89M15TluLtEZtM=',
'HTTP_X-Twilio-Email-Event-Webhook-Timestamp' => '1600112502',
], str_replace("\n", "\r\n", $payload));
}

protected function getSecret(): string
{
return 'incorrect';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Mailer\Bridge\Sendgrid\Tests\Webhook;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Mailer\Bridge\Sendgrid\RemoteEvent\SendgridPayloadConverter;
use Symfony\Component\Mailer\Bridge\Sendgrid\Webhook\SendgridRequestParser;
use Symfony\Component\Webhook\Client\RequestParserInterface;
use Symfony\Component\Webhook\Exception\RejectWebhookException;
use Symfony\Component\Webhook\Test\AbstractRequestParserTestCase;

/**
* @author WoutervanderLoop.nl <info@woutervanderloop.nl>
*/
class SendgridWrongSignatureRequestParserTest extends AbstractRequestParserTestCase
{
protected function createRequestParser(): RequestParserInterface
{
$this->expectException(RejectWebhookException::class);
$this->expectExceptionMessage('Signature is wrong.');

return new SendgridRequestParser(new SendgridPayloadConverter());
}

/**
* @see https://github.com/sendgrid/sendgrid-php/blob/9335dca98bc64456a72db73469d1dd67db72f6ea/test/unit/EventWebhookTest.php#L20
*/
protected function createRequest(string $payload): Request
{
return Request::create('/', 'POST', [], [], [], [
'Content-Type' => 'application/json',
'HTTP_X-Twilio-Email-Event-Webhook-Signature' => 'incorrect',
'HTTP_X-Twilio-Email-Event-Webhook-Timestamp' => '1600112502',
], str_replace("\n", "\r\n", $payload));
}

protected function getSecret(): string
{
return 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE83T4O/n84iotIvIW4mdBgQ/7dAfSmpqIM8kF9mN1flpVKS3GRqe62gw+2fNNRaINXvVpiglSI8eNEc6wEA3F+g==';
}
}

0 comments on commit bb57ad0

Please sign in to comment.