diff --git a/_config/email.yml b/_config/email.yml index 8842d39d29e..be991def344 100644 --- a/_config/email.yml +++ b/_config/email.yml @@ -1,12 +1,55 @@ --- -Name: emailconfig +Name: mailer --- 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' + Symfony\Component\Mailer\MailerInterface: + class: Symfony\Component\Mailer\Mailer + constructor: + 0: '%$Symfony\Component\Mailer\Transport\TransportInterface' + 1: '%$Symfony\Component\Messenger\MessageBusInterface.mailer' + Symfony\Component\Messenger\MessageBusInterface.mailer: + class: Symfony\Component\Messenger\MessageBus + constructor: + 0: + - '%$Symfony\Component\Messenger\Middleware\MiddlewareInterface.mailer' + Symfony\Component\Messenger\Middleware\MiddlewareInterface.mailer: + class: Symfony\Component\Messenger\Middleware\SendMessageMiddleware + constructor: + 0: '%$Symfony\Component\Messenger\Transport\Sender\SendersLocatorInterface.mailer' + 1: '%$Symfony\Component\EventDispatcher\EventDispatcherInterface.mailer' + Symfony\Component\Messenger\Transport\Sender\SendersLocatorInterface.mailer: + class: Symfony\Component\Messenger\Transport\Sender\SendersLocator + constructor: + 0: + '*': + - 'Symfony\Component\Messenger\Transport\Sender\SenderInterface.mailer' + 1: '%$Symfony\Component\DependencyInjection\ContainerInterface.mailer' + Symfony\Component\Messenger\Transport\Sender\SenderInterface.mailer: + class: SilverStripe\Control\Email\EmailSender + Symfony\Component\DependencyInjection\ContainerInterface.mailer: + class: Symfony\Component\DependencyInjection\Container + calls: + - ['set', ['Symfony\Component\Messenger\Transport\Sender\SenderInterface.mailer', '%$Symfony\Component\Messenger\Transport\Sender\SenderInterface.mailer']] + Symfony\Component\EventDispatcher\EventDispatcherInterface.mailer: + class: Symfony\Component\EventDispatcher\EventDispatcher +--- +Name: mailer-dsn-default-config +--- +SilverStripe\Core\Injector\Injector: + Symfony\Component\Mailer\Transport\TransportInterface: + factory: Symfony\Component\Mailer\Transport + factory_method: fromDsn + constructor: + dsn: 'sendmail://default' +--- +Name: mailer-dsn-env +After: + - 'mailer-dsn-default-config' + - 'mailer-dsn-project-config' +Only: + envvarset: MAILER_DSN +--- +SilverStripe\Core\Injector\Injector: + Symfony\Component\Mailer\Transport\TransportInterface: + constructor: + dsn: '`MAILER_DSN`' diff --git a/composer.json b/composer.json index 1cd80f8cd1d..807da131b45 100644 --- a/composer.json +++ b/composer.json @@ -37,10 +37,13 @@ "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/dependency-injection": "^6.1", "symfony/filesystem": "^6.1", + "symfony/mailer": "^6.1", + "symfony/messenger": "^6.1", + "symfony/mime": "^6.1", "symfony/translation": "^6.1", "symfony/yaml": "^6.1", "ext-ctype": "*", @@ -96,9 +99,6 @@ }, "files": [ "src/includes/constants.php" - ], - "classmap": [ - "thirdparty/swiftmailer" ] }, "include-path": [ diff --git a/src/Control/Email/Email.php b/src/Control/Email/Email.php index d6eb96801dc..37de68be5b1 100644 --- a/src/Control/Email/Email.php +++ b/src/Control/Email/Email.php @@ -2,176 +2,132 @@ namespace SilverStripe\Control\Email; -use DateTime; -use RuntimeException; +use Symfony\Component\Mime\Email as SymfonyEmail; + use Egulias\EmailValidator\EmailValidator; use Egulias\EmailValidator\Validation\RFCValidation; +use RuntimeException; 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; - -/** - * Class to support sending emails. - */ -class Email extends ViewableData +use Symfony\Component\Mailer\MailerInterface; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Part\AbstractPart; + +class Email extends SymfonyEmail { - /** - * @var array - * @config - */ - private static $send_all_emails_to = []; + use Configurable; + use Extensible; + use Injectable; - /** - * @var array - * @config - */ - private static $cc_all_emails_to = []; + private static string|array $send_all_emails_to = []; - /** - * @var array - * @config - */ - private static $bcc_all_emails_to = []; + private static string|array $cc_all_emails_to = []; - /** - * @var array - * @config - */ - private static $send_all_emails_from = []; + 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; - - /** - * @var Swift_Message - */ - private $swiftMessage; - - /** - * @var string The name of the HTML template to render the email with (without *.ss extension) */ - private $HTMLTemplate = null; + private static string|array $admin_email = ''; /** - * @var string The name of the plain text template to render the plain part of the email with + * The name of the HTML template to render the email with (without *.ss extension) */ - private $plainTemplate = null; + private string $HTMLTemplate = ''; /** - * @var Swift_MimePart + * The name of the plain text template to render the plain part of the email with */ - private $plainPart; + private string $plainTemplate = ''; /** - * @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 = null; - /** - * @var array - */ - private $failedRecipients = []; + private ?ViewableData $dataAtLastRender = null; /** * Checks for RFC822-valid email format. * - * @param string $address - * @return boolean - * * @copyright Cal Henderson * 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()); + $validator = Injector::inst()->get(EmailValidator::class); + $rfcValidation = Injector::inst()->get(RFCValidation::class); + return $validator->isValid($address, $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) - { - // 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; - } + private static function mergeConfiguredAddresses(string $configKey, string $envKey): array + { + $addresses = []; + $config = (array) static::config()->get($configKey); + $addresses = self::convertConfigToAddreses($config); + $env = Environment::getEnv($envKey); + if ($env) { + $addresses = array_merge($addresses, self::convertConfigToAddreses($env)); } - $extra = Environment::getEnv($env); - if ($extra) { - $normalised[$extra] = null; + 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); } - return $normalised; + return $addresses; } /** @@ -179,23 +135,19 @@ protected static function mergeConfiguredEmails($config, $env) * 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 '' . strrev($email) . ''; case 'visible': $obfuscated = ['@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] ']; - return strtr($email, $obfuscated); case 'hex': $encoded = ''; @@ -203,36 +155,27 @@ public static function obfuscate($email, $method = 'visible') 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 +195,10 @@ public function __construct( if ($returnPath) { $this->setReturnPath($returnPath); } - - parent::__construct(); - } - - /** - * @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; + $this->data = ViewableData::create(); + $this->dataAtLastRender = $this->getData(); } - /** - * @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 @@ -323,439 +224,164 @@ private function getDefaultFrom(): string return $defaultFrom; } - /** - * @return string[] - */ - 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 ?? ''); - } - - /** - * @param string|array $address - * @param string|null $name - * @return $this - */ - public function setFrom($address, $name = null) - { - $address = $this->sanitiseAddress($address); - $this->getSwiftMessage()->setFrom($address, $name); - - return $this; + return [new Address($address, $name)]; } /** - * @param string|array $address - * @param string|null $name - * @return $this + * @see createAddressArray() */ - public function addFrom($address, $name = null) + public function setFrom(string|array $address, string $name = ''): static { - $address = $this->sanitiseAddress($address); - $this->getSwiftMessage()->addFrom($address, $name); - - return $this; + return $this->from(...$this->createAddressArray($address, $name)); } /** - * @return string + * @see createAddressArray() */ - public function getSender() + public function setTo(string|array $address, $name = ''): static { - return $this->getSwiftMessage()->getSender(); + return $this->to(...$this->createAddressArray($address, $name)); } /** - * @param string $address - * @param string|null $name - * @return $this + * @see createAddressArray() */ - public function setSender($address, $name = null) + public function setCC(string|array $address, string $name = ''): static { - $address = $this->sanitiseAddress($address); - $this->getSwiftMessage()->setSender($address, $name); - - return $this; + return $this->cc(...$this->createAddressArray($address, $name)); } /** - * @return string + * @see createAddressArray() */ - public function getReturnPath() + public function setBCC(string|array $address, string $name = ''): static { - return $this->getSwiftMessage()->getReturnPath(); + return $this->bcc(...$this->createAddressArray($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 setSender(string $address, string $name = ''): static { - $address = $this->sanitiseAddress($address); - $this->getSwiftMessage()->setReturnPath($address); - return $this; + return $this->sender(new Address($address, $name)); } - /** - * @return array - */ - public function getTo() + public function setReplyTo(string $address, string $name = ''): static { - return $this->getSwiftMessage()->getTo(); + return $this->replyTo(new Address($address, $name)); } - /** - * 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 setSubject(string $subject): static { - $address = $this->sanitiseAddress($address); - $this->getSwiftMessage()->setTo($address, $name); - - return $this; + return $this->subject($subject); } - /** - * @param string|array $address - * @param string|null $name - * @return $this - */ - public function addTo($address, $name = null) + public function setReturnPath(string $address): static { - $address = $this->sanitiseAddress($address); - $this->getSwiftMessage()->addTo($address, $name); - - return $this; + return $this->returnPath($address); } - /** - * @return array - */ - public function getCC() + public function setPriority(int $priority): static { - return $this->getSwiftMessage()->getCc(); + return $this->priority($priority); } - /** - * @param string|array $address - * @param string|null $name - * @return $this - */ - public function setCC($address, $name = null) + public function addAttachment(string $path, ?string $alias = null, ?string $mime = null): static { - $address = $this->sanitiseAddress($address); - $this->getSwiftMessage()->setCc($address, $name); - - return $this; + return $this->attachFromPath($path, $alias, $mime); } - /** - * @param string|array $address - * @param string|null $name - * @return $this - */ - public function addCC($address, $name = null) + public function addAttachmentFromData(string $data, string $name, string $mime = null): static { - $address = $this->sanitiseAddress($address); - $this->getSwiftMessage()->addCc($address, $name); - - return $this; + return $this->attach($data, $name, $mime); } /** - * @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; - } - - /** - * @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 + * 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 addAttachment($path, $alias = null, $mime = null) + public function getData(): ViewableData { - $attachment = \Swift_Attachment::fromPath($path); - if ($alias) { - $attachment->setFilename($alias); - } - 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; } /** - * @param string $data - * @param string $name - * @param string $mime - * @return $this + * Set template data */ - public function addAttachmentFromData($data, $name, $mime = null) + public function setData(array|ViewableData $data) { - $attachment = new \Swift_Attachment($data, $name); - if ($mime) { - $attachment->setContentType($mime); + if (is_array($data)) { + $data = new ArrayData($data); } - $this->getSwiftMessage()->attach($attachment); - - return $this; - } - - /** - * @return array|ViewableData The template data - */ - public function getData() - { - return $this->data; - } - - /** - * @param array|ViewableData $data The template data to set - * @return $this - */ - public function setData($data) - { $this->data = $data; - $this->invalidateBody(); - 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 + * @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; - } else { - $this->data->$name = $value; - } - - $this->invalidateBody(); - - return $this; - } - - /** - * Remove a datum from the message - * - * @param string $name - * @return $this - */ - public function removeData($name) - { - if (is_array($this->data)) { - unset($this->data[$name]); + if (is_array($nameOrData)) { + foreach ($nameOrData as $key => $val) { + $this->data->{$key} = $val; + } } else { - $this->data->$name = null; - } - - $this->invalidateBody(); - - 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); + $this->data->{$nameOrData} = $value; } - unset($plainPart); - - $body = HTTP::absoluteURLs($body); - $this->getSwiftMessage()->setBody($body); - return $this; } /** - * @deprecated 4.12.0 Will be replaced with html() - * - * @return $this + * Remove a single piece of template data */ - public function invalidateBody() + public function removeData(string $name) { - Deprecation::notice('4.12.0', 'Will be replaced with html()'); - $this->setBody(null); - + $this->data->{$name} = 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 "

