Skip to content

Commit 05b677c

Browse files
committed
feature symfony#52916 [Mailer] Dispatch event for Postmark's "inactive recipient" API error (vicdelfant)
This PR was squashed before being merged into the 7.1 branch. Discussion ---------- [Mailer] Dispatch event for Postmark's "inactive recipient" API error | Q | A | ------------- | --- | Branch? | 7.1 | Bug fix? |no | New feature? | yes | Deprecations? | no | Issues | Fix symfony#50165 | License | MIT Given the use of Postmark and a recipient that previously generated a bounce, attempting to send another email to that email address results in an HTTP 422 response, along with the error code [`406 - Inactive Recipient`](https://postmarkapp.com/developer/api/overview#error-codes). In the real world, this situation can arise easily in cases of a typo or an email address that had temporary issues. Because `PostmarkApiTransport` requires an HTTP 200 and throws a `HttpTransportException` for any other HTTP code, something that's of minor interest to the application itself (i.e. a _possibly_ inactive e-mail address) now causes an exception. Depending on the userland logic, this can halt a process that sends out survey reminders, cause the Messenger component to queue the message for retrying etc. To handle this more elegantly, I'm proposing the following changes: * Add a `PostmarkDeliveryEventFactory` for casting any (future) delivery events to an instance of `PostmarkDeliveryEvent`. Currently, only support the 406 'inactive recipient' error is included; * Adjust the `PostmarkApiTransport` so it checks for supported delivery events on an HTTP code other than 200, and if so, dispatches the event accordingly. We cannot port this logic to the Postmark SMTP transport because, according to Postmark's own documentation, these error response codes are only provided by their API endpoints. Commits ------- 6ffd173 [Mailer] Dispatch event for Postmark's "inactive recipient" API error
2 parents 5d50fa8 + 6ffd173 commit 05b677c

File tree

7 files changed

+197
-0
lines changed

7 files changed

