Simpletest Coverage - includes/mail.inc

1 <?php
2 // $Id: mail.inc,v 1.20 2009/05/07 10:41:12 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', 'notice', $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 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 */
83 function drupal_mail($module, $key, $to, $language, $params = array(), $from = NULL, $send = TRUE) {
84 $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(
88 'id' => $module . '_' . $key,
89 'to' => $to,
90 'from' => isset($from) ? $from : $default_from,
91 'language' => $language,
92 'params' => $params,
93 'subject' => '',
94 'body' => array()
95 );
96
97 // Build the default headers
98 $headers = array(
99 'MIME-Version' => '1.0',
100 'Content-Type' => 'text/plain; charset=UTF-8; format=flowed; delsp=yes',
101 'Content-Transfer-Encoding' => '8Bit',
102 'X-Mailer' => 'Drupal'
103 );
104 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.
108 $headers['From'] = $headers['Sender'] = $headers['Return-Path'] = $headers['Errors-To'] = $default_from;
109 }
110 if ($from) {
111 $headers['From'] = $from;
112 }
113 $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().
118 if (drupal_function_exists($function = $module . '_mail')) {
119 $function($key, $message, $params);
120 }
121
122 // Invoke hook_mail_alter() to allow all modules to alter the resulting e-mail.
123 drupal_alter('mail', $message);
124
125 // Concatenate and wrap the e-mail body.
126 $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.
129 if ($send) {
130 $message['result'] = drupal_mail_send($message);
131
132 // Log errors
133 if (!$message['result']) {
134 watchdog('mail', 'Error sending e-mail (from %from to %to).', array('%from' => $message['from'], '%to' => $message['to']), WATCHDOG_ERROR);
135 drupal_set_message(t('Unable to send e-mail. Please contact the site administrator if the problem persists.'), 'error');
136 }
137 }
138
139 return $message;
140 }
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 */
173 function drupal_mail_send($message) {
174 // Allow for a custom mail backend.
175 if (variable_get('smtp_library', '') && file_exists(variable_get('smtp_library', ''))) {
176 include_once DRUPAL_ROOT . '/' . variable_get('smtp_library', '');
177 return drupal_mail_wrapper($message);
178 }
179 else {
180 $mimeheaders = array();
181 foreach ($message['headers'] as $name => $value) {
182 $mimeheaders[] = $name . ': ' . mime_header_encode($value);
183 }
184 return mail(
185 $message['to'],
186 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.
189 str_replace("\r", '', $message['body']),
190 // For headers, PHP's API suggests that we use CRLF normally,
191 // but some MTAs incorrectly replace LF with CRLF. See #234403.
192 join("\n", $mimeheaders)
193 );
194 }
195 }
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 */
211 function drupal_wrap_mail($text, $indent = '') {
212 // Convert CRLF into LF.
213 $text = str_replace("\r", '', $text);
214 // See if soft-wrapping is allowed.
215 $clean_indent = _drupal_html_to_text_clean($indent);
216 $soft = strpos($clean_indent, ' ') === FALSE;
217 // Check if the string has line breaks.
218 if (strpos($text, "\n") !== FALSE) {
219 // Remove trailing spaces to make existing breaks hard.
220 $text = preg_replace('/ +\n/m', "\n", $text);
221 // Wrap each line at the needed width.
222 $lines = explode("\n", $text);
223 array_walk($lines, '_drupal_wrap_mail_line', array('soft' => $soft, 'length' => strlen($indent)));
224 $text = implode("\n", $lines);
225 }
226 else {
227 // Wrap this line.
228 _drupal_wrap_mail_line($text, 0, array('soft' => $soft, 'length' => strlen($indent)));
229 }
230 // Empty lines with nothing but spaces.
231 $text = preg_replace('/^ +\n/m', "\n", $text);
232 // Space-stuff special lines.
233 $text = preg_replace('/^(>| |From)/m', ' $1', $text);
234 // Apply indentation. We only include non-'>' indentation on the first line.
235 $text = $indent . substr(preg_replace('/^/m', $clean_indent, $text), strlen($indent));
236
237 return $text;
238 }
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 */
261 function drupal_html_to_text($string, $allowed_tags = NULL) {
262 // Cache list of supported tags.
263 static $supported_tags;
264 if (empty($supported_tags)) {
265 $supported_tags = array('a', 'em', 'i', 'strong', 'b', 'br', 'p', 'blockquote', 'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr');
266 }
267
268 // Make sure only supported tags are kept.
269 $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.
272 $string = _filter_htmlcorrector(filter_xss($string, $allowed_tags));
273
274 // Apply inline styles.
275 $string = preg_replace('!</?(em|i)((?> +)[^>]*)?>!i', '/', $string);
276 $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.
281 _drupal_html_to_mail_urls(NULL, TRUE);
282 $pattern = '@(<a[^>]+?href="([^"]*)"[^>]*?>(.+?)</a>)@i';
283 $string = preg_replace_callback($pattern, '_drupal_html_to_mail_urls', $string);
284 $urls = _drupal_html_to_mail_urls();
285 $footnotes = '';
286 if (count($urls)) {
287 $footnotes .= "\n";
288 for ($i = 0, $max = count($urls); $i < $max; $i++) {
289 $footnotes .= '[' . ($i + 1) . '] ' . $urls[$i] . "\n";
290 }
291 }
292
293 // Split tags from text.
294 $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
298 $tag = FALSE; // Odd/even counter (tag or no tag)
299 $casing = NULL; // Case conversion function
300 $output = '';
301 $indent = array(); // All current indentation string chunks
302 $lists = array(); // Array of counters for opened lists
303 foreach ($split as $value) {
304 $chunk = NULL; // Holds a string ready to be formatted and output.
305
306 // Process HTML tags (but don't output any literally).
307 if ($tag) {
308 list($tagname) = explode(' ', strtolower($value), 2);
309 switch ($tagname) {
310 // List counters
311 case 'ul':
312 array_unshift($lists, '*');
313 break;
314 case 'ol':
315 array_unshift($lists, 1);
316 break;
317 case '/ul':
318 case '/ol':
319 array_shift($lists);
320 $chunk = ''; // Ensure blank new-line.
321 break;
322
323 // Quotation/list markers, non-fancy headers
324 case 'blockquote':
325 // Format=flowed indentation cannot be mixed with lists.
326 $indent[] = count($lists) ? ' "' : '>';
327 break;
328 case 'li':
329 $indent[] = is_numeric($lists[0]) ? ' ' . $lists[0]++ . ') ' : ' * ';
330 break;
331 case 'dd':
332 $indent[] = ' ';
333 break;
334 case 'h3':
335 $indent[] = '.... ';
336 break;
337 case 'h4':
338 $indent[] = '.. ';
339 break;
340 case '/blockquote':
341 if (count($lists)) {
342 // Append closing quote for inline quotes (immediately).
343 $output = rtrim($output, "> \n") . "\"\n";
344 $chunk = ''; // Ensure blank new-line.
345 }
346 // Fall-through
347 case '/li':
348 case '/dd':
349 array_pop($indent);
350 break;
351 case '/h3':
352 case '/h4':
353 array_pop($indent);
354 case '/h5':
355 case '/h6':
356 $chunk = ''; // Ensure blank new-line.
357 break;
358
359 // Fancy headers
360 case 'h1':
361 $indent[] = '======== ';
362 $casing = 'drupal_strtoupper';
363 break;
364 case 'h2':
365 $indent[] = '-------- ';
366 $casing = 'drupal_strtoupper';
367 break;
368 case '/h1':
369 case '/h2':
370 $casing = NULL;
371 // Pad the line with dashes.
372 $output = _drupal_html_to_text_pad($output, ($tagname == '/h1') ? '=' : '-', ' ');
373 array_pop($indent);
374 $chunk = ''; // Ensure blank new-line.
375 break;
376
377 // Horizontal rulers
378 case 'hr':
379 // Insert immediately.
380 $output .= drupal_wrap_mail('', implode('', $indent)) . "\n";
381 $output = _drupal_html_to_text_pad($output, '-');
382 break;
383
384 // Paragraphs and definition lists
385 case '/p':
386 case '/dl':
387 $chunk = ''; // Ensure blank new-line.
388 break;
389 }
390 }
391 // Process blocks of text.
392 else {
393 // Convert inline HTML text to plain text.
394 $value = trim(preg_replace('/\s+/', ' ', decode_entities($value)));
395 if (strlen($value)) {
396 $chunk = $value;
397 }
398 }
399
400 // See if there is something waiting to be output.
401 if (isset($chunk)) {
402 // Apply any necessary case conversion.
403 if (isset($casing)) {
404 $chunk = $casing($chunk);
405 }
406 // Format it and apply the current indentation.
407 $output .= drupal_wrap_mail($chunk, implode('', $indent)) . "\n";
408 // Remove non-quotation markers from indentation.
409 $indent = array_map('_drupal_html_to_text_clean', $indent);
410 }
411
412 $tag = !$tag;
413 }
414
415 return $output . $footnotes;
416 }
417
418 /**
419 * Helper function for array_walk in drupal_wrap_mail().
420 *
421 * Wraps words on a single line.
422 */
423 function _drupal_wrap_mail_line(&$line, $key, $values) {
424 // Use soft-breaks only for purely quoted or unindented text.
425 $line = wordwrap($line, 77 - $values['length'], $values['soft'] ? " \n" : "\n");
426 // Break really long words at the maximum width allowed.
427 $line = wordwrap($line, 996 - $values['length'], $values['soft'] ? " \n" : "\n");
428 }
429
430 /**
431 * Helper function for drupal_html_to_text().
432 *
433 * Keeps track of URLs and replaces them with placeholder tokens.
434 */
435 function _drupal_html_to_mail_urls($match = NULL, $reset = FALSE) {
436 global $base_url, $base_path;
437 static $urls = array(), $regexp;
438
439 if ($reset) {
440 // Reset internal URL list.
441 $urls = array();
442 }
443 else {
444 if (empty($regexp)) {
445 $regexp = '@^' . preg_quote($base_path, '@') . '@';
446 }
447 if ($match) {
448 list(, , $url, $label) = $match;
449 // Ensure all URLs are absolute.
450 $urls[] = strpos($url, '://') ? $url : preg_replace($regexp, $base_url . '/', $url);
451 return $label . ' [' . count($urls) . ']';
452 }
453 }
454 return $urls;
455 }
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 */
462 function _drupal_html_to_text_clean($indent) {
463 return preg_replace('/[^>]/', ' ', $indent);
464 }
465
466 /**
467 * Helper function for drupal_html_to_text().
468 *
469 * Pad the last line with the given character.
470 */
471 function _drupal_html_to_text_pad($text, $pad, $prefix = '') {
472 // Remove last line break.
473 $text = substr($text, 0, -1);
474 // Calculate needed padding space and add it.
475 if (($p = strrpos($text, "\n")) === FALSE) {
476 $p = -1;
477 }
478 $n = max(0, 79 - (strlen($text) - $p));
479 // Add prefix and padding, and restore linebreak.
480 return $text . $prefix . str_repeat($pad, $n - strlen($prefix)) . "\n";
481 }
482

Legend

Missed
lines code that were not excersized during program execution.
Covered
lines code were excersized during program execution.
Comment/non executable
Comment or non-executable line of code.
Dead
lines of code that according to xdebug could not be executed. This is counted as coverage code because in almost all cases it is code that runnable.