Rewrite email parser using ImapEngine, harden processing loop

Replace webklex/php-imap with directorytree/imapengine in the ticket
email parser. ImapEngine is pure PHP over sockets.

Parser improvements:
- Wrap per-message processing in try/catch so one malformed email
  can't abort the run; failures are flagged and logged with UID
- Query unseen + unflagged so previously-failed (flagged) messages
  are no longer re-processed on every cron run
- Skip vacation/auto-responder emails (RFC 3834) to prevent mail
  loops with the ticket auto-reply
- Cap messages per run (50) and attachment size (15MB); inline
  images over 2MB are stored as attachments instead of base64-embedded
  in ticket details
- Atomic lock file creation
- preg_quote() the ticket prefix in subject matching
- Dedupe CC watchers and exclude the sender
- Map webklex 'tls' encryption setting to STARTTLS for compatibility

NDR/DSN parsing now walks MIME parts via the underlying
zbateson parser instead of relying on attachment extraction.
This commit is contained in:
johnnyq
2026-06-12 16:56:39 -04:00
parent 300a1aff9f
commit 2204bd52f4
701 changed files with 111718 additions and 940 deletions

View File

@@ -0,0 +1,75 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Parser;
use ZBateson\MailMimeParser\Parser\Proxy\ParserPartProxyFactory;
/**
* Provides basic implementations for:
* - IParser::setParserManager
* - IParser::getParserMessageProxyFactory (returns $this->parserMessageProxyFactory
* which can be set via the default constructor)
* - IParser::getParserPartProxyFactory (returns $this->parserPartProxyFactory
* which can be set via the default constructor)
*
* @author Zaahid Bateson
*/
abstract class AbstractParserService implements IParserService
{
/**
* @var ParserPartProxyFactory the parser's message proxy factory service
* responsible for creating an IMessage part wrapped in a
* ParserPartProxy.
*/
protected ParserPartProxyFactory $parserMessageProxyFactory;
/**
* @var ParserPartProxyFactory the parser's part proxy factory service
* responsible for creating IMessagePart parts wrapped in a
* ParserPartProxy.
*/
protected ParserPartProxyFactory $parserPartProxyFactory;
/**
* @var PartBuilderFactory Service for creating PartBuilder objects for new
* children.
*/
protected PartBuilderFactory $partBuilderFactory;
/**
* @var ParserManagerService the ParserManager, which should call setParserManager
* when the parser is added.
*/
protected ParserManagerService $parserManager;
public function __construct(
ParserPartProxyFactory $parserMessageProxyFactory,
ParserPartProxyFactory $parserPartProxyFactory,
PartBuilderFactory $partBuilderFactory
) {
$this->parserMessageProxyFactory = $parserMessageProxyFactory;
$this->parserPartProxyFactory = $parserPartProxyFactory;
$this->partBuilderFactory = $partBuilderFactory;
}
public function setParserManager(ParserManagerService $pm) : static
{
$this->parserManager = $pm;
return $this;
}
public function getParserMessageProxyFactory() : ParserPartProxyFactory
{
return $this->parserMessageProxyFactory;
}
public function getParserPartProxyFactory() : ParserPartProxyFactory
{
return $this->parserPartProxyFactory;
}
}

View File

@@ -0,0 +1,22 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Parser;
use RuntimeException;
/**
* Exception thrown if the ParserManagerService doesn't contain a parser that
* can handle a given type of part. The default configuration of MailMimeParser
* uses NonMimeParserService that is a 'catch-all', so this would indicate a
* configuration error.
*
* @author Zaahid Bateson
*/
class CompatibleParserNotFoundException extends RuntimeException
{
}

View File

@@ -0,0 +1,67 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Parser;
use Psr\Log\LogLevel;
use ZBateson\MailMimeParser\Message\PartHeaderContainer;
/**
* Reads headers from an input stream, adding them to a PartHeaderContainer.
*
* @author Zaahid Bateson
*/
class HeaderParserService
{
/**
* Ensures the header isn't empty and contains a colon separator character,
* then splits it and adds it to the passed PartHeaderContainer.
*
* @param int $offset read offset for error reporting
* @param string $header the header line
* @param PartHeaderContainer $headerContainer the container
*/
private function addRawHeaderToPart(int $offset, string $header, PartHeaderContainer $headerContainer) : static
{
if ($header !== '') {
if (\strpos($header, ':') !== false) {
$a = \explode(':', $header, 2);
$headerContainer->add($a[0], \trim($a[1]));
} else {
$headerContainer->addError(
"Invalid header found at offset: $offset",
LogLevel::ERROR
);
}
}
return $this;
}
/**
* Reads header lines up to an empty line, adding them to the passed
* PartHeaderContainer.
*
* @param resource $handle The resource handle to read from.
* @param PartHeaderContainer $container the container to add headers to.
*/
public function parse($handle, PartHeaderContainer $container) : static
{
$header = '';
do {
$offset = \ftell($handle);
$line = MessageParserService::readLine($handle);
if ($line === false || $line === '' || $line[0] !== "\t" && $line[0] !== ' ') {
$this->addRawHeaderToPart($offset, $header, $container);
$header = '';
} else {
$line = "\r\n" . $line;
}
$header .= \rtrim($line, "\r\n");
} while ($header !== '');
return $this;
}
}

View File

@@ -0,0 +1,91 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Parser;
use ZBateson\MailMimeParser\Parser\Proxy\ParserMimePartProxy;
use ZBateson\MailMimeParser\Parser\Proxy\ParserPartProxy;
use ZBateson\MailMimeParser\Parser\Proxy\ParserPartProxyFactory;
/**
* Interface defining a message part parser.
*
* @author Zaahid Bateson
*/
interface IParserService
{
/**
* Sets up the passed ParserManager as the ParserManager for this part,
* which should be used when a new part is created (after its headers are
* read and a PartBuilder is created from it.)
*
* @param ParserManagerService $pm The ParserManager to set.
*/
public function setParserManager(ParserManagerService $pm) : static;
/**
* Called by the ParserManager to determine if the passed PartBuilder is a
* part handled by this IParser.
*/
public function canParse(PartBuilder $part) : bool;
/**
* Returns the ParserPartProxyFactory responsible for creating IMessage
* parts for this parser.
*
* This is called by ParserManager after 'canParse' if it returns true so
* a ParserPartProxy can be created out of the PartBuilder.
*/
public function getParserMessageProxyFactory() : ParserPartProxyFactory;
/**
* Returns the ParserPartProxyFactory responsible for creating IMessagePart
* parts for this parser.
*
* This is called by ParserManager after 'canParse' if it returns true so
* a ParserPartProxy can be created out of the PartBuilder.
*/
public function getParserPartProxyFactory() : ParserPartProxyFactory;
/**
* Performs read operations for content from the stream of the passed
* ParserPartProxy, and setting content bounds for the part in the passed
* ParserPartProxy.
*
* The implementation should call $proxy->setStreamContentStartPos() and
* $proxy->setStreamContentAndPartEndPos() so an IMessagePart can return
* content from the raw message.
*
* Reading should stop once the end of the current part's content has been
* reached or the end of the message has been reached. If the end of the
* message has been reached $proxy->setEof() should be called in addition to
* setStreamContentAndPartEndPos().
*/
public function parseContent(ParserPartProxy $proxy) : static;
/**
* Performs read operations to read children from the passed $proxy, using
* its stream, and reading up to (and not including) the beginning of the
* child's content if another child exists.
*
* The implementation should:
* 1. Return null if there are no more children.
* 2. Read headers
* 3. Create a PartBuilder (adding the passed $proxy as its parent)
* 4. Call ParserManager::createParserProxyFor() on the ParserManager
* previously set by a call to setParserManager(), which may determine
* that a different parser is responsible for parts represented by
* the headers and PartBuilder passed to it.
*
* The method should then return the ParserPartProxy returned by the
* ParserManager, or null if there are no more children to read.
*
* @return ParserPartProxy|null The child ParserPartProxy or null if there
* are no more children under $proxy.
*/
public function parseNextChild(ParserMimePartProxy $proxy) : ?ParserPartProxy;
}

View File

