Skip to content

Commit 7ed3a4b

Browse files
committed
feature symfony#61315 [Mailer] Add compatibility for Mailtrap's sandbox (KiloSierraCharlie)
This PR was squashed before being merged into the 7.4 branch. Discussion ---------- [Mailer] Add compatibility for Mailtrap's sandbox Mailtrap's sandbox requires the Inbox ID to be passed as part of the URI, which has previously been neglected. | Q | A | ------------- | --- | Branch? | 7.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | n/a | License | MIT Previously symfony#61305 but I messed up when re-basing to 7.4. Updates with appropriate tests and changelog. Commits ------- 3256633 [Mailer] Add compatibility for Mailtrap's sandbox
2 parents f5a0ce6 + 3256633 commit 7ed3a4b

File tree

7 files changed

+224
-12
lines changed

7 files changed

+224
-12
lines changed
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
CHANGELOG
22
=========
33

4+
7.4
5+
---
6+
7+
* Add compatibility for Mailtrap's sandbox with new DSN scheme
8+
49
7.2
510
---
611

7-
* Add the bridge
12+
* Add the bridge

src/Symfony/Component/Mailer/Bridge/Mailtrap/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@ Configuration example:
99
# SMTP
1010
MAILER_DSN=mailtrap+smtp://PASSWORD@default
1111
12-
# API
12+
# API (Live)
1313
MAILER_DSN=mailtrap+api://TOKEN@default
14+
15+
# API (Sandbox)
16+
MAILER_DSN=mailtrap+sandbox://TOKEN@default?inboxId=INBOX_ID
1417
```
1518

1619
where:
1720
- `PASSWORD` is your Mailtrap SMTP Password
1821
- `TOKEN` is your Mailtrap Server Token
22+
- `INBOX_ID` is your Mailtrap sandbox inbox's ID
1923

2024
Resources
2125
---------
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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\Mailtrap\Tests\Transport;
13+
14+
use PHPUnit\Framework\Attributes\DataProvider;
15+
use PHPUnit\Framework\TestCase;
16+
use Symfony\Component\HttpClient\MockHttpClient;
17+
use Symfony\Component\HttpClient\Response\JsonMockResponse;
18+
use Symfony\Component\Mailer\Bridge\Mailtrap\Transport\MailtrapApiSandboxTransport;
19+
use Symfony\Component\Mailer\Envelope;
20+
use Symfony\Component\Mailer\Exception\HttpTransportException;
21+
use Symfony\Component\Mailer\Exception\TransportException;
22+
use Symfony\Component\Mailer\Header\MetadataHeader;
23+
use Symfony\Component\Mailer\Header\TagHeader;
24+
use Symfony\Component\Mime\Address;
25+
use Symfony\Component\Mime\Email;
26+
use Symfony\Contracts\HttpClient\ResponseInterface;
27+
28+
class MailtrapApiSandboxTransportTest extends TestCase
29+
{
30+
#[DataProvider('getTransportData')]
31+
public function testToString(MailtrapApiSandboxTransport $transport, string $expected)
32+
{
33+
$this->assertSame($expected, (string) $transport);
34+
}
35+
36+
public static function getTransportData(): array
37+
{
38+
return [
39+
[
40+
new MailtrapApiSandboxTransport('KEY', 123456),
41+
'mailtrap+sandbox://sandbox.api.mailtrap.io/?inboxId=123456',
42+
],
43+
[
44+
(new MailtrapApiSandboxTransport('KEY', 123456))->setHost('example.com'),
45+
'mailtrap+sandbox://example.com/?inboxId=123456',
46+
],
47+
[
48+
(new MailtrapApiSandboxTransport('KEY', 123456))->setHost('example.com')->setPort(99),
49+
'mailtrap+sandbox://example.com:99/?inboxId=123456',
50+
],
51+
[
52+
new MailtrapApiSandboxTransport('KEY', 123456),
53+
'mailtrap+sandbox://sandbox.api.mailtrap.io/?inboxId=123456',
54+
],
55+
];
56+
}
57+
58+
public function testSend()
59+
{
60+
$client = new MockHttpClient(function (string $method, string $url, array $options): ResponseInterface {
61+
$this->assertSame('POST', $method);
62+
$this->assertSame('https://sandbox.api.mailtrap.io/api/send/123456', $url);
63+
64+
$body = json_decode($options['body'], true);
65+
$this->assertSame(['email' => 'fabpot@symfony.com', 'name' => 'Fabien'], $body['from']);
66+
$this->assertSame([['email' => 'kevin@symfony.com', 'name' => 'Kevin']], $body['to']);
67+
$this->assertSame('Hello!', $body['subject']);
68+
$this->assertSame('Hello There!', $body['text']);
69+
70+
return new JsonMockResponse([], [
71+
'http_code' => 200,
72+
]);
73+
});
74+
75+
$transport = new MailtrapApiSandboxTransport('KEY', 123456, $client);
76+
77+
$mail = new Email();
78+
$mail->subject('Hello!')
79+
->to(new Address('kevin@symfony.com', 'Kevin'))
80+
->from(new Address('fabpot@symfony.com', 'Fabien'))
81+
->text('Hello There!');
82+
83+
$transport->send($mail);
84+
}
85+
86+
public function testSendThrowsForErrorResponse()
87+
{
88+
$client = new MockHttpClient(static fn (string $method, string $url, array $options): ResponseInterface => new JsonMockResponse(['errors' => ['i\'m a teapot']], [
89+
'http_code' => 418,
90+
]));
91+
$transport = new MailtrapApiSandboxTransport('KEY', 123456, $client);
92+
$transport->setPort(8984);
93+
94+
$mail = new Email();
95+
$mail->subject('Hello!')
96+
->to(new Address('kevin@symfony.com', 'Kevin'))
97+
->from(new Address('fabpot@symfony.com', 'Fabien'))
98+
->text('Hello There!');
99+
100+
$this->expectException(HttpTransportException::class);
101+
$this->expectExceptionMessage('Unable to send email: "i\'m a teapot" (status code 418).');
102+
$transport->send($mail);
103+
}
104+
105+
public function testTagAndMetadataHeaders()
106+
{
107+
$email = new Email();
108+
$email->getHeaders()->add(new TagHeader('password-reset'));
109+
$email->getHeaders()->add(new MetadataHeader('Color', 'blue'));
110+
$email->getHeaders()->add(new MetadataHeader('Client-ID', '12345'));
111+
$envelope = new Envelope(new Address('alice@system.com'), [new Address('bob@system.com')]);
112+
113+
$transport = new MailtrapApiSandboxTransport('ACCESS_KEY', 123456);
114+
$method = new \ReflectionMethod(MailtrapApiSandboxTransport::class, 'getPayload');
115+
$payload = $method->invoke($transport, $email, $envelope);
116+
117+
$this->assertArrayNotHasKey('Headers', $payload);
118+
$this->assertArrayHasKey('category', $payload);
119+
$this->assertArrayHasKey('custom_variables', $payload);
120+
121+
$this->assertSame('password-reset', $payload['category']);
122+
$this->assertSame(['Color' => 'blue', 'Client-ID' => '12345'], $payload['custom_variables']);
123+
}
124+
125+
public function testMultipleTagsAreNotAllowed()
126+
{
127+
$email = new Email();
128+
$email->getHeaders()->add(new TagHeader('tag1'));
129+
$email->getHeaders()->add(new TagHeader('tag2'));
130+
$envelope = new Envelope(new Address('alice@system.com'), [new Address('bob@system.com')]);
131+
132+
$transport = new MailtrapApiSandboxTransport('ACCESS_KEY', 123456);
133+
$method = new \ReflectionMethod(MailtrapApiSandboxTransport::class, 'getPayload');
134+
135+
$this->expectException(TransportException::class);
136+
137+
$method->invoke($transport, $email, $envelope);
138+
}
139+
}

src/Symfony/Component/Mailer/Bridge/Mailtrap/Tests/Transport/MailtrapTransportFactoryTest.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Psr\Log\NullLogger;
1515
use Symfony\Component\HttpClient\MockHttpClient;
16+
use Symfony\Component\Mailer\Bridge\Mailtrap\Transport\MailtrapApiSandboxTransport;
1617
use Symfony\Component\Mailer\Bridge\Mailtrap\Transport\MailtrapApiTransport;
1718
use Symfony\Component\Mailer\Bridge\Mailtrap\Transport\MailtrapSmtpTransport;
1819
use Symfony\Component\Mailer\Bridge\Mailtrap\Transport\MailtrapTransportFactory;
@@ -37,6 +38,11 @@ public static function supportsProvider(): iterable
3738
true,
3839
];
3940

41+
yield [
42+
new Dsn('mailtrap+sandbox', 'default'),
43+
true,
44+
];
45+
4046
yield [
4147
new Dsn('mailtrap', 'default'),
4248
true,
@@ -72,6 +78,16 @@ public static function createProvider(): iterable
7278
(new MailtrapApiTransport(self::USER, new MockHttpClient(), null, $logger))->setHost('example.com')->setPort(8080),
7379
];
7480

81+
yield [
82+
new Dsn('mailtrap+sandbox', 'default', self::USER, null, null, ['inboxId' => '123456']),
83+
new MailtrapApiSandboxTransport(self::USER, 123456, new MockHttpClient(), null, $logger),
84+
];
85+
86+
yield [
87+
new Dsn('mailtrap+sandbox', 'example.com', self::USER, null, 8080, ['inboxId' => '123456']),
88+
(new MailtrapApiSandboxTransport(self::USER, 123456, new MockHttpClient(), null, $logger))->setHost('example.com')->setPort(8080),
89+
];
90+
7591
yield [
7692
new Dsn('mailtrap', 'default', self::USER),
7793
new MailtrapSmtpTransport(self::USER, null, $logger),
@@ -92,7 +108,7 @@ public static function unsupportedSchemeProvider(): iterable
92108
{
93109
yield [
94110
new Dsn('mailtrap+foo', 'default', self::USER),
95-
'The "mailtrap+foo" scheme is not supported; supported schemes for mailer "mailtrap" are: "mailtrap", "mailtrap+api", "mailtrap+smtp", "mailtrap+smtps".',
111+
'The "mailtrap+foo" scheme is not supported; supported schemes for mailer "mailtrap" are: "mailtrap", "mailtrap+api", "mailtrap+sandbox", "mailtrap+smtp", "mailtrap+smtps".',
96112
];
97113
}
98114

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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\Mailtrap\Transport;
13+
14+
use Psr\EventDispatcher\EventDispatcherInterface;
15+
use Psr\Log\LoggerInterface;
16+
use Symfony\Contracts\HttpClient\HttpClientInterface;
17+
18+
/**
19+
* @author Kieran Cross
20+
*/
21+
final class MailtrapApiSandboxTransport extends MailtrapApiTransport
22+
{
23+
protected const HOST = 'sandbox.api.mailtrap.io';
24+
25+
public function __construct(
26+
#[\SensitiveParameter] private string $token,
27+
private int $inboxId,
28+
?HttpClientInterface $client = null,
29+
?EventDispatcherInterface $dispatcher = null,
30+
?LoggerInterface $logger = null,
31+
) {
32+
parent::__construct($token, $client, $dispatcher, $logger);
33+
}
34+
35+
public function __toString(): string
36+
{
37+
return \sprintf('mailtrap+sandbox://%s%s/?inboxId=%u', $this->host ?: static::HOST, $this->port ? ':'.$this->port : '', $this->inboxId);
38+
}
39+
40+
protected function getEndpoint(): string
41+
{
42+
return parent::getEndpoint().'/'.$this->inboxId;
43+
}
44+
}

src/Symfony/Component/Mailer/Bridge/Mailtrap/Transport/MailtrapApiTransport.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@
3030
/**
3131
* @author Kevin Bond <kevinbond@gmail.com>
3232
*/
33-
final class MailtrapApiTransport extends AbstractApiTransport
33+
class MailtrapApiTransport extends AbstractApiTransport
3434
{
35-
private const HOST = 'send.api.mailtrap.io';
35+
protected const HOST = 'send.api.mailtrap.io';
3636
private const HEADERS_TO_BYPASS = ['from', 'to', 'cc', 'bcc', 'subject', 'content-type', 'sender'];
3737

3838
public function __construct(
@@ -46,12 +46,12 @@ public function __construct(
4646

4747
public function __toString(): string
4848
{
49-
return \sprintf('mailtrap+api://%s', $this->getEndpoint());
49+
return \sprintf('mailtrap+api://%s%s', $this->host ?: static::HOST, $this->port ? ':'.$this->port : '');
5050
}
5151

5252
protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $envelope): ResponseInterface
5353
{
54-
$response = $this->client->request('POST', 'https://'.$this->getEndpoint().'/api/send', [
54+
$response = $this->client->request('POST', 'https://'.$this->getEndpoint(), [
5555
'json' => $this->getPayload($email, $envelope),
5656
'auth_bearer' => $this->token,
5757
]);
@@ -143,8 +143,8 @@ private static function encodeEmail(Address $address): array
143143
return array_filter(['email' => $address->getEncodedAddress(), 'name' => $address->getName()]);
144144
}
145145

146-
private function getEndpoint(): ?string
146+
protected function getEndpoint(): string
147147
{
148-
return ($this->host ?: self::HOST).($this->port ? ':'.$this->port : '');
148+
return ($this->host ?: static::HOST).($this->port ? ':'.$this->port : '').'/api/send';
149149
}
150150
}

src/Symfony/Component/Mailer/Bridge/Mailtrap/Transport/MailtrapTransportFactory.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,15 @@ public function create(Dsn $dsn): TransportInterface
2626
$scheme = $dsn->getScheme();
2727
$user = $this->getUser($dsn);
2828

29-
if ('mailtrap+api' === $scheme) {
29+
if ('mailtrap+api' === $scheme || 'mailtrap+sandbox' === $scheme) {
3030
$host = 'default' === $dsn->getHost() ? null : $dsn->getHost();
3131
$port = $dsn->getPort();
3232

33-
return (new MailtrapApiTransport($user, $this->client, $this->dispatcher, $this->logger))->setHost($host)->setPort($port);
33+
if ('mailtrap+api' === $scheme) {
34+
return (new MailtrapApiTransport($user, $this->client, $this->dispatcher, $this->logger))->setHost($host)->setPort($port);
35+
} else {
36+
return (new MailtrapApiSandboxTransport($user, $dsn->getOption('inboxId'), $this->client, $this->dispatcher, $this->logger))->setHost($host)->setPort($port);
37+
}
3438
}
3539

3640
if ('mailtrap+smtp' === $scheme || 'mailtrap+smtps' === $scheme || 'mailtrap' === $scheme) {
@@ -42,6 +46,6 @@ public function create(Dsn $dsn): TransportInterface
4246

4347
protected function getSupportedSchemes(): array
4448
{
45-
return ['mailtrap', 'mailtrap+api', 'mailtrap+smtp', 'mailtrap+smtps'];
49+
return ['mailtrap', 'mailtrap+api', 'mailtrap+sandbox', 'mailtrap+smtp', 'mailtrap+smtps'];
4650
}
4751
}

0 commit comments

Comments
 (0)