From 2e85674cccc1586b8ee7085d0d277a0528b7c918 Mon Sep 17 00:00:00 2001 From: Steve Boyd Date: Wed, 19 Oct 2022 15:16:14 +1300 Subject: [PATCH] NEW Migrate from swiftmailer/swiftmailer to symfony/mailer --- _config/email.yml | 12 - _config/mailer.yml | 28 + composer.json | 6 +- src/Control/Email/Email.php | 919 +++++------------- src/Control/Email/MailerSubscriber.php | 100 ++ src/Control/Email/SwiftMailer.php | 82 -- src/Control/Email/SwiftPlugin.php | 83 -- src/Dev/SapphireTest.php | 34 +- src/Dev/TestMailer.php | 155 ++- src/Security/Member.php | 4 +- tests/php/Control/Email/EmailTest.php | 851 +++++++--------- .../Tests/Email/EmailTest/HtmlTemplate.ss | 14 + .../Tests/Email/EmailTest/PlainTemplate.ss | 3 + .../Control/Email/MailerSubscriberTest.php | 100 ++ tests/php/Control/Email/SwiftMailerTest.php | 78 -- tests/php/Control/Email/SwiftPluginTest.php | 110 --- .../swiftmailer/Swift/MailTransport.php | 75 -- .../Swift/Transport/MailInvoker.php | 40 - .../Swift/Transport/MailTransport.php | 313 ------ .../Swift/Transport/SimpleMailInvoker.php | 47 - 20 files changed, 898 insertions(+), 2156 deletions(-) delete mode 100644 _config/email.yml create mode 100644 _config/mailer.yml create mode 100644 src/Control/Email/MailerSubscriber.php delete mode 100644 src/Control/Email/SwiftMailer.php delete mode 100644 src/Control/Email/SwiftPlugin.php create mode 100644 tests/php/Control/Email/EmailTest/templates/SilverStripe/Control/Tests/Email/EmailTest/HtmlTemplate.ss create mode 100644 tests/php/Control/Email/EmailTest/templates/SilverStripe/Control/Tests/Email/EmailTest/PlainTemplate.ss create mode 100644 tests/php/Control/Email/MailerSubscriberTest.php delete mode 100644 tests/php/Control/Email/SwiftMailerTest.php delete mode 100644 tests/php/Control/Email/SwiftPluginTest.php delete mode 100644 thirdparty/swiftmailer/Swift/MailTransport.php delete mode 100644 thirdparty/swiftmailer/Swift/Transport/MailInvoker.php delete mode 100644 thirdparty/swiftmailer/Swift/Transport/MailTransport.php delete mode 100644 thirdparty/swiftmailer/Swift/Transport/SimpleMailInvoker.php diff --git a/_config/email.yml b/_config/email.yml deleted file mode 100644 index 8842d39d2..000000000 --- a/_config/email.yml +++ /dev/null @@ -1,12 +0,0 @@ ---- -Name: emailconfig ---- -SilverStripe\Core\Injector\Injector: - Swift_Transport: Swift_MailTransport - Swift_Mailer: - constructor: - - '%$Swift_Transport' - SilverStripe\Control\Email\Mailer: - class: SilverStripe\Control\Email\SwiftMailer - properties: - SwiftMailer: '%$Swift_Mailer' diff --git a/_config/mailer.yml b/_config/mailer.yml new file mode 100644 index 000000000..541927251 --- /dev/null +++ b/_config/mailer.yml @@ -0,0 +1,28 @@ +--- +Name: mailer +--- +SilverStripe\Core\Injector\Injector: + Symfony\Component\Mailer\MailerInterface: + class: Symfony\Component\Mailer\Mailer + constructor: + transport: '%$Symfony\Component\Mailer\Transport\TransportInterface' + Symfony\Component\EventDispatcher\EventDispatcherInterface.mailer: + class: Symfony\Component\EventDispatcher\EventDispatcher + calls: + - [addSubscriber, ['%$SilverStripe\Control\Email\MailerSubscriber']] + Symfony\Component\Mailer\Transport\TransportInterface: + factory: Symfony\Component\Mailer\Transport + factory_method: fromDsn + constructor: + dsn: 'sendmail://default' + dispatcher: '%$Symfony\Component\EventDispatcher\EventDispatcherInterface.mailer' +--- +Name: mailer-dsn-env +After: '*' +Only: + envvarset: MAILER_DSN +--- +SilverStripe\Core\Injector\Injector: + Symfony\Component\Mailer\Transport\TransportInterface: + constructor: + dsn: '`MAILER_DSN`' diff --git a/composer.json b/composer.json index 1cd80f8cd..7c1592be0 100644 --- a/composer.json +++ b/composer.json @@ -37,10 +37,11 @@ "silverstripe/assets": "^2", "silverstripe/vendor-plugin": "^2", "sminnee/callbacklist": "^0.1.1", - "swiftmailer/swiftmailer": "^6.3.0", "symfony/cache": "^6.1", "symfony/config": "^6.1", "symfony/filesystem": "^6.1", + "symfony/mailer": "^6.1", + "symfony/mime": "^6.1", "symfony/translation": "^6.1", "symfony/yaml": "^6.1", "ext-ctype": "*", @@ -96,9 +97,6 @@ }, "files": [ "src/includes/constants.php" - ], - "classmap": [ - "thirdparty/swiftmailer" ] }, "include-path": [ diff --git a/src/Control/Email/Email.php b/src/Control/Email/Email.php index d6eb96801..fa83e8c00 100644 --- a/src/Control/Email/Email.php +++ b/src/Control/Email/Email.php @@ -2,176 +2,131 @@ namespace SilverStripe\Control\Email; -use DateTime; +use Exception; use RuntimeException; use Egulias\EmailValidator\EmailValidator; use Egulias\EmailValidator\Validation\RFCValidation; use SilverStripe\Control\Director; -use SilverStripe\Control\HTTP; -use SilverStripe\Core\Convert; +use SilverStripe\Core\Config\Configurable; use SilverStripe\Core\Environment; +use SilverStripe\Core\Extensible; +use SilverStripe\Core\Injector\Injectable; use SilverStripe\Core\Injector\Injector; -use SilverStripe\Dev\Deprecation; -use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\ORM\FieldType\DBField; -use SilverStripe\ORM\FieldType\DBHTMLText; +use SilverStripe\View\ArrayData; use SilverStripe\View\Requirements; use SilverStripe\View\SSViewer; use SilverStripe\View\ThemeResourceLoader; use SilverStripe\View\ViewableData; -use Swift_Message; -use Swift_Mime_SimpleMessage; -use Swift_MimePart; +use Symfony\Component\Mailer\MailerInterface; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Email as SymfonyEmail; +use Symfony\Component\Mime\Part\AbstractPart; -/** - * Class to support sending emails. - */ -class Email extends ViewableData +class Email extends SymfonyEmail { - /** - * @var array - * @config - */ - private static $send_all_emails_to = []; - - /** - * @var array - * @config - */ - private static $cc_all_emails_to = []; - - /** - * @var array - * @config - */ - private static $bcc_all_emails_to = []; - - /** - * @var array - * @config - */ - private static $send_all_emails_from = []; + use Configurable; + use Extensible; + use Injectable; + + private static string|array $send_all_emails_to = []; + + private static string|array $cc_all_emails_to = []; + + private static string|array $bcc_all_emails_to = []; + + private static string|array $send_all_emails_from = []; /** + * The default "from" email address or array of [email => name], or the email address as a string * This will be set in the config on a site-by-site basis * @see https://docs.silverstripe.org/en/4/developer_guides/email/#administrator-emails - * - * @config - * @var string|array The default administrator email address or array of [email => name] */ - private static $admin_email = null; + private static string|array $admin_email = ''; /** - * @var Swift_Message + * The name of the HTML template to render the email with (without *.ss extension) */ - private $swiftMessage; + private string $HTMLTemplate = ''; /** - * @var string The name of the HTML template to render the email with (without *.ss extension) + * The name of the plain text template to render the plain part of the email with */ - private $HTMLTemplate = null; + private string $plainTemplate = ''; /** - * @var string The name of the plain text template to render the plain part of the email with - */ - private $plainTemplate = null; - - /** - * @var Swift_MimePart - */ - private $plainPart; - - /** - * @var array|ViewableData Additional data available in a template. + * Additional data available in a template. * Used in the same way than {@link ViewableData->customize()}. */ - private $data = []; + private ViewableData $data; - /** - * @var array - */ - private $failedRecipients = []; + private bool $dataHasBeenSet = false; /** * Checks for RFC822-valid email format. * - * @param string $address - * @return boolean - * * @copyright Cal Henderson * This code is licensed under a Creative Commons Attribution-ShareAlike 2.5 License * http://creativecommons.org/licenses/by-sa/2.5/ */ - public static function is_valid_address($address) + public static function is_valid_address(string $address): bool { $validator = new EmailValidator(); return $validator->isValid($address, new RFCValidation()); } - /** - * Get send_all_emails_to - * - * @return array Keys are addresses, values are names - */ - public static function getSendAllEmailsTo() + public static function getSendAllEmailsTo(): array { - return static::mergeConfiguredEmails('send_all_emails_to', 'SS_SEND_ALL_EMAILS_TO'); + return static::mergeConfiguredAddresses('send_all_emails_to', 'SS_SEND_ALL_EMAILS_TO'); } - /** - * Get cc_all_emails_to - * - * @return array - */ - public static function getCCAllEmailsTo() + public static function getCCAllEmailsTo(): array { - return static::mergeConfiguredEmails('cc_all_emails_to', 'SS_CC_ALL_EMAILS_TO'); + return static::mergeConfiguredAddresses('cc_all_emails_to', 'SS_CC_ALL_EMAILS_TO'); } - /** - * Get bcc_all_emails_to - * - * @return array - */ - public static function getBCCAllEmailsTo() + public static function getBCCAllEmailsTo(): array { - return static::mergeConfiguredEmails('bcc_all_emails_to', 'SS_BCC_ALL_EMAILS_TO'); + return static::mergeConfiguredAddresses('bcc_all_emails_to', 'SS_BCC_ALL_EMAILS_TO'); } - /** - * Get send_all_emails_from - * - * @return array - */ - public static function getSendAllEmailsFrom() + public static function getSendAllEmailsFrom(): array { - return static::mergeConfiguredEmails('send_all_emails_from', 'SS_SEND_ALL_EMAILS_FROM'); + return static::mergeConfiguredAddresses('send_all_emails_from', 'SS_SEND_ALL_EMAILS_FROM'); } /** * Normalise email list from config merged with env vars * - * @param string $config Config key - * @param string $env Env variable key - * @return array Array of email addresses + * @return Address[] */ - protected static function mergeConfiguredEmails($config, $env) + private static function mergeConfiguredAddresses(string $configKey, string $envKey): array { - // Normalise config list - $normalised = []; - $source = (array)static::config()->get($config); - foreach ($source as $address => $name) { - if ($address && !is_numeric($address)) { - $normalised[$address] = $name; - } elseif ($name) { - $normalised[$name] = null; + $addresses = []; + $config = (array) static::config()->get($configKey); + $addresses = self::convertConfigToAddreses($config); + $env = Environment::getEnv($envKey); + if ($env) { + $addresses = array_merge($addresses, self::convertConfigToAddreses($env)); + } + return $addresses; + } + + private static function convertConfigToAddreses(array|string $config): array + { + $addresses = []; + if (is_array($config)) { + foreach ($config as $key => $val) { + if (filter_var($key, FILTER_VALIDATE_EMAIL)) { + $addresses[] = new Address($key, $val); + } else { + $addresses[] = new Address($val); + } } + } else { + $addresses[] = new Address($config); } - $extra = Environment::getEnv($env); - if ($extra) { - $normalised[$extra] = null; - } - return $normalised; + return $addresses; } /** @@ -179,23 +134,19 @@ class Email extends ViewableData * At the moment only simple string substitutions, * which are not 100% safe from email harvesting. * - * @param string $email Email-address - * @param string $method Method for obfuscating/encoding the address - * - 'direction': Reverse the text and then use CSS to put the text direction back to normal - * - 'visible': Simple string substitution ('@' to '[at]', '.' to '[dot], '-' to [dash]) - * - 'hex': Hexadecimal URL-Encoding - useful for mailto: links - * @return string + * $method defines the method for obfuscating/encoding the address + * - 'direction': Reverse the text and then use CSS to put the text direction back to normal + * - 'visible': Simple string substitution ('@' to '[at]', '.' to '[dot], '-' to [dash]) + * - 'hex': Hexadecimal URL-Encoding - useful for mailto: links */ - public static function obfuscate($email, $method = 'visible') + public static function obfuscate(string $email, string $method = 'visible'): string { switch ($method) { case 'direction': Requirements::customCSS('span.codedirection { unicode-bidi: bidi-override; direction: rtl; }', 'codedirectionCSS'); - return '' . strrev($email) . ''; case 'visible': $obfuscated = ['@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] ']; - return strtr($email, $obfuscated); case 'hex': $encoded = ''; @@ -203,36 +154,27 @@ class Email extends ViewableData for ($x = 0; $x < $emailLength; $x++) { $encoded .= '&#x' . bin2hex($email[$x]) . ';'; } - return $encoded; default: user_error('Email::obfuscate(): Unknown obfuscation method', E_USER_NOTICE); - return $email; } } - /** - * Email constructor. - * @param string|array|null $from - * @param string|array|null $to - * @param string|null $subject - * @param string|null $body - * @param string|array|null $cc - * @param string|array|null $bcc - * @param string|null $returnPath - */ public function __construct( - $from = null, - $to = null, - $subject = null, - $body = null, - $cc = null, - $bcc = null, - $returnPath = null + string $from = '', + string $to = '', + string $subject = '', + string $body = '', + string $cc = '', + string $bcc = '', + string $returnPath = '' ) { + parent::__construct(); if ($from) { $this->setFrom($from); + } else { + $this->setFrom($this->getDefaultFrom()); } if ($to) { $this->setTo($to); @@ -252,52 +194,9 @@ class Email extends ViewableData if ($returnPath) { $this->setReturnPath($returnPath); } - - parent::__construct(); + $this->data = ViewableData::create(); } - /** - * @deprecated 4.12.0 Will be removed without equivalent functionality to replace it - * - * @return Swift_Message - */ - public function getSwiftMessage() - { - Deprecation::notice('4.12.0', 'Will be removed without equivalent functionality to replace it'); - if (!$this->swiftMessage) { - $message = new Swift_Message(null, null, 'text/html', 'utf-8'); - // Set priority to fix PHP 8.1 SimpleMessage::getPriority() sscanf() null parameter - $message->setPriority(Swift_Mime_SimpleMessage::PRIORITY_NORMAL); - $this->setSwiftMessage($message); - } - - return $this->swiftMessage; - } - - /** - * @deprecated 4.12.0 Will be removed without equivalent functionality to replace it - * - * @param Swift_Message $swiftMessage - * - * @return $this - */ - public function setSwiftMessage($swiftMessage) - { - Deprecation::notice('4.12.0', 'Will be removed without equivalent functionality to replace it'); - $dateTime = new DateTime(); - $dateTime->setTimestamp(DBDatetime::now()->getTimestamp()); - $swiftMessage->setDate($dateTime); - if (!$swiftMessage->getFrom()) { - $swiftMessage->setFrom($this->getDefaultFrom()); - } - $this->swiftMessage = $swiftMessage; - - return $this; - } - - /** - * @return string - */ private function getDefaultFrom(): string { // admin_email can have a string or an array config @@ -324,438 +223,178 @@ class Email extends ViewableData } /** - * @return string[] + * Passing a string of HTML for $body will have no affect if you also call either setData() or addData() */ - public function getFrom() + public function setBody(AbstractPart|string $body = null): static { - return $this->getSwiftMessage()->getFrom(); + if ($body instanceof AbstractPart) { + // pass to Symfony\Component\Mime\Message::setBody() + return parent::setBody($body); + } + // Set HTML content directly. + return $this->html($body); } /** - * @param string|array $address - * @return string|array + * The following arguments combinations are valid + * a) $address = 'my@email.com', $name = 'My name' + * b) $address = ['my@email.com' => 'My name', 'other@email.com' => 'My other name'] + * c) $address = ['my@email.com' => 'My name', 'other@email.com'] */ - private function sanitiseAddress($address) + private function createAddressArray(string|array $address, $name = ''): array { if (is_array($address)) { - return array_map('trim', $address ?? []); + $ret = []; + foreach ($address as $key => $val) { + $addr = is_numeric($key) ? $val : $key; + $name2 = is_numeric($key) ? '' : $val; + $ret[] = new Address($addr, $name2); + } + return $ret; } - return trim($address ?? ''); + return [new Address($address, $name)]; } /** - * @param string|array $address - * @param string|null $name - * @return $this + * @see createAddressArray() */ - public function setFrom($address, $name = null) + public function setFrom(string|array $address, string $name = ''): static { - $address = $this->sanitiseAddress($address); - $this->getSwiftMessage()->setFrom($address, $name); - - return $this; + return $this->from(...$this->createAddressArray($address, $name)); } /** - * @param string|array $address - * @param string|null $name - * @return $this + * @see createAddressArray() */ - public function addFrom($address, $name = null) + public function setTo(string|array $address, $name = ''): static { - $address = $this->sanitiseAddress($address); - $this->getSwiftMessage()->addFrom($address, $name); - - return $this; + return $this->to(...$this->createAddressArray($address, $name)); } /** - * @return string + * @see createAddressArray() */ - public function getSender() + public function setCC(string|array $address, string $name = ''): static { - return $this->getSwiftMessage()->getSender(); + return $this->cc(...$this->createAddressArray($address, $name)); } /** - * @param string $address - * @param string|null $name - * @return $this + * @see createAddressArray() */ - public function setSender($address, $name = null) + public function setBCC(string|array $address, string $name = ''): static { - $address = $this->sanitiseAddress($address); - $this->getSwiftMessage()->setSender($address, $name); - - return $this; + return $this->bcc(...$this->createAddressArray($address, $name)); } - /** - * @return string - */ - public function getReturnPath() + public function setSender(string $address, string $name = ''): static { - return $this->getSwiftMessage()->getReturnPath(); + return $this->sender(new Address($address, $name)); } - /** - * The bounce handler address - * - * @param string $address Email address where bounce notifications should be sent - * @return $this - */ - public function setReturnPath($address) + public function setReplyTo(string $address, string $name = ''): static { - $address = $this->sanitiseAddress($address); - $this->getSwiftMessage()->setReturnPath($address); - return $this; + return $this->replyTo(new Address($address, $name)); } - /** - * @return array - */ - public function getTo() + public function setSubject(string $subject): static { - return $this->getSwiftMessage()->getTo(); + return $this->subject($subject); } - /** - * Set recipient(s) of the email - * - * To send to many, pass an array: - * ['me@example.com' => 'My Name', 'other@example.com']; - * - * @param string|array $address The message recipient(s) - if sending to multiple, use an array of address => name - * @param string|null $name The name of the recipient (if one) - * @return $this - */ - public function setTo($address, $name = null) + public function setReturnPath(string $address): static { - $address = $this->sanitiseAddress($address); - $this->getSwiftMessage()->setTo($address, $name); - - return $this; + return $this->returnPath($address); } - /** - * @param string|array $address - * @param string|null $name - * @return $this - */ - public function addTo($address, $name = null) + public function setPriority(int $priority): static { - $address = $this->sanitiseAddress($address); - $this->getSwiftMessage()->addTo($address, $name); - - return $this; - } - - /** - * @return array - */ - public function getCC() - { - return $this->getSwiftMessage()->getCc(); - } - - /** - * @param string|array $address - * @param string|null $name - * @return $this - */ - public function setCC($address, $name = null) - { - $address = $this->sanitiseAddress($address); - $this->getSwiftMessage()->setCc($address, $name); - - return $this; - } - - /** - * @param string|array $address - * @param string|null $name - * @return $this - */ - public function addCC($address, $name = null) - { - $address = $this->sanitiseAddress($address); - $this->getSwiftMessage()->addCc($address, $name); - - return $this; - } - - /** - * @return array - */ - public function getBCC() - { - return $this->getSwiftMessage()->getBcc(); - } - - /** - * @param string|array $address - * @param string|null $name - * @return $this - */ - public function setBCC($address, $name = null) - { - $address = $this->sanitiseAddress($address); - $this->getSwiftMessage()->setBcc($address, $name); - - return $this; - } - - /** - * @param string|array $address - * @param string|null $name - * @return $this - */ - public function addBCC($address, $name = null) - { - $address = $this->sanitiseAddress($address); - $this->getSwiftMessage()->addBcc($address, $name); - - return $this; - } - - /** - * @return mixed - */ - public function getReplyTo() - { - return $this->getSwiftMessage()->getReplyTo(); - } - - /** - * @param string|array $address - * @param string|null $name - * @return $this - */ - public function setReplyTo($address, $name = null) - { - $address = $this->sanitiseAddress($address); - $this->getSwiftMessage()->setReplyTo($address, $name); - - return $this; - } - - /** - * @param string|array $address - * @param string|null $name - * @return $this - */ - public function addReplyTo($address, $name = null) - { - $address = $this->sanitiseAddress($address); - $this->getSwiftMessage()->addReplyTo($address, $name); - - return $this; - } - - /** - * @return string - */ - public function getSubject() - { - return $this->getSwiftMessage()->getSubject(); - } - - /** - * @param string $subject The Subject line for the email - * @return $this - */ - public function setSubject($subject) - { - $this->getSwiftMessage()->setSubject($subject); - - return $this; - } - - /** - * @return int - */ - public function getPriority() - { - return $this->getSwiftMessage()->getPriority(); - } - - /** - * @param int $priority - * @return $this - */ - public function setPriority($priority) - { - $this->getSwiftMessage()->setPriority($priority); - - return $this; + return $this->priority($priority); } /** * @param string $path Path to file * @param string $alias An override for the name of the file * @param string $mime The mime type for the attachment - * @return $this */ - public function addAttachment($path, $alias = null, $mime = null) + public function addAttachment(string $path, ?string $alias = null, ?string $mime = null): static { - $attachment = \Swift_Attachment::fromPath($path); - if ($alias) { - $attachment->setFilename($alias); - } - if ($mime) { - $attachment->setContentType($mime); - } - $this->getSwiftMessage()->attach($attachment); + return $this->attachFromPath($path, $alias, $mime); + } - return $this; + public function addAttachmentFromData(string $data, string $name, string $mime = null): static + { + return $this->attach($data, $name, $mime); } /** - * @param string $data - * @param string $name - * @param string $mime - * @return $this + * Get data which is exposed to the template + * + * The following data is exposed via this method by default: + * IsEmail: used to detect if rendering an email template rather than a page template + * BaseUrl: used to get the base URL for the email */ - public function addAttachmentFromData($data, $name, $mime = null) + public function getData(): ViewableData { - $attachment = new \Swift_Attachment($data, $name); - if ($mime) { - $attachment->setContentType($mime); + $extraData = [ + 'IsEmail' => true, + 'BaseURL' => Director::absoluteBaseURL(), + ]; + $data = clone $this->data; + foreach ($extraData as $key => $value) { + if (is_null($data->{$key})) { + $data->{$key} = $value; + } } - $this->getSwiftMessage()->attach($attachment); - - return $this; + $this->extend('updateGetData', $data); + return $data; } /** - * @return array|ViewableData The template data + * Set template data + * + * Calling setData() once means that any content set via text()/html()/setBody() will have no effect */ - public function getData() - { - return $this->data; - } - - /** - * @param array|ViewableData $data The template data to set - * @return $this - */ - public function setData($data) + public function setData(array|ViewableData $data) { + if (is_array($data)) { + $data = ArrayData::create($data); + } $this->data = $data; - $this->invalidateBody(); - + $this->dataHasBeenSet = true; return $this; } /** - * @param string|array $name The data name to add or array to names => value - * @param string|null $value The value of the data to add - * @return $this + * Add data to be used in the template + * + * Calling addData() once means that any content set via text()/html()/setBody() will have no effect + * + * @param string|array $nameOrData can be either the name to add, or an array of [name => value] */ - public function addData($name, $value = null) + public function addData(string|array $nameOrData, mixed $value = null): static { - if (is_array($name)) { - $this->data = array_merge($this->data, $name); - } elseif (is_array($this->data)) { - $this->data[$name] = $value; + if (is_array($nameOrData)) { + foreach ($nameOrData as $key => $val) { + $this->data->{$key} = $val; + } } else { - $this->data->$name = $value; + $this->data->{$nameOrData} = $value; } - - $this->invalidateBody(); - + $this->dataHasBeenSet = true; return $this; } /** - * Remove a datum from the message - * - * @param string $name - * @return $this + * Remove a single piece of template data */ - public function removeData($name) + public function removeData(string $name) { - if (is_array($this->data)) { - unset($this->data[$name]); - } else { - $this->data->$name = null; - } - - $this->invalidateBody(); - + $this->data->{$name} = null; return $this; } - /** - * @return string - */ - public function getBody() - { - return $this->getSwiftMessage()->getBody(); - } - - /** - * @param string $body The email body - * @return $this - */ - public function setBody($body) - { - $plainPart = $this->findPlainPart(); - if ($plainPart) { - $this->getSwiftMessage()->detach($plainPart); - } - unset($plainPart); - - $body = HTTP::absoluteURLs($body); - $this->getSwiftMessage()->setBody($body); - - return $this; - } - - /** - * @deprecated 4.12.0 Will be replaced with html() - * - * @return $this - */ - public function invalidateBody() - { - Deprecation::notice('4.12.0', 'Will be replaced with html()'); - $this->setBody(null); - - return $this; - } - - /** - * @deprecated 4.12.0 Will be replaced with getData() - * - * @return string The base URL for the email - */ - public function BaseURL() - { - Deprecation::notice('4.12.0', 'Will be replaced with getData()'); - return Director::absoluteBaseURL(); - } - - /** - * @deprecated Will be removed without equivalent functionality to replace it - * - * Debugging help - * - * @return string Debug info - */ - public function debug() - { - Deprecation::notice('4.12.0', 'Will be removed without equivalent functionality to replace it'); - $this->render(); - - $class = static::class; - return "

Email template {$class}:

\n" . '
' . $this->getSwiftMessage()->toString() . '
'; - } - - /** - * @return string - */ - public function getHTMLTemplate() + public function getHTMLTemplate(): string { if ($this->HTMLTemplate) { return $this->HTMLTemplate; @@ -769,237 +408,123 @@ class Email extends ViewableData /** * Set the template to render the email with - * - * @param string $template - * @return $this */ - public function setHTMLTemplate($template) + public function setHTMLTemplate(string $template): static { if (substr($template ?? '', -3) == '.ss') { $template = substr($template ?? '', 0, -3); } $this->HTMLTemplate = $template; - return $this; } /** * Get the template to render the plain part with - * - * @return string */ - public function getPlainTemplate() + public function getPlainTemplate(): string { return $this->plainTemplate; } /** * Set the template to render the plain part with - * - * @param string $template - * @return $this */ - public function setPlainTemplate($template) + public function setPlainTemplate(string $template): static { if (substr($template ?? '', -3) == '.ss') { $template = substr($template ?? '', 0, -3); } $this->plainTemplate = $template; - return $this; } - /** - * @deprecated 4.12.0 Will be removed without equivalent functionality to replace it - * - * @param array $recipients - * @return $this - */ - public function setFailedRecipients($recipients) - { - Deprecation::notice('4.12.0', 'Will be removed without equivalent functionality to replace it'); - $this->failedRecipients = $recipients; - - return $this; - } - - /** - * @deprecated 4.12.0 Will be removed without equivalent functionality to replace it - * - * @return array - */ - public function getFailedRecipients() - { - Deprecation::notice('4.12.0', 'Will be removed without equivalent functionality to replace it'); - return $this->failedRecipients; - } - - /** - * @deprecated 4.12.0 Will be replaced with getData() - * - * Used by {@link SSViewer} templates to detect if we're rendering an email template rather than a page template - * - * @return bool - */ - public function IsEmail() - { - Deprecation::notice('4.12.0', 'Will be replaced with getData()'); - return true; - } - /** * Send the message to the recipients - * - * @return bool true if successful or array of failed recipients */ - public function send() + public function send(): void { - if (!$this->getBody()) { - $this->render(); - } - if (!$this->hasPlainPart()) { - $this->generatePlainPartFromBody(); - } - return Injector::inst()->get(Mailer::class)->send($this); + $this->updateHtmlAndTextWithRenderedTemplates(); + Injector::inst()->get(MailerInterface::class)->send($this); } /** - * @return array|bool + * Send the message to the recipients as plain-only */ - public function sendPlain() + public function sendPlain(): void { - if (!$this->hasPlainPart()) { - $this->render(true); - } - return Injector::inst()->get(Mailer::class)->send($this); + $html = $this->getHtmlBody(); + $this->updateHtmlAndTextWithRenderedTemplates(true); + $this->html(null); + Injector::inst()->get(MailerInterface::class)->send($this); + $this->html($html); } /** - * @deprecated 4.12.0 Will be removed without equivalent functionality to replace it + * Call html() and/or text() after rendering email templates + * If either body html or text were previously explicitly set, those values will not be overwritten * - * Render the email - * @param bool $plainOnly Only render the message as plain text - * @return $this + * @param bool $plainOnly - if true then do not call html() */ - public function render($plainOnly = false) + private function updateHtmlAndTextWithRenderedTemplates(bool $plainOnly = false): void { - Deprecation::notice('4.12.0', 'Will be removed without equivalent functionality to replace it'); - - if ($existingPlainPart = $this->findPlainPart()) { - $this->getSwiftMessage()->detach($existingPlainPart); - } - unset($existingPlainPart); - - // Respect explicitly set body - $htmlPart = $plainOnly ? null : $this->getBody(); - $plainPart = $plainOnly ? $this->getBody() : null; + $htmlBody = $this->getHtmlBody(); + $plainBody = $this->getTextBody(); // Ensure we can at least render something $htmlTemplate = $this->getHTMLTemplate(); $plainTemplate = $this->getPlainTemplate(); - if (!$htmlTemplate && !$plainTemplate && !$plainPart && !$htmlPart) { - return $this; + if (!$htmlTemplate && !$plainTemplate && !$plainBody && !$htmlBody) { + return; + } + + $htmlRender = null; + $plainRender = null; + + if ($htmlBody && !$this->dataHasBeenSet) { + $htmlRender = $htmlBody; + } + + if ($plainBody && !$this->dataHasBeenSet) { + $plainRender = $plainBody; } // Do not interfere with emails styles Requirements::clear(); - // Render plain part - if ($plainTemplate && !$plainPart) { - $plainPart = $this->renderWith($plainTemplate, $this->getData())->Plain(); + // Render plain + if (!$plainRender && $plainTemplate) { + $plainRender = $this->getData()->renderWith($plainTemplate)->Plain(); } - // Render HTML part, either if sending html email, or a plain part is lacking - if (!$htmlPart && $htmlTemplate && (!$plainOnly || empty($plainPart))) { - $htmlPart = $this->renderWith($htmlTemplate, $this->getData()); - } - - // Plain part fails over to generated from html - if (!$plainPart && $htmlPart) { - /** @var DBHTMLText $htmlPartObject */ - $htmlPartObject = DBField::create_field('HTMLFragment', $htmlPart); - $plainPart = $htmlPartObject->Plain(); + // Render HTML + if (!$htmlRender && $htmlTemplate) { + $htmlRender = $this->getData()->renderWith($htmlTemplate)->RAW(); } // Rendering is finished Requirements::restore(); - // Fail if no email to send - if (!$plainPart && !$htmlPart) { - return $this; + // Plain render fallbacks to using the html render with html tags removed + if (!$plainRender && $htmlRender) { + // call html_entity_decode() to ensure any encoded HTML is also stripped inside ->Plain() + $dbField = DBField::create_field('HTMLFragment', html_entity_decode($htmlRender)); + $plainRender = $dbField->Plain(); } - // Build HTML / Plain components - if ($htmlPart && !$plainOnly) { - $this->setBody($htmlPart); - $this->getSwiftMessage()->setContentType('text/html'); - $this->getSwiftMessage()->setCharset('utf-8'); - if ($plainPart) { - $this->getSwiftMessage()->addPart($plainPart, 'text/plain', 'utf-8'); - } - } else { - if ($plainPart) { - $this->setBody($plainPart); - } - $this->getSwiftMessage()->setContentType('text/plain'); - $this->getSwiftMessage()->setCharset('utf-8'); + // Handle edge case where no template was found + if (!$htmlRender && $htmlBody) { + $htmlRender = $htmlBody; } - return $this; - } - - /** - * @deprecated 4.12.0 Will be removed without equivalent functionality to replace it - * - * @return Swift_MimePart|false - */ - public function findPlainPart() - { - Deprecation::notice('4.12.0', 'Will be removed without equivalent functionality to replace it'); - foreach ($this->getSwiftMessage()->getChildren() as $child) { - if ($child instanceof Swift_MimePart && $child->getContentType() == 'text/plain') { - return $child; - } + if (!$plainRender && $plainBody) { + $plainRender = $plainBody; } - return false; - } - /** - * @deprecated 4.12.0 Will be removed without equivalent functionality to replace it - * - * @return bool - */ - public function hasPlainPart() - { - Deprecation::notice('4.12.0', 'Will be removed without equivalent functionality to replace it'); - if ($this->getSwiftMessage()->getContentType() === 'text/plain') { - return true; + if ($plainRender) { + $this->text($plainRender); } - return (bool) $this->findPlainPart(); - } - - /** - * @deprecated 4.12.0 Will be removed without equivalent functionality to replace it - * - * Automatically adds a plain part to the email generated from the current Body - * - * @return $this - */ - public function generatePlainPartFromBody() - { - Deprecation::notice('4.12.0', 'Will be removed without equivalent functionality to replace it'); - $plainPart = $this->findPlainPart(); - if ($plainPart) { - $this->getSwiftMessage()->detach($plainPart); + if ($htmlRender && !$plainOnly) { + $this->html($htmlRender); } - unset($plainPart); - - $this->getSwiftMessage()->addPart( - Convert::xml2raw($this->getBody()), - 'text/plain', - 'utf-8' - ); - - return $this; } } diff --git a/src/Control/Email/MailerSubscriber.php b/src/Control/Email/MailerSubscriber.php new file mode 100644 index 000000000..5e5501b4c --- /dev/null +++ b/src/Control/Email/MailerSubscriber.php @@ -0,0 +1,100 @@ +bootEmail() + * + * See https://symfony.com/doc/current/mailer.html#mailer-events for further info + */ +class MailerSubscriber implements EventSubscriberInterface +{ + use Injectable; + use Extensible; + + public static function getSubscribedEvents() + { + return [ + MessageEvent::class => 'onMessage', + ]; + } + + public function onMessage(MessageEvent $event): void + { + $email = $event->getMessage(); + if (!($email instanceof Email)) { + throw new InvalidArgumentException('Message is not a ' . Email::class); + } + $this->applyConfig($email); + $this->updateUrls($email); + $this->extend('updateOnMessage', $email, $event); + } + + private function applyConfig(Email $email): void + { + $sendAllTo = Email::getSendAllEmailsTo(); + if (!empty($sendAllTo)) { + $this->setTo($email, $sendAllTo); + } + + $ccAllTo = Email::getCCAllEmailsTo(); + if (!empty($ccAllTo)) { + $email->addCc(...$ccAllTo); + } + + $bccAllTo = Email::getBCCAllEmailsTo(); + if (!empty($bccAllTo)) { + $email->addBcc(...$bccAllTo); + } + + $sendAllFrom = Email::getSendAllEmailsFrom(); + if (!empty($sendAllFrom)) { + $this->setFrom($email, $sendAllFrom); + } + } + + private function setTo(Email $email, array $sendAllTo): void + { + $headers = $email->getHeaders(); + // store the old data as X-Original-* Headers for debugging + if (!empty($email->getTo())) { + $headers->addMailboxListHeader('X-Original-To', $email->getTo()); + } + if (!empty($email->getCc())) { + $headers->addMailboxListHeader('X-Original-Cc', $email->getCc()); + } + if (!empty($email->getBcc())) { + $headers->addMailboxListHeader('X-Original-Bcc', $email->getBcc()); + } + // set default recipient and remove all other recipients + $email->to(...$sendAllTo); + $email->cc(...[]); + $email->bcc(...[]); + } + + private function setFrom(Email $email, array $sendAllFrom): void + { + $headers = $email->getHeaders(); + if (!empty($email->getFrom())) { + $headers->addMailboxListHeader('X-Original-From', $email->getFrom()); + } + $email->from(...$sendAllFrom); + } + + private function updateUrls(Email $email): void + { + if ($email->getHtmlBody()) { + $email->html(HTTP::absoluteURLs($email->getHtmlBody())); + } + if ($email->getTextBody()) { + $email->text(HTTP::absoluteURLs($email->getTextBody())); + } + } +} diff --git a/src/Control/Email/SwiftMailer.php b/src/Control/Email/SwiftMailer.php deleted file mode 100644 index 5dde0809f..000000000 --- a/src/Control/Email/SwiftMailer.php +++ /dev/null @@ -1,82 +0,0 @@ -getSwiftMessage(); - $failedRecipients = []; - $result = $this->sendSwift($swiftMessage, $failedRecipients); - $message->setFailedRecipients($failedRecipients); - - return $result != 0; - } - - /** - * @param Swift_Message $message - * @param array $failedRecipients - * @return int - */ - protected function sendSwift($message, &$failedRecipients = null) - { - return $this->getSwiftMailer()->send($message, $failedRecipients); - } - - /** - * @return Swift_Mailer - */ - public function getSwiftMailer() - { - return $this->swift; - } - - /** - * @param Swift_Mailer $swift - * @return $this - */ - public function setSwiftMailer($swift) - { - // register any required plugins - foreach ($this->config()->get('swift_plugins') as $plugin) { - $swift->registerPlugin(Injector::inst()->create($plugin)); - } - $this->swift = $swift; - - return $this; - } -} diff --git a/src/Control/Email/SwiftPlugin.php b/src/Control/Email/SwiftPlugin.php deleted file mode 100644 index ea9e0aa81..000000000 --- a/src/Control/Email/SwiftPlugin.php +++ /dev/null @@ -1,83 +0,0 @@ -getMessage(); - - $sendAllTo = Email::getSendAllEmailsTo(); - if (!empty($sendAllTo)) { - $this->setTo($message, $sendAllTo); - } - - $ccAllTo = Email::getCCAllEmailsTo(); - if (!empty($ccAllTo)) { - foreach ($ccAllTo as $address => $name) { - $message->addCc($address, $name); - } - } - - $bccAllTo = Email::getBCCAllEmailsTo(); - if (!empty($bccAllTo)) { - foreach ($bccAllTo as $address => $name) { - $message->addBcc($address, $name); - } - } - - $sendAllFrom = Email::getSendAllEmailsFrom(); - if (!empty($sendAllFrom)) { - $this->setFrom($message, $sendAllFrom); - } - } - - /** - * @param \Swift_Message $message - * @param array|string $to - */ - protected function setTo($message, $to) - { - $headers = $message->getHeaders(); - $origTo = $message->getTo(); - $cc = $message->getCc(); - $bcc = $message->getBcc(); - - // set default recipient and remove all other recipients - $message->setTo($to); - $headers->removeAll('Cc'); - $headers->removeAll('Bcc'); - - // store the old data as X-Original-* Headers for debugging - $headers->addMailboxHeader('X-Original-To', $origTo); - $headers->addMailboxHeader('X-Original-Cc', $cc); - $headers->addMailboxHeader('X-Original-Bcc', $bcc); - } - - /** - * @param \Swift_Message $message - * @param array|string $from - */ - protected function setFrom($message, $from) - { - $headers = $message->getHeaders(); - $origFrom = $message->getFrom(); - $headers->addMailboxHeader('X-Original-From', $origFrom); - $message->setFrom($from); - } - - public function sendPerformed(\Swift_Events_SendEvent $evt) - { - // noop - } -} diff --git a/src/Dev/SapphireTest.php b/src/Dev/SapphireTest.php index 965111b12..8b2471146 100644 --- a/src/Dev/SapphireTest.php +++ b/src/Dev/SapphireTest.php @@ -15,7 +15,6 @@ use SilverStripe\Control\Controller; use SilverStripe\Control\Cookie; use SilverStripe\Control\Director; use SilverStripe\Control\Email\Email; -use SilverStripe\Control\Email\Mailer; use SilverStripe\Control\HTTPApplication; use SilverStripe\Control\HTTPRequest; use SilverStripe\Core\Config\Config; @@ -39,6 +38,10 @@ use SilverStripe\Security\Member; use SilverStripe\Security\Permission; use SilverStripe\Security\Security; use SilverStripe\View\SSViewer; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Mailer\MailerInterface; +use Symfony\Component\Mailer\Transport\NullTransport; +use Symfony\Component\Messenger\MessageBusInterface; /** * Test case class for the Silverstripe framework. @@ -317,20 +320,17 @@ abstract class SapphireTest extends TestCase implements TestOnly SSViewer::config()->update('source_file_comments', false); } - // Set up the test mailer - if (class_exists(TestMailer::class)) { - Injector::inst()->registerService(new TestMailer(), Mailer::class); - } - - if (class_exists(Email::class)) { - Email::config()->remove('send_all_emails_to'); - Email::config()->remove('send_all_emails_from'); - Email::config()->remove('cc_all_emails_to'); - Email::config()->remove('bcc_all_emails_to'); - } + // Set up the test mailer and register it as a service + $dispatcher = Injector::inst()->get(EventDispatcherInterface::class . '.mailer'); + $transport = new NullTransport($dispatcher); + $testMailer = new TestMailer($transport, $dispatcher); + Injector::inst()->registerService($testMailer, MailerInterface::class); + Email::config()->remove('send_all_emails_to'); + Email::config()->remove('send_all_emails_from'); + Email::config()->remove('cc_all_emails_to'); + Email::config()->remove('bcc_all_emails_to'); } - /** * Helper method to determine if the current test should enable a test database * @@ -611,8 +611,8 @@ abstract class SapphireTest extends TestCase implements TestOnly */ public function clearEmails() { - /** @var Mailer $mailer */ - $mailer = Injector::inst()->get(Mailer::class); + /** @var MailerInterface $mailer */ + $mailer = Injector::inst()->get(MailerInterface::class); if ($mailer instanceof TestMailer) { $mailer->clearEmails(); return true; @@ -632,8 +632,8 @@ abstract class SapphireTest extends TestCase implements TestOnly */ public static function findEmail($to, $from = null, $subject = null, $content = null) { - /** @var Mailer $mailer */ - $mailer = Injector::inst()->get(Mailer::class); + /** @var MailerInterface $mailer */ + $mailer = Injector::inst()->get(MailerInterface::class); if ($mailer instanceof TestMailer) { return $mailer->findEmail($to, $from, $subject, $content); } diff --git a/src/Dev/TestMailer.php b/src/Dev/TestMailer.php index 44627e62c..99e1b204f 100644 --- a/src/Dev/TestMailer.php +++ b/src/Dev/TestMailer.php @@ -2,98 +2,72 @@ namespace SilverStripe\Dev; -use SilverStripe\Control\Email\Mailer; -use Swift_Attachment; +use Exception; +use InvalidArgumentException; +use SilverStripe\Control\Email\Email; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Event\MessageEvent; +use Symfony\Component\Mailer\MailerInterface; +use Symfony\Component\Mailer\Messenger\SendEmailMessage; +use Symfony\Component\Mime\RawMessage; +use Symfony\Component\Mailer\Transport\TransportInterface; +use Symfony\Component\Messenger\Event\SendMessageToTransportsEvent; +use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Messenger\Envelope as MessagerEnvelope; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Part\DataPart; -class TestMailer implements Mailer +class TestMailer implements MailerInterface { - /** - * @var array - */ - protected $emailsSent = []; + private array $emailsSent = []; - public function send($email) + private TransportInterface $transport; + private EventDispatcherInterface $dispatcher; + + public function __construct( + TransportInterface $transport, + EventDispatcherInterface $dispatcher + ) { + $this->transport = $transport; + $this->dispatcher = $dispatcher; + } + + public function send(RawMessage $message, Envelope $envelope = null): void { - // Detect body type - $htmlContent = null; - $plainContent = null; - if ($email->getSwiftMessage()->getContentType() === 'text/plain') { - $type = 'plain'; - $plainContent = $email->getBody(); - } else { - $type = 'html'; - $htmlContent = $email->getBody(); - $plainPart = $email->findPlainPart(); - if ($plainPart) { - $plainContent = $plainPart->getBody(); - } + if (!is_a($message, Email::class)) { + throw new InvalidArgumentException('$message must be a ' . Email::class); } - - // Get attachments - $attachedFiles = []; - foreach ($email->getSwiftMessage()->getChildren() as $child) { - if ($child instanceof Swift_Attachment) { - $attachedFiles[] = [ - 'contents' => $child->getBody(), - 'filename' => $child->getFilename(), - 'mimetype' => $child->getContentType(), - ]; - } - } - - // Serialise email - $serialised = [ - 'Type' => $type, - 'To' => implode(';', array_keys($email->getTo() ?: [])), - 'From' => implode(';', array_keys($email->getFrom() ?: [])), + /** @var Email $email */ + $email = $message; + $this->dispatchEvent($email, $envelope); + $this->emailsSent[] = [ + 'Type' => $email->getHtmlBody() ? 'html' : 'plain', + 'To' => $this->convertAddressesToString($email->getTo()), + 'From' => $this->convertAddressesToString($email->getFrom()), 'Subject' => $email->getSubject(), - 'Content' => $email->getBody(), - 'AttachedFiles' => $attachedFiles, - 'Headers' => $email->getSwiftMessage()->getHeaders(), + 'Content' => $email->getHtmlBody() ?: $email->getTextBody(), + 'Headers' => $email->getHeaders(), + 'PlainContent' => $email->getTextBody(), + 'HtmlContent' => $email->getHtmlBody(), + 'AttachedFiles' => array_map(fn(DataPart $attachment) => [ + 'contents' => $attachment->getBody(), + 'filename' => $attachment->getFilename(), + 'mimetype' => $attachment->getContentType() + ], $email->getAttachments()), ]; - if ($plainContent) { - $serialised['PlainContent'] = $plainContent; - } - if ($htmlContent) { - $serialised['HtmlContent'] = $htmlContent; - } - - $this->saveEmail($serialised); - - return true; - } - - /** - * Save a single email to the log - * - * @param array $data A map of information about the email - */ - protected function saveEmail($data) - { - $this->emailsSent[] = $data; - } - - /** - * Clear the log of emails sent - */ - public function clearEmails() - { - $this->emailsSent = []; } /** * Search for an email that was sent. * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression. - * - * @param string $to - * @param string $from - * @param string $subject - * @param string $content - * @return array|null Contains keys: 'Type', 'To', 'From', 'Subject', 'Content', 'PlainContent', 'AttachedFiles', - * 'HtmlContent' */ - public function findEmail($to, $from = null, $subject = null, $content = null) - { + public function findEmail( + string $to, + ?string $from = null, + ?string $subject = null, + ?string $content = null + ): ?array { $compare = [ 'To' => $to, 'From' => $from, @@ -131,9 +105,28 @@ class TestMailer implements Mailer } /** - * @param string $value + * Clear the log of emails sent */ - private function normaliseSpaces(string $value) + public function clearEmails(): void + { + $this->emailsSent = []; + } + + private function convertAddressesToString(array $addresses): string + { + return implode(',', array_map(fn(Address $address) => $address->getAddress(), $addresses)); + } + + private function dispatchEvent(Email $email, Envelope $envelope = null): void + { + $sender = $email->getSender()[0] ?? $email->getFrom()[0] ?? new Address('test.sender@example.com'); + $recipients = empty($email->getTo()) ? [new Address('test.recipient@example.com')] : $email->getTo(); + $envelope ??= new Envelope($sender, $recipients); + $event = new MessageEvent($email, $envelope, $this->transport); + $this->dispatcher->dispatch($event); + } + + private function normaliseSpaces(string $value): string { return str_replace([', ', '; '], [',', ';'], $value ?? ''); } diff --git a/src/Security/Member.php b/src/Security/Member.php index cc68bfec6..83c207e6c 100644 --- a/src/Security/Member.php +++ b/src/Security/Member.php @@ -9,7 +9,6 @@ use SilverStripe\CMS\Controllers\CMSMain; use SilverStripe\Control\Controller; use SilverStripe\Control\Director; use SilverStripe\Control\Email\Email; -use SilverStripe\Control\Email\Mailer; use SilverStripe\Control\HTTPRequest; use SilverStripe\Core\Config\Config; use SilverStripe\Core\Convert; @@ -36,6 +35,7 @@ use SilverStripe\ORM\SS_List; use SilverStripe\ORM\UnsavedRelationList; use SilverStripe\ORM\ValidationException; use SilverStripe\ORM\ValidationResult; +use Symfony\Component\Mailer\MailerInterface; /** * The member class which represents the users of the system @@ -907,7 +907,7 @@ class Member extends DataObject // We don't send emails out on dev/tests sites to prevent accidentally spamming users. // However, if TestMailer is in use this isn't a risk. // @todo some developers use external tools, so emailing might be a good idea anyway - if ((Director::isLive() || Injector::inst()->get(Mailer::class) instanceof TestMailer) + if ((Director::isLive() || Injector::inst()->get(MailerInterface::class) instanceof TestMailer) && $this->isChanged('Password') && $this->record['Password'] && $this->Email diff --git a/tests/php/Control/Email/EmailTest.php b/tests/php/Control/Email/EmailTest.php index daa69ae41..00bb462b5 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 + { + parent::tearDown(); + SSViewer::set_themes($this->origThemes); + } + + public function testAddAttachment(): void { $email = new Email(); - $email->addAttachment(__DIR__ . '/EmailTest/attachment.txt', null, 'text/plain'); - - $children = $email->getSwiftMessage()->getChildren(); - $this->assertCount(1, $children); - - /** @var Swift_Attachment $child */ - $child = reset($children); - - $this->assertInstanceOf(Swift_Attachment::class, $child); - $this->assertEquals('text/plain', $child->getContentType()); - $this->assertEquals('attachment.txt', $child->getFilename()); + $attachments = $email->getAttachments(); + $this->assertCount(1, $attachments); + $attachment = $this->getFirstAttachment($attachments); + $this->assertSame('text/plain', $attachment->getContentType()); + $this->assertSame('attachment.txt', $attachment->getFilename()); } - public function testAddAttachmentFromData() + public function testAddAttachmentFromData(): void { $email = new Email(); - $email->addAttachmentFromData('foo bar', 'foo.txt', 'text/plain'); - $children = $email->getSwiftMessage()->getChildren(); + $attachments = $email->getAttachments(); + $this->assertCount(1, $attachments); + $attachment = $this->getFirstAttachment($attachments); + $this->assertSame('text/plain', $attachment->getContentType()); + $this->assertSame('foo.txt', $attachment->getFilename()); + $this->assertSame('foo bar', $attachment->getBody()); + } - $this->assertCount(1, $children); - - /** @var Swift_Attachment $child */ - $child = reset($children); - - $this->assertInstanceOf(Swift_Attachment::class, $child); - $this->assertEquals('foo bar', $child->getBody()); - $this->assertEquals('text/plain', $child->getContentType()); - $this->assertEquals('foo.txt', $child->getFilename()); + private function getFirstAttachment(array $attachments): DataPart + { + return $attachments[0]; } /** * @dataProvider provideValidEmailAddresses */ - public function testValidEmailAddress($email) + public function testValidEmailAddress($email): void { $this->assertTrue(Email::is_valid_address($email)); } @@ -76,26 +77,26 @@ class EmailTest extends SapphireTest /** * @dataProvider provideInvalidEmailAddresses */ - public function testInvalidEmailAddress($email) + public function testInvalidEmailAddress($email): void { $this->assertFalse(Email::is_valid_address($email)); } - public function provideValidEmailAddresses() + public function provideValidEmailAddresses(): array { return [ ['test@example.com', 'test-123@sub.example.com'], ]; } - public function provideInvalidEmailAddresses() + public function provideInvalidEmailAddresses(): array { return [ ['foo.bar@', '@example.com', 'foo@'], ]; } - public function testObfuscate() + public function testObfuscate(): void { $emailAddress = 'test-1@example.com'; @@ -111,392 +112,272 @@ class EmailTest extends SapphireTest ); } - public function testSendPlain() + private function getTemplateClass(string $templateName): string { - $email = $this->makeEmailMock('Test send plain'); - - // email should not call render if a body is supplied - $email->expects($this->never())->method('renderWith'); - $successful = $email->sendPlain(); - - $this->assertTrue($successful); - $this->assertEmpty($email->getFailedRecipients()); - - /** @var TestMailer $mailer */ - $mailer = Injector::inst()->get(Mailer::class); - $sentMail = $mailer->findEmail('to@example.com'); - - $this->assertTrue(is_array($sentMail)); - - $this->assertEquals('to@example.com', $sentMail['To']); - $this->assertEquals('from@example.com', $sentMail['From']); - $this->assertEquals('Test send plain', $sentMail['Subject']); - $this->assertEquals('Body for Test send plain', $sentMail['Content']); - - $this->assertCount(1, $sentMail['AttachedFiles']); - $child = reset($sentMail['AttachedFiles']); - $this->assertEquals('text/plain', $child['mimetype']); - $this->assertEquals('attachment.txt', $child['filename']); - $this->assertEquals('Hello, I\'m a text document.', $child['contents']); + return implode('\\', ['SilverStripe', 'Control', 'Tests', 'Email', 'EmailTest', $templateName]); } - public function testSend() + private function getMailer(): TestMailer { - /** @var Email|MockObject $email */ - $email = $this->makeEmailMock('Test send HTML'); - - // email should not call render if a body is supplied - $email->expects($this->never())->method('renderWith'); - $successful = $email->send(); - - $this->assertTrue($successful); - $this->assertEmpty($email->getFailedRecipients()); - - /** @var TestMailer $mailer */ - $mailer = Injector::inst()->get(Mailer::class); - $sentMail = $mailer->findEmail('to@example.com'); - - $this->assertTrue(is_array($sentMail)); - - $this->assertEquals('to@example.com', $sentMail['To']); - $this->assertEquals('from@example.com', $sentMail['From']); - $this->assertEquals('Test send HTML', $sentMail['Subject']); - $this->assertEquals('Body for Test send HTML', $sentMail['Content']); - - $this->assertCount(1, $sentMail['AttachedFiles']); - $child = reset($sentMail['AttachedFiles']); - $this->assertEquals('text/plain', $child['mimetype']); - $this->assertEquals('attachment.txt', $child['filename']); - $this->assertEquals('Hello, I\'m a text document.', $child['contents']); + return Injector::inst()->get(MailerInterface::class); } - public function testRenderedSend() + private function createTestEmail(string $subject = 'My subject', $setPlain = true): Email { - /** @var Email|MockObject $email */ - $email = $this->getMockBuilder(Email::class) - ->enableProxyingToOriginalMethods() - ->getMock(); - $email->setFrom('from@example.com'); - $email->setTo('to@example.com'); - $email->setData([ - 'EmailContent' => 'test', - ]); - $this->assertFalse($email->hasPlainPart()); - $this->assertEmpty($email->getBody()); - // these seem to fail for some reason :/ - //$email->expects($this->once())->method('render'); - //$email->expects($this->once())->method('generatePlainPartFromBody'); - $email->send(); - $this->assertTrue($email->hasPlainPart()); - $this->assertNotEmpty($email->getBody()); - } - - public function testRenderedSendSubclass() - { - // Include dev theme - SSViewer::set_themes([ - 'silverstripe/framework:/tests/php/Control/Email/EmailTest', - '$default', - ]); - - /** @var Email|MockObject $email */ - $email = $this->getMockBuilder(EmailSubClass::class) - ->enableProxyingToOriginalMethods() - ->getMock(); - $email->setFrom('from@example.com'); - $email->setTo('to@example.com'); - $email->setData([ - 'EmailContent' => 'test', - ]); - $this->assertFalse($email->hasPlainPart()); - $this->assertEmpty($email->getBody()); - $email->send(); - $this->assertTrue($email->hasPlainPart()); - $this->assertNotEmpty($email->getBody()); - $this->assertStringContainsString('

Email Sub-class

', $email->getBody()); - } - - public function testConsturctor() - { - $email = new Email( - 'from@example.com', - 'to@example.com', - 'subject', - 'body', - 'cc@example.com', - 'bcc@example.com', - 'bounce@example.com' - ); - - $this->assertCount(1, $email->getFrom()); - $this->assertContains('from@example.com', array_keys($email->getFrom() ?? [])); - $this->assertCount(1, $email->getTo()); - $this->assertContains('to@example.com', array_keys($email->getTo() ?? [])); - $this->assertEquals('subject', $email->getSubject()); - $this->assertEquals('body', $email->getBody()); - $this->assertCount(1, $email->getCC()); - $this->assertContains('cc@example.com', array_keys($email->getCC() ?? [])); - $this->assertCount(1, $email->getBCC()); - $this->assertContains('bcc@example.com', array_keys($email->getBCC() ?? [])); - $this->assertEquals('bounce@example.com', $email->getReturnPath()); - } - - public function testGetSwiftMessage() - { - $email = new Email( - 'from@example.com', - 'to@example.com', - 'subject', - 'body', - 'cc@example.com', - 'bcc@example.com', - 'bounce@example.com' - ); - $swiftMessage = $email->getSwiftMessage(); - - $this->assertInstanceOf(Swift_Message::class, $swiftMessage); - - $this->assertCount(1, $swiftMessage->getFrom()); - $this->assertContains('from@example.com', array_keys($swiftMessage->getFrom() ?? [])); - $this->assertCount(1, $swiftMessage->getTo()); - $this->assertContains('to@example.com', array_keys($swiftMessage->getTo() ?? [])); - $this->assertEquals('subject', $swiftMessage->getSubject()); - $this->assertEquals('body', $swiftMessage->getBody()); - $this->assertCount(1, $swiftMessage->getCC()); - $this->assertContains('cc@example.com', array_keys($swiftMessage->getCc() ?? [])); - $this->assertCount(1, $swiftMessage->getBCC()); - $this->assertContains('bcc@example.com', array_keys($swiftMessage->getBcc() ?? [])); - $this->assertEquals('bounce@example.com', $swiftMessage->getReturnPath()); - } - - public function testSetSwiftMessage() - { - Email::config()->update('admin_email', 'admin@example.com'); - DBDatetime::set_mock_now('2017-01-01 07:00:00'); $email = new Email(); - $swiftMessage = new Swift_Message(); - $email->setSwiftMessage($swiftMessage); - $dateTime = new DateTime(); - $dateTime->setTimestamp(DBDatetime::now()->getTimestamp()); - $email->getSwiftMessage()->setDate($dateTime); - $this->assertCount(1, $email->getFrom()); - $this->assertContains('admin@example.com', array_keys($swiftMessage->getFrom() ?? [])); - $this->assertEquals(strtotime('2017-01-01 07:00:00'), $swiftMessage->getDate()->getTimestamp()); - $this->assertEquals($swiftMessage, $email->getSwiftMessage()); + $email->setFrom('from@example.com'); + $email->setTo('to@example.com'); + $email->setSubject($subject); + if ($setPlain) { + $email->text("Plain body for $subject"); + } + $email->html("

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; + } - // check from field is retained - $swiftMessage = new Swift_Message(); - $swiftMessage->setFrom('from@example.com'); - $email->setSwiftMessage($swiftMessage); + public function testSendPlain(): void + { + $email = $this->createTestEmail('Test send plain'); + $email->sendPlain(); + $this->assertStringNotContainsString($email->getTextBody(), 'My Plain Template'); + $sentMail = $this->getMailer()->findEmail('to@example.com'); + + $this->assertSame('to@example.com', $sentMail['To']); + $this->assertSame('from@example.com', $sentMail['From']); + $this->assertSame('Test send plain', $sentMail['Subject']); + $this->assertStringContainsString('Plain body for Test send plain', $sentMail['Content']); + + $this->assertCount(1, $sentMail['AttachedFiles']); + $child = reset($sentMail['AttachedFiles']); + $this->assertSame('text/plain', $child['mimetype']); + $this->assertSame('attachment.txt', $child['filename']); + $this->assertSame('Hello, I\'m a text document.', $child['contents']); + + // assert MIME types + // explicitly setting $email->html(null) because sendPlain() will itself set $this->html(null), and then + // revert it to its previous AFTER sending the email. For testing purposes, we need to manuall set it + // to null in order to test the MIME types for what would have been sent in practice + $email->html(null); + $this->assertSame([ + 'text/plain charset: utf-8', + 'text/plain disposition: attachment filename: attachment.txt' + ], array_map(fn(AbstractPart $part) => $part->asDebugString(), $email->getBody()->getParts())); + } + + public function testSendPlainFallback(): void + { + $email = $this->createTestEmail('Test send plain', false); + $email->sendPlain(); + $sentMail = $this->getMailer()->findEmail('to@example.com'); + // assert that it has HTML body with HTML tags removed + $this->assertSame('HTML body for Test send plain', $sentMail['Content']); + } + + public function testSendPlainThenNormalWithSetData(): void + { + $email = $this->createTestEmail('Test send plain', false); + $email->setData([ + 'EmailContent' => 'This is the content of the email', + ]); + $email->sendPlain(); + $email->send(); + $sentMail = $this->getMailer()->findEmail('to@example.com'); + $this->assertSame('This is the content of the email', $sentMail['Content']); + $email->to('to2@example.com'); + $email->send(); + $sentMail = $this->getMailer()->findEmail('to2@example.com'); + $this->assertStringContainsString('This is the content of the email', $sentMail['Content']); + } + + public function testSend(): void + { + $email = $this->createTestEmail('Test send HTML'); + + // email should not call render if a body is supplied + $email->setHTMLTemplate($this->getTemplateClass('HtmlTemplate')); + $email->send(); + $this->assertStringNotContainsString($email->getHtmlBody(), 'My HTML Template'); + + $sentMail = $this->getMailer()->findEmail('to@example.com'); + + $this->assertSame('to@example.com', $sentMail['To']); + $this->assertSame('from@example.com', $sentMail['From']); + $this->assertSame('Test send HTML', $sentMail['Subject']); + $this->assertStringContainsString('

HTML body for Test send HTML

', $sentMail['Content']); + + $this->assertCount(1, $sentMail['AttachedFiles']); + $child = reset($sentMail['AttachedFiles']); + $this->assertSame('text/plain', $child['mimetype']); + $this->assertSame('attachment.txt', $child['filename']); + $this->assertSame('Hello, I\'m a text document.', $child['contents']); + + // assert MIME types + $this->assertSame([ + implode("\n └ ", [ + 'multipart/alternative', + 'text/plain charset: utf-8', + 'text/html charset: utf-8' + ]), + 'text/plain disposition: attachment filename: attachment.txt' + ], array_map(fn(AbstractPart $part) => $part->asDebugString(), $email->getBody()->getParts())); + } + + public function testRenderedSend(): void + { + $email = new Email(to: 'to@example.com'); + $email->setHTMLTemplate($this->getTemplateClass('HtmlTemplate')); + $email->setData([ + 'EmailContent' => '

test

', + ]); + $email->send(); + $sentMail = $this->getMailer()->findEmail('to@example.com'); + $this->assertStringContainsString('My HTML Template', $sentMail['Content']); + } + + public function testRenderedSendSubclass(): void + { + $email = new EmailSubClass(to: 'to@example.com'); + $email->setData([ + 'EmailContent' => 'test', + ]); + $email->send(); + $sentMail = $this->getMailer()->findEmail('to@example.com'); + $this->assertStringContainsString('

Email Sub-class

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

body

', + 'cc@example.com', + 'bcc@example.com', + 'bounce@example.com' + ); $this->assertCount(1, $email->getFrom()); - $this->assertContains('from@example.com', array_keys($email->getFrom() ?? [])); + $this->assertSame('from@example.com', $email->getFrom()[0]->getAddress()); + $this->assertCount(1, $email->getTo()); + $this->assertSame('to@example.com', $email->getTo()[0]->getAddress()); + $this->assertEquals('subject', $email->getSubject()); + $this->assertEquals('

body

', $email->getHtmlBody()); + $this->assertCount(1, $email->getCC()); + $this->assertEquals('cc@example.com', $email->getCC()[0]->getAddress()); + $this->assertCount(1, $email->getBCC()); + $this->assertEquals('bcc@example.com', $email->getBcc()[0]->getAddress()); + $this->assertEquals('bounce@example.com', $email->getReturnPath()->getAddress()); + } + + public function testSetBody(): void + { + $email = new Email(); + $email->setBody('

body

'); + $this->assertSame('

body

', $email->getHtmlBody()); + } + + public function testSetFrom(): void + { + $email = new Email(); + $email->setFrom('from@example.com'); + $this->assertCount(1, $email->getFrom()); + $this->assertSame('from@example.com', $email->getFrom()[0]->getAddress()); + } + + public function testSender(): void + { + $email = new Email(); + $email->setSender('sender@example.com'); + $this->assertSame('sender@example.com', $email->getSender()->getAddress()); + } + + public function testSetTo(): void + { + $email = new Email(); + $email->setTo('to@example.com'); + $this->assertCount(1, $email->getTo()); + $this->assertSame('to@example.com', $email->getTo()[0]->getAddress()); + } + + public function testSetReplyTo(): void + { + $email = new Email(); + $email->setReplyTo('reply-to@example.com'); + $this->assertCount(1, $email->getReplyTo()); + $this->assertSame('reply-to@example.com', $email->getReplyTo()[0]->getAddress()); + } + + public function testSetSubject(): void + { + $email = new Email(); + $email->setSubject('my subject'); + $this->assertSame('my subject', $email->getSubject()); + } + + public function testSetReturnPath(): void + { + $email = new Email(); + $email->setReturnPath('return-path@example.com'); + $this->assertSame('return-path@example.com', $email->getReturnPath()->getAddress()); + } + + public function testSetPriority(): void + { + $email = new Email(); + // Intentionally set above 5 to test that Symfony\Component\Mime\Email->priority() is being called + $email->setPriority(7); + $this->assertSame(5, $email->getPriority()); } public function testAdminEmailApplied() { Email::config()->update('admin_email', 'admin@example.com'); $email = new Email(); - $this->assertCount(1, $email->getFrom()); - $this->assertContains('admin@example.com', array_keys($email->getFrom() ?? [])); + $this->assertSame('admin@example.com', $email->getFrom()[0]->getAddress()); } - public function testGetFrom() - { - $email = new Email('from@example.com'); - $this->assertCount(1, $email->getFrom()); - $this->assertContains('from@example.com', array_keys($email->getFrom() ?? [])); - } - - public function testSetFrom() - { - $email = new Email('from@example.com'); - $this->assertCount(1, $email->getFrom()); - $this->assertContains('from@example.com', array_keys($email->getFrom() ?? [])); - $email->setFrom('new-from@example.com'); - $this->assertCount(1, $email->getFrom()); - $this->assertContains('new-from@example.com', array_keys($email->getFrom() ?? [])); - } - - public function testAddFrom() - { - $email = new Email('from@example.com'); - $this->assertCount(1, $email->getFrom()); - $this->assertContains('from@example.com', array_keys($email->getFrom() ?? [])); - $email->addFrom('new-from@example.com'); - $this->assertCount(2, $email->getFrom()); - $this->assertContains('from@example.com', array_keys($email->getFrom() ?? [])); - $this->assertContains('new-from@example.com', array_keys($email->getFrom() ?? [])); - } - - public function testSetGetSender() + public function testDataWithArray(): void { $email = new Email(); - $this->assertEmpty($email->getSender()); - $email->setSender('sender@example.com', 'Silver Stripe'); - $this->assertEquals(['sender@example.com' => 'Silver Stripe'], $email->getSender()); - } - - public function testSetGetReturnPath() - { - $email = new Email(); - $this->assertEmpty($email->getReturnPath()); - $email->setReturnPath('return@example.com'); - $this->assertEquals('return@example.com', $email->getReturnPath()); - } - - public function testSetGetTo() - { - $email = new Email('from@example.com', 'to@example.com'); - $this->assertCount(1, $email->getTo()); - $this->assertContains('to@example.com', array_keys($email->getTo() ?? [])); - $email->setTo('new-to@example.com', 'Silver Stripe'); - $this->assertEquals(['new-to@example.com' => 'Silver Stripe'], $email->getTo()); - } - - public function testAddTo() - { - $email = new Email('from@example.com', 'to@example.com'); - $this->assertCount(1, $email->getTo()); - $this->assertContains('to@example.com', array_keys($email->getTo() ?? [])); - $email->addTo('new-to@example.com'); - $this->assertCount(2, $email->getTo()); - $this->assertContains('to@example.com', array_keys($email->getTo() ?? [])); - $this->assertContains('new-to@example.com', array_keys($email->getTo() ?? [])); - } - - public function testSetGetCC() - { - $email = new Email('from@example.com', 'to@example.com', 'subject', 'body', 'cc@example.com'); - $this->assertCount(1, $email->getCC()); - $this->assertContains('cc@example.com', array_keys($email->getCC() ?? [])); - $email->setCC('new-cc@example.com', 'Silver Stripe'); - $this->assertEquals(['new-cc@example.com' => 'Silver Stripe'], $email->getCC()); - } - - public function testAddCC() - { - $email = new Email('from@example.com', 'to@example.com', 'subject', 'body', 'cc@example.com'); - $this->assertCount(1, $email->getCC()); - $this->assertContains('cc@example.com', array_keys($email->getCC() ?? [])); - $email->addCC('new-cc@example.com', 'Silver Stripe'); - $this->assertCount(2, $email->getCC()); - $this->assertContains('cc@example.com', array_keys($email->getCC() ?? [])); - $this->assertContains('new-cc@example.com', array_keys($email->getCC() ?? [])); - } - - public function testSetGetBCC() - { - $email = new Email( - 'from@example.com', - 'to@example.com', - 'subject', - 'body', - 'cc@example.com', - 'bcc@example.com' - ); - $this->assertCount(1, $email->getBCC()); - $this->assertContains('bcc@example.com', array_keys($email->getBCC() ?? [])); - $email->setBCC('new-bcc@example.com', 'Silver Stripe'); - $this->assertEquals(['new-bcc@example.com' => 'Silver Stripe'], $email->getBCC()); - } - - public function testAddBCC() - { - $email = new Email( - 'from@example.com', - 'to@example.com', - 'subject', - 'body', - 'cc@example.com', - 'bcc@example.com' - ); - $this->assertCount(1, $email->getBCC()); - $this->assertContains('bcc@example.com', array_keys($email->getBCC() ?? [])); - $email->addBCC('new-bcc@example.com', 'Silver Stripe'); - $this->assertCount(2, $email->getBCC()); - $this->assertContains('bcc@example.com', array_keys($email->getBCC() ?? [])); - $this->assertContains('new-bcc@example.com', array_keys($email->getBCC() ?? [])); - } - - public function testReplyTo() - { - $email = new Email(); - $this->assertEmpty($email->getReplyTo()); - $email->setReplyTo('reply-to@example.com', 'Silver Stripe'); - $this->assertEquals(['reply-to@example.com' => 'Silver Stripe'], $email->getReplyTo()); - $email->addReplyTo('new-reply-to@example.com'); - $this->assertCount(2, $email->getReplyTo()); - $this->assertContains('reply-to@example.com', array_keys($email->getReplyTo() ?? [])); - $this->assertContains('new-reply-to@example.com', array_keys($email->getReplyTo() ?? [])); - } - - public function testSubject() - { - $email = new Email('from@example.com', 'to@example.com', 'subject'); - $this->assertEquals('subject', $email->getSubject()); - $email->setSubject('new subject'); - $this->assertEquals('new subject', $email->getSubject()); - } - - public function testPriority() - { - $email = new Email(); - $this->assertEquals(3, $email->getPriority()); - $email->setPriority(5); - $this->assertEquals(5, $email->getPriority()); - } - - public function testData() - { - $email = new Email(); - $this->assertEmpty($email->getData()); - $email->setData([ - 'Title' => 'My Title', - ]); - $this->assertCount(1, $email->getData()); - $this->assertEquals(['Title' => 'My Title'], $email->getData()); - + $this->assertSame(true, $email->getData()->IsEmail); + $this->assertSame(Director::absoluteBaseURL(), $email->getData()->BaseURL); + $email->setData(['Lorem' => 'Ipsum']); + $this->assertSame(true, $email->getData()->IsEmail); + $this->assertSame(Director::absoluteBaseURL(), $email->getData()->BaseURL); + $this->assertSame('Ipsum', $email->getData()->Lorem); $email->addData('Content', 'My content'); - $this->assertCount(2, $email->getData()); - $this->assertEquals([ - 'Title' => 'My Title', - 'Content' => 'My content', - ], $email->getData()); - $email->removeData('Title'); - $this->assertEquals(['Content' => 'My content'], $email->getData()); + $this->assertSame(true, $email->getData()->IsEmail); + $this->assertSame(Director::absoluteBaseURL(), $email->getData()->BaseURL); + $this->assertSame('Ipsum', $email->getData()->Lorem); + $this->assertSame('My content', $email->getData()->Content); } - public function testDataWithViewableData() + public function testDataWithViewableData(): void { + $email = new Email(); + $viewableData = new ViewableData(); + $viewableData->ABC = 'XYZ'; + $email->setData($viewableData); + $data = $email->getData(); + $this->assertSame('XYZ', $data->ABC); + $this->assertSame(true, $data->IsEmail); + $this->assertSame(Director::absoluteBaseURL(), $data->BaseURL); $member = new Member(); $member->FirstName = 'First Name'; - $email = new Email(); - $this->assertEmpty($email->getData()); $email->setData($member); - $this->assertEquals($member, $email->getData()); + $this->assertSame($member->FirstName, $email->getData()->FirstName); $email->addData('Test', 'Test value'); $this->assertEquals('Test value', $email->getData()->Test); $email->removeData('Test'); $this->assertNull($email->getData()->Test); } - public function testBody() + public function testHTMLTemplate(): void { - $email = new Email(); - $this->assertEmpty($email->getBody()); - $email->setBody('

Title

'); - $this->assertEquals('

Title

', $email->getBody()); - } - - public function testHTMLTemplate() - { - // Include dev theme - SSViewer::set_themes([ - 'silverstripe/framework:/tests/php/Control/Email/EmailTest', - '$default', - ]); - // Find template on disk $emailTemplate = ModuleResourceLoader::singleton()->resolveResource( 'silverstripe/framework:templates/SilverStripe/Control/Email/Email.ss' @@ -522,7 +403,7 @@ class EmailTest extends SapphireTest $this->assertEquals('MyTemplate', $email->getHTMLTemplate()); } - public function testPlainTemplate() + public function testPlainTemplate(): void { $email = new Email(); $this->assertEmpty($email->getPlainTemplate()); @@ -530,145 +411,64 @@ class EmailTest extends SapphireTest $this->assertEquals('MyTemplate', $email->getPlainTemplate()); } - public function testGetFailedRecipients() - { - $mailer = new SwiftMailer(); - /** @var Swift_NullTransport|MockObject $transport */ - $transport = $this->getMockBuilder(Swift_NullTransport::class)->getMock(); - $transport->expects($this->once()) - ->method('send') - ->willThrowException(new Swift_RfcComplianceException('Bad email')); - $mailer->setSwiftMailer(new Swift_Mailer($transport)); - $email = new Email(); - $email->setTo('to@example.com'); - $email->setFrom('from@example.com'); - $mailer->send($email); - $this->assertCount(1, $email->getFailedRecipients()); - } - - public function testIsEmail() - { - $this->assertTrue((new Email)->IsEmail()); - } - - public function testRenderAgain() + public function testRerender(): void { $email = new Email(); + $email->setPlainTemplate($this->getTemplateClass('PlainTemplate')); $email->setData([ - 'EmailContent' => 'my content', + 'EmailContent' => '

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

your 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()); + $email->send(); + $this->assertStringContainsString('<p>your content</p>', $email->getHtmlBody()); - // Ensure setting data causes a rerender - $email->setData([ - 'EmailContent' => 'your content' - ]); - $email->render(); - $this->assertStringContainsString('your content', $email->getBody()); - - // Ensure removing data causes a rerender + // Ensure removing data causes html() to be updated $email->removeData('EmailContent'); - $email->render(); - $this->assertStringNotContainsString('your content', $email->getBody()); + $email->send(); + $this->assertStringNotContainsString('<p>your content</p>', $email->getHtmlBody()); - // Ensure adding data causes a rerender + // Ensure adding data causes html() to be updated $email->addData([ - 'EmailContent' => 'their content' + 'EmailContent' => '

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 +495,43 @@ class EmailTest extends SapphireTest } /** - * @return MockObject|Email + * @dataProvider provideCreateAddressArray */ - protected function makeEmailMock($subject) + public function testCreateAddressArray(string|array $address, string $name, array $expected): void { - /** @var Email|MockObject $email */ - $email = $this->getMockBuilder(Email::class) - ->enableProxyingToOriginalMethods() - ->getMock(); + $method = new \ReflectionMethod(Email::class, 'createAddressArray'); + $method->setAccessible(true); + $obj = new Email(); + $actual = $method->invoke($obj, $address, $name); + for ($i = 0; $i < count($expected); $i++) { + $this->assertSame($expected[$i]->getAddress(), $actual[$i]->getAddress()); + $this->assertSame($expected[$i]->getName(), $actual[$i]->getName()); + } + } - $email->setFrom('from@example.com'); - $email->setTo('to@example.com'); - $email->setSubject($subject); - $email->setBody("Body for {$subject}"); - $email->setCC('cc@example.com'); - $email->setBCC('bcc@example.com'); - $email->addAttachment(__DIR__ . '/EmailTest/attachment.txt', null, 'text/plain'); - return $email; + public function provideCreateAddressArray(): array + { + return [ + [ + 'my@email.com', + 'My name', + [ + new Address('my@email.com', 'My name'), + ], + ], + [ + [ + 'my@email.com' => 'My name', + 'other@email.com' => 'My other name', + 'no-name@email.com' + ], + '', + [ + new Address('my@email.com', 'My name'), + new Address('other@email.com', 'My other name'), + new Address('no-name@email.com', ''), + ], + ] + ]; } } 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 000000000..c998656c8 --- /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 000000000..dfb49ab89 --- /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 000000000..a3960a2a6 --- /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)[0]->getAddress(); + } + + public function testSendAllEmailsTo(): void + { + Email::config()->update('send_all_emails_to', 'to@example.com'); + $email = $this->getEmail(); + $email->send(); + + $this->assertCount(1, $email->getTo()); + $this->assertSame('to@example.com', $email->getTo()[0]->getAddress()); + $this->assertCount(1, $email->getFrom()); + $this->assertSame('original-from@example.com', $email->getFrom()[0]->getAddress()); + + $this->assertSame('original-to@example.com', $this->getHeaderValue($email, 'X-Original-To')); + $this->assertSame('original-cc@example.com', $this->getHeaderValue($email, 'X-Original-Cc')); + $this->assertSame('original-bcc@example.com', $this->getHeaderValue($email, 'X-Original-Bcc')); + $this->assertSame(null, $this->getHeaderValue($email, 'X-Original-From')); + } + + public function testSendAllEmailsFrom(): void + { + Email::config()->update('send_all_emails_from', 'from@example.com'); + $email = $this->getEmail(); + $email->send(); + + $this->assertCount(1, $email->getTo()); + $this->assertSame('original-to@example.com', $email->getTo()[0]->getAddress()); + $this->assertCount(1, $email->getFrom()); + $this->assertSame('from@example.com', $email->getFrom()[0]->getAddress()); + + $this->assertSame(null, $this->getHeaderValue($email, 'X-Original-To')); + $this->assertSame(null, $this->getHeaderValue($email, 'X-Original-Cc')); + $this->assertSame(null, $this->getHeaderValue($email, 'X-Original-Bcc')); + $this->assertSame('original-from@example.com', $this->getHeaderValue($email, 'X-Original-From')); + } + + public function testCCAllEmailsTo(): void + { + Email::config()->update('cc_all_emails_to', 'cc@example.com'); + $email = $this->getEmail(); + $email->send(); + + $this->assertCount(2, $email->getCc()); + $this->assertSame('original-cc@example.com', $email->getCc()[0]->getAddress()); + $this->assertSame('cc@example.com', $email->getCc()[1]->getAddress()); + } + + public function testBCCAllEmailsTo(): void + { + Email::config()->update('bcc_all_emails_to', 'bcc@example.com'); + $email = $this->getEmail(); + $email->send(); + + $this->assertCount(2, $email->getBcc()); + $this->assertSame('original-bcc@example.com', $email->getBcc()[0]->getAddress()); + $this->assertSame('bcc@example.com', $email->getBcc()[1]->getAddress()); + } +} diff --git a/tests/php/Control/Email/SwiftMailerTest.php b/tests/php/Control/Email/SwiftMailerTest.php deleted file mode 100644 index 43a7f98d1..000000000 --- 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 5ca702d8e..000000000 --- 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 ec3b892a2..000000000 --- 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 2a23e24d2..000000000 --- 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 8094e5428..000000000 --- a/thirdparty/swiftmailer/Swift/Transport/SimpleMailInvoker.php +++ /dev/null @@ -1,47 +0,0 @@ -