Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 845cf9e

Browse filesBrowse files
committed
feature #30741 Add the Mailer component (fabpot)
This PR was merged into the 4.3-dev branch. Discussion ---------- Add the Mailer component | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no <!-- see https://symfony.com/bc --> | Deprecations? | no <!-- don't forget to update UPGRADE-*.md and src/**/CHANGELOG.md files --> | Tests pass? | yes <!-- please add some, will be required by reviewers --> | Fixed tickets | n/a | License | MIT | Doc PR | upcoming https://speakerdeck.com/fabpot/mailer Commits ------- 69b9ee7 added the Mailer component
2 parents 760bbd5 + 69b9ee7 commit 845cf9e
Copy full SHA for 845cf9e

File tree

Expand file treeCollapse file tree

98 files changed

+4893
-0
lines changed
Filter options

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Dismiss banner
Expand file treeCollapse file tree

98 files changed

+4893
-0
lines changed

‎src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
+17Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use Symfony\Component\HttpFoundation\Cookie;
2727
use Symfony\Component\Lock\Lock;
2828
use Symfony\Component\Lock\Store\SemaphoreStore;
29+
use Symfony\Component\Mailer\Mailer;
2930
use Symfony\Component\Messenger\MessageBusInterface;
3031
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
3132
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
@@ -112,6 +113,7 @@ public function getConfigTreeBuilder()
112113
$this->addMessengerSection($rootNode);
113114
$this->addRobotsIndexSection($rootNode);
114115
$this->addHttpClientSection($rootNode);
116+
$this->addMailerSection($rootNode);
115117

116118
return $treeBuilder;
117119
}
@@ -1344,4 +1346,19 @@ private function addHttpClientOptionsSection(NodeBuilder $rootNode)
13441346
->end()
13451347
;
13461348
}
1349+
1350+
private function addMailerSection(ArrayNodeDefinition $rootNode)
1351+
{
1352+
$rootNode
1353+
->children()
1354+
->arrayNode('mailer')
1355+
->info('Mailer configuration')
1356+
->{!class_exists(FullStack::class) && class_exists(Mailer::class) ? 'canBeDisabled' : 'canBeEnabled'}()
1357+
->children()
1358+
->scalarNode('dsn')->defaultValue('smtp://null')->end()
1359+
->end()
1360+
->end()
1361+
->end()
1362+
;
1363+
}
13471364
}

‎src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+15Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
use Symfony\Component\Lock\Store\FlockStore;
7575
use Symfony\Component\Lock\Store\StoreFactory;
7676
use Symfony\Component\Lock\StoreInterface;
77+
use Symfony\Component\Mailer\Mailer;
7778
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
7879
use Symfony\Component\Messenger\MessageBus;
7980
use Symfony\Component\Messenger\MessageBusInterface;
@@ -316,6 +317,10 @@ public function load(array $configs, ContainerBuilder $container)
316317
$this->registerHttpClientConfiguration($config['http_client'], $container, $loader);
317318
}
318319

320+
if ($this->isConfigEnabled($container, $config['mailer'])) {
321+
$this->registerMailerConfiguration($config['mailer'], $container, $loader);
322+
}
323+
319324
if ($this->isConfigEnabled($container, $config['web_link'])) {
320325
if (!class_exists(HttpHeaderSerializer::class)) {
321326
throw new LogicException('WebLink support cannot be enabled as the WebLink component is not installed. Try running "composer require symfony/weblink".');
@@ -1854,6 +1859,16 @@ public function merge(array $options, array $defaultOptions)
18541859
}
18551860
}
18561861

