@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
at recaptime-dev/main 928 lines 26 kB view raw
1<?php 2 3namespace PhpMimeMailParser; 4 5use PhpMimeMailParser\Contracts\CharsetManager; 6 7/** 8 * Parser of php-mime-mail-parser 9 * 10 * Fully Tested Mailparse Extension Wrapper for PHP 5.4+ 11 * 12 */ 13class Parser 14{ 15 /** 16 * Attachment filename argument option for ->saveAttachments(). 17 */ 18 const ATTACHMENT_DUPLICATE_THROW = 'DuplicateThrow'; 19 const ATTACHMENT_DUPLICATE_SUFFIX = 'DuplicateSuffix'; 20 const ATTACHMENT_RANDOM_FILENAME = 'RandomFilename'; 21 22 /** 23 * PHP MimeParser Resource ID 24 * 25 * @var resource $resource 26 */ 27 protected $resource; 28 29 /** 30 * A file pointer to email 31 * 32 * @var resource $stream 33 */ 34 protected $stream; 35 36 /** 37 * A text of an email 38 * 39 * @var string $data 40 */ 41 protected $data; 42 43 /** 44 * Parts of an email 45 * 46 * @var array $parts 47 */ 48 protected $parts; 49 50 /** 51 * @var CharsetManager object 52 */ 53 protected $charset; 54 55 /** 56 * Valid stream modes for reading 57 * 58 * @var array 59 */ 60 protected static $readableModes = [ 61 'r', 'r+', 'w+', 'a+', 'x+', 'c+', 'rb', 'r+b', 'w+b', 'a+b', 62 'x+b', 'c+b', 'rt', 'r+t', 'w+t', 'a+t', 'x+t', 'c+t' 63 ]; 64 65 /** 66 * Stack of middleware registered to process data 67 * 68 * @var MiddlewareStack 69 */ 70 protected $middlewareStack; 71 72 /** 73 * Parser constructor. 74 * 75 * @param CharsetManager|null $charset 76 */ 77 public function __construct(?CharsetManager $charset = null) 78 { 79 if ($charset == null) { 80 $charset = new Charset(); 81 } 82 83 $this->charset = $charset; 84 $this->middlewareStack = new MiddlewareStack(); 85 } 86 87 /** 88 * Free the held resources 89 * 90 * @return void 91 */ 92 public function __destruct() 93 { 94 // clear the email file resource 95 if (is_resource($this->stream)) { 96 fclose($this->stream); 97 } 98 // clear the MailParse resource 99 if (is_resource($this->resource)) { 100 mailparse_msg_free($this->resource); 101 } 102 } 103 104 /** 105 * Set the file path we use to get the email text 106 * 107 * @param string $path File path to the MIME mail 108 * 109 * @return Parser MimeMailParser Instance 110 */ 111 public function setPath($path) 112 { 113 if (is_writable($path)) { 114 $file = fopen($path, 'a+'); 115 fseek($file, -1, SEEK_END); 116 if (fread($file, 1) != "\n") { 117 fwrite($file, PHP_EOL); 118 } 119 fclose($file); 120 } 121 122 // should parse message incrementally from file 123 $this->resource = mailparse_msg_parse_file($path); 124 $this->stream = fopen($path, 'r'); 125 $this->parse(); 126 127 return $this; 128 } 129 130 /** 131 * Set the Stream resource we use to get the email text 132 * 133 * @param resource $stream 134 * 135 * @return Parser MimeMailParser Instance 136 * @throws Exception 137 */ 138 public function setStream($stream) 139 { 140 // streams have to be cached to file first 141 $meta = @stream_get_meta_data($stream); 142 if (!$meta || !$meta['mode'] || !in_array($meta['mode'], self::$readableModes, true)) { 143 throw new Exception( 144 'setStream() expects parameter stream to be readable stream resource.' 145 ); 146 } 147 148 /** @var resource $tmp_fp */ 149 $tmp_fp = tmpfile(); 150 if ($tmp_fp) { 151 while (!feof($stream)) { 152 fwrite($tmp_fp, fread($stream, 2028)); 153 } 154 155 if (fread($tmp_fp, 1) != "\n") { 156 fwrite($tmp_fp, PHP_EOL); 157 } 158 159 fseek($tmp_fp, 0); 160 $this->stream = &$tmp_fp; 161 } else { 162 throw new Exception( 163 'Could not create temporary files for attachments. Your tmp directory may be unwritable by PHP.' 164 ); 165 } 166 fclose($stream); 167 168 $this->resource = mailparse_msg_create(); 169 // parses the message incrementally (low memory usage but slower) 170 while (!feof($this->stream)) { 171 mailparse_msg_parse($this->resource, fread($this->stream, 2082)); 172 } 173 $this->parse(); 174 175 return $this; 176 } 177 178 /** 179 * Set the email text 180 * 181 * @param string $data 182 * 183 * @return Parser MimeMailParser Instance 184 */ 185 public function setText($data) 186 { 187 if (empty($data)) { 188 throw new Exception('You must not call MimeMailParser::setText with an empty string parameter'); 189 } 190 191 if (substr($data, -1) != "\n") { 192 $data = $data.PHP_EOL; 193 } 194 195 $this->resource = mailparse_msg_create(); 196 // does not parse incrementally, fast memory hog might explode 197 mailparse_msg_parse($this->resource, $data); 198 $this->data = $data; 199 $this->parse(); 200 201 return $this; 202 } 203 204 /** 205 * Parse the Message into parts 206 * 207 * @return void 208 */ 209 protected function parse() 210 { 211 if (!$this->resource) { 212 throw new Exception( 213 'MIME message cannot be parsed' 214 ); 215 } 216 $structure = mailparse_msg_get_structure($this->resource); 217 $this->parts = []; 218 foreach ($structure as $part_id) { 219 $part = mailparse_msg_get_part($this->resource, $part_id); 220 $part_data = mailparse_msg_get_part_data($part); 221 $mimePart = new MimePart($part_id, $part_data); 222 // let each middleware parse the part before saving 223 $this->parts[$part_id] = $this->middlewareStack->parse($mimePart)->getPart(); 224 } 225 } 226 227 /** 228 * Retrieve a specific Email Header, without charset conversion. 229 * 230 * @param string $name Header name (case-insensitive) 231 * 232 * @return string|bool 233 * @throws Exception 234 */ 235 public function getRawHeader($name) 236 { 237 $name = strtolower($name); 238 if (isset($this->parts[1])) { 239 $headers = $this->getPart('headers', $this->parts[1]); 240 241 return isset($headers[$name]) ? $headers[$name] : false; 242 } else { 243 throw new Exception( 244 'setPath() or setText() or setStream() must be called before retrieving email headers.' 245 ); 246 } 247 } 248 249 /** 250 * Retrieve a specific Email Header 251 * 252 * @param string $name Header name (case-insensitive) 253 * 254 * @return string|false 255 */ 256 public function getHeader($name) 257 { 258 $rawHeader = $this->getRawHeader($name); 259 if ($rawHeader === false) { 260 return false; 261 } 262 263 return $this->decodeHeader($rawHeader); 264 } 265 266 /** 267 * Retrieve all mail headers 268 * 269 * @return array 270 * @throws Exception 271 */ 272 public function getHeaders() 273 { 274 if (isset($this->parts[1])) { 275 $headers = $this->getPart('headers', $this->parts[1]); 276 foreach ($headers as &$value) { 277 if (is_array($value)) { 278 foreach ($value as &$v) { 279 $v = $this->decodeSingleHeader($v); 280 } 281 } else { 282 $value = $this->decodeSingleHeader($value); 283 } 284 } 285 286 return $headers; 287 } else { 288 throw new Exception( 289 'setPath() or setText() or setStream() must be called before retrieving email headers.' 290 ); 291 } 292 } 293 294 /** 295 * Retrieve the raw mail headers as a string 296 * 297 * @return string 298 * @throws Exception 299 */ 300 public function getHeadersRaw() 301 { 302 if (isset($this->parts[1])) { 303 return $this->getPartHeader($this->parts[1]); 304 } else { 305 throw new Exception( 306 'setPath() or setText() or setStream() must be called before retrieving email headers.' 307 ); 308 } 309 } 310 311 /** 312 * Retrieve the raw Header of a MIME part 313 * 314 * @return String 315 * @param $part Object 316 * @throws Exception 317 */ 318 protected function getPartHeader(&$part) 319 { 320 $header = ''; 321 if ($this->stream) { 322 $header = $this->getPartHeaderFromFile($part); 323 } elseif ($this->data) { 324 $header = $this->getPartHeaderFromText($part); 325 } 326 return $header; 327 } 328 329 /** 330 * Retrieve the Header from a MIME part from file 331 * 332 * @return String Mime Header Part 333 * @param $part Array 334 */ 335 protected function getPartHeaderFromFile(&$part) 336 { 337 $start = $part['starting-pos']; 338 $end = $part['starting-pos-body']; 339 fseek($this->stream, $start, SEEK_SET); 340 $header = fread($this->stream, $end - $start); 341 return $header; 342 } 343 344 /** 345 * Retrieve the Header from a MIME part from text 346 * 347 * @return String Mime Header Part 348 * @param $part Array 349 */ 350 protected function getPartHeaderFromText(&$part) 351 { 352 $start = $part['starting-pos']; 353 $end = $part['starting-pos-body']; 354 $header = substr($this->data, $start, $end - $start); 355 return $header; 356 } 357 358 /** 359 * Checks whether a given part ID is a child of another part 360 * eg. an RFC822 attachment may have one or more text parts 361 * 362 * @param string $partId 363 * @param string $parentPartId 364 * @return bool 365 */ 366 protected function partIdIsChildOfPart($partId, $parentPartId) 367 { 368 $parentPartId = $parentPartId.'.'; 369 return substr($partId, 0, strlen($parentPartId)) == $parentPartId; 370 } 371 372 /** 373 * Whether the given part ID is a child of any attachment part in the message. 374 * 375 * @param string $checkPartId 376 * @return bool 377 */ 378 protected function partIdIsChildOfAnAttachment($checkPartId) 379 { 380 foreach ($this->parts as $partId => $part) { 381 if ($this->getPart('content-disposition', $part) == 'attachment') { 382 if ($this->partIdIsChildOfPart($checkPartId, $partId)) { 383 return true; 384 } 385 } 386 } 387 return false; 388 } 389 390 /** 391 * Returns the email message body in the specified format 392 * 393 * @param string $type text, html or htmlEmbedded 394 * 395 * @return string Body 396 * @throws Exception 397 */ 398 public function getMessageBody($type = 'text') 399 { 400 $mime_types = [ 401 'text' => 'text/plain', 402 'html' => 'text/html', 403 'htmlEmbedded' => 'text/html', 404 ]; 405 406 if (in_array($type, array_keys($mime_types))) { 407 $part_type = $type === 'htmlEmbedded' ? 'html' : $type; 408 $inline_parts = $this->getInlineParts($part_type); 409 $body = empty($inline_parts) ? '' : $inline_parts[0]; 410 } else { 411 throw new Exception( 412 'Invalid type specified for getMessageBody(). Expected: text, html or htmlEmbedded.' 413 ); 414 } 415 416 if ($type == 'htmlEmbedded') { 417 $attachments = $this->getAttachments(); 418 foreach ($attachments as $attachment) { 419 if ($attachment->getContentID() != '') { 420 $body = str_replace( 421 '"cid:'.$attachment->getContentID().'"', 422 '"'.$this->getEmbeddedData($attachment->getContentID()).'"', 423 $body 424 ); 425 } 426 } 427 } 428 429 return $body; 430 } 431 432 /** 433 * Returns the embedded data structure 434 * 435 * @param string $contentId Content-Id 436 * 437 * @return string 438 */ 439 protected function getEmbeddedData($contentId) 440 { 441 foreach ($this->parts as $part) { 442 if ($this->getPart('content-id', $part) == $contentId) { 443 $embeddedData = 'data:'; 444 $embeddedData .= $this->getPart('content-type', $part); 445 $embeddedData .= ';'.$this->getPart('transfer-encoding', $part); 446 $embeddedData .= ','.$this->getPartBody($part); 447 return $embeddedData; 448 } 449 } 450 return ''; 451 } 452 453 /** 454 * Return an array with the following keys display, address, is_group 455 * 456 * @param string $name Header name (case-insensitive) 457 * 458 * @return array<int, array{'display': string, 'address': string, 'is_group': bool}> 459 */ 460 public function getAddresses($name) 461 { 462 $value = $this->getRawHeader($name); 463 $value = (is_array($value)) ? $value[0] : $value; 464 $addresses = mailparse_rfc822_parse_addresses($value); 465 foreach ($addresses as $i => $item) { 466 $addresses[$i]['display'] = $this->decodeHeader($item['display']); 467 } 468 return $addresses; 469 } 470 471 /** 472 * Returns the inline parts contents (text or HTML) 473 * 474 * @return string[] The decoded inline parts. 475 */ 476 public function getInlineParts($type = 'text') 477 { 478 $inline_parts = []; 479 $mime_types = [ 480 'text' => 'text/plain', 481 'html' => 'text/html', 482 ]; 483 484 if (!in_array($type, array_keys($mime_types))) { 485 throw new Exception('Invalid type specified for getInlineParts(). "type" can either be text or html.'); 486 } 487 488 foreach ($this->parts as $partId => $part) { 489 if ($this->getPart('content-type', $part) == $mime_types[$type] 490 && $this->getPart('content-disposition', $part) != 'attachment' 491 && !$this->partIdIsChildOfAnAttachment($partId) 492 ) { 493 $headers = $this->getPart('headers', $part); 494 $encodingType = array_key_exists('content-transfer-encoding', $headers) ? 495 $headers['content-transfer-encoding'] : ''; 496 $undecoded_body = $this->decodeContentTransfer($this->getPartBody($part), $encodingType); 497 $inline_parts[] = $this->charset->decodeCharset($undecoded_body, $this->getPartCharset($part)); 498 } 499 } 500 501 return $inline_parts; 502 } 503 504 /** 505 * Returns the attachments contents in order of appearance 506 * 507 * @return Attachment[] 508 */ 509 public function getAttachments($include_inline = true) 510 { 511 $attachments = []; 512 $dispositions = $include_inline ? ['attachment', 'inline'] : ['attachment']; 513 $non_attachment_types = ['text/plain', 'text/html']; 514 $nonameIter = 0; 515 516 foreach ($this->parts as $part) { 517 $disposition = $this->getPart('content-disposition', $part); 518 $filename = 'noname'; 519 520 if (isset($part['disposition-filename'])) { 521 $filename = $this->decodeHeader($part['disposition-filename']); 522 } elseif (isset($part['content-name'])) { 523 // if we have no disposition but we have a content-name, it's a valid attachment. 524 // we simulate the presence of an attachment disposition with a disposition filename 525 $filename = $this->decodeHeader($part['content-name']); 526 $disposition = 'attachment'; 527 } elseif (in_array($part['content-type'], $non_attachment_types, true) 528 && $disposition !== 'attachment') { 529 // it is a message body, no attachment 530 continue; 531 } elseif (substr($part['content-type'], 0, 10) !== 'multipart/' 532 && $part['content-type'] !== 'text/plain; (error)' && $disposition != 'inline') { 533 // if we cannot get it by getMessageBody(), we assume it is an attachment 534 $disposition = 'attachment'; 535 } 536 if (in_array($disposition, ['attachment', 'inline']) === false && !empty($disposition)) { 537 $disposition = 'attachment'; 538 } 539 540 if (in_array($disposition, $dispositions) === true) { 541 if ($filename == 'noname') { 542 $nonameIter++; 543 $filename = 'noname'.$nonameIter; 544 } else { 545 // Escape all potentially unsafe characters from the filename 546 $filename = preg_replace('((^\.)|\/|[\n|\r|\n\r]|(\.$))', '_', $filename); 547 } 548 549 $headersAttachments = $this->getPart('headers', $part); 550 $contentidAttachments = $this->getPart('content-id', $part); 551 552 $attachmentStream = $this->getAttachmentStream($part); 553 $mimePartStr = $this->getPartComplete($part); 554 555 $attachments[] = new Attachment( 556 $filename, 557 $this->getPart('content-type', $part), 558 $attachmentStream, 559 $disposition, 560 $contentidAttachments, 561 $headersAttachments, 562 $mimePartStr 563 ); 564 } 565 } 566 567 return $attachments; 568 } 569 570 /** 571 * Save attachments in a folder 572 * 573 * @param string $attach_dir directory 574 * @param bool $include_inline 575 * @param string $filenameStrategy How to generate attachment filenames 576 * 577 * @return array Saved attachments paths 578 * @throws Exception 579 */ 580 public function saveAttachments( 581 $attach_dir, 582 $include_inline = true, 583 $filenameStrategy = self::ATTACHMENT_DUPLICATE_SUFFIX 584 ) { 585 $attachments = $this->getAttachments($include_inline); 586 587 $attachments_paths = []; 588 foreach ($attachments as $attachment) { 589 $attachments_paths[] = $attachment->save($attach_dir, $filenameStrategy); 590 } 591 592 return $attachments_paths; 593 } 594 595 /** 596 * Read the attachment Body and save temporary file resource 597 * 598 * @param array $part 599 * 600 * @return resource Mime Body Part 601 * @throws Exception 602 */ 603 protected function getAttachmentStream(&$part) 604 { 605 /** @var resource $temp_fp */ 606 $temp_fp = tmpfile(); 607 608 $headers = $this->getPart('headers', $part); 609 $encodingType = array_key_exists('content-transfer-encoding', $headers) ? 610 $headers['content-transfer-encoding'] : ''; 611 612 if ($temp_fp) { 613 if ($this->stream) { 614 $start = $part['starting-pos-body']; 615 $end = $part['ending-pos-body']; 616 fseek($this->stream, $start, SEEK_SET); 617 $len = $end - $start; 618 $written = 0; 619 while ($written < $len) { 620 $write = $len; 621 $data = fread($this->stream, $write); 622 fwrite($temp_fp, $this->decodeContentTransfer($data, $encodingType)); 623 $written += $write; 624 } 625 } elseif ($this->data) { 626 $attachment = $this->decodeContentTransfer($this->getPartBodyFromText($part), $encodingType); 627 fwrite($temp_fp, $attachment, strlen($attachment)); 628 } 629 fseek($temp_fp, 0, SEEK_SET); 630 } else { 631 throw new Exception( 632 'Could not create temporary files for attachments. Your tmp directory may be unwritable by PHP.' 633 ); 634 } 635 636 return $temp_fp; 637 } 638 639 /** 640 * Decode the string from Content-Transfer-Encoding 641 * 642 * @param string $encodedString The string in its original encoded state 643 * @param string $encodingType The encoding type from the Content-Transfer-Encoding header of the part. 644 * 645 * @return string The decoded string 646 */ 647 protected function decodeContentTransfer($encodedString, $encodingType) 648 { 649 if (is_array($encodingType)) { 650 $encodingType = $encodingType[0]; 651 } 652 653 $encodingType = strtolower($encodingType); 654 if ($encodingType == 'base64') { 655 return base64_decode($encodedString); 656 } elseif ($encodingType == 'quoted-printable') { 657 return quoted_printable_decode($encodedString); 658 } else { 659 return $encodedString; 660 } 661 } 662 663 /** 664 * $input can be a string or array 665 * 666 * @param string|array $input 667 * 668 * @return string 669 */ 670 protected function decodeHeader($input) 671 { 672 //Sometimes we have 2 label From so we take only the first 673 if (is_array($input)) { 674 return $this->decodeSingleHeader($input[0]); 675 } 676 677 return $this->decodeSingleHeader($input); 678 } 679 680 /** 681 * Decodes a single header (= string) 682 * 683 * @param string $input 684 * 685 * @return string 686 */ 687 protected function decodeSingleHeader($input) 688 { 689 // For each encoded-word... 690 while (preg_match('/(=\?([^?]+)\?(q|b)\?([^?]*)\?=)((\s+)=\?)?/i', $input, $matches)) { 691 $encoded = $matches[1]; 692 $charset = $matches[2]; 693 $encoding = $matches[3]; 694 $text = $matches[4]; 695 $space = isset($matches[6]) ? $matches[6] : ''; 696 697 switch (strtolower($encoding)) { 698 case 'b': 699 $text = $this->decodeContentTransfer($text, 'base64'); 700 break; 701 702 case 'q': 703 $text = str_replace('_', ' ', $text); 704 preg_match_all('/=([a-f0-9]{2})/i', $text, $matches); 705 foreach ($matches[1] as $value) { 706 $text = str_replace('='.$value, chr(hexdec($value)), $text); 707 } 708 break; 709 } 710 711 $text = $this->charset->decodeCharset($text, $this->charset->getCharsetAlias($charset)); 712 $input = str_replace($encoded.$space, $text, $input); 713 } 714 715 return $input; 716 } 717 718 /** 719 * Return the charset of the MIME part 720 * 721 * @param array $part 722 * 723 * @return string 724 */ 725 protected function getPartCharset($part) 726 { 727 if (isset($part['charset'])) { 728 return $this->charset->getCharsetAlias($part['charset']); 729 } else { 730 return 'us-ascii'; 731 } 732 } 733 734 /** 735 * Retrieve a specified MIME part 736 * 737 * @param string $type 738 * @param array $parts 739 * 740 * @return string|array 741 */ 742 protected function getPart($type, $parts) 743 { 744 return (isset($parts[$type])) ? $parts[$type] : false; 745 } 746 747 /** 748 * Retrieve the Body of a MIME part 749 * 750 * @param array $part 751 * 752 * @return string 753 */ 754 protected function getPartBody(&$part) 755 { 756 $body = ''; 757 if ($this->stream) { 758 $body = $this->getPartBodyFromFile($part); 759 } elseif ($this->data) { 760 $body = $this->getPartBodyFromText($part); 761 } 762 763 return $body; 764 } 765 766 /** 767 * Retrieve the Body from a MIME part from file 768 * 769 * @param array $part 770 * 771 * @return string Mime Body Part 772 */ 773 protected function getPartBodyFromFile(&$part) 774 { 775 $start = $part['starting-pos-body']; 776 $end = $part['ending-pos-body']; 777 $body = ''; 778 if ($end - $start > 0) { 779 fseek($this->stream, $start, SEEK_SET); 780 $body = fread($this->stream, $end - $start); 781 } 782 783 return $body; 784 } 785 786 /** 787 * Retrieve the Body from a MIME part from text 788 * 789 * @param array $part 790 * 791 * @return string Mime Body Part 792 */ 793 protected function getPartBodyFromText(&$part) 794 { 795 $start = $part['starting-pos-body']; 796 $end = $part['ending-pos-body']; 797 798 return substr($this->data, $start, $end - $start); 799 } 800 801 /** 802 * Retrieve the content of a MIME part 803 * 804 * @param array $part 805 * 806 * @return string 807 */ 808 protected function getPartComplete(&$part) 809 { 810 $body = ''; 811 if ($this->stream) { 812 $body = $this->getPartFromFile($part); 813 } elseif ($this->data) { 814 $body = $this->getPartFromText($part); 815 } 816 817 return $body; 818 } 819 820 /** 821 * Retrieve the content from a MIME part from file 822 * 823 * @param array $part 824 * 825 * @return string Mime Content 826 */ 827 protected function getPartFromFile(&$part) 828 { 829 $start = $part['starting-pos']; 830 $end = $part['ending-pos']; 831 $body = ''; 832 if ($end - $start > 0) { 833 fseek($this->stream, $start, SEEK_SET); 834 $body = fread($this->stream, $end - $start); 835 } 836 837 return $body; 838 } 839 840 /** 841 * Retrieve the content from a MIME part from text 842 * 843 * @param array $part 844 * 845 * @return string Mime Content 846 */ 847 protected function getPartFromText(&$part) 848 { 849 $start = $part['starting-pos']; 850 $end = $part['ending-pos']; 851 852 return substr($this->data, $start, $end - $start); 853 } 854 855 /** 856 * Retrieve the resource 857 * 858 * @return resource resource 859 */ 860 public function getResource() 861 { 862 return $this->resource; 863 } 864 865 /** 866 * Retrieve the file pointer to email 867 * 868 * @return resource stream 869 */ 870 public function getStream() 871 { 872 return $this->stream; 873 } 874 875 /** 876 * Retrieve the text of an email 877 * 878 * @return string data 879 */ 880 public function getData() 881 { 882 return $this->data; 883 } 884 885 /** 886 * Retrieve the parts of an email 887 * 888 * @return array parts 889 */ 890 public function getParts() 891 { 892 return $this->parts; 893 } 894 895 /** 896 * Retrieve the charset manager object 897 * 898 * @return CharsetManager charset 899 */ 900 public function getCharset() 901 { 902 return $this->charset; 903 } 904 905 /** 906 * Add a middleware to the parser MiddlewareStack 907 * Each middleware is invoked when: 908 * a MimePart is retrieved by mailparse_msg_get_part_data() during $this->parse() 909 * The middleware will receive MimePart $part and the next MiddlewareStack $next 910 * 911 * Eg: 912 * 913 * $Parser->addMiddleware(function(MimePart $part, MiddlewareStack $next) { 914 * // do something with the $part 915 * return $next($part); 916 * }); 917 * 918 * @param callable $middleware Plain Function or Middleware Instance to execute 919 * @return void 920 */ 921 public function addMiddleware(callable $middleware) 922 { 923 if (!$middleware instanceof Middleware) { 924 $middleware = new Middleware($middleware); 925 } 926 $this->middlewareStack = $this->middlewareStack->add($middleware); 927 } 928}