Skip to content
Permalink
Browse files

feature #35050 [Mailer] added tag/metadata support (kbond)

This PR was merged into the 5.1-dev branch.

Discussion
----------

[Mailer] added tag/metadata support

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | Fix #35047
| License       | MIT
| Doc PR        | todo

This is an alternative to #34766 for adding tag and metadata support in a more generalized way.

Most transports allow for open/click tracking headers - maybe this should be handled in a similar way?

I added implementations for the Postmark (SMTP and API) and Mailgun (SMTP and API) transports. I can add others and tests/docs if this is acceptable.

### Example:

```php
use Symfony\Component\Mailer\Header\MetadataHeader;
use Symfony\Component\Mailer\Header\TagHeader;

$email->getHeaders()->add(new TagHeader('password-reset'));
$email->getHeaders()->add(new MetadataHeader('Color', 'blue'));
$email->getHeaders()->add(new MetadataHeader('Client-ID', '12345'));
```

The Postmark/Mailgun providers will parse these into their own headers/payload. For transports that don't support tags/metadata, these are just added as custom headers:

```
X-Tag: password-reset
X-Metadata-Color: blue
X-Metadata-Client-ID: 12345
```

Commits
-------

f2cdafc [Mailer] added tag/metadata support
  • Loading branch information
fabpot committed Jan 30, 2020
2 parents a2b6085 + f2cdafc commit 81abb4e156d82f10e1b882fa4f07f3bda0456822
Showing with 516 additions and 4 deletions.
  1. +40 −0 src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Transport/MandrillApiTransportTest.php
  2. +22 −0 src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Transport/MandrillHttpTransportTest.php
  3. +40 −0 src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Transport/MandrillSmtpTransportTest.php
  4. +14 −0 src/Symfony/Component/Mailer/Bridge/Mailchimp/Transport/MandrillApiTransport.php
  5. +54 −0 src/Symfony/Component/Mailer/Bridge/Mailchimp/Transport/MandrillHeadersTrait.php
  6. +2 −0 src/Symfony/Component/Mailer/Bridge/Mailchimp/Transport/MandrillHttpTransport.php
  7. +2 −0 src/Symfony/Component/Mailer/Bridge/Mailchimp/Transport/MandrillSmtpTransport.php
  8. +1 −1 src/Symfony/Component/Mailer/Bridge/Mailchimp/composer.json
  9. +27 −0 src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Transport/MailgunApiTransportTest.php
  10. +22 −0 src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Transport/MailgunHttpTransportTest.php
  11. +43 −0 src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Transport/MailgunSmtpTransportTest.php
  12. +14 −0 src/Symfony/Component/Mailer/Bridge/Mailgun/Transport/MailgunApiTransport.php
  13. +54 −0 src/Symfony/Component/Mailer/Bridge/Mailgun/Transport/MailgunHeadersTrait.php
  14. +2 −0 src/Symfony/Component/Mailer/Bridge/Mailgun/Transport/MailgunHttpTransport.php
  15. +2 −0 src/Symfony/Component/Mailer/Bridge/Mailgun/Transport/MailgunSmtpTransport.php
  16. +1 −1 src/Symfony/Component/Mailer/Bridge/Mailgun/composer.json
  17. +23 −0 src/Symfony/Component/Mailer/Bridge/Postmark/Tests/Transport/PostmarkApiTransportTest.php
  18. +57 −0 src/Symfony/Component/Mailer/Bridge/Postmark/Tests/Transport/PostmarkSmtpTransportTest.php
  19. +14 −0 src/Symfony/Component/Mailer/Bridge/Postmark/Transport/PostmarkApiTransport.php
  20. +22 −1 src/Symfony/Component/Mailer/Bridge/Postmark/Transport/PostmarkSmtpTransport.php
  21. +1 −1 src/Symfony/Component/Mailer/Bridge/Postmark/composer.json
  22. +34 −0 src/Symfony/Component/Mailer/Header/MetadataHeader.php
  23. +25 −0 src/Symfony/Component/Mailer/Header/TagHeader.php
