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
This commit is contained in:
Daniel Hensby 2017-01-13 01:48:46 +00:00 committed by Sam Minnée
parent e6ae532998
commit 3ea5015f8b
21 changed files with 1589 additions and 1758 deletions

View File

@ -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

12
_config/email.yml Normal file
View File

@ -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'

View File

@ -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",

View File

@ -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();
<div class="info" markdown="1">
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.
</div>
@ -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
}
```
<div class="alert" markdown="1">
As we've added a new template file (`MyCustomEmail`) make sure you clear the SilverStripe cache for your changes to
take affect.
</div>
## 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
<?php
class MyEmail extends Email {
protected $ss_template = "MyEmail";
public function __construct($member) {
$from = 'no-reply@mysite.com';
$to = $member->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
<?php
$member = Member::currentUser();
$email = new MyEmail($member);
$email->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');
..
<div class="info" markdown="1">
See this [Wikipedia](http://en.wikipedia.org/wiki/E-mail#Message_header) entry for a list of header names.
</div>
## 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.
<div class="info" markdown="1">
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).
</div>
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
<?php
class LocalMailer extends Mailer {
function sendHTML($to, $from, $subject, $htmlContent, $attachedFiles = false, $customheaders = false, $plainContent = false, $inlineImages = false) {
$file = ASSETS_PATH . '/_mail_'. urlencode(sprintf("%s_%s", $subject, $to));
file_put_contents($file, $htmlContent);
}
function sendPlain($to, $from, $subject, $htmlContent, $attachedFiles = false, $customheaders = false, $plainContent = false, $inlineImages = false) {
$file = ASSETS_PATH . '/_mail_'. urlencode(sprintf("%s_%s", $subject, $to));
file_put_contents($file, $htmlContent);
}
}
**mysite/_config.php**
:::php
if(Director::isLive()) {
Email::set_mailer(new PostmarkMailer());
} else {
Email::set_mailer(new LocalMailer());
}
### Setting bounce handler
A bounce handler email can be specified one of a few ways:
* Via config by setting the `Mailer.default_bounce_email` config to the desired email address.
* Via _ss_environment.php by setting the `BOUNCE_EMAIL` definition.
* Via PHP by calling `Email::mailer()->setBounceEmail('bounce@mycompany.com');`
For further information on SwiftMailer, consult their docs: http://swiftmailer.org/docs/introduction.html
## API Documentation

View File

@ -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 <cal@iamcal.com>
* This code is licensed under a Creative Commons Attribution-ShareAlike 2.5 License
* http://creativecommons.org/licenses/by-sa/2.5/
*/
public static function is_valid_address($address)
{
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 '<span class="codedirection">' . strrev($email) . '</span>';
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 "<h2>Email template $this->class</h2>\n" .
"<p><b>From:</b> $this->from\n" .
"<b>To:</b> $this->to\n" .
"<b>Cc:</b> $this->cc\n" .
"<b>Bcc:</b> $this->bcc\n" .
"<b>Subject:</b> $this->subject</p>" .
$this->body;
return "<h2>Email template {$this->class}:</h2>\n" . '<pre>' . $this->getSwiftMessage()->toString() . '</pre>';
}
/**
* 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 <cal@iamcal.com>
* This code is licensed under a Creative Commons Attribution-ShareAlike 2.5 License
* http://creativecommons.org/licenses/by-sa/2.5/
*/
public static function is_valid_address($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 '<span class="codedirection">' . strrev($email) . '</span>';
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;
}
}

View File

@ -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, '<body') === false) {
$htmlContent =
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">\n" .
"<HTML><HEAD>\n" .
"<META http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n" .
"<STYLE type=\"text/css\"></STYLE>\n\n".
"</HEAD>\n" .
"<BODY bgColor=\"#ffffff\">\n" .
$htmlContent .
"\n</BODY>\n" .
"</HTML>";
}
// 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:
*
* <img src="http://example.com/image.gif" />
*
* 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 <email@silverstripe.com>' 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);
}

View File

@ -0,0 +1,80 @@
<?php
namespace SilverStripe\Control\Email;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Core\Injector\Injector;
use Swift_Mailer;
use Swift_Message;
/**
* Mailer objects are responsible for actually sending emails.
* The default Mailer class will use PHP's mail() function.
*/
class SwiftMailer implements Mailer
{
use Configurable;
use Injectable;
/**
* @var array
* @config
*/
private static $swift_plugins = array(
SwiftPlugin::class,
);
/**
* @var Swift_Mailer
*/
private $swift;
/**
* @param Email $message
* @return bool Whether the sending was "successful" or not
*/
public function send($message)
{
$swiftMessage = $message->getSwiftMessage();
$failedRecipients = 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;
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace SilverStripe\Control\Email;
class SwiftPlugin implements \Swift_Events_SendListener
{
/**
* Before sending a message make sure all our overrides are taken into account
*
* @param \Swift_Events_SendEvent $evt
*/
public function beforeSendPerformed(\Swift_Events_SendEvent $evt)
{
/** @var \Swift_Message $message */
$message = $evt->getMessage();
$sendAllTo = Email::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
}
}

View File

@ -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');
}
/**

View File

@ -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;
}
}

View File

@ -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,

View File

@ -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']) {

View File

@ -6,7 +6,7 @@
<body>
<div class="body">
$Body
$EmailContent
</div>
</body>

View File

@ -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('<span class="codedirection">moc.elpmaxe@1-tset</span>', $direction);
$this->assertEquals('test [dash] 1 [at] example [dot] com', $visible);
$this->assertEquals(
'<span class="codedirection">moc.elpmaxe@1-tset</span>',
$direction,
'obfuscate() correctly reverses the email direction'
);
$this->assertEquals(
'test [dash] 1 [at] example [dot] com',
$visible,
'obfuscate() correctly obfuscates email characters'
);
$this->assertEquals(
'&#x74;&#x65;&#x73;&#x74;&#x2d;&#x31;&#x40;&#x65;&#x78;&#x61;&#x6d;&#x70;'
. '&#x6c;&#x65;&#x2e;&#x63;&#x6f;&#x6d;',
$hex,
'obfuscate() correctly returns hex representation of email'
'&#x74;&#x65;&#x73;&#x74;&#x2d;&#x31;&#x40;&#x65;&#x78;&#x61;&#x6d;&#x70;&#x6c;&#x65;&#x2e;&#x63;&#x6f;&#x6d;',
$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-&gt;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('<h1>Title</h1>');
$this->assertEquals('<h1>Title</h1>', $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('<h1>Test</h1>');
$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('<h1>Test</h1>', $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());
}
}

View File

@ -1,41 +0,0 @@
<?php
namespace SilverStripe\Control\Tests\Email\EmailTest;
use SilverStripe\Control\Email\Mailer;
class TestMailer extends Mailer
{
public function sendHTML(
$to,
$from,
$subject,
$htmlContent,
$attachedFiles = false,
$customheaders = false,
$plainContent = false
) {
return array(
'to' => $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
);
}
}

View File

@ -1,246 +0,0 @@
<?php
namespace SilverStripe\Control\Tests\Email;
use SilverStripe\Core\Convert;
use SilverStripe\Dev\SapphireTest;
class MailerTest extends SapphireTest
{
/**
* Replaces ----=_NextPart_214491627619 placeholders with ----=_NextPart_000000000000
*
* @param string $input
* @return string
*/
protected function normaliseDivisions($input)
{
return preg_replace('/----=_NextPart_\d+/m', '----=_NextPart_000000000000', $input);
}
/**
* Test plain text messages
*/
public function testSendPlain()
{
$mailer = new MailerTest\MockMailer();
// Test with default encoding
$testMessage = "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 and ".
"working. That's absolutely correct if your only purpose for testing is to guarantee that the ".
"public interface works.";
list($to, $subjectEncoded, $fullBody, $headersEncoded, $bounceAddress) = $mailer->sendPlain(
'<email@silverstripe.com>',
'tom@jones <tom@silverstripe.com>',
"What is the <purpose> 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 <purpose> of testing?').'?=', $subjectEncoded);
$this->assertEquals(
<<<PHP
The majority of the answers so far are saying that private methods are impl=
ementation details which don't (or at least shouldn't) matter so long as th=
e public interface is well-tested and working. That's absolutely correct if=
your only purpose for testing is to guarantee that the public interface wo=
rks.
PHP
,
Convert::nl2os($fullBody)
);
$this->assertEquals($testMessage, quoted_printable_decode($fullBody));
$this->assertEquals(
<<<PHP
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
From: tomjones <tom@silverstripe.com>
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(
'<email@silverstripe.com>',
'tom@jones <tom@silverstripe.com>',
"What is the <purpose> of testing?",
$testMessage,
null,
array('CC' => 'admin@silverstripe.com', 'bcc' => 'andrew@thing.com')
);
$this->assertEquals('bounce@silverstripe.com', $bounceAddress);
$this->assertEquals(
<<<PHP
VGhlIG1ham9yaXR5IG9mIHRoZSBhbnN3ZXJzIHNvIGZhciBhcmUgc2F5aW5n
IHRoYXQgcHJpdmF0ZSBtZXRob2RzIGFyZSBpbXBsZW1lbnRhdGlvbiBkZXRh
aWxzIHdoaWNoIGRvbid0IChvciBhdCBsZWFzdCBzaG91bGRuJ3QpIG1hdHRl
ciBzbyBsb25nIGFzIHRoZSBwdWJsaWMgaW50ZXJmYWNlIGlzIHdlbGwtdGVz
dGVkIGFuZCB3b3JraW5nLiBUaGF0J3MgYWJzb2x1dGVseSBjb3JyZWN0IGlm
IHlvdXIgb25seSBwdXJwb3NlIGZvciB0ZXN0aW5nIGlzIHRvIGd1YXJhbnRl
ZSB0aGF0IHRoZSBwdWJsaWMgaW50ZXJmYWNlIHdvcmtzLg==
PHP
,
Convert::nl2os($fullBody)
);
$this->assertEquals($testMessage, base64_decode($fullBody));
}
/**
* Test HTML messages
*/
public function testSendHTML()
{
$mailer = new MailerTest\MockMailer();
// Test with default encoding
$testMessageHTML = "<p>The majority of the <i>answers</i> so far are saying that private methods are " .
"implementation details which don&#39;t (<a href=\"http://www.google.com\">or at least shouldn&#39;t</a>) ".
"matter so long as the public interface is well-tested &amp; working</p> ".
"<p>That&#39;s absolutely correct if your only purpose for testing is to guarantee that the ".
"public interface works.</p>";
$testMessagePlain = Convert::xml2raw($testMessageHTML);
$this->assertTrue(stripos($testMessagePlain, '&#') === false);
list($to, $subjectEncoded, $fullBody, $headersEncoded, $bounceAddress) = $mailer->sendHTML(
'<email@silverstripe.com>',
'tom@jones <tom@silverstripe.com>',
"What is the <purpose> 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 <purpose> of testing?').'?=', $subjectEncoded);
$this->assertEquals(
Convert::nl2os(
<<<PHP
This is a multi-part message in MIME format.
------=_NextPart_000000000000
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
The majority of the answers so far are saying that private methods are impl=
ementation details which don't (or at least shouldn't[http://www.google.com=
]) matter so long as the public interface is well-tested & working=0A=0A=0A=
=0AThat's absolutely correct if your only purpose for testing is to guarant=
ee that the public interface works.
------=_NextPart_000000000000
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">=0A<HTML><HEA=
D>=0A<META http-equiv=3D"Content-Type" content=3D"text/html; charset=3Dutf-=
8">=0A<STYLE type=3D"text/css"></STYLE>=0A=0A</HEAD>=0A<BODY bgColor=3D"#ff=
ffff">=0A<p>The majority of the <i>answers</i> so far are saying that priva=
te methods are implementation details which don&#39;t (<a href=3D"http://ww=
w.google.com">or at least shouldn&#39;t</a>) matter so long as the public i=
nterface is well-tested &amp; working</p> <p>That&#39;s absolutely correct =
if your only purpose for testing is to guarantee that the public interface =
works.</p>=0A</BODY>=0A</HTML>
------=_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(
<<<PHP
MIME-Version: 1.0
Content-Type: multipart/alternative; boundary="----=_NextPart_000000000000"
Content-Transfer-Encoding: 7bit
From: tomjones <tom@silverstripe.com>
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(
'<email@silverstripe.com>',
'tom@jones <tom@silverstripe.com>',
"What is the <purpose> of testing?",
$testMessageHTML,
null,
array('CC' => 'admin@silverstripe.com', 'bcc' => 'andrew@thing.com')
);
$this->assertEquals('bounce@silverstripe.com', $bounceAddress);
$this->assertEquals(
<<<PHP
This is a multi-part message in MIME format.
------=_NextPart_000000000000
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: base64
VGhlIG1ham9yaXR5IG9mIHRoZSBhbnN3ZXJzIHNvIGZhciBhcmUgc2F5aW5n
IHRoYXQgcHJpdmF0ZSBtZXRob2RzIGFyZSBpbXBsZW1lbnRhdGlvbiBkZXRh
aWxzIHdoaWNoIGRvbid0IChvciBhdCBsZWFzdCBzaG91bGRuJ3RbaHR0cDov
L3d3dy5nb29nbGUuY29tXSkgbWF0dGVyIHNvIGxvbmcgYXMgdGhlIHB1Ymxp
YyBpbnRlcmZhY2UgaXMgd2VsbC10ZXN0ZWQgJiB3b3JraW5nCgoKClRoYXQn
cyBhYnNvbHV0ZWx5IGNvcnJlY3QgaWYgeW91ciBvbmx5IHB1cnBvc2UgZm9y
IHRlc3RpbmcgaXMgdG8gZ3VhcmFudGVlIHRoYXQgdGhlIHB1YmxpYyBpbnRl
cmZhY2Ugd29ya3Mu
------=_NextPart_000000000000
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: base64
PCFET0NUWVBFIEhUTUwgUFVCTElDICItLy9XM0MvL0RURCBIVE1MIDQuMCBU
cmFuc2l0aW9uYWwvL0VOIj4KPEhUTUw+PEhFQUQ+CjxNRVRBIGh0dHAtZXF1
aXY9IkNvbnRlbnQtVHlwZSIgY29udGVudD0idGV4dC9odG1sOyBjaGFyc2V0
PXV0Zi04Ij4KPFNUWUxFIHR5cGU9InRleHQvY3NzIj48L1NUWUxFPgoKPC9I
RUFEPgo8Qk9EWSBiZ0NvbG9yPSIjZmZmZmZmIj4KPHA+VGhlIG1ham9yaXR5
IG9mIHRoZSA8aT5hbnN3ZXJzPC9pPiBzbyBmYXIgYXJlIHNheWluZyB0aGF0
IHByaXZhdGUgbWV0aG9kcyBhcmUgaW1wbGVtZW50YXRpb24gZGV0YWlscyB3
aGljaCBkb24mIzM5O3QgKDxhIGhyZWY9Imh0dHA6Ly93d3cuZ29vZ2xlLmNv
bSI+b3IgYXQgbGVhc3Qgc2hvdWxkbiYjMzk7dDwvYT4pIG1hdHRlciBzbyBs
b25nIGFzIHRoZSBwdWJsaWMgaW50ZXJmYWNlIGlzIHdlbGwtdGVzdGVkICZh
bXA7IHdvcmtpbmc8L3A+IDxwPlRoYXQmIzM5O3MgYWJzb2x1dGVseSBjb3Jy
ZWN0IGlmIHlvdXIgb25seSBwdXJwb3NlIGZvciB0ZXN0aW5nIGlzIHRvIGd1
YXJhbnRlZSB0aGF0IHRoZSBwdWJsaWMgaW50ZXJmYWNlIHdvcmtzLjwvcD4K
PC9CT0RZPgo8L0hUTUw+
------=_NextPart_000000000000--
PHP
,
Convert::nl2os($this->normaliseDivisions($fullBody))
);
// Check that the text message version is somewhere in there
$this->assertTrue(stripos($fullBody, chunk_split(base64_encode($testMessagePlain), 60)) !== false);
}
}

View File

@ -1,17 +0,0 @@
<?php
namespace SilverStripe\Control\Tests\Email\MailerTest;
use SilverStripe\Control\Email\Mailer;
use SilverStripe\Dev\TestOnly;
/**
* Mocks the sending of emails without actually sending anything
*/
class MockMailer extends Mailer implements TestOnly
{
protected function email($to, $subjectEncoded, $fullBody, $headersEncoded, $bounceAddress)
{
return array($to, $subjectEncoded, $fullBody, $headersEncoded, $bounceAddress);
}
}

View File

@ -0,0 +1,76 @@
<?php
namespace SilverStripe\Control\Tests\Email;
use SilverStripe\Control\Email\Email;
use SilverStripe\Control\Email\SwiftMailer;
use SilverStripe\Dev\SapphireTest;
use Swift_Mailer;
use Swift_MailTransport;
use Swift_Message;
use Swift_NullTransport;
use Swift_Plugins_AntiFloodPlugin;
class SwiftMailerTest extends SapphireTest
{
public function testSwiftMailer()
{
$mailer = new SwiftMailer();
$mailer->setSwiftMailer($swift = new Swift_Mailer(new Swift_NullTransport()));
$this->assertEquals($swift, $mailer->getSwiftMailer());
SwiftMailer::config()->remove('swift_plugins');
SwiftMailer::config()->update('swift_plugins', 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);
}
}

View File

@ -0,0 +1,110 @@
<?php
namespace SilverStripe\Control\Tests\Email;
use SilverStripe\Control\Email\Email;
use SilverStripe\Control\Email\SwiftPlugin;
use SilverStripe\Dev\SapphireTest;
class SwiftPluginTest extends SapphireTest
{
public function setUp()
{
parent::setUp();
//clean the config
Email::config()->remove('send_all_emails_to');
Email::config()->remove('cc_all_emails_to');
Email::config()->remove('bcc_all_emails_to');
Email::config()->remove('send_all_emails_from');
}
protected function getEmail()
{
return (new Email())
->setTo('original-to@example.com')
->setCC('original-cc@example.com')
->setBCC('original-bcc@example.com')
->setFrom('original-from@example.com');
}
protected function getMailer()
{
$mailer = new \Swift_Mailer(new \Swift_NullTransport());
$mailer->registerPlugin(new SwiftPlugin());
return $mailer;
}
public function testSendAllEmailsTo()
{
Email::config()->update('send_all_emails_to', 'to@example.com');
$email = $this->getEmail();
$this->getMailer()->send($email->getSwiftMessage());
$headers = $email->getSwiftMessage()->getHeaders();
$this->assertCount(1, $email->getTo());
$this->assertContains('to@example.com', array_keys($email->getTo()));
$this->assertCount(1, $email->getFrom());
$this->assertContains('original-from@example.com', array_keys($email->getFrom()));
$this->assertTrue($headers->has('X-Original-To'));
$this->assertTrue($headers->has('X-Original-Cc'));
$this->assertTrue($headers->has('X-Original-Bcc'));
$this->assertFalse($headers->has('X-Original-From'));
$originalTo = array_keys($headers->get('X-Original-To')->getFieldBodyModel());
$originalCc = array_keys($headers->get('X-Original-Cc')->getFieldBodyModel());
$originalBcc = array_keys($headers->get('X-Original-Bcc')->getFieldBodyModel());
$this->assertCount(1, $originalTo);
$this->assertContains('original-to@example.com', $originalTo);
$this->assertCount(1, $originalCc);
$this->assertContains('original-cc@example.com', $originalCc);
$this->assertCount(1, $originalBcc);
$this->assertContains('original-bcc@example.com', $originalBcc);
}
public function testSendAllEmailsFrom()
{
Email::config()->update('send_all_emails_from', 'from@example.com');
$email = $this->getEmail();
$this->getMailer()->send($email->getSwiftMessage());
$headers = $email->getSwiftMessage()->getHeaders();
$this->assertFalse($headers->has('X-Original-To'));
$this->assertFalse($headers->has('X-Original-Cc'));
$this->assertFalse($headers->has('X-Original-Bcc'));
$this->assertTrue($headers->has('X-Original-From'));
$this->assertCount(1, $email->getFrom());
$this->assertContains('from@example.com', array_keys($email->getFrom()));
$this->assertCount(1, $headers->get('X-Original-From')->getFieldBodyModel());
$this->assertContains('original-from@example.com', array_keys($headers->get('X-Original-From')->getFieldBodyModel()));
}
public function testCCAllEmailsTo()
{
Email::config()->update('cc_all_emails_to', 'cc@example.com');
$email = $this->getEmail();
$this->getMailer()->send($email->getSwiftMessage());
$this->assertCount(2, $email->getCC());
$this->assertContains('cc@example.com', array_keys($email->getCC()));
$this->assertContains('original-cc@example.com', array_keys($email->getCC()));
}
public function testBCCAllEmailsTo()
{
Email::config()->update('bcc_all_emails_to', 'bcc@example.com');
$email = $this->getEmail();
$this->getMailer()->send($email->getSwiftMessage());
$this->assertCount(2, $email->getBCC());
$this->assertContains('bcc@example.com', array_keys($email->getBCC()));
$this->assertContains('original-bcc@example.com', array_keys($email->getBCC()));
}
}