diff --git a/_config/email.yml b/_config/email.yml index 8842d39d29e..827560ab990 100644 --- a/_config/email.yml +++ b/_config/email.yml @@ -1,12 +1,55 @@ --- -Name: emailconfig +Name: mailer --- SilverStripe\Core\Injector\Injector: - Swift_Transport: Swift_MailTransport - Swift_Mailer: - constructor: - - '%$Swift_Transport' - SilverStripe\Control\Email\Mailer: - class: SilverStripe\Control\Email\SwiftMailer - properties: - SwiftMailer: '%$Swift_Mailer' + Symfony\Component\Mailer\MailerInterface: + class: Symfony\Component\Mailer\Mailer + constructor: + 0: '%$Symfony\Component\Mailer\Transport\TransportInterface' + 1: '%$Symfony\Component\Messenger\MessageBusInterface.mailer' + 2: '%$Symfony\Component\EventDispatcher\EventDispatcherInterface.mailer' + Symfony\Component\Messenger\MessageBusInterface.mailer: + class: Symfony\Component\Messenger\MessageBus + constructor: + 0: + - '%$Symfony\Component\Messenger\Middleware\MiddlewareInterface.mailer' + Symfony\Component\Messenger\Middleware\MiddlewareInterface.mailer: + class: Symfony\Component\Messenger\Middleware\SendMessageMiddleware + constructor: + 0: '%$Symfony\Component\Messenger\Transport\Sender\SendersLocatorInterface.mailer' + Symfony\Component\Messenger\Transport\Sender\SendersLocatorInterface.mailer: + class: Symfony\Component\Messenger\Transport\Sender\SendersLocator + constructor: + 0: + '*': + - 'Symfony\Component\Messenger\Transport\Sender\SenderInterface.mailer' + 1: '%$Symfony\Component\DependencyInjection\ContainerInterface.mailer' + Symfony\Component\Messenger\Transport\Sender\SenderInterface.mailer: + class: SilverStripe\Control\Email\EmailSender + Symfony\Component\DependencyInjection\ContainerInterface.mailer: + class: Symfony\Component\DependencyInjection\Container + calls: + - ['set', ['Symfony\Component\Messenger\Transport\Sender\SenderInterface.mailer', '%$Symfony\Component\Messenger\Transport\Sender\SenderInterface.mailer']] + Symfony\Component\EventDispatcher\EventDispatcherInterface.mailer: + class: Symfony\Component\EventDispatcher\EventDispatcher +--- +Name: mailer-dsn-default-config +--- +SilverStripe\Core\Injector\Injector: + Symfony\Component\Mailer\Transport\TransportInterface: + factory: Symfony\Component\Mailer\Transport + factory_method: fromDsn + constructor: + # TODO: confirm if this should be sendmail://default or native://default + # https://symfony.com/doc/current/mailer.html#using-built-in-transports + dsn: 'sendmail://default' +--- +Name: mailer-dsn-env +After: mailer-dsn-default-config +Only: + envvarset: MAILER_DSN +--- +SilverStripe\Core\Injector\Injector: + Symfony\Component\Mailer\Transport\TransportInterface: + constructor: + dsn: '`MAILER_DSN`' diff --git a/composer.json b/composer.json index 1cd80f8cd1d..4cc5dddaa55 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,11 @@ "swiftmailer/swiftmailer": "^6.3.0", "symfony/cache": "^6.1", "symfony/config": "^6.1", + "symfony/dependency-injection": "^6.1", "symfony/filesystem": "^6.1", + "symfony/mailer": "^6.1", + "symfony/messenger": "^6.1", + "symfony/mime": "^6.1", "symfony/translation": "^6.1", "symfony/yaml": "^6.1", "ext-ctype": "*", diff --git a/src/Control/Email/Email.php b/src/Control/Email/Email.php index b2fcf07fa28..40c32c6ac38 100644 --- a/src/Control/Email/Email.php +++ b/src/Control/Email/Email.php @@ -2,175 +2,133 @@ namespace SilverStripe\Control\Email; -use DateTime; -use RuntimeException; +use Symfony\Component\Mime\Email as SymfonyEmail; + use Egulias\EmailValidator\EmailValidator; use Egulias\EmailValidator\Validation\RFCValidation; +use RuntimeException; use SilverStripe\Control\Director; -use SilverStripe\Control\HTTP; -use SilverStripe\Core\Convert; +use SilverStripe\Core\Config\Configurable; use SilverStripe\Core\Environment; +use SilverStripe\Core\Extensible; +use SilverStripe\Core\Injector\Injectable; use SilverStripe\Core\Injector\Injector; -use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\ORM\FieldType\DBField; -use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\View\Requirements; use SilverStripe\View\SSViewer; use SilverStripe\View\ThemeResourceLoader; use SilverStripe\View\ViewableData; -use Swift_Message; -use Swift_Mime_SimpleMessage; -use Swift_MimePart; - -/** - * Class to support sending emails. - */ -class Email extends ViewableData +use Symfony\Component\Mailer\MailerInterface; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Part\AbstractPart; + +class Email extends SymfonyEmail { - /** - * @var array - * @config - */ - private static $send_all_emails_to = []; + use Configurable; + use Extensible; + use Injectable; - /** - * @var array - * @config - */ - private static $cc_all_emails_to = []; + private static string|array $send_all_emails_to = []; - /** - * @var array - * @config - */ - private static $bcc_all_emails_to = []; + private static string|array $cc_all_emails_to = []; - /** - * @var array - * @config - */ - private static $send_all_emails_from = []; + private static string|array $bcc_all_emails_to = []; + + private static string|array $send_all_emails_from = []; /** + * The default administrator email address or array of [email => name] * This will be set in the config on a site-by-site basis * @see https://docs.silverstripe.org/en/4/developer_guides/email/#administrator-emails - * - * @config - * @var string|array The default administrator email address or array of [email => name] - */ - private static $admin_email = null; - - /** - * @var Swift_Message - */ - private $swiftMessage; - - /** - * @var string The name of the HTML template to render the email with (without *.ss extension) */ - private $HTMLTemplate = null; + private static string|array $admin_email = ''; /** - * @var string The name of the plain text template to render the plain part of the email with + * The name of the HTML template to render the email with (without *.ss extension) */ - private $plainTemplate = null; + private string $HTMLTemplate = ''; /** - * @var Swift_MimePart + * The name of the plain text template to render the plain part of the email with */ - private $plainPart; + private string $plainTemplate = ''; /** - * @var array|ViewableData Additional data available in a template. + * Additional data available in a template. * Used in the same way than {@link ViewableData->customize()}. */ - private $data = []; + private array|ViewableData $data = []; - /** - * @var array - */ - private $failedRecipients = []; + private array|ViewableData $dataAtLastRender = []; + + private array $failedRecipients = []; /** * Checks for RFC822-valid email format. * - * @param string $address - * @return boolean - * * @copyright Cal Henderson * This code is licensed under a Creative Commons Attribution-ShareAlike 2.5 License * http://creativecommons.org/licenses/by-sa/2.5/ */ - public static function is_valid_address($address) + public static function is_valid_address(string $address): bool { - $validator = new EmailValidator(); - return $validator->isValid($address, new RFCValidation()); + $validator = Injector::inst()->get(EmailValidator::class); + $rfcValidation = Injector::inst()->get(RFCValidation::class); + return $validator->isValid($address, $rfcValidation); } - /** - * Get send_all_emails_to - * - * @return array Keys are addresses, values are names - */ - public static function getSendAllEmailsTo() + public static function getSendAllEmailsTo(): array { - return static::mergeConfiguredEmails('send_all_emails_to', 'SS_SEND_ALL_EMAILS_TO'); + return static::mergeConfiguredAddreses('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::mergeConfiguredAddreses('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::mergeConfiguredAddreses('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::mergeConfiguredAddreses('send_all_emails_from', 'SS_SEND_ALL_EMAILS_FROM'); } /** * Normalise email list from config merged with env vars * - * @param string $config Config key - * @param string $env Env variable key - * @return array Array of email addresses + * @return Address[] */ - protected static function mergeConfiguredEmails($config, $env) - { - // Normalise config list - $normalised = []; - $source = (array)static::config()->get($config); - foreach ($source as $address => $name) { - if ($address && !is_numeric($address)) { - $normalised[$address] = $name; - } elseif ($name) { - $normalised[$name] = null; - } + private static function mergeConfiguredAddreses(string $configKey, string $envKey): array + { + $addresses = []; + $config = (array) static::config()->get($configKey); + $addresses = self::convertConfigToAddreses($config); + $env = Environment::getEnv($envKey); + if ($env) { + $addresses = array_merge($addresses, self::convertConfigToAddreses($env)); } - $extra = Environment::getEnv($env); - if ($extra) { - $normalised[$extra] = null; + return $addresses; + } + + private static function convertConfigToAddreses(array|string $config): array + { + $addresses = []; + if (is_array($config)) { + foreach ($config as $key => $val) { + if (filter_var($key, FILTER_VALIDATE_EMAIL)) { + $addresses[] = new Address($key, $val); + } else { + $addresses[] = new Address($val); + } + } + } else { + $addresses[] = new Address($config); } - return $normalised; + return $addresses; } /** @@ -178,60 +136,47 @@ protected static function mergeConfiguredEmails($config, $env) * At the moment only simple string substitutions, * which are not 100% safe from email harvesting. * - * @param string $email Email-address - * @param string $method Method for obfuscating/encoding the address - * - 'direction': Reverse the text and then use CSS to put the text direction back to normal - * - 'visible': Simple string substitution ('@' to '[at]', '.' to '[dot], '-' to [dash]) - * - 'hex': Hexadecimal URL-Encoding - useful for mailto: links - * @return string + * $method defines the method for obfuscating/encoding the address + * - 'direction': Reverse the text and then use CSS to put the text direction back to normal + * - 'visible': Simple string substitution ('@' to '[at]', '.' to '[dot], '-' to [dash]) + * - 'hex': Hexadecimal URL-Encoding - useful for mailto: links */ - public static function obfuscate($email, $method = 'visible') + public static function obfuscate(string $address, string $method = 'visible'): string { switch ($method) { case 'direction': Requirements::customCSS('span.codedirection { unicode-bidi: bidi-override; direction: rtl; }', 'codedirectionCSS'); - - return '' . strrev($email) . ''; + return '' . strrev($address) . ''; case 'visible': $obfuscated = ['@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] ']; - - return strtr($email, $obfuscated); + return strtr($address, $obfuscated); case 'hex': $encoded = ''; - $emailLength = strlen($email); + $emailLength = strlen($address); for ($x = 0; $x < $emailLength; $x++) { - $encoded .= '&#x' . bin2hex($email[$x]) . ';'; + $encoded .= '&#x' . bin2hex($address[$x]) . ';'; } - return $encoded; default: user_error('Email::obfuscate(): Unknown obfuscation method', E_USER_NOTICE); - - return $email; + return $address; } } - /** - * 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); @@ -251,46 +196,8 @@ public function __construct( if ($returnPath) { $this->setReturnPath($returnPath); } - - parent::__construct(); - } - - /** - * @return Swift_Message - */ - public function getSwiftMessage() - { - 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; } - /** - * @param Swift_Message $swiftMessage - * - * @return $this - */ - public function setSwiftMessage($swiftMessage) - { - $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 @@ -316,430 +223,152 @@ private function getDefaultFrom(): string return $defaultFrom; } - /** - * @return string[] - */ - public function getFrom() - { - return $this->getSwiftMessage()->getFrom(); - } - - /** - * @param string|array $address - * @return string|array - */ - private function sanitiseAddress($address) + public function setBody(AbstractPart|string $body = null): static { - if (is_array($address)) { - return array_map('trim', $address ?? []); + if ($body instanceof AbstractPart) { + // pass to Symfony\Component\Mime\Message::setBody() + return parent::setBody($body); } - return trim($address ?? ''); - } - - /** - * @param string|array $address - * @param string|null $name - * @return $this - */ - public function setFrom($address, $name = null) - { - $address = $this->sanitiseAddress($address); - $this->getSwiftMessage()->setFrom($address, $name); - - return $this; - } - - /** - * @param string|array $address - * @param string|null $name - * @return $this - */ - public function addFrom($address, $name = null) - { - $address = $this->sanitiseAddress($address); - $this->getSwiftMessage()->addFrom($address, $name); - - return $this; - } - - /** - * @return string - */ - public function getSender() - { - return $this->getSwiftMessage()->getSender(); - } - - /** - * @param string $address - * @param string|null $name - * @return $this - */ - public function setSender($address, $name = null) - { - $address = $this->sanitiseAddress($address); - $this->getSwiftMessage()->setSender($address, $name); - - return $this; - } - - /** - * @return string - */ - public function getReturnPath() - { - return $this->getSwiftMessage()->getReturnPath(); - } - - /** - * The bounce handler address - * - * @param string $address Email address where bounce notifications should be sent - * @return $this - */ - public function setReturnPath($address) - { - $address = $this->sanitiseAddress($address); - $this->getSwiftMessage()->setReturnPath($address); - return $this; - } - - /** - * @return array - */ - public function getTo() - { - return $this->getSwiftMessage()->getTo(); - } - - /** - * 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) - { - $address = $this->sanitiseAddress($address); - $this->getSwiftMessage()->setTo($address, $name); - - return $this; - } - - /** - * @param string|array $address - * @param string|null $name - * @return $this - */ - public function addTo($address, $name = null) - { - $address = $this->sanitiseAddress($address); - $this->getSwiftMessage()->addTo($address, $name); - - return $this; - } - - /** - * @return array - */ - public function getCC() - { - return $this->getSwiftMessage()->getCc(); + // backwards compat with 4.x + return $this->html($body); } - /** - * @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) + public function setFrom(string $address, string $name = ''): static { - $address = $this->sanitiseAddress($address); - $this->getSwiftMessage()->addCc($address, $name); - - return $this; + return $this->from(new Address($address, $name)); } - /** - * @return array - */ - public function getBCC() + public function setSender(string $address, string $name = ''): static { - return $this->getSwiftMessage()->getBcc(); + return $this->sender(new Address($address, $name)); } - /** - * @param string|array $address - * @param string|null $name - * @return $this - */ - public function setBCC($address, $name = null) + public function setTo(string $address, $name = ''): static { - $address = $this->sanitiseAddress($address); - $this->getSwiftMessage()->setBcc($address, $name); - - return $this; + return $this->to(new Address($address, $name)); } - /** - * @param string|array $address - * @param string|null $name - * @return $this - */ - public function addBCC($address, $name = null) + public function setCC(string $address, string $name = ''): static { - $address = $this->sanitiseAddress($address); - $this->getSwiftMessage()->addBcc($address, $name); - - return $this; + return $this->cc(new Address($address, $name)); } - /** - * @return mixed - */ - public function getReplyTo() + public function setBCC(string $address, string $name = ''): static { - return $this->getSwiftMessage()->getReplyTo(); + return $this->bcc(new Address($address, $name)); } - /** - * @param string|array $address - * @param string|null $name - * @return $this - */ - public function setReplyTo($address, $name = null) + public function setReplyTo(string $address, string $name = ''): static { - $address = $this->sanitiseAddress($address); - $this->getSwiftMessage()->setReplyTo($address, $name); - - return $this; + return $this->replyTo(new Address($address, $name)); } - /** - * @param string|array $address - * @param string|null $name - * @return $this - */ - public function addReplyTo($address, $name = null) + // the following aliases are largely pointless, should just deprecate in 4.12 + public function setSubject(string $subject): static { - $address = $this->sanitiseAddress($address); - $this->getSwiftMessage()->addReplyTo($address, $name); - - return $this; + return $this->subject($subject); } - /** - * @return string - */ - public function getSubject() + public function setReturnPath(string $address): static { - return $this->getSwiftMessage()->getSubject(); + return $this->returnPath($address); } - /** - * @param string $subject The Subject line for the email - * @return $this - */ - public function setSubject($subject) + public function setPriority(int $priority): static { - $this->getSwiftMessage()->setSubject($subject); - - return $this; + return $this->priority($priority); } - /** - * @return int - */ - public function getPriority() + public function addAttachment(string $path, ?string $alias = null, ?string $mime = null): static { - return $this->getSwiftMessage()->getPriority(); + return $this->attachFromPath($path, $alias, $mime); } - /** - * @param int $priority - * @return $this - */ - public function setPriority($priority) + public function addAttachmentFromData(string $data, string $name, string $mime = null): static { - $this->getSwiftMessage()->setPriority($priority); - - return $this; + return $this->attach($data, $name, $mime); } /** - * @param string $path Path to file - * @param string $alias An override for the name of the file - * @param string $mime The mime type for the attachment - * @return $this + * Get template data + * + * Hardcoded data - these used to be public methods in v4 when Email extended ViewableData: + * IsEmail: used in template detect if rendering an email template rather than a page template + * BaseUrl: used in template to get the base URL for the email */ - public function addAttachment($path, $alias = null, $mime = null) + public function getData(): array|ViewableData { - $attachment = \Swift_Attachment::fromPath($path); - if ($alias) { - $attachment->setFilename($alias); - } - if ($mime) { - $attachment->setContentType($mime); + $extraData = [ + 'IsEmail' => true, + 'BaseURL' => Director::absoluteBaseURL(), + ]; + if (is_array($this->data)) { + return array_merge($this->data, $extraData); } - $this->getSwiftMessage()->attach($attachment); - - return $this; - } - - /** - * @param string $data - * @param string $name - * @param string $mime - * @return $this - */ - public function addAttachmentFromData($data, $name, $mime = null) - { - $attachment = new \Swift_Attachment($data, $name); - if ($mime) { - $attachment->setContentType($mime); + $data = clone $this->data; + foreach ($extraData as $key => $value) { + $data->{$key} = $value; } - $this->getSwiftMessage()->attach($attachment); - - return $this; + return $data; } /** - * @return array|ViewableData The template data + * Set template data */ - 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) { $this->data = $data; - $this->invalidateBody(); - return $this; } /** - * @param string|array $name The data name to add or array to names => value - * @param string|null $value The value of the data to add - * @return $this + * Add a a single or multiple pieces of template data + * $name 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, ?string $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)) { + if (is_array($this->data)) { + $this->data = array_merge($this->data, $nameOrData); + } else { + foreach ($nameOrData as $key => $val) { + $this->data->$key = $val; + } + } } else { - $this->data->$name = $value; + if (is_array($this->data)) { + $this->data[$nameOrData] = $value; + } else { + $this->data->$nameOrData = $value; + } } - - $this->invalidateBody(); - 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(); - - 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; } - - /** - * @return $this - */ - public function invalidateBody() - { - $this->setBody(null); - - return $this; - } - - /** - * @return string The base URL for the email - */ - public function BaseURL() - { - return Director::absoluteBaseURL(); - } - + /** * Debugging help - * - * @return string Debug info */ - public function debug() + public function debug(): string { $this->render(); - $class = static::class; - return "

