Skip to content

[Mailer] Add compatibility for Mailtrap's sandbox #61315

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: 7.4
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/Symfony/Component/Mailer/Bridge/Mailtrap/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
CHANGELOG
=========

7.4
---

* Add compatibility for Mailtrap's sandbox with new DSN scheme

7.2
---

* Add the bridge
* Add the bridge
6 changes: 5 additions & 1 deletion src/Symfony/Component/Mailer/Bridge/Mailtrap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@ Configuration example:
# SMTP
MAILER_DSN=mailtrap+smtp://PASSWORD@default

# API
# API (Live)
MAILER_DSN=mailtrap+api://TOKEN@default

# API (Sandbox)
MAILER_DSN=mailtrap+sandbox://TOKEN@default?inboxId=INBOX_ID
```

where:
- `PASSWORD` is your Mailtrap SMTP Password
- `TOKEN` is your Mailtrap Server Token
- `INBOX_ID` is your Mailtrap sandbox inbox's ID

Resources
---------
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Mailer\Bridge\Mailtrap\Tests\Transport;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\JsonMockResponse;
use Symfony\Component\Mailer\Bridge\Mailtrap\Transport\MailtrapApiSandboxTransport;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\Exception\HttpTransportException;
use Symfony\Component\Mailer\Exception\TransportException;
use Symfony\Component\Mailer\Header\MetadataHeader;
use Symfony\Component\Mailer\Header\TagHeader;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
use Symfony\Contracts\HttpClient\ResponseInterface;

class MailtrapApiSandboxTransportTest extends TestCase
{
#[DataProvider('getTransportData')]
public function testToString(MailtrapApiSandboxTransport $transport, string $expected)
{
$this->assertSame($expected, (string) $transport);
}

public static function getTransportData(): array
{
return [
[
(new MailtrapApiSandboxTransport('KEY'))->setInboxId(123456),
'mailtrap+sandbox://sandbox.api.mailtrap.io/?inboxId=123456',
],
[
(new MailtrapApiSandboxTransport('KEY'))->setInboxId(123456)->setHost('example.com'),
'mailtrap+sandbox://example.com/?inboxId=123456',
],
[
(new MailtrapApiSandboxTransport('KEY'))->setInboxId(123456)->setHost('example.com')->setPort(99),
'mailtrap+sandbox://example.com:99/?inboxId=123456',
],
[
(new MailtrapApiSandboxTransport('KEY'))->setInboxId(123456),
'mailtrap+sandbox://sandbox.api.mailtrap.io/?inboxId=123456',
],
];
}

public function testSend()
{
$client = new MockHttpClient(function (string $method, string $url, array $options): ResponseInterface {
$this->assertSame('POST', $method);
$this->assertSame('https://sandbox.api.mailtrap.io/api/send/123456', $url);

$body = json_decode($options['body'], true);
$this->assertSame(['email' => 'fabpot@symfony.com', 'name' => 'Fabien'], $body['from']);
$this->assertSame([['email' => 'kevin@symfony.com', 'name' => 'Kevin']], $body['to']);
$this->assertSame('Hello!', $body['subject']);
$this->assertSame('Hello There!', $body['text']);

return new JsonMockResponse([], [
'http_code' => 200,
]);
});

$transport = (new MailtrapApiSandboxTransport('KEY', $client))->setInboxId(123456);

$mail = new Email();
$mail->subject('Hello!')
->to(new Address('kevin@symfony.com', 'Kevin'))
->from(new Address('fabpot@symfony.com', 'Fabien'))
->text('Hello There!');

$transport->send($mail);
}

public function testSendThrowsForErrorResponse()
{
$client = new MockHttpClient(static fn (string $method, string $url, array $options): ResponseInterface => new JsonMockResponse(['errors' => ['i\'m a teapot']], [
'http_code' => 418,
]));
$transport = (new MailtrapApiSandboxTransport('KEY', $client))->setInboxId(123456);
$transport->setPort(8984);

$mail = new Email();
$mail->subject('Hello!')
->to(new Address('kevin@symfony.com', 'Kevin'))
->from(new Address('fabpot@symfony.com', 'Fabien'))
->text('Hello There!');

$this->expectException(HttpTransportException::class);
$this->expectExceptionMessage('Unable to send email: "i\'m a teapot" (status code 418).');
$transport->send($mail);
}

public function testTagAndMetadataHeaders()
{
$email = new Email();
$email->getHeaders()->add(new TagHeader('password-reset'));
$email->getHeaders()->add(new MetadataHeader('Color', 'blue'));
$email->getHeaders()->add(new MetadataHeader('Client-ID', '12345'));
$envelope = new Envelope(new Address('alice@system.com'), [new Address('bob@system.com')]);

$transport = (new MailtrapApiSandboxTransport('ACCESS_KEY'))->setInboxId(123456);
$method = new \ReflectionMethod(MailtrapApiSandboxTransport::class, 'getPayload');
$payload = $method->invoke($transport, $email, $envelope);

$this->assertArrayNotHasKey('Headers', $payload);
$this->assertArrayHasKey('category', $payload);
$this->assertArrayHasKey('custom_variables', $payload);

$this->assertSame('password-reset', $payload['category']);
$this->assertSame(['Color' => 'blue', 'Client-ID' => '12345'], $payload['custom_variables']);
}

public function testMultipleTagsAreNotAllowed()
{
$email = new Email();
$email->getHeaders()->add(new TagHeader('tag1'));
$email->getHeaders()->add(new TagHeader('tag2'));
$envelope = new Envelope(new Address('alice@system.com'), [new Address('bob@system.com')]);

$transport = (new MailtrapApiSandboxTransport('ACCESS_KEY'))->setInboxId(123456);
$method = new \ReflectionMethod(MailtrapApiSandboxTransport::class, 'getPayload');

$this->expectException(TransportException::class);

$method->invoke($transport, $email, $envelope);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use Psr\Log\NullLogger;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\Mailer\Bridge\Mailtrap\Transport\MailtrapApiSandboxTransport;
use Symfony\Component\Mailer\Bridge\Mailtrap\Transport\MailtrapApiTransport;
use Symfony\Component\Mailer\Bridge\Mailtrap\Transport\MailtrapSmtpTransport;
use Symfony\Component\Mailer\Bridge\Mailtrap\Transport\MailtrapTransportFactory;
Expand All @@ -37,6 +38,11 @@ public static function supportsProvider(): iterable
true,
];

yield [
new Dsn('mailtrap+sandbox', 'default'),
true,
];

yield [
new Dsn('mailtrap', 'default'),
true,
Expand Down Expand Up @@ -72,6 +78,16 @@ public static function createProvider(): iterable
(new MailtrapApiTransport(self::USER, new MockHttpClient(), null, $logger))->setHost('example.com')->setPort(8080),
];

yield [
new Dsn('mailtrap+sandbox', 'default', self::USER, null, null, ['inboxId' => '123456']),
(new MailtrapApiSandboxTransport(self::USER, new MockHttpClient(), null, $logger))->setInboxId(123456),
];

yield [
new Dsn('mailtrap+sandbox', 'example.com', self::USER, null, 8080, ['inboxId' => '123456']),
(new MailtrapApiSandboxTransport(self::USER, new MockHttpClient(), null, $logger))->setHost('example.com')->setPort(8080)->setInboxId(123456),
];

yield [
new Dsn('mailtrap', 'default', self::USER),
new MailtrapSmtpTransport(self::USER, null, $logger),
Expand All @@ -92,7 +108,7 @@ public static function unsupportedSchemeProvider(): iterable
{
yield [
new Dsn('mailtrap+foo', 'default', self::USER),
'The "mailtrap+foo" scheme is not supported; supported schemes for mailer "mailtrap" are: "mailtrap", "mailtrap+api", "mailtrap+smtp", "mailtrap+smtps".',
'The "mailtrap+foo" scheme is not supported; supported schemes for mailer "mailtrap" are: "mailtrap", "mailtrap+api", "mailtrap+sandbox", "mailtrap+smtp", "mailtrap+smtps".',
];
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Mailer\Bridge\Mailtrap\Transport;

/**
* @author Kieran Cross
*/
final class MailtrapApiSandboxTransport extends MailtrapApiTransport
{
protected const HOST = 'sandbox.api.mailtrap.io';
private ?int $inboxId = null;

public function __toString(): string
{
return \sprintf('mailtrap+sandbox://%s', $this->getEndpoint().'/?inboxId='.$this->inboxId);
}

/**
* @return $this
*/
public function setInboxId(?int $inboxId): static
{
$this->inboxId = $inboxId;

return $this;
}

protected function getEndpointPath(): string
{
return self::ENDPOINT_PATH.'/'.$this->inboxId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@
/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
final class MailtrapApiTransport extends AbstractApiTransport
class MailtrapApiTransport extends AbstractApiTransport
{
private const HOST = 'send.api.mailtrap.io';
private const HEADERS_TO_BYPASS = ['from', 'to', 'cc', 'bcc', 'subject', 'content-type', 'sender'];
protected const HOST = 'send.api.mailtrap.io';
protected const ENDPOINT_PATH = '/api/send';
protected const HEADERS_TO_BYPASS = ['from', 'to', 'cc', 'bcc', 'subject', 'content-type', 'sender'];

public function __construct(
#[\SensitiveParameter] private string $token,
Expand All @@ -51,7 +52,7 @@ public function __toString(): string

protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $envelope): ResponseInterface
{
$response = $this->client->request('POST', 'https://'.$this->getEndpoint().'/api/send', [
$response = $this->client->request('POST', 'https://'.$this->getEndpoint().$this->getEndpointPath(), [
'json' => $this->getPayload($email, $envelope),
'auth_bearer' => $this->token,
]);
Expand All @@ -72,7 +73,7 @@ protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $e
return $response;
}

private function getPayload(Email $email, Envelope $envelope): array
protected function getPayload(Email $email, Envelope $envelope): array
{
$payload = [
'from' => self::encodeEmail($envelope->getSender()),
Expand Down Expand Up @@ -112,7 +113,7 @@ private function getPayload(Email $email, Envelope $envelope): array
return $payload;
}

private function getAttachments(Email $email): array
protected function getAttachments(Email $email): array
{
$attachments = [];

Expand All @@ -138,13 +139,23 @@ private function getAttachments(Email $email): array
return $attachments;
}

private static function encodeEmail(Address $address): array
protected static function encodeEmail(Address $address): array
{
return array_filter(['email' => $address->getEncodedAddress(), 'name' => $address->getName()]);
}

private function getEndpoint(): ?string
protected function getHost(): string
{
return ($this->host ?: self::HOST).($this->port ? ':'.$this->port : '');
return static::HOST;
}

protected function getEndpoint(): ?string
{
return ($this->host ?: $this->getHost()).($this->port ? ':'.$this->port : '');
}

protected function getEndpointPath(): string
{
return self::ENDPOINT_PATH;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,17 @@ public function create(Dsn $dsn): TransportInterface
$scheme = $dsn->getScheme();
$user = $this->getUser($dsn);

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

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

return (new MailtrapApiSandboxTransport($user, $this->client, $this->dispatcher, $this->logger))->setHost($host)->setPort($port)->setInboxId($inboxId);
}
}

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

protected function getSupportedSchemes(): array
{
return ['mailtrap', 'mailtrap+api', 'mailtrap+smtp', 'mailtrap+smtps'];
return ['mailtrap', 'mailtrap+api', 'mailtrap+sandbox', 'mailtrap+smtp', 'mailtrap+smtps'];
}
}
Loading