Email template {$class}:

\n" . '
' . $this->getSwiftMessage()->toString() . '
'; - } - - /** - * @return string - */ - public function getHTMLTemplate() + public function getHTMLTemplate(): string { if ($this->HTMLTemplate) { return $this->HTMLTemplate; @@ -769,237 +395,135 @@ public function getHTMLTemplate() /** * 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->html(null); + $this->updateHtmlAndTextWithRenderedTemplates(true, $html); + Injector::inst()->get(MailerInterface::class)->send($this); + $this->html($html); } /** - * @deprecated 4.12.0 Will be removed without equivalent functionality to replace it - * - * Render the email - * @param bool $plainOnly Only render the message as plain text - * @return $this + * 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 */ - public function render($plainOnly = false) + private function updateHtmlAndTextWithRenderedTemplates(bool $plainOnly = false, ?string $htmlFallback = null): 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() ?: $htmlFallback; + $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; } // Do not interfere with emails styles Requirements::clear(); - // Render plain part - if ($plainTemplate && !$plainPart) { - $plainPart = $this->renderWith($plainTemplate, $this->getData())->Plain(); - } + $htmlRender = null; + $plainRender = null; + + $dataChangedSincedLastRender = $this->getDataHasChangedSinceLastRender(); - // 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()); + if ($htmlBody && !$dataChangedSincedLastRender) { + $htmlRender = $htmlBody; } - // Plain part fails over to generated from html - if (!$plainPart && $htmlPart) { - /** @var DBHTMLText $htmlPartObject */ - $htmlPartObject = DBField::create_field('HTMLFragment', $htmlPart); - $plainPart = $htmlPartObject->Plain(); + if ($plainBody && !$dataChangedSincedLastRender) { + $plainRender = $plainBody; } - // Rendering is finished - Requirements::restore(); + // Render plain + if (!$plainRender && $plainTemplate) { + $plainRender = $this->getData()->renderWith($plainTemplate)->Plain(); + } - // Fail if no email to send - if (!$plainPart && !$htmlPart) { - return $this; + // Render HTML + if (!$htmlRender && $htmlTemplate) { + $htmlRender = $this->getData()->renderWith($htmlTemplate)->RAW(); } - // 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'); + // Plain render fails over to generated from html + 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(); } - return $this; - } + // Handle edge case where data changed since last render and there's no template + if (!$htmlRender && $htmlBody) { + $htmlRender = $htmlBody; + } - /** - * @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; + // Remove the $htmlRender if was only used as fail over to create $plainRender + if ($plainOnly) { + $htmlRender = null; } - 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); + // Rendering is finished + Requirements::restore(); + + if ($plainRender) { + $this->text($plainRender); + } + if ($htmlRender) { + $this->html($htmlRender); } - unset($plainPart); - $this->getSwiftMessage()->addPart( - Convert::xml2raw($this->getBody()), - 'text/plain', - 'utf-8' - ); + $this->dataAtLastRender = $this->getData();#clone $this->data; + } - return $this; + private function getDataHasChangedSinceLastRender(): bool + { + return serialize($this->getData()) !== serialize($this->dataAtLastRender); } } diff --git a/src/Control/Email/EmailSender.php b/src/Control/Email/EmailSender.php new file mode 100644 index 00000000000..d1a088f0550 --- /dev/null +++ b/src/Control/Email/EmailSender.php @@ -0,0 +1,25 @@ +getMessage(); + /** @var RawMessage $rawMessage */ + $rawMessage = $sendEmailMessage->getMessage(); + /** @var TransportInterface $transport */ + $transport = Injector::inst()->get(TransportInterface::class); + $transport->send($rawMessage); + return $envelope; + } +} diff --git a/src/Control/Email/MailerSubscriber.php b/src/Control/Email/MailerSubscriber.php new file mode 100644 index 00000000000..6a09f067903 --- /dev/null +++ b/src/Control/Email/MailerSubscriber.php @@ -0,0 +1,116 @@ +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 [ + SendMessageToTransportsEvent::class => 'onSendMessageToTransports', + ]; + } + + public function onSendMessageToTransports(SendMessageToTransportsEvent $event): void + { + $envelope = $event->getEnvelope(); + /** @var SendEmailMessage $sendEmailMessage */ + $sendEmailMessage = $envelope->getMessage(); + $email = $sendEmailMessage->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(); + $xTo = $this->convertAddressesToString($email->getTo()); + $xCc = $this->convertAddressesToString($email->getCc()); + $xBcc = $this->convertAddressesToString($email->getBcc()); + + // set default recipient and remove all other recipients + $email->to(...$sendAllTo); + $email->cc(...[]); + $email->bcc(...[]); + + // store the old data as X-Original-* Headers for debugging + if ($xTo !== '') { + $headers->addMailboxHeader('X-Original-To', $xTo); + } + if ($xCc !== '') { + $headers->addMailboxHeader('X-Original-Cc', $xCc); + } + if ($xBcc !== '') { + $headers->addMailboxHeader('X-Original-Bcc', $xBcc); + } + } + + private function setFrom(Email $email, array $sendAllFrom): void + { + $headers = $email->getHeaders(); + $xFrom = $this->convertAddressesToString($email->getFrom()); + if ($xFrom !== '') { + $headers->addMailboxHeader('X-Original-From', $xFrom); + } + $email->from(...$sendAllFrom); + } + + private function convertAddressesToString(array $addresses): string + { + return implode(',', array_map(fn(Address $address) => $address->getAddress(), $addresses)); + } + + private function updateUrls(Email $email): void + { + if ($email->getHtmlBody()) { + $email->html(HTTP::absoluteURLs($email->getHtmlBody())); + } + if ($email->getTextBody()) { + $email->text(HTTP::absoluteURLs($email->getTextBody())); + } + } +} diff --git a/src/Core/BaseKernel.php b/src/Core/BaseKernel.php index 958a91615bb..451dc1f4a4c 100644 --- a/src/Core/BaseKernel.php +++ b/src/Core/BaseKernel.php @@ -28,6 +28,8 @@ use SilverStripe\View\ThemeManifest; use SilverStripe\View\ThemeResourceLoader; use Exception; +use SilverStripe\Control\Email\MailerSubscriber; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * Simple Kernel container @@ -243,6 +245,17 @@ protected function bootErrorHandling() } } + /** + * Register a subscriber for the symfony/mailer MailerEvent + * https://symfony.com/doc/current/components/event_dispatcher.html#using-event-subscribers + */ + protected function bootEmail() + { + $subscriber = MailerSubscriber::create(); + $dipatcher = Injector::inst()->get(EventDispatcherInterface::class . '.mailer'); + $dipatcher->addSubscriber($subscriber); + } + /** * Get the environment type * diff --git a/src/Core/CoreKernel.php b/src/Core/CoreKernel.php index c16d75999cd..53044e9e2e1 100644 --- a/src/Core/CoreKernel.php +++ b/src/Core/CoreKernel.php @@ -35,6 +35,7 @@ public function boot($flush = false) $this->bootErrorHandling(); $this->bootDatabaseEnvVars(); $this->bootConfigs(); + $this->bootEmail(); $this->bootDatabaseGlobals(); $this->validateDatabase(); diff --git a/src/Core/DatabaselessKernel.php b/src/Core/DatabaselessKernel.php index d5c981ba462..3fc85d6b051 100644 --- a/src/Core/DatabaselessKernel.php +++ b/src/Core/DatabaselessKernel.php @@ -49,6 +49,7 @@ public function boot($flush = false) $this->bootManifests($flush); $this->bootErrorHandling(); $this->bootConfigs(); + $this->bootEmail(); $this->setBooted(true); } diff --git a/src/Dev/SapphireTest.php b/src/Dev/SapphireTest.php index 965111b1284..4b5f0f4fb9a 100644 --- a/src/Dev/SapphireTest.php +++ b/src/Dev/SapphireTest.php @@ -15,7 +15,6 @@ 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\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. @@ -318,19 +321,18 @@ protected function setUp(): void } // 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'); - } + $testMailer = new TestMailer( + new NullTransport(), + Injector::inst()->get(MessageBusInterface::class . '.mailer'), + Injector::inst()->get(EventDispatcherInterface::class . '.mailer'), + ); + 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 +613,8 @@ protected function tearDown(): void */ 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 +634,8 @@ public function clearEmails() */ 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); } diff --git a/src/Dev/TestMailer.php b/src/Dev/TestMailer.php index 44627e62c45..d250b08543e 100644 --- a/src/Dev/TestMailer.php +++ b/src/Dev/TestMailer.php @@ -2,98 +2,81 @@ namespace SilverStripe\Dev; -use SilverStripe\Control\Email\Mailer; -use Swift_Attachment; +use InvalidArgumentException; +use SilverStripe\Control\Email\Email; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Mailer\Envelope; +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 = []; - - public function send($email) - { - // 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(); - } - } + private array $emailsSent = []; - // 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() ?: [])), - 'Subject' => $email->getSubject(), - 'Content' => $email->getBody(), - 'AttachedFiles' => $attachedFiles, - 'Headers' => $email->getSwiftMessage()->getHeaders(), - ]; - if ($plainContent) { - $serialised['PlainContent'] = $plainContent; - } - if ($htmlContent) { - $serialised['HtmlContent'] = $htmlContent; - } - - $this->saveEmail($serialised); - - return true; - } + private TransportInterface $transport; + private ?MessageBusInterface $bus; + private ?EventDispatcherInterface $dispatcher; /** - * Save a single email to the log + * This constructor has params that match Symfony\Component\Mailer\Mailer, rather + * than Symfony\Component\Mailer\MailerInterface, so that it can be Injected in an + * indentical fashion. * - * @param array $data A map of information about the email + * Note - $transport and $bus are never actually used because Email are never supposed to + * be send. Dispatcher is however used to support SilverStripe\Control\Email\MailerSubscriber */ - protected function saveEmail($data) - { - $this->emailsSent[] = $data; + public function __construct( + TransportInterface $transport, + MessageBusInterface $bus = null, + EventDispatcherInterface $dispatcher = null + ) { + $this->transport = $transport; + $this->bus = $bus; + $this->dispatcher = $dispatcher; } - /** - * Clear the log of emails sent - */ - public function clearEmails() + public function send(RawMessage $message, Envelope $envelope = null): void { - $this->emailsSent = []; + if (!is_a($message, Email::class)) { + throw new InvalidArgumentException('$message must be a ' . Email::class); + } + /** @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->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()), + ]; } /** * 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 +114,27 @@ public function findEmail($to, $from = null, $subject = null, $content = null) } /** - * @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(RawMessage $message, Envelope $envelope = null): void + { + $sendEmailMessage = new SendEmailMessage($message, $envelope); + $messagerEnvelope = new MessagerEnvelope($sendEmailMessage); + $event = new SendMessageToTransportsEvent($messagerEnvelope); + $this->dispatcher->dispatch($event); + } + + private function normaliseSpaces(string $value): string { return str_replace([', ', '; '], [',', ';'], $value ?? ''); } diff --git a/src/Security/Member.php b/src/Security/Member.php index cc68bfec6d3..83c207e6ce3 100644 --- a/src/Security/Member.php +++ b/src/Security/Member.php @@ -9,7 +9,6 @@ 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\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 @@ public function onBeforeWrite() // 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 diff --git a/tests/php/Control/Email/EmailTest.php b/tests/php/Control/Email/EmailTest.php index daa69ae41fb..2a3e66982b6 100644 --- a/tests/php/Control/Email/EmailTest.php +++ b/tests/php/Control/Email/EmailTest.php @@ -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 { - $email = new Email(); + 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(); - - $this->assertCount(1, $children); - - /** @var Swift_Attachment $child */ - $child = reset($children); + $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->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 @@ public function testValidEmailAddress($email) /** * @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,256 @@ public function testObfuscate() ); } - 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(); + return implode('\\', ['SilverStripe', 'Control', 'Tests', 'Email', 'EmailTest', $templateName]); + } - $this->assertTrue($successful); - $this->assertEmpty($email->getFailedRecipients()); + private function getMailer(): TestMailer + { + return Injector::inst()->get(MailerInterface::class); + } - /** @var TestMailer $mailer */ - $mailer = Injector::inst()->get(Mailer::class); - $sentMail = $mailer->findEmail('to@example.com'); + private function createTestEmail(string $subject = 'My subject', $setPlain = true): Email + { + $email = new Email(); + $email->setFrom('from@example.com'); + $email->setTo('to@example.com'); + $email->setSubject($subject); + if ($setPlain) { + $email->text("Plain body for $subject"); + } + $email->html("

