Skip to content

Commit dacde4d

Browse files
[Webhook][RemoteEvent] Add Sendgrid #50704
1 parent 52a9292 commit dacde4d

File tree

14 files changed

+367
-2
lines changed

14 files changed

+367
-2
lines changed

.appveyor.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ install:
4343
- echo extension=php_pdo_sqlite.dll >> php.ini-max
4444
- echo extension=php_curl.dll >> php.ini-max
4545
- echo extension=php_sodium.dll >> php.ini-max
46+
- echo extension=php_gmp.dll >> php.ini-max
4647
- copy /Y php.ini-max php.ini
4748
- cd c:\projects\symfony
4849
- appveyor DownloadFile https://getcomposer.org/download/latest-stable/composer.phar

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@
153153
"symfony/phpunit-bridge": "^5.4|^6.0|^7.0",
154154
"symfony/runtime": "self.version",
155155
"symfony/security-acl": "~2.8|~3.0",
156+
"starkbank/ecdsa": "^2.0",
156157
"twig/cssinliner-extra": "^2.12|^3",
157158
"twig/inky-extra": "^2.12|^3",
158159
"twig/markdown-extra": "^2.12|^3",

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2615,6 +2615,7 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co
26152615
$webhookRequestParsers = [
26162616
MailerBridge\Mailgun\Webhook\MailgunRequestParser::class => 'mailer.webhook.request_parser.mailgun',
26172617
MailerBridge\Postmark\Webhook\PostmarkRequestParser::class => 'mailer.webhook.request_parser.postmark',
2618+
MailerBridge\Sendgrid\Webhook\SendgridRequestParser::class => 'mailer.webhook.request_parser.sendgrid',
26182619
];
26192620

26202621
foreach ($webhookRequestParsers as $class => $service) {
@@ -2624,6 +2625,10 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co
26242625
$container->removeDefinition($service);
26252626
}
26262627
}
2628+
2629+
if (ContainerBuilder::willBeAvailable('symfony/sendgrid-mailer', MailerBridge\Sendgrid\Webhook\SendgridRequestParser::class, ['symfony/framework-bundle', 'symfony/mailer'])) {
2630+
$container->setParameter('mailer.webhook.sendgrid.validate_signature', false);
2631+
}
26272632
}
26282633

26292634
$envelopeListener = $container->getDefinition('mailer.envelope_listener');

src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_webhook.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
use Symfony\Component\Mailer\Bridge\Mailgun\Webhook\MailgunRequestParser;
1616
use Symfony\Component\Mailer\Bridge\Postmark\RemoteEvent\PostmarkPayloadConverter;
1717
use Symfony\Component\Mailer\Bridge\Postmark\Webhook\PostmarkRequestParser;
18+
use Symfony\Component\Mailer\Bridge\Sendgrid\RemoteEvent\SendgridPayloadConverter;
19+
use Symfony\Component\Mailer\Bridge\Sendgrid\Webhook\SendgridRequestParser;
1820

1921
return static function (ContainerConfigurator $container) {
2022
$container->services()
@@ -27,5 +29,13 @@
2729
->set('mailer.webhook.request_parser.postmark', PostmarkRequestParser::class)
2830
->args([service('mailer.payload_converter.postmark')])
2931
->alias(PostmarkRequestParser::class, 'mailer.webhook.request_parser.postmark')
32+
33+
->set('mailer.payload_converter.sendgrid', SendgridPayloadConverter::class)
34+
->set('mailer.webhook.request_parser.sendgrid', SendgridRequestParser::class)
35+
->args([
36+
service('mailer.payload_converter.sendgrid'),
37+
param('mailer.webhook.sendgrid.validate_signature'),
38+
])
39+
->alias(SendgridRequestParser::class, 'mailer.webhook.request_parser.sendgrid')
3040
;
3141
};

src/Symfony/Component/Mailer/Bridge/Sendgrid/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
6.4
5+
---
6+
7+
* Add support for webhooks
8+
49
5.4
510
---
611

src/Symfony/Component/Mailer/Bridge/Sendgrid/README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,36 @@ MAILER_DSN=sendgrid+api://KEY@default
1616
where:
1717
- `KEY` is your Sendgrid API Key
1818

