Mercurial > defr > drupal > core
comparison includes/mail.inc @ 1:c1f4ac30525a 6.0
Drupal 6.0
| author | Franck Deroche <webmaster@defr.org> |
|---|---|
| date | Tue, 23 Dec 2008 14:28:28 +0100 |
| parents | |
| children | 2427550111ae |
comparison
equal
deleted
inserted
replaced
| 0:5a113a1c4740 | 1:c1f4ac30525a |
|---|---|
| 1 <?php | |
| 2 // $Id: mail.inc,v 1.8 2008/01/25 17:04:00 goba 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 */ | |
| 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['Reply-To'] = $headers['Sender'] = $headers['Return-Path'] = $headers['Errors-To'] = $default_from; | |
| 109 } | |
| 110 if ($from) { | |
| 111 $headers['From'] = $headers['Reply-To'] = $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 (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 admin, 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 './'. 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 join("\n", $mimeheaders) | |
| 191 ); | |
| 192 } | |
| 193 } | |
| 194 | |
| 195 /** | |
| 196 * Perform format=flowed soft wrapping for mail (RFC 3676). | |
| 197 * | |
| 198 * We use delsp=yes wrapping, but only break non-spaced languages when | |
| 199 * absolutely necessary to avoid compatibility issues. | |
| 200 * | |
| 201 * We deliberately use LF rather than CRLF, see drupal_mail(). | |
| 202 * | |
| 203 * @param $text | |
| 204 * The plain text to process. | |
| 205 * @param $indent (optional) | |
| 206 * A string to indent the text with. Only '>' characters are repeated on | |
| 207 * subsequent wrapped lines. Others are replaced by spaces. | |
| 208 */ | |
| 209 function drupal_wrap_mail($text, $indent = '') { | |
| 210 // Convert CRLF into LF. | |
| 211 $text = str_replace("\r", '', $text); | |
| 212 // See if soft-wrapping is allowed. | |
| 213 $clean_indent = _drupal_html_to_text_clean($indent); | |
| 214 $soft = strpos($clean_indent, ' ') === FALSE; | |
| 215 // Check if the string has line breaks. | |
| 216 if (strpos($text, "\n") !== FALSE) { | |
| 217 // Remove trailing spaces to make existing breaks hard. | |
| 218 $text = preg_replace('/ +\n/m', "\n", $text); | |
| 219 // Wrap each line at the needed width. | |
| 220 $lines = explode("\n", $text); | |
| 221 array_walk($lines, '_drupal_wrap_mail_line', array('soft' => $soft, 'length' => strlen($indent))); | |
| 222 $text = implode("\n", $lines); | |
| 223 } | |
| 224 else { | |
| 225 // Wrap this line. | |
| 226 _drupal_wrap_mail_line($text, 0, array('soft' => $soft, 'length' => strlen($indent))); | |
| 227 } | |
| 228 // Empty lines with nothing but spaces. | |
| 229 $text = preg_replace('/^ +\n/m', "\n", $text); | |
| 230 // Space-stuff special lines. | |
| 231 $text = preg_replace('/^(>| |From)/m', ' $1', $text); | |
| 232 // Apply indentation. We only include non-'>' indentation on the first line. | |
| 233 $text = $indent . substr(preg_replace('/^/m', $clean_indent, $text), strlen($indent)); | |
| 234 | |
| 235 return $text; | |
| 236 } | |
| 237 | |
| 238 /** | |
| 239 * Transform an HTML string into plain text, preserving the structure of the | |
| 240 * markup. Useful for preparing the body of a node to be sent by e-mail. | |
| 241 * | |
| 242 * The output will be suitable for use as 'format=flowed; delsp=yes' text | |
| 243 * (RFC 3676) and can be passed directly to drupal_mail() for sending. | |
| 244 * | |
| 245 * We deliberately use LF rather than CRLF, see drupal_mail(). | |
| 246 * | |
| 247 * This function provides suitable alternatives for the following tags: | |
| 248 * <a> <em> <i> <strong> <b> <br> <p> <blockquote> <ul> <ol> <li> <dl> <dt> | |
| 249 * <dd> <h1> <h2> <h3> <h4> <h5> <h6> <hr> | |
| 250 * | |
| 251 * @param $string | |
| 252 * The string to be transformed. | |
| 253 * @param $allowed_tags (optional) | |
| 254 * If supplied, a list of tags that will be transformed. If omitted, all | |
| 255 * all supported tags are transformed. | |
| 256 * @return | |
| 257 * The transformed string. | |
| 258 */ | |
| 259 function drupal_html_to_text($string, $allowed_tags = NULL) { | |
| 260 // Cache list of supported tags. | |
| 261 static $supported_tags; | |
| 262 if (empty($supported_tags)) { | |
| 263 $supported_tags = array('a', 'em', 'i', 'strong', 'b', 'br', 'p', 'blockquote', 'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr'); | |
| 264 } | |
| 265 | |
| 266 // Make sure only supported tags are kept. | |
| 267 $allowed_tags = isset($allowed_tags) ? array_intersect($supported_tags, $allowed_tags) : $supported_tags; | |
| 268 | |
| 269 // Make sure tags, entities and attributes are well-formed and properly nested. | |
| 270 $string = _filter_htmlcorrector(filter_xss($string, $allowed_tags)); | |
| 271 | |
| 272 // Apply inline styles. | |
| 273 $string = preg_replace('!</?(em|i)>!i', '/', $string); | |
| 274 $string = preg_replace('!</?(strong|b)>!i', '*', $string); | |
| 275 | |
| 276 // Replace inline <a> tags with the text of link and a footnote. | |
| 277 // 'See <a href="http://drupal.org">the Drupal site</a>' becomes | |
| 278 // 'See the Drupal site [1]' with the URL included as a footnote. | |
| 279 _drupal_html_to_mail_urls(NULL, TRUE); | |
| 280 $pattern = '@(<a[^>]+?href="([^"]*)"[^>]*?>(.+?)</a>)@i'; | |
| 281 $string = preg_replace_callback($pattern, '_drupal_html_to_mail_urls', $string); | |
| 282 $urls = _drupal_html_to_mail_urls(); | |
| 283 $footnotes = ''; | |
| 284 if (count($urls)) { | |
| 285 $footnotes .= "\n"; | |
| 286 for ($i = 0, $max = count($urls); $i < $max; $i++) { | |
| 287 $footnotes .= '['. ($i + 1) .'] '. $urls[$i] ."\n"; | |
| 288 } | |
| 289 } | |
| 290 | |
| 291 // Split tags from text. | |
| 292 $split = preg_split('/<([^>]+?)>/', $string, -1, PREG_SPLIT_DELIM_CAPTURE); | |
| 293 // Note: PHP ensures the array consists of alternating delimiters and literals | |
| 294 // and begins and ends with a literal (inserting $null as required). | |
| 295 | |
| 296 $tag = FALSE; // Odd/even counter (tag or no tag) | |
| 297 $casing = NULL; // Case conversion function | |
| 298 $output = ''; | |
| 299 $indent = array(); // All current indentation string chunks | |
| 300 $lists = array(); // Array of counters for opened lists | |
| 301 foreach ($split as $value) { | |
| 302 $chunk = NULL; // Holds a string ready to be formatted and output. | |
| 303 | |
| 304 // Process HTML tags (but don't output any literally). | |
| 305 if ($tag) { | |
| 306 list($tagname) = explode(' ', strtolower($value), 2); | |
| 307 switch ($tagname) { | |
| 308 // List counters | |
| 309 case 'ul': | |
| 310 array_unshift($lists, '*'); | |
| 311 break; | |
| 312 case 'ol': | |
| 313 array_unshift($lists, 1); | |
| 314 break; | |
| 315 case '/ul': | |
| 316 case '/ol': | |
| 317 array_shift($lists); | |
| 318 $chunk = ''; // Ensure blank new-line. | |
| 319 break; | |
| 320 | |
| 321 // Quotation/list markers, non-fancy headers | |
| 322 case 'blockquote': | |
| 323 // Format=flowed indentation cannot be mixed with lists. | |
| 324 $indent[] = count($lists) ? ' "' : '>'; | |
| 325 break; | |
| 326 case 'li': | |
| 327 $indent[] = is_numeric($lists[0]) ? ' '. $lists[0]++ .') ' : ' * '; | |
| 328 break; | |
| 329 case 'dd': | |
| 330 $indent[] = ' '; | |
| 331 break; | |
| 332 case 'h3': | |
| 333 $indent[] = '.... '; | |
| 334 break; | |
| 335 case 'h4': | |
| 336 $indent[] = '.. '; | |
| 337 break; | |
| 338 case '/blockquote': | |
| 339 if (count($lists)) { | |
| 340 // Append closing quote for inline quotes (immediately). | |
| 341 $output = rtrim($output, "> \n") ."\"\n"; | |
| 342 $chunk = ''; // Ensure blank new-line. | |
| 343 } | |
| 344 // Fall-through | |
| 345 case '/li': | |
| 346 case '/dd': | |
| 347 array_pop($indent); | |
| 348 break; | |
| 349 case '/h3': | |
| 350 case '/h4': | |
| 351 array_pop($indent); | |
| 352 case '/h5': | |
| 353 case '/h6': | |
| 354 $chunk = ''; // Ensure blank new-line. | |
| 355 break; | |
| 356 | |
| 357 // Fancy headers | |
| 358 case 'h1': | |
| 359 $indent[] = '======== '; | |
| 360 $casing = 'drupal_strtoupper'; | |
| 361 break; | |
| 362 case 'h2': | |
| 363 $indent[] = '-------- '; | |
| 364 $casing = 'drupal_strtoupper'; | |
| 365 break; | |
| 366 case '/h1': | |
| 367 case '/h2': | |
| 368 $casing = NULL; | |
| 369 // Pad the line with dashes. | |
| 370 $output = _drupal_html_to_text_pad($output, ($tagname == '/h1') ? '=' : '-', ' '); | |
| 371 array_pop($indent); | |
| 372 $chunk = ''; // Ensure blank new-line. | |
| 373 break; | |
| 374 | |
| 375 // Horizontal rulers | |
| 376 case 'hr': | |
| 377 // Insert immediately. | |
| 378 $output .= drupal_wrap_mail('', implode('', $indent)) ."\n"; | |
| 379 $output = _drupal_html_to_text_pad($output, '-'); | |
| 380 break; | |
| 381 | |
| 382 // Paragraphs and definition lists | |
| 383 case '/p': | |
| 384 case '/dl': | |
| 385 $chunk = ''; // Ensure blank new-line. | |
| 386 break; | |
| 387 } | |
| 388 } | |
| 389 // Process blocks of text. | |
| 390 else { | |
| 391 // Convert inline HTML text to plain text. | |
| 392 $value = trim(preg_replace('/\s+/', ' ', decode_entities($value))); | |
| 393 if (strlen($value)) { | |
| 394 $chunk = $value; | |
| 395 } | |
| 396 } | |
| 397 | |
| 398 // See if there is something waiting to be output. | |
| 399 if (isset($chunk)) { | |
| 400 // Apply any necessary case conversion. | |
| 401 if (isset($casing)) { | |
| 402 $chunk = $casing($chunk); | |
| 403 } | |
| 404 // Format it and apply the current indentation. | |
| 405 $output .= drupal_wrap_mail($chunk, implode('', $indent)) ."\n"; | |
| 406 // Remove non-quotation markers from indentation. | |
| 407 $indent = array_map('_drupal_html_to_text_clean', $indent); | |
| 408 } | |
| 409 | |
| 410 $tag = !$tag; | |
| 411 } | |
| 412 | |
| 413 return $output . $footnotes; | |
| 414 } | |
| 415 | |
| 416 /** | |
| 417 * Helper function for array_walk in drupal_wrap_mail(). | |
| 418 * | |
| 419 * Wraps words on a single line. | |
| 420 */ | |
| 421 function _drupal_wrap_mail_line(&$line, $key, $values) { | |
| 422 // Use soft-breaks only for purely quoted or unindented text. | |
| 423 $line = wordwrap($line, 77 - $values['length'], $values['soft'] ? " \n" : "\n"); | |
| 424 // Break really long words at the maximum width allowed. | |
| 425 $line = wordwrap($line, 996 - $values['length'], $values['soft'] ? " \n" : "\n"); | |
| 426 } | |
| 427 | |
| 428 /** | |
| 429 * Helper function for drupal_html_to_text(). | |
| 430 * | |
| 431 * Keeps track of URLs and replaces them with placeholder tokens. | |
| 432 */ | |
| 433 function _drupal_html_to_mail_urls($match = NULL, $reset = FALSE) { | |
| 434 global $base_url, $base_path; | |
| 435 static $urls = array(), $regexp; | |
| 436 | |
| 437 if ($reset) { | |
| 438 // Reset internal URL list. | |
| 439 $urls = array(); | |
| 440 } | |
| 441 else { | |
| 442 if (empty($regexp)) { | |
| 443 $regexp = '@^'. preg_quote($base_path, '@') .'@'; | |
| 444 } | |
| 445 if ($match) { | |
| 446 list(, , $url, $label) = $match; | |
| 447 // Ensure all URLs are absolute. | |
| 448 $urls[] = strpos($url, '://') ? $url : preg_replace($regexp, $base_url .'/', $url); | |
| 449 return $label .' ['. count($urls) .']'; | |
| 450 } | |
| 451 } | |
| 452 return $urls; | |
| 453 } | |
| 454 | |
| 455 /** | |
| 456 * Helper function for drupal_wrap_mail() and drupal_html_to_text(). | |
| 457 * | |
| 458 * Replace all non-quotation markers from a given piece of indentation with spaces. | |
| 459 */ | |
| 460 function _drupal_html_to_text_clean($indent) { | |
| 461 return preg_replace('/[^>]/', ' ', $indent); | |
| 462 } | |
| 463 | |
| 464 /** | |
| 465 * Helper function for drupal_html_to_text(). | |
| 466 * | |
| 467 * Pad the last line with the given character. | |
| 468 */ | |
| 469 function _drupal_html_to_text_pad($text, $pad, $prefix = '') { | |
| 470 // Remove last line break. | |
| 471 $text = substr($text, 0, -1); | |
| 472 // Calculate needed padding space and add it. | |
| 473 if (($p = strrpos($text, "\n")) === FALSE) { | |
| 474 $p = -1; | |
| 475 } | |
| 476 $n = max(0, 79 - (strlen($text) - $p)); | |
| 477 // Add prefix and padding, and restore linebreak. | |
| 478 return $text . $prefix . str_repeat($pad, $n - strlen($prefix)) ."\n"; | |
| 479 } |
