diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 44c37e270946a..59cbecf8afc16 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -90,6 +90,7 @@ use Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillTransportFactory; use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunTransportFactory; use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetTransportFactory; +use Symfony\Component\Mailer\Bridge\OhMySmtp\Transport\OhMySmtpTransportFactory; use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory; use Symfony\Component\Mailer\Bridge\Sendgrid\Transport\SendgridTransportFactory; use Symfony\Component\Mailer\Bridge\Sendinblue\Transport\SendinblueTransportFactory; @@ -2354,6 +2355,7 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co SendgridTransportFactory::class => 'mailer.transport_factory.sendgrid', SendinblueTransportFactory::class => 'mailer.transport_factory.sendinblue', SesTransportFactory::class => 'mailer.transport_factory.amazon', + OhMySmtpTransportFactory::class => 'mailer.transport_factory.ohmysmtp', ]; foreach ($classToServices as $class => $service) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php index 9c330ab2c7333..7bddfa7567cee 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php @@ -16,6 +16,7 @@ use Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillTransportFactory; use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunTransportFactory; use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetTransportFactory; +use Symfony\Component\Mailer\Bridge\OhMySmtp\Transport\OhMySmtpTransportFactory; use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory; use Symfony\Component\Mailer\Bridge\Sendgrid\Transport\SendgridTransportFactory; use Symfony\Component\Mailer\Bridge\Sendinblue\Transport\SendinblueTransportFactory; @@ -76,6 +77,10 @@ ->parent('mailer.transport_factory.abstract') ->tag('mailer.transport_factory') + ->set('mailer.transport_factory.ohmysmtp', OhMySmtpTransportFactory::class) + ->parent('mailer.transport_factory.abstract') + ->tag('mailer.transport_factory') + ->set('mailer.transport_factory.smtp', EsmtpTransportFactory::class) ->parent('mailer.transport_factory.abstract') ->tag('mailer.transport_factory', ['priority' => -100]) diff --git a/src/Symfony/Component/Mailer/Bridge/OhMySmtp/.gitattributes b/src/Symfony/Component/Mailer/Bridge/OhMySmtp/.gitattributes new file mode 100644 index 0000000000000..84c7add058fb5 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/OhMySmtp/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/src/Symfony/Component/Mailer/Bridge/OhMySmtp/.gitignore b/src/Symfony/Component/Mailer/Bridge/OhMySmtp/.gitignore new file mode 100644 index 0000000000000..c49a5d8df5c65 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/OhMySmtp/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/Mailer/Bridge/OhMySmtp/CHANGELOG.md b/src/Symfony/Component/Mailer/Bridge/OhMySmtp/CHANGELOG.md new file mode 100644 index 0000000000000..3a08c7ededfcd --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/OhMySmtp/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +5.4 +--- + + * Add the bridge diff --git a/src/Symfony/Component/Mailer/Bridge/OhMySmtp/LICENSE b/src/Symfony/Component/Mailer/Bridge/OhMySmtp/LICENSE new file mode 100644 index 0000000000000..efb17f98e7dd3 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/OhMySmtp/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2021 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/OhMySmtp/README.md b/src/Symfony/Component/Mailer/Bridge/OhMySmtp/README.md new file mode 100644 index 0000000000000..fdeeb9f8a7727 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/OhMySmtp/README.md @@ -0,0 +1,24 @@ +OhMySMTP Bridge +=============== + +Provides [OhMySMTP](https://ohmysmtp.com) integration for Symfony Mailer. + + +DSN example +----------- + +``` +MAILER_DSN=ohmysmtp+api://API_TOKEN@default +``` + +where: + - `API_TOKEN` is your OhMySMTP API 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/OhMySmtp/Tests/Transport/OhMySmtpApiTransportTest.php b/src/Symfony/Component/Mailer/Bridge/OhMySmtp/Tests/Transport/OhMySmtpApiTransportTest.php new file mode 100644 index 0000000000000..7f64a8c997fdb --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/OhMySmtp/Tests/Transport/OhMySmtpApiTransportTest.php @@ -0,0 +1,143 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\OhMySmtp\Tests\Transport; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Response\MockResponse; +use Symfony\Component\Mailer\Bridge\OhMySmtp\Transport\OhMySmtpApiTransport; +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Exception\HttpTransportException; +use Symfony\Component\Mailer\Header\TagHeader; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Email; +use Symfony\Contracts\HttpClient\ResponseInterface; + +final class OhMySmtpApiTransportTest extends TestCase +{ + /** + * @dataProvider getTransportData + */ + public function testToString(OhMySmtpApiTransport $transport, string $expected) + { + $this->assertSame($expected, (string) $transport); + } + + public function getTransportData(): array + { + return [ + [ + new OhMySmtpApiTransport('KEY'), + 'ohmysmtp+api://app.ohmysmtp.com/api/v1', + ], + [ + (new OhMySmtpApiTransport('KEY'))->setHost('example.com'), + 'ohmysmtp+api://example.com', + ], + [ + (new OhMySmtpApiTransport('KEY'))->setHost('example.com')->setPort(99), + 'ohmysmtp+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 OhMySmtpApiTransport('ACCESS_KEY'); + $method = new \ReflectionMethod(OhMySmtpApiTransport::class, 'getPayload'); + $method->setAccessible(true); + $payload = $method->invoke($transport, $email, $envelope); + + $this->assertArrayHasKey('Headers', $payload); + $this->assertCount(1, $payload['Headers']); + + $this->assertEquals(['Name' => 'foo', 'Value' => 'bar'], $payload['Headers'][0]); + } + + public function testSend() + { + $client = new MockHttpClient(function (string $method, string $url, array $options): ResponseInterface { + $this->assertSame('POST', $method); + $this->assertSame('https://app.ohmysmtp.com/api/v1/send', $url); + $this->assertStringContainsStringIgnoringCase('OhMySMTP-Server-Token: KEY', $options['headers'][1] ?? $options['request_headers'][1]); + + $body = json_decode($options['body'], true); + $this->assertSame('"Fabien" ', $body['from']); + $this->assertSame('"Saif Eddin" ', $body['to']); + $this->assertSame('Hello!', $body['subject']); + $this->assertSame('Hello There!', $body['textbody']); + + return new MockResponse(json_encode(['id' => 'foobar', 'status' => 'pending']), [ + 'http_code' => 200, + ]); + }); + + $transport = new OhMySmtpApiTransport('KEY', $client); + + $mail = new Email(); + $mail->subject('Hello!') + ->to(new Address('saif.gmati@symfony.com', 'Saif Eddin')) + ->from(new Address('fabpot@symfony.com', 'Fabien')) + ->text('Hello There!'); + + $message = $transport->send($mail); + + $this->assertSame('foobar', $message->getMessageId()); + } + + public function testSendThrowsForErrorResponse() + { + $client = new MockHttpClient(static function (string $method, string $url, array $options): ResponseInterface { + return new MockResponse(json_encode(['Message' => 'i\'m a teapot', 'ErrorCode' => 418]), [ + 'http_code' => 418, + 'response_headers' => [ + 'content-type' => 'application/json', + ], + ]); + }); + $transport = new OhMySmtpApiTransport('KEY', $client); + $transport->setPort(8984); + + $mail = new Email(); + $mail->subject('Hello!') + ->to(new Address('saif.gmati@symfony.com', 'Saif Eddin')) + ->from(new Address('fabpot@symfony.com', 'Fabien')) + ->text('Hello There!'); + + $this->expectException(HttpTransportException::class); + $this->expectExceptionMessage('Unable to send an email: i\'m a teapot (code 418).'); + $transport->send($mail); + } + + public function testTagAndMetadataHeaders() + { + $email = new Email(); + $email->getHeaders()->add(new TagHeader('password-reset')); + $email->getHeaders()->add(new TagHeader('2nd-tag')); + + $envelope = new Envelope(new Address('alice@system.com'), [new Address('bob@system.com')]); + + $transport = new OhMySmtpApiTransport('ACCESS_KEY'); + $method = new \ReflectionMethod(OhMySmtpApiTransport::class, 'getPayload'); + $method->setAccessible(true); + $payload = $method->invoke($transport, $email, $envelope); + + $this->assertArrayNotHasKey('Headers', $payload); + $this->assertArrayHasKey('tags', $payload); + + $this->assertSame(['password-reset', '2nd-tag'], $payload['tags']); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/OhMySmtp/Tests/Transport/OhMySmtpSmtpTransportTest.php b/src/Symfony/Component/Mailer/Bridge/OhMySmtp/Tests/Transport/OhMySmtpSmtpTransportTest.php new file mode 100644 index 0000000000000..20f3e48a20d5c --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/OhMySmtp/Tests/Transport/OhMySmtpSmtpTransportTest.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\OhMySmtp\Tests\Transport; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Mailer\Bridge\OhMySmtp\Transport\OhMySmtpSmtpTransport; +use Symfony\Component\Mailer\Header\TagHeader; +use Symfony\Component\Mime\Email; + +final class OhMySmtpSmtpTransportTest extends TestCase +{ + public function testCustomHeader() + { + $email = new Email(); + $email->getHeaders()->addTextHeader('foo', 'bar'); + + $transport = new OhMySmtpSmtpTransport('ACCESS_KEY'); + $method = new \ReflectionMethod(OhMySmtpSmtpTransport::class, 'addOhMySmtpHeaders'); + $method->setAccessible(true); + $method->invoke($transport, $email); + + $this->assertCount(1, $email->getHeaders()->toArray()); + $this->assertSame('foo: bar', $email->getHeaders()->get('FOO')->toString()); + } + + public function testTagAndMetadataHeaders() + { + $email = new Email(); + $email->getHeaders()->addTextHeader('foo', 'bar'); + $email->getHeaders()->add(new TagHeader('password-reset')); + $email->getHeaders()->add(new TagHeader('2nd-tag')); + + $transport = new OhMySmtpSmtpTransport('ACCESS_KEY'); + $method = new \ReflectionMethod(OhMySmtpSmtpTransport::class, 'addOhMySmtpHeaders'); + $method->setAccessible(true); + $method->invoke($transport, $email); + + $this->assertCount(2, $email->getHeaders()->toArray()); + $this->assertSame('foo: bar', $email->getHeaders()->get('FOO')->toString()); + $this->assertSame('X-OMS-Tags: password-reset, 2nd-tag', $email->getHeaders()->get('X-OMS-Tags')->toString()); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/OhMySmtp/Tests/Transport/OhMySmtpTransportFactoryTest.php b/src/Symfony/Component/Mailer/Bridge/OhMySmtp/Tests/Transport/OhMySmtpTransportFactoryTest.php new file mode 100644 index 0000000000000..10445a1176234 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/OhMySmtp/Tests/Transport/OhMySmtpTransportFactoryTest.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\OhMySmtp\Tests\Transport; + +use Symfony\Component\Mailer\Bridge\OhMySmtp\Transport\OhMySmtpApiTransport; +use Symfony\Component\Mailer\Bridge\OhMySmtp\Transport\OhMySmtpSmtpTransport; +use Symfony\Component\Mailer\Bridge\OhMySmtp\Transport\OhMySmtpTransportFactory; +use Symfony\Component\Mailer\Test\TransportFactoryTestCase; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\TransportFactoryInterface; + +final class OhMySmtpTransportFactoryTest extends TransportFactoryTestCase +{ + public function getFactory(): TransportFactoryInterface + { + return new OhMySmtpTransportFactory($this->getDispatcher(), $this->getClient(), $this->getLogger()); + } + + public function supportsProvider(): iterable + { + yield [ + new Dsn('ohmysmtp+api', 'default'), + true, + ]; + + yield [ + new Dsn('ohmysmtp', 'default'), + true, + ]; + + yield [ + new Dsn('ohmysmtp+smtp', 'default'), + true, + ]; + + yield [ + new Dsn('ohmysmtp+smtps', 'default'), + true, + ]; + + yield [ + new Dsn('ohmysmtp+smtp', 'example.com'), + true, + ]; + } + + public function createProvider(): iterable + { + $dispatcher = $this->getDispatcher(); + $logger = $this->getLogger(); + + yield [ + new Dsn('ohmysmtp+api', 'default', self::USER), + new OhMySmtpApiTransport(self::USER, $this->getClient(), $dispatcher, $logger), + ]; + + yield [ + new Dsn('ohmysmtp+api', 'example.com', self::USER, '', 8080), + (new OhMySmtpApiTransport(self::USER, $this->getClient(), $dispatcher, $logger))->setHost('example.com')->setPort(8080), + ]; + + yield [ + new Dsn('ohmysmtp', 'default', self::USER), + new OhMySmtpSmtpTransport(self::USER, $dispatcher, $logger), + ]; + + yield [ + new Dsn('ohmysmtp+smtp', 'default', self::USER), + new OhMySmtpSmtpTransport(self::USER, $dispatcher, $logger), + ]; + + yield [ + new Dsn('ohmysmtp+smtps', 'default', self::USER), + new OhMySmtpSmtpTransport(self::USER, $dispatcher, $logger), + ]; + } + + public function unsupportedSchemeProvider(): iterable + { + yield [ + new Dsn('ohmysmtp+foo', 'default', self::USER), + 'The "ohmysmtp+foo" scheme is not supported; supported schemes for mailer "ohmysmtp" are: "ohmysmtp", "ohmysmtp+api", "ohmysmtp+smtp", "ohmysmtp+smtps".', + ]; + } + + public function incompleteDsnProvider(): iterable + { + yield [new Dsn('ohmysmtp+api', 'default')]; + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/OhMySmtp/Transport/OhMySmtpApiTransport.php b/src/Symfony/Component/Mailer/Bridge/OhMySmtp/Transport/OhMySmtpApiTransport.php new file mode 100644 index 0000000000000..596ea71332fdf --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/OhMySmtp/Transport/OhMySmtpApiTransport.php @@ -0,0 +1,142 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\OhMySmtp\Transport; + +use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Exception\HttpTransportException; +use Symfony\Component\Mailer\Header\TagHeader; +use Symfony\Component\Mailer\SentMessage; +use Symfony\Component\Mailer\Transport\AbstractApiTransport; +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 Paul Oms + */ +final class OhMySmtpApiTransport extends AbstractApiTransport +{ + private const HOST = 'app.ohmysmtp.com/api/v1'; + + private $key; + + public function __construct(string $key, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) + { + $this->key = $key; + + parent::__construct($client, $dispatcher, $logger); + } + + public function __toString(): string + { + return sprintf('ohmysmtp+api://%s', $this->getEndpoint()); + } + + protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $envelope): ResponseInterface + { + $response = $this->client->request('POST', 'https://'.$this->getEndpoint().'/send', [ + 'headers' => [ + 'Accept' => 'application/json', + 'OhMySMTP-Server-Token' => $this->key, + 'Content-Type' => 'application/json', + 'User-Agent' => 'OhMySMTP Symfony Mailer', + ], + 'json' => $this->getPayload($email, $envelope), + ]); + + try { + $statusCode = $response->getStatusCode(); + $result = $response->toArray(false); + } catch (DecodingExceptionInterface $e) { + 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 OhMySMTP endpoint.', $response, 0, $e); + } + + if (200 !== $statusCode) { + throw new HttpTransportException('Unable to send an email: '.$result['Message'].sprintf(' (code %d).', $result['ErrorCode']), $response); + } + + $sentMessage->setMessageId($result['id']); + + return $response; + } + + private function getPayload(Email $email, Envelope $envelope): array + { + $payload = [ + 'from' => $envelope->getSender()->toString(), + 'to' => implode(',', $this->stringifyAddresses($this->getRecipients($email, $envelope))), + 'cc' => implode(',', $this->stringifyAddresses($email->getCc())), + 'bcc' => implode(',', $this->stringifyAddresses($email->getBcc())), + 'replyto' => implode(',', $this->stringifyAddresses($email->getReplyTo())), + 'subject' => $email->getSubject(), + 'textbody' => $email->getTextBody(), + 'htmlbody' => $email->getHtmlBody(), + 'attachments' => $this->getAttachments($email), + 'tags' => [], + ]; + + $headersToBypass = ['from', 'to', 'cc', 'bcc', 'subject', 'content-type', 'sender', 'reply-to']; + + foreach ($email->getHeaders()->all() as $name => $header) { + if (\in_array($name, $headersToBypass, true)) { + continue; + } + + if ($header instanceof TagHeader) { + $payload['tags'][] = $header->getValue(); + continue; + } + + $payload['Headers'][] = [ + 'Name' => $name, + 'Value' => $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 = [ + 'name' => $filename, + 'content' => $attachment->bodyToString(), + 'content_type' => $headers->get('Content-Type')->getBody(), + ]; + + if ('inline' === $disposition) { + $att['cid'] = 'cid:'.$filename; + } + + $attachments[] = $att; + } + + return $attachments; + } + + private function getEndpoint(): ?string + { + return ($this->host ?: self::HOST).($this->port ? ':'.$this->port : ''); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/OhMySmtp/Transport/OhMySmtpSmtpTransport.php b/src/Symfony/Component/Mailer/Bridge/OhMySmtp/Transport/OhMySmtpSmtpTransport.php new file mode 100644 index 0000000000000..2cd015a4f8ca6 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/OhMySmtp/Transport/OhMySmtpSmtpTransport.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\OhMySmtp\Transport; + +use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\Mailer\Envelope; +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 Paul Oms + */ +final class OhMySmtpSmtpTransport extends EsmtpTransport +{ + public function __construct(string $id, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) + { + parent::__construct('smtp.ohmysmtp.com', 587, false, $dispatcher, $logger); + + $this->setUsername($id); + $this->setPassword($id); + } + + public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage + { + if ($message instanceof Message) { + $this->addOhMySmtpHeaders($message); + } + + return parent::send($message, $envelope); + } + + private function addOhMySmtpHeaders(Message $message): void + { + $headers = $message->getHeaders(); + + foreach ($headers->all() as $name => $header) { + if ($header instanceof TagHeader) { + if (null != $headers->get('X-OMS-Tags')) { + $existing = $headers->get('X-OMS-Tags')->getBody(); + $headers->remove('X-OMS-Tags'); + $headers->addTextHeader('X-OMS-Tags', $existing.', '.$header->getValue()); + } else { + $headers->addTextHeader('X-OMS-Tags', $header->getValue()); + } + $headers->remove($name); + } + } + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/OhMySmtp/Transport/OhMySmtpTransportFactory.php b/src/Symfony/Component/Mailer/Bridge/OhMySmtp/Transport/OhMySmtpTransportFactory.php new file mode 100644 index 0000000000000..79103815b0c25 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/OhMySmtp/Transport/OhMySmtpTransportFactory.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\OhMySmtp\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 Paul Oms + */ +final class OhMySmtpTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): TransportInterface + { + $scheme = $dsn->getScheme(); + + if ('ohmysmtp+api' === $scheme) { + $host = 'default' === $dsn->getHost() ? null : $dsn->getHost(); + $port = $dsn->getPort(); + + return (new OhMySmtpApiTransport($this->getUser($dsn), $this->client, $this->dispatcher, $this->logger))->setHost($host)->setPort($port); + } + + if ('ohmysmtp+smtp' === $scheme || 'ohmysmtp+smtps' === $scheme || 'ohmysmtp' === $scheme) { + return new OhMySmtpSmtpTransport($this->getUser($dsn), $this->dispatcher, $this->logger); + } + + throw new UnsupportedSchemeException($dsn, 'ohmysmtp', $this->getSupportedSchemes()); + } + + protected function getSupportedSchemes(): array + { + return ['ohmysmtp', 'ohmysmtp+api', 'ohmysmtp+smtp', 'ohmysmtp+smtps']; + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/OhMySmtp/composer.json b/src/Symfony/Component/Mailer/Bridge/OhMySmtp/composer.json new file mode 100644 index 0000000000000..8ff7082908497 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/OhMySmtp/composer.json @@ -0,0 +1,37 @@ +{ + "name": "symfony/oh-my-smtp-mailer", + "type": "symfony-bridge", + "description": "Symfony OhMySMTP 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" + }, + { + "name": "Paul Oms", + "homepage": "https://ohmysmtp.com" + } + ], + "require": { + "php": ">=7.2.5", + "psr/event-dispatcher": "^1", + "symfony/mailer": "^5.3.0|^6.0" + }, + "require-dev": { + "symfony/http-client": "^4.4|^5.0|^6.0" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Mailer\\Bridge\\OhMySmtp\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/src/Symfony/Component/Mailer/Bridge/OhMySmtp/phpunit.xml.dist b/src/Symfony/Component/Mailer/Bridge/OhMySmtp/phpunit.xml.dist new file mode 100644 index 0000000000000..706e4cf3c1339 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/OhMySmtp/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 3e1660cae1075..452b8ba3508a8 100644 --- a/src/Symfony/Component/Mailer/Exception/UnsupportedSchemeException.php +++ b/src/Symfony/Component/Mailer/Exception/UnsupportedSchemeException.php @@ -52,6 +52,10 @@ class UnsupportedSchemeException extends LogicException 'class' => Bridge\Amazon\Transport\SesTransportFactory::class, 'package' => 'symfony/amazon-mailer', ], + 'ohmysmtp' => [ + 'class' => Bridge\OhMySmtp\Transport\OhMySmtpTransportFactory::class, + 'package' => 'symfony/oh-my-smtp-mailer', + ], ]; public function __construct(Dsn $dsn, string $name = null, array $supported = []) diff --git a/src/Symfony/Component/Mailer/Tests/Exception/UnsupportedSchemeExceptionTest.php b/src/Symfony/Component/Mailer/Tests/Exception/UnsupportedSchemeExceptionTest.php index a3cb6351e96f9..54685da7cb772 100644 --- a/src/Symfony/Component/Mailer/Tests/Exception/UnsupportedSchemeExceptionTest.php +++ b/src/Symfony/Component/Mailer/Tests/Exception/UnsupportedSchemeExceptionTest.php @@ -18,6 +18,7 @@ use Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillTransportFactory; use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunTransportFactory; use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetTransportFactory; +use Symfony\Component\Mailer\Bridge\OhMySmtp\Transport\OhMySmtpTransportFactory; use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory; use Symfony\Component\Mailer\Bridge\Sendgrid\Transport\SendgridTransportFactory; use Symfony\Component\Mailer\Bridge\Sendinblue\Transport\SendinblueTransportFactory; @@ -40,6 +41,7 @@ public static function setUpBeforeClass(): void PostmarkTransportFactory::class => false, SendgridTransportFactory::class => false, SendinblueTransportFactory::class => false, + OhMySmtpTransportFactory::class => false, SesTransportFactory::class => false, ]); } @@ -66,6 +68,7 @@ public function messageWhereSchemeIsPartOfSchemeToPackageMapProvider(): \Generat yield ['postmark', 'symfony/postmark-mailer']; yield ['sendgrid', 'symfony/sendgrid-mailer']; yield ['sendinblue', 'symfony/sendinblue-mailer']; + yield ['ohmysmtp', 'symfony/oh-my-smtp-mailer']; yield ['ses', 'symfony/amazon-mailer']; } diff --git a/src/Symfony/Component/Mailer/Transport.php b/src/Symfony/Component/Mailer/Transport.php index a57225ed2f65e..3334b4068e553 100644 --- a/src/Symfony/Component/Mailer/Transport.php +++ b/src/Symfony/Component/Mailer/Transport.php @@ -18,6 +18,7 @@ use Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillTransportFactory; use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunTransportFactory; use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetTransportFactory; +use Symfony\Component\Mailer\Bridge\OhMySmtp\Transport\OhMySmtpTransportFactory; use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory; use Symfony\Component\Mailer\Bridge\Sendgrid\Transport\SendgridTransportFactory; use Symfony\Component\Mailer\Bridge\Sendinblue\Transport\SendinblueTransportFactory; @@ -51,6 +52,7 @@ class Transport PostmarkTransportFactory::class, SendgridTransportFactory::class, SendinblueTransportFactory::class, + OhMySmtpTransportFactory::class, SesTransportFactory::class, ];