1862+
private function registerMailerConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader)
1863+
{
1864+
if (!class_exists(Mailer::class)) {
1865+
throw new LogicException('Mailer support cannot be enabled as the component is not installed. Try running "composer require symfony/mailer".');
1866+
}
1867+
1868+
$loader->load('mailer.xml');
1869+
$container->getDefinition('mailer.transport')->setArgument(0, $config['dsn']);
1870+
}
1871+
18571872
/**
18581873
* Returns the base path for the XSD files.
18591874
*
+28Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
3+
<container xmlns="http://symfony.com/schema/dic/services"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
6+
7+
<services>
8+
<service id="mailer" class="Symfony\Component\Mailer\Mailer">
9+
<argument type="service" id="mailer.transport" />
10+
<argument type="service" id="message_bus" on-invalid="ignore" />
11+
</service>
12+
<service id="Symfony\Component\Mailer\MailerInterface" alias="mailer" />
13+
14+
<service id="mailer.transport" class="Symfony\Component\Mailer\Transport\TransportInterface">
15+
<factory class="Symfony\Component\Mailer\Transport" method="fromDsn" />
16+
<argument /> <!-- env(MAILER_DSN) -->
17+
<argument type="service" id="event_dispatcher" />
18+
<argument type="service" id="http_client" on-invalid="ignore" />
19+
<argument type="service" id="logger" on-invalid="ignore" />
20+
</service>
21+
<service id="Symfony\Component\Mailer\Transport\TransportInterface" alias="mailer.transport" />
22+
23+
<service id="mailer.messenger.message_handler" class="Symfony\Component\Mailer\Messenger\MessageHandler">
24+
<argument type="service" id="mailer.transport" />
25+
<tag name="messenger.message_handler" />
26+
</service>
27+
</services>
28+
</container>

‎src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php
+5Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Symfony\Component\Config\Definition\Processor;
2020
use Symfony\Component\HttpClient\HttpClient;
2121
use Symfony\Component\Lock\Store\SemaphoreStore;
22+
use Symfony\Component\Mailer\Mailer;
2223
use Symfony\Component\Messenger\MessageBusInterface;
2324

2425
class ConfigurationTest extends TestCase
@@ -336,6 +337,10 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor
336337
'enabled' => !class_exists(FullStack::class) && class_exists(HttpClient::class),
337338
'clients' => [],
338339
],
340+
'mailer' => [
341+
'dsn' => 'smtp://null',
342+
'enabled' => !class_exists(FullStack::class) && class_exists(Mailer::class),
343+
],
339344
];
340345
}
341346
}

‎src/Symfony/Bundle/FrameworkBundle/composer.json

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/composer.json
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"symfony/form": "^4.3",
4343
"symfony/expression-language": "~3.4|~4.0",
4444
"symfony/http-client": "^4.3",
45+
"symfony/mailer": "^4.3",
4546
"symfony/messenger": "^4.3",
4647
"symfony/mime": "^4.3",
4748
"symfony/process": "~3.4|~4.0",

‎src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php
+5Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
2020
use Symfony\Component\DependencyInjection\Reference;
2121
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
22+
use Symfony\Component\Mailer\Mailer;
2223
use Symfony\Component\Translation\Translator;
2324
use Twig\Extension\ExtensionInterface;
2425
use Twig\Extension\RuntimeExtensionInterface;
@@ -49,6 +50,10 @@ public function load(array $configs, ContainerBuilder $container)
4950
$loader->load('console.xml');
5051
}
5152

53+
if (class_exists(Mailer::class)) {
54+
$loader->load('mailer.xml');
55+
}
56+
5257
if (!class_exists(Translator::class)) {
5358
$container->removeDefinition('twig.translation.extractor');
5459
}
+17Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
3+
<container xmlns="http://symfony.com/schema/dic/services"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
6+
7+
<services>
8+
<service id="twig.mailer.message_listener" class="Symfony\Component\Mailer\EventListener\MessageListener">
9+
<argument />
10+
<argument type="service" id="twig.mime_body_renderer" />
11+
</service>
12+
13+
<service id="twig.mime_body_renderer" class="Symfony\Bridge\Twig\Mime\BodyRenderer">
14+
<argument type="service" id="twig" />
15+
</service>
16+
</services>
17+
</container>
+7Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CHANGELOG
2+
=========
3+
4+
4.3.0
5+
-----
6+
7+
* added the bridge
+103Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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\Amazon\Http\Api;
13+
14+
use Psr\Log\LoggerInterface;
15+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
16+
use Symfony\Component\Mailer\Exception\TransportException;
17+
use Symfony\Component\Mailer\SmtpEnvelope;
18+
use Symfony\Component\Mailer\Transport\Http\Api\AbstractApiTransport;
19+
use Symfony\Component\Mime\Email;
20+
use Symfony\Contracts\HttpClient\HttpClientInterface;
21+
22+
/**
23+
* @experimental in 4.3
24+
*/
25+
class SesTransport extends AbstractApiTransport
26+
{
27+
private const ENDPOINT = 'https://email.%region%.amazonaws.com';
28+
29+
private $accessKey;
30+
private $secretKey;
31+
private $region;
32+
33+
/**
34+
* @param string $region Amazon SES region (currently one of us-east-1, us-west-2, or eu-west-1)
35+
*/
36+
public function __construct(string $accessKey, string $secretKey, string $region = null, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
37+
{
38+
$this->accessKey = $accessKey;
39+
$this->secretKey = $secretKey;
40+
$this->region = $region ?: 'eu-west-1';
41+
42+
parent::__construct($client, $dispatcher, $logger);
43+
}
44+
45+
protected function doSendEmail(Email $email, SmtpEnvelope $envelope): void
46+
{
47+
$date = gmdate('D, d M Y H:i:s e');
48+
$auth = sprintf('AWS3-HTTPS AWSAccessKeyId=%s,Algorithm=HmacSHA256,Signature=%s', $this->accessKey, $this->getSignature($date));
49+
50+
$endpoint = str_replace('%region%', $this->region, self::ENDPOINT);
51+
$response = $this->client->request('POST', $endpoint, [
52+
'headers' => [
53+
'X-Amzn-Authorization' => $auth,
54+
'Date' => $date,
55+
'Content-Type' => 'application/x-www-form-urlencoded',
56+
],
57+
'body' => $this->getPayload($email, $envelope),
58+
]);
59+
60+
if (200 !== $response->getStatusCode()) {
61+
$error = new \SimpleXMLElement($response->getContent(false));
62+
63+
throw new TransportException(sprintf('Unable to send an email: %s (code %s).', $error->Error->Message, $error->Error->Code));
64+
}
65+
}
66+
67+
private function getSignature(string $string): string
68+
{
69+
return base64_encode(hash_hmac('sha256', $string, $this->secretKey, true));
70+
}
71+
72+
private function getPayload(Email $email, SmtpEnvelope $envelope): array
73+
{
74+
if ($email->getAttachments()) {
75+
return [
76+
'Action' => 'SendRawEmail',
77+
'RawMessage.Data' => \base64_encode($email->toString()),
78+
];
79+
}
80+
81+
$payload = [
82+
'Action' => 'SendEmail',
83+
'Destination.ToAddresses.member' => $this->stringifyAddresses($this->getRecipients($email, $envelope)),
84+
'Message.Subject.Data' => $email->getSubject(),
85+
'Source' => $envelope->getSender()->toString(),
86+
];
87+
88+
if ($emails = $email->getCc()) {
89+
$payload['Destination.CcAddresses.member'] = $this->stringifyAddresses($emails);
90+
}
91+
if ($emails = $email->getBcc()) {
92+
$payload['Destination.BccAddresses.member'] = $this->stringifyAddresses($emails);
93+
}
94+
if ($email->getTextBody()) {
95+
$payload['Message.Body.Text.Data'] = $email->getTextBody();
96+
}
97+
if ($email->getHtmlBody()) {
98+
$payload['Message.Body.Html.Data'] = $email->getHtmlBody();
99+
}
100+
101+
return $payload;
102+
}
103+
}
+75Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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\Amazon\Http;
13+
14+
use Psr\Log\LoggerInterface;
15+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
16+
use Symfony\Component\HttpClient\HttpClient;
17+
use Symfony\Component\Mailer\Exception\TransportException;
18+
use Symfony\Component\Mailer\SentMessage;
19+
use Symfony\Component\Mailer\Transport\AbstractTransport;
20+
use Symfony\Contracts\HttpClient\HttpClientInterface;
21+
22+
/**
23+
* @experimental in 4.3
24+
*/
25+
class SesTransport extends AbstractTransport
26+
{
27+
private const ENDPOINT = 'https://email.%region%.amazonaws.com';
28+
29+
private $client;
30+
private $accessKey;
31+
private $secretKey;
32+
private $region;
33+
34+
/**
35+
* @param string $region Amazon SES region (currently one of us-east-1, us-west-2, or eu-west-1)
36+
*/
37+
public function __construct(string $accessKey, string $secretKey, string $region = null, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
38+
{
39+
$this->client = $client ?? HttpClient::create();
40+
$this->accessKey = $accessKey;
41+
$this->secretKey = $secretKey;
42+
$this->region = $region ?: 'eu-west-1';
43+
44+
parent::__construct($dispatcher, $logger);
45+
}
46+
47+
protected function doSend(SentMessage $message): void
48+
{
49+
$date = gmdate('D, d M Y H:i:s e');
50+
$auth = sprintf('AWS3-HTTPS AWSAccessKeyId=%s,Algorithm=HmacSHA256,Signature=%s', $this->accessKey, $this->getSignature($date));
51+
52+
$endpoint = str_replace('%region%', $this->region, self::ENDPOINT);
53+
$response = $this->client->request('POST', $endpoint, [
54+
'headers' => [
55+
'X-Amzn-Authorization' => $auth,
56+
'Date' => $date,
57+
],
58+
'body' => [
59+
'Action' => 'SendRawEmail',
60+
'RawMessage.Data' => \base64_encode($message->toString()),
61+
],
62+
]);
63+
64+
if (200 !== $response->getStatusCode()) {
65+
$error = new \SimpleXMLElement($response->getContent(false));
66+
67+
throw new TransportException(sprintf('Unable to send an email: %s (code %s).', $error->Error->Message, $error->Error->Code));
68+
}
69+
}
70+
71+
private function getSignature(string $string): string
72+
{
73+
return base64_encode(hash_hmac('sha256', $string, $this->secretKey, true));
74+
}
75+
}
+19Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2019 Fabien Potencier
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is furnished
8+
to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
THE SOFTWARE.
+12Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Amazon Mailer
2+
=============
3+
4+
Provides Amazon SES integration for Symfony Mailer.
5+
6+
Resources
7+
---------
8+
9+
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
10+
* [Report issues](https://github.com/symfony/symfony/issues) and
11+
[send Pull Requests](https://github.com/symfony/symfony/pulls)
12+
in the [main Symfony repository](https://github.com/symfony/symfony)

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.