mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
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:
parent
29e3347562
commit
e47800917a
@ -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
|
||||
|
||||
|
@ -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]`
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
/**
|
||||
|
568
email/Mailer.php
568
email/Mailer.php
@ -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
243
tests/email/MailerTest.php
Normal 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't (<a href=\"http://www.google.com\">or at least shouldn't</a>) ".
|
||||
"matter so long as the public interface is well-tested & working</p> ".
|
||||
"<p>That'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't (<a href=3D"http://ww=
|
||||
w.google.com">or at least shouldn't</a>) matter so long as the public i=
|
||||
nterface is well-tested & working</p> <p>That'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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user