From 3ea5015f8bbed1a88b5ea9ba27c5fe33cf75ff36 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Fri, 13 Jan 2017 01:48:46 +0000 Subject: [PATCH] NEW Move to SwiftMailer powered Emails (#6466) * API Replace existing Email and Mailer classes with SwiftMailer powered email system * DOCS New Email docs * Initial feedback from Damian * Making Mailer an interface * Templates relocated * Round of feedback * More robust approach to plain parts * Revert changes to TestMailer --- .upgrade.yml | 1 - _config/email.yml | 12 + composer.json | 3 +- docs/en/02_Developer_Guides/10_Email/index.md | 167 +-- src/Control/Email/Email.php | 1169 +++++++++-------- src/Control/Email/Mailer.php | 522 +------- src/Control/Email/SwiftMailer.php | 80 ++ src/Control/Email/SwiftPlugin.php | 87 ++ src/Dev/SapphireTest.php | 8 +- src/Dev/TestMailer.php | 117 +- src/Security/Member.php | 19 +- src/Security/MemberLoginForm.php | 17 +- .../Control/Email}/ChangePasswordEmail.ss | 0 .../Control/Email/Email.ss} | 2 +- .../Control/Email}/ForgotPasswordEmail.ss | 0 tests/php/Control/Email/EmailTest.php | 653 +++++++-- .../Control/Email/EmailTest/TestMailer.php | 41 - tests/php/Control/Email/MailerTest.php | 246 ---- .../Control/Email/MailerTest/MockMailer.php | 17 - tests/php/Control/Email/SwiftMailerTest.php | 76 ++ tests/php/Control/Email/SwiftPluginTest.php | 110 ++ 21 files changed, 1589 insertions(+), 1758 deletions(-) create mode 100644 _config/email.yml create mode 100644 src/Control/Email/SwiftMailer.php create mode 100644 src/Control/Email/SwiftPlugin.php rename templates/{email => SilverStripe/Control/Email}/ChangePasswordEmail.ss (100%) rename templates/{email/GenericEmail.ss => SilverStripe/Control/Email/Email.ss} (92%) rename templates/{email => SilverStripe/Control/Email}/ForgotPasswordEmail.ss (100%) delete mode 100644 tests/php/Control/Email/EmailTest/TestMailer.php delete mode 100644 tests/php/Control/Email/MailerTest.php delete mode 100644 tests/php/Control/Email/MailerTest/MockMailer.php create mode 100644 tests/php/Control/Email/SwiftMailerTest.php create mode 100644 tests/php/Control/Email/SwiftPluginTest.php diff --git a/.upgrade.yml b/.upgrade.yml index 6b02a842a..f2452214a 100644 --- a/.upgrade.yml +++ b/.upgrade.yml @@ -1067,7 +1067,6 @@ mappings: EmailTest: SilverStripe\Control\Tests\Email\EmailTest EmailTest_Mailer: SilverStripe\Control\Tests\Email\EmailTest\TestMailer MailerTest: SilverStripe\Control\Tests\Email\MailerTest - MailerTest_MockMailer: SilverStripe\Control\Tests\Email\MailerTest\MockMailer ErrorControlChainTest_Chain: SilverStripe\Core\Tests\Startup\ErrorControlChainTest\ErrorControlChainTest_Chain ErrorControlChainTest: SilverStripe\Core\Tests\Startup\ErrorControlChainTest ParameterConfirmationTokenTest_Token: SilverStripe\Core\Tests\Startup\ParameterConfirmationTokenTest\ParameterConfirmationTokenTest_Token diff --git a/_config/email.yml b/_config/email.yml new file mode 100644 index 000000000..fdcf87276 --- /dev/null +++ b/_config/email.yml @@ -0,0 +1,12 @@ +--- +Name: coreconfig +--- +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/composer.json b/composer.json index b5f2cba1d..a68f48448 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,8 @@ "monolog/monolog": "~1.11", "league/flysystem": "~1.0.12", "symfony/yaml": "~2.7", - "embed/embed": "^2.6" + "embed/embed": "^2.6", + "swiftmailer/swiftmailer": "~5.4" }, "require-dev": { "phpunit/PHPUnit": "~4.8", diff --git a/docs/en/02_Developer_Guides/10_Email/index.md b/docs/en/02_Developer_Guides/10_Email/index.md index 059e2a659..9e80c3df8 100644 --- a/docs/en/02_Developer_Guides/10_Email/index.md +++ b/docs/en/02_Developer_Guides/10_Email/index.md @@ -7,10 +7,16 @@ covers how to create an `Email` instance, customise it with a HTML template, the ## Configuration -Out of the box, SilverStripe will use the built-in PHP `mail()` command. If you are not running an SMTP server, you -will need to either configure PHP's SMTP settings (see [PHP documentation](http://php.net/mail) to include your mail -server configuration or use one of the third party SMTP services like [SparkPost](https://github.com/lekoala/silverstripe-sparkpost) -and [Postmark](https://github.com/fullscreeninteractive/silverstripe-postmarkmailer). +SilverStripe provides an API over the top of the [SwiftMailer](http://swiftmailer.org/) PHP library which comes with an +extensive list of "transports" for sending mail via different services. + +Out of the box, SilverStripe will use the built-in PHP `mail()` command via the `Swift_MailTransport` class. If you'd +like to use a more robust transport to send mail you can swap out the transport used by the `Mailer` via config: + +```yml +SilverStripe\Core\Injector\Injector: + Swift_Transport: Swift_SendmailTransport +``` ## Usage @@ -31,8 +37,8 @@ to `*text*`). $email->send();
-The default HTML template for emails is named `GenericEmail` and is located in `framework/templates/email/`. To -customise this template, copy it to the `mysite/templates/Email/` folder or use `setTemplate` when you create the +The default HTML template for emails is named `GenericEmail` and is located in `framework/templates/SilverStripe/Email/`. +To customise this template, copy it to the `mysite/templates/Email/` folder or use `setTemplate` when you create the `Email` instance.
@@ -40,7 +46,7 @@ customise this template, copy it to the `mysite/templates/Email/` folder or use ### Templates HTML emails can use custom templates using the same template language as your website template. You can also pass the -email object additional information using the `populateTemplate` method. +email object additional information using the `setData` and `addData` methods. **mysite/templates/Email/MyCustomEmail.ss** @@ -50,64 +56,29 @@ email object additional information using the `populateTemplate` method. The PHP Logic.. - :::php - $email = new Email(); - $email - ->setFrom($from) - ->setTo($to) - ->setSubject($subject) - ->setTemplate('MyCustomEmail') - ->populateTemplate(new ArrayData(array( - 'Member' => Member::currentUser(), - 'Link' => $link - ))); +```php +$email = SilverStripe\Control\Email\Email::create() + ->setTemplate('Email\\MyCustomEmail') + ->setData(array( + 'Member' => Member::currentUser(), + 'Link'=> $link, + )) + ->setFrom($from) + ->setTo($to) + ->setSubject($subject); - $email->send(); +if ($email->send()) { + //email sent successfully +} else { + // there may have been 1 or more failures +} +```
As we've added a new template file (`MyCustomEmail`) make sure you clear the SilverStripe cache for your changes to take affect.
-## Sub classing - -To keep your application code clean and your internal API clear, a better approach to generating an email is to create -a new subclass of `Email` which takes the required dependencies and handles setting the properties itself. - -**mysite/code/MyCustomEmail.php** - - :::php - Email; - $subject = "Welcome to our site."; - $link = Director::absoluteBaseUrl(); - - parent::__construct($from, $to, $subject); - - $this->populateTemplate(new ArrayData(array( - 'Member' => $member->Email, - 'Link' => $link - ))); - } - } - -Then within your application, usage of the email is much clearer to follow. - - :::php - send(); - - ## Administrator Emails You can set the default sender address of emails through the `Email.admin_email` [configuration setting](/developer_guides/configuration). @@ -115,7 +86,7 @@ You can set the default sender address of emails through the `Email.admin_email` **mysite/_config/app.yml** :::yaml - Email: + SilverStripe\Control\Email\Email: admin_email: support@silverstripe.org @@ -128,10 +99,12 @@ email marked as spam. If you want to send from another address think about using There are several other [configuration settings](/developer_guides/configuration) to manipulate the email server. -* `Email.send_all_emails_to` will redirect all emails sent to the given address. This is useful for testing and staging -servers where you do not wish to send emails out. -* `Email.cc_all_emails_to` and `Email.bcc_all_emails_to` will add an additional recipient in the BCC / CC header. -These are good for monitoring system-generated correspondence on the live systems. +* `SilverStripe\Control\Email\Email.send_all_emails_to` will redirect all emails sent to the given address. +All recipients will be removed (including CC and BCC addresses). This is useful for testing and staging servers where +you do not wish to send emails out. For debugging the original addresses are added as `X-Original-*` headers on the email. +* `SilverStripe\Control\Email\Email.cc_all_emails_to` and `SilverStripe\Control\Email\Email.bcc_all_emails_to` will add +an additional recipient in the BCC / CC header. These are good for monitoring system-generated correspondence on the +live systems. Configuration of those properties looks like the following: @@ -146,7 +119,10 @@ Configuration of those properties looks like the following: ### Setting custom "Reply To" email address. -For email messages that should have an email address which is replied to that actually differs from the original "from" email, do the following. This is encouraged especially when the domain responsible for sending the message isn't necessarily the same which should be used for return correspondence and should help prevent your message from being marked as spam. +For email messages that should have an email address which is replied to that actually differs from the original "from" +email, do the following. This is encouraged especially when the domain responsible for sending the message isn't +necessarily the same which should be used for return correspondence and should help prevent your message from being +marked as spam. :::php $email = new Email(..); @@ -154,74 +130,21 @@ For email messages that should have an email address which is replied to that ac ### Setting Custom Headers -For email headers which do not have getters or setters (like setTo(), setFrom()) you can use **addCustomHeader($header, -$value)** +For email headers which do not have getters or setters (like setTo(), setFrom()) you can manipulate the underlying +`Swift_Message` that we provide a wrapper for. :::php $email = new Email(...); - $email->addCustomHeader('HeaderName', 'HeaderValue'); + $email->getSwiftMessage()->getHeaders()->addTextHeader('HeaderName', 'HeaderValue'); ..
See this [Wikipedia](http://en.wikipedia.org/wiki/E-mail#Message_header) entry for a list of header names.
-## Newsletters +## SwiftMailer Documentation -The [newsletter module](http://silverstripe.org/newsletter-module) provides a UI and logic to send batch emails. - -## Custom Mailers - -SilverStripe supports changing out the underlying web server SMTP mailer service through the `Email::set_mailer()` -function. A `Mailer` subclass will commonly override the `sendPlain` and `sendHTML` methods to send emails through curl -or some other process that isn't the built in `mail()` command. - -
-There are a number of custom mailer add-ons available like [Mandrill](https://github.com/lekoala/silverstripe-mandrill) -and [Postmark](https://github.com/fullscreeninteractive/silverstripe-postmarkmailer). -
- -In this example, `LocalMailer` will take any email's going while the site is in Development mode and save it to the -assets folder instead. - -**mysite/code/LocalMailer.php** - - :::php - setBounceEmail('bounce@mycompany.com');` +For further information on SwiftMailer, consult their docs: http://swiftmailer.org/docs/introduction.html ## API Documentation diff --git a/src/Control/Email/Email.php b/src/Control/Email/Email.php index a1cb71465..44c0f97c8 100644 --- a/src/Control/Email/Email.php +++ b/src/Control/Email/Email.php @@ -4,25 +4,13 @@ namespace SilverStripe\Control\Email; use SilverStripe\Control\Director; use SilverStripe\Control\HTTP; +use SilverStripe\Core\Convert; use SilverStripe\Core\Injector\Injector; -use SilverStripe\Dev\Deprecation; -use SilverStripe\View\ArrayData; -use SilverStripe\View\SSViewer; +use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\View\Requirements; use SilverStripe\View\ViewableData; -use SilverStripe\View\ViewableData_Customised; - -if (isset($_SERVER['SERVER_NAME'])) { - /** - * X-Mailer header value on emails sent - */ - define('X_MAILER', 'SilverStripe Mailer - version 2006.06.21 (Sent from "'.$_SERVER['SERVER_NAME'].'")'); -} else { - /** - * @ignore - */ - define('X_MAILER', 'SilverStripe Mailer - version 2006.06.21'); -} +use Swift_Message; +use Swift_MimePart; /** * Class to support sending emails. @@ -31,70 +19,28 @@ class Email extends ViewableData { /** - * @var string $from Email-Address + * @var array + * @config */ - protected $from; + private static $send_all_emails_to = array(); /** - * @var string $to Email-Address. Use comma-separation to pass multiple email-addresses. + * @var array + * @config */ - protected $to; + private static $cc_all_emails_to = array(); /** - * @var string $subject Subject of the email + * @var array + * @config */ - protected $subject; + private static $bcc_all_emails_to = array(); /** - * Passed straight into {@link $ss_template} as $Body variable. - * - * @var string $body HTML content of the email. + * @var array + * @config */ - protected $body; - - /** - * If not set, defaults to converting the HTML-body with {@link Convert::xml2raw()}. - * - * @var string $plaintext_body Optional string for plaintext emails. - */ - protected $plaintext_body; - - /** - * @var string $cc - */ - protected $cc; - - /** - * @var string $bcc - */ - protected $bcc; - - /** - * @var array $customHeaders A map of header-name -> header-value - */ - protected $customHeaders = array(); - - /** - * @var array $attachments Internal, use {@link attachFileFromString()} or {@link attachFile()} - */ - protected $attachments = array(); - - /** - * @var boolean $parseVariables_done - */ - protected $parseVariables_done = false; - - /** - * @var string $ss_template The name of the used template (without *.ss extension) - */ - protected $ss_template = 'GenericEmail'; - - /** - * Used in the same way than {@link ViewableData->customize()}. - * - * @var ViewableData_Customised $template_data Additional data available in a template. - */ - protected $template_data; + private static $send_all_emails_from = array(); /** * This will be set in the config on a site-by-site basis @@ -102,359 +48,607 @@ class Email extends ViewableData * @config * @var string The default administrator email address. */ - private static $admin_email = ''; + private static $admin_email = null; /** - * Send every email generated by the Email class to the given address. - * - * It will also add " [addressed to (email), cc to (email), bcc to (email)]" to the end of the subject line - * - * To set this, set Email.send_all_emails_to in your yml config file. - * It can also be set in _ss_environment.php with SS_SEND_ALL_EMAILS_TO. - * - * @config - * @var string $send_all_emails_to Email-Address + * @var Swift_Message */ - private static $send_all_emails_to; + private $swiftMessage; /** - * Send every email generated by the Email class *from* the given address. - * It will also add " [, from to (email)]" to the end of the subject line - * - * To set this, set Email.send_all_emails_from in your yml config file. - * It can also be set in _ss_environment.php with SS_SEND_ALL_EMAILS_FROM. - * - * @config - * @var string $send_all_emails_from Email-Address + * @var string The name of the HTML template to render the email with (without *.ss extension) */ - private static $send_all_emails_from; + private $HTMLTemplate = self::class; /** - * @config - * @var string BCC every email generated by the Email class to the given address. + * @var string The name of the plain text template to render the plain part of the email with */ - private static $bcc_all_emails_to; + private $plainTemplate = ''; /** - * @config - * @var string CC every email generated by the Email class to the given address. + * @var Swift_MimePart */ - private static $cc_all_emails_to; + private $plainPart; /** - * Create a new email. + * @var array|ViewableData Additional data available in a template. + * Used in the same way than {@link ViewableData->customize()}. + */ + private $data = array(); + + /** + * @var array + */ + private $failedRecipients = array(); + + /** + * Checks for RFC822-valid email format. * - * @param string|null $from - * @param string|null $to + * @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) + { + return \Swift_Validate::email($address); + } + + /** + * Encode an email-address to protect it from spambots. + * 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 + */ + public static function obfuscate($email, $method = 'visible') + { + switch ($method) { + case 'direction' : + Requirements::customCSS('span.codedirection { unicode-bidi: bidi-override; direction: rtl; }', 'codedirectionCSS'); + + return '' . strrev($email) . ''; + case 'visible' : + $obfuscated = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] '); + + return strtr($email, $obfuscated); + case 'hex' : + $encoded = ''; + for ($x = 0; $x < strlen($email); $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|null $bounceHandlerURL - * @param string|null $cc - * @param string|null $bcc + * @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, - $bounceHandlerURL = null, $cc = null, - $bcc = null + $bcc = null, + $returnPath = null ) { - - if ($from !== null) { - $this->from = $from; + if ($from) { + $this->setFrom($from); } - if ($to !== null) { - $this->to = $to; + if ($to) { + $this->setTo($to); } - if ($subject !== null) { - $this->subject = $subject; + if ($subject) { + $this->setSubject($subject); } - if ($body !== null) { - $this->body = $body; + if ($body) { + $this->setBody($body); } - if ($cc !== null) { - $this->cc = $cc; + if ($cc) { + $this->setCC($cc); } - if ($bcc !== null) { - $this->bcc = $bcc; + if ($bcc) { + $this->setBCC($bcc); } - - if ($bounceHandlerURL !== null) { - Deprecation::notice('4.0', 'Use "emailbouncehandler" module'); + if ($returnPath) { + $this->setReturnPath($returnPath); } parent::__construct(); } /** - * Get the mailer. - * - * @return Mailer + * @return Swift_Message */ - public static function mailer() + public function getSwiftMessage() { - return Injector::inst()->get('SilverStripe\\Control\\Email\\Mailer'); - } - - /** - * Attach a file based on provided raw data. - * - * @param string $data The raw file data (not encoded). - * @param string $attachedFilename Name of the file that should appear once it's sent as a separate attachment. - * @param string|null $mimeType MIME type to use when attaching file. If not provided, will attempt to infer via HTTP::get_mime_type(). - * @return $this - */ - public function attachFileFromString($data, $attachedFilename, $mimeType = null) - { - $this->attachments[] = array( - 'contents' => $data, - 'filename' => $attachedFilename, - 'mimetype' => $mimeType, - ); - return $this; - } - - /** - * Attach the specified file to this email message. - * - * @param string $filename Relative or full path to file you wish to attach to this email message. - * @param string|null $attachedFilename Name of the file that should appear once it's sent as a separate attachment. - * @param string|null $mimeType MIME type to use when attaching file. If not provided, will attempt to infer via HTTP::get_mime_type(). - * @return $this - */ - public function attachFile($filename, $attachedFilename = null, $mimeType = null) - { - if (!$attachedFilename) { - $attachedFilename = basename($filename); + if (!$this->swiftMessage) { + $this->setSwiftMessage(new Swift_Message(null, null, 'text/html', 'utf-8')); } - $absoluteFileName = Director::getAbsFile($filename); - if (file_exists($absoluteFileName)) { - $this->attachFileFromString(file_get_contents($absoluteFileName), $attachedFilename, $mimeType); - } else { - user_error("Could not attach '$absoluteFileName' to email. File does not exist.", E_USER_NOTICE); + + return $this->swiftMessage; + } + + /** + * @param Swift_Message $swiftMessage + * + * @return $this + */ + public function setSwiftMessage($swiftMessage) + { + $swiftMessage->setDate(DBDatetime::now()->Format('U')); + if (!$swiftMessage->getFrom() && ($defaultFrom = $this->config()->admin_email)) { + $swiftMessage->setFrom($defaultFrom); } + $this->swiftMessage = $swiftMessage; + return $this; } /** - * @return string|null + * @return string[] */ - public function Subject() + public function getFrom() { - return $this->subject; + return $this->getSwiftMessage()->getFrom(); } /** - * @return string|null - */ - public function Body() - { - return $this->body; - } - - /** - * @return string|null - */ - public function To() - { - return $this->to; - } - - /** - * @return string|null - */ - public function From() - { - return $this->from; - } - - /** - * @return string|null - */ - public function Cc() - { - return $this->cc; - } - - /** - * @return string|null - */ - public function Bcc() - { - return $this->bcc; - } - - /** - * @param string $val + * @param string|array $address + * @param string|null $name * @return $this */ - public function setSubject($val) + public function setFrom($address, $name = null) { - $this->subject = $val; + $this->getSwiftMessage()->setFrom($address, $name); + return $this; } /** - * @param string $val + * @param string|array $address + * @param string|null $name * @return $this */ - public function setBody($val) + public function addFrom($address, $name = null) { - $this->body = $val; + $this->getSwiftMessage()->addFrom($address, $name); + return $this; } /** - * @param string $val - * @return $this + * @return array */ - public function setTo($val) + public function getSender() { - $this->to = $val; - return $this; + return $this->getSwiftMessage()->getSender(); } /** - * @param string $val + * @param string $address + * @param string|null $name * @return $this */ - public function setFrom($val) + public function setSender($address, $name = null) { - $this->from = $val; - return $this; - } + $this->getSwiftMessage()->setSender($address, $name); - /** - * @param string $val - * @return $this - */ - public function setCc($val) - { - $this->cc = $val; - return $this; - } - - /** - * @param string $val - * @return $this - */ - public function setBcc($val) - { - $this->bcc = $val; - return $this; - } - - /** - * Set the "Reply-To" header with an email address. - * - * @param string $val - * @return $this - */ - public function setReplyTo($val) - { - $this->addCustomHeader('Reply-To', $val); - return $this; - } - - /** - * Add a custom header to this email message. Useful for implementing all those cool features that we didn't think of. - * - * IMPORTANT: If the specified header already exists, the provided value will be appended! - * - * @todo Should there be an option to replace instead of append? Or maybe a new method ->setCustomHeader()? - * - * @param string $headerName - * @param string $headerValue - * @return $this - */ - public function addCustomHeader($headerName, $headerValue) - { - if ($headerName == 'Cc') { - $this->cc = $headerValue; - } elseif ($headerName == 'Bcc') { - $this->bcc = $headerValue; - } else { - // Append value instead of replacing. - if (isset($this->customHeaders[$headerName])) { - $this->customHeaders[$headerName] .= ", " . $headerValue; - } else { - $this->customHeaders[$headerName] = $headerValue; - } - } 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) + { + $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: + * 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) + { + $this->getSwiftMessage()->setTo($address, $name); + + return $this; + } + + /** + * @param string|array $address + * @param string|null $name + * @return $this + */ + public function addTo($address, $name = null) + { + $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) + { + $this->getSwiftMessage()->setCc($address, $name); + + return $this; + } + + /** + * @param string|array $address + * @param string|null $name + * @return $this + */ + public function addCC($address, $name = null) + { + $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) + { + $this->getSwiftMessage()->setBcc($address, $name); + + return $this; + } + + /** + * @param string|array $address + * @param string|null $name + * @return $this + */ + public function addBCC($address, $name = null) + { + $this->getSwiftMessage()->addBcc($address, $name); + + return $this; + } + + public function getReplyTo() + { + return $this->getSwiftMessage()->getReplyTo(); + } + + /** + * @param string|array $address + * @param string|null $name + * @return $this + */ + public function setReplyTo($address, $name = null) + { + $this->getSwiftMessage()->setReplyTo($address, $name); + + return $this; + } + + /** + * @param string|array $address + * @param string|null $name + * @return $this + */ + public function addReplyTo($address, $name = null) + { + $this->getSwiftMessage()->addReplyTo($address, $name); + + return $this; + } + + /** + * @return string + */ + public function getSubject() + { + return $this->getSwiftMessage()->getSubject(); + } + + /** + * @param string $subject The Subject line for the email + * @return $this + */ + public function setSubject($subject) + { + $this->getSwiftMessage()->setSubject($subject); + + return $this; + } + + /** + * @return int + */ + public function getPriority() + { + return $this->getSwiftMessage()->getPriority(); + } + + /** + * @param int $priority + * @return $this + */ + public function setPriority($priority) + { + $this->getSwiftMessage()->setPriority($priority); + + return $this; + } + + /** + * @param string $path Path to file + * @param string $alias An override for the name of the file + * @param string $mime The mime type for the attachment + * @return $this + */ + public function addAttachment($path, $alias = null, $mime = null) + { + $attachment = \Swift_Attachment::fromPath($path); + if ($alias) { + $attachment->setFilename($alias); + } + if ($mime) { + $attachment->setContentType($mime); + } + $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); + } + $this->getSwiftMessage()->attach($attachment); + + return $this; + } + + /** + * @return array|ViewableData The template data + */ + public function getData() + { + return $this->data; + } + + /** + * @param array|ViewableData $data The template data to set + * @return $this + */ + public function setData($data) + { + $this->data = $data; + + 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 + */ + public function addData($name, $value = null) + { + if (is_array($name)) { + $this->data = array_merge($this->data, $name); + } elseif (is_array($this->data)) { + $this->data[$name] = $value; + } else { + $this->data->$name = $value; + } + + return $this; + } + + /** + * Remove a datum from the message + * + * @param string $name + * @return $this + */ + public function removeData($name) + { + if (is_array($this->data)) { + unset($this->data[$name]); + } else { + $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) + { + $body = HTTP::absoluteURLs($body); + $this->getSwiftMessage()->setBody($body); + + return $this; + } + + /** + * @return string The base URL for the email + */ public function BaseURL() { return Director::absoluteBaseURL(); } /** - * Get an HTML string for debugging purposes. + * Debugging help * - * @return string + * @return string Debug info */ public function debug() { - $this->parseVariables(); + $this->render(); - return "

Email template $this->class

\n" . - "

From: $this->from\n" . - "To: $this->to\n" . - "Cc: $this->cc\n" . - "Bcc: $this->bcc\n" . - "Subject: $this->subject

" . - $this->body; + return "

Email template {$this->class}:

\n" . '
' . $this->getSwiftMessage()->toString() . '
'; } /** - * Set template name (without *.ss extension). + * @return string + */ + public function getHTMLTemplate() + { + return $this->HTMLTemplate; + } + + /** + * Set the template to render the email with * * @param string $template * @return $this */ - public function setTemplate($template) + public function setHTMLTemplate($template) { - $this->ss_template = $template; + 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 getTemplate() + public function getPlainTemplate() { - return $this->ss_template; + return $this->plainTemplate; } /** - * @return Email|ViewableData_Customised + * Set the template to render the plain part with + * + * @param string $template + * @return $this */ - protected function templateData() + public function setPlainTemplate($template) { - if ($this->template_data) { - return $this->template_data->customise(array( - "To" => $this->to, - "Cc" => $this->cc, - "Bcc" => $this->bcc, - "From" => $this->from, - "Subject" => $this->subject, - "Body" => $this->body, - "BaseURL" => $this->BaseURL(), - "IsEmail" => true, - )); - } else { - return $this; + if (substr($template, -3) == '.ss') { + $template = substr($template, 0, -3); } + $this->plainTemplate = $template; + + return $this; + } + + /** + * @param array $recipients + * @return $this + */ + public function setFailedRecipients($recipients) + { + $this->failedRecipients = $recipients; + + return $this; + } + + /** + * @return array + */ + public function getFailedRecipients() + { + 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() { @@ -462,307 +656,114 @@ class Email extends ViewableData } /** - * Populate this email template with values. This may be called many times. + * Send the message to the recipients * - * @param array|ViewableData $data + * @return bool true if successful or array of failed recipients + */ + public function send() + { + if (!$this->getBody()) { + $this->render(); + } + if (!$this->hasPlainPart()) { + $this->generatePlainPartFromBody(); + } + return Injector::inst()->get(Mailer::class)->send($this); + } + + /** + * @return array|bool + */ + public function sendPlain() + { + if (!$this->hasPlainPart()) { + $this->render(true); + } + return Injector::inst()->get(Mailer::class)->send($this); + } + + /** + * Render the email + * @param bool $plainOnly Only render the message as plain text * @return $this */ - public function populateTemplate($data) + public function render($plainOnly = false) { - if ($this->template_data) { - $this->template_data = $this->template_data->customise($data); - } else { - if (is_array($data)) { - $data = new ArrayData($data); - } - $this->template_data = $this->customise($data); + if ($existingPlainPart = $this->findPlainPart()) { + $this->getSwiftMessage()->detach($existingPlainPart); + } + unset($existingPlainPart); + if (!$this->getHTMLTemplate() && !$this->getPlainTemplate()) { + return $this; + } + $HTMLPart = ''; + $plainPart = ''; + + if ($this->getHTMLTemplate()) { + $HTMLPart = $this->renderWith($this->getHTMLTemplate(), $this->getData()); + } + + if ($this->getPlainTemplate()) { + $plainPart = $this->renderWith($this->getPlainTemplate(), $this->getData()); + } elseif ($HTMLPart) { + $plainPart = Convert::xml2raw($HTMLPart); + } + + 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'); + } + } elseif ($plainPart || $plainOnly) { + if ($plainPart) { + $this->setBody($plainPart); + } + $this->getSwiftMessage()->setContentType('text/plain'); + $this->getSwiftMessage()->setCharset('utf-8'); } - $this->parseVariables_done = false; return $this; } /** - * Load all the template variables into the internal variables, including - * the template into body. Called before send() or debugSend() - * $isPlain=true will cause the template to be ignored, otherwise the GenericEmail template will be used - * and it won't be plain email :) - * - * @param bool $isPlain - * @return $this + * @return Swift_MimePart|false */ - protected function parseVariables($isPlain = false) + public function findPlainPart() { - $origState = SSViewer::config()->get('source_file_comments'); - SSViewer::config()->update('source_file_comments', false); - - if (!$this->parseVariables_done) { - $this->parseVariables_done = true; - - // Parse $ variables in the base parameters - $this->templateData(); - - // Process a .SS template file - $fullBody = $this->body; - if ($this->ss_template && !$isPlain) { - // Requery data so that updated versions of To, From, Subject, etc are included - $data = $this->templateData(); - $candidateTemplates = [ - $this->ss_template, - [ 'type' => 'email', $this->ss_template ] - ]; - $template = new SSViewer($candidateTemplates); - if ($template->exists()) { - $fullBody = $template->process($data); - } + foreach ($this->getSwiftMessage()->getChildren() as $child) { + if ($child instanceof Swift_MimePart && $child->getContentType() == 'text/plain') { + return $child; } - - // Rewrite relative URLs - $this->body = HTTP::absoluteURLs($fullBody); } - SSViewer::config()->update('source_file_comments', $origState); - - return $this; + return false; } /** - * Send the email in plaintext. - * - * @see send() for sending emails with HTML content. - * @uses Mailer->sendPlain() - * - * @param string $messageID Optional message ID so the message can be identified in bounces etc. - * @return mixed Success of the sending operation from an MTA perspective. Doesn't actually give any indication if - * the mail has been delivered to the recipient properly). See Mailer->sendPlain() for return type details. - */ - public function sendPlain($messageID = null) - { - Requirements::clear(); - - $this->parseVariables(true); - - if (empty($this->from)) { - $this->from = Email::config()->admin_email; - } - - $headers = $this->customHeaders; - - if ($messageID) { - $headers['X-SilverStripeMessageID'] = project() . '.' . $messageID; - } - - if (project()) { - $headers['X-SilverStripeSite'] = project(); - } - - $to = $this->to; - $from = $this->from; - $subject = $this->subject; - if ($sendAllTo = $this->config()->send_all_emails_to) { - $subject .= " [addressed to $to"; - $to = $sendAllTo; - if ($this->cc) { - $subject .= ", cc to $this->cc"; - } - if ($this->bcc) { - $subject .= ", bcc to $this->bcc"; - } - $subject .= ']'; - unset($headers['Cc']); - unset($headers['Bcc']); - } else { - if ($this->cc) { - $headers['Cc'] = $this->cc; - } - if ($this->bcc) { - $headers['Bcc'] = $this->bcc; - } - } - - if ($ccAllTo = $this->config()->cc_all_emails_to) { - if (!empty($headers['Cc']) && trim($headers['Cc'])) { - $headers['Cc'] .= ', ' . $ccAllTo; - } else { - $headers['Cc'] = $ccAllTo; - } - } - - if ($bccAllTo = $this->config()->bcc_all_emails_to) { - if (!empty($headers['Bcc']) && trim($headers['Bcc'])) { - $headers['Bcc'] .= ', ' . $bccAllTo; - } else { - $headers['Bcc'] = $bccAllTo; - } - } - - if ($sendAllfrom = $this->config()->send_all_emails_from) { - if ($from) { - $subject .= " [from $from]"; - } - $from = $sendAllfrom; - } - - Requirements::restore(); - - return self::mailer()->sendPlain($to, $from, $subject, $this->body, $this->attachments, $headers); - } - - /** - * Send an email with HTML content. - * - * @see sendPlain() for sending plaintext emails only. - * @uses Mailer->sendHTML() - * - * @param string $messageID Optional message ID so the message can be identified in bounces etc. - * @return mixed Success of the sending operation from an MTA perspective. Doesn't actually give any indication if - * the mail has been delivered to the recipient properly). See Mailer->sendPlain() for return type details. - */ - public function send($messageID = null) - { - Requirements::clear(); - - $this->parseVariables(); - - if (empty($this->from)) { - $this->from = Email::config()->admin_email; - } - - $headers = $this->customHeaders; - - if ($messageID) { - $headers['X-SilverStripeMessageID'] = project() . '.' . $messageID; - } - - if (project()) { - $headers['X-SilverStripeSite'] = project(); - } - - - $to = $this->to; - $from = $this->from; - $subject = $this->subject; - if ($sendAllTo = $this->config()->send_all_emails_to) { - $subject .= " [addressed to $to"; - $to = $sendAllTo; - if ($this->cc) { - $subject .= ", cc to $this->cc"; - } - if ($this->bcc) { - $subject .= ", bcc to $this->bcc"; - } - $subject .= ']'; - unset($headers['Cc']); - unset($headers['Bcc']); - } else { - if ($this->cc) { - $headers['Cc'] = $this->cc; - } - if ($this->bcc) { - $headers['Bcc'] = $this->bcc; - } - } - - - if ($ccAllTo = $this->config()->cc_all_emails_to) { - if (!empty($headers['Cc']) && trim($headers['Cc'])) { - $headers['Cc'] .= ', ' . $ccAllTo; - } else { - $headers['Cc'] = $ccAllTo; - } - } - - if ($bccAllTo = $this->config()->bcc_all_emails_to) { - if (!empty($headers['Bcc']) && trim($headers['Bcc'])) { - $headers['Bcc'] .= ', ' . $bccAllTo; - } else { - $headers['Bcc'] = $bccAllTo; - } - } - - if ($sendAllfrom = $this->config()->send_all_emails_from) { - if ($from) { - $subject .= " [from $from]"; - } - $from = $sendAllfrom; - } - - Requirements::restore(); - - return self::mailer()->sendHTML( - $to, - $from, - $subject, - $this->body, - $this->attachments, - $headers, - $this->plaintext_body - ); - } - - /** - * Validates the email address to get as close to RFC 822 compliant as possible. - * - * @param string $email * @return bool - * - * @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($email) + public function hasPlainPart() { - $qtext = '[^\\x0d\\x22\\x5c\\x80-\\xff]'; - $dtext = '[^\\x0d\\x5b-\\x5d\\x80-\\xff]'; - $atom = '[^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c'. - '\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+'; - $quoted_pair = '\\x5c[\\x00-\\x7f]'; - $domain_literal = "\\x5b($dtext|$quoted_pair)*\\x5d"; - $quoted_string = "\\x22($qtext|$quoted_pair)*\\x22"; - $domain_ref = $atom; - $sub_domain = "($domain_ref|$domain_literal)"; - $word = "($atom|$quoted_string)"; - $domain = "$sub_domain(\\x2e$sub_domain)*"; - $local_part = "$word(\\x2e$word)*"; - $addr_spec = "$local_part\\x40$domain"; - - return preg_match("!^$addr_spec$!", $email) === 1; + if ($this->getSwiftMessage()->getContentType() == 'text/plain') { + return true; + } + return (bool) $this->findPlainPart(); } /** - * Encode an email-address to help protect it from spam bots. At the moment only simple string substitutions, which - * are not 100% safe from email harvesting. + * Automatically adds a plain part to the email generated from the current Body * - * @todo Integrate javascript-based solution - * - * @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 + * @return $this */ - public static function obfuscate($email, $method = 'visible') + public function generatePlainPartFromBody() { - switch ($method) { - case 'direction': - Requirements::customCSS( - 'span.codedirection { unicode-bidi: bidi-override; direction: rtl; }', - 'codedirectionCSS' - ); - return '' . strrev($email) . ''; - case 'visible': - $obfuscated = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] '); - return strtr($email, $obfuscated); - case 'hex': - $encoded = ''; - for ($x=0; $x < strlen($email); - $x++) { - $encoded .= '&#x' . bin2hex($email{$x}).';'; - } - return $encoded; - default: - user_error('Email::obfuscate(): Unknown obfuscation method', E_USER_NOTICE); - return $email; - } + $this->getSwiftMessage()->addPart( + Convert::xml2raw($this->getBody()), + 'text/plain', + 'utf-8' + ); + + return $this; } } diff --git a/src/Control/Email/Mailer.php b/src/Control/Email/Mailer.php index a32c1eca0..b661c91b8 100644 --- a/src/Control/Email/Mailer.php +++ b/src/Control/Email/Mailer.php @@ -2,526 +2,12 @@ namespace SilverStripe\Control\Email; -use InvalidArgumentException; -use SilverStripe\Control\HTTP; -use SilverStripe\Core\Convert; -use SilverStripe\Core\Object; - -/** - * Mailer objects are responsible for actually sending emails. - * The default Mailer class will use PHP's mail() function. - */ -class Mailer extends Object +interface Mailer { /** - * Default encoding type for messages. Available options are: - * - quoted-printable - * - base64 - * - * @var string - * @config + * @param Email $email + * @return bool */ - private static $default_message_encoding = 'quoted-printable'; - - /** - * Encoding type currently set - * - * @var string - */ - protected $messageEncoding = null; - - /** - * Email used for bounces - * - * @var string - * @config - */ - private static $default_bounce_email = null; - - /** - * Email used for bounces - * - * @var string - */ - protected $bounceEmail = null; - - /** - * Email used for bounces - * - * @return string - */ - public function getBounceEmail() - { - return $this->bounceEmail - ?: (defined('BOUNCE_EMAIL') ? BOUNCE_EMAIL : null) - ?: self::config()->default_bounce_email; - } - - /** - * Set the email used for bounces - * - * @param string $email - */ - public function setBounceEmail($email) - { - $this->bounceEmail = $email; - } - - /** - * Get the encoding type used for plain text messages - * - * @return string - */ - public function getMessageEncoding() - { - return $this->messageEncoding ?: static::config()->default_message_encoding; - } - - /** - * Sets encoding type for messages. Available options are: - * - quoted-printable - * - base64 - * - * @param string $encoding - */ - public function setMessageEncoding($encoding) - { - $this->messageEncoding = $encoding; - } - - /** - * Encode a message using the given encoding mechanism - * - * @param string $message - * @param string $encoding - * @return string Encoded $message - */ - protected function encodeMessage($message, $encoding) - { - switch ($encoding) { - case 'base64': - return chunk_split(base64_encode($message), 60); - case 'quoted-printable': - return quoted_printable_encode($message); - default: - return $message; - } - } - - /** - * Merge custom headers with default ones - * - * @param array $headers Default headers - * @param array $customHeaders Custom headers - * @return array Resulting message headers - */ - protected function mergeCustomHeaders($headers, $customHeaders) - { - $headers["X-Mailer"] = X_MAILER; - if (!isset($customHeaders["X-Priority"])) { - $headers["X-Priority"] = 3; - } - - // Merge! - $headers = array_merge($headers, $customHeaders); - - // Headers 'Cc' and 'Bcc' need to have the correct case - foreach (array('Bcc', 'Cc') as $correctKey) { - foreach ($headers as $key => $value) { - if (strcmp($key, $correctKey) !== 0 && strcasecmp($key, $correctKey) === 0) { - $headers[$correctKey] = $value; - unset($headers[$key]); - } - } - } - - return $headers; - } - - /** - * Send a plain-text email. - * - * @param string $to Email recipient - * @param string $from Email from - * @param string $subject Subject text - * @param string $plainContent Plain text content - * @param array $attachedFiles List of attached files - * @param array $customHeaders List of custom headers - * @return mixed Return false if failure, or list of arguments if success - */ - public function sendPlain($to, $from, $subject, $plainContent, $attachedFiles = array(), $customHeaders = array()) - { - // Prepare plain text body - $fullBody = $this->encodeMessage($plainContent, $this->getMessageEncoding()); - $headers["Content-Type"] = "text/plain; charset=utf-8"; - $headers["Content-Transfer-Encoding"] = $this->getMessageEncoding(); - - // Send prepared message - return $this->sendPreparedMessage($to, $from, $subject, $attachedFiles, $customHeaders, $fullBody, $headers); - } - - - /** - * Sends an email as a both HTML and plaintext - * - * @param string $to Email recipient - * @param string $from Email from - * @param string $subject Subject text - * @param string $htmlContent HTML Content - * @param array $attachedFiles List of attachments - * @param array $customHeaders User specified headers - * @param string $plainContent Plain text content. If omitted, will be generated from $htmlContent - * @return mixed Return false if failure, or list of arguments if success - */ - public function sendHTML( - $to, - $from, - $subject, - $htmlContent, - $attachedFiles = array(), - $customHeaders = array(), - $plainContent = '' - ) { - // Prepare both Plain and HTML components and merge - $plainPart = $this->preparePlainSubmessage($plainContent, $htmlContent); - $htmlPart = $this->prepareHTMLSubmessage($htmlContent); - list($fullBody, $headers) = $this->encodeMultipart( - array($plainPart, $htmlPart), - "multipart/alternative" - ); - - // Send prepared message - return $this->sendPreparedMessage($to, $from, $subject, $attachedFiles, $customHeaders, $fullBody, $headers); - } - - /** - * Send an email of an arbitrary format - * - * @param string $to To - * @param string $from From - * @param string $subject Subject - * @param array $attachedFiles List of attachments - * @param array $customHeaders User specified headers - * @param string $fullBody Prepared message - * @param array $headers Prepared headers - * @return mixed Return false if failure, or list of arguments if success - */ - protected function sendPreparedMessage($to, $from, $subject, $attachedFiles, $customHeaders, $fullBody, $headers) - { - // If the subject line contains extended characters, we must encode the - $subjectEncoded = "=?UTF-8?B?" . base64_encode($subject) . "?="; - $to = $this->validEmailAddress($to); - $from = $this->validEmailAddress($from); - - // Messages with attachments are handled differently - if ($attachedFiles) { - list($fullBody, $headers) = $this->encodeAttachments($attachedFiles, $headers, $fullBody); - } - - // Get bounce email - $bounceAddress = $this->getBounceEmail() ?: $from; - if (preg_match('/^([^<>]*)<([^<>]+)> *$/', $bounceAddress, $parts)) { - $bounceAddress = $parts[2]; - } - - // Get headers - $headers["From"] = $from; - $headers = $this->mergeCustomHeaders($headers, $customHeaders); - $headersEncoded = $this->processHeaders($headers); - - return $this->email($to, $subjectEncoded, $fullBody, $headersEncoded, $bounceAddress); - } - - /** - * Send the actual email - * - * @param string $to - * @param string $subjectEncoded - * @param string $fullBody - * @param string $headersEncoded - * @param string $bounceAddress - * @return mixed Return false if failure, or list of arguments if success - */ - protected function email($to, $subjectEncoded, $fullBody, $headersEncoded, $bounceAddress) - { - // Try it without the -f option if it fails - $result = @mail($to, $subjectEncoded, $fullBody, $headersEncoded, escapeshellarg("-f$bounceAddress")); - if (!$result) { - $result = mail($to, $subjectEncoded, $fullBody, $headersEncoded); - } - - if ($result) { - return array($to, $subjectEncoded, $fullBody, $headersEncoded, $bounceAddress); - } - - return false; - } - - /** - * Encode attachments into a message - * - * @param array $attachments - * @param array $headers - * @param string $body - * @return array Array containing completed body followed by headers - */ - protected function encodeAttachments($attachments, $headers, $body) - { - // The first part is the message itself - $fullMessage = $this->processHeaders($headers, $body); - $messageParts = array($fullMessage); - - // Include any specified attachments as additional parts - foreach ($attachments as $file) { - if (isset($file['tmp_name']) && isset($file['name'])) { - $messageParts[] = $this->encodeFileForEmail($file['tmp_name'], $file['name']); - } else { - $messageParts[] = $this->encodeFileForEmail($file); - } - } - - // We further wrap all of this into another multipart block - return $this->encodeMultipart($messageParts, "multipart/mixed"); - } - - /** - * Generate the plainPart of a html message - * - * @param string $plainContent Plain body - * @param string $htmlContent HTML message - * @return string Encoded headers / message in a single block - */ - protected function preparePlainSubmessage($plainContent, $htmlContent) - { - $plainEncoding = $this->getMessageEncoding(); - - // Generate plain text version if not explicitly given - if (!$plainContent) { - $plainContent = Convert::xml2raw($htmlContent); - } - - // Make the plain text part - $headers["Content-Type"] = "text/plain; charset=utf-8"; - $headers["Content-Transfer-Encoding"] = $plainEncoding; - $plainContentEncoded = $this->encodeMessage($plainContent, $plainEncoding); - - // Merge with headers - return $this->processHeaders($headers, $plainContentEncoded); - } - - /** - * Generate the html part of a html message - * - * @param string $htmlContent HTML message - * @return string Encoded headers / message in a single block - */ - protected function prepareHTMLSubmessage($htmlContent) - { - // Add basic wrapper tags if the body tag hasn't been given - if (stripos($htmlContent, '\n" . - "\n" . - "\n" . - "\n\n". - "\n" . - "\n" . - $htmlContent . - "\n\n" . - ""; - } - - // Make the HTML part - $headers["Content-Type"] = "text/html; charset=utf-8"; - $headers["Content-Transfer-Encoding"] = $this->getMessageEncoding(); - $htmlContentEncoded = $this->encodeMessage($htmlContent, $this->getMessageEncoding()); - - // Merge with headers - return $this->processHeaders($headers, $htmlContentEncoded); - } - - /** - * Encode an array of parts using multipart - * - * @param array $parts List of parts - * @param string $contentType Content-type of parts - * @param array $headers Existing headers to include in response - * @return array Array with two items, the body followed by headers - */ - protected function encodeMultipart($parts, $contentType, $headers = array()) - { - $separator = "----=_NextPart_" . preg_replace('/[^0-9]/', '', rand() * 10000000000); - - $headers["MIME-Version"] = "1.0"; - $headers["Content-Type"] = "$contentType; boundary=\"$separator\""; - $headers["Content-Transfer-Encoding"] = "7bit"; - - if ($contentType == "multipart/alternative") { - // $baseMessage = "This is an encoded HTML message. There are two parts: a plain text and an HTML message, - // open whatever suits you better."; - $baseMessage = "\nThis is a multi-part message in MIME format."; - } else { - // $baseMessage = "This is a message containing attachments. The e-mail body is contained in the first - // attachment"; - $baseMessage = "\nThis is a multi-part message in MIME format."; - } - - $separator = "\n--$separator\n"; - $body = "$baseMessage\n" . - $separator . implode("\n".$separator, $parts) . "\n" . trim($separator) . "--"; - - return array($body, $headers); - } - - - /** - * Add headers to the start of the message - * - * @param array $headers - * @param string $body - * @return string Resulting message body - */ - protected function processHeaders($headers, $body = '') - { - $result = ''; - foreach ($headers as $key => $value) { - $result .= "$key: $value\n"; - } - if ($body) { - $result .= "\n$body"; - } - - return $result; - } - - /** - * Encode the contents of a file for emailing, including headers - * - * $file can be an array, in which case it expects these members: - * 'filename' - the filename of the file - * 'contents' - the raw binary contents of the file as a string - * and can optionally include these members: - * 'mimetype' - the mimetype of the file (calculated from filename if missing) - * 'contentLocation' - the 'Content-Location' header value for the file - * - * $file can also be a string, in which case it is assumed to be the filename - * - * h5. contentLocation - * - * Content Location is one of the two methods allowed for embedding images into an html email. - * It's also the simplest, and best supported. - * - * Assume we have an email with this in the body: - * - * - * - * To display the image, an email viewer would have to download the image from the web every time - * it is displayed. Due to privacy issues, most viewers will not display any images unless - * the user clicks 'Show images in this email'. Not optimal. - * - * However, we can also include a copy of this image as an attached file in the email. - * By giving it a contentLocation of "http://example.com/image.gif" most email viewers - * will use this attached copy instead of downloading it. Better, - * most viewers will show it without a 'Show images in this email' conformation. - * - * Here is an example of passing this information through Email.php: - * - * $email = new Email(); - * $email->attachments[] = array( - * 'filename' => BASE_PATH . "/themes/mytheme/images/header.gif", - * 'contents' => file_get_contents(BASE_PATH . "/themes/mytheme/images/header.gif"), - * 'mimetype' => 'image/gif', - * 'contentLocation' => Director::absoluteBaseURL() . "/themes/mytheme/images/header.gif" - * ); - * - * @param array|string $file - * @param bool $destFileName - * @param string $disposition - * @param string $extraHeaders - * @return string - */ - protected function encodeFileForEmail($file, $destFileName = false, $disposition = null, $extraHeaders = "") - { - if (!$file) { - throw new InvalidArgumentException("Not passed a filename and/or data"); - } - - if (is_string($file)) { - $file = array('filename' => $file); - $fh = fopen($file['filename'], "rb"); - if ($fh) { - $file['contents'] = ""; - while (!feof($fh)) { - $file['contents'] .= fread($fh, 10000); - } - fclose($fh); - } - } - - // Build headers, including content type - if (!$destFileName) { - $base = basename($file['filename']); - } else { - $base = $destFileName; - } - - $mimeType = !empty($file['mimetype']) ? $file['mimetype'] : HTTP::get_mime_type($file['filename']); - if (!$mimeType) { - $mimeType = "application/unknown"; - } - if (empty($disposition)) { - $disposition = isset($file['contentLocation']) ? 'inline' : 'attachment'; - } - - // Encode for emailing - if (substr($mimeType, 0, 4) != 'text') { - $encoding = "base64"; - $file['contents'] = chunk_split(base64_encode($file['contents'])); - } else { - // This mime type is needed, otherwise some clients will show it as an inline attachment - $mimeType = 'application/octet-stream'; - $encoding = "quoted-printable"; - $file['contents'] = quoted_printable_encode($file['contents']); - } - - $headers = "Content-type: $mimeType;\n\tname=\"$base\"\n". - "Content-Transfer-Encoding: $encoding\n". - "Content-Disposition: $disposition;\n\tfilename=\"$base\"\n"; - - if (isset($file['contentLocation'])) { - $headers .= 'Content-Location: ' . $file['contentLocation'] . "\n" ; - } - - $headers .= $extraHeaders . "\n"; - - // Return completed packet - return $headers . $file['contents']; - } - - /** - * Cleans up emails which may be in 'Name ' format - * - * @param string $emailAddress - * @return string - */ - protected function validEmailAddress($emailAddress) - { - $emailAddress = trim($emailAddress); - $openBracket = strpos($emailAddress, '<'); - $closeBracket = strpos($emailAddress, '>'); - - // Unwrap email contained by braces - if ($openBracket === 0 && $closeBracket !== false) { - return substr($emailAddress, 1, $closeBracket - 1); - } - - // Ensure name component cannot be mistaken for an email address - if ($openBracket) { - $emailAddress = str_replace('@', '', substr($emailAddress, 0, $openBracket)) - . substr($emailAddress, $openBracket); - } - - return $emailAddress; - } + public function send($email); } diff --git a/src/Control/Email/SwiftMailer.php b/src/Control/Email/SwiftMailer.php new file mode 100644 index 000000000..c98287d86 --- /dev/null +++ b/src/Control/Email/SwiftMailer.php @@ -0,0 +1,80 @@ +getSwiftMessage(); + $failedRecipients = array(); + $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 new file mode 100644 index 000000000..b00edc09e --- /dev/null +++ b/src/Control/Email/SwiftPlugin.php @@ -0,0 +1,87 @@ +getMessage(); + $sendAllTo = Email::config()->send_all_emails_to; + $ccAllTo = Email::config()->cc_all_emails_to; + $bccAllTo = Email::config()->bcc_all_emails_to; + $sendAllFrom = Email::config()->send_all_emails_from; + + if (!empty($sendAllTo)) { + $this->setTo($message, $sendAllTo); + } + + if (!empty($ccAllTo)) { + if (!is_array($ccAllTo)) { + $ccAllTo = array($ccAllTo => null); + } + foreach ($ccAllTo as $address => $name) { + $message->addCc($address, $name); + } + } + + if (!empty($bccAllTo)) { + if (!is_array($bccAllTo)) { + $bccAllTo = array($bccAllTo => null); + } + foreach ($bccAllTo as $address => $name) { + $message->addBcc($address, $name); + } + } + + if (!empty($sendAllFrom)) { + $this->setFrom($message, $sendAllFrom); + } + } + + /** + * @param \Swift_Mime_Message $message + * @param 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_Mime_Message $message + * @param 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 dc8bafd47..d6b469b68 100644 --- a/src/Dev/SapphireTest.php +++ b/src/Dev/SapphireTest.php @@ -6,6 +6,7 @@ use SilverStripe\CMS\Controllers\RootURLController; use SilverStripe\CMS\Model\SiteTree; use SilverStripe\Control\Cookie; use SilverStripe\Control\Email\Email; +use SilverStripe\Control\Email\Mailer; use SilverStripe\Control\Session; use SilverStripe\Control\Controller; use SilverStripe\Control\Director; @@ -307,10 +308,13 @@ class SapphireTest extends PHPUnit_Framework_TestCase // Clear requirements Requirements::clear(); - // Set up email + // Set up the test mailer $this->mailer = new TestMailer(); - Injector::inst()->registerService($this->mailer, 'SilverStripe\\Control\\Email\\Mailer'); + Injector::inst()->registerService($this->mailer, Mailer::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'); } /** diff --git a/src/Dev/TestMailer.php b/src/Dev/TestMailer.php index 2b447795d..08567bc3a 100644 --- a/src/Dev/TestMailer.php +++ b/src/Dev/TestMailer.php @@ -3,87 +3,69 @@ namespace SilverStripe\Dev; use SilverStripe\Control\Email\Mailer; +use Swift_Attachment; -class TestMailer extends Mailer +class TestMailer implements Mailer { - protected $emailsSent = array(); - /** - * Send a plain-text email. - * TestMailer will merely record that the email was asked to be sent, without sending anything. - * - * @param string $to - * @param string $from - * @param string $subject - * @param string $plainContent - * @param bool $attachedFiles - * @param bool $customHeaders - * @return bool|mixed + * @var array */ - public function sendPlain($to, $from, $subject, $plainContent, $attachedFiles = false, $customHeaders = false) + protected $emailsSent = []; + + public function send($email) { - $this->saveEmail([ - 'Type' => 'plain', - 'To' => $to, - 'From' => $from, - 'Subject' => $subject, + // 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(); + } + } - 'Content' => $plainContent, - 'PlainContent' => $plainContent, + // Get attachments + $attachedFiles = []; + foreach ($email->getSwiftMessage()->getChildren() as $child) { + if ($child instanceof Swift_Attachment) { + $attachedFiles[] = [ + 'contents' => $child->getBody(), + 'filename' => $child->getFilename(), + 'mimetype' => $child->getContentType(), + ]; + } + } - 'AttachedFiles' => $attachedFiles, - 'CustomHeaders' => $customHeaders, - ]); + // 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 + ]; + if ($plainContent) { + $serialised['PlainContent'] = $plainContent; + } + if ($htmlContent) { + $serialised['HtmlContent'] = $htmlContent; + } - return true; - } - - /** - * Send a multi-part HTML email - * TestMailer will merely record that the email was asked to be sent, without sending anything. - * - * @param string $to - * @param string $from - * @param string $subject - * @param string $htmlContent - * @param bool $attachedFiles - * @param bool $customHeaders - * @param bool $plainContent - * @param bool $inlineImages - * @return bool|mixed - */ - public function sendHTML( - $to, - $from, - $subject, - $htmlContent, - $attachedFiles = false, - $customHeaders = false, - $plainContent = false, - $inlineImages = false - ) { - - $this->saveEmail([ - 'Type' => 'html', - 'To' => $to, - 'From' => $from, - 'Subject' => $subject, - - 'Content' => $htmlContent, - 'PlainContent' => $plainContent, - 'HtmlContent' => $htmlContent, - - 'AttachedFiles' => $attachedFiles, - 'CustomHeaders' => $customHeaders, - 'InlineImages' => $inlineImages, - ]); + $this->saveEmail($serialised); return true; } /** * Save a single email to the log - * @param $data A map of information about the email + * + * @param array $data A map of information about the email */ protected function saveEmail($data) { @@ -138,5 +120,6 @@ class TestMailer extends Mailer return $email; } } + return null; } } diff --git a/src/Security/Member.php b/src/Security/Member.php index 8b89f0b5b..129adb8e9 100644 --- a/src/Security/Member.php +++ b/src/Security/Member.php @@ -7,6 +7,7 @@ use SilverStripe\CMS\Controllers\CMSMain; use SilverStripe\Control\Cookie; use SilverStripe\Control\Director; use SilverStripe\Control\Email\Email; +use SilverStripe\Control\Email\Mailer; use SilverStripe\Control\Session; use SilverStripe\Core\Config\Config; use SilverStripe\Core\Convert; @@ -275,7 +276,7 @@ class Member extends DataObject implements TemplateGlobalProvider if (!Security::has_default_admin()) { return null; } - + // Find or create ADMIN group Group::singleton()->requireDefaultRecords(); $adminGroup = Permission::get_groups_by_permission('ADMIN')->first(); @@ -953,18 +954,17 @@ class Member extends DataObject implements TemplateGlobalProvider // 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. - if ((Director::isLive() || Email::mailer() instanceof TestMailer) + if ((Director::isLive() || Injector::inst()->get(Mailer::class) instanceof TestMailer) && $this->isChanged('Password') && $this->record['Password'] && $this->config()->notify_password_change ) { - /** @var Email $e */ - $e = Email::create(); - $e->setSubject(_t('Member.SUBJECTPASSWORDCHANGED', "Your password has been changed", 'Email subject')); - $e->setTemplate('ChangePasswordEmail'); - $e->populateTemplate($this); - $e->setTo($this->Email); - $e->send(); + Email::create() + ->setHTMLTemplate('SilverStripe\\Control\\Email\\ChangePasswordEmail') + ->setData($this) + ->setTo($this->Email) + ->setSubject(_t('Member.SUBJECTPASSWORDCHANGED', "Your password has been changed", 'Email subject')) + ->send(); } // The test on $this->ID is used for when records are initially created. @@ -1796,6 +1796,7 @@ class Member extends DataObject implements TemplateGlobalProvider $this->write(); } } + /** * Get the HtmlEditorConfig for this user to be used in the CMS. * This is set by the group. If multiple configurations are set, diff --git a/src/Security/MemberLoginForm.php b/src/Security/MemberLoginForm.php index 2912b0135..8b4156c5e 100644 --- a/src/Security/MemberLoginForm.php +++ b/src/Security/MemberLoginForm.php @@ -363,16 +363,13 @@ JS; if ($member) { $token = $member->generateAutologinTokenAndStoreHash(); - /** @var Email $e */ - $e = Email::create(); - $e->setSubject(_t('Member.SUBJECTPASSWORDRESET', "Your password reset link", 'Email subject')); - $e->setTemplate('ForgotPasswordEmail'); - $e->populateTemplate($member); - $e->populateTemplate(array( - 'PasswordResetLink' => Security::getPasswordResetLink($member, $token) - )); - $e->setTo($member->Email); - $e->send(); + Email::create() + ->setHTMLTemplate('SilverStripe\\Control\\Email\\ForgotPasswordEmail') + ->setData($member) + ->setSubject(_t('Member.SUBJECTPASSWORDRESET', "Your password reset link", 'Email subject')) + ->addData('PasswordResetLink', Security::getPasswordResetLink($member, $token)) + ->setTo($member->Email) + ->send(); return $this->controller->redirect('Security/passwordsent/' . urlencode($data['Email'])); } elseif ($data['Email']) { diff --git a/templates/email/ChangePasswordEmail.ss b/templates/SilverStripe/Control/Email/ChangePasswordEmail.ss similarity index 100% rename from templates/email/ChangePasswordEmail.ss rename to templates/SilverStripe/Control/Email/ChangePasswordEmail.ss diff --git a/templates/email/GenericEmail.ss b/templates/SilverStripe/Control/Email/Email.ss similarity index 92% rename from templates/email/GenericEmail.ss rename to templates/SilverStripe/Control/Email/Email.ss index 7f23eda00..d69591afe 100644 --- a/templates/email/GenericEmail.ss +++ b/templates/SilverStripe/Control/Email/Email.ss @@ -6,7 +6,7 @@
- $Body + $EmailContent
diff --git a/templates/email/ForgotPasswordEmail.ss b/templates/SilverStripe/Control/Email/ForgotPasswordEmail.ss similarity index 100% rename from templates/email/ForgotPasswordEmail.ss rename to templates/SilverStripe/Control/Email/ForgotPasswordEmail.ss diff --git a/tests/php/Control/Email/EmailTest.php b/tests/php/Control/Email/EmailTest.php index 106e582b5..47de7d236 100644 --- a/tests/php/Control/Email/EmailTest.php +++ b/tests/php/Control/Email/EmailTest.php @@ -2,59 +2,54 @@ namespace SilverStripe\Control\Tests\Email; -use SilverStripe\Core\Injector\Injector; -use SilverStripe\Dev\SapphireTest; +use PHPUnit_Framework_MockObject_MockObject; use SilverStripe\Control\Email\Email; -use SilverStripe\Control\Email\Mailer; +use SilverStripe\Control\Email\SwiftMailer; +use SilverStripe\Dev\SapphireTest; +use SilverStripe\ORM\FieldType\DBDatetime; +use SilverStripe\Security\Member; +use Swift_Attachment; +use Swift_Mailer; +use Swift_Message; +use Swift_NullTransport; +use Swift_RfcComplianceException; class EmailTest extends SapphireTest { - public function testAttachFiles() + public function testAddAttachment() { $email = new Email(); - $email->attachFileFromString('foo bar', 'foo.txt', 'text/plain'); - $email->attachFile(__DIR__ . '/EmailTest/attachment.txt', null, 'text/plain'); + $email->addAttachment(__DIR__ . '/EmailTest/attachment.txt', null, 'text/plain'); - $this->assertEquals( - array('contents'=>'foo bar', 'filename'=>'foo.txt', 'mimetype'=>'text/plain'), - $email->attachments[0], - 'File is attached correctly from string' - ); + $children = $email->getSwiftMessage()->getChildren(); + $this->assertCount(1, $children); - $this->assertEquals( - array('contents'=>'Hello, I\'m a text document.', 'filename'=>'attachment.txt', 'mimetype'=>'text/plain'), - $email->attachments[1], - 'File is attached correctly from file' - ); + /** @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()); } - public function testCustomHeaders() + public function testAddAttachmentFromData() { $email = new Email(); - $email->addCustomHeader('Cc', 'test1@example.com'); - $email->addCustomHeader('Bcc', 'test2@example.com'); + $email->addAttachmentFromData('foo bar', 'foo.txt', 'text/plain'); + $children = $email->getSwiftMessage()->getChildren(); - $this->assertEmpty( - $email->customHeaders, - 'addCustomHeader() doesn\'t add Cc and Bcc headers' - ); + $this->assertCount(1, $children); - $email->addCustomHeader('Reply-To', 'test1@example.com'); - $this->assertEquals( - array('Reply-To' => 'test1@example.com'), - $email->customHeaders, - 'addCustomHeader() adds headers' - ); + /** @var Swift_Attachment $child */ + $child = reset($children); - $email->addCustomHeader('Reply-To', 'test2@example.com'); - $this->assertEquals( - array('Reply-To' => 'test1@example.com, test2@example.com'), - $email->customHeaders, - 'addCustomHeader() appends data to existing headers' - ); + $this->assertInstanceOf(Swift_Attachment::class, $child); + $this->assertEquals('foo bar', $child->getBody()); + $this->assertEquals('text/plain', $child->getContentType()); + $this->assertEquals('foo.txt', $child->getFilename()); } public function testValidEmailAddress() @@ -63,17 +58,11 @@ class EmailTest extends SapphireTest $invalidEmails = array('foo.bar@', '@example.com', 'foo@'); foreach ($validEmails as $email) { - $this->assertTrue( - Email::is_valid_address($email), - 'is_valid_address() returns true for a valid email address' - ); + $this->assertTrue(Email::is_valid_address($email)); } foreach ($invalidEmails as $email) { - $this->assertFalse( - Email::is_valid_address($email), - 'is_valid_address() returns false for an invalid email address' - ); + $this->assertFalse(Email::is_valid_address($email)); } } @@ -85,122 +74,508 @@ class EmailTest extends SapphireTest $visible = Email::obfuscate($emailAddress, 'visible'); $hex = Email::obfuscate($emailAddress, 'hex'); + $this->assertEquals('moc.elpmaxe@1-tset', $direction); + $this->assertEquals('test [dash] 1 [at] example [dot] com', $visible); $this->assertEquals( - 'moc.elpmaxe@1-tset', - $direction, - 'obfuscate() correctly reverses the email direction' - ); - $this->assertEquals( - 'test [dash] 1 [at] example [dot] com', - $visible, - 'obfuscate() correctly obfuscates email characters' - ); - $this->assertEquals( - 'test-1@examp' - . 'le.com', - $hex, - 'obfuscate() correctly returns hex representation of email' + 'test-1@example.com', + $hex ); } public function testSendPlain() { - // Set custom $project - used in email headers - global $project; - $oldProject = $project; - $project = 'emailtest'; + /** @var Email|PHPUnit_Framework_MockObject_MockObject $email */ + $email = $this->getMockBuilder(Email::class) + ->enableProxyingToOriginalMethods() + ->disableOriginalConstructor() + ->setConstructorArgs(array( + 'from@example.com', + 'to@example.com', + 'Test send plain', + 'Testing Email->sendPlain()', + 'cc@example.com', + 'bcc@example.com', + )) + ->getMock(); - Injector::inst()->registerService(new EmailTest\TestMailer(), Mailer::class); - $email = new Email( - 'from@example.com', - 'to@example.com', - 'Test send plain', - 'Testing Email->sendPlain()', - null, - 'cc@example.com', - 'bcc@example.com' - ); - $email->attachFile(__DIR__ . '/EmailTest/attachment.txt', null, 'text/plain'); - $email->addCustomHeader('foo', 'bar'); - $sent = $email->sendPlain(123); + // email should not call render if a body is supplied + $email->expects($this->never())->method('render'); - // Restore old project name after sending - $project = $oldProject; + $email->addAttachment(__DIR__ . '/EmailTest/attachment.txt', null, 'text/plain'); + $successful = $email->sendPlain(); - $this->assertEquals('to@example.com', $sent['to']); - $this->assertEquals('from@example.com', $sent['from']); - $this->assertEquals('Test send plain', $sent['subject']); - $this->assertEquals('Testing Email->sendPlain()', $sent['content']); - $this->assertEquals( - array( - 0 => array( - 'contents'=>'Hello, I\'m a text document.', - 'filename'=>'attachment.txt', - 'mimetype'=>'text/plain' - ) - ), - $sent['files'] - ); - $this->assertEquals( - array( - 'foo' => 'bar', - 'X-SilverStripeMessageID' => 'emailtest.123', - 'X-SilverStripeSite' => 'emailtest', - 'Cc' => 'cc@example.com', - 'Bcc' => 'bcc@example.com' - ), - $sent['customheaders'] - ); + $this->assertTrue($successful); + $this->assertEmpty($email->getFailedRecipients()); + + $sentMail = $this->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('Testing Email->sendPlain()', $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']); } - public function testSendHTML() + public function testSend() { - // Set custom $project - used in email headers - global $project; - $oldProject = $project; - $project = 'emailtest'; + /** @var Email|PHPUnit_Framework_MockObject_MockObject $email */ + $email = $this->getMockBuilder(Email::class) + ->enableProxyingToOriginalMethods() + ->disableOriginalConstructor() + ->setConstructorArgs(array( + 'from@example.com', + 'to@example.com', + 'Test send HTML', + 'Testing Email->send()', + 'cc@example.com', + 'bcc@example.com', + )) + ->getMock(); - Injector::inst()->registerService(new EmailTest\TestMailer(), Mailer::class); + // email should not call render if a body is supplied + $email->expects($this->never())->method('render'); + + $email->addAttachment(__DIR__ . '/EmailTest/attachment.txt', null, 'text/plain'); + $successful = $email->send(); + + $this->assertTrue($successful); + $this->assertEmpty($email->getFailedRecipients()); + + $sentMail = $this->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('Testing Email->send()', $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']); + } + + public function testRenderedSend() + { + /** @var Email|PHPUnit_Framework_MockObject_MockObject $email */ + $email = $this->getMockBuilder(Email::class) + ->enableProxyingToOriginalMethods() + ->disableOriginalConstructor() + ->setConstructorArgs(array( + 'from@example.com', + 'to@example.com', + )) + ->getMock(); + $email->setData(array( + '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 testConsturctor() + { $email = new Email( 'from@example.com', 'to@example.com', - 'Test send plain', - 'Testing Email->send()', - null, + '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); + $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()); + $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())); + } + + 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())); + } + + 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() + { + $email = new Email(); + $this->assertEmpty($email->getSender()); + $email->setSender('sender@example.com', 'Silver Stripe'); + $this->assertEquals(array('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(array('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(array('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' ); - $email->attachFile(__DIR__ . '/EmailTest/attachment.txt', null, 'text/plain'); - $email->addCustomHeader('foo', 'bar'); - $sent = $email->send(123); + $this->assertCount(1, $email->getBCC()); + $this->assertContains('bcc@example.com', array_keys($email->getBCC())); + $email->setBCC('new-bcc@example.com', 'Silver Stripe'); + $this->assertEquals(array('new-bcc@example.com' => 'Silver Stripe'), $email->getBCC()); + } - // Restore old project name after sending - $project = $oldProject; + 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())); + } - $this->assertEquals('to@example.com', $sent['to']); - $this->assertEquals('from@example.com', $sent['from']); - $this->assertEquals('Test send plain', $sent['subject']); - $this->assertContains('Testing Email->send()', $sent['content']); - $this->assertNull($sent['plaincontent']); - $this->assertEquals( - array( - 0 => array( - 'contents'=>'Hello, I\'m a text document.', - 'filename'=>'attachment.txt', - 'mimetype'=>'text/plain' - ) - ), - $sent['files'] - ); - $this->assertEquals( - array( - 'foo' => 'bar', - 'X-SilverStripeMessageID' => 'emailtest.123', - 'X-SilverStripeSite' => 'emailtest', - 'Cc' => 'cc@example.com', - 'Bcc' => 'bcc@example.com' - ), - $sent['customheaders'] - ); + public function testReplyTo() + { + $email = new Email(); + $this->assertEmpty($email->getReplyTo()); + $email->setReplyTo('reply-to@example.com', 'Silver Stripe'); + $this->assertEquals(array('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(array( + 'Title' => 'My Title', + )); + $this->assertCount(1, $email->getData()); + $this->assertEquals(array('Title' => 'My Title'), $email->getData()); + + $email->addData('Content', 'My content'); + $this->assertCount(2, $email->getData()); + $this->assertEquals(array( + 'Title' => 'My Title', + 'Content' => 'My content', + ), $email->getData()); + $email->removeData('Title'); + $this->assertEquals(array('Content' => 'My content'), $email->getData()); + } + + public function testDataWithViewableData() + { + $member = new Member(); + $member->FirstName = 'First Name'; + $email = new Email(); + $this->assertEmpty($email->getData()); + $email->setData($member); + $this->assertEquals($member, $email->getData()); + $email->addData('Test', 'Test value'); + $this->assertEquals('Test value', $email->getData()->Test); + $email->removeData('Test'); + $this->assertNull($email->getData()->Test); + } + + public function testBody() + { + $email = new Email(); + $this->assertEmpty($email->getBody()); + $email->setBody('

Title

'); + $this->assertEquals('

Title

', $email->getBody()); + } + + public function testHTMLTemplate() + { + $email = new Email(); + $this->assertEquals(Email::class, $email->getHTMLTemplate()); + $email->setHTMLTemplate('MyTemplate'); + $this->assertEquals('MyTemplate', $email->getHTMLTemplate()); + } + + public function testPlainTemplate() + { + $email = new Email(); + $this->assertEmpty($email->getPlainTemplate()); + $email->setPlainTemplate('MyTemplate'); + $this->assertEquals('MyTemplate', $email->getPlainTemplate()); + } + + public function testGetFailedRecipients() + { + $mailer = new SwiftMailer(); + /** @var Swift_NullTransport|PHPUnit_Framework_MockObject_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 testRender() + { + $email = new Email(); + $email->setData(array( + 'EmailContent' => 'my content', + )); + $email->render(); + $this->assertContains('my content', $email->getBody()); + $children = $email->getSwiftMessage()->getChildren(); + $this->assertCount(1, $children); + $plainPart = reset($children); + $this->assertEquals('my content', $plainPart->getBody()); + + // ensure repeat renders don't add multiple plain parts + $email->render(); + $this->assertCount(1, $email->getSwiftMessage()->getChildren()); + } + + public function testRenderPlainOnly() + { + $email = new Email(); + $email->setData(array( + '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(array( + '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->assertContains('Test', $plainPart->getBody()); + $this->assertNotContains('

Test

', $plainPart->getBody()); + } + + public function testMultipleEmailSends() + { + $email = new Email(); + $email->setData(array( + 'EmailContent' => 'Test', + )); + $this->assertEmpty($email->getBody()); + $this->assertEmpty($email->getSwiftMessage()->getChildren()); + $email->send(); + $this->assertContains('Test', $email->getBody()); + $this->assertCount(1, $email->getSwiftMessage()->getChildren()); + $children = $email->getSwiftMessage()->getChildren(); + /** @var \Swift_MimePart $plainPart */ + $plainPart = reset($children); + $this->assertContains('Test', $plainPart->getBody()); + + + //send again + $email->send(); + $this->assertContains('Test', $email->getBody()); + $this->assertCount(1, $email->getSwiftMessage()->getChildren()); + $children = $email->getSwiftMessage()->getChildren(); + /** @var \Swift_MimePart $plainPart */ + $plainPart = reset($children); + $this->assertContains('Test', $plainPart->getBody()); } } diff --git a/tests/php/Control/Email/EmailTest/TestMailer.php b/tests/php/Control/Email/EmailTest/TestMailer.php deleted file mode 100644 index 3e5a0d0b3..000000000 --- a/tests/php/Control/Email/EmailTest/TestMailer.php +++ /dev/null @@ -1,41 +0,0 @@ - $to, - 'from' => $from, - 'subject' => $subject, - 'content' => $htmlContent, - 'files' => $attachedFiles, - 'customheaders' => $customheaders, - 'plaincontent' => $plainContent - ); - } - - public function sendPlain($to, $from, $subject, $plainContent, $attachedFiles = false, $customheaders = false) - { - return array( - 'to' => $to, - 'from' => $from, - 'subject' => $subject, - 'content' => $plainContent, - 'files' => $attachedFiles, - 'customheaders' => $customheaders - ); - } -} diff --git a/tests/php/Control/Email/MailerTest.php b/tests/php/Control/Email/MailerTest.php deleted file mode 100644 index a3b9d2f1c..000000000 --- a/tests/php/Control/Email/MailerTest.php +++ /dev/null @@ -1,246 +0,0 @@ -sendPlain( - '', - 'tom@jones ', - "What is the of testing?", - $testMessage, - null, - array('CC' => 'admin@silverstripe.com', 'bcc' => 'andrew@thing.com') - ); - - $this->assertEquals('email@silverstripe.com', $to); - $this->assertEquals('=?UTF-8?B?V2hhdCBpcyB0aGUgPHB1cnBvc2U+IG9mIHRlc3Rpbmc/?=', $subjectEncoded); - $this->assertEquals('=?UTF-8?B?'. base64_encode('What is the of testing?').'?=', $subjectEncoded); - - $this->assertEquals( - <<assertEquals($testMessage, quoted_printable_decode($fullBody)); - $this->assertEquals( - << -X-Mailer: SilverStripe Mailer - version 2006.06.21 -X-Priority: 3 -Bcc: andrew@thing.com -Cc: admin@silverstripe.com - -PHP - , - Convert::nl2os($headersEncoded) - ); - $this->assertEquals('tom@silverstripe.com', $bounceAddress); - - // Test override bounce email and alternate encoding - $mailer->setBounceEmail('bounce@silverstripe.com'); - $mailer->setMessageEncoding('base64'); - list($to, $subjectEncoded, $fullBody, $headersEncoded, $bounceAddress) = $mailer->sendPlain( - '', - 'tom@jones ', - "What is the of testing?", - $testMessage, - null, - array('CC' => 'admin@silverstripe.com', 'bcc' => 'andrew@thing.com') - ); - - $this->assertEquals('bounce@silverstripe.com', $bounceAddress); - $this->assertEquals( - <<assertEquals($testMessage, base64_decode($fullBody)); - } - - /** - * Test HTML messages - */ - public function testSendHTML() - { - $mailer = new MailerTest\MockMailer(); - - // Test with default encoding - $testMessageHTML = "

The majority of the answers so far are saying that private methods are " . - "implementation details which don't (or at least shouldn't) ". - "matter so long as the public interface is well-tested & working

". - "

That's absolutely correct if your only purpose for testing is to guarantee that the ". - "public interface works.

"; - $testMessagePlain = Convert::xml2raw($testMessageHTML); - $this->assertTrue(stripos($testMessagePlain, '&#') === false); - list($to, $subjectEncoded, $fullBody, $headersEncoded, $bounceAddress) = $mailer->sendHTML( - '', - 'tom@jones ', - "What is the of testing?", - $testMessageHTML, - null, - array('CC' => 'admin@silverstripe.com', 'bcc' => 'andrew@thing.com') - ); - - $this->assertEquals('email@silverstripe.com', $to); - $this->assertEquals('=?UTF-8?B?V2hhdCBpcyB0aGUgPHB1cnBvc2U+IG9mIHRlc3Rpbmc/?=', $subjectEncoded); - $this->assertEquals('=?UTF-8?B?'. base64_encode('What is the of testing?').'?=', $subjectEncoded); - - $this->assertEquals( - Convert::nl2os( - <<=0A=0A=0A=0A=0A=0A=0A

The majority of the answers so far are saying that priva= -te methods are implementation details which don't (or at least shouldn't) matter so long as the public i= -nterface is well-tested & working

That's absolutely correct = -if your only purpose for testing is to guarantee that the public interface = -works.

=0A=0A -------=_NextPart_000000000000-- -PHP - ), - Convert::nl2os($this->normaliseDivisions($fullBody)) - ); - // Check that the messages exist in the output - $this->assertTrue(stripos($fullBody, quoted_printable_encode($testMessagePlain)) !== false); - $this->assertEquals( - << -X-Mailer: SilverStripe Mailer - version 2006.06.21 -X-Priority: 3 -Bcc: andrew@thing.com -Cc: admin@silverstripe.com - -PHP - , - Convert::nl2os($this->normaliseDivisions($headersEncoded)) - ); - $this->assertEquals('tom@silverstripe.com', $bounceAddress); - - // Test override bounce email and alternate encoding - $mailer->setBounceEmail('bounce@silverstripe.com'); - $mailer->setMessageEncoding('base64'); - list($to, $subjectEncoded, $fullBody, $headersEncoded, $bounceAddress) = $mailer->sendHTML( - '', - 'tom@jones ', - "What is the of testing?", - $testMessageHTML, - null, - array('CC' => 'admin@silverstripe.com', 'bcc' => 'andrew@thing.com') - ); - - $this->assertEquals('bounce@silverstripe.com', $bounceAddress); - $this->assertEquals( - <<normaliseDivisions($fullBody)) - ); - - // Check that the text message version is somewhere in there - $this->assertTrue(stripos($fullBody, chunk_split(base64_encode($testMessagePlain), 60)) !== false); - } -} diff --git a/tests/php/Control/Email/MailerTest/MockMailer.php b/tests/php/Control/Email/MailerTest/MockMailer.php deleted file mode 100644 index 858582403..000000000 --- a/tests/php/Control/Email/MailerTest/MockMailer.php +++ /dev/null @@ -1,17 +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', array(Swift_Plugins_AntiFloodPlugin::class)); + + /** @var Swift_MailTransport $transport */ + $transport = $this->getMockBuilder(Swift_MailTransport::class)->getMock(); + $transport + ->expects($this->once()) + ->method('registerPlugin') + ->willReturnCallback(function ($plugin) { + $this->assertInstanceOf(Swift_Plugins_AntiFloodPlugin::class, $plugin); + }); + + /** @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->getMock(SwiftMailer::class, array('sendSwift')); + $mailer->expects($this->once())->method('sendSwift')->willReturnCallback(function ($message) { + $this->assertInstanceOf(Swift_Message::class, $message); + }); + + $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 new file mode 100644 index 000000000..348b742e7 --- /dev/null +++ b/tests/php/Control/Email/SwiftPluginTest.php @@ -0,0 +1,110 @@ +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())); + } +}