silverstripe-framework/email/Mailer.php
Sam Minnee 3ee8f505b7 MINORE: Remove training whitespace.
The main benefit of this is so that authors who make use of
.editorconfig don't end up with whitespace changes in their PRs.

Spaces vs. tabs has been left alone, although that could do with a
tidy-up in SS4 after the switch to PSR-1/2.

The command used was this:

for match in '*.ss' '*.css' '*.scss' '*.html' '*.yml' '*.php' '*.js' '*.csv' '*.inc' '*.php5'; do
	find . -path ./thirdparty -not -prune -o -path ./admin/thirdparty -not -prune -o -type f -name "$match" -exec sed -E -i '' 's/[[:space:]]+$//' {} \+
	find . -path ./thirdparty -not -prune -o -path ./admin/thirdparty -not -prune -o -type f -name "$match" | xargs perl -pi -e 's/ +$//'
done
2016-01-07 10:15:54 +13:00

612 lines
18 KiB
PHP

<?php
/**
* Mailer objects are responsible for actually sending emails.
* The default Mailer class will use PHP's mail() function.
*
* @package framework
* @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 Email recipient
* @param string $from Email from
* @param string $subject Subject text
* @param string $plainContent Plain text content
* @param array $attachedFiles List of attached files
* @param array $customHeaders List of custom headers
* @return mixed Return false if failure, or list of arguments if success
*/
public function sendPlain($to, $from, $subject, $plainContent, $attachedFiles = array(), $customHeaders = array()) {
// Prepare plain text body
$fullBody = $this->encodeMessage($plainContent, $this->getMessageEncoding());
$headers["Content-Type"] = "text/plain; charset=utf-8";
$headers["Content-Transfer-Encoding"] = $this->getMessageEncoding();
// Send prepared message
return $this->sendPreparedMessage($to, $from, $subject, $attachedFiles, $customHeaders, $fullBody, $headers);
}
/**
* Sends an email as a both HTML and plaintext
*
* @param string $to Email recipient
* @param string $from Email from
* @param string $subject Subject text
* @param string $htmlContent HTML Content
* @param array $attachedFiles List of attachments
* @param array $customHeaders User specified headers
* @param string $plainContent Plain text content. If omitted, will be generated from $htmlContent
* @return mixed Return false if failure, or list of arguments if success
*/
public function sendHTML($to, $from, $subject, $htmlContent,
$attachedFiles = array(), $customHeaders = array(), $plainContent = ''
) {
// Prepare both Plain and HTML components and merge
$plainPart = $this->preparePlainSubmessage($plainContent, $htmlContent);
$htmlPart = $this->prepareHTMLSubmessage($htmlContent);
list($fullBody, $headers) = $this->encodeMultipart(
array($plainPart, $htmlPart),
"multipart/alternative"
);
// Send prepared message
return $this->sendPreparedMessage($to, $from, $subject, $attachedFiles, $customHeaders, $fullBody, $headers);
}
/**
* Send an email of an arbitrary format
*
* @param string $to To
* @param string $from From
* @param string $subject Subject
* @param array $attachedFiles List of attachments
* @param array $customHeaders User specified headers
* @param string $fullBody Prepared message
* @param array $headers Prepared headers
* @return mixed Return false if failure, or list of arguments if success
*/
protected function sendPreparedMessage($to, $from, $subject, $attachedFiles, $customHeaders, $fullBody, $headers) {
// If the subject line contains extended characters, we must encode the
$subjectEncoded = "=?UTF-8?B?" . base64_encode($subject) . "?=";
$to = $this->validEmailAddress($to);
$from = $this->validEmailAddress($from);
// Messages with attachments are handled differently
if($attachedFiles) {
list($fullBody, $headers) = $this->encodeAttachments($attachedFiles, $headers, $fullBody);
}
// Get bounce email
$bounceAddress = $this->getBounceEmail() ?: $from;
if(preg_match('/^([^<>]*)<([^<>]+)> *$/', $bounceAddress, $parts)) $bounceAddress = $parts[2];
// Get headers
$headers["From"] = $from;
$headers = $this->mergeCustomHeaders($headers, $customHeaders);
$headersEncoded = $this->processHeaders($headers);
return $this->email($to, $subjectEncoded, $fullBody, $headersEncoded, $bounceAddress);
}
/**
* Send the actual email
*
* @param string $to
* @param string $subjectEncoded
* @param string $fullBody
* @param string $headersEncoded
* @param string $bounceAddress
* @return mixed Return false if failure, or list of arguments if success
*/
protected function email($to, $subjectEncoded, $fullBody, $headersEncoded, $bounceAddress) {
// Try it without the -f option if it fails
$result = @mail($to, $subjectEncoded, $fullBody, $headersEncoded, escapeshellarg("-f$bounceAddress"));
if(!$result) {
$result = mail($to, $subjectEncoded, $fullBody, $headersEncoded);
}
if($result) {
return array($to, $subjectEncoded, $fullBody, $headersEncoded, $bounceAddress);
}
return false;
}
/**
* Encode attachments into a message
*
* @param array $attachments
* @param array $headers
* @param string $body
* @return array Array containing completed body followed by headers
*/
protected function encodeAttachments($attachments, $headers, $body) {
// The first part is the message itself
$fullMessage = $this->processHeaders($headers, $body);
$messageParts = array($fullMessage);
// Include any specified attachments as additional parts
foreach($attachments as $file) {
if(isset($file['tmp_name']) && isset($file['name'])) {
$messageParts[] = $this->encodeFileForEmail($file['tmp_name'], $file['name']);
} else {
$messageParts[] = $this->encodeFileForEmail($file);
}
}
// We further wrap all of this into another multipart block
return $this->encodeMultipart($messageParts, "multipart/mixed");
}
/**
* Generate the plainPart of a html message
*
* @param string $plainContent Plain body
* @param string $htmlContent HTML message
* @return string Encoded headers / message in a single block
*/
protected function preparePlainSubmessage($plainContent, $htmlContent) {
$plainEncoding = $this->getMessageEncoding();
// Generate plain text version if not explicitly given
if(!$plainContent) $plainContent = Convert::xml2raw($htmlContent);
// Make the plain text part
$headers["Content-Type"] = "text/plain; charset=utf-8";
$headers["Content-Transfer-Encoding"] = $plainEncoding;
$plainContentEncoded = $this->encodeMessage($plainContent, $plainEncoding);
// Merge with headers
return $this->processHeaders($headers, $plainContentEncoded);
}
/**
* Generate the html part of a html message
*
* @param string $htmlContent HTML message
* @return string Encoded headers / message in a single block
*/
protected function prepareHTMLSubmessage($htmlContent) {
// Add basic wrapper tags if the body tag hasn't been given
if(stripos($htmlContent, '<body') === false) {
$htmlContent =
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">\n" .
"<HTML><HEAD>\n" .
"<META http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n" .
"<STYLE type=\"text/css\"></STYLE>\n\n".
"</HEAD>\n" .
"<BODY bgColor=\"#ffffff\">\n" .
$htmlContent .
"\n</BODY>\n" .
"</HTML>";
}
// Make the HTML part
$headers["Content-Type"] = "text/html; charset=utf-8";
$headers["Content-Transfer-Encoding"] = $this->getMessageEncoding();
$htmlContentEncoded = $this->encodeMessage($htmlContent, $this->getMessageEncoding());
// Merge with headers
return $this->processHeaders($headers, $htmlContentEncoded);
}
/**
* Encode an array of parts using multipart
*
* @param array $parts List of parts
* @param string $contentType Content-type of parts
* @param array $headers Existing headers to include in response
* @return array Array with two items, the body followed by headers
*/
protected function encodeMultipart($parts, $contentType, $headers = array()) {
$separator = "----=_NextPart_" . preg_replace('/[^0-9]/', '', rand() * 10000000000);
$headers["MIME-Version"] = "1.0";
$headers["Content-Type"] = "$contentType; boundary=\"$separator\"";
$headers["Content-Transfer-Encoding"] = "7bit";
if($contentType == "multipart/alternative") {
// $baseMessage = "This is an encoded HTML message. There are two parts: a plain text and an HTML message,
// open whatever suits you better.";
$baseMessage = "\nThis is a multi-part message in MIME format.";
} else {
// $baseMessage = "This is a message containing attachments. The e-mail body is contained in the first
// attachment";
$baseMessage = "\nThis is a multi-part message in MIME format.";
}
$separator = "\n--$separator\n";
$body = "$baseMessage\n" .
$separator . implode("\n".$separator, $parts) . "\n" . trim($separator) . "--";
return array($body, $headers);
}
/**
* Add headers to the start of the message
*
* @param array $headers
* @param string $body
* @return string Resulting message body
*/
protected function processHeaders($headers, $body = '') {
$result = '';
foreach($headers as $key => $value) {
$result .= "$key: $value\n";
}
if($body) $result .= "\n$body";
return $result;
}
/**
* Encode the contents of a file for emailing, including headers
*
* $file can be an array, in which case it expects these members:
* 'filename' - the filename of the file
* 'contents' - the raw binary contents of the file as a string
* and can optionally include these members:
* 'mimetype' - the mimetype of the file (calculated from filename if missing)
* 'contentLocation' - the 'Content-Location' header value for the file
*
* $file can also be a string, in which case it is assumed to be the filename
*
* h5. contentLocation
*
* Content Location is one of the two methods allowed for embedding images into an html email.
* It's also the simplest, and best supported.
*
* Assume we have an email with this in the body:
*
* <img src="http://example.com/image.gif" />
*
* To display the image, an email viewer would have to download the image from the web every time
* it is displayed. Due to privacy issues, most viewers will not display any images unless
* the user clicks 'Show images in this email'. Not optimal.
*
* However, we can also include a copy of this image as an attached file in the email.
* By giving it a contentLocation of "http://example.com/image.gif" most email viewers
* will use this attached copy instead of downloading it. Better,
* most viewers will show it without a 'Show images in this email' conformation.
*
* Here is an example of passing this information through Email.php:
*
* $email = new Email();
* $email->attachments[] = array(
* 'filename' => BASE_PATH . "/themes/mytheme/images/header.gif",
* 'contents' => file_get_contents(BASE_PATH . "/themes/mytheme/images/header.gif"),
* 'mimetype' => 'image/gif',
* 'contentLocation' => Director::absoluteBaseURL() . "/themes/mytheme/images/header.gif"
* );
*/
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;
}
if (is_string($file)) {
$file = array('filename' => $file);
$fh = fopen($file['filename'], "rb");
if ($fh) {
$file['contents'] = "";
while(!feof($fh)) $file['contents'] .= fread($fh, 10000);
fclose($fh);
}
}
// Build headers, including content type
if(!$destFileName) $base = basename($file['filename']);
else $base = $destFileName;
$mimeType = !empty($file['mimetype']) ? $file['mimetype'] : HTTP::get_mime_type($file['filename']);
if(!$mimeType) $mimeType = "application/unknown";
if (empty($disposition)) $disposition = isset($file['contentLocation']) ? 'inline' : 'attachment';
// Encode for emailing
if (substr($mimeType, 0, 4) != 'text') {
$encoding = "base64";
$file['contents'] = chunk_split(base64_encode($file['contents']));
} else {
// This mime type is needed, otherwise some clients will show it as an inline attachment
$mimeType = 'application/octet-stream';
$encoding = "quoted-printable";
$file['contents'] = quoted_printable_encode($file['contents']);
}
$headers = "Content-type: $mimeType;\n\tname=\"$base\"\n".
"Content-Transfer-Encoding: $encoding\n".
"Content-Disposition: $disposition;\n\tfilename=\"$base\"\n";
if ( isset($file['contentLocation']) ) $headers .= 'Content-Location: ' . $file['contentLocation'] . "\n" ;
$headers .= $extraHeaders . "\n";
// Return completed packet
return $headers . $file['contents'];
}
/**
* @deprecated since version 4.0
*/
public function validEmailAddr($emailAddress) {
Deprecation::notice('4.0', 'This method will be removed in 4.0. Use protected method Mailer->validEmailAddress().');
return $this->validEmailAddress($emailAddress);
}
/**
* Cleans up emails which may be in 'Name <email@silverstripe.com>' format
*
* @param string $emailAddress
* @return string
*/
protected function validEmailAddress($emailAddress) {
$emailAddress = trim($emailAddress);
$openBracket = strpos($emailAddress, '<');
$closeBracket = strpos($emailAddress, '>');
// Unwrap email contained by braces
if($openBracket === 0 && $closeBracket !== false) {
return substr($emailAddress, 1, $closeBracket - 1);
}
// Ensure name component cannot be mistaken for an email address
if($openBracket) {
$emailAddress = str_replace('@', '', substr($emailAddress, 0, $openBracket))
. substr($emailAddress, $openBracket);
}
return $emailAddress;
}
/**
* @deprecated since version 4.0
*/
public function wrapImagesInline($htmlContent) {
Deprecation::notice('4.0', 'wrapImagesInline is deprecated');
}
/**
* @deprecated since version 4.0
*/
public function wrapImagesInline_rewriter($url) {
Deprecation::notice('4.0', 'wrapImagesInline_rewriter is deprecated');
}
}
/**
* @package framework
* @subpackage email
* @deprecated 3.1
*/
function htmlEmail($to, $from, $subject, $htmlContent, $attachedFiles = false, $customheaders = false,
$plainContent = false) {
Deprecation::notice('4.0', 'Use Email->sendHTML() instead');
$mailer = Injector::inst()->create('Mailer');
return $mailer->sendHTML($to, $from, $subject, $plainContent, $attachedFiles, $customheaders = false);
}
/**
* @package framework
* @subpackage email
* @deprecated 3.1
*/
function plaintextEmail($to, $from, $subject, $plainContent, $attachedFiles, $customheaders = false) {
Deprecation::notice('4.0', 'Use Email->sendPlain() instead');
$mailer = Injector::inst()->create('Mailer');
return $mailer->sendPlain($to, $from, $subject, $plainContent, $attachedFiles, $customheaders = false);
}
/**
* @package framework
* @subpackage email
* @deprecated 3.1
*/
function encodeMultipart($parts, $contentType, $headers = false) {
Deprecation::notice('4.0', 'Use Email->$this->encodeMultipart() instead');
$mailer = Injector::inst()->create('Mailer');
return $mailer->encodeMultipart($parts, $contentType, $headers = false);
}
/**
* @package framework
* @subpackage email
* @deprecated 3.1
*/
function wrapImagesInline($htmlContent) {
Deprecation::notice('4.0', 'Functionality removed from core');
$mailer = Injector::inst()->create('Mailer');
return $mailer->wrapImagesInline($htmlContent);
}
/**
* @package framework
* @subpackage email
* @deprecated 3.1
*/
function wrapImagesInline_rewriter($url) {
Deprecation::notice('4.0', 'Functionality removed from core');
$mailer = Injector::inst()->create('Mailer');
return $mailer->wrapImagesInline_rewriter($url);
}
/**
* @package framework
* @subpackage email
* @deprecated 3.1
*/
function processHeaders($headers, $body = false) {
Deprecation::notice('4.0', 'Set headers through Email->addCustomHeader()');
$mailer = Injector::inst()->create('Mailer');
return $mailer->processHeaders($headers, $url);
}
/**
* @package framework
* @subpackage email
* @deprecated 3.1
*/
function encodeFileForEmail($file, $destFileName = false, $disposition = NULL, $extraHeaders = "") {
Deprecation::notice('4.0', 'Please add files through Email->attachFile()');
$mailer = Injector::inst()->create('Mailer');
return $mailer->encodeFileForEmail($file, $destFileName, $disposition, $extraHeaders);
}
/**
* @package framework
* @subpackage email
* @deprecated 3.1
*/
function QuotedPrintable_encode($quotprint) {
Deprecation::notice('4.0', 'No longer available, handled internally');
$mailer = Injector::inst()->create('Mailer');
return $mailer->QuotedPrintable_encode($quotprint);
}
/**
* @package framework
* @subpackage email
* @deprecated 3.1
*/
function validEmailAddr($emailAddress) {
Deprecation::notice('4.0', 'This method will be removed in 4.0. Use protected method Mailer->validEmailAddress().');
$mailer = Injector::inst()->create('Mailer');
return $mailer->validEmailAddr($emailAddress);
}