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
-------
69b9ee794c
added the Mailer component
This commit is contained in:
commit
845cf9e271
@ -26,6 +26,7 @@ use Symfony\Component\HttpClient\HttpClient;
|
|||||||
use Symfony\Component\HttpFoundation\Cookie;
|
use Symfony\Component\HttpFoundation\Cookie;
|
||||||
use Symfony\Component\Lock\Lock;
|
use Symfony\Component\Lock\Lock;
|
||||||
use Symfony\Component\Lock\Store\SemaphoreStore;
|
use Symfony\Component\Lock\Store\SemaphoreStore;
|
||||||
|
use Symfony\Component\Mailer\Mailer;
|
||||||
use Symfony\Component\Messenger\MessageBusInterface;
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
|
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
|
||||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||||
@ -112,6 +113,7 @@ class Configuration implements ConfigurationInterface
|
|||||||
$this->addMessengerSection($rootNode);
|
$this->addMessengerSection($rootNode);
|
||||||
$this->addRobotsIndexSection($rootNode);
|
$this->addRobotsIndexSection($rootNode);
|
||||||
$this->addHttpClientSection($rootNode);
|
$this->addHttpClientSection($rootNode);
|
||||||
|
$this->addMailerSection($rootNode);
|
||||||
|
|
||||||
return $treeBuilder;
|
return $treeBuilder;
|
||||||
}
|
}
|
||||||
@ -1344,4 +1346,19 @@ class Configuration implements ConfigurationInterface
|
|||||||
->end()
|
->end()
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function addMailerSection(ArrayNodeDefinition $rootNode)
|
||||||
|
{
|
||||||
|
$rootNode
|
||||||
|
->children()
|
||||||
|
->arrayNode('mailer')
|
||||||
|
->info('Mailer configuration')
|
||||||
|
->{!class_exists(FullStack::class) && class_exists(Mailer::class) ? 'canBeDisabled' : 'canBeEnabled'}()
|
||||||
|
->children()
|
||||||
|
->scalarNode('dsn')->defaultValue('smtp://null')->end()
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,6 +74,7 @@ use Symfony\Component\Lock\LockInterface;
|
|||||||
use Symfony\Component\Lock\Store\FlockStore;
|
use Symfony\Component\Lock\Store\FlockStore;
|
||||||
use Symfony\Component\Lock\Store\StoreFactory;
|
use Symfony\Component\Lock\Store\StoreFactory;
|
||||||
use Symfony\Component\Lock\StoreInterface;
|
use Symfony\Component\Lock\StoreInterface;
|
||||||
|
use Symfony\Component\Mailer\Mailer;
|
||||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||||
use Symfony\Component\Messenger\MessageBus;
|
use Symfony\Component\Messenger\MessageBus;
|
||||||
use Symfony\Component\Messenger\MessageBusInterface;
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
@ -316,6 +317,10 @@ class FrameworkExtension extends Extension
|
|||||||
$this->registerHttpClientConfiguration($config['http_client'], $container, $loader);
|
$this->registerHttpClientConfiguration($config['http_client'], $container, $loader);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->isConfigEnabled($container, $config['mailer'])) {
|
||||||
|
$this->registerMailerConfiguration($config['mailer'], $container, $loader);
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->isConfigEnabled($container, $config['web_link'])) {
|
if ($this->isConfigEnabled($container, $config['web_link'])) {
|
||||||
if (!class_exists(HttpHeaderSerializer::class)) {
|
if (!class_exists(HttpHeaderSerializer::class)) {
|
||||||
throw new LogicException('WebLink support cannot be enabled as the WebLink component is not installed. Try running "composer require symfony/weblink".');
|
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 @@ class FrameworkExtension extends Extension
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function registerMailerConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader)
|
||||||
|
{
|
||||||
|
if (!class_exists(Mailer::class)) {
|
||||||
|
throw new LogicException('Mailer support cannot be enabled as the component is not installed. Try running "composer require symfony/mailer".');
|
||||||
|
}
|
||||||
|
|
||||||
|
$loader->load('mailer.xml');
|
||||||
|
$container->getDefinition('mailer.transport')->setArgument(0, $config['dsn']);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the base path for the XSD files.
|
* Returns the base path for the XSD files.
|
||||||
*
|
*
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
|
||||||
|
<container xmlns="http://symfony.com/schema/dic/services"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
|
||||||
|
|
||||||
|
<services>
|
||||||
|
<service id="mailer" class="Symfony\Component\Mailer\Mailer">
|
||||||
|
<argument type="service" id="mailer.transport" />
|
||||||
|
<argument type="service" id="message_bus" on-invalid="ignore" />
|
||||||
|
</service>
|
||||||
|
<service id="Symfony\Component\Mailer\MailerInterface" alias="mailer" />
|
||||||
|
|
||||||
|
<service id="mailer.transport" class="Symfony\Component\Mailer\Transport\TransportInterface">
|
||||||
|
<factory class="Symfony\Component\Mailer\Transport" method="fromDsn" />
|
||||||
|
<argument /> <!-- env(MAILER_DSN) -->
|
||||||
|
<argument type="service" id="event_dispatcher" />
|
||||||
|
<argument type="service" id="http_client" on-invalid="ignore" />
|
||||||
|
<argument type="service" id="logger" on-invalid="ignore" />
|
||||||
|
</service>
|
||||||
|
<service id="Symfony\Component\Mailer\Transport\TransportInterface" alias="mailer.transport" />
|
||||||
|
|
||||||
|
<service id="mailer.messenger.message_handler" class="Symfony\Component\Mailer\Messenger\MessageHandler">
|
||||||
|
<argument type="service" id="mailer.transport" />
|
||||||
|
<tag name="messenger.message_handler" />
|
||||||
|
</service>
|
||||||
|
</services>
|
||||||
|
</container>
|
@ -19,6 +19,7 @@ use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
|
|||||||
use Symfony\Component\Config\Definition\Processor;
|
use Symfony\Component\Config\Definition\Processor;
|
||||||
use Symfony\Component\HttpClient\HttpClient;
|
use Symfony\Component\HttpClient\HttpClient;
|
||||||
use Symfony\Component\Lock\Store\SemaphoreStore;
|
use Symfony\Component\Lock\Store\SemaphoreStore;
|
||||||
|
use Symfony\Component\Mailer\Mailer;
|
||||||
use Symfony\Component\Messenger\MessageBusInterface;
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
|
|
||||||
class ConfigurationTest extends TestCase
|
class ConfigurationTest extends TestCase
|
||||||
@ -336,6 +337,10 @@ class ConfigurationTest extends TestCase
|
|||||||
'enabled' => !class_exists(FullStack::class) && class_exists(HttpClient::class),
|
'enabled' => !class_exists(FullStack::class) && class_exists(HttpClient::class),
|
||||||
'clients' => [],
|
'clients' => [],
|
||||||
],
|
],
|
||||||
|
'mailer' => [
|
||||||
|
'dsn' => 'smtp://null',
|
||||||
|
'enabled' => !class_exists(FullStack::class) && class_exists(Mailer::class),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,6 +42,7 @@
|
|||||||
"symfony/form": "^4.3",
|
"symfony/form": "^4.3",
|
||||||
"symfony/expression-language": "~3.4|~4.0",
|
"symfony/expression-language": "~3.4|~4.0",
|
||||||
"symfony/http-client": "^4.3",
|
"symfony/http-client": "^4.3",
|
||||||
|
"symfony/mailer": "^4.3",
|
||||||
"symfony/messenger": "^4.3",
|
"symfony/messenger": "^4.3",
|
||||||
"symfony/mime": "^4.3",
|
"symfony/mime": "^4.3",
|
||||||
"symfony/process": "~3.4|~4.0",
|
"symfony/process": "~3.4|~4.0",
|
||||||
|
@ -19,6 +19,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder;
|
|||||||
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
|
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
|
||||||
use Symfony\Component\DependencyInjection\Reference;
|
use Symfony\Component\DependencyInjection\Reference;
|
||||||
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
|
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
|
||||||
|
use Symfony\Component\Mailer\Mailer;
|
||||||
use Symfony\Component\Translation\Translator;
|
use Symfony\Component\Translation\Translator;
|
||||||
use Twig\Extension\ExtensionInterface;
|
use Twig\Extension\ExtensionInterface;
|
||||||
use Twig\Extension\RuntimeExtensionInterface;
|
use Twig\Extension\RuntimeExtensionInterface;
|
||||||
@ -49,6 +50,10 @@ class TwigExtension extends Extension
|
|||||||
$loader->load('console.xml');
|
$loader->load('console.xml');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (class_exists(Mailer::class)) {
|
||||||
|
$loader->load('mailer.xml');
|
||||||
|
}
|
||||||
|
|
||||||
if (!class_exists(Translator::class)) {
|
if (!class_exists(Translator::class)) {
|
||||||
$container->removeDefinition('twig.translation.extractor');
|
$container->removeDefinition('twig.translation.extractor');
|
||||||
}
|
}
|
||||||
|
17
src/Symfony/Bundle/TwigBundle/Resources/config/mailer.xml
Normal file
17
src/Symfony/Bundle/TwigBundle/Resources/config/mailer.xml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
|
||||||
|
<container xmlns="http://symfony.com/schema/dic/services"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
|
||||||
|
|
||||||
|
<services>
|
||||||
|
<service id="twig.mailer.message_listener" class="Symfony\Component\Mailer\EventListener\MessageListener">
|
||||||
|
<argument />
|
||||||
|
<argument type="service" id="twig.mime_body_renderer" />
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<service id="twig.mime_body_renderer" class="Symfony\Bridge\Twig\Mime\BodyRenderer">
|
||||||
|
<argument type="service" id="twig" />
|
||||||
|
</service>
|
||||||
|
</services>
|
||||||
|
</container>
|
7
src/Symfony/Component/Mailer/Bridge/Amazon/CHANGELOG.md
Normal file
7
src/Symfony/Component/Mailer/Bridge/Amazon/CHANGELOG.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
CHANGELOG
|
||||||
|
=========
|
||||||
|
|
||||||
|
4.3.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* added the bridge
|
@ -0,0 +1,103 @@
|
|||||||
|
<?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\Amazon\Http\Api;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\Mailer\Exception\TransportException;
|
||||||
|
use Symfony\Component\Mailer\SmtpEnvelope;
|
||||||
|
use Symfony\Component\Mailer\Transport\Http\Api\AbstractApiTransport;
|
||||||
|
use Symfony\Component\Mime\Email;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class SesTransport extends AbstractApiTransport
|
||||||
|
{
|
||||||
|
private const ENDPOINT = 'https://email.%region%.amazonaws.com';
|
||||||
|
|
||||||
|
private $accessKey;
|
||||||
|
private $secretKey;
|
||||||
|
private $region;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $region Amazon SES region (currently one of us-east-1, us-west-2, or eu-west-1)
|
||||||
|
*/
|
||||||
|
public function __construct(string $accessKey, string $secretKey, string $region = null, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
|
||||||
|
{
|
||||||
|
$this->accessKey = $accessKey;
|
||||||
|
$this->secretKey = $secretKey;
|
||||||
|
$this->region = $region ?: 'eu-west-1';
|
||||||
|
|
||||||
|
parent::__construct($client, $dispatcher, $logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doSendEmail(Email $email, SmtpEnvelope $envelope): void
|
||||||
|
{
|
||||||
|
$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));
|
||||||
|
|
||||||
|
$endpoint = str_replace('%region%', $this->region, self::ENDPOINT);
|
||||||
|
$response = $this->client->request('POST', $endpoint, [
|
||||||
|
'headers' => [
|
||||||
|
'X-Amzn-Authorization' => $auth,
|
||||||
|
'Date' => $date,
|
||||||
|
'Content-Type' => 'application/x-www-form-urlencoded',
|
||||||
|
],
|
||||||
|
'body' => $this->getPayload($email, $envelope),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (200 !== $response->getStatusCode()) {
|
||||||
|
$error = new \SimpleXMLElement($response->getContent(false));
|
||||||
|
|
||||||
|
throw new TransportException(sprintf('Unable to send an email: %s (code %s).', $error->Error->Message, $error->Error->Code));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getSignature(string $string): string
|
||||||
|
{
|
||||||
|
return base64_encode(hash_hmac('sha256', $string, $this->secretKey, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getPayload(Email $email, SmtpEnvelope $envelope): array
|
||||||
|
{
|
||||||
|
if ($email->getAttachments()) {
|
||||||
|
return [
|
||||||
|
'Action' => 'SendRawEmail',
|
||||||
|
'RawMessage.Data' => \base64_encode($email->toString()),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'Action' => 'SendEmail',
|
||||||
|
'Destination.ToAddresses.member' => $this->stringifyAddresses($this->getRecipients($email, $envelope)),
|
||||||
|
'Message.Subject.Data' => $email->getSubject(),
|
||||||
|
'Source' => $envelope->getSender()->toString(),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($emails = $email->getCc()) {
|
||||||
|
$payload['Destination.CcAddresses.member'] = $this->stringifyAddresses($emails);
|
||||||
|
}
|
||||||
|
if ($emails = $email->getBcc()) {
|
||||||
|
$payload['Destination.BccAddresses.member'] = $this->stringifyAddresses($emails);
|
||||||
|
}
|
||||||
|
if ($email->getTextBody()) {
|
||||||
|
$payload['Message.Body.Text.Data'] = $email->getTextBody();
|
||||||
|
}
|
||||||
|
if ($email->getHtmlBody()) {
|
||||||
|
$payload['Message.Body.Html.Data'] = $email->getHtmlBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
<?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\Amazon\Http;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\HttpClient\HttpClient;
|
||||||
|
use Symfony\Component\Mailer\Exception\TransportException;
|
||||||
|
use Symfony\Component\Mailer\SentMessage;
|
||||||
|
use Symfony\Component\Mailer\Transport\AbstractTransport;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class SesTransport extends AbstractTransport
|
||||||
|
{
|
||||||
|
private const ENDPOINT = 'https://email.%region%.amazonaws.com';
|
||||||
|
|
||||||
|
private $client;
|
||||||
|
private $accessKey;
|
||||||
|
private $secretKey;
|
||||||
|
private $region;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $region Amazon SES region (currently one of us-east-1, us-west-2, or eu-west-1)
|
||||||
|
*/
|
||||||
|
public function __construct(string $accessKey, string $secretKey, string $region = null, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
|
||||||
|
{
|
||||||
|
$this->client = $client ?? HttpClient::create();
|
||||||
|
$this->accessKey = $accessKey;
|
||||||
|
$this->secretKey = $secretKey;
|
||||||
|
$this->region = $region ?: 'eu-west-1';
|
||||||
|
|
||||||
|
parent::__construct($dispatcher, $logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doSend(SentMessage $message): void
|
||||||
|
{
|
||||||
|
$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));
|
||||||
|
|
||||||
|
$endpoint = str_replace('%region%', $this->region, self::ENDPOINT);
|
||||||
|
$response = $this->client->request('POST', $endpoint, [
|
||||||
|
'headers' => [
|
||||||
|
'X-Amzn-Authorization' => $auth,
|
||||||
|
'Date' => $date,
|
||||||
|
],
|
||||||
|
'body' => [
|
||||||
|
'Action' => 'SendRawEmail',
|
||||||
|
'RawMessage.Data' => \base64_encode($message->toString()),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (200 !== $response->getStatusCode()) {
|
||||||
|
$error = new \SimpleXMLElement($response->getContent(false));
|
||||||
|
|
||||||
|
throw new TransportException(sprintf('Unable to send an email: %s (code %s).', $error->Error->Message, $error->Error->Code));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getSignature(string $string): string
|
||||||
|
{
|
||||||
|
return base64_encode(hash_hmac('sha256', $string, $this->secretKey, true));
|
||||||
|
}
|
||||||
|
}
|
19
src/Symfony/Component/Mailer/Bridge/Amazon/LICENSE
Normal file
19
src/Symfony/Component/Mailer/Bridge/Amazon/LICENSE
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) 2019 Fabien Potencier
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished
|
||||||
|
to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
12
src/Symfony/Component/Mailer/Bridge/Amazon/README.md
Normal file
12
src/Symfony/Component/Mailer/Bridge/Amazon/README.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
Amazon Mailer
|
||||||
|
=============
|
||||||
|
|
||||||
|
Provides Amazon SES integration for Symfony Mailer.
|
||||||
|
|
||||||
|
Resources
|
||||||
|
---------
|
||||||
|
|
||||||
|
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
|
||||||
|
* [Report issues](https://github.com/symfony/symfony/issues) and
|
||||||
|
[send Pull Requests](https://github.com/symfony/symfony/pulls)
|
||||||
|
in the [main Symfony repository](https://github.com/symfony/symfony)
|
@ -0,0 +1,35 @@
|
|||||||
|
<?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\Amazon\Smtp;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class SesTransport extends EsmtpTransport
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param string $region Amazon SES region (currently one of us-east-1, us-west-2, or eu-west-1)
|
||||||
|
*/
|
||||||
|
public function __construct(string $username, string $password, string $region = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
|
||||||
|
{
|
||||||
|
parent::__construct(\sprintf('email-smtp.%s.amazonaws.com', $region ?: 'eu-west-1'), 587, 'tls', null, $dispatcher, $logger);
|
||||||
|
|
||||||
|
$this->setUsername($username);
|
||||||
|
$this->setPassword($password);
|
||||||
|
}
|
||||||
|
}
|
37
src/Symfony/Component/Mailer/Bridge/Amazon/composer.json
Normal file
37
src/Symfony/Component/Mailer/Bridge/Amazon/composer.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "symfony/amazon-mailer",
|
||||||
|
"type": "symfony-bridge",
|
||||||
|
"description": "Symfony Amazon Mailer Bridge",
|
||||||
|
"keywords": [],
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"license": "MIT",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Fabien Potencier",
|
||||||
|
"email": "fabien@symfony.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php": "^7.1.3",
|
||||||
|
"symfony/mailer": "^4.3"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"symfony/http-client": "^4.3"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": { "Symfony\\Component\\Mailer\\Bridge\\Amazon\\": "" },
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minimum-stability": "dev",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "4.3-dev"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
src/Symfony/Component/Mailer/Bridge/Amazon/phpunit.xml.dist
Normal file
31
src/Symfony/Component/Mailer/Bridge/Amazon/phpunit.xml.dist
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/5.2/phpunit.xsd"
|
||||||
|
backupGlobals="false"
|
||||||
|
colors="true"
|
||||||
|
bootstrap="vendor/autoload.php"
|
||||||
|
failOnRisky="true"
|
||||||
|
failOnWarning="true"
|
||||||
|
>
|
||||||
|
<php>
|
||||||
|
<ini name="error_reporting" value="-1" />
|
||||||
|
</php>
|
||||||
|
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Symfony Amazon Mailer Test Suite">
|
||||||
|
<directory>./Tests/</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
|
||||||
|
<filter>
|
||||||
|
<whitelist>
|
||||||
|
<directory>./</directory>
|
||||||
|
<exclude>
|
||||||
|
<directory>./Resources</directory>
|
||||||
|
<directory>./Tests</directory>
|
||||||
|
<directory>./vendor</directory>
|
||||||
|
</exclude>
|
||||||
|
</whitelist>
|
||||||
|
</filter>
|
||||||
|
</phpunit>
|
7
src/Symfony/Component/Mailer/Bridge/Google/CHANGELOG.md
Normal file
7
src/Symfony/Component/Mailer/Bridge/Google/CHANGELOG.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
CHANGELOG
|
||||||
|
=========
|
||||||
|
|
||||||
|
4.3.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* added the bridge
|
19
src/Symfony/Component/Mailer/Bridge/Google/LICENSE
Normal file
19
src/Symfony/Component/Mailer/Bridge/Google/LICENSE
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) 2019 Fabien Potencier
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished
|
||||||
|
to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
12
src/Symfony/Component/Mailer/Bridge/Google/README.md
Normal file
12
src/Symfony/Component/Mailer/Bridge/Google/README.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
Google Mailer
|
||||||
|
=============
|
||||||
|
|
||||||
|
Provides Google Gmail integration for Symfony Mailer.
|
||||||
|
|
||||||
|
Resources
|
||||||
|
---------
|
||||||
|
|
||||||
|
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
|
||||||
|
* [Report issues](https://github.com/symfony/symfony/issues) and
|
||||||
|
[send Pull Requests](https://github.com/symfony/symfony/pulls)
|
||||||
|
in the [main Symfony repository](https://github.com/symfony/symfony)
|
@ -0,0 +1,32 @@
|
|||||||
|
<?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\Google\Smtp;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class GmailTransport extends EsmtpTransport
|
||||||
|
{
|
||||||
|
public function __construct(string $username, string $password, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
|
||||||
|
{
|
||||||
|
parent::__construct('smtp.gmail.com', 465, 'ssl', null, $dispatcher, $logger);
|
||||||
|
|
||||||
|
$this->setUsername($username);
|
||||||
|
$this->setPassword($password);
|
||||||
|
}
|
||||||
|
}
|
37
src/Symfony/Component/Mailer/Bridge/Google/composer.json
Normal file
37
src/Symfony/Component/Mailer/Bridge/Google/composer.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "symfony/google-mailer",
|
||||||
|
"type": "symfony-bridge",
|
||||||
|
"description": "Symfony Google Mailer Bridge",
|
||||||
|
"keywords": [],
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"license": "MIT",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Fabien Potencier",
|
||||||
|
"email": "fabien@symfony.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php": "^7.1.3",
|
||||||
|
"symfony/mailer": "^4.3"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"symfony/http-client": "^4.3"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": { "Symfony\\Component\\Mailer\\Bridge\\Google\\": "" },
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minimum-stability": "dev",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "4.3-dev"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
src/Symfony/Component/Mailer/Bridge/Google/phpunit.xml.dist
Normal file
31
src/Symfony/Component/Mailer/Bridge/Google/phpunit.xml.dist
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/5.2/phpunit.xsd"
|
||||||
|
backupGlobals="false"
|
||||||
|
colors="true"
|
||||||
|
bootstrap="vendor/autoload.php"
|
||||||
|
failOnRisky="true"
|
||||||
|
failOnWarning="true"
|
||||||
|
>
|
||||||
|
<php>
|
||||||
|
<ini name="error_reporting" value="-1" />
|
||||||
|
</php>
|
||||||
|
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Symfony Google Mailer Bridge Test Suite">
|
||||||
|
<directory>./Tests/</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
|
||||||
|
<filter>
|
||||||
|
<whitelist>
|
||||||
|
<directory>./</directory>
|
||||||
|
<exclude>
|
||||||
|
<directory>./Resources</directory>
|
||||||
|
<directory>./Tests</directory>
|
||||||
|
<directory>./vendor</directory>
|
||||||
|
</exclude>
|
||||||
|
</whitelist>
|
||||||
|
</filter>
|
||||||
|
</phpunit>
|
@ -0,0 +1,7 @@
|
|||||||
|
CHANGELOG
|
||||||
|
=========
|
||||||
|
|
||||||
|
4.3.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* added the bridge
|
@ -0,0 +1,114 @@
|
|||||||
|
<?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\Http\Api;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\Mailer\Exception\TransportException;
|
||||||
|
use Symfony\Component\Mailer\SmtpEnvelope;
|
||||||
|
use Symfony\Component\Mailer\Transport\Http\Api\AbstractApiTransport;
|
||||||
|
use Symfony\Component\Mime\Email;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class MandrillTransport extends AbstractApiTransport
|
||||||
|
{
|
||||||
|
private const ENDPOINT = 'https://mandrillapp.com/api/1.0/messages/send.json';
|
||||||
|
|
||||||
|
private $key;
|
||||||
|
|
||||||
|
public function __construct(string $key, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
|
||||||
|
{
|
||||||
|
$this->key = $key;
|
||||||
|
|
||||||
|
parent::__construct($client, $dispatcher, $logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doSendEmail(Email $email, SmtpEnvelope $envelope): void
|
||||||
|
{
|
||||||
|
$response = $this->client->request('POST', self::ENDPOINT, [
|
||||||
|
'json' => $this->getPayload($email, $envelope),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (200 !== $response->getStatusCode()) {
|
||||||
|
$result = $response->toArray(false);
|
||||||
|
if ('error' === ($result['status'] ?? false)) {
|
||||||
|
throw new TransportException(sprintf('Unable to send an email: %s (code %s).', $result['message'], $result['code']));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new TransportException(sprintf('Unable to send an email (code %s).', $result['code']));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getPayload(Email $email, SmtpEnvelope $envelope): array
|
||||||
|
{
|
||||||
|
$payload = [
|
||||||
|
'key' => $this->key,
|
||||||
|
'message' => [
|
||||||
|
'html' => $email->getHtmlBody(),
|
||||||
|
'text' => $email->getTextBody(),
|
||||||
|
'subject' => $email->getSubject(),
|
||||||
|
'from_email' => $envelope->getSender()->toString(),
|
||||||
|
'to' => $this->getRecipients($email, $envelope),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($email->getAttachments() as $attachment) {
|
||||||
|
$headers = $attachment->getPreparedHeaders();
|
||||||
|
$disposition = $headers->getHeaderBody('Content-Disposition');
|
||||||
|
|
||||||
|
$att = [
|
||||||
|
'content' => $attachment->bodyToString(),
|
||||||
|
'type' => $headers->get('Content-Type')->getBody(),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ('inline' === $disposition) {
|
||||||
|
$payload['images'][] = $att;
|
||||||
|
} else {
|
||||||
|
$payload['attachments'][] = $att;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$headersToBypass = ['from', 'to', 'cc', 'bcc', 'subject', 'content-type'];
|
||||||
|
foreach ($email->getHeaders()->getAll() as $name => $header) {
|
||||||
|
if (\in_array($name, $headersToBypass, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload['message']['headers'][] = $name.': '.$header->toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getRecipients(Email $email, SmtpEnvelope $envelope): array
|
||||||
|
{
|
||||||
|
$recipients = [];
|
||||||
|
foreach ($envelope->getRecipients() as $recipient) {
|
||||||
|
$type = 'to';
|
||||||
|
if (\in_array($recipient, $email->getBcc(), true)) {
|
||||||
|
$type = 'bcc';
|
||||||
|
} elseif (\in_array($recipient, $email->getCc(), true)) {
|
||||||
|
$type = 'cc';
|
||||||
|
}
|
||||||
|
|
||||||
|
$recipients[] = [
|
||||||
|
'email' => $recipient->toString(),
|
||||||
|
'type' => $type,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $recipients;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
<?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\Http;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\HttpClient\HttpClient;
|
||||||
|
use Symfony\Component\Mailer\Exception\TransportException;
|
||||||
|
use Symfony\Component\Mailer\SentMessage;
|
||||||
|
use Symfony\Component\Mailer\Transport\AbstractTransport;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class MandrillTransport extends AbstractTransport
|
||||||
|
{
|
||||||
|
private const ENDPOINT = 'https://mandrillapp.com/api/1.0/messages/send-raw.json';
|
||||||
|
private $client;
|
||||||
|
private $key;
|
||||||
|
|
||||||
|
public function __construct(string $key, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
|
||||||
|
{
|
||||||
|
$this->key = $key;
|
||||||
|
$this->client = $client ?? HttpClient::create();
|
||||||
|
|
||||||
|
parent::__construct($dispatcher, $logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doSend(SentMessage $message): void
|
||||||
|
{
|
||||||
|
$envelope = $message->getEnvelope();
|
||||||
|
$response = $this->client->request('POST', self::ENDPOINT, [
|
||||||
|
'json' => [
|
||||||
|
'key' => $this->key,
|
||||||
|
'to' => $this->stringifyAddresses($envelope->getRecipients()),
|
||||||
|
'from_email' => $envelope->getSender()->toString(),
|
||||||
|
'raw_message' => $message->toString(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (200 !== $response->getStatusCode()) {
|
||||||
|
$result = $response->toArray(false);
|
||||||
|
if ('error' === ($result['status'] ?? false)) {
|
||||||
|
throw new TransportException(sprintf('Unable to send an email: %s (code %s).', $result['message'], $result['code']));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new TransportException(sprintf('Unable to send an email (code %s).', $result['code']));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
19
src/Symfony/Component/Mailer/Bridge/Mailchimp/LICENSE
Normal file
19
src/Symfony/Component/Mailer/Bridge/Mailchimp/LICENSE
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) 2019 Fabien Potencier
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished
|
||||||
|
to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
12
src/Symfony/Component/Mailer/Bridge/Mailchimp/README.md
Normal file
12
src/Symfony/Component/Mailer/Bridge/Mailchimp/README.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
Mailchimp Mailer
|
||||||
|
================
|
||||||
|
|
||||||
|
Provides Mandrill integration for Symfony Mailer.
|
||||||
|
|
||||||
|
Resources
|
||||||
|
---------
|
||||||
|
|
||||||
|
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
|
||||||
|
* [Report issues](https://github.com/symfony/symfony/issues) and
|
||||||
|
[send Pull Requests](https://github.com/symfony/symfony/pulls)
|
||||||
|
in the [main Symfony repository](https://github.com/symfony/symfony)
|
@ -0,0 +1,30 @@
|
|||||||
|
<?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\Smtp;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class MandrillTransport extends EsmtpTransport
|
||||||
|
{
|
||||||
|
public function __construct(string $username, string $password, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
|
||||||
|
{
|
||||||
|
parent::__construct('smtp.mandrillapp.com', 587, 'tls', null, $dispatcher, $logger);
|
||||||
|
|
||||||
|
$this->setUsername($username);
|
||||||
|
$this->setPassword($password);
|
||||||
|
}
|
||||||
|
}
|
37
src/Symfony/Component/Mailer/Bridge/Mailchimp/composer.json
Normal file
37
src/Symfony/Component/Mailer/Bridge/Mailchimp/composer.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "symfony/mailchimp-mailer",
|
||||||
|
"type": "symfony-bridge",
|
||||||
|
"description": "Symfony Mailchimp Mailer Bridge",
|
||||||
|
"keywords": [],
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"license": "MIT",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Fabien Potencier",
|
||||||
|
"email": "fabien@symfony.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php": "^7.1.3",
|
||||||
|
"symfony/mailer": "^4.3"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"symfony/http-client": "^4.3"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": { "Symfony\\Component\\Mailer\\Bridge\\Mailchimp\\": "" },
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minimum-stability": "dev",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "4.3-dev"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/5.2/phpunit.xsd"
|
||||||
|
backupGlobals="false"
|
||||||
|
colors="true"
|
||||||
|
bootstrap="vendor/autoload.php"
|
||||||
|
failOnRisky="true"
|
||||||
|
failOnWarning="true"
|
||||||
|
>
|
||||||
|
<php>
|
||||||
|
<ini name="error_reporting" value="-1" />
|
||||||
|
</php>
|
||||||
|
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Symfony Mailchimp Mailer Bridge Test Suite">
|
||||||
|
<directory>./Tests/</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
|
||||||
|
<filter>
|
||||||
|
<whitelist>
|
||||||
|
<directory>./</directory>
|
||||||
|
<exclude>
|
||||||
|
<directory>./Resources</directory>
|
||||||
|
<directory>./Tests</directory>
|
||||||
|
<directory>./vendor</directory>
|
||||||
|
</exclude>
|
||||||
|
</whitelist>
|
||||||
|
</filter>
|
||||||
|
</phpunit>
|
7
src/Symfony/Component/Mailer/Bridge/Mailgun/CHANGELOG.md
Normal file
7
src/Symfony/Component/Mailer/Bridge/Mailgun/CHANGELOG.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
CHANGELOG
|
||||||
|
=========
|
||||||
|
|
||||||
|
4.3.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* added the bridge
|
@ -0,0 +1,132 @@
|
|||||||
|
<?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\Http\Api;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\Mailer\Exception\TransportException;
|
||||||
|
use Symfony\Component\Mailer\SmtpEnvelope;
|
||||||
|
use Symfony\Component\Mailer\Transport\Http\Api\AbstractApiTransport;
|
||||||
|
use Symfony\Component\Mime\Email;
|
||||||
|
use Symfony\Component\Mime\Part\Multipart\FormDataPart;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class MailgunTransport extends AbstractApiTransport
|
||||||
|
{
|
||||||
|
private const ENDPOINT = 'https://api.mailgun.net/v3/%domain%/messages';
|
||||||
|
|
||||||
|
private $key;
|
||||||
|
private $domain;
|
||||||
|
|
||||||
|
public function __construct(string $key, string $domain, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
|
||||||
|
{
|
||||||
|
$this->key = $key;
|
||||||
|
$this->domain = $domain;
|
||||||
|
|
||||||
|
parent::__construct($client, $dispatcher, $logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doSendEmail(Email $email, SmtpEnvelope $envelope): void
|
||||||
|
{
|
||||||
|
$body = new FormDataPart($this->getPayload($email, $envelope));
|
||||||
|
$headers = [];
|
||||||
|
foreach ($body->getPreparedHeaders()->getAll() as $header) {
|
||||||
|
$headers[] = $header->toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
$endpoint = str_replace('%domain%', urlencode($this->domain), self::ENDPOINT);
|
||||||
|
$response = $this->client->request('POST', $endpoint, [
|
||||||
|
'auth_basic' => 'api:'.$this->key,
|
||||||
|
'headers' => $headers,
|
||||||
|
'body' => $body->bodyToIterable(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (200 !== $response->getStatusCode()) {
|
||||||
|
$error = $response->toArray(false);
|
||||||
|
|
||||||
|
throw new TransportException(sprintf('Unable to send an email: %s (code %s).', $error['message'], $response->getStatusCode()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getPayload(Email $email, SmtpEnvelope $envelope): array
|
||||||
|
{
|
||||||
|
$headers = $email->getHeaders();
|
||||||
|
$html = $email->getHtmlBody();
|
||||||
|
if (null !== $html) {
|
||||||
|
if (stream_get_meta_data($html)['seekable'] ?? false) {
|
||||||
|
rewind($html);
|
||||||
|
}
|
||||||
|
$html = stream_get_contents($html);
|
||||||
|
}
|
||||||
|
[$attachments, $inlines, $html] = $this->prepareAttachments($email, $html);
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'from' => $envelope->getSender()->toString(),
|
||||||
|
'to' => implode(',', $this->stringifyAddresses($this->getRecipients($email, $envelope))),
|
||||||
|
'subject' => $email->getSubject(),
|
||||||
|
'attachment' => $attachments,
|
||||||
|
'inline' => $inlines,
|
||||||
|
];
|
||||||
|
if ($emails = $email->getCc()) {
|
||||||
|
$payload['cc'] = implode(',', $this->stringifyAddresses($emails));
|
||||||
|
}
|
||||||
|
if ($emails = $email->getBcc()) {
|
||||||
|
$payload['bcc'] = implode(',', $this->stringifyAddresses($emails));
|
||||||
|
}
|
||||||
|
if ($email->getTextBody()) {
|
||||||
|
$payload['text'] = $email->getTextBody();
|
||||||
|
}
|
||||||
|
if ($html) {
|
||||||
|
$payload['html'] = $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
$headersToBypass = ['from', 'to', 'cc', 'bcc', 'subject', 'content-type'];
|
||||||
|
foreach ($headers->getAll() as $name => $header) {
|
||||||
|
if (\in_array($name, $headersToBypass, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload['h:'.$name] = $header->toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function prepareAttachments(Email $email, ?string $html): array
|
||||||
|
{
|
||||||
|
$attachments = $inlines = [];
|
||||||
|
foreach ($email->getAttachments() as $attachment) {
|
||||||
|
$headers = $attachment->getPreparedHeaders();
|
||||||
|
if ('inline' === $headers->getHeaderBody('Content-Disposition')) {
|
||||||
|
// replace the cid with just a file name (the only supported way by Mailgun)
|
||||||
|
if ($html) {
|
||||||
|
$filename = $headers->getHeaderParameter('Content-Disposition', 'filename');
|
||||||
|
$new = basename($filename);
|
||||||
|
$html = str_replace('cid:'.$filename, 'cid:'.$new, $html);
|
||||||
|
$p = new \ReflectionProperty($attachment, 'filename');
|
||||||
|
$p->setAccessible(true);
|
||||||
|
$p->setValue($attachment, $new);
|
||||||
|
}
|
||||||
|
$inlines[] = $attachment;
|
||||||
|
} else {
|
||||||
|
$attachments[] = $attachment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$attachments, $inlines, $html];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
<?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\Http;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\HttpClient\HttpClient;
|
||||||
|
use Symfony\Component\Mailer\Exception\TransportException;
|
||||||
|
use Symfony\Component\Mailer\SentMessage;
|
||||||
|
use Symfony\Component\Mailer\Transport\AbstractTransport;
|
||||||
|
use Symfony\Component\Mime\Part\DataPart;
|
||||||
|
use Symfony\Component\Mime\Part\Multipart\FormDataPart;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class MailgunTransport extends AbstractTransport
|
||||||
|
{
|
||||||
|
private const ENDPOINT = 'https://api.mailgun.net/v3/%domain%/messages.mime';
|
||||||
|
private $key;
|
||||||
|
private $domain;
|
||||||
|
private $client;
|
||||||
|
|
||||||
|
public function __construct(string $key, string $domain, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
|
||||||
|
{
|
||||||
|
$this->key = $key;
|
||||||
|
$this->domain = $domain;
|
||||||
|
$this->client = $client ?? HttpClient::create();
|
||||||
|
|
||||||
|
parent::__construct($dispatcher, $logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doSend(SentMessage $message): void
|
||||||
|
{
|
||||||
|
$body = new FormDataPart([
|
||||||
|
'to' => implode(',', $this->stringifyAddresses($message->getEnvelope()->getRecipients())),
|
||||||
|
'message' => new DataPart($message->toString(), 'message.mime'),
|
||||||
|
]);
|
||||||
|
$headers = [];
|
||||||
|
foreach ($body->getPreparedHeaders()->getAll() as $header) {
|
||||||
|
$headers[] = $header->toString();
|
||||||
|
}
|
||||||
|
$endpoint = str_replace('%domain%', urlencode($this->domain), self::ENDPOINT);
|
||||||
|
$response = $this->client->request('POST', $endpoint, [
|
||||||
|
'auth_basic' => 'api:'.$this->key,
|
||||||
|
'headers' => $headers,
|
||||||
|
'body' => $body->bodyToIterable(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (200 !== $response->getStatusCode()) {
|
||||||
|
$error = $response->toArray(false);
|
||||||
|
|
||||||
|
throw new TransportException(sprintf('Unable to send an email: %s (code %s).', $error['message'], $response->getStatusCode()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
19
src/Symfony/Component/Mailer/Bridge/Mailgun/LICENSE
Normal file
19
src/Symfony/Component/Mailer/Bridge/Mailgun/LICENSE
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) 2019 Fabien Potencier
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished
|
||||||
|
to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
12
src/Symfony/Component/Mailer/Bridge/Mailgun/README.md
Normal file
12
src/Symfony/Component/Mailer/Bridge/Mailgun/README.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
Mailgun Mailer
|
||||||
|
==============
|
||||||
|
|
||||||
|
Provides Mailgun integration for Symfony Mailer.
|
||||||
|
|
||||||
|
Resources
|
||||||
|
---------
|
||||||
|
|
||||||
|
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
|
||||||
|
* [Report issues](https://github.com/symfony/symfony/issues) and
|
||||||
|
[send Pull Requests](https://github.com/symfony/symfony/pulls)
|
||||||
|
in the [main Symfony repository](https://github.com/symfony/symfony)
|
@ -0,0 +1,32 @@
|
|||||||
|
<?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\Smtp;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class MailgunTransport extends EsmtpTransport
|
||||||
|
{
|
||||||
|
public function __construct(string $username, string $password, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
|
||||||
|
{
|
||||||
|
parent::__construct('smtp.mailgun.org', 465, 'ssl', null, $dispatcher, $logger);
|
||||||
|
|
||||||
|
$this->setUsername($username);
|
||||||
|
$this->setPassword($password);
|
||||||
|
}
|
||||||
|
}
|
37
src/Symfony/Component/Mailer/Bridge/Mailgun/composer.json
Normal file
37
src/Symfony/Component/Mailer/Bridge/Mailgun/composer.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "symfony/mailgun-mailer",
|
||||||
|
"type": "symfony-bridge",
|
||||||
|
"description": "Symfony Mailgun Mailer Bridge",
|
||||||
|
"keywords": [],
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"license": "MIT",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Fabien Potencier",
|
||||||
|
"email": "fabien@symfony.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php": "^7.1.3",
|
||||||
|
"symfony/mailer": "^4.3"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"symfony/http-client": "^4.3"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": { "Symfony\\Component\\Mailer\\Bridge\\Mailgun\\": "" },
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minimum-stability": "dev",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "4.3-dev"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
src/Symfony/Component/Mailer/Bridge/Mailgun/phpunit.xml.dist
Normal file
31
src/Symfony/Component/Mailer/Bridge/Mailgun/phpunit.xml.dist
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/5.2/phpunit.xsd"
|
||||||
|
backupGlobals="false"
|
||||||
|
colors="true"
|
||||||
|
bootstrap="vendor/autoload.php"
|
||||||
|
failOnRisky="true"
|
||||||
|
failOnWarning="true"
|
||||||
|
>
|
||||||
|
<php>
|
||||||
|
<ini name="error_reporting" value="-1" />
|
||||||
|
</php>
|
||||||
|
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Symfony Mailgun Mailer Bridge Test Suite">
|
||||||
|
<directory>./Tests/</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
|
||||||
|
<filter>
|
||||||
|
<whitelist>
|
||||||
|
<directory>./</directory>
|
||||||
|
<exclude>
|
||||||
|
<directory>./Resources</directory>
|
||||||
|
<directory>./Tests</directory>
|
||||||
|
<directory>./vendor</directory>
|
||||||
|
</exclude>
|
||||||
|
</whitelist>
|
||||||
|
</filter>
|
||||||
|
</phpunit>
|
@ -0,0 +1,7 @@
|
|||||||
|
CHANGELOG
|
||||||
|
=========
|
||||||
|
|
||||||
|
4.3.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* added the bridge
|
@ -0,0 +1,108 @@
|
|||||||
|
<?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\Postmark\Http\Api;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\Mailer\Exception\TransportException;
|
||||||
|
use Symfony\Component\Mailer\SmtpEnvelope;
|
||||||
|
use Symfony\Component\Mailer\Transport\Http\Api\AbstractApiTransport;
|
||||||
|
use Symfony\Component\Mime\Email;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class PostmarkTransport extends AbstractApiTransport
|
||||||
|
{
|
||||||
|
private const ENDPOINT = 'http://api.postmarkapp.com/email';
|
||||||
|
|
||||||
|
private $key;
|
||||||
|
|
||||||
|
public function __construct(string $key, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
|
||||||
|
{
|
||||||
|
$this->key = $key;
|
||||||
|
|
||||||
|
parent::__construct($client, $dispatcher, $logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doSendEmail(Email $email, SmtpEnvelope $envelope): void
|
||||||
|
{
|
||||||
|
$response = $this->client->request('POST', self::ENDPOINT, [
|
||||||
|
'headers' => [
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
'X-Postmark-Server-Token' => $this->key,
|
||||||
|
],
|
||||||
|
'json' => $this->getPayload($email, $envelope),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (200 !== $response->getStatusCode()) {
|
||||||
|
$error = $response->toArray(false);
|
||||||
|
|
||||||
|
throw new TransportException(sprintf('Unable to send an email: %s (code %s).', $error['Message'], $error['ErrorCode']));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getPayload(Email $email, SmtpEnvelope $envelope): array
|
||||||
|
{
|
||||||
|
$payload = [
|
||||||
|
'From' => $envelope->getSender()->toString(),
|
||||||
|
'To' => implode(',', $this->stringifyAddresses($this->getRecipients($email, $envelope))),
|
||||||
|
'Cc' => implode(',', $this->stringifyAddresses($email->getCc())),
|
||||||
|
'Bcc' => implode(',', $this->stringifyAddresses($email->getBcc())),
|
||||||
|
'Subject' => $email->getSubject(),
|
||||||
|
'TextBody' => $email->getTextBody(),
|
||||||
|
'HtmlBody' => $email->getHtmlBody(),
|
||||||
|
'Attachments' => $this->getAttachments($email),
|
||||||
|
];
|
||||||
|
|
||||||
|
$headersToBypass = ['from', 'to', 'cc', 'bcc', 'subject', 'content-type', 'sender'];
|
||||||
|
foreach ($email->getHeaders()->getAll() as $name => $header) {
|
||||||
|
if (\in_array($name, $headersToBypass, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload['Headers'][] = [
|
||||||
|
'Name' => $name,
|
||||||
|
'Value' => $header->toString(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getAttachments(Email $email): array
|
||||||
|
{
|
||||||
|
$attachments = [];
|
||||||
|
foreach ($email->getAttachments() as $attachment) {
|
||||||
|
$headers = $attachment->getPreparedHeaders();
|
||||||
|
$filename = $headers->getHeaderParameter('Content-Disposition', 'filename');
|
||||||
|
$disposition = $headers->getHeaderBody('Content-Disposition');
|
||||||
|
|
||||||
|
$att = [
|
||||||
|
'Name' => $filename,
|
||||||
|
'Content' => $attachment->bodyToString(),
|
||||||
|
'ContentType' => $headers->get('Content-Type')->getBody(),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ('inline' === $disposition) {
|
||||||
|
$att['ContentID'] = 'cid:'.$filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
$attachments[] = $att;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $attachments;
|
||||||
|
}
|
||||||
|
}
|
19
src/Symfony/Component/Mailer/Bridge/Postmark/LICENSE
Normal file
19
src/Symfony/Component/Mailer/Bridge/Postmark/LICENSE
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) 2019 Fabien Potencier
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished
|
||||||
|
to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
12
src/Symfony/Component/Mailer/Bridge/Postmark/README.md
Normal file
12
src/Symfony/Component/Mailer/Bridge/Postmark/README.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
Postmark Bridge
|
||||||
|
===============
|
||||||
|
|
||||||
|
Provides Postmark integration for Symfony Mailer.
|
||||||
|
|
||||||
|
Resources
|
||||||
|
---------
|
||||||
|
|
||||||
|
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
|
||||||
|
* [Report issues](https://github.com/symfony/symfony/issues) and
|
||||||
|
[send Pull Requests](https://github.com/symfony/symfony/pulls)
|
||||||
|
in the [main Symfony repository](https://github.com/symfony/symfony)
|
@ -0,0 +1,32 @@
|
|||||||
|
<?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\Postmark\Smtp;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class PostmarkTransport extends EsmtpTransport
|
||||||
|
{
|
||||||
|
public function __construct(string $id, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
|
||||||
|
{
|
||||||
|
parent::__construct('smtp.postmarkapp.com', 587, 'tls', null, $dispatcher, $logger);
|
||||||
|
|
||||||
|
$this->setUsername($id);
|
||||||
|
$this->setPassword($id);
|
||||||
|
}
|
||||||
|
}
|
37
src/Symfony/Component/Mailer/Bridge/Postmark/composer.json
Normal file
37
src/Symfony/Component/Mailer/Bridge/Postmark/composer.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "symfony/postmark-mailer",
|
||||||
|
"type": "symfony-bridge",
|
||||||
|
"description": "Symfony Postmark Mailer Bridge",
|
||||||
|
"keywords": [],
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"license": "MIT",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Fabien Potencier",
|
||||||
|
"email": "fabien@symfony.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php": "^7.1.3",
|
||||||
|
"symfony/mailer": "^4.3"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"symfony/http-client": "^4.3"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": { "Symfony\\Component\\Mailer\\Bridge\\Postmark\\": "" },
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minimum-stability": "dev",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "4.3-dev"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/5.2/phpunit.xsd"
|
||||||
|
backupGlobals="false"
|
||||||
|
colors="true"
|
||||||
|
bootstrap="vendor/autoload.php"
|
||||||
|
failOnRisky="true"
|
||||||
|
failOnWarning="true"
|
||||||
|
>
|
||||||
|
<php>
|
||||||
|
<ini name="error_reporting" value="-1" />
|
||||||
|
</php>
|
||||||
|
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Symfony Postmark Mailer Bridge Test Suite">
|
||||||
|
<directory>./Tests/</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
|
||||||
|
<filter>
|
||||||
|
<whitelist>
|
||||||
|
<directory>./</directory>
|
||||||
|
<exclude>
|
||||||
|
<directory>./Resources</directory>
|
||||||
|
<directory>./Tests</directory>
|
||||||
|
<directory>./vendor</directory>
|
||||||
|
</exclude>
|
||||||
|
</whitelist>
|
||||||
|
</filter>
|
||||||
|
</phpunit>
|
3
src/Symfony/Component/Mailer/Bridge/Sendgrid/.gitignore
vendored
Normal file
3
src/Symfony/Component/Mailer/Bridge/Sendgrid/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
vendor/
|
||||||
|
composer.lock
|
||||||
|
phpunit.xml
|
@ -0,0 +1,7 @@
|
|||||||
|
CHANGELOG
|
||||||
|
=========
|
||||||
|
|
||||||
|
4.3.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* added the bridge
|
@ -0,0 +1,131 @@
|
|||||||
|
<?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\Sendgrid\Http\Api;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\Mailer\Exception\TransportException;
|
||||||
|
use Symfony\Component\Mailer\SmtpEnvelope;
|
||||||
|
use Symfony\Component\Mailer\Transport\Http\Api\AbstractApiTransport;
|
||||||
|
use Symfony\Component\Mime\Address;
|
||||||
|
use Symfony\Component\Mime\Email;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class SendgridTransport extends AbstractApiTransport
|
||||||
|
{
|
||||||
|
private const ENDPOINT = 'https://api.sendgrid.com/v3/mail/send';
|
||||||
|
|
||||||
|
private $key;
|
||||||
|
|
||||||
|
public function __construct(string $key, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
|
||||||
|
{
|
||||||
|
$this->key = $key;
|
||||||
|
|
||||||
|
parent::__construct($client, $dispatcher, $logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doSendEmail(Email $email, SmtpEnvelope $envelope): void
|
||||||
|
{
|
||||||
|
$response = $this->client->request('POST', self::ENDPOINT, [
|
||||||
|
'json' => $this->getPayload($email, $envelope),
|
||||||
|
'auth_bearer' => $this->key,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (202 !== $response->getStatusCode()) {
|
||||||
|
$errors = $response->toArray(false);
|
||||||
|
|
||||||
|
throw new TransportException(sprintf('Unable to send an email: %s (code %s).', implode('; ', array_column($errors['errors'], 'message')), $response->getStatusCode()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getPayload(Email $email, SmtpEnvelope $envelope): array
|
||||||
|
{
|
||||||
|
$addressStringifier = function (Address $address) {return ['email' => $address->toString()]; };
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'personalizations' => [],
|
||||||
|
'from' => ['email' => $envelope->getSender()->toString()],
|
||||||
|
'content' => $this->getContent($email),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($email->getAttachments()) {
|
||||||
|
$payload['attachments'] = $this->getAttachments($email);
|
||||||
|
}
|
||||||
|
|
||||||
|
$personalization = [
|
||||||
|
'to' => \array_map($addressStringifier, $this->getRecipients($email, $envelope)),
|
||||||
|
'subject' => $email->getSubject(),
|
||||||
|
];
|
||||||
|
if ($emails = array_map($addressStringifier, $email->getCc())) {
|
||||||
|
$personalization['cc'] = $emails;
|
||||||
|
}
|
||||||
|
if ($emails = array_map($addressStringifier, $email->getBcc())) {
|
||||||
|
$personalization['bcc'] = $emails;
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload['personalizations'][] = $personalization;
|
||||||
|
|
||||||
|
// these headers can't be overwritten according to Sendgrid docs
|
||||||
|
// see https://developers.pepipost.com/migration-api/new-subpage/email-send
|
||||||
|
$headersToBypass = ['x-sg-id', 'x-sg-eid', 'received', 'dkim-signature', 'content-transfer-encoding', 'from', 'to', 'cc', 'bcc', 'subject', 'content-type', 'reply-to'];
|
||||||
|
foreach ($email->getHeaders()->getAll() as $name => $header) {
|
||||||
|
if (\in_array($name, $headersToBypass, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload['headers'][$name] = $header->toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getContent(Email $email): array
|
||||||
|
{
|
||||||
|
$content = [];
|
||||||
|
if (null !== $text = $email->getTextBody()) {
|
||||||
|
$content[] = ['type' => 'text/plain', 'value' => $text];
|
||||||
|
}
|
||||||
|
if (null !== $html = $email->getHtmlBody()) {
|
||||||
|
$content[] = ['type' => 'text/html', 'value' => $html];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getAttachments(Email $email): array
|
||||||
|
{
|
||||||
|
$attachments = [];
|
||||||
|
foreach ($email->getAttachments() as $attachment) {
|
||||||
|
$headers = $attachment->getPreparedHeaders();
|
||||||
|
$filename = $headers->getHeaderParameter('Content-Disposition', 'filename');
|
||||||
|
$disposition = $headers->getHeaderBody('Content-Disposition');
|
||||||
|
|
||||||
|
$att = [
|
||||||
|
'content' => $attachment->bodyToString(),
|
||||||
|
'type' => $headers->get('Content-Type')->getBody(),
|
||||||
|
'filename' => $filename,
|
||||||
|
'disposition' => $disposition,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ('inline' === $disposition) {
|
||||||
|
$att['content_id'] = $filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
$attachments[] = $att;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $attachments;
|
||||||
|
}
|
||||||
|
}
|
19
src/Symfony/Component/Mailer/Bridge/Sendgrid/LICENSE
Normal file
19
src/Symfony/Component/Mailer/Bridge/Sendgrid/LICENSE
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) 2019 Fabien Potencier
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished
|
||||||
|
to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
12
src/Symfony/Component/Mailer/Bridge/Sendgrid/README.md
Normal file
12
src/Symfony/Component/Mailer/Bridge/Sendgrid/README.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
Sendgrid Bridge
|
||||||
|
===============
|
||||||
|
|
||||||
|
Provides Sendgrid integration for Symfony Mailer.
|
||||||
|
|
||||||
|
Resources
|
||||||
|
---------
|
||||||
|
|
||||||
|
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
|
||||||
|
* [Report issues](https://github.com/symfony/symfony/issues) and
|
||||||
|
[send Pull Requests](https://github.com/symfony/symfony/pulls)
|
||||||
|
in the [main Symfony repository](https://github.com/symfony/symfony)
|
@ -0,0 +1,32 @@
|
|||||||
|
<?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\Sendgrid\Smtp;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class SendgridTransport extends EsmtpTransport
|
||||||
|
{
|
||||||
|
public function __construct(string $key, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
|
||||||
|
{
|
||||||
|
parent::__construct('smtp.sendgrid.net', 465, 'ssl', null, $dispatcher, $logger);
|
||||||
|
|
||||||
|
$this->setUsername('apikey');
|
||||||
|
$this->setPassword($key);
|
||||||
|
}
|
||||||
|
}
|
37
src/Symfony/Component/Mailer/Bridge/Sendgrid/composer.json
Normal file
37
src/Symfony/Component/Mailer/Bridge/Sendgrid/composer.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "symfony/sendgrid-mailer",
|
||||||
|
"type": "symfony-bridge",
|
||||||
|
"description": "Symfony Sendgrid Mailer Bridge",
|
||||||
|
"keywords": [],
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"license": "MIT",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Fabien Potencier",
|
||||||
|
"email": "fabien@symfony.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php": "^7.1.3",
|
||||||
|
"symfony/mailer": "^4.3"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"symfony/http-client": "^4.3"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": { "Symfony\\Component\\Mailer\\Bridge\\Sendgrid\\": "" },
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minimum-stability": "dev",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "4.3-dev"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/5.2/phpunit.xsd"
|
||||||
|
backupGlobals="false"
|
||||||
|
colors="true"
|
||||||
|
bootstrap="vendor/autoload.php"
|
||||||
|
failOnRisky="true"
|
||||||
|
failOnWarning="true"
|
||||||
|
>
|
||||||
|
<php>
|
||||||
|
<ini name="error_reporting" value="-1" />
|
||||||
|
</php>
|
||||||
|
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Symfony Sendgrid Mailer Test Suite">
|
||||||
|
<directory>./Tests/</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
|
||||||
|
<filter>
|
||||||
|
<whitelist>
|
||||||
|
<directory>./</directory>
|
||||||
|
<exclude>
|
||||||
|
<directory>./Resources</directory>
|
||||||
|
<directory>./Tests</directory>
|
||||||
|
<directory>./vendor</directory>
|
||||||
|
</exclude>
|
||||||
|
</whitelist>
|
||||||
|
</filter>
|
||||||
|
</phpunit>
|
7
src/Symfony/Component/Mailer/CHANGELOG.md
Normal file
7
src/Symfony/Component/Mailer/CHANGELOG.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
CHANGELOG
|
||||||
|
=========
|
||||||
|
|
||||||
|
4.3.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Added the component
|
55
src/Symfony/Component/Mailer/Event/MessageEvent.php
Normal file
55
src/Symfony/Component/Mailer/Event/MessageEvent.php
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<?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\Event;
|
||||||
|
|
||||||
|
use Symfony\Component\EventDispatcher\Event;
|
||||||
|
use Symfony\Component\Mailer\SmtpEnvelope;
|
||||||
|
use Symfony\Component\Mime\RawMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows the transformation of a Message.
|
||||||
|
*
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class MessageEvent extends Event
|
||||||
|
{
|
||||||
|
private $message;
|
||||||
|
private $envelope;
|
||||||
|
|
||||||
|
public function __construct(RawMessage $message, SmtpEnvelope $envelope)
|
||||||
|
{
|
||||||
|
$this->message = $message;
|
||||||
|
$this->envelope = $envelope;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMessage(): RawMessage
|
||||||
|
{
|
||||||
|
return $this->message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMessage(RawMessage $message): void
|
||||||
|
{
|
||||||
|
$this->message = $message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEnvelope(): SmtpEnvelope
|
||||||
|
{
|
||||||
|
return $this->envelope;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEnvelope(SmtpEnvelope $envelope): void
|
||||||
|
{
|
||||||
|
$this->envelope = $envelope;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
<?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\EventListener;
|
||||||
|
|
||||||
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
|
use Symfony\Component\Mailer\Event\MessageEvent;
|
||||||
|
use Symfony\Component\Mime\Address;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manipulates the Envelope of a Message.
|
||||||
|
*
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class EnvelopeListener implements EventSubscriberInterface
|
||||||
|
{
|
||||||
|
private $sender;
|
||||||
|
private $recipients;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Address|string $sender
|
||||||
|
* @param (Address|string)[] $recipients
|
||||||
|
*/
|
||||||
|
public function __construct($sender = null, array $recipients = null)
|
||||||
|
{
|
||||||
|
if (null !== $sender) {
|
||||||
|
$this->sender = Address::create($sender);
|
||||||
|
}
|
||||||
|
if (null !== $recipients) {
|
||||||
|
$this->recipients = Address::createArray($recipients);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onMessage(MessageEvent $event): void
|
||||||
|
{
|
||||||
|
if ($this->sender) {
|
||||||
|
$event->getEnvelope()->setSender($this->sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->recipients) {
|
||||||
|
$event->getEnvelope()->setRecipients($this->recipients);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getSubscribedEvents()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
// should be the last one to allow header changes by other listeners first
|
||||||
|
MessageEvent::class => ['onMessage', -255],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
<?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\EventListener;
|
||||||
|
|
||||||
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
|
use Symfony\Component\Mailer\Event\MessageEvent;
|
||||||
|
use Symfony\Component\Mime\BodyRendererInterface;
|
||||||
|
use Symfony\Component\Mime\Header\Headers;
|
||||||
|
use Symfony\Component\Mime\Message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manipulates the headers and the body of a Message.
|
||||||
|
*
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class MessageListener implements EventSubscriberInterface
|
||||||
|
{
|
||||||
|
private $headers;
|
||||||
|
private $renderer;
|
||||||
|
|
||||||
|
public function __construct(Headers $headers = null, BodyRendererInterface $renderer = null)
|
||||||
|
{
|
||||||
|
$this->headers = $headers;
|
||||||
|
$this->renderer = $renderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onMessage(MessageEvent $event): void
|
||||||
|
{
|
||||||
|
$message = $event->getMessage();
|
||||||
|
if (!$message instanceof Message) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->setHeaders($message);
|
||||||
|
$this->renderMessage($message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setHeaders(Message $message): void
|
||||||
|
{
|
||||||
|
if (!$this->headers) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$headers = $message->getHeaders();
|
||||||
|
foreach ($this->headers->getAll() as $name => $header) {
|
||||||
|
if (!$headers->has($name)) {
|
||||||
|
$headers->add($header);
|
||||||
|
} else {
|
||||||
|
if (Headers::isUniqueHeader($name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$headers->add($header);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$message->setHeaders($headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function renderMessage(Message $message): void
|
||||||
|
{
|
||||||
|
if (!$this->renderer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->renderer->render($message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getSubscribedEvents()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
MessageEvent::class => 'onMessage',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
<?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\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception interface for all exceptions thrown by the component.
|
||||||
|
*
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
interface ExceptionInterface extends \Throwable
|
||||||
|
{
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
<?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\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class HttpTransportException extends TransportException
|
||||||
|
{
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
<?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\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
|
||||||
|
{
|
||||||
|
}
|
21
src/Symfony/Component/Mailer/Exception/LogicException.php
Normal file
21
src/Symfony/Component/Mailer/Exception/LogicException.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?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\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class LogicException extends \LogicException implements ExceptionInterface
|
||||||
|
{
|
||||||
|
}
|
21
src/Symfony/Component/Mailer/Exception/RuntimeException.php
Normal file
21
src/Symfony/Component/Mailer/Exception/RuntimeException.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?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\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class RuntimeException extends \RuntimeException implements ExceptionInterface
|
||||||
|
{
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
<?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\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class TransportException extends RuntimeException implements TransportExceptionInterface
|
||||||
|
{
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
<?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\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
interface TransportExceptionInterface extends ExceptionInterface
|
||||||
|
{
|
||||||
|
}
|
19
src/Symfony/Component/Mailer/LICENSE
Normal file
19
src/Symfony/Component/Mailer/LICENSE
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) 2019 Fabien Potencier
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished
|
||||||
|
to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
45
src/Symfony/Component/Mailer/Mailer.php
Normal file
45
src/Symfony/Component/Mailer/Mailer.php
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<?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;
|
||||||
|
|
||||||
|
use Symfony\Component\Mailer\Messenger\SendEmailMessage;
|
||||||
|
use Symfony\Component\Mailer\Transport\TransportInterface;
|
||||||
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
|
use Symfony\Component\Mime\RawMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class Mailer implements MailerInterface
|
||||||
|
{
|
||||||
|
private $transport;
|
||||||
|
private $bus;
|
||||||
|
|
||||||
|
public function __construct(TransportInterface $transport, MessageBusInterface $bus = null)
|
||||||
|
{
|
||||||
|
$this->transport = $transport;
|
||||||
|
$this->bus = $bus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function send(RawMessage $message, SmtpEnvelope $envelope = null): void
|
||||||
|
{
|
||||||
|
if (null === $this->bus) {
|
||||||
|
$this->transport->send($message, $envelope);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->bus->dispatch(new SendEmailMessage($message, $envelope));
|
||||||
|
}
|
||||||
|
}
|
32
src/Symfony/Component/Mailer/MailerInterface.php
Normal file
32
src/Symfony/Component/Mailer/MailerInterface.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?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;
|
||||||
|
|
||||||
|
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||||
|
use Symfony\Component\Mime\RawMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for mailers able to send emails synchronous and/or asynchronous.
|
||||||
|
*
|
||||||
|
* Implementations must support synchronous and asynchronous sending.
|
||||||
|
*
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
interface MailerInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @throws TransportExceptionInterface
|
||||||
|
*/
|
||||||
|
public function send(RawMessage $message, SmtpEnvelope $envelope = null): void;
|
||||||
|
}
|
34
src/Symfony/Component/Mailer/Messenger/MessageHandler.php
Normal file
34
src/Symfony/Component/Mailer/Messenger/MessageHandler.php
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?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\Messenger;
|
||||||
|
|
||||||
|
use Symfony\Component\Mailer\Transport\TransportInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class MessageHandler
|
||||||
|
{
|
||||||
|
private $transport;
|
||||||
|
|
||||||
|
public function __construct(TransportInterface $transport)
|
||||||
|
{
|
||||||
|
$this->transport = $transport;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __invoke(SendEmailMessage $message)
|
||||||
|
{
|
||||||
|
$this->transport->send($message->getMessage(), $message->getEnvelope());
|
||||||
|
}
|
||||||
|
}
|
45
src/Symfony/Component/Mailer/Messenger/SendEmailMessage.php
Normal file
45
src/Symfony/Component/Mailer/Messenger/SendEmailMessage.php
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<?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\Messenger;
|
||||||
|
|
||||||
|
use Symfony\Component\Mailer\SmtpEnvelope;
|
||||||
|
use Symfony\Component\Mime\RawMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class SendEmailMessage
|
||||||
|
{
|
||||||
|
private $message;
|
||||||
|
private $envelope;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function __construct(RawMessage $message, SmtpEnvelope $envelope = null)
|
||||||
|
{
|
||||||
|
$this->message = $message;
|
||||||
|
$this->envelope = $envelope;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMessage(): RawMessage
|
||||||
|
{
|
||||||
|
return $this->message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEnvelope(): ?SmtpEnvelope
|
||||||
|
{
|
||||||
|
return $this->envelope;
|
||||||
|
}
|
||||||
|
}
|
12
src/Symfony/Component/Mailer/README.md
Normal file
12
src/Symfony/Component/Mailer/README.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
Mailer Component
|
||||||
|
================
|
||||||
|
|
||||||
|
The Mailer component helps sending emails.
|
||||||
|
|
||||||
|
Resources
|
||||||
|
---------
|
||||||
|
|
||||||
|
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
|
||||||
|
* [Report issues](https://github.com/symfony/symfony/issues) and
|
||||||
|
[send Pull Requests](https://github.com/symfony/symfony/pulls)
|
||||||
|
in the [main Symfony repository](https://github.com/symfony/symfony)
|
62
src/Symfony/Component/Mailer/SentMessage.php
Normal file
62
src/Symfony/Component/Mailer/SentMessage.php
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<?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;
|
||||||
|
|
||||||
|
use Symfony\Component\Mime\Message;
|
||||||
|
use Symfony\Component\Mime\RawMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class SentMessage
|
||||||
|
{
|
||||||
|
private $original;
|
||||||
|
private $raw;
|
||||||
|
private $envelope;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function __construct(RawMessage $message, SmtpEnvelope $envelope)
|
||||||
|
{
|
||||||
|
$this->raw = $message instanceof Message ? new RawMessage($message->toIterable()) : $message;
|
||||||
|
$this->original = $message;
|
||||||
|
$this->envelope = $envelope;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMessage(): RawMessage
|
||||||
|
{
|
||||||
|
return $this->raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOriginalMessage(): RawMessage
|
||||||
|
{
|
||||||
|
return $this->original;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEnvelope(): SmtpEnvelope
|
||||||
|
{
|
||||||
|
return $this->envelope;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toString(): string
|
||||||
|
{
|
||||||
|
return $this->raw->toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toIterable(): iterable
|
||||||
|
{
|
||||||
|
return $this->raw->toIterable();
|
||||||
|
}
|
||||||
|
}
|
114
src/Symfony/Component/Mailer/SmtpEnvelope.php
Normal file
114
src/Symfony/Component/Mailer/SmtpEnvelope.php
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
<?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;
|
||||||
|
|
||||||
|
use Symfony\Component\Mailer\Exception\InvalidArgumentException;
|
||||||
|
use Symfony\Component\Mailer\Exception\LogicException;
|
||||||
|
use Symfony\Component\Mime\Address;
|
||||||
|
use Symfony\Component\Mime\Header\Headers;
|
||||||
|
use Symfony\Component\Mime\Message;
|
||||||
|
use Symfony\Component\Mime\NamedAddress;
|
||||||
|
use Symfony\Component\Mime\RawMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class SmtpEnvelope
|
||||||
|
{
|
||||||
|
private $sender;
|
||||||
|
private $recipients = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Address[] $recipients
|
||||||
|
*/
|
||||||
|
public function __construct(Address $sender, array $recipients)
|
||||||
|
{
|
||||||
|
$this->setSender($sender);
|
||||||
|
$this->setRecipients($recipients);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function create(RawMessage $message): self
|
||||||
|
{
|
||||||
|
if ($message instanceof Message) {
|
||||||
|
$headers = $message->getHeaders();
|
||||||
|
|
||||||
|
return new self(self::getSenderFromHeaders($headers), self::getRecipientsFromHeaders($headers));
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: parse the raw message to create the envelope?
|
||||||
|
throw new InvalidArgumentException(sprintf('Unable to create an SmtpEnvelope from a "%s" message.', RawMessage::class));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSender(Address $sender): void
|
||||||
|
{
|
||||||
|
$this->sender = $sender instanceof NamedAddress ? new Address($sender->getAddress()) : $sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSender(): Address
|
||||||
|
{
|
||||||
|
return $this->sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setRecipients(array $recipients): void
|
||||||
|
{
|
||||||
|
if (!$recipients) {
|
||||||
|
throw new InvalidArgumentException('An envelope must have at least one recipient.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->recipients = [];
|
||||||
|
foreach ($recipients as $recipient) {
|
||||||
|
if ($recipient instanceof NamedAddress) {
|
||||||
|
$recipient = new Address($recipient->getAddress());
|
||||||
|
} elseif (!$recipient instanceof Address) {
|
||||||
|
throw new InvalidArgumentException(sprintf('A recipient must be an instance of "%s" (got "%s").', Address::class, \is_object($recipient) ? \get_class($recipient) : \gettype($recipient)));
|
||||||
|
}
|
||||||
|
$this->recipients[] = $recipient;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Address[]
|
||||||
|
*/
|
||||||
|
public function getRecipients(): array
|
||||||
|
{
|
||||||
|
return $this->recipients;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function getRecipientsFromHeaders(Headers $headers): array
|
||||||
|
{
|
||||||
|
$recipients = [];
|
||||||
|
foreach (['to', 'cc', 'bcc'] as $name) {
|
||||||
|
foreach ($headers->getAll($name) as $header) {
|
||||||
|
$recipients = array_merge($recipients, $header->getAddresses());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $recipients;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function getSenderFromHeaders(Headers $headers): Address
|
||||||
|
{
|
||||||
|
if ($return = $headers->get('Return-Path')) {
|
||||||
|
return $return->getAddress();
|
||||||
|
}
|
||||||
|
if ($sender = $headers->get('Sender')) {
|
||||||
|
return $sender->getAddress();
|
||||||
|
}
|
||||||
|
if ($from = $headers->get('From')) {
|
||||||
|
return $from->getAddresses()[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new LogicException('Unable to determine the sender of the message.');
|
||||||
|
}
|
||||||
|
}
|
36
src/Symfony/Component/Mailer/Tests/SentMessageTest.php
Normal file
36
src/Symfony/Component/Mailer/Tests/SentMessageTest.php
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<?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\Tests;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Mailer\SentMessage;
|
||||||
|
use Symfony\Component\Mailer\SmtpEnvelope;
|
||||||
|
use Symfony\Component\Mime\Address;
|
||||||
|
use Symfony\Component\Mime\Email;
|
||||||
|
use Symfony\Component\Mime\RawMessage;
|
||||||
|
|
||||||
|
class SentMessageTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test()
|
||||||
|
{
|
||||||
|
$m = new SentMessage($r = new RawMessage('Email'), $e = new SmtpEnvelope(new Address('fabien@example.com'), [new Address('helene@example.com')]));
|
||||||
|
$this->assertSame($r, $m->getOriginalMessage());
|
||||||
|
$this->assertSame($r, $m->getMessage());
|
||||||
|
$this->assertSame($e, $m->getEnvelope());
|
||||||
|
$this->assertEquals($r->toString(), $m->toString());
|
||||||
|
$this->assertEquals($r->toIterable(), $m->toIterable());
|
||||||
|
|
||||||
|
$m = new SentMessage($r = (new Email())->from('fabien@example.com')->to('helene@example.com')->text('text'), $e);
|
||||||
|
$this->assertSame($r, $m->getOriginalMessage());
|
||||||
|
$this->assertNotSame($r, $m->getMessage());
|
||||||
|
}
|
||||||
|
}
|
99
src/Symfony/Component/Mailer/Tests/SmtpEnvelopeTest.php
Normal file
99
src/Symfony/Component/Mailer/Tests/SmtpEnvelopeTest.php
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<?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\Tests;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Mailer\SmtpEnvelope;
|
||||||
|
use Symfony\Component\Mime\Address;
|
||||||
|
use Symfony\Component\Mime\Header\Headers;
|
||||||
|
use Symfony\Component\Mime\Message;
|
||||||
|
use Symfony\Component\Mime\NamedAddress;
|
||||||
|
use Symfony\Component\Mime\RawMessage;
|
||||||
|
|
||||||
|
class SmtpEnvelopeTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testConstructorWithAddressSender()
|
||||||
|
{
|
||||||
|
$e = new SmtpEnvelope(new Address('fabien@symfony.com'), [new Address('thomas@symfony.com')]);
|
||||||
|
$this->assertEquals(new Address('fabien@symfony.com'), $e->getSender());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testConstructorWithNamedAddressSender()
|
||||||
|
{
|
||||||
|
$e = new SmtpEnvelope(new NamedAddress('fabien@symfony.com', 'Fabien'), [new Address('thomas@symfony.com')]);
|
||||||
|
$this->assertEquals(new Address('fabien@symfony.com'), $e->getSender());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testConstructorWithAddressRecipients()
|
||||||
|
{
|
||||||
|
$e = new SmtpEnvelope(new Address('fabien@symfony.com'), [new Address('thomas@symfony.com'), new NamedAddress('lucas@symfony.com', 'Lucas')]);
|
||||||
|
$this->assertEquals([new Address('thomas@symfony.com'), new Address('lucas@symfony.com')], $e->getRecipients());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testConstructorWithNoRecipients()
|
||||||
|
{
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
$e = new SmtpEnvelope(new Address('fabien@symfony.com'), []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testConstructorWithWrongRecipients()
|
||||||
|
{
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
$e = new SmtpEnvelope(new Address('fabien@symfony.com'), ['lucas@symfony.com']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSenderFromHeaders()
|
||||||
|
{
|
||||||
|
$headers = new Headers();
|
||||||
|
$headers->addPathHeader('Return-Path', 'return@symfony.com');
|
||||||
|
$headers->addMailboxListHeader('To', ['from@symfony.com']);
|
||||||
|
$e = SmtpEnvelope::create(new Message($headers));
|
||||||
|
$this->assertEquals('return@symfony.com', $e->getSender()->getAddress());
|
||||||
|
|
||||||
|
$headers = new Headers();
|
||||||
|
$headers->addMailboxHeader('Sender', 'sender@symfony.com');
|
||||||
|
$headers->addMailboxListHeader('To', ['from@symfony.com']);
|
||||||
|
$e = SmtpEnvelope::create(new Message($headers));
|
||||||
|
$this->assertEquals('sender@symfony.com', $e->getSender()->getAddress());
|
||||||
|
|
||||||
|
$headers = new Headers();
|
||||||
|
$headers->addMailboxListHeader('From', ['from@symfony.com', 'some@symfony.com']);
|
||||||
|
$headers->addMailboxListHeader('To', ['from@symfony.com']);
|
||||||
|
$e = SmtpEnvelope::create(new Message($headers));
|
||||||
|
$this->assertEquals('from@symfony.com', $e->getSender()->getAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSenderFromHeadersWithoutData()
|
||||||
|
{
|
||||||
|
$this->expectException(\LogicException::class);
|
||||||
|
$headers = new Headers();
|
||||||
|
$headers->addMailboxListHeader('To', ['from@symfony.com']);
|
||||||
|
SmtpEnvelope::create(new Message($headers));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRecipientsFromHeaders()
|
||||||
|
{
|
||||||
|
$headers = new Headers();
|
||||||
|
$headers->addPathHeader('Return-Path', 'return@symfony.com');
|
||||||
|
$headers->addMailboxListHeader('To', ['to@symfony.com']);
|
||||||
|
$headers->addMailboxListHeader('Cc', ['cc@symfony.com']);
|
||||||
|
$headers->addMailboxListHeader('Bcc', ['bcc@symfony.com']);
|
||||||
|
$e = SmtpEnvelope::create(new Message($headers));
|
||||||
|
$this->assertEquals([new Address('to@symfony.com'), new Address('cc@symfony.com'), new Address('bcc@symfony.com')], $e->getRecipients());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCreateWithRawMessage()
|
||||||
|
{
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
SmtpEnvelope::create(new RawMessage(''));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
<?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\Tests\Transport;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Mailer\SmtpEnvelope;
|
||||||
|
use Symfony\Component\Mailer\Transport\NullTransport;
|
||||||
|
use Symfony\Component\Mime\Address;
|
||||||
|
use Symfony\Component\Mime\RawMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @group time-sensitive
|
||||||
|
*/
|
||||||
|
class AbstractTransportTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testThrottling()
|
||||||
|
{
|
||||||
|
$transport = new NullTransport();
|
||||||
|
$transport->setMaxPerSecond(2 / 10);
|
||||||
|
$message = new RawMessage('');
|
||||||
|
$envelope = new SmtpEnvelope(new Address('fabien@example.com'), [new Address('helene@example.com')]);
|
||||||
|
|
||||||
|
$start = time();
|
||||||
|
$transport->send($message, $envelope);
|
||||||
|
$this->assertEquals(0, time() - $start, '', 1);
|
||||||
|
$transport->send($message, $envelope);
|
||||||
|
$this->assertEquals(5, time() - $start, '', 1);
|
||||||
|
$transport->send($message, $envelope);
|
||||||
|
$this->assertEquals(10, time() - $start, '', 1);
|
||||||
|
$transport->send($message, $envelope);
|
||||||
|
$this->assertEquals(15, time() - $start, '', 1);
|
||||||
|
|
||||||
|
$start = time();
|
||||||
|
$transport->setMaxPerSecond(-3);
|
||||||
|
$transport->send($message, $envelope);
|
||||||
|
$this->assertEquals(0, time() - $start, '', 1);
|
||||||
|
$transport->send($message, $envelope);
|
||||||
|
$this->assertEquals(0, time() - $start, '', 1);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
<?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\Tests\Transport;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Mailer\Exception\TransportException;
|
||||||
|
use Symfony\Component\Mailer\Transport\FailoverTransport;
|
||||||
|
use Symfony\Component\Mailer\Transport\TransportInterface;
|
||||||
|
use Symfony\Component\Mime\RawMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @group time-sensitive
|
||||||
|
*/
|
||||||
|
class FailoverTransportTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testSendNoTransports()
|
||||||
|
{
|
||||||
|
$this->expectException(TransportException::class);
|
||||||
|
new FailoverTransport([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSendFirstWork()
|
||||||
|
{
|
||||||
|
$t1 = $this->createMock(TransportInterface::class);
|
||||||
|
$t1->expects($this->exactly(3))->method('send');
|
||||||
|
$t2 = $this->createMock(TransportInterface::class);
|
||||||
|
$t2->expects($this->never())->method('send');
|
||||||
|
$t = new FailoverTransport([$t1, $t2]);
|
||||||
|
$t->send(new RawMessage(''));
|
||||||
|
$t->send(new RawMessage(''));
|
||||||
|
$t->send(new RawMessage(''));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSendAllDead()
|
||||||
|
{
|
||||||
|
$t1 = $this->createMock(TransportInterface::class);
|
||||||
|
$t1->expects($this->once())->method('send')->will($this->throwException(new TransportException()));
|
||||||
|
$t2 = $this->createMock(TransportInterface::class);
|
||||||
|
$t2->expects($this->once())->method('send')->will($this->throwException(new TransportException()));
|
||||||
|
$t = new FailoverTransport([$t1, $t2]);
|
||||||
|
$this->expectException(TransportException::class);
|
||||||
|
$this->expectExceptionMessage('All transports failed.');
|
||||||
|
$t->send(new RawMessage(''));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSendOneDead()
|
||||||
|
{
|
||||||
|
$t1 = $this->createMock(TransportInterface::class);
|
||||||
|
$t1->expects($this->once())->method('send')->will($this->throwException(new TransportException()));
|
||||||
|
$t2 = $this->createMock(TransportInterface::class);
|
||||||
|
$t2->expects($this->exactly(3))->method('send');
|
||||||
|
$t = new FailoverTransport([$t1, $t2]);
|
||||||
|
$t->send(new RawMessage(''));
|
||||||
|
$t->send(new RawMessage(''));
|
||||||
|
$t->send(new RawMessage(''));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSendOneDeadButRecover()
|
||||||
|
{
|
||||||
|
$t1 = $this->createMock(TransportInterface::class);
|
||||||
|
$t1->expects($this->at(0))->method('send')->will($this->throwException(new TransportException()));
|
||||||
|
$t1->expects($this->at(1))->method('send');
|
||||||
|
$t2 = $this->createMock(TransportInterface::class);
|
||||||
|
$t2->expects($this->at(0))->method('send');
|
||||||
|
$t2->expects($this->at(1))->method('send');
|
||||||
|
$t2->expects($this->at(2))->method('send')->will($this->throwException(new TransportException()));
|
||||||
|
$t = new FailoverTransport([$t1, $t2], 1);
|
||||||
|
$t->send(new RawMessage(''));
|
||||||
|
sleep(1);
|
||||||
|
$t->send(new RawMessage(''));
|
||||||
|
sleep(1);
|
||||||
|
$t->send(new RawMessage(''));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
<?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\Tests\Transport;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Mailer\Exception\TransportException;
|
||||||
|
use Symfony\Component\Mailer\Transport\RoundRobinTransport;
|
||||||
|
use Symfony\Component\Mailer\Transport\TransportInterface;
|
||||||
|
use Symfony\Component\Mime\RawMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @group time-sensitive
|
||||||
|
*/
|
||||||
|
class RoundRobinTransportTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testSendNoTransports()
|
||||||
|
{
|
||||||
|
$this->expectException(TransportException::class);
|
||||||
|
new RoundRobinTransport([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSendAlternate()
|
||||||
|
{
|
||||||
|
$t1 = $this->createMock(TransportInterface::class);
|
||||||
|
$t1->expects($this->exactly(2))->method('send');
|
||||||
|
$t2 = $this->createMock(TransportInterface::class);
|
||||||
|
$t2->expects($this->once())->method('send');
|
||||||
|
$t = new RoundRobinTransport([$t1, $t2]);
|
||||||
|
$t->send(new RawMessage(''));
|
||||||
|
$t->send(new RawMessage(''));
|
||||||
|
$t->send(new RawMessage(''));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSendAllDead()
|
||||||
|
{
|
||||||
|
$t1 = $this->createMock(TransportInterface::class);
|
||||||
|
$t1->expects($this->once())->method('send')->will($this->throwException(new TransportException()));
|
||||||
|
$t2 = $this->createMock(TransportInterface::class);
|
||||||
|
$t2->expects($this->once())->method('send')->will($this->throwException(new TransportException()));
|
||||||
|
$t = new RoundRobinTransport([$t1, $t2]);
|
||||||
|
$this->expectException(TransportException::class);
|
||||||
|
$this->expectExceptionMessage('All transports failed.');
|
||||||
|
$t->send(new RawMessage(''));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSendOneDead()
|
||||||
|
{
|
||||||
|
$t1 = $this->createMock(TransportInterface::class);
|
||||||
|
$t1->expects($this->once())->method('send')->will($this->throwException(new TransportException()));
|
||||||
|
$t2 = $this->createMock(TransportInterface::class);
|
||||||
|
$t2->expects($this->exactly(3))->method('send');
|
||||||
|
$t = new RoundRobinTransport([$t1, $t2]);
|
||||||
|
$t->send(new RawMessage(''));
|
||||||
|
$t->send(new RawMessage(''));
|
||||||
|
$t->send(new RawMessage(''));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSendOneDeadButRecover()
|
||||||
|
{
|
||||||
|
$t1 = $this->createMock(TransportInterface::class);
|
||||||
|
$t1->expects($this->at(0))->method('send')->will($this->throwException(new TransportException()));
|
||||||
|
$t1->expects($this->at(1))->method('send');
|
||||||
|
$t2 = $this->createMock(TransportInterface::class);
|
||||||
|
$t2->expects($this->once())->method('send');
|
||||||
|
$t = new RoundRobinTransport([$t1, $t2], 1);
|
||||||
|
$t->send(new RawMessage(''));
|
||||||
|
sleep(2);
|
||||||
|
$t->send(new RawMessage(''));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
<?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\Tests\Transport\Smtp\Stream;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Mailer\Transport\Smtp\Stream\AbstractStream;
|
||||||
|
|
||||||
|
class AbstractStreamTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @dataProvider provideReplace
|
||||||
|
*/
|
||||||
|
public function testReplace(string $expected, string $from, string $to, array $chunks)
|
||||||
|
{
|
||||||
|
$result = '';
|
||||||
|
foreach (AbstractStream::replace($from, $to, $chunks) as $chunk) {
|
||||||
|
$result .= $chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertSame($expected, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideReplace()
|
||||||
|
{
|
||||||
|
yield ['ca', 'ab', 'c', ['a', 'b', 'a']];
|
||||||
|
yield ['ac', 'ab', 'c', ['a', 'ab']];
|
||||||
|
yield ['cbc', 'aba', 'c', ['ababa', 'ba']];
|
||||||
|
}
|
||||||
|
}
|
283
src/Symfony/Component/Mailer/Tests/TransportTest.php
Normal file
283
src/Symfony/Component/Mailer/Tests/TransportTest.php
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
<?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\Tests;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\Mailer\Bridge\Amazon;
|
||||||
|
use Symfony\Component\Mailer\Bridge\Google;
|
||||||
|
use Symfony\Component\Mailer\Bridge\Mailchimp;
|
||||||
|
use Symfony\Component\Mailer\Bridge\Mailgun;
|
||||||
|
use Symfony\Component\Mailer\Bridge\Postmark;
|
||||||
|
use Symfony\Component\Mailer\Bridge\Sendgrid;
|
||||||
|
use Symfony\Component\Mailer\Exception\InvalidArgumentException;
|
||||||
|
use Symfony\Component\Mailer\Exception\LogicException;
|
||||||
|
use Symfony\Component\Mailer\Transport;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
|
||||||
|
class TransportTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testFromDsnNull()
|
||||||
|
{
|
||||||
|
$dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||||
|
$logger = $this->createMock(LoggerInterface::class);
|
||||||
|
$transport = Transport::fromDsn('smtp://null', $dispatcher, null, $logger);
|
||||||
|
$this->assertInstanceOf(Transport\NullTransport::class, $transport);
|
||||||
|
$p = new \ReflectionProperty(Transport\AbstractTransport::class, 'dispatcher');
|
||||||
|
$p->setAccessible(true);
|
||||||
|
$this->assertSame($dispatcher, $p->getValue($transport));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFromDsnSendmail()
|
||||||
|
{
|
||||||
|
$dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||||
|
$logger = $this->createMock(LoggerInterface::class);
|
||||||
|
$transport = Transport::fromDsn('smtp://sendmail', $dispatcher, null, $logger);
|
||||||
|
$this->assertInstanceOf(Transport\SendmailTransport::class, $transport);
|
||||||
|
$p = new \ReflectionProperty(Transport\AbstractTransport::class, 'dispatcher');
|
||||||
|
$p->setAccessible(true);
|
||||||
|
$this->assertSame($dispatcher, $p->getValue($transport));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFromDsnSmtp()
|
||||||
|
{
|
||||||
|
$dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||||
|
$logger = $this->createMock(LoggerInterface::class);
|
||||||
|
$transport = Transport::fromDsn('smtp://localhost:44?auth_mode=plain&encryption=tls', $dispatcher, null, $logger);
|
||||||
|
$this->assertInstanceOf(Transport\Smtp\SmtpTransport::class, $transport);
|
||||||
|
$this->assertProperties($transport, $dispatcher, $logger);
|
||||||
|
$this->assertEquals('localhost', $transport->getStream()->getHost());
|
||||||
|
$this->assertEquals('plain', $transport->getAuthMode());
|
||||||
|
$this->assertTrue($transport->getStream()->isTLS());
|
||||||
|
$this->assertEquals(44, $transport->getStream()->getPort());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFromInvalidDsn()
|
||||||
|
{
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
$this->expectExceptionMessage('The "some://" mailer DSN is invalid.');
|
||||||
|
Transport::fromDsn('some://');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFromInvalidDsnNoHost()
|
||||||
|
{
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
$this->expectExceptionMessage('The "?!" mailer DSN must contain a mailer name.');
|
||||||
|
Transport::fromDsn('?!');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFromInvalidTransportName()
|
||||||
|
{
|
||||||
|
$this->expectException(LogicException::class);
|
||||||
|
Transport::fromDsn('api://foobar');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFromDsnGmail()
|
||||||
|
{
|
||||||
|
$dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||||
|
$logger = $this->createMock(LoggerInterface::class);
|
||||||
|
$transport = Transport::fromDsn('smtp://'.urlencode('u$er').':'.urlencode('pa$s').'@gmail', $dispatcher, null, $logger);
|
||||||
|
$this->assertInstanceOf(Google\Smtp\GmailTransport::class, $transport);
|
||||||
|
$this->assertEquals('u$er', $transport->getUsername());
|
||||||
|
$this->assertEquals('pa$s', $transport->getPassword());
|
||||||
|
$this->assertProperties($transport, $dispatcher, $logger);
|
||||||
|
|
||||||
|
$this->expectException(LogicException::class);
|
||||||
|
Transport::fromDsn('http://gmail');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFromDsnMailgun()
|
||||||
|
{
|
||||||
|
$dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||||
|
$logger = $this->createMock(LoggerInterface::class);
|
||||||
|
$transport = Transport::fromDsn('smtp://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun', $dispatcher, null, $logger);
|
||||||
|
$this->assertInstanceOf(Mailgun\Smtp\MailgunTransport::class, $transport);
|
||||||
|
$this->assertEquals('u$er', $transport->getUsername());
|
||||||
|
$this->assertEquals('pa$s', $transport->getPassword());
|
||||||
|
$this->assertProperties($transport, $dispatcher, $logger);
|
||||||
|
|
||||||
|
$client = $this->createMock(HttpClientInterface::class);
|
||||||
|
$transport = Transport::fromDsn('http://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun', $dispatcher, $client, $logger);
|
||||||
|
$this->assertInstanceOf(Mailgun\Http\MailgunTransport::class, $transport);
|
||||||
|
$this->assertProperties($transport, $dispatcher, $logger, [
|
||||||
|
'key' => 'u$er',
|
||||||
|
'domain' => 'pa$s',
|
||||||
|
'client' => $client,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$transport = Transport::fromDsn('api://'.urlencode('u$er').':'.urlencode('pa$s').'@mailgun', $dispatcher, $client, $logger);
|
||||||
|
$this->assertInstanceOf(Mailgun\Http\Api\MailgunTransport::class, $transport);
|
||||||
|
$this->assertProperties($transport, $dispatcher, $logger, [
|
||||||
|
'key' => 'u$er',
|
||||||
|
'domain' => 'pa$s',
|
||||||
|
'client' => $client,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->expectException(LogicException::class);
|
||||||
|
Transport::fromDsn('foo://mailgun');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFromDsnPostmark()
|
||||||
|
{
|
||||||
|
$dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||||
|
$logger = $this->createMock(LoggerInterface::class);
|
||||||
|
$transport = Transport::fromDsn('smtp://'.urlencode('u$er').'@postmark', $dispatcher, null, $logger);
|
||||||
|
$this->assertInstanceOf(Postmark\Smtp\PostmarkTransport::class, $transport);
|
||||||
|
$this->assertEquals('u$er', $transport->getUsername());
|
||||||
|
$this->assertEquals('u$er', $transport->getPassword());
|
||||||
|
$this->assertProperties($transport, $dispatcher, $logger);
|
||||||
|
|
||||||
|
$client = $this->createMock(HttpClientInterface::class);
|
||||||
|
$transport = Transport::fromDsn('api://'.urlencode('u$er').'@postmark', $dispatcher, $client, $logger);
|
||||||
|
$this->assertInstanceOf(Postmark\Http\Api\PostmarkTransport::class, $transport);
|
||||||
|
$this->assertProperties($transport, $dispatcher, $logger, [
|
||||||
|
'key' => 'u$er',
|
||||||
|
'client' => $client,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->expectException(LogicException::class);
|
||||||
|
Transport::fromDsn('http://postmark');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFromDsnSendgrid()
|
||||||
|
{
|
||||||
|
$dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||||
|
$logger = $this->createMock(LoggerInterface::class);
|
||||||
|
$transport = Transport::fromDsn('smtp://'.urlencode('u$er').'@sendgrid', $dispatcher, null, $logger);
|
||||||
|
$this->assertInstanceOf(Sendgrid\Smtp\SendgridTransport::class, $transport);
|
||||||
|
$this->assertEquals('apikey', $transport->getUsername());
|
||||||
|
$this->assertEquals('u$er', $transport->getPassword());
|
||||||
|
$this->assertProperties($transport, $dispatcher, $logger);
|
||||||
|
|
||||||
|
$client = $this->createMock(HttpClientInterface::class);
|
||||||
|
$transport = Transport::fromDsn('api://'.urlencode('u$er').'@sendgrid', $dispatcher, $client, $logger);
|
||||||
|
$this->assertInstanceOf(Sendgrid\Http\Api\SendgridTransport::class, $transport);
|
||||||
|
$this->assertProperties($transport, $dispatcher, $logger, [
|
||||||
|
'key' => 'u$er',
|
||||||
|
'client' => $client,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->expectException(LogicException::class);
|
||||||
|
Transport::fromDsn('http://sendgrid');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFromDsnAmazonSes()
|
||||||
|
{
|
||||||
|
$dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||||
|
$logger = $this->createMock(LoggerInterface::class);
|
||||||
|
$transport = Transport::fromDsn('smtp://'.urlencode('u$er').':'.urlencode('pa$s').'@ses?region=sun', $dispatcher, null, $logger);
|
||||||
|
$this->assertInstanceOf(Amazon\Smtp\SesTransport::class, $transport);
|
||||||
|
$this->assertEquals('u$er', $transport->getUsername());
|
||||||
|
$this->assertEquals('pa$s', $transport->getPassword());
|
||||||
|
$this->assertContains('.sun.', $transport->getStream()->getHost());
|
||||||
|
$this->assertProperties($transport, $dispatcher, $logger);
|
||||||
|
|
||||||
|
$client = $this->createMock(HttpClientInterface::class);
|
||||||
|
$transport = Transport::fromDsn('http://'.urlencode('u$er').':'.urlencode('pa$s').'@ses?region=sun', $dispatcher, $client, $logger);
|
||||||
|
$this->assertInstanceOf(Amazon\Http\SesTransport::class, $transport);
|
||||||
|
$this->assertProperties($transport, $dispatcher, $logger, [
|
||||||
|
'accessKey' => 'u$er',
|
||||||
|
'secretKey' => 'pa$s',
|
||||||
|
'region' => 'sun',
|
||||||
|
'client' => $client,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$transport = Transport::fromDsn('api://'.urlencode('u$er').':'.urlencode('pa$s').'@ses?region=sun', $dispatcher, $client, $logger);
|
||||||
|
$this->assertInstanceOf(Amazon\Http\Api\SesTransport::class, $transport);
|
||||||
|
$this->assertProperties($transport, $dispatcher, $logger, [
|
||||||
|
'accessKey' => 'u$er',
|
||||||
|
'secretKey' => 'pa$s',
|
||||||
|
'region' => 'sun',
|
||||||
|
'client' => $client,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->expectException(LogicException::class);
|
||||||
|
Transport::fromDsn('foo://ses');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFromDsnMailchimp()
|
||||||
|
{
|
||||||
|
$dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||||
|
$logger = $this->createMock(LoggerInterface::class);
|
||||||
|
$transport = Transport::fromDsn('smtp://'.urlencode('u$er').':'.urlencode('pa$s').'@mandrill', $dispatcher, null, $logger);
|
||||||
|
$this->assertInstanceOf(Mailchimp\Smtp\MandrillTransport::class, $transport);
|
||||||
|
$this->assertEquals('u$er', $transport->getUsername());
|
||||||
|
$this->assertEquals('pa$s', $transport->getPassword());
|
||||||
|
$this->assertProperties($transport, $dispatcher, $logger);
|
||||||
|
|
||||||
|
$client = $this->createMock(HttpClientInterface::class);
|
||||||
|
$transport = Transport::fromDsn('http://'.urlencode('u$er').'@mandrill', $dispatcher, $client, $logger);
|
||||||
|
$this->assertInstanceOf(Mailchimp\Http\MandrillTransport::class, $transport);
|
||||||
|
$this->assertProperties($transport, $dispatcher, $logger, [
|
||||||
|
'key' => 'u$er',
|
||||||
|
'client' => $client,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$transport = Transport::fromDsn('api://'.urlencode('u$er').'@mandrill', $dispatcher, $client, $logger);
|
||||||
|
$this->assertInstanceOf(Mailchimp\Http\Api\MandrillTransport::class, $transport);
|
||||||
|
$this->assertProperties($transport, $dispatcher, $logger, [
|
||||||
|
'key' => 'u$er',
|
||||||
|
'client' => $client,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->expectException(LogicException::class);
|
||||||
|
Transport::fromDsn('foo://mandrill');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFromDsnFailover()
|
||||||
|
{
|
||||||
|
$dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||||
|
$logger = $this->createMock(LoggerInterface::class);
|
||||||
|
$transport = Transport::fromDsn('smtp://null || smtp://null || smtp://null', $dispatcher, null, $logger);
|
||||||
|
$this->assertInstanceOf(Transport\FailoverTransport::class, $transport);
|
||||||
|
$p = new \ReflectionProperty(Transport\RoundRobinTransport::class, 'transports');
|
||||||
|
$p->setAccessible(true);
|
||||||
|
$transports = $p->getValue($transport);
|
||||||
|
$this->assertCount(3, $transports);
|
||||||
|
foreach ($transports as $transport) {
|
||||||
|
$this->assertProperties($transport, $dispatcher, $logger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFromDsnRoundRobin()
|
||||||
|
{
|
||||||
|
$dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||||
|
$logger = $this->createMock(LoggerInterface::class);
|
||||||
|
$transport = Transport::fromDsn('smtp://null && smtp://null && smtp://null', $dispatcher, null, $logger);
|
||||||
|
$this->assertInstanceOf(Transport\RoundRobinTransport::class, $transport);
|
||||||
|
$p = new \ReflectionProperty(Transport\RoundRobinTransport::class, 'transports');
|
||||||
|
$p->setAccessible(true);
|
||||||
|
$transports = $p->getValue($transport);
|
||||||
|
$this->assertCount(3, $transports);
|
||||||
|
foreach ($transports as $transport) {
|
||||||
|
$this->assertProperties($transport, $dispatcher, $logger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function assertProperties(Transport\TransportInterface $transport, EventDispatcherInterface $dispatcher, LoggerInterface $logger, array $props = [])
|
||||||
|
{
|
||||||
|
$p = new \ReflectionProperty(Transport\AbstractTransport::class, 'dispatcher');
|
||||||
|
$p->setAccessible(true);
|
||||||
|
$this->assertSame($dispatcher, $p->getValue($transport));
|
||||||
|
|
||||||
|
$p = new \ReflectionProperty(Transport\AbstractTransport::class, 'logger');
|
||||||
|
$p->setAccessible(true);
|
||||||
|
$this->assertSame($logger, $p->getValue($transport));
|
||||||
|
|
||||||
|
foreach ($props as $prop => $value) {
|
||||||
|
$p = new \ReflectionProperty($transport, $prop);
|
||||||
|
$p->setAccessible(true);
|
||||||
|
$this->assertEquals($value, $p->getValue($transport));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
180
src/Symfony/Component/Mailer/Transport.php
Normal file
180
src/Symfony/Component/Mailer/Transport.php
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
<?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;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\Mailer\Bridge\Amazon;
|
||||||
|
use Symfony\Component\Mailer\Bridge\Google;
|
||||||
|
use Symfony\Component\Mailer\Bridge\Mailchimp;
|
||||||
|
use Symfony\Component\Mailer\Bridge\Mailgun;
|
||||||
|
use Symfony\Component\Mailer\Bridge\Postmark;
|
||||||
|
use Symfony\Component\Mailer\Bridge\Sendgrid;
|
||||||
|
use Symfony\Component\Mailer\Exception\InvalidArgumentException;
|
||||||
|
use Symfony\Component\Mailer\Exception\LogicException;
|
||||||
|
use Symfony\Component\Mailer\Transport\TransportInterface;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class Transport
|
||||||
|
{
|
||||||
|
public static function fromDsn(string $dsn, EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null, LoggerInterface $logger = null): TransportInterface
|
||||||
|
{
|
||||||
|
// failover?
|
||||||
|
$dsns = preg_split('/\s++\|\|\s++/', $dsn);
|
||||||
|
if (\count($dsns) > 1) {
|
||||||
|
$transports = [];
|
||||||
|
foreach ($dsns as $dsn) {
|
||||||
|
$transports[] = self::createTransport($dsn, $dispatcher, $client, $logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Transport\FailoverTransport($transports);
|
||||||
|
}
|
||||||
|
|
||||||
|
// round robin?
|
||||||
|
$dsns = preg_split('/\s++&&\s++/', $dsn);
|
||||||
|
if (\count($dsns) > 1) {
|
||||||
|
$transports = [];
|
||||||
|
foreach ($dsns as $dsn) {
|
||||||
|
$transports[] = self::createTransport($dsn, $dispatcher, $client, $logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Transport\RoundRobinTransport($transports);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::createTransport($dsn, $dispatcher, $client, $logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function createTransport(string $dsn, EventDispatcherInterface $dispatcher = null, HttpClientInterface $client = null, LoggerInterface $logger = null): TransportInterface
|
||||||
|
{
|
||||||
|
if (false === $parsedDsn = parse_url($dsn)) {
|
||||||
|
throw new InvalidArgumentException(sprintf('The "%s" mailer DSN is invalid.', $dsn));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($parsedDsn['host'])) {
|
||||||
|
throw new InvalidArgumentException(sprintf('The "%s" mailer DSN must contain a mailer name.', $dsn));
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = \urldecode($parsedDsn['user'] ?? '');
|
||||||
|
$pass = \urldecode($parsedDsn['pass'] ?? '');
|
||||||
|
\parse_str($parsedDsn['query'] ?? '', $query);
|
||||||
|
|
||||||
|
switch ($parsedDsn['host']) {
|
||||||
|
case 'null':
|
||||||
|
if ('smtp' === $parsedDsn['scheme']) {
|
||||||
|
return new Transport\NullTransport($dispatcher, $logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new LogicException(sprintf('The "%s" scheme is not supported for mailer "%s".', $parsedDsn['scheme'], $parsedDsn['host']));
|
||||||
|
case 'sendmail':
|
||||||
|
if ('smtp' === $parsedDsn['scheme']) {
|
||||||
|
return new Transport\SendmailTransport(null, $dispatcher, $logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new LogicException(sprintf('The "%s" scheme is not supported for mailer "%s".', $parsedDsn['scheme'], $parsedDsn['host']));
|
||||||
|
case 'gmail':
|
||||||
|
if (!class_exists(Google\Smtp\GmailTransport::class)) {
|
||||||
|
throw new \LogicException('Unable to send emails via Gmail as the Google bridge is not installed. Try running "composer require symfony/google-bridge".');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('smtp' === $parsedDsn['scheme']) {
|
||||||
|
return new Google\Smtp\GmailTransport($user, $pass, $dispatcher, $logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new LogicException(sprintf('The "%s" scheme is not supported for mailer "%s".', $parsedDsn['scheme'], $parsedDsn['host']));
|
||||||
|
case 'mailgun':
|
||||||
|
if (!class_exists(Mailgun\Smtp\MailgunTransport::class)) {
|
||||||
|
throw new \LogicException('Unable to send emails via Mailgun as the bridge is not installed. Try running "composer require symfony/mailgun-bridge".');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('smtp' === $parsedDsn['scheme']) {
|
||||||
|
return new Mailgun\Smtp\MailgunTransport($user, $pass, $dispatcher, $logger);
|
||||||
|
}
|
||||||
|
if ('http' === $parsedDsn['scheme']) {
|
||||||
|
return new Mailgun\Http\MailgunTransport($user, $pass, $client, $dispatcher, $logger);
|
||||||
|
}
|
||||||
|
if ('api' === $parsedDsn['scheme']) {
|
||||||
|
return new Mailgun\Http\Api\MailgunTransport($user, $pass, $client, $dispatcher, $logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new LogicException(sprintf('The "%s" scheme is not supported for mailer "%s".', $parsedDsn['scheme'], $parsedDsn['host']));
|
||||||
|
case 'postmark':
|
||||||
|
if (!class_exists(Postmark\Smtp\PostmarkTransport::class)) {
|
||||||
|
throw new \LogicException('Unable to send emails via Postmark as the bridge is not installed. Try running "composer require symfony/postmark-bridge".');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('smtp' === $parsedDsn['scheme']) {
|
||||||
|
return new Postmark\Smtp\PostmarkTransport($user, $dispatcher, $logger);
|
||||||
|
}
|
||||||
|
if ('api' === $parsedDsn['scheme']) {
|
||||||
|
return new Postmark\Http\Api\PostmarkTransport($user, $client, $dispatcher, $logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new LogicException(sprintf('The "%s" scheme is not supported for mailer "%s".', $parsedDsn['scheme'], $parsedDsn['host']));
|
||||||
|
case 'sendgrid':
|
||||||
|
if (!class_exists(Sendgrid\Smtp\SendgridTransport::class)) {
|
||||||
|
throw new \LogicException('Unable to send emails via Sendgrid as the bridge is not installed. Try running "composer require symfony/sendgrid-bridge".');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('smtp' === $parsedDsn['scheme']) {
|
||||||
|
return new Sendgrid\Smtp\SendgridTransport($user, $dispatcher, $logger);
|
||||||
|
}
|
||||||
|
if ('api' === $parsedDsn['scheme']) {
|
||||||
|
return new Sendgrid\Http\Api\SendgridTransport($user, $client, $dispatcher, $logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new LogicException(sprintf('The "%s" scheme is not supported for mailer "%s".', $parsedDsn['scheme'], $parsedDsn['host']));
|
||||||
|
case 'ses':
|
||||||
|
if (!class_exists(Amazon\Smtp\SesTransport::class)) {
|
||||||
|
throw new \LogicException('Unable to send emails via Amazon SES as the bridge is not installed. Try running "composer require symfony/amazon-bridge".');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('smtp' === $parsedDsn['scheme']) {
|
||||||
|
return new Amazon\Smtp\SesTransport($user, $pass, $query['region'] ?? null, $dispatcher, $logger);
|
||||||
|
}
|
||||||
|
if ('api' === $parsedDsn['scheme']) {
|
||||||
|
return new Amazon\Http\Api\SesTransport($user, $pass, $query['region'] ?? null, $client, $dispatcher, $logger);
|
||||||
|
}
|
||||||
|
if ('http' === $parsedDsn['scheme']) {
|
||||||
|
return new Amazon\Http\SesTransport($user, $pass, $query['region'] ?? null, $client, $dispatcher, $logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new LogicException(sprintf('The "%s" scheme is not supported for mailer "%s".', $parsedDsn['scheme'], $parsedDsn['host']));
|
||||||
|
case 'mandrill':
|
||||||
|
if (!class_exists(Mailchimp\Smtp\MandrillTransport::class)) {
|
||||||
|
throw new \LogicException('Unable to send emails via Mandrill as the bridge is not installed. Try running "composer require symfony/mailchimp-bridge".');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('smtp' === $parsedDsn['scheme']) {
|
||||||
|
return new Mailchimp\Smtp\MandrillTransport($user, $pass, $dispatcher, $logger);
|
||||||
|
}
|
||||||
|
if ('api' === $parsedDsn['scheme']) {
|
||||||
|
return new Mailchimp\Http\Api\MandrillTransport($user, $client, $dispatcher, $logger);
|
||||||
|
}
|
||||||
|
if ('http' === $parsedDsn['scheme']) {
|
||||||
|
return new Mailchimp\Http\MandrillTransport($user, $client, $dispatcher, $logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new LogicException(sprintf('The "%s" scheme is not supported for mailer "%s".', $parsedDsn['scheme'], $parsedDsn['host']));
|
||||||
|
default:
|
||||||
|
if ('smtp' === $parsedDsn['scheme']) {
|
||||||
|
return new Transport\Smtp\EsmtpTransport($parsedDsn['host'], $parsedDsn['port'] ?? 25, $query['encryption'] ?? null, $query['auth_mode'] ?? null, $dispatcher, $logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new LogicException(sprintf('The "%s" mailer is not supported.', $parsedDsn['host']));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
118
src/Symfony/Component/Mailer/Transport/AbstractTransport.php
Normal file
118
src/Symfony/Component/Mailer/Transport/AbstractTransport.php
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
<?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\Transport;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Psr\Log\NullLogger;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\Mailer\Event\MessageEvent;
|
||||||
|
use Symfony\Component\Mailer\Exception\TransportException;
|
||||||
|
use Symfony\Component\Mailer\SentMessage;
|
||||||
|
use Symfony\Component\Mailer\SmtpEnvelope;
|
||||||
|
use Symfony\Component\Mime\Address;
|
||||||
|
use Symfony\Component\Mime\RawMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
abstract class AbstractTransport implements TransportInterface
|
||||||
|
{
|
||||||
|
private $dispatcher;
|
||||||
|
private $logger;
|
||||||
|
private $rate = 0;
|
||||||
|
private $lastSent = 0;
|
||||||
|
|
||||||
|
public function __construct(EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
|
||||||
|
{
|
||||||
|
$this->dispatcher = $dispatcher ?: new EventDispatcher();
|
||||||
|
$this->logger = $logger ?: new NullLogger();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the maximum number of messages to send per second (0 to disable).
|
||||||
|
*/
|
||||||
|
public function setMaxPerSecond(float $rate): self
|
||||||
|
{
|
||||||
|
if (0 >= $rate) {
|
||||||
|
$rate = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->rate = $rate;
|
||||||
|
$this->lastSent = 0;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function send(RawMessage $message, SmtpEnvelope $envelope = null): ?SentMessage
|
||||||
|
{
|
||||||
|
$message = clone $message;
|
||||||
|
if (null !== $envelope) {
|
||||||
|
$envelope = clone $envelope;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
$envelope = SmtpEnvelope::create($message);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
throw new TransportException('Cannot send message without a valid envelope.', 0, $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$event = new MessageEvent($message, $envelope);
|
||||||
|
$this->dispatcher->dispatch($event);
|
||||||
|
$envelope = $event->getEnvelope();
|
||||||
|
if (!$envelope->getRecipients()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$message = new SentMessage($event->getMessage(), $envelope);
|
||||||
|
$this->doSend($message);
|
||||||
|
|
||||||
|
$this->checkThrottling();
|
||||||
|
|
||||||
|
return $message;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract protected function doSend(SentMessage $message): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Address[] $addresses
|
||||||
|
*
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
protected function stringifyAddresses(array $addresses): array
|
||||||
|
{
|
||||||
|
return \array_map(function (Address $a) {
|
||||||
|
return $a->toString();
|
||||||
|
}, $addresses);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getLogger(): LoggerInterface
|
||||||
|
{
|
||||||
|
return $this->logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function checkThrottling()
|
||||||
|
{
|
||||||
|
if (0 == $this->rate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sleep = (1 / $this->rate) - (microtime(true) - $this->lastSent);
|
||||||
|
if (0 < $sleep) {
|
||||||
|
$this->logger->debug(sprintf('Email transport "%s" sleeps for %.2f seconds', __CLASS__, $sleep));
|
||||||
|
usleep($sleep * 1000000);
|
||||||
|
}
|
||||||
|
$this->lastSent = microtime(true);
|
||||||
|
}
|
||||||
|
}
|
33
src/Symfony/Component/Mailer/Transport/FailoverTransport.php
Normal file
33
src/Symfony/Component/Mailer/Transport/FailoverTransport.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?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\Transport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses several Transports using a failover algorithm.
|
||||||
|
*
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class FailoverTransport extends RoundRobinTransport
|
||||||
|
{
|
||||||
|
private $currentTransport;
|
||||||
|
|
||||||
|
protected function getNextTransport(): ?TransportInterface
|
||||||
|
{
|
||||||
|
if (null === $this->currentTransport || $this->isTransportDead($this->currentTransport)) {
|
||||||
|
$this->currentTransport = parent::getNextTransport();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->currentTransport;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
<?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\Transport\Http\Api;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\HttpClient\HttpClient;
|
||||||
|
use Symfony\Component\Mailer\Exception\RuntimeException;
|
||||||
|
use Symfony\Component\Mailer\SentMessage;
|
||||||
|
use Symfony\Component\Mailer\SmtpEnvelope;
|
||||||
|
use Symfony\Component\Mailer\Transport\AbstractTransport;
|
||||||
|
use Symfony\Component\Mime\Address;
|
||||||
|
use Symfony\Component\Mime\Email;
|
||||||
|
use Symfony\Component\Mime\MessageConverter;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
abstract class AbstractApiTransport extends AbstractTransport
|
||||||
|
{
|
||||||
|
protected $client;
|
||||||
|
|
||||||
|
public function __construct(HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
|
||||||
|
{
|
||||||
|
$this->client = $client;
|
||||||
|
if (null === $client) {
|
||||||
|
if (!class_exists(HttpClient::class)) {
|
||||||
|
throw new \LogicException(sprintf('You cannot use "%s" as the HttpClient component is not installed. Try running "composer require symfony/http-client".', __CLASS__));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->client = HttpClient::create();
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::__construct($dispatcher, $logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract protected function doSendEmail(Email $email, SmtpEnvelope $envelope): void;
|
||||||
|
|
||||||
|
protected function doSend(SentMessage $message): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$email = MessageConverter::toEmail($message->getOriginalMessage());
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
throw new RuntimeException(sprintf('Unable to send message with the "%s" transport: %s', __CLASS__, $e->getMessage()), 0, $e);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->doSendEmail($email, $message->getEnvelope());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getRecipients(Email $email, SmtpEnvelope $envelope): array
|
||||||
|
{
|
||||||
|
return \array_filter($envelope->getRecipients(), function (Address $address) use ($email) {
|
||||||
|
return false === \in_array($address, \array_merge($email->getCc(), $email->getBcc()), true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
28
src/Symfony/Component/Mailer/Transport/NullTransport.php
Normal file
28
src/Symfony/Component/Mailer/Transport/NullTransport.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?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\Transport;
|
||||||
|
|
||||||
|
use Symfony\Component\Mailer\SentMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pretends messages have been sent, but just ignores them.
|
||||||
|
*
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
final class NullTransport extends AbstractTransport
|
||||||
|
{
|
||||||
|
protected function doSend(SentMessage $message): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,87 @@
|
|||||||
|
<?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\Transport;
|
||||||
|
|
||||||
|
use Symfony\Component\Mailer\Exception\TransportException;
|
||||||
|
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||||
|
use Symfony\Component\Mailer\SentMessage;
|
||||||
|
use Symfony\Component\Mailer\SmtpEnvelope;
|
||||||
|
use Symfony\Component\Mime\RawMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses several Transports using a round robin algorithm.
|
||||||
|
*
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class RoundRobinTransport implements TransportInterface
|
||||||
|
{
|
||||||
|
private $deadTransports;
|
||||||
|
private $transports = [];
|
||||||
|
private $retryPeriod;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param TransportInterface[] $transports
|
||||||
|
*/
|
||||||
|
public function __construct(array $transports, int $retryPeriod = 60)
|
||||||
|
{
|
||||||
|
if (!$transports) {
|
||||||
|
throw new TransportException(__CLASS__.' must have at least one transport configured.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->transports = $transports;
|
||||||
|
$this->deadTransports = new \SplObjectStorage();
|
||||||
|
$this->retryPeriod = $retryPeriod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function send(RawMessage $message, SmtpEnvelope $envelope = null): ?SentMessage
|
||||||
|
{
|
||||||
|
while ($transport = $this->getNextTransport()) {
|
||||||
|
try {
|
||||||
|
return $transport->send($message, $envelope);
|
||||||
|
} catch (TransportExceptionInterface $e) {
|
||||||
|
$this->deadTransports[$transport] = microtime(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new TransportException('All transports failed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotates the transport list around and returns the first instance.
|
||||||
|
*/
|
||||||
|
protected function getNextTransport(): ?TransportInterface
|
||||||
|
{
|
||||||
|
while ($transport = array_shift($this->transports)) {
|
||||||
|
if (!$this->isTransportDead($transport)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ((microtime(true) - $this->deadTransports[$transport]) > $this->retryPeriod) {
|
||||||
|
$this->deadTransports->detach($transport);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($transport) {
|
||||||
|
$this->transports[] = $transport;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $transport;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function isTransportDead(TransportInterface $transport): bool
|
||||||
|
{
|
||||||
|
return $this->deadTransports->contains($transport);
|
||||||
|
}
|
||||||
|
}
|
103
src/Symfony/Component/Mailer/Transport/SendmailTransport.php
Normal file
103
src/Symfony/Component/Mailer/Transport/SendmailTransport.php
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
<?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\Transport;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\Mailer\SentMessage;
|
||||||
|
use Symfony\Component\Mailer\SmtpEnvelope;
|
||||||
|
use Symfony\Component\Mailer\Transport\Smtp\SmtpTransport;
|
||||||
|
use Symfony\Component\Mailer\Transport\Smtp\Stream\AbstractStream;
|
||||||
|
use Symfony\Component\Mailer\Transport\Smtp\Stream\ProcessStream;
|
||||||
|
use Symfony\Component\Mime\RawMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SendmailTransport for sending mail through a Sendmail/Postfix (etc..) binary.
|
||||||
|
*
|
||||||
|
* Supported modes are -bs and -t, with any additional flags desired.
|
||||||
|
* It is advised to use -bs mode since error reporting with -t mode is not
|
||||||
|
* possible.
|
||||||
|
*
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
* @author Chris Corbyn
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class SendmailTransport extends AbstractTransport
|
||||||
|
{
|
||||||
|
private $command = '/usr/sbin/sendmail -bs';
|
||||||
|
private $stream;
|
||||||
|
private $transport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*
|
||||||
|
* If using -t mode you are strongly advised to include -oi or -i in the flags.
|
||||||
|
* For example: /usr/sbin/sendmail -oi -t
|
||||||
|
* -f<sender> flag will be appended automatically if one is not present.
|
||||||
|
*
|
||||||
|
* The recommended mode is "-bs" since it is interactive and failure notifications are hence possible.
|
||||||
|
*/
|
||||||
|
public function __construct(string $command = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
|
||||||
|
{
|
||||||
|
parent::__construct($dispatcher, $logger);
|
||||||
|
|
||||||
|
if (null !== $command) {
|
||||||
|
if (false === strpos($command, ' -bs') && false === strpos($command, ' -t')) {
|
||||||
|
throw new \InvalidArgumentException(sprintf('Unsupported sendmail command flags "%s"; must be one of "-bs" or "-t" but can include additional flags.', $command));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->command = $command;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->stream = new ProcessStream();
|
||||||
|
if (false !== strpos($this->command, ' -bs')) {
|
||||||
|
$this->stream->setCommand($this->command);
|
||||||
|
$this->transport = new SmtpTransport($this->stream, $dispatcher, $logger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function send(RawMessage $message, SmtpEnvelope $envelope = null): ?SentMessage
|
||||||
|
{
|
||||||
|
if ($this->transport) {
|
||||||
|
return $this->transport->send($message, $envelope);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::send($message, $envelope);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doSend(SentMessage $message): void
|
||||||
|
{
|
||||||
|
$this->getLogger()->debug(sprintf('Email transport "%s" starting', __CLASS__));
|
||||||
|
|
||||||
|
$command = $this->command;
|
||||||
|
if (false === strpos($command, ' -f')) {
|
||||||
|
$command .= ' -f'.escapeshellarg($message->getEnvelope()->getSender()->getEncodedAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
$chunks = AbstractStream::replace("\r\n", "\n", $message->toIterable());
|
||||||
|
|
||||||
|
if (false === strpos($command, ' -i') && false === strpos($command, ' -oi')) {
|
||||||
|
$chunks = AbstractStream::replace("\n.", "\n..", $chunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->stream->setCommand($command);
|
||||||
|
$this->stream->initialize();
|
||||||
|
foreach ($chunks as $chunk) {
|
||||||
|
$this->stream->write($chunk);
|
||||||
|
}
|
||||||
|
$this->stream->flush();
|
||||||
|
$this->stream->terminate();
|
||||||
|
|
||||||
|
$this->getLogger()->debug(sprintf('Email transport "%s" stopped', __CLASS__));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
<?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\Transport\Smtp\Auth;
|
||||||
|
|
||||||
|
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||||
|
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An Authentication mechanism.
|
||||||
|
*
|
||||||
|
* @author Chris Corbyn
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
interface AuthenticatorInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Tries to authenticate the user.
|
||||||
|
*
|
||||||
|
* @throws TransportExceptionInterface
|
||||||
|
*/
|
||||||
|
public function authenticate(EsmtpTransport $client): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the name of the AUTH mechanism this Authenticator handles.
|
||||||
|
*/
|
||||||
|
public function getAuthKeyword(): string;
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
<?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\Transport\Smtp\Auth;
|
||||||
|
|
||||||
|
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles CRAM-MD5 authentication.
|
||||||
|
*
|
||||||
|
* @author Chris Corbyn
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class CramMd5Authenticator implements AuthenticatorInterface
|
||||||
|
{
|
||||||
|
public function getAuthKeyword(): string
|
||||||
|
{
|
||||||
|
return 'CRAM-MD5';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*
|
||||||
|
* @see https://www.ietf.org/rfc/rfc4954.txt
|
||||||
|
*/
|
||||||
|
public function authenticate(EsmtpTransport $client): void
|
||||||
|
{
|
||||||
|
$challenge = $client->executeCommand("AUTH CRAM-MD5\r\n", [334]);
|
||||||
|
$challenge = base64_decode(substr($challenge, 4));
|
||||||
|
$message = base64_encode($client->getUsername().' '.$this->getResponse($client->getPassword(), $challenge));
|
||||||
|
$client->executeCommand(sprintf("%s\r\n", $message), [235]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a CRAM-MD5 response from a server challenge.
|
||||||
|
*/
|
||||||
|
private function getResponse(string $secret, string $challenge): string
|
||||||
|
{
|
||||||
|
if (\strlen($secret) > 64) {
|
||||||
|
$secret = pack('H32', md5($secret));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\strlen($secret) < 64) {
|
||||||
|
$secret = str_pad($secret, 64, \chr(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
$kipad = substr($secret, 0, 64) ^ str_repeat(\chr(0x36), 64);
|
||||||
|
$kopad = substr($secret, 0, 64) ^ str_repeat(\chr(0x5C), 64);
|
||||||
|
|
||||||
|
$inner = pack('H32', md5($kipad.$challenge));
|
||||||
|
$digest = md5($kopad.$inner);
|
||||||
|
|
||||||
|
return $digest;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
<?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\Transport\Smtp\Auth;
|
||||||
|
|
||||||
|
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles LOGIN authentication.
|
||||||
|
*
|
||||||
|
* @author Chris Corbyn
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class LoginAuthenticator implements AuthenticatorInterface
|
||||||
|
{
|
||||||
|
public function getAuthKeyword(): string
|
||||||
|
{
|
||||||
|
return 'LOGIN';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*
|
||||||
|
* @see https://www.ietf.org/rfc/rfc4954.txt
|
||||||
|
*/
|
||||||
|
public function authenticate(EsmtpTransport $client): void
|
||||||
|
{
|
||||||
|
$client->executeCommand("AUTH LOGIN\r\n", [334]);
|
||||||
|
$client->executeCommand(sprintf("%s\r\n", base64_encode($client->getUsername())), [334]);
|
||||||
|
$client->executeCommand(sprintf("%s\r\n", base64_encode($client->getPassword())), [235]);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
<?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\Transport\Smtp\Auth;
|
||||||
|
|
||||||
|
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles PLAIN authentication.
|
||||||
|
*
|
||||||
|
* @author Chris Corbyn
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class PlainAuthenticator implements AuthenticatorInterface
|
||||||
|
{
|
||||||
|
public function getAuthKeyword(): string
|
||||||
|
{
|
||||||
|
return 'PLAIN';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*
|
||||||
|
* @see https://www.ietf.org/rfc/rfc4954.txt
|
||||||
|
*/
|
||||||
|
public function authenticate(EsmtpTransport $client): void
|
||||||
|
{
|
||||||
|
$client->executeCommand(sprintf("AUTH PLAIN %s\r\n", base64_encode($client->getUsername().\chr(0).$client->getUsername().\chr(0).$client->getPassword())), [235]);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
<?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\Transport\Smtp\Auth;
|
||||||
|
|
||||||
|
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles XOAUTH2 authentication.
|
||||||
|
*
|
||||||
|
* @author xu.li<AthenaLightenedMyPath@gmail.com>
|
||||||
|
*
|
||||||
|
* @see https://developers.google.com/google-apps/gmail/xoauth2_protocol
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class XOAuth2Authenticator implements AuthenticatorInterface
|
||||||
|
{
|
||||||
|
public function getAuthKeyword(): string
|
||||||
|
{
|
||||||
|
return 'XOAUTH2';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*
|
||||||
|
* @see https://developers.google.com/google-apps/gmail/xoauth2_protocol#the_sasl_xoauth2_mechanism
|
||||||
|
*/
|
||||||
|
public function authenticate(EsmtpTransport $client): void
|
||||||
|
{
|
||||||
|
$client->executeCommand('AUTH XOAUTH2 '.base64_encode('user='.$client->getUsername()."\1auth=Bearer ".$client->getPassword()."\1\1")."\r\n", [235]);
|
||||||
|
}
|
||||||
|
}
|
205
src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php
Normal file
205
src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
<?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\Transport\Smtp;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\Mailer\Exception\TransportException;
|
||||||
|
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||||
|
use Symfony\Component\Mailer\Transport\Smtp\Auth\AuthenticatorInterface;
|
||||||
|
use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends Emails over SMTP with ESMTP support.
|
||||||
|
*
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
* @author Chris Corbyn
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class EsmtpTransport extends SmtpTransport
|
||||||
|
{
|
||||||
|
private $authenticators = [];
|
||||||
|
private $username;
|
||||||
|
private $password;
|
||||||
|
private $authMode;
|
||||||
|
|
||||||
|
public function __construct(string $host = 'localhost', int $port = 25, string $encryption = null, string $authMode = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
|
||||||
|
{
|
||||||
|
parent::__construct(null, $dispatcher, $logger);
|
||||||
|
|
||||||
|
$this->authenticators = [
|
||||||
|
new Auth\PlainAuthenticator(),
|
||||||
|
new Auth\LoginAuthenticator(),
|
||||||
|
new Auth\XOAuth2Authenticator(),
|
||||||
|
new Auth\CramMd5Authenticator(),
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @var SocketStream $stream */
|
||||||
|
$stream = $this->getStream();
|
||||||
|
$stream->setHost($host);
|
||||||
|
$stream->setPort($port);
|
||||||
|
if (null !== $encryption) {
|
||||||
|
$stream->setEncryption($encryption);
|
||||||
|
}
|
||||||
|
if (null !== $authMode) {
|
||||||
|
$this->setAuthMode($authMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUsername(string $username): self
|
||||||
|
{
|
||||||
|
$this->username = $username;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUsername(): string
|
||||||
|
{
|
||||||
|
return $this->username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPassword(string $password): self
|
||||||
|
{
|
||||||
|
$this->password = $password;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPassword(): string
|
||||||
|
{
|
||||||
|
return $this->password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setAuthMode(string $mode): self
|
||||||
|
{
|
||||||
|
$this->authMode = $mode;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAuthMode(): string
|
||||||
|
{
|
||||||
|
return $this->authMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addAuthenticator(AuthenticatorInterface $authenticator): void
|
||||||
|
{
|
||||||
|
$this->authenticators[] = $authenticator;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doHeloCommand(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$response = $this->executeCommand(sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]);
|
||||||
|
} catch (TransportExceptionInterface $e) {
|
||||||
|
parent::doHeloCommand();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var SocketStream $stream */
|
||||||
|
$stream = $this->getStream();
|
||||||
|
if ($stream->isTLS()) {
|
||||||
|
$this->executeCommand("STARTTLS\r\n", [220]);
|
||||||
|
|
||||||
|
if (!$stream->startTLS()) {
|
||||||
|
throw new TransportException('Unable to connect with TLS encryption.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = $this->executeCommand(sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]);
|
||||||
|
} catch (TransportExceptionInterface $e) {
|
||||||
|
parent::doHeloCommand();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$capabilities = $this->getCapabilities($response);
|
||||||
|
if (\array_key_exists('AUTH', $capabilities)) {
|
||||||
|
$this->handleAuth($capabilities['AUTH']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getCapabilities($ehloResponse): array
|
||||||
|
{
|
||||||
|
$capabilities = [];
|
||||||
|
$lines = explode("\r\n", trim($ehloResponse));
|
||||||
|
array_shift($lines);
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
if (preg_match('/^[0-9]{3}[ -]([A-Z0-9-]+)((?:[ =].*)?)$/Di', $line, $matches)) {
|
||||||
|
$value = strtoupper(ltrim($matches[2], ' ='));
|
||||||
|
$capabilities[strtoupper($matches[1])] = $value ? explode(' ', $value) : [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handleAuth(array $modes): void
|
||||||
|
{
|
||||||
|
if (!$this->username) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$authNames = [];
|
||||||
|
$errors = [];
|
||||||
|
$modes = array_map('strtolower', $modes);
|
||||||
|
foreach ($this->getActiveAuthenticators() as $authenticator) {
|
||||||
|
if (!\in_array(strtolower($authenticator->getAuthKeyword()), $modes, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$authNames[] = $authenticator->getAuthKeyword();
|
||||||
|
try {
|
||||||
|
$authenticator->authenticate($this);
|
||||||
|
|
||||||
|
return;
|
||||||
|
} catch (TransportExceptionInterface $e) {
|
||||||
|
$this->executeCommand("RSET\r\n", [250]);
|
||||||
|
|
||||||
|
// keep the error message, but tries the other authenticators
|
||||||
|
$errors[$authenticator->getAuthKeyword()] = $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$authNames) {
|
||||||
|
throw new TransportException('Failed to find an authenticator supported by the SMTP server.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$message = sprintf('Failed to authenticate on SMTP server with username "%s" using the following authenticators: "%s".', $this->username, implode('", "', $authNames));
|
||||||
|
foreach ($errors as $name => $error) {
|
||||||
|
$message .= sprintf(' Authenticator "%s" returned "%s".', $name, $error);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new TransportException($message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return AuthenticatorInterface[]
|
||||||
|
*/
|
||||||
|
private function getActiveAuthenticators(): array
|
||||||
|
{
|
||||||
|
if (!$mode = strtolower($this->authMode)) {
|
||||||
|
return $this->authenticators;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->authenticators as $authenticator) {
|
||||||
|
if (strtolower($authenticator->getAuthKeyword()) === $mode) {
|
||||||
|
return [$authenticator];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new TransportException(sprintf('Auth mode "%s" is invalid.', $mode));
|
||||||
|
}
|
||||||
|
}
|
283
src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php
Normal file
283
src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
<?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\Transport\Smtp;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\Mailer\Exception\LogicException;
|
||||||
|
use Symfony\Component\Mailer\Exception\TransportException;
|
||||||
|
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||||
|
use Symfony\Component\Mailer\SentMessage;
|
||||||
|
use Symfony\Component\Mailer\SmtpEnvelope;
|
||||||
|
use Symfony\Component\Mailer\Transport\AbstractTransport;
|
||||||
|
use Symfony\Component\Mailer\Transport\Smtp\Stream\AbstractStream;
|
||||||
|
use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream;
|
||||||
|
use Symfony\Component\Mime\RawMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends emails over SMTP.
|
||||||
|
*
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
* @author Chris Corbyn
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
class SmtpTransport extends AbstractTransport
|
||||||
|
{
|
||||||
|
private $started = false;
|
||||||
|
private $restartThreshold = 100;
|
||||||
|
private $restartThresholdSleep = 0;
|
||||||
|
private $restartCounter;
|
||||||
|
private $stream;
|
||||||
|
private $domain = '[127.0.0.1]';
|
||||||
|
|
||||||
|
public function __construct(AbstractStream $stream = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
|
||||||
|
{
|
||||||
|
parent::__construct($dispatcher, $logger);
|
||||||
|
|
||||||
|
$this->stream = $stream ?: new SocketStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStream(): AbstractStream
|
||||||
|
{
|
||||||
|
return $this->stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the maximum number of messages to send before re-starting the transport.
|
||||||
|
*
|
||||||
|
* By default, the threshold is set to 100 (and no sleep at restart).
|
||||||
|
*
|
||||||
|
* @param int $threshold The maximum number of messages (0 to disable)
|
||||||
|
* @param int $sleep The number of seconds to sleep between stopping and re-starting the transport
|
||||||
|
*/
|
||||||
|
public function setRestartThreshold(int $threshold, int $sleep = 0): self
|
||||||
|
{
|
||||||
|
$this->restartThreshold = $threshold;
|
||||||
|
$this->restartThresholdSleep = $sleep;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the name of the local domain that will be used in HELO.
|
||||||
|
*
|
||||||
|
* This should be a fully-qualified domain name and should be truly the domain
|
||||||
|
* you're using.
|
||||||
|
*
|
||||||
|
* If your server does not have a domain name, use the IP address. This will
|
||||||
|
* automatically be wrapped in square brackets as described in RFC 5321,
|
||||||
|
* section 4.1.3.
|
||||||
|
*/
|
||||||
|
public function setLocalDomain(string $domain): self
|
||||||
|
{
|
||||||
|
if ('' !== $domain && '[' !== $domain[0]) {
|
||||||
|
if (filter_var($domain, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||||
|
$domain = '['.$domain.']';
|
||||||
|
} elseif (filter_var($domain, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||||
|
$domain = '[IPv6:'.$domain.']';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->domain = $domain;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the name of the domain that will be used in HELO.
|
||||||
|
*
|
||||||
|
* If an IP address was specified, this will be returned wrapped in square
|
||||||
|
* brackets as described in RFC 5321, section 4.1.3.
|
||||||
|
*/
|
||||||
|
public function getLocalDomain(): string
|
||||||
|
{
|
||||||
|
return $this->domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function send(RawMessage $message, SmtpEnvelope $envelope = null): ?SentMessage
|
||||||
|
{
|
||||||
|
$this->ping();
|
||||||
|
if (!$this->started) {
|
||||||
|
$this->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
$message = parent::send($message, $envelope);
|
||||||
|
|
||||||
|
$this->checkRestartThreshold();
|
||||||
|
|
||||||
|
return $message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs a command against the stream, expecting the given response codes.
|
||||||
|
*
|
||||||
|
* @param int[] $codes
|
||||||
|
*
|
||||||
|
* @return string The server response
|
||||||
|
*
|
||||||
|
* @throws TransportException when an invalid response if received
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function executeCommand(string $command, array $codes): string
|
||||||
|
{
|
||||||
|
$this->getLogger()->debug(sprintf('Email transport "%s" sent command "%s"', __CLASS__, trim($command)));
|
||||||
|
$this->stream->write($command);
|
||||||
|
$response = $this->getFullResponse();
|
||||||
|
$this->assertResponseCode($response, $codes);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doSend(SentMessage $message): void
|
||||||
|
{
|
||||||
|
$envelope = $message->getEnvelope();
|
||||||
|
$this->doMailFromCommand($envelope->getSender()->toString());
|
||||||
|
foreach ($envelope->getRecipients() as $recipient) {
|
||||||
|
$this->doRcptToCommand($recipient->toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->executeCommand("DATA\r\n", [354]);
|
||||||
|
foreach (AbstractStream::replace("\r\n.", "\r\n..", $message->toIterable()) as $chunk) {
|
||||||
|
$this->stream->write($chunk);
|
||||||
|
}
|
||||||
|
$this->stream->flush();
|
||||||
|
$this->executeCommand("\r\n.\r\n", [250]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doHeloCommand(): void
|
||||||
|
{
|
||||||
|
$this->executeCommand(sprintf("HELO %s\r\n", $this->domain), [250]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function doMailFromCommand($address): void
|
||||||
|
{
|
||||||
|
$this->executeCommand(sprintf("MAIL FROM:<%s>\r\n", $address), [250]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function doRcptToCommand($address): void
|
||||||
|
{
|
||||||
|
$this->executeCommand(sprintf("RCPT TO:<%s>\r\n", $address), [250, 251, 252]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function start(): void
|
||||||
|
{
|
||||||
|
if ($this->started) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->getLogger()->debug(sprintf('Email transport "%s" starting', __CLASS__));
|
||||||
|
|
||||||
|
$this->stream->initialize();
|
||||||
|
$this->assertResponseCode($this->getFullResponse(), [220]);
|
||||||
|
$this->doHeloCommand();
|
||||||
|
$this->started = true;
|
||||||
|
|
||||||
|
$this->getLogger()->debug(sprintf('Email transport "%s" started', __CLASS__));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function stop(): void
|
||||||
|
{
|
||||||
|
if (!$this->started) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->getLogger()->debug(sprintf('Email transport "%s" stopping', __CLASS__));
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->executeCommand("QUIT\r\n", [221]);
|
||||||
|
} catch (TransportExceptionInterface $e) {
|
||||||
|
} finally {
|
||||||
|
$this->stream->terminate();
|
||||||
|
$this->started = false;
|
||||||
|
$this->getLogger()->debug(sprintf('Email transport "%s" stopped', __CLASS__));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ping(): void
|
||||||
|
{
|
||||||
|
if (!$this->started) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->executeCommand("NOOP\r\n", [250]);
|
||||||
|
} catch (TransportExceptionInterface $e) {
|
||||||
|
try {
|
||||||
|
$this->stop();
|
||||||
|
} catch (TransportExceptionInterface $e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws TransportException if a response code is incorrect
|
||||||
|
*/
|
||||||
|
private function assertResponseCode(string $response, array $codes): void
|
||||||
|
{
|
||||||
|
if (!$codes) {
|
||||||
|
throw new LogicException('You must set the expected response code.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$response) {
|
||||||
|
throw new TransportException(sprintf('Expected response code "%s" but got an empty response.', implode('/', $codes)));
|
||||||
|
}
|
||||||
|
|
||||||
|
list($code) = sscanf($response, '%3d');
|
||||||
|
$valid = \in_array($code, $codes);
|
||||||
|
|
||||||
|
$this->getLogger()->debug(sprintf('Email transport "%s" received response "%s" (%s).', __CLASS__, trim($response), $valid ? 'ok' : 'error'));
|
||||||
|
|
||||||
|
if (!$valid) {
|
||||||
|
throw new TransportException(sprintf('Expected response code "%s" but got code "%s", with message "%s".', implode('/', $codes), $code, trim($response)), $code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getFullResponse(): string
|
||||||
|
{
|
||||||
|
$response = '';
|
||||||
|
do {
|
||||||
|
$line = $this->stream->readLine();
|
||||||
|
$response .= $line;
|
||||||
|
} while ($line && isset($line[3]) && ' ' !== $line[3]);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function checkRestartThreshold(): void
|
||||||
|
{
|
||||||
|
// when using sendmail via non-interactive mode, the transport is never "started"
|
||||||
|
if (!$this->started) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
++$this->restartCounter;
|
||||||
|
if ($this->restartCounter < $this->restartThreshold) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->stop();
|
||||||
|
if (0 < $sleep = $this->restartThresholdSleep) {
|
||||||
|
$this->getLogger()->debug(sprintf('Email transport "%s" sleeps for %d seconds after stopping', __CLASS__, $sleep));
|
||||||
|
|
||||||
|
sleep($sleep);
|
||||||
|
}
|
||||||
|
$this->start();
|
||||||
|
$this->restartCounter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __destruct()
|
||||||
|
{
|
||||||
|
$this->stop();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,119 @@
|
|||||||
|
<?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\Transport\Smtp\Stream;
|
||||||
|
|
||||||
|
use Symfony\Component\Mailer\Exception\TransportException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A stream supporting remote sockets and local processes.
|
||||||
|
*
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
* @author Nicolas Grekas <p@tchwork.com>
|
||||||
|
* @author Chris Corbyn
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
abstract class AbstractStream
|
||||||
|
{
|
||||||
|
protected $stream;
|
||||||
|
protected $in;
|
||||||
|
protected $out;
|
||||||
|
|
||||||
|
public function write(string $bytes): void
|
||||||
|
{
|
||||||
|
$bytesToWrite = \strlen($bytes);
|
||||||
|
$totalBytesWritten = 0;
|
||||||
|
while ($totalBytesWritten < $bytesToWrite) {
|
||||||
|
$bytesWritten = fwrite($this->in, substr($bytes, $totalBytesWritten));
|
||||||
|
if (false === $bytesWritten || 0 === $bytesWritten) {
|
||||||
|
throw new TransportException('Unable to write bytes on the wire.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalBytesWritten += $bytesWritten;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flushes the contents of the stream (empty it) and set the internal pointer to the beginning.
|
||||||
|
*/
|
||||||
|
public function flush(): void
|
||||||
|
{
|
||||||
|
fflush($this->in);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs any initialization needed.
|
||||||
|
*/
|
||||||
|
abstract public function initialize(): void;
|
||||||
|
|
||||||
|
public function terminate(): void
|
||||||
|
{
|
||||||
|
$this->stream = $this->out = $this->in = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function readLine(): string
|
||||||
|
{
|
||||||
|
if (feof($this->out)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$line = fgets($this->out);
|
||||||
|
if (0 === \strlen($line)) {
|
||||||
|
$metas = stream_get_meta_data($this->out);
|
||||||
|
if ($metas['timed_out']) {
|
||||||
|
throw new TransportException(sprintf('Connection to "%s" timed out.', $this->getReadConnectionDescription()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $line;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function replace(string $from, string $to, iterable $chunks): \Generator
|
||||||
|
{
|
||||||
|
if ('' === $from) {
|
||||||
|
yield from $chunks;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$carry = '';
|
||||||
|
$fromLen = \strlen($from);
|
||||||
|
|
||||||
|
foreach ($chunks as $chunk) {
|
||||||
|
if ('' === $chunk = $carry.$chunk) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (false !== strpos($chunk, $from)) {
|
||||||
|
$chunk = explode($from, $chunk);
|
||||||
|
$carry = array_pop($chunk);
|
||||||
|
|
||||||
|
yield implode($to, $chunk).$to;
|
||||||
|
} else {
|
||||||
|
$carry = $chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\strlen($carry) > $fromLen) {
|
||||||
|
yield substr($carry, 0, -$fromLen);
|
||||||
|
$carry = substr($carry, -$fromLen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('' !== $carry) {
|
||||||
|
yield $carry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract protected function getReadConnectionDescription(): string;
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
<?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\Transport\Smtp\Stream;
|
||||||
|
|
||||||
|
use Symfony\Component\Mailer\Exception\TransportException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A stream supporting local processes.
|
||||||
|
*
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
* @author Chris Corbyn
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
final class ProcessStream extends AbstractStream
|
||||||
|
{
|
||||||
|
private $command;
|
||||||
|
|
||||||
|
public function setCommand(string $command)
|
||||||
|
{
|
||||||
|
$this->command = $command;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function initialize(): void
|
||||||
|
{
|
||||||
|
$descriptorSpec = [
|
||||||
|
0 => ['pipe', 'r'],
|
||||||
|
1 => ['pipe', 'w'],
|
||||||
|
2 => ['pipe', 'w'],
|
||||||
|
];
|
||||||
|
$pipes = [];
|
||||||
|
$this->stream = proc_open($this->command, $descriptorSpec, $pipes);
|
||||||
|
stream_set_blocking($pipes[2], false);
|
||||||
|
if ($err = stream_get_contents($pipes[2])) {
|
||||||
|
throw new TransportException(sprintf('Process could not be started: %s.', $err));
|
||||||
|
}
|
||||||
|
$this->in = &$pipes[0];
|
||||||
|
$this->out = &$pipes[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function terminate(): void
|
||||||
|
{
|
||||||
|
if (null !== $this->stream) {
|
||||||
|
fclose($this->in);
|
||||||
|
fclose($this->out);
|
||||||
|
proc_close($this->stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::terminate();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getReadConnectionDescription(): string
|
||||||
|
{
|
||||||
|
return 'process '.$this->command;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,172 @@
|
|||||||
|
<?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\Transport\Smtp\Stream;
|
||||||
|
|
||||||
|
use Symfony\Component\Mailer\Exception\TransportException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A stream supporting remote sockets.
|
||||||
|
*
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
* @author Chris Corbyn
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
final class SocketStream extends AbstractStream
|
||||||
|
{
|
||||||
|
private $url;
|
||||||
|
private $host = 'localhost';
|
||||||
|
private $protocol = 'tcp';
|
||||||
|
private $port = 25;
|
||||||
|
private $timeout = 15;
|
||||||
|
private $tls = false;
|
||||||
|
private $sourceIp;
|
||||||
|
private $streamContextOptions = [];
|
||||||
|
|
||||||
|
public function setTimeout(int $timeout): self
|
||||||
|
{
|
||||||
|
$this->timeout = $timeout;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTimeout(): int
|
||||||
|
{
|
||||||
|
return $this->timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Literal IPv6 addresses should be wrapped in square brackets.
|
||||||
|
*/
|
||||||
|
public function setHost(string $host): self
|
||||||
|
{
|
||||||
|
$this->host = $host;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHost(): string
|
||||||
|
{
|
||||||
|
return $this->host;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPort(int $port): self
|
||||||
|
{
|
||||||
|
$this->port = $port;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPort(): int
|
||||||
|
{
|
||||||
|
return $this->port;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the encryption type (tls or ssl).
|
||||||
|
*/
|
||||||
|
public function setEncryption(string $encryption): self
|
||||||
|
{
|
||||||
|
$encryption = strtolower($encryption);
|
||||||
|
if ('tls' === $encryption) {
|
||||||
|
$this->protocol = 'tcp';
|
||||||
|
$this->tls = true;
|
||||||
|
} else {
|
||||||
|
$this->protocol = $encryption;
|
||||||
|
$this->tls = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isTLS(): bool
|
||||||
|
{
|
||||||
|
return $this->tls;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setStreamOptions(array $options): self
|
||||||
|
{
|
||||||
|
$this->streamContextOptions = $options;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStreamOptions(): array
|
||||||
|
{
|
||||||
|
return $this->streamContextOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the source IP.
|
||||||
|
*
|
||||||
|
* IPv6 addresses should be wrapped in square brackets.
|
||||||
|
*/
|
||||||
|
public function setSourceIp(string $ip): self
|
||||||
|
{
|
||||||
|
$this->sourceIp = $ip;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the IP used to connect to the destination.
|
||||||
|
*/
|
||||||
|
public function getSourceIp(): ?string
|
||||||
|
{
|
||||||
|
return $this->sourceIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function initialize(): void
|
||||||
|
{
|
||||||
|
$this->url = $this->host.':'.$this->port;
|
||||||
|
if ($this->protocol) {
|
||||||
|
$this->url = $this->protocol.'://'.$this->url;
|
||||||
|
}
|
||||||
|
$options = [];
|
||||||
|
if ($this->sourceIp) {
|
||||||
|
$options['socket']['bindto'] = $this->sourceIp.':0';
|
||||||
|
}
|
||||||
|
if ($this->streamContextOptions) {
|
||||||
|
$options = array_merge($options, $this->streamContextOptions);
|
||||||
|
}
|
||||||
|
$streamContext = stream_context_create($options);
|
||||||
|
$this->stream = @stream_socket_client($this->url, $errno, $errstr, $this->timeout, STREAM_CLIENT_CONNECT, $streamContext);
|
||||||
|
if (false === $this->stream) {
|
||||||
|
throw new TransportException(sprintf('Connection could not be established with host "%s": %s (%s)', $this->url, $errstr, $errno));
|
||||||
|
}
|
||||||
|
stream_set_blocking($this->stream, true);
|
||||||
|
stream_set_timeout($this->stream, $this->timeout);
|
||||||
|
$this->in = &$this->stream;
|
||||||
|
$this->out = &$this->stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function startTLS(): bool
|
||||||
|
{
|
||||||
|
return (bool) stream_socket_enable_crypto($this->stream, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function terminate(): void
|
||||||
|
{
|
||||||
|
if (null !== $this->stream) {
|
||||||
|
fclose($this->stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::terminate();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getReadConnectionDescription(): string
|
||||||
|
{
|
||||||
|
return $this->url;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
<?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\Transport;
|
||||||
|
|
||||||
|
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||||
|
use Symfony\Component\Mailer\SentMessage;
|
||||||
|
use Symfony\Component\Mailer\SmtpEnvelope;
|
||||||
|
use Symfony\Component\Mime\RawMessage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for all mailer transports.
|
||||||
|
*
|
||||||
|
* When sending emails, you should prefer MailerInterface implementations
|
||||||
|
* as they allow asynchronous sending.
|
||||||
|
*
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* @experimental in 4.3
|
||||||
|
*/
|
||||||
|
interface TransportInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @throws TransportExceptionInterface
|
||||||
|
*/
|
||||||
|
public function send(RawMessage $message, SmtpEnvelope $envelope = null): ?SentMessage;
|
||||||
|
}
|
45
src/Symfony/Component/Mailer/composer.json
Normal file
45
src/Symfony/Component/Mailer/composer.json
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "symfony/mailer",
|
||||||
|
"type": "library",
|
||||||
|
"description": "Symfony Mailer Component",
|
||||||
|
"keywords": [],
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"license": "MIT",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Fabien Potencier",
|
||||||
|
"email": "fabien@symfony.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php": "^7.1.3",
|
||||||
|
"psr/log": "~1.0",
|
||||||
|
"symfony/event-dispatcher": "^4.3",
|
||||||
|
"symfony/mime": "^4.3"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"symfony/amazon-mailer": "^4.3",
|
||||||
|
"egulias/email-validator": "^2.0",
|
||||||
|
"symfony/google-mailer": "^4.3",
|
||||||
|
"symfony/mailgun-mailer": "^4.3",
|
||||||
|
"symfony/mailchimp-mailer": "^4.3",
|
||||||
|
"symfony/postmark-mailer": "^4.3",
|
||||||
|
"symfony/sendgrid-mailer": "^4.3"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": { "Symfony\\Component\\Mailer\\": "" },
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minimum-stability": "dev",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "4.3-dev"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
src/Symfony/Component/Mailer/phpunit.xml.dist
Normal file
30
src/Symfony/Component/Mailer/phpunit.xml.dist
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/5.2/phpunit.xsd"
|
||||||
|
backupGlobals="false"
|
||||||
|
colors="true"
|
||||||
|
bootstrap="vendor/autoload.php"
|
||||||
|
failOnRisky="true"
|
||||||
|
failOnWarning="true"
|
||||||
|
>
|
||||||
|
<php>
|
||||||
|
<ini name="error_reporting" value="-1" />
|
||||||
|
</php>
|
||||||
|
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Symfony Mailer Component Test Suite">
|
||||||
|
<directory>./Tests/</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
|
||||||
|
<filter>
|
||||||
|
<whitelist>
|
||||||
|
<directory>./</directory>
|
||||||
|
<exclude>
|
||||||
|
<directory>./Tests</directory>
|
||||||
|
<directory>./vendor</directory>
|
||||||
|
</exclude>
|
||||||
|
</whitelist>
|
||||||
|
</filter>
|
||||||
|
</phpunit>
|
Reference in New Issue
Block a user