1<?php
2
3/**
4 * Core modules
5 * @package modules
6 * @subpackage core
7 */
8
9/**
10 * Format a message body that has HMTL markup
11 * @subpackage core/functions
12 * @param string $str message HTML
13 * @param bool $images allow external images
14 * @return string
15 */
16if (!hm_exists('format_msg_html')) {
17function format_msg_html($str, $images=false) {
18    $str = str_ireplace('</body>', '', $str);
19    require_once VENDOR_PATH.'autoload.php';
20    $config = HTMLPurifier_Config::createDefault();
21    $config->set('Cache.DefinitionImpl', null);
22    if (!$images) {
23        $config->set('URI.DisableExternalResources', true);
24    }
25    $config->set('URI.AllowedSchemes', array('mailto' => true, 'data' => true, 'http' => true, 'https' => true));
26    $config->set('Filter.ExtractStyleBlocks.TidyImpl', true);
27    $purifier = new HTMLPurifier($config);
28    return @$purifier->purify($str);
29}}
30
31/**
32 * Convert HTML to plain text
33 * @param string $html content to convert
34 * @return string
35 */
36if (!hm_exists('convert_html_to_text')) {
37function convert_html_to_text($html) {
38    $html = new HTMLToText($html);
39    return $html->text;
40}}
41
42/**
43 * Format image data
44 * @subpackage core/functions
45 * @param string $str binary image data
46 * @param string $mime_type type of image
47 * return string
48 */
49if (!hm_exists('format_msg_image')) {
50function format_msg_image($str, $mime_type) {
51    return '<img class="msg_img" alt="" src="data:image/'.$mime_type.';base64,'.chunk_split(base64_encode($str)).'" />';
52}}
53
54/**
55 * Format a plain text message
56 * @subpackage core/functions
57 * @param string $str message text
58 * @param object $output_mod Hm_Output_Module
59 */
60if (!hm_exists('format_msg_text')) {
61function format_msg_text($str, $output_mod, $links=true) {
62    $str = str_replace("\t", '    ', $str);
63    $str = nl2br(str_replace(' ', '<wbr>', ($output_mod->html_safe($str)))).'<br />';
64    $str = preg_replace("/(&(?!amp)[^;]+;)/", " $1", $str);
65    if ($links) {
66        $link_regex = "/((http|ftp|rtsp)s?:\/\/(%[[:digit:]A-Fa-f][[:digit:]A-Fa-f]|[-_\.!~\*';\/\?#:@&=\+$,%[:alnum:]])+)/m";
67        $str = preg_replace($link_regex, "<a href=\"$1\">$1</a>", $str);
68    }
69    $str = preg_replace("/ (&[^;]+;)/", "$1", $str);
70    $str = str_replace('<wbr>', '&#160;<wbr>', $str);
71    return preg_replace("/^(&gt;.*<br \/>)/m", "<span class=\"reply_quote\">$1</span>", $str);
72}}
73
74/**
75 * Format reply text
76 * @subpackage core/functions
77 * @param string $txt message text
78 * @return string
79 */
80if (!hm_exists('format_reply_text')) {
81function format_reply_text($txt) {
82    $lines = explode("\n", $txt);
83    $new_lines = array();
84    foreach ($lines as $line) {
85        $pre = '> ';
86        if (preg_match("/^(>\s*)+/", $line, $matches)) {
87            $pre .= $matches[1];
88        }
89        $wrap = 75 + strlen($pre);
90        $new_lines[] = preg_replace("/$pre /", "$pre", "> ".wordwrap($line, $wrap, "\n$pre"));
91    }
92    return implode("\n", $new_lines);
93}}
94
95/**
96 * Get reply to address
97 * @subpackage core/functions
98 * @param array $headers message headers
99 * @param string $type type (forward, reply, reply_all)
100 * @return string
101 */
102if (!hm_exists('reply_to_address')) {
103function reply_to_address($headers, $type) {
104    $msg_to = '';
105    $msg_cc = '';
106    $headers = lc_headers($headers);
107    $parsed = array();
108    $delivered_address = false;
109    if (array_key_exists('delivered-to', $headers)) {
110        $delivered_address = array('email' => $headers['delivered-to'],
111            'comment' => '', 'label' => '');
112    }
113
114    if ($type == 'forward') {
115        return $msg_to;
116    }
117    foreach (array('reply-to', 'from', 'sender', 'return-path') as $fld) {
118        if (array_key_exists($fld, $headers)) {
119            list($parsed, $msg_to) = format_reply_address($headers[$fld], $parsed);
120            if ($msg_to) {
121                break;
122            }
123        }
124    }
125    if ($type == 'reply_all') {
126        if ($delivered_address) {
127            $parsed[] = $delivered_address;
128        }
129        if (array_key_exists('cc', $headers)) {
130            list($cc_parsed, $msg_cc) = format_reply_address($headers['cc'], $parsed);
131            $parsed += $cc_parsed;
132        }
133        if (array_key_exists('to', $headers)) {
134            list($parsed, $recips) = format_reply_address($headers['to'], $parsed);
135            if ($recips) {
136                if ($msg_cc) {
137                    $msg_cc .= ', '.$recips;
138                }
139                else {
140                    $msg_cc = $recips;
141                }
142            }
143        }
144    }
145    return array($msg_to, $msg_cc);
146}}
147
148/*
149 * Format a reply address line
150 * @param string $fld the field values from the E-mail being replied to
151 * @param array $excluded list of parsed addresses to exclude
152 * @return string
153 */
154if (!hm_exists('format_reply_address')) {
155function format_reply_address($fld, $excluded) {
156    $addr = process_address_fld(trim($fld));
157    $res = array();
158    foreach ($addr as $v) {
159        $skip = false;
160        foreach ($excluded as $ex) {
161            if (strtolower($v['email']) == strtolower($ex['email'])) {
162                $skip = true;
163                break;
164            }
165        }
166        if (!$skip) {
167            $res[] = $v;
168        }
169    }
170    if ($res) {
171        return array($addr, implode(', ', array_map(function($v) {
172            if (trim($v['label'])) {
173                return $v['label'].' '.$v['email'];
174            }
175            else {
176                return $v['email'];
177            }
178        }, $res)));
179    }
180    return array($addr, '');
181}}
182
183/**
184 * Get reply to subject
185 * @subpackage core/functions
186 * @param array $headers message headers
187 * @param string $type type (forward, reply, reply_all)
188 * @return string
189 */
190if (!hm_exists('reply_to_subject')) {
191function reply_to_subject($headers, $type) {
192    $subject = '';
193    if (array_key_exists('Subject', $headers)) {
194        if ($type == 'reply' || $type == 'reply_all') {
195            if (!preg_match("/^re:/i", trim($headers['Subject']))) {
196                $subject = sprintf("Re: %s", $headers['Subject']);
197            }
198        }
199        elseif ($type == 'forward') {
200            if (!preg_match("/^fwd:/i", trim($headers['Subject']))) {
201                $subject = sprintf("Fwd: %s", $headers['Subject']);
202            }
203        }
204        if (!$subject) {
205            $subject = $headers['Subject'];
206        }
207    }
208    return $subject;
209}}
210
211/**
212 * Get reply message lead in
213 * @subpackage core/functions
214 * @param array $headers message headers
215 * @param string $type type (forward, reply, reply_all)
216 * @param string $to reply to value
217 * @param object $output_mod output module object
218 * @return string
219 */
220if (!hm_exists('reply_lead_in')) {
221function reply_lead_in($headers, $type, $to, $output_mod) {
222    $lead_in = '';
223    if ($type == 'reply' || $type == 'reply_all') {
224        if (array_key_exists('Date', $headers)) {
225            if ($to) {
226                $lead_in = sprintf($output_mod->trans('On %s %s said')."\n\n", $headers['Date'], $to);
227            }
228            else {
229                $lead_in = sprintf($output_mod->trans('On %s, somebody said')."\n\n", $headers['Date']);
230            }
231        }
232    }
233    elseif ($type == 'forward') {
234        $flds = array();
235        foreach( array('From', 'Date', 'Subject') as $fld) {
236            if (array_key_exists($fld, $headers)) {
237                $flds[$fld] = $headers[$fld];
238            }
239        }
240        $lead_in = "\n\n----- ".$output_mod->trans('begin forwarded message')." -----\n\n";
241        foreach ($flds as $fld => $val) {
242            $lead_in .= $fld.': '.$val."\n";
243        }
244        $lead_in .= "\n";
245    }
246    return $lead_in;
247}}
248
249/**
250 * Format reply field details
251 * @subpackage core/functions
252 * @param array $headers message headers
253 * @param string $body message body
254 * @param string $lead_in body lead in text
255 * @param string $reply_type type (forward, reply, reply_all)
256 * @param array $struct message structure details
257 * @param int $html set to 1 if the output should be HTML
258 * @return array
259 */
260if (!hm_exists('reply_format_body')) {
261function reply_format_body($headers, $body, $lead_in, $reply_type, $struct, $html) {
262    $msg = '';
263    $type = 'textplain';
264    if (array_key_exists('type', $struct) && array_key_exists('subtype', $struct)) {
265        $type = strtolower($struct['type']).strtolower($struct['subtype']);
266    }
267    if ($html == 1) {
268        $msg = format_reply_as_html($body, $type, $reply_type, $lead_in);
269    }
270    else {
271        $msg = format_reply_as_text($body, $type, $reply_type, $lead_in);
272    }
273    return $msg;
274}}
275
276/**
277 * Format reply text as HTML
278 * @subpackage core/functions
279 * @param string $body message body
280 * @param string $type MIME type
281 * @param string $reply_type type (forward, reply, reply_all)
282 * @param string $lead_in body lead in text
283 * @return string
284 */
285if (!hm_exists('format_reply_as_html')) {
286function format_reply_as_html($body, $type, $reply_type, $lead_in) {
287    if ($type == 'textplain') {
288        if ($reply_type == 'reply' || $reply_type == 'reply_all') {
289            $msg = nl2br($lead_in.format_reply_text($body));
290        }
291        elseif ($reply_type == 'forward') {
292            $msg = nl2br($lead_in.$body);
293        }
294    }
295    elseif ($type == 'texthtml') {
296        $msg = nl2br($lead_in).'<hr /><blockquote>'.format_msg_html($body).'</blockquote>';
297    }
298    return $msg;
299}}
300
301/**
302 * Format reply text as text
303 * @subpackage core/functions
304 * @param string $body message body
305 * @param string $type MIME type
306 * @param string $reply_type type (forward, reply, reply_all)
307 * @param string $lead_in body lead in text
308 * @return string
309 */
310if (!hm_exists('format_reply_as_text')) {
311function format_reply_as_text($body, $type, $reply_type, $lead_in) {
312    $msg = '';
313    if ($type == 'texthtml') {
314        if ($reply_type == 'reply' || $reply_type == 'reply_all') {
315            $msg = $lead_in.format_reply_text(convert_html_to_text($body));
316        }
317        elseif ($reply_type == 'forward') {
318            $msg = $lead_in.convert_html_to_text($body);
319        }
320    }
321    elseif ($type == 'textplain') {
322        if ($reply_type == 'reply' || $reply_type == 'reply_all') {
323            $msg = $lead_in.format_reply_text($body);
324        }
325        else {
326            $msg = $lead_in.$body;
327        }
328    }
329    return $msg;
330}}
331
332/**
333 * Convert header keys to lowercase versions
334 * @param array $headers message headers
335 * @return array
336 */
337if (!hm_exists('lc_headers')) {
338function lc_headers($headers) {
339    return array_change_key_case($headers, CASE_LOWER);
340}}
341
342/**
343 * Get the in-reply-to message id for replied
344 * @subpackage core/functions
345 * @param array $headers message headers
346 * @param string $type reply type
347 * @return string
348 */
349if (!hm_exists('reply_to_id')) {
350function reply_to_id($headers, $type) {
351    $id = '';
352    $headers = lc_headers($headers);
353    if ($type != 'forward' && array_key_exists('message-id', $headers)) {
354        $id = $headers['message-id'];
355    }
356    return $id;
357}}
358
359/**
360 * Get reply field details
361 * @subpackage core/functions
362 * @param string $body message body
363 * @param array $headers message headers
364 * @param array $struct message structure details
365 * @param int $html set to 1 if the output should be HTML
366 * @param string $type optional type (forward, reply, reply_all)
367 * @param object $output_mod output module object
368 * @param string $type the reply type
369 * @return array
370 */
371if (!hm_exists('format_reply_fields')) {
372function format_reply_fields($body, $headers, $struct, $html, $output_mod, $type='reply') {
373    $msg_to = '';
374    $msg = '';
375    $subject = reply_to_subject($headers, $type);
376    $msg_id = reply_to_id($headers, $type);
377    list($msg_to, $msg_cc) = reply_to_address($headers, $type);
378    $lead_in = reply_lead_in($headers, $type, $msg_to, $output_mod);
379    $msg = reply_format_body($headers, $body, $lead_in, $type, $struct, $html);
380    return array($msg_to, $msg_cc, $subject, $msg, $msg_id);
381}}
382
383/**
384 * decode mail fields to human readable text
385 * @param string $string field to decode
386 * @return string decoded field
387 */
388if (!hm_exists('decode_fld')) {
389function decode_fld($string) {
390    if (strpos($string, '=?') === false) {
391        return $string;
392    }
393    $string = preg_replace("/\?=\s+=\?/", '?==?', $string);
394    if (preg_match_all("/(=\?[^\?]+\?(q|b)\?[^\?]+\?=)/i", $string, $matches)) {
395        foreach ($matches[1] as $v) {
396            $fld = substr($v, 2, -2);
397            $charset = strtolower(substr($fld, 0, strpos($fld, '?')));
398            $fld = substr($fld, (strlen($charset) + 1));
399            $encoding = $fld[0];
400            $fld = substr($fld, (strpos($fld, '?') + 1));
401            if (strtoupper($encoding) == 'B') {
402                $fld = mb_convert_encoding(base64_decode($fld), 'UTF-8', $charset);
403            }
404            elseif (strtoupper($encoding) == 'Q') {
405                $fld = mb_convert_encoding(quoted_printable_decode(str_replace('_', ' ', $fld)), 'UTF-8', $charset);
406            }
407            $string = str_replace($v, $fld, $string);
408        }
409    }
410    return trim($string);
411}}
412
413/**
414 * @subpackage core/class
415 */
416class HTMLToText {
417
418    public $text = '';
419    private $current = false;
420    private $blocks = array('table', 'li', 'div', 'h1', 'h2', 'br', 'h3', 'h4', 'h5', 'p', 'tr');
421    private $skips = array('head', 'script', 'style');
422
423    function __construct($html) {
424        $doc = new DOMDocument();
425        @$doc->loadHTML($html);
426        if (trim($html) && $doc->hasChildNodes()) {
427            $this->parse_nodes($doc->childNodes);
428        }
429        $this->text = trim(strip_tags(html_entity_decode(preg_replace("/\n{2,}/m",
430            "\n\n", $this->text), ENT_QUOTES, "UTF-8")));
431    }
432
433    function block($tag) {
434        in_array($tag, $this->blocks) && $this->current != $tag ? $this->text .= "\n" : false;
435        $this->current = $tag;
436    }
437
438    function parse_nodes($nodes) {
439        $trims = chr(160).chr(194)." \t\n\r\0\x0B";
440        foreach ($nodes as $node) {
441            if (!in_array($node->nodeName, $this->skips)) {
442                $this->block($node->nodeName);
443                if ($node->nodeName == '#text' && trim($node->textContent, $trims)) {
444                    $this->text .= trim($node->textContent, $trims)." ";
445                }
446                $node->hasChildNodes() ? $this->parse_nodes($node->childNodes) : false;
447            }
448        }
449    }
450}
451
452/**
453 * trim a potential E-mail value
454 * @param $val string E-mail value
455 * @return string trimmed value
456 */
457if (!hm_exists('addr_split')) {
458function trim_email($val) {
459    $seps = array(',', ';');
460    $misc = array('"', "'", '>', '<');
461    return trim($val, implode(array_merge($misc, $seps)));
462}}
463
464/**
465 * Split an address field
466 * @param $str string field value
467 * @param $seps array break chars
468 * @return array results
469 */
470if (!hm_exists('addr_split')) {
471function addr_split($str, $seps = array(',', ';')) {
472    $str = preg_replace('/(\s){2,}/', ' ', $str);
473    $max = strlen($str);
474    $word = '';
475    $words = array();
476    $capture = false;
477    $capture_chars = array('"' => '"', '(' => ')', '<' => '>');
478    for ($i=0;$i<$max;$i++) {
479        if ($capture && $capture_chars[$capture] == $str[$i]) {
480            $capture = false;
481        }
482        elseif (!$capture && in_array($str[$i], array_keys($capture_chars))) {
483            $capture = $str[$i];
484        }
485
486        if (!$capture && in_array($str[$i], $seps)) {
487            $words[] = trim($word);
488            $word = '';
489        }
490        else {
491            $word .= $str[$i];
492        }
493    }
494    $words[] = trim($word);
495    return $words;
496}}
497
498/**
499 * Parse an address field
500 * @param $str string field value
501 * @return array results
502 */
503if (!hm_exists('addr_parse')) {
504function addr_parse($str) {
505    $label = array();
506    $email = '';
507    $comment = array();
508    foreach (addr_split($str, array(' ')) as $token) {
509        if (is_email_address(trim_email($token))) {
510            $email = trim_email($token);
511        }
512        else {
513            $label[] = $token;
514        }
515    }
516    $label = implode(' ', $label);
517    if (preg_match('/\([^)]+\)/', $label, $matches)) {
518        foreach ($matches as $match) {
519            $comment[] = $match;
520            $label = str_replace($match, '', $label);
521        }
522        $comment = implode(',', $comment);
523    }
524    else {
525        $comment = '';
526    }
527    return array('email' => $email, 'label' => trim($label, ' \'"'), 'comment' => $comment);
528}}
529
530/**
531 * Parse an address field
532 * @param $fld string field value
533 * @return array results
534 */
535if (!hm_exists('process_address_fld')) {
536function process_address_fld($fld) {
537    $res = array();
538    $count = 0;
539    $pre = false;
540    foreach (addr_split($fld) as $str) {
541        $addr = addr_parse($str);
542        if ($addr['email']) {
543            if ($pre) {
544                $addr['label'] = $pre.' '.$addr['label'];
545                $pre = false;
546            }
547            $res[$count] = $addr;
548        }
549        elseif ($addr['label']) {
550            $pre = $addr['label'];
551        }
552        $count++;
553    }
554    return $res;
555}}
556