@@ -14,6 +14,8 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillApiTransport;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\Header\MetadataHeader;
use Symfony\Component\Mailer\Header\TagHeader;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;

@@ -61,4 +63,42 @@ public function testCustomHeader()
$this->assertCount(1, $payload['message']['headers']);
$this->assertEquals('foo: bar', $payload['message']['headers'][0]);
}

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 MandrillApiTransport('ACCESS_KEY');
$method = new \ReflectionMethod(MandrillApiTransport::class, 'getPayload');
$method->setAccessible(true);
$payload = $method->invoke($transport, $email, $envelope);

$this->assertArrayHasKey('message', $payload);
$this->assertArrayNotHasKey('headers', $payload['message']);
$this->assertArrayHasKey('tags', $payload['message']);
$this->assertSame(['password-reset'], $payload['message']['tags']);
$this->assertArrayHasKey('metadata', $payload['message']);
$this->assertSame(['Color' => 'blue', 'Client-ID' => '12345'], $payload['message']['metadata']);
}

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

$transport = new MandrillApiTransport('ACCESS_KEY');
$method = new \ReflectionMethod(MandrillApiTransport::class, 'getPayload');
$method->setAccessible(true);
$payload = $method->invoke($transport, $email, $envelope);

$this->assertArrayHasKey('message', $payload);
$this->assertArrayNotHasKey('headers', $payload['message']);
$this->assertArrayHasKey('tags', $payload['message']);
$this->assertSame(['password-reset', 'user'], $payload['message']['tags']);
}
}
@@ -13,6 +13,9 @@

use PHPUnit\Framework\TestCase;
use Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillHttpTransport;
use Symfony\Component\Mailer\Header\MetadataHeader;
use Symfony\Component\Mailer\Header\TagHeader;
use Symfony\Component\Mime\Email;

class MandrillHttpTransportTest extends TestCase
{
@@ -41,4 +44,23 @@ public function getTransportData()
],
];
}

public function testTagAndMetadataHeaders()
{
$email = new Email();
$email->getHeaders()->addTextHeader('foo', 'bar');
$email->getHeaders()->add(new TagHeader('password-reset,user'));
$email->getHeaders()->add(new MetadataHeader('Color', 'blue'));
$email->getHeaders()->add(new MetadataHeader('Client-ID', '12345'));

$transport = new MandrillHttpTransport('key');
$method = new \ReflectionMethod(MandrillHttpTransport::class, 'addMandrillHeaders');
$method->setAccessible(true);
$method->invoke($transport, $email);

$this->assertCount(3, $email->getHeaders()->toArray());
$this->assertSame('foo: bar', $email->getHeaders()->get('FOO')->toString());
$this->assertSame('X-MC-Tags: password-reset,user', $email->getHeaders()->get('X-MC-Tags')->toString());
$this->assertSame('X-MC-Metadata: '.json_encode(['Color' => 'blue', 'Client-ID' => '12345']), $email->getHeaders()->get('X-MC-Metadata')->toString());
}
}
@@ -0,0 +1,40 @@
<?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\Mailchimp\Tests\Transport;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillSmtpTransport;
use Symfony\Component\Mailer\Header\MetadataHeader;
use Symfony\Component\Mailer\Header\TagHeader;
use Symfony\Component\Mime\Email;