Email template {$class}:

\n" . '
' . $this->getSwiftMessage()->toString() . '
'; + return "

Email template {$class}:

\n" . '
' . $this->getHtmlBody() . '
'; } - /** - * @return string - */ - public function getHTMLTemplate() + public function getHTMLTemplate(): string { if ($this->HTMLTemplate) { return $this->HTMLTemplate; @@ -753,215 +382,153 @@ public function getHTMLTemplate() /** * Set the template to render the email with - * - * @param string $template - * @return $this */ - public function setHTMLTemplate($template) + public function setHTMLTemplate(string $template): static { if (substr($template ?? '', -3) == '.ss') { $template = substr($template ?? '', 0, -3); } $this->HTMLTemplate = $template; - return $this; } /** * Get the template to render the plain part with - * - * @return string */ - public function getPlainTemplate() + public function getPlainTemplate(): string { return $this->plainTemplate; } /** * Set the template to render the plain part with - * - * @param string $template - * @return $this */ - public function setPlainTemplate($template) + public function setPlainTemplate(string $template): static { if (substr($template ?? '', -3) == '.ss') { $template = substr($template ?? '', 0, -3); } $this->plainTemplate = $template; - return $this; } - /** - * @param array $recipients - * @return $this - */ - public function setFailedRecipients($recipients) + public function setFailedRecipients(array $recipients): static { $this->failedRecipients = $recipients; - return $this; } - /** - * @return array - */ - public function getFailedRecipients() + public function getFailedRecipients(): array { return $this->failedRecipients; } - /** - * Used by {@link SSViewer} templates to detect if we're rendering an email template rather than a page template - * - * @return bool - */ - public function IsEmail() - { - return true; - } - /** * Send the message to the recipients * - * @return bool true if successful or array of failed recipients + * # NOTE: API change, was returning bool, now returns void */ - 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->render(); + Injector::inst()->get(MailerInterface::class)->send($this); } /** - * @return array|bool + * Send the message to the recipients as plain-only + * + * # NOTE: API change, was returning bool, now returns void */ - public function sendPlain() + public function sendPlain(): void { - if (!$this->hasPlainPart()) { - $this->render(true); - } - return Injector::inst()->get(Mailer::class)->send($this); + $html = $this->getHtmlBody(); + $this->html(null); + $this->render(true); + Injector::inst()->get(MailerInterface::class)->send($this); + $this->html($html); } /** - * Render the email - * @param bool $plainOnly Only render the message as plain text - * @return $this + * Call html() and/or text() passing a rendered email template + * If either body html or text were previously explicitly set, those values will not be overwritten */ - public function render($plainOnly = false) + public function render(bool $plainOnly = false): static { - if ($existingPlainPart = $this->findPlainPart()) { - $this->getSwiftMessage()->detach($existingPlainPart); - } - unset($existingPlainPart); - - // Respect explicitly set body - $htmlPart = $plainOnly ? null : $this->getBody(); - $plainPart = $plainOnly ? $this->getBody() : null; + // Respect explicitly set body html or text + $htmlBody = $plainOnly ? null : $this->getHtmlBody(); + $plainBody = $this->getTextBody(); // Ensure we can at least render something $htmlTemplate = $this->getHTMLTemplate(); $plainTemplate = $this->getPlainTemplate(); - if (!$htmlTemplate && !$plainTemplate && !$plainPart && !$htmlPart) { + if (!$htmlTemplate && !$plainTemplate && !$plainBody && !$htmlBody) { return $this; } // Do not interfere with emails styles Requirements::clear(); - // Render plain part - if ($plainTemplate && !$plainPart) { - $plainPart = $this->renderWith($plainTemplate, $this->getData())->Plain(); + $htmlRender = null; + $plainRender = null; + + $dataChangedSincedLastRender = $this->getDataHasChangedSinceLastRender(); + + if ($htmlBody && !$dataChangedSincedLastRender) { + $htmlRender = $htmlBody; } - // Render HTML part, either if sending html email, or a plain part is lacking - if (!$htmlPart && $htmlTemplate && (!$plainOnly || empty($plainPart))) { - $htmlPart = $this->renderWith($htmlTemplate, $this->getData()); + if ($plainBody && !$dataChangedSincedLastRender) { + $plainRender = $plainBody; } - // Plain part fails over to generated from html - if (!$plainPart && $htmlPart) { - /** @var DBHTMLText $htmlPartObject */ - $htmlPartObject = DBField::create_field('HTMLFragment', $htmlPart); - $plainPart = $htmlPartObject->Plain(); + // Render plain + if (!$plainRender && $plainTemplate) { + $plainRender = ViewableData::create()->renderWith($plainTemplate, $this->getData())->Plain(); } - // Rendering is finished - Requirements::restore(); + // Render HTML, either if sending html email, or a plain part is lacking + if (!$htmlRender && $htmlTemplate && (!$plainOnly || empty($plainRender))) { + $htmlRender = ViewableData::create()->renderWith($htmlTemplate, $this->getData())->RAW(); + } - // Fail if no email to send - if (!$plainPart && !$htmlPart) { - return $this; + // Plain render fails over to generated from html + if (!$plainRender && $htmlRender) { + $plainRender = DBField::create_field('HTMLFragment', $htmlRender)->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 data changed since last render and there's no template + if (!$htmlRender && $htmlBody) { + $htmlRender = $htmlBody; } - return $this; - } + if (!$plainRender && $plainBody) { + $plainRender = $plainBody; + } - /** - * @return Swift_MimePart|false - */ - public function findPlainPart() - { - foreach ($this->getSwiftMessage()->getChildren() as $child) { - if ($child instanceof Swift_MimePart && $child->getContentType() == 'text/plain') { - return $child; - } + // Rendering is finished + Requirements::restore(); + + if ($plainRender) { + $this->text($plainRender); + } + if ($htmlRender) { + $this->html($htmlRender); } - return false; + + $this->dataAtLastRender = $this->data; + + return $this; } - /** - * @return bool - */ - public function hasPlainPart() + private function getDataHasChangedSinceLastRender(): bool { - if ($this->getSwiftMessage()->getContentType() === 'text/plain') { + if (is_array($this->data) !== is_array($this->dataAtLastRender)) { return true; } - return (bool) $this->findPlainPart(); - } - - /** - * Automatically adds a plain part to the email generated from the current Body - * - * @return $this - */ - public function generatePlainPartFromBody() - { - $plainPart = $this->findPlainPart(); - if ($plainPart) { - $this->getSwiftMessage()->detach($plainPart); + if (is_array($this->data)) { + return $this->data !== $this->dataAtLastRender; } - unset($plainPart); - - $this->getSwiftMessage()->addPart( - Convert::xml2raw($this->getBody()), - 'text/plain', - 'utf-8' - ); - - return $this; + // ViewableData + return serialize($this->data) !== serialize($this->dataAtLastRender); } } diff --git a/src/Control/Email/EmailSender.php b/src/Control/Email/EmailSender.php new file mode 100644 index 00000000000..d1a088f0550 --- /dev/null +++ b/src/Control/Email/EmailSender.php @@ -0,0 +1,25 @@ +getMessage(); + /** @var RawMessage $rawMessage */ + $rawMessage = $sendEmailMessage->getMessage(); + /** @var TransportInterface $transport */ + $transport = Injector::inst()->get(TransportInterface::class); + $transport->send($rawMessage); + return $envelope; + } +} diff --git a/src/Control/Email/Mailer.php b/src/Control/Email/Mailer.php deleted file mode 100644 index b661c91b8db..00000000000 --- a/src/Control/Email/Mailer.php +++ /dev/null @@ -1,13 +0,0 @@ -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(); + $xTo = $this->convertAddressesToString($email->getTo()); + $xCc = $this->convertAddressesToString($email->getCc()); + $xBcc = $this->convertAddressesToString($email->getBcc()); + + // set default recipient and remove all other recipients + $email->to(...$sendAllTo); + $email->cc(...[]); + $email->bcc(...[]); + + // store the old data as X-Original-* Headers for debugging + $headers->addMailboxHeader('X-Original-To', $xTo); + $headers->addMailboxHeader('X-Original-Cc', $xCc); + $headers->addMailboxHeader('X-Original-Bcc', $xBcc); + } + + private function setFrom(Email $email, array $sendAllFrom): void + { + $headers = $email->getHeaders(); + $xFrom = $this->convertAddressesToString($email->getFrom()); + $headers->addMailboxHeader('X-Original-From', $xFrom); + $email->from(...$sendAllFrom); + } + + private function convertAddressesToString(array $addresses): string + { + return implode(',', array_map(fn(Address $address) => $address->getAddress(), $addresses)); + } + + private function updateUrls(Email $email): void + { + if ($email->getHtmlBody()) { + $email->html(HTTP::absoluteURLs($email->getHtmlBody())); + } + if ($email->getTextBody()) { + $email->text(HTTP::absoluteURLs($email->getTextBody())); + } + } +} diff --git a/src/Control/Email/SwiftMailer.php b/src/Control/Email/SwiftMailer.php deleted file mode 100644 index 1bed67c6f09..00000000000 --- a/src/Control/Email/SwiftMailer.php +++ /dev/null @@ -1,80 +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 cccc47ce91f..00000000000 --- a/src/Control/Email/SwiftPlugin.php +++ /dev/null @@ -1,80 +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/Core/BaseKernel.php b/src/Core/BaseKernel.php index 958a91615bb..451dc1f4a4c 100644 --- a/src/Core/BaseKernel.php +++ b/src/Core/BaseKernel.php @@ -28,6 +28,8 @@ use SilverStripe\View\ThemeManifest; use SilverStripe\View\ThemeResourceLoader; use Exception; +use SilverStripe\Control\Email\MailerSubscriber; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * Simple Kernel container @@ -243,6 +245,17 @@ protected function bootErrorHandling() } } + /** + * Register a subscriber for the symfony/mailer MailerEvent + * https://symfony.com/doc/current/components/event_dispatcher.html#using-event-subscribers + */ + protected function bootEmail() + { + $subscriber = MailerSubscriber::create(); + $dipatcher = Injector::inst()->get(EventDispatcherInterface::class . '.mailer'); + $dipatcher->addSubscriber($subscriber); + } + /** * Get the environment type * diff --git a/src/Core/CoreKernel.php b/src/Core/CoreKernel.php index c16d75999cd..53044e9e2e1 100644 --- a/src/Core/CoreKernel.php +++ b/src/Core/CoreKernel.php @@ -35,6 +35,7 @@ public function boot($flush = false) $this->bootErrorHandling(); $this->bootDatabaseEnvVars(); $this->bootConfigs(); + $this->bootEmail(); $this->bootDatabaseGlobals(); $this->validateDatabase(); diff --git a/src/Core/DatabaselessKernel.php b/src/Core/DatabaselessKernel.php index d5c981ba462..3fc85d6b051 100644 --- a/src/Core/DatabaselessKernel.php +++ b/src/Core/DatabaselessKernel.php @@ -49,6 +49,7 @@ public function boot($flush = false) $this->bootManifests($flush); $this->bootErrorHandling(); $this->bootConfigs(); + $this->bootEmail(); $this->setBooted(true); } diff --git a/src/Dev/SapphireTest.php b/src/Dev/SapphireTest.php index 965111b1284..4b5f0f4fb9a 100644 --- a/src/Dev/SapphireTest.php +++ b/src/Dev/SapphireTest.php @@ -15,7 +15,6 @@ use SilverStripe\Control\Cookie; use SilverStripe\Control\Director; use SilverStripe\Control\Email\Email; -use SilverStripe\Control\Email\Mailer; use SilverStripe\Control\HTTPApplication; use SilverStripe\Control\HTTPRequest; use SilverStripe\Core\Config\Config; @@ -39,6 +38,10 @@ use SilverStripe\Security\Permission; use SilverStripe\Security\Security; use SilverStripe\View\SSViewer; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Mailer\MailerInterface; +use Symfony\Component\Mailer\Transport\NullTransport; +use Symfony\Component\Messenger\MessageBusInterface; /** * Test case class for the Silverstripe framework. @@ -318,19 +321,18 @@ protected function setUp(): void } // Set up the test mailer - if (class_exists(TestMailer::class)) { - Injector::inst()->registerService(new TestMailer(), Mailer::class); - } - - if (class_exists(Email::class)) { - Email::config()->remove('send_all_emails_to'); - Email::config()->remove('send_all_emails_from'); - Email::config()->remove('cc_all_emails_to'); - Email::config()->remove('bcc_all_emails_to'); - } + $testMailer = new TestMailer( + new NullTransport(), + Injector::inst()->get(MessageBusInterface::class . '.mailer'), + Injector::inst()->get(EventDispatcherInterface::class . '.mailer'), + ); + Injector::inst()->registerService($testMailer, MailerInterface::class); + Email::config()->remove('send_all_emails_to'); + Email::config()->remove('send_all_emails_from'); + Email::config()->remove('cc_all_emails_to'); + Email::config()->remove('bcc_all_emails_to'); } - /** * Helper method to determine if the current test should enable a test database * @@ -611,8 +613,8 @@ protected function tearDown(): void */ public function clearEmails() { - /** @var Mailer $mailer */ - $mailer = Injector::inst()->get(Mailer::class); + /** @var MailerInterface $mailer */ + $mailer = Injector::inst()->get(MailerInterface::class); if ($mailer instanceof TestMailer) { $mailer->clearEmails(); return true; @@ -632,8 +634,8 @@ public function clearEmails() */ public static function findEmail($to, $from = null, $subject = null, $content = null) { - /** @var Mailer $mailer */ - $mailer = Injector::inst()->get(Mailer::class); + /** @var MailerInterface $mailer */ + $mailer = Injector::inst()->get(MailerInterface::class); if ($mailer instanceof TestMailer) { return $mailer->findEmail($to, $from, $subject, $content); } diff --git a/src/Dev/TestMailer.php b/src/Dev/TestMailer.php index 44627e62c45..fe82e5dc127 100644 --- a/src/Dev/TestMailer.php +++ b/src/Dev/TestMailer.php @@ -2,98 +2,80 @@ namespace SilverStripe\Dev; -use SilverStripe\Control\Email\Mailer; -use Swift_Attachment; +use InvalidArgumentException; +use SilverStripe\Control\Email\Email; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Event\MessageEvent; +use Symfony\Component\Mailer\MailerInterface; +use Symfony\Component\Mailer\Transport\NullTransport; +use Symfony\Component\Mime\RawMessage; +use Symfony\Component\Mailer\Transport\TransportInterface; +use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Part\DataPart; -class TestMailer implements Mailer +class TestMailer implements MailerInterface { - /** - * @var array - */ - protected $emailsSent = []; - - public function send($email) - { - // Detect body type - $htmlContent = null; - $plainContent = null; - if ($email->getSwiftMessage()->getContentType() === 'text/plain') { - $type = 'plain'; - $plainContent = $email->getBody(); - } else { - $type = 'html'; - $htmlContent = $email->getBody(); - $plainPart = $email->findPlainPart(); - if ($plainPart) { - $plainContent = $plainPart->getBody(); - } - } - - // Get attachments - $attachedFiles = []; - foreach ($email->getSwiftMessage()->getChildren() as $child) { - if ($child instanceof Swift_Attachment) { - $attachedFiles[] = [ - 'contents' => $child->getBody(), - 'filename' => $child->getFilename(), - 'mimetype' => $child->getContentType(), - ]; - } - } - - // Serialise email - $serialised = [ - 'Type' => $type, - 'To' => implode(';', array_keys($email->getTo() ?: [])), - 'From' => implode(';', array_keys($email->getFrom() ?: [])), - 'Subject' => $email->getSubject(), - 'Content' => $email->getBody(), - 'AttachedFiles' => $attachedFiles, - 'Headers' => $email->getSwiftMessage()->getHeaders(), - ]; - if ($plainContent) { - $serialised['PlainContent'] = $plainContent; - } - if ($htmlContent) { - $serialised['HtmlContent'] = $htmlContent; - } - - $this->saveEmail($serialised); + private array $emailsSent = []; - return true; - } + private TransportInterface $transport; + private ?MessageBusInterface $bus; + private ?EventDispatcherInterface $dispatcher; /** - * Save a single email to the log + * This constructor has params that match Symfony\Component\Mailer\Mailer, rather + * than Symfony\Component\Mailer\MailerInterface, so that it can be Injected in an + * indentical fashion. * - * @param array $data A map of information about the email + * Note - $transport and $bus are never actually used because Email are never supposed to + * be send. Dispatcher is however used to support SilverStripe\Control\Email\MailerSubscriber */ - protected function saveEmail($data) - { - $this->emailsSent[] = $data; + public function __construct( + TransportInterface $transport, + MessageBusInterface $bus = null, + EventDispatcherInterface $dispatcher = null + ) { + $this->transport = $transport; + $this->bus = $bus; + $this->dispatcher = $dispatcher; } - /** - * Clear the log of emails sent - */ - public function clearEmails() + public function send(RawMessage $message, Envelope $envelope = null): void { - $this->emailsSent = []; + if (!is_a($message, Email::class)) { + throw new InvalidArgumentException('$message must be a ' . Email::class); + } + /** @var Email $email */ + $email = $message; + $this->dispatchEvent($email, $envelope); + $this->emailsSent[] = [ + 'Type' => $email->getHtmlBody() ? 'html' : 'plain', + 'To' => $this->convertAddressesToString($email->getTo()), + 'From' => $this->convertAddressesToString($email->getFrom()), + 'Subject' => $email->getSubject(), + 'Content' => $email->getHtmlBody() ?: $email->getTextBody(), + 'Headers' => $email->getHeaders(), + 'PlainContent' => $email->getTextBody(), + 'HtmlContent' => $email->getHtmlBody(), + 'AttachedFiles' => array_map(fn(DataPart $attachment) => [ + 'contents' => $attachment->getBody(), + 'filename' => $attachment->getFilename(), + 'mimetype' => $attachment->getContentType() + ], $email->getAttachments()), + ]; } /** * Search for an email that was sent. * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression. - * - * @param string $to - * @param string $from - * @param string $subject - * @param string $content - * @return array|null Contains keys: 'Type', 'To', 'From', 'Subject', 'Content', 'PlainContent', 'AttachedFiles', - * 'HtmlContent' */ - public function findEmail($to, $from = null, $subject = null, $content = null) - { + public function findEmail( + string $to, + ?string $from = null, + ?string $subject = null, + ?string $content = null + ): ?array { $compare = [ 'To' => $to, 'From' => $from, @@ -131,9 +113,31 @@ public function findEmail($to, $from = null, $subject = null, $content = null) } /** - * @param string $value + * Clear the log of emails sent */ - private function normaliseSpaces(string $value) + public function clearEmails(): void + { + $this->emailsSent = []; + } + + private function convertAddressesToString(array $addresses): string + { + return implode(',', array_map(fn(Address $address) => $address->getAddress(), $addresses)); + } + + /** + * Note: unlike Symfony\Component\Mailer\Mailer, this does not clone the $message + * This is because the emails to, from, cc, and bcc may be updated by MailerSubscriber->onMessage() + * and we need to update the $this->emailsSent array with these updated values + */ + private function dispatchEvent(RawMessage $message, ?Envelope $envelope): void + { + $clonedEnvelope = null !== $envelope ? clone $envelope : Envelope::create($message); + $event = new MessageEvent($message, $clonedEnvelope, (string) $this->transport, true); + $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 40b3ee5774b..dd47ddbe154 100644 --- a/src/Security/Member.php +++ b/src/Security/Member.php @@ -9,7 +9,6 @@ use SilverStripe\Control\Controller; use SilverStripe\Control\Director; use SilverStripe\Control\Email\Email; -use SilverStripe\Control\Email\Mailer; use SilverStripe\Control\HTTPRequest; use SilverStripe\Core\Config\Config; use SilverStripe\Core\Convert; @@ -36,6 +35,7 @@ use SilverStripe\ORM\UnsavedRelationList; use SilverStripe\ORM\ValidationException; use SilverStripe\ORM\ValidationResult; +use Symfony\Component\Mailer\MailerInterface; /** * The member class which represents the users of the system @@ -907,7 +907,7 @@ public function onBeforeWrite() // We don't send emails out on dev/tests sites to prevent accidentally spamming users. // However, if TestMailer is in use this isn't a risk. // @todo some developers use external tools, so emailing might be a good idea anyway - if ((Director::isLive() || Injector::inst()->get(Mailer::class) instanceof TestMailer) + if ((Director::isLive() || Injector::inst()->get(MailerInterface::class) instanceof TestMailer) && $this->isChanged('Password') && $this->record['Password'] && $this->Email diff --git a/templates/SilverStripe/Control/Email/Email.ss b/templates/SilverStripe/Control/Email/Email.ss index d69591afeef..045fb2c19de 100644 --- a/templates/SilverStripe/Control/Email/Email.ss +++ b/templates/SilverStripe/Control/Email/Email.ss @@ -6,7 +6,7 @@
- $EmailContent + $EmailContent.RAW
diff --git a/tests/php/Control/Email/EmailTest.php b/tests/php/Control/Email/EmailTest.php index daa69ae41fb..7aa01d57a5c 100644 --- a/tests/php/Control/Email/EmailTest.php +++ b/tests/php/Control/Email/EmailTest.php @@ -2,73 +2,72 @@ 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\Part\DataPart; class EmailTest extends SapphireTest { + private array $origThemes = []; + protected function setUp(): void { parent::setUp(); Director::config()->set('alternate_base_url', 'http://www.mysite.com/'); + $this->origThemes = SSViewer::get_themes(); + SSViewer::set_themes([ + 'silverstripe/framework:/tests/php/Control/Email/EmailTest', + '$default', + ]); } - public function testAddAttachment() + protected function tearDown(): void { - $email = new Email(); + parent::tearDown(); + SSViewer::set_themes($this->origThemes); + } + public function testAddAttachment(): void + { + $email = new Email(); $email->addAttachment(__DIR__ . '/EmailTest/attachment.txt', null, 'text/plain'); - - $children = $email->getSwiftMessage()->getChildren(); - $this->assertCount(1, $children); - - /** @var Swift_Attachment $child */ - $child = reset($children); - - $this->assertInstanceOf(Swift_Attachment::class, $child); - $this->assertEquals('text/plain', $child->getContentType()); - $this->assertEquals('attachment.txt', $child->getFilename()); + $attachments = $email->getAttachments(); + $this->assertCount(1, $attachments); + $attachment = $this->getFirstAttachment($attachments); + $this->assertSame('text/plain', $attachment->getContentType()); + $this->assertSame('attachment.txt', $attachment->getFilename()); } - public function testAddAttachmentFromData() + public function testAddAttachmentFromData(): void { $email = new Email(); - $email->addAttachmentFromData('foo bar', 'foo.txt', 'text/plain'); - $children = $email->getSwiftMessage()->getChildren(); - - $this->assertCount(1, $children); - - /** @var Swift_Attachment $child */ - $child = reset($children); + $attachments = $email->getAttachments(); + $this->assertCount(1, $attachments); + $attachment = $this->getFirstAttachment($attachments); + $this->assertSame('text/plain', $attachment->getContentType()); + $this->assertSame('foo.txt', $attachment->getFilename()); + $this->assertSame('foo bar', $attachment->getBody()); + } - $this->assertInstanceOf(Swift_Attachment::class, $child); - $this->assertEquals('foo bar', $child->getBody()); - $this->assertEquals('text/plain', $child->getContentType()); - $this->assertEquals('foo.txt', $child->getFilename()); + private function getFirstAttachment(array $attachments): DataPart + { + return $attachments[0]; } /** * @dataProvider provideValidEmailAddresses */ - public function testValidEmailAddress($email) + public function testValidEmailAddress($email): void { $this->assertTrue(Email::is_valid_address($email)); } @@ -76,26 +75,26 @@ public function testValidEmailAddress($email) /** * @dataProvider provideInvalidEmailAddresses */ - public function testInvalidEmailAddress($email) + public function testInvalidEmailAddress($email): void { $this->assertFalse(Email::is_valid_address($email)); } - public function provideValidEmailAddresses() + public function provideValidEmailAddresses(): array { return [ ['test@example.com', 'test-123@sub.example.com'], ]; } - public function provideInvalidEmailAddresses() + public function provideInvalidEmailAddresses(): array { return [ ['foo.bar@', '@example.com', 'foo@'], ]; } - public function testObfuscate() + public function testObfuscate(): void { $emailAddress = 'test-1@example.com'; @@ -111,343 +110,188 @@ public function testObfuscate() ); } - public function testSendPlain() + private function getTemplateClass(string $templateName): string { - $email = $this->makeEmailMock('Test send plain'); - - // email should not call render if a body is supplied - $email->expects($this->never())->method('renderWith'); - $successful = $email->sendPlain(); + return implode('\\', ['SilverStripe', 'Control', 'Tests', 'Email', 'EmailTest', $templateName]); + } - $this->assertTrue($successful); - $this->assertEmpty($email->getFailedRecipients()); + private function getMailer(): TestMailer + { + return Injector::inst()->get(MailerInterface::class); + } - /** @var TestMailer $mailer */ - $mailer = Injector::inst()->get(Mailer::class); - $sentMail = $mailer->findEmail('to@example.com'); + private function createTestEmail(string $subject = 'My subject'): Email + { + $email = new Email(); + $email->setFrom('from@example.com'); + $email->setTo('to@example.com'); + $email->setSubject($subject); + $email->text("Plain body for $subject"); + $email->html("

HTML body for $subject

"); + $email->setCC('cc@example.com'); + $email->setBCC('bcc@example.com'); + $email->addAttachment(__DIR__ . '/EmailTest/attachment.txt', null, 'text/plain'); + return $email; + } - $this->assertTrue(is_array($sentMail)); + public function testSendPlain(): void + { + $email = $this->createTestEmail('Test send plain'); + $email->sendPlain(); + $this->assertStringNotContainsString($email->getTextBody(), 'My Plain Template'); + $sentMail = $this->getMailer()->findEmail('to@example.com'); - $this->assertEquals('to@example.com', $sentMail['To']); - $this->assertEquals('from@example.com', $sentMail['From']); - $this->assertEquals('Test send plain', $sentMail['Subject']); - $this->assertEquals('Body for Test send plain', $sentMail['Content']); + $this->assertSame('to@example.com', $sentMail['To']); + $this->assertSame('from@example.com', $sentMail['From']); + $this->assertSame('Test send plain', $sentMail['Subject']); + $this->assertStringContainsString('Plain body for Test send plain', $sentMail['Content']); $this->assertCount(1, $sentMail['AttachedFiles']); $child = reset($sentMail['AttachedFiles']); - $this->assertEquals('text/plain', $child['mimetype']); - $this->assertEquals('attachment.txt', $child['filename']); - $this->assertEquals('Hello, I\'m a text document.', $child['contents']); + $this->assertSame('text/plain', $child['mimetype']); + $this->assertSame('attachment.txt', $child['filename']); + $this->assertSame('Hello, I\'m a text document.', $child['contents']); } - public function testSend() + public function testSend(): void { - /** @var Email|MockObject $email */ - $email = $this->makeEmailMock('Test send HTML'); + $email = $this->createTestEmail('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'); + $email->setHTMLTemplate($this->getTemplateClass('HtmlTemplate')); + $email->send(); + $this->assertStringNotContainsString($email->getHtmlBody(), 'My HTML Template'); - $this->assertTrue(is_array($sentMail)); + $sentMail = $this->getMailer()->findEmail('to@example.com'); - $this->assertEquals('to@example.com', $sentMail['To']); - $this->assertEquals('from@example.com', $sentMail['From']); - $this->assertEquals('Test send HTML', $sentMail['Subject']); - $this->assertEquals('Body for Test send HTML', $sentMail['Content']); + $this->assertSame('to@example.com', $sentMail['To']); + $this->assertSame('from@example.com', $sentMail['From']); + $this->assertSame('Test send HTML', $sentMail['Subject']); + $this->assertStringContainsString('

HTML body for Test send HTML

', $sentMail['Content']); $this->assertCount(1, $sentMail['AttachedFiles']); $child = reset($sentMail['AttachedFiles']); - $this->assertEquals('text/plain', $child['mimetype']); - $this->assertEquals('attachment.txt', $child['filename']); - $this->assertEquals('Hello, I\'m a text document.', $child['contents']); + $this->assertSame('text/plain', $child['mimetype']); + $this->assertSame('attachment.txt', $child['filename']); + $this->assertSame('Hello, I\'m a text document.', $child['contents']); } - public function testRenderedSend() + public function testRenderedSend(): void { - /** @var Email|MockObject $email */ - $email = $this->getMockBuilder(Email::class) - ->enableProxyingToOriginalMethods() - ->getMock(); - $email->setFrom('from@example.com'); - $email->setTo('to@example.com'); + $email = new Email(to: 'to@example.com'); + $email->setHTMLTemplate($this->getTemplateClass('HtmlTemplate')); $email->setData([ - 'EmailContent' => 'test', + 'EmailContent' => '

test

', ]); - $this->assertFalse($email->hasPlainPart()); - $this->assertEmpty($email->getBody()); - // these seem to fail for some reason :/ - //$email->expects($this->once())->method('render'); - //$email->expects($this->once())->method('generatePlainPartFromBody'); $email->send(); - $this->assertTrue($email->hasPlainPart()); - $this->assertNotEmpty($email->getBody()); + $sentMail = $this->getMailer()->findEmail('to@example.com'); + $this->assertStringContainsString('My HTML Template', $sentMail['Content']); } - public function testRenderedSendSubclass() + public function testRenderedSendSubclass(): void { - // Include dev theme - SSViewer::set_themes([ - 'silverstripe/framework:/tests/php/Control/Email/EmailTest', - '$default', - ]); - - /** @var Email|MockObject $email */ - $email = $this->getMockBuilder(EmailSubClass::class) - ->enableProxyingToOriginalMethods() - ->getMock(); - $email->setFrom('from@example.com'); - $email->setTo('to@example.com'); + $email = new EmailSubClass(to: 'to@example.com'); $email->setData([ 'EmailContent' => 'test', ]); - $this->assertFalse($email->hasPlainPart()); - $this->assertEmpty($email->getBody()); $email->send(); - $this->assertTrue($email->hasPlainPart()); - $this->assertNotEmpty($email->getBody()); - $this->assertStringContainsString('

Email Sub-class

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

Email Sub-class

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

body

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

body

', $email->getHtmlBody()); $this->assertCount(1, $email->getCC()); - $this->assertContains('cc@example.com', array_keys($email->getCC() ?? [])); + $this->assertEquals('cc@example.com', $email->getCC()[0]->getAddress()); $this->assertCount(1, $email->getBCC()); - $this->assertContains('bcc@example.com', array_keys($email->getBCC() ?? [])); - $this->assertEquals('bounce@example.com', $email->getReturnPath()); - } - - 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()); + $this->assertEquals('bcc@example.com', $email->getBcc()[0]->getAddress()); + $this->assertEquals('bounce@example.com', $email->getReturnPath()->getAddress()); } - public function testSetSwiftMessage() + public function testSetBody(): void { - Email::config()->update('admin_email', 'admin@example.com'); - DBDatetime::set_mock_now('2017-01-01 07:00:00'); $email = new Email(); - $swiftMessage = new Swift_Message(); - $email->setSwiftMessage($swiftMessage); - $dateTime = new DateTime(); - $dateTime->setTimestamp(DBDatetime::now()->getTimestamp()); - $email->getSwiftMessage()->setDate($dateTime); - $this->assertCount(1, $email->getFrom()); - $this->assertContains('admin@example.com', array_keys($swiftMessage->getFrom() ?? [])); - $this->assertEquals(strtotime('2017-01-01 07:00:00'), $swiftMessage->getDate()->getTimestamp()); - $this->assertEquals($swiftMessage, $email->getSwiftMessage()); - - // check from field is retained - $swiftMessage = new Swift_Message(); - $swiftMessage->setFrom('from@example.com'); - $email->setSwiftMessage($swiftMessage); - $this->assertCount(1, $email->getFrom()); - $this->assertContains('from@example.com', array_keys($email->getFrom() ?? [])); + $email->setBody('

body

'); + $this->assertSame('

body

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

Title

'); - $this->assertEquals('

Title

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

my content

', $email->getHtmlBody()); + $this->assertEquals('my content', $email->getTextBody()); // ensure repeat renders don't add multiple plain parts $email->render(); - $this->assertCount(1, $email->getSwiftMessage()->getChildren()); + $this->assertStringContainsString('

my content

', $email->getHtmlBody()); + $this->assertEquals('my content', $email->getTextBody()); } - public function testRerender() + 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()); + $this->assertStringContainsString('

my content

', $email->getHtmlBody()); // Ensure setting data causes a rerender $email->setData([ - 'EmailContent' => 'your content' + 'EmailContent' => '

your content

' ]); $email->render(); - $this->assertStringContainsString('your content', $email->getBody()); + $this->assertStringContainsString('

your content

', $email->getHtmlBody()); // Ensure removing data causes a rerender $email->removeData('EmailContent'); $email->render(); - $this->assertStringNotContainsString('your content', $email->getBody()); + $this->assertStringNotContainsString('

your content

', $email->getHtmlBody()); // Ensure adding data causes a rerender $email->addData([ - 'EmailContent' => 'their content' + 'EmailContent' => '

their content

' ]); $email->render(); - $this->assertStringContainsString('their content', $email->getBody()); + $this->assertStringContainsString('

their content

', $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()); - } - - public function testHasPlainPart() - { - $email = new Email(); - $email->setData([ - '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()); + $this->assertSame('test content', $email->getTextBody()); } - public function testMultipleEmailSends() + public function testMultipleEmailSends(): void { - $email = new Email(); + $email = new Email(to: 'to@example.com'); $email->setData([ - 'EmailContent' => 'Test', + '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('

Test

', $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('

Test

', $email->getHtmlBody()); + $this->assertSame('Test', $email->getTextBody()); } - public function testGetDefaultFrom() + public function testGetDefaultFrom(): void { $email = new Email(); $class = new \ReflectionClass(Email::class); @@ -694,23 +464,25 @@ public function testGetDefaultFrom() $this->assertTrue(true); } - /** - * @return MockObject|Email - */ - protected function makeEmailMock($subject) + public function testGetData(): void { - /** @var Email|MockObject $email */ - $email = $this->getMockBuilder(Email::class) - ->enableProxyingToOriginalMethods() - ->getMock(); - - $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; + $email = new Email(); + // test array + $email->setData([ + 'Lorem' => 'Ipsum' + ]); + $this->assertSame([ + 'Lorem' => 'Ipsum', + 'IsEmail' => true, + 'BaseURL' => Director::absoluteBaseURL() + ], $email->getData()); + // test ViewableData + $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); } } diff --git a/tests/php/Control/Email/EmailTest/templates/SilverStripe/Control/Tests/Email/EmailTest/EmailSubClass.ss b/tests/php/Control/Email/EmailTest/templates/SilverStripe/Control/Tests/Email/EmailTest/EmailSubClass.ss index 5e01184f12d..b585ac395d4 100644 --- a/tests/php/Control/Email/EmailTest/templates/SilverStripe/Control/Tests/Email/EmailTest/EmailSubClass.ss +++ b/tests/php/Control/Email/EmailTest/templates/SilverStripe/Control/Tests/Email/EmailTest/EmailSubClass.ss @@ -7,7 +7,7 @@

Email Sub-class

- $EmailContent + $EmailContent.RAW
diff --git a/tests/php/Control/Email/EmailTest/templates/SilverStripe/Control/Tests/Email/EmailTest/HtmlTemplate.ss b/tests/php/Control/Email/EmailTest/templates/SilverStripe/Control/Tests/Email/EmailTest/HtmlTemplate.ss new file mode 100644 index 00000000000..ddc4160f7f6 --- /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.RAW +
+ + + diff --git a/tests/php/Control/Email/EmailTest/templates/SilverStripe/Control/Tests/Email/EmailTest/PlainTemplate.ss b/tests/php/Control/Email/EmailTest/templates/SilverStripe/Control/Tests/Email/EmailTest/PlainTemplate.ss new file mode 100644 index 00000000000..dfb49ab894b --- /dev/null +++ b/tests/php/Control/Email/EmailTest/templates/SilverStripe/Control/Tests/Email/EmailTest/PlainTemplate.ss @@ -0,0 +1,3 @@ +# My Plain Template + +$EmailContent diff --git a/tests/php/Control/Email/MailerSubscriberTest.php b/tests/php/Control/Email/MailerSubscriberTest.php new file mode 100644 index 00000000000..fc3629d3ed0 --- /dev/null +++ b/tests/php/Control/Email/MailerSubscriberTest.php @@ -0,0 +1,100 @@ +remove('send_all_emails_to'); + Email::config()->remove('cc_all_emails_to'); + Email::config()->remove('bcc_all_emails_to'); + Email::config()->remove('send_all_emails_from'); + } + + private function getEmail(): Email + { + return (new Email()) + ->setTo('original-to@example.com') + ->setCC('original-cc@example.com') + ->setBCC('original-bcc@example.com') + ->setFrom('original-from@example.com'); + } + + private function getMailer(): TestMailer + { + return Injector::inst()->get(MailerInterface::class); + } + + private function getHeaderValue(Email $email, string $headerName): ?string + { + $headers = $email->getHeaders(); + if (!$headers->has($headerName)) { + return null; + } + return $headers->getHeaderBody($headerName)->getAddress(); + } + + public function testSendAllEmailsTo(): void + { + Email::config()->update('send_all_emails_to', 'to@example.com'); + $email = $this->getEmail(); + $email->send(); + + $this->assertCount(1, $email->getTo()); + $this->assertSame('to@example.com', $email->getTo()[0]->getAddress()); + $this->assertCount(1, $email->getFrom()); + $this->assertSame('original-from@example.com', $email->getFrom()[0]->getAddress()); + + $this->assertSame('original-to@example.com', $this->getHeaderValue($email, 'X-Original-To')); + $this->assertSame('original-cc@example.com', $this->getHeaderValue($email, 'X-Original-Cc')); + $this->assertSame('original-bcc@example.com', $this->getHeaderValue($email, 'X-Original-Bcc')); + $this->assertSame(null, $this->getHeaderValue($email, 'X-Original-From')); + } + + public function testSendAllEmailsFrom(): void + { + Email::config()->update('send_all_emails_from', 'from@example.com'); + $email = $this->getEmail(); + $email->send(); + + $this->assertCount(1, $email->getTo()); + $this->assertSame('original-to@example.com', $email->getTo()[0]->getAddress()); + $this->assertCount(1, $email->getFrom()); + $this->assertSame('from@example.com', $email->getFrom()[0]->getAddress()); + + $this->assertSame(null, $this->getHeaderValue($email, 'X-Original-To')); + $this->assertSame(null, $this->getHeaderValue($email, 'X-Original-Cc')); + $this->assertSame(null, $this->getHeaderValue($email, 'X-Original-Bcc')); + $this->assertSame('original-from@example.com', $this->getHeaderValue($email, 'X-Original-From')); + } + + public function testCCAllEmailsTo(): void + { + Email::config()->update('cc_all_emails_to', 'cc@example.com'); + $email = $this->getEmail(); + $email->send(); + + $this->assertCount(2, $email->getCc()); + $this->assertSame('original-cc@example.com', $email->getCc()[0]->getAddress()); + $this->assertSame('cc@example.com', $email->getCc()[1]->getAddress()); + } + + public function testBCCAllEmailsTo(): void + { + Email::config()->update('bcc_all_emails_to', 'bcc@example.com'); + $email = $this->getEmail(); + $email->send(); + + $this->assertCount(2, $email->getBcc()); + $this->assertSame('original-bcc@example.com', $email->getBcc()[0]->getAddress()); + $this->assertSame('bcc@example.com', $email->getBcc()[1]->getAddress()); + } +} diff --git a/tests/php/Control/Email/SwiftMailerTest.php b/tests/php/Control/Email/SwiftMailerTest.php deleted file mode 100644 index 43a7f98d141..00000000000 --- a/tests/php/Control/Email/SwiftMailerTest.php +++ /dev/null @@ -1,78 +0,0 @@ -setSwiftMailer($swift = new Swift_Mailer(new Swift_NullTransport())); - - $this->assertEquals($swift, $mailer->getSwiftMailer()); - - SwiftMailer::config()->remove('swift_plugins'); - SwiftMailer::config()->update('swift_plugins', [Swift_Plugins_AntiFloodPlugin::class]); - - /** @var Swift_MailTransport $transport */ - $transport = $this->getMockBuilder(Swift_MailTransport::class)->getMock(); - $transport - ->expects($this->once()) - ->method('registerPlugin') - ->with( - $this->isInstanceOf(Swift_Plugins_AntiFloodPlugin::class) - ); - - /** @var Swift_Mailer $swift */ - $swift = $this->getMockBuilder(Swift_Mailer::class)->disableOriginalConstructor()->getMock(); - $swift - ->expects($this->once()) - ->method('registerPlugin') - ->willReturnCallback(function ($plugin) use ($transport) { - $transport->registerPlugin($plugin); - }); - - $mailer->setSwiftMailer($swift); - } - - public function testSend() - { - $email = new Email(); - $email->setTo('to@example.com'); - $email->setFrom('from@example.com'); - $email->setSubject('Subject'); - - $mailer = $this->getMockBuilder(SwiftMailer::class) - ->setMethods(['sendSwift']) - ->getMock(); - $mailer->expects($this->once())->method('sendSwift')->with( - $this->isInstanceOf(Swift_Message::class) - ); - - $mailer->send($email); - } - - public function testSendSwift() - { - $mailer = new SwiftMailer(); - $sendSwiftMethod = new \ReflectionMethod($mailer, 'sendSwift'); - $sendSwiftMethod->setAccessible(true); - $transport = $this->getMockBuilder(Swift_NullTransport::class)->getMock(); - $transport->expects($this->once()) - ->method('send'); - $mailer->setSwiftMailer(new Swift_Mailer($transport)); - $swiftMessage = new Swift_Message('Test', 'Body'); - $swiftMessage->setTo('to@example.com'); - $swiftMessage->setFrom('from@example.com'); - $sendSwiftMethod->invoke($mailer, $swiftMessage); - } -} diff --git a/tests/php/Control/Email/SwiftPluginTest.php b/tests/php/Control/Email/SwiftPluginTest.php deleted file mode 100644 index 5ca702d8efd..00000000000 --- a/tests/php/Control/Email/SwiftPluginTest.php +++ /dev/null @@ -1,110 +0,0 @@ -remove('send_all_emails_to'); - Email::config()->remove('cc_all_emails_to'); - Email::config()->remove('bcc_all_emails_to'); - Email::config()->remove('send_all_emails_from'); - } - - protected function getEmail() - { - return (new Email()) - ->setTo('original-to@example.com') - ->setCC('original-cc@example.com') - ->setBCC('original-bcc@example.com') - ->setFrom('original-from@example.com'); - } - - protected function getMailer() - { - $mailer = new \Swift_Mailer(new \Swift_NullTransport()); - $mailer->registerPlugin(new SwiftPlugin()); - - return $mailer; - } - - public function testSendAllEmailsTo() - { - Email::config()->update('send_all_emails_to', 'to@example.com'); - $email = $this->getEmail(); - $this->getMailer()->send($email->getSwiftMessage()); - $headers = $email->getSwiftMessage()->getHeaders(); - - $this->assertCount(1, $email->getTo()); - $this->assertContains('to@example.com', array_keys($email->getTo() ?? [])); - $this->assertCount(1, $email->getFrom()); - $this->assertContains('original-from@example.com', array_keys($email->getFrom() ?? [])); - - $this->assertTrue($headers->has('X-Original-To')); - $this->assertTrue($headers->has('X-Original-Cc')); - $this->assertTrue($headers->has('X-Original-Bcc')); - $this->assertFalse($headers->has('X-Original-From')); - - $originalTo = array_keys($headers->get('X-Original-To')->getFieldBodyModel() ?? []); - $originalCc = array_keys($headers->get('X-Original-Cc')->getFieldBodyModel() ?? []); - $originalBcc = array_keys($headers->get('X-Original-Bcc')->getFieldBodyModel() ?? []); - - $this->assertCount(1, $originalTo); - $this->assertContains('original-to@example.com', $originalTo); - $this->assertCount(1, $originalCc); - $this->assertContains('original-cc@example.com', $originalCc); - $this->assertCount(1, $originalBcc); - $this->assertContains('original-bcc@example.com', $originalBcc); - } - - public function testSendAllEmailsFrom() - { - Email::config()->update('send_all_emails_from', 'from@example.com'); - $email = $this->getEmail(); - $this->getMailer()->send($email->getSwiftMessage()); - - $headers = $email->getSwiftMessage()->getHeaders(); - - $this->assertFalse($headers->has('X-Original-To')); - $this->assertFalse($headers->has('X-Original-Cc')); - $this->assertFalse($headers->has('X-Original-Bcc')); - $this->assertTrue($headers->has('X-Original-From')); - - $this->assertCount(1, $email->getFrom()); - $this->assertContains('from@example.com', array_keys($email->getFrom() ?? [])); - - $this->assertCount(1, $headers->get('X-Original-From')->getFieldBodyModel()); - $this->assertContains('original-from@example.com', array_keys($headers->get('X-Original-From')->getFieldBodyModel() ?? [])); - } - - public function testCCAllEmailsTo() - { - Email::config()->update('cc_all_emails_to', 'cc@example.com'); - $email = $this->getEmail(); - $this->getMailer()->send($email->getSwiftMessage()); - - $this->assertCount(2, $email->getCC()); - $this->assertContains('cc@example.com', array_keys($email->getCC() ?? [])); - $this->assertContains('original-cc@example.com', array_keys($email->getCC() ?? [])); - } - - public function testBCCAllEmailsTo() - { - Email::config()->update('bcc_all_emails_to', 'bcc@example.com'); - $email = $this->getEmail(); - $this->getMailer()->send($email->getSwiftMessage()); - - $this->assertCount(2, $email->getBCC()); - $this->assertContains('bcc@example.com', array_keys($email->getBCC() ?? [])); - $this->assertContains('original-bcc@example.com', array_keys($email->getBCC() ?? [])); - } -}