Code coverage for /20080809/includes/mail.inc

Line #Times calledCode
1
<?php
2
// $Id: mail.inc,v 1.14 2008/05/19 19:25:24 dries Exp $
3
4
/**
5
 * Compose and optionally send an e-mail message.
6
 *
7
 * Sending an e-mail works with defining an e-mail template (subject, text
8
 * and possibly e-mail headers) and the replacement values to use in the
9
 * appropriate places in the template. Processed e-mail templates are
10
 * requested from hook_mail() from the module sending the e-mail. Any
module
11
 * can modify the composed e-mail message array using hook_mail_alter().
12
 * Finally drupal_mail_send() sends the e-mail, which can be reused
13
 * if the exact same composed e-mail is to be sent to multiple recipients.
14
 *
15
 * Finding out what language to send the e-mail with needs some
consideration.
16
 * If you send e-mail to a user, her preferred language should be fine, so
17
 * use user_preferred_language(). If you send email based on form values
18
 * filled on the page, there are two additional choices if you are not
19
 * sending the e-mail to a user on the site. You can either use the
language
20
 * used to generate the page ($language global variable) or the site
default
21
 * language. See language_default(). The former is good if sending e-mail
to
22
 * the person filling the form, the later is good if you send e-mail to an
23
 * address previously set up (like contact addresses in a contact form).
24
 *
25
 * Taking care of always using the proper language is even more important
26
 * when sending e-mails in a row to multiple users. Hook_mail() abstracts
27
 * whether the mail text comes from an administrator setting or is
28
 * static in the source code. It should also deal with common mail tokens,
29
 * only receiving $params which are unique to the actual e-mail at hand.
30
 *
31
 * An example:
32
 *
33
 * @code
34
 *   function example_notify($accounts) {
35
 *     foreach ($accounts as $account) {
36
 *       $params['account'] = $account;
37
 *       // example_mail() will be called based on the first drupal_mail()
parameter.
38
 *       drupal_mail('example', 'notify', $account->mail,
user_preferred_language($account), $params);
39
 *     }
40
 *   }
41
 *
42
 *   function example_mail($key, &$message, $params) {
43
 *     $language = $message['language'];
44
 *     $variables = user_mail_tokens($params['account'], $language);
45
 *     switch($key) {
46
 *       case 'notice':
47
 *         $message['subject'] = t('Notification from !site', $variables,
$language->language);
48
 *         $message['body'] = t("Dear !username\n\nThere is new content
available on the site.", $variables, $language->language);
49
 *         break;
50
 *     }
51
 *   }
52
 * @endcode
53
 *
54
 * @param $module
55
 *   A module name to invoke hook_mail() on. The {$module}_mail() hook will
be
56
 *   called to complete the $message structure which will already contain
common
57
 *   defaults.
58
 * @param $key
59
 *   A key to identify the e-mail sent. The final e-mail id for e-mail
altering
60
 *   will be {$module}_{$key}.
61
 * @param $to
62
 *   The e-mail address or addresses where the message will be sent to.
The
63
 *   formatting of this string must comply with RFC 2822. Some examples
are:
64
 *    user@example.com
65
 *    user@example.com, anotheruser@example.com
66
 *    User <user@example.com>
67
 *    User <user@example.com>, Another User <anotheruser@example.com>
68
 * @param $language
69
 *   Language object to use to compose the e-mail.
70
 * @param $params
71
 *   Optional parameters to build the e-mail.
72
 * @param $from
73
 *   Sets From, Reply-To, Return-Path and Error-To to this value, if
given.
74
 * @param $send
75
 *   Send the message directly, without calling drupal_mail_send()
manually.
76
 * @return
77
 *   The $message array structure containing all details of the
78
 *   message. If already sent ($send = TRUE), then the 'result' element
79
 *   will contain the success indicator of the e-mail, failure being
already
80
 *   written to the watchdog. (Success means nothing more than the message
being
81
 *   accepted at php-level, which still doesn't guarantee it to be
delivered.)
82
 */