HTML body for $subject

"); + $email->setCC('cc@example.com'); + $email->setBCC('bcc@example.com'); + $email->addAttachment(__DIR__ . '/EmailTest/attachment.txt', null, 'text/plain'); + return $email; + } - $this->assertTrue(is_array($sentMail)); + 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->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->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->assertEquals('text/plain', $child['mimetype']); - $this->assertEquals('attachment.txt', $child['filename']); - $this->assertEquals('Hello, I\'m a text document.', $child['contents']); + $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 testSend() + public function testSendPlainFallback(): void { - /** @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(); + $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']); + } - $this->assertTrue($successful); - $this->assertEmpty($email->getFailedRecipients()); + public function testSend(): void + { + $email = $this->createTestEmail('Test send HTML'); - /** @var TestMailer $mailer */ - $mailer = Injector::inst()->get(Mailer::class); - $sentMail = $mailer->findEmail('to@example.com'); + // email should not call render if a body is supplied + $email->setHTMLTemplate($this->getTemplateClass('HtmlTemplate')); + $email->send(); + $this->assertStringNotContainsString($email->getHtmlBody(), 'My HTML Template'); - $this->assertTrue(is_array($sentMail)); + $sentMail = $this->getMailer()->findEmail('to@example.com'); - $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->assertSame('to@example.com', $sentMail['To']); + $this->assertSame('from@example.com', $sentMail['From']); + $this->assertSame('Test send HTML', $sentMail['Subject']); + $this->assertStringContainsString('

