Skip to content

Commit

Permalink
feature #33967 [Mailer] Add Message-Id to SentMessage when sending an…
Browse files Browse the repository at this point in the history
… email (fabpot)

This PR was squashed before being merged into the 4.4 branch (closes #33967).

Discussion
----------

[Mailer] Add Message-Id to SentMessage when sending an email

| Q             | A
| ------------- | ---
| Branch?       | 4.4
| Bug fix?      | yes-ish
| New feature?  | yes-ish as well
| Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files -->
| Tickets       | fixes #33681
| License       | MIT
| Doc PR        | -

This adds `SentMessage::getMessageId()` to retrieve the message id as generated internally OR by the provider sending the email.

Commits
-------

d97d1f9 [Mailer] Fix Message ID for Postmark SMTP
b42c269 Add Message-Id to SentMessage when sending an email
  • Loading branch information
fabpot committed Oct 13, 2019
2 parents 004ee73 + d97d1f9 commit 805449d
Show file tree
Hide file tree
Showing 13 changed files with 94 additions and 33 deletions.
Expand Up @@ -14,6 +14,7 @@
use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\Exception\HttpTransportException;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mailer\Transport\AbstractApiTransport;
use Symfony\Component\Mime\Email;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
Expand Down Expand Up @@ -48,7 +49,7 @@ public function __toString(): string
return sprintf('ses+api://%s@%s', $this->accessKey, $this->getEndpoint());
}

protected function doSendApi(Email $email, Envelope $envelope): ResponseInterface
protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $envelope): ResponseInterface
{
$date = gmdate('D, d M Y H:i:s e');
$auth = sprintf('AWS3-HTTPS AWSAccessKeyId=%s,Algorithm=HmacSHA256,Signature=%s', $this->accessKey, $this->getSignature($date));
Expand All @@ -62,12 +63,13 @@ protected function doSendApi(Email $email, Envelope $envelope): ResponseInterfac
'body' => $this->getPayload($email, $envelope),
]);

$result = new \SimpleXMLElement($response->getContent(false));
if (200 !== $response->getStatusCode()) {
$error = new \SimpleXMLElement($response->getContent(false));

throw new HttpTransportException(sprintf('Unable to send an email: %s (code %s).', $error->Error->Message, $error->Error->Code), $response);
throw new HttpTransportException(sprintf('Unable to send an email: %s (code %s).', $result->Error->Message, $result->Error->Code), $response);
}

$sentMessage->setMessageId($result->SendEmailResult->MessageId);

return $response;
}

Expand Down
Expand Up @@ -63,12 +63,13 @@ protected function doSendHttp(SentMessage $message): ResponseInterface
],
]);

$result = new \SimpleXMLElement($response->getContent(false));
if (200 !== $response->getStatusCode()) {
$error = new \SimpleXMLElement($response->getContent(false));

throw new HttpTransportException(sprintf('Unable to send an email: %s (code %s).', $error->Error->Message, $error->Error->Code), $response);
throw new HttpTransportException(sprintf('Unable to send an email: %s (code %s).', $result->Error->Message, $result->Error->Code), $response);
}

$message->setMessageId($result->SendEmailResult->MessageId);

return $response;
}

Expand Down
Expand Up @@ -14,6 +14,7 @@
use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\Exception\HttpTransportException;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mailer\Transport\AbstractApiTransport;
use Symfony\Component\Mime\Email;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
Expand Down Expand Up @@ -41,21 +42,23 @@ public function __toString(): string
return sprintf('mandrill+api://%s', $this->getEndpoint());
}

protected function doSendApi(Email $email, Envelope $envelope): ResponseInterface
protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $envelope): ResponseInterface
{
$response = $this->client->request('POST', 'https://'.$this->getEndpoint().'/api/1.0/messages/send.json', [
'json' => $this->getPayload($email, $envelope),
]);

$result = $response->toArray(false);
if (200 !== $response->getStatusCode()) {
$result = $response->toArray(false);
if ('error' === ($result['status'] ?? false)) {
throw new HttpTransportException(sprintf('Unable to send an email: %s (code %s).', $result['message'], $result['code']), $response);
}

throw new HttpTransportException(sprintf('Unable to send an email (code %s).', $result['code']), $response);
}

$sentMessage->setMessageId($result['_id']);

return $response;
}

Expand Down
Expand Up @@ -51,15 +51,17 @@ protected function doSendHttp(SentMessage $message): ResponseInterface
],
]);

$result = $response->toArray(false);
if (200 !== $response->getStatusCode()) {
$result = $response->toArray(false);
if ('error' === ($result['status'] ?? false)) {
throw new HttpTransportException(sprintf('Unable to send an email: %s (code %s).', $result['message'], $result['code']), $response);
}

throw new HttpTransportException(sprintf('Unable to send an email (code %s).', $result['code']), $response);
}