@@ -0,0 +1,95 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Parser;
use Psr\Http\Message\StreamInterface;
use ZBateson\MailMimeParser\IMessage;
use ZBateson\MailMimeParser\Message\Factory\PartHeaderContainerFactory;
/**
* Parses a mail mime message into its component parts. To invoke, call
* {@see MailMimeParser::parse()}.
*
* @author Zaahid Bateson
*/
class MessageParserService
{
/**
* @var PartHeaderContainerFactory To create a container to read the
* message's headers into.
*/
protected PartHeaderContainerFactory $partHeaderContainerFactory;
/**
* @var ParserManagerService To figure out what parser is responsible for parsing a
* message.
*/
protected ParserManagerService $parserManager;
/**
* @var PartBuilderFactory To create a PartBuilder representing this
* message, and to pass it to ParserManager.
*/
protected PartBuilderFactory $partBuilderFactory;
/**
* @var HeaderParserService To parse the headers into a PartHeaderContainer.
*/
protected HeaderParserService $headerParser;
public function __construct(
PartBuilderFactory $pbf,
PartHeaderContainerFactory $phcf,
ParserManagerService $pm,
HeaderParserService $hp
) {
$this->partBuilderFactory = $pbf;
$this->partHeaderContainerFactory = $phcf;
$this->parserManager = $pm;
$this->headerParser = $hp;
}
/**
* Convenience method to read a line of up to 4096 characters from the
* passed resource handle.
*
* If the line is larger than 4096 characters, the remaining characters in
* the line are read and discarded, and only the first 4096 characters are
* returned.
*
* @param resource $handle
* @return string|false the read line or false on EOF or on error.
*/
public static function readLine($handle) : string|false
{
$size = 4096;
$ret = $line = \fgets($handle, $size);
while (\strlen($line) === $size - 1 && \substr($line, -1) !== "\n") {
$line = \fgets($handle, $size);
}
return $ret;
}
/**
* Parses the passed stream into an {@see ZBateson\MailMimeParser\IMessage}
* object and returns it.
*
* @param StreamInterface $stream the stream to parse the message from
*/
public function parse(StreamInterface $stream) : IMessage
{
$headerContainer = $this->partHeaderContainerFactory->newInstance();
$partBuilder = $this->partBuilderFactory->newPartBuilder($headerContainer, $stream);
$this->headerParser->parse(
$partBuilder->getMessageResourceHandle(),
$headerContainer
);
$proxy = $this->parserManager->createParserProxyFor($partBuilder);
return $proxy->getPart();
}
}

View File

@@ -0,0 +1,167 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Parser;
use ZBateson\MailMimeParser\Message\Factory\PartHeaderContainerFactory;
use ZBateson\MailMimeParser\Message\PartHeaderContainer;
use ZBateson\MailMimeParser\Parser\Proxy\ParserMessageProxyFactory;
use ZBateson\MailMimeParser\Parser\Proxy\ParserMimePartProxy;
use ZBateson\MailMimeParser\Parser\Proxy\ParserMimePartProxyFactory;
use ZBateson\MailMimeParser\Parser\Proxy\ParserPartProxy;
/**
* Parses content and children of MIME parts.
*
* @author Zaahid Bateson
*/
class MimeParserService extends AbstractParserService
{
/**
* @var PartHeaderContainerFactory Factory service for creating
* PartHeaderContainers for headers.
*/
protected PartHeaderContainerFactory $partHeaderContainerFactory;
/**
* @var HeaderParserService The HeaderParser service.
*/
protected HeaderParserService $headerParser;
public function __construct(
ParserMessageProxyFactory $parserMessageProxyFactory,
ParserMimePartProxyFactory $parserMimePartProxyFactory,
PartBuilderFactory $partBuilderFactory,
PartHeaderContainerFactory $partHeaderContainerFactory,
HeaderParserService $headerParser
) {
parent::__construct($parserMessageProxyFactory, $parserMimePartProxyFactory, $partBuilderFactory);
$this->partHeaderContainerFactory = $partHeaderContainerFactory;
$this->headerParser = $headerParser;
}
/**
* Returns true if the passed PartBuilder::isMime() method returns true.
*
*/
public function canParse(PartBuilder $part) : bool
{
return $part->isMime();
}
/**
* Reads up to 2048 bytes of input from the passed resource handle,
* discarding portions of a line that are longer than that, and returning
* the read portions of the line.
*
* The method also calls $proxy->setLastLineEndingLength which is used in
* findContentBoundary() to set the exact end byte of a part.
*
* @param resource $handle
*/
private function readBoundaryLine($handle, ParserMimePartProxy $proxy) : string
{
$size = 2048;
$isCut = false;
$line = \fgets($handle, $size);
while (\strlen($line) === $size - 1 && \substr($line, -1) !== "\n") {
$line = \fgets($handle, $size);
$isCut = true;
}
$ret = \rtrim($line, "\r\n");
$proxy->setLastLineEndingLength(\strlen($line) - \strlen($ret));
return ($isCut) ? '' : $ret;
}
/**
* Reads 2048-byte lines from the passed $handle, calling
* $partBuilder->setEndBoundaryFound with the passed line until it returns
* true or the stream is at EOF.
*
* setEndBoundaryFound returns true if the passed line matches a boundary
* for the $partBuilder itself or any of its parents.
*
* Lines longer than 2048 bytes are returned as single lines of 2048 bytes,
* the longer line is not returned separately but is simply discarded.
*
* Once a boundary is found, setStreamPartAndContentEndPos is called with
* the passed $handle's read pos before the boundary and its line separator
* were read.
*/
private function findContentBoundary(ParserMimePartProxy $proxy) : static
{
$handle = $proxy->getMessageResourceHandle();
// last separator before a boundary belongs to the boundary, and is not
// part of the current part, if a part is immediately followed by a
// boundary, this could result in a '-1' or '-2' content length
while (!\feof($handle)) {
$endPos = \ftell($handle) - $proxy->getLastLineEndingLength();
$line = $this->readBoundaryLine($handle, $proxy);
if (\substr($line, 0, 2) === '--' && $proxy->setEndBoundaryFound($line)) {
$proxy->setStreamPartAndContentEndPos($endPos);
return $this;
}
}
$proxy->setStreamPartAndContentEndPos(\ftell($handle));
$proxy->setEof();
return $this;
}
public function parseContent(ParserPartProxy $proxy) : static
{
$proxy->setStreamContentStartPos($proxy->getMessageResourceHandlePos());
$this->findContentBoundary($proxy);
return $this;
}
/**
* Calls the header parser to fill the passed $headerContainer, then calls
* $this->parserManager->createParserProxyFor($child);
*
* The method first checks though if the 'part' represents hidden content
* past a MIME end boundary, which some messages like to include, for
* instance:
*
* ```
* --outer-boundary--
* --boundary
* content
* --boundary--
* some hidden information
* --outer-boundary--
* ```
*
* In this case, $this->parserPartProxyFactory is called directly to create
* a part, $this->parseContent is called immediately to parse it and discard
* it, and null is returned.
*/
private function createPart(ParserMimePartProxy $parent, PartHeaderContainer $headerContainer, PartBuilder $child) : ?ParserPartProxy
{
if (!$parent->isEndBoundaryFound()) {
$this->headerParser->parse(
$child->getMessageResourceHandle(),
$headerContainer
);
$parserProxy = $this->parserManager->createParserProxyFor($child);
return $parserProxy;
}
// reads content past an end boundary if there is any
$parserProxy = $this->parserPartProxyFactory->newInstance($child, $this);
$this->parseContent($parserProxy);
return null;
}
public function parseNextChild(ParserMimePartProxy $proxy) : ?ParserPartProxy
{
if ($proxy->isParentBoundaryFound()) {
return null;
}
$headerContainer = $this->partHeaderContainerFactory->newInstance();
$child = $this->partBuilderFactory->newChildPartBuilder($headerContainer, $proxy);
return $this->createPart($proxy, $headerContainer, $child);
}
}

View File

