diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 981a865672296..4d7df44ec53e3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -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) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_webhook.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_webhook.php index 7780b3df51e78..30ea50dade127 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_webhook.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_webhook.php @@ -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() @@ -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') ; }; diff --git a/src/Symfony/Component/Mailer/Bridge/Sendgrid/CHANGELOG.md b/src/Symfony/Component/Mailer/Bridge/Sendgrid/CHANGELOG.md index 40a44c58c4614..67c32a2d7ebd4 100644 --- a/src/Symfony/Component/Mailer/Bridge/Sendgrid/CHANGELOG.md +++ b/src/Symfony/Component/Mailer/Bridge/Sendgrid/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +6.4 +--- + + * Add support for webhooks + 5.4 --- diff --git a/src/Symfony/Component/Mailer/Bridge/Sendgrid/README.md b/src/Symfony/Component/Mailer/Bridge/Sendgrid/README.md index 7482d7b8903a2..1e7bb2020d79a 100644 --- a/src/Symfony/Component/Mailer/Bridge/Sendgrid/README.md +++ b/src/Symfony/Component/Mailer/Bridge/Sendgrid/README.md @@ -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 --------- diff --git a/src/Symfony/Component/Mailer/Bridge/Sendgrid/RemoteEvent/SendgridPayloadConverter.php b/src/Symfony/Component/Mailer/Bridge/Sendgrid/RemoteEvent/SendgridPayloadConverter.php new file mode 100644 index 0000000000000..ff6753384431e --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Sendgrid/RemoteEvent/SendgridPayloadConverter.php @@ -0,0 +1,58 @@ + + * + * 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 + */ +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; + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Webhook/Fixtures/webhook.json b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Webhook/Fixtures/webhook.json new file mode 100644 index 0000000000000..fed081a6ba9c6 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Webhook/Fixtures/webhook.json @@ -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":"","timestamp":1600112492}] diff --git a/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Webhook/Fixtures/webhook.php b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Webhook/Fixtures/webhook.php new file mode 100644 index 0000000000000..e762c87a853f8 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Webhook/Fixtures/webhook.php @@ -0,0 +1,12 @@ +setRecipientEmail('hello@world.com'); +$wh->setTags([]); +$wh->setMetadata([]); +$wh->setReason('Bounced Address'); +$wh->setDate(\DateTimeImmutable::createFromFormat('U', 1600112492)); + +return $wh; diff --git a/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Webhook/SendgridMissingSignedRequestParserTest.php b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Webhook/SendgridMissingSignedRequestParserTest.php new file mode 100644 index 0000000000000..f6aa96f702e2a --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Webhook/SendgridMissingSignedRequestParserTest.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\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 + */ +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=='; + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Webhook/SendgridSignedRequestParserTest.php b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Webhook/SendgridSignedRequestParserTest.php new file mode 100644 index 0000000000000..aa15c5df0e6ab --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Webhook/SendgridSignedRequestParserTest.php @@ -0,0 +1,46 @@ + + * + * 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 + */ +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=='; + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Webhook/SendgridUnsignedRequestParserTest.php b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Webhook/SendgridUnsignedRequestParserTest.php new file mode 100644 index 0000000000000..b93a327ecc912 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Webhook/SendgridUnsignedRequestParserTest.php @@ -0,0 +1,39 @@ + + * + * 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 + */ +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)); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Webhook/SendgridWrongSecretRequestParserTest.php b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Webhook/SendgridWrongSecretRequestParserTest.php new file mode 100644 index 0000000000000..e5f50fab5ad5a --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Webhook/SendgridWrongSecretRequestParserTest.php @@ -0,0 +1,50 @@ + + * + * 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 + */ +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'; + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Webhook/SendgridWrongSignatureRequestParserTest.php b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Webhook/SendgridWrongSignatureRequestParserTest.php new file mode 100644 index 0000000000000..b217f1e882054 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Webhook/SendgridWrongSignatureRequestParserTest.php @@ -0,0 +1,50 @@ + + * + * 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 + */ +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=='; + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Sendgrid/Webhook/SendgridRequestParser.php b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Webhook/SendgridRequestParser.php new file mode 100644 index 0000000000000..ecae4205ccc4b --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Webhook/SendgridRequestParser.php @@ -0,0 +1,108 @@ + + * + * 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\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\Sendgrid\RemoteEvent\SendgridPayloadConverter; +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; + +/** + * @author WoutervanderLoop.nl + */ +final class SendgridRequestParser extends AbstractRequestParser +{ + public function __construct( + private readonly SendgridPayloadConverter $converter, + ) { + } + + protected function getRequestMatcher(): RequestMatcherInterface + { + return new ChainRequestMatcher([ + new MethodRequestMatcher('POST'), + new IsJsonRequestMatcher(), + ]); + } + + protected function doParse(Request $request, string $secret): ?AbstractMailerEvent + { + $content = $request->toArray(); + if ( + !isset($content[0]['email']) + || !isset($content[0]['timestamp']) + || !isset($content[0]['event']) + || !isset($content[0]['sg_message_id']) + ) { + throw new RejectWebhookException(406, 'Payload is malformed.'); + } + + if ($secret) { + if (!$request->headers->get('X-Twilio-Email-Event-Webhook-Signature') + || !$request->headers->get('X-Twilio-Email-Event-Webhook-Timestamp') + ) { + throw new RejectWebhookException(406, 'Signature is required.'); + } + + $this->validateSignature( + $request->headers->get('X-Twilio-Email-Event-Webhook-Signature'), + $request->headers->get('X-Twilio-Email-Event-Webhook-Timestamp'), + $request->getContent(), + $secret, + ); + } + + try { + return $this->converter->convert($content[0]); + } catch (ParseException $e) { + throw new RejectWebhookException(406, $e->getMessage(), $e); + } + } + + /** + * Verify signed event webhook requests. + * + * @param string $signature value obtained from the + * 'X-Twilio-Email-Event-Webhook-Signature' header + * @param string $timestamp value obtained from the + * 'X-Twilio-Email-Event-Webhook-Timestamp' header + * @param string $payload event payload in the request body + * @param string $secret base64-encoded DER public key + * + * @see https://docs.sendgrid.com/for-developers/tracking-events/getting-started-event-webhook-security-features + */ + private function validateSignature( + string $signature, + string $timestamp, + string $payload, + string $secret, + ): void { + $timestampedPayload = $timestamp.$payload; + + // Sendgrid provides the verification key as base64-encoded DER data. Openssl wants a PEM format, which is a multiline version of the base64 data. + $pemKey = "-----BEGIN PUBLIC KEY-----\n".chunk_split($secret, 64, "\n")."-----END PUBLIC KEY-----\n"; + + if (!$publicKey = openssl_pkey_get_public($pemKey)) { + throw new RejectWebhookException(406, 'Public key is wrong.'); + } + + if (1 !== openssl_verify($timestampedPayload, base64_decode($signature), $publicKey, \OPENSSL_ALGO_SHA256)) { + throw new RejectWebhookException(406, 'Signature is wrong.'); + } + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Sendgrid/composer.json b/src/Symfony/Component/Mailer/Bridge/Sendgrid/composer.json index 460b176b58374..7fbad0f51513a 100644 --- a/src/Symfony/Component/Mailer/Bridge/Sendgrid/composer.json +++ b/src/Symfony/Component/Mailer/Bridge/Sendgrid/composer.json @@ -20,10 +20,12 @@ "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.3|^7.0" }, "conflict": { - "symfony/mime": "<6.2" + "symfony/mime": "<6.2", + "symfony/http-foundation": "<6.2" }, "autoload": { "psr-4": { "Symfony\\Component\\Mailer\\Bridge\\Sendgrid\\": "" },