diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 75cfe1d1f0d84..e6fd4e58e811d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -2647,6 +2647,7 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co MailerBridge\Mailchimp\Transport\MandrillTransportFactory::class => 'mailer.transport_factory.mailchimp', MailerBridge\Postal\Transport\PostalTransportFactory::class => 'mailer.transport_factory.postal', MailerBridge\Postmark\Transport\PostmarkTransportFactory::class => 'mailer.transport_factory.postmark', + MailerBridge\Mailtrap\Transport\MailtrapTransportFactory::class => 'mailer.transport_factory.mailtrap', MailerBridge\Resend\Transport\ResendTransportFactory::class => 'mailer.transport_factory.resend', MailerBridge\Scaleway\Transport\ScalewayTransportFactory::class => 'mailer.transport_factory.scaleway', MailerBridge\Sendgrid\Transport\SendgridTransportFactory::class => 'mailer.transport_factory.sendgrid', diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php index 8c7131b79468e..c0e7cc06a4eb8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php @@ -22,6 +22,7 @@ use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetTransportFactory; use Symfony\Component\Mailer\Bridge\Mailomat\Transport\MailomatTransportFactory; use Symfony\Component\Mailer\Bridge\MailPace\Transport\MailPaceTransportFactory; +use Symfony\Component\Mailer\Bridge\Mailtrap\Transport\MailtrapTransportFactory; use Symfony\Component\Mailer\Bridge\Postal\Transport\PostalTransportFactory; use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory; use Symfony\Component\Mailer\Bridge\Resend\Transport\ResendTransportFactory; @@ -61,6 +62,7 @@ 'null' => NullTransportFactory::class, 'postal' => PostalTransportFactory::class, 'postmark' => PostmarkTransportFactory::class, + 'mailtrap' => MailtrapTransportFactory::class, 'resend' => ResendTransportFactory::class, 'scaleway' => ScalewayTransportFactory::class, 'sendgrid' => SendgridTransportFactory::class, diff --git a/src/Symfony/Component/Mailer/Bridge/Mailtrap/.gitattributes b/src/Symfony/Component/Mailer/Bridge/Mailtrap/.gitattributes new file mode 100644 index 0000000000000..14c3c35940427 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailtrap/.gitattributes @@ -0,0 +1,3 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.git* export-ignore diff --git a/src/Symfony/Component/Mailer/Bridge/Mailtrap/.gitignore b/src/Symfony/Component/Mailer/Bridge/Mailtrap/.gitignore new file mode 100644 index 0000000000000..c49a5d8df5c65 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailtrap/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/Mailer/Bridge/Mailtrap/CHANGELOG.md b/src/Symfony/Component/Mailer/Bridge/Mailtrap/CHANGELOG.md new file mode 100644 index 0000000000000..00149ea5ac6f5 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailtrap/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +7.2 +--- + + * Add the bridge diff --git a/src/Symfony/Component/Mailer/Bridge/Mailtrap/LICENSE b/src/Symfony/Component/Mailer/Bridge/Mailtrap/LICENSE new file mode 100644 index 0000000000000..e374a5c8339d3 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailtrap/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2024-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Component/Mailer/Bridge/Mailtrap/README.md b/src/Symfony/Component/Mailer/Bridge/Mailtrap/README.md new file mode 100644 index 0000000000000..e55a0baf8b9b0 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailtrap/README.md @@ -0,0 +1,26 @@ +Mailtrap Bridge +=============== + +Provides Mailtrap integration for Symfony Mailer. + +Configuration example: + +```env +# SMTP +MAILER_DSN=mailtrap+smtp://PASSWORD@default + +# API +MAILER_DSN=mailtrap+api://TOKEN@default +``` + +where: + - `PASSWORD` is your Mailtrap SMTP Password + - `TOKEN` is your Mailtrap Server Token + +Resources +--------- + + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/src/Symfony/Component/Mailer/Bridge/Mailtrap/Tests/Transport/MailtrapApiTransportTest.php b/src/Symfony/Component/Mailer/Bridge/Mailtrap/Tests/Transport/MailtrapApiTransportTest.php new file mode 100644 index 0000000000000..286f577fc5052 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailtrap/Tests/Transport/MailtrapApiTransportTest.php @@ -0,0 +1,150 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Mailtrap\Tests\Transport; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Response\JsonMockResponse; +use Symfony\Component\Mailer\Bridge\Mailtrap\Transport\MailtrapApiTransport; +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Exception\HttpTransportException; +use Symfony\Component\Mailer\Exception\TransportException; +use Symfony\Component\Mailer\Header\MetadataHeader; +use Symfony\Component\Mailer\Header\TagHeader; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Email; +use Symfony\Contracts\HttpClient\ResponseInterface; + +class MailtrapApiTransportTest extends TestCase +{ + /** + * @dataProvider getTransportData + */ + public function testToString(MailtrapApiTransport $transport, string $expected) + { + $this->assertSame($expected, (string) $transport); + } + + public static function getTransportData(): array + { + return [ + [ + new MailtrapApiTransport('KEY'), + 'mailtrap+api://send.api.mailtrap.io', + ], + [ + (new MailtrapApiTransport('KEY'))->setHost('example.com'), + 'mailtrap+api://example.com', + ], + [ + (new MailtrapApiTransport('KEY'))->setHost('example.com')->setPort(99), + 'mailtrap+api://example.com:99', + ], + ]; + } + + public function testCustomHeader() + { + $email = new Email(); + $email->getHeaders()->addTextHeader('foo', 'bar'); + $envelope = new Envelope(new Address('alice@system.com'), [new Address('bob@system.com')]); + + $transport = new MailtrapApiTransport('ACCESS_KEY'); + $method = new \ReflectionMethod(MailtrapApiTransport::class, 'getPayload'); + $payload = $method->invoke($transport, $email, $envelope); + + $this->assertArrayHasKey('headers', $payload); + $this->assertSame(['foo' => 'bar'], $payload['headers']); + } + + public function testSend() + { + $client = new MockHttpClient(function (string $method, string $url, array $options): ResponseInterface { + $this->assertSame('POST', $method); + $this->assertSame('https://send.api.mailtrap.io/api/send', $url); + + $body = json_decode($options['body'], true); + $this->assertSame(['email' => 'fabpot@symfony.com', 'name' => 'Fabien'], $body['from']); + $this->assertSame([['email' => 'kevin@symfony.com', 'name' => 'Kevin']], $body['to']); + $this->assertSame('Hello!', $body['subject']); + $this->assertSame('Hello There!', $body['text']); + + return new JsonMockResponse([], [ + 'http_code' => 200, + ]); + }); + + $transport = new MailtrapApiTransport('KEY', $client); + + $mail = new Email(); + $mail->subject('Hello!') + ->to(new Address('kevin@symfony.com', 'Kevin')) + ->from(new Address('fabpot@symfony.com', 'Fabien')) + ->text('Hello There!'); + + $transport->send($mail); + } + + public function testSendThrowsForErrorResponse() + { + $client = new MockHttpClient(static fn (string $method, string $url, array $options): ResponseInterface => new JsonMockResponse(['errors' => ['i\'m a teapot']], [ + 'http_code' => 418, + ])); + $transport = new MailtrapApiTransport('KEY', $client); + $transport->setPort(8984); + + $mail = new Email(); + $mail->subject('Hello!') + ->to(new Address('kevin@symfony.com', 'Kevin')) + ->from(new Address('fabpot@symfony.com', 'Fabien')) + ->text('Hello There!'); + + $this->expectException(HttpTransportException::class); + $this->expectExceptionMessage('Unable to send email: "i\'m a teapot" (status code 418).'); + $transport->send($mail); + } + + public function testTagAndMetadataHeaders() + { + $email = new Email(); + $email->getHeaders()->add(new TagHeader('password-reset')); + $email->getHeaders()->add(new MetadataHeader('Color', 'blue')); + $email->getHeaders()->add(new MetadataHeader('Client-ID', '12345')); + $envelope = new Envelope(new Address('alice@system.com'), [new Address('bob@system.com')]); + + $transport = new MailtrapApiTransport('ACCESS_KEY'); + $method = new \ReflectionMethod(MailtrapApiTransport::class, 'getPayload'); + $payload = $method->invoke($transport, $email, $envelope); + + $this->assertArrayNotHasKey('Headers', $payload); + $this->assertArrayHasKey('category', $payload); + $this->assertArrayHasKey('custom_variables', $payload); + + $this->assertSame('password-reset', $payload['category']); + $this->assertSame(['Color' => 'blue', 'Client-ID' => '12345'], $payload['custom_variables']); + } + + public function testMultipleTagsAreNotAllowed() + { + $email = new Email(); + $email->getHeaders()->add(new TagHeader('tag1')); + $email->getHeaders()->add(new TagHeader('tag2')); + $envelope = new Envelope(new Address('alice@system.com'), [new Address('bob@system.com')]); + + $transport = new MailtrapApiTransport('ACCESS_KEY'); + $method = new \ReflectionMethod(MailtrapApiTransport::class, 'getPayload'); + + $this->expectException(TransportException::class); + + $method->invoke($transport, $email, $envelope); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailtrap/Tests/Transport/MailtrapSmtpTransportTest.php b/src/Symfony/Component/Mailer/Bridge/Mailtrap/Tests/Transport/MailtrapSmtpTransportTest.php new file mode 100644 index 0000000000000..25b36fe7b2523 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailtrap/Tests/Transport/MailtrapSmtpTransportTest.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Mailtrap\Tests\Transport; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Mailer\Bridge\Mailtrap\Transport\MailtrapSmtpTransport; +use Symfony\Component\Mailer\Exception\TransportException; +use Symfony\Component\Mailer\Header\MetadataHeader; +use Symfony\Component\Mailer\Header\TagHeader; +use Symfony\Component\Mime\Email; + +class MailtrapSmtpTransportTest extends TestCase +{ + public function testCustomHeader() + { + $email = new Email(); + $email->getHeaders()->addTextHeader('foo', 'bar'); + + $transport = new MailtrapSmtpTransport('ACCESS_KEY'); + $method = new \ReflectionMethod(MailtrapSmtpTransport::class, 'addMailtrapHeaders'); + $method->invoke($transport, $email); + + $this->assertCount(1, $email->getHeaders()->toArray()); + $this->assertSame('foo: bar', $email->getHeaders()->get('FOO')->toString()); + } + + public function testTagAndMetadata() + { + $email = new Email(); + $email->getHeaders()->addTextHeader('foo', 'bar'); + $email->getHeaders()->add(new TagHeader('password-reset')); + $email->getHeaders()->add(new MetadataHeader('Color', 'blue')); + $email->getHeaders()->add(new MetadataHeader('Client-ID', '12345')); + + $transport = new MailtrapSmtpTransport('ACCESS_KEY'); + $method = new \ReflectionMethod(MailtrapSmtpTransport::class, 'addMailtrapHeaders'); + $method->invoke($transport, $email); + + $this->assertCount(3, $email->getHeaders()->toArray()); + $this->assertSame('foo: bar', $email->getHeaders()->get('FOO')->toString()); + $this->assertSame('X-MT-Category: password-reset', $email->getHeaders()->get('X-MT-Category')->toString()); + $this->assertSame('X-MT-Custom-Variables: {"Color":"blue","Client-ID":"12345"}', $email->getHeaders()->get('X-MT-Custom-Variables')->toString()); + } + + public function testMultipleTagsAreNotAllowed() + { + $email = new Email(); + $email->getHeaders()->add(new TagHeader('tag1')); + $email->getHeaders()->add(new TagHeader('tag2')); + + $transport = new MailtrapSmtpTransport('ACCESS_KEY'); + $method = new \ReflectionMethod(MailtrapSmtpTransport::class, 'addMailtrapHeaders'); + + $this->expectException(TransportException::class); + + $method->invoke($transport, $email); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailtrap/Tests/Transport/MailtrapTransportFactoryTest.php b/src/Symfony/Component/Mailer/Bridge/Mailtrap/Tests/Transport/MailtrapTransportFactoryTest.php new file mode 100644 index 0000000000000..df824e150d6c9 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailtrap/Tests/Transport/MailtrapTransportFactoryTest.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Mailtrap\Tests\Transport; + +use Psr\Log\NullLogger; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\Mailer\Bridge\Mailtrap\Transport\MailtrapApiTransport; +use Symfony\Component\Mailer\Bridge\Mailtrap\Transport\MailtrapSmtpTransport; +use Symfony\Component\Mailer\Bridge\Mailtrap\Transport\MailtrapTransportFactory; +use Symfony\Component\Mailer\Test\TransportFactoryTestCase; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\TransportFactoryInterface; + +class MailtrapTransportFactoryTest extends TransportFactoryTestCase +{ + public function getFactory(): TransportFactoryInterface + { + return new MailtrapTransportFactory(null, new MockHttpClient(), new NullLogger()); + } + + public static function supportsProvider(): iterable + { + yield [ + new Dsn('mailtrap+api', 'default'), + true, + ]; + + yield [ + new Dsn('mailtrap', 'default'), + true, + ]; + + yield [ + new Dsn('mailtrap+smtp', 'default'), + true, + ]; + + yield [ + new Dsn('mailtrap+smtps', 'default'), + true, + ]; + + yield [ + new Dsn('mailtrap+smtp', 'example.com'), + true, + ]; + } + + public static function createProvider(): iterable + { + $logger = new NullLogger(); + + yield [ + new Dsn('mailtrap+api', 'default', self::USER), + new MailtrapApiTransport(self::USER, new MockHttpClient(), null, $logger), + ]; + + yield [ + new Dsn('mailtrap+api', 'example.com', self::USER, '', 8080), + (new MailtrapApiTransport(self::USER, new MockHttpClient(), null, $logger))->setHost('example.com')->setPort(8080), + ]; + + yield [ + new Dsn('mailtrap', 'default', self::USER), + new MailtrapSmtpTransport(self::USER, null, $logger), + ]; + + yield [ + new Dsn('mailtrap+smtp', 'default', self::USER), + new MailtrapSmtpTransport(self::USER, null, $logger), + ]; + + yield [ + new Dsn('mailtrap+smtps', 'default', self::USER), + new MailtrapSmtpTransport(self::USER, null, $logger), + ]; + } + + public static function unsupportedSchemeProvider(): iterable + { + yield [ + new Dsn('mailtrap+foo', 'default', self::USER), + 'The "mailtrap+foo" scheme is not supported; supported schemes for mailer "mailtrap" are: "mailtrap", "mailtrap+api", "mailtrap+smtp", "mailtrap+smtps".', + ]; + } + + public static function incompleteDsnProvider(): iterable + { + yield [new Dsn('mailtrap+api', 'default')]; + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailtrap/Transport/MailtrapApiTransport.php b/src/Symfony/Component/Mailer/Bridge/Mailtrap/Transport/MailtrapApiTransport.php new file mode 100644 index 0000000000000..09e7c2f245642 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailtrap/Transport/MailtrapApiTransport.php @@ -0,0 +1,150 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Mailtrap\Transport; + +use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Exception\HttpTransportException; +use Symfony\Component\Mailer\Exception\TransportException; +use Symfony\Component\Mailer\Header\MetadataHeader; +use Symfony\Component\Mailer\Header\TagHeader; +use Symfony\Component\Mailer\SentMessage; +use Symfony\Component\Mailer\Transport\AbstractApiTransport; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Email; +use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; + +/** + * @author Kevin Bond + */ +final class MailtrapApiTransport extends AbstractApiTransport +{ + private const HOST = 'send.api.mailtrap.io'; + private const HEADERS_TO_BYPASS = ['from', 'to', 'cc', 'bcc', 'subject', 'content-type', 'sender']; + + public function __construct( + #[\SensitiveParameter] private string $token, + ?HttpClientInterface $client = null, + ?EventDispatcherInterface $dispatcher = null, + ?LoggerInterface $logger = null, + ) { + parent::__construct($client, $dispatcher, $logger); + } + + public function __toString(): string + { + return \sprintf('mailtrap+api://%s', $this->getEndpoint()); + } + + protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $envelope): ResponseInterface + { + $response = $this->client->request('POST', 'https://'.$this->getEndpoint().'/api/send', [ + 'json' => $this->getPayload($email, $envelope), + 'auth_bearer' => $this->token, + ]); + + try { + $statusCode = $response->getStatusCode(); + $result = $response->toArray(false); + } catch (DecodingExceptionInterface) { + throw new HttpTransportException('Unable to send an email: '.$response->getContent(false).\sprintf(' (code %d).', $statusCode), $response); + } catch (TransportExceptionInterface $e) { + throw new HttpTransportException('Could not reach the remote Mailtrap server.', $response, 0, $e); + } + + if (200 !== $statusCode) { + throw new HttpTransportException(\sprintf('Unable to send email: "%s" (status code %d).', implode(', ', $result['errors']), $statusCode), $response); + } + + return $response; + } + + private function getPayload(Email $email, Envelope $envelope): array + { + $payload = [ + 'from' => self::encodeEmail($envelope->getSender()), + 'to' => array_map(self::encodeEmail(...), $email->getTo()), + 'cc' => array_map(self::encodeEmail(...), $email->getCc()), + 'bcc' => array_map(self::encodeEmail(...), $email->getBcc()), + 'subject' => $email->getSubject(), + 'text' => $email->getTextBody(), + 'html' => $email->getHtmlBody(), + 'attachments' => $this->getAttachments($email), + ]; + + foreach ($email->getHeaders()->all() as $name => $header) { + if (\in_array($name, self::HEADERS_TO_BYPASS, true)) { + continue; + } + + if ($header instanceof TagHeader) { + if (isset($payload['category'])) { + throw new TransportException('Mailtrap only allows a single category per email.'); + } + + $payload['category'] = $header->getValue(); + + continue; + } + + if ($header instanceof MetadataHeader) { + $payload['custom_variables'][$header->getKey()] = $header->getValue(); + + continue; + } + + $payload['headers'][$header->getName()] = $header->getBodyAsString(); + } + + return $payload; + } + + private function getAttachments(Email $email): array + { + $attachments = []; + + foreach ($email->getAttachments() as $attachment) { + $headers = $attachment->getPreparedHeaders(); + $filename = $headers->getHeaderParameter('Content-Disposition', 'filename'); + $disposition = $headers->getHeaderBody('Content-Disposition'); + + $att = [ + 'content' => $attachment->bodyToString(), + 'type' => $headers->get('Content-Type')->getBody(), + 'filename' => $filename, + 'disposition' => $disposition, + ]; + + if ('inline' === $disposition) { + $att['content_id'] = $attachment->hasContentId() ? $attachment->getContentId() : $filename; + } + + $attachments[] = $att; + } + + return $attachments; + } + + private static function encodeEmail(Address $address): array + { + return array_filter(['email' => $address->getEncodedAddress(), 'name' => $address->getName()]); + } + + private function getEndpoint(): ?string + { + return ($this->host ?: self::HOST).($this->port ? ':'.$this->port : ''); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailtrap/Transport/MailtrapSmtpTransport.php b/src/Symfony/Component/Mailer/Bridge/Mailtrap/Transport/MailtrapSmtpTransport.php new file mode 100644 index 0000000000000..608c4c1d39f9c --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailtrap/Transport/MailtrapSmtpTransport.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Mailtrap\Transport; + +use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Exception\TransportException; +use Symfony\Component\Mailer\Header\MetadataHeader; +use Symfony\Component\Mailer\Header\TagHeader; +use Symfony\Component\Mailer\SentMessage; +use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; +use Symfony\Component\Mime\Message; +use Symfony\Component\Mime\RawMessage; + +/** + * @author Kevin Bond + */ +final class MailtrapSmtpTransport extends EsmtpTransport +{ + public function __construct(#[\SensitiveParameter] string $password, ?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null) + { + parent::__construct('live.smtp.mailtrap.io', 587, false, $dispatcher, $logger); + + $this->setUsername('api'); + $this->setPassword($password); + } + + public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage + { + if ($message instanceof Message) { + $this->addMailtrapHeaders($message); + } + + return parent::send($message, $envelope); + } + + private function addMailtrapHeaders(Message $message): void + { + $headers = $message->getHeaders(); + $customVariables = []; + + foreach ($headers->all() as $name => $header) { + if ($header instanceof TagHeader) { + if ($headers->has('X-MT-Category')) { + throw new TransportException('Mailtrap only allows a single category per email.'); + } + + $headers->addTextHeader('X-MT-Category', $header->getValue()); + $headers->remove($name); + } + + if ($header instanceof MetadataHeader) { + $customVariables[$header->getKey()] = $header->getValue(); + $headers->remove($name); + } + } + + if ($customVariables) { + $headers->addTextHeader('X-MT-Custom-Variables', json_encode($customVariables)); + } + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailtrap/Transport/MailtrapTransportFactory.php b/src/Symfony/Component/Mailer/Bridge/Mailtrap/Transport/MailtrapTransportFactory.php new file mode 100644 index 0000000000000..0abdf901d0708 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailtrap/Transport/MailtrapTransportFactory.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Mailtrap\Transport; + +use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; +use Symfony\Component\Mailer\Transport\AbstractTransportFactory; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\TransportInterface; + +/** + * @author Kevin Bond + */ +final class MailtrapTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): TransportInterface + { + $scheme = $dsn->getScheme(); + $user = $this->getUser($dsn); + + if ('mailtrap+api' === $scheme) { + $host = 'default' === $dsn->getHost() ? null : $dsn->getHost(); + $port = $dsn->getPort(); + + return (new MailtrapApiTransport($user, $this->client, $this->dispatcher, $this->logger))->setHost($host)->setPort($port); + } + + if ('mailtrap+smtp' === $scheme || 'mailtrap+smtps' === $scheme || 'mailtrap' === $scheme) { + return new MailtrapSmtpTransport($user, $this->dispatcher, $this->logger); + } + + throw new UnsupportedSchemeException($dsn, 'mailtrap', $this->getSupportedSchemes()); + } + + protected function getSupportedSchemes(): array + { + return ['mailtrap', 'mailtrap+api', 'mailtrap+smtp', 'mailtrap+smtps']; + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailtrap/composer.json b/src/Symfony/Component/Mailer/Bridge/Mailtrap/composer.json new file mode 100644 index 0000000000000..a1558309baef9 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailtrap/composer.json @@ -0,0 +1,33 @@ +{ + "name": "symfony/mailtrap-mailer", + "type": "symfony-mailer-bridge", + "description": "Symfony Mailtrap Mailer Bridge", + "keywords": [], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.2", + "psr/event-dispatcher": "^1", + "symfony/mailer": "^7.2" + }, + "require-dev": { + "symfony/http-client": "^6.4|^7.0" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Mailer\\Bridge\\Mailtrap\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/src/Symfony/Component/Mailer/Bridge/Mailtrap/phpunit.xml.dist b/src/Symfony/Component/Mailer/Bridge/Mailtrap/phpunit.xml.dist new file mode 100644 index 0000000000000..66332ce2b9cb9 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Mailtrap/phpunit.xml.dist @@ -0,0 +1,31 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + + ./Resources + ./Tests + ./vendor + + + diff --git a/src/Symfony/Component/Mailer/Exception/UnsupportedSchemeException.php b/src/Symfony/Component/Mailer/Exception/UnsupportedSchemeException.php index b363146c86212..5f25c8a0f609d 100644 --- a/src/Symfony/Component/Mailer/Exception/UnsupportedSchemeException.php +++ b/src/Symfony/Component/Mailer/Exception/UnsupportedSchemeException.php @@ -68,6 +68,10 @@ class UnsupportedSchemeException extends LogicException 'class' => Bridge\Postmark\Transport\PostmarkTransportFactory::class, 'package' => 'symfony/postmark-mailer', ], + 'mailtrap' => [ + 'class' => Bridge\Mailtrap\Transport\MailtrapTransportFactory::class, + 'package' => 'symfony/mailtrap-mailer', + ], 'resend' => [ 'class' => Bridge\Resend\Transport\ResendTransportFactory::class, 'package' => 'symfony/resend-mailer', diff --git a/src/Symfony/Component/Mailer/Tests/Exception/UnsupportedSchemeExceptionTest.php b/src/Symfony/Component/Mailer/Tests/Exception/UnsupportedSchemeExceptionTest.php index fef94fad117d7..b4d00dd38b4f4 100644 --- a/src/Symfony/Component/Mailer/Tests/Exception/UnsupportedSchemeExceptionTest.php +++ b/src/Symfony/Component/Mailer/Tests/Exception/UnsupportedSchemeExceptionTest.php @@ -24,6 +24,7 @@ use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetTransportFactory; use Symfony\Component\Mailer\Bridge\Mailomat\Transport\MailomatTransportFactory; use Symfony\Component\Mailer\Bridge\MailPace\Transport\MailPaceTransportFactory; +use Symfony\Component\Mailer\Bridge\Mailtrap\Transport\MailtrapTransportFactory; use Symfony\Component\Mailer\Bridge\Postal\Transport\PostalTransportFactory; use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory; use Symfony\Component\Mailer\Bridge\Resend\Transport\ResendTransportFactory; @@ -54,6 +55,7 @@ public static function setUpBeforeClass(): void MandrillTransportFactory::class => false, PostalTransportFactory::class => false, PostmarkTransportFactory::class => false, + MailtrapTransportFactory::class => false, ResendTransportFactory::class => false, ScalewayTransportFactory::class => false, SendgridTransportFactory::class => false, @@ -89,6 +91,7 @@ public static function messageWhereSchemeIsPartOfSchemeToPackageMapProvider(): \ yield ['mandrill', 'symfony/mailchimp-mailer']; yield ['postal', 'symfony/postal-mailer']; yield ['postmark', 'symfony/postmark-mailer']; + yield ['mailtrap', 'symfony/mailtrap-mailer']; yield ['resend', 'symfony/resend-mailer']; yield ['scaleway', 'symfony/scaleway-mailer']; yield ['sendgrid', 'symfony/sendgrid-mailer']; diff --git a/src/Symfony/Component/Mailer/Transport.php b/src/Symfony/Component/Mailer/Transport.php index 68c21f1ffc60f..026e033af0525 100644 --- a/src/Symfony/Component/Mailer/Transport.php +++ b/src/Symfony/Component/Mailer/Transport.php @@ -24,6 +24,7 @@ use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetTransportFactory; use Symfony\Component\Mailer\Bridge\Mailomat\Transport\MailomatTransportFactory; use Symfony\Component\Mailer\Bridge\MailPace\Transport\MailPaceTransportFactory; +use Symfony\Component\Mailer\Bridge\Mailtrap\Transport\MailtrapTransportFactory; use Symfony\Component\Mailer\Bridge\Postal\Transport\PostalTransportFactory; use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory; use Symfony\Component\Mailer\Bridge\Resend\Transport\ResendTransportFactory; @@ -63,6 +64,7 @@ final class Transport MandrillTransportFactory::class, PostalTransportFactory::class, PostmarkTransportFactory::class, + MailtrapTransportFactory::class, ResendTransportFactory::class, ScalewayTransportFactory::class, SendgridTransportFactory::class, 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