Skip to content

Commit

Permalink
Make configured mailer transports selectable in the back end (see #1830)
Browse files Browse the repository at this point in the history
Description
-----------

| Q                | A
| -----------------| ---
| Fixed issues     | Fixes #1613
| Docs PR or issue | contao/docs#465

_Note:_ this PR depends and is based on #1829. It will be rebased, once #1829 got merged.

This is the alternative version of #1469, based on the [Symfony Mailer Component](https://symfony.com/doc/4.4/mailer.html) instead of the Swiftmailer Bundle. It makes the configured `framework.mailer.transports` selectable in the back end. Example:

```yml
# config/config.yml
framework:
  mailer:
    transports: 
      app: smtps://[email protected]:[email protected]:465
      page: smtps://[email protected]:[email protected]:465
      forms: smtps://[email protected]:[email protected]:465
      newsletter: smtps://[email protected]:[email protected]:465

contao:
  mailer:
    transports:
      page: ~
      forms: ~
      newsletter: ~
```

<img src="https://user-images.githubusercontent.com/4970961/84607561-e3a06d00-aea5-11ea-9f89-daac495b8a85.png" alt="mailer_transport_01" width="576">

_Note:_ only the transports configured in `contao.mailer.transports` will be available for selection.

You can also provide translations:

```yml
# translations/mailer_transports.en.yml
page: 'Page'
forms: 'Forms'
newsletter: 'Newsletters'
```

<img src="https://user-images.githubusercontent.com/4970961/84607577-f7e46a00-aea5-11ea-9cd1-59271843609b.png" alt="mailer_transport_02" width="576">

And you can override the `From` address for each transport in the Contao configuration:

```yml
contao:
  mailer:
    transports:
      page:
        from: Contao Page <[email protected]>
      forms:
        from: Contao Forms <[email protected]>
      newsletter:
        from: Contao Newsletter <[email protected]>
```

<img src="https://user-images.githubusercontent.com/4970961/84607478-670d8e80-aea5-11ea-9bdd-1cb2b090fd5a.png" alt="mailer_transport_03" width="576">

_Note:_ only the transports configured in `contao.mailer.transports` will be available for selection.

Using the Symfony Mailer Component for this seems more elegant to me, since it requires no change whatsoever in the `\Contao\Email` class ([see the comparison](fritzmg/contao@feature/symfony-mailer...feature/available-symfony-mailers)). With the Symfony Mailer Component, the transport to be used is simply chosen with an `X-Transport` header in the email message itself.

This PR decorates the `mailer` service and automatically sets an `X-Transport` header based on the website root settings - and automatically overrides the `From` address based on the chosen transport.

Commits
-------

fd3da56a switch to symfony/mailer
6c21d672 change MAILER_DSN back to MAILER_URL
eeaea321 dynamically add default mailer
6ad300c4 add \Swift_Mailer fallback
71a5553a use MAILER_DSN with MAILER_URL fallback
cc348243 remove Email deprecation
8c2480a7 increase symfony/mailer dependency
39267663 switch to symfony/mailer
3fd2799c dynamically add default mailer
43aa11c3 provide mailer transport selection and from override
c38e8646 add missing model property
8cee6db5 fix AvailableTransportsTest
08c9b201 fix code style
a8d9b591 fix yml style
ca478014 only show configured mailer transports within Contao
e4f774f6 change translation domain
1722e810 restore previous version requirement
21b55d24 code style fix
1abfc387 rename mailer_transport DCA field to mailerTransport
af8563c3 use Annotations for mailerTransport options callback
058da895 implement some early outs
4585e9dd add missing model methods
a2da52c8 fix code style
f8020035 Merge remote-tracking branch 'origin/master' into feature/available-symfony-mailers
a5d7c747 add more unit tests
94203d73 use assertSame
38c3de95 improve testAnnotatedCallbacks test
3ca8e024 Rearrange the form fields in the back end
33f38c43 add missing methods
b7159fe0 change wording to 'mailer transport'
ba4f4232 merge with master
b8ffb367 Apply suggestions from code review

Co-authored-by: Leo Feyer <[email protected]>
  • Loading branch information
fritzmg and leofeyer authored Jul 17, 2020
1 parent 72c8a9b commit 23a529c
Show file tree
Hide file tree
Showing 20 changed files with 787 additions and 5 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
"contao/core": "*",
"contao/manager-bundle": "4.5.* <4.5.2",
"contao/manager-plugin": "<2.0 || >=3.0",
"symfony/mime": "4.4.* <4.4.10 || 5.0.* <5.0.10 || 5.1.0",
"symfony/security-bundle": "4.4.* <4.4.5",
"terminal42/contao-ce-access": "<3.0"
},
Expand Down
2 changes: 2 additions & 0 deletions src/ContaoCoreBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
namespace Contao\CoreBundle;

