123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554 |
- <?php
- /**
- * @file
- * Common mail functions for sending e-mail. Originally written by Gerhard.
- *
- * Allie Micka <allie at pajunas dot com>
- */
- /**
- * 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.
- 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) {
- preg_match('/[a-z\d\-\.\+_]+@(?:[a-z\d\-]+\.)+[a-z\d]{2,4}/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) {
- $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 = '/(<link[^>]+href=[\'"]?|<object[^>]+codebase=[\'"]?|@import |[\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)) {
- 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($file) || $content)) {
- if (!$name) {
- $name = (@is_file($file)) ? basename($file) : 'attachment.dat';
- }
- if (!$type) {
- $type = ($name) ? file_get_mimetype($name) : file_get_mimetype($file);
- }
- $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) {
- $boundary = md5(uniqid($_SERVER['REQUEST_TIME'], TRUE));
- $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('|<body.*?</body>|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 = '/(<a[^>]+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.
- *
- * @return string
- * A processed URL.
- */
- function _mimemail_url($url, $embed_file = NULL) {
- global $base_url;
- $url = urldecode($url);
- // If the URL is absolute or a mailto, return it as-is.
- if (strpos($url, '://') !== FALSE || preg_match('!(mailto|callto|tel)\:!', $url)) {
- $url = str_replace(' ', '%20', $url);
- return $url;
- }
- // If the image embedding is disabled, return the absolute URL for the image.
- elseif (variable_get('mimemail_linkonly', 0) && preg_match('!\.(png|gif|jpg|jpeg)$!i', $url)) {
- $url = $base_url . $url;
- $url = str_replace(' ', '%20', $url);
- return $url;
- }
- $url = preg_replace('!^' . base_path() . '!', '', $url, 1);
- // If we're processing to embed the file, we're done here so return.
- if ($embed_file) {
- return $url;
- }
- if (!preg_match('!^\?q=*!', $url)) {
- $strip_clean = TRUE;
- }
- $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.
- $args = explode('/', $path);
- foreach ($languages as $lang) {
- if ($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 (isset($strip_clean) && $strip_clean) {
- $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'])) {
- $output = '';
- 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;
- }
|