@@ -0,0 +1,111 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Parser;
use ZBateson\MailMimeParser\Parser\Part\UUEncodedPartHeaderContainerFactory;
use ZBateson\MailMimeParser\Parser\Proxy\ParserMimePartProxy;
use ZBateson\MailMimeParser\Parser\Proxy\ParserNonMimeMessageProxy;
use ZBateson\MailMimeParser\Parser\Proxy\ParserNonMimeMessageProxyFactory;
use ZBateson\MailMimeParser\Parser\Proxy\ParserPartProxy;
use ZBateson\MailMimeParser\Parser\Proxy\ParserUUEncodedPartProxy;
use ZBateson\MailMimeParser\Parser\Proxy\ParserUUEncodedPartProxyFactory;
/**
* Parses content for non-mime messages and uu-encoded child parts.
*
* @author Zaahid Bateson
*/
class NonMimeParserService extends AbstractParserService
{
protected UUEncodedPartHeaderContainerFactory $partHeaderContainerFactory;
public function __construct(
ParserNonMimeMessageProxyFactory $parserNonMimeMessageProxyFactory,
ParserUUEncodedPartProxyFactory $parserUuEncodedPartProxyFactory,
PartBuilderFactory $partBuilderFactory,
UUEncodedPartHeaderContainerFactory $uuEncodedPartHeaderContainerFactory
) {
parent::__construct($parserNonMimeMessageProxyFactory, $parserUuEncodedPartProxyFactory, $partBuilderFactory);
$this->partHeaderContainerFactory = $uuEncodedPartHeaderContainerFactory;
}
/**
* Always returns true, and should therefore be the last parser reached by
* a ParserManager.
*/
public function canParse(PartBuilder $part) : bool
{
return true;
}
/**
* Creates a UUEncodedPartHeaderContainer attached to a PartBuilder, and
* calls $this->parserManager->createParserProxyFor().
*
* It also sets the PartBuilder's stream part start pos and content start
* pos to that of $parent->getNextParStart() (since a 'begin' line is read
* prior to another child being created, see parseNextPart()).
*/
private function createPart(ParserNonMimeMessageProxy $parent) : ParserPartProxy
{
$hc = $this->partHeaderContainerFactory->newInstance($parent->getNextPartMode(), $parent->getNextPartFilename());
$pb = $this->partBuilderFactory->newChildPartBuilder($hc, $parent);
$proxy = $this->parserManager->createParserProxyFor($pb);
$pb->setStreamPartStartPos($parent->getNextPartStart());
$pb->setStreamContentStartPos($parent->getNextPartStart());
return $proxy;
}
/**
* Reads content from the passed ParserPartProxy's stream till a uu-encoded
* 'begin' line is found, setting $proxy->setStreamPartContentAndEndPos() to
* the last byte read before the begin line.
*
* @param ParserNonMimeMessageProxy|ParserUUEncodedPartProxy $proxy
*/
private function parseNextPart(ParserPartProxy $proxy) : static
{
$handle = $proxy->getMessageResourceHandle();
while (!\feof($handle)) {
$start = \ftell($handle);
$line = \trim(MessageParserService::readLine($handle));
if (\preg_match('/^begin ([0-7]{3}) (.*)$/', $line, $matches)) {
$proxy->setNextPartStart($start);
$proxy->setNextPartMode((int) $matches[1]);
$proxy->setNextPartFilename($matches[2]);
return $this;
}
$proxy->setStreamPartAndContentEndPos(\ftell($handle));
}
return $this;
}
public function parseContent(ParserPartProxy $proxy) : static
{
$handle = $proxy->getMessageResourceHandle();
if ($proxy->getNextPartStart() !== null || \feof($handle)) {
return $this;
}
if ($proxy->getStreamContentStartPos() === null) {
$proxy->setStreamContentStartPos(\ftell($handle));
}
$this->parseNextPart($proxy);
return $this;
}
public function parseNextChild(ParserMimePartProxy $proxy) : ?ParserPartProxy
{
$handle = $proxy->getMessageResourceHandle();
if ($proxy->getNextPartStart() === null || \feof($handle)) {
return null;
}
$child = $this->createPart($proxy);
$proxy->clearNextPart();
return $child;
}
}

View File

@@ -0,0 +1,92 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Parser;
use ZBateson\MailMimeParser\Parser\Proxy\ParserPartProxy;
/**
* Manages a prioritized list of IParser objects for parsing messages and parts
* and creating proxied parts.
*
* The default ParserManager sets up a MimeParser in priority 0, and a
* NonMimeParser in priority 1.
*
* @author Zaahid Bateson
*/
class ParserManagerService
{
/**
* @var IParserService[] List of parsers in order of priority (0 is highest
* priority).
*/
protected array $parsers = [];
public function __construct(MimeParserService $mimeParser, NonMimeParserService $nonMimeParser)
{
$this->setParsers([$mimeParser, $nonMimeParser]);
}
/**
* Overrides the internal prioritized list of parses with the passed list,
* calling $parser->setParserManager($this) on each one.
*
* @param IParserService[] $parsers
*/
public function setParsers(array $parsers) : static
{
foreach ($parsers as $parser) {
$parser->setParserManager($this);
}
$this->parsers = $parsers;
return $this;
}
/**
* Adds an IParser at the highest priority (up front), calling
* $parser->setParserManager($this) on it.
*
* @param IParserService $parser The parser to add.
*/
public function prependParser(IParserService $parser) : static
{
$parser->setParserManager($this);
\array_unshift($this->parsers, $parser);
return $this;
}
/**
* Creates a ParserPartProxy for the passed $partBuilder using a compatible
* IParser.
*
* Loops through registered IParsers calling 'canParse()' on each with the
* passed PartBuilder, then calling either 'getParserMessageProxyFactory()'
* or 'getParserPartProxyFactory()' depending on if the PartBuilder has a
* parent, and finally calling 'newInstance' on the returned
* ParserPartProxyFactory passing it the IParser, and returning the new
* ParserPartProxy instance that was created.
*
* @param PartBuilder $partBuilder The PartBuilder to wrap in a proxy with
* an IParser
* @throws CompatibleParserNotFoundException if a compatible parser for the
* type is not configured.
* @return ParserPartProxy The created ParserPartProxy tied to a new
* IMessagePart and associated IParser.
*/
public function createParserProxyFor(PartBuilder $partBuilder) : ParserPartProxy
{
foreach ($this->parsers as $parser) {
if ($parser->canParse($partBuilder)) {
$factory = ($partBuilder->getParent() === null) ?
$parser->getParserMessageProxyFactory() :
$parser->getParserPartProxyFactory();
return $factory->newInstance($partBuilder, $parser);
}
}
throw new CompatibleParserNotFoundException('Compatible parser for a part cannot be found with content-type: ' . $partBuilder->getHeaderContainer()->get('Content-Type'));
}
}

View File

@@ -0,0 +1,53 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Parser\Part;
use ZBateson\MailMimeParser\Message\PartChildrenContainer;
use ZBateson\MailMimeParser\Parser\Proxy\ParserMimePartProxy;
/**
* A child container that proxies calls to a parser when attempting to access
* child parts.
*
* @author Zaahid Bateson
*/
class ParserPartChildrenContainer extends PartChildrenContainer
{
/**
* @var ParserMimePartProxy The parser to proxy requests to when trying to
* get child parts.
*/
protected ParserMimePartProxy $parserProxy;
/**
* @var bool Set to true once all parts have been parsed, and requests to
* the proxy won't result in any more child parts.
*/
private bool $allParsed = false;
public function __construct(ParserMimePartProxy $parserProxy)
{
parent::__construct([]);
$this->parserProxy = $parserProxy;
}
public function offsetExists($offset) : bool
{
$exists = parent::offsetExists($offset);
while (!$exists && !$this->allParsed) {
$child = $this->parserProxy->popNextChild();
if ($child === null) {
$this->allParsed = true;
} else {
$this->add($child);
}
$exists = parent::offsetExists($offset);
}
return $exists;
}
}

View File

@@ -0,0 +1,23 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Parser\Part;
use ZBateson\MailMimeParser\Parser\Proxy\ParserMimePartProxy;
/**
* Creates ParserPartChildrenContainer instances.
*
* @author Zaahid Bateson
*/
class ParserPartChildrenContainerFactory
{
public function newInstance(ParserMimePartProxy $parserProxy) : ParserPartChildrenContainer
{
return new ParserPartChildrenContainer($parserProxy);
}
}

View File

