Skip to content

Commit f5927d6

Browse files
committed
feature #42710 [Mailer] Added OhMySMTP Bridge (paul-oms)
This PR was squashed before being merged into the 5.4 branch. Discussion ---------- [Mailer] Added OhMySMTP Bridge This adds the https://ohmysmtp.com bridge to enable sending over the [OhMySMTP](https://ohmysmtp.com) API. | Q | A | ------------- | --- | Branch? | 5.4 <!-- see below --> | Bug fix? | no | New feature? | yes <!-- please update src/**/CHANGELOG.md files --> | Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files --> | Tickets | none <!-- prefix each issue number with "Fix #", no need to create an issue if none exist, explain below instead --> | License | MIT | Doc PR | symfony/symfony-docs#15747... <!-- required for new features --> <!-- Replace this notice by a short README for your feature/bugfix. This will help people understand your PR and can be used as a start for the documentation. Additionally (see https://symfony.com/releases): - Always add tests and ensure they pass. - Never break backward compatibility (see https://symfony.com/bc). - Bug fixes must be submitted against the lowest maintained branch where they apply (lowest branches are regularly merged to upper ones so they get the fixes too.) - Features and deprecations must be submitted against branch 5.x. - Changelog entry should follow https://symfony.com/doc/current/contributing/code/conventions.html#writing-a-changelog-entry --> Commits ------- ce72fd8 [Mailer] Added OhMySMTP Bridge
2 parents b975e4c + ce72fd8 commit f5927d6

18 files changed

+684
-0
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
use Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillTransportFactory;
9191
use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunTransportFactory;
9292
use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetTransportFactory;
93+
use Symfony\Component\Mailer\Bridge\OhMySmtp\Transport\OhMySmtpTransportFactory;
9394
use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory;
9495
use Symfony\Component\Mailer\Bridge\Sendgrid\Transport\SendgridTransportFactory;
9596
use Symfony\Component\Mailer\Bridge\Sendinblue\Transport\SendinblueTransportFactory;
@@ -2357,6 +2358,7 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co
23572358
SendgridTransportFactory::class => 'mailer.transport_factory.sendgrid',
23582359
SendinblueTransportFactory::class => 'mailer.transport_factory.sendinblue',
23592360
SesTransportFactory::class => 'mailer.transport_factory.amazon',
2361+
OhMySmtpTransportFactory::class => 'mailer.transport_factory.ohmysmtp',
23602362
];
23612363