class MandrillSmtpTransportTest extends TestCase
{
public function testTagAndMetadataHeaders()
{
$email = new Email();
$email->getHeaders()->addTextHeader('foo', 'bar');
$email->getHeaders()->add(new TagHeader('password-reset,user'));
$email->getHeaders()->add(new MetadataHeader('Color', 'blue'));
$email->getHeaders()->add(new MetadataHeader('Client-ID', '12345'));

$transport = new MandrillSmtpTransport('user', 'password');
$method = new \ReflectionMethod(MandrillSmtpTransport::class, 'addMandrillHeaders');
$method->setAccessible(true);
$method->invoke($transport, $email);

$this->assertCount(3, $email->getHeaders()->toArray());
$this->assertSame('foo: bar', $email->getHeaders()->get('FOO')->toString());
$this->assertSame('X-MC-Tags: password-reset,user', $email->getHeaders()->get('X-MC-Tags')->toString());
$this->assertSame('X-MC-Metadata: '.json_encode(['Color' => 'blue', 'Client-ID' => '12345']), $email->getHeaders()->get('X-MC-Metadata')->toString());
}
}
@@ -14,6 +14,8 @@
use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\Exception\HttpTransportException;
use Symfony\Component\Mailer\Header\MetadataHeader;
use Symfony\Component\Mailer\Header\TagHeader;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mailer\Transport\AbstractApiTransport;
use Symfony\Component\Mime\Email;
@@ -111,6 +113,18 @@ private function getPayload(Email $email, Envelope $envelope): array
continue;
}

if ($header instanceof TagHeader) {
$payload['message']['tags'] = explode(',', $header->getValue());

continue;
}

if ($header instanceof MetadataHeader) {
$payload['message']['metadata'][$header->getKey()] = $header->getValue();

continue;
}

$payload['message']['headers'][] = $name.': '.$header->getBodyAsString();
}

@@ -0,0 +1,54 @@
<?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\Mailchimp\Transport;

use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\Header\MetadataHeader;
use Symfony\Component\Mailer\Header\TagHeader;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mime\Message;
use Symfony\Component\Mime\RawMessage;

/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
trait MandrillHeadersTrait
{
public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage
{
if ($message instanceof Message) {
$this->addMandrillHeaders($message);
}

return parent::send($message, $envelope);
}

private function addMandrillHeaders(Message $message): void
{
$headers = $message->getHeaders();
$metadata = [];

foreach ($headers->all() as $name => $header) {
if ($header instanceof TagHeader) {
$headers->addTextHeader('X-MC-Tags', $header->getValue());
$headers->remove($name);
} elseif ($header instanceof MetadataHeader) {
$metadata[$header->getKey()] = $header->getValue();
$headers->remove($name);
}
}

if ($metadata) {
$headers->addTextHeader('X-MC-Metadata', json_encode($metadata));
}
}
}
@@ -24,6 +24,8 @@
*/
class MandrillHttpTransport extends AbstractHttpTransport
{
use MandrillHeadersTrait;

private const HOST = 'mandrillapp.com';
private $key;

@@ -20,6 +20,8 @@
*/
class MandrillSmtpTransport extends EsmtpTransport
{
use MandrillHeadersTrait;

public function __construct(string $username, string $password, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
{
parent::__construct('smtp.mandrillapp.com', 587, true, $dispatcher, $logger);
@@ -17,7 +17,7 @@
],
"require": {
"php": "^7.2.5",
"symfony/mailer": "^4.4|^5.0"
"symfony/mailer": "^5.1"
},
"require-dev": {
"symfony/http-client": "^4.4|^5.0"
@@ -14,6 +14,8 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunApiTransport;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\Header\MetadataHeader;
use Symfony\Component\Mailer\Header\TagHeader;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;

@@ -64,4 +66,29 @@ public function testCustomHeader()
$this->assertArrayHasKey('h:x-mailgun-variables', $payload);
$this->assertEquals($json, $payload['h:x-mailgun-variables']);
}

public function testTagAndMetadataHeaders()
{
$json = json_encode(['foo' => 'bar']);
$email = new Email();
$email->getHeaders()->addTextHeader('X-Mailgun-Variables', $json);
$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 MailgunApiTransport('ACCESS_KEY', 'DOMAIN');
$method = new \ReflectionMethod(MailgunApiTransport::class, 'getPayload');
$method->setAccessible(true);
$payload = $method->invoke($transport, $email, $envelope);

$this->assertArrayHasKey('h:x-mailgun-variables', $payload);
$this->assertEquals($json, $payload['h:x-mailgun-variables']);
$this->assertArrayHasKey('o:tag', $payload);
$this->assertSame('password-reset', $payload['o:tag']);
$this->assertArrayHasKey('v:Color', $payload);
$this->assertSame('blue', $payload['v:Color']);
$this->assertArrayHasKey('v:Client-ID', $payload);
$this->assertSame('12345', $payload['v:Client-ID']);
}
}
@@ -13,6 +13,9 @@

use PHPUnit\Framework\TestCase;
use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunHttpTransport;
use Symfony\Component\Mailer\Header\MetadataHeader;
use Symfony\Component\Mailer\Header\TagHeader;
use Symfony\Component\Mime\Email;

class MailgunHttpTransportTest extends TestCase
{
@@ -45,4 +48,23 @@ public function getTransportData()
],
];
}