@@ -0,0 +1,161 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Parser\Part;
use Psr\Http\Message\StreamInterface;
use Psr\Log\LoggerInterface;
use SplObserver;
use SplSubject;
use ZBateson\MailMimeParser\Message\IMessagePart;
use ZBateson\MailMimeParser\Message\PartStreamContainer;
use ZBateson\MailMimeParser\Parser\Proxy\ParserPartProxy;
use ZBateson\MailMimeParser\Stream\MessagePartStreamDecorator;
use ZBateson\MailMimeParser\Stream\StreamFactory;
use ZBateson\MbWrapper\MbWrapper;
/**
* A part stream container that proxies requests for content streams to a parser
* to read the content.
*
* Keeps reference to the original stream a part was parsed from, using that
* stream as the part's stream instead of the PartStreamContainer's
* MessagePartStream (which dynamically creates a stream from an IMessagePart)
* unless the part changed.
*
* The ParserPartStreamContainer must also be attached to its underlying part
* with SplSubject::attach() so the ParserPartStreamContainer gets notified of
* any changes.
*
* @author Zaahid Bateson
*/
class ParserPartStreamContainer extends PartStreamContainer implements SplObserver
{
/**
* @var ParserPartProxy The parser proxy to ferry requests to on-demand.
*/
protected ParserPartProxy $parserProxy;
/**
* @var MessagePartStreamDecorator the original stream for a parsed message,
* wrapped in a MessagePartStreamDecorator, and used when the message
* hasn't changed
*/
protected ?MessagePartStreamDecorator $parsedStream = null;
/**
* @var bool set to true if the part's been updated since it was created.
*/
protected bool $partUpdated = false;
/**
* @var bool false if the content for the part represented by this container
* has not yet been requested from the parser.
*/
protected bool $contentParseRequested = false;
public function __construct(
LoggerInterface $logger,
StreamFactory $streamFactory,
MbWrapper $mbWrapper,
bool $throwExceptionReadingPartContentFromUnsupportedCharsets,
ParserPartProxy $parserProxy
) {
parent::__construct($logger, $streamFactory, $mbWrapper, $throwExceptionReadingPartContentFromUnsupportedCharsets);
$this->parserProxy = $parserProxy;
}
public function __destruct()
{
if ($this->detachParsedStream && $this->parsedStream !== null) {
$this->parsedStream->detach();
}
}
/**
* Requests content from the parser if not previously requested, and calls
* PartStreamContainer::setContentStream().
*/
protected function requestParsedContentStream() : static
{
if (!$this->contentParseRequested) {
$this->contentParseRequested = true;
$this->parserProxy->parseContent();
parent::setContentStream($this->streamFactory->getLimitedContentStream(
$this->parserProxy
));
}
return $this;
}
/**
* Ensures the parser has parsed the entire part, and sets
* $this->parsedStream to the original parsed stream (or a limited part of
* it corresponding to the current part this stream container belongs to).
*/
protected function requestParsedStream() : static
{
if ($this->parsedStream === null) {
$this->parserProxy->parseAll();
$this->parsedStream = $this->streamFactory->newDecoratedMessagePartStream(
$this->parserProxy->getPart(),
$this->streamFactory->getLimitedPartStream(
$this->parserProxy
)
);
if ($this->parsedStream !== null) {
$this->detachParsedStream = ($this->parsedStream->getMetadata('mmp-detached-stream') === true);
}
}
return $this;
}
public function hasContent() : bool
{
$this->requestParsedContentStream();
return parent::hasContent();
}
public function getContentStream(IMessagePart $part, ?string $transferEncoding, ?string $fromCharset, ?string $toCharset) : ?MessagePartStreamDecorator
{
$this->requestParsedContentStream();
return parent::getContentStream($part, $transferEncoding, $fromCharset, $toCharset);
}
public function getBinaryContentStream(IMessagePart $part, ?string $transferEncoding = null) : ?MessagePartStreamDecorator
{
$this->requestParsedContentStream();
return parent::getBinaryContentStream($part, $transferEncoding);
}
public function setContentStream(?StreamInterface $contentStream = null) : static
{
// has to be overridden because requestParsedContentStream calls
// parent::setContentStream as well, so needs to be parsed before
// overriding the contentStream with a manual 'set'.
$this->requestParsedContentStream();
parent::setContentStream($contentStream);
return $this;
}
public function getStream() : MessagePartStreamDecorator
{
$this->requestParsedStream();
if (!$this->partUpdated) {
if ($this->parsedStream !== null) {
$this->parsedStream->rewind();
return $this->parsedStream;
}
}
return parent::getStream();
}
public function update(SplSubject $subject) : void
{
$this->partUpdated = true;
}
}

View File

@@ -0,0 +1,52 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Parser\Part;
use Psr\Log\LoggerInterface;
use ZBateson\MailMimeParser\Parser\Proxy\ParserPartProxy;
use ZBateson\MailMimeParser\Stream\StreamFactory;
use ZBateson\MbWrapper\MbWrapper;
/**
* Creates ParserPartStreamContainer instances.
*
* @author Zaahid Bateson
*/
class ParserPartStreamContainerFactory
{
protected LoggerInterface $logger;
protected StreamFactory $streamFactory;
protected MbWrapper $mbWrapper;
protected bool $throwExceptionReadingPartContentFromUnsupportedCharsets;
public function __construct(
LoggerInterface $logger,
StreamFactory $streamFactory,
MbWrapper $mbWrapper,
bool $throwExceptionReadingPartContentFromUnsupportedCharsets
) {
$this->logger = $logger;
$this->streamFactory = $streamFactory;
$this->mbWrapper = $mbWrapper;
$this->throwExceptionReadingPartContentFromUnsupportedCharsets = $throwExceptionReadingPartContentFromUnsupportedCharsets;
}
public function newInstance(ParserPartProxy $parserProxy) : ParserPartStreamContainer
{
return new ParserPartStreamContainer(
$this->logger,
$this->streamFactory,
$this->mbWrapper,
$this->throwExceptionReadingPartContentFromUnsupportedCharsets,
$parserProxy
);
}
}

View File

@@ -0,0 +1,66 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Parser\Part;
use ZBateson\MailMimeParser\Message\PartHeaderContainer;
/**
* Header container representing the start line of a uu-encoded part.
*
* The line may contain a unix file mode and a filename.
*
* @author Zaahid Bateson
*/
class UUEncodedPartHeaderContainer extends PartHeaderContainer
{
/**
* @var ?int the unix file permission
*/
protected ?int $mode = null;
/**
* @var ?string the name of the file in the uuencoding 'header'.
*/
protected ?string $filename = null;
/**
* Returns the file mode included in the uuencoded 'begin' line for this
* part.
*/
public function getUnixFileMode() : ?int
{
return $this->mode;
}
/**
* Sets the unix file mode for the uuencoded 'begin' line.
*/
public function setUnixFileMode(int $mode) : static
{
$this->mode = $mode;
return $this;
}
/**
* Returns the filename included in the uuencoded 'begin' line for this
* part.
*/
public function getFilename() : ?string
{
return $this->filename;
}
/**
* Sets the filename included in the uuencoded 'begin' line.
*/
public function setFilename(string $filename) : static
{
$this->filename = $filename;
return $this;
}
}

View File

@@ -0,0 +1,48 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Parser\Part;
use Psr\Log\LoggerInterface;
use ZBateson\MailMimeParser\Header\HeaderFactory;
/**
* Creates UUEncodedPartHeaderContainer instances.
*
* @author Zaahid Bateson
*/
class UUEncodedPartHeaderContainerFactory
{
protected LoggerInterface $logger;
/**
* @var HeaderFactory the HeaderFactory passed to
* UUEncodedPartHeaderContainer instances.
*/
protected HeaderFactory $headerFactory;
/**
* Constructor
*
*/
public function __construct(LoggerInterface $logger, HeaderFactory $headerFactory)
{
$this->logger = $logger;
$this->headerFactory = $headerFactory;
}
/**
* Creates and returns a UUEncodedPartHeaderContainer.
*/
public function newInstance(int $mode, string $filename) : UUEncodedPartHeaderContainer
{
$container = new UUEncodedPartHeaderContainer($this->logger, $this->headerFactory);
$container->setUnixFileMode($mode);
$container->setFilename($filename);
return $container;
}
}

View File

