diff --git a/core/Convert.php b/core/Convert.php index d4e303805..c472f7c57 100644 --- a/core/Convert.php +++ b/core/Convert.php @@ -286,11 +286,11 @@ class Convert { /** * Simple conversion of HTML to plaintext. * - * @param $data string - * @param $preserveLinks boolean - * @param $wordwrap array + * @param string $data Input data + * @param bool $preserveLinks + * @param int $wordwrap */ - public static function html2raw($data, $preserveLinks = false, $wordWrap = 60, $config = null) { + public static function html2raw($data, $preserveLinks = false, $wordWrap = 0, $config = null) { $defaultConfig = array( 'PreserveLinks' => false, 'ReplaceBoldAsterisk' => true, @@ -343,9 +343,7 @@ class Convert { $data = preg_replace('/<\/p>/i', "\n\n", $data ); // Replace HTML entities - //$data = preg_replace("/&#([0-9]+);/e", 'chr(\1)', $data); - //$data = str_replace(array("<",">","&"," "), array("<", ">", "&", " "), $data); - $data = html_entity_decode($data, ENT_COMPAT , 'UTF-8'); + $data = html_entity_decode($data, ENT_QUOTES, 'UTF-8'); // Remove all tags (but optionally keep links) // strip_tags seemed to be restricting the length of the output @@ -355,7 +353,10 @@ class Convert { } else { $data = strip_tags($data, ''); } - return trim(wordwrap(trim($data), $wordWrap)); + + // Wrap + if($wordWrap) $data = wordwrap(trim($data), $wordWrap); + return trim($data); } /** diff --git a/docs/en/changelogs/3.2.0.md b/docs/en/changelogs/3.2.0.md index dbd0f30e5..3d747fd00 100644 --- a/docs/en/changelogs/3.2.0.md +++ b/docs/en/changelogs/3.2.0.md @@ -13,6 +13,8 @@ * `ClassInfo::dataClassesFor` now returns classes which should have tables, regardless of whether those 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 diff --git a/docs/en/topics/email.md b/docs/en/topics/email.md index 51f1dd581..c8ef08459 100644 --- a/docs/en/topics/email.md +++ b/docs/en/topics/email.md @@ -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]` diff --git a/email/Email.php b/email/Email.php index 8da323705..1ccb93d37 100644 --- a/email/Email.php +++ b/email/Email.php @@ -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'); } /** diff --git a/email/Mailer.php b/email/Mailer.php index 73b3b9b9f..7f61d314f 100644 --- a/email/Mailer.php +++ b/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:
"; - 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:
"; - 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, '\n" . ""; } - - $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 ' 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'); } } diff --git a/tests/core/ConvertTest.php b/tests/core/ConvertTest.php index a9c684b14..07f56c239 100644 --- a/tests/core/ConvertTest.php +++ b/tests/core/ConvertTest.php @@ -38,34 +38,63 @@ class ConvertTest extends SapphireTest { public function testHtml2raw() { $val1 = 'This has a strong tag.'; - $this->assertEquals('This has a *strong tag*.', Convert::xml2raw($val1), + $this->assertEquals('This has a *strong tag*.', Convert::html2raw($val1), 'Strong tags are replaced with asterisks'); $val1 = 'This has a b tag with attributes.'; - $this->assertEquals('This has a *b tag with attributes*.', Convert::xml2raw($val1), + $this->assertEquals('This has a *b tag with attributes*.', Convert::html2raw($val1), 'B tags with attributes are replaced with asterisks'); $val2 = 'This has a strong tag with attributes.'; - $this->assertEquals('This has a *strong tag with attributes*.', Convert::xml2raw($val2), + $this->assertEquals('This has a *strong tag with attributes*.', Convert::html2raw($val2), 'Strong tags with attributes are replaced with asterisks'); $val3 = ''; - $this->assertEquals('', Convert::xml2raw($val3), + $this->assertEquals('', Convert::html2raw($val3), 'Script tags are completely removed'); $val4 = ''; - $this->assertEquals('', Convert::xml2raw($val4), + $this->assertEquals('', Convert::html2raw($val4), 'Style tags are completely removed'); $val5 = ''; - $this->assertEquals('', Convert::xml2raw($val5), + $this->assertEquals('', Convert::html2raw($val5), 'Multiline script tags are completely removed'); $val6 = ''; - $this->assertEquals('', Convert::xml2raw($val6), + $this->assertEquals('', Convert::html2raw($val6), 'Multiline style tags are completely removed'); + + $val7 = '

That's absolutely correct

'; + $this->assertEquals( + "That's absolutely correct", + Convert::html2raw($val7), + "Single quotes are decoded correctly" + ); + + $val8 = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor '. + 'incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud '. + 'exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute '. + 'irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla '. + 'pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia '. + 'deserunt mollit anim id est laborum.'; + $this->assertEquals($val8, Convert::html2raw($val8), 'Test long text is unwrapped'); + $this->assertEquals(<<sendPlain( + '', + 'tom@jones ', + "What is the of testing?", + $testMessage, + null, + array('CC' => 'admin@silverstripe.com', 'bcc' => 'andrew@thing.com') + ); + + $this->assertEquals('email@silverstripe.com', $to); + $this->assertEquals('=?UTF-8?B?V2hhdCBpcyB0aGUgPHB1cnBvc2U+IG9mIHRlc3Rpbmc/?=', $subjectEncoded); + $this->assertEquals('=?UTF-8?B?'. base64_encode('What is the of testing?').'?=', $subjectEncoded); + + $this->assertEquals(<<assertEquals($testMessage, quoted_printable_decode($fullBody)); + $this->assertEquals(<< +X-Mailer: SilverStripe Mailer - version 2006.06.21 +X-Priority: 3 +Bcc: andrew@thing.com +Cc: admin@silverstripe.com + +PHP + , + Convert::nl2os($headersEncoded) + ); + $this->assertEquals('tom@silverstripe.com', $bounceAddress); + + // Test override bounce email and alternate encoding + $mailer->setBounceEmail('bounce@silverstripe.com'); + $mailer->setMessageEncoding('base64'); + list($to, $subjectEncoded, $fullBody, $headersEncoded, $bounceAddress) = $mailer->sendPlain( + '', + 'tom@jones ', + "What is the of testing?", + $testMessage, + null, + array('CC' => 'admin@silverstripe.com', 'bcc' => 'andrew@thing.com') + ); + + $this->assertEquals('bounce@silverstripe.com', $bounceAddress); + $this->assertEquals(<<assertEquals($testMessage, base64_decode($fullBody)); + } + + /** + * Test HTML messages + */ + public function testSendHTML() { + $mailer = new MailerTest_MockMailer(); + + // Test with default encoding + $testMessageHTML = "

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

". + "

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

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

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

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

=0A=0A +------=_NextPart_000000000000-- +PHP + ), + Convert::nl2os($this->normaliseDivisions($fullBody)) + ); + // Check that the messages exist in the output + $this->assertTrue(stripos($fullBody, quoted_printable_encode($testMessagePlain)) !== false); + $this->assertEquals(<< +X-Mailer: SilverStripe Mailer - version 2006.06.21 +X-Priority: 3 +Bcc: andrew@thing.com +Cc: admin@silverstripe.com + +PHP + , + Convert::nl2os($this->normaliseDivisions($headersEncoded)) + ); + $this->assertEquals('tom@silverstripe.com', $bounceAddress); + + // Test override bounce email and alternate encoding + $mailer->setBounceEmail('bounce@silverstripe.com'); + $mailer->setMessageEncoding('base64'); + list($to, $subjectEncoded, $fullBody, $headersEncoded, $bounceAddress) = $mailer->sendHTML( + '', + 'tom@jones ', + "What is the of testing?", + $testMessageHTML, + null, + array('CC' => 'admin@silverstripe.com', 'bcc' => 'andrew@thing.com') + ); + + $this->assertEquals('bounce@silverstripe.com', $bounceAddress); + $this->assertEquals(<<normaliseDivisions($fullBody)) + ); + + // Check that the text message version is somewhere in there + $this->assertTrue(stripos($fullBody, chunk_split(base64_encode($testMessagePlain), 60)) !== false); + } +} + +/** + * 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); + } +}