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