@@ -0,0 +1,262 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Parser;
use GuzzleHttp\Psr7\StreamWrapper;
use Psr\Http\Message\StreamInterface;
use ZBateson\MailMimeParser\Header\HeaderConsts;
use ZBateson\MailMimeParser\Message\PartHeaderContainer;
use ZBateson\MailMimeParser\Parser\Proxy\ParserMimePartProxy;
/**
* Holds generic/all purpose information about a part while it's being parsed.
*
* The class holds:
* - a HeaderContainer to hold headers
* - stream positions (part start/end positions, content start/end)
* - the message's psr7 stream and a resource handle created from it (held
* only for a top-level PartBuilder representing the message, child
* PartBuilders do not duplicate/hold a separate stream).
*
* More specific information a parser needs to keep about a message as it's
* parsing it should be stored in its ParserPartProxy.
*
* @author Zaahid Bateson
*/
class PartBuilder
{
/**
* @var int The offset read start position for this part (beginning of
* headers) in the message's stream.
*/
private int $streamPartStartPos;
/**
* @var int The offset read end position for this part. If the part is a
* multipart mime part, the end position is after all of this parts
* children.
*/
private int $streamPartEndPos;
/**
* @var ?int The offset read start position in the message's stream for the
* beginning of this part's content (body).
*/
private ?int $streamContentStartPos = null;
/**
* @var ?int The offset read end position in the message's stream for the
* end of this part's content (body).
*/
private ?int $streamContentEndPos = null;
/**
* @var PartHeaderContainer The parsed part's headers.
*/
private PartHeaderContainer $headerContainer;
/**
* @var StreamInterface the raw message input stream for a message, or null
* for a child part.
*/
private ?StreamInterface $messageStream = null;
/**
* @var resource the raw message input stream handle constructed from
* $messageStream or null for a child part
*/
private mixed $messageHandle = null;
/**
* @var ParserMimePartProxy The parent proxy part if one is set, or null if
* the part being built doesn't have a parent.
*/
private ?ParserMimePartProxy $parent = null;
public function __construct(PartHeaderContainer $headerContainer, ?StreamInterface $messageStream = null, ?ParserMimePartProxy $parent = null)
{
$this->headerContainer = $headerContainer;
$this->messageStream = $messageStream;
$this->parent = $parent;
if ($messageStream !== null) {
$this->messageHandle = StreamWrapper::getResource($messageStream);
}
$this->setStreamPartStartPos($this->getMessageResourceHandlePos());
}
public function __destruct()
{
if ($this->messageHandle) {
\fclose($this->messageHandle);
}
}
/**
* The ParserPartProxy parent of this PartBuilder.
*/
public function getParent() : ?ParserMimePartProxy
{
return $this->parent;
}
/**
* Returns this part's PartHeaderContainer.
*/
public function getHeaderContainer() : PartHeaderContainer
{
return $this->headerContainer;
}
/**
* Returns the raw message StreamInterface for a message, getting it from
* the parent part if this is a child part.
*/
public function getStream() : StreamInterface
{
return ($this->messageStream === null && $this->parent !== null) ?
$this->parent->getStream() :
$this->messageStream;
}
/**
* Returns the resource handle for a the message's stream, getting it from
* the parent part if this is a child part.
*
* @return resource
*/
public function getMessageResourceHandle() : mixed
{
return ($this->messageHandle === null && $this->parent !== null) ?
$this->parent->getMessageResourceHandle() :
$this->messageHandle;
}
/**
* Shortcut for calling ftell($partBuilder->getMessageResourceHandle()).
*/
public function getMessageResourceHandlePos() : int
{
return \ftell($this->getMessageResourceHandle());
}
/**
* Returns the byte offset start position for this part within the message
* stream.
*/
public function getStreamPartStartPos() : int
{
return $this->streamPartStartPos;
}
/**
* Returns the number of raw bytes this part has.
*
* This method does not perform checks on whether the start pos and end pos
* of this part have been set, and so could cause errors if called before
* being set and are still null.
*
*/
public function getStreamPartLength() : int
{
return $this->streamPartEndPos - $this->streamPartStartPos;
}
/**
* Returns the byte offset start position of the content of this part within
* the main raw message stream, or null if not set.
*
*/
public function getStreamContentStartPos() : ?int
{
return $this->streamContentStartPos;
}
/**
* Returns the length of this part's content stream.
*
* This method does not perform checks on whether the start pos and end pos
* of this part's content have been set, and so could cause errors if called
* before being set and are still null.
*
*/
public function getStreamContentLength() : int
{
return $this->streamContentEndPos - $this->streamContentStartPos;
}
/**
* Sets the byte offset start position of the part in the raw message
* stream.
*/
public function setStreamPartStartPos(int $streamPartStartPos) : static
{
$this->streamPartStartPos = $streamPartStartPos;
return $this;
}
/**
* Sets the byte offset end position of the part in the raw message stream,
* and also calls its parent's setParentStreamPartEndPos to expand to parent
* PartBuilders.
*/
public function setStreamPartEndPos(int $streamPartEndPos) : static
{
$this->streamPartEndPos = $streamPartEndPos;
if ($this->parent !== null) {
$this->parent->setStreamPartEndPos($streamPartEndPos);
}
return $this;
}
/**
* Sets the byte offset start position of the content in the raw message
* stream.
*/
public function setStreamContentStartPos(int $streamContentStartPos) : static
{
$this->streamContentStartPos = $streamContentStartPos;
return $this;
}
/**
* Sets the byte offset end position of the content and part in the raw
* message stream.
*/
public function setStreamPartAndContentEndPos(int $streamContentEndPos) : static
{
$this->streamContentEndPos = $streamContentEndPos;
$this->setStreamPartEndPos($streamContentEndPos);
return $this;
}
/**
* Returns true if the byte offset positions for this part's content have
* been set.
*
* @return bool true if set.
*/
public function isContentParsed() : bool
{
return ($this->streamContentEndPos !== null);
}
/**
* Returns true if this part, or any parent, have a Content-Type or
* MIME-Version header set.
*
* @return bool true if it's a mime message or child of a mime message.
*/
public function isMime() : bool
{
if ($this->getParent() !== null) {
return $this->getParent()->isMime();
}
return ($this->headerContainer->exists(HeaderConsts::CONTENT_TYPE) ||
$this->headerContainer->exists(HeaderConsts::MIME_VERSION));
}
}

View File

@@ -0,0 +1,41 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Parser;
use Psr\Http\Message\StreamInterface;
use ZBateson\MailMimeParser\Message\PartHeaderContainer;
use ZBateson\MailMimeParser\Parser\Proxy\ParserPartProxy;
/**
* Responsible for creating PartBuilder instances.
*
* @author Zaahid Bateson
*/
class PartBuilderFactory
{
/**
* Constructs a top-level (message) PartBuilder object and returns it.
*/
public function newPartBuilder(PartHeaderContainer $headerContainer, StreamInterface $messageStream) : PartBuilder
{
return new PartBuilder($headerContainer, $messageStream);
}
/**
* Constructs a child PartBuilder object with the passed $parent as its
* parent, and returns it.
*/
public function newChildPartBuilder(PartHeaderContainer $headerContainer, ParserPartProxy $parent) : PartBuilder
{
return new PartBuilder(
$headerContainer,
null,
$parent
);
}
}

View File

@@ -0,0 +1,36 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Parser\Proxy;
/**
* A bi-directional parser-to-part proxy for IMessage objects created by
* MimeParser.
*
* @author Zaahid Bateson
*/
class ParserMessageProxy extends ParserMimePartProxy
{
/**
* @var int maintains the character length of the last line separator,
* typically 2 for CRLF, to keep track of the correct 'end' position
* for a part because the CRLF before a boundary is considered part of
* the boundary.
*/
protected int $lastLineEndingLength = 0;
public function getLastLineEndingLength() : int
{
return $this->lastLineEndingLength;
}
public function setLastLineEndingLength(int $lastLineEndingLength) : static
{
$this->lastLineEndingLength = $lastLineEndingLength;
return $this;
}
}

View File

