NEW Migrate from swiftmailer/swiftmailer to symfony/mailer

This commit is contained in:
Steve Boyd 2022-10-19 15:16:14 +13:00
parent 83a09daa3a
commit 2e85674ccc
20 changed files with 898 additions and 2156 deletions

View File

@ -1,12 +0,0 @@
---
Name: emailconfig
---
SilverStripe\Core\Injector\Injector:
Swift_Transport: Swift_MailTransport
Swift_Mailer:
constructor:
- '%$Swift_Transport'
SilverStripe\Control\Email\Mailer:
class: SilverStripe\Control\Email\SwiftMailer
properties:
SwiftMailer: '%$Swift_Mailer'

28
_config/mailer.yml Normal file
View File

@ -0,0 +1,28 @@
---
Name: mailer
---
SilverStripe\Core\Injector\Injector:
Symfony\Component\Mailer\MailerInterface:
class: Symfony\Component\Mailer\Mailer
constructor:
transport: '%$Symfony\Component\Mailer\Transport\TransportInterface'
Symfony\Component\EventDispatcher\EventDispatcherInterface.mailer:
class: Symfony\Component\EventDispatcher\EventDispatcher
calls:
- [addSubscriber, ['%$SilverStripe\Control\Email\MailerSubscriber']]
Symfony\Component\Mailer\Transport\TransportInterface:
factory: Symfony\Component\Mailer\Transport
factory_method: fromDsn
constructor:
dsn: 'sendmail://default'
dispatcher: '%$Symfony\Component\EventDispatcher\EventDispatcherInterface.mailer'
---
Name: mailer-dsn-env
After: '*'
Only:
envvarset: MAILER_DSN
---
SilverStripe\Core\Injector\Injector:
Symfony\Component\Mailer\Transport\TransportInterface:
constructor:
dsn: '`MAILER_DSN`'

View File