use Contao\CoreBundle\DependencyInjection\Compiler\AddAssetsPackagesPass;
use Contao\CoreBundle\DependencyInjection\Compiler\AddAvailableTransportsPass;
use Contao\CoreBundle\DependencyInjection\Compiler\AddCronJobsPass;
use Contao\CoreBundle\DependencyInjection\Compiler\AddPackagesPass;
use Contao\CoreBundle\DependencyInjection\Compiler\AddResourcesPathsPass;
Expand Down Expand Up @@ -93,5 +94,6 @@ public function build(ContainerBuilder $container): void
$container->addCompilerPass(new SearchIndexerPass()); // Must be before the CrawlerPass
$container->addCompilerPass(new CrawlerPass());
$container->addCompilerPass(new AddCronJobsPass());
$container->addCompilerPass(new AddAvailableTransportsPass());
}
}
58 changes: 58 additions & 0 deletions src/DependencyInjection/Compiler/AddAvailableTransportsPass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

declare(strict_types=1);

/*
* This file is part of Contao.
*
* (c) Leo Feyer
*
* @license LGPL-3.0-or-later
*/

namespace Contao\CoreBundle\DependencyInjection\Compiler;

use Contao\CoreBundle\Mailer\AvailableTransports;
use Contao\CoreBundle\Mailer\TransportConfig;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;

class AddAvailableTransportsPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if (!$container->has(AvailableTransports::class)) {
return;
}

$contaoConfig = array_merge(...$container->getExtensionConfig('contao'));
$contaoMailerConfig = $contaoConfig['mailer']['transports'] ?? [];

if (empty($contaoMailerConfig)) {
return;
}

$frameworkConfig = $container->getExtensionConfig('framework');
$definition = $container->findDefinition(AvailableTransports::class);