@@ -0,0 +1,74 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Parser\Proxy;
use Psr\Log\LoggerInterface;
use ZBateson\MailMimeParser\Message;
use ZBateson\MailMimeParser\Message\Factory\PartHeaderContainerFactory;
use ZBateson\MailMimeParser\Message\Helper\MultipartHelper;
use ZBateson\MailMimeParser\Message\Helper\PrivacyHelper;
use ZBateson\MailMimeParser\Parser\IParserService;
use ZBateson\MailMimeParser\Parser\Part\ParserPartChildrenContainerFactory;
use ZBateson\MailMimeParser\Parser\Part\ParserPartStreamContainerFactory;
use ZBateson\MailMimeParser\Parser\PartBuilder;
use ZBateson\MailMimeParser\Stream\StreamFactory;
/**
* Responsible for creating proxied IMessage instances wrapped in a
* ParserMessageProxy.
*
* @author Zaahid Bateson
*/
class ParserMessageProxyFactory extends ParserMimePartProxyFactory
{
protected MultipartHelper $multipartHelper;
protected PrivacyHelper $privacyHelper;
public function __construct(
LoggerInterface $logger,
StreamFactory $sdf,
PartHeaderContainerFactory $phcf,
ParserPartStreamContainerFactory $pscf,
ParserPartChildrenContainerFactory $ppccf,
MultipartHelper $multipartHelper,
PrivacyHelper $privacyHelper
) {
parent::__construct($logger, $sdf, $phcf, $pscf, $ppccf);
$this->multipartHelper = $multipartHelper;
$this->privacyHelper = $privacyHelper;
}
/**
* Constructs a new ParserMessageProxy wrapping an IMessage object that will
* dynamically parse a message's content and parts as they're requested.
*/
public function newInstance(PartBuilder $partBuilder, IParserService $parser) : ParserMessageProxy
{
$parserProxy = new ParserMessageProxy($partBuilder, $parser);
$streamContainer = $this->parserPartStreamContainerFactory->newInstance($parserProxy);
$headerContainer = $this->partHeaderContainerFactory->newInstance($parserProxy->getHeaderContainer());
$childrenContainer = $this->parserPartChildrenContainerFactory->newInstance($parserProxy);
$message = new Message(
$this->logger,
$streamContainer,
$headerContainer,
$childrenContainer,
$this->multipartHelper,
$this->privacyHelper
);
$parserProxy->setPart($message);
$streamContainer->setStream($this->streamFactory->newMessagePartStream($message));
$message->attach($streamContainer);
return $parserProxy;
}
}

View File

@@ -0,0 +1,286 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Parser\Proxy;
use Psr\Log\LogLevel;
use ZBateson\MailMimeParser\Header\HeaderConsts;
use ZBateson\MailMimeParser\Header\ParameterHeader;
use ZBateson\MailMimeParser\Message\IMessagePart;
/**
* A bi-directional parser-to-part proxy for MimeParser and IMimeParts.
*
* @author Zaahid Bateson
*/
class ParserMimePartProxy extends ParserPartProxy
{
/**
* @var bool set to true once the end boundary of the currently-parsed
* part is found.
*/
protected bool $endBoundaryFound = false;
/**
* @var bool set to true once a boundary belonging to this parent's part
* is found.
*/
protected bool $parentBoundaryFound = false;
/**
* @var bool true once all children of this part have been parsed.
*/
protected bool $allChildrenParsed = false;
/**
* @var ParserPartProxy[] Array of all parsed children.
*/
protected array $children = [];
/**
* @var ParserPartProxy[] Parsed children used as a 'first-in-first-out'
* stack as children are parsed.
*/
protected array $childrenStack = [];
/**
* @var ParserPartProxy Reference to the last child added to this part.
*/
protected ?ParserPartProxy $lastAddedChild = null;
/**
* @var ?string NULL if the current part does not have a boundary, and
* otherwise contains the value of the boundary parameter of the
* content-type header if the part contains one.
*/
private ?string $mimeBoundary = null;
/**
* @var bool FALSE if not queried for in the content-type header of this
* part and set in $mimeBoundary.
*/
private bool $mimeBoundaryQueried = false;
/**
* Ensures that the last child added to this part is fully parsed (content
* and children).
*/
protected function ensureLastChildParsed() : static
{
if ($this->lastAddedChild !== null) {
$this->lastAddedChild->parseAll();
}
return $this;
}
/**
* Parses the next child of this part and adds it to the 'stack' of
* children.
*/
protected function parseNextChild() : static
{
if ($this->allChildrenParsed) {
return $this;
}
$this->parseContent();
$this->ensureLastChildParsed();
$next = $this->parser->parseNextChild($this);
if ($next !== null) {
$this->children[] = $next;
$this->childrenStack[] = $next;
$this->lastAddedChild = $next;
} else {
$this->allChildrenParsed = true;
}
return $this;
}
/**
* Returns the next child part if one exists, popping it from the internal
* 'stack' of children, attempting to parse a new one if the stack is empty,
* and returning null if there are no more children.
*
* @return ?IMessagePart the child part.
*/
public function popNextChild() : ?IMessagePart
{
if (empty($this->childrenStack)) {
$this->parseNextChild();
}
$proxy = \array_shift($this->childrenStack);
return ($proxy !== null) ? $proxy->getPart() : null;
}
/**
* Parses all content and children for this part.
*/
public function parseAll() : static
{
$this->parseContent();
while (!$this->allChildrenParsed) {
$this->parseNextChild();
}
return $this;
}
/**
* Returns a ParameterHeader representing the parsed Content-Type header for
* this part.
*/
public function getContentType() : ?ParameterHeader
{
return $this->getHeaderContainer()->get(HeaderConsts::CONTENT_TYPE);
}
/**
* Returns the parsed boundary parameter of the Content-Type header if set
* for a multipart message part.
*
*/
public function getMimeBoundary() : ?string
{
if ($this->mimeBoundaryQueried === false) {
$this->mimeBoundaryQueried = true;
$contentType = $this->getContentType();
if ($contentType !== null) {
$this->mimeBoundary = $contentType->getValueFor('boundary');
}
}
return $this->mimeBoundary;
}
/**
* Returns true if the passed $line of read input matches this part's mime
* boundary, or any of its parent's mime boundaries for a multipart message.
*
* If the passed $line is the ending boundary for the current part,
* $this->isEndBoundaryFound will return true after.
*/
public function setEndBoundaryFound(string $line) : bool
{
$boundary = $this->getMimeBoundary();
if ($this->getParent() !== null && $this->getParent()->setEndBoundaryFound($line)) {
$this->parentBoundaryFound = true;
return true;
} elseif ($boundary !== null) {
if ($line === "--$boundary--") {
$this->endBoundaryFound = true;
return true;
} elseif ($line === "--$boundary") {
return true;
}
}
return false;
}
/**
* Returns true if the parser passed an input line to setEndBoundary that
* matches a parent's mime boundary, and the following input belongs to a
* new part under its parent.
*
*/
public function isParentBoundaryFound() : bool
{
return ($this->parentBoundaryFound);
}
/**
* Returns true if an end boundary was found for this part.
*
*/
public function isEndBoundaryFound() : bool
{
return ($this->endBoundaryFound);
}
/**
* Called once EOF is reached while reading content. The method sets the
* flag used by isParentBoundaryFound() to true on this part and all parent
* parts.
*
*/
public function setEof() : static
{
$this->parentBoundaryFound = true;
if ($this->getParent() !== null) {
$this->getParent()->setEof();
}
return $this;
}
/**
* Overridden to set a 0-length content length, and a stream end pos of -2
* if the passed end pos is before the start pos (can happen if a mime
* end boundary doesn't have an empty line before the next parent start
* boundary).
*/
public function setStreamPartAndContentEndPos(int $streamContentEndPos) : static
{
// check if we're expecting a boundary and didn't find one
if (!$this->endBoundaryFound && !$this->parentBoundaryFound) {
if (!empty($this->mimeBoundary) || ($this->getParent() !== null && !empty($this->getParent()->mimeBoundary))) {
$this->addError('End boundary for part not found', LogLevel::WARNING);
}
}
$start = $this->getStreamContentStartPos();
if ($streamContentEndPos - $start < 0) {
parent::setStreamPartAndContentEndPos($start);
$this->setStreamPartEndPos($streamContentEndPos);
} else {
parent::setStreamPartAndContentEndPos($streamContentEndPos);
}
return $this;
}
/**
* Sets the length of the last line ending read by MimeParser (e.g. 2 for
* '\r\n', or 1 for '\n').
*
* The line ending may not belong specifically to this part, so
* ParserMimePartProxy simply calls setLastLineEndingLength on its parent,
* which must eventually reach a ParserMessageProxy which actually stores
* the length.
*/
public function setLastLineEndingLength(int $length) : static
{
$this->getParent()->setLastLineEndingLength($length);
return $this;
}
/**
* Returns the length of the last line ending read by MimeParser (e.g. 2 for
* '\r\n', or 1 for '\n').
*
* The line ending may not belong specifically to this part, so
* ParserMimePartProxy simply calls getLastLineEndingLength on its parent,
* which must eventually reach a ParserMessageProxy which actually keeps
* the length and returns it.
*
* @return int the length of the last line ending read
*/
public function getLastLineEndingLength() : int
{
return $this->getParent()->getLastLineEndingLength();
}
/**
* Returns the last part that was added.
*/
public function getLastAddedChild() : ?ParserPartProxy
{
return $this->lastAddedChild;
}
/**
* Returns the added child at the provided index, useful for looking at
* previously parsed children.
*/
public function getAddedChildAt(int $index) : ?ParserPartProxy
{
return $this->children[$index] ?? null;
}
}