$message->setMessageId($result['_id']);

return $response;
}

Expand Down
Expand Up @@ -14,6 +14,7 @@
use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\Exception\HttpTransportException;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mailer\Transport\AbstractApiTransport;
use Symfony\Component\Mime\Email;
use Symfony\Component\Mime\Part\Multipart\FormDataPart;
Expand Down Expand Up @@ -46,7 +47,7 @@ public function __toString(): string
return sprintf('mailgun+api://%s?domain=%s', $this->getEndpoint(), $this->domain);
}

protected function doSendApi(Email $email, Envelope $envelope): ResponseInterface
protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $envelope): ResponseInterface
{
$body = new FormDataPart($this->getPayload($email, $envelope));
$headers = [];
Expand All @@ -61,14 +62,17 @@ protected function doSendApi(Email $email, Envelope $envelope): ResponseInterfac
'body' => $body->bodyToIterable(),
]);

$result = $response->toArray(false);
if (200 !== $response->getStatusCode()) {
if ('application/json' === $response->getHeaders(false)['content-type'][0]) {
throw new HttpTransportException(sprintf('Unable to send an email: %s (code %s).', $response->toArray(false)['message'], $response->getStatusCode()), $response);
throw new HttpTransportException(sprintf('Unable to send an email: %s (code %s).', $result['message'], $response->getStatusCode()), $response);
}

throw new HttpTransportException(sprintf('Unable to send an email: %s (code %s).', $response->getContent(false), $response->getStatusCode()), $response);
}

$sentMessage->setMessageId($result['id']);

return $response;
}

Expand Down
Expand Up @@ -64,14 +64,17 @@ protected function doSendHttp(SentMessage $message): ResponseInterface
'body' => $body->bodyToIterable(),
]);

$result = $response->toArray(false);
if (200 !== $response->getStatusCode()) {
if ('application/json' === $response->getHeaders(false)['content-type'][0]) {
throw new HttpTransportException(sprintf('Unable to send an email: %s (code %s).', $response->toArray(false)['message'], $response->getStatusCode()), $response);
throw new HttpTransportException(sprintf('Unable to send an email: %s (code %s).', $result['message'], $response->getStatusCode()), $response);
}

throw new HttpTransportException(sprintf('Unable to send an email: %s (code %s).', $response->getContent(false), $response->getStatusCode()), $response);
}

$message->setMessageId($result['id']);

return $response;
}

Expand Down
Expand Up @@ -14,6 +14,7 @@
use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\Exception\HttpTransportException;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mailer\Transport\AbstractApiTransport;
use Symfony\Component\Mime\Email;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
Expand Down Expand Up @@ -41,7 +42,7 @@ public function __toString(): string
return sprintf('postmark+api://%s', $this->getEndpoint());
}

