Skip to content

Commit db84ab3

Browse files
jonasclaesfabpot
authored andcommitted
[Mailer] Implement Postal mailer
1 parent 5938c51 commit db84ab3

16 files changed

+489
-0
lines changed

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

+1
Original file line numberDiff line numberDiff line change
@@ -2642,6 +2642,7 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co
26422642
MailerBridge\Mailomat\Transport\MailomatTransportFactory::class => 'mailer.transport_factory.mailomat',
26432643
MailerBridge\MailPace\Transport\MailPaceTransportFactory::class => 'mailer.transport_factory.mailpace',
26442644
MailerBridge\Mailchimp\Transport\MandrillTransportFactory::class => 'mailer.transport_factory.mailchimp',
2645+
MailerBridge\Postal\Transport\PostalTransportFactory::class => 'mailer.transport_factory.postal',
26452646
MailerBridge\Postmark\Transport\PostmarkTransportFactory::class => 'mailer.transport_factory.postmark',
26462647
MailerBridge\Resend\Transport\ResendTransportFactory::class => 'mailer.transport_factory.resend',
26472648
MailerBridge\Scaleway\Transport\ScalewayTransportFactory::class => 'mailer.transport_factory.scaleway',

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

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetTransportFactory;
2323
use Symfony\Component\Mailer\Bridge\Mailomat\Transport\MailomatTransportFactory;
2424
use Symfony\Component\Mailer\Bridge\MailPace\Transport\MailPaceTransportFactory;
25+
use Symfony\Component\Mailer\Bridge\Postal\Transport\PostalTransportFactory;
2526
use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory;
2627
use Symfony\Component\Mailer\Bridge\Resend\Transport\ResendTransportFactory;
2728
use Symfony\Component\Mailer\Bridge\Scaleway\Transport\ScalewayTransportFactory;
@@ -57,6 +58,7 @@
5758
'mailpace' => MailPaceTransportFactory::class,
5859
'native' => NativeTransportFactory::class,
5960
'null' => NullTransportFactory::class,
61+
'postal' => PostalTransportFactory::class,
6062
'postmark' => PostmarkTransportFactory::class,
6163
'resend' => ResendTransportFactory::class,
6264
'scaleway' => ScalewayTransportFactory::class,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/Tests export-ignore
2+
/phpunit.xml.dist export-ignore
3+
/.git* export-ignore
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
vendor/
2+
composer.lock
3+
phpunit.xml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CHANGELOG
2+
=========
3+
4+
7.2
5+
---
6+
7+
* Add the bridge
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2024-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.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
Postal Mailer
2+
=============
3+
4+
Provides [Postal Email](https://docs.postalserver.io/) integration for Symfony Mailer.
5+
6+
Configuration example:
7+
8+
```env
9+
# API
10+
MAILER_DSN=postal+api://API_TOKEN@BASE_URL
11+
```
12+
13+
where:
14+
- `API_TOKEN` is your Postal Email API Key
15+
- `BASE_URL` is your Postal installation base URL
16+
17+
Resources
18+
---------
19+
20+
* [Postal API Docs](https://docs.postalserver.io/developer/api)
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)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Postal\Tests\Transport;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\HttpClient\MockHttpClient;
16+
use Symfony\Component\HttpClient\Response\JsonMockResponse;
17+
use Symfony\Component\Mailer\Bridge\Postal\Transport\PostalApiTransport;
18+
use Symfony\Component\Mailer\Exception\HttpTransportException;
19+
use Symfony\Component\Mime\Address;
20+
use Symfony\Component\Mime\Email;
21+
use Symfony\Component\Mime\Part\DataPart;
22+
use Symfony\Contracts\HttpClient\ResponseInterface;
23+
24+
class PostalApiTransportTest extends TestCase
25+
{
26+
/**
27+
* @dataProvider getTransportData
28+
*/
29+
public function testToString(PostalApiTransport $transport, string $expected)
30+
{
31+
$this->assertSame($expected, (string) $transport);
32+
}
33+
34+
public static function getTransportData(): array
35+
{
36+
return [
37+
[
38+
(new PostalApiTransport('TOKEN', 'postal.localhost')),
39+
'postal+api://postal.localhost',
40+
],
41+
[
42+
(new PostalApiTransport('TOKEN', 'postal.localhost'))->setPort(99),
43+
'postal+api://postal.localhost:99',
44+
],
45+
];
46+
}
47+
48+
public function testSend()
49+
{
50+
$client = new MockHttpClient(function (string $method, string $url, array $options): ResponseInterface {
51+
$this->assertSame('POST', $method);
52+
$this->assertSame('https://postal.localhost:8984/api/v1/send/message', $url);
53+
$this->assertStringContainsString('X-Server-API-Key: TOKEN', $options['headers'][0] ?? $options['request_headers'][0]);
54+
55+
$body = json_decode($options['body'], true);
56+
$this->assertSame('fabpot@symfony.com', $body['from']);
57+
$this->assertSame('saif.gmati@symfony.com', $body['to'][0]);
58+
$this->assertSame('Hello!', $body['subject']);
59+
$this->assertSame('Hello There!', $body['plain_body']);
60+
$this->assertSame('<h1>Hello There!</h1>', $body['html_body']);
61+
$this->assertCount(1, $body['attachments']);
62+
$this->assertSame('attachment.txt', $body['attachments'][0]['name']);
63+
$this->assertSame('text/plain', $body['attachments'][0]['content_type']);
64+
$this->assertSame(base64_encode('some attachment'), $body['attachments'][0]['data']);
65+
$this->assertSame('foo@bar.fr', $body['reply_to']);
66+
67+
return new JsonMockResponse(['message_id' => 'foobar'], [
68+
'http_code' => 200,
69+
]);
70+
});
71+
$transport = new PostalApiTransport('TOKEN', 'postal.localhost', $client);
72+
$transport->setPort(8984);
73+
74+
$mail = new Email();
75+
$mail->subject('Hello!')
76+
->to(new Address('saif.gmati@symfony.com', 'Saif Eddin'))
77+
->from(new Address('fabpot@symfony.com', 'Fabien'))
78+
->replyTo(new Address('foo@bar.fr', 'Foo'))
79+
->text('Hello There!')
80+
->html('<h1>Hello There!</h1>')
81+
->addPart(new DataPart('some attachment', 'attachment.txt', 'text/plain'));
82+
83+
$message = $transport->send($mail);
84+
85+
$this->assertSame('foobar', $message->getMessageId());
86+
}
87+
88+
public function testSendThrowsForErrorResponse()
89+
{
90+
$client = new MockHttpClient(function (string $method, string $url, array $options): ResponseInterface {
91+
return new JsonMockResponse(['message' => 'i\'m a teapot'], [
92+
'http_code' => 418,
93+
]);
94+
});
95+
$transport = new PostalApiTransport('TOKEN', 'postal.localhost', $client);
96+
97+
$mail = new Email();
98+
$mail->subject('Hello!')
99+
->to(new Address('saif.gmati@symfony.com', 'Saif Eddin'))
100+
->from(new Address('fabpot@symfony.com', 'Fabien'))
101+
->text('Hello There!');
102+
103+
$this->expectException(HttpTransportException::class);
104+
$this->expectExceptionMessage('Unable to send an email: i\'m a teapot (code 418).');
105+
$transport->send($mail);
106+
}
107+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Postal\Tests\Transport;
13+
14+
use Psr\Log\NullLogger;
15+
use Symfony\Component\HttpClient\MockHttpClient;
16+
use Symfony\Component\Mailer\Bridge\Postal\Transport\PostalApiTransport;
17+
use Symfony\Component\Mailer\Bridge\Postal\Transport\PostalTransportFactory;
18+
use Symfony\Component\Mailer\Test\TransportFactoryTestCase;
19+
use Symfony\Component\Mailer\Transport\Dsn;
20+
use Symfony\Component\Mailer\Transport\TransportFactoryInterface;
21+
22+
class PostalTransportFactoryTest extends TransportFactoryTestCase
23+
{
24+
public function getFactory(): TransportFactoryInterface
25+
{
26+
return new PostalTransportFactory(null, new MockHttpClient(), new NullLogger());
27+
}
28+
29+
public static function supportsProvider(): iterable
30+
{
31+
yield [
32+
new Dsn('postal+api', 'postal.localhost'),
33+
true,
34+
];
35+
36+
yield [
37+
new Dsn('postal', 'postal.localhost'),
38+
true,
39+
];
40+
}
41+
42+
public static function createProvider(): iterable
43+
{
44+
$logger = new NullLogger();
45+
46+
yield [
47+
new Dsn('postal+api', 'postal.localhost', null, self::PASSWORD),
48+
(new PostalApiTransport(self::PASSWORD, 'postal.localhost', new MockHttpClient(), null, $logger)),
49+
];
50+
51+
yield [
52+
new Dsn('postal', 'postal.localhost', null, self::PASSWORD),
53+
(new PostalApiTransport(self::PASSWORD, 'postal.localhost', new MockHttpClient(), null, $logger)),
54+
];
55+
}
56+
57+
public static function unsupportedSchemeProvider(): iterable
58+
{
59+
yield [
60+
new Dsn('postal+foo', 'postal.localhost', null, self::PASSWORD),
61+
'The "postal+foo" scheme is not supported; supported schemes for mailer "postal" are: "postal", "postal+api".',
62+
];
63+
}
64+
65+
public static function incompleteDsnProvider(): iterable
66+
{
67+
yield [new Dsn('postal+api', 'postal.localhost', null)];
68+
}
69+
}

0 commit comments

Comments
 (0)