832027
function drupal_mail($module, $key, $to, $language, $params = array(),
$from = NULL, $send = TRUE) {
846
  $default_from = variable_get('site_mail', ini_get('sendmail_from'));
85
86
  // Bundle up the variables into a structured array for altering.
87
  $message = array(
886
    'id'       => $module . '_' . $key,
896
    'to'       => $to,
906
    'from'     => isset($from) ? $from : $default_from,
916
    'language' => $language,
926
    'params'   => $params,
936
    'subject'  => '',
946
    'body'     => array()
956
  );
96
97
  // Build the default headers
98
  $headers = array(
996
    'MIME-Version'              => '1.0',
1006
    'Content-Type'              => 'text/plain; charset=UTF-8;
format=flowed; delsp=yes',
1016
    'Content-Transfer-Encoding' => '8Bit',
102
    'X-Mailer'                  => 'Drupal'
1036
  );
1046
  if ($default_from) {
105
    // To prevent e-mail from looking like spam, the addresses in the
Sender and
106
    // Return-Path headers should have a domain authorized to use the
originating
107
    // SMTP server. Errors-To is redundant, but shouldn't hurt.
1080
    $headers['From'] = $headers['Reply-To'] = $headers['Sender'] =
$headers['Return-Path'] = $headers['Errors-To'] = $default_from;
1090
  }
1106
  if ($from) {
1114
    $headers['From'] = $headers['Reply-To'] = $from;
1124
  }
1136
  $message['headers'] = $headers;
114
115
  // Build the e-mail (get subject and body, allow additional headers) by
116
  // invoking hook_mail() on this module. We cannot use module_invoke() as
117
  // we need to have $message by reference in hook_mail().
1186
  if (drupal_function_exists($function = $module . '_mail')) {
1196
    $function($key, $message, $params);
1206
  }
121
122
  // Invoke hook_mail_alter() to allow all modules to alter the resulting
e-mail.
1236
  drupal_alter('mail', $message);
124
125
  // Concatenate and wrap the e-mail body.
1266
  $message['body'] = is_array($message['body']) ?
drupal_wrap_mail(implode("\n\n", $message['body'])) :
drupal_wrap_mail($message['body']);
127
128
  // Optionally send e-mail.
1296
  if ($send) {
1306
    $message['result'] = drupal_mail_send($message);
131
132
    // Log errors
1336
    if (!$message['result']) {
1340
      watchdog('mail', 'Error sending e-mail (from %from to %to).',
array('%from' => $message['from'], '%to' => $message['to']),
WATCHDOG_ERROR);
1350
      drupal_set_message(t('Unable to send e-mail. Please contact the site
admin, if the problem persists.'), 'error');
1360
    }
1376
  }
138
1396
  return $message;
1400
}
141
142
/**
143
 * Send an e-mail message, using Drupal variables and default settings.
144
 * More information in the <a
href="http://php.net/manual/en/function.mail.php">
145
 * PHP function reference for mail()</a>. See drupal_mail() for information
on
146
 * how $message is composed.
147
 *
148
 * @param $message
149
 *  Message array with at least the following elements:
150
 *   - id
151
 *      A unique identifier of the e-mail type. Examples:
'contact_user_copy',
152
 *      'user_password_reset'.
153
 *   - to
154
 *      The mail address or addresses where the message will be sent to.
The
155
 *      formatting of this string must comply with RFC 2822. Some examples
are:
156
 *       user@example.com
157
 *       user@example.com, anotheruser@example.com
158
 *       User <user@example.com>
159
 *       User <user@example.com>, Another User <anotheruser@example.com>
160
 *   - subject
161
 *      Subject of the e-mail to be sent. This must not contain any
newline
162
 *      characters, or the mail may not be sent properly.
163
 *   - body
164
 *      Message to be sent. Accepts both CRLF and LF line-endings.
165
 *      E-mail bodies must be wrapped. You can use drupal_wrap_mail() for
166
 *      smart plain text wrapping.
167
 *   - headers
168
 *      Associative array containing all mail headers.
169
 * @return
170
 *   Returns TRUE if the mail was successfully accepted for delivery,
171
 *   FALSE otherwise.
172
 */
1732027
function drupal_mail_send($message) {
174
  // Allow for a custom mail backend.
1756
  if (variable_get('smtp_library', '') &&
file_exists(variable_get('smtp_library', ''))) {
1760
    include_once './' . variable_get('smtp_library', '');
1770
    return drupal_mail_wrapper($message);
1780
  }
179
  else {
1806
    $mimeheaders = array();
1816
    foreach ($message['headers'] as $name => $value) {
1826
      $mimeheaders[] = $name . ': ' . mime_header_encode($value);
1836
    }
1846
    return mail(
1856
      $message['to'],
1866
      mime_header_encode($message['subject']),
187
      // Note: e-mail uses CRLF for line-endings, but PHP's API requires
LF.
188
      // They will appear correctly in the actual e-mail that is sent.
1896
      str_replace("\r", '', $message['body']),
190
      // For headers, PHP's API suggests that we use CRLF normally,
191
      // but some MTAs incorrecly replace LF with CRLF. See #234403.
1926
      join("\n", $mimeheaders)
1936
    );
194
  }
1950
}
196
197
/**
198
 * Perform format=flowed soft wrapping for mail (RFC 3676).
199
 *
200
 * We use delsp=yes wrapping, but only break non-spaced languages when
201
 * absolutely necessary to avoid compatibility issues.
202
 *
203
 * We deliberately use LF rather than CRLF, see drupal_mail().
204
 *
205
 * @param $text
206
 *   The plain text to process.
207
 * @param $indent (optional)
208
 *   A string to indent the text with. Only '>' characters are repeated on
209
 *   subsequent wrapped lines. Others are replaced by spaces.
210
 */
2112027
function drupal_wrap_mail($text, $indent = '') {
212
  // Convert CRLF into LF.
2136
  $text = str_replace("\r", '', $text);
214
  // See if soft-wrapping is allowed.
2156
  $clean_indent = _drupal_html_to_text_clean($indent);
2166
  $soft = strpos($clean_indent, ' ') === FALSE;
217
  // Check if the string has line breaks.
2186
  if (strpos($text, "\n") !== FALSE) {
219
    // Remove trailing spaces to make existing breaks hard.
2206
    $text = preg_replace('/ +\n/m', "\n", $text);
221
    // Wrap each line at the needed width.
2226
    $lines = explode("\n", $text);
2236
    array_walk($lines, '_drupal_wrap_mail_line', array('soft' => $soft,
'length' => strlen($indent)));
2246
    $text = implode("\n", $lines);
2256
  }
226
  else {
227
    // Wrap this line.
2283
    _drupal_wrap_mail_line($text, 0, array('soft' => $soft, 'length' =>
strlen($indent)));
229
  }
230
  // Empty lines with nothing but spaces.
2316
  $text = preg_replace('/^ +\n/m', "\n", $text);
232
  // Space-stuff special lines.
2336
  $text = preg_replace('/^(>| |From)/m', ' $1', $text);
234
  // Apply indentation. We only include non-'>' indentation on the first
line.
2356
  $text = $indent . substr(preg_replace('/^/m', $clean_indent, $text),
strlen($indent));
236
2376
  return $text;
2380
}
239
240
/**
241
 * Transform an HTML string into plain text, preserving the structure of
the
242
 * markup. Useful for preparing the body of a node to be sent by e-mail.
243
 *
244
 * The output will be suitable for use as 'format=flowed; delsp=yes' text
245
 * (RFC 3676) and can be passed directly to drupal_mail() for sending.
246
 *
247
 * We deliberately use LF rather than CRLF, see drupal_mail().
248
 *
249
 * This function provides suitable alternatives for the following tags:
250
 * <a> <em> <i> <strong> <b> <br> <p> <blockquote> <ul> <ol> <li> <dl>
<dt>
251
 * <dd> <h1> <h2> <h3> <h4> <h5> <h6> <hr>
252
 *
253
 * @param $string
254
 *   The string to be transformed.
255
 * @param $allowed_tags (optional)
256
 *   If supplied, a list of tags that will be transformed. If omitted, all
257
 *   all supported tags are transformed.
258
 * @return
259
 *   The transformed string.
260
 */
2612027
function drupal_html_to_text($string, $allowed_tags = NULL) {
262
  // Cache list of supported tags.
2630
  static $supported_tags;
2640
  if (empty($supported_tags)) {
2650
    $supported_tags = array('a', 'em', 'i', 'strong', 'b', 'br', 'p',
'blockquote', 'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'h1', 'h2', 'h3', 'h4',
'h5', 'h6', 'hr');
2660
  }
267
268
  // Make sure only supported tags are kept.
2690
  $allowed_tags = isset($allowed_tags) ? array_intersect($supported_tags,
$allowed_tags) : $supported_tags;
270
271
  // Make sure tags, entities and attributes are well-formed and properly
nested.
2720
  $string = _filter_htmlcorrector(filter_xss($string, $allowed_tags));
273
274
  // Apply inline styles.
2750
  $string = preg_replace('!</?(em|i)((?> +)[^>]*)?>!i', '/', $string);
2760
  $string = preg_replace('!</?(strong|b)((?> +)[^>]*)?>!i', '*', $string);
277
278
  // Replace inline <a> tags with the text of link and a footnote.
279
  // 'See <a href="http://drupal.org">the Drupal site</a>' becomes
280
  // 'See the Drupal site [1]' with the URL included as a footnote.
2810
  _drupal_html_to_mail_urls(NULL, TRUE);
2820
  $pattern = '@(<a[^>]+?href="([^"]*)"[^>]*?>(.+?)</a>)@i';
2830
  $string = preg_replace_callback($pattern, '_drupal_html_to_mail_urls',
$string);
2840
  $urls = _drupal_html_to_mail_urls();
2850
  $footnotes = '';
2860
  if (count($urls)) {
2870
    $footnotes .= "\n";
2880
    for ($i = 0, $max = count($urls); $i < $max; $i++) {
2890
      $footnotes .= '[' . ($i + 1) . '] ' . $urls[$i] . "\n";
2900
    }
2910
  }
292
293
  // Split tags from text.
2940
  $split = preg_split('/<([^>]+?)>/', $string, -1,
PREG_SPLIT_DELIM_CAPTURE);
295
  // Note: PHP ensures the array consists of alternating delimiters and
literals
296
  // and begins and ends with a literal (inserting $null as required).
297
2980
  $tag = FALSE; // Odd/even counter (tag or no tag)
2990
  $casing = NULL; // Case conversion function
3000
  $output = '';
3010
  $indent = array(); // All current indentation string chunks
3020
  $lists = array(); // Array of counters for opened lists
3030
  foreach ($split as $value) {
3040
    $chunk = NULL; // Holds a string ready to be formatted and output.
305
306
    // Process HTML tags (but don't output any literally).
3070
    if ($tag) {
3080
      list($tagname) = explode(' ', strtolower($value), 2);
309
      switch ($tagname) {
310
        // List counters
3110
        case 'ul':
3120
          array_unshift($lists, '*');
3130
          break;
3140
        case 'ol':
3150
          array_unshift($lists, 1);
3160
          break;
3170
        case '/ul':
3180
        case '/ol':
3190
          array_shift($lists);
3200
          $chunk = ''; // Ensure blank new-line.
3210
          break;
322
323
        // Quotation/list markers, non-fancy headers
3240
        case 'blockquote':
325
          // Format=flowed indentation cannot be mixed with lists.
3260
          $indent[] = count($lists) ? ' "' : '>';
3270
          break;
3280
        case 'li':
3290
          $indent[] = is_numeric($lists[0]) ? ' ' . $lists[0]++ . ') ' : '
* ';
3300
          break;
3310
        case 'dd':
3320
          $indent[] = '    ';
3330
          break;
3340
        case 'h3':
3350
          $indent[] = '.... ';
3360
          break;
3370
        case 'h4':
3380
          $indent[] = '.. ';
3390
          break;
3400
        case '/blockquote':
3410
          if (count($lists)) {
342
            // Append closing quote for inline quotes (immediately).
3430
            $output = rtrim($output, "> \n") . "\"\n";
3440
            $chunk = ''; // Ensure blank new-line.
3450
          }
346
          // Fall-through
3470
        case '/li':
3480
        case '/dd':
3490
          array_pop($indent);
3500
          break;
3510
        case '/h3':
3520
        case '/h4':
3530
          array_pop($indent);
3540
        case '/h5':
3550
        case '/h6':
3560
          $chunk = ''; // Ensure blank new-line.
3570
          break;
358
359
        // Fancy headers
3600
        case 'h1':
3610
          $indent[] = '======== ';
3620
          $casing = 'drupal_strtoupper';
3630
          break;
3640
        case 'h2':
3650
          $indent[] = '-------- ';
3660
          $casing = 'drupal_strtoupper';
3670
          break;
3680
        case '/h1':
3690
        case '/h2':
3700
          $casing = NULL;
371
          // Pad the line with dashes.
3720
          $output = _drupal_html_to_text_pad($output, ($tagname == '/h1') ?
'=' : '-', ' ');
3730
          array_pop($indent);
3740
          $chunk = ''; // Ensure blank new-line.
3750
          break;
376
377
        // Horizontal rulers
3780
        case 'hr':
379
          // Insert immediately.
3800
          $output .= drupal_wrap_mail('', implode('', $indent)) . "\n";
3810
          $output = _drupal_html_to_text_pad($output, '-');
3820
          break;
383
384
        // Paragraphs and definition lists
3850
        case '/p':
3860
        case '/dl':
3870
          $chunk = ''; // Ensure blank new-line.
3880
          break;
3890
      }
3900
    }
391
    // Process blocks of text.
392
    else {
393
      // Convert inline HTML text to plain text.
3940
      $value = trim(preg_replace('/\s+/', ' ', decode_entities($value)));
3950
      if (strlen($value)) {
3960
        $chunk = $value;
3970
      }
398
    }
399
400
    // See if there is something waiting to be output.
4010
    if (isset($chunk)) {
402
      // Apply any necessary case conversion.
4030
      if (isset($casing)) {
4040
        $chunk = $casing($chunk);
4050
      }
406
      // Format it and apply the current indentation.
4070
      $output .= drupal_wrap_mail($chunk, implode('', $indent)) . "\n";
408
      // Remove non-quotation markers from indentation.
4090
      $indent = array_map('_drupal_html_to_text_clean', $indent);
4100
    }
411
4120
    $tag = !$tag;
4130
  }
414
4150
  return $output . $footnotes;
4160
}
417
418
/**
419
 * Helper function for array_walk in drupal_wrap_mail().
420
 *
421
 * Wraps words on a single line.
422
 */
4232027
function _drupal_wrap_mail_line(&$line, $key, $values) {
424
  // Use soft-breaks only for purely quoted or unindented text.
4256
  $line = wordwrap($line, 77 - $values['length'], $values['soft'] ? "  \n"
: "\n");
426
  // Break really long words at the maximum width allowed.
4276
  $line = wordwrap($line, 996 - $values['length'], $values['soft'] ? " \n"
: "\n");
4286
}
429
430
/**
431
 * Helper function for drupal_html_to_text().
432
 *
433
 * Keeps track of URLs and replaces them with placeholder tokens.
434
 */
4352027
function _drupal_html_to_mail_urls($match = NULL, $reset = FALSE) {
4360
  global $base_url, $base_path;
4370
  static $urls = array(), $regexp;
438
4390
  if ($reset) {
440
    // Reset internal URL list.
4410
    $urls = array();
4420
  }
443
  else {
4440
    if (empty($regexp)) {
4450
      $regexp = '@^' . preg_quote($base_path, '@') . '@';
4460
    }
4470
    if ($match) {
4480
      list(, , $url, $label) = $match;
449
      // Ensure all URLs are absolute.
4500
      $urls[] = strpos($url, '://') ? $url : preg_replace($regexp,
$base_url . '/', $url);
4510
      return $label . ' [' . count($urls) . ']';
4520
    }
453
  }
4540
  return $urls;
4550
}
456
457
/**
458
 * Helper function for drupal_wrap_mail() and drupal_html_to_text().
459
 *
460
 * Replace all non-quotation markers from a given piece of indentation with
spaces.
461
 */
4622027
function _drupal_html_to_text_clean($indent) {
4636
  return preg_replace('/[^>]/', ' ', $indent);
4640
}
465
466
/**
467
 * Helper function for drupal_html_to_text().
468
 *
469
 * Pad the last line with the given character.
470
 */
4712027
function _drupal_html_to_text_pad($text, $pad, $prefix = '') {
472
  // Remove last line break.
4730
  $text = substr($text, 0, -1);
474
  // Calculate needed padding space and add it.
4750
  if (($p = strrpos($text, "\n")) === FALSE) {
4760
    $p = -1;
4770
  }
4780
  $n = max(0, 79 - (strlen($text) - $p));
479
  // Add prefix and padding, and restore linebreak.
4800
  return $text . $prefix . str_repeat($pad, $n - strlen($prefix)) . "\n";
4810
}
4822027