protected function doSendApi(Email $email, Envelope $envelope): ResponseInterface
protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $envelope): ResponseInterface
{
$response = $this->client->request('POST', 'https://'.$this->getEndpoint().'/email', [
'headers' => [
Expand All @@ -51,12 +52,13 @@ protected function doSendApi(Email $email, Envelope $envelope): ResponseInterfac
'json' => $this->getPayload($email, $envelope),
]);

$result = $response->toArray(false);
if (200 !== $response->getStatusCode()) {
$error = $response->toArray(false);

throw new HttpTransportException(sprintf('Unable to send an email: %s (code %s).', $error['Message'], $error['ErrorCode']), $response);
throw new HttpTransportException(sprintf('Unable to send an email: %s (code %s).', $result['Message'], $result['ErrorCode']), $response);
}

$sentMessage->setMessageId($result['MessageID']);

return $response;
}

Expand Down
Expand Up @@ -12,7 +12,11 @@
namespace Symfony\Component\Mailer\Bridge\Postmark\Transport;

use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
use Symfony\Component\Mime\Message;
use Symfony\Component\Mime\RawMessage;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

/**
Expand All @@ -27,4 +31,13 @@ public function __construct(string $id, EventDispatcherInterface $dispatcher = n
$this->setUsername($id);
$this->setPassword($id);
}

public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage
{
if ($message instanceof Message) {
$message->getHeaders()->addTextHeader('X-PM-KeepID', 'true');
}

return parent::send($message, $envelope);
}
}
Expand Up @@ -59,6 +59,10 @@ public function testSend()
->expects($this->once())
->method('getStatusCode')
->willReturn(202);
$response
->expects($this->once())
->method('getHeaders')
->willReturn(['x-message-id' => '1']);

$httpClient = $this->createMock(HttpClientInterface::class);

Expand Down
Expand Up @@ -14,6 +14,7 @@
use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\Exception\HttpTransportException;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mailer\Transport\AbstractApiTransport;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
Expand Down Expand Up @@ -42,7 +43,7 @@ public function __toString(): string
return sprintf('sendgrid+api://%s', $this->getEndpoint());
}

protected function doSendApi(Email $email, Envelope $envelope): ResponseInterface
protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $envelope): ResponseInterface
{
$response = $this->client->request('POST', 'https://'.$this->getEndpoint().'/v3/mail/send', [
'json' => $this->getPayload($email, $envelope),
Expand All @@ -55,6 +56,8 @@ protected function doSendApi(Email $email, Envelope $envelope): ResponseInterfac
throw new HttpTransportException(sprintf('Unable to send an email: %s (code %s).', implode('; ', array_column($errors['errors'], 'message')), $response->getStatusCode()), $response);
}

$sentMessage->setMessageId($response->getHeaders(false)['x-message-id'][0]);

return $response;
}

Expand Down
24 changes: 23 additions & 1 deletion src/Symfony/Component/Mailer/SentMessage.php
Expand Up @@ -22,6 +22,7 @@ class SentMessage
private $original;
private $raw;
private $envelope;
private $messageId;
private $debug = '';

/**
Expand All @@ -31,9 +32,20 @@ public function __construct(RawMessage $message, Envelope $envelope)
{
$message->ensureValidity();

$this->raw = $message instanceof Message ? new RawMessage($message->toIterable()) : $message;
$this->original = $message;
$this->envelope = $envelope;

if ($message instanceof Message) {
$message = clone $message;
$headers = $message->getHeaders();
if (!$headers->has('Message-ID')) {
$headers->addIdHeader('Message-ID', $message->generateMessageId());
}
$this->messageId = $headers->get('Message-ID')->getId();
$this->raw = new RawMessage($message->toIterable());
} else {
$this->raw = $message;
}
}

public function getMessage(): RawMessage
Expand All @@ -51,6 +63,16 @@ public function getEnvelope(): Envelope
return $this->envelope;
}

public function setMessageId(string $id): void
{
$this->messageId = $id;
}

public function getMessageId(): string
{
return $this->messageId;
}

public function getDebug(): string
{
return $this->debug;
Expand Down
Expand Up @@ -24,7 +24,7 @@
*/
abstract class AbstractApiTransport extends AbstractHttpTransport
{
abstract protected function doSendApi(Email $email, Envelope $envelope): ResponseInterface;
abstract protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $envelope): ResponseInterface;

protected function doSendHttp(SentMessage $message): ResponseInterface
{
Expand All @@ -34,7 +34,7 @@ protected function doSendHttp(SentMessage $message): ResponseInterface
throw new RuntimeException(sprintf('Unable to send message with the "%s" transport: %s', __CLASS__, $e->getMessage()), 0, $e);
}

return $this->doSendApi($email, $message->getEnvelope());
return $this->doSendApi($message, $email, $message->getEnvelope());
}

protected function getRecipients(Email $email, Envelope $envelope): array
Expand Down
26 changes: 14 additions & 12 deletions src/Symfony/Component/Mime/Message.php
Expand Up @@ -32,9 +32,7 @@ public function __construct(Headers $headers = null, AbstractPart $body = null)

public function __clone()
{
if (null !== $this->headers) {
$this->headers = clone $this->headers;
}
$this->headers = clone $this->headers;

if (null !== $this->body) {
$this->body = clone $this->body;
Expand Down Expand Up @@ -86,16 +84,12 @@ public function getPreparedHeaders(): Headers
}

// determine the "real" sender
$senders = $headers->get('From')->getAddresses();
$sender = $senders[0];
if ($headers->has('Sender')) {
$sender = $headers->get('Sender')->getAddress();
} elseif (\count($senders) > 1) {
$headers->addMailboxHeader('Sender', $sender);
if (!$headers->has('Sender') && \count($froms = $headers->get('From')->getAddresses()) > 1) {
$headers->addMailboxHeader('Sender', $froms[0]);
}

if (!$headers->has('Message-ID')) {
$headers->addIdHeader('Message-ID', $this->generateMessageId($sender->getAddress()));
$headers->addIdHeader('Message-ID', $this->generateMessageId());
}

// remove the Bcc field which should NOT be part of the sent message
Expand Down Expand Up @@ -132,9 +126,17 @@ public function ensureValidity()
parent::ensureValidity();
}

private function generateMessageId(string $email): string
public function generateMessageId(): string
{
return bin2hex(random_bytes(16)).strstr($email, '@');
if ($this->headers->has('Sender')) {
$sender = $this->headers->get('Sender')->getAddress();
} elseif ($this->headers->has('From')) {
$sender = $this->headers->get('From')->getAddresses()[0];
} else {
throw new LogicException('An email must have a "From" or a "Sender" header to compute a Messsage ID.');
}

return bin2hex(random_bytes(16)).strstr($sender->getAddress(), '@');
}

public function __serialize(): array
Expand Down

0 comments on commit 805449d

Please sign in to comment.