public function testTagAndMetadataHeaders()
{
$email = new Email();
$email->getHeaders()->addTextHeader('foo', 'bar');
$email->getHeaders()->add(new TagHeader('password-reset'));
$email->getHeaders()->add(new MetadataHeader('Color', 'blue'));
$email->getHeaders()->add(new MetadataHeader('Client-ID', '12345'));

$transport = new MailgunHttpTransport('key', 'domain');
$method = new \ReflectionMethod(MailgunHttpTransport::class, 'addMailgunHeaders');
$method->setAccessible(true);
$method->invoke($transport, $email);

$this->assertCount(3, $email->getHeaders()->toArray());
$this->assertSame('foo: bar', $email->getHeaders()->get('foo')->toString());
$this->assertSame('X-Mailgun-Tag: password-reset', $email->getHeaders()->get('X-Mailgun-Tag')->toString());
$this->assertSame('X-Mailgun-Variables: '.json_encode(['Color' => 'blue', 'Client-ID' => '12345']), $email->getHeaders()->get('X-Mailgun-Variables')->toString());
}
}
@@ -0,0 +1,43 @@
<?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\Mailgun\Tests\Transport;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunSmtpTransport;
use Symfony\Component\Mailer\Header\MetadataHeader;
use Symfony\Component\Mailer\Header\TagHeader;
use Symfony\Component\Mime\Email;

/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
class MailgunSmtpTransportTest extends TestCase
{
public function testTagAndMetadataHeaders()
{
$email = new Email();
$email->getHeaders()->addTextHeader('foo', 'bar');
$email->getHeaders()->add(new TagHeader('password-reset'));
$email->getHeaders()->add(new MetadataHeader('Color', 'blue'));
$email->getHeaders()->add(new MetadataHeader('Client-ID', '12345'));

$transport = new MailgunSmtpTransport('user', 'password');
$method = new \ReflectionMethod(MailgunSmtpTransport::class, 'addMailgunHeaders');
$method->setAccessible(true);
$method->invoke($transport, $email);

$this->assertCount(3, $email->getHeaders()->toArray());
$this->assertSame('foo: bar', $email->getHeaders()->get('foo')->toString());
$this->assertSame('X-Mailgun-Tag: password-reset', $email->getHeaders()->get('X-Mailgun-Tag')->toString());
$this->assertSame('X-Mailgun-Variables: '.json_encode(['Color' => 'blue', 'Client-ID' => '12345']), $email->getHeaders()->get('X-Mailgun-Variables')->toString());
}
}
@@ -14,6 +14,8 @@
use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\Exception\HttpTransportException;
use Symfony\Component\Mailer\Header\MetadataHeader;
use Symfony\Component\Mailer\Header\TagHeader;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mailer\Transport\AbstractApiTransport;
use Symfony\Component\Mime\Email;
@@ -114,6 +116,18 @@ private function getPayload(Email $email, Envelope $envelope): array
continue;
}

if ($header instanceof TagHeader) {
$payload['o:tag'] = $header->getValue();

continue;
}

if ($header instanceof MetadataHeader) {
$payload['v:'.$header->getKey()] = $header->getValue();

continue;
}

$payload['h:'.$name] = $header->getBodyAsString();
}

0 comments on commit 81abb4e

Please sign in to comment.
You can’t perform that action at this time.