From e6cc458b1ce8965879962b1d453a7b1f05a49f63 Mon Sep 17 00:00:00 2001 From: Marcus Hill Date: Tue, 31 Jan 2023 14:11:02 +0000 Subject: [PATCH] Add PHP Mime Mail Parser - https://github.com/php/pecl-mail-mailparse --- .../php-mime-mail-parser/src/Attachment.php | 276 ++++++ plugins/php-mime-mail-parser/src/Charset.php | 370 +++++++ .../src/Contracts/CharsetManager.php | 24 + .../src/Contracts/Middleware.php | 23 + .../php-mime-mail-parser/src/Exception.php | 8 + .../php-mime-mail-parser/src/Middleware.php | 29 + .../src/MiddlewareStack.php | 89 ++ plugins/php-mime-mail-parser/src/MimePart.php | 119 +++ plugins/php-mime-mail-parser/src/Parser.php | 923 ++++++++++++++++++ .../src/php-mime-mail-parser-8.0.0.zip | Bin 0 -> 22820 bytes .../.github/workflows/main.yml | 63 ++ .../src/php-mime-mail-parser-8.0.0/LICENSE | 21 + .../src/php-mime-mail-parser-8.0.0/README.md | 257 +++++ .../compile_mailparse.sh | 10 + .../php-mime-mail-parser-8.0.0/composer.json | 60 ++ .../mailparse-stubs.php | 303 ++++++ .../phpunit.xml.dist | 6 + 17 files changed, 2581 insertions(+) create mode 100644 plugins/php-mime-mail-parser/src/Attachment.php create mode 100644 plugins/php-mime-mail-parser/src/Charset.php create mode 100644 plugins/php-mime-mail-parser/src/Contracts/CharsetManager.php create mode 100644 plugins/php-mime-mail-parser/src/Contracts/Middleware.php create mode 100644 plugins/php-mime-mail-parser/src/Exception.php create mode 100644 plugins/php-mime-mail-parser/src/Middleware.php create mode 100644 plugins/php-mime-mail-parser/src/MiddlewareStack.php create mode 100644 plugins/php-mime-mail-parser/src/MimePart.php create mode 100644 plugins/php-mime-mail-parser/src/Parser.php create mode 100644 plugins/php-mime-mail-parser/src/php-mime-mail-parser-8.0.0.zip create mode 100644 plugins/php-mime-mail-parser/src/php-mime-mail-parser-8.0.0/.github/workflows/main.yml create mode 100644 plugins/php-mime-mail-parser/src/php-mime-mail-parser-8.0.0/LICENSE create mode 100644 plugins/php-mime-mail-parser/src/php-mime-mail-parser-8.0.0/README.md create mode 100644 plugins/php-mime-mail-parser/src/php-mime-mail-parser-8.0.0/compile_mailparse.sh create mode 100644 plugins/php-mime-mail-parser/src/php-mime-mail-parser-8.0.0/composer.json create mode 100644 plugins/php-mime-mail-parser/src/php-mime-mail-parser-8.0.0/mailparse-stubs.php create mode 100644 plugins/php-mime-mail-parser/src/php-mime-mail-parser-8.0.0/phpunit.xml.dist diff --git a/plugins/php-mime-mail-parser/src/Attachment.php b/plugins/php-mime-mail-parser/src/Attachment.php new file mode 100644 index 00000000..1a731635 --- /dev/null +++ b/plugins/php-mime-mail-parser/src/Attachment.php @@ -0,0 +1,276 @@ +filename = $filename; + $this->contentType = $contentType; + $this->stream = $stream; + $this->content = null; + $this->contentDisposition = $contentDisposition; + $this->contentId = $contentId; + $this->headers = $headers; + $this->mimePartStr = $mimePartStr; + } + + /** + * retrieve the attachment filename + * + * @return string + */ + public function getFilename() + { + return $this->filename; + } + + /** + * Retrieve the Attachment Content-Type + * + * @return string + */ + public function getContentType() + { + return $this->contentType; + } + + /** + * Retrieve the Attachment Content-Disposition + * + * @return string + */ + public function getContentDisposition() + { + return $this->contentDisposition; + } + + /** + * Retrieve the Attachment Content-ID + * + * @return string + */ + public function getContentID() + { + return $this->contentId; + } + + /** + * Retrieve the Attachment Headers + * + * @return array + */ + public function getHeaders() + { + return $this->headers; + } + + /** + * Get a handle to the stream + * + * @return resource + */ + public function getStream() + { + return $this->stream; + } + + /** + * Rename a file if it already exists at its destination. + * Renaming is done by adding a duplicate number to the file name. E.g. existingFileName_1.ext. + * After a max duplicate number, renaming the file will switch over to generating a random suffix. + * + * @param string $fileName Complete path to the file. + * @return string The suffixed file name. + */ + protected function suffixFileName(string $fileName): string + { + $pathInfo = pathinfo($fileName); + $dirname = $pathInfo['dirname'].DIRECTORY_SEPARATOR; + $filename = $pathInfo['filename']; + $extension = empty($pathInfo['extension']) ? '' : '.'.$pathInfo['extension']; + + $i = 0; + do { + $i++; + + if ($i > $this->maxDuplicateNumber) { + $duplicateExtension = uniqid(); + } else { + $duplicateExtension = $i; + } + + $resultName = $dirname.$filename."_$duplicateExtension".$extension; + } while (file_exists($resultName)); + + return $resultName; + } + + /** + * Read the contents a few bytes at a time until completed + * Once read to completion, it always returns false + * + * @param int $bytes (default: 2082) + * + * @return string|bool + */ + public function read($bytes = 2082) + { + return feof($this->stream) ? false : fread($this->stream, $bytes); + } + + /** + * Retrieve the file content in one go + * Once you retrieve the content you cannot use MimeMailParser_attachment::read() + * + * @return string + */ + public function getContent() + { + if ($this->content === null) { + fseek($this->stream, 0); + while (($buf = $this->read()) !== false) { + $this->content .= $buf; + } + } + + return $this->content; + } + + /** + * Get mime part string for this attachment + * + * @return string + */ + public function getMimePartStr() + { + return $this->mimePartStr; + } + + /** + * Save the attachment individually + * + * @param string $attach_dir + * @param string $filenameStrategy + * + * @return string + */ + public function save( + $attach_dir, + $filenameStrategy = Parser::ATTACHMENT_DUPLICATE_SUFFIX + ) { + $attach_dir = rtrim($attach_dir, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR; + if (!is_dir($attach_dir)) { + mkdir($attach_dir); + } + + // Determine filename + switch ($filenameStrategy) { + case Parser::ATTACHMENT_RANDOM_FILENAME: + $fileInfo = pathinfo($this->getFilename()); + $extension = empty($fileInfo['extension']) ? '' : '.'.$fileInfo['extension']; + $attachment_path = $attach_dir.uniqid().$extension; + break; + case Parser::ATTACHMENT_DUPLICATE_THROW: + case Parser::ATTACHMENT_DUPLICATE_SUFFIX: + $attachment_path = $attach_dir.$this->getFilename(); + break; + default: + throw new Exception('Invalid filename strategy argument provided.'); + } + + // Handle duplicate filename + if (file_exists($attachment_path)) { + switch ($filenameStrategy) { + case Parser::ATTACHMENT_DUPLICATE_THROW: + throw new Exception('Could not create file for attachment: duplicate filename.'); + case Parser::ATTACHMENT_DUPLICATE_SUFFIX: + $attachment_path = $this->suffixFileName($attachment_path); + break; + } + } + + /** @var resource $fp */ + if ($fp = fopen($attachment_path, 'w')) { + while ($bytes = $this->read()) { + fwrite($fp, $bytes); + } + fclose($fp); + return realpath($attachment_path); + } else { + throw new Exception('Could not write attachments. Your directory may be unwritable by PHP.'); + } + } +} diff --git a/plugins/php-mime-mail-parser/src/Charset.php b/plugins/php-mime-mail-parser/src/Charset.php new file mode 100644 index 00000000..cd219f22 --- /dev/null +++ b/plugins/php-mime-mail-parser/src/Charset.php @@ -0,0 +1,370 @@ + 'us-ascii', + 'us-ascii' => 'us-ascii', + 'ansi_x3.4-1968' => 'us-ascii', + '646' => 'us-ascii', + 'iso-8859-1' => 'iso-8859-1', + 'iso-8859-2' => 'iso-8859-2', + 'iso-8859-3' => 'iso-8859-3', + 'iso-8859-4' => 'iso-8859-4', + 'iso-8859-5' => 'iso-8859-5', + 'iso-8859-6' => 'iso-8859-6', + 'iso-8859-6-i' => 'iso-8859-6-i', + 'iso-8859-6-e' => 'iso-8859-6-e', + 'iso-8859-7' => 'iso-8859-7', + 'iso-8859-8' => 'iso-8859-8', + 'iso-8859-8-i' => 'iso-8859-8', + 'iso-8859-8-e' => 'iso-8859-8-e', + 'iso-8859-9' => 'iso-8859-9', + 'iso-8859-10' => 'iso-8859-10', + 'iso-8859-11' => 'iso-8859-11', + 'iso-8859-13' => 'iso-8859-13', + 'iso-8859-14' => 'iso-8859-14', + 'iso-8859-15' => 'iso-8859-15', + 'iso-8859-16' => 'iso-8859-16', + 'iso-ir-111' => 'iso-ir-111', + 'iso-2022-cn' => 'iso-2022-cn', + 'iso-2022-cn-ext' => 'iso-2022-cn', + 'iso-2022-kr' => 'iso-2022-kr', + 'iso-2022-jp' => 'iso-2022-jp', + 'utf-16be' => 'utf-16be', + 'utf-16le' => 'utf-16le', + 'utf-16' => 'utf-16', + 'windows-1250' => 'windows-1250', + 'windows-1251' => 'windows-1251', + 'windows-1252' => 'windows-1252', + 'windows-1253' => 'windows-1253', + 'windows-1254' => 'windows-1254', + 'windows-1255' => 'windows-1255', + 'windows-1256' => 'windows-1256', + 'windows-1257' => 'windows-1257', + 'windows-1258' => 'windows-1258', + 'ibm866' => 'ibm866', + 'ibm850' => 'ibm850', + 'ibm852' => 'ibm852', + 'ibm855' => 'ibm855', + 'ibm857' => 'ibm857', + 'ibm862' => 'ibm862', + 'ibm864' => 'ibm864', + 'utf-8' => 'utf-8', + 'utf-7' => 'utf-7', + 'shift_jis' => 'shift_jis', + 'big5' => 'big5', + 'euc-jp' => 'euc-jp', + 'euc-kr' => 'euc-kr', + 'gb2312' => 'gb2312', + 'gb18030' => 'gb18030', + 'viscii' => 'viscii', + 'koi8-r' => 'koi8-r', + 'koi8_r' => 'koi8-r', + 'cskoi8r' => 'koi8-r', + 'koi' => 'koi8-r', + 'koi8' => 'koi8-r', + 'koi8-u' => 'koi8-u', + 'tis-620' => 'tis-620', + 't.61-8bit' => 't.61-8bit', + 'hz-gb-2312' => 'hz-gb-2312', + 'big5-hkscs' => 'big5-hkscs', + 'gbk' => 'gbk', + 'cns11643' => 'x-euc-tw', + 'x-imap4-modified-utf7' => 'x-imap4-modified-utf7', + 'x-euc-tw' => 'x-euc-tw', + 'x-mac-ce' => 'macce', + 'x-mac-turkish' => 'macturkish', + 'x-mac-greek' => 'macgreek', + 'x-mac-icelandic' => 'macicelandic', + 'x-mac-croatian' => 'maccroatian', + 'x-mac-romanian' => 'macromanian', + 'x-mac-cyrillic' => 'maccyrillic', + 'x-mac-ukrainian' => 'macukrainian', + 'x-mac-hebrew' => 'machebrew', + 'x-mac-arabic' => 'macarabic', + 'x-mac-farsi' => 'macfarsi', + 'x-mac-devanagari' => 'macdevanagari', + 'x-mac-gujarati' => 'macgujarati', + 'x-mac-gurmukhi' => 'macgurmukhi', + 'armscii-8' => 'armscii-8', + 'x-viet-tcvn5712' => 'x-viet-tcvn5712', + 'x-viet-vps' => 'x-viet-vps', + 'iso-10646-ucs-2' => 'utf-16be', + 'x-iso-10646-ucs-2-be' => 'utf-16be', + 'x-iso-10646-ucs-2-le' => 'utf-16le', + 'x-user-defined' => 'x-user-defined', + 'x-johab' => 'x-johab', + 'latin1' => 'iso-8859-1', + 'iso_8859-1' => 'iso-8859-1', + 'iso8859-1' => 'iso-8859-1', + 'iso8859-2' => 'iso-8859-2', + 'iso8859-3' => 'iso-8859-3', + 'iso8859-4' => 'iso-8859-4', + 'iso8859-5' => 'iso-8859-5', + 'iso8859-6' => 'iso-8859-6', + 'iso8859-7' => 'iso-8859-7', + 'iso8859-8' => 'iso-8859-8', + 'iso8859-9' => 'iso-8859-9', + 'iso8859-10' => 'iso-8859-10', + 'iso8859-11' => 'iso-8859-11', + 'iso8859-13' => 'iso-8859-13', + 'iso8859-14' => 'iso-8859-14', + 'iso8859-15' => 'iso-8859-15', + 'iso_8859-1:1987' => 'iso-8859-1', + 'iso-ir-100' => 'iso-8859-1', + 'l1' => 'iso-8859-1', + 'ibm819' => 'iso-8859-1', + 'cp819' => 'iso-8859-1', + 'csisolatin1' => 'iso-8859-1', + 'latin2' => 'iso-8859-2', + 'iso_8859-2' => 'iso-8859-2', + 'iso_8859-2:1987' => 'iso-8859-2', + 'iso-ir-101' => 'iso-8859-2', + 'l2' => 'iso-8859-2', + 'csisolatin2' => 'iso-8859-2', + 'latin3' => 'iso-8859-3', + 'iso_8859-3' => 'iso-8859-3', + 'iso_8859-3:1988' => 'iso-8859-3', + 'iso-ir-109' => 'iso-8859-3', + 'l3' => 'iso-8859-3', + 'csisolatin3' => 'iso-8859-3', + 'latin4' => 'iso-8859-4', + 'iso_8859-4' => 'iso-8859-4', + 'iso_8859-4:1988' => 'iso-8859-4', + 'iso-ir-110' => 'iso-8859-4', + 'l4' => 'iso-8859-4', + 'csisolatin4' => 'iso-8859-4', + 'cyrillic' => 'iso-8859-5', + 'iso_8859-5' => 'iso-8859-5', + 'iso_8859-5:1988' => 'iso-8859-5', + 'iso-ir-144' => 'iso-8859-5', + 'csisolatincyrillic' => 'iso-8859-5', + 'arabic' => 'iso-8859-6', + 'iso_8859-6' => 'iso-8859-6', + 'iso_8859-6:1987' => 'iso-8859-6', + 'iso-ir-127' => 'iso-8859-6', + 'ecma-114' => 'iso-8859-6', + 'asmo-708' => 'iso-8859-6', + 'csisolatinarabic' => 'iso-8859-6', + 'csiso88596i' => 'iso-8859-6-i', + 'csiso88596e' => 'iso-8859-6-e', + 'greek' => 'iso-8859-7', + 'greek8' => 'iso-8859-7', + 'sun_eu_greek' => 'iso-8859-7', + 'iso_8859-7' => 'iso-8859-7', + 'iso_8859-7:1987' => 'iso-8859-7', + 'iso-ir-126' => 'iso-8859-7', + 'elot_928' => 'iso-8859-7', + 'ecma-118' => 'iso-8859-7', + 'csisolatingreek' => 'iso-8859-7', + 'hebrew' => 'iso-8859-8', + 'iso_8859-8' => 'iso-8859-8', + 'visual' => 'iso-8859-8', + 'iso_8859-8:1988' => 'iso-8859-8', + 'iso-ir-138' => 'iso-8859-8', + 'csisolatinhebrew' => 'iso-8859-8', + 'csiso88598i' => 'iso-8859-8', + 'iso-8859-8i' => 'iso-8859-8', + 'logical' => 'iso-8859-8', + 'csiso88598e' => 'iso-8859-8-e', + 'latin5' => 'iso-8859-9', + 'iso_8859-9' => 'iso-8859-9', + 'iso_8859-9:1989' => 'iso-8859-9', + 'iso-ir-148' => 'iso-8859-9', + 'l5' => 'iso-8859-9', + 'csisolatin5' => 'iso-8859-9', + 'unicode-1-1-utf-8' => 'utf-8', + 'utf8' => 'utf-8', + 'x-sjis' => 'shift_jis', + 'shift-jis' => 'shift_jis', + 'ms_kanji' => 'shift_jis', + 'csshiftjis' => 'shift_jis', + 'windows-31j' => 'shift_jis', + 'cp932' => 'shift_jis', + 'sjis' => 'shift_jis', + 'cseucpkdfmtjapanese' => 'euc-jp', + 'x-euc-jp' => 'euc-jp', + 'csiso2022jp' => 'iso-2022-jp', + 'iso-2022-jp-2' => 'iso-2022-jp', + 'csiso2022jp2' => 'iso-2022-jp', + 'csbig5' => 'big5', + 'cn-big5' => 'big5', + 'x-x-big5' => 'big5', + 'zh_tw-big5' => 'big5', + 'cseuckr' => 'euc-kr', + 'ks_c_5601-1987' => 'euc-kr', + 'iso-ir-149' => 'euc-kr', + 'ks_c_5601-1989' => 'euc-kr', + 'ksc_5601' => 'euc-kr', + 'ksc5601' => 'euc-kr', + 'korean' => 'euc-kr', + 'csksc56011987' => 'euc-kr', + '5601' => 'euc-kr', + 'windows-949' => 'euc-kr', + 'gb_2312-80' => 'gb2312', + 'iso-ir-58' => 'gb2312', + 'chinese' => 'gb2312', + 'csiso58gb231280' => 'gb2312', + 'csgb2312' => 'gb2312', + 'zh_cn.euc' => 'gb2312', + 'gb_2312' => 'gb2312', + 'x-cp1250' => 'windows-1250', + 'x-cp1251' => 'windows-1251', + 'x-cp1252' => 'windows-1252', + 'x-cp1253' => 'windows-1253', + 'x-cp1254' => 'windows-1254', + 'x-cp1255' => 'windows-1255', + 'x-cp1256' => 'windows-1256', + 'x-cp1257' => 'windows-1257', + 'x-cp1258' => 'windows-1258', + 'windows-874' => 'windows-874', + 'ibm874' => 'windows-874', + 'dos-874' => 'windows-874', + 'macintosh' => 'macintosh', + 'x-mac-roman' => 'macintosh', + 'mac' => 'macintosh', + 'csmacintosh' => 'macintosh', + 'cp866' => 'ibm866', + 'cp-866' => 'ibm866', + '866' => 'ibm866', + 'csibm866' => 'ibm866', + 'cp850' => 'ibm850', + '850' => 'ibm850', + 'csibm850' => 'ibm850', + 'cp852' => 'ibm852', + '852' => 'ibm852', + 'csibm852' => 'ibm852', + 'cp855' => 'ibm855', + '855' => 'ibm855', + 'csibm855' => 'ibm855', + 'cp857' => 'ibm857', + '857' => 'ibm857', + 'csibm857' => 'ibm857', + 'cp862' => 'ibm862', + '862' => 'ibm862', + 'csibm862' => 'ibm862', + 'cp864' => 'ibm864', + '864' => 'ibm864', + 'csibm864' => 'ibm864', + 'ibm-864' => 'ibm864', + 't.61' => 't.61-8bit', + 'iso-ir-103' => 't.61-8bit', + 'csiso103t618bit' => 't.61-8bit', + 'x-unicode-2-0-utf-7' => 'utf-7', + 'unicode-2-0-utf-7' => 'utf-7', + 'unicode-1-1-utf-7' => 'utf-7', + 'csunicode11utf7' => 'utf-7', + 'csunicode' => 'utf-16be', + 'csunicode11' => 'utf-16be', + 'iso-10646-ucs-basic' => 'utf-16be', + 'csunicodeascii' => 'utf-16be', + 'iso-10646-unicode-latin1' => 'utf-16be', + 'csunicodelatin1' => 'utf-16be', + 'iso-10646' => 'utf-16be', + 'iso-10646-j-1' => 'utf-16be', + 'latin6' => 'iso-8859-10', + 'iso-ir-157' => 'iso-8859-10', + 'l6' => 'iso-8859-10', + 'csisolatin6' => 'iso-8859-10', + 'iso_8859-15' => 'iso-8859-15', + 'csisolatin9' => 'iso-8859-15', + 'l9' => 'iso-8859-15', + 'ecma-cyrillic' => 'iso-ir-111', + 'csiso111ecmacyrillic' => 'iso-ir-111', + 'csiso2022kr' => 'iso-2022-kr', + 'csviscii' => 'viscii', + 'zh_tw-euc' => 'x-euc-tw', + 'iso88591' => 'iso-8859-1', + 'iso88592' => 'iso-8859-2', + 'iso88593' => 'iso-8859-3', + 'iso88594' => 'iso-8859-4', + 'iso88595' => 'iso-8859-5', + 'iso88596' => 'iso-8859-6', + 'iso88597' => 'iso-8859-7', + 'iso88598' => 'iso-8859-8', + 'iso88599' => 'iso-8859-9', + 'iso885910' => 'iso-8859-10', + 'iso885911' => 'iso-8859-11', + 'iso885912' => 'iso-8859-12', + 'iso885913' => 'iso-8859-13', + 'iso885914' => 'iso-8859-14', + 'iso885915' => 'iso-8859-15', + 'tis620' => 'tis-620', + 'cp1250' => 'windows-1250', + 'cp1251' => 'windows-1251', + 'cp1252' => 'windows-1252', + 'cp1253' => 'windows-1253', + 'cp1254' => 'windows-1254', + 'cp1255' => 'windows-1255', + 'cp1256' => 'windows-1256', + 'cp1257' => 'windows-1257', + 'cp1258' => 'windows-1258', + 'x-gbk' => 'gbk', + 'windows-936' => 'gbk', + 'ansi-1251' => 'windows-1251', + ]; + + /** + * {@inheritdoc} + */ + public function decodeCharset($encodedString, $charset) + { + $charset = $this->getCharsetAlias($charset); + + if ($charset == 'utf-8' || $charset == 'us-ascii') { + return $encodedString; + } + + if (function_exists('mb_convert_encoding')) { + if ($charset == 'iso-2022-jp') { + return mb_convert_encoding($encodedString, 'utf-8', 'iso-2022-jp-ms'); + } + + if (array_search($charset, $this->getSupportedEncodings())) { + return mb_convert_encoding($encodedString, 'utf-8', $charset); + } + } + + return iconv($charset, 'utf-8//translit//ignore', $encodedString); + } + + /** + * {@inheritdoc} + */ + public function getCharsetAlias($charset) + { + $charset = strtolower($charset); + + if (array_key_exists($charset, $this->charsetAlias)) { + return $this->charsetAlias[$charset]; + } + + return 'us-ascii'; + } + + private function getSupportedEncodings() + { + return + array_map( + 'strtolower', + array_unique( + array_merge( + $enc = mb_list_encodings(), + call_user_func_array( + 'array_merge', + array_map( + "mb_encoding_aliases", + $enc + ) + ) + ) + ) + ); + } +} diff --git a/plugins/php-mime-mail-parser/src/Contracts/CharsetManager.php b/plugins/php-mime-mail-parser/src/Contracts/CharsetManager.php new file mode 100644 index 00000000..660ec00c --- /dev/null +++ b/plugins/php-mime-mail-parser/src/Contracts/CharsetManager.php @@ -0,0 +1,24 @@ +parser = $fn; + } + + /** + * Process a mime part, optionally delegating parsing to the $next MiddlewareStack + */ + public function parse(MimePart $part, MiddlewareStack $next) + { + return call_user_func($this->parser, $part, $next); + } +} diff --git a/plugins/php-mime-mail-parser/src/MiddlewareStack.php b/plugins/php-mime-mail-parser/src/MiddlewareStack.php new file mode 100644 index 00000000..3ef6da93 --- /dev/null +++ b/plugins/php-mime-mail-parser/src/MiddlewareStack.php @@ -0,0 +1,89 @@ +add($Middleware) + * + * @param Middleware $middleware + */ + public function __construct(MiddleWareContracts $middleware = null) + { + $this->middleware = $middleware; + } + + /** + * Creates a chained middleware in MiddlewareStack + * + * @param Middleware $middleware + * @return MiddlewareStack Immutable MiddlewareStack + */ + public function add(MiddleWareContracts $middleware) + { + $stack = new static($middleware); + $stack->next = $this; + return $stack; + } + + /** + * Parses the MimePart by passing it through the Middleware + * @param MimePart $part + * @return MimePart + */ + public function parse(MimePart $part) + { + if (!$this->middleware) { + return $part; + } + $part = call_user_func(array($this->middleware, 'parse'), $part, $this->next); + return $part; + } + + /** + * Creates a MiddlewareStack based on an array of middleware + * + * @param Middleware[] $middlewares + * @return MiddlewareStack + */ + public static function factory(array $middlewares = array()) + { + $stack = new static; + foreach ($middlewares as $middleware) { + $stack = $stack->add($middleware); + } + return $stack; + } + + /** + * Allow calling MiddlewareStack instance directly to invoke parse() + * + * @param MimePart $part + * @return MimePart + */ + public function __invoke(MimePart $part) + { + return $this->parse($part); + } +} diff --git a/plugins/php-mime-mail-parser/src/MimePart.php b/plugins/php-mime-mail-parser/src/MimePart.php new file mode 100644 index 00000000..d2211b7c --- /dev/null +++ b/plugins/php-mime-mail-parser/src/MimePart.php @@ -0,0 +1,119 @@ +getPart(); + * $part['headers']['from'] = 'modified@example.com'; + * $MimePart->setPart($part); + */ +class MimePart implements \ArrayAccess +{ + /** + * Internal mime part + * + * @var array + */ + protected $part = array(); + + /** + * Immutable Part Id + * + * @var string + */ + private $id; + + /** + * Create a mime part + * + * @param array $part + * @param string $id + */ + public function __construct($id, array $part) + { + $this->part = $part; + $this->id = $id; + } + + /** + * Retrieve the part Id + * + * @return string + */ + public function getId() + { + return $this->id; + } + + /** + * Retrieve the part data + * + * @return array + */ + public function getPart() + { + return $this->part; + } + + /** + * Set the mime part data + * + * @param array $part + * @return void + */ + public function setPart(array $part) + { + $this->part = $part; + } + + /** + * ArrayAccess + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) + { + if (is_null($offset)) { + $this->part[] = $value; + return; + } + $this->part[$offset] = $value; + } + + /** + * ArrayAccess + */ + #[\ReturnTypeWillChange] + public function offsetExists($offset) + { + return isset($this->part[$offset]); + } + + /** + * ArrayAccess + */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset) + { + unset($this->part[$offset]); + } + + /** + * ArrayAccess + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + return isset($this->part[$offset]) ? $this->part[$offset] : null; + } +} diff --git a/plugins/php-mime-mail-parser/src/Parser.php b/plugins/php-mime-mail-parser/src/Parser.php new file mode 100644 index 00000000..6502b57e --- /dev/null +++ b/plugins/php-mime-mail-parser/src/Parser.php @@ -0,0 +1,923 @@ +saveAttachments(). + */ + const ATTACHMENT_DUPLICATE_THROW = 'DuplicateThrow'; + const ATTACHMENT_DUPLICATE_SUFFIX = 'DuplicateSuffix'; + const ATTACHMENT_RANDOM_FILENAME = 'RandomFilename'; + + /** + * PHP MimeParser Resource ID + * + * @var resource $resource + */ + protected $resource; + + /** + * A file pointer to email + * + * @var resource $stream + */ + protected $stream; + + /** + * A text of an email + * + * @var string $data + */ + protected $data; + + /** + * Parts of an email + * + * @var array $parts + */ + protected $parts; + + /** + * @var CharsetManager object + */ + protected $charset; + + /** + * Valid stream modes for reading + * + * @var array + */ + protected static $readableModes = [ + 'r', 'r+', 'w+', 'a+', 'x+', 'c+', 'rb', 'r+b', 'w+b', 'a+b', + 'x+b', 'c+b', 'rt', 'r+t', 'w+t', 'a+t', 'x+t', 'c+t' + ]; + + /** + * Stack of middleware registered to process data + * + * @var MiddlewareStack + */ + protected $middlewareStack; + + /** + * Parser constructor. + * + * @param CharsetManager|null $charset + */ + public function __construct(CharsetManager $charset = null) + { + if ($charset == null) { + $charset = new Charset(); + } + + $this->charset = $charset; + $this->middlewareStack = new MiddlewareStack(); + } + + /** + * Free the held resources + * + * @return void + */ + public function __destruct() + { + // clear the email file resource + if (is_resource($this->stream)) { + fclose($this->stream); + } + // clear the MailParse resource + if (is_resource($this->resource)) { + mailparse_msg_free($this->resource); + } + } + + /** + * Set the file path we use to get the email text + * + * @param string $path File path to the MIME mail + * + * @return Parser MimeMailParser Instance + */ + public function setPath($path) + { + if (is_writable($path)) { + $file = fopen($path, 'a+'); + fseek($file, -1, SEEK_END); + if (fread($file, 1) != "\n") { + fwrite($file, PHP_EOL); + } + fclose($file); + } + + // should parse message incrementally from file + $this->resource = mailparse_msg_parse_file($path); + $this->stream = fopen($path, 'r'); + $this->parse(); + + return $this; + } + + /** + * Set the Stream resource we use to get the email text + * + * @param resource $stream + * + * @return Parser MimeMailParser Instance + * @throws Exception + */ + public function setStream($stream) + { + // streams have to be cached to file first + $meta = @stream_get_meta_data($stream); + if (!$meta || !$meta['mode'] || !in_array($meta['mode'], self::$readableModes, true) || $meta['eof']) { + throw new Exception( + 'setStream() expects parameter stream to be readable stream resource.' + ); + } + + /** @var resource $tmp_fp */ + $tmp_fp = tmpfile(); + if ($tmp_fp) { + while (!feof($stream)) { + fwrite($tmp_fp, fread($stream, 2028)); + } + + if (fread($tmp_fp, 1) != "\n") { + fwrite($tmp_fp, PHP_EOL); + } + + fseek($tmp_fp, 0); + $this->stream = &$tmp_fp; + } else { + throw new Exception( + 'Could not create temporary files for attachments. Your tmp directory may be unwritable by PHP.' + ); + } + fclose($stream); + + $this->resource = mailparse_msg_create(); + // parses the message incrementally (low memory usage but slower) + while (!feof($this->stream)) { + mailparse_msg_parse($this->resource, fread($this->stream, 2082)); + } + $this->parse(); + + return $this; + } + + /** + * Set the email text + * + * @param string $data + * + * @return Parser MimeMailParser Instance + */ + public function setText($data) + { + if (empty($data)) { + throw new Exception('You must not call MimeMailParser::setText with an empty string parameter'); + } + + if (substr($data, -1) != "\n") { + $data = $data.PHP_EOL; + } + + $this->resource = mailparse_msg_create(); + // does not parse incrementally, fast memory hog might explode + mailparse_msg_parse($this->resource, $data); + $this->data = $data; + $this->parse(); + + return $this; + } + + /** + * Parse the Message into parts + * + * @return void + */ + protected function parse() + { + $structure = mailparse_msg_get_structure($this->resource); + $this->parts = []; + foreach ($structure as $part_id) { + $part = mailparse_msg_get_part($this->resource, $part_id); + $part_data = mailparse_msg_get_part_data($part); + $mimePart = new MimePart($part_id, $part_data); + // let each middleware parse the part before saving + $this->parts[$part_id] = $this->middlewareStack->parse($mimePart)->getPart(); + } + } + + /** + * Retrieve a specific Email Header, without charset conversion. + * + * @param string $name Header name (case-insensitive) + * + * @return string|bool + * @throws Exception + */ + public function getRawHeader($name) + { + $name = strtolower($name); + if (isset($this->parts[1])) { + $headers = $this->getPart('headers', $this->parts[1]); + + return isset($headers[$name]) ? $headers[$name] : false; + } else { + throw new Exception( + 'setPath() or setText() or setStream() must be called before retrieving email headers.' + ); + } + } + + /** + * Retrieve a specific Email Header + * + * @param string $name Header name (case-insensitive) + * + * @return string|bool + */ + public function getHeader($name) + { + $rawHeader = $this->getRawHeader($name); + if ($rawHeader === false) { + return false; + } + + return $this->decodeHeader($rawHeader); + } + + /** + * Retrieve all mail headers + * + * @return array + * @throws Exception + */ + public function getHeaders() + { + if (isset($this->parts[1])) { + $headers = $this->getPart('headers', $this->parts[1]); + foreach ($headers as &$value) { + if (is_array($value)) { + foreach ($value as &$v) { + $v = $this->decodeSingleHeader($v); + } + } else { + $value = $this->decodeSingleHeader($value); + } + } + + return $headers; + } else { + throw new Exception( + 'setPath() or setText() or setStream() must be called before retrieving email headers.' + ); + } + } + + /** + * Retrieve the raw mail headers as a string + * + * @return string + * @throws Exception + */ + public function getHeadersRaw() + { + if (isset($this->parts[1])) { + return $this->getPartHeader($this->parts[1]); + } else { + throw new Exception( + 'setPath() or setText() or setStream() must be called before retrieving email headers.' + ); + } + } + + /** + * Retrieve the raw Header of a MIME part + * + * @return String + * @param $part Object + * @throws Exception + */ + protected function getPartHeader(&$part) + { + $header = ''; + if ($this->stream) { + $header = $this->getPartHeaderFromFile($part); + } elseif ($this->data) { + $header = $this->getPartHeaderFromText($part); + } + return $header; + } + + /** + * Retrieve the Header from a MIME part from file + * + * @return String Mime Header Part + * @param $part Array + */ + protected function getPartHeaderFromFile(&$part) + { + $start = $part['starting-pos']; + $end = $part['starting-pos-body']; + fseek($this->stream, $start, SEEK_SET); + $header = fread($this->stream, $end - $start); + return $header; + } + + /** + * Retrieve the Header from a MIME part from text + * + * @return String Mime Header Part + * @param $part Array + */ + protected function getPartHeaderFromText(&$part) + { + $start = $part['starting-pos']; + $end = $part['starting-pos-body']; + $header = substr($this->data, $start, $end - $start); + return $header; + } + + /** + * Checks whether a given part ID is a child of another part + * eg. an RFC822 attachment may have one or more text parts + * + * @param string $partId + * @param string $parentPartId + * @return bool + */ + protected function partIdIsChildOfPart($partId, $parentPartId) + { + $parentPartId = $parentPartId.'.'; + return substr($partId, 0, strlen($parentPartId)) == $parentPartId; + } + + /** + * Whether the given part ID is a child of any attachment part in the message. + * + * @param string $checkPartId + * @return bool + */ + protected function partIdIsChildOfAnAttachment($checkPartId) + { + foreach ($this->parts as $partId => $part) { + if ($this->getPart('content-disposition', $part) == 'attachment') { + if ($this->partIdIsChildOfPart($checkPartId, $partId)) { + return true; + } + } + } + return false; + } + + /** + * Returns the email message body in the specified format + * + * @param string $type text, html or htmlEmbedded + * + * @return string Body + * @throws Exception + */ + public function getMessageBody($type = 'text') + { + $mime_types = [ + 'text' => 'text/plain', + 'html' => 'text/html', + 'htmlEmbedded' => 'text/html', + ]; + + if (in_array($type, array_keys($mime_types))) { + $part_type = $type === 'htmlEmbedded' ? 'html' : $type; + $inline_parts = $this->getInlineParts($part_type); + $body = empty($inline_parts) ? '' : $inline_parts[0]; + } else { + throw new Exception( + 'Invalid type specified for getMessageBody(). Expected: text, html or htmlEmbeded.' + ); + } + + if ($type == 'htmlEmbedded') { + $attachments = $this->getAttachments(); + foreach ($attachments as $attachment) { + if ($attachment->getContentID() != '') { + $body = str_replace( + '"cid:'.$attachment->getContentID().'"', + '"'.$this->getEmbeddedData($attachment->getContentID()).'"', + $body + ); + } + } + } + + return $body; + } + + /** + * Returns the embedded data structure + * + * @param string $contentId Content-Id + * + * @return string + */ + protected function getEmbeddedData($contentId) + { + foreach ($this->parts as $part) { + if ($this->getPart('content-id', $part) == $contentId) { + $embeddedData = 'data:'; + $embeddedData .= $this->getPart('content-type', $part); + $embeddedData .= ';'.$this->getPart('transfer-encoding', $part); + $embeddedData .= ','.$this->getPartBody($part); + return $embeddedData; + } + } + return ''; + } + + /** + * Return an array with the following keys display, address, is_group + * + * @param string $name Header name (case-insensitive) + * + * @return array + */ + public function getAddresses($name) + { + $value = $this->getRawHeader($name); + $value = (is_array($value)) ? $value[0] : $value; + $addresses = mailparse_rfc822_parse_addresses($value); + foreach ($addresses as $i => $item) { + $addresses[$i]['display'] = $this->decodeHeader($item['display']); + } + return $addresses; + } + + /** + * Returns the attachments contents in order of appearance + * + * @return Attachment[] + */ + public function getInlineParts($type = 'text') + { + $inline_parts = []; + $mime_types = [ + 'text' => 'text/plain', + 'html' => 'text/html', + ]; + + if (!in_array($type, array_keys($mime_types))) { + throw new Exception('Invalid type specified for getInlineParts(). "type" can either be text or html.'); + } + + foreach ($this->parts as $partId => $part) { + if ($this->getPart('content-type', $part) == $mime_types[$type] + && $this->getPart('content-disposition', $part) != 'attachment' + && !$this->partIdIsChildOfAnAttachment($partId) + ) { + $headers = $this->getPart('headers', $part); + $encodingType = array_key_exists('content-transfer-encoding', $headers) ? + $headers['content-transfer-encoding'] : ''; + $undecoded_body = $this->decodeContentTransfer($this->getPartBody($part), $encodingType); + $inline_parts[] = $this->charset->decodeCharset($undecoded_body, $this->getPartCharset($part)); + } + } + + return $inline_parts; + } + + /** + * Returns the attachments contents in order of appearance + * + * @return Attachment[] + */ + public function getAttachments($include_inline = true) + { + $attachments = []; + $dispositions = $include_inline ? ['attachment', 'inline'] : ['attachment']; + $non_attachment_types = ['text/plain', 'text/html']; + $nonameIter = 0; + + foreach ($this->parts as $part) { + $disposition = $this->getPart('content-disposition', $part); + $filename = 'noname'; + + if (isset($part['disposition-filename'])) { + $filename = $this->decodeHeader($part['disposition-filename']); + } elseif (isset($part['content-name'])) { + // if we have no disposition but we have a content-name, it's a valid attachment. + // we simulate the presence of an attachment disposition with a disposition filename + $filename = $this->decodeHeader($part['content-name']); + $disposition = 'attachment'; + } elseif (in_array($part['content-type'], $non_attachment_types, true) + && $disposition !== 'attachment') { + // it is a message body, no attachment + continue; + } elseif (substr($part['content-type'], 0, 10) !== 'multipart/' + && $part['content-type'] !== 'text/plain; (error)') { + // if we cannot get it by getMessageBody(), we assume it is an attachment + $disposition = 'attachment'; + } + if (in_array($disposition, ['attachment', 'inline']) === false && !empty($disposition)) { + $disposition = 'attachment'; + } + + if (in_array($disposition, $dispositions) === true) { + if ($filename == 'noname') { + $nonameIter++; + $filename = 'noname'.$nonameIter; + } else { + // Escape all potentially unsafe characters from the filename + $filename = preg_replace('((^\.)|\/|[\n|\r|\n\r]|(\.$))', '_', $filename); + } + + $headersAttachments = $this->getPart('headers', $part); + $contentidAttachments = $this->getPart('content-id', $part); + + $attachmentStream = $this->getAttachmentStream($part); + $mimePartStr = $this->getPartComplete($part); + + $attachments[] = new Attachment( + $filename, + $this->getPart('content-type', $part), + $attachmentStream, + $disposition, + $contentidAttachments, + $headersAttachments, + $mimePartStr + ); + } + } + + return $attachments; + } + + /** + * Save attachments in a folder + * + * @param string $attach_dir directory + * @param bool $include_inline + * @param string $filenameStrategy How to generate attachment filenames + * + * @return array Saved attachments paths + * @throws Exception + */ + public function saveAttachments( + $attach_dir, + $include_inline = true, + $filenameStrategy = self::ATTACHMENT_DUPLICATE_SUFFIX + ) { + $attachments = $this->getAttachments($include_inline); + + $attachments_paths = []; + foreach ($attachments as $attachment) { + $attachments_paths[] = $attachment->save($attach_dir, $filenameStrategy); + } + + return $attachments_paths; + } + + /** + * Read the attachment Body and save temporary file resource + * + * @param array $part + * + * @return resource Mime Body Part + * @throws Exception + */ + protected function getAttachmentStream(&$part) + { + /** @var resource $temp_fp */ + $temp_fp = tmpfile(); + + $headers = $this->getPart('headers', $part); + $encodingType = array_key_exists('content-transfer-encoding', $headers) ? + $headers['content-transfer-encoding'] : ''; + + if ($temp_fp) { + if ($this->stream) { + $start = $part['starting-pos-body']; + $end = $part['ending-pos-body']; + fseek($this->stream, $start, SEEK_SET); + $len = $end - $start; + $written = 0; + while ($written < $len) { + $write = $len; + $data = fread($this->stream, $write); + fwrite($temp_fp, $this->decodeContentTransfer($data, $encodingType)); + $written += $write; + } + } elseif ($this->data) { + $attachment = $this->decodeContentTransfer($this->getPartBodyFromText($part), $encodingType); + fwrite($temp_fp, $attachment, strlen($attachment)); + } + fseek($temp_fp, 0, SEEK_SET); + } else { + throw new Exception( + 'Could not create temporary files for attachments. Your tmp directory may be unwritable by PHP.' + ); + } + + return $temp_fp; + } + + /** + * Decode the string from Content-Transfer-Encoding + * + * @param string $encodedString The string in its original encoded state + * @param string $encodingType The encoding type from the Content-Transfer-Encoding header of the part. + * + * @return string The decoded string + */ + protected function decodeContentTransfer($encodedString, $encodingType) + { + if (is_array($encodingType)) { + $encodingType = $encodingType[0]; + } + + $encodingType = strtolower($encodingType); + if ($encodingType == 'base64') { + return base64_decode($encodedString); + } elseif ($encodingType == 'quoted-printable') { + return quoted_printable_decode($encodedString); + } else { + return $encodedString; + } + } + + /** + * $input can be a string or array + * + * @param string|array $input + * + * @return string + */ + protected function decodeHeader($input) + { + //Sometimes we have 2 label From so we take only the first + if (is_array($input)) { + return $this->decodeSingleHeader($input[0]); + } + + return $this->decodeSingleHeader($input); + } + + /** + * Decodes a single header (= string) + * + * @param string $input + * + * @return string + */ + protected function decodeSingleHeader($input) + { + // For each encoded-word... + while (preg_match('/(=\?([^?]+)\?(q|b)\?([^?]*)\?=)((\s+)=\?)?/i', $input, $matches)) { + $encoded = $matches[1]; + $charset = $matches[2]; + $encoding = $matches[3]; + $text = $matches[4]; + $space = isset($matches[6]) ? $matches[6] : ''; + + switch (strtolower($encoding)) { + case 'b': + $text = $this->decodeContentTransfer($text, 'base64'); + break; + + case 'q': + $text = str_replace('_', ' ', $text); + preg_match_all('/=([a-f0-9]{2})/i', $text, $matches); + foreach ($matches[1] as $value) { + $text = str_replace('='.$value, chr(hexdec($value)), $text); + } + break; + } + + $text = $this->charset->decodeCharset($text, $this->charset->getCharsetAlias($charset)); + $input = str_replace($encoded.$space, $text, $input); + } + + return $input; + } + + /** + * Return the charset of the MIME part + * + * @param array $part + * + * @return string + */ + protected function getPartCharset($part) + { + if (isset($part['charset'])) { + return $this->charset->getCharsetAlias($part['charset']); + } else { + return 'us-ascii'; + } + } + + /** + * Retrieve a specified MIME part + * + * @param string $type + * @param array $parts + * + * @return string|array + */ + protected function getPart($type, $parts) + { + return (isset($parts[$type])) ? $parts[$type] : false; + } + + /** + * Retrieve the Body of a MIME part + * + * @param array $part + * + * @return string + */ + protected function getPartBody(&$part) + { + $body = ''; + if ($this->stream) { + $body = $this->getPartBodyFromFile($part); + } elseif ($this->data) { + $body = $this->getPartBodyFromText($part); + } + + return $body; + } + + /** + * Retrieve the Body from a MIME part from file + * + * @param array $part + * + * @return string Mime Body Part + */ + protected function getPartBodyFromFile(&$part) + { + $start = $part['starting-pos-body']; + $end = $part['ending-pos-body']; + $body = ''; + if ($end - $start > 0) { + fseek($this->stream, $start, SEEK_SET); + $body = fread($this->stream, $end - $start); + } + + return $body; + } + + /** + * Retrieve the Body from a MIME part from text + * + * @param array $part + * + * @return string Mime Body Part + */ + protected function getPartBodyFromText(&$part) + { + $start = $part['starting-pos-body']; + $end = $part['ending-pos-body']; + + return substr($this->data, $start, $end - $start); + } + + /** + * Retrieve the content of a MIME part + * + * @param array $part + * + * @return string + */ + protected function getPartComplete(&$part) + { + $body = ''; + if ($this->stream) { + $body = $this->getPartFromFile($part); + } elseif ($this->data) { + $body = $this->getPartFromText($part); + } + + return $body; + } + + /** + * Retrieve the content from a MIME part from file + * + * @param array $part + * + * @return string Mime Content + */ + protected function getPartFromFile(&$part) + { + $start = $part['starting-pos']; + $end = $part['ending-pos']; + $body = ''; + if ($end - $start > 0) { + fseek($this->stream, $start, SEEK_SET); + $body = fread($this->stream, $end - $start); + } + + return $body; + } + + /** + * Retrieve the content from a MIME part from text + * + * @param array $part + * + * @return string Mime Content + */ + protected function getPartFromText(&$part) + { + $start = $part['starting-pos']; + $end = $part['ending-pos']; + + return substr($this->data, $start, $end - $start); + } + + /** + * Retrieve the resource + * + * @return resource resource + */ + public function getResource() + { + return $this->resource; + } + + /** + * Retrieve the file pointer to email + * + * @return resource stream + */ + public function getStream() + { + return $this->stream; + } + + /** + * Retrieve the text of an email + * + * @return string data + */ + public function getData() + { + return $this->data; + } + + /** + * Retrieve the parts of an email + * + * @return array parts + */ + public function getParts() + { + return $this->parts; + } + + /** + * Retrieve the charset manager object + * + * @return CharsetManager charset + */ + public function getCharset() + { + return $this->charset; + } + + /** + * Add a middleware to the parser MiddlewareStack + * Each middleware is invoked when: + * a MimePart is retrieved by mailparse_msg_get_part_data() during $this->parse() + * The middleware will receive MimePart $part and the next MiddlewareStack $next + * + * Eg: + * + * $Parser->addMiddleware(function(MimePart $part, MiddlewareStack $next) { + * // do something with the $part + * return $next($part); + * }); + * + * @param callable $middleware Plain Function or Middleware Instance to execute + * @return void + */ + public function addMiddleware(callable $middleware) + { + if (!$middleware instanceof Middleware) { + $middleware = new Middleware($middleware); + } + $this->middlewareStack = $this->middlewareStack->add($middleware); + } +} diff --git a/plugins/php-mime-mail-parser/src/php-mime-mail-parser-8.0.0.zip b/plugins/php-mime-mail-parser/src/php-mime-mail-parser-8.0.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..87c8087b1c23eb6442a14fa2eb2c4ee1bb5c90e5 GIT binary patch literal 22820 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 fKvE1MDJp0#X=ZL_Dk>o=DRJsetPath($path); + +// 2. Specify the raw mime mail text (string) +$parser->setText(file_get_contents($path)); + +// 3. Specify a php file resource (stream) +$parser->setStream(fopen($path, "r")); + +// 4. Specify a stream to work with mail server (stream) +$parser->setStream(fopen("php://stdin", "r")); +``` + +### Get the metadata of the message + +Get the sender and the receiver: + +```php +$rawHeaderTo = $parser->getHeader('to'); +// return "test" , "test2" + +$arrayHeaderTo = $parser->getAddresses('to'); +// return [["display"=>"test", "address"=>"test@example.com", false]] + +$rawHeaderFrom = $parser->getHeader('from'); +// return "test" + +$arrayHeaderFrom = $parser->getAddresses('from'); +// return [["display"=>"test", "address"=>"test@example.com", "is_group"=>false]] +``` + +Get the subject: + +```php +$subject = $parser->getHeader('subject'); +``` + +Get other headers: + +```php +$stringHeaders = $parser->getHeadersRaw(); +// return all headers as a string, no charset conversion + +$arrayHeaders = $parser->getHeaders(); +// return all headers as an array, with charset conversion +``` + +### Get the body of the message + +```php +$text = $parser->getMessageBody('text'); +// return the text version + +$html = $parser->getMessageBody('html'); +// return the html version + +$htmlEmbedded = $parser->getMessageBody('htmlEmbedded'); +// return the html version with the embedded contents like images + +``` + +### Get attachments + +Save all attachments in a directory + +```php +$parser->saveAttachments('/path/to/save/attachments/'); +// return all attachments saved in the directory (include inline attachments) + +$parser->saveAttachments('/path/to/save/attachments/', false); +// return all attachments saved in the directory (exclude inline attachments) + +// Save all attachments with the strategy ATTACHMENT_DUPLICATE_SUFFIX (default) +$parser->saveAttachments('/path/to/save/attachments/', false, Parser::ATTACHMENT_DUPLICATE_SUFFIX); +// return all attachments saved in the directory: logo.jpg, logo_1.jpg, ..., logo_100.jpg, YY34UFHBJ.jpg + +// Save all attachments with the strategy ATTACHMENT_RANDOM_FILENAME +$parser->saveAttachments('/path/to/save/attachments/', false, Parser::ATTACHMENT_RANDOM_FILENAME); +// return all attachments saved in the directory: YY34UFHBJ.jpg and F98DBZ9FZF.jpg + +// Save all attachments with the strategy ATTACHMENT_DUPLICATE_THROW +$parser->saveAttachments('/path/to/save/attachments/', false, Parser::ATTACHMENT_DUPLICATE_THROW); +// return an exception when there is attachments duplicate. + +``` + +Get all attachments + +```php +$attachments = $parser->getAttachments(); +// return an array of all attachments (include inline attachments) + +$attachments = $parser->getAttachments(false); +// return an array of all attachments (exclude inline attachments) +``` + + +Loop through all the Attachments +```php +foreach ($attachments as $attachment) { + echo 'Filename : '.$attachment->getFilename().'
'; + // return logo.jpg + + echo 'Filesize : '.filesize($attach_dir.$attachment->getFilename()).'
'; + // return 1000 + + echo 'Filetype : '.$attachment->getContentType().'
'; + // return image/jpeg + + echo 'MIME part string : '.$attachment->getMimePartStr().'
'; + // return the whole MIME part of the attachment + + $attachment->save('/path/to/save/myattachment/', Parser::ATTACHMENT_DUPLICATE_SUFFIX); + // return the path and the filename saved (same strategy available than saveAttachments) +} +``` + +## Postfix configuration to manage email from a mail server + +Next you need to forward emails to this script above. For that I'm using [Postfix](http://www.postfix.org/) like a mail server, you need to configure /etc/postfix/master.cf + +Add this line at the end of the file (specify myhook to send all emails to the script test.php) +``` +myhook unix - n n - - pipe + flags=F user=www-data argv=php -c /etc/php5/apache2/php.ini -f /var/www/test.php ${sender} ${size} ${recipient} +``` + +Edit this line (register myhook) +``` +smtp inet n - - - - smtpd + -o content_filter=myhook:dummy +``` + +The php script must use the fourth method to work with this configuration. + +And finally the easiest way is to use my SaaS https://mailcare.io + + +## Can I contribute? + +Feel free to contribute! + + git clone https://github.com/php-mime-mail-parser/php-mime-mail-parser + cd php-mime-mail-parser + composer install + ./vendor/bin/phpunit + +If you report an issue, please provide the raw email that triggered it. This helps us reproduce the issue and fix it more quickly. + +## License + +The php-mime-mail-parser/php-mime-mail-parser is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT) diff --git a/plugins/php-mime-mail-parser/src/php-mime-mail-parser-8.0.0/compile_mailparse.sh b/plugins/php-mime-mail-parser/src/php-mime-mail-parser-8.0.0/compile_mailparse.sh new file mode 100644 index 00000000..8505077f --- /dev/null +++ b/plugins/php-mime-mail-parser/src/php-mime-mail-parser-8.0.0/compile_mailparse.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +git clone https://github.com/php/pecl-mail-mailparse.git +cd pecl-mail-mailparse +phpize +./configure +sed -i 's/#if\s!HAVE_MBSTRING/#ifndef MBFL_MBFILTER_H/' ./mailparse.c +make +sudo mv modules/mailparse.so /home/travis/.phpenv/versions/7.3.2/lib/php/extensions/no-debug-zts-20180731/ +echo 'extension=mailparse.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini \ No newline at end of file diff --git a/plugins/php-mime-mail-parser/src/php-mime-mail-parser-8.0.0/composer.json b/plugins/php-mime-mail-parser/src/php-mime-mail-parser-8.0.0/composer.json new file mode 100644 index 00000000..6cc88c42 --- /dev/null +++ b/plugins/php-mime-mail-parser/src/php-mime-mail-parser-8.0.0/composer.json @@ -0,0 +1,60 @@ +{ + "name": "php-mime-mail-parser/php-mime-mail-parser", + "type": "library", + "description": "A fully tested email parser for PHP 8.0+ (mailparse extension wrapper).", + "keywords": ["mime", "mail", "mailparse", "MimeMailParser", "parser", "php"], + "homepage": "https://github.com/php-mime-mail-parser/php-mime-mail-parser", + "license": "MIT", + "authors": [ + { + "name":"eXorus", + "email":"exorus.spam@gmail.com", + "homepage":"https://github.com/eXorus/", + "role":"Developer" + }, + { + "name":"M.Valinskis", + "email":"M.Valins@gmail.com", + "homepage":"https://code.google.com/p/php-mime-mail-parser", + "role":"Developer" + }, + { + "name":"eugene.emmett.wood", + "email":"gene_w@cementhorizon.com", + "homepage":"https://code.google.com/p/php-mime-mail-parser", + "role":"Developer" + }, + { + "name":"alknetso", + "email":"alkne@gmail.com", + "homepage":"https://code.google.com/p/php-mime-mail-parser", + "role":"Developer" + }, + { + "name":"bucabay", + "email":"gabe@fijiwebdesign.com", + "homepage":"http://www.fijiwebdesign.com", + "role":"Developer" + } + ], + "repository":{ + "type":"git", + "url":"https://github.com/php-mime-mail-parser/php-mime-mail-parser.git" + }, + "require": { + "php": "^8.0", + "ext-mailparse": "*" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "php-coveralls/php-coveralls": "^2.2", + "squizlabs/php_codesniffer": "^3.5" + }, + "replace": { + "exorus/php-mime-mail-parser": "*", + "messaged/php-mime-mail-parser": "*" + }, + "autoload": { + "psr-4": { "PhpMimeMailParser\\": "src/" } + } +} diff --git a/plugins/php-mime-mail-parser/src/php-mime-mail-parser-8.0.0/mailparse-stubs.php b/plugins/php-mime-mail-parser/src/php-mime-mail-parser-8.0.0/mailparse-stubs.php new file mode 100644 index 00000000..e2b5e0e2 --- /dev/null +++ b/plugins/php-mime-mail-parser/src/php-mime-mail-parser-8.0.0/mailparse-stubs.php @@ -0,0 +1,303 @@ + + + + tests + +