HTML 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']); + $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() + public function testRenderedSend(): void { - /** @var Email|MockObject $email */ - $email = $this->getMockBuilder(Email::class) - ->enableProxyingToOriginalMethods() - ->getMock(); - $email->setFrom('from@example.com'); - $email->setTo('to@example.com'); + $email = new Email(to: 'to@example.com'); + $email->setHTMLTemplate($this->getTemplateClass('HtmlTemplate')); $email->setData([ - 'EmailContent' => 'test', + '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()); + $sentMail = $this->getMailer()->findEmail('to@example.com'); + $this->assertStringContainsString('My HTML Template', $sentMail['Content']); } - public function testRenderedSendSubclass() + public function testRenderedSendSubclass(): void { - // 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 = new EmailSubClass(to: '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('

Email Sub-class

', $email->getBody()); + $sentMail = $this->getMailer()->findEmail('to@example.com'); + $this->assertStringContainsString('

Email Sub-class

', $sentMail['Content']); } - public function testConsturctor() + public function testConstructor(): void { $email = new Email( 'from@example.com', 'to@example.com', 'subject', - 'body', + '

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->assertSame('from@example.com', $email->getFrom()[0]->getAddress()); $this->assertCount(1, $email->getTo()); - $this->assertContains('to@example.com', array_keys($email->getTo() ?? [])); + $this->assertSame('to@example.com', $email->getTo()[0]->getAddress()); $this->assertEquals('subject', $email->getSubject()); - $this->assertEquals('body', $email->getBody()); + $this->assertEquals('

body

', $email->getHtmlBody()); $this->assertCount(1, $email->getCC()); - $this->assertContains('cc@example.com', array_keys($email->getCC() ?? [])); + $this->assertEquals('cc@example.com', $email->getCC()[0]->getAddress()); $this->assertCount(1, $email->getBCC()); - $this->assertContains('bcc@example.com', array_keys($email->getBCC() ?? [])); - $this->assertEquals('bounce@example.com', $email->getReturnPath()); + $this->assertEquals('bcc@example.com', $email->getBcc()[0]->getAddress()); + $this->assertEquals('bounce@example.com', $email->getReturnPath()->getAddress()); } - public function testGetSwiftMessage() + public function testSetBody(): void { - $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()); - - // check from field is retained - $swiftMessage = new Swift_Message(); - $swiftMessage->setFrom('from@example.com'); - $email->setSwiftMessage($swiftMessage); - $this->assertCount(1, $email->getFrom()); - $this->assertContains('from@example.com', array_keys($email->getFrom() ?? [])); + $email->setBody('

body

'); + $this->assertSame('

body

', $email->getHtmlBody()); } - public function testAdminEmailApplied() + public function testSetFrom(): void { - 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() ?? [])); - } - - 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'); + $email->setFrom('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() ?? [])); + $this->assertSame('from@example.com', $email->getFrom()[0]->getAddress()); } - public function testSetGetSender() + public function testSender(): void { $email = new Email(); - $this->assertEmpty($email->getSender()); - $email->setSender('sender@example.com', 'Silver Stripe'); - $this->assertEquals(['sender@example.com' => 'Silver Stripe'], $email->getSender()); + $email->setSender('sender@example.com'); + $this->assertSame('sender@example.com', $email->getSender()->getAddress()); } - public function testSetGetReturnPath() + public function testSetTo(): void { $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'); + $email->setTo('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()); + $this->assertSame('to@example.com', $email->getTo()[0]->getAddress()); } - public function testAddCC() + public function testSetReplyTo(): void { - $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()); + $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 testAddBCC() + public function testSetSubject(): void { - $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() ?? [])); + $email = new Email(); + $email->setSubject('my subject'); + $this->assertSame('my subject', $email->getSubject()); } - public function testReplyTo() + public function testSetReturnPath(): void { $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() ?? [])); + $email->setReturnPath('return-path@example.com'); + $this->assertSame('return-path@example.com', $email->getReturnPath()->getAddress()); } - public function testSubject() + public function testSetPriority(): void { - $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()); + $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 testPriority() + public function testAdminEmailApplied() { + Email::config()->update('admin_email', 'admin@example.com'); $email = new Email(); - $this->assertEquals(3, $email->getPriority()); - $email->setPriority(5); - $this->assertEquals(5, $email->getPriority()); + $this->assertCount(1, $email->getFrom()); + $this->assertSame('admin@example.com', $email->getFrom()[0]->getAddress()); } - public function testData() + public function testDataWithArray(): void { $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() - { - $email = new Email(); - $this->assertEmpty($email->getBody()); - $email->setBody('

Title

'); - $this->assertEquals('

Title

', $email->getBody()); - } - - public function testHTMLTemplate() + public function testHTMLTemplate(): void { - // 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 +387,7 @@ public function testHTMLTemplate() $this->assertEquals('MyTemplate', $email->getHTMLTemplate()); } - public function testPlainTemplate() + public function testPlainTemplate(): void { $email = new Email(); $this->assertEmpty($email->getPlainTemplate()); @@ -530,145 +395,64 @@ public function testPlainTemplate() $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' => '

my content

', ]); - $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()); - - // ensure repeat renders don't add multiple plain parts - $email->render(); - $this->assertCount(1, $email->getSwiftMessage()->getChildren()); - } + $email->send(); + $this->assertStringContainsString('<p>my content</p>', $email->getHtmlBody()); - public function testRerender() - { - $email = new Email(); - $email->setData([ - 'EmailContent' => 'my content', - ]); - $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()); - - // Ensure setting data causes a rerender + // Ensure setting data causes html() to be updated $email->setData([ - 'EmailContent' => 'your content' + 'EmailContent' => '

your content

' ]); - $email->render(); - $this->assertStringContainsString('your content', $email->getBody()); + $email->send(); + $this->assertStringContainsString('<p>your content</p>', $email->getHtmlBody()); - // 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' => '

their content

' ]); - $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' => '

Test

', ]); - //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('

Test

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

Test

', $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 +479,43 @@ public function testGetDefaultFrom() } /** - * @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', ''), + ], + ] + ]; } } diff --git a/tests/php/Control/Email/EmailTest/templates/SilverStripe/Control/Tests/Email/EmailTest/HtmlTemplate.ss b/tests/php/Control/Email/EmailTest/templates/SilverStripe/Control/Tests/Email/EmailTest/HtmlTemplate.ss new file mode 100644 index 00000000000..c998656c8ba --- /dev/null +++ b/tests/php/Control/Email/EmailTest/templates/SilverStripe/Control/Tests/Email/EmailTest/HtmlTemplate.ss @@ -0,0 +1,14 @@ + + + + <% base_tag %> + + + +
+

