├── CHANGELOG.md ├── LICENSE ├── README.md ├── RemoteEvent └── MailchimpPayloadConverter.php ├── Transport ├── MandrillApiTransport.php ├── MandrillHeadersTrait.php ├── MandrillHttpTransport.php ├── MandrillSmtpTransport.php └── MandrillTransportFactory.php ├── Webhook └── MailchimpRequestParser.php └── composer.json /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | 7.2 5 | --- 6 | 7 | * Add support for webhook 8 | 9 | 4.4.0 10 | ----- 11 | 12 | * [BC BREAK] Renamed and moved `Symfony\Component\Mailer\Bridge\Mailchimp\Http\Api\MandrillTransport` 13 | to `Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillApiTransport`, `Symfony\Component\Mailer\Bridge\Mailchimp\Http\MandrillTransport` 14 | to `Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillHttpTransport`, `Symfony\Component\Mailer\Bridge\Mailchimp\Smtp\MandrillTransport` 15 | to `Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillSmtpTransport`. 16 | 17 | 4.3.0 18 | ----- 19 | 20 | * Added the bridge 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019-present Fabien Potencier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Mailchimp Mailer 2 | ================ 3 | 4 | Provides Mandrill integration for Symfony Mailer. 5 | 6 | Configuration example: 7 | 8 | ```env 9 | # SMTP 10 | MAILER_DSN=mandrill+smtp://USERNAME:PASSWORD@default 11 | 12 | # HTTP 13 | MAILER_DSN=mandrill+https://KEY@default 14 | 15 | # API 16 | MAILER_DSN=mandrill+api://KEY@default 17 | ``` 18 | 19 | where: 20 | - `KEY` is your Mailchimp API key 21 | 22 | Resources 23 | --------- 24 | 25 | * [Contributing](https://symfony.com/doc/current/contributing/index.html) 26 | * [Report issues](https://github.com/symfony/symfony/issues) and 27 | [send Pull Requests](https://github.com/symfony/symfony/pulls) 28 | in the [main Symfony repository](https://github.com/symfony/symfony) 29 | -------------------------------------------------------------------------------- /RemoteEvent/MailchimpPayloadConverter.php: -------------------------------------------------------------------------------- 1 | 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\Mailchimp\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 | final class MailchimpPayloadConverter implements PayloadConverterInterface 21 | { 22 | public function convert(array $payload): AbstractMailerEvent 23 | { 24 | if (\in_array($payload['event'], ['send', 'deferral', 'soft_bounce', 'hard_bounce', 'delivered', 'reject'], true)) { 25 | $name = match ($payload['event']) { 26 | 'send' => MailerDeliveryEvent::RECEIVED, 27 | 'deferral', => MailerDeliveryEvent::DEFERRED, 28 | 'soft_bounce', 'hard_bounce' => MailerDeliveryEvent::BOUNCE, 29 | 'delivered' => MailerDeliveryEvent::DELIVERED, 30 | 'reject' => MailerDeliveryEvent::DROPPED, 31 | }; 32 | 33 | $event = new MailerDeliveryEvent($name, $payload['msg']['_id'], $payload); 34 | // reason is only available on failed messages 35 | $event->setReason($this->getReason($payload)); 36 | } else { 37 | $name = match ($payload['event']) { 38 | 'click' => MailerEngagementEvent::CLICK, 39 | 'open' => MailerEngagementEvent::OPEN, 40 | 'spam' => MailerEngagementEvent::SPAM, 41 | 'unsub' => MailerEngagementEvent::UNSUBSCRIBE, 42 | default => throw new ParseException(\sprintf('Unsupported event "%s".', $payload['event'])), 43 | }; 44 | $event = new MailerEngagementEvent($name, $payload['msg']['_id'], $payload); 45 | } 46 | 47 | if (!$date = \DateTimeImmutable::createFromFormat('U', $payload['msg']['ts'])) { 48 | throw new ParseException(\sprintf('Invalid date "%s".', $payload['msg']['ts'])); 49 | } 50 | $event->setDate($date); 51 | $event->setRecipientEmail($payload['msg']['email']); 52 | $event->setMetadata($payload['msg']['metadata']); 53 | $event->setTags($payload['msg']['tags']); 54 | 55 | return $event; 56 | } 57 | 58 | private function getReason(array $payload): string 59 | { 60 | if (null !== $payload['msg']['diag']) { 61 | return $payload['msg']['diag']; 62 | } 63 | if (null !== $payload['msg']['bounce_description']) { 64 | return $payload['msg']['bounce_description']; 65 | } 66 | 67 | if (null !== $payload['msg']['smtp_events'] && [] !== $payload['msg']['smtp_events']) { 68 | $reasons = []; 69 | foreach ($payload['msg']['smtp_events'] as $event) { 70 | $reasons[] = \sprintf('type: %s diag: %s', $event['type'], $event['diag']); 71 | } 72 | 73 | // Return concatenated reasons or an empty string if no reasons found 74 | return implode(' ', $reasons); 75 | } 76 | 77 | return ''; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Transport/MandrillApiTransport.php: -------------------------------------------------------------------------------- 1 | 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\Mailchimp\Transport; 13 | 14 | use Psr\EventDispatcher\EventDispatcherInterface; 15 | use Psr\Log\LoggerInterface; 16 | use Symfony\Component\Mailer\Envelope; 17 | use Symfony\Component\Mailer\Exception\HttpTransportException; 18 | use Symfony\Component\Mailer\Header\MetadataHeader; 19 | use Symfony\Component\Mailer\Header\TagHeader; 20 | use Symfony\Component\Mailer\SentMessage; 21 | use Symfony\Component\Mailer\Transport\AbstractApiTransport; 22 | use Symfony\Component\Mime\Email; 23 | use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; 24 | use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; 25 | use Symfony\Contracts\HttpClient\HttpClientInterface; 26 | use Symfony\Contracts\HttpClient\ResponseInterface; 27 | 28 | /** 29 | * @author Kevin Verschaeve 30 | */ 31 | class MandrillApiTransport extends AbstractApiTransport 32 | { 33 | private const HOST = 'mandrillapp.com'; 34 | 35 | public function __construct( 36 | #[\SensitiveParameter] private string $key, 37 | ?HttpClientInterface $client = null, 38 | ?EventDispatcherInterface $dispatcher = null, 39 | ?LoggerInterface $logger = null, 40 | ) { 41 | parent::__construct($client, $dispatcher, $logger); 42 | } 43 | 44 | public function __toString(): string 45 | { 46 | return \sprintf('mandrill+api://%s', $this->getEndpoint()); 47 | } 48 | 49 | protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $envelope): ResponseInterface 50 | { 51 | $response = $this->client->request('POST', 'https://'.$this->getEndpoint().'/api/1.0/messages/send.json', [ 52 | 'json' => $this->getPayload($email, $envelope), 53 | ]); 54 | 55 | try { 56 | $statusCode = $response->getStatusCode(); 57 | $result = $response->toArray(false); 58 | } catch (DecodingExceptionInterface) { 59 | throw new HttpTransportException('Unable to send an email: '.$response->getContent(false).\sprintf(' (code %d).', $statusCode), $response); 60 | } catch (TransportExceptionInterface $e) { 61 | throw new HttpTransportException('Could not reach the remote Mandrill server.', $response, 0, $e); 62 | } 63 | 64 | if (200 !== $statusCode) { 65 | if ('error' === ($result['status'] ?? false)) { 66 | throw new HttpTransportException('Unable to send an email: '.$result['message'].\sprintf(' (code %d).', $result['code']), $response); 67 | } 68 | 69 | throw new HttpTransportException(\sprintf('Unable to send an email (code %d).', $result['code']), $response); 70 | } 71 | 72 | $firstRecipient = reset($result); 73 | $sentMessage->setMessageId($firstRecipient['_id']); 74 | 75 | return $response; 76 | } 77 | 78 | private function getEndpoint(): ?string 79 | { 80 | return ($this->host ?: self::HOST).($this->port ? ':'.$this->port : ''); 81 | } 82 | 83 | private function getPayload(Email $email, Envelope $envelope): array 84 | { 85 | $payload = [ 86 | 'key' => $this->key, 87 | 'message' => [ 88 | 'html' => $email->getHtmlBody(), 89 | 'text' => $email->getTextBody(), 90 | 'subject' => $email->getSubject(), 91 | 'from_email' => $envelope->getSender()->getAddress(), 92 | 'to' => $this->getRecipientsPayload($email, $envelope), 93 | ], 94 | ]; 95 | 96 | if ('' !== $envelope->getSender()->getName()) { 97 | $payload['message']['from_name'] = $envelope->getSender()->getName(); 98 | } 99 | 100 | foreach ($email->getAttachments() as $attachment) { 101 | $headers = $attachment->getPreparedHeaders(); 102 | $disposition = $headers->getHeaderBody('Content-Disposition'); 103 | 104 | $att = [ 105 | 'content' => $attachment->bodyToString(), 106 | 'type' => $headers->get('Content-Type')->getBody(), 107 | ]; 108 | 109 | if ($name = $headers->getHeaderParameter('Content-Disposition', 'name')) { 110 | $att['name'] = $name; 111 | } 112 | 113 | if ('inline' === $disposition) { 114 | $payload['message']['images'][] = $att; 115 | } else { 116 | $payload['message']['attachments'][] = $att; 117 | } 118 | } 119 | 120 | $headersToBypass = ['from', 'to', 'cc', 'bcc', 'subject', 'content-type']; 121 | foreach ($email->getHeaders()->all() as $name => $header) { 122 | if (\in_array($name, $headersToBypass, true)) { 123 | continue; 124 | } 125 | 126 | if ($header instanceof TagHeader) { 127 | $payload['message']['tags'] = array_merge( 128 | $payload['message']['tags'] ?? [], 129 | explode(',', $header->getValue()) 130 | ); 131 | 132 | continue; 133 | } 134 | 135 | if ($header instanceof MetadataHeader) { 136 | $payload['message']['metadata'][$header->getKey()] = $header->getValue(); 137 | 138 | continue; 139 | } 140 | 141 | $payload['message']['headers'][$header->getName()] = $header->getBodyAsString(); 142 | } 143 | 144 | return $payload; 145 | } 146 | 147 | private function getRecipientsPayload(Email $email, Envelope $envelope): array 148 | { 149 | $recipients = []; 150 | foreach ($envelope->getRecipients() as $recipient) { 151 | $type = 'to'; 152 | if (\in_array($recipient, $email->getBcc(), true)) { 153 | $type = 'bcc'; 154 | } elseif (\in_array($recipient, $email->getCc(), true)) { 155 | $type = 'cc'; 156 | } 157 | 158 | $recipientPayload = [ 159 | 'email' => $recipient->getAddress(), 160 | 'type' => $type, 161 | ]; 162 | 163 | if ('' !== $recipient->getName()) { 164 | $recipientPayload['name'] = $recipient->getName(); 165 | } 166 | 167 | $recipients[] = $recipientPayload; 168 | } 169 | 170 | return $recipients; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /Transport/MandrillHeadersTrait.php: -------------------------------------------------------------------------------- 1 | 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\Mailchimp\Transport; 13 | 14 | use Symfony\Component\Mailer\Envelope; 15 | use Symfony\Component\Mailer\Header\MetadataHeader; 16 | use Symfony\Component\Mailer\Header\TagHeader; 17 | use Symfony\Component\Mailer\SentMessage; 18 | use Symfony\Component\Mime\Message; 19 | use Symfony\Component\Mime\RawMessage; 20 | 21 | /** 22 | * @author Kevin Bond 23 | */ 24 | trait MandrillHeadersTrait 25 | { 26 | public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage 27 | { 28 | if ($message instanceof Message) { 29 | $this->addMandrillHeaders($message); 30 | } 31 | 32 | return parent::send($message, $envelope); 33 | } 34 | 35 | private function addMandrillHeaders(Message $message): void 36 | { 37 | $headers = $message->getHeaders(); 38 | $metadata = []; 39 | $tags = []; 40 | 41 | foreach ($headers->all() as $name => $header) { 42 | if ($header instanceof TagHeader) { 43 | $tags[] = $header->getValue(); 44 | $headers->remove($name); 45 | } elseif ($header instanceof MetadataHeader) { 46 | $metadata[$header->getKey()] = $header->getValue(); 47 | $headers->remove($name); 48 | } 49 | } 50 | 51 | if ($tags) { 52 | $headers->addTextHeader('X-MC-Tags', implode(',', $tags)); 53 | } 54 | 55 | if ($metadata) { 56 | $headers->addTextHeader('X-MC-Metadata', json_encode($metadata)); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Transport/MandrillHttpTransport.php: -------------------------------------------------------------------------------- 1 | 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\Mailchimp\Transport; 13 | 14 | use Psr\EventDispatcher\EventDispatcherInterface; 15 | use Psr\Log\LoggerInterface; 16 | use Symfony\Component\Mailer\Exception\HttpTransportException; 17 | use Symfony\Component\Mailer\SentMessage; 18 | use Symfony\Component\Mailer\Transport\AbstractHttpTransport; 19 | use Symfony\Component\Mime\Address; 20 | use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; 21 | use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; 22 | use Symfony\Contracts\HttpClient\HttpClientInterface; 23 | use Symfony\Contracts\HttpClient\ResponseInterface; 24 | 25 | /** 26 | * @author Kevin Verschaeve 27 | */ 28 | class MandrillHttpTransport extends AbstractHttpTransport 29 | { 30 | use MandrillHeadersTrait; 31 | 32 | private const HOST = 'mandrillapp.com'; 33 | 34 | public function __construct( 35 | #[\SensitiveParameter] private string $key, 36 | ?HttpClientInterface $client = null, 37 | ?EventDispatcherInterface $dispatcher = null, 38 | ?LoggerInterface $logger = null, 39 | ) { 40 | parent::__construct($client, $dispatcher, $logger); 41 | } 42 | 43 | public function __toString(): string 44 | { 45 | return \sprintf('mandrill+https://%s', $this->getEndpoint()); 46 | } 47 | 48 | protected function doSendHttp(SentMessage $message): ResponseInterface 49 | { 50 | $envelope = $message->getEnvelope(); 51 | $response = $this->client->request('POST', 'https://'.$this->getEndpoint().'/api/1.0/messages/send-raw.json', [ 52 | 'json' => [ 53 | 'key' => $this->key, 54 | 'to' => array_map(fn (Address $recipient): string => $recipient->getAddress(), $envelope->getRecipients()), 55 | 'from_email' => $envelope->getSender()->getAddress(), 56 | 'from_name' => $envelope->getSender()->getName(), 57 | 'raw_message' => $message->toString(), 58 | ], 59 | ]); 60 | 61 | try { 62 | $statusCode = $response->getStatusCode(); 63 | $result = $response->toArray(false); 64 | } catch (DecodingExceptionInterface) { 65 | throw new HttpTransportException('Unable to send an email: '.$response->getContent(false).\sprintf(' (code %d).', $statusCode), $response); 66 | } catch (TransportExceptionInterface $e) { 67 | throw new HttpTransportException('Could not reach the remote Mandrill server.', $response, 0, $e); 68 | } 69 | 70 | if (200 !== $statusCode) { 71 | if ('error' === ($result['status'] ?? false)) { 72 | throw new HttpTransportException('Unable to send an email: '.$result['message'].\sprintf(' (code %d).', $result['code']), $response); 73 | } 74 | 75 | throw new HttpTransportException(\sprintf('Unable to send an email (code %d).', $result['code']), $response); 76 | } 77 | 78 | $message->setMessageId($result[0]['_id']); 79 | 80 | return $response; 81 | } 82 | 83 | private function getEndpoint(): ?string 84 | { 85 | return ($this->host ?: self::HOST).($this->port ? ':'.$this->port : ''); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Transport/MandrillSmtpTransport.php: -------------------------------------------------------------------------------- 1 | 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\Mailchimp\Transport; 13 | 14 | use Psr\EventDispatcher\EventDispatcherInterface; 15 | use Psr\Log\LoggerInterface; 16 | use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; 17 | 18 | /** 19 | * @author Kevin Verschaeve 20 | */ 21 | class MandrillSmtpTransport extends EsmtpTransport 22 | { 23 | use MandrillHeadersTrait; 24 | 25 | public function __construct(string $username, #[\SensitiveParameter] string $password, ?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null) 26 | { 27 | parent::__construct('smtp.mandrillapp.com', 587, false, $dispatcher, $logger); 28 | 29 | $this->setUsername($username); 30 | $this->setPassword($password); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Transport/MandrillTransportFactory.php: -------------------------------------------------------------------------------- 1 | 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\Mailchimp\Transport; 13 | 14 | use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; 15 | use Symfony\Component\Mailer\Transport\AbstractTransportFactory; 16 | use Symfony\Component\Mailer\Transport\Dsn; 17 | use Symfony\Component\Mailer\Transport\TransportInterface; 18 | 19 | /** 20 | * @author Konstantin Myakshin 21 | */ 22 | final class MandrillTransportFactory extends AbstractTransportFactory 23 | { 24 | public function create(Dsn $dsn): TransportInterface 25 | { 26 | $scheme = $dsn->getScheme(); 27 | $user = $this->getUser($dsn); 28 | $host = 'default' === $dsn->getHost() ? null : $dsn->getHost(); 29 | $port = $dsn->getPort(); 30 | 31 | if ('mandrill+api' === $scheme) { 32 | return (new MandrillApiTransport($user, $this->client, $this->dispatcher, $this->logger))->setHost($host)->setPort($port); 33 | } 34 | 35 | if ('mandrill+https' === $scheme || 'mandrill' === $scheme) { 36 | return (new MandrillHttpTransport($user, $this->client, $this->dispatcher, $this->logger))->setHost($host)->setPort($port); 37 | } 38 | 39 | if ('mandrill+smtp' === $scheme || 'mandrill+smtps' === $scheme) { 40 | $password = $this->getPassword($dsn); 41 | 42 | return new MandrillSmtpTransport($user, $password, $this->dispatcher, $this->logger); 43 | } 44 | 45 | throw new UnsupportedSchemeException($dsn, 'mandrill', $this->getSupportedSchemes()); 46 | } 47 | 48 | protected function getSupportedSchemes(): array 49 | { 50 | return ['mandrill', 'mandrill+api', 'mandrill+https', 'mandrill+smtp', 'mandrill+smtps']; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Webhook/MailchimpRequestParser.php: -------------------------------------------------------------------------------- 1 | 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\Mailchimp\Webhook; 13 | 14 | use Symfony\Component\HttpFoundation\ChainRequestMatcher; 15 | use Symfony\Component\HttpFoundation\Request; 16 | use Symfony\Component\HttpFoundation\RequestMatcher\IsJsonRequestMatcher; 17 | use Symfony\Component\HttpFoundation\RequestMatcher\MethodRequestMatcher; 18 | use Symfony\Component\HttpFoundation\RequestMatcherInterface; 19 | use Symfony\Component\Mailer\Bridge\Mailchimp\RemoteEvent\MailchimpPayloadConverter; 20 | use Symfony\Component\RemoteEvent\Exception\ParseException; 21 | use Symfony\Component\RemoteEvent\RemoteEvent; 22 | use Symfony\Component\Webhook\Client\AbstractRequestParser; 23 | use Symfony\Component\Webhook\Exception\RejectWebhookException; 24 | 25 | final class MailchimpRequestParser extends AbstractRequestParser 26 | { 27 | public function __construct( 28 | private readonly MailchimpPayloadConverter $converter, 29 | ) { 30 | } 31 | 32 | protected function getRequestMatcher(): RequestMatcherInterface 33 | { 34 | return new ChainRequestMatcher([ 35 | new MethodRequestMatcher('POST'), 36 | new IsJsonRequestMatcher(), 37 | ]); 38 | } 39 | 40 | protected function doParse(Request $request, #[\SensitiveParameter] string $secret): RemoteEvent|array|null 41 | { 42 | $content = $request->toArray(); 43 | if (!isset($content['mandrill_events'][0]['event']) 44 | || !isset($content['mandrill_events'][0]['msg']) 45 | ) { 46 | throw new RejectWebhookException(400, 'Payload malformed.'); 47 | } 48 | 49 | $this->validateSignature($content, $secret, $request->getUri(), $request->headers->get('X-Mandrill-Signature')); 50 | 51 | try { 52 | return array_map($this->converter->convert(...), $content['mandrill_events']); 53 | } catch (ParseException $e) { 54 | throw new RejectWebhookException(406, $e->getMessage(), $e); 55 | } 56 | } 57 | 58 | /** 59 | * @see https://mailchimp.com/developer/transactional/guides/track-respond-activity-webhooks/#authenticating-webhook-requests 60 | */ 61 | private function validateSignature(array $content, string $secret, string $webhookUrl, ?string $mandrillHeaderSignature): void 62 | { 63 | if (null === $mandrillHeaderSignature || false === isset($content['mandrill_events'])) { 64 | throw new RejectWebhookException(400, 'Signature is wrong.'); 65 | } 66 | // First add url to signedData. 67 | $signedData = $webhookUrl; 68 | 69 | // When no params is set we know its a test and we set the key to test. 70 | if ('[]' === $content['mandrill_events']) { 71 | $secret = 'test-webhook'; 72 | } 73 | 74 | // Sort params and add to signed data. 75 | ksort($content); 76 | foreach ($content as $key => $value) { 77 | // Add keys and values. 78 | $signedData .= $key; 79 | $signedData .= \is_array($value) ? $this->stringifyArray($value) : $value; 80 | } 81 | 82 | if ($mandrillHeaderSignature !== base64_encode(hash_hmac('sha1', $signedData, $secret, true))) { 83 | throw new RejectWebhookException(400, 'Signature is wrong.'); 84 | } 85 | } 86 | 87 | /** 88 | * Recursively converts an array to a string representation. 89 | * 90 | * @param array $array the array to be converted 91 | */ 92 | private function stringifyArray(array $array): string 93 | { 94 | ksort($array); 95 | $result = ''; 96 | foreach ($array as $key => $value) { 97 | $result .= $key; 98 | if (\is_array($value)) { 99 | $result .= $this->stringifyArray($value); 100 | } else { 101 | $result .= $value; 102 | } 103 | } 104 | 105 | return $result; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symfony/mailchimp-mailer", 3 | "type": "symfony-mailer-bridge", 4 | "description": "Symfony Mailchimp Mailer Bridge", 5 | "keywords": [], 6 | "homepage": "https://symfony.com", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Fabien Potencier", 11 | "email": "fabien@symfony.com" 12 | }, 13 | { 14 | "name": "Symfony Community", 15 | "homepage": "https://symfony.com/contributors" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=8.2", 20 | "symfony/mailer": "^7.2" 21 | }, 22 | "require-dev": { 23 | "symfony/http-client": "^6.4|^7.0", 24 | "symfony/webhook": "^7.2" 25 | }, 26 | "conflict": { 27 | "symfony/webhook": "<7.2" 28 | }, 29 | "autoload": { 30 | "psr-4": { "Symfony\\Component\\Mailer\\Bridge\\Mailchimp\\": "" }, 31 | "exclude-from-classmap": [ 32 | "/Tests/" 33 | ] 34 | }, 35 | "minimum-stability": "dev" 36 | } 37 | --------------------------------------------------------------------------------