PluginProbe
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress / 11.0.3
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress v11.0.3
11.1.1 11.1.0 11.0.5 11.0.4 11.0.3 11.0.2 11.0.1 11.0.0 10.11.1 10.11.0 10.10.2 10.10.1 10.10.0 10.9.1 trunk 10.0.0 10.0.1 10.1.0 10.1.1 10.1.2 10.1.3 10.1.4 10.2.0 10.2.1 10.2.2 All 62 releases
acymailing / back / Libraries / Imap2 / Roundcube / Mime.php

Mime.php in AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress 11.0.3, at back/Libraries/Imap2/Roundcube/Mime.php

684 lines 23.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * +-----------------------------------------------------------------------+
5 * | This file is part of the Roundcube Webmail client |
6 * | |
7 * | Copyright (C) The Roundcube Dev Team |
8 * | Copyright (C) Kolab Systems AG |
9 * | |
10 * | Licensed under the GNU General Public License version 3 or |
11 * | any later version with exceptions for skins & plugins. |
12 * | See the README file for a full license statement. |
13 * | |
14 * | PURPOSE: |
15 * | MIME message parsing utilities |
16 * +-----------------------------------------------------------------------+
17 * | Author: Thomas Bruederli <[email protected]> |
18 * | Author: Aleksander Machniak <[email protected]> |
19 * +-----------------------------------------------------------------------+
20 */
21
22 namespace AcyMailing\Libraries\Imap2\Roundcube;
23
24 /**
25 * Class for parsing MIME messages
26 *
27 * @package Framework
28 * @subpackage Storage
29 */
30 class Mime
31 {
32 private static $default_charset;
33
34
35 /**
36 * Object constructor.
37 */
38 function __construct($default_charset = null)
39 {
40 self::$default_charset = $default_charset;
41 }
42
43 /**
44 * Returns message/object character set name
45 *
46 * @return string Character set name
47 */
48 public static function get_charset()
49 {
50 if (self::$default_charset) {
51 return self::$default_charset;
52 }
53
54 if ($charset = IMAP2_CHARSET) {
55 return $charset;
56 }
57
58 return IMAP2_CHARSET;
59 }
60
61 /**
62 * Split an address list into a structured array list
63 *
64 * @param string|array $input Input string (or list of strings)
65 * @param int $max List only this number of addresses
66 * @param boolean $decode Decode address strings
67 * @param string $fallback Fallback charset if none specified
68 * @param boolean $addronly Return flat array with e-mail addresses only
69 *
70 * @return array Indexed list of addresses
71 */
72 static function decode_address_list($input, $max = null, $decode = true, $fallback = null, $addronly = false)
73 {
74 // A common case when the same header is used many times in a mail message
75 if (is_array($input)) {
76 $input = implode(', ', $input);
77 }
78
79 $a = self::parse_address_list($input, $decode, $fallback);
80 $out = [];
81 $j = 0;
82
83 // Special chars as defined by RFC 822 need to in quoted string (or escaped).
84 $special_chars = '[\(\)\<\>\\\.\[\]@,;:"]';
85
86 if (!is_array($a)) {
87 return $out;
88 }
89
90 foreach ($a as $val) {
91 $j++;
92 $address = trim($val['address']);
93
94 if ($addronly) {
95 $out[$j] = $address;
96 } else {
97 $name = trim($val['name']);
98 if ($name && $address && $name != $address) {
99 $string = sprintf('%s <%s>', preg_match("/$special_chars/", $name) ? '"'.addcslashes($name, '"').'"' : $name, $address);
100 } elseif ($address) {
101 $string = $address;
102 } elseif ($name) {
103 $string = $name;
104 }
105
106 $out[$j] = ['name' => $name, 'mailto' => $address, 'string' => $string];
107 }
108
109 if ($max && $j == $max) {
110 break;
111 }
112 }
113
114 return $out;
115 }
116
117 /**
118 * Decode a message header value
119 *
120 * @param string $input Header value
121 * @param string $fallback Fallback charset if none specified
122 *
123 * @return string Decoded string
124 */
125 public static function decode_header($input, $fallback = null)
126 {
127 $str = self::decode_mime_string((string)$input, $fallback);
128
129 return $str;
130 }
131
132 /**
133 * Decode a mime-encoded string to internal charset
134 *
135 * @param string $input Header value
136 * @param string $fallback Fallback charset if none specified
137 *
138 * @return string Decoded string
139 */
140 public static function decode_mime_string($input, $fallback = null)
141 {
142 $default_charset = $fallback ? : self::get_charset();
143
144 // rfc: all line breaks or other characters not found
145 // in the Base64 Alphabet must be ignored by decoding software
146 // delete all blanks between MIME-lines, differently we can
147 // receive unnecessary blanks and broken utf-8 symbols
148 $input = preg_replace("/\?=\s+=\?/", '?==?', $input);
149
150 // encoded-word regexp
151 $re = '/=\?([^?]+)\?([BbQq])\?([^\n]*?)\?=/';
152
153 // Find all RFC2047's encoded words
154 if (preg_match_all($re, $input, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
155 // Initialize variables
156 $tmp = [];
157 $out = '';
158 $start = 0;
159
160 foreach ($matches as $idx => $m) {
161 $pos = $m[0][1];
162 $charset = $m[1][0];
163 $encoding = $m[2][0];
164 $text = $m[3][0];
165 $length = strlen($m[0][0]);
166
167 // Append everything that is before the text to be decoded
168 if ($start != $pos) {
169 $substr = substr($input, $start, $pos - $start);
170 $out .= Charset::convert($substr, $default_charset);
171 $start = $pos;
172 }
173 $start += $length;
174
175 // Per RFC2047, each string part "MUST represent an integral number
176 // of characters . A multi-octet character may not be split across
177 // adjacent encoded-words." However, some mailers break this, so we
178 // try to handle characters spanned across parts anyway by iterating
179 // through and aggregating sequential encoded parts with the same
180 // character set and encoding, then perform the decoding on the
181 // aggregation as a whole.
182
183 $tmp[] = $text;
184 if ($next_match = $matches[$idx + 1]) {
185 if ($next_match[0][1] == $start
186 && $next_match[1][0] == $charset
187 && $next_match[2][0] == $encoding
188 ) {
189 continue;
190 }
191 }
192
193 $count = count($tmp);
194 $text = '';
195
196 // Decode and join encoded-word's chunks
197 if ($encoding == 'B' || $encoding == 'b') {
198 $rest = '';
199 // base64 must be decoded a segment at a time.
200 // However, there are broken implementations that continue
201 // in the following word, we'll handle that (#6048)
202 for ($i = 0 ; $i < $count ; $i++) {
203 $chunk = $rest.$tmp[$i];
204 $length = strlen($chunk);
205 if ($length % 4) {
206 $length = floor($length / 4) * 4;
207 $rest = substr($chunk, $length);
208 $chunk = substr($chunk, 0, $length);
209 }
210
211 $text .= base64_decode($chunk);
212 }
213 } else { //if ($encoding == 'Q' || $encoding == 'q') {
214 // quoted printable can be combined and processed at once
215 for ($i = 0 ; $i < $count ; $i++) {
216 $text .= $tmp[$i];
217 }
218
219 $text = str_replace('_', ' ', $text);
220 $text = quoted_printable_decode($text);
221 }
222
223 $out .= Charset::convert($text, $charset);
224 $tmp = [];
225 }
226
227 // add the last part of the input string
228 if ($start != strlen($input)) {
229 $out .= Charset::convert(substr($input, $start), $default_charset);
230 }
231
232 // return the results
233 return $out;
234 }
235
236 // no encoding information, use fallback
237 return Charset::convert($input, $default_charset);
238 }
239
240 /**
241 * Decode a mime part
242 *
243 * @param string $input Input string
244 * @param string $encoding Part encoding
245 *
246 * @return string Decoded string
247 */
248 public static function decode($input, $encoding = '7bit')
249 {
250 switch (strtolower($encoding)) {
251 case 'quoted-printable':
252 return quoted_printable_decode($input);
253 case 'base64':
254 return base64_decode($input);
255 case 'x-uuencode':
256 case 'x-uue':
257 case 'uue':
258 case 'uuencode':
259 return convert_uudecode($input);
260 case '7bit':
261 default:
262 return $input;
263 }
264 }
265
266 /**
267 * Split RFC822 header string into an associative array
268 */
269 public static function parse_headers($headers)
270 {
271 $a_headers = [];
272 $headers = preg_replace('/\r?\n(\t| )+/', ' ', $headers);
273 $lines = explode("\n", $headers);
274 $count = count($lines);
275
276 for ($i = 0 ; $i < $count ; $i++) {
277 if ($p = strpos($lines[$i], ': ')) {
278 $field = strtolower(substr($lines[$i], 0, $p));
279 $value = trim(substr($lines[$i], $p + 1));
280 if (!empty($value)) {
281 $a_headers[$field] = $value;
282 }
283 }
284 }
285
286 return $a_headers;
287 }
288
289 /**
290 * E-mail address list parser
291 */
292 private static function parse_address_list($str, $decode = true, $fallback = null)
293 {
294 // remove any newlines and carriage returns before
295 $str = preg_replace('/\r?\n(\s|\t)?/', ' ', $str);
296
297 // extract list items, remove comments
298 $str = self::explode_header_string(',;', $str, true);
299 $result = [];
300
301 // simplified regexp, supporting quoted local part
302 $email_rx = '(\S+|("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+"))@\S+';
303
304 foreach ($str as $key => $val) {
305 $name = '';
306 $address = '';
307 $val = trim($val);
308
309 if (preg_match('/(.*)<('.$email_rx.')>$/', $val, $m)) {
310 $address = $m[2];
311 $name = trim($m[1]);
312 } elseif (preg_match('/^('.$email_rx.')$/', $val, $m)) {
313 $address = $m[1];
314 $name = '';
315 } // special case (#1489092)
316 elseif (preg_match('/(\s*<MAILER-DAEMON>)$/', $val, $m)) {
317 $address = 'MAILER-DAEMON';
318 $name = substr($val, 0, -strlen($m[1]));
319 } elseif (preg_match('/('.$email_rx.')/', $val, $m)) {
320 $name = $m[1];
321 } else {
322 $name = $val;
323 }
324
325 // dequote and/or decode name
326 if ($name) {
327 if ($name[0] == '"' && $name[strlen($name) - 1] == '"') {
328 $name = substr($name, 1, -1);
329 $name = stripslashes($name);
330 }
331 if ($decode) {
332 $name = self::decode_header($name, $fallback);
333 // some clients encode addressee name with quotes around it
334 if ($name[0] == '"' && $name[strlen($name) - 1] == '"') {
335 $name = substr($name, 1, -1);
336 }
337 }
338 }
339
340 if (!$address && $name) {
341 $address = $name;
342 $name = '';
343 }
344
345 if ($address) {
346 $address = self::fix_email($address);
347 $result[$key] = ['name' => $name, 'address' => $address];
348 }
349 }
350
351 return $result;
352 }
353
354 /**
355 * Explodes header (e.g. address-list) string into array of strings
356 * using specified separator characters with proper handling
357 * of quoted-strings and comments (RFC2822)
358 *
359 * @param string $separator String containing separator characters
360 * @param string $str Header string
361 * @param bool $remove_comments Enable to remove comments
362 *
363 * @return array Header items
364 */
365 public static function explode_header_string($separator, $str, $remove_comments = false)
366 {
367 $length = strlen($str);
368 $result = [];
369 $quoted = false;
370 $comment = 0;
371 $out = '';
372
373 for ($i = 0 ; $i < $length ; $i++) {
374 // we're inside a quoted string
375 if ($quoted) {
376 if ($str[$i] == '"') {
377 $quoted = false;
378 } elseif ($str[$i] == "\\") {
379 if ($comment <= 0) {
380 $out .= "\\";
381 }
382 $i++;
383 }
384 } // we are inside a comment string
385 elseif ($comment > 0) {
386 if ($str[$i] == ')') {
387 $comment--;
388 } elseif ($str[$i] == '(') {
389 $comment++;
390 } elseif ($str[$i] == "\\") {
391 $i++;
392 }
393 continue;
394 } // separator, add to result array
395 elseif (strpos($separator, $str[$i]) !== false) {
396 if ($out) {
397 $result[] = $out;
398 }
399 $out = '';
400 continue;
401 } // start of quoted string
402 elseif ($str[$i] == '"') {
403 $quoted = true;
404 } // start of comment
405 elseif ($remove_comments && $str[$i] == '(') {
406 $comment++;
407 }
408
409 if ($comment <= 0) {
410 $out .= $str[$i];
411 }
412 }
413
414 if ($out && $comment <= 0) {
415 $result[] = $out;
416 }
417
418 return $result;
419 }
420
421 /**
422 * Interpret a format=flowed message body according to RFC 2646
423 *
424 * @param string $text Raw body formatted as flowed text
425 * @param string $mark Mark each flowed line with specified character
426 * @param boolean $delsp Remove the trailing space of each flowed line
427 *
428 * @return string Interpreted text with unwrapped lines and stuffed space removed
429 */
430 public static function unfold_flowed($text, $mark = null, $delsp = false)
431 {
432 $text = preg_split('/\r?\n/', $text);
433 $last = -1;
434 $q_level = 0;
435 $marks = [];
436
437 foreach ($text as $idx => $line) {
438 if ($q = strspn($line, '>')) {
439 // remove quote chars
440 $line = substr($line, $q);
441 // remove (optional) space-staffing
442 if ($line[0] === ' ') $line = substr($line, 1);
443
444 // The same paragraph (We join current line with the previous one) when:
445 // - the same level of quoting
446 // - previous line was flowed
447 // - previous line contains more than only one single space (and quote char(s))
448 if ($q == $q_level
449 && isset($text[$last]) && $text[$last][strlen($text[$last]) - 1] == ' '
450 && !preg_match('/^>+ {0,1}$/', $text[$last])
451 ) {
452 if ($delsp) {
453 $text[$last] = substr($text[$last], 0, -1);
454 }
455 $text[$last] .= $line;
456 unset($text[$idx]);
457
458 if ($mark) {
459 $marks[$last] = true;
460 }
461 } else {
462 $last = $idx;
463 }
464 } else {
465 if ($line == '-- ') {
466 $last = $idx;
467 } else {
468 // remove space-stuffing
469 if ($line[0] === ' ') $line = substr($line, 1);
470
471 if (isset($text[$last]) && $line && !$q_level
472 && $text[$last] != '-- '
473 && $text[$last][strlen($text[$last]) - 1] == ' '
474 ) {
475 if ($delsp) {
476 $text[$last] = substr($text[$last], 0, -1);
477 }
478 $text[$last] .= $line;
479 unset($text[$idx]);
480
481 if ($mark) {
482 $marks[$last] = true;
483 }
484 } else {
485 $text[$idx] = $line;
486 $last = $idx;
487 }
488 }
489 }
490 $q_level = $q;
491 }
492
493 if (!empty($marks)) {
494 foreach (array_keys($marks) as $mk) {
495 $text[$mk] = $mark.$text[$mk];
496 }
497 }
498
499 return implode("\r\n", $text);
500 }
501
502 /**
503 * Wrap the given text to comply with RFC 2646
504 *
505 * @param string $text Text to wrap
506 * @param int $length Length
507 * @param string $charset Character encoding of $text
508 *
509 * @return string Wrapped text
510 */
511 public static function format_flowed($text, $length = 72, $charset = null)
512 {
513 $text = preg_split('/\r?\n/', $text);
514
515 foreach ($text as $idx => $line) {
516 if ($line != '-- ') {
517 if ($level = strspn($line, '>')) {
518 // remove quote chars
519 $line = substr($line, $level);
520 // remove (optional) space-staffing and spaces before the line end
521 $line = rtrim($line, ' ');
522 if ($line[0] === ' ') $line = substr($line, 1);
523
524 $prefix = str_repeat('>', $level).' ';
525 $line = $prefix.self::wordwrap($line, $length - $level - 2, " \r\n$prefix", false, $charset);
526 } elseif ($line) {
527 $line = self::wordwrap(rtrim($line), $length - 2, " \r\n", false, $charset);
528 // space-stuffing
529 $line = preg_replace('/(^|\r\n)(From| |>)/', '\\1 \\2', $line);
530 }
531
532 $text[$idx] = $line;
533 }
534 }
535
536 return implode("\r\n", $text);
537 }
538
539 /**
540 * Improved wordwrap function with multibyte support.
541 * The code is based on Zend_Text_MultiByte::wordWrap().
542 *
543 * @param string $string Text to wrap
544 * @param int $width Line width
545 * @param string $break Line separator
546 * @param bool $cut Enable to cut word
547 * @param string $charset Charset of $string
548 * @param bool $wrap_quoted When enabled quoted lines will not be wrapped
549 *
550 * @return string Text
551 */
552 public static function wordwrap($string, $width = 75, $break = "\n", $cut = false, $charset = null, $wrap_quoted = true)
553 {
554 // Note: Never try to use iconv instead of mbstring functions here
555 // Iconv's substr/strlen are 100x slower (#1489113)
556
557 if ($charset && $charset != IMAP2_CHARSET) {
558 mb_internal_encoding($charset);
559 }
560
561 // Convert \r\n to \n, this is our line-separator
562 $string = str_replace("\r\n", "\n", $string);
563 $separator = "\n"; // must be 1 character length
564 $result = [];
565
566 while (($stringLength = mb_strlen($string)) > 0) {
567 $breakPos = mb_strpos($string, $separator, 0);
568
569 // quoted line (do not wrap)
570 if ($wrap_quoted && $string[0] == '>') {
571 if ($breakPos === $stringLength - 1 || $breakPos === false) {
572 $subString = $string;
573 $cutLength = null;
574 } else {
575 $subString = mb_substr($string, 0, $breakPos);
576 $cutLength = $breakPos + 1;
577 }
578 } // next line found and current line is shorter than the limit
579 elseif ($breakPos !== false && $breakPos < $width) {
580 if ($breakPos === $stringLength - 1) {
581 $subString = $string;
582 $cutLength = null;
583 } else {
584 $subString = mb_substr($string, 0, $breakPos);
585 $cutLength = $breakPos + 1;
586 }
587 } else {
588 $subString = mb_substr($string, 0, $width);
589
590 // last line
591 if ($breakPos === false && $subString === $string) {
592 $cutLength = null;
593 } else {
594 $nextChar = mb_substr($string, $width, 1);
595
596 if ($nextChar === ' ' || $nextChar === $separator) {
597 $afterNextChar = mb_substr($string, $width + 1, 1);
598
599 // Note: mb_substr() does never return False
600 if ($afterNextChar === false || $afterNextChar === '') {
601 $subString .= $nextChar;
602 }
603
604 $cutLength = mb_strlen($subString) + 1;
605 } else {
606 $spacePos = mb_strrpos($subString, ' ', 0);
607
608 if ($spacePos !== false) {
609 $subString = mb_substr($subString, 0, $spacePos);
610 $cutLength = $spacePos + 1;
611 } elseif ($cut === false) {
612 $spacePos = mb_strpos($string, ' ', 0);
613
614 if ($spacePos !== false && ($breakPos === false || $spacePos < $breakPos)) {
615 $subString = mb_substr($string, 0, $spacePos);
616 $cutLength = $spacePos + 1;
617 } elseif ($breakPos === false) {
618 $subString = $string;
619 $cutLength = null;
620 } else {
621 $subString = mb_substr($string, 0, $breakPos);
622 $cutLength = $breakPos + 1;
623 }
624 } else {
625 $cutLength = $width;
626 }
627 }
628 }
629 }
630
631 $result[] = $subString;
632
633 if ($cutLength !== null) {
634 $string = mb_substr($string, $cutLength, ($stringLength - $cutLength));
635 } else {
636 break;
637 }
638 }
639
640 if ($charset && $charset != IMAP2_CHARSET) {
641 mb_internal_encoding(IMAP2_CHARSET);
642 }
643
644 return implode($break, $result);
645 }
646
647 /**
648 * Detect image type of the given binary data by checking magic numbers.
649 *
650 * @param string $data Binary file content
651 *
652 * @return string Detected mime-type or jpeg as fallback
653 */
654 public static function image_content_type($data)
655 {
656 $type = 'jpeg';
657 if (preg_match('/^\x89\x50\x4E\x47/', $data)) {
658 $type = 'png';
659 } elseif (preg_match('/^\x47\x49\x46\x38/', $data)) {
660 $type = 'gif';
661 } elseif (preg_match('/^\x00\x00\x01\x00/', $data)) $type = 'ico';
662
663 // else if (preg_match('/^\xFF\xD8\xFF\xE0/', $data)) $type = 'jpeg';
664
665 return 'image/'.$type;
666 }
667
668 /**
669 * Try to fix invalid email addresses
670 */
671 public static function fix_email($email)
672 {
673 $parts = Utils::explode_quoted_string('@', $email);
674 foreach ($parts as $idx => $part) {
675 // remove redundant quoting (#1490040)
676 if ($part[0] == '"' && preg_match('/^"([a-zA-Z0-9._+=-]+)"$/', $part, $m)) {
677 $parts[$idx] = $m[1];
678 }
679 }
680
681 return implode('@', $parts);
682 }
683 }
684