|
1 <?php |
|
2 |
|
3 /** |
|
4 * @file |
|
5 * API functions for processing and sending e-mail. |
|
6 */ |
|
7 |
|
8 /** |
|
9 * Auto-detect appropriate line endings for e-mails. |
|
10 * |
|
11 * $conf['mail_line_endings'] will override this setting. |
|
12 */ |
|
13 define('MAIL_LINE_ENDINGS', isset($_SERVER['WINDIR']) || (isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'Win32') !== FALSE) ? "\r\n" : "\n"); |
|
14 |
|
15 /** |
|
16 * Composes and optionally sends an e-mail message. |
|
17 * |
|
18 * Sending an e-mail works with defining an e-mail template (subject, text |
|
19 * and possibly e-mail headers) and the replacement values to use in the |
|
20 * appropriate places in the template. Processed e-mail templates are |
|
21 * requested from hook_mail() from the module sending the e-mail. Any module |
|
22 * can modify the composed e-mail message array using hook_mail_alter(). |
|
23 * Finally drupal_mail_system()->mail() sends the e-mail, which can |
|
24 * be reused if the exact same composed e-mail is to be sent to multiple |
|
25 * recipients. |
|
26 * |
|
27 * Finding out what language to send the e-mail with needs some consideration. |
|
28 * If you send e-mail to a user, her preferred language should be fine, so |
|
29 * use user_preferred_language(). If you send email based on form values |
|
30 * filled on the page, there are two additional choices if you are not |
|
31 * sending the e-mail to a user on the site. You can either use the language |
|
32 * used to generate the page ($language global variable) or the site default |
|
33 * language. See language_default(). The former is good if sending e-mail to |
|
34 * the person filling the form, the later is good if you send e-mail to an |
|
35 * address previously set up (like contact addresses in a contact form). |
|
36 * |
|
37 * Taking care of always using the proper language is even more important |
|
38 * when sending e-mails in a row to multiple users. Hook_mail() abstracts |
|
39 * whether the mail text comes from an administrator setting or is |
|
40 * static in the source code. It should also deal with common mail tokens, |
|
41 * only receiving $params which are unique to the actual e-mail at hand. |
|
42 * |
|
43 * An example: |
|
44 * |
|
45 * @code |
|
46 * function example_notify($accounts) { |
|
47 * foreach ($accounts as $account) { |
|
48 * $params['account'] = $account; |
|
49 * // example_mail() will be called based on the first drupal_mail() parameter. |
|
50 * drupal_mail('example', 'notice', $account->mail, user_preferred_language($account), $params); |
|
51 * } |
|
52 * } |
|
53 * |
|
54 * function example_mail($key, &$message, $params) { |
|
55 * $data['user'] = $params['account']; |
|
56 * $options['language'] = $message['language']; |
|
57 * user_mail_tokens($variables, $data, $options); |
|
58 * switch($key) { |
|
59 * case 'notice': |
|
60 * // If the recipient can receive such notices by instant-message, do |
|
61 * // not send by email. |
|
62 * if (example_im_send($key, $message, $params)) { |
|
63 * $message['send'] = FALSE; |
|
64 * break; |
|
65 * } |
|
66 * $langcode = $message['language']->language; |
|
67 * $message['subject'] = t('Notification from !site', $variables, array('langcode' => $langcode)); |
|
68 * $message['body'][] = t("Dear !username\n\nThere is new content available on the site.", $variables, array('langcode' => $langcode)); |
|
69 * break; |
|
70 * } |
|
71 * } |
|
72 * @endcode |
|
73 * |
|
74 * Another example, which uses drupal_mail() to format a message for sending |
|
75 * later: |
|
76 * |
|
77 * @code |
|
78 * $params = array('current_conditions' => $data); |
|
79 * $to = 'user@example.com'; |
|
80 * $message = drupal_mail('example', 'notice', $to, $language, $params, FALSE); |
|
81 * // Only add to the spool if sending was not canceled. |
|
82 * if ($message['send']) { |
|
83 * example_spool_message($message); |
|
84 * } |
|
85 * @endcode |
|
86 * |
|
87 * @param $module |
|
88 * A module name to invoke hook_mail() on. The {$module}_mail() hook will be |
|
89 * called to complete the $message structure which will already contain common |
|
90 * defaults. |
|
91 * @param $key |
|
92 * A key to identify the e-mail sent. The final e-mail id for e-mail altering |
|
93 * will be {$module}_{$key}. |
|
94 * @param $to |
|
95 * The e-mail address or addresses where the message will be sent to. The |
|
96 * formatting of this string will be validated with the |
|
97 * @link http://php.net/manual/filter.filters.validate.php PHP e-mail validation filter. @endlink |
|
98 * Some examples are: |
|
99 * - user@example.com |
|
100 * - user@example.com, anotheruser@example.com |
|
101 * - User <user@example.com> |
|
102 * - User <user@example.com>, Another User <anotheruser@example.com> |
|
103 * @param $language |
|
104 * Language object to use to compose the e-mail. |
|
105 * @param $params |
|
106 * Optional parameters to build the e-mail. |
|
107 * @param $from |
|
108 * Sets From to this value, if given. |
|
109 * @param $send |
|
110 * If TRUE, drupal_mail() will call drupal_mail_system()->mail() to deliver |
|
111 * the message, and store the result in $message['result']. Modules |
|
112 * implementing hook_mail_alter() may cancel sending by setting |
|
113 * $message['send'] to FALSE. |
|
114 * |
|
115 * @return |
|
116 * The $message array structure containing all details of the |
|
117 * message. If already sent ($send = TRUE), then the 'result' element |
|
118 * will contain the success indicator of the e-mail, failure being already |
|
119 * written to the watchdog. (Success means nothing more than the message being |
|
120 * accepted at php-level, which still doesn't guarantee it to be delivered.) |
|
121 */ |
|
122 function drupal_mail($module, $key, $to, $language, $params = array(), $from = NULL, $send = TRUE) { |
|
123 $default_from = variable_get('site_mail', ini_get('sendmail_from')); |
|
124 |
|
125 // Bundle up the variables into a structured array for altering. |
|
126 $message = array( |
|
127 'id' => $module . '_' . $key, |
|
128 'module' => $module, |
|
129 'key' => $key, |
|
130 'to' => $to, |
|
131 'from' => isset($from) ? $from : $default_from, |
|
132 'language' => $language, |
|
133 'params' => $params, |
|
134 'send' => TRUE, |
|
135 'subject' => '', |
|
136 'body' => array() |
|
137 ); |
|
138 |
|
139 // Build the default headers |
|
140 $headers = array( |
|
141 'MIME-Version' => '1.0', |
|
142 'Content-Type' => 'text/plain; charset=UTF-8; format=flowed; delsp=yes', |
|
143 'Content-Transfer-Encoding' => '8Bit', |
|
144 'X-Mailer' => 'Drupal' |
|
145 ); |
|
146 if ($default_from) { |
|
147 // To prevent e-mail from looking like spam, the addresses in the Sender and |
|
148 // Return-Path headers should have a domain authorized to use the originating |
|
149 // SMTP server. |
|
150 $headers['From'] = $headers['Sender'] = $headers['Return-Path'] = $default_from; |
|
151 } |
|
152 if ($from) { |
|
153 $headers['From'] = $from; |
|
154 } |
|
155 $message['headers'] = $headers; |
|
156 |
|
157 // Build the e-mail (get subject and body, allow additional headers) by |
|
158 // invoking hook_mail() on this module. We cannot use module_invoke() as |
|
159 // we need to have $message by reference in hook_mail(). |
|
160 if (function_exists($function = $module . '_mail')) { |
|
161 $function($key, $message, $params); |
|
162 } |
|
163 |
|
164 // Invoke hook_mail_alter() to allow all modules to alter the resulting e-mail. |
|
165 drupal_alter('mail', $message); |
|
166 |
|
167 // Retrieve the responsible implementation for this message. |
|
168 $system = drupal_mail_system($module, $key); |
|
169 |
|
170 // Format the message body. |
|
171 $message = $system->format($message); |
|
172 |
|
173 // Optionally send e-mail. |
|
174 if ($send) { |
|
175 // The original caller requested sending. Sending was canceled by one or |
|
176 // more hook_mail_alter() implementations. We set 'result' to NULL, because |
|
177 // FALSE indicates an error in sending. |
|
178 if (empty($message['send'])) { |
|
179 $message['result'] = NULL; |
|
180 } |
|
181 // Sending was originally requested and was not canceled. |
|
182 else { |
|
183 $message['result'] = $system->mail($message); |
|
184 // Log errors. |
|
185 if (!$message['result']) { |
|
186 watchdog('mail', 'Error sending e-mail (from %from to %to).', array('%from' => $message['from'], '%to' => $message['to']), WATCHDOG_ERROR); |
|
187 drupal_set_message(t('Unable to send e-mail. Contact the site administrator if the problem persists.'), 'error'); |
|
188 } |
|
189 } |
|
190 } |
|
191 |
|
192 return $message; |
|
193 } |
|
194 |
|
195 /** |
|
196 * Returns an object that implements the MailSystemInterface interface. |
|
197 * |
|
198 * Allows for one or more custom mail backends to format and send mail messages |
|
199 * composed using drupal_mail(). |
|
200 * |
|
201 * An implementation needs to implement the following methods: |
|
202 * - format: Allows to preprocess, format, and postprocess a mail |
|
203 * message before it is passed to the sending system. By default, all messages |
|
204 * may contain HTML and are converted to plain-text by the DefaultMailSystem |
|
205 * implementation. For example, an alternative implementation could override |
|
206 * the default implementation and additionally sanitize the HTML for usage in |
|
207 * a MIME-encoded e-mail, but still invoking the DefaultMailSystem |
|
208 * implementation to generate an alternate plain-text version for sending. |
|
209 * - mail: Sends a message through a custom mail sending engine. |
|
210 * By default, all messages are sent via PHP's mail() function by the |
|
211 * DefaultMailSystem implementation. |
|
212 * |
|
213 * The selection of a particular implementation is controlled via the variable |
|
214 * 'mail_system', which is a keyed array. The default implementation |
|
215 * is the class whose name is the value of 'default-system' key. A more specific |
|
216 * match first to key and then to module will be used in preference to the |
|
217 * default. To specify a different class for all mail sent by one module, set |
|
218 * the class name as the value for the key corresponding to the module name. To |
|
219 * specify a class for a particular message sent by one module, set the class |
|
220 * name as the value for the array key that is the message id, which is |
|
221 * "${module}_${key}". |
|
222 * |
|
223 * For example to debug all mail sent by the user module by logging it to a |
|
224 * file, you might set the variable as something like: |
|
225 * |
|
226 * @code |
|
227 * array( |
|
228 * 'default-system' => 'DefaultMailSystem', |
|
229 * 'user' => 'DevelMailLog', |
|
230 * ); |
|
231 * @endcode |
|
232 * |
|
233 * Finally, a different system can be specified for a specific e-mail ID (see |
|
234 * the $key param), such as one of the keys used by the contact module: |
|
235 * |
|
236 * @code |
|
237 * array( |
|
238 * 'default-system' => 'DefaultMailSystem', |
|
239 * 'user' => 'DevelMailLog', |
|
240 * 'contact_page_autoreply' => 'DrupalDevNullMailSend', |
|
241 * ); |
|
242 * @endcode |
|
243 * |
|
244 * Other possible uses for system include a mail-sending class that actually |
|
245 * sends (or duplicates) each message to SMS, Twitter, instant message, etc, or |
|
246 * a class that queues up a large number of messages for more efficient bulk |
|
247 * sending or for sending via a remote gateway so as to reduce the load |
|
248 * on the local server. |
|
249 * |
|
250 * @param $module |
|
251 * The module name which was used by drupal_mail() to invoke hook_mail(). |
|
252 * @param $key |
|
253 * A key to identify the e-mail sent. The final e-mail ID for the e-mail |
|
254 * alter hook in drupal_mail() would have been {$module}_{$key}. |
|
255 * |
|
256 * @return MailSystemInterface |
|
257 */ |
|
258 function drupal_mail_system($module, $key) { |
|
259 $instances = &drupal_static(__FUNCTION__, array()); |
|
260 |
|
261 $id = $module . '_' . $key; |
|
262 |
|
263 $configuration = variable_get('mail_system', array('default-system' => 'DefaultMailSystem')); |
|
264 |
|
265 // Look for overrides for the default class, starting from the most specific |
|
266 // id, and falling back to the module name. |
|
267 if (isset($configuration[$id])) { |
|
268 $class = $configuration[$id]; |
|
269 } |
|
270 elseif (isset($configuration[$module])) { |
|
271 $class = $configuration[$module]; |
|
272 } |
|
273 else { |
|
274 $class = $configuration['default-system']; |
|
275 } |
|
276 |
|
277 if (empty($instances[$class])) { |
|
278 $interfaces = class_implements($class); |
|
279 if (isset($interfaces['MailSystemInterface'])) { |
|
280 $instances[$class] = new $class(); |
|
281 } |
|
282 else { |
|
283 throw new Exception(t('Class %class does not implement interface %interface', array('%class' => $class, '%interface' => 'MailSystemInterface'))); |
|
284 } |
|
285 } |
|
286 return $instances[$class]; |
|
287 } |
|
288 |
|
289 /** |
|
290 * An interface for pluggable mail back-ends. |
|
291 */ |
|
292 interface MailSystemInterface { |
|
293 /** |
|
294 * Format a message composed by drupal_mail() prior sending. |
|
295 * |
|
296 * @param $message |
|
297 * A message array, as described in hook_mail_alter(). |
|
298 * |
|
299 * @return |
|
300 * The formatted $message. |
|
301 */ |
|
302 public function format(array $message); |
|
303 |
|
304 /** |
|
305 * Send a message composed by drupal_mail(). |
|
306 * |
|
307 * @param $message |
|
308 * Message array with at least the following elements: |
|
309 * - id: A unique identifier of the e-mail type. Examples: 'contact_user_copy', |
|
310 * 'user_password_reset'. |
|
311 * - to: The mail address or addresses where the message will be sent to. |
|
312 * The formatting of this string will be validated with the |
|
313 * @link http://php.net/manual/filter.filters.validate.php PHP e-mail validation filter. @endlink |
|
314 * Some examples are: |
|
315 * - user@example.com |
|
316 * - user@example.com, anotheruser@example.com |
|
317 * - User <user@example.com> |
|
318 * - User <user@example.com>, Another User <anotheruser@example.com> |
|
319 * - subject: Subject of the e-mail to be sent. This must not contain any |
|
320 * newline characters, or the mail may not be sent properly. |
|
321 * - body: Message to be sent. Accepts both CRLF and LF line-endings. |
|
322 * E-mail bodies must be wrapped. You can use drupal_wrap_mail() for |
|
323 * smart plain text wrapping. |
|
324 * - headers: Associative array containing all additional mail headers not |
|
325 * defined by one of the other parameters. PHP's mail() looks for Cc and |
|
326 * Bcc headers and sends the mail to addresses in these headers too. |
|
327 * |
|
328 * @return |
|
329 * TRUE if the mail was successfully accepted for delivery, otherwise FALSE. |
|
330 */ |
|
331 public function mail(array $message); |
|
332 } |
|
333 |
|
334 /** |
|
335 * Performs format=flowed soft wrapping for mail (RFC 3676). |
|
336 * |
|
337 * We use delsp=yes wrapping, but only break non-spaced languages when |
|
338 * absolutely necessary to avoid compatibility issues. |
|
339 * |
|
340 * We deliberately use LF rather than CRLF, see drupal_mail(). |
|
341 * |
|
342 * @param string $text |
|
343 * The plain text to process. |
|
344 * @param string $indent (optional) |
|
345 * A string to indent the text with. Only '>' characters are repeated on |
|
346 * subsequent wrapped lines. Others are replaced by spaces. |
|
347 * |
|
348 * @return string |
|
349 * The content of the email as a string with formatting applied. |
|
350 */ |
|
351 function drupal_wrap_mail($text, $indent = '') { |
|
352 // Convert CRLF into LF. |
|
353 $text = str_replace("\r", '', $text); |
|
354 // See if soft-wrapping is allowed. |
|
355 $clean_indent = _drupal_html_to_text_clean($indent); |
|
356 $soft = strpos($clean_indent, ' ') === FALSE; |
|
357 // Check if the string has line breaks. |
|
358 if (strpos($text, "\n") !== FALSE) { |
|
359 // Remove trailing spaces to make existing breaks hard, but leave signature |
|
360 // marker untouched (RFC 3676, Section 4.3). |
|
361 $text = preg_replace('/(?(?<!^--) +\n| +\n)/m', "\n", $text); |
|
362 // Wrap each line at the needed width. |
|
363 $lines = explode("\n", $text); |
|
364 array_walk($lines, '_drupal_wrap_mail_line', array('soft' => $soft, 'length' => strlen($indent))); |
|
365 $text = implode("\n", $lines); |
|
366 } |
|
367 else { |
|
368 // Wrap this line. |
|
369 _drupal_wrap_mail_line($text, 0, array('soft' => $soft, 'length' => strlen($indent))); |
|
370 } |
|
371 // Empty lines with nothing but spaces. |
|
372 $text = preg_replace('/^ +\n/m', "\n", $text); |
|
373 // Space-stuff special lines. |
|
374 $text = preg_replace('/^(>| |From)/m', ' $1', $text); |
|
375 // Apply indentation. We only include non-'>' indentation on the first line. |
|
376 $text = $indent . substr(preg_replace('/^/m', $clean_indent, $text), strlen($indent)); |
|
377 |
|
378 return $text; |
|
379 } |
|
380 |
|
381 /** |
|
382 * Transforms an HTML string into plain text, preserving its structure. |
|
383 * |
|
384 * The output will be suitable for use as 'format=flowed; delsp=yes' text |
|
385 * (RFC 3676) and can be passed directly to drupal_mail() for sending. |
|
386 * |
|
387 * We deliberately use LF rather than CRLF, see drupal_mail(). |
|
388 * |
|
389 * This function provides suitable alternatives for the following tags: |
|
390 * <a> <em> <i> <strong> <b> <br> <p> <blockquote> <ul> <ol> <li> <dl> <dt> |
|
391 * <dd> <h1> <h2> <h3> <h4> <h5> <h6> <hr> |
|
392 * |
|
393 * @param $string |
|
394 * The string to be transformed. |
|
395 * @param $allowed_tags (optional) |
|
396 * If supplied, a list of tags that will be transformed. If omitted, all |
|
397 * all supported tags are transformed. |
|
398 * |
|
399 * @return |
|
400 * The transformed string. |
|
401 */ |
|
402 function drupal_html_to_text($string, $allowed_tags = NULL) { |
|
403 // Cache list of supported tags. |
|
404 static $supported_tags; |
|
405 if (empty($supported_tags)) { |
|
406 $supported_tags = array('a', 'em', 'i', 'strong', 'b', 'br', 'p', 'blockquote', 'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr'); |
|
407 } |
|
408 |
|
409 // Make sure only supported tags are kept. |
|
410 $allowed_tags = isset($allowed_tags) ? array_intersect($supported_tags, $allowed_tags) : $supported_tags; |
|
411 |
|
412 // Make sure tags, entities and attributes are well-formed and properly nested. |
|
413 $string = _filter_htmlcorrector(filter_xss($string, $allowed_tags)); |
|
414 |
|
415 // Apply inline styles. |
|
416 $string = preg_replace('!</?(em|i)((?> +)[^>]*)?>!i', '/', $string); |
|
417 $string = preg_replace('!</?(strong|b)((?> +)[^>]*)?>!i', '*', $string); |
|
418 |
|
419 // Replace inline <a> tags with the text of link and a footnote. |
|
420 // 'See <a href="http://drupal.org">the Drupal site</a>' becomes |
|
421 // 'See the Drupal site [1]' with the URL included as a footnote. |
|
422 _drupal_html_to_mail_urls(NULL, TRUE); |
|
423 $pattern = '@(<a[^>]+?href="([^"]*)"[^>]*?>(.+?)</a>)@i'; |
|
424 $string = preg_replace_callback($pattern, '_drupal_html_to_mail_urls', $string); |
|
425 $urls = _drupal_html_to_mail_urls(); |
|
426 $footnotes = ''; |
|
427 if (count($urls)) { |
|
428 $footnotes .= "\n"; |
|
429 for ($i = 0, $max = count($urls); $i < $max; $i++) { |
|
430 $footnotes .= '[' . ($i + 1) . '] ' . $urls[$i] . "\n"; |
|
431 } |
|
432 } |
|
433 |
|
434 // Split tags from text. |
|
435 $split = preg_split('/<([^>]+?)>/', $string, -1, PREG_SPLIT_DELIM_CAPTURE); |
|
436 // Note: PHP ensures the array consists of alternating delimiters and literals |
|
437 // and begins and ends with a literal (inserting $null as required). |
|
438 |
|
439 $tag = FALSE; // Odd/even counter (tag or no tag) |
|
440 $casing = NULL; // Case conversion function |
|
441 $output = ''; |
|
442 $indent = array(); // All current indentation string chunks |
|
443 $lists = array(); // Array of counters for opened lists |
|
444 foreach ($split as $value) { |
|
445 $chunk = NULL; // Holds a string ready to be formatted and output. |
|
446 |
|
447 // Process HTML tags (but don't output any literally). |
|
448 if ($tag) { |
|
449 list($tagname) = explode(' ', strtolower($value), 2); |
|
450 switch ($tagname) { |
|
451 // List counters |
|
452 case 'ul': |
|
453 array_unshift($lists, '*'); |
|
454 break; |
|
455 case 'ol': |
|
456 array_unshift($lists, 1); |
|
457 break; |
|
458 case '/ul': |
|
459 case '/ol': |
|
460 array_shift($lists); |
|
461 $chunk = ''; // Ensure blank new-line. |
|
462 break; |
|
463 |
|
464 // Quotation/list markers, non-fancy headers |
|
465 case 'blockquote': |
|
466 // Format=flowed indentation cannot be mixed with lists. |
|
467 $indent[] = count($lists) ? ' "' : '>'; |
|
468 break; |
|
469 case 'li': |
|
470 $indent[] = isset($lists[0]) && is_numeric($lists[0]) ? ' ' . $lists[0]++ . ') ' : ' * '; |
|
471 break; |
|
472 case 'dd': |
|
473 $indent[] = ' '; |
|
474 break; |
|
475 case 'h3': |
|
476 $indent[] = '.... '; |
|
477 break; |
|
478 case 'h4': |
|
479 $indent[] = '.. '; |
|
480 break; |
|
481 case '/blockquote': |
|
482 if (count($lists)) { |
|
483 // Append closing quote for inline quotes (immediately). |
|
484 $output = rtrim($output, "> \n") . "\"\n"; |
|
485 $chunk = ''; // Ensure blank new-line. |
|
486 } |
|
487 // Fall-through |
|
488 case '/li': |
|
489 case '/dd': |
|
490 array_pop($indent); |
|
491 break; |
|
492 case '/h3': |
|
493 case '/h4': |
|
494 array_pop($indent); |
|
495 case '/h5': |
|
496 case '/h6': |
|
497 $chunk = ''; // Ensure blank new-line. |
|
498 break; |
|
499 |
|
500 // Fancy headers |
|
501 case 'h1': |
|
502 $indent[] = '======== '; |
|
503 $casing = 'drupal_strtoupper'; |
|
504 break; |
|
505 case 'h2': |
|
506 $indent[] = '-------- '; |
|
507 $casing = 'drupal_strtoupper'; |
|
508 break; |
|
509 case '/h1': |
|
510 case '/h2': |
|
511 $casing = NULL; |
|
512 // Pad the line with dashes. |
|
513 $output = _drupal_html_to_text_pad($output, ($tagname == '/h1') ? '=' : '-', ' '); |
|
514 array_pop($indent); |
|
515 $chunk = ''; // Ensure blank new-line. |
|
516 break; |
|
517 |
|
518 // Horizontal rulers |
|
519 case 'hr': |
|
520 // Insert immediately. |
|
521 $output .= drupal_wrap_mail('', implode('', $indent)) . "\n"; |
|
522 $output = _drupal_html_to_text_pad($output, '-'); |
|
523 break; |
|
524 |
|
525 // Paragraphs and definition lists |
|
526 case '/p': |
|
527 case '/dl': |
|
528 $chunk = ''; // Ensure blank new-line. |
|
529 break; |
|
530 } |
|
531 } |
|
532 // Process blocks of text. |
|
533 else { |
|
534 // Convert inline HTML text to plain text; not removing line-breaks or |
|
535 // white-space, since that breaks newlines when sanitizing plain-text. |
|
536 $value = trim(decode_entities($value)); |
|
537 if (drupal_strlen($value)) { |
|
538 $chunk = $value; |
|
539 } |
|
540 } |
|
541 |
|
542 // See if there is something waiting to be output. |
|
543 if (isset($chunk)) { |
|
544 // Apply any necessary case conversion. |
|
545 if (isset($casing)) { |
|
546 $chunk = $casing($chunk); |
|
547 } |
|
548 // Format it and apply the current indentation. |
|
549 $output .= drupal_wrap_mail($chunk, implode('', $indent)) . MAIL_LINE_ENDINGS; |
|
550 // Remove non-quotation markers from indentation. |
|
551 $indent = array_map('_drupal_html_to_text_clean', $indent); |
|
552 } |
|
553 |
|
554 $tag = !$tag; |
|
555 } |
|
556 |
|
557 return $output . $footnotes; |
|
558 } |
|
559 |
|
560 /** |
|
561 * Wraps words on a single line. |
|
562 * |
|
563 * Callback for array_walk() winthin drupal_wrap_mail(). |
|
564 */ |
|
565 function _drupal_wrap_mail_line(&$line, $key, $values) { |
|
566 // Use soft-breaks only for purely quoted or unindented text. |
|
567 $line = wordwrap($line, 77 - $values['length'], $values['soft'] ? " \n" : "\n"); |
|
568 // Break really long words at the maximum width allowed. |
|
569 $line = wordwrap($line, 996 - $values['length'], $values['soft'] ? " \n" : "\n", TRUE); |
|
570 } |
|
571 |
|
572 /** |
|
573 * Keeps track of URLs and replaces them with placeholder tokens. |
|
574 * |
|
575 * Callback for preg_replace_callback() within drupal_html_to_text(). |
|
576 */ |
|
577 function _drupal_html_to_mail_urls($match = NULL, $reset = FALSE) { |
|
578 global $base_url, $base_path; |
|
579 static $urls = array(), $regexp; |
|
580 |
|
581 if ($reset) { |
|
582 // Reset internal URL list. |
|
583 $urls = array(); |
|
584 } |
|
585 else { |
|
586 if (empty($regexp)) { |
|
587 $regexp = '@^' . preg_quote($base_path, '@') . '@'; |
|
588 } |
|
589 if ($match) { |
|
590 list(, , $url, $label) = $match; |
|
591 // Ensure all URLs are absolute. |
|
592 $urls[] = strpos($url, '://') ? $url : preg_replace($regexp, $base_url . '/', $url); |
|
593 return $label . ' [' . count($urls) . ']'; |
|
594 } |
|
595 } |
|
596 return $urls; |
|
597 } |
|
598 |
|
599 /** |
|
600 * Replaces non-quotation markers from a given piece of indentation with spaces. |
|
601 * |
|
602 * Callback for array_map() within drupal_html_to_text(). |
|
603 */ |
|
604 function _drupal_html_to_text_clean($indent) { |
|
605 return preg_replace('/[^>]/', ' ', $indent); |
|
606 } |
|
607 |
|
608 /** |
|
609 * Pads the last line with the given character. |
|
610 * |
|
611 * @see drupal_html_to_text() |
|
612 */ |
|
613 function _drupal_html_to_text_pad($text, $pad, $prefix = '') { |
|
614 // Remove last line break. |
|
615 $text = substr($text, 0, -1); |
|
616 // Calculate needed padding space and add it. |
|
617 if (($p = strrpos($text, "\n")) === FALSE) { |
|
618 $p = -1; |
|
619 } |
|
620 $n = max(0, 79 - (strlen($text) - $p) - strlen($prefix)); |
|
621 // Add prefix and padding, and restore linebreak. |
|
622 return $text . $prefix . str_repeat($pad, $n) . "\n"; |
|
623 } |