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:
Fabien Potencier 2019-03-30 12:38:45 +01:00
commit 845cf9e271
98 changed files with 4893 additions and 0 deletions

View File

@ -26,6 +26,7 @@ use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\Lock\Lock;
use Symfony\Component\Lock\Store\SemaphoreStore;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
@ -112,6 +113,7 @@ class Configuration implements ConfigurationInterface
$this->addMessengerSection($rootNode);
$this->addRobotsIndexSection($rootNode);
$this->addHttpClientSection($rootNode);
$this->addMailerSection($rootNode);
return $treeBuilder;
}
@ -1344,4 +1346,19 @@ class Configuration implements ConfigurationInterface
->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()
;
}
}

View File

@ -74,6 +74,7 @@ use Symfony\Component\Lock\LockInterface;
use Symfony\Component\Lock\Store\FlockStore;
use Symfony\Component\Lock\Store\StoreFactory;
use Symfony\Component\Lock\StoreInterface;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
use Symfony\Component\Messenger\MessageBus;
use Symfony\Component\Messenger\MessageBusInterface;
@ -316,6 +317,10 @@ class FrameworkExtension extends Extension
$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 (!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".');
@ -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.
*

View File

@ -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>

View File

@ -19,6 +19,7 @@ use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Config\Definition\Processor;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\Lock\Store\SemaphoreStore;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Messenger\MessageBusInterface;
class ConfigurationTest extends TestCase
@ -336,6 +337,10 @@ class ConfigurationTest extends TestCase
'enabled' => !class_exists(FullStack::class) && class_exists(HttpClient::class),
'clients' => [],
],
'mailer' => [
'dsn' => 'smtp://null',
'enabled' => !class_exists(FullStack::class) && class_exists(Mailer::class),
],
];
}
}

View File

@ -42,6 +42,7 @@
"symfony/form": "^4.3",
"symfony/expression-language": "~3.4|~4.0",
"symfony/http-client": "^4.3",
"symfony/mailer": "^4.3",
"symfony/messenger": "^4.3",
"symfony/mime": "^4.3",
"symfony/process": "~3.4|~4.0",

View File

@ -19,6 +19,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Translation\Translator;
use Twig\Extension\ExtensionInterface;
use Twig\Extension\RuntimeExtensionInterface;
@ -49,6 +50,10 @@ class TwigExtension extends Extension
$loader->load('console.xml');
}
if (class_exists(Mailer::class)) {
$loader->load('mailer.xml');
}
if (!class_exists(Translator::class)) {
$container->removeDefinition('twig.translation.extractor');
}

View 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>

View File

@ -0,0 +1,7 @@
CHANGELOG
=========
4.3.0
-----
* added the bridge

View 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\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;
}
}

View File

@ -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));
}
}

View 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.

View 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)

View File

@ -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);
}
}

View 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"
}
}
}

View 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>

View File

@ -0,0 +1,7 @@
CHANGELOG
=========
4.3.0
-----
* added the bridge

View 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.

View 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)

View 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\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);
}
}

View 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"
}
}
}

View 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>

View File

@ -0,0 +1,7 @@
CHANGELOG
=========
4.3.0
-----
* added the bridge

View 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\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;
}
}

View File

@ -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']));
}
}
}

View 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.

View 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)

View File

@ -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);
}
}

View 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"
}
}
}

View 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 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>

View File

@ -0,0 +1,7 @@
CHANGELOG
=========
4.3.0
-----
* added the bridge

View File

@ -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];
}
}

View File

@ -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()));
}
}
}

View 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.

View 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)

View 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\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);
}
}

View 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"
}
}
}

View 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>

View File

@ -0,0 +1,7 @@
CHANGELOG
=========
4.3.0
-----
* added the bridge

View File

@ -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;
}
}

View 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.

View 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)

View 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\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);
}
}

View 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"
}
}
}

View 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 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>

View File

@ -0,0 +1,3 @@
vendor/
composer.lock
phpunit.xml

View File

@ -0,0 +1,7 @@
CHANGELOG
=========
4.3.0
-----
* added the bridge

View File

@ -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;
}
}

View 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.

View 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)

View 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\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);
}
}

View 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"
}
}
}

View 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 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>

View File

@ -0,0 +1,7 @@
CHANGELOG
=========
4.3.0
-----
* Added the component

View 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;
}
}

View 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\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],
];
}
}

View File

@ -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',
];
}
}

View File

@ -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
{
}

View 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 HttpTransportException extends TransportException
{
}

View 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 InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}

View 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
{
}

View 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
{
}

View 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 TransportException extends RuntimeException implements TransportExceptionInterface
{
}

View 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
*/
interface TransportExceptionInterface extends ExceptionInterface
{
}

View 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.

View 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));
}
}

View 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;
}

View 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());
}
}

View 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;
}
}

View 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)

View 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();
}
}

View 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.');
}
}

View 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());
}
}

View 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(''));
}
}

View File

@ -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);
}
}

View File

@ -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(''));
}
}

View File

@ -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(''));
}
}

View File

@ -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']];
}
}

View 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));
}
}
}

View 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']));
}
}
}

View 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);
}
}

View 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;
}
}

View File

@ -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);
});
}
}

View 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
{
}
}

View File

@ -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);
}
}

View 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__));
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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]);
}
}

View File

@ -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]);
}
}

View File

@ -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]);
}
}

View 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));
}
}

View 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();
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View 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"
}
}
}

View 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>