Bitrix-D7 23.9
 
Загрузка...
Поиск...
Не найдено
message.php
1<?php
2
3namespace Bitrix\Mail;
4
8use CTimeZone;
9
10Main\Localization\Loc::loadMessages(__FILE__);
11
13{
14
15 //const QUOTE_START_MARKER = '-- Bitrix24 Mail begin ---';
16 //const QUOTE_END_MARKER = '-- Bitrix24 Mail end ---';
17
18 const QUOTE_START_MARKER_HTML = '<div id="srvb24mqsm" style="font-family: \'srvb24mqsm\', serif;">&nbsp;</div>';
19 const QUOTE_END_MARKER_HTML = '<div id="qemb24msrv" style="font-family: \'qemb24msrv\', serif;">&nbsp;</div>';
20
21 const QUOTE_HTML_REGEX = '/<div\s[^>]+srvb24mqsm[^>]+>.*?<\/div>(.*)<div\s[^>]+qemb24msrv[^>]+>.*?<\/div>/is';
22
23 const QUOTE_PLACEHOLDER = '__QUOTE_PLACEHOLDER__';
24
25 protected $type;
26 protected $headers, $subject, $from, $to;
28 protected $secret;
29
30 public function __construct(array &$message, $type)
31 {
32 $this->type = $type;
33
34 $properties = array(
35 'headers', 'subject', 'from', 'to',
36 'text', 'html', 'attachments',
37 'secret'
38 );
39
40 foreach ($properties as $property)
41 {
42 if (isset($message[$property]))
43 $this->$property = $message[$property];
44 }
45 }
46
47 public static function stripQuotes($text): string
48 {
49 return preg_replace('/^("(.*)"|\'(.*)\')$/', '$2$3', $text);
50 }
51
52 private static function convertContactListToString($list): string
53 {
54 $string = '';
55
56 foreach ($list as $contact)
57 {
58 $name = static::stripQuotes($contact['name']);
59 $email = $contact['email'];
60
61 if ($name === $email)
62 {
63 $name = '';
64 }
65
66 $string .= Loc::getMessage('MAIL_QUOTE_MESSAGE_HEADER_CONTACT', ['#NAME#' => $name,'#EMAIL#' => $email]);
67
68 if (next($list))
69 {
70 $string .= ', ';
71 }
72 }
73
74 return $string;
75 }
76
77 final public static function wrapTheMessageWithAQuote($body, $subject, $timeString, $from = [], $to = [], $cc = [], bool $sanitized = false): string
78 {
79 $fieldDateInTimeStamp = makeTimestamp($timeString);
80 $titleDateFormat = Context::getCurrent()->getCulture()->getFullDateFormat()."&#013;H:i:s";
81 $formattedDate = FormatDate($titleDateFormat, $fieldDateInTimeStamp, (time() + CTimeZone::getOffset()));
82
83 $wrap = '';
84
85 $fromList = static::convertContactListToString($from);
86 $toList = static::convertContactListToString($to);
87 $ccList = static::convertContactListToString($cc);
88 if (!$sanitized)
89 {
90 $body = Helper\Message::sanitizeHtml($body);
91 }
92
93 if (empty($ccList))
94 {
95 $wrap .= Loc::getMessage('MAIL_QUOTE_MESSAGE_HEADER_WITHOUT_CC', [
96 '#DATE#' => $formattedDate,
97 '#SUBJECT#' => $subject,
98 '#BODY#' => $body,
99 '#FROM_LIST#' => $fromList,
100 '#TO_LIST#' => $toList,
101 '[blockquote]' => '<blockquote style="margin: 0 0 0 5px; padding: 5px 5px 5px 8px; border-left: 4px solid #e2e3e5; ">',
102 '[/blockquote]' => '</blockquote>'
103 ]);
104 }
105 else
106 {
107 $wrap .= Loc::getMessage('MAIL_QUOTE_MESSAGE_HEADER', [
108 '#DATE#' => $formattedDate,
109 '#SUBJECT#' => $subject,
110 '#BODY#' => $body,
111 '#FROM_LIST#' => $fromList,
112 '#TO_LIST#' => $toList,
113 '#CC_LIST#' => $ccList,
114 '[blockquote]' => '<blockquote style="margin: 0 0 0 5px; padding: 5px 5px 5px 8px; border-left: 4px solid #e2e3e5; ">',
115 '[/blockquote]' => '</blockquote>'
116 ]);
117 }
118
119 return $wrap;
120 }
121
128 final public static function getQuoteStartMarker($html = false)
129 {
130 return $html ? static::QUOTE_START_MARKER_HTML : static::QUOTE_START_MARKER;
131 }
132
139 final public static function getQuoteEndMarker($html = false)
140 {
141 return $html ? static::QUOTE_END_MARKER_HTML : static::QUOTE_END_MARKER;
142 }
143
149 public function attachmentsCount()
150 {
151 return is_array($this->attachments) ? count($this->attachments) : 0;
152 }
153
159 protected function parse()
160 {
161 if (isset($this->html))
162 {
164
165 $html = str_replace(array("\r", "\n"), '', $html);
166 $html = preg_replace('/<br\s*\/?>/is', "\n", $html);
167
168 $html = str_ireplace('</div>', "</div>\n", $html);
169 $html = str_ireplace('</p>', "</p>\n", $html);
170 $html = preg_replace('/<\/h([1-6])>/i', "</h\\1>\n", $html);
171 $html = str_ireplace('</table>', "</table>\n", $html);
172 $html = str_ireplace('</tr>', "</tr>\n", $html);
173 $html = str_ireplace('</pre>', "</pre>\n", $html);
174
175 $html = preg_replace('/(\n\s*)?<div/i', "\n<div", $html);
176 $html = preg_replace('/(\n\s*)?<p(?=\s|>)/i', "\n<p", $html);
177 $html = preg_replace('/(\n\s*)?<h([1-6])/i', "\n<h\\2", $html);
178 $html = preg_replace('/(\n\s*)?<table/i', "\n<table", $html);
179 $html = preg_replace('/(\n\s*)?<tr/i', "\n<tr", $html);
180 $html = preg_replace('/(\n\s*)?<pre/i', "\n<pre", $html);
181
182 $html = preg_replace('/(\n\s*)?<hr[^>]*>(\s*\n)?/i', "\n<hr>\n", $html);
183
184 if ($this->type == 'reply' and $parts = $this->splitHtml($html))
185 {
186 list($before, $quote, $after) = $parts;
187 $html = sprintf('%s%s%s', $before, static::QUOTE_PLACEHOLDER, $after);
188 }
189
190 if ($this->attachmentsCount())
191 {
192 foreach ($this->attachments as $item)
193 {
194 $html = preg_replace(
195 sprintf('/<img[^>]+src\s*=\s*(\'|\")?\s*(cid:%s)\s*\1[^>]*>/is', preg_quote($item['contentId'], '/')),
196 sprintf('[ATTACHMENT=%s]', $item['uniqueId']),
197 $html
198 );
199 }
200 }
201
202 // TODO: Sanitizer
203 $html = preg_replace('/<style[^>]*>.*?<\/style>/is', '', $html);
204 $html = preg_replace('/<script[^>]*>.*?<\/script>/is', '', $html);
205 $html = preg_replace('/<title[^>]*>.*?<\/title>/is', '', $html);
206 $html = preg_replace('/<caption[^>]*>.*?<\/caption>/is', '', $html);
207
208 // TODO: Sanitizer
209 $html = preg_replace('/<a\s[^>]*href\s*=\s*([^\'\"\s>]+)\s*[^>]*>/is', '<a href="\1">', $html);
210
211 // TODO: TextParser
212 $html = preg_replace('/<strong[^>]*>(.*?)<\/strong>/is', '<b>\1</b>', $html);
213 $html = preg_replace('/<em[^>]*>(.*?)<\/em>/is', '<i>\1</i>', $html);
214 $html = preg_replace('/<blockquote[^>]*>(.*?)<\/blockquote>/is', '<quote>\1</quote>', $html);
215 $html = preg_replace('/<hr[^>]*>/is', '________________________________________', $html);
216 $html = preg_replace('/<del[^>]*>(.*?)<\/del>/is', '<s>\1</s>', $html);
217 $html = preg_replace('/<ins[^>]*>(.*?)<\/ins>/is', '<u>\1</u>', $html);
218 $html = preg_replace('/<h([1-6])[^>]*>(.*?)<\/h\1>/is', '<b>\2</b>', $html);
219 $html = preg_replace('/<dl[^>]*>(.*?)<\/dl>/is', '<ul>\1</ul>', $html);
220 $html = preg_replace('/<dt[^>]*>/is', '<li>', $html);
221 $html = preg_replace('/<dd[^>]*>/is', ' - ', $html);
222 $html = preg_replace('/<(sub|sup)[^>]*>(.*?)<\/\1>/is', '(\2)', $html);
223
224 // TODO: TextParser
225 $html = preg_replace('/<th[^>]*>(.*?)<\/th>/is', '<td>\1</td>', $html);
226
227 $sanitizer = new \CBXSanitizer();
228 //$sanitizer->setLevel(\CBXSanitizer::SECURE_LEVEL_MIDDLE);
229 $sanitizer->addTags(array(
230 'a' => array('href'),
231 'b' => array(),
232 'u' => array(),
233 's' => array(),
234 'i' => array(),
235 'img' => array('src'),
236 'font' => array('color', 'size', 'face'),
237 'ul' => array(),
238 'ol' => array(),
239 'li' => array(),
240 'table' => array(),
241 'tr' => array(),
242 'td' => array(),
243 'th' => array(),
244 'quote' => array(),
245 'br' => array(),
246 //'big' => array(),
247 //'small' => array(),
248 ));
249 $sanitizer->applyDoubleEncode(false);
250 $html = $sanitizer->sanitizeHtml($html);
251
252 $parser = new \CTextParser();
253 $text = $parser->convertHtmlToBB($html);
254
255 $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML401, LANG_CHARSET);
256
257 // TODO: TextParser
258 $text = preg_replace('/<\/?([abuis]|img|font|ul|ol|li|table|tr|td|th|quote|br)(?=\s|>)[^>]*>/i', '', $text);
259
260 $text = preg_replace('/[\t\x20]+/', "\x20", $text);
261 }
262 else
263 {
265
266 $text = str_replace("\r\n", "\n", $text);
267 $text = str_replace("\r", "\n", $text);
268
269 if ($this->type == 'reply' and $parts = $this->splitText($text))
270 {
271 list($before, $quote, $after) = $parts;
272 $text = sprintf('%s%s%s', $before, static::QUOTE_PLACEHOLDER, $after);
273 }
274
275 if ($this->attachmentsCount())
276 {
277 foreach ($this->attachments as $item)
278 {
279 $text = str_replace(
280 sprintf('[cid:%s]', $item['contentId']),
281 sprintf('[ATTACHMENT=%s]', $item['uniqueId']),
282 $text
283 );
284 }
285 }
286 }
287
288 if ($this->type == 'reply' && mb_strpos($text, static::QUOTE_PLACEHOLDER))
289 {
290 $text = $this->removeReplyHead($text);
291 $text = preg_replace(sprintf('/\s*%s\s*/', preg_quote(static::QUOTE_PLACEHOLDER, '/')), "\n\n", $text);
292 }
293
294 if ($this->type == 'forward')
295 $text = $this->removeForwardHead($text);
296
297 // TODO: TextParser
298 $text = preg_replace('/\[tr\]\s*\[\/tr\]/is', '', $text);
299 $text = preg_replace('/\[table\]\s*\[\/table\]/is', '', $text);
300
301 $text = trim($text);
302 $text = preg_replace('/(\s*\n){2,}/', "\n\n", $text);
303
304 if (empty($text) && $this->attachmentsCount() == 1)
305 $text = sprintf('[ATTACHMENT=%s]', $item['uniqueId']);
306
307 if (!empty($this->secret))
308 $text = str_replace($this->secret, 'xxxxxxxx', $text);
309
310 return $text;
311 }
312
313 public static function parseMessage(array &$message)
314 {
315 $message = new static($message, null);
316
317 return $message->parse();
318 }
319
326 public static function parseReply(array &$message)
327 {
328 $reply = new static($message, 'reply');
329
330 return $reply->parse();
331 }
332
339 public static function parseForward(array &$message)
340 {
341 $forward = new static($message, 'forward');
342
343 return $forward->parse();
344 }
345
352 protected function splitHtml(&$html)
353 {
354 $parts = preg_split('/(<blockquote.+?<\/blockquote>)/is', $html, null, PREG_SPLIT_DELIM_CAPTURE);
355
356 if (count($parts) > 3)
357 {
358 $parts = array_merge(
359 array(join(array_slice($parts, 0, -2))),
360 array_slice($parts, -2)
361 );
362 }
363 else
364 {
365 if (count($parts) == 3)
366 $parts = preg_split('/(<blockquote.+<\/blockquote>)/is', $html, null, PREG_SPLIT_DELIM_CAPTURE);
367 }
368
369 if (count($parts) < 3)
370 $parts = preg_split(static::QUOTE_HTML_REGEX, $html, null, PREG_SPLIT_DELIM_CAPTURE);
371
372 if (count($parts) == 3)
373 return $parts;
374
375 return false;
376 }
377
384 protected function splitText(&$text)
385 {
386 $parts = preg_split('/((?:^>.*$\n?){2,})/m', $text, null, PREG_SPLIT_DELIM_CAPTURE);
387
388 if (count($parts) < 3)
389 $parts = preg_split('/((?:^\|.*$\n?){2,})/m', $text, null, PREG_SPLIT_DELIM_CAPTURE);
390
391 if (count($parts) < 3)
392 {
393 $outlookRegex = '/(
394 (?:^_{20,}\n(?:[\t\x20]*\n)?)?
395 (?:^(?:from|to|subject|sent|date):\x20[^\n]+$\n?){2,8}.*
396 )/ismx';
397 $parts = preg_split($outlookRegex, $text, null, PREG_SPLIT_DELIM_CAPTURE);
398 }
399
400 if (count($parts) == 3)
401 return $parts;
402
403 return false;
404 }
405
412 protected function scoreFullHead(&$head)
413 {
414 $score = 0;
415
416 if (preg_match_all('/^([^\:\n]{1,20}):[\t\x20]+(.+)$/m'.BX_UTF_PCRE_MODIFIER, $head, $matches, PREG_SET_ORDER))
417 {
418 $subject = array(
419 'value' => $this->subject,
420 'strlen' => mb_strlen($this->subject),
421 'sgnlen' => mb_strlen(trim($this->subject))
422 );
423
424 $isHeader = function($key, $value) use (&$subject)
425 {
426 if (mb_strlen(trim($value)) >= 10 && $subject['sgnlen'] >= 10)
427 {
428 $dist = $subject['strlen'] - mb_strlen($value);
429
430 if (abs($dist) < 10)
431 {
432 if ($dist >= 0 && mb_strpos($subject['value'], $value) !== false)
433 {
434 return true;
435 }
436
437 if (max($subject['strlen'], mb_strlen($value)) < 256 && levenshtein($subject['value'], $value) < 10)
438 {
439 return true;
440 }
441 }
442 }
443
444 $date = preg_replace('/(?<=[\s\d])UT$/i', '+0000', trim($value));
445 if (preg_match('/\d{1,2}:\d{2}(:\d{2})?\x20?(am|pm)?/i', $date) && strtotime($date) !== false)
446 {
447 return true;
448 }
449
450 if (preg_match('/([a-z\d_](\.?[a-z\d_-]+)*)?[a-z\d_]@(([a-z\d][a-z\d-]*)?[a-z\d]\.?)+/i', $value))
451 {
452 return true;
453 }
454
455 return false;
456 };
457
458 foreach ($matches as $item)
459 {
460 $score += (int) $isHeader($item[1], $item[2]);
461 }
462 }
463
464 return $score;
465 }
466
473 protected function scoreShortHead(&$head)
474 {
475 $score = 0;
476
477 $regex = '/(?:^|\n)
478 (?<date>.{5,50}\d),?\x20
479 [^\d\n]{0,20}(?<time>\d{1,2}\:\d{2}(?:\:\d{2})?\x20?(?:am|pm)?),?\x20
480 (?<from>.+):\s*$
481 /ix'.BX_UTF_PCRE_MODIFIER;
482 if (preg_match($regex, $head, $matches))
483 {
484 $matches['date'] = trim($matches['date']);
485 if (strtotime($matches['date']) !== false)
486 {
487 $score++;
488 }
489 else if (preg_match('/^[^\x20]+\x20+((?:[^\x20]+\x20+)?(.+))$/', $matches['date'], $date))
490 {
491 if (strtotime($date[1]) !== false || strtotime($date[2]) !== false)
492 $score++;
493 }
494
495 if (preg_match('/([a-z\d_](\.?[a-z\d_-]+)*)?[a-z\d_]@(([a-z\d][a-z\d-]*)?[a-z\d]\.?)+/i', $matches['from']))
496 $score++;
497 }
498
499 return $score;
500 }
501
508 protected function removeReplyHead(&$text)
509 {
510 list($before, $after) = explode(static::QUOTE_PLACEHOLDER, $text, 2);
511
512 if (!trim($before))
513 return $text;
514
515 $data = static::reduceTags($before);
516
526 $fullHeadRegex = '/(?:^|\n\n)
527 (?<hr>_{20,}\n(?:[\t\x20]*\n)?)?
528 (?<head>(?:[^\:\n]{1,20}:[\t\x20]+.+(?:\n|$)){2,6})\s*$
529 /x'.BX_UTF_PCRE_MODIFIER;
530 if (preg_match($fullHeadRegex, $data, $matches))
531 {
532 $score = (int) !empty($matches['hr']);
533 $score += $this->scoreFullHead($matches['head']);
534
535 if ($score > 1)
536 {
537 $pattern = preg_replace(array('/.+/', '/\n/'), array('.+', '\n'), $matches[0]);
538 $before = preg_replace_callback(
539 sprintf('/%s$/', $pattern),
540 function($matches)
541 {
542 return Message::reduceHead($matches[0]);
543 },
544 $before
545 );
546
547 return sprintf('%s%s%s', $before, static::QUOTE_PLACEHOLDER, $after);
548 }
549 }
550
556 $shortHeadRegex = '/(?:^|\n)
557 (?<date>.{5,50}\d),?\x20
558 [^\d\n]{0,20}(?<time>\d{1,2}\:\d{2}(?:\:\d{2})?\x20?(?:am|pm)?),?\x20
559 (?<from>.+):\s*$
560 /ix'.BX_UTF_PCRE_MODIFIER;
561 if (preg_match($shortHeadRegex, $data, $matches))
562 {
563 $score = 0;
564 $score += $this->scoreShortHead($matches[0]);
565
566 if ($score > 0)
567 {
568 $pattern = preg_replace(array('/.+/', '/\n/'), array('.+', '\n'), $matches[0]);
569 $before = preg_replace_callback(
570 sprintf('/%s$/', $pattern),
571 function($matches)
572 {
573 return Message::reduceHead($matches[0]);
574 },
575 $before
576 );
577
578 return sprintf('%s%s%s', $before, static::QUOTE_PLACEHOLDER, $after);
579 }
580 }
581
582 return $text;
583 }
584
591 protected function removeForwardHead(&$text)
592 {
593 if (!trim($text))
594 return $text;
595
596 $data = static::reduceTags($text);
597
598 $shortHeadRegex = '/(?:^|\n)\s*
599 -{3,}.{4,40}?-{3,}[\t\x20]*\n
600 (?<head>(?:[\t\x20]*\n)?
601 (?<date>.{5,50}\d),?\x20
602 [^\d\n]{0,20}(?<time>\d{1,2}\:\d{2}(?:\:\d{2})?\x20?(?:am|pm)?),?\x20
603 (?<from>.+):(?:\s*\n)?)
604 /ix'.BX_UTF_PCRE_MODIFIER;
605
606 $hasMarker = preg_match($shortHeadRegex, $data);
607 $fullHeadRegex = '/(?:^|\n\n)\s*
608 (?<marker>-{3,}.{4,40}?-{3,}[\t\x20]*\n)'.($hasMarker ? '' : '?').'
609 (?<head>(?:[\t\x20]*\n)?
610 (?<lines>(?:[^\:\n]{1,20}:[\t\x20]+.+(?:\n|$)){2,6}))
611 \s*(?:\n|$)
612 /x'.BX_UTF_PCRE_MODIFIER;
613
614 if (preg_match($fullHeadRegex, $data, $matches, PREG_OFFSET_CAPTURE))
615 {
616 $score = (int) !empty($matches['marker'][0]);
617 $score += $this->scoreFullHead($matches['lines'][0]);
618
619 if ($score > 1)
620 {
621 // @TODO: Main\Text\BinaryString::getSubstring()
622 $pattern = preg_replace(
623 array('/.+/', '/\n/'), array('.+', '\n'),
624 array(substr($data, 0, $matches['head'][1]), $matches['head'][0])
625 );
626
627 return preg_replace_callback(
628 sprintf('/^(%s)(%s)/', $pattern[0], $pattern[1]),
629 function($matches)
630 {
631 return sprintf("%s\n\n%s", $matches[1], Message::reduceHead($matches[2]));
632 },
633 $text
634 );
635 }
636 }
637
638 if (preg_match($shortHeadRegex, $data, $matches, PREG_OFFSET_CAPTURE))
639 {
640 $score = 0;
641 $score += $this->scoreShortHead($matches['head'][0]);
642
643 if ($score > 0)
644 {
645 // @TODO: Main\Text\BinaryString::getSubstring()
646 $pattern = preg_replace(
647 array('/.+/', '/\n/'), array('.+', '\n'),
648 array(substr($data, 0, $matches['head'][1]), $matches['head'][0])
649 );
650
651 return preg_replace_callback(
652 sprintf('/^(%s)(%s)/', $pattern[0], $pattern[1]),
653 function($matches)
654 {
655 return sprintf("%s\n\n%s", $matches[1], Message::reduceHead($matches[2]));
656 },
657 $text
658 );
659 }
660 }
661
662 return $text;
663 }
664
671 protected static function reduceTags(&$text)
672 {
673 $data = $text;
674
675 $data = preg_replace('/^(\[\/?(\*|[busi]|img|table|tr|td|th|quote|(url|size|color|font|list)(=.+?)?)\])+$/im', "\t", $data);
676 $data = preg_replace('/\[\/?(\*|[busi]|img|table|tr|td|th|quote|(url|size|color|font|list)(=.+?)?)\]/i', '', $data);
677
678 return $data;
679 }
680
687 public static function reduceHead(&$text)
688 {
689 preg_match_all('/\[\/?([busi]|img|table|tr|td|th|quote|(url|size|color|font|list)(=.+?)?)\]/is', $text, $tags);
690
691 $result = join($tags[0]);
692 unset($tags);
693
694 do
695 {
696 $result = preg_replace('/\[([busi]|img|table|tr|td|th|quote|url|size|color|font|list)(=.+?)?\]\[\/\1\]/is', '', $result, -1, $n2);
697 }
698 while ($n2 > 0);
699
700 return $result;
701 }
702
703}
removeForwardHead(&$text)
Definition message.php:591
static stripQuotes($text)
Definition message.php:47
static wrapTheMessageWithAQuote($body, $subject, $timeString, $from=[], $to=[], $cc=[], bool $sanitized=false)
Definition message.php:77
static reduceHead(&$text)
Definition message.php:687
const QUOTE_START_MARKER_HTML
Definition message.php:18
static parseReply(array &$message)
Definition message.php:326
scoreFullHead(&$head)
Definition message.php:412
static reduceTags(&$text)
Definition message.php:671
__construct(array &$message, $type)
Definition message.php:30
const QUOTE_END_MARKER_HTML
Definition message.php:19
scoreShortHead(&$head)
Definition message.php:473
static getQuoteStartMarker($html=false)
Definition message.php:128
static getQuoteEndMarker($html=false)
Definition message.php:139
static parseForward(array &$message)
Definition message.php:339
removeReplyHead(&$text)
Definition message.php:508
const QUOTE_PLACEHOLDER
Definition message.php:23
static parseMessage(array &$message)
Definition message.php:313
static getCurrent()
Definition context.php:241
static getMessage($code, $replace=null, $language=null)
Definition loc.php:29