Something went wrong. Try again.
@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
Something went wrong. Try again.
26 kB · 928 lines
PHP
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929<?php
namespace PhpMimeMailParser;
use PhpMimeMailParser\Contracts\CharsetManager;
/** * Parser of php-mime-mail-parser * * Fully Tested Mailparse Extension Wrapper for PHP 5.4+ * */class Parser{ /** * Attachment filename argument option for ->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() { if (!$this->resource) { throw new Exception( 'MIME message cannot be parsed' ); } $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 htmlEmbedded.' ); }
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<int, array{'display': string, 'address': string, 'is_group': bool}> */ 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 inline parts contents (text or HTML) * * @return string[] The decoded inline parts. */ 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); }}