@ -37,10 +37,11 @@
"silverstripe/assets": "^2",
"silverstripe/vendor-plugin": "^2",
"sminnee/callbacklist": "^0.1.1",
"swiftmailer/swiftmailer": "^6.3.0",
"symfony/cache": "^6.1",
"symfony/config": "^6.1",
"symfony/filesystem": "^6.1",
"symfony/mailer": "^6.1",
"symfony/mime": "^6.1",
"symfony/translation": "^6.1",
"symfony/yaml": "^6.1",
"ext-ctype": "*",
@ -96,9 +97,6 @@
},
"files": [
"src/includes/constants.php"
],
"classmap": [
"thirdparty/swiftmailer"
]
},
"include-path": [

View File

@ -2,176 +2,131 @@
namespace SilverStripe\Control\Email;
use DateTime;
use Exception;
use RuntimeException;
use Egulias\EmailValidator\EmailValidator;
use Egulias\EmailValidator\Validation\RFCValidation;
use SilverStripe\Control\Director;
use SilverStripe\Control\HTTP;
use SilverStripe\Core\Convert;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Environment;
use SilverStripe\Core\Extensible;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\Deprecation;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\View\ArrayData;
use SilverStripe\View\Requirements;
use SilverStripe\View\SSViewer;
use SilverStripe\View\ThemeResourceLoader;
use SilverStripe\View\ViewableData;
use Swift_Message;
use Swift_Mime_SimpleMessage;
use Swift_MimePart;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email as SymfonyEmail;
use Symfony\Component\Mime\Part\AbstractPart;
/**
* Class to support sending emails.
*/
class Email extends ViewableData
class Email extends SymfonyEmail
{
/**
* @var array
* @config
*/
private static $send_all_emails_to = [];
/**
* @var array
* @config
*/
private static $cc_all_emails_to = [];
/**
* @var array
* @config
*/
private static $bcc_all_emails_to = [];
/**
* @var array
* @config
*/
private static $send_all_emails_from = [];
use Configurable;
use Extensible;
use Injectable;
private static string|array $send_all_emails_to = [];
private static string|array $cc_all_emails_to = [];
private static string|array $bcc_all_emails_to = [];
private static string|array $send_all_emails_from = [];
/**
* The default "from" email address or array of [email => name], or the email address as a string
* This will be set in the config on a site-by-site basis
* @see https://docs.silverstripe.org/en/4/developer_guides/email/#administrator-emails
*
* @config
* @var string|array The default administrator email address or array of [email => name]
*/
private static $admin_email = null;
private static string|array $admin_email = '';
/**
* @var Swift_Message
* The name of the HTML template to render the email with (without *.ss extension)
*/
private $swiftMessage;
private string $HTMLTemplate = '';
/**
* @var string The name of the HTML template to render the email with (without *.ss extension)
* The name of the plain text template to render the plain part of the email with
*/
private $HTMLTemplate = null;
private string $plainTemplate = '';
/**
* @var string The name of the plain text template to render the plain part of the email with
*/
private $plainTemplate = null;
/**
* @var Swift_MimePart
*/
private $plainPart;
/**
* @var array|ViewableData Additional data available in a template.
* Additional data available in a template.
* Used in the same way than {@link ViewableData->customize()}.
*/
private $data = [];
private ViewableData $data;
/**
* @var array
*/
private $failedRecipients = [];
private bool $dataHasBeenSet = false;
/**
* Checks for RFC822-valid email format.
*
* @param string $address
* @return boolean
*
* @copyright Cal Henderson <cal@iamcal.com>
* This code is licensed under a Creative Commons Attribution-ShareAlike 2.5 License
* http://creativecommons.org/licenses/by-sa/2.5/
*/
public static function is_valid_address($address)
public static function is_valid_address(string $address): bool
{
$validator = new EmailValidator();
return $validator->isValid($address, new RFCValidation());
}
/**
* Get send_all_emails_to
*
* @return array Keys are addresses, values are names
*/
public static function getSendAllEmailsTo()
public static function getSendAllEmailsTo(): array
{
return static::mergeConfiguredEmails('send_all_emails_to', 'SS_SEND_ALL_EMAILS_TO');
return static::mergeConfiguredAddresses('send_all_emails_to', 'SS_SEND_ALL_EMAILS_TO');
}
/**
* Get cc_all_emails_to
*
* @return array
*/
public static function getCCAllEmailsTo()
public static function getCCAllEmailsTo(): array
{
return static::mergeConfiguredEmails('cc_all_emails_to', 'SS_CC_ALL_EMAILS_TO');
return static::mergeConfiguredAddresses('cc_all_emails_to', 'SS_CC_ALL_EMAILS_TO');
}
/**
* Get bcc_all_emails_to
*
* @return array
*/
public static function getBCCAllEmailsTo()
public static function getBCCAllEmailsTo(): array
{
return static::mergeConfiguredEmails('bcc_all_emails_to', 'SS_BCC_ALL_EMAILS_TO');
return static::mergeConfiguredAddresses('bcc_all_emails_to', 'SS_BCC_ALL_EMAILS_TO');
}
/**
* Get send_all_emails_from
*
* @return array
*/
public static function getSendAllEmailsFrom()
public static function getSendAllEmailsFrom(): array
{
return static::mergeConfiguredEmails('send_all_emails_from', 'SS_SEND_ALL_EMAILS_FROM');
return static::mergeConfiguredAddresses('send_all_emails_from', 'SS_SEND_ALL_EMAILS_FROM');
}
/**
* Normalise email list from config merged with env vars
*
* @param string $config Config key
* @param string $env Env variable key
* @return array Array of email addresses
* @return Address[]
*/
protected static function mergeConfiguredEmails($config, $env)
private static function mergeConfiguredAddresses(string $configKey, string $envKey): array
{
// Normalise config list
$normalised = [];
$source = (array)static::config()->get($config);
foreach ($source as $address => $name) {
if ($address && !is_numeric($address)) {
$normalised[$address] = $name;
} elseif ($name) {
$normalised[$name] = null;
$addresses = [];
$config = (array) static::config()->get($configKey);
$addresses = self::convertConfigToAddreses($config);
$env = Environment::getEnv($envKey);
if ($env) {
$addresses = array_merge($addresses, self::convertConfigToAddreses($env));
}
return $addresses;
}
private static function convertConfigToAddreses(array|string $config): array
{
$addresses = [];
if (is_array($config)) {
foreach ($config as $key => $val) {
if (filter_var($key, FILTER_VALIDATE_EMAIL)) {
$addresses[] = new Address($key, $val);
} else {
$addresses[] = new Address($val);
}
}
} else {
$addresses[] = new Address($config);
}
$extra = Environment::getEnv($env);
if ($extra) {
$normalised[$extra] = null;
}
return $normalised;
return $addresses;
}
/**
@ -179,23 +134,19 @@ class Email extends ViewableData
* At the moment only simple string substitutions,
* which are not 100% safe from email harvesting.
*
* @param string $email Email-address
* @param string $method Method for obfuscating/encoding the address
* - 'direction': Reverse the text and then use CSS to put the text direction back to normal
* - 'visible': Simple string substitution ('@' to '[at]', '.' to '[dot], '-' to [dash])
* - 'hex': Hexadecimal URL-Encoding - useful for mailto: links
* @return string
* $method defines the method for obfuscating/encoding the address
* - 'direction': Reverse the text and then use CSS to put the text direction back to normal
* - 'visible': Simple string substitution ('@' to '[at]', '.' to '[dot], '-' to [dash])
* - 'hex': Hexadecimal URL-Encoding - useful for mailto: links
*/
public static function obfuscate($email, $method = 'visible')
public static function obfuscate(string $email, string $method = 'visible'): string
{
switch ($method) {
case 'direction':
Requirements::customCSS('span.codedirection { unicode-bidi: bidi-override; direction: rtl; }', 'codedirectionCSS');
return '<span class="codedirection">' . strrev($email) . '</span>';
case 'visible':
$obfuscated = ['@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] '];
return strtr($email, $obfuscated);
case 'hex':
$encoded = '';
@ -203,36 +154,27 @@ class Email extends ViewableData
for ($x = 0; $x < $emailLength; $x++) {
$encoded .= '&#x' . bin2hex($email[$x]) . ';';
}
return $encoded;
default:
user_error('Email::obfuscate(): Unknown obfuscation method', E_USER_NOTICE);
return $email;
}
}
/**
* Email constructor.
* @param string|array|null $from
* @param string|array|null $to
* @param string|null $subject
* @param string|null $body
* @param string|array|null $cc
* @param string|array|null $bcc
* @param string|null $returnPath
*/
public function __construct(
$from = null,
$to = null,
$subject = null,
$body = null,
$cc = null,
$bcc = null,
$returnPath = null
string $from = '',
string $to = '',
string $subject = '',
string $body = '',
string $cc = '',
string $bcc = '',
string $returnPath = ''
) {
parent::__construct();
if ($from) {
$this->setFrom($from);
} else {
$this->setFrom($this->getDefaultFrom());
}
if ($to) {
$this->setTo($to);
@ -252,52 +194,9 @@ class Email extends ViewableData
if ($returnPath) {
$this->setReturnPath($returnPath);
}
parent::__construct();
$this->data = ViewableData::create();
}
/**
* @deprecated 4.12.0 Will be removed without equivalent functionality to replace it
*
* @return Swift_Message
*/
public function getSwiftMessage()
{
Deprecation::notice('4.12.0', 'Will be removed without equivalent functionality to replace it');
if (!$this->swiftMessage) {
$message = new Swift_Message(null, null, 'text/html', 'utf-8');
// Set priority to fix PHP 8.1 SimpleMessage::getPriority() sscanf() null parameter
$message->setPriority(Swift_Mime_SimpleMessage::PRIORITY_NORMAL);
$this->setSwiftMessage($message);
}
return $this->swiftMessage;
}
/**
* @deprecated 4.12.0 Will be removed without equivalent functionality to replace it
*
* @param Swift_Message $swiftMessage
*
* @return $this
*/
public function setSwiftMessage($swiftMessage)
{
Deprecation::notice('4.12.0', 'Will be removed without equivalent functionality to replace it');
$dateTime = new DateTime();
$dateTime->setTimestamp(DBDatetime::now()->getTimestamp());
$swiftMessage->setDate($dateTime);
if (!$swiftMessage->getFrom()) {
$swiftMessage->setFrom($this->getDefaultFrom());
}
$this->swiftMessage = $swiftMessage;
return $this;
}
/**
* @return string
*/
private function getDefaultFrom(): string
{
// admin_email can have a string or an array config
@ -324,438 +223,178 @@ class Email extends ViewableData
}
/**
* @return string[]
* Passing a string of HTML for $body will have no affect if you also call either setData() or addData()
*/
public function getFrom()
public function setBody(AbstractPart|string $body = null): static
{
return $this->getSwiftMessage()->getFrom();
if ($body instanceof AbstractPart) {
// pass to Symfony\Component\Mime\Message::setBody()
return parent::setBody($body);
}
// Set HTML content directly.
return $this->html($body);
}
/**
* @param string|array $address
* @return string|array
* The following arguments combinations are valid
* a) $address = 'my@email.com', $name = 'My name'
* b) $address = ['my@email.com' => 'My name', 'other@email.com' => 'My other name']
* c) $address = ['my@email.com' => 'My name', 'other@email.com']
*/
private function sanitiseAddress($address)
private function createAddressArray(string|array $address, $name = ''): array
{
if (is_array($address)) {
return array_map('trim', $address ?? []);
$ret = [];
foreach ($address as $key => $val) {
$addr = is_numeric($key) ? $val : $key;
$name2 = is_numeric($key) ? '' : $val;
$ret[] = new Address($addr, $name2);
}
return $ret;
}
return trim($address ?? '');
return [new Address($address, $name)];
}
/**
* @param string|array $address
* @param string|null $name
* @return $this
* @see createAddressArray()
*/
public function setFrom($address, $name = null)
public function setFrom(string|array $address, string $name = ''): static
{
$address = $this->sanitiseAddress($address);
$this->getSwiftMessage()->setFrom($address, $name);
return $this;
return $this->from(...$this->createAddressArray($address, $name));
}
/**
* @param string|array $address
* @param string|null $name
* @return $this
* @see createAddressArray()
*/
public function addFrom($address, $name = null)
public function setTo(string|array $address, $name = ''): static
{
$address = $this->sanitiseAddress($address);
$this->getSwiftMessage()->addFrom($address, $name);
return $this;
return $this->to(...$this->createAddressArray($address, $name));
}
/**
* @return string
* @see createAddressArray()
*/
public function getSender()
public function setCC(string|array $address, string $name = ''): static
{
return $this->getSwiftMessage()->getSender();
return $this->cc(...$this->createAddressArray($address, $name));
}
/**
* @param string $address
* @param string|null $name
* @return $this
* @see createAddressArray()
*/
public function setSender($address, $name = null)
public function setBCC(string|array $address, string $name = ''): static
{
$address = $this->sanitiseAddress($address);
$this->getSwiftMessage()->setSender($address, $name);
return $this;
return $this->bcc(...$this->createAddressArray($address, $name));
}
/**
* @return string
*/
public function getReturnPath()
public function setSender(string $address, string $name = ''): static
{
return $this->getSwiftMessage()->getReturnPath();
return $this->sender(new Address($address, $name));
}
/**
* The bounce handler address
*
* @param string $address Email address where bounce notifications should be sent
* @return $this
*/
public function setReturnPath($address)
public function setReplyTo(string $address, string $name = ''): static
{
$address = $this->sanitiseAddress($address);
$this->getSwiftMessage()->setReturnPath($address);
return $this;
return $this->replyTo(new Address($address, $name));
}
/**
* @return array
*/
public function getTo()
public function setSubject(string $subject): static
{
return $this->getSwiftMessage()->getTo();
return $this->subject($subject);
}
/**
* Set recipient(s) of the email
*
* To send to many, pass an array:
* ['me@example.com' => 'My Name', 'other@example.com'];
*
* @param string|array $address The message recipient(s) - if sending to multiple, use an array of address => name
* @param string|null $name The name of the recipient (if one)
* @return $this
*/
public function setTo($address, $name = null)
public function setReturnPath(string $address): static
{
$address = $this->sanitiseAddress($address);
$this->getSwiftMessage()->setTo($address, $name);
return $this;
return $this->returnPath($address);
}
/**
* @param string|array $address
* @param string|null $name
* @return $this
*/
public function addTo($address, $name = null)
public function setPriority(int $priority): static
{
$address = $this->sanitiseAddress($address);
$this->getSwiftMessage()->addTo($address, $name);
return $this;
}
/**
* @return array
*/
public function getCC()
{
return $this->getSwiftMessage()->getCc();
}
/**
* @param string|array $address
* @param string|null $name
* @return $this
*/
public function setCC($address, $name = null)
{
$address = $this->sanitiseAddress($address);
$this->getSwiftMessage()->setCc($address, $name);
return $this;
}
/**
* @param string|array $address
* @param string|null $name
* @return $this
*/
public function addCC($address, $name = null)
{
$address = $this->sanitiseAddress($address);
$this->getSwiftMessage()->addCc($address, $name);
return $this;
}
/**
* @return array
*/
public function getBCC()
{
return $this->getSwiftMessage()->getBcc();
}
/**
* @param string|array $address
* @param string|null $name
* @return $this
*/
public function setBCC($address, $name = null)
{
$address = $this->sanitiseAddress($address);
$this->getSwiftMessage()->setBcc($address, $name);
return $this;
}
/**
* @param string|array $address
* @param string|null $name
* @return $this
*/
public function addBCC($address, $name = null)
{
$address = $this->sanitiseAddress($address);
$this->getSwiftMessage()->addBcc($address, $name);
return $this;
}
/**
* @return mixed
*/
public function getReplyTo()
{
return $this->getSwiftMessage()->getReplyTo();
}
/**
* @param string|array $address
* @param string|null $name
* @return $this
*/
public function setReplyTo($address, $name = null)
{
$address = $this->sanitiseAddress($address);
$this->getSwiftMessage()->setReplyTo($address, $name);
return $this;
}
/**
* @param string|array $address
* @param string|null $name
* @return $this
*/
public function addReplyTo($address, $name = null)
{
$address = $this->sanitiseAddress($address);
$this->getSwiftMessage()->addReplyTo($address, $name);
return $this;
}
/**
* @return string
*/
public function getSubject()
{
return $this->getSwiftMessage()->getSubject();
}
/**
* @param string $subject The Subject line for the email
* @return $this
*/
public function setSubject($subject)
{
$this->getSwiftMessage()->setSubject($subject);
return $this;
}
/**
* @return int
*/
public function getPriority()
{
return $this->getSwiftMessage()->getPriority();
}
/**
* @param int $priority
* @return $this
*/
public function setPriority($priority)
{
$this->getSwiftMessage()->setPriority($priority);
return $this;
return $this->priority($priority);
}
/**
* @param string $path Path to file
* @param string $alias An override for the name of the file
* @param string $mime The mime type for the attachment
* @return $this
*/
public function addAttachment($path, $alias = null, $mime = null)
public function addAttachment(string $path, ?string $alias = null, ?string $mime = null): static
{
$attachment = \Swift_Attachment::fromPath($path);
if ($alias) {
$attachment->setFilename($alias);
}
if ($mime) {
$attachment->setContentType($mime);
}
$this->getSwiftMessage()->attach($attachment);
return $this->attachFromPath($path, $alias, $mime);
}
return $this;
public function addAttachmentFromData(string $data, string $name, string $mime = null): static
{
return $this->attach($data, $name, $mime);
}
/**
* @param string $data
* @param string $name
* @param string $mime
* @return $this
* Get data which is exposed to the template
*
* The following data is exposed via this method by default:
* IsEmail: used to detect if rendering an email template rather than a page template
* BaseUrl: used to get the base URL for the email
*/
public function addAttachmentFromData($data, $name, $mime = null)
public function getData(): ViewableData
{
$attachment = new \Swift_Attachment($data, $name);
if ($mime) {
$attachment->setContentType($mime);
$extraData = [
'IsEmail' => true,
'BaseURL' => Director::absoluteBaseURL(),
];
$data = clone $this->data;
foreach ($extraData as $key => $value) {
if (is_null($data->{$key})) {
$data->{$key} = $value;
}
}
$this->getSwiftMessage()->attach($attachment);
return $this;
$this->extend('updateGetData', $data);
return $data;
}
/**
* @return array|ViewableData The template data
* Set template data
*
* Calling setData() once means that any content set via text()/html()/setBody() will have no effect
*/
public function getData()
{
return $this->data;
}
/**
* @param array|ViewableData $data The template data to set
* @return $this
*/
public function setData($data)
public function setData(array|ViewableData $data)
{
if (is_array($data)) {
$data = ArrayData::create($data);
}
$this->data = $data;
$this->invalidateBody();
$this->dataHasBeenSet = true;
return $this;
}
/**
* @param string|array $name The data name to add or array to names => value
* @param string|null $value The value of the data to add
* @return $this
* Add data to be used in the template
*
* Calling addData() once means that any content set via text()/html()/setBody() will have no effect
*
* @param string|array $nameOrData can be either the name to add, or an array of [name => value]
*/
public function addData($name, $value = null)
public function addData(string|array $nameOrData, mixed $value = null): static
{
if (is_array($name)) {
$this->data = array_merge($this->data, $name);
} elseif (is_array($this->data)) {
$this->data[$name] = $value;
if (is_array($nameOrData)) {
foreach ($nameOrData as $key => $val) {
$this->data->{$key} = $val;
}
} else {
$this->data->$name = $value;
$this->data->{$nameOrData} = $value;
}
$this->invalidateBody();
$this->dataHasBeenSet = true;
return $this;
}
/**
* Remove a datum from the message
*
* @param string $name
* @return $this
* Remove a single piece of template data
*/
public function removeData($name)
public function removeData(string $name)
{
if (is_array($this->data)) {
unset($this->data[$name]);
} else {
$this->data->$name = null;
}
$this->invalidateBody();
$this->data->{$name} = null;
return $this;
}
/**
* @return string
*/
public function getBody()
{
return $this->getSwiftMessage()->getBody();
}
/**
* @param string $body The email body
* @return $this
*/
public function setBody($body)
{
$plainPart = $this->findPlainPart();
if ($plainPart) {
$this->getSwiftMessage()->detach($plainPart);
}
unset($plainPart);
$body = HTTP::absoluteURLs($body);
$this->getSwiftMessage()->setBody($body);
return $this;
}
/**
* @deprecated 4.12.0 Will be replaced with html()
*
* @return $this
*/
public function invalidateBody()
{
Deprecation::notice('4.12.0', 'Will be replaced with html()');
$this->setBody(null);
return $this;
}
/**
* @deprecated 4.12.0 Will be replaced with getData()
*
* @return string The base URL for the email
*/
public function BaseURL()
{
Deprecation::notice('4.12.0', 'Will be replaced with getData()');
return Director::absoluteBaseURL();
}
/**
* @deprecated Will be removed without equivalent functionality to replace it
*
* Debugging help
*
* @return string Debug info
*/
public function debug()
{
Deprecation::notice('4.12.0', 'Will be removed without equivalent functionality to replace it');
$this->render();
$class = static::class;
return "<h2>Email template {$class}:</h2>\n" . '<pre>' . $this->getSwiftMessage()->toString() . '</pre>';
}
/**
* @return string
*/
public function getHTMLTemplate()
public function getHTMLTemplate(): string
{
if ($this->HTMLTemplate) {
return $this->HTMLTemplate;
@ -769,237 +408,123 @@ class Email extends ViewableData
/**
* Set the template to render the email with
*
* @param string $template
* @return $this
*/
public function setHTMLTemplate($template)
public function setHTMLTemplate(string $template): static
{
if (substr($template ?? '', -3) == '.ss') {
$template = substr($template ?? '', 0, -3);
}
$this->HTMLTemplate = $template;
return $this;
}
/**
* Get the template to render the plain part with
*
* @return string
*/
public function getPlainTemplate()
public function getPlainTemplate(): string
{
return $this->plainTemplate;
}
/**
* Set the template to render the plain part with
*
* @param string $template
* @return $this
*/
public function setPlainTemplate($template)
public function setPlainTemplate(string $template): static
{
if (substr($template ?? '', -3) == '.ss') {
$template = substr($template ?? '', 0, -3);
}
$this->plainTemplate = $template;
return $this;
}
/**
* @deprecated 4.12.0 Will be removed without equivalent functionality to replace it
*
* @param array $recipients
* @return $this
*/
public function setFailedRecipients($recipients)
{
Deprecation::notice('4.12.0', 'Will be removed without equivalent functionality to replace it');
$this->failedRecipients = $recipients;
return $this;
}
/**
* @deprecated 4.12.0 Will be removed without equivalent functionality to replace it
*
* @return array
*/
public function getFailedRecipients()
{
Deprecation::notice('4.12.0', 'Will be removed without equivalent functionality to replace it');
return $this->failedRecipients;
}
/**
* @deprecated 4.12.0 Will be replaced with getData()
*
* Used by {@link SSViewer} templates to detect if we're rendering an email template rather than a page template
*
* @return bool
*/
public function IsEmail()
{
Deprecation::notice('4.12.0', 'Will be replaced with getData()');
return true;
}
/**
* Send the message to the recipients
*
* @return bool true if successful or array of failed recipients
*/
public function send()
public function send(): void
{
if (!$this->getBody()) {
$this->render();
}
if (!$this->hasPlainPart()) {
$this->generatePlainPartFromBody();
}
return Injector::inst()->get(Mailer::class)->send($this);
$this->updateHtmlAndTextWithRenderedTemplates();
Injector::inst()->get(MailerInterface::class)->send($this);
}
/**
* @return array|bool
* Send the message to the recipients as plain-only
*/
public function sendPlain()
public function sendPlain(): void
{
if (!$this->hasPlainPart()) {
$this->render(true);
}
return Injector::inst()->get(Mailer::class)->send($this);
$html = $this->getHtmlBody();
$this->updateHtmlAndTextWithRenderedTemplates(true);
$this->html(null);
Injector::inst()->get(MailerInterface::class)->send($this);
$this->html($html);
}
/**
* @deprecated 4.12.0 Will be removed without equivalent functionality to replace it
* Call html() and/or text() after rendering email templates
* If either body html or text were previously explicitly set, those values will not be overwritten
*
* Render the email
* @param bool $plainOnly Only render the message as plain text
* @return $this
* @param bool $plainOnly - if true then do not call html()
*/
public function render($plainOnly = false)
private function updateHtmlAndTextWithRenderedTemplates(bool $plainOnly = false): void
{
Deprecation::notice('4.12.0', 'Will be removed without equivalent functionality to replace it');
if ($existingPlainPart = $this->findPlainPart()) {
$this->getSwiftMessage()->detach($existingPlainPart);
}
unset($existingPlainPart);
// Respect explicitly set body
$htmlPart = $plainOnly ? null : $this->getBody();
$plainPart = $plainOnly ? $this->getBody() : null;
$htmlBody = $this->getHtmlBody();
$plainBody = $this->getTextBody();
// Ensure we can at least render something
$htmlTemplate = $this->getHTMLTemplate();
$plainTemplate = $this->getPlainTemplate();
if (!$htmlTemplate && !$plainTemplate && !$plainPart && !$htmlPart) {
return $this;
if (!$htmlTemplate && !$plainTemplate && !$plainBody && !$htmlBody) {
return;
}
$htmlRender = null;
$plainRender = null;
if ($htmlBody && !$this->dataHasBeenSet) {
$htmlRender = $htmlBody;
}
if ($plainBody && !$this->dataHasBeenSet) {
$plainRender = $plainBody;
}
// Do not interfere with emails styles
Requirements::clear();
// Render plain part
if ($plainTemplate && !$plainPart) {
$plainPart = $this->renderWith($plainTemplate, $this->getData())->Plain();
// Render plain
if (!$plainRender && $plainTemplate) {
$plainRender = $this->getData()->renderWith($plainTemplate)->Plain();
}
// Render HTML part, either if sending html email, or a plain part is lacking
if (!$htmlPart && $htmlTemplate && (!$plainOnly || empty($plainPart))) {
$htmlPart = $this->renderWith($htmlTemplate, $this->getData());
}
// Plain part fails over to generated from html
if (!$plainPart && $htmlPart) {
/** @var DBHTMLText $htmlPartObject */
$htmlPartObject = DBField::create_field('HTMLFragment', $htmlPart);
$plainPart = $htmlPartObject->Plain();
// Render HTML
if (!$htmlRender && $htmlTemplate) {
$htmlRender = $this->getData()->renderWith($htmlTemplate)->RAW();
}
// Rendering is finished
Requirements::restore();
// Fail if no email to send
if (!$plainPart && !$htmlPart) {
return $this;
// Plain render fallbacks to using the html render with html tags removed
if (!$plainRender && $htmlRender) {
// call html_entity_decode() to ensure any encoded HTML is also stripped inside ->Plain()
$dbField = DBField::create_field('HTMLFragment', html_entity_decode($htmlRender));
$plainRender = $dbField->Plain();
}
// Build HTML / Plain components
if ($htmlPart && !$plainOnly) {
$this->setBody($htmlPart);
$this->getSwiftMessage()->setContentType('text/html');
$this->getSwiftMessage()->setCharset('utf-8');
if ($plainPart) {
$this->getSwiftMessage()->addPart($plainPart, 'text/plain', 'utf-8');
}
} else {
if ($plainPart) {
$this->setBody($plainPart);
}
$this->getSwiftMessage()->setContentType('text/plain');
$this->getSwiftMessage()->setCharset('utf-8');
// Handle edge case where no template was found
if (!$htmlRender && $htmlBody) {
$htmlRender = $htmlBody;
}
return $this;
}
/**
* @deprecated 4.12.0 Will be removed without equivalent functionality to replace it
*
* @return Swift_MimePart|false
*/
public function findPlainPart()
{
Deprecation::notice('4.12.0', 'Will be removed without equivalent functionality to replace it');
foreach ($this->getSwiftMessage()->getChildren() as $child) {
if ($child instanceof Swift_MimePart && $child->getContentType() == 'text/plain') {
return $child;
}
if (!$plainRender && $plainBody) {
$plainRender = $plainBody;
}
return false;
}
/**
* @deprecated 4.12.0 Will be removed without equivalent functionality to replace it
*
* @return bool
*/
public function hasPlainPart()
{
Deprecation::notice('4.12.0', 'Will be removed without equivalent functionality to replace it');
if ($this->getSwiftMessage()->getContentType() === 'text/plain') {
return true;
if ($plainRender) {
$this->text($plainRender);
}
return (bool) $this->findPlainPart();
}
/**
* @deprecated 4.12.0 Will be removed without equivalent functionality to replace it
*
* Automatically adds a plain part to the email generated from the current Body
*
* @return $this
*/
public function generatePlainPartFromBody()
{
Deprecation::notice('4.12.0', 'Will be removed without equivalent functionality to replace it');
$plainPart = $this->findPlainPart();
if ($plainPart) {
$this->getSwiftMessage()->detach($plainPart);
if ($htmlRender && !$plainOnly) {
$this->html($htmlRender);
}
unset($plainPart);
$this->getSwiftMessage()->addPart(
Convert::xml2raw($this->getBody()),
'text/plain',
'utf-8'
);
return $this;
}
}

View File

@ -0,0 +1,100 @@
<?php
namespace SilverStripe\Control\Email;
use InvalidArgumentException;
use SilverStripe\Control\HTTP;
use SilverStripe\Core\Extensible;
use SilverStripe\Core\Injector\Injectable;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Mailer\Event\MessageEvent;
/**
* This subscriber is registered in BaseKernel->bootEmail()
*
* See https://symfony.com/doc/current/mailer.html#mailer-events for further info
*/
class MailerSubscriber implements EventSubscriberInterface
{
use Injectable;
use Extensible;
public static function getSubscribedEvents()
{
return [
MessageEvent::class => 'onMessage',
];
}
public function onMessage(MessageEvent $event): void
{
$email = $event->getMessage();
if (!($email instanceof Email)) {
throw new InvalidArgumentException('Message is not a ' . Email::class);
}
$this->applyConfig($email);
$this->updateUrls($email);
$this->extend('updateOnMessage', $email, $event);
}
private function applyConfig(Email $email): void
{
$sendAllTo = Email::getSendAllEmailsTo();
if (!empty($sendAllTo)) {
$this->setTo($email, $sendAllTo);
}
$ccAllTo = Email::getCCAllEmailsTo();
if (!empty($ccAllTo)) {
$email->addCc(...$ccAllTo);
}
$bccAllTo = Email::getBCCAllEmailsTo();
if (!empty($bccAllTo)) {
$email->addBcc(...$bccAllTo);
}
$sendAllFrom = Email::getSendAllEmailsFrom();
if (!empty($sendAllFrom)) {
$this->setFrom($email, $sendAllFrom);
}
}
private function setTo(Email $email, array $sendAllTo): void
{
$headers = $email->getHeaders();
// store the old data as X-Original-* Headers for debugging
if (!empty($email->getTo())) {
$headers->addMailboxListHeader('X-Original-To', $email->getTo());
}
if (!empty($email->getCc())) {
$headers->addMailboxListHeader('X-Original-Cc', $email->getCc());
}
if (!empty($email->getBcc())) {
$headers->addMailboxListHeader('X-Original-Bcc', $email->getBcc());
}
// set default recipient and remove all other recipients
$email->to(...$sendAllTo);
$email->cc(...[]);
$email->bcc(...[]);
}
private function setFrom(Email $email, array $sendAllFrom): void
{
$headers = $email->getHeaders();
if (!empty($email->getFrom())) {
$headers->addMailboxListHeader('X-Original-From', $email->getFrom());
}
$email->from(...$sendAllFrom);
}
private function updateUrls(Email $email): void
{
if ($email->getHtmlBody()) {
$email->html(HTTP::absoluteURLs($email->getHtmlBody()));
}
if ($email->getTextBody()) {
$email->text(HTTP::absoluteURLs($email->getTextBody()));
}
}
}

View File

@ -1,82 +0,0 @@
<?php
namespace SilverStripe\Control\Email;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Core\Injector\Injector;
use Swift_Mailer;
use Swift_Message;
/**
* @deprecated 4.12.0 Will be replaced with symfony/mailer
*
* Mailer objects are responsible for actually sending emails.
* The default Mailer class will use PHP's mail() function.
*/
class SwiftMailer implements Mailer
{
use Configurable;
use Injectable;
/**
* @var array
* @config
*/
private static $swift_plugins = [
SwiftPlugin::class,
];
/**
* @var Swift_Mailer
*/
private $swift;
/**
* @param Email $message
* @return bool Whether the sending was "successful" or not
*/
public function send($message)
{
$swiftMessage = $message->getSwiftMessage();
$failedRecipients = [];
$result = $this->sendSwift($swiftMessage, $failedRecipients);
$message->setFailedRecipients($failedRecipients);
return $result != 0;
}
/**
* @param Swift_Message $message
* @param array $failedRecipients
* @return int
*/
protected function sendSwift($message, &$failedRecipients = null)
{
return $this->getSwiftMailer()->send($message, $failedRecipients);
}
/**
* @return Swift_Mailer
*/
public function getSwiftMailer()
{
return $this->swift;
}
/**
* @param Swift_Mailer $swift
* @return $this
*/
public function setSwiftMailer($swift)
{
// register any required plugins
foreach ($this->config()->get('swift_plugins') as $plugin) {
$swift->registerPlugin(Injector::inst()->create($plugin));
}
$this->swift = $swift;
return $this;
}
}

View File

@ -1,83 +0,0 @@
<?php
namespace SilverStripe\Control\Email;
/**
* @deprecated 4.12.0 Will be replaced with symfony/mailer
*/
class SwiftPlugin implements \Swift_Events_SendListener
{
/**
* Before sending a message make sure all our overrides are taken into account
*
* @param \Swift_Events_SendEvent $evt
*/
public function beforeSendPerformed(\Swift_Events_SendEvent $evt)
{
/** @var \Swift_Message $message */
$message = $evt->getMessage();
$sendAllTo = Email::getSendAllEmailsTo();
if (!empty($sendAllTo)) {
$this->setTo($message, $sendAllTo);
}
$ccAllTo = Email::getCCAllEmailsTo();
if (!empty($ccAllTo)) {
foreach ($ccAllTo as $address => $name) {
$message->addCc($address, $name);
}
}
$bccAllTo = Email::getBCCAllEmailsTo();
if (!empty($bccAllTo)) {
foreach ($bccAllTo as $address => $name) {
$message->addBcc($address, $name);
}
}
$sendAllFrom = Email::getSendAllEmailsFrom();
if (!empty($sendAllFrom)) {
$this->setFrom($message, $sendAllFrom);
}
}
/**
* @param \Swift_Message $message
* @param array|string $to
*/
protected function setTo($message, $to)
{
$headers = $message->getHeaders();
$origTo = $message->getTo();
$cc = $message->getCc();
$bcc = $message->getBcc();
// set default recipient and remove all other recipients
$message->setTo($to);
$headers->removeAll('Cc');
$headers->removeAll('Bcc');
// store the old data as X-Original-* Headers for debugging
$headers->addMailboxHeader('X-Original-To', $origTo);
$headers->addMailboxHeader('X-Original-Cc', $cc);
$headers->addMailboxHeader('X-Original-Bcc', $bcc);
}
/**
* @param \Swift_Message $message
* @param array|string $from
*/
protected function setFrom($message, $from)
{
$headers = $message->getHeaders();
$origFrom = $message->getFrom();
$headers->addMailboxHeader('X-Original-From', $origFrom);
$message->setFrom($from);
}
public function sendPerformed(\Swift_Events_SendEvent $evt)
{
// noop
}
}

View File

@ -15,7 +15,6 @@ use SilverStripe\Control\Controller;
use SilverStripe\Control\Cookie;
use SilverStripe\Control\Director;
use SilverStripe\Control\Email\Email;
use SilverStripe\Control\Email\Mailer;
use SilverStripe\Control\HTTPApplication;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Core\Config\Config;
@ -39,6 +38,10 @@ use SilverStripe\Security\Member;
use SilverStripe\Security\Permission;
use SilverStripe\Security\Security;
use SilverStripe\View\SSViewer;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mailer\Transport\NullTransport;
use Symfony\Component\Messenger\MessageBusInterface;
/**
* Test case class for the Silverstripe framework.
@ -317,20 +320,17 @@ abstract class SapphireTest extends TestCase implements TestOnly
SSViewer::config()->update('source_file_comments', false);
}
// Set up the test mailer
if (class_exists(TestMailer::class)) {
Injector::inst()->registerService(new TestMailer(), Mailer::class);
}
if (class_exists(Email::class)) {
Email::config()->remove('send_all_emails_to');
Email::config()->remove('send_all_emails_from');
Email::config()->remove('cc_all_emails_to');
Email::config()->remove('bcc_all_emails_to');
}
// Set up the test mailer and register it as a service
$dispatcher = Injector::inst()->get(EventDispatcherInterface::class . '.mailer');
$transport = new NullTransport($dispatcher);
$testMailer = new TestMailer($transport, $dispatcher);
Injector::inst()->registerService($testMailer, MailerInterface::class);
Email::config()->remove('send_all_emails_to');
Email::config()->remove('send_all_emails_from');
Email::config()->remove('cc_all_emails_to');
Email::config()->remove('bcc_all_emails_to');
}
/**
* Helper method to determine if the current test should enable a test database
*
@ -611,8 +611,8 @@ abstract class SapphireTest extends TestCase implements TestOnly
*/
public function clearEmails()
{
/** @var Mailer $mailer */
$mailer = Injector::inst()->get(Mailer::class);
/** @var MailerInterface $mailer */
$mailer = Injector::inst()->get(MailerInterface::class);
if ($mailer instanceof TestMailer) {
$mailer->clearEmails();
return true;
@ -632,8 +632,8 @@ abstract class SapphireTest extends TestCase implements TestOnly
*/
public static function findEmail($to, $from = null, $subject = null, $content = null)
{
/** @var Mailer $mailer */
$mailer = Injector::inst()->get(Mailer::class);
/** @var MailerInterface $mailer */
$mailer = Injector::inst()->get(MailerInterface::class);
if ($mailer instanceof TestMailer) {
return $mailer->findEmail($to, $from, $subject, $content);
}

View File

@ -2,98 +2,72 @@
namespace SilverStripe\Dev;
use SilverStripe\Control\Email\Mailer;
use Swift_Attachment;
use Exception;
use InvalidArgumentException;
use SilverStripe\Control\Email\Email;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\Event\MessageEvent;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mailer\Messenger\SendEmailMessage;
use Symfony\Component\Mime\RawMessage;
use Symfony\Component\Mailer\Transport\TransportInterface;
use Symfony\Component\Messenger\Event\SendMessageToTransportsEvent;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Envelope as MessagerEnvelope;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Part\DataPart;
class TestMailer implements Mailer
class TestMailer implements MailerInterface
{
/**
* @var array
*/
protected $emailsSent = [];
private array $emailsSent = [];
public function send($email)
private TransportInterface $transport;
private EventDispatcherInterface $dispatcher;
public function __construct(
TransportInterface $transport,
EventDispatcherInterface $dispatcher
) {
$this->transport = $transport;
$this->dispatcher = $dispatcher;
}
public function send(RawMessage $message, Envelope $envelope = null): void
{
// Detect body type
$htmlContent = null;
$plainContent = null;
if ($email->getSwiftMessage()->getContentType() === 'text/plain') {
$type = 'plain';
$plainContent = $email->getBody();
} else {
$type = 'html';
$htmlContent = $email->getBody();
$plainPart = $email->findPlainPart();
if ($plainPart) {
$plainContent = $plainPart->getBody();
}
if (!is_a($message, Email::class)) {
throw new InvalidArgumentException('$message must be a ' . Email::class);
}
// Get attachments
$attachedFiles = [];
foreach ($email->getSwiftMessage()->getChildren() as $child) {
if ($child instanceof Swift_Attachment) {
$attachedFiles[] = [
'contents' => $child->getBody(),
'filename' => $child->getFilename(),
'mimetype' => $child->getContentType(),
];
}
}
// Serialise email
$serialised = [
'Type' => $type,
'To' => implode(';', array_keys($email->getTo() ?: [])),
'From' => implode(';', array_keys($email->getFrom() ?: [])),
/** @var Email $email */
$email = $message;
$this->dispatchEvent($email, $envelope);
$this->emailsSent[] = [
'Type' => $email->getHtmlBody() ? 'html' : 'plain',
'To' => $this->convertAddressesToString($email->getTo()),
'From' => $this->convertAddressesToString($email->getFrom()),
'Subject' => $email->getSubject(),
'Content' => $email->getBody(),
'AttachedFiles' => $attachedFiles,
'Headers' => $email->getSwiftMessage()->getHeaders(),
'Content' => $email->getHtmlBody() ?: $email->getTextBody(),
'Headers' => $email->getHeaders(),
'PlainContent' => $email->getTextBody(),
'HtmlContent' => $email->getHtmlBody(),
'AttachedFiles' => array_map(fn(DataPart $attachment) => [
'contents' => $attachment->getBody(),
'filename' => $attachment->getFilename(),
'mimetype' => $attachment->getContentType()
], $email->getAttachments()),
];
if ($plainContent) {
$serialised['PlainContent'] = $plainContent;
}
if ($htmlContent) {
$serialised['HtmlContent'] = $htmlContent;
}
$this->saveEmail($serialised);
return true;
}
/**
* Save a single email to the log
*
* @param array $data A map of information about the email
*/
protected function saveEmail($data)
{
$this->emailsSent[] = $data;
}
/**
* Clear the log of emails sent
*/
public function clearEmails()
{
$this->emailsSent = [];
}
/**
* Search for an email that was sent.
* All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
*
* @param string $to
* @param string $from
* @param string $subject
* @param string $content
* @return array|null Contains keys: 'Type', 'To', 'From', 'Subject', 'Content', 'PlainContent', 'AttachedFiles',
* 'HtmlContent'
*/
public function findEmail($to, $from = null, $subject = null, $content = null)
{
public function findEmail(
string $to,
?string $from = null,
?string $subject = null,
?string $content = null
): ?array {
$compare = [
'To' => $to,
'From' => $from,
@ -131,9 +105,28 @@ class TestMailer implements Mailer
}
/**
* @param string $value
* Clear the log of emails sent
*/
private function normaliseSpaces(string $value)
public function clearEmails(): void
{
$this->emailsSent = [];
}
private function convertAddressesToString(array $addresses): string
{
return implode(',', array_map(fn(Address $address) => $address->getAddress(), $addresses));
}
private function dispatchEvent(Email $email, Envelope $envelope = null): void
{
$sender = $email->getSender()[0] ?? $email->getFrom()[0] ?? new Address('test.sender@example.com');
$recipients = empty($email->getTo()) ? [new Address('test.recipient@example.com')] : $email->getTo();
$envelope ??= new Envelope($sender, $recipients);
$event = new MessageEvent($email, $envelope, $this->transport);
$this->dispatcher->dispatch($event);
}
private function normaliseSpaces(string $value): string
{
return str_replace([', ', '; '], [',', ';'], $value ?? '');
}

View File

@ -9,7 +9,6 @@ use SilverStripe\CMS\Controllers\CMSMain;
use SilverStripe\Control\Controller;
use SilverStripe\Control\Director;
use SilverStripe\Control\Email\Email;
use SilverStripe\Control\Email\Mailer;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Convert;
@ -36,6 +35,7 @@ use SilverStripe\ORM\SS_List;
use SilverStripe\ORM\UnsavedRelationList;
use SilverStripe\ORM\ValidationException;
use SilverStripe\ORM\ValidationResult;
use Symfony\Component\Mailer\MailerInterface;
/**
* The member class which represents the users of the system
@ -907,7 +907,7 @@ class Member extends DataObject
// We don't send emails out on dev/tests sites to prevent accidentally spamming users.
// However, if TestMailer is in use this isn't a risk.
// @todo some developers use external tools, so emailing might be a good idea anyway
if ((Director::isLive() || Injector::inst()->get(Mailer::class) instanceof TestMailer)
if ((Director::isLive() || Injector::inst()->get(MailerInterface::class) instanceof TestMailer)
&& $this->isChanged('Password')
&& $this->record['Password']
&& $this->Email

View File

@ -2,73 +2,74 @@
namespace SilverStripe\Control\Tests\Email;
use DateTime;
use PHPUnit\Framework\MockObject\MockObject;
use SilverStripe\Control\Director;
use SilverStripe\Control\Email\Email;
use SilverStripe\Control\Email\Mailer;
use SilverStripe\Control\Email\SwiftMailer;
use SilverStripe\Control\Tests\Email\EmailTest\EmailSubClass;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Manifest\ModuleResourceLoader;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Dev\TestMailer;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\Security\Member;
use SilverStripe\View\SSViewer;
use Swift_Attachment;
use Swift_Mailer;
use Swift_Message;
use Swift_NullTransport;
use Swift_RfcComplianceException;
use SilverStripe\View\ViewableData;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Part\DataPart;
use Symfony\Component\Mime\Part\AbstractPart;
class EmailTest extends SapphireTest
{
private array $origThemes = [];
protected function setUp(): void
{
parent::setUp();
Director::config()->set('alternate_base_url', 'http://www.mysite.com/');
$this->origThemes = SSViewer::get_themes();
SSViewer::set_themes([
'silverstripe/framework:/tests/php/Control/Email/EmailTest',
'$default',
]);
}
public function testAddAttachment()
protected function tearDown(): void
{
parent::tearDown();
SSViewer::set_themes($this->origThemes);
}
public function testAddAttachment(): void
{
$email = new Email();
$email->addAttachment(__DIR__ . '/EmailTest/attachment.txt', null, 'text/plain');
$children = $email->getSwiftMessage()->getChildren();
$this->assertCount(1, $children);
/** @var Swift_Attachment $child */
$child = reset($children);
$this->assertInstanceOf(Swift_Attachment::class, $child);
$this->assertEquals('text/plain', $child->getContentType());
$this->assertEquals('attachment.txt', $child->getFilename());
$attachments = $email->getAttachments();
$this->assertCount(1, $attachments);
$attachment = $this->getFirstAttachment($attachments);
$this->assertSame('text/plain', $attachment->getContentType());
$this->assertSame('attachment.txt', $attachment->getFilename());
}
public function testAddAttachmentFromData()
public function testAddAttachmentFromData(): void
{
$email = new Email();
$email->addAttachmentFromData('foo bar', 'foo.txt', 'text/plain');
$children = $email->getSwiftMessage()->getChildren();
$attachments = $email->getAttachments();
$this->assertCount(1, $attachments);
$attachment = $this->getFirstAttachment($attachments);
$this->assertSame('text/plain', $attachment->getContentType());
$this->assertSame('foo.txt', $attachment->getFilename());
$this->assertSame('foo bar', $attachment->getBody());
}
$this->assertCount(1, $children);
/** @var Swift_Attachment $child */
$child = reset($children);
$this->assertInstanceOf(Swift_Attachment::class, $child);
$this->assertEquals('foo bar', $child->getBody());
$this->assertEquals('text/plain', $child->getContentType());
$this->assertEquals('foo.txt', $child->getFilename());
private function getFirstAttachment(array $attachments): DataPart
{
return $attachments[0];
}
/**
* @dataProvider provideValidEmailAddresses
*/
public function testValidEmailAddress($email)
public function testValidEmailAddress($email): void
{
$this->assertTrue(Email::is_valid_address($email));
}
@ -76,26 +77,26 @@ class EmailTest extends SapphireTest
/**
* @dataProvider provideInvalidEmailAddresses
*/
public function testInvalidEmailAddress($email)
public function testInvalidEmailAddress($email): void
{
$this->assertFalse(Email::is_valid_address($email));
}
public function provideValidEmailAddresses()
public function provideValidEmailAddresses(): array
{
return [
['test@example.com', 'test-123@sub.example.com'],
];
}
public function provideInvalidEmailAddresses()
public function provideInvalidEmailAddresses(): array
{
return [
['foo.bar@', '@example.com', 'foo@'],
];
}
public function testObfuscate()
public function testObfuscate(): void
{
$emailAddress = 'test-1@example.com';
@ -111,392 +112,272 @@ class EmailTest extends SapphireTest
);
}
public function testSendPlain()
private function getTemplateClass(string $templateName): string
{
$email = $this->makeEmailMock('Test send plain');
// email should not call render if a body is supplied
$email->expects($this->never())->method('renderWith');
$successful = $email->sendPlain();
$this->assertTrue($successful);
$this->assertEmpty($email->getFailedRecipients());
/** @var TestMailer $mailer */
$mailer = Injector::inst()->get(Mailer::class);
$sentMail = $mailer->findEmail('to@example.com');
$this->assertTrue(is_array($sentMail));
$this->assertEquals('to@example.com', $sentMail['To']);
$this->assertEquals('from@example.com', $sentMail['From']);
$this->assertEquals('Test send plain', $sentMail['Subject']);
$this->assertEquals('Body for Test send plain', $sentMail['Content']);
$this->assertCount(1, $sentMail['AttachedFiles']);
$child = reset($sentMail['AttachedFiles']);
$this->assertEquals('text/plain', $child['mimetype']);
$this->assertEquals('attachment.txt', $child['filename']);
$this->assertEquals('Hello, I\'m a text document.', $child['contents']);
return implode('\\', ['SilverStripe', 'Control', 'Tests', 'Email', 'EmailTest', $templateName]);
}
public function testSend()
private function getMailer(): TestMailer
{
/** @var Email|MockObject $email */
$email = $this->makeEmailMock('Test send HTML');
// email should not call render if a body is supplied
$email->expects($this->never())->method('renderWith');
$successful = $email->send();
$this->assertTrue($successful);
$this->assertEmpty($email->getFailedRecipients());
/** @var TestMailer $mailer */
$mailer = Injector::inst()->get(Mailer::class);
$sentMail = $mailer->findEmail('to@example.com');
$this->assertTrue(is_array($sentMail));
$this->assertEquals('to@example.com', $sentMail['To']);
$this->assertEquals('from@example.com', $sentMail['From']);
$this->assertEquals('Test send HTML', $sentMail['Subject']);
$this->assertEquals('Body for Test send HTML', $sentMail['Content']);
$this->assertCount(1, $sentMail['AttachedFiles']);
$child = reset($sentMail['AttachedFiles']);
$this->assertEquals('text/plain', $child['mimetype']);
$this->assertEquals('attachment.txt', $child['filename']);
$this->assertEquals('Hello, I\'m a text document.', $child['contents']);
return Injector::inst()->get(MailerInterface::class);
}
public function testRenderedSend()
private function createTestEmail(string $subject = 'My subject', $setPlain = true): Email
{
/** @var Email|MockObject $email */
$email = $this->getMockBuilder(Email::class)
->enableProxyingToOriginalMethods()
->getMock();
$email->setFrom('from@example.com');
$email->setTo('to@example.com');
$email->setData([
'EmailContent' => 'test',
]);
$this->assertFalse($email->hasPlainPart());
$this->assertEmpty($email->getBody());
// these seem to fail for some reason :/
//$email->expects($this->once())->method('render');
//$email->expects($this->once())->method('generatePlainPartFromBody');
$email->send();
$this->assertTrue($email->hasPlainPart());
$this->assertNotEmpty($email->getBody());
}
public function testRenderedSendSubclass()
{
// Include dev theme
SSViewer::set_themes([
'silverstripe/framework:/tests/php/Control/Email/EmailTest',
'$default',
]);
/** @var Email|MockObject $email */
$email = $this->getMockBuilder(EmailSubClass::class)
->enableProxyingToOriginalMethods()
->getMock();
$email->setFrom('from@example.com');
$email->setTo('to@example.com');
$email->setData([
'EmailContent' => 'test',
]);
$this->assertFalse($email->hasPlainPart());
$this->assertEmpty($email->getBody());
$email->send();
$this->assertTrue($email->hasPlainPart());
$this->assertNotEmpty($email->getBody());
$this->assertStringContainsString('<h1>Email Sub-class</h1>', $email->getBody());
}
public function testConsturctor()
{
$email = new Email(
'from@example.com',
'to@example.com',
'subject',
'body',
'cc@example.com',
'bcc@example.com',
'bounce@example.com'
);
$this->assertCount(1, $email->getFrom());
$this->assertContains('from@example.com', array_keys($email->getFrom() ?? []));
$this->assertCount(1, $email->getTo());
$this->assertContains('to@example.com', array_keys($email->getTo() ?? []));
$this->assertEquals('subject', $email->getSubject());
$this->assertEquals('body', $email->getBody());
$this->assertCount(1, $email->getCC());
$this->assertContains('cc@example.com', array_keys($email->getCC() ?? []));
$this->assertCount(1, $email->getBCC());
$this->assertContains('bcc@example.com', array_keys($email->getBCC() ?? []));
$this->assertEquals('bounce@example.com', $email->getReturnPath());
}
public function testGetSwiftMessage()
{
$email = new Email(
'from@example.com',
'to@example.com',
'subject',
'body',
'cc@example.com',
'bcc@example.com',
'bounce@example.com'
);
$swiftMessage = $email->getSwiftMessage();
$this->assertInstanceOf(Swift_Message::class, $swiftMessage);
$this->assertCount(1, $swiftMessage->getFrom());
$this->assertContains('from@example.com', array_keys($swiftMessage->getFrom() ?? []));
$this->assertCount(1, $swiftMessage->getTo());
$this->assertContains('to@example.com', array_keys($swiftMessage->getTo() ?? []));
$this->assertEquals('subject', $swiftMessage->getSubject());
$this->assertEquals('body', $swiftMessage->getBody());
$this->assertCount(1, $swiftMessage->getCC());
$this->assertContains('cc@example.com', array_keys($swiftMessage->getCc() ?? []));
$this->assertCount(1, $swiftMessage->getBCC());
$this->assertContains('bcc@example.com', array_keys($swiftMessage->getBcc() ?? []));
$this->assertEquals('bounce@example.com', $swiftMessage->getReturnPath());
}
public function testSetSwiftMessage()
{
Email::config()->update('admin_email', 'admin@example.com');
DBDatetime::set_mock_now('2017-01-01 07:00:00');
$email = new Email();
$swiftMessage = new Swift_Message();
$email->setSwiftMessage($swiftMessage);
$dateTime = new DateTime();
$dateTime->setTimestamp(DBDatetime::now()->getTimestamp());
$email->getSwiftMessage()->setDate($dateTime);
$this->assertCount(1, $email->getFrom());
$this->assertContains('admin@example.com', array_keys($swiftMessage->getFrom() ?? []));
$this->assertEquals(strtotime('2017-01-01 07:00:00'), $swiftMessage->getDate()->getTimestamp());
$this->assertEquals($swiftMessage, $email->getSwiftMessage());
$email->setFrom('from@example.com');
$email->setTo('to@example.com');
$email->setSubject($subject);
if ($setPlain) {
$email->text("Plain body for $subject");
}
$email->html("<p>HTML body for $subject</p>");
$email->setCC('cc@example.com');
$email->setBCC('bcc@example.com');
$email->addAttachment(__DIR__ . '/EmailTest/attachment.txt', null, 'text/plain');
return $email;
}
// check from field is retained
$swiftMessage = new Swift_Message();
$swiftMessage->setFrom('from@example.com');
$email->setSwiftMessage($swiftMessage);
public function testSendPlain(): void
{
$email = $this->createTestEmail('Test send plain');
$email->sendPlain();
$this->assertStringNotContainsString($email->getTextBody(), 'My Plain Template');
$sentMail = $this->getMailer()->findEmail('to@example.com');
$this->assertSame('to@example.com', $sentMail['To']);
$this->assertSame('from@example.com', $sentMail['From']);
$this->assertSame('Test send plain', $sentMail['Subject']);
$this->assertStringContainsString('Plain body for Test send plain', $sentMail['Content']);
$this->assertCount(1, $sentMail['AttachedFiles']);
$child = reset($sentMail['AttachedFiles']);
$this->assertSame('text/plain', $child['mimetype']);
$this->assertSame('attachment.txt', $child['filename']);
$this->assertSame('Hello, I\'m a text document.', $child['contents']);
// assert MIME types
// explicitly setting $email->html(null) because sendPlain() will itself set $this->html(null), and then
// revert it to its previous AFTER sending the email. For testing purposes, we need to manuall set it
// to null in order to test the MIME types for what would have been sent in practice
$email->html(null);
$this->assertSame([
'text/plain charset: utf-8',
'text/plain disposition: attachment filename: attachment.txt'
], array_map(fn(AbstractPart $part) => $part->asDebugString(), $email->getBody()->getParts()));
}
public function testSendPlainFallback(): void
{
$email = $this->createTestEmail('Test send plain', false);
$email->sendPlain();
$sentMail = $this->getMailer()->findEmail('to@example.com');
// assert that it has HTML body with HTML tags removed
$this->assertSame('HTML body for Test send plain', $sentMail['Content']);
}
public function testSendPlainThenNormalWithSetData(): void
{
$email = $this->createTestEmail('Test send plain', false);
$email->setData([
'EmailContent' => 'This is the content of the email',
]);
$email->sendPlain();
$email->send();
$sentMail = $this->getMailer()->findEmail('to@example.com');
$this->assertSame('This is the content of the email', $sentMail['Content']);
$email->to('to2@example.com');
$email->send();
$sentMail = $this->getMailer()->findEmail('to2@example.com');
$this->assertStringContainsString('This is the content of the email', $sentMail['Content']);
}
public function testSend(): void
{
$email = $this->createTestEmail('Test send HTML');
// email should not call render if a body is supplied
$email->setHTMLTemplate($this->getTemplateClass('HtmlTemplate'));
$email->send();
$this->assertStringNotContainsString($email->getHtmlBody(), 'My HTML Template');
$sentMail = $this->getMailer()->findEmail('to@example.com');
$this->assertSame('to@example.com', $sentMail['To']);
$this->assertSame('from@example.com', $sentMail['From']);
$this->assertSame('Test send HTML', $sentMail['Subject']);
$this->assertStringContainsString('<p>HTML body for Test send HTML</p>', $sentMail['Content']);
$this->assertCount(1, $sentMail['AttachedFiles']);
$child = reset($sentMail['AttachedFiles']);
$this->assertSame('text/plain', $child['mimetype']);
$this->assertSame('attachment.txt', $child['filename']);
$this->assertSame('Hello, I\'m a text document.', $child['contents']);
// assert MIME types
$this->assertSame([
implode("\n", [
'multipart/alternative',
'text/plain charset: utf-8',
'text/html charset: utf-8'
]),
'text/plain disposition: attachment filename: attachment.txt'
], array_map(fn(AbstractPart $part) => $part->asDebugString(), $email->getBody()->getParts()));
}
public function testRenderedSend(): void
{
$email = new Email(to: 'to@example.com');
$email->setHTMLTemplate($this->getTemplateClass('HtmlTemplate'));
$email->setData([
'EmailContent' => '<p>test</p>',
]);
$email->send();
$sentMail = $this->getMailer()->findEmail('to@example.com');
$this->assertStringContainsString('My HTML Template', $sentMail['Content']);
}
public function testRenderedSendSubclass(): void
{
$email = new EmailSubClass(to: 'to@example.com');
$email->setData([
'EmailContent' => 'test',
]);
$email->send();
$sentMail = $this->getMailer()->findEmail('to@example.com');
$this->assertStringContainsString('<h1>Email Sub-class</h1>', $sentMail['Content']);
}
public function testConstructor(): void
{
$email = new Email(
'from@example.com',
'to@example.com',
'subject',
'<p>body</p>',
'cc@example.com',
'bcc@example.com',
'bounce@example.com'
);
$this->assertCount(1, $email->getFrom());
$this->assertContains('from@example.com', array_keys($email->getFrom() ?? []));
$this->assertSame('from@example.com', $email->getFrom()[0]->getAddress());
$this->assertCount(1, $email->getTo());
$this->assertSame('to@example.com', $email->getTo()[0]->getAddress());
$this->assertEquals('subject', $email->getSubject());
$this->assertEquals('<p>body</p>', $email->getHtmlBody());
$this->assertCount(1, $email->getCC());
$this->assertEquals('cc@example.com', $email->getCC()[0]->getAddress());
$this->assertCount(1, $email->getBCC());
$this->assertEquals('bcc@example.com', $email->getBcc()[0]->getAddress());
$this->assertEquals('bounce@example.com', $email->getReturnPath()->getAddress());
}
public function testSetBody(): void
{
$email = new Email();
$email->setBody('<p>body</p>');
$this->assertSame('<p>body</p>', $email->getHtmlBody());
}
public function testSetFrom(): void
{
$email = new Email();
$email->setFrom('from@example.com');
$this->assertCount(1, $email->getFrom());
$this->assertSame('from@example.com', $email->getFrom()[0]->getAddress());
}
public function testSender(): void
{
$email = new Email();
$email->setSender('sender@example.com');
$this->assertSame('sender@example.com', $email->getSender()->getAddress());
}
public function testSetTo(): void
{
$email = new Email();
$email->setTo('to@example.com');
$this->assertCount(1, $email->getTo());
$this->assertSame('to@example.com', $email->getTo()[0]->getAddress());
}
public function testSetReplyTo(): void
{
$email = new Email();
$email->setReplyTo('reply-to@example.com');
$this->assertCount(1, $email->getReplyTo());
$this->assertSame('reply-to@example.com', $email->getReplyTo()[0]->getAddress());
}
public function testSetSubject(): void
{
$email = new Email();
$email->setSubject('my subject');
$this->assertSame('my subject', $email->getSubject());
}
public function testSetReturnPath(): void
{
$email = new Email();
$email->setReturnPath('return-path@example.com');
$this->assertSame('return-path@example.com', $email->getReturnPath()->getAddress());
}
public function testSetPriority(): void
{
$email = new Email();
// Intentionally set above 5 to test that Symfony\Component\Mime\Email->priority() is being called
$email->setPriority(7);
$this->assertSame(5, $email->getPriority());
}
public function testAdminEmailApplied()
{
Email::config()->update('admin_email', 'admin@example.com');
$email = new Email();
$this->assertCount(1, $email->getFrom());
$this->assertContains('admin@example.com', array_keys($email->getFrom() ?? []));
$this->assertSame('admin@example.com', $email->getFrom()[0]->getAddress());
}
public function testGetFrom()
{
$email = new Email('from@example.com');
$this->assertCount(1, $email->getFrom());
$this->assertContains('from@example.com', array_keys($email->getFrom() ?? []));
}
public function testSetFrom()
{
$email = new Email('from@example.com');
$this->assertCount(1, $email->getFrom());
$this->assertContains('from@example.com', array_keys($email->getFrom() ?? []));
$email->setFrom('new-from@example.com');
$this->assertCount(1, $email->getFrom());
$this->assertContains('new-from@example.com', array_keys($email->getFrom() ?? []));
}
public function testAddFrom()
{
$email = new Email('from@example.com');
$this->assertCount(1, $email->getFrom());
$this->assertContains('from@example.com', array_keys($email->getFrom() ?? []));
$email->addFrom('new-from@example.com');
$this->assertCount(2, $email->getFrom());
$this->assertContains('from@example.com', array_keys($email->getFrom() ?? []));
$this->assertContains('new-from@example.com', array_keys($email->getFrom() ?? []));
}
public function testSetGetSender()
public function testDataWithArray(): void
{
$email = new Email();
$this->assertEmpty($email->getSender());
$email->setSender('sender@example.com', 'Silver Stripe');
$this->assertEquals(['sender@example.com' => 'Silver Stripe'], $email->getSender());
}
public function testSetGetReturnPath()
{
$email = new Email();
$this->assertEmpty($email->getReturnPath());
$email->setReturnPath('return@example.com');
$this->assertEquals('return@example.com', $email->getReturnPath());
}
public function testSetGetTo()
{
$email = new Email('from@example.com', 'to@example.com');
$this->assertCount(1, $email->getTo());
$this->assertContains('to@example.com', array_keys($email->getTo() ?? []));
$email->setTo('new-to@example.com', 'Silver Stripe');
$this->assertEquals(['new-to@example.com' => 'Silver Stripe'], $email->getTo());
}
public function testAddTo()
{
$email = new Email('from@example.com', 'to@example.com');
$this->assertCount(1, $email->getTo());
$this->assertContains('to@example.com', array_keys($email->getTo() ?? []));
$email->addTo('new-to@example.com');
$this->assertCount(2, $email->getTo());
$this->assertContains('to@example.com', array_keys($email->getTo() ?? []));
$this->assertContains('new-to@example.com', array_keys($email->getTo() ?? []));
}
public function testSetGetCC()
{
$email = new Email('from@example.com', 'to@example.com', 'subject', 'body', 'cc@example.com');
$this->assertCount(1, $email->getCC());
$this->assertContains('cc@example.com', array_keys($email->getCC() ?? []));
$email->setCC('new-cc@example.com', 'Silver Stripe');
$this->assertEquals(['new-cc@example.com' => 'Silver Stripe'], $email->getCC());
}
public function testAddCC()
{
$email = new Email('from@example.com', 'to@example.com', 'subject', 'body', 'cc@example.com');
$this->assertCount(1, $email->getCC());
$this->assertContains('cc@example.com', array_keys($email->getCC() ?? []));
$email->addCC('new-cc@example.com', 'Silver Stripe');
$this->assertCount(2, $email->getCC());
$this->assertContains('cc@example.com', array_keys($email->getCC() ?? []));
$this->assertContains('new-cc@example.com', array_keys($email->getCC() ?? []));
}
public function testSetGetBCC()
{
$email = new Email(
'from@example.com',
'to@example.com',
'subject',
'body',
'cc@example.com',
'bcc@example.com'
);
$this->assertCount(1, $email->getBCC());
$this->assertContains('bcc@example.com', array_keys($email->getBCC() ?? []));
$email->setBCC('new-bcc@example.com', 'Silver Stripe');
$this->assertEquals(['new-bcc@example.com' => 'Silver Stripe'], $email->getBCC());
}
public function testAddBCC()
{
$email = new Email(
'from@example.com',
'to@example.com',
'subject',
'body',
'cc@example.com',
'bcc@example.com'
);
$this->assertCount(1, $email->getBCC());
$this->assertContains('bcc@example.com', array_keys($email->getBCC() ?? []));
$email->addBCC('new-bcc@example.com', 'Silver Stripe');
$this->assertCount(2, $email->getBCC());
$this->assertContains('bcc@example.com', array_keys($email->getBCC() ?? []));
$this->assertContains('new-bcc@example.com', array_keys($email->getBCC() ?? []));
}
public function testReplyTo()
{
$email = new Email();
$this->assertEmpty($email->getReplyTo());
$email->setReplyTo('reply-to@example.com', 'Silver Stripe');
$this->assertEquals(['reply-to@example.com' => 'Silver Stripe'], $email->getReplyTo());
$email->addReplyTo('new-reply-to@example.com');
$this->assertCount(2, $email->getReplyTo());
$this->assertContains('reply-to@example.com', array_keys($email->getReplyTo() ?? []));
$this->assertContains('new-reply-to@example.com', array_keys($email->getReplyTo() ?? []));
}
public function testSubject()
{
$email = new Email('from@example.com', 'to@example.com', 'subject');
$this->assertEquals('subject', $email->getSubject());
$email->setSubject('new subject');
$this->assertEquals('new subject', $email->getSubject());
}
public function testPriority()
{
$email = new Email();
$this->assertEquals(3, $email->getPriority());
$email->setPriority(5);
$this->assertEquals(5, $email->getPriority());
}
public function testData()
{
$email = new Email();
$this->assertEmpty($email->getData());
$email->setData([
'Title' => 'My Title',
]);
$this->assertCount(1, $email->getData());
$this->assertEquals(['Title' => 'My Title'], $email->getData());
$this->assertSame(true, $email->getData()->IsEmail);
$this->assertSame(Director::absoluteBaseURL(), $email->getData()->BaseURL);
$email->setData(['Lorem' => 'Ipsum']);
$this->assertSame(true, $email->getData()->IsEmail);
$this->assertSame(Director::absoluteBaseURL(), $email->getData()->BaseURL);
$this->assertSame('Ipsum', $email->getData()->Lorem);
$email->addData('Content', 'My content');
$this->assertCount(2, $email->getData());
$this->assertEquals([
'Title' => 'My Title',
'Content' => 'My content',
], $email->getData());
$email->removeData('Title');
$this->assertEquals(['Content' => 'My content'], $email->getData());
$this->assertSame(true, $email->getData()->IsEmail);
$this->assertSame(Director::absoluteBaseURL(), $email->getData()->BaseURL);
$this->assertSame('Ipsum', $email->getData()->Lorem);
$this->assertSame('My content', $email->getData()->Content);
}
public function testDataWithViewableData()
public function testDataWithViewableData(): void
{
$email = new Email();
$viewableData = new ViewableData();
$viewableData->ABC = 'XYZ';
$email->setData($viewableData);
$data = $email->getData();
$this->assertSame('XYZ', $data->ABC);
$this->assertSame(true, $data->IsEmail);
$this->assertSame(Director::absoluteBaseURL(), $data->BaseURL);
$member = new Member();
$member->FirstName = 'First Name';
$email = new Email();
$this->assertEmpty($email->getData());
$email->setData($member);
$this->assertEquals($member, $email->getData());
$this->assertSame($member->FirstName, $email->getData()->FirstName);
$email->addData('Test', 'Test value');
$this->assertEquals('Test value', $email->getData()->Test);
$email->removeData('Test');
$this->assertNull($email->getData()->Test);
}
public function testBody()
public function testHTMLTemplate(): void
{
$email = new Email();
$this->assertEmpty($email->getBody());
$email->setBody('<h1>Title</h1>');
$this->assertEquals('<h1>Title</h1>', $email->getBody());
}
public function testHTMLTemplate()
{
// Include dev theme
SSViewer::set_themes([
'silverstripe/framework:/tests/php/Control/Email/EmailTest',
'$default',
]);
// Find template on disk
$emailTemplate = ModuleResourceLoader::singleton()->resolveResource(
'silverstripe/framework:templates/SilverStripe/Control/Email/Email.ss'
@ -522,7 +403,7 @@ class EmailTest extends SapphireTest
$this->assertEquals('MyTemplate', $email->getHTMLTemplate());
}
public function testPlainTemplate()
public function testPlainTemplate(): void
{
$email = new Email();
$this->assertEmpty($email->getPlainTemplate());
@ -530,145 +411,64 @@ class EmailTest extends SapphireTest
$this->assertEquals('MyTemplate', $email->getPlainTemplate());
}
public function testGetFailedRecipients()
{
$mailer = new SwiftMailer();
/** @var Swift_NullTransport|MockObject $transport */
$transport = $this->getMockBuilder(Swift_NullTransport::class)->getMock();
$transport->expects($this->once())
->method('send')
->willThrowException(new Swift_RfcComplianceException('Bad email'));
$mailer->setSwiftMailer(new Swift_Mailer($transport));
$email = new Email();
$email->setTo('to@example.com');
$email->setFrom('from@example.com');
$mailer->send($email);
$this->assertCount(1, $email->getFailedRecipients());
}
public function testIsEmail()
{
$this->assertTrue((new Email)->IsEmail());
}
public function testRenderAgain()
public function testRerender(): void
{
$email = new Email();
$email->setPlainTemplate($this->getTemplateClass('PlainTemplate'));
$email->setData([
'EmailContent' => 'my content',
'EmailContent' => '<p>my content</p>',
]);
$email->render();
$this->assertStringContainsString('my content', $email->getBody());
$children = $email->getSwiftMessage()->getChildren();
$this->assertCount(1, $children);
$plainPart = reset($children);
$this->assertEquals('my content', $plainPart->getBody());
$email->send();
$this->assertStringContainsString('&lt;p&gt;my content&lt;/p&gt;', $email->getHtmlBody());
// ensure repeat renders don't add multiple plain parts
$email->render();
$this->assertCount(1, $email->getSwiftMessage()->getChildren());
}
public function testRerender()
{
$email = new Email();
// Ensure setting data causes html() to be updated
$email->setData([
'EmailContent' => 'my content',
'EmailContent' => '<p>your content</p>'
]);
$email->render();
$this->assertStringContainsString('my content', $email->getBody());
$children = $email->getSwiftMessage()->getChildren();
$this->assertCount(1, $children);
$plainPart = reset($children);
$this->assertEquals('my content', $plainPart->getBody());
$email->send();
$this->assertStringContainsString('&lt;p&gt;your content&lt;/p&gt;', $email->getHtmlBody());
// Ensure setting data causes a rerender
$email->setData([
'EmailContent' => 'your content'
]);
$email->render();
$this->assertStringContainsString('your content', $email->getBody());
// Ensure removing data causes a rerender
// Ensure removing data causes html() to be updated
$email->removeData('EmailContent');
$email->render();
$this->assertStringNotContainsString('your content', $email->getBody());
$email->send();
$this->assertStringNotContainsString('&lt;p&gt;your content&lt;/p&gt;', $email->getHtmlBody());
// Ensure adding data causes a rerender
// Ensure adding data causes html() to be updated
$email->addData([
'EmailContent' => 'their content'
'EmailContent' => '<p>their content</p>'
]);
$email->render();
$this->assertStringContainsString('their content', $email->getBody());
$email->send();
$this->assertStringContainsString('&lt;p&gt;their content&lt;/p&gt;', $email->getHtmlBody());
}
public function testRenderPlainOnly()
public function testRenderPlainOnly(): void
{
$email = new Email();
$email->setData([
'EmailContent' => 'test content',
]);
$email->render(true);
$this->assertEquals('text/plain', $email->getSwiftMessage()->getContentType());
$this->assertEmpty($email->getSwiftMessage()->getChildren());
$email->sendPlain();
$this->assertSame('test content', $email->getTextBody());
}
public function testHasPlainPart()
public function testMultipleEmailSends(): void
{
$email = new Email();
$email = new Email(to: 'to@example.com');
$email->setData([
'EmailContent' => 'test',
'EmailContent' => '<p>Test</p>',
]);
//emails are assumed to be HTML by default
$this->assertFalse($email->hasPlainPart());
//make sure plain attachments aren't picked up as a plain part
$email->addAttachmentFromData('data', 'attachent.txt', 'text/plain');
$this->assertFalse($email->hasPlainPart());
$email->getSwiftMessage()->addPart('plain', 'text/plain');
$this->assertTrue($email->hasPlainPart());
}
public function testGeneratePlainPartFromBody()
{
$email = new Email();
$email->setBody('<h1>Test</h1>');
$this->assertEmpty($email->getSwiftMessage()->getChildren());
$email->generatePlainPartFromBody();
$children = $email->getSwiftMessage()->getChildren();
$this->assertCount(1, $children);
$plainPart = reset($children);
$this->assertStringContainsString('Test', $plainPart->getBody());
$this->assertStringNotContainsString('<h1>Test</h1>', $plainPart->getBody());
}
public function testMultipleEmailSends()
{
$email = new Email();
$email->setData([
'EmailContent' => 'Test',
]);
$this->assertEmpty($email->getBody());
$this->assertEmpty($email->getSwiftMessage()->getChildren());
$this->assertSame(null, $email->getHtmlBody());
$this->assertSame(null, $email->getTextBody());
$email->send();
$this->assertStringContainsString('Test', $email->getBody());
$this->assertCount(1, $email->getSwiftMessage()->getChildren());
$children = $email->getSwiftMessage()->getChildren();
/** @var \Swift_MimePart $plainPart */
$plainPart = reset($children);
$this->assertStringContainsString('Test', $plainPart->getBody());
$this->assertStringContainsString('&lt;p&gt;Test&lt;/p&gt;', $email->getHtmlBody());
$this->assertSame('Test', $email->getTextBody());
//send again
$email->send();
$this->assertStringContainsString('Test', $email->getBody());
$this->assertCount(1, $email->getSwiftMessage()->getChildren());
$children = $email->getSwiftMessage()->getChildren();
/** @var \Swift_MimePart $plainPart */
$plainPart = reset($children);
$this->assertStringContainsString('Test', $plainPart->getBody());
$this->assertStringContainsString('&lt;p&gt;Test&lt;/p&gt;', $email->getHtmlBody());
$this->assertSame('Test', $email->getTextBody());
}
public function testGetDefaultFrom()
public function testGetDefaultFrom(): void
{
$email = new Email();
$class = new \ReflectionClass(Email::class);
@ -695,22 +495,43 @@ class EmailTest extends SapphireTest
}
/**
* @return MockObject|Email
* @dataProvider provideCreateAddressArray
*/
protected function makeEmailMock($subject)
public function testCreateAddressArray(string|array $address, string $name, array $expected): void
{
/** @var Email|MockObject $email */
$email = $this->getMockBuilder(Email::class)
->enableProxyingToOriginalMethods()
->getMock();
$method = new \ReflectionMethod(Email::class, 'createAddressArray');
$method->setAccessible(true);
$obj = new Email();
$actual = $method->invoke($obj, $address, $name);
for ($i = 0; $i < count($expected); $i++) {
$this->assertSame($expected[$i]->getAddress(), $actual[$i]->getAddress());
$this->assertSame($expected[$i]->getName(), $actual[$i]->getName());
}
}
$email->setFrom('from@example.com');
$email->setTo('to@example.com');
$email->setSubject($subject);
$email->setBody("Body for {$subject}");
$email->setCC('cc@example.com');
$email->setBCC('bcc@example.com');
$email->addAttachment(__DIR__ . '/EmailTest/attachment.txt', null, 'text/plain');
return $email;
public function provideCreateAddressArray(): array
{
return [
[
'my@email.com',
'My name',
[
new Address('my@email.com', 'My name'),
],
],
[
[
'my@email.com' => 'My name',
'other@email.com' => 'My other name',
'no-name@email.com'
],
'',
[
new Address('my@email.com', 'My name'),
new Address('other@email.com', 'My other name'),
new Address('no-name@email.com', ''),
],
]
];
}
}

View File

@ -0,0 +1,14 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<% base_tag %>
</head>
<body>
<div class="body">
<h1>My HTML Template</h1>
$EmailContent
</div>
</body>
</html>

View File

@ -0,0 +1,3 @@
# My Plain Template
$EmailContent

View File

@ -0,0 +1,100 @@
<?php
namespace SilverStripe\Control\Tests\Email;
use SilverStripe\Control\Email\Email;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Dev\TestMailer;
use Symfony\Component\Mailer\MailerInterface;
class MailerSubscriberTest extends SapphireTest
{
protected function setUp(): void
{
parent::setUp();
Email::config()->remove('send_all_emails_to');
Email::config()->remove('cc_all_emails_to');
Email::config()->remove('bcc_all_emails_to');
Email::config()->remove('send_all_emails_from');
}
private function getEmail(): Email
{
return (new Email())
->setTo('original-to@example.com')
->setCC('original-cc@example.com')
->setBCC('original-bcc@example.com')
->setFrom('original-from@example.com');
}
private function getMailer(): TestMailer
{
return Injector::inst()->get(MailerInterface::class);
}
private function getHeaderValue(Email $email, string $headerName): ?string
{
$headers = $email->getHeaders();
if (!$headers->has($headerName)) {
return null;
}
return $headers->getHeaderBody($headerName)[0]->getAddress();
}
public function testSendAllEmailsTo(): void
{
Email::config()->update('send_all_emails_to', 'to@example.com');
$email = $this->getEmail();
$email->send();
$this->assertCount(1, $email->getTo());
$this->assertSame('to@example.com', $email->getTo()[0]->getAddress());
$this->assertCount(1, $email->getFrom());
$this->assertSame('original-from@example.com', $email->getFrom()[0]->getAddress());
$this->assertSame('original-to@example.com', $this->getHeaderValue($email, 'X-Original-To'));
$this->assertSame('original-cc@example.com', $this->getHeaderValue($email, 'X-Original-Cc'));
$this->assertSame('original-bcc@example.com', $this->getHeaderValue($email, 'X-Original-Bcc'));
$this->assertSame(null, $this->getHeaderValue($email, 'X-Original-From'));
}
public function testSendAllEmailsFrom(): void
{
Email::config()->update('send_all_emails_from', 'from@example.com');
$email = $this->getEmail();
$email->send();
$this->assertCount(1, $email->getTo());
$this->assertSame('original-to@example.com', $email->getTo()[0]->getAddress());
$this->assertCount(1, $email->getFrom());
$this->assertSame('from@example.com', $email->getFrom()[0]->getAddress());
$this->assertSame(null, $this->getHeaderValue($email, 'X-Original-To'));
$this->assertSame(null, $this->getHeaderValue($email, 'X-Original-Cc'));
$this->assertSame(null, $this->getHeaderValue($email, 'X-Original-Bcc'));
$this->assertSame('original-from@example.com', $this->getHeaderValue($email, 'X-Original-From'));
}
public function testCCAllEmailsTo(): void
{
Email::config()->update('cc_all_emails_to', 'cc@example.com');
$email = $this->getEmail();
$email->send();
$this->assertCount(2, $email->getCc());
$this->assertSame('original-cc@example.com', $email->getCc()[0]->getAddress());
$this->assertSame('cc@example.com', $email->getCc()[1]->getAddress());
}
public function testBCCAllEmailsTo(): void
{
Email::config()->update('bcc_all_emails_to', 'bcc@example.com');
$email = $this->getEmail();
$email->send();
$this->assertCount(2, $email->getBcc());
$this->assertSame('original-bcc@example.com', $email->getBcc()[0]->getAddress());
$this->assertSame('bcc@example.com', $email->getBcc()[1]->getAddress());
}
}

View File

@ -1,78 +0,0 @@
<?php
namespace SilverStripe\Control\Tests\Email;
use SilverStripe\Control\Email\Email;
use SilverStripe\Control\Email\SwiftMailer;
use SilverStripe\Dev\SapphireTest;
use Swift_Mailer;
use Swift_MailTransport;
use Swift_Message;
use Swift_NullTransport;
use Swift_Plugins_AntiFloodPlugin;
class SwiftMailerTest extends SapphireTest
{
public function testSwiftMailer()
{
$mailer = new SwiftMailer();
$mailer->setSwiftMailer($swift = new Swift_Mailer(new Swift_NullTransport()));
$this->assertEquals($swift, $mailer->getSwiftMailer());
SwiftMailer::config()->remove('swift_plugins');
SwiftMailer::config()->update('swift_plugins', [Swift_Plugins_AntiFloodPlugin::class]);
/** @var Swift_MailTransport $transport */
$transport = $this->getMockBuilder(Swift_MailTransport::class)->getMock();
$transport
->expects($this->once())
->method('registerPlugin')
->with(
$this->isInstanceOf(Swift_Plugins_AntiFloodPlugin::class)
);
/** @var Swift_Mailer $swift */
$swift = $this->getMockBuilder(Swift_Mailer::class)->disableOriginalConstructor()->getMock();
$swift
->expects($this->once())
->method('registerPlugin')
->willReturnCallback(function ($plugin) use ($transport) {
$transport->registerPlugin($plugin);
});
$mailer->setSwiftMailer($swift);
}
public function testSend()
{
$email = new Email();
$email->setTo('to@example.com');
$email->setFrom('from@example.com');
$email->setSubject('Subject');
$mailer = $this->getMockBuilder(SwiftMailer::class)
->setMethods(['sendSwift'])
->getMock();
$mailer->expects($this->once())->method('sendSwift')->with(
$this->isInstanceOf(Swift_Message::class)
);
$mailer->send($email);
}
public function testSendSwift()
{
$mailer = new SwiftMailer();
$sendSwiftMethod = new \ReflectionMethod($mailer, 'sendSwift');
$sendSwiftMethod->setAccessible(true);
$transport = $this->getMockBuilder(Swift_NullTransport::class)->getMock();
$transport->expects($this->once())
->method('send');
$mailer->setSwiftMailer(new Swift_Mailer($transport));
$swiftMessage = new Swift_Message('Test', 'Body');
$swiftMessage->setTo('to@example.com');
$swiftMessage->setFrom('from@example.com');
$sendSwiftMethod->invoke($mailer, $swiftMessage);
}
}

View File

@ -1,110 +0,0 @@
<?php
namespace SilverStripe\Control\Tests\Email;
use SilverStripe\Control\Email\Email;
use SilverStripe\Control\Email\SwiftPlugin;
use SilverStripe\Dev\SapphireTest;
class SwiftPluginTest extends SapphireTest
{
protected function setUp(): void
{
parent::setUp();
//clean the config
Email::config()->remove('send_all_emails_to');
Email::config()->remove('cc_all_emails_to');
Email::config()->remove('bcc_all_emails_to');
Email::config()->remove('send_all_emails_from');
}
protected function getEmail()
{
return (new Email())
->setTo('original-to@example.com')
->setCC('original-cc@example.com')
->setBCC('original-bcc@example.com')
->setFrom('original-from@example.com');
}
protected function getMailer()
{
$mailer = new \Swift_Mailer(new \Swift_NullTransport());
$mailer->registerPlugin(new SwiftPlugin());
return $mailer;
}
public function testSendAllEmailsTo()
{
Email::config()->update('send_all_emails_to', 'to@example.com');
$email = $this->getEmail();
$this->getMailer()->send($email->getSwiftMessage());
$headers = $email->getSwiftMessage()->getHeaders();
$this->assertCount(1, $email->getTo());
$this->assertContains('to@example.com', array_keys($email->getTo() ?? []));
$this->assertCount(1, $email->getFrom());
$this->assertContains('original-from@example.com', array_keys($email->getFrom() ?? []));
$this->assertTrue($headers->has('X-Original-To'));
$this->assertTrue($headers->has('X-Original-Cc'));
$this->assertTrue($headers->has('X-Original-Bcc'));
$this->assertFalse($headers->has('X-Original-From'));
$originalTo = array_keys($headers->get('X-Original-To')->getFieldBodyModel() ?? []);
$originalCc = array_keys($headers->get('X-Original-Cc')->getFieldBodyModel() ?? []);
$originalBcc = array_keys($headers->get('X-Original-Bcc')->getFieldBodyModel() ?? []);
$this->assertCount(1, $originalTo);
$this->assertContains('original-to@example.com', $originalTo);
$this->assertCount(1, $originalCc);
$this->assertContains('original-cc@example.com', $originalCc);
$this->assertCount(1, $originalBcc);
$this->assertContains('original-bcc@example.com', $originalBcc);
}
public function testSendAllEmailsFrom()
{
Email::config()->update('send_all_emails_from', 'from@example.com');
$email = $this->getEmail();
$this->getMailer()->send($email->getSwiftMessage());
$headers = $email->getSwiftMessage()->getHeaders();
$this->assertFalse($headers->has('X-Original-To'));
$this->assertFalse($headers->has('X-Original-Cc'));
$this->assertFalse($headers->has('X-Original-Bcc'));
$this->assertTrue($headers->has('X-Original-From'));
$this->assertCount(1, $email->getFrom());
$this->assertContains('from@example.com', array_keys($email->getFrom() ?? []));
$this->assertCount(1, $headers->get('X-Original-From')->getFieldBodyModel());
$this->assertContains('original-from@example.com', array_keys($headers->get('X-Original-From')->getFieldBodyModel() ?? []));
}
public function testCCAllEmailsTo()
{
Email::config()->update('cc_all_emails_to', 'cc@example.com');
$email = $this->getEmail();
$this->getMailer()->send($email->getSwiftMessage());
$this->assertCount(2, $email->getCC());
$this->assertContains('cc@example.com', array_keys($email->getCC() ?? []));
$this->assertContains('original-cc@example.com', array_keys($email->getCC() ?? []));
}
public function testBCCAllEmailsTo()
{
Email::config()->update('bcc_all_emails_to', 'bcc@example.com');
$email = $this->getEmail();
$this->getMailer()->send($email->getSwiftMessage());
$this->assertCount(2, $email->getBCC());
$this->assertContains('bcc@example.com', array_keys($email->getBCC() ?? []));
$this->assertContains('original-bcc@example.com', array_keys($email->getBCC() ?? []));
}
}

View File

@ -1,75 +0,0 @@
<?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

@ -1,40 +0,0 @@
<?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

@ -1,313 +0,0 @@
<?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

@ -1,47 +0,0 @@
<?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 ?? '');
}
}