foreach ($frameworkConfig as $v) {
if (!isset($v['mailer']['transports'])) {
continue;
}

foreach (array_keys($v['mailer']['transports']) as $transportName) {
if (!\array_key_exists($transportName, $contaoMailerConfig)) {
continue;
}

$from = $contaoMailerConfig[$transportName]['from'] ?? null;

$definition->addMethodCall(
'addTransport',
[new Definition(TransportConfig::class, [$transportName, $from])]
);
}
}
}
}
23 changes: 23 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ static function (string $value): string {
->append($this->addSecurityNode())
->append($this->addSearchNode())
->append($this->addCrawlNode())
->append($this->addMailerNode())
->end()
;

Expand Down Expand Up @@ -418,6 +419,28 @@ static function (array $uris): bool {
;
}

private function addMailerNode(): NodeDefinition
{
return (new TreeBuilder('mailer'))
->getRootNode()
->addDefaultsIfNotSet()
->children()
->arrayNode('transports')
->info('Specifies the mailer transports available for selection within Contao.')
->useAttributeAsKey('name')
->arrayPrototype()
->children()
->scalarNode('from')
->info('Overrides the "From" address for any e-mails sent with this mailer transport.')
->defaultNull()
->end()
->end()
->end()
->end()
->end()
;
}

/**
* @return array<string>
*/
Expand Down
81 changes: 81 additions & 0 deletions src/Mailer/AvailableTransports.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

declare(strict_types=1);

/*
* This file is part of Contao.
*
* (c) Leo Feyer
*
* @license LGPL-3.0-or-later
*/

namespace Contao\CoreBundle\Mailer;

use Contao\CoreBundle\ServiceAnnotation\Callback;
use Symfony\Contracts\Translation\TranslatorInterface;
use Terminal42\ServiceAnnotationBundle\ServiceAnnotationInterface;

class AvailableTransports implements ServiceAnnotationInterface
{
/**
* @var array<TransportConfig>
*/
private $transports = [];

/**
* @var TranslatorInterface
*/
private $translator;

public function __construct(?TranslatorInterface $translator = null)
{
$this->translator = $translator;
}

public function addTransport(TransportConfig $transportConfig): void
{
$this->transports[$transportConfig->getName()] = $transportConfig;
}

/**
* @return array<string, TransportConfig>
*/
public function getTransports(): array
{
return $this->transports;
}

/**
* Returns the available transports as options suitable for widgets.
*
* @return array<string, string>
*
* @Callback(table="tl_page", target="fields.mailerTransport.options")
* @Callback(table="tl_form", target="fields.mailerTransport.options")
*/
public function getTransportOptions(): array
{
$options = [];

foreach ($this->transports as $name => $config) {
$label = null !== $this->translator ? $this->translator->trans($name, [], 'mailer_transports') : $name;

if (null !== ($from = $config->getFrom())) {
$label .= ' ('.$from.')';
}

$options[$name] = htmlentities($label);
}

return $options;
}

/**
* Returns a specific transport configuration by the transport name.
*/
public function getTransport(string $name): ?TransportConfig
{
return $this->transports[$name] ?? null;
}
}
121 changes: 121 additions & 0 deletions src/Mailer/ContaoMailer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php

declare(strict_types=1);

/*
* This file is part of Contao.
*
* (c) Leo Feyer
*
* @license LGPL-3.0-or-later
*/

namespace Contao\CoreBundle\Mailer;

use Contao\PageModel;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
use Symfony\Component\Mime\Message;
use Symfony\Component\Mime\RawMessage;

final class ContaoMailer implements MailerInterface
{
/**
* @var MailerInterface
*/
private $mailer;

/**
* @var AvailableTransports
*/
private $transports;

/**
* @var RequestStack
*/
private $requestStack;

public function __construct(MailerInterface $mailer, AvailableTransports $transports, RequestStack $requestStack)
{
$this->mailer = $mailer;
$this->transports = $transports;
$this->requestStack = $requestStack;
}

public function send(RawMessage $message, ?Envelope $envelope = null): void
{
if ($message instanceof Message) {
$this->setTransport($message);
}

if ($message instanceof Email) {
$this->setFrom($message);
}

$this->mailer->send($message, $envelope);
}

/**
* Sets the transport defined in the website root.
*/
private function setTransport(Message $message): void
{
if ($message->getHeaders()->has('X-Transport')) {
return;
}

$request = $this->requestStack->getCurrentRequest();

if (null === $request) {
return;
}

$attributes = $this->requestStack->getCurrentRequest()->attributes;

if (!$attributes->has('pageModel')) {
return;
}

$page = $attributes->get('pageModel');

if (!$page instanceof PageModel) {
return;
}

/** @var PageModel $page */
$page->loadDetails();

if (empty($page->mailerTransport) || null === $this->transports->getTransport($page->mailerTransport)) {
return;
}

$message->getHeaders()->addTextHeader('X-Transport', $page->mailerTransport);
}

/**
* Overrides the from address according to the transport.
*/
private function setFrom(Email $message): void
{
if (!$message->getHeaders()->has('X-Transport')) {
return;
}

$transportName = $message->getHeaders()->get('X-Transport')->getBodyAsString();
$transport = $this->transports->getTransport($transportName);

if (null === $transport) {
return;
}

$from = $transport->getFrom();

if (null === $from) {
return;
}

$message->from($from);
}
}
42 changes: 42 additions & 0 deletions src/Mailer/TransportConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

/*
* This file is part of Contao.
*
* (c) Leo Feyer
*
* @license LGPL-3.0-or-later
*/

namespace Contao\CoreBundle\Mailer;

final class TransportConfig
{
/**
* @var string
*/
private $name;

/**
* @var string
*/
private $from;

public function __construct(string $name, string $from = null)
{
$this->name = $name;
$this->from = $from;
}

public function getName(): string
{
return $this->name;
}

public function getFrom(): ?string
{
return $this->from;
}
}
11 changes: 11 additions & 0 deletions src/Resources/config/services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,17 @@ services:
tags:
- { name: container.service_subscriber, id: contao.assets.files_context }

Contao\CoreBundle\Mailer\AvailableTransports:
arguments:
- '@?translator'

Contao\CoreBundle\Mailer\ContaoMailer:
decorates: mailer
arguments:
- '@Contao\CoreBundle\Mailer\ContaoMailer.inner'
- '@Contao\CoreBundle\Mailer\AvailableTransports'
- '@request_stack'

contao.menu.backend_menu_builder:
class: Contao\CoreBundle\Menu\BackendMenuBuilder
arguments:
Expand Down
Loading

0 comments on commit 23a529c

Please sign in to comment.