+197
-0
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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\Postmark\Event;
13+
14+
use Symfony\Component\Mime\Header\Headers;
15+
16+
class PostmarkDeliveryEvent
17+
{
18+
public const CODE_INACTIVE_RECIPIENT = 406;
19+
20+
private int $errorCode;
21+
22+
private Headers $headers;
23+
24+
private ?string $message;
25+
26+
public function __construct(string $message, int $errorCode)
27+
{
28+
$this->message = $message;
29+
$this->errorCode = $errorCode;
30+
31+
$this->headers = new Headers();
32+
}
33+
34+
public function getErrorCode(): int
35+
{
36+
return $this->errorCode;
37+
}
38+
39+
public function getHeaders(): Headers
40+
{
41+
return $this->headers;
42+
}
43+
44+
public function getMessage(): ?string
45+
{
46+
return $this->message;
47+
}
48+
49+
public function getMessageId(): ?string
50+
{
51+
if (!$this->headers->has('Message-ID')) {
52+
return null;
53+
}
54+
55+
return $this->headers->get('Message-ID')->getBodyAsString();
56+
}
57+
58+
public function setHeaders(Headers $headers): self
59+
{
60+
$this->headers = $headers;
61+
62+
return $this;
63+
}
64+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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\Postmark\Event;
13+
14+
use Symfony\Component\Mime\Email;
15+
16+
class PostmarkDeliveryEventFactory
17+
{
18+
public function create(int $errorCode, string $message, Email $email): PostmarkDeliveryEvent
19+
{
20+
if (!$this->supports($errorCode)) {
21+
throw new \InvalidArgumentException(sprintf('Error code "%s" is not supported.', $errorCode));
22+
}
23+
24+
return (new PostmarkDeliveryEvent($message, $errorCode))
25+
->setHeaders($email->getHeaders());
26+
}
27+
28+
public function supports(int $errorCode): bool
29+
{
30+
return \in_array($errorCode, [
31+
PostmarkDeliveryEvent::CODE_INACTIVE_RECIPIENT,
32+
]);
33+
}
34+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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\Postmark\Event;
13+
14+
class PostmarkEvents
15+
{
16+
public const DELIVERY = 'postmark.delivery';
17+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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\Postmark\Tests\Event;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Mailer\Bridge\Postmark\Event\PostmarkDeliveryEvent;
16+
use Symfony\Component\Mailer\Bridge\Postmark\Event\PostmarkDeliveryEventFactory;
17+
18+
class PostmarkDeliveryEventFactoryTest extends TestCase
19+
{
20+
public function testFactorySupportsInactiveRecipient()
21+
{
22+
$factory = new PostmarkDeliveryEventFactory();
23+
24+
$this->assertTrue($factory->supports(PostmarkDeliveryEvent::CODE_INACTIVE_RECIPIENT));
25+
}
26+
}

src/Symfony/Component/Mailer/Bridge/Postmark/Tests/Transport/PostmarkApiTransportTest.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
namespace Symfony\Component\Mailer\Bridge\Postmark\Tests\Transport;
1313

1414
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
1516
use Symfony\Component\HttpClient\MockHttpClient;
1617
use Symfony\Component\HttpClient\Response\JsonMockResponse;
18+
use Symfony\Component\Mailer\Bridge\Postmark\Event\PostmarkDeliveryEvent;
1719
use Symfony\Component\Mailer\Bridge\Postmark\Transport\MessageStreamHeader;
1820
use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkApiTransport;
1921
use Symfony\Component\Mailer\Envelope;
@@ -119,6 +121,38 @@ public function testSendThrowsForErrorResponse()
119121
$transport->send($mail);
120122
}
121123

124+
public function testSendDeliveryEventIsDispatched()
125+
{
126+
$client = new MockHttpClient(static fn (string $method, string $url, array $options): ResponseInterface => new JsonMockResponse(['Message' => 'Inactive recipient', 'ErrorCode' => 406], [
127+
'http_code' => 422,
128+
]));
129+
130+
$mail = new Email();
131+
$mail->subject('Hello!')
132+
->to(new Address('saif.gmati@symfony.com', 'Saif Eddin'))
133+
->from(new Address('fabpot@symfony.com', 'Fabien'))
134+
->text('Hello There!');
135+
136+
$expectedEvent = (new PostmarkDeliveryEvent('Inactive recipient', 406))
137+
->setHeaders($mail->getHeaders());
138+
139+
$dispatcher = $this->createMock(EventDispatcherInterface::class);
140+
$dispatcher
141+
->method('dispatch')
142+
->willReturnCallback(function ($event) use ($expectedEvent) {
143+
if ($event instanceof PostmarkDeliveryEvent) {
144+
$this->assertEquals($event, $expectedEvent);
145+
}
146+
147+
return $event;
148+
});
149+
150+
$transport = new PostmarkApiTransport('KEY', $client, $dispatcher);
151+
$transport->setPort(8984);
152+
153+
$transport->send($mail);
154+
}
155+
122156
public function testTagAndMetadataAndMessageStreamHeaders()
123157
{
124158
$email = new Email();

src/Symfony/Component/Mailer/Bridge/Postmark/Transport/PostmarkApiTransport.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
use Psr\EventDispatcher\EventDispatcherInterface;
1515
use Psr\Log\LoggerInterface;
16+
use Symfony\Component\Mailer\Bridge\Postmark\Event\PostmarkDeliveryEventFactory;
17+
use Symfony\Component\Mailer\Bridge\Postmark\Event\PostmarkEvents;
1618
use Symfony\Component\Mailer\Envelope;
1719
use Symfony\Component\Mailer\Exception\HttpTransportException;
1820
use Symfony\Component\Mailer\Exception\TransportException;
@@ -33,13 +35,16 @@ class PostmarkApiTransport extends AbstractApiTransport
3335
{
3436
private const HOST = 'api.postmarkapp.com';
3537

38+
private ?EventDispatcherInterface $dispatcher;
39+
3640
private string $key;
3741

3842
private ?string $messageStream = null;
3943

4044
public function __construct(string $key, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
4145
{
4246
$this->key = $key;
47+
$this->dispatcher = $dispatcher;
4348

4449
parent::__construct($client, $dispatcher, $logger);
4550
}
@@ -69,6 +74,18 @@ protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $e
6974
}
7075

7176
if (200 !== $statusCode) {
77+
$eventFactory = new PostmarkDeliveryEventFactory();
78+
79+
// Some delivery issues can be handled silently - route those through EventDispatcher
80+
if (null !== $this->dispatcher && $eventFactory->supports($result['ErrorCode'])) {
81+
$this->dispatcher->dispatch(
82+
$eventFactory->create($result['ErrorCode'], $result['Message'], $email),
83+
PostmarkEvents::DELIVERY,
84+
);
85+
86+
return $response;
87+
}
88+
7289
throw new HttpTransportException('Unable to send an email: '.$result['Message'].sprintf(' (code %d).', $result['ErrorCode']), $response);
7390
}
7491

src/Symfony/Component/Mailer/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
7.1
5+
---
6+
7+
* Dispatch Postmark's "406 - Inactive recipient" API error code as a `PostmarkDeliveryEvent` instead of throwing an exception
8+
49
7.0
510
---
611

0 commit comments

Comments
 (0)