View File

@@ -0,0 +1,77 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Parser\Proxy;
use Psr\Log\LoggerInterface;
use ZBateson\MailMimeParser\Message\Factory\PartHeaderContainerFactory;
use ZBateson\MailMimeParser\Message\MimePart;
use ZBateson\MailMimeParser\Parser\IParserService;
use ZBateson\MailMimeParser\Parser\Part\ParserPartChildrenContainerFactory;
use ZBateson\MailMimeParser\Parser\Part\ParserPartStreamContainerFactory;
use ZBateson\MailMimeParser\Parser\PartBuilder;
use ZBateson\MailMimeParser\Stream\StreamFactory;
/**
* Responsible for creating proxied IMimePart instances wrapped in a
* ParserMimePartProxy with a MimeParser.
*
* @author Zaahid Bateson
*/
class ParserMimePartProxyFactory extends ParserPartProxyFactory
{
protected LoggerInterface $logger;
protected StreamFactory $streamFactory;
protected ParserPartStreamContainerFactory $parserPartStreamContainerFactory;
protected PartHeaderContainerFactory $partHeaderContainerFactory;
protected ParserPartChildrenContainerFactory $parserPartChildrenContainerFactory;
public function __construct(
LoggerInterface $logger,
StreamFactory $sdf,
PartHeaderContainerFactory $phcf,
ParserPartStreamContainerFactory $pscf,
ParserPartChildrenContainerFactory $ppccf
) {
$this->logger = $logger;
$this->streamFactory = $sdf;
$this->partHeaderContainerFactory = $phcf;
$this->parserPartStreamContainerFactory = $pscf;
$this->parserPartChildrenContainerFactory = $ppccf;
}
/**
* Constructs a new ParserMimePartProxy wrapping an IMimePart object that
* will dynamically parse a message's content and parts as they're
* requested.
*/
public function newInstance(PartBuilder $partBuilder, IParserService $parser) : ParserMimePartProxy
{
$parserProxy = new ParserMimePartProxy($partBuilder, $parser);
$streamContainer = $this->parserPartStreamContainerFactory->newInstance($parserProxy);
$headerContainer = $this->partHeaderContainerFactory->newInstance($parserProxy->getHeaderContainer());
$childrenContainer = $this->parserPartChildrenContainerFactory->newInstance($parserProxy);
$part = new MimePart(
$partBuilder->getParent()->getPart(),
$this->logger,
$streamContainer,
$headerContainer,
$childrenContainer
);
$parserProxy->setPart($part);
$streamContainer->setStream($this->streamFactory->newMessagePartStream($part));
$part->attach($streamContainer);
return $parserProxy;
}
}

View File

@@ -0,0 +1,108 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Parser\Proxy;
/**
* A bi-directional parser-to-part proxy for IMessage objects created by
* NonMimeParser.
*
* @author Zaahid Bateson
*/
class ParserNonMimeMessageProxy extends ParserMessageProxy
{
/**
* @var ?int The next part's start position within the message's raw stream
* or null if not set, not discovered, or there are no more parts.
*/
protected ?int $nextPartStart = null;
/**
* @var ?int The next part's unix file mode in a uu-encoded 'begin' line if
* exists, or null otherwise.
*/
protected ?int $nextPartMode = null;
/**
* @var ?string The next part's file name in a uu-encoded 'begin' line if
* exists, or null otherwise.
*/
protected ?string $nextPartFilename = null;
/**
* Returns the next part's start position within the message's raw stream,
* or null if not set, not discovered, or there are no more parts under this
* message.
*
* @return int|null The start position or null
*/
public function getNextPartStart() : ?int
{
return $this->nextPartStart;
}
/**
* Returns the next part's unix file mode in a uu-encoded 'begin' line if
* one exists, or null otherwise.
*
* @return int|null The file mode or null
*/
public function getNextPartMode() : ?int
{
return $this->nextPartMode;
}
/**
* Returns the next part's filename in a uu-encoded 'begin' line if one
* exists, or null otherwise.
*
* @return string|null The file name or null
*/
public function getNextPartFilename() : ?string
{
return $this->nextPartFilename;
}
/**
* Sets the next part's start position within the message's raw stream.
*/
public function setNextPartStart(int $nextPartStart) : static
{
$this->nextPartStart = $nextPartStart;
return $this;
}
/**
* Sets the next part's unix file mode from its 'begin' line.
*/
public function setNextPartMode(int $nextPartMode) : static
{
$this->nextPartMode = $nextPartMode;
return $this;
}
/**
* Sets the next part's filename from its 'begin' line.
*
*/
public function setNextPartFilename(string $nextPartFilename) : static
{
$this->nextPartFilename = $nextPartFilename;
return $this;
}
/**
* Sets the next part start position, file mode, and filename to null
*/
public function clearNextPart() : static
{
$this->nextPartStart = null;
$this->nextPartMode = null;
$this->nextPartFilename = null;
return $this;
}
}

View File

@@ -0,0 +1,47 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Parser\Proxy;
use ZBateson\MailMimeParser\Message;
use ZBateson\MailMimeParser\Parser\IParserService;
use ZBateson\MailMimeParser\Parser\PartBuilder;
/**
* Responsible for creating proxied IMessage instances wrapped in a
* ParserNonMimeMessageProxy for NonMimeParser.
*
* @author Zaahid Bateson
*/
class ParserNonMimeMessageProxyFactory extends ParserMessageProxyFactory
{
/**
* Constructs a new ParserNonMimeMessageProxy wrapping an IMessage object.
*/
public function newInstance(PartBuilder $partBuilder, IParserService $parser) : ParserNonMimeMessageProxy
{
$parserProxy = new ParserNonMimeMessageProxy($partBuilder, $parser);
$streamContainer = $this->parserPartStreamContainerFactory->newInstance($parserProxy);
$headerContainer = $this->partHeaderContainerFactory->newInstance($parserProxy->getHeaderContainer());
$childrenContainer = $this->parserPartChildrenContainerFactory->newInstance($parserProxy);
$message = new Message(
$this->logger,
$streamContainer,
$headerContainer,
$childrenContainer,
$this->multipartHelper,
$this->privacyHelper
);
$parserProxy->setPart($message);
$streamContainer->setStream($this->streamFactory->newMessagePartStream($message));
$message->attach($streamContainer);
return $parserProxy;
}
}

View File

