You are here

mimemail.inc in Mime Mail 6

Same filename and directory in other branches
  1. 5 mimemail.inc
  2. 7 mimemail.inc

Common mail functions for sending e-mail. Originally written by Gerhard.

Allie Micka < allie at pajunas dot com >

File

mimemail.inc
View source
<?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 $headers
 *   An array of headers.
 *
 * @return
 *   A string containing the header.
 */
function mimemail_rfc_headers($headers) {
  $header = '';
  $crlf = variable_get('mimemail_crlf', "\n");
  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 $headers
 *   An array of headers.
 *
 * @return
 *   An array containing the encoded headers with the default values.
 */
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;
}

/**
 * Helper function to extracts links to local images from HTML documents.
 *
 * @param $html
 *   A string containing the HTML document.
 * @param $name
 *   A string containing the document's name.
 *
 * @return
 *   An array of arrays containing the extraced files.
 */
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 (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 $url
 *   The URL of the file.
 * @param $content
 *   The actual file content.
 *
 * @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) {

    // The file exists on the server as-is. Allows for non-web-accessible files.
    if (@is_file($url)) {
      $file = $url;
    }
    else {
      $url = _mimemail_url($url, 'TRUE');

      // If the url is absolute, we're done here.
      if (strpos($url, '://') !== FALSE || preg_match('!(mailto|callto|tel)\\:!', $url)) {
        return $url;
      }
      else {

        // Make sure ImageCache images are existing before trying to attach.
        if (module_exists('imagecache') && strpos($url, 'imagecache') !== FALSE) {
          _mimemail_imagecache($url);
        }

        // Download method is private, and the URL needs conversion.
        if (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC) == FILE_DOWNLOADS_PRIVATE && strpos($url, 'system/files/') !== 0) {
          $file = file_create_path(drupal_substr($url, strpos($url, 'system/files/') + drupal_strlen('system/files/')));
        }
        else {
          $file = $url;
        }
      }
    }
  }
  elseif ($content) {
    $file = $content;
  }
  if (isset($file) && (@is_file($file) || $content)) {
    $public_path = file_directory_path();
    $no_access = !user_access('send arbitrary files - Warning: has security implications!');
    $not_in_public_path = strpos(realpath($file), realpath($public_path)) === FALSE;
    if (@is_file($file) && $not_in_public_path && $no_access) {
      return $url;
    }
    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;
  }
  elseif ($url) {
    return $url;
  }
  $ret = $files;
  $files = array();
  $ids = array();
  return $ret;
}

/**
 * Helper function to ensure that ImageCache files are created before attaching.
 */
function _mimemail_imagecache($url) {
  static $urls = array();

  // Prevent rechecking.
  if (isset($urls[$url])) {
    return;
  }
  $segments = explode('/', $url);

  // Extract the preset from the path.
  $key = array_search('imagecache', $segments);
  $preset = $segments[$key + 1];

  // Remove unnecessary segments to get the original path.
  unset($segments[$key], $segments[$key + 1]);
  $path = implode('/', $segments);
  $urls[$url] = TRUE;
  return imagecache_generate_image($preset, $path);
}

/**
 * Helper function to build multipart messages.
 *
 * @param $parts
 *   An array of arrays of parts to be included:
 *   - name: The name of the attachment.
 *   - content: Textual content.
 *   - file: A file.
 *   - Content-Type: Content type of either file or content. Mandatory for content, optional for file.
 *      If not present, it will be derived from the file if mime_content_type is available. If not,
 *      application/octet-stream is used.
 *   - Content-Disposition: Inline is assumed (optional).
 *   - Content-Transfer-Encoding: Base64 is assumed for files, 8bit for other content (optional).
 *   - Content-ID: ID for in-mail references to attachements (optional).
 *   Name is mandatory, one of content and file is required, they are mutually exclusive.
 * @param $content_type
 *   A string containing the content-type for the combined message (optional).
 *
 * @return
 *   An array containing the following elements:
 *   - headers: An array that includes some headers for the mail to be sent.
 *   - body: A string containing the mime encoded multipart body of a mail.
 */
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(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 .= $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.
 *
 * The first MIME part is a multipart/alternative containing MIME-encoded
 * sub-parts for HTML and plaintext. Each subsequent part is the required
 * image/attachment.
 *
 * @param $body
 *   An HTML message body
 * @param $subject
 *   The message subject
 * @param $plaintext
 *   TRUE if the recipient prefers plaintext-only messages. Defaults to FALSE.
 *
 * @return
 *   An array containing the headers and the body of the message.
 */
function mimemail_html_body($body, $subject, $plaintext = FALSE, $text = NULL, $attachments = array()) {
  if (empty($text)) {

    // @todo Remove once filter_xss() can handle direct descendant selectors in inline CSS.
    // @see http://drupal.org/node/1116930, http://drupal.org/node/370903
    // Pull out the message body.
    preg_match('|<body.*?</body>|mis', $body, $matches);
    $text = drupal_html_to_text($matches[0]);
  }
  if ($plaintext) {

    // Plain mail without attachment.
    if (empty($attachments)) {
      $content_type = 'text/plain';
      return array(
        'body' => $text,
        'headers' => array(
          'Content-Type' => 'text/plain; charset=utf-8',
        ),
      );
    }
    else {
      $content_type = 'multipart/mixed';
      $parts = array(
        array(
          'content' => $text,
          'Content-Type' => 'text/plain; charset=utf-8',
        ),
      );
    }
  }
  else {
    $content_type = 'multipart/mixed';
    $text_part = array(
      'Content-Type' => 'text/plain; charset=utf-8',
      'content' => $text,
    );

    // 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(
      $text_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;

      // Check the list parameter if its set or ignore it (Upload module support).
      if (!isset($a->list) || $a->list) {
        _mimemail_file($a->filepath, $a->filecontent, $a->filename, $a->filemime, 'attachment');
        $parts = array_merge($parts, _mimemail_file());
      }
    }
  }
  return mimemail_multipart_body($parts, $content_type);
}

/*
 * Parse an incoming message.
 */
function mimemail_parse($message) {

  // Provides a "headers", "content-type" and "body" element.
  $mail = mimemail_parse_headers($message);

  // Get an address-only version of "From" (useful for user_load() and such).
  $mail['from'] = preg_replace('/.*\\b([a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4})\\b.*/i', '\\1', drupal_strtolower($mail['headers']['From']));

  // Get a subject line, which may be cleaned up/modified later.
  $mail['subject'] = $mail['headers']['Subject'];

  // Make an array to hold any non-content attachments.
  $mail['attachments'] = array();

  // We're dealing with a multi-part message.
  $mail['parts'] = mimemail_parse_boundary($mail);
  foreach ($mail['parts'] as $i => $part_body) {
    $part = mimemail_parse_headers($part_body);
    $sub_parts = mimemail_parse_boundary($part);

    // Content is encoded in a multipart/alternative section
    if (count($sub_parts) > 1) {
      foreach ($sub_parts as $j => $sub_part_body) {
        $sub_part = mimemail_parse_headers($sub_part_body);
        if ($sub_part['content-type'] == 'text/plain') {
          $mail['text'] = mimemail_parse_content($sub_part);
        }
        if ($sub_part['content-type'] == 'text/html') {
          $mail['html'] = mimemail_parse_content($sub_part);
        }
        else {
          $mail['attachments'][] = mimemail_parse_attachment($sub_part);
        }
      }
    }
    if ($part['content-type'] == 'text/plain' && !isset($mail['text'])) {
      $mail['text'] = mimemail_parse_content($part);
    }
    elseif ($part['content-type'] == 'text/html' && !isset($mail['html'])) {
      $mail['html'] = mimemail_parse_content($part);
    }
    else {
      $mail['attachments'][] = mimemail_parse_attachment($part);
    }
  }

  // Make sure our text and HTML parts are accounted for.
  if (isset($mail['html']) && !isset($mail['text'])) {
    $mail['text'] = preg_replace('|<style.*</style>|mis', '', $mail['html']);
    $mail['text'] = drupal_html_to_text($mail['text']);
  }
  elseif (isset($mail['text']) && !isset($mail['html'])) {
    $format = variable_get('mimemail_format', FILTER_FORMAT_DEFAULT);
    $mail['html'] = check_markup($mail['text'], $format, FALSE);
  }

  // Last ditch attempt, use the body as-is.
  if (!isset($mail['text'])) {
    $mail['text'] = mimemail_parse_content($mail);
    $format = variable_get('mimemail_format', FILTER_FORMAT_DEFAULT);
    $mail['html'] = check_markup($mail['text'], $format, FALSE);
  }
  return $mail;
}

/*
 * Split a multi-part message using MIME boundaries.
 */
function mimemail_parse_boundary($part) {
  $m = array();
  if (preg_match('/.*boundary="?([^";]+)"?.*/', $part['headers']['Content-Type'], $m)) {
    $boundary = "\n--" . $m[1];
    $body = str_replace("{$boundary}--", '', $part['body']);
    return array_slice(explode($boundary, $body), 1);
  }
  return array(
    $part['body'],
  );
}

/*
 * Split a message (or message part) into its headers and body section.
 */
function mimemail_parse_headers($message) {

  // Split out body and headers.
  if (preg_match("/^(.*?)\r?\n\r?\n(.*)/s", $message, $match)) {
    list($hdr, $body) = array(
      $match[1],
      $match[2],
    );
  }

  // Un-fold the headers.
  $hdr = preg_replace(array(
    "/\r/",
    "/\n(\t| )+/",
  ), array(
    '',
    ' ',
  ), $hdr);
  $headers = array();
  foreach (explode("\n", trim($hdr)) as $row) {
    $split = strpos($row, ':');
    $name = trim(drupal_substr($row, 0, $split));
    $val = trim(drupal_substr($row, $split + 1));
    $headers[$name] = $val;
  }
  $type = preg_replace('/\\s*([^;]+).*/', '\\1', $headers['Content-Type']);
  return array(
    'headers' => $headers,
    'body' => $body,
    'content-type' => $type,
  );
}

/*
 * Return a decoded MIME part in UTF-8.
 */
function mimemail_parse_content($part) {
  $content = $part['body'];

  // Decode this part.
  if ($encoding = drupal_strtolower($part['headers']['Content-Transfer-Encoding'])) {
    switch ($encoding) {
      case 'base64':
        $content = base64_decode($content);
        break;
      case 'quoted-printable':
        $content = quoted_printable_decode($content);
        break;
      case '7bit':

        // 7bit is the RFC default
        break;
    }
  }

  // Try to convert character set to UTF-8.
  if (preg_match('/.*charset="?([^";]+)"?.*/', $part['headers']['Content-Type'], $m)) {
    $content = drupal_convert_to_utf8($content, $m[1]);
  }
  return $content;
}

/*
 * Convert a MIME part into a file array.
 */
function mimemail_parse_attachment($part) {
  $m = array();
  if (preg_match('/.*filename="?([^";])"?.*/', $part['headers']['Content-Disposition'], $m)) {
    $name = $m[1];
  }
  elseif (preg_match('/.*name="?([^";])"?.*/', $part['headers']['Content-Type'], $m)) {
    $name = $m[1];
  }
  return array(
    'filename' => $name,
    'filemime' => $part['content-type'],
    'content' => mimemail_parse_content($part),
  );
}

/**
 * Helper function to format URLs.
 *
 * @param $url
 *   A string containing an URL.
 * @param $embed_file
 *   TRUE if the file should be embedded in the message. Defaults to FALSE.
 *
 * @return
 *   A string containing an absolute URL or mailto.
 */
function _mimemail_url($url, $embed_file = FALSE) {
  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;
  }
  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 (array_keys($languages) as $lang) {
    if ($args[0] == $lang) {
      $prefix = array_shift($args) . '/';
      $language = $languages[$lang];
      $path = implode('/', $args);
      break;
    }
  }
  $options = array(
    'query' => $query,
    '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 ($strip_clean) {
    $url = preg_replace('!\\?q=!', '', $url);
  }
  $url = str_replace('+', '%2B', $url);
  return $url;
}

/**
 * Helper function to format an address string.
 *
 * @todo Could use some enhancement and stress testing.
 *
 * @param $address
 *   A user object, a text email address or an array containing name, mail.
 *
 * @return
 *   A formatted address string or FALSE.
 */
function mimemail_address($address) {
  $simple_address = variable_get('mimemail_simple_address', 0);
  if (is_array($address)) {

    // It's an array containing mail and/or name.
    if (isset($address['mail'])) {
      $output = '';
      if (empty($address['name']) || $simple_address) {
        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) || $simple_address) {
      return $address->mail;
    }
    else {
      return '"' . addslashes(mime_header_encode($address->name)) . '" <' . $address->mail . '>';
    }
  }

  // Its a formatted or an unformatted string.
  // @todo Shouldn't assume it's valid, should try to re-parse.
  if (is_string($address)) {
    return $address;
  }

  // It's null, return the site default address.
  if (is_null($address)) {
    return array(
      'name' => mime_header_encode(variable_get('site_name', 'Drupal')),
      'mail' => variable_get('site_mail', ini_get('sendmail_from')),
    );
  }
  return FALSE;
}

Functions

Namesort descending Description
mimemail_address Helper function to format an address string.
mimemail_extract_files Helper function to extracts links to local images from HTML documents.
mimemail_headers Gives useful defaults for standard email headers.
mimemail_html_body Generate a multipart message body with a text alternative for some HTML text.
mimemail_multipart_body Helper function to build multipart messages.
mimemail_parse
mimemail_parse_attachment
mimemail_parse_boundary
mimemail_parse_content
mimemail_parse_headers
mimemail_rfc_headers Attempts to RFC822-compliant headers for the mail message or its MIME parts.
_mimemail_expand_links Callback for preg_replace_callback()
_mimemail_file Helper function to extract local files.
_mimemail_imagecache Helper function to ensure that ImageCache files are created before attaching.
_mimemail_replace_files Callback function for preg_replace_callback().
_mimemail_url Helper function to format URLs.