API Update SwiftMailer from v5 to v6 (#10048)

* Update SwiftMailer from v5 to v6

- Fixes #9834
- Update default Swift_Transport to use Swift_SendmailTransport
- Update version restraint for Swiftmailer
- Address new parameter type for Swift_Message::setDate()
- Update class references in docblocks

Co-authored-by: Danaë Miller-Clendon <danae.millerclendon@silverstripe.com>
This commit is contained in:
Steve Boyd 2021-08-18 12:16:45 +12:00 committed by GitHub
parent 2838625a09
commit 92f47da08b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 556 additions and 15 deletions

View File

@ -34,7 +34,7 @@
"silverstripe/assets": "^1@dev", "silverstripe/assets": "^1@dev",
"silverstripe/vendor-plugin": "^1.4", "silverstripe/vendor-plugin": "^1.4",
"sminnee/callbacklist": "^0.1", "sminnee/callbacklist": "^0.1",
"swiftmailer/swiftmailer": "~5.4", "swiftmailer/swiftmailer": "^6.2",
"symfony/cache": "^3.3 || ^4", "symfony/cache": "^3.3 || ^4",
"symfony/config": "^3.2 || ^4", "symfony/config": "^3.2 || ^4",
"symfony/translation": "^2.8 || ^3 || ^4", "symfony/translation": "^2.8 || ^3 || ^4",
@ -90,6 +90,9 @@
}, },
"files": [ "files": [
"src/includes/constants.php" "src/includes/constants.php"
],
"classmap": [
"thirdparty/swiftmailer"
] ]
}, },
"include-path": [ "include-path": [

View File

@ -6,23 +6,36 @@ icon: envelope-open
# Email # Email
Creating and sending email in Silverstripe CMS is done through the [Email](api:SilverStripe\Control\Email\Email) and [Mailer](api:SilverStripe\Control\Email\Mailer) classes. This document Creating and sending email in Silverstripe CMS is done through the [Email](api:SilverStripe\Control\Email\Email) and [Mailer](api:SilverStripe\Control\Email\Mailer) classes. This document covers how to create an `Email` instance, customise it with a HTML template, then send it through a custom `Mailer`.
covers how to create an `Email` instance, customise it with a HTML template, then send it through a custom `Mailer`.
## Configuration ## Configuration
Silverstripe CMS provides an API over the top of the [SwiftMailer](http://swiftmailer.org/) PHP library which comes with an Silverstripe CMS provides an API over the top of the [SwiftMailer](http://swiftmailer.org/) PHP library which comes with an extensive list of "transports" for sending mail via different services.
extensive list of "transports" for sending mail via different services.
Out of the box, Silverstripe CMS will use the built-in PHP `mail()` command via the `Swift_MailTransport` class. If you'd For legacy reasons, Silverstripe CMS 4 defaults to using the built-in PHP `mail()` command via a deprecated class `Swift_MailTransport`. However, using this layer is less secure and is strongly discouraged.
like to use a more robust transport to send mail you can swap out the transport used by the `Mailer` via config:
It's highly recommended you upgrade to a more robust transport for additional security. The Sendmail transport is the most common one. The `sendmail` binary is widely available across most Linux/Unix servers.
You can use any of the Transport classes provided natively by SwiftMailer. There are also countless PHP libraries offering custom Transports to integrate with third party mailing service:
- read the [SwiftMailer Transport Types documentation](https://swiftmailer.symfony.com/docs/sending.html#transport-types) for a full list of native Transport
- search [Packagist for SwiftMailer Transport](https://packagist.org/?query=SwiftMailer+Transport) to discover additional third party integrations
To swap out the transport used by the `Mailer`, create a file `app/_config/email.yml`
To use a `sendmail` binary:
```yml ```yml
---
Name: myemailconfig
After:
- '#emailconfig'
---
SilverStripe\Core\Injector\Injector: SilverStripe\Core\Injector\Injector:
Swift_Transport: Swift_SendmailTransport Swift_Transport:
class: Swift_SendmailTransport
``` ```
For example, to use SMTP, create a file `app/_config/email.yml`: To use SMTP:
```yml ```yml
--- ---
@ -45,6 +58,42 @@ SilverStripe\Core\Injector\Injector:
Note the usage of backticks to designate environment variables for the credentials - ensure you set these in your `.env` file or in your webserver configuration. Note the usage of backticks to designate environment variables for the credentials - ensure you set these in your `.env` file or in your webserver configuration.
### Mailer Configuration for dev environments
You may wish to use a different mailer configuration in your development environment. This can be used to suppress outgoing messages or to capture them for debugging purposes in a service like [MailCatcher](https://mailcatcher.me/).
You can suppress all emails by using the [`Swift_Transport_NullTransport`](https://github.com/swiftmailer/swiftmailer/blob/master/lib/classes/Swift/Transport/NullTransport.php).
```yml
---
Name: mydevemailconfig
After:
- '#emailconfig'
Only:
environment: dev
---
SilverStripe\Core\Injector\Injector:
Swift_Transport:
class: Swift_Transport_NullTransport
```
If you're using MailCatcher, or a similar tool, you can tell `Swift_SendmailTransport` to use a different binary.
```yml
---
Name: mydevemailconfig
After:
- '#emailconfig'
Only:
environment: dev
---
SilverStripe\Core\Injector\Injector:
Swift_Transport:
class: Swift_SendmailTransport
constructor:
0: '/usr/bin/env catchmail -t'
```
## Usage ## Usage
### Sending plain text only ### Sending plain text only
@ -224,6 +273,7 @@ SilverStripe\Core\Injector\Injector:
For further information on SwiftMailer, consult their docs: http://swiftmailer.org/docs/introduction.html For further information on SwiftMailer, consult their docs: http://swiftmailer.org/docs/introduction.html
## API Documentation ## API Documentation
* [Email](api:SilverStripe\Control\Email\Email) * [Email](api:SilverStripe\Control\Email\Email)

View File

@ -2,6 +2,9 @@
namespace SilverStripe\Control\Email; namespace SilverStripe\Control\Email;
use DateTime;
use Egulias\EmailValidator\EmailValidator;
use Egulias\EmailValidator\Validation\RFCValidation;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use SilverStripe\Control\HTTP; use SilverStripe\Control\HTTP;
use SilverStripe\Core\Convert; use SilverStripe\Core\Convert;
@ -99,7 +102,8 @@ class Email extends ViewableData
*/ */
public static function is_valid_address($address) public static function is_valid_address($address)
{ {
return \Swift_Validate::email($address); $validator = new EmailValidator();
return $validator->isValid($address, new RFCValidation());
} }
/** /**
@ -269,7 +273,9 @@ class Email extends ViewableData
*/ */
public function setSwiftMessage($swiftMessage) public function setSwiftMessage($swiftMessage)
{ {
$swiftMessage->setDate(DBDatetime::now()->getTimestamp()); $dateTime = new DateTime();
$dateTime->setTimestamp(DBDatetime::now()->getTimestamp());
$swiftMessage->setDate($dateTime);
if (!$swiftMessage->getFrom() && ($defaultFrom = $this->config()->get('admin_email'))) { if (!$swiftMessage->getFrom() && ($defaultFrom = $this->config()->get('admin_email'))) {
$swiftMessage->setFrom($defaultFrom); $swiftMessage->setFrom($defaultFrom);
} }
@ -451,6 +457,9 @@ class Email extends ViewableData
return $this; return $this;
} }
/**
* @return mixed
*/
public function getReplyTo() public function getReplyTo()
{ {
return $this->getSwiftMessage()->getReplyTo(); return $this->getSwiftMessage()->getReplyTo();

View File

@ -40,7 +40,7 @@ class SwiftPlugin implements \Swift_Events_SendListener
} }
/** /**
* @param \Swift_Mime_Message $message * @param \Swift_Message $message
* @param array|string $to * @param array|string $to
*/ */
protected function setTo($message, $to) protected function setTo($message, $to)
@ -62,7 +62,7 @@ class SwiftPlugin implements \Swift_Events_SendListener
} }
/** /**
* @param \Swift_Mime_Message $message * @param \Swift_Message $message
* @param array|string $from * @param array|string $from
*/ */
protected function setFrom($message, $from) protected function setFrom($message, $from)

View File

@ -2,6 +2,7 @@
namespace SilverStripe\Control\Tests\Email; namespace SilverStripe\Control\Tests\Email;
use DateTime;
use PHPUnit_Framework_MockObject_MockObject; use PHPUnit_Framework_MockObject_MockObject;
use SilverStripe\Control\Email\Email; use SilverStripe\Control\Email\Email;
use SilverStripe\Control\Email\Mailer; use SilverStripe\Control\Email\Mailer;
@ -269,9 +270,12 @@ class EmailTest extends SapphireTest
$email = new Email(); $email = new Email();
$swiftMessage = new Swift_Message(); $swiftMessage = new Swift_Message();
$email->setSwiftMessage($swiftMessage); $email->setSwiftMessage($swiftMessage);
$dateTime = new DateTime();
$dateTime->setTimestamp(DBDatetime::now()->getTimestamp());
$email->getSwiftMessage()->setDate($dateTime);
$this->assertCount(1, $email->getFrom()); $this->assertCount(1, $email->getFrom());
$this->assertContains('admin@example.com', array_keys($swiftMessage->getFrom())); $this->assertContains('admin@example.com', array_keys($swiftMessage->getFrom()));
$this->assertEquals(strtotime('2017-01-01 07:00:00'), $swiftMessage->getDate()); $this->assertEquals(strtotime('2017-01-01 07:00:00'), $swiftMessage->getDate()->getTimestamp());
$this->assertEquals($swiftMessage, $email->getSwiftMessage()); $this->assertEquals($swiftMessage, $email->getSwiftMessage());
// check from field is retained // check from field is retained

View File

@ -0,0 +1,75 @@
<?php
/**
* This file was copied in from swiftmailer/swiftmailer v5.4.12 after it was removed from switftmailer v6
* It has been slightly modified to meet phpcs standards and initialise Swift_DependencyContainer
*/
/*
* This file is part of SwiftMailer.
* (c) 2004-2009 Chris Corbyn
*
* For the full copyright and license information, please view the LICENSE file (MIT)
* https://github.com/swiftmailer/swiftmailer/blob/181b89f18a90f8925ef805f950d47a7190e9b950/LICENSE
*/
/**
* Sends Messages using the mail() function.
*
* @author Chris Corbyn
*
* at deprecated since 5.4.5 (to be removed in 6.0)
*/
// @codingStandardsIgnoreStart
// ignore missing namespace
class Swift_MailTransport extends Swift_Transport_MailTransport
// @codingStandardsIgnoreEnd
{
/**
* Create a new MailTransport, optionally specifying $extraParams.
*
* @param string $extraParams
*/
public function __construct($extraParams = '-f%s')
{
call_user_func_array(
[$this, 'Swift_Transport_MailTransport::__construct'],
$this->getDependencies()
);
$this->setExtraParams($extraParams);
}
/**
* Create a new MailTransport instance.
*
* @param string $extraParams To be passed to mail()
*
* @return self
*/
public static function newInstance($extraParams = '-f%s')
{
return new self($extraParams);
}
/**
* Add in deps for MailTransport which was removed as part of SwiftMailer v6
* @see transport_deps.php
*
* @return array
*/
private function getDependencies(): array
{
$deps = Swift_DependencyContainer::getInstance()->createDependenciesFor('transport.mail');
if (empty($deps)) {
Swift_DependencyContainer::getInstance()
->register('transport.mail')
->asNewInstanceOf('Swift_Transport_MailTransport')
->withDependencies(['transport.mailinvoker', 'transport.eventdispatcher'])
->register('transport.mailinvoker')
->asSharedInstanceOf('Swift_Transport_SimpleMailInvoker');
$deps = Swift_DependencyContainer::getInstance()->createDependenciesFor('transport.mail');
}
return $deps;
}
}

View File

@ -0,0 +1,40 @@
<?php
/**
* This file was copied in from swiftmailer/swiftmailer v5.4.12 after it was removed from switftmailer v6
* It has been slightly modified to meet phpcs standards
*/
/*
* This file is part of SwiftMailer.
* (c) 2004-2009 Chris Corbyn
*
* For the full copyright and license information, please view the LICENSE file (MIT)
* https://github.com/swiftmailer/swiftmailer/blob/181b89f18a90f8925ef805f950d47a7190e9b950/LICENSE
*/
/**
* This interface intercepts calls to the mail() function.
*
* @author Chris Corbyn
*/
// @codingStandardsIgnoreStart
// ignore missing namespace
interface Swift_Transport_MailInvoker
// @codingStandardsIgnoreEnd
{
/**
* Send mail via the mail() function.
*
* This method takes the same arguments as PHP mail().
*
* @param string $to
* @param string $subject
* @param string $body
* @param string $headers
* @param string $extraParams
*
* @return bool
*/
public function mail($to, $subject, $body, $headers = null, $extraParams = null);
}

View File

@ -0,0 +1,313 @@
<?php
/**
* This file was copied in from swiftmailer/swiftmailer v5.4.12 after it was removed from switftmailer v6
* It has been slightly modified to meet phpcs standards and to update method signatures to match the swiftmailer v6
*/
/*
* This file is part of SwiftMailer.
* (c) 2004-2009 Chris Corbyn
*
* For the full copyright and license information, please view the LICENSE file (MIT)
* https://github.com/swiftmailer/swiftmailer/blob/181b89f18a90f8925ef805f950d47a7190e9b950/LICENSE
*/
/**
* Sends Messages using the mail() function.
*
* It is advised that users do not use this transport if at all possible
* since a number of plugin features cannot be used in conjunction with this
* transport due to the internal interface in PHP itself.
*
* The level of error reporting with this transport is incredibly weak, again
* due to limitations of PHP's internal mail() function. You'll get an
* all-or-nothing result from sending.
*
* @author Chris Corbyn
*
* at deprecated since 5.4.5 (to be removed in 6.0)
*/
// @codingStandardsIgnoreStart
// ignore missing namespace
class Swift_Transport_MailTransport implements Swift_Transport
// @codingStandardsIgnoreEnd
{
/** Additional parameters to pass to mail() */
private $_extraParams = '-f%s';
/** The event dispatcher from the plugin API */
private $_eventDispatcher;
/** An invoker that calls the mail() function */
private $_invoker;
/**
* Create a new MailTransport with the $log.
*
* @param Swift_Transport_MailInvoker $invoker
* @param Swift_Events_EventDispatcher $eventDispatcher
*/
public function __construct(Swift_Transport_MailInvoker $invoker, Swift_Events_EventDispatcher $eventDispatcher)
{
// @trigger_error(sprintf('The %s class is deprecated since version 5.4.5 and will be removed in 6.0. Use the Sendmail or SMTP transport instead.', __CLASS__), E_USER_DEPRECATED);
$this->_invoker = $invoker;
$this->_eventDispatcher = $eventDispatcher;
}
/**
* Not used.
*/
public function isStarted()
{
return false;
}
/**
* Not used.
*/
public function start()
{
}
/**
* Not used.
*/
public function stop()
{
}
/**
* Set the additional parameters used on the mail() function.
*
* This string is formatted for sprintf() where %s is the sender address.
*
* @param string $params
*
* @return $this
*/
public function setExtraParams($params)
{
$this->_extraParams = $params;
return $this;
}
/**
* Get the additional parameters used on the mail() function.
*
* This string is formatted for sprintf() where %s is the sender address.
*
* @return string
*/
public function getExtraParams()
{
return $this->_extraParams;
}
/**
* Send the given Message.
*
* Recipient/sender data will be retrieved from the Message API.
* The return value is the number of recipients who were accepted for delivery.
*
* @param Swift_Mime_Message $message
* @param string[] $failedRecipients An array of failures by-reference
*
* @return int
*/
public function send(Swift_Mime_SimpleMessage $message, &$failedRecipients = null)
{
$failedRecipients = (array) $failedRecipients;
if ($evt = $this->_eventDispatcher->createSendEvent($this, $message)) {
$this->_eventDispatcher->dispatchEvent($evt, 'beforeSendPerformed');
if ($evt->bubbleCancelled()) {
return 0;
}
}
$count = (
count((array) $message->getTo())
+ count((array) $message->getCc())
+ count((array) $message->getBcc())
);
$toHeader = $message->getHeaders()->get('To');
$subjectHeader = $message->getHeaders()->get('Subject');
if (0 === $count) {
$this->_throwException(new Swift_TransportException('Cannot send message without a recipient'));
}
$to = $toHeader ? $toHeader->getFieldBody() : '';
$subject = $subjectHeader ? $subjectHeader->getFieldBody() : '';
$reversePath = $this->_getReversePath($message);
// Remove headers that would otherwise be duplicated
$message->getHeaders()->remove('To');
$message->getHeaders()->remove('Subject');
$messageStr = $message->toString();
if ($toHeader) {
$message->getHeaders()->set($toHeader);
}
$message->getHeaders()->set($subjectHeader);
// Separate headers from body
if (false !== $endHeaders = strpos($messageStr, "\r\n\r\n")) {
$headers = substr($messageStr, 0, $endHeaders) . "\r\n"; //Keep last EOL
$body = substr($messageStr, $endHeaders + 4);
} else {
$headers = $messageStr . "\r\n";
$body = '';
}
unset($messageStr);
if ("\r\n" != PHP_EOL) {
// Non-windows (not using SMTP)
$headers = str_replace("\r\n", PHP_EOL, $headers);
$subject = str_replace("\r\n", PHP_EOL, $subject);
$body = str_replace("\r\n", PHP_EOL, $body);
$to = str_replace("\r\n", PHP_EOL, $to);
} else {
// Windows, using SMTP
$headers = str_replace("\r\n.", "\r\n..", $headers);
$subject = str_replace("\r\n.", "\r\n..", $subject);
$body = str_replace("\r\n.", "\r\n..", $body);
$to = str_replace("\r\n.", "\r\n..", $to);
}
if ($this->_invoker->mail($to, $subject, $body, $headers, $this->_formatExtraParams($this->_extraParams, $reversePath))) {
if ($evt) {
$evt->setResult(Swift_Events_SendEvent::RESULT_SUCCESS);
$evt->setFailedRecipients($failedRecipients);
$this->_eventDispatcher->dispatchEvent($evt, 'sendPerformed');
}
} else {
$failedRecipients = array_merge(
$failedRecipients,
array_keys((array) $message->getTo()),
array_keys((array) $message->getCc()),
array_keys((array) $message->getBcc())
);
if ($evt) {
$evt->setResult(Swift_Events_SendEvent::RESULT_FAILED);
$evt->setFailedRecipients($failedRecipients);
$this->_eventDispatcher->dispatchEvent($evt, 'sendPerformed');
}
$message->generateId();
$count = 0;
}
return $count;
}
/**
* Register a plugin.
*
* @param Swift_Events_EventListener $plugin
*/
public function registerPlugin(Swift_Events_EventListener $plugin)
{
$this->_eventDispatcher->bindEventListener($plugin);
}
/** Throw a TransportException, first sending it to any listeners */
protected function _throwException(Swift_TransportException $e)
{
if ($evt = $this->_eventDispatcher->createTransportExceptionEvent($this, $e)) {
$this->_eventDispatcher->dispatchEvent($evt, 'exceptionThrown');
if (!$evt->bubbleCancelled()) {
throw $e;
}
} else {
throw $e;
}
}
/** Determine the best-use reverse path for this message */
private function _getReversePath(Swift_Message $message)
{
$return = $message->getReturnPath();
// casting to array to fixed incorrect PHPDOC in Swift_Mime_SimpleMessage which specifies @string
$sender = (array) $message->getSender();
$from = $message->getFrom();
$path = null;
if (!empty($return)) {
$path = $return;
} elseif (!empty($sender)) {
$keys = array_keys($sender);
$path = array_shift($keys);
} elseif (!empty($from)) {
$keys = array_keys($from);
$path = array_shift($keys);
}
return $path;
}
/**
* Fix CVE-2016-10074 by disallowing potentially unsafe shell characters.
*
* Note that escapeshellarg and escapeshellcmd are inadequate for our purposes, especially on Windows.
*
* @param string $string The string to be validated
*
* @return bool
*/
private function _isShellSafe($string)
{
// Future-proof
if (escapeshellcmd($string) !== $string || !in_array(escapeshellarg($string), ["'$string'", "\"$string\""])) {
return false;
}
$length = strlen($string);
for ($i = 0; $i < $length; ++$i) {
$c = $string[$i];
// All other characters have a special meaning in at least one common shell, including = and +.
// Full stop (.) has a special meaning in cmd.exe, but its impact should be negligible here.
// Note that this does permit non-Latin alphanumeric characters based on the current locale.
if (!ctype_alnum($c) && strpos('@_-.', $c) === false) {
return false;
}
}
return true;
}
/**
* Return php mail extra params to use for invoker->mail.
*
* @param $extraParams
* @param $reversePath
*
* @return string|null
*/
private function _formatExtraParams($extraParams, $reversePath)
{
if (false !== strpos($extraParams, '-f%s')) {
if (empty($reversePath) || false === $this->_isShellSafe($reversePath)) {
$extraParams = str_replace('-f%s', '', $extraParams);
} else {
$extraParams = sprintf($extraParams, $reversePath);
}
}
return !empty($extraParams) ? $extraParams : null;
}
/**
* {@inheritdoc}
*/
public function ping()
{
}
}

View File

@ -0,0 +1,47 @@
<?php
/**
* This file was copied in from swiftmailer/swiftmailer v5.4.12 after it was removed from switftmailer v6
* It has been slightly modified to meet phpcs standards
*/
/*
* This file is part of SwiftMailer.
* (c) 2004-2009 Chris Corbyn
*
* For the full copyright and license information, please view the LICENSE file (MIT)
* https://github.com/swiftmailer/swiftmailer/blob/181b89f18a90f8925ef805f950d47a7190e9b950/LICENSE
*/
/**
* This is the implementation class for {@link Swift_Transport_MailInvoker}.
*
* @author Chris Corbyn
*/
// @codingStandardsIgnoreStart
// ignore missing namespace
class Swift_Transport_SimpleMailInvoker implements Swift_Transport_MailInvoker
// @codingStandardsIgnoreEnd* It has been slightly modified to meet phpcs standards
{
/**
* Send mail via the mail() function.
*
* This method takes the same arguments as PHP mail().
*
* @param string $to
* @param string $subject
* @param string $body
* @param string $headers
* @param string $extraParams
*
* @return bool
*/
public function mail($to, $subject, $body, $headers = null, $extraParams = null)
{
if (!ini_get('safe_mode')) {
return @mail($to, $subject, $body, $headers, $extraParams);
}
return @mail($to, $subject, $body, $headers);
}
}