*/ /** * 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; }