Server IP : 213.176.29.180 / Your IP : 18.188.249.160 Web Server : Apache System : Linux 213.176.29.180.hostiran.name 4.18.0-553.22.1.el8_10.x86_64 #1 SMP Tue Sep 24 05:16:59 EDT 2024 x86_64 User : webtaragh ( 1001) PHP Version : 7.4.33 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : ON Directory (0750) : /home/webtaragh/public_html/wp-content/../ |
[ Home ] | [ C0mmand ] | [ Upload File ] |
---|
mb-wrapper/src/MbWrapper.php 0000644 00000036265 14736103045 0012036 0 ustar 00 <?php /** * This file is part of the ZBateson\MbWrapper project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MbWrapper; /** * Helper class for converting strings between charsets, finding a multibyte * strings length, and creating a substring. * * MbWrapper prefers PHP's mb_* extension first, and reverts to iconv_* if the * charsets aren't listed as supported by mb_list_encodings(). * * A list of aliased charsets are maintained to support the greatest number of * charsets. In addition, when searching for a charset, separator characters * such as dashes are removed, and searches are always performed * case-insensitively. This is to support strange reported encodings in emails, * etc... * * @author Zaahid Bateson */ class MbWrapper { /** * @var array aliased charsets supported by mb_convert_encoding. * The alias is stripped of any non-alphanumeric characters (so CP367 * is equal to CP-367) when comparing. * Some of these translations are already supported by * mb_convert_encoding on "my" PHP 5.5.9, but may not be supported in * other implementations or versions since they're not part of * documented support. */ public static $mbAliases = [ // supported but not included in mb_list_encodings for some reason... 'CP850' => 'CP850', 'GB2312' => 'GB2312', 'SJIS2004' => 'SJIS-2004', // aliases 'ANSIX341968' => 'ASCII', 'ANSIX341986' => 'ASCII', 'ARABIC' => 'ISO-8859-6', 'ASMO708' => 'ISO-8859-6', 'BIG5' => 'BIG-5', 'BIG5TW' => 'BIG-5', 'CHINESE' => 'GB2312', 'CP367' => 'ASCII', 'CP819' => 'ISO-8859-1', 'CP1251' => 'WINDOWS-1251', 'CP1252' => 'WINDOWS-1252', 'CP1254' => 'WINDOWS-1254', 'CP1255' => 'ISO-8859-8', 'CSASCII' => 'ASCII', 'CSBIG5' => 'BIG-5', 'CSIBM866' => 'CP866', 'CSISO2022JP' => 'ISO-2022-JP', 'CSISO2022KR' => 'ISO-2022-KR', 'CSISO58GB231280' => 'GB2312', 'CSISOLATIN1' => 'ISO-8859-1', 'CSISOLATIN2' => 'ISO-8859-2', 'CSISOLATIN3' => 'ISO-8859-3', 'CSISOLATIN4' => 'ISO-8859-4', 'CSISOLATIN5' => 'ISO-8859-9', 'CSISOLATIN6' => 'ISO-8859-10', 'CSISOLATINARABIC' => 'ISO-8859-6', 'CSISOLATINCYRILLIC' => 'ISO-8859-5', 'CSISOLATINGREEK' => 'ISO-8859-7', 'CSISOLATINHEBREW' => 'ISO-8859-8', 'CSKOI8R' => 'KOI8-R', 'CSPC850MULTILINGUAL' => 'CP850', 'CSSHIFTJIS' => 'SJIS', 'CYRILLIC' => 'ISO-8859-5', 'ECMA114' => 'ISO-8859-6', 'ECMA118' => 'ISO-8859-7', 'ELOT928' => 'ISO-8859-7', 'EUCCN' => 'GB2312', 'EUCGB2312CN' => 'GB2312', 'GB180302000' => 'GB18030', 'GB23121980' => 'GB2312', 'GB231280' => 'GB2312', 'GBK' => 'CP936', 'GREEK8' => 'ISO-8859-7', 'GREEK' => 'ISO-8859-7', 'HEBREW' => 'ISO-8859-8', 'HZGB2312' => 'HZ', 'HZGB' => 'HZ', 'IBM367' => 'ASCII', 'IBM819' => 'ISO-8859-1', 'IBM850' => 'CP850', 'IBM866' => 'CP866', 'ISO2022JP2004' => 'ISO-2022-JP-2004', 'ISO646IRV1991' => 'ASCII', 'ISO646US' => 'ASCII', 'ISO8859' => 'ISO-8859-1', 'ISO8859101992' => 'ISO-8859-10', 'ISO885911987' => 'ISO-8859-1', 'ISO8859141998' => 'ISO-8859-14', 'ISO8859162001' => 'ISO-8859-16', 'ISO885921987' => 'ISO-8859-2', 'ISO885931988' => 'ISO-8859-3', 'ISO885941988' => 'ISO-8859-4', 'ISO885951988' => 'ISO-8859-5', 'ISO885961987' => 'ISO-8859-6', 'ISO885971987' => 'ISO-8859-7', 'ISO885981988' => 'ISO-8859-8', 'ISO88598I' => 'ISO-8859-8', 'ISO885991989' => 'ISO-8859-9', 'ISOCELTIC' => 'ISO-8859-14', 'ISOIR100' => 'ISO-8859-1', 'ISOIR101' => 'ISO-8859-2', 'ISOIR109' => 'ISO-8859-3', 'ISOIR110' => 'ISO-8859-4', 'ISOIR126' => 'ISO-8859-7', 'ISOIR127' => 'ISO-8859-6', 'ISOIR138' => 'ISO-8859-8', 'ISOIR144' => 'ISO-8859-5', 'ISOIR148' => 'ISO-8859-9', 'ISOIR157' => 'ISO-8859-10', 'ISOIR199' => 'ISO-8859-14', 'ISOIR226' => 'ISO-8859-16', 'ISOIR58' => 'GB2312', 'ISOIR6' => 'ASCII', 'KOI8R' => 'KOI8-R', 'KOREAN' => 'EUC-KR', 'KSC56011987' => 'EUC-KR', 'KSC5601' => 'EUC-KR', 'KSX1001' => 'EUC-KR', 'L1' => 'ISO-8859-1', 'L2' => 'ISO-8859-2', 'L3' => 'ISO-8859-3', 'L4' => 'ISO-8859-4', 'L5' => 'ISO-8859-9', 'L6' => 'ISO-8859-10', 'L8' => 'ISO-8859-14', 'L10' => 'ISO-8859-16', 'LATIN' => 'ISO-8859-1', 'LATIN1' => 'ISO-8859-1', 'LATIN2' => 'ISO-8859-2', 'LATIN3' => 'ISO-8859-3', 'LATIN4' => 'ISO-8859-4', 'LATIN5' => 'ISO-8859-9', 'LATIN6' => 'ISO-8859-10', 'LATIN8' => 'ISO-8859-14', 'LATIN10' => 'ISO-8859-16', 'MS932' => 'CP932', 'ms936' => 'CP936', 'MS950' => 'CP950', 'MSKANJI' => 'CP932', 'SHIFTJIS2004' => 'SJIS', 'SHIFTJIS' => 'SJIS', 'UJIS' => 'EUC-JP', 'US' => 'ASCII', 'USASCII' => 'ASCII', 'WE8MSWIN1252' => 'WINDOWS-1252', 'WINDOWS1251' => 'WINDOWS-1251', 'WINDOWS1252' => 'WINDOWS-1252', 'WINDOWS1254' => 'WINDOWS-1254', 'WINDOWS1255' => 'ISO-8859-8', '0' => 'WINDOWS-1252', '128' => 'SJIS', '129' => 'EUC-KR', '134' => 'GB2312', '136' => 'BIG-5', '161' => 'WINDOWS-1253', '162' => 'WINDOWS-1254', '177' => 'WINDOWS-1255', '178' => 'WINDOWS-1256', '186' => 'WINDOWS-1257', '204' => 'WINDOWS-1251', '222' => 'WINDOWS-874', '238' => 'WINDOWS-1250', '646' => 'ASCII', '850' => 'CP850', '866' => 'CP866', '932' => 'CP932', '936' => 'CP936', '950' => 'CP950', '1251' => 'WINDOWS-1251', '1252' => 'WINDOWS-1252', '1254' => 'WINDOWS-1254', '1255' => 'ISO-8859-8', '8859' => 'ISO-8859-1', ]; /** * @var array aliased charsets supported by iconv. */ public static $iconvAliases = [ // iconv aliases -- a lot of these may already be supported 'CP154' => 'PT154', 'CPGR' => 'CP869', 'CPIS' => 'CP861', 'CSHPROMAN8' => 'ROMAN8', 'CSIBM037' => 'CP037', 'CSIBM1026' => 'CP1026', 'CSIBM424' => 'CP424', 'CSIBM500' => 'CP500', 'CSIBM860' => 'CP860', 'CSIBM861' => 'CP861', 'CSIBM863' => 'CP863', 'CSIBM864' => 'CP864', 'CSIBM865' => 'CP865', 'CSIBM869' => 'CP869', 'CSPC775BALTIC' => 'CP775', 'CSPC862LATINHEBREW' => 'CP862', 'CSPC8CODEPAGE437' => 'CP437', 'CSPTCP154' => 'PT154', 'CYRILLICASIAN' => 'PT154', 'EBCDICCPBE' => 'CP500', 'EBCDICCPCA' => 'CP037', 'EBCDICCPCH' => 'CP500', 'EBCDICCPHE' => 'CP424', 'EBCDICCPNL' => 'CP037', 'EBCDICCPUS' => 'CP037', 'EBCDICCPWT' => 'CP037', 'HKSCS' => 'BIG5HKSCS', 'HPROMAN8' => 'ROMAN8', 'IBM037' => 'CP037', 'IBM039' => 'CP037', 'IBM424' => 'CP424', 'IBM437' => 'CP437', 'IBM500' => 'CP500', 'IBM775' => 'CP775', 'IBM860' => 'CP860', 'IBM861' => 'CP861', 'IBM862' => 'CP862', 'IBM863' => 'CP863', 'IBM864' => 'CP864', 'IBM865' => 'CP865', 'IBM869' => 'CP869', 'IBM1026' => 'CP1026', 'IBM1140' => 'CP1140', 'ISO2022JP2' => 'ISO2022JP2', 'ISO8859112001' => 'ISO885911', 'ISO885911' => 'ISO885911', 'ISOIR166' => 'TIS620', 'JOHAB' => 'CP1361', 'MACCYRILLIC' => 'MACCYRILLIC', 'MS1361' => 'CP1361', 'MS949' => 'CP949', 'PTCP154' => 'PT154', 'R8' => 'ROMAN8', 'ROMAN8' => 'ROMAN8', 'THAI' => 'ISO885911', 'TIS6200' => 'TIS620', 'TIS62025290' => 'TIS620', 'TIS62025291' => 'TIS620', 'TIS620' => 'TIS620', 'UHC' => 'CP949', 'WINDOWS1250' => 'CP1250', 'WINDOWS1253' => 'CP1253', 'WINDOWS1256' => 'CP1256', 'WINDOWS1257' => 'CP1257', 'WINDOWS1258' => 'CP1258', '037' => 'CP037', '424' => 'CP424', '437' => 'CP437', '500' => 'CP500', '775' => 'CP775', '860' => 'CP860', '861' => 'CP861', '862' => 'CP862', '863' => 'CP863', '864' => 'CP864', '865' => 'CP865', '869' => 'CP869', '949' => 'CP949', '1026' => 'CP1026', '1140' => 'CP1140', '1250' => 'CP1250', '1253' => 'CP1253', '1256' => 'CP1256', '1257' => 'CP1257', '1258' => 'CP1258', ]; /** * @var string[] An array of encodings supported by the mb_* extension, as * returned by mb_list_encodings(), with the key set to the charset's * name afte */ private static $mbListedEncodings; /** * @var string[] cached lookups for quicker retrieval */ protected $mappedMbCharsets = [ 'UTF8' => 'UTF-8', 'USASCII' => 'US-ASCII', 'ISO88591' => 'ISO-8859-1', ]; /** * Initializes the static mb_* encoding array. */ public function __construct() { if (self::$mbListedEncodings === null) { $cs = mb_list_encodings(); $keys = $this->getNormalizedCharset($cs); self::$mbListedEncodings = array_combine($keys, $cs); } } /** * The passed charset is uppercased, and stripped of non-alphanumeric * characters before being returned. * * @param string|string[] $charset * @return string|string[] */ private function getNormalizedCharset($charset) { $upper = null; if (is_array($charset)) { $upper = array_map('strtoupper', $charset); } else { $upper = strtoupper($charset); } return preg_replace('/[^A-Z0-9]+/', '', $upper); } /** * Converts the passed string's charset from the passed $fromCharset to the * passed $toCharset * * The function attempts to use mb_convert_encoding if possible, and falls * back to iconv if not. If the source or destination character sets aren't * supported, a blank string is returned. * * @param string $str * @return string */ public function convert($str, $fromCharset, $toCharset) { // there may be some mb-supported encodings not supported by iconv (on my libiconv for instance // HZ isn't supported), and so it may happen that failing an mb_convert_encoding, an iconv // may also fail even though both support an encoding separately. // For cases like that, a two-way encoding is done with UTF-8 as an intermediary. $from = $this->getMbCharset($fromCharset); $to = $this->getMbCharset($toCharset); if ($str !== '') { if ($from !== false && $to === false) { $str = mb_convert_encoding($str, 'UTF-8', $from); return iconv('UTF-8', $this->getIconvAlias($toCharset) . '//TRANSLIT//IGNORE', $str); } elseif ($from === false && $to !== false) { $str = iconv($this->getIconvAlias($fromCharset), 'UTF-8//TRANSLIT//IGNORE', $str); return mb_convert_encoding($str, $to, 'UTF-8'); } elseif ($from !== false && $to !== false) { return mb_convert_encoding($str, $to, $from); } return iconv( $this->getIconvAlias($fromCharset), $this->getIconvAlias($toCharset) . '//TRANSLIT//IGNORE', $str ); } return $str; } /** * Returns true if the passed string is valid in the $charset encoding. * * Either uses mb_check_encoding, or iconv if it's not a supported mb * encoding. * * @param type $str * @param type $charset */ public function checkEncoding($str, $charset) { $mb = $this->getMbCharset($charset); if ($mb !== false) { return mb_check_encoding($str, $mb); } $ic = $this->getIconvAlias($charset); return (@iconv($ic, $ic, $str) !== false); } /** * Uses either mb_strlen or iconv_strlen to return the number of characters * in the passed $str for the given $charset * * @param string $str * @param string $charset * @return int */ public function getLength($str, $charset) { $mb = $this->getMbCharset($charset); if ($mb !== false) { return mb_strlen($str, $mb); } return iconv_strlen($str, $this->getIconvAlias($charset)); } /** * Uses either mb_substr or iconv_substr to create and return a substring of * the passed $str. * * @param string $str * @param string $charset * @param int $start * @param int $length * @return string */ public function getSubstr($str, $charset, $start, $length = null) { $mb = $this->getMbCharset($charset); if ($mb !== false) { return mb_substr($str, $start, $length, $mb); } $ic = $this->getIconvAlias($charset); if ($ic === 'CP1258') { // iconv_substr fails with CP1258 for some reason, and returns only // a subset of characters (e.g. the first 5, instead of $length) $str = $this->convert($str, $ic, 'UTF-8'); return $this->convert($this->getSubstr($str, 'UTF-8', $start, $length), 'UTF-8', $ic); } if ($length === null) { $length = iconv_strlen($str, $ic); } return iconv_substr($str, $start, $length, $ic); } /** * Looks up a charset from mb_list_encodings and identified aliases, * checking if the lookup has been cached already first. * * If the encoding is not listed, the method will return false. * * On success, the method will return the charset name as accepted by mb_*. * * @param string $cs * @param bool $mbSupported * @return string|bool */ private function getMbCharset($cs) { $normalized = $this->getNormalizedCharset($cs); if (array_key_exists($normalized, self::$mbListedEncodings)) { return self::$mbListedEncodings[$normalized]; } elseif (array_key_exists($normalized, self::$mbAliases)) { return self::$mbAliases[$normalized]; } return false; } /** * Looks up the passed charset in self::$iconvAliases, returning the mapped * charset if applicable. Otherwise returns charset. * * @param string $cs * @return string the mapped charset (if mapped) or $cs otherwise */ private function getIconvAlias($cs) { $normalized = $this->getNormalizedCharset($cs); if (array_key_exists($normalized, self::$iconvAliases)) { return static::$iconvAliases[$normalized]; } return $cs; } } mb-wrapper/README.md 0000644 00000005164 14736103045 0010110 0 ustar 00 # zbateson/mb-wrapper Charset conversion and string manipulation wrapper with a large defined set of aliases. [![Build Status](https://travis-ci.org/zbateson/mb-wrapper.svg?branch=master)](https://travis-ci.org/zbateson/mb-wrapper) [![Code Coverage](https://scrutinizer-ci.com/g/zbateson/mb-wrapper/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/zbateson/mb-wrapper/?branch=master) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/zbateson/mb-wrapper/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/zbateson/mb-wrapper/?branch=master) [![Total Downloads](https://poser.pugx.org/zbateson/mb-wrapper/downloads)](https://packagist.org/packages/zbateson/mb-wrapper) [![Latest Stable Version](https://poser.pugx.org/zbateson/mb-wrapper/version)](https://packagist.org/packages/zbateson/mb-wrapper) The goals of this project are to be: * Well written * Tested where possible * Support as wide a range of charset aliases as possible To include it for use in your project, please install via composer: ``` composer require zbateson/mb-wrapper ``` ## Requirements mb-wrapper requires PHP 5.4 or newer. Tested on PHP 5.4, 5.5, 5.6, 7, 7.1, 7.2 and 7.3 on travis. Please note: hhvm support has been dropped as it no longer supports 'php' as of version 4. Previous versions of hhvm may still work, but are no longer supported. ## Description MbWrapper is intended for use wherever mb_* or iconv_* is used. It scans supported charsets returned by mb_list_encodings(), and prefers mb_* functions, but will fallback to iconv if a charset isn't supported. A list of aliased charsets is maintained for both mb_* and iconv, where a supported charset exists for an alias. This is useful for mail and http parsing as other systems may report encodings not recognized by mb_* or iconv. Charset lookup is done by removing non-alphanumeric characters as well, so UTF8 will always be matched to UTF-8, etc... ## Usage The following wrapper methods are exposed: * mb_convert_encoding, iconv with MbWrapper::convert * mb_substr, iconv_substr with MbWrapper::getSubstr * mb_strlen, iconv_strlen with MbWrapper::getLength * mb_check_encoding, iconv (for verification) with MbWrapper::checkEncoding ```php $mbWrapper = new \ZBateson\MbWrapper\MbWrapper(); $fromCharset = 'ISO-8859-1'; $toCharset = 'UTF-8'; $mbWrapper->convert('data', $fromCharset, $toCharset); $mbWrapper->getLength('data', 'UTF-8'); $mbWrapper->substr('data', 'UTF-8', 1, 2); if ($mbWrapper->checkEncoding('data', 'UTF-8')) { echo 'Compatible'; } ``` ## License BSD licensed - please see [license agreement](https://github.com/zbateson/mb-wrapper/blob/master/LICENSE). mb-wrapper/LICENSE 0000644 00000002447 14736103045 0007637 0 ustar 00 BSD 2-Clause License Copyright (c) 2018, Zaahid Bateson All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. mail-mime-parser/version.txt 0000644 00000000006 14736103045 0012132 0 ustar 00 1.3.0 mail-mime-parser/src/Message/MessageFactory.php 0000644 00000004160 14736103045 0015511 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Message; use Psr\Http\Message\StreamInterface; use ZBateson\MailMimeParser\Message; use ZBateson\MailMimeParser\Message\Helper\MessageHelperService; use ZBateson\MailMimeParser\Message\Part\PartBuilder; use ZBateson\MailMimeParser\Message\Part\Factory\MimePartFactory; use ZBateson\MailMimeParser\Message\Part\Factory\PartStreamFilterManagerFactory; use ZBateson\MailMimeParser\Message\PartFilterFactory; use ZBateson\MailMimeParser\Stream\StreamFactory; /** * Responsible for creating Message instances. * * @author Zaahid Bateson */ class MessageFactory extends MimePartFactory { /** * @var MessageHelperService helper class for message manipulation routines. */ protected $messageHelperService; /** * Constructor * * @param StreamFactory $sdf * @param PartStreamFilterManagerFactory $psf * @param PartFilterFactory $pf * @param MessageHelperService $mhs */ public function __construct( StreamFactory $sdf, PartStreamFilterManagerFactory $psf, PartFilterFactory $pf, MessageHelperService $mhs ) { parent::__construct($sdf, $psf, $pf); $this->messageHelperService = $mhs; } /** * Constructs a new Message object and returns it * * @param PartBuilder $partBuilder * @param StreamInterface $stream * @return \ZBateson\MailMimeParser\Message\Part\MimePart */ public function newInstance(PartBuilder $partBuilder, StreamInterface $stream = null) { $contentStream = null; if ($stream !== null) { $contentStream = $this->streamFactory->getLimitedContentStream($stream, $partBuilder); } return new Message( $this->partStreamFilterManagerFactory->newInstance(), $this->streamFactory, $this->partFilterFactory, $partBuilder, $this->messageHelperService, $stream, $contentStream ); } } mail-mime-parser/src/Message/PartFilterFactory.php 0000644 00000004354 14736103045 0016206 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Message; /** * Injectable factory class used by MimePart to construct PartFilter instances * in a testable way. * * Users are expected to use the static PartFilter methods directly -- this * class simply encapsulates them in an object: * o PartFilter::fromContentType * o PartFilter::fromInlineContentType * o PartFilter::fromDisposition * * @see PartFilter * @author Zaahid Bateson */ class PartFilterFactory { /** * Creates a filter for the passed mime content-type. * * This method just calls PartFilter::fromContentType. * * @see PartFilter::fromContentType * @param string $mimeType * @return PartFilter */ public function newFilterFromContentType($mimeType) { return PartFilter::fromContentType($mimeType); } /** * Creates an 'inline' filter for the passed mime content-type. * * This method just calls PartFilter::fromInlineContentType. * * @see PartFilter::fromInlineContentType * @param string $mimeType * @return PartFilter */ public function newFilterFromInlineContentType($mimeType) { return PartFilter::fromInlineContentType($mimeType); } /** * Creates a filter for the passed disposition and optional multipart * filter. * * This method just calls PartFilter::newFilterFromDisposition. * * @see PartFilter::fromDisposition * @param string $disposition * @param int $multipart one of PartFilter::FILTER_OFF, * PartFilter::FILTER_INCLUDE or PartFilter::FILTER_EXCLUDE * @return PartFilter */ public function newFilterFromDisposition($disposition, $multipart = PartFilter::FILTER_OFF) { return PartFilter::fromDisposition($disposition, $multipart); } /** * Constructs a PartFilter from the passed array of options and returns it. * * @see PartFilter::__construct * @param array $init * @return PartFilter */ public function newFilterFromArray(array $init) { return new PartFilter($init); } } mail-mime-parser/src/Message/PartFilter.php 0000644 00000031224 14736103045 0014652 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Message; use ZBateson\MailMimeParser\Message\Part\MessagePart; use ZBateson\MailMimeParser\Message\Part\MimePart; use InvalidArgumentException; /** * Provides a way to define a filter of MessagePart for use in various calls to * add/remove MessagePart. * * A PartFilter is defined as a set of properties in the class, set to either be * 'included' or 'excluded'. The filter is simplistic in that a property * defined as included must be set on a part for it to be passed, and an * excluded filter must not be set for the part to be passed. There is no * provision for creating logical conditions. * * The only property set by default is $signedpart, which defaults to * FILTER_EXCLUDE. * * A PartFilter can be instantiated with an array of keys matching class * properties, and values to set them for convenience. * * ```php * $inlineParts = $message->getAllParts(new PartFilter([ * 'multipart' => PartFilter::FILTER_INCLUDE, * 'headers' => [ * FILTER_EXCLUDE => [ * 'Content-Disposition': 'attachment' * ] * ] * ])); * * $inlineTextPart = $message->getAllParts(PartFilter::fromInlineContentType('text/plain')); * ``` * * @author Zaahid Bateson */ class PartFilter { /** * @var int indicates a filter is not in use */ const FILTER_OFF = 0; /** * @var int an excluded filter must not be included in a part */ const FILTER_EXCLUDE = 1; /** * @var int an included filter must be included in a part */ const FILTER_INCLUDE = 2; /** * @var int filters based on whether MessagePart::hasContent is true */ private $hascontent = PartFilter::FILTER_OFF; /** * @var int filters based on whether MimePart::isMultiPart is true */ private $multipart = PartFilter::FILTER_OFF; /** * @var int filters based on whether MessagePart::isTextPart is true */ private $textpart = PartFilter::FILTER_OFF; /** * @var int filters based on whether the parent of a part is a * multipart/signed part and this part has a content-type equal to its * parent's 'protocol' parameter in its content-type header */ private $signedpart = PartFilter::FILTER_EXCLUDE; /** * @var string calculated hash of the filter */ private $hashCode; /** * @var string[][] array of header rules. The top-level contains keys of * FILTER_INCLUDE and/or FILTER_EXCLUDE, which contain key => value mapping * of header names => values to search for. Note that when searching * MimePart::getHeaderValue is used (so additional parameters need not be * matched) and strcasecmp is used. * * ```php * $filter = new PartFilter(); * $filter->headers = [ PartFilter::FILTER_INCLUDE => [ 'Content-Type' => 'text/plain' ] ]; * ``` */ private $headers = []; /** * Convenience method to filter for a specific mime type. * * @param string $mimeType * @return PartFilter */ public static function fromContentType($mimeType) { return new static([ 'headers' => [ static::FILTER_INCLUDE => [ 'Content-Type' => $mimeType ] ] ]); } /** * Convenience method to look for parts of a specific mime-type, and that * do not specifically have a Content-Disposition equal to 'attachment'. * * @param string $mimeType * @return PartFilter */ public static function fromInlineContentType($mimeType) { return new static([ 'headers' => [ static::FILTER_INCLUDE => [ 'Content-Type' => $mimeType ], static::FILTER_EXCLUDE => [ 'Content-Disposition' => 'attachment' ] ] ]); } /** * Convenience method to search for parts with a specific * Content-Disposition, optionally including multipart parts. * * @param string $disposition * @param int $multipart * @return PartFilter */ public static function fromDisposition($disposition, $multipart = PartFilter::FILTER_OFF) { return new static([ 'multipart' => $multipart, 'headers' => [ static::FILTER_INCLUDE => [ 'Content-Disposition' => $disposition ] ] ]); } /** * Constructs a PartFilter, optionally instantiating member variables with * values in the passed array. * * The passed array must use keys equal to member variable names, e.g. * 'multipart', 'textpart', 'signedpart' and 'headers'. * * @param array $filter */ public function __construct(array $filter = []) { $params = [ 'hascontent', 'multipart', 'textpart', 'signedpart', 'headers' ]; foreach ($params as $param) { if (isset($filter[$param])) { $this->__set($param, $filter[$param]); } } } /** * Validates an argument passed to __set to insure it's set to a value in * $valid. * * @param string $name Name of the member variable * @param string $value The value to test * @param array $valid an array of valid values * @throws InvalidArgumentException */ private function validateArgument($name, $value, array $valid) { if (!in_array($value, $valid)) { $last = array_pop($valid); throw new InvalidArgumentException( '$value parameter for ' . $name . ' must be one of ' . join(', ', $valid) . ' or ' . $last . ' - "' . $value . '" provided' ); } } /** * Sets the PartFilter's headers filter to the passed array after validating * it. * * @param array $headers * @throws InvalidArgumentException */ public function setHeaders(array $headers) { array_walk($headers, function ($v, $k) { $this->validateArgument( 'headers', $k, [ static::FILTER_EXCLUDE, static::FILTER_INCLUDE ] ); if (!is_array($v)) { throw new InvalidArgumentException( '$value must be an array with keys set to FILTER_EXCLUDE, ' . 'FILTER_INCLUDE and values set to an array of header ' . 'name => values' ); } }); $this->headers = $headers; } /** * Sets the member variable denoted by $name to the passed $value after * validating it. * * @param string $name * @param int|array $value * @throws InvalidArgumentException */ public function __set($name, $value) { if ($name === 'hascontent' || $name === 'multipart' || $name === 'textpart' || $name === 'signedpart') { if (is_array($value)) { throw new InvalidArgumentException('$value must be not be an array'); } $this->validateArgument( $name, $value, [ static::FILTER_OFF, static::FILTER_EXCLUDE, static::FILTER_INCLUDE ] ); $this->$name = $value; } elseif ($name === 'headers') { if (!is_array($value)) { throw new InvalidArgumentException('$value must be an array'); } $this->setHeaders($value); } } /** * Returns true if the variable denoted by $name is a member variable of * PartFilter. * * @param string $name * @return bool */ public function __isset($name) { return isset($this->$name); } /** * Returns the value of the member variable denoted by $name * * @param string $name * @return mixed */ public function __get($name) { return $this->$name; } /** * Returns true if the passed MessagePart fails the filter's hascontent * filter settings. * * @param MessagePart $part * @return bool */ private function failsHasContentFilter(MessagePart $part) { return ($this->hascontent === static::FILTER_EXCLUDE && $part->hasContent()) || ($this->hascontent === static::FILTER_INCLUDE && !$part->hasContent()); } /** * Returns true if the passed MessagePart fails the filter's multipart * filter settings. * * @param MessagePart $part * @return bool */ private function failsMultiPartFilter(MessagePart $part) { if (!($part instanceof MimePart)) { return $this->multipart !== static::FILTER_EXCLUDE; } return ($this->multipart === static::FILTER_EXCLUDE && $part->isMultiPart()) || ($this->multipart === static::FILTER_INCLUDE && !$part->isMultiPart()); } /** * Returns true if the passed MessagePart fails the filter's textpart filter * settings. * * @param MessagePart $part * @return bool */ private function failsTextPartFilter(MessagePart $part) { return ($this->textpart === static::FILTER_EXCLUDE && $part->isTextPart()) || ($this->textpart === static::FILTER_INCLUDE && !$part->isTextPart()); } /** * Returns true if the passed MessagePart fails the filter's signedpart * filter settings. * * @param MessagePart $part * @return boolean */ private function failsSignedPartFilter(MessagePart $part) { if ($this->signedpart === static::FILTER_OFF) { return false; } elseif (!$part->isMime() || $part->getParent() === null) { return ($this->signedpart === static::FILTER_INCLUDE); } $partMimeType = $part->getContentType(); $parentMimeType = $part->getParent()->getContentType(); $parentProtocol = $part->getParent()->getHeaderParameter('Content-Type', 'protocol'); if (strcasecmp($parentMimeType, 'multipart/signed') === 0 && strcasecmp($partMimeType, $parentProtocol) === 0) { return ($this->signedpart === static::FILTER_EXCLUDE); } return ($this->signedpart === static::FILTER_INCLUDE); } /** * Tests a single header value against $part, and returns true if the test * fails. * * @staticvar array $map * @param MessagePart $part * @param int $type * @param string $name * @param string $header * @return boolean */ private function failsHeaderFor(MessagePart $part, $type, $name, $header) { $headerValue = null; static $map = [ 'content-type' => 'getContentType', 'content-disposition' => 'getContentDisposition', 'content-transfer-encoding' => 'getContentTransferEncoding', 'content-id' => 'getContentId' ]; $lower = strtolower($name); if (isset($map[$lower])) { $headerValue = call_user_func([$part, $map[$lower]]); } elseif (!($part instanceof MimePart)) { return ($type === static::FILTER_INCLUDE); } else { $headerValue = $part->getHeaderValue($name); } return (($type === static::FILTER_EXCLUDE && strcasecmp($headerValue, $header) === 0) || ($type === static::FILTER_INCLUDE && strcasecmp($headerValue, $header) !== 0)); } /** * Returns true if the passed MessagePart fails the filter's header filter * settings. * * @param MessagePart $part * @return boolean */ private function failsHeaderPartFilter(MessagePart $part) { foreach ($this->headers as $type => $values) { foreach ($values as $name => $header) { if ($this->failsHeaderFor($part, $type, $name, $header)) { return true; } } } return false; } /** * Determines if the passed MessagePart should be filtered out or not. * If the MessagePart passes all filter tests, true is returned. Otherwise * false is returned. * * @param MessagePart $part * @return boolean */ public function filter(MessagePart $part) { return !($this->failsMultiPartFilter($part) || $this->failsTextPartFilter($part) || $this->failsSignedPartFilter($part) || $this->failsHeaderPartFilter($part)); } } mail-mime-parser/src/Message/Part/MessagePart.php 0000644 00000041452 14736103045 0015723 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Message\Part; use GuzzleHttp\Psr7; use GuzzleHttp\Psr7\StreamWrapper; use Psr\Http\Message\StreamInterface; use ZBateson\MailMimeParser\MailMimeParser; use ZBateson\MailMimeParser\Stream\StreamFactory; /** * Represents a single part of a message. * * A MessagePart object may have any number of child parts, or may be a child * itself with its own parent or parents. * * @author Zaahid Bateson */ abstract class MessagePart { /** * @var PartStreamFilterManager manages attached filters to $contentHandle */ protected $partStreamFilterManager; /** * @var StreamFactory for creating MessagePartStream objects */ protected $streamFactory; /** * @var ParentPart parent part */ protected $parent; /** * @var StreamInterface a Psr7 stream containing this part's headers, * content and children */ protected $stream; /** * @var StreamInterface a Psr7 stream containing this part's content */ protected $contentStream; /** * @var string can be used to set an override for content's charset in cases * where a user knows the charset on the content is not what it claims * to be. */ protected $charsetOverride; /** * @var boolean set to true when a user attaches a stream manually, it's * assumed to already be decoded or to have relevant transfer encoding * decorators attached already. */ protected $ignoreTransferEncoding; /** * Constructor * * @param PartStreamFilterManager $partStreamFilterManager * @param StreamFactory $streamFactory * @param StreamInterface $stream * @param StreamInterface $contentStream */ public function __construct( PartStreamFilterManager $partStreamFilterManager, StreamFactory $streamFactory, StreamInterface $stream = null, StreamInterface $contentStream = null ) { $this->partStreamFilterManager = $partStreamFilterManager; $this->streamFactory = $streamFactory; $this->stream = $stream; $this->contentStream = $contentStream; if ($contentStream !== null) { $partStreamFilterManager->setStream( $contentStream ); } } /** * Overridden to close streams. */ public function __destruct() { if ($this->stream !== null) { $this->stream->close(); } if ($this->contentStream !== null) { $this->contentStream->close(); } } /** * Called when operations change the content of the MessagePart. * * The function causes calls to getStream() to return a dynamic * MessagePartStream instead of the read stream for this MessagePart and all * parent MessageParts. */ protected function onChange() { $this->markAsChanged(); if ($this->parent !== null) { $this->parent->onChange(); } } /** * Marks the part as changed, forcing the part to be rewritten when saved. * * Normal operations to a MessagePart automatically mark the part as * changed and markAsChanged() doesn't need to be called in those cases. * * The function can be called to indicate an external change that requires * rewriting this part, for instance changing a message from a non-mime * message to a mime one, would require rewriting non-mime children to * insure suitable headers are written. * * Internally, the function discards the part's stream, forcing a stream to * be created when calling getStream(). */ public function markAsChanged() { // the stream is not closed because $this->contentStream may still be // attached to it. GuzzleHttp will clean it up when destroyed. $this->stream = null; } /** * Returns true if there's a content stream associated with the part. * * @return boolean */ public function hasContent() { return ($this->contentStream !== null); } /** * Returns true if this part's mime type is text/plain, text/html or has a * text/* and has a defined 'charset' attribute. * * @return bool */ public abstract function isTextPart(); /** * Returns the mime type of the content. * * @return string */ public abstract function getContentType(); /** * Returns the charset of the content, or null if not applicable/defined. * * @return string */ public abstract function getCharset(); /** * Returns the content's disposition. * * @return string */ public abstract function getContentDisposition(); /** * Returns the content-transfer-encoding used for this part. * * @return string */ public abstract function getContentTransferEncoding(); /** * Returns a filename for the part if one is defined, or null otherwise. * * @return string */ public function getFilename() { return null; } /** * Returns true if the current part is a mime part. * * @return bool */ public abstract function isMime(); /** * Returns the Content ID of the part, or null if not defined. * * @return string|null */ public abstract function getContentId(); /** * Returns a resource handle containing this part, including any headers for * a MimePart, its content, and all its children. * * @return resource the resource handle */ public function getResourceHandle() { return StreamWrapper::getResource($this->getStream()); } /** * Returns a Psr7 StreamInterface containing this part, including any * headers for a MimePart, its content, and all its children. * * @return StreamInterface the resource handle */ public function getStream() { if ($this->stream === null) { return $this->streamFactory->newMessagePartStream($this); } $this->stream->rewind(); return $this->stream; } /** * Overrides the default character set used for reading content from content * streams in cases where a user knows the source charset is not what is * specified. * * If set, the returned value from MessagePart::getCharset is ignored. * * Note that setting an override on a Message and calling getTextStream, * getTextContent, getHtmlStream or getHtmlContent will not be applied to * those sub-parts, unless the text/html part is the Message itself. * Instead, Message:getTextPart() should be called, and setCharsetOverride * called on the returned MessagePart. * * @param string $charsetOverride * @param boolean $onlyIfNoCharset if true, $charsetOverride is used only if * getCharset returns null. */ public function setCharsetOverride($charsetOverride, $onlyIfNoCharset = false) { if (!$onlyIfNoCharset || $this->getCharset() === null) { $this->charsetOverride = $charsetOverride; } } /** * Returns a resource handle for the content's stream, or null if the part * doesn't have a content stream. * * The method wraps a call to {@see MessagePart::getContentStream()} and * returns a resource handle for the returned Stream. * * Note: this method should *not* be used and has been deprecated. Instead, * use Psr7 streams with getContentStream. Multibyte chars will not be read * correctly with fread. * * @param string $charset * @deprecated since version 1.2.1 * @return resource|null */ public function getContentResourceHandle($charset = MailMimeParser::DEFAULT_CHARSET) { trigger_error("getContentResourceHandle is deprecated since version 1.2.1", E_USER_DEPRECATED); $stream = $this->getContentStream($charset); if ($stream !== null) { return StreamWrapper::getResource($stream); } return null; } /** * Returns the StreamInterface for the part's content or null if the part * doesn't have a content section. * * The library automatically handles decoding and charset conversion (to the * target passed $charset) based on the part's transfer encoding as returned * by {@see MessagePart::getContentTransferEncoding()} and the part's * charset as returned by {@see MessagePart::getCharset()}. The returned * stream is ready to be read from directly. * * Note that the returned Stream is a shared object. If called multiple * time with the same $charset, and the value of the part's * Content-Transfer-Encoding header not having changed, the stream will be * rewound. This would affect other existing variables referencing the * stream, for example: * * ``` * // assuming $part is a part containing the following * // string for its content: '12345678' * $stream = $part->getContentStream(); * $someChars = $part->read(4); * * $stream2 = $part->getContentStream(); * $moreChars = $part->read(4); * echo ($someChars === $moreChars); //1 * ``` * * In this case the Stream was rewound, and $stream's second call to read 4 * bytes reads the same first 4. * * @param string $charset * @return StreamInterface */ public function getContentStream($charset = MailMimeParser::DEFAULT_CHARSET) { if ($this->hasContent()) { $tr = ($this->ignoreTransferEncoding) ? '' : $this->getContentTransferEncoding(); $ch = ($this->charsetOverride !== null) ? $this->charsetOverride : $this->getCharset(); return $this->partStreamFilterManager->getContentStream( $tr, $ch, $charset ); } return null; } /** * Returns the raw data stream for the current part, if it exists, or null * if there's no content associated with the stream. * * This is basically the same as calling * {@see MessagePart::getContentStream()}, except no automatic charset * conversion is done. Note that for non-text streams, this doesn't have an * effect, as charset conversion is not performed in that case, and is * useful only when: * * - The charset defined is not correct, and the conversion produces errors; * or * - You'd like to read the raw contents without conversion, for instance to * save it to file or allow a user to download it as-is (in a download * link for example). * * @param string $charset * @return StreamInterface */ public function getBinaryContentStream() { if ($this->hasContent()) { $tr = ($this->ignoreTransferEncoding) ? '' : $this->getContentTransferEncoding(); return $this->partStreamFilterManager->getBinaryStream($tr); } return null; } /** * Returns a resource handle for the content's raw data stream, or null if * the part doesn't have a content stream. * * The method wraps a call to {@see MessagePart::getBinaryContentStream()} * and returns a resource handle for the returned Stream. * * @return resource|null */ public function getBinaryContentResourceHandle() { $stream = $this->getBinaryContentStream(); if ($stream !== null) { return StreamWrapper::getResource($stream); } return null; } /** * Saves the binary content of the stream to the passed file, resource or * stream. * * Note that charset conversion is not performed in this case, and the * contents of the part are saved in their binary format as transmitted (but * after any content-transfer decoding is performed). {@see * MessagePart::getBinaryContentStream()} for a more detailed description of * the stream. * * If the passed parameter is a string, it's assumed to be a filename to * write to. The file is opened in 'w+' mode, and closed before returning. * * When passing a resource or Psr7 Stream, the resource is not closed, nor * rewound. * * @param string|resource|Stream $filenameResourceOrStream */ public function saveContent($filenameResourceOrStream) { $resourceOrStream = $filenameResourceOrStream; if (is_string($filenameResourceOrStream)) { $resourceOrStream = fopen($filenameResourceOrStream, 'w+'); } $stream = Psr7\stream_for($resourceOrStream); Psr7\copy_to_stream($this->getBinaryContentStream(), $stream); if (!is_string($filenameResourceOrStream) && !($filenameResourceOrStream instanceof StreamInterface)) { // only detach if it wasn't a string or StreamInterface, so the // fopen call can be properly closed if it was $stream->detach(); } } /** * Shortcut to reading stream content and assigning it to a string. Returns * null if the part doesn't have a content stream. * * The returned string is encoded to the passed $charset character encoding, * defaulting to UTF-8. * * @see MessagePart::getContentStream() * @param string $charset * @return string */ public function getContent($charset = MailMimeParser::DEFAULT_CHARSET) { $stream = $this->getContentStream($charset); if ($stream !== null) { return $stream->getContents(); } return null; } /** * Returns this part's parent. * * @return \ZBateson\MailMimeParser\Message\Part\MimePart */ public function getParent() { return $this->parent; } /** * Attaches the stream or resource handle for the part's content. The * stream is closed when another stream is attached, or the MimePart is * destroyed. * * @param StreamInterface $stream * @param string $streamCharset */ public function attachContentStream(StreamInterface $stream, $streamCharset = MailMimeParser::DEFAULT_CHARSET) { if ($this->contentStream !== null && $this->contentStream !== $stream) { $this->contentStream->close(); } $this->contentStream = $stream; $ch = ($this->charsetOverride !== null) ? $this->charsetOverride : $this->getCharset(); if ($ch !== null && $streamCharset !== $ch) { $this->charsetOverride = $streamCharset; } $this->ignoreTransferEncoding = true; $this->partStreamFilterManager->setStream($stream); $this->onChange(); } /** * Detaches and closes the content stream. */ public function detachContentStream() { $this->contentStream = null; $this->partStreamFilterManager->setStream(null); $this->onChange(); } /** * Sets the content of the part to the passed resource. * * @param string|resource|StreamInterface $resource * @param string $charset */ public function setContent($resource, $charset = MailMimeParser::DEFAULT_CHARSET) { $stream = Psr7\stream_for($resource); $this->attachContentStream($stream, $charset); // this->onChange called in attachContentStream } /** * Saves the message/part to the passed file, resource, or stream. * * If the passed parameter is a string, it's assumed to be a filename to * write to. The file is opened in 'w+' mode, and closed before returning. * * When passing a resource or Psr7 Stream, the resource is not closed, nor * rewound. * * @param string|resource|StreamInterface $filenameResourceOrStream */ public function save($filenameResourceOrStream) { $resourceOrStream = $filenameResourceOrStream; if (is_string($filenameResourceOrStream)) { $resourceOrStream = fopen($filenameResourceOrStream, 'w+'); } $partStream = $this->getStream(); $partStream->rewind(); $stream = Psr7\stream_for($resourceOrStream); Psr7\copy_to_stream($partStream, $stream); if (!is_string($filenameResourceOrStream) && !($filenameResourceOrStream instanceof StreamInterface)) { // only detach if it wasn't a string or StreamInterface, so the // fopen call can be properly closed if it was $stream->detach(); } } /** * Returns the message/part as a string. * * Convenience method for calling getStream()->getContents(). * * @return string */ public function __toString() { return $this->getStream()->getContents(); } } mail-mime-parser/src/Message/Part/Factory/UUEncodedPartFactory.php 0000644 00000002536 14736103045 0021111 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Message\Part\Factory; use Psr\Http\Message\StreamInterface; use ZBateson\MailMimeParser\Message\Part\UUEncodedPart; use ZBateson\MailMimeParser\Message\Part\PartBuilder; /** * Responsible for creating UUEncodedPart instances. * * @author Zaahid Bateson */ class UUEncodedPartFactory extends MessagePartFactory { /** * Constructs a new UUEncodedPart object and returns it * * @param PartBuilder $partBuilder * @param StreamInterface $messageStream * @return \ZBateson\MailMimeParser\Message\Part\NonMimePart */ public function newInstance(PartBuilder $partBuilder, StreamInterface $messageStream = null) { $partStream = null; $contentStream = null; if ($messageStream !== null) { $partStream = $this->streamFactory->getLimitedPartStream($messageStream, $partBuilder); $contentStream = $this->streamFactory->getLimitedContentStream($messageStream, $partBuilder); } return new UUEncodedPart( $this->partStreamFilterManagerFactory->newInstance(), $this->streamFactory, $partBuilder, $partStream, $contentStream ); } } mail-mime-parser/src/Message/Part/Factory/NonMimePartFactory.php 0000644 00000002473 14736103045 0020640 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Message\Part\Factory; use Psr\Http\Message\StreamInterface; use ZBateson\MailMimeParser\Message\Part\NonMimePart; use ZBateson\MailMimeParser\Message\Part\PartBuilder; /** * Responsible for creating NoneMimePart instances. * * @author Zaahid Bateson */ class NonMimePartFactory extends MessagePartFactory { /** * Constructs a new NonMimePart object and returns it * * @param PartBuilder $partBuilder * @param StreamInterface $messageStream * @return \ZBateson\MailMimeParser\Message\Part\NonMimePart */ public function newInstance(PartBuilder $partBuilder, StreamInterface $messageStream = null) { $partStream = null; $contentStream = null; if ($messageStream !== null) { $partStream = $this->streamFactory->getLimitedPartStream($messageStream, $partBuilder); $contentStream = $this->streamFactory->getLimitedContentStream($messageStream, $partBuilder); } return new NonMimePart( $this->partStreamFilterManagerFactory->newInstance(), $this->streamFactory, $partStream, $contentStream ); } } mail-mime-parser/src/Message/Part/Factory/PartBuilderFactory.php 0000644 00000002656 14736103045 0020667 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Message\Part\Factory; use ZBateson\MailMimeParser\Header\HeaderFactory; use ZBateson\MailMimeParser\Message\Part\PartBuilder; /** * Responsible for creating PartBuilder instances. * * The PartBuilder instance must be constructed with a MessagePartFactory * instance to construct a MessagePart sub-class after parsing a message into * PartBuilder instances. * * @author Zaahid Bateson */ class PartBuilderFactory { /** * @var \ZBateson\MailMimeParser\Header\HeaderFactory the HeaderFactory * instance */ protected $headerFactory; /** * Initializes dependencies * * @param HeaderFactory $headerFactory */ public function __construct(HeaderFactory $headerFactory) { $this->headerFactory = $headerFactory; } /** * Constructs a new PartBuilder object and returns it * * @param \ZBateson\MailMimeParser\Message\Part\Factory\MessagePartFactory * $messagePartFactory * @return \ZBateson\MailMimeParser\Message\Part\PartBuilder */ public function newPartBuilder(MessagePartFactory $messagePartFactory) { return new PartBuilder( $messagePartFactory, $this->headerFactory->newHeaderContainer() ); } } mail-mime-parser/src/Message/Part/Factory/MessagePartFactory.php 0000644 00000006650 14736103045 0020663 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Message\Part\Factory; use ReflectionClass; use Psr\Http\Message\StreamInterface; use ZBateson\MailMimeParser\Message\Helper\MessageHelperService; use ZBateson\MailMimeParser\Message\PartFilterFactory; use ZBateson\MailMimeParser\Message\Part\PartBuilder; use ZBateson\MailMimeParser\Stream\StreamFactory; /** * Abstract factory for subclasses of MessagePart. * * @author Zaahid Bateson */ abstract class MessagePartFactory { /** * @var PartStreamFilterManagerFactory responsible for creating * PartStreamFilterManager instances */ protected $partStreamFilterManagerFactory; /** * @var StreamFactory the StreamFactory instance */ protected $streamFactory; /** * @var MessagePartFactory[] cached instances of MessagePartFactory * sub-classes */ private static $instances = null; /** * Initializes class dependencies. * * @param StreamFactory $streamFactory * @param PartStreamFilterManagerFactory $psf */ public function __construct( StreamFactory $streamFactory, PartStreamFilterManagerFactory $psf ) { $this->streamFactory = $streamFactory; $this->partStreamFilterManagerFactory = $psf; } /** * Sets a cached singleton instance. * * @param MessagePartFactory $instance */ protected static function setCachedInstance(MessagePartFactory $instance) { if (self::$instances === null) { self::$instances = []; } $class = get_called_class(); self::$instances[$class] = $instance; } /** * Returns a cached singleton instance if one exists, or null if one hasn't * been created yet. * * @return MessagePartFactory */ protected static function getCachedInstance() { $class = get_called_class(); if (self::$instances === null || !isset(self::$instances[$class])) { return null; } return self::$instances[$class]; } /** * Returns the singleton instance for the class. * * @param StreamFactory $sdf * @param PartStreamFilterManagerFactory $psf * @param PartFilterFactory $pf * @param MessageHelperService $mhs * @return MessagePartFactory */ public static function getInstance( StreamFactory $sdf, PartStreamFilterManagerFactory $psf, PartFilterFactory $pf = null, MessageHelperService $mhs = null ) { $instance = static::getCachedInstance(); if ($instance === null) { $ref = new ReflectionClass(get_called_class()); $n = $ref->getConstructor()->getNumberOfParameters(); $args = []; for ($i = 0; $i < $n; ++$i) { $args[] = func_get_arg($i); } $instance = $ref->newInstanceArgs($args); static::setCachedInstance($instance); } return $instance; } /** * Constructs a new MessagePart object and returns it * * @param PartBuilder $partBuilder * @param StreamInterface $messageStream * @return \ZBateson\MailMimeParser\Message\Part\MessagePart */ public abstract function newInstance(PartBuilder $partBuilder, StreamInterface $messageStream = null); } mail-mime-parser/src/Message/Part/Factory/PartFactoryService.php 0000644 00000006173 14736103045 0020677 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Message\Part\Factory; use ZBateson\MailMimeParser\Stream\StreamFactory; use ZBateson\MailMimeParser\Message\Helper\MessageHelperService; use ZBateson\MailMimeParser\Message\MessageFactory; use ZBateson\MailMimeParser\Message\PartFilterFactory; /** * Responsible for creating singleton instances of MessagePartFactory and its * subclasses. * * @author Zaahid Bateson */ class PartFactoryService { /** * @var PartFilterFactory the PartFilterFactory instance */ protected $partFilterFactory; /** * @var PartStreamFilterManagerFactory the PartStreamFilterManagerFactory * instance */ protected $partStreamFilterManagerFactory; /** * @var StreamFactory the StreamFactory instance */ protected $streamFactory; /** * @var MessageHelperService the MessageHelperService instance */ protected $messageHelperService; /** * @param PartFilterFactory $partFilterFactory * @param StreamFactory $streamFactory * @param PartStreamFilterManagerFactory $partStreamFilterManagerFactory * @param MessageHelperService $messageHelperService */ public function __construct( PartFilterFactory $partFilterFactory, StreamFactory $streamFactory, PartStreamFilterManagerFactory $partStreamFilterManagerFactory, MessageHelperService $messageHelperService ) { $this->partFilterFactory = $partFilterFactory; $this->streamFactory = $streamFactory; $this->partStreamFilterManagerFactory = $partStreamFilterManagerFactory; $this->messageHelperService = $messageHelperService; } /** * Returns the MessageFactory singleton instance. * * @return MessageFactory */ public function getMessageFactory() { return MessageFactory::getInstance( $this->streamFactory, $this->partStreamFilterManagerFactory, $this->partFilterFactory, $this->messageHelperService ); } /** * Returns the MimePartFactory singleton instance. * * @return MimePartFactory */ public function getMimePartFactory() { return MimePartFactory::getInstance( $this->streamFactory, $this->partStreamFilterManagerFactory, $this->partFilterFactory ); } /** * Returns the NonMimePartFactory singleton instance. * * @return NonMimePartFactory */ public function getNonMimePartFactory() { return NonMimePartFactory::getInstance( $this->streamFactory, $this->partStreamFilterManagerFactory ); } /** * Returns the UUEncodedPartFactory singleton instance. * * @return UUEncodedPartFactory */ public function getUUEncodedPartFactory() { return UUEncodedPartFactory::getInstance( $this->streamFactory, $this->partStreamFilterManagerFactory ); } } mail-mime-parser/src/Message/Part/Factory/MimePartFactory.php 0000644 00000003750 14736103045 0020164 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Message\Part\Factory; use Psr\Http\Message\StreamInterface; use ZBateson\MailMimeParser\Stream\StreamFactory; use ZBateson\MailMimeParser\Message\PartFilterFactory; use ZBateson\MailMimeParser\Message\Part\MimePart; use ZBateson\MailMimeParser\Message\Part\PartBuilder; /** * Responsible for creating MimePart instances. * * @author Zaahid Bateson */ class MimePartFactory extends MessagePartFactory { /** * @var PartFilterFactory an instance used for creating MimePart objects */ protected $partFilterFactory; /** * Initializes dependencies. * * @param StreamFactory $sdf * @param PartStreamFilterManagerFactory $psf * @param PartFilterFactory $pf */ public function __construct( StreamFactory $sdf, PartStreamFilterManagerFactory $psf, PartFilterFactory $pf ) { parent::__construct($sdf, $psf); $this->partFilterFactory = $pf; } /** * Constructs a new MimePart object and returns it * * @param PartBuilder $partBuilder * @param StreamInterface $messageStream * @return \ZBateson\MailMimeParser\Message\Part\MimePart */ public function newInstance(PartBuilder $partBuilder, StreamInterface $messageStream = null) { $partStream = null; $contentStream = null; if ($messageStream !== null) { $partStream = $this->streamFactory->getLimitedPartStream($messageStream, $partBuilder); $contentStream = $this->streamFactory->getLimitedContentStream($messageStream, $partBuilder); } return new MimePart( $this->partStreamFilterManagerFactory->newInstance(), $this->streamFactory, $this->partFilterFactory, $partBuilder, $partStream, $contentStream ); } } mail-mime-parser/src/Message/Part/Factory/PartStreamFilterManagerFactory.php 0000644 00000002137 14736103045 0023167 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Message\Part\Factory; use ZBateson\MailMimeParser\Stream\StreamFactory; use ZBateson\MailMimeParser\Message\Part\PartStreamFilterManager; /** * Responsible for creating PartStreamFilterManager instances. * * @author Zaahid Bateson */ class PartStreamFilterManagerFactory { /** * @var StreamFactory the StreamFactory needed to * initialize a new PartStreamFilterManager. */ protected $streamFactory; /** * Initializes dependencies * * @param StreamFactory $streamFactory */ public function __construct(StreamFactory $streamFactory) { $this->streamFactory = $streamFactory; } /** * Constructs a new PartStreamFilterManager object and returns it. * * @return \ZBateson\MailMimeParser\Message\Part\PartStreamFilterManager */ public function newInstance() { return new PartStreamFilterManager($this->streamFactory); } } mail-mime-parser/src/Message/Part/ParentHeaderPart.php 0000644 00000017134 14736103045 0016701 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Message\Part; use Psr\Http\Message\StreamInterface; use ZBateson\MailMimeParser\Header\ParameterHeader; use ZBateson\MailMimeParser\Stream\StreamFactory; use ZBateson\MailMimeParser\Message\PartFilterFactory; use ZBateson\MailMimeParser\Header\HeaderContainer; /** * A parent part containing headers. * * @author Zaahid Bateson */ abstract class ParentHeaderPart extends ParentPart { /** * @var HeaderContainer Contains headers for this part. */ protected $headerContainer; /** * Constructor * * @param PartStreamFilterManager $partStreamFilterManager * @param StreamFactory $streamFactory * @param PartFilterFactory $partFilterFactory * @param PartBuilder $partBuilder * @param StreamInterface $stream * @param StreamInterface $contentStream */ public function __construct( PartStreamFilterManager $partStreamFilterManager, StreamFactory $streamFactory, PartFilterFactory $partFilterFactory, PartBuilder $partBuilder, StreamInterface $stream = null, StreamInterface $contentStream = null ) { parent::__construct( $partStreamFilterManager, $streamFactory, $partFilterFactory, $partBuilder, $stream, $contentStream ); $this->headerContainer = $partBuilder->getHeaderContainer(); } /** * Returns the AbstractHeader object for the header with the given $name. * If the optional $offset is passed, and multiple headers exist with the * same name, the one at the passed offset is returned. * * Note that mime headers aren't case sensitive. * * @param string $name * @param int $offset * @return \ZBateson\MailMimeParser\Header\AbstractHeader| * \ZBateson\MailMimeParser\Header\AddressHeader| * \ZBateson\MailMimeParser\Header\DateHeader| * \ZBateson\MailMimeParser\Header\GenericHeader| * \ZBateson\MailMimeParser\Header\IdHeader| * \ZBateson\MailMimeParser\Header\ParameterHeader| * \ZBateson\MailMimeParser\Header\ReceivedHeader| * \ZBateson\MailMimeParser\Header\SubjectHeader */ public function getHeader($name, $offset = 0) { return $this->headerContainer->get($name, $offset); } /** * Returns an array of headers in this part. * * @return \ZBateson\MailMimeParser\Header\AbstractHeader[] */ public function getAllHeaders() { return $this->headerContainer->getHeaderObjects(); } /** * Returns an array of headers that match the passed name. * * @param string $name * @return \ZBateson\MailMimeParser\Header\AbstractHeader[] */ public function getAllHeadersByName($name) { return $this->headerContainer->getAll($name); } /** * Returns an array of all headers for the mime part with the first element * holding the name, and the second its value. * * @return string[][] */ public function getRawHeaders() { return $this->headerContainer->getHeaders(); } /** * Returns an iterator to the headers in this collection. Each returned * element is an array with its first element set to the header's name, and * the second to its raw value: * * [ 'Header-Name', 'Header Value' ] * * @return \Iterator */ public function getRawHeaderIterator() { return $this->headerContainer->getIterator(); } /** * Returns the string value for the header with the given $name. * * Note that mime headers aren't case sensitive. * * @param string $name * @param string $defaultValue * @return string */ public function getHeaderValue($name, $defaultValue = null) { $header = $this->getHeader($name); if ($header !== null) { return $header->getValue(); } return $defaultValue; } /** * Returns a parameter of the header $header, given the parameter named * $param. * * Only headers of type * \ZBateson\MailMimeParser\Header\ParameterHeader have parameters. * Content-Type and Content-Disposition are examples of headers with * parameters. "Charset" is a common parameter of Content-Type. * * @param string $header * @param string $param * @param string $defaultValue * @return string */ public function getHeaderParameter($header, $param, $defaultValue = null) { $obj = $this->getHeader($header); if ($obj && $obj instanceof ParameterHeader) { return $obj->getValueFor($param, $defaultValue); } return $defaultValue; } /** * Adds a header with the given $name and $value. An optional $offset may * be passed, which will overwrite a header if one exists with the given * name and offset. Otherwise a new header is added. The passed $offset may * be ignored in that case if it doesn't represent the next insert position * for the header with the passed name... instead it would be 'pushed' on at * the next position. * * ```php * $part = $myParentHeaderPart; * $part->setRawHeader('New-Header', 'value'); * echo $part->getHeaderValue('New-Header'); // 'value' * * $part->setRawHeader('New-Header', 'second', 4); * echo is_null($part->getHeader('New-Header', 4)); // '1' (true) * echo $part->getHeader('New-Header', 1) * ->getValue(); // 'second' * ``` * * A new \ZBateson\MailMimeParser\Header\AbstractHeader object is created * from the passed value. No processing on the passed string is performed, * and so the passed name and value must be formatted correctly according to * related RFCs. In particular, be careful to encode non-ascii data, to * keep lines under 998 characters in length, and to follow any special * formatting required for the type of header. * * @param string $name * @param string $value * @param int $offset */ public function setRawHeader($name, $value, $offset = 0) { $this->headerContainer->set($name, $value, $offset); $this->onChange(); } /** * Adds a header with the given $name and $value. * * Note: If a header with the passed name already exists, a new header is * created with the same name. This should only be used when that is * intentional - in most cases setRawHeader should be called. * * Creates a new \ZBateson\MailMimeParser\Header\AbstractHeader object and * registers it as a header. * * @param string $name * @param string $value */ public function addRawHeader($name, $value) { $this->headerContainer->add($name, $value); $this->onChange(); } /** * Removes all headers from this part with the passed name. * * @param string $name */ public function removeHeader($name) { $this->headerContainer->removeAll($name); $this->onChange(); } /** * Removes a single header with the passed name (in cases where more than * one may exist, and others should be preserved). * * @param string $name */ public function removeSingleHeader($name, $offset = 0) { $this->headerContainer->remove($name, $offset); $this->onChange(); } } mail-mime-parser/src/Message/Part/PartStreamFilterManager.php 0000644 00000017260 14736103045 0020233 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Message\Part; use Psr\Http\Message\StreamInterface; use GuzzleHttp\Psr7\CachingStream; use ZBateson\MailMimeParser\Stream\StreamFactory; /** * Manages attached stream filters for a MessagePart's content resource handle. * * The attached stream filters are: * o Content-Transfer-Encoding filter to manage decoding from a supported * encoding: quoted-printable, base64 and x-uuencode. * o Charset conversion filter to convert to UTF-8 * * @author Zaahid Bateson */ class PartStreamFilterManager { /** * @var StreamInterface the underlying content stream without filters * applied */ protected $stream; /** * @var StreamInterface the content stream after attaching transfer encoding * streams to $stream. */ protected $decodedStream; /** * @var StreamInterface the content stream after attaching charset streams * to $binaryStream */ protected $charsetStream; /** * @var array map of the active encoding filter on the current handle. */ private $encoding = [ 'type' => null, 'filter' => null ]; /** * @var array map of the active charset filter on the current handle. */ private $charset = [ 'from' => null, 'to' => null, 'filter' => null ]; /** * @var StreamFactory used to apply psr7 stream decorators to the * attached StreamInterface based on encoding. */ private $streamFactory; /** * Sets up filter names used for stream_filter_append * * @param StreamFactory $streamFactory */ public function __construct(StreamFactory $streamFactory) { $this->streamFactory = $streamFactory; } /** * Sets the URL used to open the content resource handle. * * The function also closes the currently attached handle if any. * * @param StreamInterface $stream */ public function setStream(StreamInterface $stream = null) { $this->stream = $stream; $this->decodedStream = null; $this->charsetStream = null; } /** * Returns true if the attached stream filter used for decoding the content * on the current handle is different from the one passed as an argument. * * @param string $transferEncoding * @return boolean */ private function isTransferEncodingFilterChanged($transferEncoding) { return ($transferEncoding !== $this->encoding['type']); } /** * Returns true if the attached stream filter used for charset conversion on * the current handle is different from the one needed based on the passed * arguments. * * @param string $fromCharset * @param string $toCharset * @return boolean */ private function isCharsetFilterChanged($fromCharset, $toCharset) { return ($fromCharset !== $this->charset['from'] || $toCharset !== $this->charset['to']); } /** * Attaches a decoding filter to the attached content handle, for the passed * $transferEncoding. * * @param string $transferEncoding */ protected function attachTransferEncodingFilter($transferEncoding) { if ($this->decodedStream !== null) { $this->encoding['type'] = $transferEncoding; $assign = null; switch ($transferEncoding) { case 'base64': $assign = $this->streamFactory->newBase64Stream($this->decodedStream); break; case 'x-uuencode': $assign = $this->streamFactory->newUUStream($this->decodedStream); break; case 'quoted-printable': $assign = $this->streamFactory->newQuotedPrintableStream($this->decodedStream); break; } if ($assign !== null) { $this->decodedStream = new CachingStream($assign); } } } /** * Attaches a charset conversion filter to the attached content handle, for * the passed arguments. * * @param string $fromCharset the character set the content is encoded in * @param string $toCharset the target encoding to return */ protected function attachCharsetFilter($fromCharset, $toCharset) { if ($this->charsetStream !== null) { $this->charsetStream = new CachingStream($this->streamFactory->newCharsetStream( $this->charsetStream, $fromCharset, $toCharset )); $this->charset['from'] = $fromCharset; $this->charset['to'] = $toCharset; } } /** * Resets just the charset stream, and rewinds the decodedStream. */ private function resetCharsetStream() { $this->charset = [ 'from' => null, 'to' => null, 'filter' => null ]; $this->decodedStream->rewind(); $this->charsetStream = $this->decodedStream; } /** * Resets cached encoding and charset streams, and rewinds the stream. */ public function reset() { $this->encoding = [ 'type' => null, 'filter' => null ]; $this->charset = [ 'from' => null, 'to' => null, 'filter' => null ]; $this->stream->rewind(); $this->decodedStream = $this->stream; $this->charsetStream = $this->stream; } /** * Checks what transfer-encoding decoder stream and charset conversion * stream are currently attached on the underlying stream, and resets them * if the requested arguments differ from the currently assigned ones. * * @param string $transferEncoding * @param string $fromCharset the character set the content is encoded in * @param string $toCharset the target encoding to return * @return StreamInterface */ public function getContentStream($transferEncoding, $fromCharset, $toCharset) { if ($this->stream === null) { return null; } if (empty($fromCharset) || empty($toCharset)) { return $this->getBinaryStream($transferEncoding); } if ($this->charsetStream === null || $this->isTransferEncodingFilterChanged($transferEncoding) || $this->isCharsetFilterChanged($fromCharset, $toCharset)) { if ($this->charsetStream === null || $this->isTransferEncodingFilterChanged($transferEncoding)) { $this->reset(); $this->attachTransferEncodingFilter($transferEncoding); } $this->resetCharsetStream(); $this->attachCharsetFilter($fromCharset, $toCharset); } $this->charsetStream->rewind(); return $this->charsetStream; } /** * Checks what transfer-encoding decoder stream is attached on the * underlying stream, and resets it if the requested arguments differ. * * @param string $transferEncoding * @return StreamInterface */ public function getBinaryStream($transferEncoding) { if ($this->stream === null) { return null; } if ($this->decodedStream === null || $this->isTransferEncodingFilterChanged($transferEncoding)) { $this->reset(); $this->attachTransferEncodingFilter($transferEncoding); } $this->decodedStream->rewind(); return $this->decodedStream; } } mail-mime-parser/src/Message/Part/PartBuilder.php 0000644 00000026757 14736103045 0015740 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Message\Part; use Psr\Http\Message\StreamInterface; use ZBateson\MailMimeParser\Header\HeaderContainer; use ZBateson\MailMimeParser\Message\Part\Factory\MessagePartFactory; /** * Used by MessageParser to keep information about a parsed message as an * intermediary before creating a Message object and its MessagePart children. * * @author Zaahid Bateson */ class PartBuilder { /** * @var int The offset read start position for this part (beginning of * headers) in the message's stream. */ private $streamPartStartPos = 0; /** * @var int The offset read end position for this part. If the part is a * multipart mime part, the end position is after all of this parts * children. */ private $streamPartEndPos = 0; /** * @var int The offset read start position in the message's stream for the * beginning of this part's content (body). */ private $streamContentStartPos = 0; /** * @var int The offset read end position in the message's stream for the * end of this part's content (body). */ private $streamContentEndPos = 0; /** * @var MessagePartFactory the factory * needed for creating the Message or MessagePart for the parsed part. */ private $messagePartFactory; /** * @var boolean set to true once the end boundary of the currently-parsed * part is found. */ private $endBoundaryFound = false; /** * @var boolean set to true once a boundary belonging to this parent's part * is found. */ private $parentBoundaryFound = false; /** * @var boolean|null|string false if not queried for in the content-type * header of this part, null if the current part does not have a * boundary, or the value of the boundary parameter of the content-type * header if the part contains one. */ private $mimeBoundary = false; /** * @var HeaderContainer a container for found and parsed headers. */ private $headerContainer; /** * @var PartBuilder[] an array of children found below this part for a mime * email */ private $children = []; /** * @var PartBuilder the parent part. */ private $parent = null; /** * @var string[] key => value pairs of properties passed on to the * $messagePartFactory when constructing the Message and its children. */ private $properties = []; /** * Sets up class dependencies. * * @param MessagePartFactory $mpf * @param HeaderContainer $headerContainer */ public function __construct( MessagePartFactory $mpf, HeaderContainer $headerContainer ) { $this->messagePartFactory = $mpf; $this->headerContainer = $headerContainer; } /** * Adds a header with the given $name and $value to the headers array. * * Removes non-alphanumeric characters from $name, and sets it to lower-case * to use as a key in the private headers array. Sets the original $name * and $value as elements in the headers' array value for the calculated * key. * * @param string $name * @param string $value */ public function addHeader($name, $value) { $this->headerContainer->add($name, $value); } /** * Returns the HeaderContainer object containing parsed headers. * * @return HeaderContainer */ public function getHeaderContainer() { return $this->headerContainer; } /** * Sets the specified property denoted by $name to $value. * * @param string $name * @param mixed $value */ public function setProperty($name, $value) { $this->properties[$name] = $value; } /** * Returns the value of the property with the given $name. * * @param string $name * @return mixed */ public function getProperty($name) { if (!isset($this->properties[$name])) { return null; } return $this->properties[$name]; } /** * Registers the passed PartBuilder as a child of the current PartBuilder. * * @param \ZBateson\MailMimeParser\Message\PartBuilder $partBuilder */ public function addChild(PartBuilder $partBuilder) { $partBuilder->parent = $this; // discard parts added after the end boundary if (!$this->endBoundaryFound) { $this->children[] = $partBuilder; } } /** * Returns all children PartBuilder objects. * * @return \ZBateson\MailMimeParser\Message\PartBuilder[] */ public function getChildren() { return $this->children; } /** * Returns this PartBuilder's parent. * * @return PartBuilder */ public function getParent() { return $this->parent; } /** * Returns true if either a Content-Type or Mime-Version header are defined * in this PartBuilder's headers. * * @return boolean */ public function isMime() { return ($this->headerContainer->exists('Content-Type') || $this->headerContainer->exists('Mime-Version')); } /** * Returns a ParameterHeader representing the parsed Content-Type header for * this PartBuilder. * * @return \ZBateson\MailMimeParser\Header\ParameterHeader */ public function getContentType() { return $this->headerContainer->get('Content-Type'); } /** * Returns the parsed boundary parameter of the Content-Type header if set * for a multipart message part. * * @return string */ public function getMimeBoundary() { if ($this->mimeBoundary === false) { $this->mimeBoundary = null; $contentType = $this->getContentType(); if ($contentType !== null) { $this->mimeBoundary = $contentType->getValueFor('boundary'); } } return $this->mimeBoundary; } /** * Returns true if this part's content-type is multipart/* * * @return boolean */ public function isMultiPart() { $contentType = $this->getContentType(); if ($contentType !== null) { // casting to bool, preg_match returns 1 for true return (bool) (preg_match( '~multipart/.*~i', $contentType->getValue() )); } return false; } /** * Returns true if the passed $line of read input matches this PartBuilder's * mime boundary, or any of its parent's mime boundaries for a multipart * message. * * If the passed $line is the ending boundary for the current PartBuilder, * $this->isEndBoundaryFound will return true after. * * @param string $line * @return boolean */ public function setEndBoundaryFound($line) { $boundary = $this->getMimeBoundary(); if ($this->parent !== null && $this->parent->setEndBoundaryFound($line)) { $this->parentBoundaryFound = true; return true; } elseif ($boundary !== null) { if ($line === "--$boundary--") { $this->endBoundaryFound = true; return true; } elseif ($line === "--$boundary") { return true; } } return false; } /** * Returns true if MessageParser passed an input line to setEndBoundary that * matches a parent's mime boundary, and the following input belongs to a * new part under its parent. * * @return boolean */ public function isParentBoundaryFound() { return ($this->parentBoundaryFound); } /** * Called once EOF is reached while reading content. The method sets the * flag used by PartBuilder::isParentBoundaryFound to true on this part and * all parent PartBuilders. */ public function setEof() { $this->parentBoundaryFound = true; if ($this->parent !== null) { $this->parent->parentBoundaryFound = true; } } /** * Returns false if this part has a parent part in which endBoundaryFound is * set to true (i.e. this isn't a discardable part following the parent's * end boundary line). * * @return boolean */ public function canHaveHeaders() { return ($this->parent === null || !$this->parent->endBoundaryFound); } /** * Returns the offset for this part's stream within its parent stream. * * @return int */ public function getStreamPartStartOffset() { if ($this->parent) { return $this->streamPartStartPos - $this->parent->streamPartStartPos; } return $this->streamPartStartPos; } /** * Returns the length of this part's stream. * * @return int */ public function getStreamPartLength() { return $this->streamPartEndPos - $this->streamPartStartPos; } /** * Returns the offset for this part's content within its part stream. * * @return int */ public function getStreamContentStartOffset() { if ($this->parent) { return $this->streamContentStartPos - $this->parent->streamPartStartPos; } return $this->streamContentStartPos; } /** * Returns the length of this part's content stream. * * @return int */ public function getStreamContentLength() { return $this->streamContentEndPos - $this->streamContentStartPos; } /** * Sets the start position of the part in the input stream. * * @param int $streamPartStartPos */ public function setStreamPartStartPos($streamPartStartPos) { $this->streamPartStartPos = $streamPartStartPos; } /** * Sets the end position of the part in the input stream, and also calls * parent->setParentStreamPartEndPos to expand to parent parts. * * @param int $streamPartEndPos */ public function setStreamPartEndPos($streamPartEndPos) { $this->streamPartEndPos = $streamPartEndPos; if ($this->parent !== null) { $this->parent->setStreamPartEndPos($streamPartEndPos); } } /** * Sets the start position of the content in the input stream. * * @param int $streamContentStartPos */ public function setStreamContentStartPos($streamContentStartPos) { $this->streamContentStartPos = $streamContentStartPos; } /** * Sets the end position of the content and part in the input stream. * * @param int $streamContentEndPos */ public function setStreamPartAndContentEndPos($streamContentEndPos) { $this->streamContentEndPos = $streamContentEndPos; $this->setStreamPartEndPos($streamContentEndPos); } /** * Creates a MessagePart and returns it using the PartBuilder's * MessagePartFactory passed in during construction. * * @param StreamInterface $stream * @return MessagePart */ public function createMessagePart(StreamInterface $stream = null) { return $this->messagePartFactory->newInstance( $this, $stream ); } } mail-mime-parser/src/Message/Part/UUEncodedPart.php 0000644 00000006762 14736103045 0016157 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Message\Part; use Psr\Http\Message\StreamInterface; use ZBateson\MailMimeParser\Stream\StreamFactory; /** * A specialized NonMimePart representing a uuencoded part. * * This represents part of a message that is not a mime message. A multi-part * mime message may have a part with a Content-Transfer-Encoding of x-uuencode * but that would be represented by a normal MimePart. * * UUEncodedPart extends NonMimePart to return a Content-Transfer-Encoding of * x-uuencode, a Content-Type of application-octet-stream, and a * Content-Disposition of 'attachment'. It also expects a mode and filename to * initialize it, and adds 'filename' parts to the Content-Disposition and * 'name' to Content-Type. * * @author Zaahid Bateson */ class UUEncodedPart extends NonMimePart { /** * @var int the unix file permission */ protected $mode = null; /** * @var string the name of the file in the uuencoding 'header'. */ protected $filename = null; /** * Constructor * * @param PartStreamFilterManager $partStreamFilterManager * @param StreamFactory $streamFactory * @param PartBuilder $partBuilder * @param StreamInterface $stream * @param StreamInterface $contentStream */ public function __construct( PartStreamFilterManager $partStreamFilterManager, StreamFactory $streamFactory, PartBuilder $partBuilder, StreamInterface $stream = null, StreamInterface $contentStream = null ) { parent::__construct($partStreamFilterManager, $streamFactory, $stream, $contentStream); $this->mode = intval($partBuilder->getProperty('mode')); $this->filename = $partBuilder->getProperty('filename'); } /** * Returns the file mode included in the uuencoded header for this part. * * @return int */ public function getUnixFileMode() { return $this->mode; } /** * Returns the filename included in the uuencoded header for this part. * * @return string */ public function getFilename() { return $this->filename; } /** * Sets the unix file mode for the uuencoded header. * * @param int $mode */ public function setUnixFileMode($mode) { $this->mode = $mode; $this->onChange(); } /** * Sets the filename included in the uuencoded header. * * @param string $filename */ public function setFilename($filename) { $this->filename = $filename; $this->onChange(); } /** * Returns false. * * @return bool */ public function isTextPart() { return false; } /** * Returns text/plain * * @return string */ public function getContentType() { return 'application/octet-stream'; } /** * Returns null * * @return string */ public function getCharset() { return null; } /** * Returns 'inline'. * * @return string */ public function getContentDisposition() { return 'attachment'; } /** * Returns 'x-uuencode'. * * @return string */ public function getContentTransferEncoding() { return 'x-uuencode'; } } mail-mime-parser/src/Message/Part/ParentPart.php 0000644 00000021152 14736103045 0015563 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Message\Part; use Psr\Http\Message\StreamInterface; use ZBateson\MailMimeParser\Message\PartFilterFactory; use ZBateson\MailMimeParser\Message\PartFilter; use ZBateson\MailMimeParser\Stream\StreamFactory; /** * A MessagePart that contains children. * * @author Zaahid Bateson */ abstract class ParentPart extends MessagePart { /** * @var PartFilterFactory factory object responsible for create PartFilters */ protected $partFilterFactory; /** * @var MessagePart[] array of child parts */ protected $children = []; /** * Constructor * * @param PartStreamFilterManager $partStreamFilterManager * @param StreamFactory $streamFactory * @param PartFilterFactory $partFilterFactory * @param PartBuilder $partBuilder * @param StreamInterface $stream * @param StreamInterface $contentStream */ public function __construct( PartStreamFilterManager $partStreamFilterManager, StreamFactory $streamFactory, PartFilterFactory $partFilterFactory, PartBuilder $partBuilder, StreamInterface $stream = null, StreamInterface $contentStream = null ) { parent::__construct($partStreamFilterManager, $streamFactory, $stream, $contentStream); $this->partFilterFactory = $partFilterFactory; $pbChildren = $partBuilder->getChildren(); if (!empty($pbChildren)) { $this->children = array_map(function ($child) use ($stream) { $childPart = $child->createMessagePart($stream); $childPart->parent = $this; return $childPart; }, $pbChildren); } } /** * Returns all parts, including the current object, and all children below * it (including children of children, etc...) * * @return MessagePart[] */ protected function getAllNonFilteredParts() { $parts = [ $this ]; foreach ($this->children as $part) { if ($part instanceof MimePart) { $parts = array_merge( $parts, $part->getAllNonFilteredParts() ); } else { array_push($parts, $part); } } return $parts; } /** * Returns the part at the given 0-based index, or null if none is set. * * Note that the first part returned is the current part itself. This is * often desirable for queries with a PartFilter, e.g. looking for a * MessagePart with a specific Content-Type that may be satisfied by the * current part. * * @param int $index * @param PartFilter $filter * @return MessagePart */ public function getPart($index, PartFilter $filter = null) { $parts = $this->getAllParts($filter); if (!isset($parts[$index])) { return null; } return $parts[$index]; } /** * Returns the current part, all child parts, and child parts of all * children optionally filtering them with the provided PartFilter. * * The first part returned is always the current MimePart. This is often * desirable as it may be a valid MimePart for the provided PartFilter. * * @param PartFilter $filter an optional filter * @return MessagePart[] */ public function getAllParts(PartFilter $filter = null) { $parts = $this->getAllNonFilteredParts(); if (!empty($filter)) { return array_values(array_filter( $parts, [ $filter, 'filter' ] )); } return $parts; } /** * Returns the total number of parts in this and all children. * * Note that the current part is considered, so the minimum getPartCount is * 1 without a filter. * * @param PartFilter $filter * @return int */ public function getPartCount(PartFilter $filter = null) { return count($this->getAllParts($filter)); } /** * Returns the direct child at the given 0-based index, or null if none is * set. * * @param int $index * @param PartFilter $filter * @return MessagePart */ public function getChild($index, PartFilter $filter = null) { $parts = $this->getChildParts($filter); if (!isset($parts[$index])) { return null; } return $parts[$index]; } /** * Returns all direct child parts. * * If a PartFilter is provided, the PartFilter is applied before returning. * * @param PartFilter $filter * @return MessagePart[] */ public function getChildParts(PartFilter $filter = null) { if ($filter !== null) { return array_values(array_filter($this->children, [ $filter, 'filter' ])); } return $this->children; } /** * Returns the number of direct children under this part. * * @param PartFilter $filter * @return int */ public function getChildCount(PartFilter $filter = null) { return count($this->getChildParts($filter)); } /** * Returns the part associated with the passed mime type, at the passed * index, if it exists. * * @param string $mimeType * @param int $index * @return MessagePart|null */ public function getPartByMimeType($mimeType, $index = 0) { $partFilter = $this->partFilterFactory->newFilterFromContentType($mimeType); return $this->getPart($index, $partFilter); } /** * Returns an array of all parts associated with the passed mime type if any * exist or null otherwise. * * @param string $mimeType * @return MessagePart[] or null */ public function getAllPartsByMimeType($mimeType) { $partFilter = $this->partFilterFactory->newFilterFromContentType($mimeType); return $this->getAllParts($partFilter); } /** * Returns the number of parts matching the passed $mimeType * * @param string $mimeType * @return int */ public function getCountOfPartsByMimeType($mimeType) { $partFilter = $this->partFilterFactory->newFilterFromContentType($mimeType); return $this->getPartCount($partFilter); } /** * Registers the passed part as a child of the current part. * * If the $position parameter is non-null, adds the part at the passed * position index. * * @param MessagePart $part * @param int $position */ public function addChild(MessagePart $part, $position = null) { if ($part !== $this) { $part->parent = $this; array_splice( $this->children, ($position === null) ? count($this->children) : $position, 0, [ $part ] ); $this->onChange(); } } /** * Removes the child part from this part and returns its position or * null if it wasn't found. * * Note that if the part is not a direct child of this part, the returned * position is its index within its parent (calls removePart on its direct * parent). * * @param MessagePart $part * @return int or null if not found */ public function removePart(MessagePart $part) { $parent = $part->getParent(); if ($this !== $parent && $parent !== null) { return $parent->removePart($part); } else { $position = array_search($part, $this->children, true); if ($position !== false && is_int($position)) { array_splice($this->children, $position, 1); $this->onChange(); return $position; } } return null; } /** * Removes all parts that are matched by the passed PartFilter. * * Note: the current part will not be removed. Although the function naming * matches getAllParts, which returns the current part, it also doesn't only * remove direct children like getChildParts. Internally this function uses * getAllParts but the current part is filtered out if returned. * * @param \ZBateson\MailMimeParser\Message\PartFilter $filter */ public function removeAllParts(PartFilter $filter = null) { foreach ($this->getAllParts($filter) as $part) { if ($part === $this) { continue; } $this->removePart($part); } } } mail-mime-parser/src/Message/Part/MimePart.php 0000644 00000011647 14736103045 0015231 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Message\Part; use ZBateson\MailMimeParser\Message\PartFilter; /** * Represents a single part of a multi-part mime message. * * A MimePart object may have any number of child parts, or may be a child * itself with its own parent or parents. * * The content of the part can be read from its PartStream resource handle, * accessible via MessagePart::getContentResourceHandle. * * @author Zaahid Bateson */ class MimePart extends ParentHeaderPart { /** * Returns true if this part's mime type is multipart/* * * @return bool */ public function isMultiPart() { // casting to bool, preg_match returns 1 for true return (bool) (preg_match( '~multipart/.*~i', $this->getContentType() )); } /** * Returns a filename for the part if one is defined, or null otherwise. * * @return string */ public function getFilename() { return $this->getHeaderParameter( 'Content-Disposition', 'filename', $this->getHeaderParameter( 'Content-Type', 'name' ) ); } /** * Returns true. * * @return bool */ public function isMime() { return true; } /** * Returns true if this part's mime type is text/plain, text/html or if the * Content-Type header defines a charset. * * @return bool */ public function isTextPart() { return ($this->getCharset() !== null); } /** * Returns the lower-cased, trimmed value of the Content-Type header. * * Parses the Content-Type header, defaults to returning text/plain if not * defined. * * @param string $default pass to override the returned value when not set * @return string */ public function getContentType($default = 'text/plain') { return trim(strtolower($this->getHeaderValue('Content-Type', $default))); } /** * Returns the upper-cased charset of the Content-Type header's charset * parameter if set, ISO-8859-1 if the Content-Type is text/plain or * text/html and the charset parameter isn't set, or null otherwise. * * If the charset parameter is set to 'binary' it is ignored and considered * 'not set' (returns ISO-8859-1 for text/plain, text/html or null * otherwise). * * @return string */ public function getCharset() { $charset = $this->getHeaderParameter('Content-Type', 'charset'); if ($charset === null || strcasecmp($charset, 'binary') === 0) { $contentType = $this->getContentType(); if ($contentType === 'text/plain' || $contentType === 'text/html') { return 'ISO-8859-1'; } return null; } return trim(strtoupper($charset)); } /** * Returns the content's disposition, defaulting to 'inline' if not set. * * @param string $default pass to override the default returned disposition * when not set. * @return string */ public function getContentDisposition($default = 'inline') { return strtolower($this->getHeaderValue('Content-Disposition', $default)); } /** * Returns the content-transfer-encoding used for this part, defaulting to * '7bit' if not set. * * @param string $default pass to override the default when not set. * @return string */ public function getContentTransferEncoding($default = '7bit') { static $translated = [ 'x-uue' => 'x-uuencode', 'uue' => 'x-uuencode', 'uuencode' => 'x-uuencode' ]; $type = strtolower($this->getHeaderValue('Content-Transfer-Encoding', $default)); if (isset($translated[$type])) { return $translated[$type]; } return $type; } /** * Returns the Content ID of the part. * * In MimePart, this is merely a shortcut to calling * ``` $part->getHeaderValue('Content-ID'); ```. * * @return string|null */ public function getContentId() { return $this->getHeaderValue('Content-ID'); } /** * Convenience method to find a part by its Content-ID header. * * @param string $contentId * @return MessagePart */ public function getPartByContentId($contentId) { $sanitized = preg_replace('/^\s*<|>\s*$/', '', $contentId); $filter = $this->partFilterFactory->newFilterFromArray([ 'headers' => [ PartFilter::FILTER_INCLUDE => [ 'Content-ID' => $sanitized ] ] ]); return $this->getPart(0, $filter); } } mail-mime-parser/src/Message/Part/NonMimePart.php 0000644 00000003276 14736103045 0015703 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Message\Part; /** * Represents part of a non-mime message. The part could either be a plain text * part or a uuencoded attachment and could be extended for other pre-mime * message encoding types. * * This allows clients to handle all messages as mime messages by providing a * Content-Type header. NonMimePart returns text/plain. * * @author Zaahid Bateson */ class NonMimePart extends MessagePart { /** * Returns true. * * @return bool */ public function isTextPart() { return true; } /** * Returns text/plain * * @return string */ public function getContentType() { return 'text/plain'; } /** * Returns ISO-8859-1 * * @return string */ public function getCharset() { return 'ISO-8859-1'; } /** * Returns 'inline'. * * @return string */ public function getContentDisposition() { return 'inline'; } /** * Returns '7bit'. * * @return string */ public function getContentTransferEncoding() { return '7bit'; } /** * Returns false. * * @return bool */ public function isMime() { return false; } /** * Returns the Content ID of the part. * * NonMimeParts do not have a Content ID, and so this simply returns null. * * @return string|null */ public function getContentId() { return null; } } mail-mime-parser/src/Message/MessageParser.php 0000644 00000023357 14736103045 0015347 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Message; use Psr\Http\Message\StreamInterface; use ZBateson\MailMimeParser\Message\Part\PartBuilder; use ZBateson\MailMimeParser\Message\Part\Factory\PartBuilderFactory; use ZBateson\MailMimeParser\Message\Part\Factory\PartFactoryService; use GuzzleHttp\Psr7\StreamWrapper; /** * Parses a mail mime message into its component parts. To invoke, call * MailMimeParser::parse. * * @author Zaahid Bateson */ class MessageParser { /** * @var PartFactoryService service instance used to create MimePartFactory * objects. */ protected $partFactoryService; /** * @var PartBuilderFactory used to create PartBuilders */ protected $partBuilderFactory; /** * @var int maintains the character length of the last line separator, * typically 2 for CRLF, to keep track of the correct 'end' position * for a part because the CRLF before a boundary is considered part of * the boundary. */ private $lastLineSeparatorLength = 0; /** * Sets up the parser with its dependencies. * * @param PartFactoryService $pfs * @param PartBuilderFactory $pbf */ public function __construct( PartFactoryService $pfs, PartBuilderFactory $pbf ) { $this->partFactoryService = $pfs; $this->partBuilderFactory = $pbf; } /** * Parses the passed stream into a ZBateson\MailMimeParser\Message object * and returns it. * * @param StreamInterface $stream the stream to parse the message from * @return \ZBateson\MailMimeParser\Message */ public function parse(StreamInterface $stream) { $partBuilder = $this->read($stream); return $partBuilder->createMessagePart($stream); } /** * Ensures the header isn't empty and contains a colon separator character, * then splits it and calls $partBuilder->addHeader. * * @param string $header * @param PartBuilder $partBuilder */ private function addRawHeaderToPart($header, PartBuilder $partBuilder) { if ($header !== '' && strpos($header, ':') !== false) { $a = explode(':', $header, 2); $partBuilder->addHeader($a[0], trim($a[1])); } } /** * Reads a line of up to 4096 characters. If the line is larger than that, * the remaining characters in the line are read and discarded, and only the * first 4096 characters are returned. * * @param resource $handle * @return string */ private function readLine($handle) { $size = 4096; $ret = $line = fgets($handle, $size); while (strlen($line) === $size - 1 && substr($line, -1) !== "\n") { $line = fgets($handle, $size); } return $ret; } /** * Reads a line of 2048 characters. If the line is larger than that, the * remaining characters in the line are read and * discarded, and only the first part is returned. * * This method is identical to readLine, except it calculates the number of * characters that make up the line's new line characters (e.g. 2 for "\r\n" * or 1 for "\n"). * * @param resource $handle * @param int $lineSeparatorLength * @return string */ private function readBoundaryLine($handle, &$lineSeparatorLength = 0) { $size = 2048; $isCut = false; $line = fgets($handle, $size); while (strlen($line) === $size - 1 && substr($line, -1) !== "\n") { $line = fgets($handle, $size); $isCut = true; } $ret = rtrim($line, "\r\n"); $lineSeparatorLength = strlen($line) - strlen($ret); return ($isCut) ? '' : $ret; } /** * Reads header lines up to an empty line, adding them to the passed * $partBuilder. * * @param resource $handle the resource handle to read from * @param PartBuilder $partBuilder the current part to add headers to */ protected function readHeaders($handle, PartBuilder $partBuilder) { $header = ''; do { $line = $this->readLine($handle); if (empty($line) || $line[0] !== "\t" && $line[0] !== ' ') { $this->addRawHeaderToPart($header, $partBuilder); $header = ''; } else { $line = "\r\n" . $line; } $header .= rtrim($line, "\r\n"); } while ($header !== ''); } /** * Reads lines from the passed $handle, calling * $partBuilder->setEndBoundaryFound with the passed line until it returns * true or the stream is at EOF. * * setEndBoundaryFound returns true if the passed line matches a boundary * for the $partBuilder itself or any of its parents. * * Once a boundary is found, setStreamPartAndContentEndPos is called with * the passed $handle's read pos before the boundary and its line separator * were read. * * @param resource $handle * @param PartBuilder $partBuilder */ private function findContentBoundary($handle, PartBuilder $partBuilder) { // last separator before a boundary belongs to the boundary, and is not // part of the current part while (!feof($handle)) { $endPos = ftell($handle) - $this->lastLineSeparatorLength; $line = $this->readBoundaryLine($handle, $this->lastLineSeparatorLength); if ($line !== '' && $partBuilder->setEndBoundaryFound($line)) { $partBuilder->setStreamPartAndContentEndPos($endPos); return; } } $partBuilder->setStreamPartAndContentEndPos(ftell($handle)); $partBuilder->setEof(); } /** * Reads content for a non-mime message. If there are uuencoded attachment * parts in the message (denoted by 'begin' lines), those parts are read and * added to the passed $partBuilder as children. * * @param resource $handle * @param PartBuilder $partBuilder * @return string */ protected function readUUEncodedOrPlainTextMessage($handle, PartBuilder $partBuilder) { $partBuilder->setStreamContentStartPos(ftell($handle)); $part = $partBuilder; while (!feof($handle)) { $start = ftell($handle); $line = trim($this->readLine($handle)); if (preg_match('/^begin ([0-7]{3}) (.*)$/', $line, $matches)) { $part = $this->partBuilderFactory->newPartBuilder( $this->partFactoryService->getUUEncodedPartFactory() ); $part->setStreamPartStartPos($start); // 'begin' line is part of the content $part->setStreamContentStartPos($start); $part->setProperty('mode', $matches[1]); $part->setProperty('filename', $matches[2]); $partBuilder->addChild($part); } $part->setStreamPartAndContentEndPos(ftell($handle)); } $partBuilder->setStreamPartEndPos(ftell($handle)); } /** * Reads content for a single part of a MIME message. * * If the part being read is in turn a multipart part, readPart is called on * it recursively to read its headers and content. * * The start/end positions of the part's content are set on the passed * $partBuilder, which in turn sets the end position of the part and its * parents. * * @param resource $handle * @param PartBuilder $partBuilder */ private function readPartContent($handle, PartBuilder $partBuilder) { $partBuilder->setStreamContentStartPos(ftell($handle)); $this->findContentBoundary($handle, $partBuilder); if ($partBuilder->isMultiPart()) { while (!$partBuilder->isParentBoundaryFound()) { $child = $this->partBuilderFactory->newPartBuilder( $this->partFactoryService->getMimePartFactory() ); $partBuilder->addChild($child); $this->readPart($handle, $child); } } } /** * Reads a part and any of its children, into the passed $partBuilder, * either by calling readUUEncodedOrPlainTextMessage or readPartContent * after reading headers. * * @param resource $handle * @param PartBuilder $partBuilder */ protected function readPart($handle, PartBuilder $partBuilder) { $partBuilder->setStreamPartStartPos(ftell($handle)); if ($partBuilder->canHaveHeaders()) { $this->readHeaders($handle, $partBuilder); $this->lastLineSeparatorLength = 0; } if ($partBuilder->getParent() === null && !$partBuilder->isMime()) { $this->readUUEncodedOrPlainTextMessage($handle, $partBuilder); } else { $this->readPartContent($handle, $partBuilder); } } /** * Reads the message from the passed stream and returns a PartBuilder * representing it. * * @param StreamInterface $stream * @return PartBuilder */ protected function read(StreamInterface $stream) { $partBuilder = $this->partBuilderFactory->newPartBuilder( $this->partFactoryService->getMessageFactory() ); // the remaining parts use a resource handle for better performance... // it seems fgets does much better than Psr7\readline (not specifically // measured, but difference in running tests is big) $this->readPart(StreamWrapper::getResource($stream), $partBuilder); return $partBuilder; } } mail-mime-parser/src/Message/Helper/PrivacyHelper.php 0000644 00000017001 14736103045 0016567 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Message\Helper; use ZBateson\MailMimeParser\Message; use ZBateson\MailMimeParser\Message\Part\Factory\MimePartFactory; use ZBateson\MailMimeParser\Message\Part\Factory\PartBuilderFactory; use ZBateson\MailMimeParser\Message\Part\Factory\UUEncodedPartFactory; use ZBateson\MailMimeParser\Message\Part\ParentPart; use ZBateson\MailMimeParser\Message\PartFilter; /** * Provides routines to set or retrieve the signature part of a signed message. * * @author Zaahid Bateson */ class PrivacyHelper extends AbstractHelper { /** * @var GenericHelper a GenericHelper instance */ private $genericHelper; /** * @var MultipartHelper a MultipartHelper instance */ private $multipartHelper; /** * Constructor * * @param MimePartFactory $mimePartFactory * @param UUEncodedPartFactory $uuEncodedPartFactory * @param PartBuilderFactory $partBuilderFactory * @param GenericHelper $genericHelper * @param MultipartHelper $multipartHelper */ public function __construct( MimePartFactory $mimePartFactory, UUEncodedPartFactory $uuEncodedPartFactory, PartBuilderFactory $partBuilderFactory, GenericHelper $genericHelper, MultipartHelper $multipartHelper ) { parent::__construct($mimePartFactory, $uuEncodedPartFactory, $partBuilderFactory); $this->genericHelper = $genericHelper; $this->multipartHelper = $multipartHelper; } /** * The passed message is set as multipart/signed, and a new part is created * below it with content headers, content and children copied from the * message. * * @param Message $message * @param string $micalg * @param string $protocol */ public function setMessageAsMultipartSigned(Message $message, $micalg, $protocol) { if (strcasecmp($message->getContentType(), 'multipart/signed') !== 0) { $this->multipartHelper->enforceMime($message); $messagePart = $this->partBuilderFactory->newPartBuilder($this->mimePartFactory)->createMessagePart(); $this->genericHelper->movePartContentAndChildren($message, $messagePart); $message->addChild($messagePart); $boundary = $this->multipartHelper->getUniqueBoundary('multipart/signed'); $message->setRawHeader( 'Content-Type', "multipart/signed;\r\n\tboundary=\"$boundary\";\r\n\tmicalg=\"$micalg\"; protocol=\"$protocol\"" ); } $this->overwrite8bitContentEncoding($message); $this->ensureHtmlPartFirstForSignedMessage($message); $this->setSignature($message, 'Empty'); } /** * Sets the signature of the message to $body, creating a signature part if * one doesn't exist. * * @param Message $message * @param string $body */ public function setSignature(Message $message, $body) { $signedPart = $message->getSignaturePart(); if ($signedPart === null) { $signedPart = $this->partBuilderFactory->newPartBuilder($this->mimePartFactory)->createMessagePart(); $message->addChild($signedPart); } $signedPart->setRawHeader( 'Content-Type', $message->getHeaderParameter('Content-Type', 'protocol') ); $signedPart->setContent($body); } /** * Loops over parts of the message and sets the content-transfer-encoding * header to quoted-printable for text/* mime parts, and to base64 * otherwise for parts that are '8bit' encoded. * * Used for multipart/signed messages which doesn't support 8bit transfer * encodings. * * @param Message $message */ public function overwrite8bitContentEncoding(Message $message) { $parts = $message->getAllParts(new PartFilter([ 'headers' => [ PartFilter::FILTER_INCLUDE => [ 'Content-Transfer-Encoding' => '8bit' ] ] ])); foreach ($parts as $part) { $contentType = strtolower($part->getContentType()); if ($contentType === 'text/plain' || $contentType === 'text/html') { $part->setRawHeader('Content-Transfer-Encoding', 'quoted-printable'); } else { $part->setRawHeader('Content-Transfer-Encoding', 'base64'); } } } /** * Ensures a non-text part comes first in a signed multipart/alternative * message as some clients seem to prefer the first content part if the * client doesn't understand multipart/signed. * * @param Message $message */ public function ensureHtmlPartFirstForSignedMessage(Message $message) { $alt = $message->getPartByMimeType('multipart/alternative'); if ($alt !== null && $alt instanceof ParentPart) { $cont = $this->multipartHelper->getContentPartContainerFromAlternative('text/html', $alt); $children = $alt->getChildParts(); $pos = array_search($cont, $children, true); if ($pos !== false && $pos !== 0) { $alt->removePart($children[0]); $alt->addChild($children[0]); } } } /** * Returns a stream that can be used to read the content part of a signed * message, which can be used to sign an email or verify a signature. * * The method simply returns the stream for the first child. No * verification of whether the message is in fact a signed message is * performed. * * Note that unlike getSignedMessageAsString, getSignedMessageStream doesn't * replace new lines. * * @param Message $message * @return \Psr\Http\Message\StreamInterface or null if the message doesn't * have any children */ public function getSignedMessageStream(Message $message) { $child = $message->getChild(0); if ($child !== null) { return $child->getStream(); } return null; } /** * Returns a string containing the entire body (content) of a signed message * for verification or calculating a signature. * * Non-CRLF new lines are replaced to always be CRLF. * * @param Message $message * @return string or null if the message doesn't have any children */ public function getSignedMessageAsString(Message $message) { $stream = $this->getSignedMessageStream($message); if ($stream !== null) { return preg_replace( '/\r\n|\r|\n/', "\r\n", $stream->getContents() ); } return null; } /** * Returns the signature part of a multipart/signed message or null. * * The signature part is determined to always be the 2nd child of a * multipart/signed message, the first being the 'body'. * * Using the 'protocol' parameter of the Content-Type header is unreliable * in some instances (for instance a difference of x-pgp-signature versus * pgp-signature). * * @param Message $message * @return \ZBateson\MailMimeParser\Message\Part\MimePart */ public function getSignaturePart(Message $message) { if (strcasecmp($message->getContentType(), 'multipart/signed') === 0) { return $message->getChild(1); } else { return null; } } } mail-mime-parser/src/Message/Helper/AbstractHelper.php 0000644 00000002615 14736103045 0016722 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Message\Helper; use ZBateson\MailMimeParser\Message\Part\Factory\MimePartFactory; use ZBateson\MailMimeParser\Message\Part\Factory\PartBuilderFactory; use ZBateson\MailMimeParser\Message\Part\Factory\UUEncodedPartFactory; /** * Base class for message helpers. * * @author Zaahid Bateson */ abstract class AbstractHelper { /** * @var MimePartFactory to create parts for attachments/content */ protected $mimePartFactory; /** * @var UUEncodedPartFactory to create parts for attachments */ protected $uuEncodedPartFactory; /** * @var PartBuilderFactory to create parts for attachments/content */ protected $partBuilderFactory; /** * Constructor * * @param MimePartFactory $mimePartFactory * @param UUEncodedPartFactory $uuEncodedPartFactory * @param PartBuilderFactory $partBuilderFactory */ public function __construct( MimePartFactory $mimePartFactory, UUEncodedPartFactory $uuEncodedPartFactory, PartBuilderFactory $partBuilderFactory ) { $this->mimePartFactory = $mimePartFactory; $this->uuEncodedPartFactory = $uuEncodedPartFactory; $this->partBuilderFactory = $partBuilderFactory; } } mail-mime-parser/src/Message/Helper/GenericHelper.php 0000644 00000012722 14736103045 0016533 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Message\Helper; use ZBateson\MailMimeParser\MailMimeParser; use ZBateson\MailMimeParser\Message; use ZBateson\MailMimeParser\Message\Part\MimePart; use ZBateson\MailMimeParser\Message\Part\ParentHeaderPart; /** * Provides common Message helper routines for Message manipulation. * * @author Zaahid Bateson */ class GenericHelper extends AbstractHelper { /** * @var string[] List of content headers grabbed from * https://tools.ietf.org/html/rfc4021#section-2.2 */ private static $contentHeaders = [ 'Content-Type', 'Content-Transfer-Encoding', 'Content-Disposition', 'Content-ID', 'Content-Description', 'Content-Language', 'Content-Base', 'Content-Location', 'Content-features', 'Content-Alternative', 'Content-MD5', 'Content-Duration' ]; /** * Copies the passed $header from $from, to $to or sets the header to * $default if it doesn't exist in $from. * * @param ParentHeaderPart $from * @param ParentHeaderPart $to * @param string $header * @param string $default */ public function copyHeader(ParentHeaderPart $from, ParentHeaderPart $to, $header, $default = null) { $fromHeader = $from->getHeader($header); $set = ($fromHeader !== null) ? $fromHeader->getRawValue() : $default; if ($set !== null) { $to->setRawHeader($header, $set); } } /** * Removes Content-* headers (permanent ones as defined in * https://tools.ietf.org/html/rfc4021#section-2.2) from the passed part, * then detaches its content stream. * * @param ParentHeaderPart $part */ public function removeContentHeadersAndContent(ParentHeaderPart $part) { foreach (self::$contentHeaders as $header) { $part->removeHeader($header); } $part->detachContentStream(); } /** * Copies Content-* headers (permanent ones as defined in * https://tools.ietf.org/html/rfc4021#section-2.2) * from the $from header into the $to header. If the Content-Type header * isn't defined in $from, defaults to text/plain with utf-8 and * quoted-printable. * * @param ParentHeaderPart $from * @param ParentHeaderPart $to * @param bool $move */ public function copyContentHeadersAndContent(ParentHeaderPart $from, ParentHeaderPart $to, $move = false) { $this->copyHeader($from, $to, 'Content-Type', 'text/plain; charset=utf-8'); if ($from->getHeader('Content-Type') === null) { $this->copyHeader($from, $to, 'Content-Transfer-Encoding', 'quoted-printable'); } else { $this->copyHeader($from, $to, 'Content-Transfer-Encoding'); } $rem = array_diff(self::$contentHeaders, [ 'Content-Type', 'Content-Transfer-Encoding']); foreach ($rem as $header) { $this->copyHeader($from, $to, $header); } if ($from->hasContent()) { $to->attachContentStream($from->getContentStream(), MailMimeParser::DEFAULT_CHARSET); } if ($move) { $this->removeContentHeadersAndContent($from); } } /** * Creates a new content part from the passed part, allowing the part to be * used for something else (e.g. changing a non-mime message to a multipart * mime message). * * @param ParentHeaderPart $part * @return MimePart the newly-created MimePart */ public function createNewContentPartFrom(ParentHeaderPart $part) { $mime = $this->partBuilderFactory->newPartBuilder($this->mimePartFactory)->createMessagePart(); $this->copyContentHeadersAndContent($part, $mime, true); return $mime; } /** * Copies type headers (Content-Type, Content-Disposition, * Content-Transfer-Encoding) from the $from MimePart to $to. Attaches the * content resource handle of $from to $to, and loops over child parts, * removing them from $from and adding them to $to. * * @param ParentHeaderPart $from * @param ParentHeaderPart $to */ public function movePartContentAndChildren(ParentHeaderPart $from, ParentHeaderPart $to) { $this->copyContentHeadersAndContent($from, $to, true); foreach ($from->getChildParts() as $child) { $from->removePart($child); $to->addChild($child); } } /** * Replaces the $part ParentHeaderPart with $replacement. * * Essentially removes $part from its parent, and adds $replacement in its * same position. If $part is this Message, then $part can't be removed and * replaced, and instead $replacement's type headers are copied to $message, * and any children below $replacement are added directly below $message. * * @param Message $message * @param ParentHeaderPart $part * @param ParentHeaderPart $replacement */ public function replacePart(Message $message, ParentHeaderPart $part, ParentHeaderPart $replacement) { $position = $message->removePart($replacement); if ($part === $message) { $this->movePartContentAndChildren($replacement, $part); return; } $parent = $part->getParent(); $parent->addChild($replacement, $position); } } mail-mime-parser/src/Message/Helper/MultipartHelper.php 0000644 00000040663 14736103045 0017145 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Message\Helper; use ZBateson\MailMimeParser\Message; use ZBateson\MailMimeParser\Message\Part\Factory\MimePartFactory; use ZBateson\MailMimeParser\Message\Part\Factory\PartBuilderFactory; use ZBateson\MailMimeParser\Message\Part\Factory\UUEncodedPartFactory; use ZBateson\MailMimeParser\Message\Part\MessagePart; use ZBateson\MailMimeParser\Message\Part\MimePart; use ZBateson\MailMimeParser\Message\Part\ParentHeaderPart; use ZBateson\MailMimeParser\Message\PartFilter; /** * Provides various routines to manipulate and create multipart messages from an * existing message (e.g. to make space for attachments in a message, or to * change a simple message to a multipart/alternative one, etc...) * * @author Zaahid Bateson */ class MultipartHelper extends AbstractHelper { /** * @var GenericHelper a GenericHelper instance */ private $genericHelper; /** * Constructor * * @param MimePartFactory $mimePartFactory * @param UUEncodedPartFactory $uuEncodedPartFactory * @param PartBuilderFactory $partBuilderFactory * @param GenericHelper $genericHelper */ public function __construct( MimePartFactory $mimePartFactory, UUEncodedPartFactory $uuEncodedPartFactory, PartBuilderFactory $partBuilderFactory, GenericHelper $genericHelper ) { parent::__construct($mimePartFactory, $uuEncodedPartFactory, $partBuilderFactory); $this->genericHelper = $genericHelper; } /** * Creates and returns a unique boundary. * * @param string $mimeType first 3 characters of a multipart type are used, * e.g. REL for relative or ALT for alternative * @return string */ public function getUniqueBoundary($mimeType) { $type = ltrim(strtoupper(preg_replace('/^(multipart\/(.{3}).*|.*)$/i', '$2-', $mimeType)), '-'); return uniqid('----=MMP-' . $type . '.', true); } /** * Creates a unique mime boundary and assigns it to the passed part's * Content-Type header with the passed mime type. * * @param ParentHeaderPart $part * @param string $mimeType */ public function setMimeHeaderBoundaryOnPart(ParentHeaderPart $part, $mimeType) { $part->setRawHeader( 'Content-Type', "$mimeType;\r\n\tboundary=\"" . $this->getUniqueBoundary($mimeType) . '"' ); } /** * Sets the passed message as multipart/mixed. * * If the message has content, a new part is created and added as a child of * the message. The message's content and content headers are moved to the * new part. * * @param Message $message */ public function setMessageAsMixed(Message $message) { if ($message->hasContent()) { $part = $this->genericHelper->createNewContentPartFrom($message); $message->addChild($part, 0); } $this->setMimeHeaderBoundaryOnPart($message, 'multipart/mixed'); $atts = $message->getAllAttachmentParts(); if (!empty($atts)) { foreach ($atts as $att) { $att->markAsChanged(); } } } /** * Sets the passed message as multipart/alternative. * * If the message has content, a new part is created and added as a child of * the message. The message's content and content headers are moved to the * new part. * * @param Message $message */ public function setMessageAsAlternative(Message $message) { if ($message->hasContent()) { $part = $this->genericHelper->createNewContentPartFrom($message); $message->addChild($part, 0); } $this->setMimeHeaderBoundaryOnPart($message, 'multipart/alternative'); } /** * Searches the passed $alternativePart for a part with the passed mime type * and returns its parent. * * Used for alternative mime types that have a multipart/mixed or * multipart/related child containing a content part of $mimeType, where * the whole mixed/related part should be removed. * * @param string $mimeType the content-type to find below $alternativePart * @param ParentHeaderPart $alternativePart The multipart/alternative part to look * under * @return boolean|MimePart false if a part is not found */ public function getContentPartContainerFromAlternative($mimeType, ParentHeaderPart $alternativePart) { $part = $alternativePart->getPart(0, PartFilter::fromInlineContentType($mimeType)); $contPart = null; do { if ($part === null) { return false; } $contPart = $part; $part = $part->getParent(); } while ($part !== $alternativePart); return $contPart; } /** * Removes all parts of $mimeType from $alternativePart. * * If $alternativePart contains a multipart/mixed or multipart/relative part * with other parts of different content-types, the multipart part is * removed, and parts of different content-types can optionally be moved to * the main message part. * * @param Message $message * @param string $mimeType * @param ParentHeaderPart $alternativePart * @param bool $keepOtherContent * @return bool */ public function removeAllContentPartsFromAlternative(Message $message, $mimeType, ParentHeaderPart $alternativePart, $keepOtherContent) { $rmPart = $this->getContentPartContainerFromAlternative($mimeType, $alternativePart); if ($rmPart === false) { return false; } if ($keepOtherContent) { $this->moveAllPartsAsAttachmentsExcept($message, $rmPart, $mimeType); $alternativePart = $message->getPart(0, PartFilter::fromInlineContentType('multipart/alternative')); } else { $rmPart->removeAllParts(); } $message->removePart($rmPart); if ($alternativePart !== null) { if ($alternativePart->getChildCount() === 1) { $this->genericHelper->replacePart($message, $alternativePart, $alternativePart->getChild(0)); } elseif ($alternativePart->getChildCount() === 0) { $message->removePart($alternativePart); } } while ($message->getChildCount() === 1) { $this->genericHelper->replacePart($message, $message, $message->getChild(0)); } return true; } /** * Creates a new mime part as a multipart/alternative and assigns the passed * $contentPart as a part below it before returning it. * * @param Message $message * @param MessagePart $contentPart * @return MimePart the alternative part */ public function createAlternativeContentPart(Message $message, MessagePart $contentPart) { $altPart = $this->partBuilderFactory->newPartBuilder($this->mimePartFactory)->createMessagePart(); $this->setMimeHeaderBoundaryOnPart($altPart, 'multipart/alternative'); $message->removePart($contentPart); $message->addChild($altPart, 0); $altPart->addChild($contentPart, 0); return $altPart; } /** * Moves all parts under $from into this message except those with a * content-type equal to $exceptMimeType. If the message is not a * multipart/mixed message, it is set to multipart/mixed first. * * @param Message $message * @param ParentHeaderPart $from * @param string $exceptMimeType */ public function moveAllPartsAsAttachmentsExcept(Message $message, ParentHeaderPart $from, $exceptMimeType) { $parts = $from->getAllParts(new PartFilter([ 'multipart' => PartFilter::FILTER_EXCLUDE, 'headers' => [ PartFilter::FILTER_EXCLUDE => [ 'Content-Type' => $exceptMimeType ] ] ])); if (strcasecmp($message->getContentType(), 'multipart/mixed') !== 0) { $this->setMessageAsMixed($message); } foreach ($parts as $part) { $from->removePart($part); $message->addChild($part); } } /** * Enforces the message to be a mime message for a non-mime (e.g. uuencoded * or unspecified) message. If the message has uuencoded attachments, sets * up the message as a multipart/mixed message and creates a separate * content part. * * @param Message $message */ public function enforceMime(Message $message) { if (!$message->isMime()) { if ($message->getAttachmentCount()) { $this->setMessageAsMixed($message); } else { $message->setRawHeader('Content-Type', "text/plain;\r\n\tcharset=\"iso-8859-1\""); } $message->setRawHeader('Mime-Version', '1.0'); } } /** * Creates a multipart/related part out of 'inline' children of $parent and * returns it. * * @param ParentHeaderPart $parent * @return MimePart */ public function createMultipartRelatedPartForInlineChildrenOf(ParentHeaderPart $parent) { $relatedPart = $this->partBuilderFactory->newPartBuilder($this->mimePartFactory)->createMessagePart(); $this->setMimeHeaderBoundaryOnPart($relatedPart, 'multipart/related'); foreach ($parent->getChildParts(PartFilter::fromDisposition('inline', PartFilter::FILTER_EXCLUDE)) as $part) { $parent->removePart($part); $relatedPart->addChild($part); } $parent->addChild($relatedPart, 0); return $relatedPart; } /** * Finds an alternative inline part in the message and returns it if one * exists. * * If the passed $mimeType is text/plain, searches for a text/html part. * Otherwise searches for a text/plain part to return. * * @param Message $message * @param string $mimeType * @return \ZBateson\MailMimeParser\Message\Part\MimeType or null if not * found */ public function findOtherContentPartFor(Message $message, $mimeType) { $altPart = $message->getPart( 0, PartFilter::fromInlineContentType(($mimeType === 'text/plain') ? 'text/html' : 'text/plain') ); if ($altPart !== null && $altPart->getParent() !== null && $altPart->getParent()->isMultiPart()) { $altPartParent = $altPart->getParent(); if ($altPartParent->getChildCount(PartFilter::fromDisposition('inline', PartFilter::FILTER_EXCLUDE)) !== 1) { $altPart = $this->createMultipartRelatedPartForInlineChildrenOf($altPartParent); } } return $altPart; } /** * Creates a new content part for the passed mimeType and charset, making * space by creating a multipart/alternative if needed * * @param Message $message * @param string $mimeType * @param string $charset * @return \ZBateson\MailMimeParser\Message\Part\MimePart */ public function createContentPartForMimeType(Message $message, $mimeType, $charset) { $builder = $this->partBuilderFactory->newPartBuilder($this->mimePartFactory); $builder->addHeader('Content-Type', "$mimeType;\r\n\tcharset=\"$charset\""); $builder->addHeader('Content-Transfer-Encoding', 'quoted-printable'); $this->enforceMime($message); $mimePart = $builder->createMessagePart(); $altPart = $this->findOtherContentPartFor($message, $mimeType); if ($altPart === $message) { $this->setMessageAsAlternative($message); $message->addChild($mimePart); } elseif ($altPart !== null) { $mimeAltPart = $this->createAlternativeContentPart($message, $altPart); $mimeAltPart->addChild($mimePart, 1); } else { $message->addChild($mimePart, 0); } return $mimePart; } /** * Creates and adds a MimePart for the passed content and options as an * attachment. * * @param Message $message * @param string|resource|Psr\Http\Message\StreamInterface\StreamInterface * $resource * @param string $mimeType * @param string $disposition * @param string $filename * @param string $encoding * @return \ZBateson\MailMimeParser\Message\Part\MimePart */ public function createAndAddPartForAttachment(Message $message, $resource, $mimeType, $disposition, $filename = null, $encoding = 'base64') { if ($filename === null) { $filename = 'file' . uniqid(); } $safe = iconv('UTF-8', 'US-ASCII//translit//ignore', $filename); if ($message->isMime()) { $builder = $this->partBuilderFactory->newPartBuilder($this->mimePartFactory); $builder->addHeader('Content-Transfer-Encoding', $encoding); if (strcasecmp($message->getContentType(), 'multipart/mixed') !== 0) { $this->setMessageAsMixed($message); } $builder->addHeader('Content-Type', "$mimeType;\r\n\tname=\"$safe\""); $builder->addHeader('Content-Disposition', "$disposition;\r\n\tfilename=\"$safe\""); } else { $builder = $this->partBuilderFactory->newPartBuilder( $this->uuEncodedPartFactory ); $builder->setProperty('filename', $safe); } $part = $builder->createMessagePart(); $part->setContent($resource); $message->addChild($part); } /** * Removes the content part of the message with the passed mime type. If * there is a remaining content part and it is an alternative part of the * main message, the content part is moved to the message part. * * If the content part is part of an alternative part beneath the message, * the alternative part is replaced by the remaining content part, * optionally keeping other parts if $keepOtherContent is set to true. * * @param Message $message * @param string $mimeType * @param bool $keepOtherContent * @return boolean true on success */ public function removeAllContentPartsByMimeType(Message $message, $mimeType, $keepOtherContent = false) { $alt = $message->getPart(0, PartFilter::fromInlineContentType('multipart/alternative')); if ($alt !== null) { return $this->removeAllContentPartsFromAlternative($message, $mimeType, $alt, $keepOtherContent); } $message->removeAllParts(PartFilter::fromInlineContentType($mimeType)); return true; } /** * Removes the 'inline' part with the passed contentType, at the given index * defaulting to the first * * @param Message $message * @param string $mimeType * @param int $index * @return boolean true on success */ public function removePartByMimeType(Message $message, $mimeType, $index = 0) { $parts = $message->getAllParts(PartFilter::fromInlineContentType($mimeType)); $alt = $message->getPart(0, PartFilter::fromInlineContentType('multipart/alternative')); if ($parts === null || !isset($parts[$index])) { return false; } elseif (count($parts) === 1) { return $this->removeAllContentPartsByMimeType($message, $mimeType, true); } $part = $parts[$index]; $message->removePart($part); if ($alt !== null && $alt->getChildCount() === 1) { $this->genericHelper->replacePart($message, $alt, $alt->getChild(0)); } return true; } /** * Either creates a mime part or sets the existing mime part with the passed * mimeType to $strongOrHandle. * * @param Message $message * @param string $mimeType * @param string|resource $stringOrHandle * @param string $charset */ public function setContentPartForMimeType(Message $message, $mimeType, $stringOrHandle, $charset) { $part = ($mimeType === 'text/html') ? $message->getHtmlPart() : $message->getTextPart(); if ($part === null) { $part = $this->createContentPartForMimeType($message, $mimeType, $charset); } else { $contentType = $part->getContentType(); $part->setRawHeader('Content-Type', "$contentType;\r\n\tcharset=\"$charset\""); } $part->setContent($stringOrHandle); } } mail-mime-parser/src/Message/Helper/MessageHelperService.php 0000644 00000006075 14736103045 0020070 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Message\Helper; use ZBateson\MailMimeParser\Message\Part\Factory\PartBuilderFactory; use ZBateson\MailMimeParser\Message\Part\Factory\PartFactoryService; /** * Responsible for creating helper singletons. * * @author Zaahid Bateson */ class MessageHelperService { /** * @var PartBuilderFactory the PartBuilderFactory */ private $partBuilderFactory; /** * @var GenericHelper the GenericHelper singleton */ private $genericHelper; /** * @var MultipartHelper the MultipartHelper singleton */ private $multipartHelper; /** * @var PrivacyHelper the PrivacyHelper singleton */ private $privacyHelper; /** * @var PartFactoryService the PartFactoryService */ private $partFactoryService; /** * Constructor * * @param PartBuilderFactory $partBuilderFactory */ public function __construct(PartBuilderFactory $partBuilderFactory) { $this->partBuilderFactory = $partBuilderFactory; } /** * Set separately to avoid circular dependencies (PartFactoryService needs a * MessageHelperService). * * @param PartFactoryService $partFactoryService */ public function setPartFactoryService(PartFactoryService $partFactoryService) { $this->partFactoryService = $partFactoryService; } /** * Returns the GenericHelper singleton * * @return GenericHelper */ public function getGenericHelper() { if ($this->genericHelper === null) { $this->genericHelper = new GenericHelper( $this->partFactoryService->getMimePartFactory(), $this->partFactoryService->getUUEncodedPartFactory(), $this->partBuilderFactory ); } return $this->genericHelper; } /** * Returns the MultipartHelper singleton * * @return MultipartHelper */ public function getMultipartHelper() { if ($this->multipartHelper === null) { $this->multipartHelper = new MultipartHelper( $this->partFactoryService->getMimePartFactory(), $this->partFactoryService->getUUEncodedPartFactory(), $this->partBuilderFactory, $this->getGenericHelper() ); } return $this->multipartHelper; } /** * Returns the PrivacyHelper singleton * * @return PrivacyHelper */ public function getPrivacyHelper() { if ($this->privacyHelper === null) { $this->privacyHelper = new PrivacyHelper( $this->partFactoryService->getMimePartFactory(), $this->partFactoryService->getUUEncodedPartFactory(), $this->partBuilderFactory, $this->getGenericHelper(), $this->getMultipartHelper() ); } return $this->privacyHelper; } } mail-mime-parser/src/Container.php 0000644 00000017132 14736103045 0013136 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser; use ZBateson\MailMimeParser\Header\Consumer\ConsumerService; use ZBateson\MailMimeParser\Header\HeaderFactory; use ZBateson\MailMimeParser\Header\Part\HeaderPartFactory; use ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory; use ZBateson\MailMimeParser\Message\Helper\MessageHelperService; use ZBateson\MailMimeParser\Message\MessageParser; use ZBateson\MailMimeParser\Message\Part\Factory\PartBuilderFactory; use ZBateson\MailMimeParser\Message\Part\Factory\PartFactoryService; use ZBateson\MailMimeParser\Message\Part\Factory\PartStreamFilterManagerFactory; use ZBateson\MbWrapper\MbWrapper; /** * Dependency injection container for use by ZBateson\MailMimeParser - because a * more complex one seems like overkill. * * Constructs objects and whatever dependencies they require. * * @author Zaahid Bateson */ class Container { /** * @var PartBuilderFactory The PartBuilderFactory instance */ protected $partBuilderFactory; /** * @var PartFactoryService The PartFactoryService instance */ protected $partFactoryService; /** * @var PartFilterFactory The PartFilterFactory instance */ protected $partFilterFactory; /** * @var PartStreamFilterManagerFactory The PartStreamFilterManagerFactory * instance */ protected $partStreamFilterManagerFactory; /** * @var \ZBateson\MailMimeParser\Header\HeaderFactory singleton 'service' * instance */ protected $headerFactory; /** * @var \ZBateson\MailMimeParser\Header\Part\HeaderPartFactory singleton * 'service' instance */ protected $headerPartFactory; /** * @var \ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory * singleton 'service' instance */ protected $mimeLiteralPartFactory; /** * @var \ZBateson\MailMimeParser\Header\Consumer\ConsumerService singleton * 'service' instance */ protected $consumerService; /** * @var MessageHelperService Used to get MessageHelper singletons */ protected $messageHelperService; /** * @var StreamFactory */ protected $streamFactory; /** * Constructs a Container - call singleton() to invoke */ public function __construct() { } /** * Returns a singleton 'service' instance for the given service named $var * with a class type of $class. * * @param string $var the name of the service * @param string $class the name of the class * @return mixed the service object */ protected function getInstance($var, $class) { if ($this->$var === null) { $this->$var = new $class(); } return $this->$var; } /** * Constructs and returns a new MessageParser object. * * @return \ZBateson\MailMimeParser\Message\MessageParser */ public function newMessageParser() { return new MessageParser( $this->getPartFactoryService(), $this->getPartBuilderFactory() ); } /** * Returns a MessageHelperService instance. * * @return MessageHelperService */ public function getMessageHelperService() { if ($this->messageHelperService === null) { $this->messageHelperService = new MessageHelperService( $this->getPartBuilderFactory() ); $this->messageHelperService->setPartFactoryService( $this->getPartFactoryService() ); } return $this->messageHelperService; } /** * Returns a PartFilterFactory instance * * @return PartFilterFactory */ public function getPartFilterFactory() { return $this->getInstance( 'partFilterFactory', __NAMESPACE__ . '\Message\PartFilterFactory' ); } /** * Returns a PartFactoryService singleton. * * @return PartFactoryService */ public function getPartFactoryService() { if ($this->partFactoryService === null) { $this->partFactoryService = new PartFactoryService( $this->getPartFilterFactory(), $this->getStreamFactory(), $this->getPartStreamFilterManagerFactory(), $this->getMessageHelperService() ); } return $this->partFactoryService; } /** * Returns a PartBuilderFactory instance. * * @return PartBuilderFactory */ public function getPartBuilderFactory() { if ($this->partBuilderFactory === null) { $this->partBuilderFactory = new PartBuilderFactory( $this->getHeaderFactory() ); } return $this->partBuilderFactory; } /** * Returns the header factory service instance. * * @return \ZBateson\MailMimeParser\Header\HeaderFactory */ public function getHeaderFactory() { if ($this->headerFactory === null) { $this->headerFactory = new HeaderFactory( $this->getConsumerService(), $this->getMimeLiteralPartFactory() ); } return $this->headerFactory; } /** * Returns a StreamFactory. * * @return StreamFactory */ public function getStreamFactory() { return $this->getInstance( 'streamFactory', __NAMESPACE__ . '\Stream\StreamFactory' ); } /** * Returns a PartStreamFilterManagerFactory. * * @return PartStreamFilterManagerFactory */ public function getPartStreamFilterManagerFactory() { if ($this->partStreamFilterManagerFactory === null) { $this->partStreamFilterManagerFactory = new PartStreamFilterManagerFactory( $this->getStreamFactory() ); } return $this->getInstance( 'partStreamFilterManagerFactory', __NAMESPACE__ . '\Message\Part\PartStreamFilterManagerFactory' ); } /** * Returns a MbWrapper. * * @return MbWrapper */ public function getCharsetConverter() { return new MbWrapper(); } /** * Returns the part factory service * * @return \ZBateson\MailMimeParser\Header\Part\HeaderPartFactory */ public function getHeaderPartFactory() { if ($this->headerPartFactory === null) { $this->headerPartFactory = new HeaderPartFactory($this->getCharsetConverter()); } return $this->headerPartFactory; } /** * Returns the MimeLiteralPartFactory service * * @return \ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory */ public function getMimeLiteralPartFactory() { if ($this->mimeLiteralPartFactory === null) { $this->mimeLiteralPartFactory = new MimeLiteralPartFactory($this->getCharsetConverter()); } return $this->mimeLiteralPartFactory; } /** * Returns the header consumer service * * @return \ZBateson\MailMimeParser\Header\Consumer\ConsumerService */ public function getConsumerService() { if ($this->consumerService === null) { $this->consumerService = new ConsumerService( $this->getHeaderPartFactory(), $this->getMimeLiteralPartFactory() ); } return $this->consumerService; } } mail-mime-parser/src/Message.php 0000644 00000042121 14736103045 0012574 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser; use GuzzleHttp\Psr7; use Psr\Http\Message\StreamInterface; use ZBateson\MailMimeParser\Message\Helper\MessageHelperService; use ZBateson\MailMimeParser\Message\Part\MimePart; use ZBateson\MailMimeParser\Message\Part\MessagePart; use ZBateson\MailMimeParser\Message\Part\PartBuilder; use ZBateson\MailMimeParser\Message\Part\PartStreamFilterManager; use ZBateson\MailMimeParser\Message\PartFilter; use ZBateson\MailMimeParser\Message\PartFilterFactory; use ZBateson\MailMimeParser\Stream\StreamFactory; /** * A parsed mime message with optional mime parts depending on its type. * * A mime message may have any number of mime parts, and each part may have any * number of sub-parts, etc... * * @author Zaahid Bateson */ class Message extends MimePart { /** * @var MessageHelperService helper class with various message manipulation * routines. */ protected $messageHelperService; /** * Constructor * * @param PartStreamFilterManager $partStreamFilterManager * @param StreamFactory $streamFactory * @param PartFilterFactory $partFilterFactory * @param PartBuilder $partBuilder * @param MessageHelperService $messageHelperService * @param StreamInterface $stream * @param StreamInterface $contentStream */ public function __construct( PartStreamFilterManager $partStreamFilterManager, StreamFactory $streamFactory, PartFilterFactory $partFilterFactory, PartBuilder $partBuilder, MessageHelperService $messageHelperService, StreamInterface $stream = null, StreamInterface $contentStream = null ) { parent::__construct( $partStreamFilterManager, $streamFactory, $partFilterFactory, $partBuilder, $stream, $contentStream ); $this->messageHelperService = $messageHelperService; } /** * Convenience method to parse a handle or string into a Message without * requiring including MailMimeParser, instantiating it, and calling parse. * * @param resource|string $handleOrString the resource handle to the input * stream of the mime message, or a string containing a mime message * @return Message */ public static function from($handleOrString) { $mmp = new MailMimeParser(); return $mmp->parse($handleOrString); } /** * Returns the text/plain part at the given index (or null if not found.) * * @param int $index * @return MessagePart */ public function getTextPart($index = 0) { return $this->getPart( $index, $this->partFilterFactory->newFilterFromInlineContentType('text/plain') ); } /** * Returns the number of text/plain parts in this message. * * @return int */ public function getTextPartCount() { return $this->getPartCount( $this->partFilterFactory->newFilterFromInlineContentType('text/plain') ); } /** * Returns the text/html part at the given index (or null if not found.) * * @param int $index * @return MessagePart */ public function getHtmlPart($index = 0) { return $this->getPart( $index, $this->partFilterFactory->newFilterFromInlineContentType('text/html') ); } /** * Returns the number of text/html parts in this message. * * @return int */ public function getHtmlPartCount() { return $this->getPartCount( $this->partFilterFactory->newFilterFromInlineContentType('text/html') ); } /** * Returns the attachment part at the given 0-based index, or null if none * is set. * * @param int $index * @return MessagePart */ public function getAttachmentPart($index) { $attachments = $this->getAllAttachmentParts(); if (!isset($attachments[$index])) { return null; } return $attachments[$index]; } /** * Returns all attachment parts. * * "Attachments" are any non-multipart, non-signature and any text or html * html part witha Content-Disposition set to 'attachment'. * * @return MessagePart[] */ public function getAllAttachmentParts() { $parts = $this->getAllParts( $this->partFilterFactory->newFilterFromArray([ 'multipart' => PartFilter::FILTER_EXCLUDE ]) ); return array_values(array_filter( $parts, function ($part) { return !( $part->isTextPart() && $part->getContentDisposition() === 'inline' ); } )); } /** * Returns the number of attachments available. * * @return int */ public function getAttachmentCount() { return count($this->getAllAttachmentParts()); } /** * Returns a Psr7 Stream for the 'inline' text/plain content at the passed * $index, or null if unavailable. * * @param int $index * @param string $charset * @return StreamInterface */ public function getTextStream($index = 0, $charset = MailMimeParser::DEFAULT_CHARSET) { $textPart = $this->getTextPart($index); if ($textPart !== null) { return $textPart->getContentStream($charset); } return null; } /** * Returns a resource handle for the 'inline' text/plain content at the * passed $index, or null if unavailable. * * Note: this method should *not* be used and has been deprecated. Instead, * use Psr7 streams with getTextStream. Multibyte chars will not be read * correctly with getTextResourceHandle/fread. * * @param int $index * @param string $charset * @deprecated since version 1.2.1 * @return resource */ public function getTextResourceHandle($index = 0, $charset = MailMimeParser::DEFAULT_CHARSET) { trigger_error("getTextResourceHandle is deprecated since version 1.2.1", E_USER_DEPRECATED); $textPart = $this->getTextPart($index); if ($textPart !== null) { return $textPart->getContentResourceHandle($charset); } return null; } /** * Returns the content of the inline text/plain part at the given index. * * Reads the entire stream content into a string and returns it. Returns * null if the message doesn't have an inline text part. * * @param int $index * @param string $charset * @return string */ public function getTextContent($index = 0, $charset = MailMimeParser::DEFAULT_CHARSET) { $part = $this->getTextPart($index); if ($part !== null) { return $part->getContent($charset); } return null; } /** * Returns a Psr7 Stream for the 'inline' text/html content at the passed * $index, or null if unavailable. * * @param int $index * @param string $charset * @return StreamInterface */ public function getHtmlStream($index = 0, $charset = MailMimeParser::DEFAULT_CHARSET) { $htmlPart = $this->getHtmlPart($index); if ($htmlPart !== null) { return $htmlPart->getContentStream($charset); } return null; } /** * Returns a resource handle for the 'inline' text/html content at the * passed $index, or null if unavailable. * * Note: this method should *not* be used and has been deprecated. Instead, * use Psr7 streams with getHtmlStream. Multibyte chars will not be read * correctly with getHtmlResourceHandle/fread. * * @param int $index * @param string $charset * @deprecated since version 1.2.1 * @return resource */ public function getHtmlResourceHandle($index = 0, $charset = MailMimeParser::DEFAULT_CHARSET) { trigger_error("getHtmlResourceHandle is deprecated since version 1.2.1", E_USER_DEPRECATED); $htmlPart = $this->getHtmlPart($index); if ($htmlPart !== null) { return $htmlPart->getContentResourceHandle($charset); } return null; } /** * Returns the content of the inline text/html part at the given index. * * Reads the entire stream content into a string and returns it. Returns * null if the message doesn't have an inline html part. * * @param int $index * @param string $charset * @return string */ public function getHtmlContent($index = 0, $charset = MailMimeParser::DEFAULT_CHARSET) { $part = $this->getHtmlPart($index); if ($part !== null) { return $part->getContent($charset); } return null; } /** * Returns true if either a Content-Type or Mime-Version header are defined * in this Message. * * @return bool */ public function isMime() { $contentType = $this->getHeaderValue('Content-Type'); $mimeVersion = $this->getHeaderValue('Mime-Version'); return ($contentType !== null || $mimeVersion !== null); } /** * Sets the text/plain part of the message to the passed $stringOrHandle, * either creating a new part if one doesn't exist for text/plain, or * assigning the value of $stringOrHandle to an existing text/plain part. * * The optional $charset parameter is the charset for saving to. * $stringOrHandle is expected to be in UTF-8 regardless of the target * charset. * * @param string|resource|StreamInterface $resource * @param string $charset */ public function setTextPart($resource, $charset = 'UTF-8') { $this->messageHelperService ->getMultipartHelper() ->setContentPartForMimeType( $this, 'text/plain', $resource, $charset ); } /** * Sets the text/html part of the message to the passed $stringOrHandle, * either creating a new part if one doesn't exist for text/html, or * assigning the value of $stringOrHandle to an existing text/html part. * * The optional $charset parameter is the charset for saving to. * $stringOrHandle is expected to be in UTF-8 regardless of the target * charset. * * @param string|resource|StreamInterface $resource * @param string $charset */ public function setHtmlPart($resource, $charset = 'UTF-8') { $this->messageHelperService ->getMultipartHelper() ->setContentPartForMimeType( $this, 'text/html', $resource, $charset ); } /** * Removes the text/plain part of the message at the passed index if one * exists. Returns true on success. * * @param int $index * @return bool true on success */ public function removeTextPart($index = 0) { return $this->messageHelperService ->getMultipartHelper() ->removePartByMimeType( $this, 'text/plain', $index ); } /** * Removes all text/plain inline parts in this message, optionally keeping * other inline parts as attachments on the main message (defaults to * keeping them). * * @param bool $keepOtherPartsAsAttachments * @return bool true on success */ public function removeAllTextParts($keepOtherPartsAsAttachments = true) { return $this->messageHelperService ->getMultipartHelper() ->removeAllContentPartsByMimeType( $this, 'text/plain', $keepOtherPartsAsAttachments ); } /** * Removes the html part of the message if one exists. Returns true on * success. * * @param int $index * @return bool true on success */ public function removeHtmlPart($index = 0) { return $this->messageHelperService ->getMultipartHelper() ->removePartByMimeType( $this, 'text/html', $index ); } /** * Removes all text/html inline parts in this message, optionally keeping * other inline parts as attachments on the main message (defaults to * keeping them). * * @param bool $keepOtherPartsAsAttachments * @return bool true on success */ public function removeAllHtmlParts($keepOtherPartsAsAttachments = true) { return $this->messageHelperService ->getMultipartHelper() ->removeAllContentPartsByMimeType( $this, 'text/html', $keepOtherPartsAsAttachments ); } /** * Adds an attachment part for the passed raw data string or handle and * given parameters. * * @param string|resource|StreamInterface $resource * @param string $mimeType * @param string $filename * @param string $disposition * @param string $encoding defaults to 'base64', only applied for a mime * email */ public function addAttachmentPart($resource, $mimeType, $filename = null, $disposition = 'attachment', $encoding = 'base64') { $this->messageHelperService ->getMultipartHelper() ->createAndAddPartForAttachment($this, $resource, $mimeType, $disposition, $filename, $encoding); } /** * Adds an attachment part using the passed file. * * Essentially creates a file stream and uses it. * * @param string $filePath * @param string $mimeType * @param string $filename * @param string $disposition */ public function addAttachmentPartFromFile($filePath, $mimeType, $filename = null, $disposition = 'attachment', $encoding = 'base64') { $handle = Psr7\stream_for(fopen($filePath, 'r')); if ($filename === null) { $filename = basename($filePath); } $this->addAttachmentPart($handle, $mimeType, $filename, $disposition, $encoding); } /** * Removes the attachment with the given index * * @param int $index */ public function removeAttachmentPart($index) { $part = $this->getAttachmentPart($index); $this->removePart($part); } /** * Returns a stream that can be used to read the content part of a signed * message, which can be used to sign an email or verify a signature. * * The method simply returns the stream for the first child. No * verification of whether the message is in fact a signed message is * performed. * * Note that unlike getSignedMessageAsString, getSignedMessageStream doesn't * replace new lines. * * @return StreamInterface or null if the message doesn't have any children */ public function getSignedMessageStream() { return $this ->messageHelperService ->getPrivacyHelper() ->getSignedMessageStream($this); } /** * Returns a string containing the entire body of a signed message for * verification or calculating a signature. * * Non-CRLF new lines are replaced to always be CRLF. * * @return string or null if the message doesn't have any children */ public function getSignedMessageAsString() { return $this ->messageHelperService ->getPrivacyHelper() ->getSignedMessageAsString($this); } /** * Returns the signature part of a multipart/signed message or null. * * The signature part is determined to always be the 2nd child of a * multipart/signed message, the first being the 'body'. * * Using the 'protocol' parameter of the Content-Type header is unreliable * in some instances (for instance a difference of x-pgp-signature versus * pgp-signature). * * @return MimePart */ public function getSignaturePart() { return $this ->messageHelperService ->getPrivacyHelper() ->getSignaturePart($this); } /** * Turns the message into a multipart/signed message, moving the actual * message into a child part, sets the content-type of the main message to * multipart/signed and adds an empty signature part as well. * * After calling setAsMultipartSigned, call getSignedMessageAsString to * return a * * @param string $micalg The Message Integrity Check algorithm being used * @param string $protocol The mime-type of the signature body */ public function setAsMultipartSigned($micalg, $protocol) { $this ->messageHelperService ->getPrivacyHelper() ->setMessageAsMultipartSigned($this, $micalg, $protocol); } /** * Sets the signature body of the message to the passed $body for a * multipart/signed message. * * @param string $body */ public function setSignature($body) { $this->messageHelperService->getPrivacyHelper() ->setSignature($this, $body); } } mail-mime-parser/src/Header/AddressHeader.php 0000644 00000006573 14736103045 0015111 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header; use ZBateson\MailMimeParser\Header\Consumer\AbstractConsumer; use ZBateson\MailMimeParser\Header\Consumer\ConsumerService; use ZBateson\MailMimeParser\Header\Part\AddressPart; use ZBateson\MailMimeParser\Header\Part\AddressGroupPart; /** * Reads an address list header using the AddressBaseConsumer. * * An address list may consist of one or more addresses and address groups. * Each address separated by a comma, and each group separated by a semi-colon. * * For full specifications, see https://www.ietf.org/rfc/rfc2822.txt * * @author Zaahid Bateson */ class AddressHeader extends AbstractHeader { /** * @var \ZBateson\MailMimeParser\Header\Part\AddressPart[] array of * addresses */ protected $addresses = []; /** * @var \ZBateson\MailMimeParser\Header\Part\AddressGroupPart[] array of * address groups */ protected $groups = []; /** * Returns an AddressBaseConsumer. * * @param ConsumerService $consumerService * @return \ZBateson\MailMimeParser\Header\Consumer\AbstractConsumer */ protected function getConsumer(ConsumerService $consumerService) { return $consumerService->getAddressBaseConsumer(); } /** * Overridden to extract all addresses into addresses array. * * @param AbstractConsumer $consumer */ protected function setParseHeaderValue(AbstractConsumer $consumer) { parent::setParseHeaderValue($consumer); foreach ($this->parts as $part) { if ($part instanceof AddressPart) { $this->addresses[] = $part; } elseif ($part instanceof AddressGroupPart) { $this->addresses = array_merge($this->addresses, $part->getAddresses()); $this->groups[] = $part; } } } /** * Returns all address parts in the header including all addresses that are * in groups. * * @return \ZBateson\MailMimeParser\Header\Part\AddressPart[] */ public function getAddresses() { return $this->addresses; } /** * Returns all group parts in the header. * * @return \ZBateson\MailMimeParser\Header\Part\AddressGroupPart[] */ public function getGroups() { return $this->groups; } /** * Returns true if an address exists with the passed email address. * * Comparison is done case insensitively. * * @param string $email * @return boolean */ public function hasAddress($email) { foreach ($this->addresses as $addr) { if (strcasecmp($addr->getEmail(), $email) === 0) { return true; } } return false; } /** * Same as getValue, but for clarity to match AddressPart. * * @return string */ public function getEmail() { return $this->getValue(); } /** * Returns the name associated with the first email address to complement * getValue(). * * @return string */ public function getPersonName() { if (!empty($this->parts)) { return $this->parts[0]->getName(); } return null; } } mail-mime-parser/src/Header/AbstractHeader.php 0000644 00000006513 14736103045 0015261 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header; use ZBateson\MailMimeParser\Header\Consumer\AbstractConsumer; use ZBateson\MailMimeParser\Header\Consumer\ConsumerService; /** * Abstract base class representing a mime email's header. * * The base class sets up the header's consumer, sets the name of the header and * calls the consumer to parse the header's value. * * AbstractHeader::getConsumer is an abstract method that must be overridden to * return an appropriate Consumer\AbstractConsumer type. * * @author Zaahid Bateson */ abstract class AbstractHeader { /** * @var string the name of the header */ protected $name; /** * @var \ZBateson\MailMimeParser\Header\Part\HeaderPart[] the header's parts * (as returned from the consumer) */ protected $parts; /** * @var string the raw value */ protected $rawValue; /** * Assigns the header's name and raw value, then calls getConsumer and * setParseHeaderValue to extract a parsed value. * * @param ConsumerService $consumerService * @param string $name * @param string $value */ public function __construct(ConsumerService $consumerService, $name, $value) { $this->name = $name; $this->rawValue = $value; $consumer = $this->getConsumer($consumerService); $this->setParseHeaderValue($consumer); } /** * Returns the header's Consumer * * @param ConsumerService $consumerService * @return \ZBateson\MailMimeParser\Header\Consumer\AbstractConsumer */ abstract protected function getConsumer(ConsumerService $consumerService); /** * Calls the consumer and assigns the parsed parts to member variables. * * The default implementation assigns the returned value to $this->part. * * @param AbstractConsumer $consumer */ protected function setParseHeaderValue(AbstractConsumer $consumer) { $this->parts = $consumer($this->rawValue); } /** * Returns an array of HeaderPart objects associated with this header. * * @return \ZBateson\MailMimeParser\Header\Part\HeaderPart[] */ public function getParts() { return $this->parts; } /** * Returns the parsed value of the header -- calls getValue on $this->part * * @return string */ public function getValue() { if (!empty($this->parts)) { return $this->parts[0]->getValue(); } return null; } /** * Returns the raw value of the header prior to any processing. * * @return string */ public function getRawValue() { return $this->rawValue; } /** * Returns the name of the header. * * @return string */ public function getName() { return $this->name; } /** * Returns the string representation of the header. At the moment this is * just in the form of: * * <HeaderName>: <RawValue> * * No additional processing is performed (for instance to wrap long lines.) * * @return string */ public function __toString() { return "{$this->name}: {$this->rawValue}"; } } mail-mime-parser/src/Header/ParameterHeader.php 0000644 00000004335 14736103045 0015436 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header; use ZBateson\MailMimeParser\Header\Consumer\ConsumerService; use ZBateson\MailMimeParser\Header\Consumer\AbstractConsumer; use ZBateson\MailMimeParser\Header\Part\ParameterPart; /** * Represents a header containing a primary value part and subsequent name/value * parts using a ParameterConsumer. * * @author Zaahid Bateson */ class ParameterHeader extends AbstractHeader { /** * @var \ZBateson\MailMimeParser\Header\Part\ParameterPart[] key map of * lower-case parameter names and associated ParameterParts. */ protected $parameters = []; /** * Returns a ParameterConsumer. * * @param ConsumerService $consumerService * @return \ZBateson\MailMimeParser\Header\Consumer\AbstractConsumer */ protected function getConsumer(ConsumerService $consumerService) { return $consumerService->getParameterConsumer(); } /** * Overridden to assign ParameterParts to a map of lower-case parameter * names to ParameterParts. * * @param AbstractConsumer $consumer */ protected function setParseHeaderValue(AbstractConsumer $consumer) { parent::setParseHeaderValue($consumer); foreach ($this->parts as $part) { if ($part instanceof ParameterPart) { $this->parameters[strtolower($part->getName())] = $part; } } } /** * Returns true if a parameter exists with the passed name. * * @param string $name * @return boolean */ public function hasParameter($name) { return isset($this->parameters[strtolower($name)]); } /** * Returns the value of the parameter with the given name, or $defaultValue * if not set. * * @param string $name * @param string $defaultValue * @return string */ public function getValueFor($name, $defaultValue = null) { if (!$this->hasParameter($name)) { return $defaultValue; } return $this->parameters[strtolower($name)]->getValue(); } } mail-mime-parser/src/Header/ReceivedHeader.php 0000644 00000021730 14736103045 0015242 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header; use ZBateson\MailMimeParser\Header\Consumer\ConsumerService; use ZBateson\MailMimeParser\Header\Consumer\AbstractConsumer; use ZBateson\MailMimeParser\Header\Part\CommentPart; use ZBateson\MailMimeParser\Header\Part\DatePart; /** * Represents a Received header. * * The returned header value (as returned by a call to {@see * ReceivedHeader::getValue()}) for a * ReceivedHeader is the same as the raw value (as returned by a call to * {@see ReceivedHeader::getRawValue()}) since the header doesn't have a single * 'value' to extract. * * The parsed parts of a Received header can be accessed as parameters. To * check if a part exists, call {@see ReceivedHeader::hasParameter()} with the * name of the part, for example: ``` $header->hasParameter('from') ``` or * ``` $header->hasParameter('id') ```. The value of the part can be obtained * by calling {@see ReceivedHeader::getValueFor()}, for example * ``` $header->getValueFor('with'); ```. * * Additional parsing is performed on the "FROM" and "BY" parts of a received * header in an attempt to extract the self-identified name of the server, its * hostname, and its address (depending on what's included). These can be * accessed directly from the ReceivedHeader object by calling one of the * following methods: * * o {@see ReceivedHeader::getFromName()} -- the name portion of the FROM part * o {@see ReceivedHeader::getFromHostname()} -- the hostname of the FROM part * o {@see ReceivedHeader::getFromAddress()} -- the adddress portion of the FROM * part * o {@see ReceivedHeader::getByName()} -- same as getFromName, but for the BY * part, and etc... below * o {@see ReceivedHeader::getByHostname()} * o {@see ReceivedHeader::getByAddress()} * * The parsed parts of the FROM and BY parts are determined as follows: * * o Anything outside and before a parenthesized expression is considered "the * name", for example "FROM AlainDeBotton", "AlainDeBotton" would be the name, * but also if the name is an address, but exists outside the parenthesized * expression, it's still considered "the name". For example: * "From [1.2.3.4]", getFromName would return "[1.2.3.4]". * o A parenthesized expression MUST match what looks like either a domain name * on its own, or a domain name and an address. Otherwise the parenthesized * expression is considered a comment, and not parsed into hostname and * address. The rules are defined loosely because many implementations differ * in how strictly they follow the standard. For a domain, it's enough that * the expression starts with any alphanumeric character and contains at least * one '.', followed by any number of '.', '-' and alphanumeric characters. * The address portion must be surrounded in square brackets, and contain any * sequence of '.', ':', numbers, and characters 'a' through 'f'. In addition * the string 'ipv6' may start the expression (for instance, '[ipv6:::1]' * would be valid). A port number may also be considered valid as part of the * address, for example: [1.2.3.4:3231]. No additional validation on the * address is done, and so an invalid address such as '....' could be * returned, so users using the 'address' header are encouraged to validate it * before using it. The square brackets are parsed out of the returned * address, so the value returned by getFromAddress() would be "2.2.2.2", not * "[2.2.2.2]". * * The date/time stamp can be accessed as a DateTime object by calling * {@see ReceivedHeader::getDateTime()}. * * Parsed comments can be accessed by calling {@see * ReceivedHeader::getComments()}. Some implementations may include connection * encryption information or other details in non-standardized comments. * * @author Zaahid Bateson */ class ReceivedHeader extends ParameterHeader { /** * @var string[] an array of comments in the header. */ protected $comments = []; /** * @var DateTime the date/time stamp in the header. */ protected $date; /** * Returns a ReceivedConsumer. * * @param ConsumerService $consumerService * @return \ZBateson\MailMimeParser\Header\Consumer\AbstractConsumer */ protected function getConsumer(ConsumerService $consumerService) { return $consumerService->getReceivedConsumer(); } /** * Overridden to assign comments to $this->comments, and the DateTime to * $this->date. * * @param AbstractConsumer $consumer */ protected function setParseHeaderValue(AbstractConsumer $consumer) { parent::setParseHeaderValue($consumer); foreach ($this->parts as $part) { if ($part instanceof CommentPart) { $this->comments[] = $part->getComment(); } elseif ($part instanceof DatePart) { $this->date = $part->getDateTime(); } } } /** * Returns the raw, unparsed header value, same as {@see * ReceivedHeader::getRawValue()}. * * @return string */ public function getValue() { return $this->rawValue; } /** * Returns the name identified in the FROM part of the header. * * The returned value may either be a name or an address in the form * "[1.2.3.4]". Validation is not performed on this value, and so whatever * exists in this position is returned -- be it contains spaces, or invalid * characters, etc... * * @return string */ public function getFromName() { return (isset($this->parameters['from'])) ? $this->parameters['from']->getEhloName() : null; } /** * Returns the hostname part of a parenthesized FROM part. * * For example, "FROM name (host.name)" would return the string "host.name". * Validation of the hostname is not performed, and the returned value may * not be valid. More details on how the value is parsed and extracted can * be found in the class description for {@see ReceivedHeader}. * * @return string */ public function getFromHostname() { return (isset($this->parameters['from'])) ? $this->parameters['from']->getHostname() : null; } /** * Returns the address part of a parenthesized FROM part. * * For example, "FROM name ([1.2.3.4])" would return the string "1.2.3.4". * Validation of the address is not performed, and the returned value may * not be valid. More details on how the value is parsed and extracted can * be found in the class description for {@see ReceivedHeader}. * * @return string */ public function getFromAddress() { return (isset($this->parameters['from'])) ? $this->parameters['from']->getAddress() : null; } /** * Returns the name identified in the BY part of the header. * * The returned value may either be a name or an address in the form * "[1.2.3.4]". Validation is not performed on this value, and so whatever * exists in this position is returned -- be it contains spaces, or invalid * characters, etc... * * @return string */ public function getByName() { return (isset($this->parameters['by'])) ? $this->parameters['by']->getEhloName() : null; } /** * Returns the hostname part of a parenthesized BY part. * * For example, "BY name (host.name)" would return the string "host.name". * Validation of the hostname is not performed, and the returned value may * not be valid. More details on how the value is parsed and extracted can * be found in the class description for {@see ReceivedHeader}. * * @return string */ public function getByHostname() { return (isset($this->parameters['by'])) ? $this->parameters['by']->getHostname() : null; } /** * Returns the address part of a parenthesized BY part. * * For example, "BY name ([1.2.3.4])" would return the string "1.2.3.4". * Validation of the address is not performed, and the returned value may * not be valid. More details on how the value is parsed and extracted can * be found in the class description for {@see ReceivedHeader}. * * @return string */ public function getByAddress() { return (isset($this->parameters['by'])) ? $this->parameters['by']->getAddress() : null; } /** * Returns an array of comments parsed from the header. If there are no * comments in the header, an empty array is returned. * * @return string[] */ public function getComments() { return $this->comments; } /** * Returns the date/time stamp for the received header. * * @return \DateTime */ public function getDateTime() { return $this->date; } } mail-mime-parser/src/Header/HeaderFactory.php 0000644 00000011426 14736103046 0015125 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header; use ZBateson\MailMimeParser\Header\Consumer\ConsumerService; use ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory; /** * Constructs various AbstractHeader types depending on the type of header * passed. * * If the passed header resolves to a specific defined header type, it is parsed * as such. Otherwise, a GenericHeader is instantiated and returned. Headers * are mapped as follows: * * AddressHeader: From, To, Cc, Bcc, Sender, Reply-To, Resent-From, Resent-To, * Resent-Cc, Resent-Bcc, Resent-Reply-To, Delivered-To, Return-Path * DateHeader: Date, Resent-Date, Delivery-Date, Expires, Expiry-Date, Reply-By * ParameterHeader: Content-Type, Content-Disposition * IdHeader: Message-ID, Content-ID, In-Reply-To, References * ReceivedHeader: Received * * @author Zaahid Bateson */ class HeaderFactory { /** * @var ConsumerService the passed ConsumerService providing * AbstractConsumer singletons. */ protected $consumerService; /** * @var \ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory for * mime decoding. */ protected $mimeLiteralPartFactory; /** * @var string[][] maps AbstractHeader types to headers. */ protected $types = [ 'ZBateson\MailMimeParser\Header\AddressHeader' => [ 'from', 'to', 'cc', 'bcc', 'sender', 'replyto', 'resentfrom', 'resentto', 'resentcc', 'resentbcc', 'resentreplyto', 'returnpath', 'deliveredto', ], 'ZBateson\MailMimeParser\Header\DateHeader' => [ 'date', 'resentdate', 'deliverydate', 'expires', 'expirydate', 'replyby', ], 'ZBateson\MailMimeParser\Header\ParameterHeader' => [ 'contenttype', 'contentdisposition', ], 'ZBateson\MailMimeParser\Header\SubjectHeader' => [ 'subject', ], 'ZBateson\MailMimeParser\Header\IdHeader' => [ 'messageid', 'contentid', 'inreplyto', 'references' ], 'ZBateson\MailMimeParser\Header\ReceivedHeader' => [ 'received' ] ]; /** * @var string Defines the generic AbstractHeader type to use for headers * that aren't mapped in $types */ protected $genericType = 'ZBateson\MailMimeParser\Header\GenericHeader'; /** * Instantiates member variables with the passed objects. * * @param ConsumerService $consumerService * @param MimeLiteralPartFactory $mimeLiteralPartFactory */ public function __construct(ConsumerService $consumerService, MimeLiteralPartFactory $mimeLiteralPartFactory) { $this->consumerService = $consumerService; $this->mimeLiteralPartFactory = $mimeLiteralPartFactory; } /** * Returns the string in lower-case, and with non-alphanumeric characters * stripped out. * * @param string $header * @return string */ public function getNormalizedHeaderName($header) { return preg_replace('/[^a-z0-9]/', '', strtolower($header)); } /** * Returns the name of an AbstractHeader class for the passed header name. * * @param string $name * @return string */ private function getClassFor($name) { $test = $this->getNormalizedHeaderName($name); foreach ($this->types as $class => $matchers) { foreach ($matchers as $matcher) { if ($test === $matcher) { return $class; } } } return $this->genericType; } /** * Creates an AbstractHeader instance for the passed header name and value, * and returns it. * * @param string $name * @param string $value * @return \ZBateson\MailMimeParser\Header\AbstractHeader */ public function newInstance($name, $value) { $class = $this->getClassFor($name); if (is_a($class, 'ZBateson\MailMimeParser\Header\MimeEncodedHeader', true)) { return new $class( $this->mimeLiteralPartFactory, $this->consumerService, $name, $value ); } return new $class($this->consumerService, $name, $value); } /** * Creates and returns a HeaderContainer. * * @return HeaderContainer; */ public function newHeaderContainer() { return new HeaderContainer($this); } } mail-mime-parser/src/Header/HeaderContainer.php 0000644 00000020754 14736103046 0015444 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header; use ArrayIterator; use IteratorAggregate; use ZBateson\MailMimeParser\Header\HeaderFactory; /** * Maintains a collection of headers for a part. * * @author Zaahid Bateson */ class HeaderContainer implements IteratorAggregate { /** * @var HeaderFactory the HeaderFactory object used for created headers */ protected $headerFactory; /** * @var string[][] Each element in the array is an array with its first * element set to the header's name, and the second its value. */ private $headers = []; /** * @var \ZBateson\MailMimeParser\Header\AbstractHeader[] Each element is an * AbstractHeader representing the header at the same index in the * $headers array. If an AbstractHeader has not been constructed for * the header at that index, the element would be set to null. */ private $headerObjects = []; /** * @var array Maps header names by their "normalized" (lower-cased, * non-alphanumeric characters stripped) name to an array of indexes in * the $headers array. For example: * $headerMap['contenttype] = [ 1, 4 ] * would indicate that the headers in $headers[1] and $headers[4] are * both headers with the name 'Content-Type' or 'contENTtype'. */ private $headerMap = []; /** * @var int the next index to use for $headers and $headerObjects. */ private $nextIndex = 0; /** * Constructor * * @param HeaderFactory $headerFactory */ public function __construct(HeaderFactory $headerFactory) { $this->headerFactory = $headerFactory; } /** * Returns true if the passed header exists in this collection. * * @param string $name * @param int $offset * @return boolean */ public function exists($name, $offset = 0) { $s = $this->headerFactory->getNormalizedHeaderName($name); return isset($this->headerMap[$s][$offset]); } /** * Returns an array of header indexes with names that more closely match * the passed $name if available: for instance if there are two headers in * an email, "Content-Type" and "ContentType", and the query is for a header * with the name "Content-Type", only headers that match exactly * "Content-Type" would be returned. * * @param string $name * @return int[] */ private function getAllWithOriginalHeaderNameIfSet($name) { $s = $this->headerFactory->getNormalizedHeaderName($name); if (isset($this->headerMap[$s])) { $self = $this; $filtered = array_filter($this->headerMap[$s], function ($h) use ($name, $self) { return (strcasecmp($self->headers[$h][0], $name) === 0); }); return (!empty($filtered)) ? $filtered : $this->headerMap[$s]; } return null; } /** * Returns the AbstractHeader object for the header with the given $name and * at the optional offset (defaulting to the first header in the collection * where more than one header with the same name exists). * * Note that mime headers aren't case sensitive. * * @param string $name * @param int $offset * @return \ZBateson\MailMimeParser\Header\AbstractHeader */ public function get($name, $offset = 0) { $a = $this->getAllWithOriginalHeaderNameIfSet($name); if (!empty($a) && isset($a[$offset])) { return $this->getByIndex($a[$offset]); } return null; } /** * Returns all headers with the passed name. * * @param string $name * @return \ZBateson\MailMimeParser\Header\AbstractHeader[] */ public function getAll($name) { $a = $this->getAllWithOriginalHeaderNameIfSet($name); if (!empty($a)) { $self = $this; return array_map(function ($index) use ($self) { return $self->getByIndex($index); }, $a); } return []; } /** * Returns the header in the headers array at the passed 0-based integer * index. * * @param int $index * @return \ZBateson\MailMimeParser\Header\AbstractHeader */ private function getByIndex($index) { if (!isset($this->headers[$index])) { return null; } if ($this->headerObjects[$index] === null) { $this->headerObjects[$index] = $this->headerFactory->newInstance( $this->headers[$index][0], $this->headers[$index][1] ); } return $this->headerObjects[$index]; } /** * Removes the header from the collection with the passed name. Defaults to * removing the first instance of the header for a collection that contains * more than one with the same passed name. * * @param string $name * @param int $offset * @return boolean */ public function remove($name, $offset = 0) { $s = $this->headerFactory->getNormalizedHeaderName($name); if (isset($this->headerMap[$s][$offset])) { $index = $this->headerMap[$s][$offset]; array_splice($this->headerMap[$s], $offset, 1); unset($this->headers[$index]); unset($this->headerObjects[$index]); return true; } return false; } /** * Removes all headers that match the passed name. * * @param string $name * @return boolean */ public function removeAll($name) { $s = $this->headerFactory->getNormalizedHeaderName($name); if (!empty($this->headerMap[$s])) { foreach ($this->headerMap[$s] as $i) { unset($this->headers[$i]); unset($this->headerObjects[$i]); } $this->headerMap[$s] = []; return true; } return false; } /** * Adds the header to the collection. * * @param string $name * @param string $value */ public function add($name, $value) { $s = $this->headerFactory->getNormalizedHeaderName($name); $this->headers[$this->nextIndex] = [ $name, $value ]; $this->headerObjects[$this->nextIndex] = null; if (!isset($this->headerMap[$s])) { $this->headerMap[$s] = []; } array_push($this->headerMap[$s], $this->nextIndex); $this->nextIndex++; } /** * If a header exists with the passed name, and at the passed offset if more * than one exists, its value is updated. * * If a header with the passed name doesn't exist at the passed offset, it * is created at the next available offset (offset is ignored when adding). * * @param string $name * @param string $value * @param int $offset */ public function set($name, $value, $offset = 0) { $s = $this->headerFactory->getNormalizedHeaderName($name); if (!isset($this->headerMap[$s][$offset])) { $this->add($name, $value); return; } $i = $this->headerMap[$s][$offset]; $this->headers[$i] = [ $name, $value ]; $this->headerObjects[$i] = null; } /** * Returns an array of AbstractHeader objects representing all headers in * this collection. * * @return AbstractHeader */ public function getHeaderObjects() { return array_filter(array_map([ $this, 'getByIndex' ], array_keys($this->headers))); } /** * Returns an array of headers in this collection. Each returned element in * the array is an array with the first element set to the name, and the * second its value: * * [ * [ 'Header-Name', 'Header Value' ], * [ 'Second-Header-Name', 'Second-Header-Value' ], * // etc... * ] * * @return string[][] */ public function getHeaders() { return array_values(array_filter($this->headers)); } /** * Returns an iterator to the headers in this collection. Each returned * element is an array with its first element set to the header's name, and * the second to its value: * * [ 'Header-Name', 'Header Value' ] * * @return ArrayIterator */ public function getIterator() { return new ArrayIterator($this->getHeaders()); } } mail-mime-parser/src/Header/DateHeader.php 0000644 00000002066 14736103046 0014373 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header; use ZBateson\MailMimeParser\Header\Consumer\ConsumerService; use ZBateson\MailMimeParser\Header\Part\DatePart; /** * Reads a DatePart value header in either RFC 2822 or RFC 822 format. * * @author Zaahid Bateson */ class DateHeader extends AbstractHeader { /** * Returns a DateConsumer. * * @param ConsumerService $consumerService * @return \ZBateson\MailMimeParser\Header\Consumer\AbstractConsumer */ protected function getConsumer(ConsumerService $consumerService) { return $consumerService->getDateConsumer(); } /** * Convenience method returning the part's DateTime object. * * @return \DateTime */ public function getDateTime() { if (!empty($this->parts) && $this->parts[0] instanceof DatePart) { return $this->parts[0]->getDateTime(); } return null; } } mail-mime-parser/src/Header/Consumer/AddressGroupConsumer.php 0000644 00000004661 14736103046 0020321 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header\Consumer; use ZBateson\MailMimeParser\Header\Part\AddressGroupPart; /** * Parses a single group of addresses (as a named-group part of an address * header). * * Finds addresses using its AddressConsumer sub-consumer separated by commas, * and ends processing once a semi-colon is found. * * Prior to returning to its calling client, AddressGroupConsumer constructs a * single Part\AddressGroupPart object filling it with all located addresses, and * returns it. * * The AddressGroupConsumer extends AddressBaseConsumer to define start/end * tokens, token separators, and construct a Part\AddressGroupPart for returning to * clients. * * @author Zaahid Bateson */ class AddressGroupConsumer extends AddressBaseConsumer { /** * Overridden to return patterns matching the beginning and end markers of a * group address: colon and semi-colon (":" and ";") characters. * * @return string[] the patterns */ public function getTokenSeparators() { return [':', ';']; } /** * AddressGroupConsumer returns true if the passed token is a semi-colon. * * @param string $token * @return boolean false */ protected function isEndToken($token) { return ($token === ';'); } /** * AddressGroupConsumer returns true if the passed token is a colon. * * @param string $token * @return boolean false */ protected function isStartToken($token) { return ($token === ':'); } /** * Performs post-processing on parsed parts. * * AddressGroupConsumer returns an array with a single Part\AddressGroupPart * element with all email addresses from this and any sub-groups. * * @param \ZBateson\MailMimeParser\Header\Part\HeaderPart[] $parts * @return AddressGroupPart[]|array */ protected function processParts(array $parts) { $emails = []; foreach ($parts as $part) { if ($part instanceof AddressGroupPart) { $emails = array_merge($emails, $part->getAddresses()); continue; } $emails[] = $part; } $group = $this->partFactory->newAddressGroupPart($emails); return [$group]; } } mail-mime-parser/src/Header/Consumer/IdConsumer.php 0000644 00000001707 14736103046 0016251 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header\Consumer; /** * Parses a single ID from an ID header. Begins consuming on a '<' char, and * ends on a '>' char. * * @author Zaahid Bateson */ class IdConsumer extends GenericConsumer { /** * Overridden to return patterns matching the beginning part of an ID ('<' * and '>' chars). * * @return string[] the patterns */ public function getTokenSeparators() { return ['\s+', '<', '>']; } /** * Returns true for '>'. */ protected function isEndToken($token) { return ($token === '>'); } /** * Returns true for '<'. * * @param string $token * @return boolean false */ protected function isStartToken($token) { return ($token === '<'); } } mail-mime-parser/src/Header/Consumer/AddressConsumer.php 0000644 00000011436 14736103046 0017302 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header\Consumer; use ZBateson\MailMimeParser\Header\Part\HeaderPart; use ZBateson\MailMimeParser\Header\Part\Token; use ZBateson\MailMimeParser\Header\Part\AddressGroupPart; /** * Parses a single part of an address header. * * Represents a single part of a list of addresses. A part could be one email * address, or one 'group' containing multiple addresses. The consumer ends on * finding either a comma token, representing a separation between addresses, or * a semi-colon token representing the end of a group. * * A single email address may consist of just an email, or a name and an email * address. Both of these are valid examples of a From header: * - From: jonsnow@winterfell.com * - From: Jon Snow <jonsnow@winterfell.com> * * Groups must be named, for example: * - To: Winterfell: jonsnow@winterfell.com, Arya Stark <arya@winterfell.com>; * * Addresses may contain quoted parts and comments, and names may be mime-header * encoded. * * @author Zaahid Bateson */ class AddressConsumer extends AbstractConsumer { /** * Returns the following as sub-consumers: * - \ZBateson\MailMimeParser\Header\Consumer\AddressGroupConsumer * - \ZBateson\MailMimeParser\Header\Consumer\CommentConsumer * - \ZBateson\MailMimeParser\Header\Consumer\QuotedStringConsumer * * @return AbstractConsumer[] the sub-consumers */ protected function getSubConsumers() { return [ $this->consumerService->getAddressGroupConsumer(), $this->consumerService->getCommentConsumer(), $this->consumerService->getQuotedStringConsumer(), ]; } /** * Overridden to return patterns matching the beginning part of an address * in a name/address part ("<" and ">" chars), end tokens ("," and ";"), and * whitespace. * * @return string[] the patterns */ public function getTokenSeparators() { return ['<', '>', ',', ';', '\s+']; } /** * Returns true for commas and semi-colons. * * Although the semi-colon is not strictly the end token of an * AddressConsumer, it could end a parent AddressGroupConsumer. I can't * think of a valid scenario where this would be an issue, but additional * thought may be needed (and documented here). * * @param string $token * @return boolean false */ protected function isEndToken($token) { return ($token === ',' || $token === ';'); } /** * AddressConsumer is "greedy", so this always returns true. * * @param string $token * @return boolean false */ protected function isStartToken($token) { return true; } /** * Checks if the passed part represents the beginning or end of an address * part (less than/greater than characters) and either appends the value of * the part to the passed $strValue, or sets up $strName * * @param HeaderPart $part * @param string $strName * @param string $strValue */ private function processSinglePart(HeaderPart $part, &$strName, &$strValue) { $pValue = $part->getValue(); if ($part instanceof Token) { if ($pValue === '<') { $strName = $strValue; $strValue = ''; return; } elseif ($pValue === '>') { return; } } $strValue .= $pValue; } /** * Performs final processing on parsed parts. * * AddressConsumer's implementation looks for tokens representing the * beginning of an address part, to create a Part\AddressPart out of a * name/address pair, or assign the name part to a parsed Part\AddressGroupPart * returned from its AddressGroupConsumer sub-consumer. * * The returned array consists of a single element - either a * Part\AddressPart or a Part\AddressGroupPart. * * @param \ZBateson\MailMimeParser\Header\Part\HeaderPart[] $parts * @return \ZBateson\MailMimeParser\Header\Part\HeaderPart[]|array */ protected function processParts(array $parts) { $strName = ''; $strValue = ''; foreach ($parts as $part) { if ($part instanceof AddressGroupPart) { return [ $this->partFactory->newAddressGroupPart( $part->getAddresses(), $strValue ) ]; } $this->processSinglePart($part, $strName, $strValue); } return [$this->partFactory->newAddressPart($strName, $strValue)]; } } mail-mime-parser/src/Header/Consumer/CommentConsumer.php 0000644 00000007152 14736103046 0017317 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header\Consumer; use ZBateson\MailMimeParser\Header\Part\LiteralPart; use ZBateson\MailMimeParser\Header\Part\CommentPart; use Iterator; /** * Consumes all tokens within parentheses as comments. * * Parenthetical comments in mime-headers can be nested within one * another. The outer-level continues after an inner-comment ends. * Additionally, quoted-literals may exist with comments as well meaning * a parenthesis inside a quoted string would not begin or end a comment * section. * * In order to satisfy these specifications, CommentConsumer inherits * from GenericConsumer which defines CommentConsumer and * QuotedStringConsumer as sub-consumers. * * Examples: * X-Mime-Header: Some value (comment) * X-Mime-Header: Some value (comment (nested comment) still in comment) * X-Mime-Header: Some value (comment "and part of original ) comment" - * still a comment) * * @author Zaahid Bateson */ class CommentConsumer extends GenericConsumer { /** * Returns patterns matching open and close parenthesis characters * as separators. * * @return string[] the patterns */ protected function getTokenSeparators() { return ['\(', '\)']; } /** * Returns true if the token is an open parenthesis character, '('. * * @param string $token * @return bool */ protected function isStartToken($token) { return ($token === '('); } /** * Returns true if the token is a close parenthesis character, ')'. * * @param string $token * @return bool */ protected function isEndToken($token) { return ($token === ')'); } /** * Instantiates and returns Part\Token objects. Tokens from this * and sub-consumers are combined into a Part\CommentPart in * combineParts. * * @param string $token * @param bool $isLiteral * @return \ZBateson\MailMimeParser\Header\Part\HeaderPart */ protected function getPartForToken($token, $isLiteral) { return $this->partFactory->newToken($token); } /** * Calls $tokens->next() and returns. * * The default implementation checks if the current token is an end token, * and will not advance past it. Because a comment part of a header can be * nested, its implementation must advance past its own 'end' token. * * @param Iterator $tokens * @param bool $isStartToken */ protected function advanceToNextToken(Iterator $tokens, $isStartToken) { $tokens->next(); } /** * Post processing involves creating a single Part\CommentPart out of * generated parts from tokens. The Part\CommentPart is returned in an * array. * * @param \ZBateson\MailMimeParser\Header\Part\HeaderPart[] $parts * @return \ZBateson\MailMimeParser\Header\Part\HeaderPart[]|array */ protected function processParts(array $parts) { $comment = ''; foreach ($parts as $part) { // order is important here - CommentPart extends LiteralPart if ($part instanceof CommentPart) { $comment .= '(' . $part->getComment() . ')'; } elseif ($part instanceof LiteralPart) { $comment .= '"' . $part->getValue() . '"'; } else { $comment .= $part->getValue(); } } return [$this->partFactory->newCommentPart($comment)]; } } mail-mime-parser/src/Header/Consumer/SubjectConsumer.php 0000644 00000006210 14736103046 0017306 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header\Consumer; use ZBateson\MailMimeParser\Header\Part\HeaderPart; use ZBateson\MailMimeParser\Header\Part\Token; use Iterator; /** * Extends GenericConsumer to remove its sub consumers. * * Prior to this, subject headers were parsed using the GenericConsumer which * meant if the subject contained text within parentheses, it would not be * included as part of the returned value in a getHeaderValue. Mime-encoded * parts within quotes would be ignored, and backslash characters denoted an * escaped character. * * From testing in ThunderBird and Outlook web mail it seems quoting parts * doesn't have an effect (e.g. quoting a "mime-literal" encoded part still * comes out decoded), and parts in parentheses (comments) are displayed * normally. * * @author Zaahid Bateson */ class SubjectConsumer extends GenericConsumer { /** * Returns an empty array * * @return AbstractConsumer[] the sub-consumers */ protected function getSubConsumers() { return []; } /** * Overridden to preserve whitespace. * * Whitespace between two words is preserved unless the whitespace begins * with a newline (\n or \r\n), in which case the entire string of * whitespace is discarded, and a single space ' ' character is used in its * place. * * @param string $token the token * @param bool $isLiteral set to true if the token represents a literal - * e.g. an escaped token * @return \ZBateson\MailMimeParser\Header\Part\HeaderPart|null the * constructed header part or null if the token should be ignored */ protected function getPartForToken($token, $isLiteral) { if ($isLiteral) { return $this->partFactory->newLiteralPart($token); } elseif (preg_match('/^\s+$/', $token)) { if (preg_match('/^[\r\n]/', $token)) { return $this->partFactory->newToken(' '); } return $this->partFactory->newToken($token); } return $this->partFactory->newInstance($token); } /** * Returns an array of \ZBateson\MailMimeParser\Header\Part\HeaderPart for * the current token on the iterator. * * Overridden from AbstractConsumer to remove special filtering for * backslash escaping, which also seems to not apply to Subject headers at * least in ThunderBird's implementation. * * @param Iterator $tokens * @return \ZBateson\MailMimeParser\Header\Part\HeaderPart[]|array */ protected function getTokenParts(Iterator $tokens) { return $this->getConsumerTokenParts($tokens); } /** * Overridden to not split out backslash characters and its next character * as a special case defined in AbastractConsumer * * @return string the regex pattern */ protected function getTokenSplitPattern() { $sChars = implode('|', $this->getAllTokenSeparators()); return '~(' . $sChars . ')~'; } } mail-mime-parser/src/Header/Consumer/QuotedStringConsumer.php 0000644 00000004006 14736103046 0020340 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header\Consumer; /** * Represents a quoted part of a header value starting at a single quote, and * ending at the next single quote. * * A quoted-pair part in a header is a literal. There are no sub-consumers for * it and a Part\LiteralPart is returned. * * Newline characters (CR and LF) are stripped entirely from the quoted part. * This is based on the example at: * * https://tools.ietf.org/html/rfc822#section-3.1.1 * * And https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html in section 7.2.1 * splitting the boundary. * * @author Zaahid Bateson */ class QuotedStringConsumer extends GenericConsumer { /** * QuotedStringConsumer doesn't have any sub-consumers. This method returns * an empty array. * * @return array */ public function getSubConsumers() { return []; } /** * Returns true if the token is a double quote. * * @param string $token * @return bool */ protected function isStartToken($token) { return ($token === '"'); } /** * Returns true if the token is a double quote. * * @param string $token * @return boolean */ protected function isEndToken($token) { return ($token === '"'); } /** * Returns a single regex pattern for a double quote. * * @return string[] */ protected function getTokenSeparators() { return ['\"']; } /** * Constructs a Part\LiteralPart and returns it. * * @param string $token * @param bool $isLiteral not used - everything in a quoted string is a * literal * @return \ZBateson\MailMimeParser\Header\Part\LiteralPart */ protected function getPartForToken($token, $isLiteral) { return $this->partFactory->newLiteralPart($token); } } mail-mime-parser/src/Header/Consumer/ReceivedConsumer.php 0000644 00000010302 14736103046 0017432 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header\Consumer; use ZBateson\MailMimeParser\Header\Part\Token; use Iterator; /** * Parses a Received header into ReceivedParts, ReceivedDomainParts, a DatePart, * and CommentParts. * * Parts that don't correspond to any of the above are discarded. * * @author Zaahid Bateson */ class ReceivedConsumer extends AbstractConsumer { /** * ReceivedConsumer doesn't have any token separators of its own. * Sub-Consumers will return separators matching 'part' word separators, for * example 'from' and 'by', and ';' for date, etc... * * @return string[] an array of regex pattern matchers */ protected function getTokenSeparators() { return []; } /** * ReceivedConsumer doesn't have an end token, and so this just returns * false. * * @param string $token * @return boolean false */ protected function isEndToken($token) { return false; } /** * ReceivedConsumer doesn't start consuming at a specific token, it's the * base handler for the Received header, and so this always returns false. * * @codeCoverageIgnore * @param string $token * @return boolean false */ protected function isStartToken($token) { return false; } /** * Returns two {@see Received/DomainConsumer} instances, with FROM and BY as * part names, and 4 {@see Received/GenericReceivedConsumer} instances for * VIA, WITH, ID, and FOR part names, and * 1 {@see Received/ReceivedDateConsumer} for the date/time stamp, and one * {@see CommentConsumer} to consume any comments. * * @return AbstractConsumer[] the sub-consumers */ protected function getSubConsumers() { return [ $this->consumerService->getSubReceivedConsumer('from'), $this->consumerService->getSubReceivedConsumer('by'), $this->consumerService->getSubReceivedConsumer('via'), $this->consumerService->getSubReceivedConsumer('with'), $this->consumerService->getSubReceivedConsumer('id'), $this->consumerService->getSubReceivedConsumer('for'), $this->consumerService->getSubReceivedConsumer('date'), $this->consumerService->getCommentConsumer() ]; } /** * Overridden to exclude the MimeLiteralPart pattern that comes by default * in AbstractConsumer. * * @return string the regex pattern */ protected function getTokenSplitPattern() { $sChars = implode('|', $this->getAllTokenSeparators()); return '~(' . $sChars . ')~'; } /** * Overridden to /not/ advance when the end token matches a start token for * a sub-consumer. * * @param Iterator $tokens * @param bool $isStartToken */ protected function advanceToNextToken(Iterator $tokens, $isStartToken) { if ($isStartToken) { $tokens->next(); } elseif ($tokens->valid() && !$this->isEndToken($tokens->current())) { foreach ($this->getSubConsumers() as $consumer) { if ($consumer->isStartToken($tokens->current())) { return; } } $tokens->next(); } } /** * Overridden to combine all part values into a single string and return it * as an array with a single element. * * @param \ZBateson\MailMimeParser\Header\Part\HeaderPart[] $parts * @return \ZBateson\MailMimeParser\Header\Part\HeaderPart[]| * \ZBateson\MailMimeParser\Header\Part\ReceivedDomainPart[]| * \ZBateson\MailMimeParser\Header\Part\ReceivedPart[]| * \ZBateson\MailMimeParser\Header\Part\DatePart[]| * \ZBateson\MailMimeParser\Header\Part\CommentPart[]|array */ protected function processParts(array $parts) { $ret = []; foreach ($parts as $part) { if ($part instanceof Token) { continue; } $ret[] = $part; } return $ret; } } mail-mime-parser/src/Header/Consumer/IdBaseConsumer.php 0000644 00000005717 14736103046 0017051 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header\Consumer; use ZBateson\MailMimeParser\Header\Part\CommentPart; /** * Serves as a base-consumer for ID headers (like Message-ID and Content-ID). * * IdBaseConsumer handles invalidly-formatted IDs not within '<' and '>' * characters. Processing for validly-formatted IDs are passed on to its * sub-consumer, IdConsumer. * * @author Zaahid Bateson */ class IdBaseConsumer extends AbstractConsumer { /** * Returns the following as sub-consumers: * - \ZBateson\MailMimeParser\Header\Consumer\CommentConsumer * - \ZBateson\MailMimeParser\Header\Consumer\QuotedStringConsumer * - \ZBateson\MailMimeParser\Header\Consumer\IdConsumer * * @return AbstractConsumer[] the sub-consumers */ protected function getSubConsumers() { return [ $this->consumerService->getCommentConsumer(), $this->consumerService->getQuotedStringConsumer(), $this->consumerService->getIdConsumer() ]; } /** * Returns '\s+' as a whitespace separator. * * @return string[] an array of regex pattern matchers */ protected function getTokenSeparators() { return ['\s+']; } /** * IdBaseConsumer doesn't have start/end tokens, and so always returns * false. * * @param string $token * @return boolean false */ protected function isEndToken($token) { return false; } /** * IdBaseConsumer doesn't have start/end tokens, and so always returns * false. * * @codeCoverageIgnore * @param string $token * @return boolean false */ protected function isStartToken($token) { return false; } /** * Returns null for whitespace, and LiteralPart for anything else. * * @param string $token the token * @param bool $isLiteral set to true if the token represents a literal - * e.g. an escaped token * @return \ZBateson\MailMimeParser\Header\Part\HeaderPart|null the * constructed header part or null if the token should be ignored */ protected function getPartForToken($token, $isLiteral) { if (preg_match('/^\s+$/', $token)) { return null; } return $this->partFactory->newLiteralPart($token); } /** * Overridden to filter out any found CommentPart objects. * * @param \ZBateson\MailMimeParser\Header\Part\HeaderPart[] $parts * @return \ZBateson\MailMimeParser\Header\Part\HeaderPart[] */ protected function processParts(array $parts) { return array_values(array_filter($parts, function ($part) { if (empty($part) || $part instanceof CommentPart) { return false; } return true; })); } } mail-mime-parser/src/Header/Consumer/GenericConsumer.php 0000644 00000014355 14736103046 0017274 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header\Consumer; use ZBateson\MailMimeParser\Header\Part\HeaderPart; use ZBateson\MailMimeParser\Header\Part\Token; /** * A minimal implementation of AbstractConsumer defining a CommentConsumer and * QuotedStringConsumer as sub-consumers, and splitting tokens by whitespace. * * Note that GenericConsumer should be instantiated with a * MimeLiteralPartFactory instead of a HeaderPartFactory. Sub-classes may not * need MimeLiteralPartFactory instances though. * * @author Zaahid Bateson */ class GenericConsumer extends AbstractConsumer { /** * Returns \ZBateson\MailMimeParser\Header\Consumer\CommentConsumer and * \ZBateson\MailMimeParser\Header\Consumer\QuotedStringConsumer as * sub-consumers. * * @return AbstractConsumer[] the sub-consumers */ protected function getSubConsumers() { return [ $this->consumerService->getCommentConsumer(), $this->consumerService->getQuotedStringConsumer(), ]; } /** * Returns the regex '\s+' (whitespace) pattern matcher as a token marker so * the header value is split along whitespace characters. GenericConsumer * filters out whitespace-only tokens from getPartForToken. * * The whitespace character delimits mime-encoded parts for decoding. * * @return string[] an array of regex pattern matchers */ protected function getTokenSeparators() { return ['\s+']; } /** * GenericConsumer doesn't have start/end tokens, and so always returns * false. * * @param string $token * @return boolean false */ protected function isEndToken($token) { return false; } /** * GenericConsumer doesn't have start/end tokens, and so always returns * false. * * @codeCoverageIgnore * @param string $token * @return boolean false */ protected function isStartToken($token) { return false; } /** * Returns true if a space should be added based on the passed last and next * parts. * * @param \ZBateson\MailMimeParser\Header\Part\HeaderPart $nextPart * @param \ZBateson\MailMimeParser\Header\Part\HeaderPart $lastPart * @return bool */ private function shouldAddSpace(HeaderPart $nextPart, HeaderPart $lastPart) { return (!$lastPart->ignoreSpacesAfter() || !$nextPart->ignoreSpacesBefore()); } /** * Loops over the $parts array from the current position, checks if the * space should be added, then adds it to $retParts and returns. * * @param \ZBateson\MailMimeParser\Header\Part\HeaderPart[] $parts * @param \ZBateson\MailMimeParser\Header\Part\HeaderPart[] $retParts * @param int $curIndex * @param \ZBateson\MailMimeParser\Header\Part\HeaderPart $spacePart * @param \ZBateson\MailMimeParser\Header\Part\HeaderPart $lastPart */ private function addSpaceToRetParts( array $parts, array &$retParts, $curIndex, HeaderPart &$spacePart, HeaderPart $lastPart ) { $nextPart = $parts[$curIndex]; if ($this->shouldAddSpace($nextPart, $lastPart)) { $retParts[] = $spacePart; $spacePart = null; } } /** * Checks if the passed space part should be added to the returned parts and * adds it. * * Never adds a space if it's the first part, otherwise only add it if * either part isn't set to ignore the space * * @param \ZBateson\MailMimeParser\Header\Part\HeaderPart[] $parts * @param \ZBateson\MailMimeParser\Header\Part\HeaderPart[] $retParts * @param int $curIndex * @param \ZBateson\MailMimeParser\Header\Part\HeaderPart $spacePart */ private function addSpaces(array $parts, array &$retParts, $curIndex, HeaderPart &$spacePart = null) { $lastPart = end($retParts); if ($spacePart !== null && $curIndex < count($parts) && $parts[$curIndex]->getValue() !== '' && $lastPart !== false) { $this->addSpaceToRetParts($parts, $retParts, $curIndex, $spacePart, $lastPart); } } /** * Returns true if the passed HeaderPart is a Token instance and a space. * * @param HeaderPart $part * @return bool */ private function isSpaceToken(HeaderPart $part) { return ($part instanceof Token && $part->isSpace()); } /** * Filters out ignorable spaces between parts in the passed array. * * Spaces with parts on either side of it that specify they can be ignored * are filtered out. filterIgnoredSpaces is called from within * processParts, and if needed by an implementing class that overrides * processParts, must be specifically called. * * @param \ZBateson\MailMimeParser\Header\Part\HeaderPart[] $parts * @return \ZBateson\MailMimeParser\Header\Part\HeaderPart[] */ protected function filterIgnoredSpaces(array $parts) { $partsFiltered = array_values(array_filter($parts)); $retParts = []; $spacePart = null; $count = count($partsFiltered); for ($i = 0; $i < $count; ++$i) { $part = $partsFiltered[$i]; if ($this->isSpaceToken($part)) { $spacePart = $part; continue; } $this->addSpaces($partsFiltered, $retParts, $i, $spacePart); $retParts[] = $part; } // ignore trailing spaces return $retParts; } /** * Overridden to combine all part values into a single string and return it * as an array with a single element. * * @param \ZBateson\MailMimeParser\Header\Part\HeaderPart[] $parts * @return \ZBateson\MailMimeParser\Header\Part\LiteralPart[]|array */ protected function processParts(array $parts) { $strValue = ''; $filtered = $this->filterIgnoredSpaces($parts); foreach ($filtered as $part) { $strValue .= $part->getValue(); } return [$this->partFactory->newLiteralPart($strValue)]; } } mail-mime-parser/src/Header/Consumer/Received/ReceivedDateConsumer.php 0000644 00000001712 14736103046 0021763 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header\Consumer\Received; use ZBateson\MailMimeParser\Header\Consumer\DateConsumer; /** * Parses the date portion of a Received header into a DatePart. * * The only difference between DateConsumer and ReceivedDateConsumer is the * addition of a start token, ';', and a token separator (also ';'). * * @author Zaahid Bateson */ class ReceivedDateConsumer extends DateConsumer { /** * Returns true if the token is a ';' * * @param string $token * @return boolean */ protected function isStartToken($token) { return ($token === ';'); } /** * Returns an array containing ';'. * * @return string[] an array of regex pattern matchers */ protected function getTokenSeparators() { return [';']; } } mail-mime-parser/src/Header/Consumer/Received/GenericReceivedConsumer.php 0000644 00000011143 14736103046 0022461 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header\Consumer\Received; use ZBateson\MailMimeParser\Header\Consumer\ConsumerService; use ZBateson\MailMimeParser\Header\Part\HeaderPartFactory; use ZBateson\MailMimeParser\Header\Consumer\GenericConsumer; use ZBateson\MailMimeParser\Header\Part\CommentPart; /** * Consumes simple literal strings for parts of a Received header. * * Starts consuming when the initialized $partName string is located, for * instance when initialized with "FROM", will start consuming on " FROM" or * "FROM ". * * The consumer ends when any possible "Received" header part is found, namely * on one of the following tokens: from, by, via, with, id, for, or when the * start token for the date stamp is found, ';'. * * The consumer allows comments in and around the consumer... although the * Received header specification only allows them before a part, for example, * technically speaking this is valid: * * "FROM machine (host) (comment) BY machine" * * However, this is not: * * "FROM machine (host) BY machine WITH (comment) ESMTP" * * The consumer will allow both. * * @author Zaahid Bateson */ class GenericReceivedConsumer extends GenericConsumer { /** * @var string the current part name being parsed. */ protected $partName; /** * Constructor overridden to include $partName parameter. * * @param ConsumerService $consumerService * @param HeaderPartFactory $partFactory * @param string $partName */ public function __construct(ConsumerService $consumerService, HeaderPartFactory $partFactory, $partName) { parent::__construct($consumerService, $partFactory); $this->partName = $partName; } /** * Returns the name of the part being parsed. * * This is always the lower-case name provided to the constructor, not the * actual string that started the consumer, which could be in any case. * * @return string */ protected function getPartName() { return $this->partName; } /** * Overridden to return a CommentConsumer. * * @return AbstractConsumer[] the sub-consumers */ protected function getSubConsumers() { return [ $this->consumerService->getCommentConsumer() ]; } /** * Returns true if the passed token matches (case-insensitively) * $this->getPartName() with optional whitespace surrounding it. * * @param string $token * @return bool */ protected function isStartToken($token) { $pattern = '/^\s*(' . preg_quote($this->getPartName(), '/') . ')\s*$/i'; return (preg_match($pattern, $token) === 1); } /** * Returns true if the token matches (case-insensitively) any of the * following, with optional surrounding whitespace: * * o from * o by * o via * o with * o id * o for * o ; * * @param string $token * @return boolean */ protected function isEndToken($token) { return (preg_match('/^\s*(from|by|via|with|id|for|;)\s*$/i', $token) === 1); } /** * Returns a whitespace separator (for filtering ignorable whitespace * between parts), and a separator matching the current part name as * returned by $this->getPartName(). * * @return string[] an array of regex pattern matchers */ protected function getTokenSeparators() { return [ '\s+', '(\A\s*)?(?i)' . preg_quote($this->getPartName(), '/') . '(?-i)\s+' ]; } /** * Overridden to combine all part values into a single string and return it * as the first element, followed by any comment elements as subsequent * elements. * * @param \ZBateson\MailMimeParser\Header\Part\HeaderPart[] $parts * @return \ZBateson\MailMimeParser\Header\Part\HeaderPart[]| * \ZBateson\MailMimeParser\Header\Part\CommentPart[]| * array */ protected function processParts(array $parts) { $strValue = ''; $ret = []; $filtered = $this->filterIgnoredSpaces($parts); foreach ($filtered as $part) { if ($part instanceof CommentPart) { $ret[] = $part; continue; // getValue() is empty anyway, but for clarity... } $strValue .= $part->getValue(); } array_unshift($ret, $this->partFactory->newReceivedPart($this->getPartName(), $strValue)); return $ret; } } mail-mime-parser/src/Header/Consumer/Received/DomainConsumer.php 0000644 00000011006 14736103046 0020643 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header\Consumer\Received; use ZBateson\MailMimeParser\Header\Part\CommentPart; /** * Parses a so-called "extended-domain" (from and by) part of a Received header. * * Looks for and extracts the following fields from an extended-domain part: * Name, Hostname and Address. * * The Name part is always the portion of the extended-domain part existing on * its own, outside of the parenthesized hostname and address part. This is * true regardless of whether an address is used as the name, as its assumed to * be the string used to identify the server, whatever it may be. * * The parenthesized part normally (but not necessarily) following a name must * "look like" a tcp-info section of an extended domain as defined by RFC5321. * The validation is very purposefully very loose to be accommodating to many * erroneous implementations. Strictly speaking, a domain part, if it exists, * must start with an alphanumeric character. There must be at least one '.' in * the domain part, followed by any number of more alphanumeric, '.', and '-' * characters. The address part must be within square brackets, '[]'... * although an address outside of square brackets could be matched by the domain * matcher if it exists alone within the parentheses. The address, strictly * speaking, is any number of '.', numbers, ':' and letters a-f. This allows it * to match ipv6 addresses as well. In addition, the address may start with the * string "ipv6", and may be followed by a port number as some implementations * seem to do. * * Strings in parentheses not matching the aforementioned 'domain/address' * pattern will be considered comments, and will be returned as a separate * CommentPart. * * @see https://tools.ietf.org/html/rfc5321#section-4.4 * @see https://github.com/Te-k/pyreceived/blob/master/test.py * @author Zaahid Bateson */ class DomainConsumer extends GenericReceivedConsumer { /** * Overridden to return true if the passed token is a closing parenthesis. * * @param string $token * @return bool */ protected function isEndToken($token) { if ($token === ')') { return true; } return parent::isEndToken($token); } /** * Attempts to match a parenthesized expression to find a hostname and an * address. Returns true if the expression matched, and either hostname or * address were found. * * @param string $value * @param string $hostname * @param string $address * @return boolean */ private function matchHostPart($value, &$hostname, &$address) { $matches = []; $pattern = '~^(?P<name>[a-z0-9\-]+\.[a-z0-9\-\.]+)?\s*(\[(IPv[64])?(?P<addr>[a-f\d\.\:]+)\])?$~i'; if (preg_match($pattern, $value, $matches)) { if (!empty($matches['name'])) { $hostname = $matches['name']; } if (!empty($matches['addr'])) { $address = $matches['addr']; } return true; } return false; } /** * Creates a single ReceivedDomainPart out of matched parts. If an * unmatched parenthesized expression was found, it's returned as a * CommentPart. * * @param \ZBateson\MailMimeParser\Header\Part\HeaderPart[] $parts * @return \ZBateson\MailMimeParser\Header\Part\ReceivedDomainPart[]| * \ZBateson\MailMimeParser\Header\Part\CommentPart[]array */ protected function processParts(array $parts) { $ehloName = null; $hostname = null; $address = null; $commentPart = null; $filtered = $this->filterIgnoredSpaces($parts); foreach ($filtered as $part) { if ($part instanceof CommentPart) { $commentPart = $part; continue; } $ehloName .= $part->getValue(); } $strValue = $ehloName; if ($commentPart !== null && $this->matchHostPart($commentPart->getComment(), $hostname, $address)) { $strValue .= ' (' . $commentPart->getComment() . ')'; $commentPart = null; } $domainPart = $this->partFactory->newReceivedDomainPart( $this->getPartName(), $strValue, $ehloName, $hostname, $address ); return array_filter([ $domainPart, $commentPart ]); } } mail-mime-parser/src/Header/Consumer/AddressBaseConsumer.php 0000644 00000005664 14736103046 0020103 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header\Consumer; use Iterator; /** * Serves as a base-consumer for recipient/sender email address headers (like * From and To). * * AddressBaseConsumer passes on token processing to its sub-consumer, an * AddressConsumer, and collects Part\AddressPart objects processed and returned * by AddressConsumer. * * @author Zaahid Bateson */ class AddressBaseConsumer extends AbstractConsumer { /** * Returns \ZBateson\MailMimeParser\Header\Consumer\AddressConsumer as a * sub-consumer. * * @return AbstractConsumer[] the sub-consumers */ protected function getSubConsumers() { return [ $this->consumerService->getAddressConsumer() ]; } /** * Returns an empty array. * * @return string[] an array of regex pattern matchers */ protected function getTokenSeparators() { return []; } /** * Disables advancing for start tokens. * * The start token for AddressBaseConsumer is part of an AddressPart (or a * sub-consumer) and so must be passed on. * * @param Iterator $tokens * @param bool $isStartToken */ protected function advanceToNextToken(Iterator $tokens, $isStartToken) { if ($isStartToken) { return; } parent::advanceToNextToken($tokens, $isStartToken); } /** * AddressBaseConsumer doesn't have start/end tokens, and so always returns * false. * * @param string $token * @return boolean false */ protected function isEndToken($token) { return false; } /** * AddressBaseConsumer doesn't have start/end tokens, and so always returns * false. * * @codeCoverageIgnore * @param string $token * @return boolean false */ protected function isStartToken($token) { return false; } /** * Overridden so tokens aren't handled at this level, and instead are passed * on to AddressConsumer. * * @param Iterator $tokens * @return \ZBateson\MailMimeParser\Header\Part\HeaderPart[]|array */ protected function getTokenParts(Iterator $tokens) { return $this->getConsumerTokenParts($tokens); } /** * Never reached by AddressBaseConsumer. Overridden to satisfy * AbstractConsumer. * * @codeCoverageIgnore * @param string $token the token * @param bool $isLiteral set to true if the token represents a literal - * e.g. an escaped token * @return \ZBateson\MailMimeParser\Header\Part\HeaderPart the constructed * header part or null if the token should be ignored */ protected function getPartForToken($token, $isLiteral) { return null; } } mail-mime-parser/src/Header/Consumer/AbstractConsumer.php 0000644 00000026302 14736103046 0017456 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header\Consumer; use ZBateson\MailMimeParser\Header\Consumer\ConsumerService; use ZBateson\MailMimeParser\Header\Part\HeaderPartFactory; use ZBateson\MailMimeParser\Header\Part\MimeLiteralPart; use ArrayIterator; use Iterator; use NoRewindIterator; /** * Abstract base class for all header token consumers. * * Defines the base parser that loops over tokens, consuming them and creating * header parts. * * @author Zaahid Bateson */ abstract class AbstractConsumer { /** * @var \ZBateson\MailMimeParser\Header\Consumer\ConsumerService used to * get consumer instances for sub-consumers */ protected $consumerService; /** * @var \ZBateson\MailMimeParser\Header\Part\HeaderPartFactory used to construct * HeaderPart objects */ protected $partFactory; /** * Initializes the instance. * * @param ConsumerService $consumerService * @param HeaderPartFactory $partFactory */ public function __construct(ConsumerService $consumerService, HeaderPartFactory $partFactory) { $this->consumerService = $consumerService; $this->partFactory = $partFactory; } /** * Returns the singleton instance for the class. * * @param ConsumerService $consumerService * @param HeaderPartFactory $partFactory */ public static function getInstance(ConsumerService $consumerService, HeaderPartFactory $partFactory) { static $instances = []; $class = get_called_class(); if (!isset($instances[$class])) { $instances[$class] = new static($consumerService, $partFactory); } return $instances[$class]; } /** * Invokes parsing of a header's value into header parts. * * @param string $value the raw header value * @return \ZBateson\MailMimeParser\Header\Part\HeaderPart[] the array of parsed * parts */ public function __invoke($value) { if ($value !== '') { return $this->parseRawValue($value); } return []; } /** * Called during construction to set up the list of sub-consumers that will * take control from this consumer should a token match a sub-consumer's * start token. * * @return AbstractConsumer[] the array of consumers */ abstract protected function getSubConsumers(); /** * Returns this consumer and all unique sub consumers. * * Loops into the sub-consumers (and their sub-consumers, etc...) finding * all unique consumers, and returns them in an array. * * @return \ZBateson\MailMimeParser\Header\AbstractConsumer[] */ protected function getAllConsumers() { $found = [$this]; do { $current = current($found); $subConsumers = $current->getSubConsumers(); foreach ($subConsumers as $consumer) { if (!in_array($consumer, $found)) { $found[] = $consumer; } } } while (next($found) !== false); return $found; } /** * Called by __invoke to parse the raw header value into header parts. * * Calls splitTokens to split the value into token part strings, then calls * parseParts to parse the returned array. * * @param string $value * @return \ZBateson\MailMimeParser\Header\Part\HeaderPart[] the array of parsed * parts */ private function parseRawValue($value) { $tokens = $this->splitRawValue($value); return $this->parseTokensIntoParts(new NoRewindIterator(new ArrayIterator($tokens))); } /** * Returns an array of regular expression separators specific to this * consumer. The returned patterns are used to split the header value into * tokens for the consumer to parse into parts. * * Each array element makes part of a generated regular expression that is * used in a call to preg_split(). RegEx patterns can be used, and care * should be taken to escape special characters. * * @return string[] the array of patterns */ abstract protected function getTokenSeparators(); /** * Returns a list of regular expression markers for this consumer and all * sub-consumers by calling 'getTokenSeparators'.. * * @return string[] an array of regular expression markers */ protected function getAllTokenSeparators() { $markers = $this->getTokenSeparators(); $subConsumers = $this->getAllConsumers(); foreach ($subConsumers as $consumer) { $markers = array_merge($consumer->getTokenSeparators(), $markers); } return array_unique($markers); } /** * Returns a regex pattern used to split the input header string. The * default implementation calls getAllTokenSeparators and implodes the * returned array with the regex OR '|' character as its glue. * * @return string the regex pattern */ protected function getTokenSplitPattern() { $sChars = implode('|', $this->getAllTokenSeparators()); $mimePartPattern = MimeLiteralPart::MIME_PART_PATTERN; return '~(' . $mimePartPattern . '|\\\\.|' . $sChars . ')~'; } /** * Returns an array of split tokens from the input string. * * The method calls preg_split using getTokenSplitPattern. The split * array will not contain any empty parts and will contain the markers. * * @param string $rawValue the raw string * @return array the array of tokens */ protected function splitRawValue($rawValue) { return preg_split( $this->getTokenSplitPattern(), $rawValue, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY ); } /** * Returns true if the passed string token marks the beginning marker for * the current consumer. * * @param string $token the current token * @return bool */ abstract protected function isStartToken($token); /** * Returns true if the passed string token marks the end marker for the * current consumer. * * @param string $token the current token * @return bool */ abstract protected function isEndToken($token); /** * Constructs and returns a \ZBateson\MailMimeParser\Header\Part\HeaderPart * for the passed string token. If the token should be ignored, the * function must return null. * * The default created part uses the instance's partFactory->newInstance * method. * * @param string $token the token * @param bool $isLiteral set to true if the token represents a literal - * e.g. an escaped token * @return \ZBateson\MailMimeParser\Header\Part\HeaderPart|null the * constructed header part or null if the token should be ignored */ protected function getPartForToken($token, $isLiteral) { if ($isLiteral) { return $this->partFactory->newLiteralPart($token); } elseif (preg_match('/^\s+$/', $token)) { return $this->partFactory->newToken(' '); } return $this->partFactory->newInstance($token); } /** * Iterates through this consumer's sub-consumers checking if the current * token triggers a sub-consumer's start token and passes control onto that * sub-consumer's parseTokenIntoParts. If no sub-consumer is responsible * for the current token, calls getPartForToken and returns it in an array. * * @param Iterator $tokens * @return \ZBateson\MailMimeParser\Header\Part\HeaderPart[]|array */ protected function getConsumerTokenParts(Iterator $tokens) { $token = $tokens->current(); $subConsumers = $this->getSubConsumers(); foreach ($subConsumers as $consumer) { if ($consumer->isStartToken($token)) { $this->advanceToNextToken($tokens, true); return $consumer->parseTokensIntoParts($tokens); } } return [$this->getPartForToken($token, false)]; } /** * Returns an array of \ZBateson\MailMimeParser\Header\Part\HeaderPart for * the current token on the iterator. * * If the current token is a start token from a sub-consumer, the sub- * consumer's parseTokensIntoParts method is called. * * @param Iterator $tokens * @return \ZBateson\MailMimeParser\Header\Part\HeaderPart[]|array */ protected function getTokenParts(Iterator $tokens) { $token = $tokens->current(); if (strlen($token) === 2 && $token[0] === '\\') { return [$this->getPartForToken(substr($token, 1), true)]; } return $this->getConsumerTokenParts($tokens); } /** * Determines if the iterator should be advanced to the next token after * reading tokens or finding a start token. * * The default implementation will advance for a start token, but not * advance on the end token of the current consumer, allowing the end token * to be passed up to a higher-level consumer. * * @param Iterator $tokens * @param bool $isStartToken */ protected function advanceToNextToken(Iterator $tokens, $isStartToken) { if (($isStartToken) || ($tokens->valid() && !$this->isEndToken($tokens->current()))) { $tokens->next(); } } /** * Iterates over the passed token Iterator and returns an array of parsed * \ZBateson\MailMimeParser\Header\Part\HeaderPart objects. * * The method checks each token to see if the token matches a sub-consumer's * start token, or if it matches the current consumer's end token to stop * processing. * * If a sub-consumer's start token is matched, the sub-consumer is invoked * and its returned parts are merged to the current consumer's header parts. * * After all tokens are read and an array of Header\Parts are constructed, * the array is passed to AbstractConsumer::processParts for any final * processing. * * @param Iterator $tokens an iterator over a string of tokens * @return \ZBateson\MailMimeParser\Header\Part\HeaderPart[] an array of * parsed parts */ protected function parseTokensIntoParts(Iterator $tokens) { $parts = []; while ($tokens->valid() && !$this->isEndToken($tokens->current())) { $parts = array_merge($parts, $this->getTokenParts($tokens)); $this->advanceToNextToken($tokens, false); } return $this->processParts($parts); } /** * Performs any final processing on the array of parsed parts before * returning it to the consumer client. * * The default implementation simply returns the passed array after * filtering out null/empty parts. * * @param \ZBateson\MailMimeParser\Header\Part\HeaderPart[] $parts * @return \ZBateson\MailMimeParser\Header\Part\HeaderPart[] */ protected function processParts(array $parts) { return array_values(array_filter($parts)); } } mail-mime-parser/src/Header/Consumer/ParameterConsumer.php 0000644 00000015775 14736103046 0017647 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header\Consumer; use ZBateson\MailMimeParser\Header\Part\Token; use ZBateson\MailMimeParser\Header\Part\MimeLiteralPart; use ZBateson\MailMimeParser\Header\Part\SplitParameterToken; use ArrayObject; /** * Reads headers separated into parameters consisting of a main value, and * subsequent name/value pairs - for example text/html; charset=utf-8. * * A ParameterConsumer's parts are separated by a semi-colon. Its name/value * pairs are separated with an '=' character. * * Parts may be mime-encoded entities. Additionally, a value can be quoted and * comments may exist. * * @author Zaahid Bateson */ class ParameterConsumer extends GenericConsumer { /** * Returns semi-colon and equals char as token separators. * * @return string[] */ protected function getTokenSeparators() { return [';', '=']; } /** * Overridden to use a specialized regex for finding mime-encoded parts * (RFC 2047). * * Some implementations seem to place mime-encoded parts within quoted * parameters, and split the mime-encoded parts across multiple split * parameters. The specialized regex doesn't allow double quotes inside a * mime encoded part, so it can be "continued" in another parameter. * * @return string the regex pattern */ protected function getTokenSplitPattern() { $sChars = implode('|', $this->getAllTokenSeparators()); $mimePartPattern = MimeLiteralPart::MIME_PART_PATTERN_NO_QUOTES; return '~(' . $mimePartPattern . '|\\\\.|' . $sChars . ')~'; } /** * Creates and returns a \ZBateson\MailMimeParser\Header\Part\Token out of * the passed string token and returns it, unless the token is an escaped * literal, in which case a LiteralPart is returned. * * @param string $token * @param bool $isLiteral * @return \ZBateson\MailMimeParser\Header\Part\HeaderPart */ protected function getPartForToken($token, $isLiteral) { if ($isLiteral) { return $this->partFactory->newLiteralPart($token); } return $this->partFactory->newToken($token); } /** * Adds the passed parameter with the given name and value to a * SplitParameterToken, at the passed index. If one with the given name * doesn't exist, it is created. * * @param ArrayObject $splitParts * @param string $name * @param string $value * @param int $index * @param boolean $isEncoded */ private function addToSplitPart(ArrayObject $splitParts, $name, $value, $index, $isEncoded) { $ret = null; if (!isset($splitParts[trim($name)])) { $ret = $this->partFactory->newSplitParameterToken($name); $splitParts[$name] = $ret; } $splitParts[$name]->addPart($value, $isEncoded, $index); return $ret; } /** * Instantiates and returns either a MimeLiteralPart if $strName is empty, * a SplitParameterToken if the parameter is a split parameter and is the * first in a series, null if it's a split parameter but is not the first * part in its series, or a ParameterPart is returned otherwise. * * If the part is a SplitParameterToken, it's added to the passed * $splitParts as well with its name as a key. * * @param string $strName * @param string $strValue * @param ArrayObject $splitParts * @return \ZBateson\MailMimeParser\Header\Part\MimeLiteralPart * |SplitParameterToken|\ZBateson\MailMimeParser\Header\Part\ParameterPart */ private function getPartFor($strName, $strValue, ArrayObject $splitParts) { if ($strName === '') { return $this->partFactory->newMimeLiteralPart($strValue); } elseif (preg_match('~^\s*([^\*]+)\*(\d*)(\*)?$~', $strName, $matches)) { return $this->addToSplitPart( $splitParts, $matches[1], $strValue, $matches[2], (empty($matches[2]) || !empty($matches[3])) ); } return $this->partFactory->newParameterPart($strName, $strValue); } /** * Handles parameter separator tokens during final processing. * * If the end token is found, a new HeaderPart is assigned to the passed * $combined array. If an '=' character is found, $strCat is assigned to * $strName and emptied. * * Returns true if the token was processed, and false otherwise. * * @param string $tokenValue * @param ArrayObject $combined * @param ArrayObject $splitParts * @param string $strName * @param string $strCat * @return boolean */ private function processTokenPart( $tokenValue, ArrayObject $combined, ArrayObject $splitParts, &$strName, &$strCat ) { if ($tokenValue === ';') { $combined[] = $this->getPartFor($strName, $strCat, $splitParts); $strName = ''; $strCat = ''; return true; } elseif ($tokenValue === '=' && $strCat !== '') { $strName = $strCat; $strCat = ''; return true; } return false; } /** * Loops over parts in the passed array, creating ParameterParts out of any * parsed SplitParameterTokens, replacing them in the array. * * The method then calls filterIgnoreSpaces to filter out empty elements in * the combined array and returns an array. * * @param ArrayObject $combined * @return HeaderPart[]|array */ private function finalizeParameterParts(ArrayObject $combined) { foreach ($combined as $key => $part) { if ($part instanceof SplitParameterToken) { $combined[$key] = $this->partFactory->newParameterPart( $part->getName(), $part->getValue(), $part->getLanguage() ); } } return $this->filterIgnoredSpaces($combined->getArrayCopy()); } /** * Post processing involves creating Part\LiteralPart or Part\ParameterPart * objects out of created Token and LiteralParts. * * @param HeaderPart[] $parts * @return HeaderPart[]|array */ protected function processParts(array $parts) { $combined = new ArrayObject(); $splitParts = new ArrayObject(); $strCat = ''; $strName = ''; $parts[] = $this->partFactory->newToken(';'); foreach ($parts as $part) { $pValue = $part->getValue(); if ($part instanceof Token && $this->processTokenPart($pValue, $combined, $splitParts, $strName, $strCat)) { continue; } $strCat .= $pValue; } return $this->finalizeParameterParts($combined); } } mail-mime-parser/src/Header/Consumer/DateConsumer.php 0000644 00000002341 14736103046 0016565 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header\Consumer; /** * Parses a date header into a Part\DatePart taking care of comment and quoted * parts as necessary. * * @author Zaahid Bateson */ class DateConsumer extends GenericConsumer { /** * Returns a Part\LiteralPart for the current token * * @param string $token * @param bool $isLiteral * @return \ZBateson\MailMimeParser\Header\Part\HeaderPart */ protected function getPartForToken($token, $isLiteral) { return $this->partFactory->newLiteralPart($token); } /** * Concatenates the passed parts and constructs a single Part\DatePart, * returning it in an array with a single element. * * @param \ZBateson\MailMimeParser\Header\Part\HeaderPart[] $parts * @return \ZBateson\MailMimeParser\Header\Part\HeaderPart[]|array */ protected function processParts(array $parts) { $strValue = ''; foreach ($parts as $part) { $strValue .= $part->getValue(); } return [$this->partFactory->newDatePart($strValue)]; } } mail-mime-parser/src/Header/Consumer/ConsumerService.php 0000644 00000014033 14736103046 0017311 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header\Consumer; use ZBateson\MailMimeParser\Header\Part\HeaderPartFactory; use ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory; use ZBateson\MailMimeParser\Header\Consumer\Received\DomainConsumer; use ZBateson\MailMimeParser\Header\Consumer\Received\GenericReceivedConsumer; use ZBateson\MailMimeParser\Header\Consumer\Received\ReceivedDateConsumer; /** * Simple service provider for consumer singletons. * * @author Zaahid Bateson */ class ConsumerService { /** * @var \ZBateson\MailMimeParser\Header\Part\HeaderPartFactory the * HeaderPartFactory instance used to create HeaderParts. */ protected $partFactory; /** * @var \ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory used for * GenericConsumer instances. */ protected $mimeLiteralPartFactory; /** * @var Received\DomainConsumer[]| * Received\GenericReceivedConsumer[]| * Received\ReceivedDateConsumer[] an array of sub-received header * consumer instances. */ protected $receivedConsumers = [ 'from' => null, 'by' => null, 'via' => null, 'with' => null, 'id' => null, 'for' => null, 'date' => null ]; /** * Sets up the HeaderPartFactory member variable. * * @param HeaderPartFactory $partFactory * @param MimeLiteralPartFactory $mimeLiteralPartFactory */ public function __construct(HeaderPartFactory $partFactory, MimeLiteralPartFactory $mimeLiteralPartFactory) { $this->partFactory = $partFactory; $this->mimeLiteralPartFactory = $mimeLiteralPartFactory; } /** * Returns the AddressBaseConsumer singleton instance. * * @return \ZBateson\MailMimeParser\Header\Consumer\AddressBaseConsumer */ public function getAddressBaseConsumer() { return AddressBaseConsumer::getInstance($this, $this->partFactory); } /** * Returns the AddressConsumer singleton instance. * * @return \ZBateson\MailMimeParser\Header\Consumer\AddressConsumer */ public function getAddressConsumer() { return AddressConsumer::getInstance($this, $this->partFactory); } /** * Returns the AddressGroupConsumer singleton instance. * * @return \ZBateson\MailMimeParser\Header\Consumer\AddressGroupConsumer */ public function getAddressGroupConsumer() { return AddressGroupConsumer::getInstance($this, $this->partFactory); } /** * Returns the CommentConsumer singleton instance. * * @return \ZBateson\MailMimeParser\Header\Consumer\CommentConsumer */ public function getCommentConsumer() { return CommentConsumer::getInstance($this, $this->partFactory); } /** * Returns the GenericConsumer singleton instance. * * @return \ZBateson\MailMimeParser\Header\Consumer\GenericConsumer */ public function getGenericConsumer() { return GenericConsumer::getInstance($this, $this->mimeLiteralPartFactory); } /** * Returns the SubjectConsumer singleton instance. * * @return \ZBateson\MailMimeParser\Header\Consumer\SubjectConsumer */ public function getSubjectConsumer() { return SubjectConsumer::getInstance($this, $this->mimeLiteralPartFactory); } /** * Returns the QuotedStringConsumer singleton instance. * * @return \ZBateson\MailMimeParser\Header\Consumer\QuotedStringConsumer */ public function getQuotedStringConsumer() { return QuotedStringConsumer::getInstance($this, $this->partFactory); } /** * Returns the DateConsumer singleton instance. * * @return \ZBateson\MailMimeParser\Header\Consumer\DateConsumer */ public function getDateConsumer() { return DateConsumer::getInstance($this, $this->partFactory); } /** * Returns the ParameterConsumer singleton instance. * * @return \ZBateson\MailMimeParser\Header\Consumer\ParameterConsumer */ public function getParameterConsumer() { return ParameterConsumer::getInstance($this, $this->partFactory); } /** * Returns the consumer instance corresponding to the passed part name of a * Received header. * * @param string $partName * @return \ZBateson\MailMimeParser\Header\Consumer\Received\FromConsumer */ public function getSubReceivedConsumer($partName) { if (empty($this->receivedConsumers[$partName])) { $consumer = null; if ($partName === 'from' || $partName === 'by') { $consumer = new DomainConsumer($this, $this->partFactory, $partName); } else if ($partName === 'date') { $consumer = new ReceivedDateConsumer($this, $this->partFactory); } else { $consumer = new GenericReceivedConsumer($this, $this->partFactory, $partName); } $this->receivedConsumers[$partName] = $consumer; } return $this->receivedConsumers[$partName]; } /** * Returns the ReceivedConsumer singleton instance. * * @return \ZBateson\MailMimeParser\Header\Consumer\ReceivedConsumer */ public function getReceivedConsumer() { return ReceivedConsumer::getInstance($this, $this->partFactory); } /** * Returns the IdConsumer singleton instance. * * @return \ZBateson\MailMimeParser\Header\Consumer\IdConsumer */ public function getIdConsumer() { return IdConsumer::getInstance($this, $this->partFactory); } /** * Returns the IdBaseConsumer singleton instance. * * @return \ZBateson\MailMimeParser\Header\Consumer\IdBaseConsumer */ public function getIdBaseConsumer() { return IdBaseConsumer::getInstance($this, $this->partFactory); } } mail-mime-parser/src/Header/GenericHeader.php 0000644 00000001453 14736103046 0015071 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header; use ZBateson\MailMimeParser\Header\Consumer\ConsumerService; /** * Reads the header using GenericConsumer. * * Header's may contain mime-encoded parts, quoted parts, and comments. * GenericConsumer returns a single part value. * * @author Zaahid Bateson */ class GenericHeader extends AbstractHeader { /** * Returns a GenericConsumer. * * @param ConsumerService $consumerService * @return \ZBateson\MailMimeParser\Header\Consumer\AbstractConsumer */ protected function getConsumer(ConsumerService $consumerService) { return $consumerService->getGenericConsumer(); } } mail-mime-parser/src/Header/Part/ParameterPart.php 0000644 00000003475 14736103046 0016067 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header\Part; use ZBateson\MbWrapper\MbWrapper; /** * Represents a name/value pair part of a header. * * @author Zaahid Bateson */ class ParameterPart extends MimeLiteralPart { /** * @var string the name of the parameter */ protected $name; /** * @var string the RFC-1766 language tag if set. */ protected $language; /** * Constructs a ParameterPart out of a name/value pair. The name and * value are both mime-decoded if necessary. * * If $language is provided, $name and $value are not mime-decoded. Instead, * they're taken as literals as part of a SplitParameterToken. * * @param MbWrapper $charsetConverter * @param string $name * @param string $value * @param string $language */ public function __construct(MbWrapper $charsetConverter, $name, $value, $language = null) { if ($language !== null) { parent::__construct($charsetConverter, ''); $this->name = $name; $this->value = $value; $this->language = $language; } else { parent::__construct($charsetConverter, trim($value)); $this->name = $this->decodeMime(trim($name)); } } /** * Returns the name of the parameter. * * @return string */ public function getName() { return $this->name; } /** * Returns the RFC-1766 (or subset) language tag, if the parameter is a * split RFC-2231 part with a language tag set. * * @return string */ public function getLanguage() { return $this->language; } } mail-mime-parser/src/Header/Part/MimeLiteralPartFactory.php 0000644 00000001135 14736103046 0017672 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header\Part; /** * Extends HeaderPartFactory to instantiate MimeLiteralParts for its newInstance * function. * * @author Zaahid Bateson */ class MimeLiteralPartFactory extends HeaderPartFactory { /** * Creates and returns a MimeLiteralPart. * * @param string $value * @return HeaderPart */ public function newInstance($value) { return $this->newMimeLiteralPart($value); } } mail-mime-parser/src/Header/Part/ReceivedDomainPart.php 0000644 00000005532 14736103046 0017021 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header\Part; use ZBateson\MbWrapper\MbWrapper; /** * Holds extra information about a parsed Received header part, for FROM and BY * parts, namely: ehlo name, hostname, and address. * * The parsed parts would be mapped as follows: * * FROM ehlo name (hostname [address]), for example: FROM computer (domain.com * [1.2.3.4]) would contain "computer" for getEhloName(), domain.com for * getHostname and 1.2.3.4 for getAddress(). * * This doesn't change if the ehlo name is an address, it is still returned in * getEhloName(), and not in getAddress(). Additionally square brackets are not * stripped from getEhloName() if its an address. For example: "FROM [1.2.3.4]" * would return "[1.2.3.4]" in a call to getEhloName(). * * For further information on how the header's parsed, check the documentation * for {@see \ZBateson\MailMimeParser\Header\Consumer\Received\DomainConsumer}. * * @author Zaahid Bateson */ class ReceivedDomainPart extends ReceivedPart { /** * @var string The name used to identify the server in the EHLO line. */ protected $ehloName; /** * @var string The hostname. */ protected $hostname; /** * @var string The address. */ protected $address; /** * * @param MbWrapper $charsetConverter * @param string $name * @param string $value * @param string $ehloName * @param string $hostname * @param string $address */ public function __construct(MbWrapper $charsetConverter, $name, $value, $ehloName = null, $hostname = null, $address = null) { parent::__construct($charsetConverter, $name, $value); $this->ehloName = $ehloName; $this->hostname = $hostname; $this->address = $address; } /** * Returns the name used to identify the server in the first part of the * extended-domain line. Note that this is not necessarily the name used in * the EHLO line to an SMTP server, since implementations differ so much, * not much can be guaranteed except the position it was parsed in. * * @return string */ public function getEhloName() { return $this->ehloName; } /** * Returns the hostname of the server, or whatever string in the hostname * position when parsing (but never an address). * * @return string */ public function getHostname() { return $this->hostname; } /** * Returns the address of the server, or whatever string that looks like an * address in the address position when parsing (but never a hostname). * * @return string */ public function getAddress() { return $this->address; } } mail-mime-parser/src/Header/Part/HeaderPartFactory.php 0000644 00000011534 14736103046 0016662 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header\Part; use ZBateson\MbWrapper\MbWrapper; /** * Constructs and returns HeaderPart objects. * * @author Zaahid Bateson */ class HeaderPartFactory { /** * @var MbWrapper $charsetConverter passed to HeaderPart constructors * for converting strings in HeaderPart::convertEncoding */ protected $charsetConverter; /** * Sets up dependencies. * * @param MbWrapper $charsetConverter */ public function __construct(MbWrapper $charsetConverter) { $this->charsetConverter = $charsetConverter; } /** * Creates and returns a default HeaderPart for this factory, allowing * subclass factories for specialized HeaderParts. * * The default implementation returns a new Token. * * @param string $value * @return HeaderPart */ public function newInstance($value) { return $this->newToken($value); } /** * Initializes and returns a new Token. * * @param string $value * @return \ZBateson\MailMimeParser\Header\Part\Token */ public function newToken($value) { return new Token($this->charsetConverter, $value); } /** * Instantiates and returns a SplitParameterToken with the given name. * * @param string $name * @return SplitParameterToken */ public function newSplitParameterToken($name) { return new SplitParameterToken($this->charsetConverter, $name); } /** * Initializes and returns a new LiteralPart. * * @param string $value * @return \ZBateson\MailMimeParser\Header\Part\LiteralPart */ public function newLiteralPart($value) { return new LiteralPart($this->charsetConverter, $value); } /** * Initializes and returns a new MimeLiteralPart. * * @param string $value * @return \ZBateson\MailMimeParser\Header\Part\MimeLiteralPart */ public function newMimeLiteralPart($value) { return new MimeLiteralPart($this->charsetConverter, $value); } /** * Initializes and returns a new CommentPart. * * @param string $value * @return \ZBateson\MailMimeParser\Header\Part\CommentPart */ public function newCommentPart($value) { return new CommentPart($this->charsetConverter, $value); } /** * Initializes and returns a new AddressPart. * * @param string $name * @param string $email * @return \ZBateson\MailMimeParser\Header\Part\AddressPart */ public function newAddressPart($name, $email) { return new AddressPart($this->charsetConverter, $name, $email); } /** * Initializes and returns a new AddressGroupPart * * @param array $addresses * @param string $name * @return \ZBateson\MailMimeParser\Header\Part\AddressGroupPart */ public function newAddressGroupPart(array $addresses, $name = '') { return new AddressGroupPart($this->charsetConverter, $addresses, $name); } /** * Initializes and returns a new DatePart * * @param string $value * @return \ZBateson\MailMimeParser\Header\Part\DatePart */ public function newDatePart($value) { return new DatePart($this->charsetConverter, $value); } /** * Initializes and returns a new ParameterPart. * * @param string $name * @param string $value * @param string $language * @return \ZBateson\MailMimeParser\Header\Part\ParameterPart */ public function newParameterPart($name, $value, $language = null) { return new ParameterPart($this->charsetConverter, $name, $value, $language); } /** * Initializes and returns a new ReceivedPart. * * @param string $name * @param string $value * @return \ZBateson\MailMimeParser\Header\Part\ReceivedPart */ public function newReceivedPart($name, $value) { return new ReceivedPart($this->charsetConverter, $name, $value); } /** * Initializes and returns a new ReceivedDomainPart. * * @param string $name * @param string $value * @param string $ehloName * @param string $hostName * @param string $hostAddress * @return \ZBateson\MailMimeParser\Header\Part\ReceivedDomainPart */ public function newReceivedDomainPart( $name, $value, $ehloName = null, $hostName = null, $hostAddress = null ) { return new ReceivedDomainPart( $this->charsetConverter, $name, $value, $ehloName, $hostName, $hostAddress ); } } mail-mime-parser/src/Header/Part/DatePart.php 0000644 00000003472 14736103046 0015021 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header\Part; use ZBateson\MbWrapper\MbWrapper; use DateTime; use Exception; /** * Parses a header into a DateTime object. * * @author Zaahid Bateson */ class DatePart extends LiteralPart { /** * @var DateTime the parsed date, or null if the date could not be parsed */ protected $date; /** * Tries parsing the passed token as an RFC 2822 date, and failing that into * an RFC 822 date, and failing that, tries to parse it by calling * ``` new DateTime($value) ```. * * @param MbWrapper $charsetConverter * @param string $token */ public function __construct(MbWrapper $charsetConverter, $token) { $dateToken = trim($token); // parent::__construct converts character encoding -- may cause problems sometimes. parent::__construct($charsetConverter, $dateToken); // Missing "+" in timezone definition. eg: Thu, 13 Mar 2014 15:02:47 0000 (not RFC compliant) // Won't result in an Exception, but in a valid DateTime in year `0000` - therefore we need to check this first: if (preg_match('# [0-9]{4}$#', $dateToken)) { $dateToken = preg_replace('# ([0-9]{4})$#', ' +$1', $dateToken); // @see https://bugs.php.net/bug.php?id=42486 } elseif (preg_match('#UT$#', $dateToken)) { $dateToken = $dateToken . 'C'; } try { $this->date = new DateTime($dateToken); } catch (Exception $e) { } } /** * Returns a DateTime object or false if it can't be parsed. * * @return DateTime */ public function getDateTime() { return $this->date; } } mail-mime-parser/src/Header/Part/AddressPart.php 0000644 00000002572 14736103046 0015531 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header\Part; use ZBateson\MbWrapper\MbWrapper; /** * Holds a single address or name/address pair. * * The name part of the address may be mime-encoded, but the email address part * can't be mime-encoded. Any whitespace in the email address part is stripped * out. * * A convenience method, getEmail, is provided for clarity -- but getValue * returns the email address as well. * * @author Zaahid Bateson */ class AddressPart extends ParameterPart { /** * Performs mime-decoding and initializes the address' name and email. * * The passed $name may be mime-encoded. $email is stripped of any * whitespace. * * @param MbWrapper $charsetConverter * @param string $name * @param string $email */ public function __construct(MbWrapper $charsetConverter, $name, $email) { parent::__construct( $charsetConverter, $name, '' ); // can't be mime-encoded $this->value = $this->convertEncoding(preg_replace('/\s+/', '', $email)); } /** * Returns the email address. * * @return string */ public function getEmail() { return $this->value; } } mail-mime-parser/src/Header/Part/LiteralPart.php 0000644 00000001671 14736103046 0015537 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header\Part; use ZBateson\MailMimeParser\Header\Part\HeaderPart; use ZBateson\MbWrapper\MbWrapper; /** * A literal header string part. The value of the part is stripped of CR and LF * characters, but otherwise not transformed or changed in any way. * * @author Zaahid Bateson */ class LiteralPart extends HeaderPart { /** * Creates a LiteralPart out of the passed string token * * @param MbWrapper $charsetConverter * @param string $token */ public function __construct(MbWrapper $charsetConverter, $token = null) { parent::__construct($charsetConverter); $this->value = $token; if ($token !== null) { $this->value = preg_replace('/\r|\n/', '', $this->convertEncoding($token)); } } } mail-mime-parser/src/Header/Part/SplitParameterToken.php 0000644 00000012176 14736103046 0017253 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header\Part; use ZBateson\MbWrapper\MbWrapper; /** * Holds a running value for an RFC-2231 split header parameter. * * ParameterConsumer creates SplitParameterTokens when a split header parameter * is first found, and adds subsequent split parts to an already created one if * the parameter name matches. * * @author Zaahid Bateson */ class SplitParameterToken extends HeaderPart { /** * @var string name of the parameter. */ protected $name; /** * @var string[] keeps encoded parts values that need to be decoded. Keys * are set to the index part of the split parameter and used for * sorting before decoding/concatenating. */ protected $encodedParts = []; /** * @var string[] contains literal parts that don't require any decoding (and * are therefore ISO-8859-1 (technically should be 7bit US-ASCII but * allowing 8bit shouldn't be an issue as elsewhere in MMP). */ protected $literalParts = []; /** * @var string RFC-1766 (or subset) language code with optional subtags, * regions, etc... */ protected $language; /** * @var string charset of content in $encodedParts. */ protected $charset = 'ISO-8859-1'; /** * Initializes a SplitParameterToken. * * @param MbWrapper $charsetConverter * @param string $name the parameter's name */ public function __construct(MbWrapper $charsetConverter, $name) { parent::__construct($charsetConverter); $this->name = trim($name); } /** * Extracts charset and language from an encoded value, setting them on the * current object if $index is 0 and adds the value part to the encodedParts * array. * * @param string $value * @param int $index */ protected function extractMetaInformationAndValue($value, $index) { if (preg_match('~^([^\']*)\'([^\']*)\'(.*)$~', $value, $matches)) { if ($index === 0) { $this->charset = (!empty($matches[1])) ? $matches[1] : $this->charset; $this->language = (!empty($matches[2])) ? $matches[2] : $this->language; } $value = $matches[3]; } $this->encodedParts[$index] = $value; } /** * Adds the passed part to the running array of values. * * If $isEncoded is true, language and charset info is extracted from the * value, and the value is decoded before returning in getValue. * * The value of the parameter is sorted based on the passed $index * arguments when adding before concatenating when re-constructing the * value. * * @param string $value * @param boolean $isEncoded * @param int $index */ public function addPart($value, $isEncoded, $index) { if (empty($index)) { $index = 0; } if ($isEncoded) { $this->extractMetaInformationAndValue($value, $index); } else { $this->literalParts[$index] = $this->convertEncoding($value); } } /** * Traverses $this->encodedParts until a non-sequential key is found, or the * end of the array is found. * * This allows encoded parts of a split parameter to be split anywhere and * reconstructed. * * The returned string is converted to UTF-8 before being returned. * * @return string */ private function getNextEncodedValue() { $cur = current($this->encodedParts); $key = key($this->encodedParts); $running = ''; while ($cur !== false) { $running .= $cur; $cur = next($this->encodedParts); $nKey = key($this->encodedParts); if ($nKey !== $key + 1) { break; } $key = $nKey; } return $this->convertEncoding( rawurldecode($running), $this->charset, true ); } /** * Reconstructs the value of the split parameter into a single UTF-8 string * and returns it. * * @return string */ public function getValue() { $parts = $this->literalParts; reset($this->encodedParts); ksort($this->encodedParts); while (current($this->encodedParts) !== false) { $parts[key($this->encodedParts)] = $this->getNextEncodedValue(); } ksort($parts); return array_reduce( $parts, function ($carry, $item) { return $carry . $item; }, '' ); } /** * Returns the name of the parameter. * * @return string */ public function getName() { return $this->name; } /** * Returns the language of the parameter if set, or null if not. * * @return string */ public function getLanguage() { return $this->language; } } mail-mime-parser/src/Header/Part/CommentPart.php 0000644 00000002170 14736103046 0015540 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header\Part; use ZBateson\MbWrapper\MbWrapper; /** * Represents a mime header comment -- text in a structured mime header * value existing within parentheses. * * @author Zaahid Bateson */ class CommentPart extends MimeLiteralPart { /** * @var string the contents of the comment */ protected $comment; /** * Constructs a MimeLiteralPart, decoding the value if it's mime-encoded. * * @param MbWrapper $charsetConverter * @param string $token */ public function __construct(MbWrapper $charsetConverter, $token) { parent::__construct($charsetConverter, $token); $this->comment = $this->value; $this->value = ''; $this->canIgnoreSpacesBefore = true; $this->canIgnoreSpacesAfter = true; } /** * Returns the comment's text. * * @return string */ public function getComment() { return $this->comment; } } mail-mime-parser/src/Header/Part/HeaderPart.php 0000644 00000005375 14736103046 0015340 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header\Part; use ZBateson\MbWrapper\MbWrapper; /** * Abstract base class representing a single part of a parsed header. * * @author Zaahid Bateson */ abstract class HeaderPart { /** * @var string the value of the part */ protected $value; /** * @var MbWrapper $charsetConverter the charset converter used for * converting strings in HeaderPart::convertEncoding */ protected $charsetConverter; /** * Sets up dependencies. * * @param MbWrapper $charsetConverter */ public function __construct(MbWrapper $charsetConverter) { $this->charsetConverter = $charsetConverter; } /** * Returns the part's value. * * @return string the value of the part */ public function getValue() { return $this->value; } /** * Returns the value of the part (which is a string). * * @return string the value */ public function __toString() { return $this->value; } /** * Returns true if spaces before this part should be ignored. True is only * returned for MimeLiterals if the part begins with a mime-encoded string, * Tokens if the Token's value is a single space, and for CommentParts. * * @return bool */ public function ignoreSpacesBefore() { return false; } /** * Returns true if spaces after this part should be ignored. True is only * returned for MimeLiterals if the part ends with a mime-encoded string * Tokens if the Token's value is a single space, and for CommentParts. * * @return bool */ public function ignoreSpacesAfter() { return false; } /** * Ensures the encoding of the passed string is set to UTF-8. * * The method does nothing if the passed $from charset is UTF-8 already, or * if $force is set to false and mb_check_encoding for $str returns true * for 'UTF-8'. * * @param string $str * @param string $from * @param boolean $force * @return string utf-8 string */ protected function convertEncoding($str, $from = 'ISO-8859-1', $force = false) { if ($from !== 'UTF-8') { // mime header part decoding will force it. This is necessary for // UTF-7 because mb_check_encoding will return true if ($force || !($this->charsetConverter->checkEncoding($str, 'UTF-8'))) { return $this->charsetConverter->convert($str, $from, 'UTF-8'); } } return $str; } } mail-mime-parser/src/Header/Part/ReceivedPart.php 0000644 00000001653 14736103046 0015671 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header\Part; use ZBateson\MbWrapper\MbWrapper; /** * Represents one parameter in a parsed 'Received' header, e.g. the FROM or VIA * part. * * Note that FROM and BY actually get parsed into a sub-class, * ReceivedDomainPart which keeps track of other sub-parts that can be parsed * from them. * * @author Zaahid Bateson */ class ReceivedPart extends ParameterPart { /** * Constructor. * * @param MbWrapper $charsetConverter * @param string $name * @param string $value */ public function __construct(MbWrapper $charsetConverter, $name, $value) { parent::__construct($charsetConverter, '', ''); // can't be mime-encoded $this->name = trim($name); $this->value = trim($value); } } mail-mime-parser/src/Header/Part/MimeLiteralPart.php 0000644 00000014033 14736103046 0016343 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header\Part; use ZBateson\MbWrapper\MbWrapper; /** * Represents a single mime header part token, with the possibility of it being * MIME-Encoded as per RFC-2047. * * MimeLiteralPart automatically decodes the value if it's encoded. * * @author Zaahid Bateson */ class MimeLiteralPart extends LiteralPart { /** * @var string regex pattern matching a mime-encoded part */ const MIME_PART_PATTERN = '=\?[^?=]+\?[QBqb]\?[^\?]+\?='; /** * @var string regex pattern used when parsing parameterized headers */ const MIME_PART_PATTERN_NO_QUOTES = '=\?[^\?=]+\?[QBqb]\?[^\?"]+\?='; /** * @var bool set to true to ignore spaces before this part */ protected $canIgnoreSpacesBefore = false; /** * @var bool set to true to ignore spaces after this part */ protected $canIgnoreSpacesAfter = false; /** * @var array maintains an array mapping rfc1766 language tags to parts of * text in the value. * * Each array element is an array containing two elements, one with key * 'lang', and another with key 'value'. */ protected $languages = []; /** * Decoding the passed token value if it's mime-encoded and assigns the * decoded value to a member variable. Sets canIgnoreSpacesBefore and * canIgnoreSpacesAfter. * * @param MbWrapper $charsetConverter * @param string $token */ public function __construct(MbWrapper $charsetConverter, $token) { parent::__construct($charsetConverter); $this->value = $this->decodeMime($token); // preg_match returns int $pattern = self::MIME_PART_PATTERN; $this->canIgnoreSpacesBefore = (bool) preg_match("/^\s*{$pattern}/", $token); $this->canIgnoreSpacesAfter = (bool) preg_match("/{$pattern}\s*\$/", $token); } /** * Finds and replaces mime parts with their values. * * The method splits the token value into an array on mime-part-patterns, * either replacing a mime part with its value by calling iconv_mime_decode * or converts the encoding on the text part by calling convertEncoding. * * @param string $value * @return string */ protected function decodeMime($value) { $pattern = self::MIME_PART_PATTERN; // remove whitespace between two adjacent mime encoded parts $value = preg_replace("/($pattern)\\s+(?=$pattern)/", '$1', $value); // with PREG_SPLIT_DELIM_CAPTURE, matched and unmatched parts are returned $aMimeParts = preg_split("/($pattern)/", $value, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); $ret = ''; foreach ($aMimeParts as $entity) { $ret .= $this->decodeSplitPart($entity); } return $ret; } /** * Decodes a matched mime entity part into a string and returns it, after * adding the string into the languages array. * * @param string[] $matches * @return string */ private function decodeMatchedEntity($matches) { $body = $matches[4]; if (strtoupper($matches[3]) === 'Q') { $body = quoted_printable_decode(str_replace('_', '=20', $body)); } else { $body = base64_decode($body); } $language = $matches[2]; $decoded = $this->convertEncoding($body, $matches[1], true); $this->addToLanguage($decoded, $language); return $decoded; } /** * Decodes a single mime-encoded entity. * * Unfortunately, mb_decode_header fails for many charsets on PHP 5.4 and * PHP 5.5 (even if they're listed as supported). iconv_mime_decode doesn't * support all charsets. * * Parsing out the charset and body of the encoded entity seems to be the * way to go to support the most charsets. * * @param string $entity * @return string */ private function decodeSplitPart($entity) { if (preg_match("/^=\?([A-Za-z\-_0-9]+)\*?([A-Za-z\-_0-9]+)?\?([QBqb])\?([^\?]+)\?=$/", $entity, $matches)) { return $this->decodeMatchedEntity($matches); } $decoded = $this->convertEncoding($entity); $this->addToLanguage($decoded); return $decoded; } /** * Returns true if spaces before this part should be ignored. * * Overridden to return $this->canIgnoreSpacesBefore which is setup in the * constructor. * * @return bool */ public function ignoreSpacesBefore() { return $this->canIgnoreSpacesBefore; } /** * Returns true if spaces before this part should be ignored. * * Overridden to return $this->canIgnoreSpacesAfter which is setup in the * constructor. * * @return bool */ public function ignoreSpacesAfter() { return $this->canIgnoreSpacesAfter; } /** * Adds the passed part into the languages array with the given language. * * @param string $part * @param string|null $language */ protected function addToLanguage($part, $language = null) { $this->languages[] = [ 'lang' => $language, 'value' => $part ]; } /** * Returns an array of parts mapped to languages in the header value, for * instance the string: * * 'Hello and =?UTF-8*fr-be?Q?bonjour_?= =?UTF-8*it?Q?mi amici?=. Welcome!' * * Would be mapped in the returned array as follows: * * ```php * [ * 0 => [ 'lang' => null, 'value' => 'Hello and ' ], * 1 => [ 'lang' => 'fr-be', 'value' => 'bonjour ' ], * 3 => [ 'lang' => 'it', 'value' => 'mi amici' ], * 4 => [ 'lang' => null, 'value' => ' Welcome!' ] * ] * ``` * * @return string[][] */ public function getLanguageArray() { return $this->languages; } } mail-mime-parser/src/Header/Part/AddressGroupPart.php 0000644 00000003537 14736103046 0016550 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header\Part; use ZBateson\MbWrapper\MbWrapper; /** * Holds a group of addresses, and an optional group name. * * Because AddressGroupConsumer is only called once a colon (":") character is * found, an AddressGroupPart is initially constructed without a $name. Once it is * returned to AddressConsumer, a new AddressGroupPart is created out of * AddressGroupConsumer's AddressGroupPart. * * @author Zaahid Bateson */ class AddressGroupPart extends MimeLiteralPart { /** * @var AddressPart[] an array of AddressParts */ protected $addresses; /** * Creates an AddressGroupPart out of the passed array of AddressParts and an * optional name (which may be mime-encoded). * * @param MbWrapper $charsetConverter * @param AddressPart[] $addresses * @param string $name */ public function __construct(MbWrapper $charsetConverter, array $addresses, $name = '') { parent::__construct($charsetConverter, trim($name)); $this->addresses = $addresses; } /** * Return the AddressGroupPart's array of addresses. * * @return AddressPart[] */ public function getAddresses() { return $this->addresses; } /** * Returns the AddressPart at the passed index or null. * * @param int $index * @return Address */ public function getAddress($index) { if (!isset($this->addresses[$index])) { return null; } return $this->addresses[$index]; } /** * Returns the name of the group * * @return string */ public function getName() { return $this->value; } } mail-mime-parser/src/Header/Part/Token.php 0000644 00000003115 14736103046 0014367 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header\Part; use ZBateson\MailMimeParser\Header\Part\HeaderPart; use ZBateson\MbWrapper\MbWrapper; /** * Holds a string value token that will require additional processing by a * consumer prior to returning to a client. * * A Token is meant to hold a value for further processing -- for instance when * consuming an address list header (like From or To) -- before it's known what * type of HeaderPart it is (could be an email address, could be a name, or * could be a group.) * * @author Zaahid Bateson */ class Token extends HeaderPart { /** * Initializes a token. * * @param MbWrapper $charsetConverter * @param string $value the token's value */ public function __construct(MbWrapper $charsetConverter, $value) { parent::__construct($charsetConverter); $this->value = $value; } /** * Returns true if the value of the token is equal to a single space. * * @return bool */ public function isSpace() { return (preg_match('/^\s+$/', $this->value) === 1); } /** * Returns true if the value is a space. * * @return bool */ public function ignoreSpacesBefore() { return $this->isSpace(); } /** * Returns true if the value is a space. * * @return bool */ public function ignoreSpacesAfter() { return $this->isSpace(); } } mail-mime-parser/src/Header/MimeEncodedHeader.php 0000644 00000003601 14736103046 0015663 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header; use ZBateson\MailMimeParser\Header\Consumer\AbstractConsumer; use ZBateson\MailMimeParser\Header\Consumer\ConsumerService; use ZBateson\MailMimeParser\Header\Part\MimeLiteralPart; use ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory; /** * Allows a header to be mime-encoded and be decoded with a consumer after * decoding. * * The entire header's value must only consist of mime-encoded parts for this to * apply. * * @author Zaahid Bateson */ abstract class MimeEncodedHeader extends AbstractHeader { /** * @var \ZBateson\MailMimeParser\Header\Part\MimeLiteralPartFactory for * mime decoding. */ protected $mimeLiteralPartFactory; /** * Includes * * @param ConsumerService $consumerService * @param string $name * @param string $value */ public function __construct( MimeLiteralPartFactory $mimeLiteralPartFactory, ConsumerService $consumerService, $name, $value ) { $this->mimeLiteralPartFactory = $mimeLiteralPartFactory; parent::__construct($consumerService, $name, $value); } /** * Mime-decodes the raw value if the whole raw value only consists of mime- * encoded parts and whitespace prior to invoking the passed consumer. * * @param AbstractConsumer $consumer */ protected function setParseHeaderValue(AbstractConsumer $consumer) { $value = $this->rawValue; $matchp = '~^(\s*' . MimeLiteralPart::MIME_PART_PATTERN . '\s*)+$~'; if (preg_match($matchp, $value)) { $p = $this->mimeLiteralPartFactory->newInstance($value); $value = $p->getValue(); } $this->parts = $consumer($value); } } mail-mime-parser/src/Header/HeaderConsts.php 0000644 00000002600 14736103046 0014761 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header; abstract class HeaderConsts { // Headers according to the table at https://tools.ietf.org/html/rfc5322#section-3.6 const RETURN_PATH = 'Return-Path'; const RECEIVED = 'Received'; const RESENT_DATE = 'Resent-Date'; const RESENT_FROM = 'Resent-From'; const RESENT_SENDER = 'Resent-Sender'; const RESENT_TO = 'Resent-To'; const RESENT_CC = 'Resent-Cc'; const RESENT_BCC = 'Resent-Bcc'; const RESENT_MSD_ID = 'Resent-Message-ID'; const RESENT_MESSAGE_ID = self::RESENT_MSD_ID; const ORIG_DATE = 'Date'; const DATE = self::ORIG_DATE; const FROM = 'From'; const SENDER = 'Sender'; const REPLY_TO = 'Reply-To'; const TO = 'To'; const CC = 'Cc'; const BCC = 'Bcc'; const MESSAGE_ID = 'Message-ID'; const IN_REPLY_TO = 'In-Reply-To'; const REFERENCES = 'References'; const SUBJECT = 'Subject'; const COMMENTS = 'Comments'; const KEYWORDS = 'Keywords'; const MIME_VERSION = 'MIME-Version'; // https://tools.ietf.org/html/rfc2045#section-4 const CONTENT_TYPE = 'Content-Type'; // https://tools.ietf.org/html/rfc2045#section-5 const AUTO_SUBMITTED = 'Auto-Submitted'; // https://tools.ietf.org/html/rfc3834#section-5 } mail-mime-parser/src/Header/SubjectHeader.php 0000644 00000001261 14736103046 0015111 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header; use ZBateson\MailMimeParser\Header\Consumer\ConsumerService; /** * Reads the header using a SubjectConsumer. * * @author Zaahid Bateson */ class SubjectHeader extends AbstractHeader { /** * Returns a SubjectConsumer. * * @param ConsumerService $consumerService * @return \ZBateson\MailMimeParser\Header\Consumer\AbstractConsumer */ protected function getConsumer(ConsumerService $consumerService) { return $consumerService->getSubjectConsumer(); } } mail-mime-parser/src/Header/IdHeader.php 0000644 00000002371 14736103046 0014051 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Header; use ZBateson\MailMimeParser\Header\Consumer\ConsumerService; /** * Represents a Content-ID, Message-ID, In-Reply-To or References header. * * For a multi-id header like In-Reply-To or References, all IDs can be * retrieved by calling ``` getIds() ```. Otherwise, to retrieve the first (or * only) ID call ``` getValue() ```. * * @author Zaahid Bateson */ class IdHeader extends MimeEncodedHeader { /** * Returns an IdBaseConsumer. * * @param ConsumerService $consumerService * @return \ZBateson\MailMimeParser\Header\Consumer\AbstractConsumer */ protected function getConsumer(ConsumerService $consumerService) { return $consumerService->getIdBaseConsumer(); } /** * Synonym for getValue(). * * @return string|null */ public function getId() { return $this->getValue(); } /** * Returns all IDs parsed for a multi-id header like References or * In-Reply-To. * * @return string[] */ public function getIds() { return $this->parts; } } mail-mime-parser/src/Stream/MessagePartStream.php 0000644 00000013177 14736103046 0016044 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Stream; use GuzzleHttp\Psr7; use GuzzleHttp\Psr7\AppendStream; use GuzzleHttp\Psr7\StreamDecoratorTrait; use Psr\Http\Message\StreamInterface; use ZBateson\MailMimeParser\MailMimeParser; use ZBateson\MailMimeParser\Message\Part\MessagePart; use ZBateson\MailMimeParser\Message\Part\ParentHeaderPart; use ZBateson\MailMimeParser\Stream\StreamFactory; /** * Provides a readable stream for a MessagePart. * * @author Zaahid Bateson */ class MessagePartStream implements StreamInterface { use StreamDecoratorTrait; /** * @var StreamFactory For creating needed stream decorators. */ protected $streamFactory; /** * @var MessagePart The part to read from. */ protected $part; /** * Constructor * * @param StreamFactory $sdf * @param MessagePart $part */ public function __construct(StreamFactory $sdf, MessagePart $part) { $this->streamFactory = $sdf; $this->part = $part; } /** * Attaches and returns a CharsetStream decorator to the passed $stream. * * If the current attached MessagePart doesn't specify a charset, $stream is * returned as-is. * * @param StreamInterface $stream * @return StreamInterface */ private function getCharsetDecoratorForStream(StreamInterface $stream) { $charset = $this->part->getCharset(); if (!empty($charset)) { $stream = $this->streamFactory->newCharsetStream( $stream, $charset, MailMimeParser::DEFAULT_CHARSET ); } return $stream; } /** * Attaches and returns a transfer encoding stream decorator to the passed * $stream. * * The attached stream decorator is based on the attached part's returned * value from MessagePart::getContentTransferEncoding, using one of the * following stream decorators as appropriate: * * o QuotedPrintableStream * o Base64Stream * o UUStream * * @param StreamInterface $stream * @return StreamInterface */ private function getTransferEncodingDecoratorForStream(StreamInterface $stream) { $encoding = $this->part->getContentTransferEncoding(); $decorator = null; switch ($encoding) { case 'quoted-printable': $decorator = $this->streamFactory->newQuotedPrintableStream($stream); break; case 'base64': $decorator = $this->streamFactory->newBase64Stream( $this->streamFactory->newChunkSplitStream($stream)); break; case 'x-uuencode': $decorator = $this->streamFactory->newUUStream($stream); $decorator->setFilename($this->part->getFilename()); break; default: return $stream; } return $decorator; } /** * Writes out the content portion of the attached mime part to the passed * $stream. * * @param StreamInterface $stream */ private function writePartContentTo(StreamInterface $stream) { $contentStream = $this->part->getContentStream(); if ($contentStream !== null) { $copyStream = $this->streamFactory->newNonClosingStream($stream); $es = $this->getTransferEncodingDecoratorForStream($copyStream); $cs = $this->getCharsetDecoratorForStream($es); Psr7\copy_to_stream($contentStream, $cs); $cs->close(); } } /** * Creates an array of streams based on the attached part's mime boundary * and child streams. * * @param ParentHeaderPart $part passed in because $this->part is declared * as MessagePart * @return StreamInterface[] */ protected function getBoundaryAndChildStreams(ParentHeaderPart $part) { $boundary = $part->getHeaderParameter('Content-Type', 'boundary'); if ($boundary === null) { return array_map( function ($child) { return $child->getStream(); }, $part->getChildParts() ); } $streams = []; foreach ($part->getChildParts() as $i => $child) { if ($i !== 0 || $part->hasContent()) { $streams[] = Psr7\stream_for("\r\n"); } $streams[] = Psr7\stream_for("--$boundary\r\n"); $streams[] = $child->getStream(); } $streams[] = Psr7\stream_for("\r\n--$boundary--\r\n"); return $streams; } /** * Returns an array of Psr7 Streams representing the attached part and it's * direct children. * * @return StreamInterface[] */ protected function getStreamsArray() { $content = Psr7\stream_for(); $this->writePartContentTo($content); $content->rewind(); $streams = [ $this->streamFactory->newHeaderStream($this->part), $content ]; /** * @var ParentHeaderPart */ $part = $this->part; if ($part instanceof ParentHeaderPart && $part->getChildCount()) { $streams = array_merge($streams, $this->getBoundaryAndChildStreams($part)); } return $streams; } /** * Creates the underlying stream lazily when required. * * @return StreamInterface */ protected function createStream() { return new AppendStream($this->getStreamsArray()); } } mail-mime-parser/src/Stream/HeaderStream.php 0000644 00000005030 14736103046 0015006 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Stream; use ArrayIterator; use GuzzleHttp\Psr7; use GuzzleHttp\Psr7\StreamDecoratorTrait; use Psr\Http\Message\StreamInterface; use ZBateson\MailMimeParser\Message\Part\ParentHeaderPart; use ZBateson\MailMimeParser\Message\Part\MessagePart; /** * Psr7 stream decorator implementation providing a readable stream for a part's * headers. * * HeaderStream is only used by a MimePart parent. It can accept any * MessagePart - for non-MimeParts, only type headers are generated based on * available information. * * @author Zaahid Bateson */ class HeaderStream implements StreamInterface { use StreamDecoratorTrait; /** * @var MessagePart the part to read from. */ protected $part; /** * Constructor * * @param MessagePart $part */ public function __construct(MessagePart $part) { $this->part = $part; } /** * Returns a header array for the current part. * * If the part is not a MimePart, Content-Type, Content-Disposition and * Content-Transfer-Encoding headers are generated manually. * * @return array */ private function getPartHeadersIterator() { if ($this->part instanceof ParentHeaderPart) { return $this->part->getRawHeaderIterator(); } elseif ($this->part->getParent() !== null && $this->part->getParent()->isMime()) { return new ArrayIterator([ [ 'Content-Type', $this->part->getContentType() ], [ 'Content-Disposition', $this->part->getContentDisposition() ], [ 'Content-Transfer-Encoding', $this->part->getContentTransferEncoding() ] ]); } return new ArrayIterator(); } /** * Writes out headers for $this->part and follows them with an empty line. * * @param StreamInterface $stream */ public function writePartHeadersTo(StreamInterface $stream) { foreach ($this->getPartHeadersIterator() as $header) { $stream->write("${header[0]}: ${header[1]}\r\n"); } $stream->write("\r\n"); } /** * Creates the underlying stream lazily when required. * * @return StreamInterface */ protected function createStream() { $stream = Psr7\stream_for(); $this->writePartHeadersTo($stream); $stream->rewind(); return $stream; } } mail-mime-parser/src/Stream/StreamFactory.php 0000644 00000011256 14736103046 0015234 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser\Stream; use Psr\Http\Message\StreamInterface; use ZBateson\StreamDecorators\Base64Stream; use ZBateson\StreamDecorators\CharsetStream; use ZBateson\StreamDecorators\ChunkSplitStream; use ZBateson\StreamDecorators\SeekingLimitStream; use ZBateson\StreamDecorators\NonClosingStream; use ZBateson\StreamDecorators\PregReplaceFilterStream; use ZBateson\StreamDecorators\QuotedPrintableStream; use ZBateson\StreamDecorators\UUStream; use ZBateson\MailMimeParser\Message\Part\MessagePart; use ZBateson\MailMimeParser\Message\Part\PartBuilder; /** * Factory class for Psr7 stream decorators used in MailMimeParser. * * @author Zaahid Bateson */ class StreamFactory { /** * Returns a SeekingLimitStream using $part->getStreamPartLength() and * $part->getStreamPartStartOffset() * * @param StreamInterface $stream * @param PartBuilder $part * @return SeekingLimitStream */ public function getLimitedPartStream(StreamInterface $stream, PartBuilder $part) { return $this->newLimitStream( $stream, $part->getStreamPartLength(), $part->getStreamPartStartOffset() ); } /** * Returns a SeekingLimitStream using $part->getStreamContentLength() and * $part->getStreamContentStartOffset() * * @param StreamInterface $stream * @param PartBuilder $part * @return SeekingLimitStream */ public function getLimitedContentStream(StreamInterface $stream, PartBuilder $part) { $length = $part->getStreamContentLength(); if ($length !== 0) { return $this->newLimitStream( $stream, $part->getStreamContentLength(), $part->getStreamContentStartOffset() ); } return null; } /** * Creates and returns a SeekingLimitedStream. * * @param StreamInterface $stream * @param int $length * @param int $start * @return SeekingLimitStream */ private function newLimitStream(StreamInterface $stream, $length, $start) { return new SeekingLimitStream( $this->newNonClosingStream($stream), $length, $start ); } /** * Creates a non-closing stream that doesn't close it's internal stream when * closing/detaching. * * @param StreamInterface $stream * @return NonClosingStream */ public function newNonClosingStream(StreamInterface $stream) { return new NonClosingStream($stream); } /** * Creates a ChunkSplitStream. * * @param StreamInterface $stream * @return ChunkSplitStream */ public function newChunkSplitStream(StreamInterface $stream) { return new ChunkSplitStream($stream); } /** * Creates and returns a Base64Stream with an internal * PregReplaceFilterStream that filters out non-base64 characters. * * @param StreamInterface $stream * @return Base64Stream */ public function newBase64Stream(StreamInterface $stream) { return new Base64Stream( new PregReplaceFilterStream($stream, '/[^a-zA-Z0-9\/\+=]/', '') ); } /** * Creates and returns a QuotedPrintableStream. * * @param StreamInterface $stream * @return QuotedPrintableStream */ public function newQuotedPrintableStream(StreamInterface $stream) { return new QuotedPrintableStream($stream); } /** * Creates and returns a UUStream * * @param StreamInterface $stream * @return UUStream */ public function newUUStream(StreamInterface $stream) { return new UUStream($stream); } /** * Creates and returns a CharsetStream * * @param StreamInterface $stream * @param string $fromCharset * @param string $toCharset * @return CharsetStream */ public function newCharsetStream(StreamInterface $stream, $fromCharset, $toCharset) { return new CharsetStream($stream, $fromCharset, $toCharset); } /** * Creates and returns a MessagePartStream * * @param MessagePart $part * @return MessagePartStream */ public function newMessagePartStream(MessagePart $part) { return new MessagePartStream($this, $part); } /** * Creates and returns a HeaderStream * * @param MessagePart $part * @return HeaderStream */ public function newHeaderStream(MessagePart $part) { return new HeaderStream($part); } } mail-mime-parser/src/MailMimeParser.php 0000644 00000004313 14736103046 0014061 0 ustar 00 <?php /** * This file is part of the ZBateson\MailMimeParser project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\MailMimeParser; use GuzzleHttp\Psr7; /** * Parses a MIME message into a \ZBateson\MailMimeParser\Message object. * * To invoke, call parse on a MailMimeParser object. * * $handle = fopen('path/to/file.txt'); * $parser = new MailMimeParser(); * $parser->parse($handle); * fclose($handle); * * @author Zaahid Bateson */ class MailMimeParser { /** * @var string the default charset used to encode strings (or string content * like streams) returned by MailMimeParser (for e.g. the string * returned by calling $message->getTextContent()). */ const DEFAULT_CHARSET = 'UTF-8'; /** * @var \ZBateson\MailMimeParser\Container dependency injection container */ protected $di; /** * Sets up the parser. * * @param Container $di pass a Container object to use it for * initialization. */ public function __construct(Container $di = null) { if ($di === null) { $di = new Container(); } $this->di = $di; } /** * Parses the passed stream handle into a ZBateson\MailMimeParser\Message * object and returns it. * * Internally, the message is first copied to a temp stream (with php://temp * which may keep it in memory or write it to disk) and its stream is used. * That way if the message is too large to hold in memory it can be written * to a temporary file if need be. * * @param resource|string $handleOrString the resource handle to the input * stream of the mime message, or a string containing a mime message * @return \ZBateson\MailMimeParser\Message */ public function parse($handleOrString) { $stream = Psr7\stream_for($handleOrString); $copy = Psr7\stream_for(fopen('php://temp', 'r+')); Psr7\copy_to_stream($stream, $copy); $copy->rewind(); // don't close it when $stream gets destroyed $stream->detach(); $parser = $this->di->newMessageParser(); return $parser->parse($copy); } } mail-mime-parser/README.md 0000644 00000011136 14736103046 0011172 0 ustar 00 # zbateson/mail-mime-parser Testable and PSR-compliant mail mime parser alternative to PHP's imap* functions and Pear libraries for reading messages in _Internet Message Format_ [RFC 822](http://tools.ietf.org/html/rfc822) (and later revisions [RFC 2822](http://tools.ietf.org/html/rfc2822), [RFC 5322](http://tools.ietf.org/html/rfc5322)). [![Build Status](https://travis-ci.com/zbateson/mail-mime-parser.svg?branch=master)](https://travis-ci.com/zbateson/mail-mime-parser) [![Code Coverage](https://scrutinizer-ci.com/g/zbateson/mail-mime-parser/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/zbateson/mail-mime-parser/?branch=master) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/zbateson/mail-mime-parser/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/zbateson/mail-mime-parser/?branch=master) [![Total Downloads](https://poser.pugx.org/zbateson/mail-mime-parser/downloads)](https://packagist.org/packages/zbateson/mail-mime-parser) [![Latest Stable Version](https://poser.pugx.org/zbateson/mail-mime-parser/version)](https://packagist.org/packages/zbateson/mail-mime-parser) The goals of this project are to be: * Well written * Standards-compliant but forgiving * Tested where possible To include it for use in your project, please install via composer: ``` composer require zbateson/mail-mime-parser ``` ## Deprecation Notice (since 1.2.1) getContentResourceHandle, getTextResourceHandle, and getHtmlResourceHandle have all been deprecated due to #106. fread() will only return a single byte of a multibyte char, and so will cause potentially unexpected results/warnings in some cases, and psr7 streams should be used instead. Note that this deprecation doesn’t apply to getBinaryContentResourceHandle or getResourceHandle. ## Requirements MailMimeParser requires PHP 5.4 or newer. Tested on PHP 5.4, 5.5, 5.6, 7, 7.1, 7.2, 7.3 and 7.4 on travis. Please note: hhvm support has been dropped as it no longer supports 'php' as of version 4. Previous versions of hhvm may still work, but are no longer supported. ## Usage ```php use ZBateson\MailMimeParser\MailMimeParser; use ZBateson\MailMimeParser\Message; use ZBateson\MailMimeParser\Header\HeaderConsts; // use an instance of MailMimeParser as a class dependency $mailParser = new MailMimeParser(); $handle = fopen('file.mime', 'r'); // parse() accepts a string, resource or Psr7 StreamInterface $message = $mailParser->parse($handle); // returns `Message` fclose($handle); // OR: use this procedurally (Message::from also accepts a string, // resource or Psr7 StreamInterface $message = Message::from($string); echo $message->getHeaderValue(HeaderConsts::FROM); // user@example.com echo $message ->getHeader(HeaderConsts::FROM) // AddressHeader ->getPersonName(); // Person Name echo $message->getHeaderValue(HeaderConsts::SUBJECT); // The email's subject echo $message ->getHeader(HeaderConsts::TO) // also AddressHeader ->getAddresses()[0] // AddressPart ->getName(); // Person Name echo $message ->getHeader(HeaderConsts::CC) // also AddressHeader ->getAddresses()[0] // AddressPart ->getEmail(); // user@example.com echo $message->getTextContent(); // or getHtmlContent() echo $message->getHeader('X-Foo'); // for custom or undocumented headers $att = $message->getAttachmentPart(0); // first attachment echo $att->getHeaderValue(HeaderConsts::CONTENT_TYPE); // e.g. "text/plain" echo $att->getHeaderParameter( // value of "charset" part 'content-type', 'charset' ); echo $att->getContent(); // get the attached file's contents $stream = $att->getContentStream(); // the file is decoded automatically $dest = \GuzzleHttp\Psr7\stream_for( fopen('my-file.ext') ); \GuzzleHttp\Psr7\copy_to_stream( $stream, $dest ); // OR: more simply if saving or copying to another stream $att->saveContent('my-file.ext'); // writes to my-file.ext $att->saveContent($stream); // copies to the stream ``` ## Documentation * [Usage Guide](https://mail-mime-parser.org/) * [API Reference](https://mail-mime-parser.org/api/1.3) ## Upgrading to 1.x * [Upgrade Guide](https://mail-mime-parser.org/upgrade-1.0) ## License BSD licensed - please see [license agreement](https://github.com/zbateson/mail-mime-parser/blob/master/LICENSE). mail-mime-parser/LICENSE 0000644 00000002427 14736103046 0010723 0 ustar 00 Copyright (c) 2014-2015, Zaahid Bateson All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. stream-decorators/src/QuotedPrintableStream.php 0000644 00000014066 14736103046 0015773 0 ustar 00 <?php /** * This file is part of the ZBateson\StreamDecorators project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\StreamDecorators; use Psr\Http\Message\StreamInterface; use GuzzleHttp\Psr7\StreamDecoratorTrait; use RuntimeException; /** * GuzzleHttp\Psr7 stream decoder decorator for quoted printable streams. * * @author Zaahid Bateson */ class QuotedPrintableStream implements StreamInterface { use StreamDecoratorTrait; /** * @var int current read/write position */ private $position = 0; /** * @var string Last line of written text (used to maintain good line-breaks) */ private $lastLine = ''; /** * Overridden to return the position in the target encoding. * * @return int */ public function tell() { return $this->position; } /** * Returns null, getSize isn't supported * * @return null */ public function getSize() { return null; } /** * Not supported. * * @param int $offset * @param int $whence * @throws RuntimeException */ public function seek($offset, $whence = SEEK_SET) { throw new RuntimeException('Cannot seek a QuotedPrintableStream'); } /** * Overridden to return false * * @return boolean */ public function isSeekable() { return false; } /** * Reads $length chars from the underlying stream, prepending the past $pre * to it first. * * If the characters read (including the prepended $pre) contain invalid * quoted-printable characters, the underlying stream is rewound by the * total number of characters ($length + strlen($pre)). * * The quoted-printable encoded characters are returned. If the characters * read are invalid, '3D' is returned indicating an '=' character. * * @param int $length * @param string $pre * @return string */ private function readEncodedChars($length, $pre = '') { $str = $pre . $this->stream->read($length); $len = strlen($str); if ($len > 0 && !preg_match('/^[0-9a-f]{2}$|^[\r\n]{1,2}.?$/is', $str) && $this->stream->isSeekable()) { $this->stream->seek(-$len, SEEK_CUR); return '3D'; // '=' character } return $str; } /** * Decodes the passed $block of text. * * If the last or before last character is an '=' char, indicating the * beginning of a quoted-printable encoded char, 1 or 2 additional bytes are * read from the underlying stream respectively. * * The decoded string is returned. * * @param string $block * @return string */ private function decodeBlock($block) { if (substr($block, -1) === '=') { $block .= $this->readEncodedChars(2); } elseif (substr($block, -2, 1) === '=') { $first = substr($block, -1); $block = substr($block, 0, -1); $block .= $this->readEncodedChars(1, $first); } return quoted_printable_decode($block); } /** * Reads up to $length characters, appends them to the passed $str string, * and returns the total number of characters read. * * -1 is returned if there are no more bytes to read. * * @param int $length * @param string $append * @return int */ private function readRawDecodeAndAppend($length, &$str) { $block = $this->stream->read($length); if ($block === false || $block === '') { return -1; } $decoded = $this->decodeBlock($block); $count = strlen($decoded); $str .= $decoded; return $count; } /** * Reads up to $length decoded bytes from the underlying quoted-printable * encoded stream and returns them. * * @param int $length * @return string */ public function read($length) { // let Guzzle decide what to do. if ($length <= 0 || $this->eof()) { return $this->stream->read($length); } $count = 0; $bytes = ''; while ($count < $length) { $nRead = $this->readRawDecodeAndAppend($length - $count, $bytes); if ($nRead === -1) { break; } $this->position += $nRead; $count += $nRead; } return $bytes; } /** * Writes the passed string to the underlying stream after encoding it as * quoted-printable. * * Note that reading and writing to the same stream without rewinding is not * supported. * * @param string $string * @return int the number of bytes written */ public function write($string) { $encodedLine = quoted_printable_encode($this->lastLine); $lineAndString = rtrim(quoted_printable_encode($this->lastLine . $string), "\r\n"); $write = substr($lineAndString, strlen($encodedLine)); $this->stream->write($write); $written = strlen($string); $this->position += $written; $lpos = strrpos($lineAndString, "\n"); $lastLine = $lineAndString; if ($lpos !== false) { $lastLine = substr($lineAndString, $lpos + 1); } $this->lastLine = quoted_printable_decode($lastLine); return $written; } /** * Writes out a final CRLF if the current line isn't empty. */ private function beforeClose() { if ($this->isWritable() && $this->lastLine !== '') { $this->stream->write("\r\n"); $this->lastLine = ''; } } /** * Closes the underlying stream and writes a final CRLF if the current line * isn't empty. */ public function close() { $this->beforeClose(); $this->stream->close(); } /** * Closes the underlying stream and writes a final CRLF if the current line * isn't empty. */ public function detach() { $this->beforeClose(); $this->stream->detach(); } } stream-decorators/src/ChunkSplitStream.php 0000644 00000006513 14736103046 0014753 0 ustar 00 <?php /** * This file is part of the ZBateson\StreamDecorators project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\StreamDecorators; use Psr\Http\Message\StreamInterface; use GuzzleHttp\Psr7\StreamDecoratorTrait; /** * Inserts line ending characters after the set number of characters have been * written to the underlying stream. * * @author Zaahid Bateson */ class ChunkSplitStream implements StreamInterface { use StreamDecoratorTrait; /** * @var int Number of bytes written, and importantly, if non-zero, writes a * final $lineEnding on close (and so maintained instead of using * tell() directly) */ private $position; /** * @var int The number of characters in a line before inserting $lineEnding. */ private $lineLength; /** * @var string The line ending characters to insert. */ private $lineEnding; /** * @var int The strlen() of $lineEnding */ private $lineEndingLength; /** * @param StreamInterface $stream * @param int $lineLength * @param string $lineEnding */ public function __construct(StreamInterface $stream, $lineLength = 76, $lineEnding = "\r\n") { $this->stream = $stream; $this->lineLength = $lineLength; $this->lineEnding = $lineEnding; $this->lineEndingLength = strlen($this->lineEnding); } /** * Inserts the line ending character after each line length characters in * the passed string, making sure previously written bytes are taken into * account. * * @param string $string * @return string */ private function getChunkedString($string) { $firstLine = ''; if ($this->tell() !== 0) { $next = $this->lineLength - ($this->position % ($this->lineLength + $this->lineEndingLength)); if (strlen($string) > $next) { $firstLine = substr($string, 0, $next) . $this->lineEnding; $string = substr($string, $next); } } // chunk_split always ends with the passed line ending $chunked = $firstLine . chunk_split($string, $this->lineLength, $this->lineEnding); return substr($chunked, 0, strlen($chunked) - $this->lineEndingLength); } /** * Writes the passed string to the underlying stream, ensuring line endings * are inserted every "line length" characters in the string. * * @param string $string * @return number of bytes written */ public function write($string) { $chunked = $this->getChunkedString($string); $this->position += strlen($chunked); return $this->stream->write($chunked); } /** * Inserts a final line ending character. */ private function beforeClose() { if ($this->position !== 0) { $this->stream->write($this->lineEnding); } } /** * Closes the stream after ensuring a final line ending character is * inserted. */ public function close() { $this->beforeClose(); $this->stream->close(); } /** * Detaches the stream after ensuring a final line ending character is * inserted. */ public function detach() { $this->beforeClose(); $this->stream->detach(); } } stream-decorators/src/NonClosingStream.php 0000644 00000002545 14736103046 0014741 0 ustar 00 <?php /** * This file is part of the ZBateson\StreamDecorators project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\StreamDecorators; use Psr\Http\Message\StreamInterface; use GuzzleHttp\Psr7\StreamDecoratorTrait; /** * Doesn't close the underlying stream when 'close' is called on it. Instead, * calling close simply removes any reference to the underlying stream. Note * that GuzzleHttp\Psr7\Stream calls close in __destruct, so a reference to the * Stream needs to be kept. For example: * * ``` * $f = fopen('php://temp', 'r+'); * $test = new NonClosingStream(Psr7\stream_for('test')); * // work * $test->close(); * rewind($f); // error, $f is a closed resource * ``` * * Instead, this would work: * * ``` * $stream = Psr7\stream_for(fopen('php://temp', 'r+')); * $test = new NonClosingStream($stream); * // work * $test->close(); * $stream->rewind(); // works * ``` * * @author Zaahid Bateson */ class NonClosingStream implements StreamInterface { use StreamDecoratorTrait; /** * Overridden to detach the underlying stream without closing it. */ public function close() { $this->stream = null; } /** * Overridden to detach the underlying stream without closing it. */ public function detach() { $this->stream = null; } } stream-decorators/src/UUStream.php 0000644 00000020604 14736103046 0013215 0 ustar 00 <?php /** * This file is part of the ZBateson\StreamDecorators project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\StreamDecorators; use Psr\Http\Message\StreamInterface; use GuzzleHttp\Psr7\StreamDecoratorTrait; use GuzzleHttp\Psr7\BufferStream; use RuntimeException; /** * GuzzleHttp\Psr7 stream decoder extension for UU-Encoded streams. * * The size of the underlying stream and the position of bytes can't be * determined because the number of encoded bytes is indeterminate without * reading the entire stream. * * @author Zaahid Bateson */ class UUStream implements StreamInterface { use StreamDecoratorTrait; /** * @var string name of the UUEncoded file */ protected $filename = null; /** * @var BufferStream of read and decoded bytes */ private $buffer; /** * @var string remainder of write operation if the bytes didn't align to 3 * bytes */ private $remainder = ''; /** * @var int read/write position */ private $position = 0; /** * @var boolean set to true when 'write' is called */ private $isWriting = false; /** * @param StreamInterface $stream Stream to decorate * @param string $filename optional file name */ public function __construct(StreamInterface $stream, $filename = null) { $this->stream = $stream; $this->filename = $filename; $this->buffer = new BufferStream(); } /** * Overridden to return the position in the target encoding. * * @return int */ public function tell() { return $this->position; } /** * Returns null, getSize isn't supported * * @return null */ public function getSize() { return null; } /** * Not supported. * * @param int $offset * @param int $whence * @throws RuntimeException */ public function seek($offset, $whence = SEEK_SET) { throw new RuntimeException('Cannot seek a UUStream'); } /** * Overridden to return false * * @return boolean */ public function isSeekable() { return false; } /** * Finds the next end-of-line character to ensure a line isn't broken up * while buffering. * * @return string */ private function readToEndOfLine($length) { $str = $this->stream->read($length); if ($str === false || $str === '') { return $str; } while (substr($str, -1) !== "\n") { $chr = $this->stream->read(1); if ($chr === false || $chr === '') { break; } $str .= $chr; } return $str; } /** * Removes invalid characters from a uuencoded string, and 'BEGIN' and 'END' * line headers and footers from the passed string before returning it. * * @param string $str * @return string */ private function filterAndDecode($str) { $ret = str_replace("\r", '', $str); $ret = preg_replace('/[^\x21-\xf5`\n]/', '`', $ret); if ($this->position === 0) { $matches = []; if (preg_match('/^\s*begin\s+[^\s+]\s+([^\r\n]+)\s*$/im', $ret, $matches)) { $this->filename = $matches[1]; } $ret = preg_replace('/^\s*begin[^\r\n]+\s*$/im', '', $ret); } else { $ret = preg_replace('/^\s*end\s*$/im', '', $ret); } return convert_uudecode(trim($ret)); } /** * Buffers bytes into $this->buffer, removing uuencoding headers and footers * and decoding them. */ private function fillBuffer($length) { // 5040 = 63 * 80, seems to be good balance for buffering in benchmarks // testing with a simple 'if ($length < x)' and calculating a better // size reduces speeds by up to 4x while ($this->buffer->getSize() < $length) { $read = $this->readToEndOfLine(5040); if ($read === false || $read === '') { break; } $this->buffer->write($this->filterAndDecode($read)); } } /** * Returns true if the end of stream has been reached. * * @return boolean */ public function eof() { return ($this->buffer->eof() && $this->stream->eof()); } /** * Attempts to read $length bytes after decoding them, and returns them. * * @param int $length * @return string */ public function read($length) { // let Guzzle decide what to do. if ($length <= 0 || $this->eof()) { return $this->stream->read($length); } $this->fillBuffer($length); $read = $this->buffer->read($length); $this->position += strlen($read); return $read; } /** * Writes the 'begin' UU header line. */ private function writeUUHeader() { $filename = (empty($this->filename)) ? 'null' : $this->filename; $this->stream->write("begin 666 $filename"); } /** * Writes the '`' and 'end' UU footer lines. */ private function writeUUFooter() { $this->stream->write("\r\n`\r\nend\r\n"); } /** * Writes the passed bytes to the underlying stream after encoding them. * * @param string $bytes */ private function writeEncoded($bytes) { $encoded = preg_replace('/\r\n|\r|\n/', "\r\n", rtrim(convert_uuencode($bytes))); // removes ending '`' line $this->stream->write("\r\n" . rtrim(substr($encoded, 0, -1))); } /** * Prepends any existing remainder to the passed string, then checks if the * string fits into a uuencoded line, and removes and keeps any remainder * from the string to write. Full lines ready for writing are returned. * * @param string $string * @return string */ private function handleRemainder($string) { $write = $this->remainder . $string; $nRem = strlen($write) % 45; $this->remainder = ''; if ($nRem !== 0) { $this->remainder = substr($write, -$nRem); $write = substr($write, 0, -$nRem); } return $write; } /** * Writes the passed string to the underlying stream after encoding it. * * Note that reading and writing to the same stream without rewinding is not * supported. * * Also note that some bytes may not be written until close or detach are * called. This happens if written data doesn't align to a complete * uuencoded 'line' of 45 bytes. In addition, the UU footer is only written * when closing or detaching as well. * * @param string $string * @return int the number of bytes written */ public function write($string) { $this->isWriting = true; if ($this->position === 0) { $this->writeUUHeader(); } $write = $this->handleRemainder($string); if ($write !== '') { $this->writeEncoded($write); } $written = strlen($string); $this->position += $written; return $written; } /** * Returns the filename set in the UUEncoded header (or null) * * @return string */ public function getFilename() { return $this->filename; } /** * Sets the UUEncoded header file name written in the 'begin' header line. * * @param string $filename */ public function setFilename($filename) { $this->filename = $filename; } /** * Writes out any remaining bytes and the UU footer. */ private function beforeClose() { if (!$this->isWriting) { return; } if ($this->remainder !== '') { $this->writeEncoded($this->remainder); } $this->remainder = ''; $this->isWriting = false; $this->writeUUFooter(); } /** * Writes any remaining bytes out followed by the uu-encoded footer, then * closes the stream. */ public function close() { $this->beforeClose(); $this->stream->close(); } /** * Writes any remaining bytes out followed by the uu-encoded footer, then * detaches the stream. */ public function detach() { $this->beforeClose(); $this->stream->detach(); } } stream-decorators/src/SeekingLimitStream.php 0000644 00000013434 14736103046 0015253 0 ustar 00 <?php /** * This file is part of the ZBateson\StreamDecorators project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\StreamDecorators; use Psr\Http\Message\StreamInterface; use GuzzleHttp\Psr7\StreamDecoratorTrait; /** * Maintains an internal 'read' position, and seeks to it before reading, then * seeks back to the original position of the underlying stream after reading if * the attached stream supports seeking. * * Although based on LimitStream, it's not inherited from it since $offset and * $limit are set to private on LimitStream, and most other functions are re- * implemented anyway. This also decouples the implementation from upstream * changes. * * @author Zaahid Bateson */ class SeekingLimitStream implements StreamInterface { use StreamDecoratorTrait; /** @var int Offset to start reading from */ private $offset; /** @var int Limit the number of bytes that can be read */ private $limit; /** * @var int Number of bytes written, and importantly, if non-zero, writes a * final $lineEnding on close (and so maintained instead of using * tell() directly) */ private $position = 0; /** * @param StreamInterface $stream Stream to wrap * @param int $limit Total number of bytes to allow to be read * from the stream. Pass -1 for no limit. * @param int $offset Position to seek to before reading (only * works on seekable streams). */ public function __construct( StreamInterface $stream, $limit = -1, $offset = 0 ) { $this->stream = $stream; $this->setLimit($limit); $this->setOffset($offset); } /** * Returns the current relative read position of this stream subset. * * @return int */ public function tell() { return $this->position; } /** * Returns the size of the limited subset of data, or null if the wrapped * stream returns null for getSize. * * @return int|null */ public function getSize() { $size = $this->stream->getSize(); if ($size === null) { // this shouldn't happen on a seekable stream I don't think... $pos = $this->stream->tell(); $this->stream->seek(0, SEEK_END); $size = $this->stream->tell(); $this->stream->seek($pos); } if ($this->limit === -1) { return $size - $this->offset; } else { return min([$this->limit, $size - $this->offset]); } } /** * Returns true if the current read position is at the end of the limited * stream * * @return boolean */ public function eof() { $size = $this->limit; if ($size === -1) { $size = $this->getSize(); } return ($this->position >= $size); } /** * Ensures the seek position specified is within the stream's bounds, and * sets the internal position pointer (doesn't actually seek). * * @param int $pos */ private function doSeek($pos) { if ($this->limit !== -1) { $pos = min([$pos, $this->limit]); } $this->position = max([0, $pos]); } /** * Seeks to the passed position within the confines of the limited stream's * bounds. * * For SeekingLimitStream, no actual seek is performed on the underlying * wrapped stream. Instead, an internal pointer is set, and the stream is * 'seeked' on read operations * * @param int $offset * @param int $whence */ public function seek($offset, $whence = SEEK_SET) { $pos = $offset; switch ($whence) { case SEEK_CUR: $pos = $this->position + $offset; break; case SEEK_END: $pos = $this->limit + $offset; break; default: break; } $this->doSeek($pos); } /** * Sets the offset to start reading from the wrapped stream. * * @param int $offset * @throws \RuntimeException if the stream cannot be seeked. */ public function setOffset($offset) { $this->offset = $offset; $this->position = 0; } /** * Sets the length of the stream to the passed $limit. * * @param int $limit */ public function setLimit($limit) { $this->limit = $limit; } /** * Seeks to the current position and reads up to $length bytes, or less if * it would result in reading past $this->limit * * @param int $length * @return string */ public function seekAndRead($length) { $this->stream->seek($this->offset + $this->position); if ($this->limit !== -1) { $length = min($length, $this->limit - $this->position); if ($length <= 0) { return ''; } } return $this->stream->read($length); } /** * Reads from the underlying stream after seeking to the position within the * bounds set for this limited stream. After reading, the wrapped stream is * 'seeked' back to its position prior to the call to read(). * * @param int $length * @return string */ public function read($length) { $pos = $this->stream->tell(); $ret = $this->seekAndRead($length); $this->position += strlen($ret); $this->stream->seek($pos); if ($this->limit !== -1 && $this->position > $this->limit) { $ret = substr($ret, 0, -($this->position - $this->limit)); $this->position = $this->limit; } return $ret; } } stream-decorators/src/Base64Stream.php 0000644 00000013301 14736103046 0013704 0 ustar 00 <?php /** * This file is part of the ZBateson\StreamDecorators project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\StreamDecorators; use Psr\Http\Message\StreamInterface; use GuzzleHttp\Psr7\StreamDecoratorTrait; use GuzzleHttp\Psr7\BufferStream; use RuntimeException; /** * GuzzleHttp\Psr7 stream decoder extension for base64 streams. * * Note that it's expected the underlying stream will only contain valid base64 * characters (normally the stream should be wrapped in a * PregReplaceFilterStream to filter out non-base64 characters for reading). * * ``` * $f = fopen(...); * $stream = new Base64Stream(new PregReplaceFilterStream( * Psr7\stream_for($f), '/[^a-zA-Z0-9\/\+=]/', '' * )); * //... * ``` * * For writing, a ChunkSplitStream could come in handy so the output is split * into lines: * * ``` * $f = fopen(...); * $stream = new Base64Stream(new ChunkSplitStream(new PregReplaceFilterStream( * Psr7\stream_for($f), '/[^a-zA-Z0-9\/\+=]/', '' * ))); * //... * ``` * * @author Zaahid Bateson */ class Base64Stream implements StreamInterface { use StreamDecoratorTrait; /** * @var BufferStream buffered bytes */ private $buffer; /** * @var string remainder of write operation if the bytes didn't align to 3 * bytes */ private $remainder = ''; /** * @var int current number of read/written bytes (for tell()) */ private $position = 0; /** * @param StreamInterface $stream */ public function __construct(StreamInterface $stream) { $this->stream = $stream; $this->buffer = new BufferStream(); } /** * Returns the current position of the file read/write pointer * * @return int */ public function tell() { return $this->position; } /** * Returns null, getSize isn't supported * * @return null */ public function getSize() { return null; } /** * Not implemented (yet). * * Seek position can be calculated. * * @param int $offset * @param int $whence * @throws RuntimeException */ public function seek($offset, $whence = SEEK_SET) { throw new RuntimeException('Cannot seek a Base64Stream'); } /** * Overridden to return false * * @return boolean */ public function isSeekable() { return false; } /** * Returns true if the end of stream has been reached. * * @return boolean */ public function eof() { return ($this->buffer->eof() && $this->stream->eof()); } /** * Fills the internal byte buffer after reading and decoding data from the * underlying stream. * * Note that it's expected the underlying stream will only contain valid * base64 characters (normally the stream should be wrapped in a * PregReplaceFilterStream to filter out non-base64 characters). * * @param int $length */ private function fillBuffer($length) { $fill = 8192; while ($this->buffer->getSize() < $length) { $read = $this->stream->read($fill); if ($read === false || $read === '') { break; } $this->buffer->write(base64_decode($read)); } } /** * Attempts to read $length bytes after decoding them, and returns them. * * Note that reading and writing to the same stream may result in wrongly * encoded data and is not supported. * * @param int $length * @return string */ public function read($length) { // let Guzzle decide what to do. if ($length <= 0 || $this->eof()) { return $this->stream->read($length); } $this->fillBuffer($length); $ret = $this->buffer->read($length); $this->position += strlen($ret); return $ret; } /** * Writes the passed string to the underlying stream after encoding it to * base64. * * Base64Stream::close or detach must be called. Failing to do so may * result in 1-2 bytes missing from the end of the stream if there's a * remainder. Note that the default Stream destructor calls close as well. * * Note that reading and writing to the same stream may result in wrongly * encoded data and is not supported. * * @param string $string * @return int the number of bytes written */ public function write($string) { $bytes = $this->remainder . $string; $len = strlen($bytes); if (($len % 3) !== 0) { $this->remainder = substr($bytes, -($len % 3)); $bytes = substr($bytes, 0, $len - ($len % 3)); } else { $this->remainder = ''; } $this->stream->write(base64_encode($bytes)); $written = strlen($string); $this->position += $len; return $written; } /** * Writes out any remaining bytes at the end of the stream and closes. */ private function beforeClose() { if ($this->isWritable() && $this->remainder !== '') { $this->stream->write(base64_encode($this->remainder)); $this->remainder = ''; } } /** * Closes the underlying stream after writing out any remaining bytes * needing to be encoded. */ public function close() { $this->beforeClose(); $this->stream->close(); } /** * Detaches the underlying stream after writing out any remaining bytes * needing to be encoded. */ public function detach() { $this->beforeClose(); $this->stream->detach(); } } stream-decorators/src/CharsetStream.php 0000644 00000011541 14736103046 0014255 0 ustar 00 <?php /** * This file is part of the ZBateson\StreamDecorator project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\StreamDecorators; use Psr\Http\Message\StreamInterface; use GuzzleHttp\Psr7\StreamDecoratorTrait; use ZBateson\MbWrapper\MbWrapper; use RuntimeException; /** * GuzzleHttp\Psr7 stream decoder extension for charset conversion. * * @author Zaahid Bateson */ class CharsetStream implements StreamInterface { use StreamDecoratorTrait; /** * @var MbWrapper the charset converter */ protected $converter = null; /** * @var string charset of the source stream */ protected $streamCharset = 'ISO-8859-1'; /** * @var string charset of strings passed in write operations, and returned * in read operations. */ protected $stringCharset = 'UTF-8'; /** * @var int current read/write position */ private $position = 0; /** * @var int number of $stringCharset characters in $buffer */ private $bufferLength = 0; /** * @var string a buffer of characters read in the original $streamCharset * encoding */ private $buffer = ''; /** * @param StreamInterface $stream Stream to decorate * @param string $streamCharset The underlying stream's charset * @param string $stringCharset The charset to encode strings to (or * expected for write) */ public function __construct(StreamInterface $stream, $streamCharset = 'ISO-8859-1', $stringCharset = 'UTF-8') { $this->stream = $stream; $this->converter = new MbWrapper(); $this->streamCharset = $streamCharset; $this->stringCharset = $stringCharset; } /** * Overridden to return the position in the target encoding. * * @return int */ public function tell() { return $this->position; } /** * Returns null, getSize isn't supported * * @return null */ public function getSize() { return null; } /** * Not supported. * * @param int $offset * @param int $whence * @throws RuntimeException */ public function seek($offset, $whence = SEEK_SET) { throw new RuntimeException('Cannot seek a CharsetStream'); } /** * Overridden to return false * * @return boolean */ public function isSeekable() { return false; } /** * Reads a minimum of $length characters from the underlying stream in its * encoding into $this->buffer. * * Aligning to 4 bytes seemed to solve an issue reading from UTF-16LE * streams and pass testReadUtf16LeToEof, although the buffered string * should've solved that on its own. * * @param int $length */ private function readRawCharsIntoBuffer($length) { $n = ceil(($length + 32) / 4.0) * 4; while ($this->bufferLength < $n) { $raw = $this->stream->read($n + 512); if ($raw === false || $raw === '') { return; } $this->buffer .= $raw; $this->bufferLength = $this->converter->getLength($this->buffer, $this->streamCharset); } } /** * Returns true if the end of stream has been reached. * * @return boolean */ public function eof() { return ($this->bufferLength === 0 && $this->stream->eof()); } /** * Reads up to $length decoded chars from the underlying stream and returns * them after converting to the target string charset. * * @param int $length * @return string */ public function read($length) { // let Guzzle decide what to do. if ($length <= 0 || $this->eof()) { return $this->stream->read($length); } $this->readRawCharsIntoBuffer($length); $numChars = min([$this->bufferLength, $length]); $chars = $this->converter->getSubstr($this->buffer, $this->streamCharset, 0, $numChars); $this->position += $numChars; $this->buffer = $this->converter->getSubstr($this->buffer, $this->streamCharset, $numChars); $this->bufferLength = $this->bufferLength - $numChars; return $this->converter->convert($chars, $this->streamCharset, $this->stringCharset); } /** * Writes the passed string to the underlying stream after converting it to * the target stream encoding. * * @param string $string * @return int the number of bytes written */ public function write($string) { $converted = $this->converter->convert($string, $this->stringCharset, $this->streamCharset); $written = $this->converter->getLength($converted, $this->streamCharset); $this->position += $written; return $this->stream->write($converted); } } stream-decorators/src/PregReplaceFilterStream.php 0000644 00000005210 14736103046 0016217 0 ustar 00 <?php /** * This file is part of the ZBateson\StreamDecorators project. * * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace ZBateson\StreamDecorators; use Psr\Http\Message\StreamInterface; use GuzzleHttp\Psr7\StreamDecoratorTrait; use GuzzleHttp\Psr7\BufferStream; use RuntimeException; /** * Calls preg_replace on each read operation with the passed pattern and * replacement string. Should only really be used to find single characters, * since a pattern intended to match more may be split across multiple read() * operations. * * @author Zaahid Bateson */ class PregReplaceFilterStream implements StreamInterface { use StreamDecoratorTrait; /** * @var string The regex pattern */ private $pattern; /** * @var string The replacement */ private $replacement; /** * @var BufferStream Buffered stream of input from the underlying stream */ private $buffer; public function __construct(StreamInterface $stream, $pattern, $replacement) { $this->stream = $stream; $this->pattern = $pattern; $this->replacement = $replacement; $this->buffer = new BufferStream(); } /** * Returns true if the end of stream has been reached. * * @return boolean */ public function eof() { return ($this->buffer->eof() && $this->stream->eof()); } /** * Not supported by PregReplaceFilterStream * * @param int $offset * @param int $whence * @throws RuntimeException */ public function seek($offset, $whence = SEEK_SET) { throw new RuntimeException('Cannot seek a PregReplaceFilterStream'); } /** * Overridden to return false * * @return boolean */ public function isSeekable() { return false; } /** * Fills the BufferStream with at least 8192 characters of input for future * read operations. * * @param int $length */ private function fillBuffer($length) { $fill = intval(max([$length, 8192])); while ($this->buffer->getSize() < $length) { $read = $this->stream->read($fill); if ($read === false || $read === '') { break; } $this->buffer->write(preg_replace($this->pattern, $this->replacement, $read)); } } /** * Reads from the underlying stream, filters it and returns up to $length * bytes. * * @param int $length * @return string */ public function read($length) { $this->fillBuffer($length); return $this->buffer->read($length); } } stream-decorators/README.md 0000644 00000010026 14736103046 0011464 0 ustar 00 # zbateson/stream-decorators Psr7 stream decorators for character set conversion and common mail format content encodings. [![Build Status](https://travis-ci.org/zbateson/stream-decorators.svg?branch=master)](https://travis-ci.org/zbateson/stream-decorators) [![Code Coverage](https://scrutinizer-ci.com/g/zbateson/stream-decorators/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/zbateson/stream-decorators/?branch=master) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/zbateson/stream-decorators/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/zbateson/stream-decorators/?branch=master) [![Total Downloads](https://poser.pugx.org/zbateson/stream-decorators/downloads)](https://packagist.org/packages/zbateson/stream-decorators) [![Latest Stable Version](https://poser.pugx.org/zbateson/stream-decorators/version)](https://packagist.org/packages/zbateson/stream-decorators) The goals of this project are to be: * Well written * Standards-compliant but forgiving * Tested where possible To include it for use in your project, please install via composer: ``` composer require zbateson/stream-decorators ``` ## Requirements StreamDecorators requires PHP 5.4 or newer. Tested on PHP 5.4, 5.5, 5.6, 7, 7.1, 7.2 and 7.3 on travis. Please note: hhvm support has been dropped as it no longer supports 'php' as of version 4. Previous versions of hhvm may still work, but are no longer supported. ## Usage ```php $stream = GuzzleHttp\Psr7\stream_for($handle); $b64Stream = new ZBateson\StreamDecorators\Base64Stream($stream); $charsetStream = new ZBateson\StreamDecorators\CharsetStream($b64Stream, 'UTF-32', 'UTF-8'); while (($line = GuzzleHttp\Psr7\readline()) !== false) { echo $line, "\r\n"; } ``` Note that CharsetStream, depending on the target encoding, may return multiple bytes when a single 'char' is read. If using php's 'fread', this will result in a warning: 'read x bytes more data than requested (xxxx read, xxxx max) - excess data will be lost This is because the parameter to 'fread' is bytes, and so when CharsetStream returns, say, 4 bytes representing a single UTF-32 character, fread will truncate to the first byte when requesting '1' byte. It is recommended to **not** convert to a stream handle (with StreamWrapper) for this reason when using CharsetStream. The library consists of the following Psr\Http\Message\StreamInterface implementations: * ZBateson\StreamDecorators\QuotedPrintableStream - decodes on read and encodes on write to quoted-printable * ZBateson\StreamDecorators\Base64Stream - decodes on read and encodes on write to base64 * ZBateson\StreamDecorators\UUStream - decodes on read, encodes on write to uu-encoded * ZBateson\StreamDecorators\CharsetStream - encodes from $streamCharset to $stringCharset on read, and vice-versa on write * ZBateson\StreamDecorators\NonClosingStream - overrides close() and detach(), and simply unsets the attached stream without closing it * ZBateson\StreamDecorators\ChunkSplitStream - splits written characters into lines of $lineLength long (stream implementation of php's chunk_split) * ZBateson\StreamDecorators\PregReplaceFilterStream - calls preg_replace on with passed arguments on every read() call * ZBateson\StreamDecorators\SeekingLimitStream - similar to GuzzleHttp's LimitStream, but maintains an internal current read position, seeking to it when read() is called, and seeking back to the wrapped stream's position after reading QuotedPrintableStream, Base64Stream and UUStream's constructors take a single argument of a StreamInterface. CharsetStreams's constructor also takes $streamCharset and $stringCharset as arguments respectively, ChunkSplitStream optionally takes a $lineLength argument (defaults to 76) and a $lineEnding argument (defaults to CRLF). PregReplaceFilterStream takes a $pattern argument and a $replacement argument. SeekingLimitStream takes optional $limit and $offset parameters, similar to GuzzleHttp's LimitStream. ## License BSD licensed - please see [license agreement](https://github.com/zbateson/stream-decorators/blob/master/LICENSE). stream-decorators/LICENSE 0000644 00000002447 14736103046 0011222 0 ustar 00 BSD 2-Clause License Copyright (c) 2017, Zaahid Bateson All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.