├── CHANGELOG.md ├── Event └── PostmarkDeliveryEvent.php ├── LICENSE ├── README.md ├── RemoteEvent └── PostmarkPayloadConverter.php ├── Transport ├── MessageStreamHeader.php ├── PostmarkApiTransport.php ├── PostmarkSmtpTransport.php └── PostmarkTransportFactory.php ├── Webhook └── PostmarkRequestParser.php └── composer.json /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | 6.3 5 | --- 6 | 7 | * Add support for webhooks 8 | 9 | 4.4.0 10 | ----- 11 | 12 | * added `ReplyTo` option 13 | * [BC BREAK] Renamed and moved `Symfony\Component\Mailer\Bridge\Postmark\Http\Api\PostmarkTransport` 14 | to `Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkApiTransport`, `Symfony\Component\Mailer\Bridge\Postmark\Smtp\PostmarkTransport` 15 | to `Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkSmtpTransport`. 16 | 17 | 4.3.0 18 | ----- 19 | 20 | * Added the bridge 21 | -------------------------------------------------------------------------------- /Event/PostmarkDeliveryEvent.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\Postmark\Event; 13 | 14 | use Symfony\Component\Mime\Header\Headers; 15 | 16 | class PostmarkDeliveryEvent 17 | { 18 | public function __construct( 19 | private readonly string $message, 20 | private readonly int $errorCode, 21 | private readonly Headers $headers, 22 | ) { 23 | } 24 | 25 | public function getErrorCode(): int 26 | { 27 | return $this->errorCode; 28 | } 29 | 30 | public function getHeaders(): Headers 31 | { 32 | return $this->headers; 33 | } 34 | 35 | public function getMessage(): string 36 | { 37 | return $this->message; 38 | } 39 | 40 | public function getMessageId(): ?string 41 | { 42 | if (!$this->headers->has('Message-ID')) { 43 | return null; 44 | } 45 | 46 | return $this->headers->get('Message-ID')->getBodyAsString(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /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 | Postmark Bridge 2 | =============== 3 | 4 | Provides Postmark integration for Symfony Mailer. 5 | 6 | Configuration example: 7 | 8 | ```env 9 | # SMTP 10 | MAILER_DSN=postmark+smtp://ID@default 11 | 12 | # API 13 | MAILER_DSN=postmark+api://KEY@default 14 | ``` 15 | 16 | where: 17 | - `ID` is your Postmark Server Token 18 | - `KEY` is your Postmark Server Token 19 | 20 | Resources 21 | --------- 22 | 23 | * [Contributing](https://symfony.com/doc/current/contributing/index.html) 24 | * [Report issues](https://github.com/symfony/symfony/issues) and 25 | [send Pull Requests](https://github.com/symfony/symfony/pulls) 26 | in the [main Symfony repository](https://github.com/symfony/symfony) 27 | -------------------------------------------------------------------------------- /RemoteEvent/PostmarkPayloadConverter.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\Postmark\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 PostmarkPayloadConverter implements PayloadConverterInterface 21 | { 22 | public function convert(array $payload): AbstractMailerEvent 23 | { 24 | if (\in_array($payload['RecordType'], ['Delivery', 'Bounce'], true)) { 25 | $name = match ($payload['RecordType']) { 26 | 'Delivery' => MailerDeliveryEvent::DELIVERED, 27 | 'Bounce' => MailerDeliveryEvent::BOUNCE, 28 | }; 29 | $event = new MailerDeliveryEvent($name, $payload['MessageID'], $payload); 30 | $event->setReason($payload['Description'] ?? ''); 31 | } else { 32 | $name = match ($payload['RecordType']) { 33 | 'Click' => MailerEngagementEvent::CLICK, 34 | 'SubscriptionChange' => MailerEngagementEvent::UNSUBSCRIBE, 35 | 'Open' => MailerEngagementEvent::OPEN, 36 | 'SpamComplaint' => MailerEngagementEvent::SPAM, 37 | default => throw new ParseException(\sprintf('Unsupported event "%s".', $payload['RecordType'])), 38 | }; 39 | $event = new MailerEngagementEvent($name, $payload['MessageID'], $payload); 40 | } 41 | $payloadDate = match ($payload['RecordType']) { 42 | 'Delivery' => $payload['DeliveredAt'], 43 | 'Bounce' => $payload['BouncedAt'], 44 | 'Click' => $payload['ReceivedAt'], 45 | 'SubscriptionChange' => $payload['ChangedAt'], 46 | 'Open' => $payload['ReceivedAt'], 47 | 'SpamComplaint' => $payload['BouncedAt'], 48 | default => throw new ParseException(\sprintf('Unsupported event "%s".', $payload['RecordType'])), 49 | }; 50 | 51 | $date = \DateTimeImmutable::createFromFormat(\DateTimeInterface::ATOM, $payloadDate) 52 | // microseconds, 6 digits 53 | ?: \DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.uP', $payloadDate) 54 | // microseconds, 7 digits 55 | ?: \DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.u?P', $payloadDate); 56 | 57 | if (!$date) { 58 | throw new ParseException(\sprintf('Invalid date "%s".', $payloadDate)); 59 | } 60 | $event->setDate($date); 61 | $event->setRecipientEmail($payload['Recipient'] ?? $payload['Email']); 62 | 63 | if (isset($payload['Metadata'])) { 64 | $event->setMetadata($payload['Metadata']); 65 | } 66 | if (isset($payload['Tag'])) { 67 | $event->setTags([$payload['Tag']]); 68 | } 69 | 70 | return $event; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Transport/MessageStreamHeader.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\Postmark\Transport; 13 | 14 | use Symfony\Component\Mime\Header\UnstructuredHeader; 15 | 16 | final class MessageStreamHeader extends UnstructuredHeader 17 | { 18 | public function __construct(string $value) 19 | { 20 | parent::__construct('X-PM-Message-Stream', $value); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Transport/PostmarkApiTransport.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\Postmark\Transport; 13 | 14 | use Psr\EventDispatcher\EventDispatcherInterface; 15 | use Psr\Log\LoggerInterface; 16 | use Symfony\Component\Mailer\Bridge\Postmark\Event\PostmarkDeliveryEvent; 17 | use Symfony\Component\Mailer\Envelope; 18 | use Symfony\Component\Mailer\Exception\HttpTransportException; 19 | use Symfony\Component\Mailer\Exception\TransportException; 20 | use Symfony\Component\Mailer\Header\MetadataHeader; 21 | use Symfony\Component\Mailer\Header\TagHeader; 22 | use Symfony\Component\Mailer\SentMessage; 23 | use Symfony\Component\Mailer\Transport\AbstractApiTransport; 24 | use Symfony\Component\Mime\Email; 25 | use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; 26 | use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; 27 | use Symfony\Contracts\HttpClient\HttpClientInterface; 28 | use Symfony\Contracts\HttpClient\ResponseInterface; 29 | 30 | /** 31 | * @author Kevin Verschaeve 32 | */ 33 | class PostmarkApiTransport extends AbstractApiTransport 34 | { 35 | private const HOST = 'api.postmarkapp.com'; 36 | private const CODE_INACTIVE_RECIPIENT = 406; 37 | 38 | private ?string $messageStream = null; 39 | 40 | public function __construct( 41 | #[\SensitiveParameter] private string $key, 42 | ?HttpClientInterface $client = null, 43 | private ?EventDispatcherInterface $dispatcher = null, 44 | ?LoggerInterface $logger = null, 45 | ) { 46 | parent::__construct($client, $dispatcher, $logger); 47 | } 48 | 49 | /** 50 | * @return $this 51 | */ 52 | public function setMessageStream(string $messageStream): static 53 | { 54 | $this->messageStream = $messageStream; 55 | 56 | return $this; 57 | } 58 | 59 | public function __toString(): string 60 | { 61 | return \sprintf('postmark+api://%s', $this->getEndpoint()).($this->messageStream ? '?message_stream='.$this->messageStream : ''); 62 | } 63 | 64 | protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $envelope): ResponseInterface 65 | { 66 | $response = $this->client->request('POST', 'https://'.$this->getEndpoint().'/email', [ 67 | 'headers' => [ 68 | 'Accept' => 'application/json', 69 | 'X-Postmark-Server-Token' => $this->key, 70 | ], 71 | 'json' => $this->getPayload($email, $envelope), 72 | ]); 73 | 74 | try { 75 | $statusCode = $response->getStatusCode(); 76 | $result = $response->toArray(false); 77 | } catch (DecodingExceptionInterface) { 78 | throw new HttpTransportException('Unable to send an email: '.$response->getContent(false).\sprintf(' (code %d).', $statusCode), $response); 79 | } catch (TransportExceptionInterface $e) { 80 | throw new HttpTransportException('Could not reach the remote Postmark server.', $response, 0, $e); 81 | } 82 | 83 | if (200 !== $statusCode) { 84 | // Some delivery issues can be handled silently - route those through EventDispatcher 85 | if (null !== $this->dispatcher && self::CODE_INACTIVE_RECIPIENT === $result['ErrorCode']) { 86 | $this->dispatcher->dispatch(new PostmarkDeliveryEvent($result['Message'], $result['ErrorCode'], $email->getHeaders())); 87 | 88 | return $response; 89 | } 90 | 91 | throw new HttpTransportException('Unable to send an email: '.$result['Message'].\sprintf(' (code %d).', $result['ErrorCode']), $response); 92 | } 93 | 94 | $sentMessage->setMessageId($result['MessageID']); 95 | 96 | return $response; 97 | } 98 | 99 | private function getPayload(Email $email, Envelope $envelope): array 100 | { 101 | $payload = [ 102 | 'From' => $envelope->getSender()->toString(), 103 | 'To' => implode(',', $this->stringifyAddresses($this->getRecipients($email, $envelope))), 104 | 'Cc' => implode(',', $this->stringifyAddresses($email->getCc())), 105 | 'Bcc' => implode(',', $this->stringifyAddresses($email->getBcc())), 106 | 'ReplyTo' => implode(',', $this->stringifyAddresses($email->getReplyTo())), 107 | 'Subject' => $email->getSubject(), 108 | 'TextBody' => $email->getTextBody(), 109 | 'HtmlBody' => $email->getHtmlBody(), 110 | 'Attachments' => $this->getAttachments($email), 111 | ]; 112 | 113 | $headersToBypass = ['from', 'to', 'cc', 'bcc', 'subject', 'content-type', 'sender', 'reply-to', 'date']; 114 | foreach ($email->getHeaders()->all() as $name => $header) { 115 | if (\in_array($name, $headersToBypass, true)) { 116 | continue; 117 | } 118 | 119 | if ($header instanceof TagHeader) { 120 | if (isset($payload['Tag'])) { 121 | throw new TransportException('Postmark only allows a single tag per email.'); 122 | } 123 | 124 | $payload['Tag'] = $header->getValue(); 125 | 126 | continue; 127 | } 128 | 129 | if ($header instanceof MetadataHeader) { 130 | $payload['Metadata'][$header->getKey()] = $header->getValue(); 131 | 132 | continue; 133 | } 134 | 135 | if ($header instanceof MessageStreamHeader) { 136 | $payload['MessageStream'] = $header->getValue(); 137 | 138 | continue; 139 | } 140 | 141 | $payload['Headers'][] = [ 142 | 'Name' => $header->getName(), 143 | 'Value' => $header->getBodyAsString(), 144 | ]; 145 | } 146 | 147 | if (null !== $this->messageStream && !isset($payload['MessageStream'])) { 148 | $payload['MessageStream'] = $this->messageStream; 149 | } 150 | 151 | return $payload; 152 | } 153 | 154 | private function getAttachments(Email $email): array 155 | { 156 | $attachments = []; 157 | foreach ($email->getAttachments() as $attachment) { 158 | $headers = $attachment->getPreparedHeaders(); 159 | $filename = $headers->getHeaderParameter('Content-Disposition', 'filename'); 160 | $disposition = $headers->getHeaderBody('Content-Disposition'); 161 | 162 | $att = [ 163 | 'Name' => $filename, 164 | 'Content' => $attachment->bodyToString(), 165 | 'ContentType' => $headers->get('Content-Type')->getBody(), 166 | ]; 167 | 168 | if ('inline' === $disposition) { 169 | $att['ContentID'] = 'cid:'.($attachment->hasContentId() ? $attachment->getContentId() : $filename); 170 | } 171 | 172 | $attachments[] = $att; 173 | } 174 | 175 | return $attachments; 176 | } 177 | 178 | private function getEndpoint(): ?string 179 | { 180 | return ($this->host ?: self::HOST).($this->port ? ':'.$this->port : ''); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /Transport/PostmarkSmtpTransport.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\Postmark\Transport; 13 | 14 | use Psr\EventDispatcher\EventDispatcherInterface; 15 | use Psr\Log\LoggerInterface; 16 | use Symfony\Component\Mailer\Envelope; 17 | use Symfony\Component\Mailer\Exception\TransportException; 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\Smtp\EsmtpTransport; 22 | use Symfony\Component\Mime\Message; 23 | use Symfony\Component\Mime\RawMessage; 24 | 25 | /** 26 | * @author Kevin Verschaeve 27 | */ 28 | class PostmarkSmtpTransport extends EsmtpTransport 29 | { 30 | private ?string $messageStream = null; 31 | 32 | public function __construct(#[\SensitiveParameter] string $id, ?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null) 33 | { 34 | parent::__construct('smtp.postmarkapp.com', 587, false, $dispatcher, $logger); 35 | 36 | $this->setUsername($id); 37 | $this->setPassword($id); 38 | } 39 | 40 | public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage 41 | { 42 | if ($message instanceof Message) { 43 | $this->addPostmarkHeaders($message); 44 | } 45 | 46 | return parent::send($message, $envelope); 47 | } 48 | 49 | private function addPostmarkHeaders(Message $message): void 50 | { 51 | $message->getHeaders()->addTextHeader('X-PM-KeepID', 'true'); 52 | 53 | $headers = $message->getHeaders(); 54 | 55 | foreach ($headers->all() as $name => $header) { 56 | if ($header instanceof TagHeader) { 57 | if ($headers->has('X-PM-Tag')) { 58 | throw new TransportException('Postmark only allows a single tag per email.'); 59 | } 60 | 61 | $headers->addTextHeader('X-PM-Tag', $header->getValue()); 62 | $headers->remove($name); 63 | } 64 | 65 | if ($header instanceof MetadataHeader) { 66 | $headers->addTextHeader('X-PM-Metadata-'.$header->getKey(), $header->getValue()); 67 | $headers->remove($name); 68 | } 69 | } 70 | 71 | if (null !== $this->messageStream && !$message->getHeaders()->has('X-PM-Message-Stream')) { 72 | $headers->addTextHeader('X-PM-Message-Stream', $this->messageStream); 73 | } 74 | } 75 | 76 | /** 77 | * @return $this 78 | */ 79 | public function setMessageStream(string $messageStream): static 80 | { 81 | $this->messageStream = $messageStream; 82 | 83 | return $this; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Transport/PostmarkTransportFactory.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\Postmark\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 PostmarkTransportFactory extends AbstractTransportFactory 23 | { 24 | public function create(Dsn $dsn): TransportInterface 25 | { 26 | $transport = null; 27 | $scheme = $dsn->getScheme(); 28 | $user = $this->getUser($dsn); 29 | 30 | if ('postmark+api' === $scheme) { 31 | $host = 'default' === $dsn->getHost() ? null : $dsn->getHost(); 32 | $port = $dsn->getPort(); 33 | 34 | $transport = (new PostmarkApiTransport($user, $this->client, $this->dispatcher, $this->logger))->setHost($host)->setPort($port); 35 | } 36 | 37 | if ('postmark+smtp' === $scheme || 'postmark+smtps' === $scheme || 'postmark' === $scheme) { 38 | $transport = new PostmarkSmtpTransport($user, $this->dispatcher, $this->logger); 39 | } 40 | 41 | if (null !== $transport) { 42 | $messageStream = $dsn->getOption('message_stream'); 43 | 44 | if (null !== $messageStream) { 45 | $transport->setMessageStream($messageStream); 46 | } 47 | 48 | return $transport; 49 | } 50 | 51 | throw new UnsupportedSchemeException($dsn, 'postmark', $this->getSupportedSchemes()); 52 | } 53 | 54 | protected function getSupportedSchemes(): array 55 | { 56 | return ['postmark', 'postmark+api', 'postmark+smtp', 'postmark+smtps']; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Webhook/PostmarkRequestParser.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\Postmark\Webhook; 13 | 14 | use Symfony\Component\HttpFoundation\ChainRequestMatcher; 15 | use Symfony\Component\HttpFoundation\Request; 16 | use Symfony\Component\HttpFoundation\RequestMatcher\IpsRequestMatcher; 17 | use Symfony\Component\HttpFoundation\RequestMatcher\IsJsonRequestMatcher; 18 | use Symfony\Component\HttpFoundation\RequestMatcher\MethodRequestMatcher; 19 | use Symfony\Component\HttpFoundation\RequestMatcherInterface; 20 | use Symfony\Component\Mailer\Bridge\Postmark\RemoteEvent\PostmarkPayloadConverter; 21 | use Symfony\Component\RemoteEvent\Event\Mailer\AbstractMailerEvent; 22 | use Symfony\Component\RemoteEvent\Exception\ParseException; 23 | use Symfony\Component\Webhook\Client\AbstractRequestParser; 24 | use Symfony\Component\Webhook\Exception\RejectWebhookException; 25 | 26 | final class PostmarkRequestParser extends AbstractRequestParser 27 | { 28 | public function __construct( 29 | private readonly PostmarkPayloadConverter $converter, 30 | 31 | // https://postmarkapp.com/support/article/800-ips-for-firewalls#webhooks 32 | // localhost is added for testing 33 | private readonly array $allowedIPs = ['3.134.147.250', '50.31.156.6', '50.31.156.77', '18.217.206.57', '127.0.0.1'], 34 | ) { 35 | } 36 | 37 | protected function getRequestMatcher(): RequestMatcherInterface 38 | { 39 | return new ChainRequestMatcher([ 40 | new MethodRequestMatcher('POST'), 41 | new IpsRequestMatcher($this->allowedIPs), 42 | new IsJsonRequestMatcher(), 43 | ]); 44 | } 45 | 46 | protected function doParse(Request $request, #[\SensitiveParameter] string $secret): ?AbstractMailerEvent 47 | { 48 | $payload = $request->toArray(); 49 | if ( 50 | !isset($payload['RecordType']) 51 | || !isset($payload['MessageID']) 52 | || !(isset($payload['Recipient']) || isset($payload['Email'])) 53 | ) { 54 | throw new RejectWebhookException(406, 'Payload is malformed.'); 55 | } 56 | 57 | try { 58 | return $this->converter->convert($payload); 59 | } catch (ParseException $e) { 60 | throw new RejectWebhookException(406, $e->getMessage(), $e); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symfony/postmark-mailer", 3 | "type": "symfony-mailer-bridge", 4 | "description": "Symfony Postmark 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 | "psr/event-dispatcher": "^1", 21 | "symfony/mailer": "^7.2" 22 | }, 23 | "require-dev": { 24 | "symfony/http-client": "^6.4|^7.0", 25 | "symfony/webhook": "^6.4|^7.0" 26 | }, 27 | "conflict": { 28 | "symfony/http-foundation": "<6.4" 29 | }, 30 | "autoload": { 31 | "psr-4": { "Symfony\\Component\\Mailer\\Bridge\\Postmark\\": "" }, 32 | "exclude-from-classmap": [ 33 | "/Tests/" 34 | ] 35 | }, 36 | "minimum-stability": "dev" 37 | } 38 | --------------------------------------------------------------------------------