23622364
foreach ($classToServices as $class => $service) {

src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillTransportFactory;
1717
use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunTransportFactory;
1818
use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetTransportFactory;
19+
use Symfony\Component\Mailer\Bridge\OhMySmtp\Transport\OhMySmtpTransportFactory;
1920
use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory;
2021
use Symfony\Component\Mailer\Bridge\Sendgrid\Transport\SendgridTransportFactory;
2122
use Symfony\Component\Mailer\Bridge\Sendinblue\Transport\SendinblueTransportFactory;
@@ -76,6 +77,10 @@
7677
->parent('mailer.transport_factory.abstract')
7778
->tag('mailer.transport_factory')
7879

80+
->set('mailer.transport_factory.ohmysmtp', OhMySmtpTransportFactory::class)
81+
->parent('mailer.transport_factory.abstract')
82+
->tag('mailer.transport_factory')
83+
7984
->set('mailer.transport_factory.smtp', EsmtpTransportFactory::class)
8085
->parent('mailer.transport_factory.abstract')
8186
->tag('mailer.transport_factory', ['priority' => -100])
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/Tests export-ignore
2+
/phpunit.xml.dist export-ignore
3+
/.gitattributes export-ignore
4+
/.gitignore export-ignore
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
vendor/
2+
composer.lock
3+
phpunit.xml
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CHANGELOG
2+
=========
3+
4+
5.4
5+
---
6+
7+
* Add the bridge
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2021 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.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
OhMySMTP Bridge
2+
===============
3+
4+
Provides [OhMySMTP](https://ohmysmtp.com) integration for Symfony Mailer.
5+
6+
7+
DSN example
8+
-----------
9+
10+
```
11+
MAILER_DSN=ohmysmtp+api://API_TOKEN@default
12+
```
13+
14+
where:
15+
- `API_TOKEN` is your OhMySMTP API Token
16+
17+
18+
Resources
19+
---------
20+
21+
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
22+
* [Report issues](https://github.com/symfony/symfony/issues) and
23+
[send Pull Requests](https://github.com/symfony/symfony/pulls)
24+
in the [main Symfony repository](https://github.com/symfony/symfony)
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\OhMySmtp\Tests\Transport;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\HttpClient\MockHttpClient;
16+
use Symfony\Component\HttpClient\Response\MockResponse;
17+
use Symfony\Component\Mailer\Bridge\OhMySmtp\Transport\OhMySmtpApiTransport;
18+
use Symfony\Component\Mailer\Envelope;
19+
use Symfony\Component\Mailer\Exception\HttpTransportException;
20+
use Symfony\Component\Mailer\Header\TagHeader;
21+
use Symfony\Component\Mime\Address;
22+
use Symfony\Component\Mime\Email;
23+
use Symfony\Contracts\HttpClient\ResponseInterface;
24+
25+
final class OhMySmtpApiTransportTest extends TestCase
26+
{
27+
/**
28+
* @dataProvider getTransportData
29+
*/
30+
public function testToString(OhMySmtpApiTransport $transport, string $expected)
31+
{
32+
$this->assertSame($expected, (string) $transport);
33+
}
34+
35+
public function getTransportData(): array
36+
{
37+
return [
38+
[
39+
new OhMySmtpApiTransport('KEY'),
40+
'ohmysmtp+api://app.ohmysmtp.com/api/v1',
41+
],
42+
[
43+
(new OhMySmtpApiTransport('KEY'))->setHost('example.com'),
44+
'ohmysmtp+api://example.com',
45+
],
46+
[
47+
(new OhMySmtpApiTransport('KEY'))->setHost('example.com')->setPort(99),
48+
'ohmysmtp+api://example.com:99',
49+
],
50+
];
51+
}
52+
53+
public function testCustomHeader()
54+
{
55+
$email = new Email();
56+
$email->getHeaders()->addTextHeader('foo', 'bar');
57+
$envelope = new Envelope(new Address('[email protected]'), [new Address('[email protected]')]);
58+
59+
$transport = new OhMySmtpApiTransport('ACCESS_KEY');
60+
$method = new \ReflectionMethod(OhMySmtpApiTransport::class, 'getPayload');
61+
$method->setAccessible(true);
62+
$payload = $method->invoke($transport, $email, $envelope);
63+
64+
$this->assertArrayHasKey('Headers', $payload);
65+
$this->assertCount(1, $payload['Headers']);
66+
67+
$this->assertEquals(['Name' => 'foo', 'Value' => 'bar'], $payload['Headers'][0]);
68+
}
69+
70+
public function testSend()
71+
{
72+
$client = new MockHttpClient(function (string $method, string $url, array $options): ResponseInterface {
73+
$this->assertSame('POST', $method);
74+
$this->assertSame('https://app.ohmysmtp.com/api/v1/send', $url);
75+
$this->assertStringContainsStringIgnoringCase('OhMySMTP-Server-Token: KEY', $options['headers'][1] ?? $options['request_headers'][1]);
76+
77+
$body = json_decode($options['body'], true);
78+
$this->assertSame('"Fabien" <[email protected]>', $body['from']);
79+
$this->assertSame('"Saif Eddin" <[email protected]>', $body['to']);
80+
$this->assertSame('Hello!', $body['subject']);
81+
$this->assertSame('Hello There!', $body['textbody']);
82+
83+
return new MockResponse(json_encode(['id' => 'foobar', 'status' => 'pending']), [
84+
'http_code' => 200,
85+
]);
86+
});
87+
88+
$transport = new OhMySmtpApiTransport('KEY', $client);
89+
90+
$mail = new Email();
91+
$mail->subject('Hello!')
92+
->to(new Address('[email protected]', 'Saif Eddin'))
93+
->from(new Address('[email protected]', 'Fabien'))
94+
->text('Hello There!');
95+
96+
$message = $transport->send($mail);
97+
98+
$this->assertSame('foobar', $message->getMessageId());
99+
}
100+
101+
public function testSendThrowsForErrorResponse()
102+
{
103+
$client = new MockHttpClient(static function (string $method, string $url, array $options): ResponseInterface {
104+
return new MockResponse(json_encode(['Message' => 'i\'m a teapot', 'ErrorCode' => 418]), [
105+
'http_code' => 418,
106+
'response_headers' => [
107+
'content-type' => 'application/json',
108+
],
109+
]);
110+
});
111+
$transport = new OhMySmtpApiTransport('KEY', $client);
112+
$transport->setPort(8984);
113+
114+
$mail = new Email();
115+
$mail->subject('Hello!')
116+
->to(new Address('[email protected]', 'Saif Eddin'))
117+
->from(new Address('[email protected]', 'Fabien'))
118+
->text('Hello There!');
119+
120+
$this->expectException(HttpTransportException::class);
121+
$this->expectExceptionMessage('Unable to send an email: i\'m a teapot (code 418).');
122+
$transport->send($mail);
123+
}
124+
125+
public function testTagAndMetadataHeaders()
126+
{
127+
$email = new Email();
128+
$email->getHeaders()->add(new TagHeader('password-reset'));
129+
$email->getHeaders()->add(new TagHeader('2nd-tag'));
130+
131+
$envelope = new Envelope(new Address('[email protected]'), [new Address('[email protected]')]);
132+
133+
$transport = new OhMySmtpApiTransport('ACCESS_KEY');
134+
$method = new \ReflectionMethod(OhMySmtpApiTransport::class, 'getPayload');
135+
$method->setAccessible(true);
136+
$payload = $method->invoke($transport, $email, $envelope);
137+
138+
$this->assertArrayNotHasKey('Headers', $payload);
139+
$this->assertArrayHasKey('tags', $payload);
140+
141+
$this->assertSame(['password-reset', '2nd-tag'], $payload['tags']);
142+
}
143+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\OhMySmtp\Tests\Transport;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Mailer\Bridge\OhMySmtp\Transport\OhMySmtpSmtpTransport;
16+
use Symfony\Component\Mailer\Header\TagHeader;
17+
use Symfony\Component\Mime\Email;
18+
19+
final class OhMySmtpSmtpTransportTest extends TestCase
20+
{
21+
public function testCustomHeader()
22+
{
23+
$email = new Email();
24+
$email->getHeaders()->addTextHeader('foo', 'bar');
25+
26+
$transport = new OhMySmtpSmtpTransport('ACCESS_KEY');
27+
$method = new \ReflectionMethod(OhMySmtpSmtpTransport::class, 'addOhMySmtpHeaders');
28+
$method->setAccessible(true);
29+
$method->invoke($transport, $email);
30+
31+
$this->assertCount(1, $email->getHeaders()->toArray());
32+
$this->assertSame('foo: bar', $email->getHeaders()->get('FOO')->toString());
33+
}
34+
35+
public function testTagAndMetadataHeaders()
36+
{
37+
$email = new Email();
38+
$email->getHeaders()->addTextHeader('foo', 'bar');
39+
$email->getHeaders()->add(new TagHeader('password-reset'));
40+
$email->getHeaders()->add(new TagHeader('2nd-tag'));
41+
42+
$transport = new OhMySmtpSmtpTransport('ACCESS_KEY');
43+
$method = new \ReflectionMethod(OhMySmtpSmtpTransport::class, 'addOhMySmtpHeaders');
44+
$method->setAccessible(true);
45+
$method->invoke($transport, $email);
46+
47+
$this->assertCount(2, $email->getHeaders()->toArray());
48+
$this->assertSame('foo: bar', $email->getHeaders()->get('FOO')->toString());
49+
$this->assertSame('X-OMS-Tags: password-reset, 2nd-tag', $email->getHeaders()->get('X-OMS-Tags')->toString());
50+
}
51+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\OhMySmtp\Tests\Transport;
13+
14+
use Symfony\Component\Mailer\Bridge\OhMySmtp\Transport\OhMySmtpApiTransport;
15+
use Symfony\Component\Mailer\Bridge\OhMySmtp\Transport\OhMySmtpSmtpTransport;
16+
use Symfony\Component\Mailer\Bridge\OhMySmtp\Transport\OhMySmtpTransportFactory;
17+
use Symfony\Component\Mailer\Test\TransportFactoryTestCase;
18+
use Symfony\Component\Mailer\Transport\Dsn;
19+
use Symfony\Component\Mailer\Transport\TransportFactoryInterface;
20+
21+
final class OhMySmtpTransportFactoryTest extends TransportFactoryTestCase
22+
{
23+
public function getFactory(): TransportFactoryInterface
24+
{
25+
return new OhMySmtpTransportFactory($this->getDispatcher(), $this->getClient(), $this->getLogger());
26+
}
27+
28+
public function supportsProvider(): iterable
29+
{
30+
yield [
31+
new Dsn('ohmysmtp+api', 'default'),
32+
true,
33+
];
34+
35+
yield [
36+
new Dsn('ohmysmtp', 'default'),
37+
true,
38+
];
39+
40+
yield [
41+
new Dsn('ohmysmtp+smtp', 'default'),
42+
true,
43+
];
44+
45+
yield [
46+
new Dsn('ohmysmtp+smtps', 'default'),
47+
true,
48+
];
49+
50+
yield [
51+
new Dsn('ohmysmtp+smtp', 'example.com'),
52+
true,
53+
];
54+
}
55+
56+
public function createProvider(): iterable
57+
{
58+
$dispatcher = $this->getDispatcher();
59+
$logger = $this->getLogger();
60+
61+
yield [
62+
new Dsn('ohmysmtp+api', 'default', self::USER),
63+
new OhMySmtpApiTransport(self::USER, $this->getClient(), $dispatcher, $logger),
64+
];
65+
66+
yield [
67+
new Dsn('ohmysmtp+api', 'example.com', self::USER, '', 8080),
68+
(new OhMySmtpApiTransport(self::USER, $this->getClient(), $dispatcher, $logger))->setHost('example.com')->setPort(8080),
69+
];
70+
71+
yield [
72+
new Dsn('ohmysmtp', 'default', self::USER),
73+
new OhMySmtpSmtpTransport(self::USER, $dispatcher, $logger),
74+
];
75+
76+
yield [
77+
new Dsn('ohmysmtp+smtp', 'default', self::USER),
78+
new OhMySmtpSmtpTransport(self::USER, $dispatcher, $logger),
79+
];
80+
81+
yield [
82+
new Dsn('ohmysmtp+smtps', 'default', self::USER),
83+
new OhMySmtpSmtpTransport(self::USER, $dispatcher, $logger),
84+
];
85+
}
86+
87+
public function unsupportedSchemeProvider(): iterable
88+
{
89+
yield [
90+
new Dsn('ohmysmtp+foo', 'default', self::USER),
91+
'The "ohmysmtp+foo" scheme is not supported; supported schemes for mailer "ohmysmtp" are: "ohmysmtp", "ohmysmtp+api", "ohmysmtp+smtp", "ohmysmtp+smtps".',
92+
];
93+
}
94+
95+
public function incompleteDsnProvider(): iterable
96+
{
97+
yield [new Dsn('ohmysmtp+api', 'default')];
98+
}
99+
}

0 commit comments

Comments
 (0)