├── version.txt ├── .github ├── FUNDING.yml └── workflows │ └── tests.yml ├── PHPStanConstants.php ├── phpstan.neon ├── .php-cs-fixer.dist.php ├── src ├── Message │ ├── Factory │ │ ├── PartChildrenContainerFactory.php │ │ ├── IUUEncodedPartFactory.php │ │ ├── PartHeaderContainerFactory.php │ │ ├── IMessagePartFactory.php │ │ ├── PartStreamContainerFactory.php │ │ └── IMimePartFactory.php │ ├── Helper │ │ └── AbstractHelper.php │ ├── IUUEncodedPart.php │ ├── NonMimePart.php │ ├── UUEncodedPart.php │ └── PartFilter.php ├── Parser │ ├── Part │ │ ├── ParserPartChildrenContainerFactory.php │ │ ├── UUEncodedPartHeaderContainerFactory.php │ │ ├── ParserPartChildrenContainer.php │ │ ├── ParserPartStreamContainerFactory.php │ │ └── UUEncodedPartHeaderContainer.php │ ├── CompatibleParserNotFoundException.php │ ├── Proxy │ │ ├── ParserPartProxyFactory.php │ │ ├── ParserMessageProxy.php │ │ ├── ParserNonMimeMessageProxyFactory.php │ │ ├── ParserUUEncodedPartProxyFactory.php │ │ ├── ParserMessageProxyFactory.php │ │ ├── ParserMimePartProxyFactory.php │ │ ├── ParserNonMimeMessageProxy.php │ │ └── ParserUUEncodedPartProxy.php │ ├── PartBuilderFactory.php │ ├── HeaderParserService.php │ ├── AbstractParserService.php │ ├── MessageParserService.php │ ├── ParserManagerService.php │ ├── IParserService.php │ └── NonMimeParserService.php ├── Header │ ├── Part │ │ ├── MimeTokenPartFactory.php │ │ ├── ReceivedPart.php │ │ ├── SubjectToken.php │ │ ├── QuotedLiteralPart.php │ │ ├── AddressPart.php │ │ ├── NameValuePart.php │ │ ├── Token.php │ │ ├── CommentPart.php │ │ ├── DatePart.php │ │ ├── AddressGroupPart.php │ │ ├── SplitParameterPart.php │ │ ├── MimeToken.php │ │ ├── ReceivedDomainPart.php │ │ ├── HeaderPart.php │ │ ├── ParameterPart.php │ │ └── ContainerPart.php │ ├── Consumer │ │ ├── IConsumerService.php │ │ ├── GenericConsumerMimeLiteralPartService.php │ │ ├── GenericConsumerService.php │ │ ├── QuotedStringMimeLiteralPartConsumerService.php │ │ ├── Received │ │ │ ├── ReceivedDateConsumerService.php │ │ │ ├── DomainConsumerService.php │ │ │ └── GenericReceivedConsumerService.php │ │ ├── QuotedStringMimeLiteralPartTokenSplitPatternTrait.php │ │ ├── DateConsumerService.php │ │ ├── IdConsumerService.php │ │ ├── ParameterValueConsumerService.php │ │ ├── AbstractGenericConsumerService.php │ │ ├── SubjectConsumerService.php │ │ ├── QuotedStringConsumerService.php │ │ ├── AddressEmailConsumerService.php │ │ ├── IdBaseConsumerService.php │ │ ├── AddressBaseConsumerService.php │ │ ├── ParameterNameValueConsumerService.php │ │ ├── AddressGroupConsumerService.php │ │ ├── ParameterConsumerService.php │ │ ├── CommentConsumerService.php │ │ └── ReceivedConsumerService.php │ ├── IHeaderPart.php │ ├── SubjectHeader.php │ ├── GenericHeader.php │ ├── DateHeader.php │ ├── IdHeader.php │ ├── MimeEncodedHeader.php │ ├── HeaderConsts.php │ ├── IHeader.php │ ├── ParameterHeader.php │ └── AddressHeader.php ├── Stream │ ├── MessagePartStreamReadException.php │ ├── MessagePartStreamDecorator.php │ └── HeaderStream.php ├── IErrorBag.php ├── Error.php ├── di_config.php └── ErrorBag.php ├── LICENSE └── composer.json /version.txt: -------------------------------------------------------------------------------- 1 | 3.0.4 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: zbateson 4 | -------------------------------------------------------------------------------- /PHPStanConstants.php: -------------------------------------------------------------------------------- 1 | setFinder(PhpCsFixer\Finder::create() 12 | ->exclude('vendor') 13 | ->in(__DIR__.'/src') 14 | ->in(__DIR__.'/tests') 15 | ); 16 | -------------------------------------------------------------------------------- /src/Message/Factory/PartChildrenContainerFactory.php: -------------------------------------------------------------------------------- 1 | newMimeToken($value); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Header/Consumer/IConsumerService.php: -------------------------------------------------------------------------------- 1 | name = $name; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Stream/MessagePartStreamReadException.php: -------------------------------------------------------------------------------- 1 | part = $part; 30 | } 31 | 32 | public function getPart() : IMessagePart 33 | { 34 | return $this->part; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Header/Consumer/GenericConsumerMimeLiteralPartService.php: -------------------------------------------------------------------------------- 1 | mimePartFactory = $mimePartFactory; 35 | $this->uuEncodedPartFactory = $uuEncodedPartFactory; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Header/SubjectHeader.php: -------------------------------------------------------------------------------- 1 | get(LoggerInterface::class), 33 | $consumerService ?? $di->get(SubjectConsumerService::class), 34 | $name, 35 | $value 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Parser/Proxy/ParserMessageProxy.php: -------------------------------------------------------------------------------- 1 | lastLineEndingLength; 29 | } 30 | 31 | public function setLastLineEndingLength(int $lastLineEndingLength) : static 32 | { 33 | $this->lastLineEndingLength = $lastLineEndingLength; 34 | return $this; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Header/Consumer/QuotedStringMimeLiteralPartConsumerService.php: -------------------------------------------------------------------------------- 1 | partFactory->newMimeToken($token); 30 | } 31 | return $this->partFactory->newToken($token, $isLiteral); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Header/Consumer/Received/ReceivedDateConsumerService.php: -------------------------------------------------------------------------------- 1 | partStreamContainerFactory->newInstance(); 27 | $part = new UUEncodedPart( 28 | null, 29 | null, 30 | $parent, 31 | $this->logger, 32 | $streamContainer 33 | ); 34 | $streamContainer->setStream($this->streamFactory->newMessagePartStream($part)); 35 | return $part; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | os: [ubuntu-latest, windows-latest] 12 | php: [8.5, 8.4, 8.3, 8.2, 8.1, 8.0] 13 | stability: [prefer-stable] 14 | 15 | name: P${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }} 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v2 20 | 21 | - name: Setup PHP 22 | uses: shivammathur/setup-php@v2 23 | with: 24 | php-version: ${{ matrix.php }} 25 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo 26 | coverage: none 27 | 28 | - name: Setup problem matchers 29 | run: | 30 | echo "::add-matcher::${{ runner.tool_cache }}/php.json" 31 | echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" 32 | - name: Install dependencies 33 | run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction 34 | 35 | - name: Execute tests 36 | run: ./vendor/bin/phpunit -c tests/phpunit.xml 37 | -------------------------------------------------------------------------------- /src/Parser/PartBuilderFactory.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 35 | $this->headerFactory = $headerFactory; 36 | } 37 | 38 | /** 39 | * Creates and returns a PartHeaderContainer. 40 | */ 41 | public function newInstance(?PartHeaderContainer $from = null) : PartHeaderContainer 42 | { 43 | return new PartHeaderContainer($this->logger, $this->headerFactory, $from); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Message/Factory/IMessagePartFactory.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 34 | $this->streamFactory = $streamFactory; 35 | $this->partStreamContainerFactory = $partStreamContainerFactory; 36 | } 37 | 38 | /** 39 | * Constructs a new IMessagePart object and returns it 40 | */ 41 | abstract public function newInstance(?IMimePart $parent = null) : IMessagePart; 42 | } 43 | -------------------------------------------------------------------------------- /src/Header/Part/SubjectToken.php: -------------------------------------------------------------------------------- 1 | value = \preg_replace(['/(\r|\n)+(\s)\s*/', '/(\r|\n)+/'], ['$2', ' '], $value); 32 | $this->isSpace = (\preg_match('/^\s*$/m', $this->value) === 1); 33 | $this->canIgnoreSpacesBefore = $this->canIgnoreSpacesAfter = $this->isSpace; 34 | } 35 | 36 | public function getValue() : string 37 | { 38 | return $this->convertEncoding($this->value); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2015, Zaahid Bateson 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /src/Header/Consumer/QuotedStringMimeLiteralPartTokenSplitPatternTrait.php: -------------------------------------------------------------------------------- 1 | getAllTokenSeparators()); 34 | $mimePartPattern = MimeToken::MIME_PART_PATTERN_NO_QUOTES; 35 | return '~(' . $mimePartPattern . '|\\\\\r\n|\\\\.|' . $sChars . ')~ms'; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Parser/Part/UUEncodedPartHeaderContainerFactory.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 35 | $this->headerFactory = $headerFactory; 36 | } 37 | 38 | /** 39 | * Creates and returns a UUEncodedPartHeaderContainer. 40 | */ 41 | public function newInstance(int $mode, string $filename) : UUEncodedPartHeaderContainer 42 | { 43 | $container = new UUEncodedPartHeaderContainer($this->logger, $this->headerFactory); 44 | $container->setUnixFileMode($mode); 45 | $container->setFilename($filename); 46 | return $container; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Header/Consumer/DateConsumerService.php: -------------------------------------------------------------------------------- 1 | partFactory->newToken($token, false); 30 | } 31 | 32 | /** 33 | * Constructs a single Part\DatePart of any parsed parts returning it in an 34 | * array with a single element. 35 | * 36 | * @param \ZBateson\MailMimeParser\Header\IHeaderPart[] $parts The parsed 37 | * parts. 38 | * @return \ZBateson\MailMimeParser\Header\IHeaderPart[] Array of resulting 39 | * final parts. 40 | */ 41 | protected function processParts(array $parts) : array 42 | { 43 | return [$this->partFactory->newDatePart($parts)]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Header/GenericHeader.php: -------------------------------------------------------------------------------- 1 | get(LoggerInterface::class), 33 | $consumerService ?? $di->get(DateConsumerService::class), 34 | $name, 35 | $value 36 | ); 37 | parent::__construct($logger, $consumerService, $name, $value); 38 | } 39 | 40 | public function getValue() : ?string 41 | { 42 | if (!empty($this->parts)) { 43 | return \implode('', \array_map(function($p) { return $p->getValue(); }, $this->parts)); 44 | } 45 | return null; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Header/Consumer/IdConsumerService.php: -------------------------------------------------------------------------------- 1 | ' char. 15 | * 16 | * @author Zaahid Bateson 17 | */ 18 | class IdConsumerService extends GenericConsumerService 19 | { 20 | /** 21 | * Overridden to return patterns matching the beginning part of an ID ('<' 22 | * and '>' chars). 23 | * 24 | * @return string[] the patterns 25 | */ 26 | public function getTokenSeparators() : array 27 | { 28 | return \array_merge(parent::getTokenSeparators(), ['<', '>']); 29 | } 30 | 31 | /** 32 | * Returns true for '>'. 33 | */ 34 | protected function isEndToken(string $token) : bool 35 | { 36 | return ($token === '>'); 37 | } 38 | 39 | /** 40 | * Returns true for '<'. 41 | */ 42 | protected function isStartToken(string $token) : bool 43 | { 44 | return ($token === '<'); 45 | } 46 | 47 | /** 48 | * Returns null for whitespace, and Token for anything else. 49 | */ 50 | protected function getPartForToken(string $token, bool $isLiteral) : ?IHeaderPart 51 | { 52 | if (\preg_match('/^\s+$/', $token)) { 53 | return null; 54 | } 55 | return $this->partFactory->newToken($token, true); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zbateson/mail-mime-parser", 3 | "description": "MIME email message parser", 4 | "keywords": ["mail", "mime", "parser", "email", "php-imap", "mailparse", "mimeparse", "MimeMailParser"], 5 | "homepage": "https://mail-mime-parser.org", 6 | "license": "BSD-2-Clause", 7 | "authors": [ 8 | { 9 | "name": "Zaahid Bateson" 10 | }, 11 | { 12 | "name": "Contributors", 13 | "homepage": "https://github.com/zbateson/mail-mime-parser/graphs/contributors" 14 | } 15 | ], 16 | "support": { 17 | "issues": "https://github.com/zbateson/mail-mime-parser/issues", 18 | "source": "https://github.com/zbateson/mail-mime-parser", 19 | "docs": "https://mail-mime-parser.org/#usage-guide" 20 | }, 21 | "require": { 22 | "php": ">=8.0", 23 | "guzzlehttp/psr7": "^2.5", 24 | "zbateson/mb-wrapper": "^2.0", 25 | "zbateson/stream-decorators": "^2.1", 26 | "php-di/php-di": "^6.0|^7.0", 27 | "psr/log": "^1|^2|^3" 28 | }, 29 | "require-dev": { 30 | "phpunit/phpunit": "^9.6", 31 | "friendsofphp/php-cs-fixer": "*", 32 | "phpstan/phpstan": "*", 33 | "monolog/monolog": "^2|^3" 34 | }, 35 | "suggest": { 36 | "ext-mbstring": "For best support/performance", 37 | "ext-iconv": "For best support/performance" 38 | }, 39 | "autoload": { 40 | "psr-4": {"ZBateson\\MailMimeParser\\": "src/"} 41 | }, 42 | "autoload-dev": { 43 | "psr-4": {"ZBateson\\MailMimeParser\\": "tests/MailMimeParser"} 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Message/IUUEncodedPart.php: -------------------------------------------------------------------------------- 1 | $key + 1) ? $parts[$key + 1] : null; 33 | if ($last !== null && $next !== null && $cur->isSpace && ( 34 | $last->canIgnoreSpacesAfter 35 | && $next->canIgnoreSpacesBefore 36 | && $last instanceof MimeToken 37 | && $next instanceof MimeToken 38 | )) { 39 | return $carry; 40 | } 41 | return \array_merge($carry ?? [], [$cur]); 42 | } 43 | ); 44 | return $filtered; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Message/Factory/PartStreamContainerFactory.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 37 | $this->streamFactory = $streamFactory; 38 | $this->mbWrapper = $mbWrapper; 39 | $this->throwExceptionReadingPartContentFromUnsupportedCharsets = $throwExceptionReadingPartContentFromUnsupportedCharsets; 40 | } 41 | 42 | public function newInstance() : PartStreamContainer 43 | { 44 | return new PartStreamContainer( 45 | $this->logger, 46 | $this->streamFactory, 47 | $this->mbWrapper, 48 | $this->throwExceptionReadingPartContentFromUnsupportedCharsets 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Parser/Part/ParserPartChildrenContainer.php: -------------------------------------------------------------------------------- 1 | parserProxy = $parserProxy; 37 | } 38 | 39 | public function offsetExists($offset) : bool 40 | { 41 | $exists = parent::offsetExists($offset); 42 | while (!$exists && !$this->allParsed) { 43 | $child = $this->parserProxy->popNextChild(); 44 | if ($child === null) { 45 | $this->allParsed = true; 46 | } else { 47 | $this->add($child); 48 | } 49 | $exists = parent::offsetExists($offset); 50 | } 51 | return $exists; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Message/NonMimePart.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 37 | $this->streamFactory = $streamFactory; 38 | $this->mbWrapper = $mbWrapper; 39 | $this->throwExceptionReadingPartContentFromUnsupportedCharsets = $throwExceptionReadingPartContentFromUnsupportedCharsets; 40 | } 41 | 42 | public function newInstance(ParserPartProxy $parserProxy) : ParserPartStreamContainer 43 | { 44 | return new ParserPartStreamContainer( 45 | $this->logger, 46 | $this->streamFactory, 47 | $this->mbWrapper, 48 | $this->throwExceptionReadingPartContentFromUnsupportedCharsets, 49 | $parserProxy 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Header/Part/AddressPart.php: -------------------------------------------------------------------------------- 1 | getValue(); 32 | } elseif ($p instanceof QuotedLiteralPart && $p->getValue() !== '') { 33 | return '"' . \preg_replace('/(["\\\])/', '\\\$1', $p->getValue()) . '"'; 34 | } 35 | return \preg_replace('/\s+/', '', $p->getValue()); 36 | }, 37 | $parts 38 | )); 39 | } 40 | 41 | /** 42 | * Returns the email address. 43 | * 44 | * @return string The email address. 45 | */ 46 | public function getEmail() : string 47 | { 48 | return $this->value; 49 | } 50 | 51 | protected function validate() : void 52 | { 53 | if (empty($this->value)) { 54 | $this->addError('Address doesn\'t contain an email address', LogLevel::ERROR); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Parser/Part/UUEncodedPartHeaderContainer.php: -------------------------------------------------------------------------------- 1 | mode; 38 | } 39 | 40 | /** 41 | * Sets the unix file mode for the uuencoded 'begin' line. 42 | */ 43 | public function setUnixFileMode(int $mode) : static 44 | { 45 | $this->mode = $mode; 46 | return $this; 47 | } 48 | 49 | /** 50 | * Returns the filename included in the uuencoded 'begin' line for this 51 | * part. 52 | */ 53 | public function getFilename() : ?string 54 | { 55 | return $this->filename; 56 | } 57 | 58 | /** 59 | * Sets the filename included in the uuencoded 'begin' line. 60 | */ 61 | public function setFilename(string $filename) : static 62 | { 63 | $this->filename = $filename; 64 | return $this; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Parser/Proxy/ParserNonMimeMessageProxyFactory.php: -------------------------------------------------------------------------------- 1 | parserPartStreamContainerFactory->newInstance($parserProxy); 30 | $headerContainer = $this->partHeaderContainerFactory->newInstance($parserProxy->getHeaderContainer()); 31 | $childrenContainer = $this->parserPartChildrenContainerFactory->newInstance($parserProxy); 32 | 33 | $message = new Message( 34 | $this->logger, 35 | $streamContainer, 36 | $headerContainer, 37 | $childrenContainer, 38 | $this->multipartHelper, 39 | $this->privacyHelper 40 | ); 41 | $parserProxy->setPart($message); 42 | 43 | $streamContainer->setStream($this->streamFactory->newMessagePartStream($message)); 44 | $message->attach($streamContainer); 45 | return $parserProxy; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Stream/MessagePartStreamDecorator.php: -------------------------------------------------------------------------------- 1 | part = $part; 36 | $this->stream = $stream; 37 | } 38 | 39 | /** 40 | * Overridden to wrap exceptions in MessagePartReadException which provides 41 | * 'getPart' to inspect the part the error occurs on. 42 | * 43 | * @throws MessagePartStreamReadException 44 | */ 45 | public function read(int $length) : string 46 | { 47 | try { 48 | return $this->decoratorRead($length); 49 | } catch (MessagePartStreamReadException $me) { 50 | throw $me; 51 | } catch (RuntimeException $e) { 52 | throw new MessagePartStreamReadException( 53 | $this->part, 54 | 'Exception occurred reading a part stream: cid=' . $this->part->getContentId() 55 | . ' type=' . $this->part->getContentType() . ', message: ' . $e->getMessage(), 56 | $e->getCode(), 57 | $e 58 | ); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Header/Part/NameValuePart.php: -------------------------------------------------------------------------------- 1 | charsetConverter = $charsetConverter; 35 | $this->name = (!empty($nameParts)) ? $this->getNameFromParts($nameParts) : ''; 36 | parent::__construct($logger, $charsetConverter, $valueParts); 37 | \array_unshift($this->children, ...$nameParts); 38 | } 39 | 40 | /** 41 | * Creates the string 'name' representation of this part constructed from 42 | * the child name parts passed to it. 43 | * 44 | * @param HeaderParts[] $parts 45 | */ 46 | protected function getNameFromParts(array $parts) : string 47 | { 48 | return \array_reduce($this->filterIgnoredSpaces($parts), fn ($c, $p) => $c . $p->getValue(), ''); 49 | } 50 | 51 | /** 52 | * Returns the name of the name/value part. 53 | */ 54 | public function getName() : string 55 | { 56 | return $this->name; 57 | } 58 | 59 | protected function validate() : void 60 | { 61 | if ($this->value === '') { 62 | $this->addError('NameValuePart value is empty', LogLevel::NOTICE); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Header/Consumer/ParameterValueConsumerService.php: -------------------------------------------------------------------------------- 1 | partHeaderContainerFactory = $partHeaderContainerFactory; 35 | $this->partChildrenContainerFactory = $partChildrenContainerFactory; 36 | } 37 | 38 | /** 39 | * Constructs a new IMimePart object and returns it 40 | */ 41 | public function newInstance(?IMimePart $parent = null) : IMimePart 42 | { 43 | $streamContainer = $this->partStreamContainerFactory->newInstance(); 44 | $headerContainer = $this->partHeaderContainerFactory->newInstance(); 45 | $part = new MimePart( 46 | $parent, 47 | $this->logger, 48 | $streamContainer, 49 | $headerContainer, 50 | $this->partChildrenContainerFactory->newInstance() 51 | ); 52 | $streamContainer->setStream($this->streamFactory->newMessagePartStream($part)); 53 | return $part; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Header/DateHeader.php: -------------------------------------------------------------------------------- 1 | get(LoggerInterface::class), 33 | $consumerService ?? $di->get(DateConsumerService::class), 34 | $name, 35 | $value 36 | ); 37 | } 38 | 39 | /** 40 | * Convenience method returning the part's DateTime object, or null if the 41 | * date could not be parsed. 42 | * 43 | * @return ?DateTime The parsed DateTime object. 44 | */ 45 | public function getDateTime() : ?DateTime 46 | { 47 | if (!empty($this->parts) && $this->parts[0] instanceof DatePart) { 48 | return $this->parts[0]->getDateTime(); 49 | } 50 | return null; 51 | } 52 | 53 | /** 54 | * Returns a DateTimeImmutable for the part's DateTime object, or null if 55 | * the date could not be parsed. 56 | * 57 | * @return ?DateTimeImmutable The parsed DateTimeImmutable object. 58 | */ 59 | public function getDateTimeImmutable() : ?DateTimeImmutable 60 | { 61 | $dateTime = $this->getDateTime(); 62 | if ($dateTime !== null) { 63 | return DateTimeImmutable::createFromMutable($dateTime); 64 | } 65 | return null; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Header/Part/Token.php: -------------------------------------------------------------------------------- 1 | rawValue = $value; 40 | if (!$isLiteral) { 41 | $this->value = \preg_replace(['/(\r|\n)+(\s)/', '/(\r|\n)+/'], ['$2', ' '], $value); 42 | if (!$preserveSpaces) { 43 | $this->value = \preg_replace('/^\s+$/m', ' ', $this->value); 44 | } 45 | } 46 | $this->isSpace = ($this->value === '' || (!$isLiteral && \preg_match('/^\s*$/m', $this->value) === 1)); 47 | $this->canIgnoreSpacesBefore = $this->canIgnoreSpacesAfter = $this->isSpace; 48 | } 49 | 50 | /** 51 | * Returns the part's representative value after any necessary processing 52 | * has been performed. For the raw value, call getRawValue(). 53 | */ 54 | public function getValue() : string 55 | { 56 | return $this->convertEncoding($this->value); 57 | } 58 | 59 | /** 60 | * Returns the part's raw value. 61 | */ 62 | public function getRawValue() : string 63 | { 64 | return $this->rawValue; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Parser/Proxy/ParserUUEncodedPartProxyFactory.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 37 | $this->streamFactory = $sdf; 38 | $this->parserPartStreamContainerFactory = $parserPartStreamContainerFactory; 39 | } 40 | 41 | /** 42 | * Constructs a new ParserUUEncodedPartProxy wrapping an IUUEncoded object. 43 | */ 44 | public function newInstance(PartBuilder $partBuilder, IParserService $parser) : ParserUUEncodedPartProxy 45 | { 46 | $parserProxy = new ParserUUEncodedPartProxy($partBuilder, $parser); 47 | $streamContainer = $this->parserPartStreamContainerFactory->newInstance($parserProxy); 48 | 49 | $part = new UUEncodedPart( 50 | $parserProxy->getUnixFileMode(), 51 | $parserProxy->getFileName(), 52 | $partBuilder->getParent()->getPart(), 53 | $this->logger, 54 | $streamContainer 55 | ); 56 | $parserProxy->setPart($part); 57 | 58 | $streamContainer->setStream($this->streamFactory->newMessagePartStream($part)); 59 | $part->attach($streamContainer); 60 | return $parserProxy; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Header/IdHeader.php: -------------------------------------------------------------------------------- 1 | get(LoggerInterface::class), 37 | $mimeTokenPartFactory ?? $di->get(MimeTokenPartFactory::class), 38 | $consumerService ?? $di->get(IdBaseConsumerService::class), 39 | $name, 40 | $value 41 | ); 42 | } 43 | 44 | /** 45 | * Returns the ID. Synonymous to calling getValue(). 46 | * 47 | * @return string|null The ID 48 | */ 49 | public function getId() : ?string 50 | { 51 | return $this->getValue(); 52 | } 53 | 54 | /** 55 | * Returns all IDs parsed for a multi-id header like References or 56 | * In-Reply-To. 57 | * 58 | * @return string[] An array of IDs 59 | */ 60 | public function getIds() : array 61 | { 62 | return \array_values(\array_map( 63 | function($p) { 64 | return $p->getValue(); 65 | }, 66 | \array_filter($this->parts, function($p) { 67 | return !($p instanceof CommentPart); 68 | }) 69 | )); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Header/Consumer/AbstractGenericConsumerService.php: -------------------------------------------------------------------------------- 1 | partFactory->newContainerPart($parts)]; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Header/Consumer/SubjectConsumerService.php: -------------------------------------------------------------------------------- 1 | partFactory->newMimeToken($token); 44 | } 45 | return $this->partFactory->newSubjectToken($token); 46 | } 47 | 48 | /** 49 | * Returns an array of \ZBateson\MailMimeParser\Header\Part\HeaderPart for 50 | * the current token on the iterator. 51 | * 52 | * Overridden from AbstractConsumerService to remove special filtering for 53 | * backslash escaping, which also seems to not apply to Subject headers at 54 | * least in ThunderBird's implementation. 55 | * 56 | * @return IHeaderPart[] 57 | */ 58 | protected function getTokenParts(Iterator $tokens) : array 59 | { 60 | return $this->getConsumerTokenParts($tokens); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Header/Part/CommentPart.php: -------------------------------------------------------------------------------- 1 | partFactory = $partFactory; 38 | parent::__construct($logger, $charsetConverter, $children); 39 | $this->comment = $this->value; 40 | $this->value = ''; 41 | $this->isSpace = true; 42 | $this->canIgnoreSpacesBefore = true; 43 | $this->canIgnoreSpacesAfter = true; 44 | } 45 | 46 | protected function getValueFromParts(array $parts) : string 47 | { 48 | $partFactory = $this->partFactory; 49 | return parent::getValueFromParts(\array_map( 50 | function($p) use ($partFactory) { 51 | if ($p instanceof CommentPart) { 52 | return $partFactory->newQuotedLiteralPart([$partFactory->newToken('(' . $p->getComment() . ')')]); 53 | } elseif ($p instanceof QuotedLiteralPart) { 54 | return $partFactory->newQuotedLiteralPart([$partFactory->newToken('"' . \str_replace('(["\\])', '\$1', $p->getValue()) . '"')]); 55 | } 56 | return $p; 57 | }, 58 | $parts 59 | )); 60 | } 61 | 62 | /** 63 | * Returns the comment's text. 64 | */ 65 | public function getComment() : string 66 | { 67 | return $this->comment; 68 | } 69 | 70 | /** 71 | * Returns an empty string. 72 | */ 73 | public function getValue() : string 74 | { 75 | return ''; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Parser/HeaderParserService.php: -------------------------------------------------------------------------------- 1 | add($a[0], \trim($a[1])); 34 | } else { 35 | $headerContainer->addError( 36 | "Invalid header found at offset: $offset", 37 | LogLevel::ERROR 38 | ); 39 | } 40 | } 41 | return $this; 42 | } 43 | 44 | /** 45 | * Reads header lines up to an empty line, adding them to the passed 46 | * PartHeaderContainer. 47 | * 48 | * @param resource $handle The resource handle to read from. 49 | * @param PartHeaderContainer $container the container to add headers to. 50 | */ 51 | public function parse($handle, PartHeaderContainer $container) : static 52 | { 53 | $header = ''; 54 | do { 55 | $offset = \ftell($handle); 56 | $line = MessageParserService::readLine($handle); 57 | if ($line === false || $line === '' || $line[0] !== "\t" && $line[0] !== ' ') { 58 | $this->addRawHeaderToPart($offset, $header, $container); 59 | $header = ''; 60 | } else { 61 | $line = "\r\n" . $line; 62 | } 63 | $header .= \rtrim($line, "\r\n"); 64 | } while ($header !== ''); 65 | return $this; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Header/Consumer/QuotedStringConsumerService.php: -------------------------------------------------------------------------------- 1 | partFactory->newToken($token, $isLiteral, true); 63 | } 64 | 65 | /** 66 | * Overridden to combine all part values into a single string and return it 67 | * as an array with a single element. 68 | * 69 | * The returned IHeaderParts is an array containing a single 70 | * QuotedLiteralPart. 71 | * 72 | * @param IHeaderPart[] $parts 73 | * @return IHeaderPart[] 74 | */ 75 | protected function processParts(array $parts) : array 76 | { 77 | return [$this->partFactory->newQuotedLiteralPart($parts)]; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Header/MimeEncodedHeader.php: -------------------------------------------------------------------------------- 1 | mimeTokenPartFactory = $mimeTokenPartFactory; 42 | parent::__construct($logger, $consumerService, $name, $value); 43 | } 44 | 45 | /** 46 | * Mime-decodes any mime-encoded parts prior to invoking 47 | * parent::parseHeaderValue. 48 | */ 49 | protected function parseHeaderValue(IConsumerService $consumer, string $value) : void 50 | { 51 | // handled differently from MimeLiteralPart's decoding which ignores 52 | // whitespace between parts, etc... 53 | $matchp = '~(' . MimeToken::MIME_PART_PATTERN . ')~'; 54 | $aMimeParts = \preg_split($matchp, $value, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); 55 | $this->mimeEncodedParsedParts = \array_map([$this->mimeTokenPartFactory, 'newInstance'], $aMimeParts); 56 | parent::parseHeaderValue( 57 | $consumer, 58 | \implode('', \array_map(fn ($part) => $part->getValue(), $this->mimeEncodedParsedParts)) 59 | ); 60 | } 61 | 62 | protected function getErrorBagChildren() : array 63 | { 64 | return \array_values(\array_filter(\array_merge($this->getAllParts(), $this->mimeEncodedParsedParts))); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Header/Part/DatePart.php: -------------------------------------------------------------------------------- 1 | value = $dateToken = \trim($this->value); 44 | 45 | // Missing "+" in timezone definition. eg: Thu, 13 Mar 2014 15:02:47 0000 (not RFC compliant) 46 | // Won't result in an Exception, but in a valid DateTime in year `0000` - therefore we need to check this first: 47 | if (\preg_match('# [0-9]{4}$#', $dateToken)) { 48 | $dateToken = \preg_replace('# ([0-9]{4})$#', ' +$1', $dateToken); 49 | // @see https://bugs.php.net/bug.php?id=42486 50 | } elseif (\preg_match('#UT$#', $dateToken)) { 51 | $dateToken = $dateToken . 'C'; 52 | } 53 | 54 | try { 55 | $this->date = new DateTime($dateToken); 56 | } catch (Exception $e) { 57 | $this->addError( 58 | "Unable to parse date from header: \"{$dateToken}\"", 59 | LogLevel::ERROR, 60 | $e 61 | ); 62 | } 63 | } 64 | 65 | /** 66 | * Returns a DateTime object or null if it can't be parsed. 67 | */ 68 | public function getDateTime() : ?DateTime 69 | { 70 | return $this->date; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Header/Consumer/AddressEmailConsumerService.php: -------------------------------------------------------------------------------- 1 | . 16 | * 17 | * The address portion found within the '<' and '>' chars may contain comments 18 | * and quoted portions. 19 | * 20 | * @author Zaahid Bateson 21 | */ 22 | class AddressEmailConsumerService extends AbstractConsumerService 23 | { 24 | public function __construct( 25 | LoggerInterface $logger, 26 | HeaderPartFactory $partFactory, 27 | CommentConsumerService $commentConsumerService, 28 | QuotedStringConsumerService $quotedStringConsumerService 29 | ) { 30 | parent::__construct( 31 | $logger, 32 | $partFactory, 33 | [$commentConsumerService, $quotedStringConsumerService] 34 | ); 35 | } 36 | 37 | /** 38 | * Overridden to return patterns matching the beginning/end part of an 39 | * address in a name/address part ("<" and ">" chars). 40 | * 41 | * @return string[] the patterns 42 | */ 43 | public function getTokenSeparators() : array 44 | { 45 | return ['<', '>']; 46 | } 47 | 48 | /** 49 | * Returns true for the '>' char. 50 | */ 51 | protected function isEndToken(string $token) : bool 52 | { 53 | return ($token === '>'); 54 | } 55 | 56 | /** 57 | * Returns true for the '<' char. 58 | */ 59 | protected function isStartToken(string $token) : bool 60 | { 61 | return ($token === '<'); 62 | } 63 | 64 | /** 65 | * Returns a single {@see ZBateson\MailMimeParser\Header\Part\AddressPart} 66 | * with its 'email' portion set, so an {@see AddressConsumerService} can 67 | * identify it and create an 68 | * {@see ZBateson\MailMimeParser\Header\Part\AddressPart} Address with 69 | * both a name and email set. 70 | * 71 | * @param \ZBateson\MailMimeParser\Header\IHeaderPart[] $parts 72 | * @return \ZBateson\MailMimeParser\Header\IHeaderPart[]|array 73 | */ 74 | protected function processParts(array $parts) : array 75 | { 76 | return [$this->partFactory->newAddress([], $parts)]; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Header/Part/AddressGroupPart.php: -------------------------------------------------------------------------------- 1 | addresses = \array_merge(...\array_map( 46 | fn ($p) => ($p instanceof AddressGroupPart) ? $p->getAddresses() : [$p], 47 | $addressesAndGroupParts 48 | )); 49 | // for backwards compatibility 50 | $this->value = $this->name; 51 | } 52 | 53 | /** 54 | * Return the AddressGroupPart's array of addresses. 55 | * 56 | * @return AddressPart[] An array of address parts. 57 | */ 58 | public function getAddresses() : array 59 | { 60 | return $this->addresses; 61 | } 62 | 63 | /** 64 | * Returns the AddressPart at the passed index or null. 65 | * 66 | * @param int $index The 0-based index. 67 | * @return ?AddressPart The address. 68 | */ 69 | public function getAddress(int $index) : ?AddressPart 70 | { 71 | if (!isset($this->addresses[$index])) { 72 | return null; 73 | } 74 | return $this->addresses[$index]; 75 | } 76 | 77 | protected function validate() : void 78 | { 79 | if ($this->name === null || \mb_strlen($this->name) === 0) { 80 | $this->addError('Address group doesn\'t have a name', LogLevel::ERROR); 81 | } 82 | if (empty($this->addresses)) { 83 | $this->addError('Address group doesn\'t have any email addresses defined in it', LogLevel::NOTICE); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Parser/AbstractParserService.php: -------------------------------------------------------------------------------- 1 | parserMessageProxyFactory 16 | * which can be set via the default constructor) 17 | * - IParser::getParserPartProxyFactory (returns $this->parserPartProxyFactory 18 | * which can be set via the default constructor) 19 | * 20 | * @author Zaahid Bateson 21 | */ 22 | abstract class AbstractParserService implements IParserService 23 | { 24 | /** 25 | * @var ParserPartProxyFactory the parser's message proxy factory service 26 | * responsible for creating an IMessage part wrapped in a 27 | * ParserPartProxy. 28 | */ 29 | protected ParserPartProxyFactory $parserMessageProxyFactory; 30 | 31 | /** 32 | * @var ParserPartProxyFactory the parser's part proxy factory service 33 | * responsible for creating IMessagePart parts wrapped in a 34 | * ParserPartProxy. 35 | */ 36 | protected ParserPartProxyFactory $parserPartProxyFactory; 37 | 38 | /** 39 | * @var PartBuilderFactory Service for creating PartBuilder objects for new 40 | * children. 41 | */ 42 | protected PartBuilderFactory $partBuilderFactory; 43 | 44 | /** 45 | * @var ParserManagerService the ParserManager, which should call setParserManager 46 | * when the parser is added. 47 | */ 48 | protected ParserManagerService $parserManager; 49 | 50 | public function __construct( 51 | ParserPartProxyFactory $parserMessageProxyFactory, 52 | ParserPartProxyFactory $parserPartProxyFactory, 53 | PartBuilderFactory $partBuilderFactory 54 | ) { 55 | $this->parserMessageProxyFactory = $parserMessageProxyFactory; 56 | $this->parserPartProxyFactory = $parserPartProxyFactory; 57 | $this->partBuilderFactory = $partBuilderFactory; 58 | } 59 | 60 | public function setParserManager(ParserManagerService $pm) : static 61 | { 62 | $this->parserManager = $pm; 63 | return $this; 64 | } 65 | 66 | public function getParserMessageProxyFactory() : ParserPartProxyFactory 67 | { 68 | return $this->parserMessageProxyFactory; 69 | } 70 | 71 | public function getParserPartProxyFactory() : ParserPartProxyFactory 72 | { 73 | return $this->parserPartProxyFactory; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Header/HeaderConsts.php: -------------------------------------------------------------------------------- 1 | : ' 80 | * 81 | * @return string The string representation. 82 | */ 83 | public function __toString() : string; 84 | } 85 | -------------------------------------------------------------------------------- /src/Header/Consumer/IdBaseConsumerService.php: -------------------------------------------------------------------------------- 1 | ' 18 | * characters. Processing for validly-formatted IDs are passed on to its 19 | * sub-consumer, IdConsumer. 20 | * 21 | * @author Zaahid Bateson 22 | */ 23 | class IdBaseConsumerService extends AbstractConsumerService 24 | { 25 | public function __construct( 26 | LoggerInterface $logger, 27 | HeaderPartFactory $partFactory, 28 | CommentConsumerService $commentConsumerService, 29 | QuotedStringConsumerService $quotedStringConsumerService, 30 | IdConsumerService $idConsumerService 31 | ) { 32 | parent::__construct( 33 | $logger, 34 | $partFactory, 35 | [ 36 | $commentConsumerService, 37 | $quotedStringConsumerService, 38 | $idConsumerService 39 | ] 40 | ); 41 | } 42 | 43 | /** 44 | * Returns '\s+' as a whitespace separator. 45 | * 46 | * @return string[] an array of regex pattern matchers. 47 | */ 48 | protected function getTokenSeparators() : array 49 | { 50 | return ['\s+']; 51 | } 52 | 53 | /** 54 | * IdBaseConsumerService doesn't have start/end tokens, and so always 55 | * returns false. 56 | */ 57 | protected function isEndToken(string $token) : bool 58 | { 59 | return false; 60 | } 61 | 62 | /** 63 | * IdBaseConsumerService doesn't have start/end tokens, and so always 64 | * returns false. 65 | * 66 | * @codeCoverageIgnore 67 | */ 68 | protected function isStartToken(string $token) : bool 69 | { 70 | return false; 71 | } 72 | 73 | /** 74 | * Returns null for whitespace, and 75 | * {@see ZBateson\MailMimeParser\Header\Part\Token} for anything else. 76 | * 77 | * @param string $token the token 78 | * @param bool $isLiteral set to true if the token represents a literal - 79 | * e.g. an escaped token 80 | * @return ?IHeaderPart The constructed header part or null if the token 81 | * should be ignored 82 | */ 83 | protected function getPartForToken(string $token, bool $isLiteral) : ?IHeaderPart 84 | { 85 | if (\preg_match('/^\s+$/', $token)) { 86 | return null; 87 | } 88 | return $this->partFactory->newToken($token, true); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Parser/Proxy/ParserMessageProxyFactory.php: -------------------------------------------------------------------------------- 1 | multipartHelper = $multipartHelper; 45 | $this->privacyHelper = $privacyHelper; 46 | } 47 | 48 | /** 49 | * Constructs a new ParserMessageProxy wrapping an IMessage object that will 50 | * dynamically parse a message's content and parts as they're requested. 51 | */ 52 | public function newInstance(PartBuilder $partBuilder, IParserService $parser) : ParserMessageProxy 53 | { 54 | $parserProxy = new ParserMessageProxy($partBuilder, $parser); 55 | 56 | $streamContainer = $this->parserPartStreamContainerFactory->newInstance($parserProxy); 57 | $headerContainer = $this->partHeaderContainerFactory->newInstance($parserProxy->getHeaderContainer()); 58 | $childrenContainer = $this->parserPartChildrenContainerFactory->newInstance($parserProxy); 59 | 60 | $message = new Message( 61 | $this->logger, 62 | $streamContainer, 63 | $headerContainer, 64 | $childrenContainer, 65 | $this->multipartHelper, 66 | $this->privacyHelper 67 | ); 68 | $parserProxy->setPart($message); 69 | 70 | $streamContainer->setStream($this->streamFactory->newMessagePartStream($message)); 71 | $message->attach($streamContainer); 72 | return $parserProxy; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Parser/Proxy/ParserMimePartProxyFactory.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 45 | $this->streamFactory = $sdf; 46 | $this->partHeaderContainerFactory = $phcf; 47 | $this->parserPartStreamContainerFactory = $pscf; 48 | $this->parserPartChildrenContainerFactory = $ppccf; 49 | } 50 | 51 | /** 52 | * Constructs a new ParserMimePartProxy wrapping an IMimePart object that 53 | * will dynamically parse a message's content and parts as they're 54 | * requested. 55 | */ 56 | public function newInstance(PartBuilder $partBuilder, IParserService $parser) : ParserMimePartProxy 57 | { 58 | $parserProxy = new ParserMimePartProxy($partBuilder, $parser); 59 | 60 | $streamContainer = $this->parserPartStreamContainerFactory->newInstance($parserProxy); 61 | $headerContainer = $this->partHeaderContainerFactory->newInstance($parserProxy->getHeaderContainer()); 62 | $childrenContainer = $this->parserPartChildrenContainerFactory->newInstance($parserProxy); 63 | 64 | $part = new MimePart( 65 | $partBuilder->getParent()->getPart(), 66 | $this->logger, 67 | $streamContainer, 68 | $headerContainer, 69 | $childrenContainer 70 | ); 71 | $parserProxy->setPart($part); 72 | 73 | $streamContainer->setStream($this->streamFactory->newMessagePartStream($part)); 74 | $part->attach($streamContainer); 75 | return $parserProxy; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Header/Consumer/Received/DomainConsumerService.php: -------------------------------------------------------------------------------- 1 | partFactory->newReceivedDomainPart($this->partName, $parts)]; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Header/Consumer/AddressBaseConsumerService.php: -------------------------------------------------------------------------------- 1 | getConsumerTokenParts($tokens); 92 | } 93 | 94 | /** 95 | * Never reached by AddressBaseConsumerService. Overridden to satisfy 96 | * AbstractConsumerService. 97 | * 98 | * @codeCoverageIgnore 99 | */ 100 | protected function getPartForToken(string $token, bool $isLiteral) : ?IHeaderPart 101 | { 102 | return null; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Parser/MessageParserService.php: -------------------------------------------------------------------------------- 1 | partBuilderFactory = $pbf; 52 | $this->partHeaderContainerFactory = $phcf; 53 | $this->parserManager = $pm; 54 | $this->headerParser = $hp; 55 | } 56 | 57 | /** 58 | * Convenience method to read a line of up to 4096 characters from the 59 | * passed resource handle. 60 | * 61 | * If the line is larger than 4096 characters, the remaining characters in 62 | * the line are read and discarded, and only the first 4096 characters are 63 | * returned. 64 | * 65 | * @param resource $handle 66 | * @return string|false the read line or false on EOF or on error. 67 | */ 68 | public static function readLine($handle) : string|false 69 | { 70 | $size = 4096; 71 | $ret = $line = \fgets($handle, $size); 72 | while (\strlen($line) === $size - 1 && \substr($line, -1) !== "\n") { 73 | $line = \fgets($handle, $size); 74 | } 75 | return $ret; 76 | } 77 | 78 | /** 79 | * Parses the passed stream into an {@see ZBateson\MailMimeParser\IMessage} 80 | * object and returns it. 81 | * 82 | * @param StreamInterface $stream the stream to parse the message from 83 | */ 84 | public function parse(StreamInterface $stream) : IMessage 85 | { 86 | $headerContainer = $this->partHeaderContainerFactory->newInstance(); 87 | $partBuilder = $this->partBuilderFactory->newPartBuilder($headerContainer, $stream); 88 | $this->headerParser->parse( 89 | $partBuilder->getMessageResourceHandle(), 90 | $headerContainer 91 | ); 92 | $proxy = $this->parserManager->createParserProxyFor($partBuilder); 93 | return $proxy->getPart(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Parser/Proxy/ParserNonMimeMessageProxy.php: -------------------------------------------------------------------------------- 1 | nextPartStart; 46 | } 47 | 48 | /** 49 | * Returns the next part's unix file mode in a uu-encoded 'begin' line if 50 | * one exists, or null otherwise. 51 | * 52 | * @return int|null The file mode or null 53 | */ 54 | public function getNextPartMode() : ?int 55 | { 56 | return $this->nextPartMode; 57 | } 58 | 59 | /** 60 | * Returns the next part's filename in a uu-encoded 'begin' line if one 61 | * exists, or null otherwise. 62 | * 63 | * @return string|null The file name or null 64 | */ 65 | public function getNextPartFilename() : ?string 66 | { 67 | return $this->nextPartFilename; 68 | } 69 | 70 | /** 71 | * Sets the next part's start position within the message's raw stream. 72 | */ 73 | public function setNextPartStart(int $nextPartStart) : static 74 | { 75 | $this->nextPartStart = $nextPartStart; 76 | return $this; 77 | } 78 | 79 | /** 80 | * Sets the next part's unix file mode from its 'begin' line. 81 | */ 82 | public function setNextPartMode(int $nextPartMode) : static 83 | { 84 | $this->nextPartMode = $nextPartMode; 85 | return $this; 86 | } 87 | 88 | /** 89 | * Sets the next part's filename from its 'begin' line. 90 | * 91 | */ 92 | public function setNextPartFilename(string $nextPartFilename) : static 93 | { 94 | $this->nextPartFilename = $nextPartFilename; 95 | return $this; 96 | } 97 | 98 | /** 99 | * Sets the next part start position, file mode, and filename to null 100 | */ 101 | public function clearNextPart() : static 102 | { 103 | $this->nextPartStart = null; 104 | $this->nextPartMode = null; 105 | $this->nextPartFilename = null; 106 | return $this; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Message/UUEncodedPart.php: -------------------------------------------------------------------------------- 1 | get(LoggerInterface::class), 40 | $streamContainer ?? $di->get(PartStreamContainer::class), 41 | $parent 42 | ); 43 | $this->mode = $mode; 44 | $this->filename = $filename; 45 | } 46 | 47 | /** 48 | * Returns the filename included in the uuencoded 'begin' line for this 49 | * part. 50 | */ 51 | public function getFilename() : ?string 52 | { 53 | return $this->filename; 54 | } 55 | 56 | public function setFilename(string $filename) : static 57 | { 58 | $this->filename = $filename; 59 | $this->notify(); 60 | return $this; 61 | } 62 | 63 | /** 64 | * Returns false. 65 | * 66 | * Although the part may be plain text, there is no reliable way of 67 | * determining its type since uuencoded 'begin' lines only include a file 68 | * name and no mime type. The file name's extension may be a hint. 69 | * 70 | * @return false 71 | */ 72 | public function isTextPart() : bool 73 | { 74 | return false; 75 | } 76 | 77 | /** 78 | * Returns 'application/octet-stream'. 79 | */ 80 | public function getContentType(string $default = 'application/octet-stream') : ?string 81 | { 82 | return 'application/octet-stream'; 83 | } 84 | 85 | /** 86 | * Returns null 87 | */ 88 | public function getCharset() : ?string 89 | { 90 | return null; 91 | } 92 | 93 | /** 94 | * Returns 'attachment'. 95 | */ 96 | public function getContentDisposition(?string $default = 'attachment') : ?string 97 | { 98 | return 'attachment'; 99 | } 100 | 101 | /** 102 | * Returns 'x-uuencode'. 103 | */ 104 | public function getContentTransferEncoding(?string $default = 'x-uuencode') : ?string 105 | { 106 | return 'x-uuencode'; 107 | } 108 | 109 | public function getUnixFileMode() : ?int 110 | { 111 | return $this->mode; 112 | } 113 | 114 | public function setUnixFileMode(int $mode) : static 115 | { 116 | $this->mode = $mode; 117 | $this->notify(); 118 | return $this; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Stream/HeaderStream.php: -------------------------------------------------------------------------------- 1 | attach($this); 41 | 42 | // unsetting the property forces the first access to go through 43 | // __get(). 44 | unset($this->stream); 45 | } 46 | 47 | public function __destruct() 48 | { 49 | $this->part->detach($this); 50 | } 51 | 52 | public function update(SplSubject $subject) : void 53 | { 54 | if ($this->stream !== null) { 55 | $this->stream = $this->createStream(); 56 | } 57 | } 58 | 59 | /** 60 | * Returns a header array for the current part. 61 | * 62 | * If the part is not a MimePart, Content-Type, Content-Disposition and 63 | * Content-Transfer-Encoding headers are generated manually. 64 | */ 65 | private function getPartHeadersIterator() : Traversable 66 | { 67 | if ($this->part instanceof IMimePart) { 68 | return $this->part->getRawHeaderIterator(); 69 | } elseif ($this->part->getParent() !== null && $this->part->getParent()->isMime()) { 70 | return new ArrayIterator([ 71 | [HeaderConsts::CONTENT_TYPE, $this->part->getContentType()], 72 | [HeaderConsts::CONTENT_DISPOSITION, $this->part->getContentDisposition()], 73 | [HeaderConsts::CONTENT_TRANSFER_ENCODING, $this->part->getContentTransferEncoding()] 74 | ]); 75 | } 76 | return new ArrayIterator(); 77 | } 78 | 79 | /** 80 | * Writes out headers for $this->part and follows them with an empty line. 81 | */ 82 | public function writePartHeadersTo(StreamInterface $stream) : static 83 | { 84 | foreach ($this->getPartHeadersIterator() as $header) { 85 | $stream->write("{$header[0]}: {$header[1]}\r\n"); 86 | } 87 | $stream->write("\r\n"); 88 | return $this; 89 | } 90 | 91 | /** 92 | * Creates the underlying stream lazily when required. 93 | */ 94 | protected function createStream() : StreamInterface 95 | { 96 | $stream = Psr7\Utils::streamFor(); 97 | $this->writePartHeadersTo($stream); 98 | $stream->rewind(); 99 | return $stream; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/IErrorBag.php: -------------------------------------------------------------------------------- 1 | partFactory->newContainerPart($parts)]; 91 | } 92 | return [$this->partFactory->newParameterPart( 93 | $nameOnly, 94 | $valuePart 95 | )]; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Parser/ParserManagerService.php: -------------------------------------------------------------------------------- 1 | setParsers([$mimeParser, $nonMimeParser]); 32 | } 33 | 34 | /** 35 | * Overrides the internal prioritized list of parses with the passed list, 36 | * calling $parser->setParserManager($this) on each one. 37 | * 38 | * @param IParserService[] $parsers 39 | */ 40 | public function setParsers(array $parsers) : static 41 | { 42 | foreach ($parsers as $parser) { 43 | $parser->setParserManager($this); 44 | } 45 | $this->parsers = $parsers; 46 | return $this; 47 | } 48 | 49 | /** 50 | * Adds an IParser at the highest priority (up front), calling 51 | * $parser->setParserManager($this) on it. 52 | * 53 | * @param IParserService $parser The parser to add. 54 | */ 55 | public function prependParser(IParserService $parser) : static 56 | { 57 | $parser->setParserManager($this); 58 | \array_unshift($this->parsers, $parser); 59 | return $this; 60 | } 61 | 62 | /** 63 | * Creates a ParserPartProxy for the passed $partBuilder using a compatible 64 | * IParser. 65 | * 66 | * Loops through registered IParsers calling 'canParse()' on each with the 67 | * passed PartBuilder, then calling either 'getParserMessageProxyFactory()' 68 | * or 'getParserPartProxyFactory()' depending on if the PartBuilder has a 69 | * parent, and finally calling 'newInstance' on the returned 70 | * ParserPartProxyFactory passing it the IParser, and returning the new 71 | * ParserPartProxy instance that was created. 72 | * 73 | * @param PartBuilder $partBuilder The PartBuilder to wrap in a proxy with 74 | * an IParser 75 | * @throws CompatibleParserNotFoundException if a compatible parser for the 76 | * type is not configured. 77 | * @return ParserPartProxy The created ParserPartProxy tied to a new 78 | * IMessagePart and associated IParser. 79 | */ 80 | public function createParserProxyFor(PartBuilder $partBuilder) : ParserPartProxy 81 | { 82 | foreach ($this->parsers as $parser) { 83 | if ($parser->canParse($partBuilder)) { 84 | $factory = ($partBuilder->getParent() === null) ? 85 | $parser->getParserMessageProxyFactory() : 86 | $parser->getParserPartProxyFactory(); 87 | return $factory->newInstance($partBuilder, $parser); 88 | } 89 | } 90 | throw new CompatibleParserNotFoundException('Compatible parser for a part cannot be found with content-type: ' . $partBuilder->getHeaderContainer()->get('Content-Type')); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Header/Consumer/AddressGroupConsumerService.php: -------------------------------------------------------------------------------- 1 | subConsumers = [$subConsumer]; 47 | } 48 | 49 | /** 50 | * Overridden to return patterns matching the beginning and end markers of a 51 | * group address: colon and semi-colon (":" and ";") characters. 52 | * 53 | * @return string[] the patterns 54 | */ 55 | public function getTokenSeparators() : array 56 | { 57 | return [':', ';']; 58 | } 59 | 60 | /** 61 | * Returns true if the passed token is a semi-colon. 62 | */ 63 | protected function isEndToken(string $token) : bool 64 | { 65 | return ($token === ';'); 66 | } 67 | 68 | /** 69 | * Returns true if the passed token is a colon. 70 | */ 71 | protected function isStartToken(string $token) : bool 72 | { 73 | return ($token === ':'); 74 | } 75 | 76 | /** 77 | * Overridden to always call processParts even for an empty set of 78 | * addresses, since a group could be empty. 79 | * 80 | * @param Iterator $tokens 81 | * @return IHeaderPart[] 82 | */ 83 | protected function parseTokensIntoParts(Iterator $tokens) : array 84 | { 85 | $ret = parent::parseTokensIntoParts($tokens); 86 | if ($ret === []) { 87 | return $this->processParts([]); 88 | } 89 | return $ret; 90 | } 91 | 92 | /** 93 | * Performs post-processing on parsed parts. 94 | * 95 | * Returns an array with a single 96 | * {@see AddressGroupPart} element with all email addresses from this and 97 | * any sub-groups. 98 | * 99 | * @param \ZBateson\MailMimeParser\Header\IHeaderPart[] $parts 100 | * @return AddressGroupPart[]|array 101 | */ 102 | protected function processParts(array $parts) : array 103 | { 104 | return [$this->partFactory->newAddressGroupPart([], $parts)]; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Error.php: -------------------------------------------------------------------------------- 1 | 44 | */ 45 | private array $levelMap = [ 46 | LogLevel::EMERGENCY => 0, 47 | LogLevel::ALERT => 1, 48 | LogLevel::CRITICAL => 2, 49 | LogLevel::ERROR => 3, 50 | LogLevel::WARNING => 4, 51 | LogLevel::NOTICE => 5, 52 | LogLevel::INFO => 6, 53 | LogLevel::DEBUG => 7, 54 | ]; 55 | 56 | /** 57 | * 58 | * @throws InvalidArgumentException if the passed $psrLogLevelAsErrorLevel 59 | * is not a known PSR log level (see \Psr\Log\LogLevel) 60 | */ 61 | public function __construct(string $message, string $psrLogLevelAsErrorLevel, ErrorBag $object, ?Throwable $exception = null) 62 | { 63 | if (!isset($this->levelMap[$psrLogLevelAsErrorLevel])) { 64 | throw new InvalidArgumentException($psrLogLevelAsErrorLevel . ' is not a known PSR Log Level'); 65 | } 66 | $this->message = $message; 67 | $this->psrLevel = $psrLogLevelAsErrorLevel; 68 | $this->object = $object; 69 | $this->exception = $exception; 70 | } 71 | 72 | /** 73 | * Returns the error message. 74 | */ 75 | public function getMessage() : string 76 | { 77 | return $this->message; 78 | } 79 | 80 | /** 81 | * Returns the PSR string log level for this error message. 82 | */ 83 | public function getPsrLevel() : string 84 | { 85 | return $this->psrLevel; 86 | } 87 | 88 | /** 89 | * Returns the class type the error occurred on. 90 | */ 91 | public function getClass() : string 92 | { 93 | return \get_class($this->object); 94 | } 95 | 96 | /** 97 | * Returns the object the error occurred on. 98 | */ 99 | public function getObject() : ErrorBag 100 | { 101 | return $this->object; 102 | } 103 | 104 | /** 105 | * Returns the exception that occurred, if any, or null. 106 | */ 107 | public function getException() : ?Throwable 108 | { 109 | return $this->exception; 110 | } 111 | 112 | /** 113 | * Returns true if the PSR log level for this error is equal to or greater 114 | * than the one passed, e.g. passing LogLevel::ERROR would return true for 115 | * LogLevel::ERROR and LogLevel::CRITICAL, ALERT and EMERGENCY. 116 | */ 117 | public function isPsrLevelGreaterOrEqualTo(string $minLevel) : bool 118 | { 119 | $minIntLevel = $this->levelMap[$minLevel] ?? 1000; 120 | $thisLevel = $this->levelMap[$this->psrLevel]; 121 | return ($minIntLevel >= $thisLevel); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Header/ParameterHeader.php: -------------------------------------------------------------------------------- 1 | getValue() ``` would return 32 | * 'zb@example.com', as would calling ```php $header->getValueFor('addr'); ```. 33 | * 34 | * @author Zaahid Bateson 35 | */ 36 | class ParameterHeader extends AbstractHeader 37 | { 38 | /** 39 | * @var ParameterPart[] key map of lower-case parameter names and associated 40 | * ParameterParts. 41 | */ 42 | protected array $parameters = []; 43 | 44 | public function __construct( 45 | string $name, 46 | string $value, 47 | ?LoggerInterface $logger = null, 48 | ?ParameterConsumerService $consumerService = null 49 | ) { 50 | $di = MailMimeParser::getGlobalContainer(); 51 | parent::__construct( 52 | $logger ?? $di->get(LoggerInterface::class), 53 | $consumerService ?? $di->get(ParameterConsumerService::class), 54 | $name, 55 | $value 56 | ); 57 | } 58 | 59 | /** 60 | * Overridden to assign ParameterParts to a map of lower-case parameter 61 | * names to ParameterParts. 62 | */ 63 | protected function parseHeaderValue(IConsumerService $consumer, string $value) : void 64 | { 65 | parent::parseHeaderValue($consumer, $value); 66 | foreach ($this->parts as $part) { 67 | if ($part instanceof NameValuePart) { 68 | $this->parameters[\strtolower($part->getName())] = $part; 69 | } 70 | } 71 | } 72 | 73 | /** 74 | * Returns true if a parameter exists with the passed name. 75 | * 76 | * @param string $name The parameter to look up. 77 | */ 78 | public function hasParameter(string $name) : bool 79 | { 80 | return isset($this->parameters[\strtolower($name)]); 81 | } 82 | 83 | /** 84 | * Returns the value of the parameter with the given name, or $defaultValue 85 | * if not set. 86 | * 87 | * @param string $name The parameter to retrieve. 88 | * @param string $defaultValue Optional default value (defaulting to null if 89 | * not provided). 90 | * @return string|null The parameter's value. 91 | */ 92 | public function getValueFor(string $name, ?string $defaultValue = null) : ?string 93 | { 94 | if (!$this->hasParameter($name)) { 95 | return $defaultValue; 96 | } 97 | return $this->parameters[\strtolower($name)]->getValue(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/di_config.php: -------------------------------------------------------------------------------- 1 | new AutowireDefinitionHelper(NullLogger::class), 22 | 23 | // only affects reading part content, not for instance decoding mime encoded 24 | // header parts 25 | 'throwExceptionReadingPartContentFromUnsupportedCharsets' => false, 26 | 27 | 'fromDomainConsumerService' => (new AutowireDefinitionHelper(DomainConsumerService::class)) 28 | ->constructorParameter('partName', 'from'), 29 | 'byDomainConsumerService' => (new AutowireDefinitionHelper(DomainConsumerService::class)) 30 | ->constructorParameter('partName', 'by'), 31 | 'viaGenericReceivedConsumerService' => (new AutowireDefinitionHelper(GenericReceivedConsumerService::class)) 32 | ->constructorParameter('partName', 'via'), 33 | 'withGenericReceivedConsumerService' => (new AutowireDefinitionHelper(GenericReceivedConsumerService::class)) 34 | ->constructorParameter('partName', 'with'), 35 | 'idGenericReceivedConsumerService' => (new AutowireDefinitionHelper(GenericReceivedConsumerService::class)) 36 | ->constructorParameter('partName', 'id'), 37 | 'forGenericReceivedConsumerService' => (new AutowireDefinitionHelper(GenericReceivedConsumerService::class)) 38 | ->constructorParameter('partName', 'for'), 39 | ReceivedConsumerService::class => (new AutowireDefinitionHelper()) 40 | ->constructor( 41 | fromDomainConsumerService: new Reference('fromDomainConsumerService'), 42 | byDomainConsumerService: new Reference('byDomainConsumerService'), 43 | viaGenericReceivedConsumerService: new Reference('viaGenericReceivedConsumerService'), 44 | withGenericReceivedConsumerService: new Reference('withGenericReceivedConsumerService'), 45 | idGenericReceivedConsumerService: new Reference('idGenericReceivedConsumerService'), 46 | forGenericReceivedConsumerService: new Reference('forGenericReceivedConsumerService') 47 | ), 48 | PartStreamContainer::class => (new AutowireDefinitionHelper()) 49 | ->constructor( 50 | throwExceptionReadingPartContentFromUnsupportedCharsets: new Reference('throwExceptionReadingPartContentFromUnsupportedCharsets') 51 | ), 52 | PartStreamContainerFactory::class => (new AutowireDefinitionHelper()) 53 | ->constructor( 54 | throwExceptionReadingPartContentFromUnsupportedCharsets: new Reference('throwExceptionReadingPartContentFromUnsupportedCharsets') 55 | ), 56 | ParserPartStreamContainerFactory::class => (new AutowireDefinitionHelper()) 57 | ->constructor( 58 | throwExceptionReadingPartContentFromUnsupportedCharsets: new Reference('throwExceptionReadingPartContentFromUnsupportedCharsets') 59 | ), 60 | StreamFactory::class => (new AutowireDefinitionHelper()) 61 | ->constructor( 62 | throwExceptionReadingPartContentFromUnsupportedCharsets: new Reference('throwExceptionReadingPartContentFromUnsupportedCharsets') 63 | ), 64 | ]; 65 | -------------------------------------------------------------------------------- /src/Header/Part/SplitParameterPart.php: -------------------------------------------------------------------------------- 1 | partFactory = $headerPartFactory; 41 | NameValuePart::__construct($logger, $charsetConverter, [$children[0]], $children); 42 | $this->children = $children; 43 | } 44 | 45 | protected function getNameFromParts(array $parts) : string 46 | { 47 | return $parts[0]->getName(); 48 | } 49 | 50 | private function getMimeTokens(string $value) : array 51 | { 52 | $pattern = MimeToken::MIME_PART_PATTERN; 53 | // remove whitespace between two adjacent mime encoded parts 54 | $normed = \preg_replace("/($pattern)\\s+(?=$pattern)/", '$1', $value); 55 | // with PREG_SPLIT_DELIM_CAPTURE, matched and unmatched parts are returned 56 | $aMimeParts = \preg_split("/($pattern)/", $normed, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); 57 | return \array_map( 58 | fn ($p) => (\preg_match("/$pattern/", $p)) ? $this->partFactory->newMimeToken($p) : $this->partFactory->newToken($p, true, true), 59 | $aMimeParts 60 | ); 61 | } 62 | 63 | private function combineAdjacentUnencodedParts(array $parts) : array 64 | { 65 | $runningValue = ''; 66 | $returnedParts = []; 67 | foreach ($parts as $part) { 68 | if (!$part->encoded) { 69 | $runningValue .= $part->value; 70 | continue; 71 | } 72 | if (!empty($runningValue)) { 73 | $returnedParts = \array_merge($returnedParts, $this->getMimeTokens($runningValue)); 74 | $runningValue = ''; 75 | } 76 | $returnedParts[] = $part; 77 | } 78 | if (!empty($runningValue)) { 79 | $returnedParts = \array_merge($returnedParts, $this->getMimeTokens($runningValue)); 80 | } 81 | return $returnedParts; 82 | } 83 | 84 | protected function getValueFromParts(array $parts) : string 85 | { 86 | $sorted = $parts; 87 | \usort($sorted, fn ($a, $b) => $a->index <=> $b->index); 88 | 89 | $first = $sorted[0]; 90 | $this->language = $first->language; 91 | $charset = $this->charset = $first->charset; 92 | 93 | $combined = $this->combineAdjacentUnencodedParts($sorted); 94 | 95 | return \implode('', \array_map( 96 | fn ($p) => ($p instanceof ParameterPart && $p->encoded) 97 | ? $this->decodePartValue($p->getValue(), ($p->charset === null) ? $charset : $p->charset) 98 | : $p->getValue(), 99 | $combined 100 | )); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Parser/IParserService.php: -------------------------------------------------------------------------------- 1 | setStreamContentStartPos() and 60 | * $proxy->setStreamContentAndPartEndPos() so an IMessagePart can return 61 | * content from the raw message. 62 | * 63 | * Reading should stop once the end of the current part's content has been 64 | * reached or the end of the message has been reached. If the end of the 65 | * message has been reached $proxy->setEof() should be called in addition to 66 | * setStreamContentAndPartEndPos(). 67 | */ 68 | public function parseContent(ParserPartProxy $proxy) : static; 69 | 70 | /** 71 | * Performs read operations to read children from the passed $proxy, using 72 | * its stream, and reading up to (and not including) the beginning of the 73 | * child's content if another child exists. 74 | * 75 | * The implementation should: 76 | * 1. Return null if there are no more children. 77 | * 2. Read headers 78 | * 3. Create a PartBuilder (adding the passed $proxy as its parent) 79 | * 4. Call ParserManager::createParserProxyFor() on the ParserManager 80 | * previously set by a call to setParserManager(), which may determine 81 | * that a different parser is responsible for parts represented by 82 | * the headers and PartBuilder passed to it. 83 | * 84 | * The method should then return the ParserPartProxy returned by the 85 | * ParserManager, or null if there are no more children to read. 86 | * 87 | * @return ParserPartProxy|null The child ParserPartProxy or null if there 88 | * are no more children under $proxy. 89 | */ 90 | public function parseNextChild(ParserMimePartProxy $proxy) : ?ParserPartProxy; 91 | } 92 | -------------------------------------------------------------------------------- /src/Header/Part/MimeToken.php: -------------------------------------------------------------------------------- 1 | value = $this->decodeMime(\preg_replace('/\r|\n/', '', $this->value)); 47 | $pattern = self::MIME_PART_PATTERN; 48 | $this->canIgnoreSpacesBefore = (bool) \preg_match("/^\s*{$pattern}|\s+/", $this->rawValue); 49 | $this->canIgnoreSpacesAfter = (bool) \preg_match("/{$pattern}\s*|\s+\$/", $this->rawValue); 50 | } 51 | 52 | /** 53 | * Finds and replaces mime parts with their values. 54 | * 55 | * The method splits the token value into an array on mime-part-patterns, 56 | * either replacing a mime part with its value by calling iconv_mime_decode 57 | * or converts the encoding on the text part by calling convertEncoding. 58 | */ 59 | protected function decodeMime(string $value) : string 60 | { 61 | if (\preg_match('/^=\?([A-Za-z\-_0-9]+)\*?([A-Za-z\-_0-9]+)?\?([QBqb])\?([^\?]*)\?=$/', $value, $matches)) { 62 | return $this->decodeMatchedEntity($matches); 63 | } 64 | return $this->convertEncoding($value); 65 | } 66 | 67 | /** 68 | * Decodes a matched mime entity part into a string and returns it, after 69 | * adding the string into the languages array. 70 | * 71 | * @param string[] $matches 72 | */ 73 | private function decodeMatchedEntity(array $matches) : string 74 | { 75 | $body = $matches[4]; 76 | if (\strtoupper($matches[3]) === 'Q') { 77 | $body = \quoted_printable_decode(\str_replace('_', '=20', $body)); 78 | } else { 79 | $body = \base64_decode($body); 80 | } 81 | $this->charset = $matches[1]; 82 | $this->language = (!empty($matches[2])) ? $matches[2] : null; 83 | if ($this->charset !== null) { 84 | return $this->convertEncoding($body, $this->charset, true); 85 | } 86 | return $this->convertEncoding($body, 'ISO-8859-1', true); 87 | } 88 | 89 | /** 90 | * Returns the language code for the mime part. 91 | */ 92 | public function getLanguage() : ?string 93 | { 94 | return $this->language; 95 | } 96 | 97 | /** 98 | * Returns the charset for the encoded part. 99 | */ 100 | public function getCharset() : ?string 101 | { 102 | return $this->charset; 103 | } 104 | 105 | public function getRawValue() : string 106 | { 107 | return $this->rawValue; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Header/Part/ReceivedDomainPart.php: -------------------------------------------------------------------------------- 1 | ehloName = ($this->value !== '') ? $this->value : null; 62 | $cps = $this->getComments(); 63 | $commentPart = (!empty($cps)) ? $cps[0] : null; 64 | 65 | $pattern = '~^(\[(IPv[64])?(?P[a-f\d\.\:]+)\])?\s*(helo=)?(?P[a-z0-9\-]+[a-z0-9\-\.]+)?\s*(\[(IPv[64])?(?P[a-f\d\.\:]+)\])?$~i'; 66 | if ($commentPart !== null && \preg_match($pattern, $commentPart->getComment(), $matches)) { 67 | $this->value .= ' (' . $commentPart->getComment() . ')'; 68 | $this->hostname = (!empty($matches['name'])) ? $matches['name'] : null; 69 | $this->address = (!empty($matches['addr1'])) ? $matches['addr1'] : ((!empty($matches['addr2'])) ? $matches['addr2'] : null); 70 | } 71 | } 72 | 73 | /** 74 | * Returns the name used to identify the server in the first part of the 75 | * extended-domain line. 76 | * 77 | * Note that this is not necessarily the name used in the EHLO line to an 78 | * SMTP server, since implementations differ so much, not much can be 79 | * guaranteed except the position it was parsed in. 80 | */ 81 | public function getEhloName() : ?string 82 | { 83 | return $this->ehloName; 84 | } 85 | 86 | /** 87 | * Returns the hostname of the server, or whatever string in the hostname 88 | * position when parsing (but never an address). 89 | */ 90 | public function getHostname() : ?string 91 | { 92 | return $this->hostname; 93 | } 94 | 95 | /** 96 | * Returns the address of the server, or whatever string that looks like an 97 | * address in the address position when parsing (but never a hostname). 98 | */ 99 | public function getAddress() : ?string 100 | { 101 | return $this->address; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Header/Consumer/ParameterConsumerService.php: -------------------------------------------------------------------------------- 1 | partFactory; 73 | return \array_values(\array_map( 74 | function($partsArray) use ($factory) { 75 | if (\count($partsArray) > 1) { 76 | return $factory->newSplitParameterPart($partsArray); 77 | } 78 | return $partsArray[0]; 79 | }, 80 | \array_merge_recursive(...\array_map( 81 | function($p) { 82 | // if $p->getIndex is non-null, it's a split-parameter part 83 | // and an array of one element consisting of name => ParameterPart 84 | // is returned, which is then merged into name => array-of-parameter-parts 85 | // or ';' object_id . ';' for non-split parts with a value of a single 86 | // element array of [ParameterPart] 87 | if ($p instanceof ParameterPart && $p->getIndex() !== null) { 88 | return [\strtolower($p->getName()) => [$p]]; 89 | } 90 | return [';' . \spl_object_id($p) . ';' => [$p]]; 91 | }, 92 | $parts 93 | )) 94 | )); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Header/Part/HeaderPart.php: -------------------------------------------------------------------------------- 1 | charsetConverter = $charsetConverter; 56 | $this->value = $value; 57 | } 58 | 59 | /** 60 | * Returns the part's representative value after any necessary processing 61 | * has been performed. For the raw value, call getRawValue(). 62 | */ 63 | public function getValue() : string 64 | { 65 | return $this->value; 66 | } 67 | 68 | /** 69 | * Returns the value of the part (which is a string). 70 | * 71 | * @return string the value 72 | */ 73 | public function __toString() : string 74 | { 75 | return $this->value; 76 | } 77 | 78 | /** 79 | * Ensures the encoding of the passed string is set to UTF-8. 80 | * 81 | * The method does nothing if the passed $from charset is UTF-8 already, or 82 | * if $force is set to false and mb_check_encoding for $str returns true 83 | * for 'UTF-8'. 84 | * 85 | * @return string utf-8 string 86 | */ 87 | protected function convertEncoding(string $str, string $from = 'ISO-8859-1', bool $force = false) : string 88 | { 89 | if ($from !== 'UTF-8') { 90 | // mime header part decoding will force it. This is necessary for 91 | // UTF-7 because mb_check_encoding will return true 92 | if ($force || !($this->charsetConverter->checkEncoding($str, 'UTF-8'))) { 93 | try { 94 | return $this->charsetConverter->convert($str, $from, 'UTF-8'); 95 | } catch (UnsupportedCharsetException $ce) { 96 | $this->addError('Unable to convert charset', LogLevel::ERROR, $ce); 97 | return $this->charsetConverter->convert($str, 'ISO-8859-1', 'UTF-8'); 98 | } 99 | } 100 | } 101 | return $str; 102 | } 103 | 104 | public function getComments() : array 105 | { 106 | return []; 107 | } 108 | 109 | /** 110 | * Default implementation returns an empty array. 111 | */ 112 | protected function getErrorBagChildren() : array 113 | { 114 | return []; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Header/Consumer/CommentConsumerService.php: -------------------------------------------------------------------------------- 1 | partFactory->newInstance($token); 86 | } 87 | 88 | /** 89 | * Calls $tokens->next() and returns. 90 | * 91 | * The default implementation checks if the current token is an end token, 92 | * and will not advance past it. Because a comment part of a header can be 93 | * nested, its implementation must advance past its own 'end' token. 94 | */ 95 | protected function advanceToNextToken(Iterator $tokens, bool $isStartToken) : static 96 | { 97 | $tokens->next(); 98 | return $this; 99 | } 100 | 101 | /** 102 | * Post processing involves creating a single Part\CommentPart out of 103 | * generated parts from tokens. The Part\CommentPart is returned in an 104 | * array. 105 | * 106 | * @param IHeaderPart[] $parts 107 | * @return IHeaderPart[] 108 | */ 109 | protected function processParts(array $parts) : array 110 | { 111 | return [$this->partFactory->newCommentPart($parts)]; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/ErrorBag.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 36 | } 37 | 38 | /** 39 | * Returns the class name. Override to identify objects in logs. 40 | * 41 | */ 42 | public function getErrorLoggingContextName() : string 43 | { 44 | return static::class; 45 | } 46 | 47 | /** 48 | * Return any children ErrorBag objects. 49 | * 50 | * @return IErrorBag[] 51 | */ 52 | abstract protected function getErrorBagChildren() : array; 53 | 54 | /** 55 | * Perform any extra validation and call 'addError'. 56 | * 57 | * getErrors and getAllErrors call validate() if their $validate parameter 58 | * is true. validate() is only called once on an object with getErrors 59 | * getAllErrors. 60 | */ 61 | protected function validate() : void 62 | { 63 | // do nothing 64 | } 65 | 66 | public function addError(string $message, string $psrLogLevel, ?Throwable $exception = null) : static 67 | { 68 | $error = new Error($message, $psrLogLevel, $this, $exception); 69 | $this->errors[] = $error; 70 | $this->logger->log( 71 | $psrLogLevel, 72 | '{contextName} {message} {exception}', 73 | [ 74 | 'contextName' => $this->getErrorLoggingContextName(), 75 | 'message' => $message, 76 | 'exception' => $exception ?? '' 77 | ] 78 | ); 79 | return $this; 80 | } 81 | 82 | public function getErrors(bool $validate = false, string $minPsrLevel = LogLevel::ERROR) : array 83 | { 84 | if ($validate && !$this->validated) { 85 | $this->validated = true; 86 | $this->validate(); 87 | } 88 | return \array_values(\array_filter( 89 | $this->errors, 90 | function($e) use ($minPsrLevel) { 91 | return $e->isPsrLevelGreaterOrEqualTo($minPsrLevel); 92 | } 93 | )); 94 | } 95 | 96 | public function hasErrors(bool $validate = false, string $minPsrLevel = LogLevel::ERROR) : bool 97 | { 98 | return (\count($this->getErrors($validate, $minPsrLevel)) > 0); 99 | } 100 | 101 | public function getAllErrors(bool $validate = false, string $minPsrLevel = LogLevel::ERROR) : array 102 | { 103 | $arr = \array_values(\array_map( 104 | function($e) use ($validate, $minPsrLevel) { 105 | return $e->getAllErrors($validate, $minPsrLevel); 106 | }, 107 | $this->getErrorBagChildren() 108 | )); 109 | return \array_merge($this->getErrors($validate, $minPsrLevel), ...$arr); 110 | } 111 | 112 | public function hasAnyErrors(bool $validate = false, string $minPsrLevel = LogLevel::ERROR) : bool 113 | { 114 | if ($this->hasErrors($validate, $minPsrLevel)) { 115 | return true; 116 | } 117 | foreach ($this->getErrorBagChildren() as $ch) { 118 | if ($ch->hasAnyErrors($validate, $minPsrLevel)) { 119 | return true; 120 | } 121 | } 122 | return false; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Header/Consumer/Received/GenericReceivedConsumerService.php: -------------------------------------------------------------------------------- 1 | partName = $partName; 62 | } 63 | 64 | /** 65 | * Returns true if the passed token matches (case-insensitively) 66 | * $this->getPartName() with optional whitespace surrounding it. 67 | */ 68 | protected function isStartToken(string $token) : bool 69 | { 70 | $pattern = '/^' . \preg_quote($this->partName, '/') . '$/i'; 71 | return (\preg_match($pattern, $token) === 1); 72 | } 73 | 74 | /** 75 | * Returns true if the token matches (case-insensitively) any of the 76 | * following, with optional surrounding whitespace: 77 | * 78 | * o by 79 | * o via 80 | * o with 81 | * o id 82 | * o for 83 | * o ; 84 | */ 85 | protected function isEndToken(string $token) : bool 86 | { 87 | return (\preg_match('/^(by|via|with|id|for|;)$/i', $token) === 1); 88 | } 89 | 90 | /** 91 | * Returns a whitespace separator (for filtering ignorable whitespace 92 | * between parts), and a separator matching the current part name as 93 | * set on $this->partName. 94 | * 95 | * @return string[] an array of regex pattern matchers 96 | */ 97 | protected function getTokenSeparators() : array 98 | { 99 | return [ 100 | '\s+', 101 | '(\A\s*|\s+)(?i)' . \preg_quote($this->partName, '/') . '(?-i)(?=\s+)' 102 | ]; 103 | } 104 | 105 | /** 106 | * @param \ZBateson\MailMimeParser\Header\IHeaderPart[] $parts 107 | * @return \ZBateson\MailMimeParser\Header\IHeaderPart[] 108 | */ 109 | protected function processParts(array $parts) : array 110 | { 111 | return [$this->partFactory->newReceivedPart($this->partName, $parts)]; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Header/Part/ParameterPart.php: -------------------------------------------------------------------------------- 1 | children); 52 | } 53 | 54 | protected function getNameFromParts(array $parts) : string 55 | { 56 | $name = parent::getNameFromParts($parts); 57 | if (\preg_match('~^\s*([^\*]+)\*(\d*)(\*)?$~', $name, $matches)) { 58 | $name = $matches[1]; 59 | $this->index = ($matches[2] !== '') ? (int) ($matches[2]) : null; 60 | $this->encoded = (($matches[2] === '') || !empty($matches[3])); 61 | } 62 | return $name; 63 | } 64 | 65 | protected function decodePartValue(string $value, ?string $charset = null) : string 66 | { 67 | if ($charset !== null) { 68 | return $this->convertEncoding(\rawurldecode($value), $charset, true); 69 | } 70 | return $this->convertEncoding(\rawurldecode($value)); 71 | } 72 | 73 | protected function getValueFromParts(array $parts) : string 74 | { 75 | $value = parent::getValueFromParts($parts); 76 | if ($this->encoded && \preg_match('~^([^\']*)\'?([^\']*)\'?(.*)$~', $value, $matches)) { 77 | $this->charset = (!empty($matches[1]) && !empty($matches[3])) ? $matches[1] : $this->charset; 78 | $this->language = (!empty($matches[2])) ? $matches[2] : $this->language; 79 | $ev = (empty($matches[3])) ? $matches[1] : $matches[3]; 80 | // only if it's not part of a SplitParameterPart 81 | if ($this->index === null) { 82 | // subsequent parts are decoded as a SplitParameterPart since only 83 | // the first part are supposed to have charset/language fields 84 | return $this->decodePartValue($ev, $this->charset); 85 | } 86 | return $ev; 87 | } 88 | return $value; 89 | } 90 | 91 | /** 92 | * Returns the charset if the part is an RFC-2231 part with a charset set. 93 | */ 94 | public function getCharset() : ?string 95 | { 96 | return $this->charset; 97 | } 98 | 99 | /** 100 | * Returns the RFC-1766 (or subset) language tag, if the parameter is an 101 | * RFC-2231 part with a language tag set. 102 | * 103 | * @return ?string the language if set, or null if not 104 | */ 105 | public function getLanguage() : ?string 106 | { 107 | return $this->language; 108 | } 109 | 110 | public function isUrlEncoded() : bool 111 | { 112 | return $this->encoded; 113 | } 114 | 115 | public function getIndex() : ?int 116 | { 117 | return $this->index; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Header/Part/ContainerPart.php: -------------------------------------------------------------------------------- 1 | charsetConverter = $charsetConverter; 42 | $this->children = $children; 43 | $str = (!empty($children)) ? $this->getValueFromParts($children) : ''; 44 | parent::__construct( 45 | $logger, 46 | $this->charsetConverter, 47 | $str 48 | ); 49 | } 50 | 51 | /** 52 | * Filters out ignorable space tokens. 53 | * 54 | * Spaces are removed if parts on either side of it have their 55 | * canIgnoreSpaceAfter/canIgnoreSpaceBefore properties set to true. 56 | * 57 | * @param HeaderPart[] $parts 58 | * @return HeaderPart[] 59 | */ 60 | protected function filterIgnoredSpaces(array $parts) : array 61 | { 62 | $ends = (object) ['isSpace' => true, 'canIgnoreSpacesAfter' => true, 'canIgnoreSpacesBefore' => true, 'value' => '']; 63 | 64 | $spaced = \array_merge($parts, [$ends]); 65 | $filtered = \array_slice(\array_reduce( 66 | \array_slice(\array_keys($spaced), 0, -1), 67 | function($carry, $key) use ($spaced, $ends) { 68 | $p = $spaced[$key]; 69 | $l = \end($carry); 70 | $a = $spaced[$key + 1]; 71 | if ($p->isSpace && $a === $ends) { 72 | // trim 73 | if ($l->isSpace) { 74 | \array_pop($carry); 75 | } 76 | return $carry; 77 | } elseif ($p->isSpace && ($l->isSpace || ($l->canIgnoreSpacesAfter && $a->canIgnoreSpacesBefore))) { 78 | return $carry; 79 | } 80 | return \array_merge($carry, [$p]); 81 | }, 82 | [$ends] 83 | ), 1); 84 | return $filtered; 85 | } 86 | 87 | /** 88 | * Creates the string value representation of this part constructed from the 89 | * child parts passed to it. 90 | * 91 | * The default implementation filters out ignorable whitespace between 92 | * parts, and concatenates parts calling 'getValue'. 93 | * 94 | * @param HeaderParts[] $parts 95 | */ 96 | protected function getValueFromParts(array $parts) : string 97 | { 98 | return \array_reduce($this->filterIgnoredSpaces($parts), fn ($c, $p) => $c . $p->getValue(), ''); 99 | } 100 | 101 | /** 102 | * Returns the child parts this container part consists of. 103 | * 104 | * @return IHeaderPart[] 105 | */ 106 | public function getChildParts() : array 107 | { 108 | return $this->children; 109 | } 110 | 111 | public function getComments() : array 112 | { 113 | return \array_merge(...\array_filter(\array_map( 114 | fn ($p) => ($p instanceof CommentPart) ? [$p] : $p->getComments(), 115 | $this->children 116 | ))); 117 | } 118 | 119 | /** 120 | * Returns this part's children, same as getChildParts(). 121 | * 122 | * @return ErrorBag 123 | */ 124 | protected function getErrorBagChildren() : array 125 | { 126 | return $this->children; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Header/AddressHeader.php: -------------------------------------------------------------------------------- 1 | get(LoggerInterface::class), 50 | $consumerService ?? $di->get(AddressBaseConsumerService::class), 51 | $name, 52 | $value 53 | ); 54 | } 55 | 56 | /** 57 | * Filters $this->allParts into the parts required by $this->parts 58 | * and assignes it. 59 | * 60 | * The AbstractHeader::filterAndAssignToParts method filters out CommentParts. 61 | */ 62 | protected function filterAndAssignToParts() : void 63 | { 64 | parent::filterAndAssignToParts(); 65 | foreach ($this->parts as $part) { 66 | if ($part instanceof AddressPart) { 67 | $this->addresses[] = $part; 68 | } elseif ($part instanceof AddressGroupPart) { 69 | $this->addresses = \array_merge($this->addresses, $part->getAddresses()); 70 | $this->groups[] = $part; 71 | } 72 | } 73 | } 74 | 75 | /** 76 | * Returns all address parts in the header including any addresses that are 77 | * in groups (lists). 78 | * 79 | * @return AddressPart[] The addresses. 80 | */ 81 | public function getAddresses() : array 82 | { 83 | return $this->addresses; 84 | } 85 | 86 | /** 87 | * Returns all group parts (lists) in the header. 88 | * 89 | * @return AddressGroupPart[] 90 | */ 91 | public function getGroups() : array 92 | { 93 | return $this->groups; 94 | } 95 | 96 | /** 97 | * Returns true if an address exists with the passed email address. 98 | * 99 | * Comparison is done case insensitively. 100 | * 101 | */ 102 | public function hasAddress(string $email) : bool 103 | { 104 | foreach ($this->addresses as $addr) { 105 | if (\strcasecmp($addr->getEmail(), $email) === 0) { 106 | return true; 107 | } 108 | } 109 | return false; 110 | } 111 | 112 | /** 113 | * Returns the first email address in the header. 114 | * 115 | * @return ?string The email address 116 | */ 117 | public function getEmail() : ?string 118 | { 119 | if (!empty($this->addresses)) { 120 | return $this->addresses[0]->getEmail(); 121 | } 122 | return null; 123 | } 124 | 125 | /** 126 | * Returns the name associated with the first email address to complement 127 | * getEmail() if one is set, or null if not. 128 | * 129 | * @return string|null The person name. 130 | */ 131 | public function getPersonName() : ?string 132 | { 133 | if (!empty($this->addresses)) { 134 | return $this->addresses[0]->getName(); 135 | } 136 | return null; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Parser/Proxy/ParserUUEncodedPartProxy.php: -------------------------------------------------------------------------------- 1 | getParent()->getNextPartStart(); 40 | } 41 | 42 | /** 43 | * Returns the next part's unix file mode in a uu-encoded 'begin' line if 44 | * one exists, or null otherwise. 45 | * 46 | * As this is a message-wide setting, ParserUUEncodedPartProxy calls 47 | * getNextPartMode() on its parent (a ParserNonMimeMessageProxy, which 48 | * stores/returns this information). 49 | * 50 | * @return int|null The file mode or null 51 | */ 52 | public function getNextPartMode() : ?int 53 | { 54 | return $this->getParent()->getNextPartMode(); 55 | } 56 | 57 | /** 58 | * Returns the next part's filename in a uu-encoded 'begin' line if one 59 | * exists, or null otherwise. 60 | * 61 | * As this is a message-wide setting, ParserUUEncodedPartProxy calls 62 | * getNextPartFilename() on its parent (a ParserNonMimeMessageProxy, which 63 | * stores/returns this information). 64 | * 65 | * @return ?string The file name or null 66 | */ 67 | public function getNextPartFilename() : ?string 68 | { 69 | return $this->getParent()->getNextPartFilename(); 70 | } 71 | 72 | /** 73 | * Sets the next part's start position within the message's raw stream. 74 | * 75 | * As this is a message-wide setting, ParserUUEncodedPartProxy calls 76 | * setNextPartStart() on its parent (a ParserNonMimeMessageProxy, which 77 | * stores/returns this information). 78 | */ 79 | public function setNextPartStart(int $nextPartStart) : static 80 | { 81 | $this->getParent()->setNextPartStart($nextPartStart); 82 | return $this; 83 | } 84 | 85 | /** 86 | * Sets the next part's unix file mode from its 'begin' line. 87 | * 88 | * As this is a message-wide setting, ParserUUEncodedPartProxy calls 89 | * setNextPartMode() on its parent (a ParserNonMimeMessageProxy, which 90 | * stores/returns this information). 91 | */ 92 | public function setNextPartMode(int $nextPartMode) : static 93 | { 94 | $this->getParent()->setNextPartMode($nextPartMode); 95 | return $this; 96 | } 97 | 98 | /** 99 | * Sets the next part's filename from its 'begin' line. 100 | * 101 | * As this is a message-wide setting, ParserUUEncodedPartProxy calls 102 | * setNextPartFilename() on its parent (a ParserNonMimeMessageProxy, which 103 | * stores/returns this information). 104 | */ 105 | public function setNextPartFilename(string $nextPartFilename) : static 106 | { 107 | $this->getParent()->setNextPartFilename($nextPartFilename); 108 | return $this; 109 | } 110 | 111 | /** 112 | * Returns the file mode included in the uuencoded 'begin' line for this 113 | * part. 114 | */ 115 | public function getUnixFileMode() : ?int 116 | { 117 | return $this->getHeaderContainer()->getUnixFileMode(); 118 | } 119 | 120 | /** 121 | * Returns the filename included in the uuencoded 'begin' line for this 122 | * part. 123 | */ 124 | public function getFilename() : ?string 125 | { 126 | return $this->getHeaderContainer()->getFilename(); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Parser/NonMimeParserService.php: -------------------------------------------------------------------------------- 1 | partHeaderContainerFactory = $uuEncodedPartHeaderContainerFactory; 35 | } 36 | 37 | /** 38 | * Always returns true, and should therefore be the last parser reached by 39 | * a ParserManager. 40 | */ 41 | public function canParse(PartBuilder $part) : bool 42 | { 43 | return true; 44 | } 45 | 46 | /** 47 | * Creates a UUEncodedPartHeaderContainer attached to a PartBuilder, and 48 | * calls $this->parserManager->createParserProxyFor(). 49 | * 50 | * It also sets the PartBuilder's stream part start pos and content start 51 | * pos to that of $parent->getNextParStart() (since a 'begin' line is read 52 | * prior to another child being created, see parseNextPart()). 53 | */ 54 | private function createPart(ParserNonMimeMessageProxy $parent) : ParserPartProxy 55 | { 56 | $hc = $this->partHeaderContainerFactory->newInstance($parent->getNextPartMode(), $parent->getNextPartFilename()); 57 | $pb = $this->partBuilderFactory->newChildPartBuilder($hc, $parent); 58 | $proxy = $this->parserManager->createParserProxyFor($pb); 59 | $pb->setStreamPartStartPos($parent->getNextPartStart()); 60 | $pb->setStreamContentStartPos($parent->getNextPartStart()); 61 | return $proxy; 62 | } 63 | 64 | /** 65 | * Reads content from the passed ParserPartProxy's stream till a uu-encoded 66 | * 'begin' line is found, setting $proxy->setStreamPartContentAndEndPos() to 67 | * the last byte read before the begin line. 68 | * 69 | * @param ParserNonMimeMessageProxy|ParserUUEncodedPartProxy $proxy 70 | */ 71 | private function parseNextPart(ParserPartProxy $proxy) : static 72 | { 73 | $handle = $proxy->getMessageResourceHandle(); 74 | while (!\feof($handle)) { 75 | $start = \ftell($handle); 76 | $line = \trim(MessageParserService::readLine($handle)); 77 | if (\preg_match('/^begin ([0-7]{3}) (.*)$/', $line, $matches)) { 78 | $proxy->setNextPartStart($start); 79 | $proxy->setNextPartMode((int) $matches[1]); 80 | $proxy->setNextPartFilename($matches[2]); 81 | return $this; 82 | } 83 | $proxy->setStreamPartAndContentEndPos(\ftell($handle)); 84 | } 85 | return $this; 86 | } 87 | 88 | public function parseContent(ParserPartProxy $proxy) : static 89 | { 90 | $handle = $proxy->getMessageResourceHandle(); 91 | if ($proxy->getNextPartStart() !== null || \feof($handle)) { 92 | return $this; 93 | } 94 | if ($proxy->getStreamContentStartPos() === null) { 95 | $proxy->setStreamContentStartPos(\ftell($handle)); 96 | } 97 | $this->parseNextPart($proxy); 98 | return $this; 99 | } 100 | 101 | public function parseNextChild(ParserMimePartProxy $proxy) : ?ParserPartProxy 102 | { 103 | $handle = $proxy->getMessageResourceHandle(); 104 | if ($proxy->getNextPartStart() === null || \feof($handle)) { 105 | return null; 106 | } 107 | $child = $this->createPart($proxy); 108 | $proxy->clearNextPart(); 109 | return $child; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Message/PartFilter.php: -------------------------------------------------------------------------------- 1 | getContentType(); 31 | $disp = $part->getContentDisposition(); 32 | if (\in_array($type, ['text/plain', 'text/html']) && $disp !== null && \strcasecmp($disp, 'inline') === 0) { 33 | return false; 34 | } 35 | return !(($part instanceof IMimePart) 36 | && ($part->isMultiPart() || $part->isSignaturePart())); 37 | }; 38 | } 39 | 40 | /** 41 | * Provides a filter that keeps parts that contain a header of $name with a 42 | * value that matches $value (case insensitive). 43 | * 44 | * By default signed parts are excluded. Pass FALSE to the third parameter 45 | * to include them. 46 | * 47 | * @param string $name The header name to look up 48 | * @param string $value The value to match 49 | * @param bool $excludeSignedParts Optional signed parts exclusion (defaults 50 | * to true). 51 | */ 52 | public static function fromHeaderValue(string $name, string $value, bool $excludeSignedParts = true) : callable 53 | { 54 | return function(IMessagePart $part) use ($name, $value, $excludeSignedParts) { 55 | if ($part instanceof IMimePart) { 56 | if ($excludeSignedParts && $part->isSignaturePart()) { 57 | return false; 58 | } 59 | return (\strcasecmp($part->getHeaderValue($name, ''), $value) === 0); 60 | } 61 | return false; 62 | }; 63 | } 64 | 65 | /** 66 | * Includes only parts that match the passed $mimeType in the return value 67 | * of a call to 'getContentType()'. 68 | * 69 | * @param string $mimeType Mime type of parts to find. 70 | */ 71 | public static function fromContentType(string $mimeType) : callable 72 | { 73 | return function(IMessagePart $part) use ($mimeType) { 74 | return \strcasecmp($part->getContentType() ?: '', $mimeType) === 0; 75 | }; 76 | } 77 | 78 | /** 79 | * Returns parts matching $mimeType that do not have a Content-Disposition 80 | * set to 'attachment'. 81 | * 82 | * @param string $mimeType Mime type of parts to find. 83 | */ 84 | public static function fromInlineContentType(string $mimeType) : callable 85 | { 86 | return function(IMessagePart $part) use ($mimeType) { 87 | $disp = $part->getContentDisposition(); 88 | return (\strcasecmp($part->getContentType() ?: '', $mimeType) === 0) && ($disp === null 89 | || \strcasecmp($disp, 'attachment') !== 0); 90 | }; 91 | } 92 | 93 | /** 94 | * Finds parts with the passed disposition (matching against 95 | * IMessagePart::getContentDisposition()), optionally including 96 | * multipart parts and signed parts. 97 | * 98 | * @param string $disposition The disposition to find. 99 | * @param bool $includeMultipart Optionally include multipart parts by 100 | * passing true (defaults to false). 101 | * @param bool $includeSignedParts Optionally include signed parts (defaults 102 | * to false). 103 | */ 104 | public static function fromDisposition(string $disposition, bool $includeMultipart = false, bool $includeSignedParts = false) : callable 105 | { 106 | return function(IMessagePart $part) use ($disposition, $includeMultipart, $includeSignedParts) { 107 | if (($part instanceof IMimePart) && ((!$includeMultipart && $part->isMultiPart()) || (!$includeSignedParts && $part->isSignaturePart()))) { 108 | return false; 109 | } 110 | $disp = $part->getContentDisposition(); 111 | return ($disp !== null && \strcasecmp($disp, $disposition) === 0); 112 | }; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Header/Consumer/ReceivedConsumerService.php: -------------------------------------------------------------------------------- 1 | getAllTokenSeparators()); 98 | return '~(' . $sChars . ')~'; 99 | } 100 | 101 | /** 102 | * Overridden to /not/ advance when the end token matches a start token for 103 | * a sub-consumer. 104 | */ 105 | protected function advanceToNextToken(Iterator $tokens, bool $isStartToken) : static 106 | { 107 | if ($isStartToken) { 108 | $tokens->next(); 109 | } elseif ($tokens->valid() && !$this->isEndToken($tokens->current())) { 110 | foreach ($this->subConsumers as $consumer) { 111 | if ($consumer->isStartToken($tokens->current())) { 112 | return $this; 113 | } 114 | } 115 | $tokens->next(); 116 | } 117 | return $this; 118 | } 119 | 120 | /** 121 | * @param \ZBateson\MailMimeParser\Header\IHeaderPart[] $parts 122 | * @return \ZBateson\MailMimeParser\Header\IHeaderPart[] 123 | */ 124 | protected function processParts(array $parts) : array 125 | { 126 | // filtering out tokens (filters out the names, e.g. 'by' or 'with') 127 | return \array_values(\array_filter($parts, fn ($p) => !$p instanceof Token)); 128 | } 129 | } 130 | --------------------------------------------------------------------------------