Skip to content

Commit 685d131

Browse files
committed
feature #57456 [Mailer] Add mailomat bridge (scuben)
This PR was merged into the 7.2 branch. Discussion ---------- [Mailer] Add mailomat bridge | Q | A | ------------- | --- | Branch? | 7.2 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | n/a | License | MIT | Doc PR | symfony/symfony-docs#19978 | Recipe PR | symfony/recipes#1322 Adding a Bridge for [mailomat.swiss](https://mailomat.swiss) with remote events ([API documentation](https://api.mailomat.swiss/docs)). Commits ------- d20088b [Mailer] Add mailomat bridge
2 parents 8bf6952 + d20088b commit 685d131

33 files changed

+1127
-0
lines changed

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

+2
Original file line numberDiff line numberDiff line change
@@ -2624,6 +2624,7 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co
26242624
MailerBridge\MailerSend\Transport\MailerSendTransportFactory::class => 'mailer.transport_factory.mailersend',
26252625
MailerBridge\Mailgun\Transport\MailgunTransportFactory::class => 'mailer.transport_factory.mailgun',
26262626
MailerBridge\Mailjet\Transport\MailjetTransportFactory::class => 'mailer.transport_factory.mailjet',
2627+
MailerBridge\Mailomat\Transport\MailomatTransportFactory::class => 'mailer.transport_factory.mailomat',
26272628
MailerBridge\MailPace\Transport\MailPaceTransportFactory::class => 'mailer.transport_factory.mailpace',
26282629
MailerBridge\Mailchimp\Transport\MandrillTransportFactory::class => 'mailer.transport_factory.mailchimp',
26292630
MailerBridge\Postmark\Transport\PostmarkTransportFactory::class => 'mailer.transport_factory.postmark',
@@ -2647,6 +2648,7 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co
26472648
MailerBridge\MailerSend\Webhook\MailerSendRequestParser::class => 'mailer.webhook.request_parser.mailersend',
26482649
MailerBridge\Mailgun\Webhook\MailgunRequestParser::class => 'mailer.webhook.request_parser.mailgun',
26492650
MailerBridge\Mailjet\Webhook\MailjetRequestParser::class => 'mailer.webhook.request_parser.mailjet',
2651+
MailerBridge\Mailomat\Webhook\MailomatRequestParser::class => 'mailer.webhook.request_parser.mailomat',
26502652
MailerBridge\Postmark\Webhook\PostmarkRequestParser::class => 'mailer.webhook.request_parser.postmark',
26512653
MailerBridge\Resend\Webhook\ResendRequestParser::class => 'mailer.webhook.request_parser.resend',
26522654
MailerBridge\Sendgrid\Webhook\SendgridRequestParser::class => 'mailer.webhook.request_parser.sendgrid',

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

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Symfony\Component\Mailer\Bridge\MailerSend\Transport\MailerSendTransportFactory;
2121
use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunTransportFactory;
2222
use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetTransportFactory;
23+
use Symfony\Component\Mailer\Bridge\Mailomat\Transport\MailomatTransportFactory;
2324
use Symfony\Component\Mailer\Bridge\MailPace\Transport\MailPaceTransportFactory;
2425
use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory;
2526
use Symfony\Component\Mailer\Bridge\Resend\Transport\ResendTransportFactory;
@@ -52,6 +53,7 @@
5253
'mailersend' => MailerSendTransportFactory::class,
5354
'mailgun' => MailgunTransportFactory::class,
5455
'mailjet' => MailjetTransportFactory::class,
56+
'mailomat' => MailomatTransportFactory::class,
5557
'mailpace' => MailPaceTransportFactory::class,
5658
'native' => NativeTransportFactory::class,
5759
'null' => NullTransportFactory::class,

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

+7
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
use Symfony\Component\Mailer\Bridge\Mailgun\Webhook\MailgunRequestParser;
2020
use Symfony\Component\Mailer\Bridge\Mailjet\RemoteEvent\MailjetPayloadConverter;
2121
use Symfony\Component\Mailer\Bridge\Mailjet\Webhook\MailjetRequestParser;
22+
use Symfony\Component\Mailer\Bridge\Mailomat\RemoteEvent\MailomatPayloadConverter;
23+
use Symfony\Component\Mailer\Bridge\Mailomat\Webhook\MailomatRequestParser;
2224
use Symfony\Component\Mailer\Bridge\Postmark\RemoteEvent\PostmarkPayloadConverter;
2325
use Symfony\Component\Mailer\Bridge\Postmark\Webhook\PostmarkRequestParser;
2426
use Symfony\Component\Mailer\Bridge\Resend\RemoteEvent\ResendPayloadConverter;
@@ -48,6 +50,11 @@
4850
->args([service('mailer.payload_converter.mailjet')])
4951
->alias(MailjetRequestParser::class, 'mailer.webhook.request_parser.mailjet')
5052