My HTML Template

+ $EmailContent +
+ + + diff --git a/tests/php/Control/Email/EmailTest/templates/SilverStripe/Control/Tests/Email/EmailTest/PlainTemplate.ss b/tests/php/Control/Email/EmailTest/templates/SilverStripe/Control/Tests/Email/EmailTest/PlainTemplate.ss new file mode 100644 index 00000000000..dfb49ab894b --- /dev/null +++ b/tests/php/Control/Email/EmailTest/templates/SilverStripe/Control/Tests/Email/EmailTest/PlainTemplate.ss @@ -0,0 +1,3 @@ +# My Plain Template + +$EmailContent diff --git a/tests/php/Control/Email/MailerSubscriberTest.php b/tests/php/Control/Email/MailerSubscriberTest.php new file mode 100644 index 00000000000..fc3629d3ed0 --- /dev/null +++ b/tests/php/Control/Email/MailerSubscriberTest.php @@ -0,0 +1,100 @@ +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)->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()); + } +} diff --git a/tests/php/Control/Email/SwiftMailerTest.php b/tests/php/Control/Email/SwiftMailerTest.php deleted file mode 100644 index 43a7f98d141..00000000000 --- a/tests/php/Control/Email/SwiftMailerTest.php +++ /dev/null @@ -1,78 +0,0 @@ -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); - } -} diff --git a/tests/php/Control/Email/SwiftPluginTest.php b/tests/php/Control/Email/SwiftPluginTest.php deleted file mode 100644 index 5ca702d8efd..00000000000 --- a/tests/php/Control/Email/SwiftPluginTest.php +++ /dev/null @@ -1,110 +0,0 @@ -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() ?? [])); - } -} diff --git a/thirdparty/swiftmailer/Swift/MailTransport.php b/thirdparty/swiftmailer/Swift/MailTransport.php deleted file mode 100644 index ec3b892a2cd..00000000000 --- a/thirdparty/swiftmailer/Swift/MailTransport.php +++ /dev/null @@ -1,75 +0,0 @@ -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; - } -} diff --git a/thirdparty/swiftmailer/Swift/Transport/MailInvoker.php b/thirdparty/swiftmailer/Swift/Transport/MailInvoker.php deleted file mode 100644 index 2a23e24d2d5..00000000000 --- a/thirdparty/swiftmailer/Swift/Transport/MailInvoker.php +++ /dev/null @@ -1,40 +0,0 @@ -_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() - { - } -} diff --git a/thirdparty/swiftmailer/Swift/Transport/SimpleMailInvoker.php b/thirdparty/swiftmailer/Swift/Transport/SimpleMailInvoker.php deleted file mode 100644 index 8094e542856..00000000000 --- a/thirdparty/swiftmailer/Swift/Transport/SimpleMailInvoker.php +++ /dev/null @@ -1,47 +0,0 @@ -