19+
20+
Webhook:
21+
--------
22+
Create route:
23+
```yaml
24+
framework:
25+
webhook:
26+
routing:
27+
sendgrid:
28+
service: mailer.webhook.request_parser.sendgrid
29+
secret: '!SENDGRID_VALIDATION_SECRET!'
30+
```
31+
Create consumer:
32+
```php
33+
#[\Symfony\Component\RemoteEvent\Attribute\AsRemoteEventConsumer(name: 'sendgrid')]
34+
class SendGridConsumer implements ConsumerInterface
35+
{
36+
public function consume(RemoteEvent|MailerDeliveryEvent $event): void
37+
{
38+
//your code
39+
}
40+
}
41+
```
42+
43+
Default sendgrid enforces to validate the signature, to disable the signature validation:
44+
```yaml
45+
parameters:
46+
mailer.webhook.sendgrid.validate_signature: true
47+
```
48+
1949
Resources
2050
---------
2151
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Mailer\Bridge\Sendgrid\RemoteEvent;
13+
14+
use Symfony\Component\RemoteEvent\Event\Mailer\AbstractMailerEvent;
15+
use Symfony\Component\RemoteEvent\Event\Mailer\MailerDeliveryEvent;
16+
use Symfony\Component\RemoteEvent\Event\Mailer\MailerEngagementEvent;
17+
use Symfony\Component\RemoteEvent\Exception\ParseException;
18+
use Symfony\Component\RemoteEvent\PayloadConverterInterface;
19+
20+
/**
21+
* @author WoutervanderLoop.nl <info@woutervanderloop.nl>
22+
*/
23+
final class SendgridPayloadConverter implements PayloadConverterInterface
24+
{
25+
public function convert(array $payload): AbstractMailerEvent
26+
{
27+
if (\in_array($payload['event'], ['processed', 'delivered', 'bounce', 'dropped', 'deferred'], true)) {
28+
$name = match ($payload['event']) {
29+
'processed', 'delivered' => MailerDeliveryEvent::DELIVERED,
30+
'dropped' => MailerDeliveryEvent::DROPPED,
31+
'deferred' => MailerDeliveryEvent::DEFERRED,
32+
'bounce' => MailerDeliveryEvent::BOUNCE,
33+
};
34+
$event = new MailerDeliveryEvent($name, $payload['sg_message_id'], $payload);
35+
$event->setReason($payload['reason'] ?? '');
36+
} else {
37+
$name = match ($payload['event']) {
38+
'click' => MailerEngagementEvent::CLICK,
39+
'unsubscribe' => MailerEngagementEvent::UNSUBSCRIBE,
40+
'open' => MailerEngagementEvent::OPEN,
41+
'spamreport' => MailerEngagementEvent::SPAM,
42+
default => throw new ParseException(sprintf('Unsupported event "%s".', $payload['unsubscribe'])),
43+
};
44+
$event = new MailerEngagementEvent($name, $payload['sg_message_id'], $payload);
45+
}
46+
47+
if (!$date = \DateTimeImmutable::createFromFormat('U', $payload['timestamp'])) {
48+
throw new ParseException(sprintf('Invalid date "%s".', $payload['timestamp']));
49+
}
50+
51+
$event->setDate($date);
52+
$event->setRecipientEmail($payload['email']);
53+
$event->setMetadata([]);
54+
$event->setTags($payload['category'] ?? []);
55+
56+
return $event;
57+
}
58+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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}]
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
use Symfony\Component\RemoteEvent\Event\Mailer\MailerDeliveryEvent;
4+
5+
$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]);
6+
$wh->setRecipientEmail('hello@world.com');
7+
$wh->setTags([]);
8+
$wh->setMetadata([]);
9+
$wh->setReason('Bounced Address');
10+
$wh->setDate(\DateTimeImmutable::createFromFormat('U', 1600112492));
11+
12+
return $wh;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Mailer\Bridge\Sendgrid\Tests\Webhook;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\Mailer\Bridge\Sendgrid\RemoteEvent\SendgridPayloadConverter;
16+
use Symfony\Component\Mailer\Bridge\Sendgrid\Webhook\SendgridRequestParser;
17+
use Symfony\Component\Webhook\Client\RequestParserInterface;
18+
use Symfony\Component\Webhook\Exception\RejectWebhookException;
19+
use Symfony\Component\Webhook\Test\AbstractRequestParserTestCase;
20+
21+
/**
22+
* @author WoutervanderLoop.nl <info@woutervanderloop.nl>
23+
*/
24+
class SendgridMissingSignedRequestParserTest extends AbstractRequestParserTestCase
25+
{
26+
protected function createRequestParser(): RequestParserInterface
27+
{
28+
$this->expectException(RejectWebhookException::class);
29+
$this->expectExceptionMessage('Signature is required.');
30+
31+
return new SendgridRequestParser(new SendgridPayloadConverter(), true);
32+
}
33+
34+
/**
35+
* @see https://github.com/sendgrid/sendgrid-php/blob/9335dca98bc64456a72db73469d1dd67db72f6ea/test/unit/EventWebhookTest.php#L20
36+
*/
37+
protected function createRequest(string $payload): Request
38+
{
39+
return Request::create('/', 'POST', [], [], [], [
40+
'Content-Type' => 'application/json',
41+
], str_replace("\n", "\r\n", $payload));
42+
}
43+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy