You are here

mimemail.inc in Mime Mail 7

Same filename and directory in other branches
  1. 5 mimemail.inc
  2. 6 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 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(implode(";{$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 = '/(<link[^>]+href=[\'"]?|<object[^>]+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;
      }
      else {
        $file = drupal_realpath($url) ? drupal_realpath($url) : file_create_url($url);
      }
    }
  }
  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;
  }
  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 attachments.
 *   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'] . '"';
      }
    }
    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.
 *
 * 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.
 *
 * @param string $body
 *   The HTML message body.
 * @param string $subject
 *   The message subject.
 * @param bool $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.
 */
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',
        ),
      );
    }
    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.
 * @param bool $to_embed
 *   (optional) Whether 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 bool $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;
}

Functions

Namesort descending Description
mimemail_address Formats an address string.
mimemail_extract_files 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 Build a multipart body.
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_replace_files Callback function for preg_replace_callback().
_mimemail_url Helper function to format URLs.