API Mailer can be configured to use different encoding mechanisms, and added support for unicode quoted-string encoding

API Mailer bounce email can now be configured
API Mailer no longer calls Convert::xml2raw on all email subjects
API Deprecate dead Mailer code and refactored duplicate or mis-documented code.
This commit is contained in:
Damian Mooyman 2014-09-25 16:04:56 +12:00
parent 29e3347562
commit e47800917a
5 changed files with 567 additions and 268 deletions

View File

@ -14,6 +14,7 @@
tables actually exist.
* `SS_Filterable`, `SS_Limitable` and `SS_Sortable` now explicitly extend `SS_List`
* `Convert::html2raw` no longer wraps text by default and can decode single quotes.
* `Mailer` no longer calls `xml2raw` on all email subject line, and now must be passed in via plain text.
#### Deprecated classes/methods removed

View File

@ -116,6 +116,14 @@ See [Wikipedia E-mail Message header](http://en.wikipedia.org/wiki/E-mail#Messag
The [newsletter module](http://silverstripe.org/newsletter-module) provides a UI and logic to send batch emails.
### 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');`
## API Documentation
`[api:Email]`

View File

@ -62,17 +62,11 @@ class Email extends ViewableData {
protected $bcc;
/**
* @param Mailer $mailer Instance of a {@link Mailer} class.
*/
protected static $mailer;
/**
* This can be used to provide a mailer class other than the default, e.g. for testing.
*
* @param Mailer $mailer
* @deprecated since version 3.3
*/
public static function set_mailer(Mailer $mailer) {
self::$mailer = $mailer;
Deprecation::notice('3.3.0', 'Use Injector to override the Mailer service');
Injector::inst()->registerService($mailer, 'Mailer');
}
/**
@ -81,8 +75,7 @@ class Email extends ViewableData {
* @return Mailer
*/
public static function mailer() {
if(!self::$mailer) self::$mailer = new Mailer();
return self::$mailer;
return Injector::inst()->get('Mailer');
}
/**

View File

@ -8,143 +8,289 @@
* @subpackage email
*/
class Mailer extends Object {
/**
* Default encoding type for messages. Available options are:
* - quoted-printable
* - base64
*
* @var string
* @config
*/
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
* @param string $from
* @param string §subject
* @param string $plainContent
* @param bool $attachedFiles
* @param array $customheaders
* @return bool
* @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 = false, $customheaders = false) {
// Not ensurely where this is supposed to be set, but defined it false for now to remove php notices
$plainEncoding = false;
if ($customheaders && is_array($customheaders) == false) {
echo "htmlEmail($to, $from, $subject, ...) could not send mail: improper \$customheaders passed:<BR>";
dieprintr($customheaders);
}
// If the subject line contains extended characters, we must encode it
$subject = Convert::xml2raw($subject);
$subject = "=?UTF-8?B?" . base64_encode($subject) . "?=";
// Make the plain text part
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"] = $plainEncoding ? $plainEncoding : "quoted-printable";
$headers["Content-Transfer-Encoding"] = $this->getMessageEncoding();
$plainContent = ($plainEncoding == "base64")
? chunk_split(base64_encode($plainContent),60)
: $this->QuotedPrintable_encode($plainContent);
// 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) {
// The first part is the message itself
$fullMessage = $this->processHeaders($headers, $plainContent);
$messageParts = array($fullMessage);
// Include any specified attachments as additional parts
foreach($attachedFiles 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
list($fullBody, $headers) = $this->encodeMultipart($messageParts, "multipart/mixed");
// Messages without attachments do not require such treatment
} else {
$fullBody = $plainContent;
}
// Email headers
$headers["From"] = $this->validEmailAddr($from);
// Messages with the X-SilverStripeMessageID header can be tracked
if(isset($customheaders["X-SilverStripeMessageID"]) && defined('BOUNCE_EMAIL')) {
$bounceAddress = BOUNCE_EMAIL;
// Get the human name from the from address, if there is one
if(preg_match('/^([^<>]+)<([^<>])> *$/', $from, $parts))
$bounceAddress = "$parts[1]<$bounceAddress>";
} else {
$bounceAddress = $from;
list($fullBody, $headers) = $this->encodeAttachments($attachedFiles, $headers, $fullBody);
}
// $headers["Sender"] = $from;
$headers["X-Mailer"] = X_MAILER;
if(!isset($customheaders["X-Priority"])) {
$headers["X-Priority"] = 3;
}
$headers = array_merge((array)$headers, (array)$customheaders);
// Get bounce email
$bounceAddress = $this->getBounceEmail() ?: $from;
if(preg_match('/^([^<>]*)<([^<>]+)> *$/', $bounceAddress, $parts)) $bounceAddress = $parts[2];
// the carbon copy header has to be 'Cc', not 'CC' or 'cc' -- ensure this.
if (isset($headers['CC'])) { $headers['Cc'] = $headers['CC']; unset($headers['CC']); }
if (isset($headers['cc'])) { $headers['Cc'] = $headers['cc']; unset($headers['cc']); }
// Send the email
$headers = $this->processHeaders($headers);
$to = $this->validEmailAddr($to);
// 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
if(!$result = @mail($to, $subject, $fullBody, $headers, "-f$bounceAddress"))
$result = mail($to, $subject, $fullBody, $headers);
$result = @mail($to, $subjectEncoded, $fullBody, $headersEncoded, escapeshellarg("-f$bounceAddress"));
if(!$result) {
$result = mail($to, $subjectEncoded, $fullBody, $headersEncoded);
}
if($result)
return array($to,$subject,$fullBody,$headers);
if($result) {
return array($to, $subjectEncoded, $fullBody, $headersEncoded, $bounceAddress);
}
return false;
}
/**
* Sends an email as a both HTML and plaintext
*
* $attachedFiles should be an array of file names
* - if you pass the entire $_FILES entry, the user-uploaded filename will be preserved
* use $plainContent to override default plain-content generation
*
* @return bool
* Encode attachments into a message
*
* @param array $attachments
* @param array $headers
* @param string $body
* @return array Array containing completed body followed by headers
*/
public function sendHTML($to, $from, $subject, $htmlContent, $attachedFiles = false, $customheaders = false,
$plainContent = false) {
protected function encodeAttachments($attachments, $headers, $body) {
// The first part is the message itself
$fullMessage = $this->processHeaders($headers, $body);
$messageParts = array($fullMessage);
if ($customheaders && is_array($customheaders) == false) {
echo "htmlEmail($to, $from, $subject, ...) could not send mail: improper \$customheaders passed:<BR>";
dieprintr($customheaders);
// 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);
}
}
$bodyIsUnicode = (strpos($htmlContent,"&#") !== false);
$plainEncoding = "";
// 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();
// We generate plaintext content by default, but you can pass custom stuff
$plainEncoding = '';
if(!$plainContent) {
$plainContent = Convert::xml2raw($htmlContent);
if(isset($bodyIsUnicode) && $bodyIsUnicode) $plainEncoding = "base64";
}
// If the subject line contains extended characters, we must encode the
$subject = Convert::xml2raw($subject);
$subject = "=?UTF-8?B?" . base64_encode($subject) . "?=";
// 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 ? $plainEncoding : "quoted-printable";
$headers["Content-Transfer-Encoding"] = $plainEncoding;
$plainContentEncoded = $this->encodeMessage($plainContent, $plainEncoding);
$plainPart = $this->processHeaders($headers, ($plainEncoding == "base64")
? chunk_split(base64_encode($plainContent),60)
: wordwrap($this->QuotedPrintable_encode($plainContent),75));
// Merge with headers
return $this->processHeaders($headers, $plainContentEncoded);
}
// Make the HTML part
$headers["Content-Type"] = "text/html; charset=utf-8";
/**
* 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 =
@ -158,84 +304,25 @@ class Mailer extends Object {
"\n</BODY>\n" .
"</HTML>";
}
$headers["Content-Transfer-Encoding"] = "quoted-printable";
$htmlPart = $this->processHeaders($headers, wordwrap($this->QuotedPrintable_encode($htmlContent),75));
list($messageBody, $messageHeaders) = $this->encodeMultipart(
array($plainPart,$htmlPart),
"multipart/alternative"
);
// Messages with attachments are handled differently
if($attachedFiles && is_array($attachedFiles)) {
// The first part is the message itself
$fullMessage = $this->processHeaders($messageHeaders, $messageBody);
$messageParts = array($fullMessage);
// Include any specified attachments as additional parts
foreach($attachedFiles 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
list($fullBody, $headers) = $this->encodeMultipart($messageParts, "multipart/mixed");
// Messages without attachments do not require such treatment
} else {
$headers = $messageHeaders;
$fullBody = $messageBody;
}
// Email headers
$headers["From"] = $this->validEmailAddr($from);
// Messages with the X-SilverStripeMessageID header can be tracked
if(isset($customheaders["X-SilverStripeMessageID"]) && defined('BOUNCE_EMAIL')) {
$bounceAddress = BOUNCE_EMAIL;
} else {
$bounceAddress = $from;
}
// Strip the human name from the bounce address
if(preg_match('/^([^<>]*)<([^<>]+)> *$/', $bounceAddress, $parts)) $bounceAddress = $parts[2];
// $headers["Sender"] = $from;
$headers["X-Mailer"] = X_MAILER;
if (!isset($customheaders["X-Priority"])) $headers["X-Priority"] = 3;
$headers = array_merge((array)$headers, (array)$customheaders);
// Make the HTML part
$headers["Content-Type"] = "text/html; charset=utf-8";
$headers["Content-Transfer-Encoding"] = $this->getMessageEncoding();
$htmlContentEncoded = $this->encodeMessage($htmlContent, $this->getMessageEncoding());
// the carbon copy header has to be 'Cc', not 'CC' or 'cc' -- ensure this.
if (isset($headers['CC'])) { $headers['Cc'] = $headers['CC']; unset($headers['CC']); }
if (isset($headers['cc'])) { $headers['Cc'] = $headers['cc']; unset($headers['cc']); }
// the carbon copy header has to be 'Bcc', not 'BCC' or 'bcc' -- ensure this.
if (isset($headers['BCC'])) {$headers['Bcc']=$headers['BCC']; unset($headers['BCC']); }
if (isset($headers['bcc'])) {$headers['Bcc']=$headers['bcc']; unset($headers['bcc']); }
// Send the email
$headers = $this->processHeaders($headers);
$to = $this->validEmailAddr($to);
// Try it without the -f option if it fails
if(!$bounceAddress ||
!($result = @mail($to, $subject, $fullBody, $headers, escapeshellarg("-f$bounceAddress")))) {
$result = mail($to, $subject, $fullBody, $headers);
}
return $result;
// Merge with headers
return $this->processHeaders($headers, $htmlContentEncoded);
}
/**
* @todo Make visibility protected in 3.2
/**
* 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
*/
function encodeMultipart($parts, $contentType, $headers = false) {
protected function encodeMultipart($parts, $contentType, $headers = array()) {
$separator = "----=_NextPart_" . preg_replace('/[^0-9]/', '', rand() * 10000000000);
$headers["MIME-Version"] = "1.0";
@ -259,19 +346,22 @@ class Mailer extends Object {
return array($body, $headers);
}
/**
* @todo Make visibility protected in 3.2
*/
function processHeaders($headers, $body = false) {
$res = '';
if(is_array($headers)) {
while(list($k, $v) = each($headers)) {
$res .= "$k: $v\n";
}
}
if($body) $res .= "\n$body";
return $res;
/**
* 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;
}
/**
@ -313,10 +403,8 @@ class Mailer extends Object {
* 'mimetype' => 'image/gif',
* 'contentLocation' => Director::absoluteBaseURL() . "/themes/mytheme/images/header.gif"
* );
*
* @todo Make visibility protected in 3.2
*/
function encodeFileForEmail($file, $destFileName = false, $disposition = NULL, $extraHeaders = "") {
protected function encodeFileForEmail($file, $destFileName = false, $disposition = NULL, $extraHeaders = "") {
if(!$file) {
user_error("encodeFileForEmail: not passed a filename and/or data", E_USER_WARNING);
return;
@ -348,7 +436,7 @@ class Mailer extends Object {
// This mime type is needed, otherwise some clients will show it as an inline attachment
$mimeType = 'application/octet-stream';
$encoding = "quoted-printable";
$file['contents'] = $this->QuotedPrintable_encode($file['contents']);
$file['contents'] = quoted_printable_encode($file['contents']);
}
$headers = "Content-type: $mimeType;\n\tname=\"$base\"\n".
@ -364,76 +452,42 @@ class Mailer extends Object {
}
/**
* @todo Make visibility protected in 3.2
* Cleans up emails which may be in 'Name <email@silverstripe.com>' format
*
* @param string $emailAddress
* @return string
*/
function QuotedPrintable_encode($quotprint) {
$quotprint = (string)str_replace('\r\n',chr(13).chr(10),$quotprint);
$quotprint = (string)str_replace('\n', chr(13).chr(10),$quotprint);
$quotprint = (string)preg_replace_callback("~([\x01-\x1F\x3D\x7F-\xFF])~", function($matches) {
return sprintf('=%02X', ord($matches[1]));
}, $quotprint);
//$quotprint = (string)str_replace('\=0D=0A',"=0D=0A",$quotprint);
$quotprint = (string)str_replace('=0D=0A',"\n",$quotprint);
$quotprint = (string)str_replace('=0A=0D',"\n",$quotprint);
$quotprint = (string)str_replace('=0D',"\n",$quotprint);
$quotprint = (string)str_replace('=0A',"\n",$quotprint);
return (string) $quotprint;
}
/**
* @todo Make visibility protected in 3.2
*/
function validEmailAddr($emailAddress) {
protected function validEmailAddress($emailAddress) {
$emailAddress = trim($emailAddress);
$angBrack = strpos($emailAddress, '<');
if($angBrack === 0) {
$emailAddress = substr($emailAddress, 1, strpos($emailAddress,'>')-1);
} else if($angBrack) {
$emailAddress = str_replace('@', '', substr($emailAddress, 0, $angBrack))
. substr($emailAddress, $angBrack);
$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;
}
/*
* Return a multipart/related e-mail chunk for the given HTML message and its linked images
* Decodes absolute URLs, accessing the appropriate local images
/**
* @deprecated since version 3.2.0
*/
function wrapImagesInline($htmlContent) {
global $_INLINED_IMAGES;
$_INLINED_IMAGES = null;
$replacedContent = imageRewriter($htmlContent, 'wrapImagesInline_rewriter($URL)');
// Make the HTML part
$headers["Content-Type"] = "text/html; charset=utf-8";
$headers["Content-Transfer-Encoding"] = "quoted-printable";
$multiparts[] = processHeaders($headers, QuotedPrintable_encode($replacedContent));
// Make all the image parts
global $_INLINED_IMAGES;
foreach($_INLINED_IMAGES as $url => $cid) {
$multiparts[] = encodeFileForEmail($url, false, "inline", "Content-ID: <$cid>\n");
}
// Merge together in a multipart
list($body, $headers) = encodeMultipart($multiparts, "multipart/related");
return processHeaders($headers, $body);
public function wrapImagesInline($htmlContent) {
Deprecation::notice('3.2.0', 'wrapImagesInline is deprecated');
}
function wrapImagesInline_rewriter($url) {
$url = relativiseURL($url);
global $_INLINED_IMAGES;
if(!$_INLINED_IMAGES[$url]) {
$identifier = "automatedmessage." . rand(1000,1000000000) . "@silverstripe.com";
$_INLINED_IMAGES[$url] = $identifier;
}
return "cid:" . $_INLINED_IMAGES[$url];
/**
* @deprecated since version 3.2.0
*/
public function wrapImagesInline_rewriter($url) {
Deprecation::notice('3.2.0', 'wrapImagesInline_rewriter is deprecated');
}
}

243
tests/email/MailerTest.php Normal file
View File

@ -0,0 +1,243 @@
<?php
/**
* @package framework
* @subpackage tests
*/
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);
}
}
/**
* Mocks the sending of emails without actually sending anything
*/
class MailerTest_MockMailer extends Mailer implements TestOnly {
protected function email($to, $subjectEncoded, $fullBody, $headersEncoded, $bounceAddress) {
return array($to, $subjectEncoded, $fullBody, $headersEncoded, $bounceAddress);
}
}