diff --git a/externals/mimemailparser/Attachment.php b/externals/mimemailparser/Attachment.php new file mode 100644 index 0000000000..1a731635f8 --- /dev/null +++ b/externals/mimemailparser/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/externals/mimemailparser/Charset.php b/externals/mimemailparser/Charset.php new file mode 100644 index 0000000000..04315c1e7c --- /dev/null +++ b/externals/mimemailparser/Charset.php @@ -0,0 +1,338 @@ + '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' => 'x-mac-ce', + 'x-mac-turkish' => 'x-mac-turkish', + 'x-mac-greek' => 'x-mac-greek', + 'x-mac-icelandic' => 'x-mac-icelandic', + 'x-mac-croatian' => 'x-mac-croatian', + 'x-mac-romanian' => 'x-mac-romanian', + 'x-mac-cyrillic' => 'x-mac-cyrillic', + 'x-mac-ukrainian' => 'x-mac-cyrillic', + 'x-mac-hebrew' => 'x-mac-hebrew', + 'x-mac-arabic' => 'x-mac-arabic', + 'x-mac-farsi' => 'x-mac-farsi', + 'x-mac-devanagari' => 'x-mac-devanagari', + 'x-mac-gujarati' => 'x-mac-gujarati', + 'x-mac-gurmukhi' => 'x-mac-gurmukhi', + '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) + { + if (strtolower($charset) == 'utf-8' || strtolower($charset) == 'us-ascii') { + return $encodedString; + } else { + return iconv($this->getCharsetAlias($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]; + } else { + return null; + } + } +} diff --git a/externals/mimemailparser/Contracts/CharsetManager.php b/externals/mimemailparser/Contracts/CharsetManager.php new file mode 100644 index 0000000000..660ec00cb0 --- /dev/null +++ b/externals/mimemailparser/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/externals/mimemailparser/MiddlewareStack.php b/externals/mimemailparser/MiddlewareStack.php new file mode 100644 index 0000000000..3ef6da9349 --- /dev/null +++ b/externals/mimemailparser/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/externals/mimemailparser/MimeMailParser.class.php b/externals/mimemailparser/MimeMailParser.class.php deleted file mode 100644 index dfd7a2fa11..0000000000 --- a/externals/mimemailparser/MimeMailParser.class.php +++ /dev/null @@ -1,494 +0,0 @@ -attachment_streams = array(); - } - - /** - * Free the held resouces - * @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); - } - // remove attachment resources - foreach($this->attachment_streams as $stream) { - fclose($stream); - } - } - - /** - * Set the file path we use to get the email text - * @return Object MimeMailParser Instance - * @param $mail_path Object - */ - public function setPath($path) { - // 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 - * @return Object MimeMailParser Instance - * @param $stream Resource - */ - public function setStream($stream) { - - // streams have to be cached to file first - if (get_resource_type($stream) == 'stream') { - $tmp_fp = tmpfile(); - if ($tmp_fp) { - while(!feof($stream)) { - fwrite($tmp_fp, fread($stream, 2028)); - } - 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.'); - return false; - } - fclose($stream); - } else { - $this->stream = $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 - * @return Object MimeMailParser Instance - * @param $data String - */ - public function setText($data) { - // NOTE: This has been modified for Phabricator. If the input data does not - // end in a newline, Mailparse fails to include the last line in the mail - // body. This happens somewhere deep, deep inside the mailparse extension, - // so adding a newline here seems like the most straightforward fix. - if (!preg_match('/\n\z/', $data)) { - $data = $data."\n"; - } - - $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 - * @private - */ - private function parse() { - $structure = mailparse_msg_get_structure($this->resource); - $this->parts = array(); - foreach($structure as $part_id) { - $part = mailparse_msg_get_part($this->resource, $part_id); - $this->parts[$part_id] = mailparse_msg_get_part_data($part); - } - } - - /** - * Retrieve the Email Headers - * @return Array - */ - public function getHeaders() { - if (isset($this->parts[1])) { - return $this->getPartHeaders($this->parts[1]); - } else { - throw new Exception('MimeMailParser::setPath() or MimeMailParser::setText() must be called before retrieving email headers.'); - } - return false; - } - /** - * Retrieve the raw Email Headers - * @return string - */ - public function getHeadersRaw() { - if (isset($this->parts[1])) { - return $this->getPartHeaderRaw($this->parts[1]); - } else { - throw new Exception('MimeMailParser::setPath() or MimeMailParser::setText() must be called before retrieving email headers.'); - } - return false; - } - - /** - * Retrieve a specific Email Header - * @return String - * @param $name String Header name - */ - public function getHeader($name) { - if (isset($this->parts[1])) { - $headers = $this->getPartHeaders($this->parts[1]); - if (isset($headers[$name])) { - return $headers[$name]; - } - } else { - throw new Exception('MimeMailParser::setPath() or MimeMailParser::setText() must be called before retrieving email headers.'); - } - return false; - } - - /** - * Returns the email message body in the specified format - * @return Mixed String Body or False if not found - * @param $type Object[optional] - */ - public function getMessageBody($type = 'text') { - - // NOTE: This function has been modified for Phabricator. The default - // implementation returns the last matching part, which throws away text - // for many emails. Instead, we concatenate all matching parts. See - // issue 22 for discussion: - // http://code.google.com/p/php-mime-mail-parser/issues/detail?id=22 - - $body = false; - $mime_types = array( - 'text'=> 'text/plain', - 'html'=> 'text/html' - ); - if (in_array($type, array_keys($mime_types))) { - foreach($this->parts as $part) { - $disposition = $this->getPartContentDisposition($part); - if ($disposition == 'attachment') { - // text/plain parts with "Content-Disposition: attachment" are - // attachments, not part of the text body. - continue; - } - if ($this->getPartContentType($part) == $mime_types[$type]) { - $headers = $this->getPartHeaders($part); - // Concatenate all the matching parts into the body text. For example, - // if a user sends a message with some text, then an image, and then - // some more text, the text body of the email gets split over several - // attachments. - $body .= $this->decode( - $this->getPartBody($part), - array_key_exists('content-transfer-encoding', $headers) - ? $headers['content-transfer-encoding'] - : ''); - } - } - } else { - throw new Exception('Invalid type specified for MimeMailParser::getMessageBody. "type" can either be text or html.'); - } - return $body; - } - - /** - * get the headers for the message body part. - * @return Array - * @param $type Object[optional] - */ - public function getMessageBodyHeaders($type = 'text') { - $headers = false; - $mime_types = array( - 'text'=> 'text/plain', - 'html'=> 'text/html' - ); - if (in_array($type, array_keys($mime_types))) { - foreach($this->parts as $part) { - if ($this->getPartContentType($part) == $mime_types[$type]) { - $headers = $this->getPartHeaders($part); - } - } - } else { - throw new Exception('Invalid type specified for MimeMailParser::getMessageBody. "type" can either be text or html.'); - } - return $headers; - } - - /** - * Returns the attachments contents in order of appearance - * @return Array - * @param $type Object[optional] - */ - public function getAttachments() { - // NOTE: This has been modified for Phabricator. Some mail clients do not - // send attachments with "Content-Disposition" headers. - $attachments = array(); - $dispositions = array("attachment","inline"); - $non_attachment_types = array("text/plain", "text/html"); - $nonameIter = 0; - foreach ($this->parts as $part) { - $disposition = $this->getPartContentDisposition($part); - $filename = 'noname'; - if (isset($part['disposition-filename'])) { - $filename = $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 = $part['content-name']; - $disposition = 'attachment'; - } elseif (!in_array($part['content-type'], $non_attachment_types, true) - && substr($part['content-type'], 0, 10) !== 'multipart/' - ) { - // if we cannot get it with getMessageBody, we assume it is an attachment - $disposition = 'attachment'; - } - - if (in_array($disposition, $dispositions) && isset($filename) === true) { - if ($filename == 'noname') { - $nonameIter++; - $filename = 'noname'.$nonameIter; - } - $attachments[] = new MimeMailParser_attachment( - $filename, - $this->getPartContentType($part), - $this->getAttachmentStream($part), - $disposition, - $this->getPartHeaders($part) - ); - } - } - return $attachments; - } - - /** - * Return the Headers for a MIME part - * @return Array - * @param $part Array - */ - private function getPartHeaders($part) { - if (isset($part['headers']) && $part['headers']) { - return $part['headers']; - } - throw new Exception('MimeMailParser::getHeaders() could not parse any email headers.'); - } - - /** - * Return a Specific Header for a MIME part - * @return Array - * @param $part Array - * @param $header String Header Name - */ - private function getPartHeader($part, $header) { - if (isset($part['headers'][$header])) { - return $part['headers'][$header]; - } - return false; - } - - /** - * Return the ContentType of the MIME part - * @return String - * @param $part Array - */ - private function getPartContentType($part) { - if (isset($part['content-type'])) { - return $part['content-type']; - } - return false; - } - - /** - * Return the Content Disposition - * @return String - * @param $part Array - */ - private function getPartContentDisposition($part) { - if (isset($part['content-disposition'])) { - return $part['content-disposition']; - } - return false; - } - - /** - * Retrieve the raw Header of a MIME part - * @return String - * @param $part Object - */ - private function getPartHeaderRaw(&$part) { - $header = ''; - if ($this->stream) { - $header = $this->getPartHeaderFromFile($part); - } else if ($this->data) { - $header = $this->getPartHeaderFromText($part); - } else { - throw new Exception('MimeMailParser::setPath() or MimeMailParser::setText() must be called before retrieving email parts.'); - } - return $header; - } - /** - * Retrieve the Body of a MIME part - * @return String - * @param $part Object - */ - private function getPartBody(&$part) { - $body = ''; - if ($this->stream) { - $body = $this->getPartBodyFromFile($part); - } else if ($this->data) { - $body = $this->getPartBodyFromText($part); - } else { - throw new Exception('MimeMailParser::setPath() or MimeMailParser::setText() must be called before retrieving email parts.'); - } - return $body; - } - - /** - * Retrieve the Header from a MIME part from file - * @return String Mime Header Part - * @param $part Array - */ - private 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 Body from a MIME part from file - * @return String Mime Body Part - * @param $part Array - */ - private function getPartBodyFromFile(&$part) { - $start = $part['starting-pos-body']; - $end = $part['ending-pos-body']; - fseek($this->stream, $start, SEEK_SET); - $body = fread($this->stream, $end-$start); - return $body; - } - - /** - * Retrieve the Header from a MIME part from text - * @return String Mime Header Part - * @param $part Array - */ - private function getPartHeaderFromText(&$part) { - $start = $part['starting-pos']; - $end = $part['starting-pos-body']; - $header = substr($this->data, $start, $end-$start); - return $header; - } - /** - * Retrieve the Body from a MIME part from text - * @return String Mime Body Part - * @param $part Array - */ - private function getPartBodyFromText(&$part) { - $start = $part['starting-pos-body']; - $end = $part['ending-pos-body']; - $body = substr($this->data, $start, $end-$start); - return $body; - } - - /** - * Read the attachment Body and save temporary file resource - * @return String Mime Body Part - * @param $part Array - */ - private function getAttachmentStream(&$part) { - $temp_fp = tmpfile(); - - array_key_exists('content-transfer-encoding', $part['headers']) ? $encoding = $part['headers']['content-transfer-encoding'] : $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; - $write = 2028; - $body = ''; - while($written < $len) { - if (($written+$write < $len )) { - $write = $len - $written; - } - $part = fread($this->stream, $write); - fwrite($temp_fp, $this->decode($part, $encoding)); - $written += $write; - } - } else if ($this->data) { - $attachment = $this->decode($this->getPartBodyFromText($part), $encoding); - 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 false; - } - return $temp_fp; - } - - - /** - * Decode the string depending on encoding type. - * @return String the decoded string. - * @param $encodedString The string in its original encoded state. - * @param $encodingType The encoding type from the Content-Transfer-Encoding header of the part. - */ - private function decode($encodedString, $encodingType) { - if (strtolower($encodingType) == 'base64') { - return base64_decode($encodedString); - } else if (strtolower($encodingType) == 'quoted-printable') { - return quoted_printable_decode($encodedString); - } else { - return $encodedString; - } - } - -} - - -?> diff --git a/externals/mimemailparser/MimePart.php b/externals/mimemailparser/MimePart.php new file mode 100644 index 0000000000..d2211b7c04 --- /dev/null +++ b/externals/mimemailparser/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/externals/mimemailparser/Parser.php b/externals/mimemailparser/Parser.php new file mode 100644 index 0000000000..7f8a1de94d --- /dev/null +++ b/externals/mimemailparser/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)) { + 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|false + */ + 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)' && $disposition != 'inline') { + // 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/externals/mimemailparser/README b/externals/mimemailparser/README deleted file mode 100644 index 2975add6d9..0000000000 --- a/externals/mimemailparser/README +++ /dev/null @@ -1,3 +0,0 @@ -From: - - http://code.google.com/p/php-mime-mail-parser/ diff --git a/externals/mimemailparser/README.md b/externals/mimemailparser/README.md new file mode 100644 index 0000000000..23d32f274f --- /dev/null +++ b/externals/mimemailparser/README.md @@ -0,0 +1,267 @@ +# php-mime-mail-parser + +A fully tested email parser for PHP 8.0+ (mailparse extension wrapper). + +It's the most effective PHP email parser around in terms of performance, foreign character encoding, attachment handling, and ease of use. +Internet Message Format RFC [822](https://tools.ietf.org/html/rfc822), [2822](https://tools.ietf.org/html/rfc2822), [5322](https://tools.ietf.org/html/rfc5322). + +[![Latest Version](https://img.shields.io/packagist/v/php-mime-mail-parser/php-mime-mail-parser.svg?style=flat-square)](https://github.com/php-mime-mail-parser/php-mime-mail-parser/releases) +[![Total Downloads](https://img.shields.io/packagist/dt/php-mime-mail-parser/php-mime-mail-parser.svg?style=flat-square)](https://packagist.org/packages/php-mime-mail-parser/php-mime-mail-parser) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) + +## Why? + +This extension can be used to... + * Parse and read email from Postfix + * Read messages (Filename extension: `.eml`) + * Create webmail + * Store email information such a subject, HTML body, attachments, etc. into a database + +## Is it reliable? + +Yes. All known issues have been reproduced, fixed and tested. + +We use GitHub Actions, Codecov, Codacy to help ensure code quality. You can see real-time statistics below: + +[![CI](https://github.com/php-mime-mail-parser/php-mime-mail-parser/actions/workflows/main.yml/badge.svg?style=flat-square)](https://github.com/php-mime-mail-parser/php-mime-mail-parser/actions/workflows/main.yml) +[![Coverage](https://codecov.io/gh/php-mime-mail-parser/php-mime-mail-parser/branch/main/graph/badge.svg?token=wTSIbXJDL0)](https://codecov.io/gh/php-mime-mail-parser/php-mime-mail-parser) +[![Code Quality](https://app.codacy.com/project/badge/Grade/8cbfe0fcd84c4b2b9282b9a0b4467607)](https://www.codacy.com/gh/php-mime-mail-parser/php-mime-mail-parser/dashboard?utm_source=github.com&utm_medium=referral&utm_content=php-mime-mail-parser/php-mime-mail-parser&utm_campaign=Badge_Grade) + +## How do I install it? + +The easiest way is via [Composer](https://getcomposer.org/). + +To install the latest version of PHP MIME Mail Parser, run the command below: + + composer require php-mime-mail-parser/php-mime-mail-parser + +## Requirements + +The following versions of PHP are supported: + +* PHP 8.0 +* PHP 8.1 +* PHP 8.2 +* PHP 8.3 + +Previous Versions: + +| PHP Compatibility | Version | +|-------------------|-----------------------------| +| HHVM | [php-mime-mail-parser 2.11.1](https://github.com/php-mime-mail-parser/php-mime-mail-parser/releases/tag/2.11.1) | +| PHP 5.4 | [php-mime-mail-parser 2.11.1](https://github.com/php-mime-mail-parser/php-mime-mail-parser/releases/tag/2.11.1) | +| PHP 5.5 | [php-mime-mail-parser 2.11.1](https://github.com/php-mime-mail-parser/php-mime-mail-parser/releases/tag/2.11.1) | +| PHP 5.6 | [php-mime-mail-parser 3.0.4](https://github.com/php-mime-mail-parser/php-mime-mail-parser/releases/tag/3.0.4) | +| PHP 7.0 | [php-mime-mail-parser 3.0.4](https://github.com/php-mime-mail-parser/php-mime-mail-parser/releases/tag/3.0.4) | +| PHP 7.1 | [php-mime-mail-parser 5.0.5](https://github.com/php-mime-mail-parser/php-mime-mail-parser/releases/tag/5.0.5) | +| PHP 7.2 | [php-mime-mail-parser 7.1.2](https://github.com/php-mime-mail-parser/php-mime-mail-parser/releases/tag/7.1.2) | +| PHP 7.3 | [php-mime-mail-parser 7.1.2](https://github.com/php-mime-mail-parser/php-mime-mail-parser/releases/tag/7.1.2) | +| PHP 7.4 | [php-mime-mail-parser 7.1.2](https://github.com/php-mime-mail-parser/php-mime-mail-parser/releases/tag/7.1.2) | + +Make sure you have the mailparse extension (http://php.net/manual/en/book.mailparse.php) properly installed. The command line `php -m | grep mailparse` needs to return "mailparse". + + +### Install mailparse extension + +#### Debian, Ubuntu & derivatives +``` +sudo apt install php-cli php-mailparse +``` + +#### MacOS +``` +brew install php +pecl install mailparse +``` + +#### Other platforms +``` +sudo apt install php-cli php-pear php-dev php-mbstring +pecl install mailparse +``` + +#### From source + +AAAAMMDD should be `php-config --extension-dir` +``` +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 /usr/lib/php/AAAAMMDD/ +echo "extension=mailparse.so" | sudo tee /etc/php/7.1/mods-available/mailparse.ini +sudo phpenmod mailparse +``` + +#### Windows +You need to download mailparse DLL from http://pecl.php.net/package/mailparse and add the line `extension=php_mailparse.dll` to `php.ini` accordingly. + +## How do I use it? + +### Loading an email + +You can load an email in 4 differents ways: + +```php +require_once __DIR__.'/vendor/autoload.php'; + +$path = 'path/to/email.eml'; +$parser = new PhpMimeMailParser\Parser(); + +// 1. Either specify a file path (string) +$parser->setPath($path); + +// 2. or specify the raw mime mail text (string) +$parser->setText(file_get_contents($path)); + +// 3. or specify a php file resource (stream) +$parser->setStream(fopen($path, "r")); + +// 4. or specify a stream to work with a 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 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 + + $stream = $attachment->getStream(); + // get the stream of the attachment file + + $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 + +To forward mails from [Postfix](http://www.postfix.org/) to the PHP script above, add this line at the end of your `/etc/postfix/master.cf` +(to 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 (see above) 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/externals/mimemailparser/attachment.class.php b/externals/mimemailparser/attachment.class.php deleted file mode 100644 index 2631a4fc44..0000000000 --- a/externals/mimemailparser/attachment.class.php +++ /dev/null @@ -1,136 +0,0 @@ -filename = $filename; - $this->content_type = $content_type; - $this->stream = $stream; - $this->content = null; - $this->content_disposition = $content_disposition; - $this->headers = $headers; - } - - /** - * retrieve the attachment filename - * @return String - */ - public function getFilename() { - return $this->filename; - } - - /** - * Retrieve the Attachment Content-Type - * @return String - */ - public function getContentType() { - return $this->content_type; - } - - /** - * Retrieve the Attachment Content-Disposition - * @return String - */ - public function getContentDisposition() { - return $this->content_disposition; - } - - /** - * Retrieve the Attachment Headers - * @return String - */ - public function getHeaders() { - return $this->headers; - } - - /** - * Retrieve the file extension - * @return String - */ - public function getFileExtension() { - if (!$this->extension) { - $ext = substr(strrchr($this->filename, '.'), 1); - if ($ext == 'gz') { - // special case, tar.gz - // todo: other special cases? - $ext = preg_match("/\.tar\.gz$/i", $ext) ? 'tar.gz' : 'gz'; - } - $this->extension = $ext; - } - return $this->extension; - } - - /** - * Read the contents a few bytes at a time until completed - * Once read to completion, it always returns false - * @return String - * @param $bytes Int[optional] - */ - 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; - } - - /** - * Allow the properties - * MimeMailParser_attachment::$name, - * MimeMailParser_attachment::$extension - * to be retrieved as public properties - * @param $name Object - */ - public function __get($name) { - if ($name == 'content') { - return $this->getContent(); - } else if ($name == 'extension') { - return $this->getFileExtension(); - } - return null; - } - -} - -?> diff --git a/scripts/mail/mail_handler.php b/scripts/mail/mail_handler.php index 5631310a2b..4cff35699d 100755 --- a/scripts/mail/mail_handler.php +++ b/scripts/mail/mail_handler.php @@ -14,7 +14,15 @@ if ($argc > 1) { $root = dirname(dirname(dirname(__FILE__))); require_once $root.'/scripts/__init_script__.php'; -require_once $root.'/externals/mimemailparser/MimeMailParser.class.php'; +require_once $root.'/externals/mimemailparser/Contracts/CharsetManager.php'; +require_once $root.'/externals/mimemailparser/Contracts/Middleware.php'; +require_once $root.'/externals/mimemailparser/Parser.php'; +require_once $root.'/externals/mimemailparser/Charset.php'; +require_once $root.'/externals/mimemailparser/Attachment.php'; +require_once $root.'/externals/mimemailparser/Exception.php'; +require_once $root.'/externals/mimemailparser/Middleware.php'; +require_once $root.'/externals/mimemailparser/MiddlewareStack.php'; +require_once $root.'/externals/mimemailparser/MimePart.php'; $args = new PhutilArgumentParser($argv); $args->parseStandardArguments(); @@ -32,25 +40,19 @@ $args->parse( ), )); -$parser = new MimeMailParser(); +if (!extension_loaded('mailparse')) { + throw new Exception( + pht( + 'PhpMimeMailParser for handling incoming mail requires the PHP '. + 'mailparse extension to be installed.')); +} + +$parser = new \PhpMimeMailParser\Parser(); $parser->setText(file_get_contents('php://stdin')); $content = array(); foreach (array('text', 'html') as $part) { $part_body = $parser->getMessageBody($part); - - if (strlen($part_body) && !phutil_is_utf8($part_body)) { - $part_headers = $parser->getMessageBodyHeaders($part); - if (!is_array($part_headers)) { - $part_headers = array(); - } - $content_type = idx($part_headers, 'content-type'); - if (preg_match('/charset="(.*?)"/', $content_type, $matches) || - preg_match('/charset=(\S+)/', $content_type, $matches)) { - $part_body = phutil_utf8_convert($part_body, 'UTF-8', $matches[1]); - } - } - $content[$part] = $part_body; }