53+
->set('mailer.payload_converter.mailomat', MailomatPayloadConverter::class)
54+
->set('mailer.webhook.request_parser.mailomat', MailomatRequestParser::class)
55+
->args([service('mailer.payload_converter.mailomat')])
56+
->alias(MailomatRequestParser::class, 'mailer.webhook.request_parser.mailomat')
57+
5158
->set('mailer.payload_converter.postmark', PostmarkPayloadConverter::class)
5259
->set('mailer.webhook.request_parser.postmark', PostmarkRequestParser::class)
5360
->args([service('mailer.payload_converter.postmark')])
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,71 @@
1+
Mailomat Bridge
2+
===============
3+
4+
Provides [Mailomat](https://mailomat.swiss) integration for Symfony Mailer.
5+
6+
Mailer
7+
-------
8+
9+
Configuration example:
10+
11+
```env
12+
# .env.local
13+
14+
# SMTP
15+
MAILER_DSN=mailomat+smtp://USERNAME:PASSWORD@default
16+
17+
# API
18+
MAILER_DSN=mailomat+api://KEY@default
19+
```
20+
21+
Where:
22+
- `USERNAME` is your Mailomat SMTP username (must use your full email address)
23+
- `PASSWORD` is your Mailomat SMTP password
24+
- `KEY` is your Mailomat API key
25+
26+
27+
Webhook
28+
-------
29+
30+
Create a route:
31+
32+
```yaml
33+
framework:
34+
webhook:
35+
routing:
36+
mailomat:
37+
service: mailer.webhook.request_parser.mailomat
38+
secret: '%env(WEBHOOK_MAILOMAT_SECRET)%'
39+
```
40+
41+
The configuration:
42+
43+
```env
44+
# .env.local
45+
46+
WEBHOOK_MAILOMAT_SECRET=your-mailomat-webhook-secret
47+
```
48+
49+
And a consumer:
50+
51+
```php
52+
#[\Symfony\Component\RemoteEvent\Attribute\AsRemoteEventConsumer(name: 'mailomat')]
53+
class MailomatConsumer implements ConsumerInterface
54+
{
55+
public function consume(AbstractMailerEvent $event): void
56+
{
57+
// your code
58+
}
59+
}
60+
```
61+
62+
Where:
63+
- `WEBHOOK_MAILOMAT_SECRET` is your Mailomat Webhook secret
64+
65+
Resources
66+
---------
67+
68+
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
69+
* [Report issues](https://github.com/symfony/symfony/issues) and
70+
[send Pull Requests](https://github.com/symfony/symfony/pulls)
71+
in the [main Symfony repository](https://github.com/symfony/symfony)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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\Mailomat\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 MailomatPayloadConverter implements PayloadConverterInterface
21+
{
22+
public function convert(array $payload): AbstractMailerEvent
23+
{
24+
if (\in_array($payload['eventType'], ['accepted', 'not_accepted', 'delivered', 'failure_tmp', 'failure_perm'], true)) {
25+
$name = match ($payload['eventType']) {
26+
'accepted' => MailerDeliveryEvent::RECEIVED,
27+
'not_accepted' => MailerDeliveryEvent::DROPPED,
28+
'delivered' => MailerDeliveryEvent::DELIVERED,
29+
'failure_tmp' => MailerDeliveryEvent::DEFERRED,
30+
'failure_perm' => MailerDeliveryEvent::BOUNCE,
31+
};
32+
$event = new MailerDeliveryEvent($name, $payload['id'], $payload);
33+
if (isset($payload['payload']['reason'])) {
34+
$event->setReason($payload['payload']['reason']);
35+
}
36+
} else {
37+
$name = match ($payload['eventType']) {
38+
'opened' => MailerEngagementEvent::OPEN,
39+
'clicked' => MailerEngagementEvent::CLICK,
40+
default => throw new ParseException(sprintf('Unsupported event "%s".', $payload['eventType'])),
41+
};
42+
$event = new MailerEngagementEvent($name, $payload['id'], $payload);
43+
}
44+
45+
if (!$date = \DateTimeImmutable::createFromFormat(\DateTimeInterface::ATOM, $payload['occurredAt'])) {
46+
throw new ParseException(sprintf('Invalid date "%s".', $payload['occurredAt']));
47+
}
48+
49+
$event->setDate($date);
50+
$event->setRecipientEmail($payload['recipient']);
51+
52+
return $event;
53+
}
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
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\Mailomat\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\Mailomat\Transport\MailomatApiTransport;
18+
use Symfony\Component\Mailer\Exception\HttpTransportException;
19+
use Symfony\Component\Mime\Address;
20+
use Symfony\Component\Mime\Email;
21+
use Symfony\Contracts\HttpClient\ResponseInterface;
22+
23+
class MailomatApiTransportTest extends TestCase
24+
{
25+
private const KEY = 'K3Y';
26+
27+
/**
28+
* @dataProvider getTransportData
29+
*/
30+
public function testToString(MailomatApiTransport $transport, string $expected): void
31+
{
32+
$this->assertSame($expected, (string) $transport);
33+
}
34+
35+
public static function getTransportData(): iterable
36+
{
37+
yield [
38+
new MailomatApiTransport(self::KEY),
39+
'mailomat+api://api.mailomat.swiss',
40+
];
41+
42+
yield [
43+
(new MailomatApiTransport(self::KEY))->setHost('example.com'),
44+
'mailomat+api://example.com',
45+
];
46+
47+
yield [
48+
(new MailomatApiTransport(self::KEY))->setHost('example.com')->setPort(99),
49+
'mailomat+api://example.com:99',
50+
];
51+
}
52+
53+
public function testSend()
54+
{
55+
$client = new MockHttpClient(function (string $method, string $url, array $options): ResponseInterface {
56+
$this->assertSame('POST', $method);
57+
$this->assertSame('https://api.mailomat.swiss/message', $url);
58+
$this->assertContains('Authorization: Bearer '.self::KEY, $options['headers']);
59+
$this->assertContains('Content-Type: application/json', $options['headers']);
60+
$this->assertContains('Accept: application/json', $options['headers']);
61+
62+
$body = json_decode($options['body'], true);
63+
$this->assertSame('from@mailomat.swiss', $body['from']['email']);
64+
$this->assertSame('From Doe', $body['from']['name']);
65+
66+
$this->assertSame('to@mailomat.swiss', $body['to'][0]['email']);
67+
$this->assertSame('To Doe', $body['to'][0]['name']);
68+
$this->assertSame('to-simple@mailomat.swiss', $body['to'][1]['email']);
69+
70+
$this->assertSame('cc@mailomat.swiss', $body['cc'][0]['email']);
71+
$this->assertSame('Cc Doe', $body['cc'][0]['name']);
72+
$this->assertSame('cc-simple@mailomat.swiss', $body['cc'][1]['email']);
73+
74+
$this->assertSame('bcc@mailomat.swiss', $body['bcc'][0]['email']);
75+
$this->assertSame('Bcc Doe', $body['bcc'][0]['name']);
76+
$this->assertSame('bcc-simple@mailomat.swiss', $body['bcc'][1]['email']);
77+
78+
$this->assertSame('replyto@mailomat.swiss', $body['replyTo'][0]['email']);
79+
$this->assertSame('ReplyTo Doe', $body['replyTo'][0]['name']);
80+
$this->assertSame('replyto-simple@mailomat.swiss', $body['replyTo'][1]['email']);
81+
82+
$this->assertSame('Hello!', $body['subject']);
83+
$this->assertSame('Hello There!', $body['text']);
84+
$this->assertSame('<p>Hello There!</p>', $body['html']);
85+
86+
return new JsonMockResponse(['messageUuid' => 'foobar'], [
87+
'http_code' => 202,
88+
]);
89+
});
90+
91+
$transport = new MailomatApiTransport(self::KEY, $client);
92+
93+
$mail = new Email();
94+
$mail->subject('Hello!')
95+
->from(new Address('from@mailomat.swiss', 'From Doe'))
96+
->to(new Address('to@mailomat.swiss', 'To Doe'), 'to-simple@mailomat.swiss')
97+
->cc(new Address('cc@mailomat.swiss', 'Cc Doe'), 'cc-simple@mailomat.swiss')
98+
->bcc(new Address('bcc@mailomat.swiss', 'Bcc Doe'), 'bcc-simple@mailomat.swiss')
99+
->replyTo(new Address('replyto@mailomat.swiss', 'ReplyTo Doe'), 'replyto-simple@mailomat.swiss')
100+
->text('Hello There!')
101+
->html('<p>Hello There!</p>');
102+
103+
$message = $transport->send($mail);
104+
105+
$this->assertSame('foobar', $message->getMessageId());
106+
}
107+
108+
public function testSendThrowsForErrorResponse()
109+
{
110+
$client = new MockHttpClient(static fn (string $method, string $url, array $options): ResponseInterface => new JsonMockResponse(
111+
[
112+
'status' => 422,
113+
'violations' => [
114+
[
115+
'propertyPath' => '',
116+
'message' => 'You must specify either text or html',
117+
],
118+
[
119+
'propertyPath' => 'from',
120+
'message' => 'Dieser Wert sollte nicht null sein.',
121+
],
122+
[
123+
'propertyPath' => 'to[1].email',
124+
'message' => 'Dieser Wert sollte nicht leer sein.',
125+
],
126+
[
127+
'propertyPath' => 'subject',
128+
'message' => 'Dieser Wert sollte nicht leer sein.',
129+
],
130+
],
131+
], [
132+
'http_code' => 422,
133+
]));
134+
$transport = new MailomatApiTransport(self::KEY, $client);
135+
$transport->setPort(8984);
136+
137+
$mail = new Email();
138+
$mail->subject('Hello!')
139+
->to(new Address('to@mailomat.swiss', 'To Doe'))
140+
->from(new Address('from@mailomat.swiss', 'From Doe'))
141+
->text('Hello There!');
142+
143+
$this->expectException(HttpTransportException::class);
144+
$this->expectExceptionMessage('Unable to send an email: You must specify either text or html; (from) Dieser Wert sollte nicht null sein.; (to[1].email) Dieser Wert sollte nicht leer sein.; (subject) Dieser Wert sollte nicht leer sein. (code 422)');
145+
$transport->send($mail);
146+
}
147+
}

0 commit comments

Comments
 (0)