mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Merge pull request #10494 from creative-commoners/pulls/5/symfony-mailer
NEW Migrate from swiftmailer/swiftmailer to symfony/mailer
This commit is contained in:
commit
919cfcf435
@ -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
28
_config/mailer.yml
Normal 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`'
|
@ -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": [
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
100
src/Control/Email/MailerSubscriber.php
Normal file
100
src/Control/Email/MailerSubscriber.php
Normal 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()));
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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 ?? '');
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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('<p>my content</p>', $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('<p>your content</p>', $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('<p>your content</p>', $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('<p>their content</p>', $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('<p>Test</p>', $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('<p>Test</p>', $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', ''),
|
||||
],
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
@ -0,0 +1,3 @@
|
||||
# My Plain Template
|
||||
|
||||
$EmailContent
|
100
tests/php/Control/Email/MailerSubscriberTest.php
Normal file
100
tests/php/Control/Email/MailerSubscriberTest.php
Normal 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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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() ?? []));
|
||||
}
|
||||
}
|
75
thirdparty/swiftmailer/Swift/MailTransport.php
vendored
75
thirdparty/swiftmailer/Swift/MailTransport.php
vendored
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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()
|
||||
{
|
||||
}
|
||||
}
|
@ -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 ?? '');
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user