*/
/**
* Attempts to RFC822-compliant headers for the mail message or its MIME parts.
*
* @todo Could use some enhancement and stress testing.
*
* @param array $headers
* An array of headers.
*
* @return string
* A string containing the headers.
*/
function mimemail_rfc_headers($headers) {
$header = '';
$crlf = variable_get('mimemail_crlf', MAIL_LINE_ENDINGS);
foreach ($headers as $key => $value) {
$key = trim($key);
// Collapse spaces and get rid of newline characters.
$value = preg_replace('/(\s+|\n|\r|^\s|\s$)/', ' ', $value);
// Fold headers if they're too long.
// A CRLF may be inserted before any WSP.
// @see http://tools.ietf.org/html/rfc2822#section-2.2.3
if (drupal_strlen($value) > 60) {
// If there's a semicolon, use that to separate.
if (count($array = preg_split('/;\s*/', $value)) > 1) {
$value = trim(join(";$crlf ", $array));
}
else {
$value = wordwrap($value, 50, "$crlf ", FALSE);
}
}
$header .= $key . ": " . $value . $crlf;
}
return trim($header);
}
/**
* Gives useful defaults for standard email headers.
*
* @param array $headers
* Message headers.
* @param string $from
* The address of the sender.
*
* @return array
* Overwrited headers.
*/
function mimemail_headers($headers, $from = NULL) {
$default_from = variable_get('site_mail', ini_get('sendmail_from'));
// Overwrite standard headers.
if ($from) {
if (!isset($headers['From']) || $headers['From'] == $default_from) {
$headers['From'] = $from;
}
if (!isset($headers['Sender']) || $headers['Sender'] == $default_from) {
$headers['Sender'] = $from;
}
// This may not work. The MTA may rewrite the Return-Path.
if (!isset($headers['Return-Path']) || $headers['Return-Path'] == $default_from) {
// According to IANA the current longest TLD is 23 characters.
if (preg_match('/[a-z\d\-\.\+_]+@(?:[a-z\d\-]+\.)+[a-z\d]{2,23}/i', $from, $matches)) {
$headers['Return-Path'] = "<$matches[0]>";
}
}
}
// Convert From header if it is an array.
if (is_array($headers['From'])) {
$headers['From'] = mimemail_address($headers['From']);
}
// Run all headers through mime_header_encode() to convert non-ascii
// characters to an rfc compliant string, similar to drupal_mail().
foreach ($headers as $key => $value) {
// According to RFC 2047 addresses MUST NOT be encoded.
if ($key !== 'From' && $key !== 'Sender') {
$headers[$key] = mime_header_encode($value);
}
}
return $headers;
}
/**
* Extracts links to local images from HTML documents.
*
* @param string $html
* A string containing the HTML source of the message.
*
* @return array
* An array containing the document body and the extracted files like the following.
* array(
* array(
* 'name' => document name
* 'content' => html text, local image urls replaced by Content-IDs,
* 'Content-Type' => 'text/html; charset=utf-8')
* array(
* 'name' => file name,
* 'file' => reference to local file,
* 'Content-ID' => generated Content-ID,
* 'Content-Type' => derived using mime_content_type if available, educated guess otherwise
* )
* )
*/
function mimemail_extract_files($html) {
$pattern = '/( ]+href=[\'"]?|]+codebase=[\'"]?|@import (?:url\()?[\'"]?|[\s]src=[\'"]?)([^\'>")]+)([\'"]?)/mis';
$content = preg_replace_callback($pattern, '_mimemail_replace_files', $html);
$encoding = '8Bit';
$body = explode("\n", $content);
foreach ($body as $line) {
if (drupal_strlen($line) > 998) {
$encoding = 'base64';
break;
}
}
if ($encoding == 'base64') {
$content = rtrim(chunk_split(base64_encode($content)));
}
$document = array(array(
'Content-Type' => "text/html; charset=utf-8",
'Content-Transfer-Encoding' => $encoding,
'content' => $content,
));
$files = _mimemail_file();
return array_merge($document, $files);
}
/**
* Callback function for preg_replace_callback().
*/
function _mimemail_replace_files($matches) {
return stripslashes($matches[1]) . _mimemail_file($matches[2]) . stripslashes($matches[3]);
}
/**
* Helper function to extract local files.
*
* @param string $url
* (optional) The URI or the absolute URL to the file.
* @param string $content
* (optional) The actual file content.
* @param string $name
* (optional) The file name.
* @param string $type
* (optional) The file type.
* @param string $disposition
* (optional) The content disposition. Defaults to inline.
*
* @return
* The Content-ID and/or an array of the files on success or the URL on failure.
*/
function _mimemail_file($url = NULL, $content = NULL, $name = '', $type = '', $disposition = 'inline') {
static $files = array();
static $ids = array();
if ($url) {
$image = preg_match('!\.(png|gif|jpg|jpeg)$!i', $url);
$linkonly = variable_get('mimemail_linkonly', 0);
// The file exists on the server as-is. Allows for non-web-accessible files.
if (@is_file($url) && $image && !$linkonly) {
$file = $url;
}
else {
$url = _mimemail_url($url, 'TRUE');
// The $url is absolute, we're done here.
$scheme = file_uri_scheme($url);
if ($scheme == 'http' || $scheme == 'https' || preg_match('!mailto:!', $url) || preg_match('!^data:!', $url)) {
return $url;
}
// The $url is a non-local URI that needs to be converted to a URL.
else {
$file = (drupal_realpath($url)) ? drupal_realpath($url) : file_create_url($url);
}
}
}
// We have the actual content.
elseif ($content) {
$file = $content;
}
if (isset($file)) {
$is_file = @is_file($file);
if ($is_file) {
$access = user_access('send arbitrary files');
$in_public_path = strpos(@drupal_realpath($file), drupal_realpath('public://')) === 0;
if (!$in_public_path && !$access) {
return $url;
}
}
if (!$name) {
$name = $is_file ? basename($file) : 'attachment.dat';
}
if (!$type) {
$type = $is_file ? file_get_mimetype($file) : file_get_mimetype($name);
}
$id = md5($file) . '@' . $_SERVER['HTTP_HOST'];
// Prevent duplicate items.
if (isset($ids[$id])) {
return 'cid:' . $ids[$id];
}
$new_file = array(
'name' => $name,
'file' => $file,
'Content-ID' => $id,
'Content-Disposition' => $disposition,
'Content-Type' => $type,
);
$files[] = $new_file;
$ids[$id] = $id;
return 'cid:' . $id;
}
// The $file does not exist and no $content, return the $url if possible.
elseif ($url) {
return $url;
}
$ret = $files;
$files = array();
$ids = array();
return $ret;
}
/**
* Build a multipart body.
*
* @param array $parts
* An associative array containing the parts to be included:
* - name: A string containing the name of the attachment.
* - content: A string containing textual content.
* - file: A string containing file content.
* - Content-Type: A string containing the content type of either file or content. Mandatory
* for content, optional for file. If not present, it will be derived from file the file if
* mime_content_type is available. If not, application/octet-stream is used.
* - Content-Disposition: (optional) A string containing the disposition. Defaults to inline.
* - Content-Transfer-Encoding: (optional) Base64 is assumed for files, 8bit for other content.
* - Content-ID: (optional) for in-mail references to attachements.
* Name is mandatory, one of content and file is required, they are mutually exclusive.
* @param string $content_type
* (optional) A string containing the content-type for the combined message. Defaults to
* multipart/mixed.
*
* @return array
* An associative array containing the following elements:
* - body: A string containing the MIME-encoded multipart body of a mail.
* - headers: An array that includes some headers for the mail to be sent.
*/
function mimemail_multipart_body($parts, $content_type = 'multipart/mixed; charset=utf-8', $sub_part = FALSE) {
// Control variable to avoid boundary collision.
static $part_num = 0;
$boundary = sha1(uniqid($_SERVER['REQUEST_TIME'], TRUE)) . $part_num++;
$body = '';
$headers = array(
'Content-Type' => "$content_type; boundary=\"$boundary\"",
);
if (!$sub_part) {
$headers['MIME-Version'] = '1.0';
$body = "This is a multi-part message in MIME format.\n";
}
foreach ($parts as $part) {
$part_headers = array();
if (isset($part['Content-ID'])) {
$part_headers['Content-ID'] = '<' . $part['Content-ID'] . '>';
}
if (isset($part['Content-Type'])) {
$part_headers['Content-Type'] = $part['Content-Type'];
}
if (isset($part['Content-Disposition'])) {
$part_headers['Content-Disposition'] = $part['Content-Disposition'];
}
elseif (strpos($part['Content-Type'], 'multipart/alternative') === FALSE) {
$part_headers['Content-Disposition'] = 'inline';
}
if (isset($part['Content-Transfer-Encoding'])) {
$part_headers['Content-Transfer-Encoding'] = $part['Content-Transfer-Encoding'];
}
// Mail content provided as a string.
if (isset($part['content']) && $part['content']) {
if (!isset($part['Content-Transfer-Encoding'])) {
$part_headers['Content-Transfer-Encoding'] = '8bit';
}
$part_body = $part['content'];
if (isset($part['name'])) {
$part_headers['Content-Type'] .= '; name="' . $part['name'] . '"';
$part_headers['Content-Disposition'] .= '; filename="' . $part['name'] . '"';
}
// Mail content references in a filename.
}
else {
if (!isset($part['Content-Transfer-Encoding'])) {
$part_headers['Content-Transfer-Encoding'] = 'base64';
}
if (!isset($part['Content-Type'])) {
$part['Content-Type'] = file_get_mimetype($part['file']);
}
if (isset($part['name'])) {
$part_headers['Content-Type'] .= '; name="' . $part['name'] . '"';
$part_headers['Content-Disposition'] .= '; filename="' . $part['name'] . '"';
}
if (isset($part['file'])) {
$file = (@is_file($part['file'])) ? file_get_contents($part['file']) : $part['file'];
$part_body = chunk_split(base64_encode($file), 76, variable_get('mimemail_crlf', "\n"));
}
}
$body .= "\n--$boundary\n";
$body .= mimemail_rfc_headers($part_headers) . "\n\n";
$body .= isset($part_body) ? $part_body : '';
}
$body .= "\n--$boundary--\n";
return array('headers' => $headers, 'body' => $body);
}
/**
* Callback for preg_replace_callback().
*/
function _mimemail_expand_links($matches) {
return $matches[1] . _mimemail_url($matches[2]);
}
/**
* Generate a multipart message body with a text alternative for some HTML text.
*
* @param string $body
* The HTML message body.
* @param string $subject
* The message subject.
* @param boolean $plain
* (optional) Whether the recipient prefers plaintext-only messages. Defaults to FALSE.
* @param string $plaintext
* (optional) The plaintext message body.
* @param array $attachments
* (optional) The files to be attached to the message.
*
* @return array
* An associative array containing the following elements:
* - body: A string containing the MIME-encoded multipart body of a mail.
* - headers: An array that includes some headers for the mail to be sent.
*
* The first mime part is a multipart/alternative containing mime-encoded sub-parts for
* HTML and plaintext. Each subsequent part is the required image or attachment.
*/
function mimemail_html_body($body, $subject, $plain = FALSE, $plaintext = NULL, $attachments = array()) {
if (empty($plaintext)) {
// @todo Remove once filter_xss() can handle direct descendant selectors in inline CSS.
// @see http://drupal.org/node/1116930
// @see http://drupal.org/node/370903
// Pull out the message body.
preg_match('||mis', $body, $matches);
$plaintext = drupal_html_to_text($matches[0]);
}
if ($plain) {
// Plain mail without attachment.
if (empty($attachments)) {
$content_type = 'text/plain';
return array(
'body' => $plaintext,
'headers' => array('Content-Type' => 'text/plain; charset=utf-8'),
);
}
// Plain mail with attachement.
else {
$content_type = 'multipart/mixed';
$parts = array(array(
'content' => $plaintext,
'Content-Type' => 'text/plain; charset=utf-8',
));
}
}
else {
$content_type = 'multipart/mixed';
$plaintext_part = array('Content-Type' => 'text/plain; charset=utf-8', 'content' => $plaintext);
// Expand all local links.
$pattern = '/(]+href=")([^"]*)/mi';
$body = preg_replace_callback($pattern, '_mimemail_expand_links', $body);
$mime_parts = mimemail_extract_files($body);
$content = array($plaintext_part, array_shift($mime_parts));
$content = mimemail_multipart_body($content, 'multipart/alternative', TRUE);
$parts = array(array('Content-Type' => $content['headers']['Content-Type'], 'content' => $content['body']));
if ($mime_parts) {
$parts = array_merge($parts, $mime_parts);
$content = mimemail_multipart_body($parts, 'multipart/related; type="multipart/alternative"', TRUE);
$parts = array(array('Content-Type' => $content['headers']['Content-Type'], 'content' => $content['body']));
}
}
if (is_array($attachments) && !empty($attachments)) {
foreach ($attachments as $a) {
$a = (object) $a;
$path = isset($a->uri) ? $a->uri : (isset($a->filepath) ? $a->filepath : NULL);
$content = isset($a->filecontent) ? $a->filecontent : NULL;
$name = isset($a->filename) ? $a->filename : NULL;
$type = isset($a->filemime) ? $a->filemime : NULL;
_mimemail_file($path, $content, $name, $type, 'attachment');
$parts = array_merge($parts, _mimemail_file());
}
}
return mimemail_multipart_body($parts, $content_type);
}
/**
* Helper function to format URLs.
*
* @param string $url
* The file path.
* @param boolean $to_embed
* (optional) Wheter the URL is used to embed the file. Defaults to NULL.
*
* @return string
* A processed URL.
*/
function _mimemail_url($url, $to_embed = NULL) {
$url = urldecode($url);
$to_link = variable_get('mimemail_linkonly', 0);
$is_image = preg_match('!\.(png|gif|jpg|jpeg)!i', $url);
$is_absolute = file_uri_scheme($url) != FALSE || preg_match('!(mailto|callto|tel)\:!', $url);
if (!$to_embed) {
if ($is_absolute) {
return str_replace(' ', '%20', $url);
}
}
else {
$url = preg_replace('!^' . base_path() . '!', '', $url, 1);
if ($is_image) {
// Remove security token from URL, this allows for styled image embedding.
// @see https://drupal.org/drupal-7.20-release-notes
$url = preg_replace('/\\?itok=.*$/', '', $url);
if ($to_link) {
// Exclude images from embedding if needed.
$url = file_create_url($url);
$url = str_replace(' ', '%20', $url);
}
}
return $url;
}
$url = str_replace('?q=', '', $url);
@list($url, $fragment) = explode('#', $url, 2);
@list($path, $query) = explode('?', $url, 2);
// If we're dealing with an intra-document reference, return it.
if (empty($path)) {
return '#' . $fragment;
}
// Get a list of enabled languages.
$languages = language_list('enabled');
$languages = $languages[1];
// Default language settings.
$prefix = '';
$language = language_default();
// Check for language prefix.
$path = trim($path, '/');
$args = explode('/', $path);
foreach ($languages as $lang) {
if (!empty($args) && $args[0] == $lang->prefix) {
$prefix = array_shift($args);
$language = $lang;
$path = implode('/', $args);
break;
}
}
$options = array(
'query' => ($query) ? drupal_get_query_array($query) : array(),
'fragment' => $fragment,
'absolute' => TRUE,
'language' => $language,
'prefix' => $prefix,
);
$url = url($path, $options);
// If url() added a ?q= where there should not be one, remove it.
if (preg_match('!^\?q=*!', $url)) {
$url = preg_replace('!\?q=!', '', $url);
}
$url = str_replace('+', '%2B', $url);
return $url;
}
/**
* Formats an address string.
*
* @todo Could use some enhancement and stress testing.
*
* @param mixed $address
* A user object, a text email address or an array containing name, mail.
* @param boolean $simplify
* Determines if the address needs to be simplified. Defaults to FALSE.
*
* @return string
* A formatted address string or FALSE.
*/
function mimemail_address($address, $simplify = FALSE) {
if (is_array($address)) {
// It's an array containing 'mail' and/or 'name'.
if (isset($address['mail'])) {
if (empty($address['name']) || $simplify) {
return $address['mail'];
}
else {
return '"' . addslashes(mime_header_encode($address['name'])) . '" <' . $address['mail'] . '>';
}
}
// It's an array of address items.
$addresses = array();
foreach ($address as $a) {
$addresses[] = mimemail_address($a);
}
return $addresses;
}
// It's a user object.
if (is_object($address) && isset($address->mail)) {
if (empty($address->name) || $simplify) {
return $address->mail;
}
else {
return '"' . addslashes(mime_header_encode($address->name)) . '" <' . $address->mail . '>';
}
}
// It's formatted or unformatted string.
// @todo: shouldn't assume it's valid - should try to re-parse
if (is_string($address)) {
return $address;
}
return FALSE;
}