@@ -0,0 +1,184 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Parser\Proxy;
use Psr\Http\Message\StreamInterface;
use ZBateson\MailMimeParser\Message\IMessagePart;
use ZBateson\MailMimeParser\Message\PartHeaderContainer;
use ZBateson\MailMimeParser\Parser\IParserService;
use ZBateson\MailMimeParser\Parser\PartBuilder;
/**
* Proxy between a MessagePart and a Parser.
*
* ParserPartProxy objects are responsible for ferrying requests from message
* parts to a proxy as they're requested, and for maintaining state information
* for a parser as necessary.
*
* @author Zaahid Bateson
*/
abstract class ParserPartProxy extends PartBuilder
{
/**
* @var IParserService The parser.
*/
protected IParserService $parser;
/**
* @var PartBuilder The part's PartBuilder.
*/
protected PartBuilder $partBuilder;
/**
* @var IMessagePart The part.
*/
private IMessagePart $part;
public function __construct(PartBuilder $partBuilder, IParserService $parser)
{
$this->partBuilder = $partBuilder;
$this->parser = $parser;
}
/**
* Sets the associated part.
*
* @param IMessagePart $part The part
*/
public function setPart(IMessagePart $part) : static
{
$this->part = $part;
return $this;
}
/**
* Returns the IMessagePart associated with this proxy.
*
* @return IMessagePart the part.
*/
public function getPart() : IMessagePart
{
return $this->part;
}
/**
* Requests the parser to parse this part's content, and call
* setStreamContentStartPos/EndPos to setup this part's boundaries within
* the main message's raw stream.
*
* The method first checks to see if the content has already been parsed,
* and is safe to call multiple times.
*/
public function parseContent() : static
{
if (!$this->isContentParsed()) {
$this->parser->parseContent($this);
}
return $this;
}
/**
* Parses everything under this part.
*
* For ParserPartProxy, this is just content, but sub-classes may override
* this to parse all children as well for example.
*/
public function parseAll() : static
{
$this->parseContent();
return $this;
}
public function getParent() : ?ParserMimePartProxy
{
return $this->partBuilder->getParent();
}
public function getHeaderContainer() : PartHeaderContainer
{
return $this->partBuilder->getHeaderContainer();
}
public function getStream() : StreamInterface
{
return $this->partBuilder->getStream();
}
/**
* @return resource
*/
public function getMessageResourceHandle() : mixed
{
return $this->partBuilder->getMessageResourceHandle();
}
public function getMessageResourceHandlePos() : int
{
return $this->partBuilder->getMessageResourceHandlePos();
}
public function getStreamPartStartPos() : int
{
return $this->partBuilder->getStreamPartStartPos();
}
public function getStreamPartLength() : int
{
return $this->partBuilder->getStreamPartLength();
}
public function getStreamContentStartPos() : ?int
{
return $this->partBuilder->getStreamContentStartPos();
}
public function getStreamContentLength() : int
{
return $this->partBuilder->getStreamContentLength();
}
public function setStreamPartStartPos(int $streamPartStartPos) : static
{
$this->partBuilder->setStreamPartStartPos($streamPartStartPos);
return $this;
}
public function setStreamPartEndPos(int $streamPartEndPos) : static
{
$this->partBuilder->setStreamPartEndPos($streamPartEndPos);
return $this;
}
public function setStreamContentStartPos(int $streamContentStartPos) : static
{
$this->partBuilder->setStreamContentStartPos($streamContentStartPos);
return $this;
}
public function setStreamPartAndContentEndPos(int $streamContentEndPos) : static
{
$this->partBuilder->setStreamPartAndContentEndPos($streamContentEndPos);
return $this;
}
public function isContentParsed() : bool
{
return $this->partBuilder->isContentParsed();
}
public function isMime() : bool
{
return $this->partBuilder->isMime();
}
public function addError(string $message, string $level) : ParserPartProxy
{
$this->part->addError($message, $level);
return $this;
}
}

View File

@@ -0,0 +1,25 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Parser\Proxy;
use ZBateson\MailMimeParser\Parser\IParserService;
use ZBateson\MailMimeParser\Parser\PartBuilder;
/**
* Base class for factories creating ParserPartProxy classes.
*
* @author Zaahid Bateson
*/
abstract class ParserPartProxyFactory
{
/**
* Constructs a new ParserPartProxy wrapping an IMessagePart object.
*
*/
abstract public function newInstance(PartBuilder $partBuilder, IParserService $parser) : ParserPartProxy;
}

View File

@@ -0,0 +1,128 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Parser\Proxy;
/**
* A bi-directional parser-to-part proxy for NonMimeParser and IUUEncodedParts.
*
* @author Zaahid Bateson
*/
class ParserUUEncodedPartProxy extends ParserPartProxy
{
/**
* Only has a single parent of type ParserNonMimeMessageProxy, overridden to
* specify ParserNonMimeMessageProxy as the return type.
*/
public function getParent() : ParserNonMimeMessageProxy
{
return parent::getParent();
}
/**
* Returns the next part's start position within the message's raw stream,
* or null if not set, not discovered, or there are no more parts under this
* message.
*
* As this is a message-wide setting, ParserUUEncodedPartProxy calls
* getNextPartStart() on its parent (a ParserNonMimeMessageProxy, which
* stores/returns this information).
*
* @return int|null The start position or null
*/
public function getNextPartStart() : ?int
{
return $this->getParent()->getNextPartStart();
}
/**
* Returns the next part's unix file mode in a uu-encoded 'begin' line if
* one exists, or null otherwise.
*
* As this is a message-wide setting, ParserUUEncodedPartProxy calls
* getNextPartMode() on its parent (a ParserNonMimeMessageProxy, which
* stores/returns this information).
*
* @return int|null The file mode or null
*/
public function getNextPartMode() : ?int
{
return $this->getParent()->getNextPartMode();
}
/**
* Returns the next part's filename in a uu-encoded 'begin' line if one
* exists, or null otherwise.
*
* As this is a message-wide setting, ParserUUEncodedPartProxy calls
* getNextPartFilename() on its parent (a ParserNonMimeMessageProxy, which
* stores/returns this information).
*
* @return ?string The file name or null
*/
public function getNextPartFilename() : ?string
{
return $this->getParent()->getNextPartFilename();
}
/**
* Sets the next part's start position within the message's raw stream.
*
* As this is a message-wide setting, ParserUUEncodedPartProxy calls
* setNextPartStart() on its parent (a ParserNonMimeMessageProxy, which
* stores/returns this information).
*/
public function setNextPartStart(int $nextPartStart) : static
{
$this->getParent()->setNextPartStart($nextPartStart);
return $this;
}
/**
* Sets the next part's unix file mode from its 'begin' line.
*
* As this is a message-wide setting, ParserUUEncodedPartProxy calls
* setNextPartMode() on its parent (a ParserNonMimeMessageProxy, which
* stores/returns this information).
*/
public function setNextPartMode(int $nextPartMode) : static
{
$this->getParent()->setNextPartMode($nextPartMode);
return $this;
}
/**
* Sets the next part's filename from its 'begin' line.
*
* As this is a message-wide setting, ParserUUEncodedPartProxy calls
* setNextPartFilename() on its parent (a ParserNonMimeMessageProxy, which
* stores/returns this information).
*/
public function setNextPartFilename(string $nextPartFilename) : static
{
$this->getParent()->setNextPartFilename($nextPartFilename);
return $this;
}
/**
* Returns the file mode included in the uuencoded 'begin' line for this
* part.
*/
public function getUnixFileMode() : ?int
{
return $this->getHeaderContainer()->getUnixFileMode();
}
/**
* Returns the filename included in the uuencoded 'begin' line for this
* part.
*/
public function getFilename() : ?string
{
return $this->getHeaderContainer()->getFilename();
}
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Parser\Proxy;
use Psr\Log\LoggerInterface;
use ZBateson\MailMimeParser\Message\UUEncodedPart;
use ZBateson\MailMimeParser\Parser\IParserService;
use ZBateson\MailMimeParser\Parser\Part\ParserPartStreamContainerFactory;
use ZBateson\MailMimeParser\Parser\PartBuilder;
use ZBateson\MailMimeParser\Stream\StreamFactory;
/**
* Responsible for creating proxied IUUEncodedPart instances wrapped in a
* ParserUUEncodedPartProxy and used by NonMimeParser.
*
* @author Zaahid Bateson
*/
class ParserUUEncodedPartProxyFactory extends ParserPartProxyFactory
{
protected LoggerInterface $logger;
protected StreamFactory $streamFactory;
protected ParserPartStreamContainerFactory $parserPartStreamContainerFactory;
public function __construct(
LoggerInterface $logger,
StreamFactory $sdf,
ParserPartStreamContainerFactory $parserPartStreamContainerFactory
) {
$this->logger = $logger;
$this->streamFactory = $sdf;
$this->parserPartStreamContainerFactory = $parserPartStreamContainerFactory;
}
/**
* Constructs a new ParserUUEncodedPartProxy wrapping an IUUEncoded object.
*/
public function newInstance(PartBuilder $partBuilder, IParserService $parser) : ParserUUEncodedPartProxy
{
$parserProxy = new ParserUUEncodedPartProxy($partBuilder, $parser);
$streamContainer = $this->parserPartStreamContainerFactory->newInstance($parserProxy);
$part = new UUEncodedPart(
$parserProxy->getUnixFileMode(),
$parserProxy->getFileName(),
$partBuilder->getParent()->getPart(),
$this->logger,
$streamContainer
);
$parserProxy->setPart($part);
$streamContainer->setStream($this->streamFactory->newMessagePartStream($part));
$part->attach($streamContainer);
return $parserProxy;
}
}