Update imapengine dependancies too

This commit is contained in:
johnnyq
2026-08-02 01:26:40 -04:00
parent cf4446f405
commit 3e532fc792
185 changed files with 9018 additions and 4442 deletions

View File

@@ -9,7 +9,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
php: [8.5, 8.4, 8.3, 8.2, 8.1, 8.0]
php: [8.5, 8.4, 8.3, 8.2, 8.1]
stability: [prefer-stable]
name: P${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }}

View File

@@ -3,8 +3,6 @@
Testable and PSR-compliant mail mime parser alternative to PHP's imap* functions and Pear libraries for reading messages in _Internet Message Format_ [RFC 822](http://tools.ietf.org/html/rfc822) (and later revisions [RFC 2822](http://tools.ietf.org/html/rfc2822), [RFC 5322](http://tools.ietf.org/html/rfc5322)).
[![Build Status](https://github.com/zbateson/mail-mime-parser/actions/workflows/tests.yml/badge.svg)](https://github.com/zbateson/mail-mime-parser/actions/workflows/tests.yml)
[![Code Coverage](https://scrutinizer-ci.com/g/zbateson/mail-mime-parser/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/zbateson/mail-mime-parser/?branch=master)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/zbateson/mail-mime-parser/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/zbateson/mail-mime-parser/?branch=master)
[![Total Downloads](https://poser.pugx.org/zbateson/mail-mime-parser/downloads)](//packagist.org/packages/zbateson/mail-mime-parser)
[![Latest Stable Version](https://poser.pugx.org/zbateson/mail-mime-parser/v)](//packagist.org/packages/zbateson/mail-mime-parser)
@@ -28,23 +26,13 @@ A huge thank you to [all my sponsors](https://github.com/sponsors/zbateson). <3
If this project's helped you, please consider [sponsoring me](https://github.com/sponsors/zbateson).
## Php 7 Support Dropped
## New in 4.0
As of mail-mime-parser 3.0, support for php 7 has been dropped.
## New in 3.0
Most changes in 3.0 are 'backend' changes, for example switching to PHP-DI for dependency injection, and basic usage should not be affected.
The header class method 'getAllParts' includes comment parts in 3.0.
Error, validation, and logging support has been added.
For a more complete list of changes, please visit the [3.0 Upgrade Guide](https://mail-mime-parser.org/upgrade-3.0) and the [Usage Guide](https://mail-mime-parser.org/).
Version 4.0 requires PHP 8.1+ and focuses on API cleanup and improved configurability. For details, see the [4.0 Upgrade Guide](https://mail-mime-parser.org/upgrade-4.0).
## Requirements
MailMimeParser requires PHP 8.0 or newer. Tested on PHP 8.0, 8.1, 8.2, 8.3 and 8.4.
MailMimeParser requires PHP 8.1 or newer. Tested on PHP 8.1, 8.2, 8.3, 8.4 and 8.5.
## Usage
@@ -109,16 +97,29 @@ fclose($handle);
```
## Encryption and Signing Plugins
Optional companion packages add S/MIME and PGP/MIME support for decrypting,
encrypting, signing and verifying messages:
* [zbateson/mmp-crypt-smime](https://github.com/zbateson/mmp-crypt-smime) -- S/MIME via PHP's OpenSSL extension
* [zbateson/mmp-crypt-gpg](https://github.com/zbateson/mmp-crypt-gpg) -- PGP/MIME via PEAR's Crypt_GPG
Install either package and encrypted/signed messages are automatically detected
and decrypted during parsing. See the [Usage Guide](https://mail-mime-parser.org/#encryption-and-signing)
for examples of reading encrypted messages and composing signed/encrypted ones.
## Documentation
* [Usage Guide](https://mail-mime-parser.org/)
* [API Reference](https://mail-mime-parser.org/api/3.0)
* [API Reference](https://mail-mime-parser.org/api/4.0)
## Upgrade guides
* [1.x Upgrade Guide](https://mail-mime-parser.org/upgrade-1.0)
* [2.x Upgrade Guide](https://mail-mime-parser.org/upgrade-2.0)
* [3.x Upgrade Guide](https://mail-mime-parser.org/upgrade-3.0)
* [4.x Upgrade Guide](https://mail-mime-parser.org/upgrade-4.0)
## License

View File

@@ -19,17 +19,17 @@
"docs": "https://mail-mime-parser.org/#usage-guide"
},
"require": {
"php": ">=8.0",
"guzzlehttp/psr7": "^2.5",
"zbateson/mb-wrapper": "^2.0",
"zbateson/stream-decorators": "^2.1",
"php": ">=8.1",
"guzzlehttp/psr7": "^2.5 || ^3.0",
"zbateson/mb-wrapper": "^2.0 || ^3.0",
"zbateson/stream-decorators": "^2.1 || ^3.0",
"php-di/php-di": "^6.0|^7.0",
"psr/log": "^1|^2|^3"
},
"require-dev": {
"phpunit/phpunit": "^9.6",
"friendsofphp/php-cs-fixer": "*",
"phpstan/phpstan": "*",
"phpunit/phpunit": "^10.5",
"friendsofphp/php-cs-fixer": "^3.0",
"phpstan/phpstan": "^2.0",
"monolog/monolog": "^2|^3"
},
"suggest": {

View File

@@ -19,26 +19,6 @@ use Throwable;
*/
class Error
{
/**
* @var string The error message.
*/
protected string $message;
/**
* @var string The PSR log level for this error.
*/
protected string $psrLevel;
/**
* @var ErrorBag The object the error/notice occurred on.
*/
protected ErrorBag $object;
/**
* @var ?Throwable An Exception object if one happened, or null if not
*/
protected ?Throwable $exception;
/**
* @var array<string, int>
*/
@@ -55,18 +35,18 @@ class Error
/**
*
* @throws InvalidArgumentException if the passed $psrLogLevelAsErrorLevel
* @throws InvalidArgumentException if the passed $psrLevel
* is not a known PSR log level (see \Psr\Log\LogLevel)
*/
public function __construct(string $message, string $psrLogLevelAsErrorLevel, ErrorBag $object, ?Throwable $exception = null)
{
if (!isset($this->levelMap[$psrLogLevelAsErrorLevel])) {
throw new InvalidArgumentException($psrLogLevelAsErrorLevel . ' is not a known PSR Log Level');
public function __construct(
protected readonly string $message,
protected readonly string $psrLevel,
protected readonly ErrorBag $object,
protected readonly ?Throwable $exception = null
) {
if (!isset($this->levelMap[$psrLevel])) {
throw new InvalidArgumentException($psrLevel . ' is not a known PSR Log Level');
}
$this->message = $message;
$this->psrLevel = $psrLogLevelAsErrorLevel;
$this->object = $object;
$this->exception = $exception;
}
/**

View File

@@ -18,8 +18,6 @@ use Throwable;
*/
abstract class ErrorBag implements IErrorBag
{
protected LoggerInterface $logger;
/**
* @var Error[] array of Error objects belonging to this object.
*/
@@ -30,9 +28,8 @@ abstract class ErrorBag implements IErrorBag
*/
private bool $validated = false;
public function __construct(LoggerInterface $logger)
public function __construct(protected LoggerInterface $logger)
{
$this->logger = $logger;
}
/**
@@ -95,9 +92,7 @@ abstract class ErrorBag implements IErrorBag
}
return \array_values(\array_filter(
$this->errors,
function($e) use ($minPsrLevel) {
return $e->isPsrLevelGreaterOrEqualTo($minPsrLevel);
}
fn($e) => $e->isPsrLevelGreaterOrEqualTo($minPsrLevel)
));
}
@@ -109,9 +104,7 @@ abstract class ErrorBag implements IErrorBag
public function getAllErrors(bool $validate = false, string $minPsrLevel = LogLevel::ERROR) : array
{
$arr = \array_values(\array_map(
function($e) use ($validate, $minPsrLevel) {
return $e->getAllErrors($validate, $minPsrLevel);
},
fn($e) => $e->getAllErrors($validate, $minPsrLevel),
$this->getErrorBagChildren()
));
return \array_merge($this->getErrors($validate, $minPsrLevel), ...$arr);

View File

@@ -50,6 +50,21 @@ abstract class AbstractHeader extends ErrorBag implements IHeader
*/
private ?array $comments = null;
/**
* Resolves a nullable service from the global DI container, returning the
* provided instance if non-null.
*
* @template T of object
* @param T|null $service
* @param class-string<T> $class
* @return T
*/
protected static function resolveService(?object $service, string $class): object
{
/** @var T */
return $service ?? MailMimeParser::getGlobalContainer()->get($class);
}
/**
* Assigns the header's name and raw value, then calls parseHeaderValue to
* extract a parsed value.
@@ -78,9 +93,7 @@ abstract class AbstractHeader extends ErrorBag implements IHeader
*/
protected function filterAndAssignToParts() : void
{
$this->parts = \array_values(\array_filter($this->allParts, function($p) {
return !($p instanceof CommentPart);
}));
$this->parts = \array_values(\array_filter($this->allParts, fn($p) => !($p instanceof CommentPart)));
}
/**
@@ -139,6 +152,11 @@ abstract class AbstractHeader extends ErrorBag implements IHeader
return $this->rawValue;
}
public function getDecodedValue() : string
{
return \implode('', \array_map(fn(IHeaderPart $p) => $p->getValue(), $this->parts));
}
public function getName() : string
{
return $this->name;

View File

@@ -11,7 +11,6 @@ use Psr\Log\LoggerInterface;
use ZBateson\MailMimeParser\Header\Consumer\AddressBaseConsumerService;
use ZBateson\MailMimeParser\Header\Part\AddressGroupPart;
use ZBateson\MailMimeParser\Header\Part\AddressPart;
use ZBateson\MailMimeParser\MailMimeParser;
/**
* A header containing one or more email addresses and/or groups of addresses.
@@ -44,10 +43,9 @@ class AddressHeader extends AbstractHeader
?LoggerInterface $logger = null,
?AddressBaseConsumerService $consumerService = null
) {
$di = MailMimeParser::getGlobalContainer();
parent::__construct(
$logger ?? $di->get(LoggerInterface::class),
$consumerService ?? $di->get(AddressBaseConsumerService::class),
self::resolveService($logger, LoggerInterface::class),
self::resolveService($consumerService, AddressBaseConsumerService::class),
$name,
$value
);
@@ -55,7 +53,7 @@ class AddressHeader extends AbstractHeader
/**
* Filters $this->allParts into the parts required by $this->parts
* and assignes it.
* and assigns it.
*
* The AbstractHeader::filterAndAssignToParts method filters out CommentParts.
*/
@@ -109,6 +107,30 @@ class AddressHeader extends AbstractHeader
return false;
}
public function getDecodedValue() : string
{
$parts = [];
foreach ($this->parts as $part) {
if ($part instanceof AddressGroupPart) {
$addrs = \array_map(fn(AddressPart $a) => $this->formatAddress($a), $part->getAddresses());
$parts[] = $part->getName() . ': ' . \implode(', ', $addrs) . ';';
} elseif ($part instanceof AddressPart) {
$parts[] = $this->formatAddress($part);
}
}
return \implode(', ', $parts);
}
private function formatAddress(AddressPart $address) : string
{
$name = $address->getName();
$email = $address->getEmail();
if ($name !== '') {
return $name . ' <' . $email . '>';
}
return $email;
}
/**
* Returns the first email address in the header.
*

View File

@@ -25,19 +25,6 @@ use ZBateson\MailMimeParser\Header\Part\MimeToken;
*/
abstract class AbstractConsumerService implements IConsumerService
{
protected LoggerInterface $logger;
/**
* @var HeaderPartFactory used to construct IHeaderPart objects
*/
protected HeaderPartFactory $partFactory;
/**
* @var AbstractConsumerService[] array of sub-consumers used by this
* consumer if any, or an empty array if none exist.
*/
protected array $subConsumers = [];
/**
* @var ?string the generated token split pattern on first run, so it doesn't
* need to be regenerated every time.
@@ -47,11 +34,11 @@ abstract class AbstractConsumerService implements IConsumerService
/**
* @param AbstractConsumerService[] $subConsumers
*/
public function __construct(LoggerInterface $logger, HeaderPartFactory $partFactory, array $subConsumers = [])
{
$this->logger = $logger;
$this->partFactory = $partFactory;
$this->subConsumers = $subConsumers;
public function __construct(
protected readonly LoggerInterface $logger,
protected readonly HeaderPartFactory $partFactory,
protected array $subConsumers = []
) {
}
public function __invoke(string $value) : array

View File

@@ -61,7 +61,7 @@ class AddressBaseConsumerService extends AbstractConsumerService
* AddressBaseConsumerService doesn't have start/end tokens, and so always
* returns false.
*
* @return false
* @return bool
*/
protected function isEndToken(string $token) : bool
{
@@ -73,7 +73,7 @@ class AddressBaseConsumerService extends AbstractConsumerService
* returns false.
*
* @codeCoverageIgnore
* @return false
* @return bool
*/
protected function isStartToken(string $token) : bool
{

View File

@@ -9,6 +9,7 @@ namespace ZBateson\MailMimeParser\Header\Consumer;
use Psr\Log\LoggerInterface;
use Iterator;
use ZBateson\MailMimeParser\Header\IHeaderPart;
use ZBateson\MailMimeParser\Header\Part\AddressGroupPart;
use ZBateson\MailMimeParser\Header\Part\HeaderPartFactory;
@@ -78,7 +79,7 @@ class AddressGroupConsumerService extends AddressBaseConsumerService
* addresses, since a group could be empty.
*
* @param Iterator $tokens
* @return IHeaderPart[]
* @return \ZBateson\MailMimeParser\Header\IHeaderPart[]
*/
protected function parseTokensIntoParts(Iterator $tokens) : array
{

View File

@@ -39,14 +39,6 @@ use ZBateson\MailMimeParser\Header\Part\HeaderPartFactory;
*/
class GenericReceivedConsumerService extends AbstractGenericConsumerService
{
/**
* @var string the current part name being parsed.
*
* This is always the lower-case name provided to the constructor, not the
* actual string that started the consumer, which could be in any case.
*/
protected $partName;
/**
* Constructor overridden to include $partName parameter.
*
@@ -55,10 +47,9 @@ class GenericReceivedConsumerService extends AbstractGenericConsumerService
LoggerInterface $logger,
HeaderPartFactory $partFactory,
CommentConsumerService $commentConsumerService,
string $partName
protected readonly string $partName
) {
parent::__construct($logger, $partFactory, [$commentConsumerService]);
$this->partName = $partName;
}
/**

View File

@@ -12,7 +12,6 @@ use DateTimeImmutable;
use Psr\Log\LoggerInterface;
use ZBateson\MailMimeParser\Header\Consumer\DateConsumerService;
use ZBateson\MailMimeParser\Header\Part\DatePart;
use ZBateson\MailMimeParser\MailMimeParser;
/**
* Reads a DatePart value header in either RFC 2822 or RFC 822 format.
@@ -27,10 +26,9 @@ class DateHeader extends AbstractHeader
?LoggerInterface $logger = null,
?DateConsumerService $consumerService = null
) {
$di = MailMimeParser::getGlobalContainer();
parent::__construct(
$logger ?? $di->get(LoggerInterface::class),
$consumerService ?? $di->get(DateConsumerService::class),
self::resolveService($logger, LoggerInterface::class),
self::resolveService($consumerService, DateConsumerService::class),
$name,
$value
);

View File

@@ -9,7 +9,6 @@ namespace ZBateson\MailMimeParser\Header;
use Psr\Log\LoggerInterface;
use ZBateson\MailMimeParser\Header\Consumer\GenericConsumerMimeLiteralPartService;
use ZBateson\MailMimeParser\MailMimeParser;
/**
* Reads a generic header.
@@ -27,20 +26,18 @@ class GenericHeader extends AbstractHeader
?LoggerInterface $logger = null,
?GenericConsumerMimeLiteralPartService $consumerService = null
) {
$di = MailMimeParser::getGlobalContainer();
parent::__construct(
$logger ?? $di->get(LoggerInterface::class),
$consumerService ?? $di->get(DateConsumerService::class),
self::resolveService($logger, LoggerInterface::class),
self::resolveService($consumerService, GenericConsumerMimeLiteralPartService::class),
$name,
$value
);
parent::__construct($logger, $consumerService, $name, $value);
}
public function getValue() : ?string
{
if (!empty($this->parts)) {
return \implode('', \array_map(function($p) { return $p->getValue(); }, $this->parts));
return \implode('', \array_map(fn($p) => $p->getValue(), $this->parts));
}
return null;
}

View File

@@ -9,6 +9,7 @@ namespace ZBateson\MailMimeParser\Header;
use Psr\Log\LoggerInterface;
use ReflectionClass;
use ReflectionNamedType;
use ZBateson\MailMimeParser\Header\Consumer\AddressBaseConsumerService;
use ZBateson\MailMimeParser\Header\Consumer\DateConsumerService;
use ZBateson\MailMimeParser\Header\Consumer\GenericConsumerMimeLiteralPartService;
@@ -41,22 +42,15 @@ use ZBateson\MailMimeParser\Header\Part\MimeTokenPartFactory;
*/
class HeaderFactory
{
protected LoggerInterface $logger;
/**
* @var IConsumerService[] array of available consumer service classes
*/
protected array $consumerServices;
/**
* @var MimeTokenPartFactory for mime decoding.
*/
protected MimeTokenPartFactory $mimeTokenPartFactory;
/**
* @var string[][] maps IHeader types to headers.
*/
protected $types = [
protected array $types = [
AddressHeader::class => [
'from',
'to',
@@ -106,11 +100,11 @@ class HeaderFactory
* @var string Defines the generic IHeader type to use for headers that
* aren't mapped in $types
*/
protected $genericType = GenericHeader::class;
protected string $genericType = GenericHeader::class;
public function __construct(
LoggerInterface $logger,
MimeTokenPartFactory $mimeTokenPartFactory,
protected readonly LoggerInterface $logger,
protected readonly MimeTokenPartFactory $mimeTokenPartFactory,
AddressBaseConsumerService $addressBaseConsumerService,
DateConsumerService $dateConsumerService,
GenericConsumerMimeLiteralPartService $genericConsumerMimeLiteralPartService,
@@ -119,8 +113,6 @@ class HeaderFactory
ReceivedConsumerService $receivedConsumerService,
SubjectConsumerService $subjectConsumerService
) {
$this->logger = $logger;
$this->mimeTokenPartFactory = $mimeTokenPartFactory;
$this->consumerServices = [
AddressBaseConsumerService::class => $addressBaseConsumerService,
DateConsumerService::class => $dateConsumerService,
@@ -195,19 +187,23 @@ class HeaderFactory
$ref = new ReflectionClass($iHeaderClass);
$params = $ref->getConstructor()->getParameters();
if ($ref->isSubclassOf(MimeEncodedHeader::class)) {
$type = $params[4]->getType();
\assert($type instanceof ReflectionNamedType);
return new $iHeaderClass(
$name,
$value,
$this->logger,
$this->mimeTokenPartFactory,
$this->consumerServices[$params[4]->getType()->getName()]
$this->consumerServices[$type->getName()]
);
}
$type = $params[3]->getType();
\assert($type instanceof ReflectionNamedType);
return new $iHeaderClass(
$name,
$value,
$this->logger,
$this->consumerServices[$params[3]->getType()->getName()]
$this->consumerServices[$type->getName()]
);
}
}

View File

@@ -55,7 +55,7 @@ interface IHeader extends IErrorBag
* or parameter headers (Content-Type), the 'value' is the value of the
* first parsed part that isn't a comment.
*
* @return string The value
* @return ?string The value, or null if the header has no parts.
*/
public function getValue() : ?string;
@@ -66,6 +66,19 @@ interface IHeader extends IErrorBag
*/
public function getRawValue() : string;
/**
* Returns the full decoded and unfolded value of the header, reconstructed
* from the parsed parts (excluding comments).
*
* Unlike getValue() which returns only the value of the first part, this
* returns the full decoded representation. For example, for an
* AddressHeader with `=?UTF-8?Q?J=C3=B6hn?= <john@example.com>`, this
* would return `Jöhn <john@example.com>`.
*
* @return string The full decoded value.
*/
public function getDecodedValue() : string;
/**
* Returns the name of the header.
*

View File

@@ -30,7 +30,7 @@ interface IHeaderPart extends IErrorBag, Stringable
/**
* Returns any CommentParts under this part container.
*
* @return CommentPart[]
* @return Part\CommentPart[]
*/
public function getComments() : array;
}

View File

@@ -11,7 +11,6 @@ use Psr\Log\LoggerInterface;
use ZBateson\MailMimeParser\Header\Consumer\IdBaseConsumerService;
use ZBateson\MailMimeParser\Header\Part\CommentPart;
use ZBateson\MailMimeParser\Header\Part\MimeTokenPartFactory;
use ZBateson\MailMimeParser\MailMimeParser;
/**
* Represents a Content-ID, Message-ID, In-Reply-To or References header.
@@ -31,11 +30,10 @@ class IdHeader extends MimeEncodedHeader
?MimeTokenPartFactory $mimeTokenPartFactory = null,
?IdBaseConsumerService $consumerService = null
) {
$di = MailMimeParser::getGlobalContainer();
parent::__construct(
$logger ?? $di->get(LoggerInterface::class),
$mimeTokenPartFactory ?? $di->get(MimeTokenPartFactory::class),
$consumerService ?? $di->get(IdBaseConsumerService::class),
self::resolveService($logger, LoggerInterface::class),
self::resolveService($mimeTokenPartFactory, MimeTokenPartFactory::class),
self::resolveService($consumerService, IdBaseConsumerService::class),
$name,
$value
);
@@ -60,12 +58,8 @@ class IdHeader extends MimeEncodedHeader
public function getIds() : array
{
return \array_values(\array_map(
function($p) {
return $p->getValue();
},
\array_filter($this->parts, function($p) {
return !($p instanceof CommentPart);
})
fn($p) => $p->getValue(),
\array_filter($this->parts, fn($p) => !($p instanceof CommentPart))
));
}
}

View File

@@ -21,24 +21,18 @@ use ZBateson\MailMimeParser\Header\Part\MimeTokenPartFactory;
abstract class MimeEncodedHeader extends AbstractHeader
{
/**
* @var MimeTokenPartFactory for mime decoding.
*/
protected MimeTokenPartFactory $mimeTokenPartFactory;
/**
* @var MimeLiteralPart[] the mime encoded parsed parts contained in this
* @var IHeaderPart[] the mime encoded parsed parts contained in this
* header
*/
protected $mimeEncodedParsedParts = [];
protected array $mimeEncodedParsedParts = [];
public function __construct(
LoggerInterface $logger,
MimeTokenPartFactory $mimeTokenPartFactory,
protected readonly MimeTokenPartFactory $mimeTokenPartFactory,
IConsumerService $consumerService,
string $name,
string $value
) {
$this->mimeTokenPartFactory = $mimeTokenPartFactory;
parent::__construct($logger, $consumerService, $name, $value);
}
@@ -52,7 +46,7 @@ abstract class MimeEncodedHeader extends AbstractHeader
// whitespace between parts, etc...
$matchp = '~(' . MimeToken::MIME_PART_PATTERN . ')~';
$aMimeParts = \preg_split($matchp, $value, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
$this->mimeEncodedParsedParts = \array_map([$this->mimeTokenPartFactory, 'newInstance'], $aMimeParts);
$this->mimeEncodedParsedParts = \array_map($this->mimeTokenPartFactory->newInstance(...), $aMimeParts);
parent::parseHeaderValue(
$consumer,
\implode('', \array_map(fn ($part) => $part->getValue(), $this->mimeEncodedParsedParts))
@@ -61,6 +55,6 @@ abstract class MimeEncodedHeader extends AbstractHeader
protected function getErrorBagChildren() : array
{
return \array_values(\array_filter(\array_merge($this->getAllParts(), $this->mimeEncodedParsedParts)));
return \array_values(\array_merge($this->getAllParts(), $this->mimeEncodedParsedParts));
}
}

View File

@@ -11,7 +11,6 @@ use Psr\Log\LoggerInterface;
use ZBateson\MailMimeParser\Header\Consumer\IConsumerService;
use ZBateson\MailMimeParser\Header\Consumer\ParameterConsumerService;
use ZBateson\MailMimeParser\Header\Part\NameValuePart;
use ZBateson\MailMimeParser\MailMimeParser;
/**
* Represents a header containing an optional main value part and subsequent
@@ -36,8 +35,8 @@ use ZBateson\MailMimeParser\MailMimeParser;
class ParameterHeader extends AbstractHeader
{
/**
* @var ParameterPart[] key map of lower-case parameter names and associated
* ParameterParts.
* @var array<string, NameValuePart> key map of lower-case parameter names and associated
* NameValueParts.
*/
protected array $parameters = [];
@@ -47,10 +46,9 @@ class ParameterHeader extends AbstractHeader
?LoggerInterface $logger = null,
?ParameterConsumerService $consumerService = null
) {
$di = MailMimeParser::getGlobalContainer();
parent::__construct(
$logger ?? $di->get(LoggerInterface::class),
$consumerService ?? $di->get(ParameterConsumerService::class),
self::resolveService($logger, LoggerInterface::class),
self::resolveService($consumerService, ParameterConsumerService::class),
$name,
$value
);
@@ -70,6 +68,17 @@ class ParameterHeader extends AbstractHeader
}
}
public function getDecodedValue() : string
{
$value = $this->getValue() ?? '';
foreach ($this->parameters as $param) {
if ($param->getName() !== '') {
$value .= '; ' . $param->getName() . '=' . $param->getValue();
}
}
return $value;
}
/**
* Returns true if a parameter exists with the passed name.
*

View File

@@ -9,6 +9,7 @@ namespace ZBateson\MailMimeParser\Header\Part;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use ZBateson\MailMimeParser\Header\IHeaderPart;
use ZBateson\MbWrapper\MbWrapper;
/**
@@ -27,8 +28,8 @@ class AddressGroupPart extends NameValuePart
* Creates an AddressGroupPart out of the passed array of AddressParts/
* AddressGroupParts and name.
*
* @param HeaderPart[] $nameParts
* @param AddressPart[]|AddressGroupPart[] $addressesAndGroupParts
* @param IHeaderPart[] $nameParts
* @param IHeaderPart[] $addressesAndGroupParts
*/
public function __construct(
LoggerInterface $logger,
@@ -76,7 +77,7 @@ class AddressGroupPart extends NameValuePart
protected function validate() : void
{
if ($this->name === null || \mb_strlen($this->name) === 0) {
if (\mb_strlen($this->name) === 0) {
$this->addError('Address group doesn\'t have a name', LogLevel::ERROR);
}
if (empty($this->addresses)) {

View File

@@ -8,6 +8,7 @@
namespace ZBateson\MailMimeParser\Header\Part;
use Psr\Log\LoggerInterface;
use ZBateson\MailMimeParser\Header\IHeaderPart;
use ZBateson\MbWrapper\MbWrapper;
/**
@@ -28,6 +29,9 @@ class CommentPart extends ContainerPart
*/
protected string $comment;
/**
* @param array<IHeaderPart> $children
*/
public function __construct(
LoggerInterface $logger,
MbWrapper $charsetConverter,

View File

@@ -9,6 +9,7 @@ namespace ZBateson\MailMimeParser\Header\Part;
use Psr\Log\LoggerInterface;
use ZBateson\MailMimeParser\ErrorBag;
use ZBateson\MailMimeParser\Header\IHeaderPart;
use ZBateson\MbWrapper\MbWrapper;
/**
@@ -27,11 +28,14 @@ use ZBateson\MbWrapper\MbWrapper;
class ContainerPart extends HeaderPart
{
/**
* @var HeaderPart[] parts that were used to create this part, collected for
* @var IHeaderPart[] parts that were used to create this part, collected for
* proper error reporting and validation.
*/
protected $children = [];
protected array $children = [];
/**
* @param array<IHeaderPart> $children
*/
public function __construct(
LoggerInterface $logger,
MbWrapper $charsetConverter,
@@ -54,8 +58,8 @@ class ContainerPart extends HeaderPart
* Spaces are removed if parts on either side of it have their
* canIgnoreSpaceAfter/canIgnoreSpaceBefore properties set to true.
*
* @param HeaderPart[] $parts
* @return HeaderPart[]
* @param IHeaderPart[] $parts
* @return IHeaderPart[]
*/
protected function filterIgnoredSpaces(array $parts) : array
{
@@ -91,7 +95,7 @@ class ContainerPart extends HeaderPart
* The default implementation filters out ignorable whitespace between
* parts, and concatenates parts calling 'getValue'.
*
* @param HeaderParts[] $parts
* @param IHeaderPart[] $parts
*/
protected function getValueFromParts(array $parts) : string
{
@@ -119,10 +123,11 @@ class ContainerPart extends HeaderPart
/**
* Returns this part's children, same as getChildParts().
*
* @return ErrorBag
* @return array<ErrorBag>
*/
protected function getErrorBagChildren() : array
{
/** @var array<ErrorBag> */
return $this->children;
}
}

View File

@@ -11,6 +11,7 @@ use DateTime;
use Exception;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use ZBateson\MailMimeParser\Header\IHeaderPart;
use ZBateson\MbWrapper\MbWrapper;
/**
@@ -31,7 +32,7 @@ class DatePart extends ContainerPart
* an RFC 822 date, and failing that, tries to parse it by calling
* new DateTime($value).
*
* @param HeaderPart[] $children
* @param IHeaderPart[] $children
*/
public function __construct(
LoggerInterface $logger,

View File

@@ -21,19 +21,6 @@ use ZBateson\MbWrapper\UnsupportedCharsetException;
*/
abstract class HeaderPart extends ErrorBag implements IHeaderPart
{
/**
* @var string the representative value of the part after any conversion or
* processing has been done on it (e.g. removing new lines, converting,
* whatever else).
*/
protected string $value;
/**
* @var MbWrapper $charsetConverter the charset converter used for
* converting strings in HeaderPart::convertEncoding
*/
protected MbWrapper $charsetConverter;
/**
* @var bool set to true to ignore spaces before this part
*/
@@ -49,11 +36,12 @@ abstract class HeaderPart extends ErrorBag implements IHeaderPart
*/
protected bool $isSpace = false;
public function __construct(LoggerInterface $logger, MbWrapper $charsetConverter, string $value)
{
public function __construct(
LoggerInterface $logger,
protected MbWrapper $charsetConverter,
protected string $value
) {
parent::__construct($logger);
$this->charsetConverter = $charsetConverter;
$this->value = $value;
}
/**
@@ -101,6 +89,9 @@ abstract class HeaderPart extends ErrorBag implements IHeaderPart
return $str;
}
/**
* @return CommentPart[]
*/
public function getComments() : array
{
return [];

View File

@@ -18,18 +18,10 @@ use ZBateson\MbWrapper\MbWrapper;
*/
class HeaderPartFactory
{
/**
* @var MbWrapper $charsetConverter passed to IHeaderPart constructors
* for converting strings in IHeaderPart::convertEncoding
*/
protected MbWrapper $charsetConverter;
protected LoggerInterface $logger;
public function __construct(LoggerInterface $logger, MbWrapper $charsetConverter)
{
$this->logger = $logger;
$this->charsetConverter = $charsetConverter;
public function __construct(
protected readonly LoggerInterface $logger,
protected readonly MbWrapper $charsetConverter
) {
}
/**
@@ -70,7 +62,7 @@ class HeaderPartFactory
/**
* Initializes and returns a new ContainerPart.
*
* @param HeaderPart[] $children
* @param IHeaderPart[] $children
*/
public function newContainerPart(array $children) : ContainerPart
{
@@ -90,7 +82,7 @@ class HeaderPartFactory
/**
* Initializes and returns a new QuotedLiteralPart.
*
* @param HeaderPart[] $parts
* @param IHeaderPart[] $parts
*/
public function newQuotedLiteralPart(array $parts) : QuotedLiteralPart
{
@@ -100,7 +92,7 @@ class HeaderPartFactory
/**
* Initializes and returns a new CommentPart.
*
* @param HeaderPart[] $children
* @param IHeaderPart[] $children
*/
public function newCommentPart(array $children) : CommentPart
{
@@ -110,8 +102,8 @@ class HeaderPartFactory
/**
* Initializes and returns a new AddressPart.
*
* @param HeaderPart[] $nameParts
* @param HeaderPart[] $emailParts
* @param IHeaderPart[] $nameParts
* @param IHeaderPart[] $emailParts
*/
public function newAddress(array $nameParts, array $emailParts) : AddressPart
{
@@ -121,8 +113,8 @@ class HeaderPartFactory
/**
* Initializes and returns a new AddressGroupPart
*
* @param HeaderPart[] $nameParts
* @param AddressPart[]|AddressGroupPart[] $addressesAndGroups
* @param IHeaderPart[] $nameParts
* @param IHeaderPart[] $addressesAndGroups
*/
public function newAddressGroupPart(array $nameParts, array $addressesAndGroups) : AddressGroupPart
{
@@ -132,7 +124,7 @@ class HeaderPartFactory
/**
* Initializes and returns a new DatePart
*
* @param HeaderPart[] $children
* @param IHeaderPart[] $children
*/
public function newDatePart(array $children) : DatePart
{
@@ -142,7 +134,7 @@ class HeaderPartFactory
/**
* Initializes and returns a new ParameterPart.
*
* @param HeaderPart[] $nameParts
* @param IHeaderPart[] $nameParts
*/
public function newParameterPart(array $nameParts, ContainerPart $valuePart) : ParameterPart
{
@@ -152,7 +144,7 @@ class HeaderPartFactory
/**
* Initializes and returns a new ReceivedPart.
*
* @param HeaderPart[] $children
* @param IHeaderPart[] $children
*/
public function newReceivedPart(string $name, array $children) : ReceivedPart
{
@@ -162,7 +154,7 @@ class HeaderPartFactory
/**
* Initializes and returns a new ReceivedDomainPart.
*
* @param HeaderPart[] $children
* @param IHeaderPart[] $children
*/
public function newReceivedDomainPart(string $name, array $children) : ReceivedDomainPart
{

View File

@@ -81,10 +81,7 @@ class MimeToken extends Token
}
$this->charset = $matches[1];
$this->language = (!empty($matches[2])) ? $matches[2] : null;
if ($this->charset !== null) {
return $this->convertEncoding($body, $this->charset, true);
}
return $this->convertEncoding($body, 'ISO-8859-1', true);
return $this->convertEncoding($body, $this->charset, true);
}
/**

View File

@@ -10,6 +10,7 @@ namespace ZBateson\MailMimeParser\Header\Part;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use ZBateson\MailMimeParser\ErrorBag;
use ZBateson\MailMimeParser\Header\IHeaderPart;
use ZBateson\MbWrapper\MbWrapper;
/**
@@ -24,6 +25,10 @@ class NameValuePart extends ContainerPart
*/
protected string $name;
/**
* @param array<IHeaderPart> $nameParts
* @param array<IHeaderPart> $valueParts
*/
public function __construct(
LoggerInterface $logger,
MbWrapper $charsetConverter,
@@ -41,7 +46,7 @@ class NameValuePart extends ContainerPart
* Creates the string 'name' representation of this part constructed from
* the child name parts passed to it.
*
* @param HeaderParts[] $parts
* @param IHeaderPart[] $parts
*/
protected function getNameFromParts(array $parts) : string
{

View File

@@ -8,6 +8,7 @@
namespace ZBateson\MailMimeParser\Header\Part;
use Psr\Log\LoggerInterface;
use ZBateson\MailMimeParser\Header\IHeaderPart;
use ZBateson\MbWrapper\MbWrapper;
/**
@@ -40,7 +41,7 @@ class ParameterPart extends NameValuePart
protected bool $encoded = false;
/**
* @param HeaderPart[] $nameParts
* @param IHeaderPart[] $nameParts
*/
public function __construct(
LoggerInterface $logger,

View File

@@ -8,6 +8,7 @@
namespace ZBateson\MailMimeParser\Header\Part;
use Psr\Log\LoggerInterface;
use ZBateson\MailMimeParser\Header\IHeaderPart;
use ZBateson\MbWrapper\MbWrapper;
/**
@@ -48,7 +49,7 @@ class ReceivedDomainPart extends ReceivedPart
protected ?string $address = null;
/**
* @param HeaderPart[] $children
* @param IHeaderPart[] $children
*/
public function __construct(
LoggerInterface $logger,

View File

@@ -8,6 +8,7 @@
namespace ZBateson\MailMimeParser\Header\Part;
use Psr\Log\LoggerInterface;
use ZBateson\MailMimeParser\Header\IHeaderPart;
use ZBateson\MbWrapper\MbWrapper;
/**
@@ -23,7 +24,7 @@ use ZBateson\MbWrapper\MbWrapper;
class ReceivedPart extends NameValuePart
{
/**
* @param HeaderPart[] $children
* @param IHeaderPart[] $children
*/
public function __construct(
LoggerInterface $logger,

View File

@@ -47,6 +47,9 @@ class SplitParameterPart extends ParameterPart
return $parts[0]->getName();
}
/**
* @return array<MimeToken|Token>
*/
private function getMimeTokens(string $value) : array
{
$pattern = MimeToken::MIME_PART_PATTERN;
@@ -60,6 +63,10 @@ class SplitParameterPart extends ParameterPart
);
}
/**
* @param array<ParameterPart> $parts
* @return array<ParameterPart|MimeToken|Token>
*/
private function combineAdjacentUnencodedParts(array $parts) : array
{
$runningValue = '';
@@ -81,6 +88,9 @@ class SplitParameterPart extends ParameterPart
return $returnedParts;
}
/**
* @param array<ParameterPart> $parts
*/
protected function getValueFromParts(array $parts) : string
{
$sorted = $parts;

View File

@@ -11,7 +11,7 @@ use DateTime;
use Psr\Log\LoggerInterface;
use ZBateson\MailMimeParser\Header\Consumer\ReceivedConsumerService;
use ZBateson\MailMimeParser\Header\Part\DatePart;
use ZBateson\MailMimeParser\MailMimeParser;
use ZBateson\MailMimeParser\Header\Part\ReceivedDomainPart;
/**
* Represents a Received header.
@@ -80,6 +80,12 @@ use ZBateson\MailMimeParser\MailMimeParser;
*/
class ReceivedHeader extends ParameterHeader
{
/**
* @var array<string, ReceivedDomainPart> key map of lower-case parameter names and associated
* ReceivedDomainParts.
*/
protected array $parameters = [];
/**
* @var DateTime the date/time stamp in the header.
*/
@@ -96,10 +102,9 @@ class ReceivedHeader extends ParameterHeader
?LoggerInterface $logger = null,
?ReceivedConsumerService $consumerService = null
) {
$di = MailMimeParser::getGlobalContainer();
AbstractHeader::__construct(
$logger ?? $di->get(LoggerInterface::class),
$consumerService ?? $di->get(ReceivedConsumerService::class),
self::resolveService($logger, LoggerInterface::class),
self::resolveService($consumerService, ReceivedConsumerService::class),
$name,
$value
);

View File

@@ -9,7 +9,6 @@ namespace ZBateson\MailMimeParser\Header;
use Psr\Log\LoggerInterface;
use ZBateson\MailMimeParser\Header\Consumer\SubjectConsumerService;
use ZBateson\MailMimeParser\MailMimeParser;
/**
* Reads a subject header.
@@ -27,10 +26,9 @@ class SubjectHeader extends AbstractHeader
?LoggerInterface $logger = null,
?SubjectConsumerService $consumerService = null
) {
$di = MailMimeParser::getGlobalContainer();
parent::__construct(
$logger ?? $di->get(LoggerInterface::class),
$consumerService ?? $di->get(SubjectConsumerService::class),
self::resolveService($logger, LoggerInterface::class),
self::resolveService($consumerService, SubjectConsumerService::class),
$name,
$value
);

View File

@@ -66,7 +66,7 @@ interface IMessage extends IMimePart
* returned.
*
* @see IMessage::getHtmlStream() to get the html content stream directly.
* @see IMessage::getHtmlStream() to get the html content in a string.
* @see IMessage::getHtmlContent() to get the html content in a string.
* @see IMessage::getTextPart() to get the text part(s).
* @see IMessage::getTextPartCount() to get a count of text parts.
* @see IMessage::getHtmlPartCount() to get a count of html parts.
@@ -191,15 +191,12 @@ interface IMessage extends IMimePart
* Removes the text/plain part of the message at the passed index if one
* exists (defaults to first part if an index isn't passed).
*
* Returns true if a part exists at the passed index and has been removed.
*
* @see IMessage::setTextPart() to set the text part
* @see IMessage::removeHtmlPart() to remove an html part
* @see IMessage::removeAllTextParts() to remove all text parts
* @param int $index Optional 0-based index of inline text part to remove.
* @return bool true on success
*/
public function removeTextPart(int $index = 0) : bool;
public function removeTextPart(int $index = 0) : static;
/**
* Removes all text/plain inline parts in this message.
@@ -225,23 +222,19 @@ interface IMessage extends IMimePart
* @see IMessage::removeAllHtmlParts() to remove all html parts
* @param bool $moveRelatedPartsBelowMessage Optionally pass false to remove
* related parts.
* @return bool true on success
*/
public function removeAllTextParts(bool $moveRelatedPartsBelowMessage = true) : bool;
public function removeAllTextParts(bool $moveRelatedPartsBelowMessage = true) : static;
/**
* Removes the text/html part of the message at the passed index if one
* exists (defaults to first part if an index isn't passed).
*
* Returns true if a part exists at the passed index and has been removed.
*
* @see IMessage::setHtmlPart() to set the html part
* @see IMessage::removeTextPart() to remove a text part
* @see IMessage::removeAllHtmlParts() to remove all html parts
* @param int $index Optional 0-based index of inline html part to remove.
* @return bool true on success
*/
public function removeHtmlPart(int $index = 0) : bool;
public function removeHtmlPart(int $index = 0) : static;
/**
* Removes all text/html inline parts in this message.
@@ -264,12 +257,11 @@ interface IMessage extends IMimePart
*
* @see IMessage::setHtmlPart() to set the html part
* @see IMessage::removeHtmlPart() to remove an html part
* @see IMessage::removeAllTextParts() to remove all html parts
* @see IMessage::removeAllTextParts() to remove all text parts
* @param bool $moveRelatedPartsBelowMessage Optionally pass false to remove
* related parts.
* @return bool true on success
*/
public function removeAllHtmlParts(bool $moveRelatedPartsBelowMessage = true) : bool;
public function removeAllHtmlParts(bool $moveRelatedPartsBelowMessage = true) : static;
/**
* Returns the attachment part at the given 0-based index, or null if none

View File

@@ -1,4 +1,5 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
@@ -7,6 +8,7 @@
namespace ZBateson\MailMimeParser;
use Composer\InstalledVersions;
use DI\Container;
use DI\ContainerBuilder;
use DI\Definition\Source\DefinitionSource;
@@ -61,6 +63,12 @@ class MailMimeParser
*/
private const DEFAULT_DEFINITIONS_FILE = __DIR__ . '/di_config.php';
/**
* The key in a package's composer.json "extra" section that MMP looks
* for to auto-discover plugin DI configurations.
*/
private const PLUGIN_EXTRA_KEY = 'mail-mime-parser';
/**
* @var Container The instance's dependency injection container.
*/
@@ -77,13 +85,15 @@ class MailMimeParser
private static ?Container $globalContainer = null;
/**
* @var array<array|string|DefinitionSource> an array of global definitions
* @var array<array<string, mixed>|string|DefinitionSource> an array of global definitions
* being used.
*/
private static array $globalDefinitions = [self::DEFAULT_DEFINITIONS_FILE];
/**
* Returns the default ContainerBuilder with default loaded definitions.
*
* @return ContainerBuilder<Container>
*/
private static function getGlobalContainerBuilder() : ContainerBuilder
{
@@ -91,15 +101,77 @@ class MailMimeParser
foreach (self::$globalDefinitions as $def) {
$builder->addDefinitions($def);
}
foreach (self::discoverPluginConfigs() as $configFile) {
$builder->addDefinitions($configFile);
}
return $builder;
}
/**
* Discovers plugin DI config files from installed Composer packages.
*
* Uses Composer's InstalledVersions runtime API to enumerate installed
* packages and reads each package's composer.json for the
* "extra.mail-mime-parser.di_config" entry.
*
* Returns an empty array in non-Composer environments.
*
* @return string[] Absolute paths to discovered config files
*/
private static function discoverPluginConfigs() : array
{
if (!\class_exists(InstalledVersions::class)) {
return [];
}
$configs = [];
foreach (InstalledVersions::getInstalledPackages() as $packageName) {
$installPath = InstalledVersions::getInstallPath($packageName);
if ($installPath === null) {
continue;
}
$configFile = self::readPluginConfigPath($installPath);
if ($configFile !== null) {
$configs[] = $configFile;
}
}
return $configs;
}
/**
* Reads a package's composer.json and returns the absolute path to its
* MMP DI config file, or null if the package is not an MMP plugin.
*
* Looks for "extra.mail-mime-parser.di_config" in the package's
* composer.json.
*/
public static function readPluginConfigPath(string $packageInstallPath) : ?string
{
$composerJsonPath = $packageInstallPath . '/composer.json';
if (!\file_exists($composerJsonPath)) {
return null;
}
$data = \json_decode(\file_get_contents($composerJsonPath), true);
if (!\is_array($data)) {
return null;
}
$extra = $data['extra'][self::PLUGIN_EXTRA_KEY] ?? null;
if (!\is_array($extra) || !isset($extra['di_config'])) {
return null;
}
$configFile = \realpath($packageInstallPath . '/' . $extra['di_config']);
if ($configFile === false || !\file_exists($configFile)) {
return null;
}
return $configFile;
}
/**
* Sets global configuration for php-di. Overrides all previously set
* definitions. You can optionally not use the default MMP definitions file
* by passing 'false' to the $useDefaultDefinitionsFile argument.
*
* @var array<array|string|DefinitionSource> array of definitions
* @param array<array<string, mixed>|string|DefinitionSource> $phpDiConfigs array of definitions
*/
public static function setGlobalPhpDiConfigurations(array $phpDiConfigs, bool $useDefaultDefinitionsFile = true) : void
{
@@ -110,6 +182,9 @@ class MailMimeParser
self::$globalContainer = null;
}
/**
* @param array<string, mixed>|string|DefinitionSource $phpDiConfig
*/
public static function addGlobalPhpDiContainerDefinition(array|string|DefinitionSource $phpDiConfig) : void
{
self::$globalDefinitions[] = $phpDiConfig;
@@ -135,6 +210,19 @@ class MailMimeParser
return self::$globalContainer;
}
/**
* Sets the fallback charset used for text/* content parts that don't
* declare a charset. Defaults to 'ISO-8859-1' per RFC 2045.
*
* Many modern messages omit the charset and are actually UTF-8, so you
* may want to set this to 'UTF-8'.
*/
public static function setFallbackCharset(string $charset) : void
{
self::$globalDefinitions[] = ['defaultFallbackCharset' => $charset];
self::$globalContainer = null;
}
/**
* Registers the provided logger globally.
*/
@@ -162,6 +250,8 @@ class MailMimeParser
* @see MailMimeParser::setGlobalPhpDiConfiguration() to register
* configuration globally.
* @see MailMimeParser::setGlobalLogger() to set a global logger
*
* @param array<string, mixed>|string|DefinitionSource|null $phpDiContainerConfig
*/
public function __construct(
?LoggerInterface $logger = null,
@@ -194,22 +284,21 @@ class MailMimeParser
*
* If the passed $resource is a resource handle or StreamInterface, the
* resource must remain open while the returned IMessage object exists.
* Pass true as the second argument to have the resource attached to the
* IMessage and closed for you when it's destroyed, or pass false to
* manually close it if it should remain open after the IMessage object is
* destroyed.
* Pass true as the second argument to have the resource automatically
* closed when the returned IMessage is destroyed, or pass false to
* manage the resource lifecycle yourself.
*
* @param resource|StreamInterface|string $resource The resource handle to
* the input stream of the mime message, or a string containing a
* mime message.
* @param bool $attached pass true to have it attached to the returned
* IMessage and destroyed with it.
* @param bool $autoClose pass true to have the resource closed
* automatically when the returned IMessage is destroyed.
*/
public function parse(mixed $resource, bool $attached) : IMessage
public function parse(mixed $resource, bool $autoClose) : IMessage
{
$stream = Utils::streamFor(
$resource,
['metadata' => ['mmp-detached-stream' => ($attached !== true)]]
['metadata' => ['mmp-detached-stream' => ($autoClose !== true)]]
);
if (!$stream->isSeekable()) {
$stream = new CachingStream($stream);

View File

@@ -47,14 +47,16 @@ class Message extends MimePart implements IMessage
?PartHeaderContainer $headerContainer = null,
?PartChildrenContainer $partChildrenContainer = null,
?MultipartHelper $multipartHelper = null,
?PrivacyHelper $privacyHelper = null
?PrivacyHelper $privacyHelper = null,
string $defaultFallbackCharset = 'ISO-8859-1'
) {
parent::__construct(
null,
$logger,
$streamContainer,
$headerContainer,
$partChildrenContainer
$partChildrenContainer,
$defaultFallbackCharset
);
$di = MailMimeParser::getGlobalContainer();
$this->multipartHelper = $multipartHelper ?? $di->get(MultipartHelper::class);
@@ -67,24 +69,23 @@ class Message extends MimePart implements IMessage
*
* If the passed $resource is a resource handle or StreamInterface, the
* resource must remain open while the returned IMessage object exists.
* Pass true as the second argument to have the resource attached to the
* IMessage and closed for you when it's destroyed, or pass false to
* manually close it if it should remain open after the IMessage object is
* destroyed.
* Pass true as the second argument to have the resource automatically
* closed when the returned IMessage is destroyed, or pass false to
* manage the resource lifecycle yourself.
*
* @param resource|StreamInterface|string $resource The resource handle to
* the input stream of the mime message, or a string containing a
* mime message.
* @param bool $attached pass true to have it attached to the returned
* IMessage and destroyed with it.
* @param bool $autoClose pass true to have the resource closed
* automatically when the returned IMessage is destroyed.
*/
public static function from(mixed $resource, bool $attached) : IMessage
public static function from(mixed $resource, bool $autoClose) : IMessage
{
static $mmp = null;
if ($mmp === null) {
$mmp = new MailMimeParser();
}
return $mmp->parse($resource, $attached);
return $mmp->parse($resource, $autoClose);
}
/**
@@ -196,44 +197,48 @@ class Message extends MimePart implements IMessage
return $this;
}
public function removeTextPart(int $index = 0) : bool
public function removeTextPart(int $index = 0) : static
{
return $this->multipartHelper
$this->multipartHelper
->removePartByMimeType(
$this,
'text/plain',
$index
);
return $this;
}
public function removeAllTextParts(bool $moveRelatedPartsBelowMessage = true) : bool
public function removeAllTextParts(bool $moveRelatedPartsBelowMessage = true) : static
{
return $this->multipartHelper
$this->multipartHelper
->removeAllContentPartsByMimeType(
$this,
'text/plain',
$moveRelatedPartsBelowMessage
);
return $this;
}
public function removeHtmlPart(int $index = 0) : bool
public function removeHtmlPart(int $index = 0) : static
{
return $this->multipartHelper
$this->multipartHelper
->removePartByMimeType(
$this,
'text/html',
$index
);
return $this;
}
public function removeAllHtmlParts(bool $moveRelatedPartsBelowMessage = true) : bool
public function removeAllHtmlParts(bool $moveRelatedPartsBelowMessage = true) : static
{
return $this->multipartHelper
$this->multipartHelper
->removeAllContentPartsByMimeType(
$this,
'text/html',
$moveRelatedPartsBelowMessage
);
return $this;
}
public function getAttachmentPart(int $index) : ?IMessagePart
@@ -332,11 +337,12 @@ class Message extends MimePart implements IMessage
{
$params = '';
if (!empty($this->getMessageId())) {
$params .= ', message-id=' . $this->getContentId();
$params .= ', message-id=' . $this->getMessageId();
}
$params .= ', content-type=' . $this->getContentType();
$nsClass = static::class;
$class = \substr($nsClass, (\strrpos($nsClass, '\\') ?? -1) + 1);
$pos = \strrpos($nsClass, '\\');
$class = ($pos !== false) ? \substr($nsClass, $pos + 1) : $nsClass;
return $class . '(' . \spl_object_id($this) . $params . ')';
}
}

View File

@@ -19,20 +19,12 @@ use ZBateson\MailMimeParser\Stream\StreamFactory;
*/
abstract class IMessagePartFactory
{
protected LoggerInterface $logger;
protected StreamFactory $streamFactory;
protected PartStreamContainerFactory $partStreamContainerFactory;
public function __construct(
LoggerInterface $logger,
StreamFactory $streamFactory,
PartStreamContainerFactory $partStreamContainerFactory
protected readonly LoggerInterface $logger,
protected readonly StreamFactory $streamFactory,
protected readonly PartStreamContainerFactory $partStreamContainerFactory,
protected readonly string $defaultFallbackCharset = 'ISO-8859-1'
) {
$this->logger = $logger;
$this->streamFactory = $streamFactory;
$this->partStreamContainerFactory = $partStreamContainerFactory;
}
/**

View File

@@ -19,20 +19,15 @@ use ZBateson\MailMimeParser\Stream\StreamFactory;
*/
class IMimePartFactory extends IMessagePartFactory
{
protected PartHeaderContainerFactory $partHeaderContainerFactory;
protected PartChildrenContainerFactory $partChildrenContainerFactory;
public function __construct(
LoggerInterface $logger,
StreamFactory $streamFactory,
PartStreamContainerFactory $partStreamContainerFactory,
PartHeaderContainerFactory $partHeaderContainerFactory,
PartChildrenContainerFactory $partChildrenContainerFactory
protected readonly PartHeaderContainerFactory $partHeaderContainerFactory,
protected readonly PartChildrenContainerFactory $partChildrenContainerFactory,
string $defaultFallbackCharset = 'ISO-8859-1'
) {
parent::__construct($logger, $streamFactory, $partStreamContainerFactory);
$this->partHeaderContainerFactory = $partHeaderContainerFactory;
$this->partChildrenContainerFactory = $partChildrenContainerFactory;
parent::__construct($logger, $streamFactory, $partStreamContainerFactory, $defaultFallbackCharset);
}
/**
@@ -47,7 +42,8 @@ class IMimePartFactory extends IMessagePartFactory
$this->logger,
$streamContainer,
$headerContainer,
$this->partChildrenContainerFactory->newInstance()
$this->partChildrenContainerFactory->newInstance(),
$this->defaultFallbackCharset
);
$streamContainer->setStream($this->streamFactory->newMessagePartStream($part));
return $part;

View File

@@ -29,7 +29,8 @@ class IUUEncodedPartFactory extends IMessagePartFactory
null,
$parent,
$this->logger,
$streamContainer
$streamContainer,
$this->defaultFallbackCharset
);
$streamContainer->setStream($this->streamFactory->newMessagePartStream($part));
return $part;

View File

@@ -18,21 +18,10 @@ use ZBateson\MailMimeParser\Message\PartHeaderContainer;
*/
class PartHeaderContainerFactory
{
protected LoggerInterface $logger;
/**
* @var HeaderFactory the HeaderFactory passed to HeaderContainer instances.
*/
protected HeaderFactory $headerFactory;
/**
* Constructor
*
*/
public function __construct(LoggerInterface $logger, HeaderFactory $headerFactory)
{
$this->logger = $logger;
$this->headerFactory = $headerFactory;
public function __construct(
protected readonly LoggerInterface $logger,
protected readonly HeaderFactory $headerFactory
) {
}
/**

View File

@@ -19,24 +19,12 @@ use ZBateson\MbWrapper\MbWrapper;
*/
class PartStreamContainerFactory
{
protected LoggerInterface $logger;
protected StreamFactory $streamFactory;
protected MbWrapper $mbWrapper;
protected bool $throwExceptionReadingPartContentFromUnsupportedCharsets;
public function __construct(
LoggerInterface $logger,
StreamFactory $streamFactory,
MbWrapper $mbWrapper,
bool $throwExceptionReadingPartContentFromUnsupportedCharsets
protected readonly LoggerInterface $logger,
protected readonly StreamFactory $streamFactory,
protected readonly MbWrapper $mbWrapper,
protected readonly bool $throwExceptionReadingPartContentFromUnsupportedCharsets
) {
$this->logger = $logger;
$this->streamFactory = $streamFactory;
$this->mbWrapper = $mbWrapper;
$this->throwExceptionReadingPartContentFromUnsupportedCharsets = $throwExceptionReadingPartContentFromUnsupportedCharsets;
}
public function newInstance() : PartStreamContainer

View File

@@ -17,21 +17,9 @@ use ZBateson\MailMimeParser\Message\Factory\IUUEncodedPartFactory;
*/
abstract class AbstractHelper
{
/**
* @var IMimePartFactory to create parts for attachments/content
*/
protected IMimePartFactory $mimePartFactory;
/**
* @var IUUEncodedPartFactory to create parts for attachments
*/
protected IUUEncodedPartFactory $uuEncodedPartFactory;
public function __construct(
IMimePartFactory $mimePartFactory,
IUUEncodedPartFactory $uuEncodedPartFactory
protected readonly IMimePartFactory $mimePartFactory,
protected readonly IUUEncodedPartFactory $uuEncodedPartFactory
) {
$this->mimePartFactory = $mimePartFactory;
$this->uuEncodedPartFactory = $uuEncodedPartFactory;
}
}

View File

@@ -11,6 +11,7 @@ use ZBateson\MailMimeParser\Header\HeaderConsts;
use ZBateson\MailMimeParser\Header\IHeader;
use ZBateson\MailMimeParser\IMessage;
use ZBateson\MailMimeParser\MailMimeParser;
use ZBateson\MailMimeParser\Message\IMessagePart;
use ZBateson\MailMimeParser\Message\IMimePart;
/**
@@ -31,6 +32,9 @@ class GenericHelper extends AbstractHelper
* one defined in the static $nonMimeContentFields
*
*/
/**
* @param string[] $exceptions
*/
private function isMimeContentField(IHeader $header, array $exceptions = []) : bool
{
return (\stripos($header->getName(), 'Content') === 0
@@ -145,15 +149,20 @@ class GenericHelper extends AbstractHelper
* replaced, and instead $replacement's type headers are copied to $message,
* and any children below $replacement are added directly below $message.
*/
public function replacePart(IMessage $message, IMimePart $part, IMimePart $replacement) : static
public function replacePart(IMessage $message, IMessagePart $part, IMessagePart $replacement) : static
{
$position = $message->removePart($replacement);
$replacementParent = $replacement->getParent();
$position = ($replacementParent !== null)
? \array_search($replacement, $replacementParent->getChildParts(), true)
: false;
$message->removePart($replacement);
if ($part === $message) {
\assert($replacement instanceof IMimePart);
$this->movePartContentAndChildren($replacement, $message);
return $this;
}
$parent = $part->getParent();
$parent->addChild($replacement, $position);
$parent->addChild($replacement, $position !== false ? $position : null);
$parent->removePart($part);
return $this;

View File

@@ -25,18 +25,12 @@ use ZBateson\MailMimeParser\Message\PartFilter;
*/
class MultipartHelper extends AbstractHelper
{
/**
* @var GenericHelper a GenericHelper instance
*/
private GenericHelper $genericHelper;
public function __construct(
IMimePartFactory $mimePartFactory,
IUUEncodedPartFactory $uuEncodedPartFactory,
GenericHelper $genericHelper
private readonly GenericHelper $genericHelper
) {
parent::__construct($mimePartFactory, $uuEncodedPartFactory);
$this->genericHelper = $genericHelper;
}
/**
@@ -117,9 +111,9 @@ class MultipartHelper extends AbstractHelper
* @param string $mimeType the content-type to find below $alternativePart
* @param IMimePart $alternativePart The multipart/alternative part to look
* under
* @return bool|IMimePart false if a part is not found
* @return IMimePart|false false if a part is not found
*/
public function getContentPartContainerFromAlternative($mimeType, IMimePart $alternativePart) : bool|IMimePart
public function getContentPartContainerFromAlternative($mimeType, IMimePart $alternativePart) : IMimePart|false
{
$part = $alternativePart->getPart(0, PartFilter::fromInlineContentType($mimeType));
$contPart = null;
@@ -130,6 +124,7 @@ class MultipartHelper extends AbstractHelper
$contPart = $part;
$part = $part->getParent();
} while ($part !== $alternativePart);
\assert($contPart instanceof IMimePart);
return $contPart;
}
@@ -156,7 +151,7 @@ class MultipartHelper extends AbstractHelper
$alternativePart = $message->getPart(0, PartFilter::fromInlineContentType('multipart/alternative'));
}
$message->removePart($rmPart);
if ($alternativePart !== null && $alternativePart instanceof IMultiPart) {
if ($alternativePart instanceof IMultiPart) {
if ($alternativePart->getChildCount() === 1) {
$this->genericHelper->replacePart($message, $alternativePart, $alternativePart->getChild(0));
} elseif ($alternativePart->getChildCount() === 0) {
@@ -250,15 +245,15 @@ class MultipartHelper extends AbstractHelper
* If the passed $mimeType is text/plain, searches for a text/html part.
* Otherwise searches for a text/plain part to return.
*
* @return IMimePart or null if not found
* @return IMessagePart or null if not found
*/
public function findOtherContentPartFor(IMessage $message, string $mimeType) : ?IMimePart
public function findOtherContentPartFor(IMessage $message, string $mimeType) : ?IMessagePart
{
$altPart = $message->getPart(
0,
PartFilter::fromInlineContentType(($mimeType === 'text/plain') ? 'text/html' : 'text/plain')
);
if ($altPart !== null && $altPart->getParent() !== null && $altPart->getParent()->isMultiPart()) {
if ($altPart?->getParent()?->isMultiPart()) {
$altPartParent = $altPart->getParent();
if ($altPartParent->getChildCount(PartFilter::fromDisposition('inline')) !== 1) {
$altPart = $this->createMultipartRelatedPartForInlineChildrenOf($altPartParent);
@@ -345,6 +340,7 @@ class MultipartHelper extends AbstractHelper
{
$alt = $message->getPart(0, PartFilter::fromInlineContentType('multipart/alternative'));
if ($alt !== null) {
\assert($alt instanceof IMimePart);
return $this->removeAllContentPartsFromAlternative($message, $mimeType, $alt, $keepOtherContent);
}
$message->removeAllParts(PartFilter::fromInlineContentType($mimeType));
@@ -361,14 +357,14 @@ class MultipartHelper extends AbstractHelper
{
$parts = $message->getAllParts(PartFilter::fromInlineContentType($mimeType));
$alt = $message->getPart(0, PartFilter::fromInlineContentType('multipart/alternative'));
if ($parts === null || !isset($parts[$index])) {
if (!isset($parts[$index])) {
return false;
} elseif (\count($parts) === 1) {
return $this->removeAllContentPartsByMimeType($message, $mimeType, true);
}
$part = $parts[$index];
$message->removePart($part);
if ($alt !== null && $alt->getChildCount() === 1) {
if ($alt?->getChildCount() === 1) {
$this->genericHelper->replacePart($message, $alt, $alt->getChild(0));
}
return true;

View File

@@ -21,25 +21,13 @@ use ZBateson\MailMimeParser\Message\IMessagePart;
*/
class PrivacyHelper extends AbstractHelper
{
/**
* @var GenericHelper a GenericHelper instance
*/
private GenericHelper $genericHelper;
/**
* @var MultipartHelper a MultipartHelper instance
*/
private MultipartHelper $multipartHelper;
public function __construct(
IMimePartFactory $mimePartFactory,
IUUEncodedPartFactory $uuEncodedPartFactory,
GenericHelper $genericHelper,
MultipartHelper $multipartHelper
private readonly GenericHelper $genericHelper,
private readonly MultipartHelper $multipartHelper
) {
parent::__construct($mimePartFactory, $uuEncodedPartFactory);
$this->genericHelper = $genericHelper;
$this->multipartHelper = $multipartHelper;
}
/**
@@ -99,9 +87,7 @@ class PrivacyHelper extends AbstractHelper
*/
public function overwrite8bitContentEncoding(IMessage $message) : static
{
$parts = $message->getAllParts(function(IMessagePart $part) {
return \strcasecmp($part->getContentTransferEncoding(), '8bit') === 0;
});
$parts = $message->getAllParts(fn(IMessagePart $part) => \strcasecmp($part->getContentTransferEncoding(), '8bit') === 0);
foreach ($parts as $part) {
$contentType = \strtolower($part->getContentType());
$part->setRawHeader(

View File

@@ -8,7 +8,6 @@
namespace ZBateson\MailMimeParser\Message;
use Psr\Http\Message\StreamInterface;
use SplSubject;
use ZBateson\MailMimeParser\IErrorBag;
use ZBateson\MailMimeParser\MailMimeParser;
@@ -19,12 +18,9 @@ use ZBateson\MailMimeParser\MailMimeParser;
* contain content, have a parent, and identify the type of content (e.g.
* mime-type or charset) agnostically.
*
* The interface extends SplSubject -- any modifications to a message must
* notify any attached observers.
*
* @author Zaahid Bateson
*/
interface IMessagePart extends IErrorBag, SplSubject
interface IMessagePart extends IErrorBag
{
/**
* Returns this part's parent.
@@ -47,10 +43,10 @@ interface IMessagePart extends IErrorBag, SplSubject
* Returns the mime type of the content, or $default if one is not set.
*
* @param string $default Optional override for the default return value of
* 'text/plain.
* 'text/plain'.
* @return string the mime type
*/
public function getContentType(string $default = 'text/plain') : ?string;
public function getContentType(string $default = 'text/plain') : string;
/**
* Returns the charset of the content, or null if not applicable/defined.
@@ -127,7 +123,7 @@ interface IMessagePart extends IErrorBag, SplSubject
* doesn't have a content section.
*
* To get a stream without charset conversion if you know the part's content
* contains a binary stream, call {@see self::getBinaryContentStream()}
* contains a binary stream, call {@see IMessagePart::getBinaryContentStream()}
* instead.
*
* The library automatically handles decoding and charset conversion (to the
@@ -282,7 +278,7 @@ interface IMessagePart extends IErrorBag, SplSubject
*
* If the part has not been modified and represents a parsed part, the
* original stream should be returned. Otherwise a stream representation of
* the part including its modifications should be returned. This insures
* the part including its modifications should be returned. This ensures
* that an unmodified, signed message could be passed on that way even after
* parsing and reading.
*
@@ -299,7 +295,7 @@ interface IMessagePart extends IErrorBag, SplSubject
* resource handle.
* @see IMessagePart::__toString() to write the part to a string and return
* it.
* @see IMessage::save() to write the part to a file, resource handle or
* @see IMessagePart::save() to write the part to a file, resource handle or
* Psr7 stream.
* @return resource the resource handle containing the part.
*/
@@ -311,7 +307,7 @@ interface IMessagePart extends IErrorBag, SplSubject
*
* If the part has not been modified and represents a parsed part, the
* original stream should be returned. Otherwise a stream representation of
* the part including its modifications should be returned. This insures
* the part including its modifications should be returned. This ensures
* that an unmodified, signed message could be passed on that way even after
* parsing and reading.
*
@@ -327,7 +323,7 @@ interface IMessagePart extends IErrorBag, SplSubject
* @see IMessagePart::getResourceHandle() to get a resource handle.
* @see IMessagePart::__toString() to write the part to a string and return
* it.
* @see IMessage::save() to write the part to a file, resource handle or
* @see IMessagePart::save() to write the part to a file, resource handle or
* Psr7 stream.
* @return StreamInterface the stream containing the part.
*/
@@ -340,7 +336,7 @@ interface IMessagePart extends IErrorBag, SplSubject
* If the part has not been modified and represents a parsed part, the
* original stream should be written to the file. Otherwise a stream
* representation of the part including its modifications should be written.
* This insures that an unmodified, signed message could be passed on this
* This ensures that an unmodified, signed message could be passed on this
* way even after parsing and reading.
*
* The written stream is not guaranteed to be RFC822 (or greater) compliant
@@ -360,7 +356,7 @@ interface IMessagePart extends IErrorBag, SplSubject
*
* @see IMessagePart::getResourceHandle() to get a resource handle.
* @see IMessagePart::__toString() to get the part in a string.
* @see IMessage::save() to write the part to a file, resource handle or
* @see IMessagePart::save() to write the part to a file, resource handle or
* Psr7 stream.
* @param string|resource|StreamInterface $filenameResourceOrStream the
* file, resource, or stream to write to.
@@ -379,7 +375,7 @@ interface IMessagePart extends IErrorBag, SplSubject
* @see IMessagePart::getStream() to get a Psr7 StreamInterface instead of a
* string.
* @see IMessagePart::getResourceHandle() to get a resource handle.
* @see IMessage::save() to write the part to a file, resource handle or
* @see IMessagePart::save() to write the part to a file, resource handle or
* Psr7 stream.
*/
public function __toString() : string;

View File

@@ -36,7 +36,8 @@ interface IMimePart extends IMultiPart
* same name, the one at the passed offset is returned.
*
* Note that mime header names aren't case sensitive, and the '-' character
* is ignored, so ret
* is ignored, so 'Content-Type', 'content-type', and 'ContentType' are
* equivalent.
*
* If a header with the given $name and $offset doesn't exist, null is
* returned.
@@ -58,7 +59,10 @@ interface IMimePart extends IMultiPart
* @param string $name The name of the header to retrieve.
* @param int $offset Optional offset if there are multiple headers with the
* given name.
* @return ?IHeader the header object if it exists, or null if not
* @return ?IHeader The concrete header type depends on the header name
* (AddressHeader, DateHeader, GenericHeader, IdHeader,
* ParameterHeader, ReceivedHeader, or SubjectHeader). See
* {@see HeaderFactory} for the mapping.
*/
public function getHeader(string $name, int $offset = 0) : ?IHeader;
@@ -70,6 +74,8 @@ interface IMimePart extends IMultiPart
* same name, the one at the passed offset is returned.
*
* Note that mime headers aren't case sensitive, and the '-' character is
* ignored, so 'Content-Type', 'content-type', and 'ContentType' are
* equivalent.
*
* If a header with the given $name and $offset doesn't exist, null is
* returned.

View File

@@ -184,7 +184,7 @@ interface IMultiPart extends IMessagePart
* this part.
* @see IMultiPart::getAllParts() to get an array of all parts with an
* optional filter.
* @return RecursiveIterator<IMessagePart>
* @return RecursiveIterator<int, IMessagePart>
*/
public function getChildIterator() : RecursiveIterator;
@@ -250,18 +250,16 @@ interface IMultiPart extends IMessagePart
* If the $position parameter is non-null, adds the part at the passed
* position index, otherwise adds it as the last child.
*
* @param MessagePart $part The part to add.
* @param IMessagePart $part The part to add.
* @param int $position Optional insertion position 0-based index.
*/
public function addChild(MessagePart $part, ?int $position = null) : static;
public function addChild(IMessagePart $part, ?int $position = null) : static;
/**
* Removes the child part from this part and returns its previous position
* or null if it wasn't found.
* Removes the child part from this part.
*
* Note that if the part is not a direct child of this part, the returned
* position is its index within its parent (calls removePart on its direct
* parent).
* Note that if the part is not a direct child of this part, it is removed
* from its direct parent instead.
*
* This also means that parts from unrelated parts/messages could be removed
* by a call to removePart -- it will always remove the part from its parent
@@ -269,10 +267,8 @@ interface IMultiPart extends IMessagePart
* ```php $part->getParent()->removePart(); ```.
*
* @param IMessagePart $part The part to remove
* @return int|null The previous index position of the part within its old
* parent.
*/
public function removePart(IMessagePart $part) : ?int;
public function removePart(IMessagePart $part) : static;
/**
* Removes all parts below the current part. If a callable filter is

View File

@@ -13,27 +13,17 @@ use Psr\Http\Message\StreamInterface;
use Psr\Log\LoggerInterface;
use SplObjectStorage;
use SplObserver;
use SplSubject;
use ZBateson\MailMimeParser\ErrorBag;
use ZBateson\MailMimeParser\MailMimeParser;
use ZBateson\MailMimeParser\Stream\MessagePartStreamDecorator;
/**
* Most basic representation of a single part of an email.
*
* @author Zaahid Bateson
*/
abstract class MessagePart extends ErrorBag implements IMessagePart
abstract class MessagePart extends ErrorBag implements IMessagePart, SplSubject
{
/**
* @var ?IMimePart parent part
*/
protected ?IMimePart $parent;
/**
* @var PartStreamContainer holds 'stream' and 'content stream'.
*/
protected PartStreamContainer $partStreamContainer;
/**
* @var ?string can be used to set an override for content's charset in cases
* where a user knows the charset on the content is not what it claims
@@ -49,19 +39,17 @@ abstract class MessagePart extends ErrorBag implements IMessagePart
protected bool $ignoreTransferEncoding = false;
/**
* @var SplObjectStorage attached observers that need to be notified of
* @var SplObjectStorage<SplObserver, null> attached observers that need to be notified of
* modifications to this part.
*/
protected SplObjectStorage $observers;
public function __construct(
LoggerInterface $logger,
PartStreamContainer $streamContainer,
?IMimePart $parent = null
protected readonly PartStreamContainer $partStreamContainer,
protected ?IMimePart $parent = null
) {
parent::__construct($logger);
$this->partStreamContainer = $streamContainer;
$this->parent = $parent;
$this->observers = new SplObjectStorage();
}
@@ -108,7 +96,7 @@ abstract class MessagePart extends ErrorBag implements IMessagePart
return $this;
}
public function getContentStream(string $charset = MailMimeParser::DEFAULT_CHARSET) : ?MessagePartStreamDecorator
public function getContentStream(string $charset = MailMimeParser::DEFAULT_CHARSET) : ?StreamInterface
{
if ($this->hasContent()) {
$tr = ($this->ignoreTransferEncoding) ? '' : $this->getContentTransferEncoding();
@@ -123,7 +111,7 @@ abstract class MessagePart extends ErrorBag implements IMessagePart
return null;
}
public function getBinaryContentStream() : ?MessagePartStreamDecorator
public function getBinaryContentStream() : ?StreamInterface
{
if ($this->hasContent()) {
$tr = ($this->ignoreTransferEncoding) ? '' : $this->getContentTransferEncoding();
@@ -188,10 +176,10 @@ abstract class MessagePart extends ErrorBag implements IMessagePart
return $this;
}
public function setContent($resource, string $charset = MailMimeParser::DEFAULT_CHARSET) : static
public function setContent($resource, string $resourceCharset = MailMimeParser::DEFAULT_CHARSET) : static
{
$stream = Utils::streamFor($resource);
$this->attachContentStream($stream, $charset);
$this->attachContentStream($stream, $resourceCharset);
// this->notify() called in attachContentStream
return $this;
}
@@ -240,7 +228,8 @@ abstract class MessagePart extends ErrorBag implements IMessagePart
}
$params .= ', content-type=' . $this->getContentType();
$nsClass = static::class;
$class = \substr($nsClass, (\strrpos($nsClass, '\\') ?? -1) + 1);
$pos = \strrpos($nsClass, '\\');
$class = ($pos !== false) ? \substr($nsClass, $pos + 1) : $nsClass;
return $class . '(' . \spl_object_id($this) . $params . ')';
}

View File

@@ -29,12 +29,18 @@ class MimePart extends MultiPart implements IMimePart
*/
protected PartHeaderContainer $headerContainer;
/**
* @var string Fallback charset for text parts without a declared charset.
*/
protected string $fallbackCharset;
public function __construct(
?IMimePart $parent = null,
?LoggerInterface $logger = null,
?PartStreamContainer $streamContainer = null,
?PartHeaderContainer $headerContainer = null,
?PartChildrenContainer $partChildrenContainer = null
?PartChildrenContainer $partChildrenContainer = null,
string $defaultFallbackCharset = 'ISO-8859-1'
) {
$di = MailMimeParser::getGlobalContainer();
parent::__construct(
@@ -44,6 +50,7 @@ class MimePart extends MultiPart implements IMimePart
$parent
);
$this->headerContainer = $headerContainer ?? $di->get(PartHeaderContainer::class);
$this->fallbackCharset = $defaultFallbackCharset;
}
/**
@@ -115,7 +122,7 @@ class MimePart extends MultiPart implements IMimePart
* than text/plain if needed.
* @return string the mime type
*/
public function getContentType(string $default = 'text/plain') : ?string
public function getContentType(string $default = 'text/plain') : string
{
return \strtolower($this->getHeaderValue(HeaderConsts::CONTENT_TYPE, $default));
}
@@ -137,7 +144,7 @@ class MimePart extends MultiPart implements IMimePart
if ($charset === null || \strcasecmp($charset, 'binary') === 0) {
$contentType = $this->getContentType();
if ($contentType === 'text/plain' || $contentType === 'text/html') {
return 'ISO-8859-1';
return $this->fallbackCharset;
}
return null;
}

View File

@@ -21,19 +21,13 @@ use RecursiveIteratorIterator;
*/
abstract class MultiPart extends MessagePart implements IMultiPart
{
/**
* @var PartChildrenContainer child part container
*/
protected PartChildrenContainer $partChildrenContainer;
public function __construct(
LoggerInterface $logger,
PartStreamContainer $streamContainer,
PartChildrenContainer $partChildrenContainer,
protected readonly PartChildrenContainer $partChildrenContainer,
?IMimePart $parent = null
) {
parent::__construct($logger, $streamContainer, $parent);
$this->partChildrenContainer = $partChildrenContainer;
}
private function getAllPartsIterator() : AppendIterator
@@ -90,6 +84,9 @@ abstract class MultiPart extends MessagePart implements IMultiPart
);
}
/**
* @return RecursiveIterator<int, IMessagePart>
*/
public function getChildIterator() : RecursiveIterator
{
return $this->partChildrenContainer;
@@ -133,9 +130,10 @@ abstract class MultiPart extends MessagePart implements IMultiPart
});
}
public function addChild(MessagePart $part, ?int $position = null) : static
public function addChild(IMessagePart $part, ?int $position = null) : static
{
if ($part !== $this) {
\assert($part instanceof MessagePart);
$part->parent = $this;
$this->partChildrenContainer->add($part, $position);
$this->notify();
@@ -143,18 +141,19 @@ abstract class MultiPart extends MessagePart implements IMultiPart
return $this;
}
public function removePart(IMessagePart $part) : ?int
public function removePart(IMessagePart $part) : static
{
$parent = $part->getParent();
if ($this !== $parent && $parent !== null) {
return $parent->removePart($part);
$parent->removePart($part);
return $this;
}
$position = $this->partChildrenContainer->remove($part);
if ($position !== null) {
$this->notify();
}
return $position;
return $this;
}
public function removeAllParts(?callable $fnFilter = null) : int

View File

@@ -7,6 +7,8 @@
namespace ZBateson\MailMimeParser\Message;
use Psr\Log\LoggerInterface;
/**
* Represents part of a non-mime message.
*
@@ -14,6 +16,17 @@ namespace ZBateson\MailMimeParser\Message;
*/
abstract class NonMimePart extends MessagePart
{
protected string $fallbackCharset = 'ISO-8859-1';
public function __construct(
LoggerInterface $logger,
PartStreamContainer $partStreamContainer,
?IMimePart $parent = null,
string $defaultFallbackCharset = 'ISO-8859-1'
) {
parent::__construct($logger, $partStreamContainer, $parent);
$this->fallbackCharset = $defaultFallbackCharset;
}
/**
* Returns true.
*
@@ -26,17 +39,17 @@ abstract class NonMimePart extends MessagePart
/**
* Returns text/plain
*/
public function getContentType(string $default = 'text/plain') : ?string
public function getContentType(string $default = 'text/plain') : string
{
return $default;
}
/**
* Returns ISO-8859-1
* Returns the configured fallback charset (ISO-8859-1 by default).
*/
public function getCharset() : ?string
{
return 'ISO-8859-1';
return $this->fallbackCharset;
}
/**

View File

@@ -14,27 +14,23 @@ use RecursiveIterator;
/**
* Container of IMessagePart items for a parent IMultiPart.
*
* @implements ArrayAccess<int, IMessagePart>
* @implements RecursiveIterator<int, IMessagePart>
*
* @author Zaahid Bateson
*/
class PartChildrenContainer implements ArrayAccess, RecursiveIterator
{
/**
* @var IMessagePart[] array of child parts of the IMultiPart object that is
* holding this container.
*/
protected array $children;
/**
* @var int current key position within $children for iteration.
*/
protected $position = 0;
protected int $position = 0;
/**
* @param IMessagePart[] $children
*/
public function __construct(array $children = [])
public function __construct(protected array $children = [])
{
$this->children = $children;
}
/**
@@ -50,7 +46,7 @@ class PartChildrenContainer implements ArrayAccess, RecursiveIterator
* If the current element points to an IMultiPart, its child iterator is
* returned by calling {@see IMultiPart::getChildIterator()}.
*
* @return RecursiveIterator<IMessagePart>|null the iterator
* @return RecursiveIterator<int, IMessagePart>|null the iterator
*/
public function getChildren() : ?RecursiveIterator
{
@@ -98,7 +94,7 @@ class PartChildrenContainer implements ArrayAccess, RecursiveIterator
* @param int $position An optional index position (0-based) to add the
* child at.
*/
public function add(IMessagePart $part, $position = null) : static
public function add(IMessagePart $part, ?int $position = null) : static
{
if ($position === null || $position >= \count($this->children)) {
$this->children[] = $part;
@@ -147,7 +143,7 @@ class PartChildrenContainer implements ArrayAccess, RecursiveIterator
}
/**
* @param int $offset
* @param int|null $offset
* @param IMessagePart $value
*/
public function offsetSet(mixed $offset, mixed $value) : void
@@ -167,7 +163,7 @@ class PartChildrenContainer implements ArrayAccess, RecursiveIterator
/**
* @param int $offset
*/
public function offsetUnset($offset) : void
public function offsetUnset(mixed $offset) : void
{
\array_splice($this->children, $offset, 1);
if ($this->position >= $offset) {

View File

@@ -13,8 +13,12 @@ namespace ZBateson\MailMimeParser\Message;
*
* @author Zaahid Bateson
*/
abstract class PartFilter
final class PartFilter
{
private function __construct()
{
}
/**
* Provides an 'attachment' filter used by Message::getAttachmentPart.
*
@@ -70,9 +74,7 @@ abstract class PartFilter
*/
public static function fromContentType(string $mimeType) : callable
{
return function(IMessagePart $part) use ($mimeType) {
return \strcasecmp($part->getContentType() ?: '', $mimeType) === 0;
};
return fn(IMessagePart $part) => \strcasecmp($part->getContentType(), $mimeType) === 0;
}
/**
@@ -85,7 +87,7 @@ abstract class PartFilter
{
return function(IMessagePart $part) use ($mimeType) {
$disp = $part->getContentDisposition();
return (\strcasecmp($part->getContentType() ?: '', $mimeType) === 0) && ($disp === null
return (\strcasecmp($part->getContentType(), $mimeType) === 0) && ($disp === null
|| \strcasecmp($disp, 'attachment') !== 0);
};
}

View File

@@ -18,6 +18,8 @@ use ZBateson\MailMimeParser\Header\IHeader;
/**
* Maintains a collection of headers for a part.
*
* @implements IteratorAggregate<int, array<string>>
*
* @author Zaahid Bateson
*/
class PartHeaderContainer extends ErrorBag implements IteratorAggregate
@@ -25,36 +27,36 @@ class PartHeaderContainer extends ErrorBag implements IteratorAggregate
/**
* @var HeaderFactory the HeaderFactory object used for created headers
*/
protected $headerFactory;
protected HeaderFactory $headerFactory;
/**
* @var string[][] Each element in the array is an array with its first
* element set to the header's name, and the second its value.
*/
private $headers = [];
private array $headers = [];
/**
* @var \ZBateson\MailMimeParser\Header\IHeader[] Each element is an IHeader
* @var array<int, ?IHeader> Each element is an IHeader
* representing the header at the same index in the $headers array. If
* an IHeader has not been constructed for the header at that index,
* the element would be set to null.
*/
private $headerObjects = [];
private array $headerObjects = [];
/**
* @var array Maps header names by their "normalized" (lower-cased,
* @var array<string, int[]> Maps header names by their "normalized" (lower-cased,
* non-alphanumeric characters stripped) name to an array of indexes in
* the $headers array. For example:
* $headerMap['contenttype'] = [ 1, 4 ]
* would indicate that the headers in $headers[1] and $headers[4] are
* both headers with the name 'Content-Type' or 'contENTtype'.
*/
private $headerMap = [];
private array $headerMap = [];
/**
* @var int the next index to use for $headers and $headerObjects.
*/
private $nextIndex = 0;
private int $nextIndex = 0;
/**
* Pass a PartHeaderContainer as the second parameter. This is useful when
@@ -103,10 +105,7 @@ class PartHeaderContainer extends ErrorBag implements IteratorAggregate
{
$s = $this->headerFactory->getNormalizedHeaderName($name);
if (isset($this->headerMap[$s])) {
$self = $this;
$filtered = \array_filter($this->headerMap[$s], function($h) use ($name, $self) {
return (\strcasecmp($self->headers[$h][0], $name) === 0);
});
$filtered = \array_filter($this->headerMap[$s], fn($h) => \strcasecmp($this->headers[$h][0], $name) === 0);
return (!empty($filtered)) ? $filtered : $this->headerMap[$s];
}
return null;
@@ -157,10 +156,7 @@ class PartHeaderContainer extends ErrorBag implements IteratorAggregate
{
$a = $this->getAllWithOriginalHeaderNameIfSet($name);
if (!empty($a)) {
$self = $this;
return \array_map(function($index) use ($self) {
return $self->getByIndex($index);
}, $a);
return \array_map(fn($index) => $this->getByIndex($index), $a);
}
return [];
}
@@ -284,7 +280,7 @@ class PartHeaderContainer extends ErrorBag implements IteratorAggregate
*/
public function getHeaderObjects() : array
{
return \array_filter(\array_map([$this, 'getByIndex'], \array_keys($this->headers)));
return \array_filter(\array_map($this->getByIndex(...), \array_keys($this->headers)));
}
/**
@@ -312,7 +308,7 @@ class PartHeaderContainer extends ErrorBag implements IteratorAggregate
*
* [ 'Header-Name', 'Header Value' ]
*
* return Traversable<array<string>>
* @return Traversable<int, array<string>>
*/
public function getIterator() : Traversable
{

View File

@@ -12,7 +12,6 @@ use Psr\Http\Message\StreamInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use ZBateson\MailMimeParser\ErrorBag;
use ZBateson\MailMimeParser\Stream\MessagePartStreamDecorator;
use ZBateson\MailMimeParser\Stream\StreamFactory;
use ZBateson\MbWrapper\MbWrapper;
use ZBateson\MbWrapper\UnsupportedCharsetException;
@@ -35,29 +34,10 @@ use ZBateson\MbWrapper\UnsupportedCharsetException;
class PartStreamContainer extends ErrorBag
{
/**
* @var MbWrapper to test charsets and see if they're supported.
* @var StreamInterface stream containing the part's headers, content and
* children
*/
protected MbWrapper $mbWrapper;
/**
* @var bool if false, reading from a content stream with an unsupported
* charset will be tried with the default charset, otherwise the stream
* created with the unsupported charset, and an exception will be
* thrown when read from.
*/
protected bool $throwExceptionReadingPartContentFromUnsupportedCharsets;
/**
* @var StreamFactory used to apply psr7 stream decorators to the
* attached StreamInterface based on encoding.
*/
protected StreamFactory $streamFactory;
/**
* @var MessagePartStreamDecorator stream containing the part's headers,
* content and children wrapped in a MessagePartStreamDecorator
*/
protected MessagePartStreamDecorator $stream;
protected StreamInterface $stream;
/**
* @var StreamInterface a stream containing this part's content
@@ -82,7 +62,7 @@ class PartStreamContainer extends ErrorBag
protected bool $detachParsedStream = false;
/**
* @var array<string, null> map of the active encoding filter on the current handle.
* @var array<string, ?string> map of the active encoding filter on the current handle.
*/
private array $encoding = [
'type' => null,
@@ -90,7 +70,7 @@ class PartStreamContainer extends ErrorBag
];
/**
* @var array<string, null> map of the active charset filter on the current handle.
* @var array<string, ?string> map of the active charset filter on the current handle.
*/
private array $charset = [
'from' => null,
@@ -100,21 +80,18 @@ class PartStreamContainer extends ErrorBag
public function __construct(
LoggerInterface $logger,
StreamFactory $streamFactory,
MbWrapper $mbWrapper,
bool $throwExceptionReadingPartContentFromUnsupportedCharsets
protected readonly StreamFactory $streamFactory,
protected readonly MbWrapper $mbWrapper,
protected readonly bool $throwExceptionReadingPartContentFromUnsupportedCharsets
) {
parent::__construct($logger);
$this->streamFactory = $streamFactory;
$this->mbWrapper = $mbWrapper;
$this->throwExceptionReadingPartContentFromUnsupportedCharsets = $throwExceptionReadingPartContentFromUnsupportedCharsets;
}
/**
* Sets the part's stream containing the part's headers, content, and
* children.
*/
public function setStream(MessagePartStreamDecorator $stream) : static
public function setStream(StreamInterface $stream) : static
{
$this->stream = $stream;
return $this;
@@ -124,7 +101,7 @@ class PartStreamContainer extends ErrorBag
* Returns the part's stream containing the part's headers, content, and
* children.
*/
public function getStream() : MessagePartStreamDecorator
public function getStream() : StreamInterface
{
// error out if called before setStream, getStream should never return
// null.
@@ -285,7 +262,7 @@ class PartStreamContainer extends ErrorBag
?string $transferEncoding,
?string $fromCharset,
?string $toCharset
) : ?MessagePartStreamDecorator {
) : ?StreamInterface {
if ($this->contentStream === null) {
return null;
}
@@ -314,7 +291,7 @@ class PartStreamContainer extends ErrorBag
* Checks what transfer-encoding decoder stream is attached on the
* underlying stream, and resets it if the requested arguments differ.
*/
public function getBinaryContentStream(IMessagePart $part, ?string $transferEncoding = null) : ?MessagePartStreamDecorator
public function getBinaryContentStream(IMessagePart $part, ?string $transferEncoding = null) : ?StreamInterface
{
if ($this->contentStream === null) {
return null;

View File

@@ -17,31 +17,21 @@ use ZBateson\MailMimeParser\MailMimeParser;
*/
class UUEncodedPart extends NonMimePart implements IUUEncodedPart
{
/**
* @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;
public function __construct(
?int $mode = null,
?string $filename = null,
protected ?int $mode = null,
protected ?string $filename = null,
?IMimePart $parent = null,
?LoggerInterface $logger = null,
?PartStreamContainer $streamContainer = null
?PartStreamContainer $streamContainer = null,
string $defaultFallbackCharset = 'ISO-8859-1'
) {
$di = MailMimeParser::getGlobalContainer();
parent::__construct(
$logger ?? $di->get(LoggerInterface::class),
$streamContainer ?? $di->get(PartStreamContainer::class),
$parent
$parent,
$defaultFallbackCharset
);
$this->mode = $mode;
$this->filename = $filename;
}
/**
@@ -77,7 +67,7 @@ class UUEncodedPart extends NonMimePart implements IUUEncodedPart
/**
* Returns 'application/octet-stream'.
*/
public function getContentType(string $default = 'application/octet-stream') : ?string
public function getContentType(string $default = 'application/octet-stream') : string
{
return 'application/octet-stream';
}

View File

@@ -21,26 +21,6 @@ use ZBateson\MailMimeParser\Parser\Proxy\ParserPartProxyFactory;
*/
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.
@@ -48,13 +28,10 @@ abstract class AbstractParserService implements IParserService
protected ParserManagerService $parserManager;
public function __construct(
ParserPartProxyFactory $parserMessageProxyFactory,
ParserPartProxyFactory $parserPartProxyFactory,
PartBuilderFactory $partBuilderFactory
protected readonly ParserPartProxyFactory $parserMessageProxyFactory,
protected readonly ParserPartProxyFactory $parserPartProxyFactory,
protected readonly PartBuilderFactory $partBuilderFactory
) {
$this->parserMessageProxyFactory = $parserMessageProxyFactory;
$this->parserPartProxyFactory = $parserPartProxyFactory;
$this->partBuilderFactory = $partBuilderFactory;
}
public function setParserManager(ParserManagerService $pm) : static

View File

@@ -17,14 +17,10 @@ use ZBateson\MailMimeParser\Message\PartHeaderContainer;
*/
class HeaderParserService
{
private int $maxHeaderCount;
private int $maxHeaderSizeBytes;
public function __construct(int $maxHeaderCount = 1000, int $maxHeaderSizeBytes = 1048576)
{
$this->maxHeaderCount = $maxHeaderCount;
$this->maxHeaderSizeBytes = $maxHeaderSizeBytes;
public function __construct(
private readonly int $maxHeaderCount = 1000,
private readonly int $maxHeaderSizeBytes = 1048576
) {
}
/**

View File

@@ -19,39 +19,12 @@ use ZBateson\MailMimeParser\Message\Factory\PartHeaderContainerFactory;
*/
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
protected readonly PartBuilderFactory $partBuilderFactory,
protected readonly PartHeaderContainerFactory $partHeaderContainerFactory,
protected readonly ParserManagerService $parserManager,
protected readonly HeaderParserService $headerParser
) {
$this->partBuilderFactory = $pbf;
$this->partHeaderContainerFactory = $phcf;
$this->parserManager = $pm;
$this->headerParser = $hp;
}
/**
@@ -90,6 +63,8 @@ class MessageParserService
$headerContainer
);
$proxy = $this->parserManager->createParserProxyFor($partBuilder);
return $proxy->getPart();
$part = $proxy->getPart();
\assert($part instanceof IMessage);
return $part;
}
}

View File

@@ -22,34 +22,15 @@ use ZBateson\MailMimeParser\Parser\Proxy\ParserPartProxy;
*/
class MimeParserService extends AbstractParserService
{
/**
* @var PartHeaderContainerFactory Factory service for creating
* PartHeaderContainers for headers.
*/
protected PartHeaderContainerFactory $partHeaderContainerFactory;
/**
* @var HeaderParserService The HeaderParser service.
*/
protected HeaderParserService $headerParser;
/**
* @var int Maximum multipart nesting depth.
*/
protected int $maxMimePartDepth;
public function __construct(
ParserMessageProxyFactory $parserMessageProxyFactory,
ParserMimePartProxyFactory $parserMimePartProxyFactory,
PartBuilderFactory $partBuilderFactory,
PartHeaderContainerFactory $partHeaderContainerFactory,
HeaderParserService $headerParser,
int $maxMimePartDepth = 256
protected readonly PartHeaderContainerFactory $partHeaderContainerFactory,
protected readonly HeaderParserService $headerParser,
protected readonly int $maxMimePartDepth = 256
) {
parent::__construct($parserMessageProxyFactory, $parserMimePartProxyFactory, $partBuilderFactory);
$this->partHeaderContainerFactory = $partHeaderContainerFactory;
$this->headerParser = $headerParser;
$this->maxMimePartDepth = $maxMimePartDepth;
}
/**
@@ -121,6 +102,7 @@ class MimeParserService extends AbstractParserService
public function parseContent(ParserPartProxy $proxy) : static
{
\assert($proxy instanceof ParserMimePartProxy);
$proxy->setStreamContentStartPos($proxy->getMessageResourceHandlePos());
$this->findContentBoundary($proxy);
return $this;

View File

@@ -22,16 +22,13 @@ use ZBateson\MailMimeParser\Parser\Proxy\ParserUUEncodedPartProxyFactory;
*/
class NonMimeParserService extends AbstractParserService
{
protected UUEncodedPartHeaderContainerFactory $partHeaderContainerFactory;
public function __construct(
ParserNonMimeMessageProxyFactory $parserNonMimeMessageProxyFactory,
ParserUUEncodedPartProxyFactory $parserUuEncodedPartProxyFactory,
PartBuilderFactory $partBuilderFactory,
UUEncodedPartHeaderContainerFactory $uuEncodedPartHeaderContainerFactory
protected readonly UUEncodedPartHeaderContainerFactory $partHeaderContainerFactory
) {
parent::__construct($parserNonMimeMessageProxyFactory, $parserUuEncodedPartProxyFactory, $partBuilderFactory);
$this->partHeaderContainerFactory = $uuEncodedPartHeaderContainerFactory;
}
/**
@@ -68,7 +65,7 @@ class NonMimeParserService extends AbstractParserService
*
* @param ParserNonMimeMessageProxy|ParserUUEncodedPartProxy $proxy
*/
private function parseNextPart(ParserPartProxy $proxy) : static
private function parseNextPart(ParserNonMimeMessageProxy|ParserUUEncodedPartProxy $proxy) : static
{
$handle = $proxy->getMessageResourceHandle();
while (!\feof($handle)) {
@@ -87,6 +84,7 @@ class NonMimeParserService extends AbstractParserService
public function parseContent(ParserPartProxy $proxy) : static
{
\assert($proxy instanceof ParserNonMimeMessageProxy || $proxy instanceof ParserUUEncodedPartProxy);
$handle = $proxy->getMessageResourceHandle();
if ($proxy->getNextPartStart() !== null || \feof($handle)) {
return $this;
@@ -100,6 +98,7 @@ class NonMimeParserService extends AbstractParserService
public function parseNextChild(ParserMimePartProxy $proxy) : ?ParserPartProxy
{
\assert($proxy instanceof ParserNonMimeMessageProxy);
$handle = $proxy->getMessageResourceHandle();
if ($proxy->getNextPartStart() === null || \feof($handle)) {
return null;

View File

@@ -18,22 +18,15 @@ use ZBateson\MailMimeParser\Parser\Proxy\ParserMimePartProxy;
*/
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)
public function __construct(protected readonly ParserMimePartProxy $parserProxy)
{
parent::__construct([]);
$this->parserProxy = $parserProxy;
}
public function offsetExists($offset) : bool

View File

@@ -14,7 +14,6 @@ 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;
@@ -36,16 +35,10 @@ use ZBateson\MbWrapper\MbWrapper;
class ParserPartStreamContainer extends PartStreamContainer implements SplObserver
{
/**
* @var ParserPartProxy The parser proxy to ferry requests to on-demand.
* @var StreamInterface|null the original stream for a parsed message, used
* when the message hasn't changed
*/
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;
protected ?StreamInterface $parsedStream = null;
/**
* @var bool set to true if the part's been updated since it was created.
@@ -63,10 +56,9 @@ class ParserPartStreamContainer extends PartStreamContainer implements SplObserv
StreamFactory $streamFactory,
MbWrapper $mbWrapper,
bool $throwExceptionReadingPartContentFromUnsupportedCharsets,
ParserPartProxy $parserProxy
protected readonly ParserPartProxy $parserProxy
) {
parent::__construct($logger, $streamFactory, $mbWrapper, $throwExceptionReadingPartContentFromUnsupportedCharsets);
$this->parserProxy = $parserProxy;
}
public function __destruct()
@@ -107,9 +99,7 @@ class ParserPartStreamContainer extends PartStreamContainer implements SplObserv
$this->parserProxy
)
);
if ($this->parsedStream !== null) {
$this->detachParsedStream = ($this->parsedStream->getMetadata('mmp-detached-stream') === true);
}
$this->detachParsedStream = ($this->parsedStream->getMetadata('mmp-detached-stream') === true);
}
return $this;
}
@@ -120,13 +110,13 @@ class ParserPartStreamContainer extends PartStreamContainer implements SplObserv
return parent::hasContent();
}
public function getContentStream(IMessagePart $part, ?string $transferEncoding, ?string $fromCharset, ?string $toCharset) : ?MessagePartStreamDecorator
public function getContentStream(IMessagePart $part, ?string $transferEncoding, ?string $fromCharset, ?string $toCharset) : ?StreamInterface
{
$this->requestParsedContentStream();
return parent::getContentStream($part, $transferEncoding, $fromCharset, $toCharset);
}
public function getBinaryContentStream(IMessagePart $part, ?string $transferEncoding = null) : ?MessagePartStreamDecorator
public function getBinaryContentStream(IMessagePart $part, ?string $transferEncoding = null) : ?StreamInterface
{
$this->requestParsedContentStream();
return parent::getBinaryContentStream($part, $transferEncoding);
@@ -142,7 +132,7 @@ class ParserPartStreamContainer extends PartStreamContainer implements SplObserv
return $this;
}
public function getStream() : MessagePartStreamDecorator
public function getStream() : StreamInterface
{
$this->requestParsedStream();
if (!$this->partUpdated) {

View File

@@ -19,24 +19,12 @@ use ZBateson\MbWrapper\MbWrapper;
*/
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
protected readonly LoggerInterface $logger,
protected readonly StreamFactory $streamFactory,
protected readonly MbWrapper $mbWrapper,
protected readonly bool $throwExceptionReadingPartContentFromUnsupportedCharsets
) {
$this->logger = $logger;
$this->streamFactory = $streamFactory;
$this->mbWrapper = $mbWrapper;
$this->throwExceptionReadingPartContentFromUnsupportedCharsets = $throwExceptionReadingPartContentFromUnsupportedCharsets;
}
public function newInstance(ParserPartProxy $parserProxy) : ParserPartStreamContainer

View File

@@ -17,22 +17,10 @@ use ZBateson\MailMimeParser\Header\HeaderFactory;
*/
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;
public function __construct(
protected readonly LoggerInterface $logger,
protected readonly HeaderFactory $headerFactory
) {
}
/**

View File

@@ -12,6 +12,7 @@ use Psr\Http\Message\StreamInterface;
use ZBateson\MailMimeParser\Header\HeaderConsts;
use ZBateson\MailMimeParser\Message\PartHeaderContainer;
use ZBateson\MailMimeParser\Parser\Proxy\ParserMimePartProxy;
use ZBateson\MailMimeParser\Parser\Proxy\ParserPartProxy;
/**
* Holds generic/all purpose information about a part while it's being parsed.
@@ -67,18 +68,18 @@ class PartBuilder
private ?StreamInterface $messageStream = null;
/**
* @var resource the raw message input stream handle constructed from
* @var resource|null 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
* @var ParserPartProxy|null The parent proxy part if one is set, or null if
* the part being built doesn't have a parent.
*/
private ?ParserMimePartProxy $parent = null;
private ?ParserPartProxy $parent = null;
public function __construct(PartHeaderContainer $headerContainer, ?StreamInterface $messageStream = null, ?ParserMimePartProxy $parent = null)
public function __construct(PartHeaderContainer $headerContainer, ?StreamInterface $messageStream = null, ?ParserPartProxy $parent = null)
{
$this->headerContainer = $headerContainer;
$this->messageStream = $messageStream;
@@ -91,7 +92,7 @@ class PartBuilder
public function __destruct()
{
if ($this->messageHandle) {
if ($this->messageHandle !== null) {
\fclose($this->messageHandle);
}
}
@@ -99,7 +100,7 @@ class PartBuilder
/**
* The ParserPartProxy parent of this PartBuilder.
*/
public function getParent() : ?ParserMimePartProxy
public function getParent() : ?ParserPartProxy
{
return $this->parent;
}

View File

@@ -27,22 +27,17 @@ use ZBateson\MailMimeParser\Stream\StreamFactory;
*/
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
StreamFactory $streamFactory,
PartHeaderContainerFactory $partHeaderContainerFactory,
ParserPartStreamContainerFactory $parserPartStreamContainerFactory,
ParserPartChildrenContainerFactory $parserPartChildrenContainerFactory,
protected readonly MultipartHelper $multipartHelper,
protected readonly PrivacyHelper $privacyHelper,
string $defaultFallbackCharset = 'ISO-8859-1'
) {
parent::__construct($logger, $sdf, $phcf, $pscf, $ppccf);
$this->multipartHelper = $multipartHelper;
$this->privacyHelper = $privacyHelper;
parent::__construct($logger, $streamFactory, $partHeaderContainerFactory, $parserPartStreamContainerFactory, $parserPartChildrenContainerFactory, $defaultFallbackCharset);
}
/**
@@ -63,7 +58,8 @@ class ParserMessageProxyFactory extends ParserMimePartProxyFactory
$headerContainer,
$childrenContainer,
$this->multipartHelper,
$this->privacyHelper
$this->privacyHelper,
$this->defaultFallbackCharset
);
$parserProxy->setPart($message);

View File

@@ -133,7 +133,12 @@ class ParserMimePartProxy extends ParserPartProxy
*/
public function getContentType() : ?ParameterHeader
{
return $this->getHeaderContainer()->get(HeaderConsts::CONTENT_TYPE);
$header = $this->getHeaderContainer()->get(HeaderConsts::CONTENT_TYPE);
if ($header === null) {
return null;
}
\assert($header instanceof ParameterHeader);
return $header;
}
/**
@@ -163,7 +168,7 @@ class ParserMimePartProxy extends ParserPartProxy
public function setEndBoundaryFound(string $line) : bool
{
$boundary = $this->getMimeBoundary();
if ($this->getParent() !== null && $this->getParent()->setEndBoundaryFound($line)) {
if ($this->getParent()?->setEndBoundaryFound($line)) {
$this->parentBoundaryFound = true;
return true;
} elseif ($boundary !== null) {
@@ -222,7 +227,7 @@ class ParserMimePartProxy extends ParserPartProxy
{
// 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))) {
if (!empty($this->mimeBoundary) || !empty($this->getParent()->mimeBoundary)) {
$this->addError('End boundary for part not found', LogLevel::WARNING);
}
}

View File

@@ -9,6 +9,7 @@ namespace ZBateson\MailMimeParser\Parser\Proxy;
use Psr\Log\LoggerInterface;
use ZBateson\MailMimeParser\Message\Factory\PartHeaderContainerFactory;
use ZBateson\MailMimeParser\Message\IMimePart;
use ZBateson\MailMimeParser\Message\MimePart;
use ZBateson\MailMimeParser\Parser\IParserService;
use ZBateson\MailMimeParser\Parser\Part\ParserPartChildrenContainerFactory;
@@ -24,28 +25,14 @@ use ZBateson\MailMimeParser\Stream\StreamFactory;
*/
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
protected readonly LoggerInterface $logger,
protected readonly StreamFactory $streamFactory,
protected readonly PartHeaderContainerFactory $partHeaderContainerFactory,
protected readonly ParserPartStreamContainerFactory $parserPartStreamContainerFactory,
protected readonly ParserPartChildrenContainerFactory $parserPartChildrenContainerFactory,
protected readonly string $defaultFallbackCharset = 'ISO-8859-1'
) {
$this->logger = $logger;
$this->streamFactory = $sdf;
$this->partHeaderContainerFactory = $phcf;
$this->parserPartStreamContainerFactory = $pscf;
$this->parserPartChildrenContainerFactory = $ppccf;
}
/**
@@ -61,12 +48,15 @@ class ParserMimePartProxyFactory extends ParserPartProxyFactory
$headerContainer = $this->partHeaderContainerFactory->newInstance($parserProxy->getHeaderContainer());
$childrenContainer = $this->parserPartChildrenContainerFactory->newInstance($parserProxy);
$parent = $partBuilder->getParent()?->getPart();
\assert($parent === null || $parent instanceof IMimePart);
$part = new MimePart(
$partBuilder->getParent()->getPart(),
$parent,
$this->logger,
$streamContainer,
$headerContainer,
$childrenContainer
$childrenContainer,
$this->defaultFallbackCharset
);
$parserProxy->setPart($part);

View File

@@ -36,7 +36,8 @@ class ParserNonMimeMessageProxyFactory extends ParserMessageProxyFactory
$headerContainer,
$childrenContainer,
$this->multipartHelper,
$this->privacyHelper
$this->privacyHelper,
$this->defaultFallbackCharset
);
$parserProxy->setPart($message);

View File

@@ -24,25 +24,15 @@ use ZBateson\MailMimeParser\Parser\PartBuilder;
*/
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;
public function __construct(
protected readonly PartBuilder $partBuilder,
protected readonly IParserService $parser
) {
}
/**
@@ -94,7 +84,7 @@ abstract class ParserPartProxy extends PartBuilder
return $this;
}
public function getParent() : ?ParserMimePartProxy
public function getParent() : ?ParserPartProxy
{
return $this->partBuilder->getParent();
}

View File

@@ -20,7 +20,9 @@ class ParserUUEncodedPartProxy extends ParserPartProxy
*/
public function getParent() : ParserNonMimeMessageProxy
{
return parent::getParent();
$parent = parent::getParent();
\assert($parent instanceof ParserNonMimeMessageProxy);
return $parent;
}
/**

View File

@@ -8,6 +8,7 @@
namespace ZBateson\MailMimeParser\Parser\Proxy;
use Psr\Log\LoggerInterface;
use ZBateson\MailMimeParser\Message\IMimePart;
use ZBateson\MailMimeParser\Message\UUEncodedPart;
use ZBateson\MailMimeParser\Parser\IParserService;
use ZBateson\MailMimeParser\Parser\Part\ParserPartStreamContainerFactory;
@@ -22,20 +23,12 @@ use ZBateson\MailMimeParser\Stream\StreamFactory;
*/
class ParserUUEncodedPartProxyFactory extends ParserPartProxyFactory
{
protected LoggerInterface $logger;
protected StreamFactory $streamFactory;
protected ParserPartStreamContainerFactory $parserPartStreamContainerFactory;
public function __construct(
LoggerInterface $logger,
StreamFactory $sdf,
ParserPartStreamContainerFactory $parserPartStreamContainerFactory
protected readonly LoggerInterface $logger,
protected readonly StreamFactory $streamFactory,
protected readonly ParserPartStreamContainerFactory $parserPartStreamContainerFactory,
protected readonly string $defaultFallbackCharset = 'ISO-8859-1'
) {
$this->logger = $logger;
$this->streamFactory = $sdf;
$this->parserPartStreamContainerFactory = $parserPartStreamContainerFactory;
}
/**
@@ -46,12 +39,15 @@ class ParserUUEncodedPartProxyFactory extends ParserPartProxyFactory
$parserProxy = new ParserUUEncodedPartProxy($partBuilder, $parser);
$streamContainer = $this->parserPartStreamContainerFactory->newInstance($parserProxy);
$parent = $partBuilder->getParent()?->getPart();
\assert($parent === null || $parent instanceof IMimePart);
$part = new UUEncodedPart(
$parserProxy->getUnixFileMode(),
$parserProxy->getFileName(),
$partBuilder->getParent()->getPart(),
$parent,
$this->logger,
$streamContainer
$streamContainer,
$this->defaultFallbackCharset
);
$parserProxy->setPart($part);

View File

@@ -39,9 +39,8 @@ class HeaderStream extends MessagePartStreamDecorator implements SplObserver, St
parent::__construct($part);
$part->attach($this);
// unsetting the property forces the first access to go through
// __get().
unset($this->stream);
// Don't initialize $stream - let the StreamDecoratorTrait's __get()
// call createStream() lazily when needed.
}
public function __destruct()
@@ -61,12 +60,14 @@ class HeaderStream extends MessagePartStreamDecorator implements SplObserver, St
*
* If the part is not a MimePart, Content-Type, Content-Disposition and
* Content-Transfer-Encoding headers are generated manually.
*
* @return Traversable<int, array{0: string, 1: string}>
*/
private function getPartHeadersIterator() : Traversable
{
if ($this->part instanceof IMimePart) {
return $this->part->getRawHeaderIterator();
} elseif ($this->part->getParent() !== null && $this->part->getParent()->isMime()) {
} elseif ($this->part->getParent()?->isMime()) {
return new ArrayIterator([
[HeaderConsts::CONTENT_TYPE, $this->part->getContentType()],
[HeaderConsts::CONTENT_DISPOSITION, $this->part->getContentDisposition()],

View File

@@ -56,9 +56,8 @@ class MessagePartStream extends MessagePartStreamDecorator implements SplObserve
$this->throwExceptionReadingPartContentFromUnsupportedCharsets = $throwExceptionReadingPartContentFromUnsupportedCharsets;
$part->attach($this);
// unsetting the property forces the first access to go through
// __get().
unset($this->stream);
// Don't initialize $stream - let the StreamDecoratorTrait's __get()
// call createStream() lazily when needed.
}
public function __destruct()
@@ -69,8 +68,7 @@ class MessagePartStream extends MessagePartStreamDecorator implements SplObserve
public function update(SplSubject $subject) : void
{
if ($this->appendStream !== null) {
// unset forces recreation in StreamDecoratorTrait with a call to __get
unset($this->stream);
$this->stream = null;
$this->appendStream = null;
}
}
@@ -121,9 +119,7 @@ class MessagePartStream extends MessagePartStreamDecorator implements SplObserve
$boundary = $part->getHeaderParameter(HeaderConsts::CONTENT_TYPE, 'boundary');
if ($boundary === null) {
return \array_map(
function($child) {
return $child->getStream();
},
fn($child) => $child->getStream(),
$part->getChildParts()
);
}

View File

@@ -20,20 +20,42 @@ use ZBateson\MailMimeParser\Message\IMessagePart;
class MessagePartStreamDecorator implements StreamInterface
{
use StreamDecoratorTrait {
StreamDecoratorTrait::__construct as private traitConstruct;
read as private decoratorRead;
}
/**
* @var IMessagePart The part to read from.
*/
protected ?StreamInterface $stream = null;
protected IMessagePart $part;
protected ?StreamInterface $stream;
public function __construct(IMessagePart $part, ?StreamInterface $stream = null)
{
public function __construct(
IMessagePart $part,
?StreamInterface $stream = null
) {
$this->part = $part;
$this->stream = $stream;
if ($stream !== null) {
$this->stream = $stream;
}
}
/**
* Returns the underlying stream, lazily creating it via createStream() if
* not yet initialized.
*/
protected function resolveStream() : StreamInterface
{
if ($this->stream === null) {
$this->stream = $this->createStream();
}
return $this->stream;
}
public function __get(string $name) : StreamInterface
{
if ($name === 'stream') {
return $this->resolveStream();
}
throw new \UnexpectedValueException("$name not found on class");
}
/**
@@ -45,7 +67,7 @@ class MessagePartStreamDecorator implements StreamInterface
public function read(int $length) : string
{
try {
return $this->decoratorRead($length);
return $this->resolveStream()->read($length);
} catch (MessagePartStreamReadException $me) {
throw $me;
} catch (RuntimeException $e) {
@@ -58,4 +80,96 @@ class MessagePartStreamDecorator implements StreamInterface
);
}
}
public function close() : void
{
$this->resolveStream()->close();
}
/**
* @return mixed
*/
public function getMetadata($key = null)
{
return $this->resolveStream()->getMetadata($key);
}
public function detach()
{
return $this->resolveStream()->detach();
}
public function getSize() : ?int
{
return $this->resolveStream()->getSize();
}
public function eof() : bool
{
return $this->resolveStream()->eof();
}
public function tell() : int
{
return $this->resolveStream()->tell();
}
public function isReadable() : bool
{
return $this->resolveStream()->isReadable();
}
public function isWritable() : bool
{
return $this->resolveStream()->isWritable();
}
public function isSeekable() : bool
{
return $this->resolveStream()->isSeekable();
}
public function seek($offset, $whence = SEEK_SET) : void
{
$this->resolveStream()->seek($offset, $whence);
}
public function write($string) : int
{
return $this->resolveStream()->write($string);
}
public function rewind() : void
{
$this->seek(0);
}
public function __toString() : string
{
try {
if ($this->isSeekable()) {
$this->seek(0);
}
return $this->getContents();
} catch (\Throwable $e) {
throw $e;
}
}
public function getContents() : string
{
return \GuzzleHttp\Psr7\Utils::copyToString($this);
}
/**
* @param array<mixed> $args
* @return mixed
*/
public function __call(string $method, array $args)
{
/** @var callable $callable */
$callable = [$this->resolveStream(), $method];
$result = ($callable)(...$args);
return $result === $this->stream ? $this : $result;
}
}

View File

@@ -27,15 +27,9 @@ use ZBateson\StreamDecorators\UUStream;
*/
class StreamFactory
{
/**
* @var bool if true, saving a content stream with an unsupported charset
* will be written in the default charset.
*/
protected bool $throwExceptionReadingPartContentFromUnsupportedCharsets;
public function __construct(bool $throwExceptionReadingPartContentFromUnsupportedCharsets)
{
$this->throwExceptionReadingPartContentFromUnsupportedCharsets = $throwExceptionReadingPartContentFromUnsupportedCharsets;
public function __construct(
protected readonly bool $throwExceptionReadingPartContentFromUnsupportedCharsets
) {
}
/**
@@ -136,24 +130,15 @@ class StreamFactory
public function getTransferEncodingDecoratedStream(StreamInterface $stream, ?string $transferEncoding, ?string $filename = null) : StreamInterface
{
$decorated = null;
switch ($transferEncoding) {
case 'quoted-printable':
$decorated = $this->newQuotedPrintableStream($stream);
break;
case 'base64':
$decorated = $this->newBase64Stream(
$this->newChunkSplitStream($stream)
);
break;
case 'x-uuencode':
$decorated = $this->newUUStream($stream);
if ($filename !== null) {
$decorated->setFilename($filename);
}
break;
default:
return $stream;
$decorated = match ($transferEncoding) {
'quoted-printable' => $this->newQuotedPrintableStream($stream),
'base64' => $this->newBase64Stream($this->newChunkSplitStream($stream)),
'x-uuencode' => $this->newUUStream($stream),
default => $stream,
};
if ($transferEncoding === 'x-uuencode' && $filename !== null) {
\assert($decorated instanceof UUStream);
$decorated->setFilename($filename);
}
return $decorated;
}
@@ -169,7 +154,7 @@ class StreamFactory
/**
* Creates and returns a MessagePartStream
*/
public function newMessagePartStream(IMessagePart $part) : MessagePartStreamDecorator
public function newMessagePartStream(IMessagePart $part) : StreamInterface
{
return new MessagePartStream($this, $part, $this->throwExceptionReadingPartContentFromUnsupportedCharsets);
}
@@ -191,7 +176,7 @@ class StreamFactory
return new HeaderStream($part);
}
public function newDecoratedMessagePartStream(IMessagePart $part, StreamInterface $stream) : MessagePartStreamDecorator
public function newDecoratedMessagePartStream(IMessagePart $part, StreamInterface $stream) : StreamInterface
{
return new MessagePartStreamDecorator($part, $stream);
}

View File

@@ -12,11 +12,18 @@ use Psr\Log\NullLogger;
use ZBateson\MailMimeParser\Header\Consumer\Received\DomainConsumerService;
use ZBateson\MailMimeParser\Header\Consumer\Received\GenericReceivedConsumerService;
use ZBateson\MailMimeParser\Header\Consumer\ReceivedConsumerService;
use ZBateson\MailMimeParser\Message\Factory\IMessagePartFactory;
use ZBateson\MailMimeParser\Message\Factory\IMimePartFactory;
use ZBateson\MailMimeParser\Message\Factory\IUUEncodedPartFactory;
use ZBateson\MailMimeParser\Message\Factory\PartStreamContainerFactory;
use ZBateson\MailMimeParser\Message\PartStreamContainer;
use ZBateson\MailMimeParser\Parser\HeaderParserService;
use ZBateson\MailMimeParser\Parser\MimeParserService;
use ZBateson\MailMimeParser\Parser\Part\ParserPartStreamContainerFactory;
use ZBateson\MailMimeParser\Parser\Proxy\ParserMessageProxyFactory;
use ZBateson\MailMimeParser\Parser\Proxy\ParserMimePartProxyFactory;
use ZBateson\MailMimeParser\Parser\Proxy\ParserNonMimeMessageProxyFactory;
use ZBateson\MailMimeParser\Parser\Proxy\ParserUUEncodedPartProxyFactory;
use ZBateson\MailMimeParser\Stream\StreamFactory;
return [
@@ -26,6 +33,11 @@ return [
// header parts
'throwExceptionReadingPartContentFromUnsupportedCharsets' => false,
// Fallback charset for text/* content parts without a declared charset.
// Per RFC 2045, the default is ISO-8859-1 but many modern messages omit the
// charset and are actually UTF-8. Override this to 'UTF-8' if desired.
'defaultFallbackCharset' => 'ISO-8859-1',
// Maximum multipart nesting depth before parsing stops with a recorded error.
'maxMimePartDepth' => 256,
@@ -70,6 +82,34 @@ return [
->constructor(
throwExceptionReadingPartContentFromUnsupportedCharsets: new Reference('throwExceptionReadingPartContentFromUnsupportedCharsets')
),
IMessagePartFactory::class => (new AutowireDefinitionHelper())
->constructor(
defaultFallbackCharset: new Reference('defaultFallbackCharset')
),
IMimePartFactory::class => (new AutowireDefinitionHelper())
->constructor(
defaultFallbackCharset: new Reference('defaultFallbackCharset')
),
IUUEncodedPartFactory::class => (new AutowireDefinitionHelper())
->constructor(
defaultFallbackCharset: new Reference('defaultFallbackCharset')
),
ParserMimePartProxyFactory::class => (new AutowireDefinitionHelper())
->constructor(
defaultFallbackCharset: new Reference('defaultFallbackCharset')
),
ParserUUEncodedPartProxyFactory::class => (new AutowireDefinitionHelper())
->constructor(
defaultFallbackCharset: new Reference('defaultFallbackCharset')
),
ParserMessageProxyFactory::class => (new AutowireDefinitionHelper())
->constructor(
defaultFallbackCharset: new Reference('defaultFallbackCharset')
),
ParserNonMimeMessageProxyFactory::class => (new AutowireDefinitionHelper())
->constructor(
defaultFallbackCharset: new Reference('defaultFallbackCharset')
),
HeaderParserService::class => (new AutowireDefinitionHelper())
->constructor(
maxHeaderCount: new Reference('maxHeaderCount'),

View File

@@ -1 +1 @@
3.0.6
4.0.3

View File

@@ -0,0 +1,277 @@
<?php
$config = new PhpCsFixer\Config();
return $config
->setRiskyAllowed(true)
->setIndent(' ')
->setLineEnding("\n")
->setRules([
// Each line of multi-line DocComments must have an asterisk [PSR-5] and must be aligned with the first one.
'align_multiline_comment' => ['comment_type'=>'all_multiline'],
// Each element of an array must be indented exactly once.
'array_indentation' => true,
// Converts simple usages of `array_push($x, $y);` to `$x[] = $y;`.
'array_push' => true,
// PHP arrays should be declared using the configured syntax.
'array_syntax' => ['syntax'=>'short'],
// Converts backtick operators to `shell_exec` calls.
'backtick_to_shell_exec' => true,
// Binary operators should be surrounded by space as configured.
'binary_operator_spaces' => true,
// There MUST be one blank line after the namespace declaration.
'blank_line_after_namespace' => true,
// Ensure there is no code on the same line as the PHP open tag and it is followed by a blank line.
'blank_line_after_opening_tag' => true,
// An empty line feed must precede any configured statement.
'blank_line_before_statement' => false,
// A single space or none should be between cast and variable.
'cast_spaces' => ['space'=>'single'],
// Class, trait and interface elements must be separated with one or none blank line.
'class_attributes_separation' => true,
// Whitespace around the keywords of a class, trait or interfaces definition should be one space.
'class_definition' => true,
// Namespace must not contain spacing, comments or PHPDoc.
'clean_namespace' => true,
// Using `isset($var) &&` multiple times should be done in one call.
'combine_consecutive_issets' => true,
// Calling `unset` on multiple items should be done in one call.
'combine_consecutive_unsets' => true,
// Replace multiple nested calls of `dirname` by only one call with second `$level` parameter. Requires PHP >= 7.0.
'combine_nested_dirname' => false,
// Comments with annotation should be docblock when used on structural elements.
'comment_to_phpdoc' => false,
// Remove extra spaces in a nullable typehint.
'compact_nullable_type_declaration' => true,
// Concatenation should be spaced according configuration.
'concat_space' => ['spacing'=>'one'],
// The PHP constants `true`, `false`, and `null` MUST be written using the correct casing.
'constant_case' => true,
// Equal sign in declare statement should be surrounded by spaces or not following configuration.
'declare_equal_normalize' => ['space'=>'single'],
// Replaces `dirname(__FILE__)` expression with equivalent `__DIR__` constant.
'dir_constant' => true,
// The keyword `elseif` should be used instead of `else if` so that all control keywords look like single words.
'elseif' => true,
// PHP code MUST use only UTF-8 without BOM (remove BOM).
'encoding' => true,
// Replace deprecated `ereg` regular expression functions with `preg`.
'ereg_to_preg' => true,
// Add curly braces to indirect variables to make them clear to understand. Requires PHP >= 7.0.
'explicit_indirect_variable' => false,
// Converts implicit variables into explicit ones in double-quoted strings or heredoc syntax.
'explicit_string_variable' => false,
// Order the flags in `fopen` calls, `b` and `t` must be last.
'fopen_flag_order' => true,
// PHP code must use the long `<?php` tags or short-echo `<?=` tags and not other tag variations.
'full_opening_tag' => true,
// Spaces should be properly placed in a function declaration.
'function_declaration' => ['closure_function_spacing'=>'none'],
// Replace core functions calls returning constants with the constants.
'function_to_constant' => true,
// Ensure single space between function's argument and its typehint.
'type_declaration_spaces' => true,
// Renames PHPDoc tags.
'general_phpdoc_tag_rename' => true,
// Function `implode` must be called with 2 arguments in the documented order.
'implode_call' => true,
// Include/Require and file path should be divided with a single space. File path should not be placed under brackets.
'include' => true,
// Code MUST use configured indentation type.
'indentation_type' => true,
// Replaces `is_null($var)` expression with `null === $var`.
'is_null' => true,
// All PHP files must use same line ending.
'line_ending' => true,
// Ensure there is no code on the same line as the PHP open tag.
'linebreak_after_opening_tag' => true,
// List (`array` destructuring) assignment should be declared using the configured syntax. Requires PHP >= 7.1.
'list_syntax' => false,
// Use `&&` and `||` logical operators instead of `and` and `or`.
'logical_operators' => true,
// Cast should be written in lower case.
'lowercase_cast' => true,
// PHP keywords MUST be in lower case.
'lowercase_keywords' => true,
// Class static references `self`, `static` and `parent` MUST be in lower case.
'lowercase_static_reference' => true,
// Magic constants should be referred to using the correct casing.
'magic_constant_casing' => true,
// Magic method definitions and calls must be using the correct casing.
'magic_method_casing' => true,
// In method arguments and method call, there MUST NOT be a space before each comma and there MUST be one space after each comma. Argument lists MAY be split across multiple lines, where each subsequent line is indented once. When doing so, the first item in the list MUST be on the next line, and there MUST be only one argument per line.
'method_argument_space' => true,
// Method chaining MUST be properly indented. Method chaining with different levels of indentation is not supported.
'method_chaining_indentation' => true,
// Replaces `intval`, `floatval`, `doubleval`, `strval` and `boolval` function calls with according type casting operator.
'modernize_types_casting' => true,
// Forbid multi-line whitespace before the closing semicolon or move the semicolon to the new line for chained calls.
'multiline_whitespace_before_semicolons' => true,
// Function defined by PHP should be called using the correct casing.
'native_function_casing' => true,
// Add leading `\` before function invocation to speed up resolving.
'native_function_invocation' => ['include'=>['@all','trans']],
// Native type hints for functions should use the correct case.
'native_type_declaration_casing' => true,
// All instances created with new keyword must be followed by braces.
'new_with_parentheses' => true,
// Master functions shall be used instead of aliases.
'no_alias_functions' => true,
// Master language constructs shall be used instead of aliases.
'no_alias_language_construct_call' => true,
// Replace control structure alternative syntax to use braces.
'no_alternative_syntax' => true,
// There should not be blank lines between docblock and the documented element.
'no_blank_lines_after_phpdoc' => true,
// There must be a comment when fall-through is intentional in a non-empty case body.
'no_break_comment' => ['comment_text'=>'Intentionally fall through'],
// The closing `? >` tag MUST be omitted from files containing only PHP.
'no_closing_tag' => true,
// There should not be any empty comments.
'no_empty_comment' => true,
// There should not be empty PHPDoc blocks.
'no_empty_phpdoc' => true,
// Remove useless (semicolon) statements.
'no_empty_statement' => true,
// Replace accidental usage of homoglyphs (non ascii characters) in names.
'no_homoglyph_names' => true,
// Remove leading slashes in `use` clauses.
'no_leading_import_slash' => true,
// The namespace declaration line shouldn't contain leading whitespace.
'no_leading_namespace_whitespace' => true,
// Either language construct `print` or `echo` should be used.
'no_mixed_echo_print' => true,
// Operator `=>` should not be surrounded by multi-line whitespaces.
'no_multiline_whitespace_around_double_arrow' => true,
// Convert PHP4-style constructors to `__construct`.
'no_php4_constructor' => true,
// Short cast `bool` using double exclamation mark should not be used.
'no_short_bool_cast' => true,
// When making a method or function call, there MUST NOT be a space between the method or function name and the opening parenthesis.
'no_spaces_after_function_name' => true,
// Removes `@param`, `@return` and `@var` tags that don't provide any useful information.
'no_superfluous_phpdoc_tags' => true,
// Remove trailing whitespace at the end of non-blank lines.
'no_trailing_whitespace' => true,
// There MUST be no trailing spaces inside comment or PHPDoc.
'no_trailing_whitespace_in_comment' => true,
// Removes unneeded parentheses around control statements.
'no_unneeded_control_parentheses' => false,
// Removes unneeded curly braces that are superfluous and aren't part of a control structure's body.
'no_unneeded_braces' => true,
// A `final` class must not have `final` methods and `private` methods must not be `final`.
'no_unneeded_final_method' => true,
// In function arguments there must not be arguments with default values before non-default ones.
'no_unreachable_default_argument_value' => true,
// Variables must be set `null` instead of using `(unset)` casting.
'no_unset_cast' => true,
// Properties should be set to `null` instead of using `unset`.
'no_unset_on_property' => false,
// Unused `use` statements must be removed.
'no_unused_imports' => true,
// There should not be useless `else` cases.
'no_useless_else' => true,
// There should not be an empty `return` statement at the end of a function.
'no_useless_return' => true,
// There must be no `sprintf` calls with only the first argument.
'no_useless_sprintf' => true,
// In array declaration, there MUST NOT be a whitespace before each comma.
'no_whitespace_before_comma_in_array' => true,
// Remove trailing whitespace at the end of blank lines.
'no_whitespace_in_blank_line' => true,
// Remove Zero-width space (ZWSP), Non-breaking space (NBSP) and other invisible unicode symbols.
'non_printable_character' => ['use_escape_sequences_in_strings'=>true],
// Array index should always be written by using square braces.
'normalize_index_brace' => true,
// Logical NOT operators (`!`) should have one trailing whitespace.
'not_operator_with_successor_space' => false,
// Adds or removes `?` before type declarations for parameters with a default `null` value.
'nullable_type_declaration_for_default_null_value' => true,
// There should not be space before or after object operators `->` and `?->`.
'object_operator_without_whitespace' => true,
// Orders the elements of classes/interfaces/traits.
'ordered_class_elements' => ['order'=>['use_trait','constant_public','constant_protected','constant_private','property_public','property_protected','property_private']],
// Ordering `use` statements.
'ordered_imports' => true,
// Orders the interfaces in an `implements` or `interface extends` clause.
'ordered_interfaces' => true,
// Trait `use` statements must be sorted alphabetically.
'ordered_traits' => true,
// Classy that does not inherit must not have `@inheritdoc` tags.
'phpdoc_no_useless_inheritdoc' => true,
// Annotations in PHPDoc should be ordered so that `@param` annotations come first, then `@throws` annotations, then `@return` annotations.
'phpdoc_order' => true,
// The type of `@return` annotations of methods returning a reference to itself must the configured one.
'phpdoc_return_self_reference' => true,
// Scalar types should always be written in the same form. `int` not `integer`, `bool` not `boolean`, `float` not `real` or `double`.
'phpdoc_scalar' => true,
// Fixes casing of PHPDoc tags.
'phpdoc_tag_casing' => true,
// Converts `protected` variables and methods to `private` where possible.
'protected_to_private' => true,
// Classes must be in a path that matches their namespace, be at least one namespace deep and the class name should match the file name.
'psr_autoloading' => true,
// There should be one or no space before colon, and one space after it in return type declarations, according to configuration.
'return_type_declaration' => ['space_before'=>'one'],
// Instructions must be terminated with a semicolon.
'semicolon_after_instruction' => true,
// Cast shall be used, not `settype`.
'set_type_to_cast' => true,
// Cast `(boolean)` and `(integer)` should be written as `(bool)` and `(int)`, `(double)` and `(real)` as `(float)`, `(binary)` as `(string)`.
'short_scalar_cast' => true,
// Converts explicit variables in double-quoted strings and heredoc syntax from simple to complex format (`${` to `{$`).
'simple_to_complex_string_variable' => false,
// Simplify `if` control structures that return the boolean result of their condition.
'simplified_if_return' => true,
// A return statement wishing to return `void` should not return `null`.
'simplified_null_return' => false,
// A PHP file without end tag must always end with a single empty line feed.
'single_blank_line_at_eof' => true,
// There should be exactly one blank line before a namespace declaration.
'blank_lines_before_namespace' => ['max_line_breaks' => 2, 'min_line_breaks' => 2],
// There MUST NOT be more than one property or constant declared per statement.
'single_class_element_per_statement' => true,
// There MUST be one use keyword per declaration.
'single_import_per_statement' => true,
// Each namespace use MUST go on its own line and there MUST be one blank line after the use statements block.
'single_line_after_imports' => true,
// Single-line comments and multi-line comments with only one line of actual content should use the `//` syntax.
'single_line_comment_style' => true,
// Convert double quotes to single quotes for simple strings.
'single_quote' => true,
// Each trait `use` must be done as single statement.
'single_trait_insert_per_statement' => true,
// There MUST NOT be a space after the opening parenthesis. There MUST NOT be a space before the closing parenthesis.
'spaces_inside_parentheses' => true,
// Replace all `<>` with `!=`.
'standardize_not_equals' => true,
// Lambdas not (indirect) referencing `$this` must be declared `static`.
'static_lambda' => false,
// All multi-line strings must use correct line ending.
'string_line_ending' => true,
// A case should be followed by a colon and not a semicolon.
'switch_case_semicolon_to_colon' => true,
// Removes extra spaces between colon and case value.
'switch_case_space' => true,
// Switch case must not be ended with `continue` but with `break`.
'switch_continue_to_break' => true,
// Standardize spaces around ternary operator.
'ternary_operator_spaces' => true,
// Use the Elvis operator `?:` where possible.
'ternary_to_elvis_operator' => true,
// Use `null` coalescing operator `??` where possible. Requires PHP >= 7.0.
'ternary_to_null_coalescing' => false,
// Arrays should be formatted like function/method arguments, without leading or trailing single line space.
'trim_array_spaces' => true,
// Unary operators should be placed adjacent to their operands.
'unary_operator_spaces' => true,
// Visibility MUST be declared on all properties and methods; `abstract` and `final` MUST be declared before the visibility; `static` MUST be declared after the visibility.
'visibility_required' => true,
// Add `void` return type to functions with missing or empty return statements, but priority is given to `@return` annotations. Requires PHP >= 7.1.
'void_return' => true,
// In array declaration, there MUST be a whitespace after each comma.
'whitespace_after_comma_in_array' => true,
// Write conditions in Yoda style (`true`), non-Yoda style (`['equal' => false, 'identical' => false, 'less_and_greater' => false]`) or ignore those conditions (`null`) based on configuration.
'yoda_style' => false,
]);

View File

@@ -2,51 +2,41 @@
Charset conversion and string manipulation wrapper with a large defined set of aliases.
[![Tests](https://github.com/zbateson/mb-wrapper.svg/actions/workflows/tests.yml/badge.svg)](https://github.com/zbateson/mb-wrapper.svg/actions/workflows/tests.yml)
[![Code Coverage](https://scrutinizer-ci.com/g/zbateson/mb-wrapper/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/zbateson/mb-wrapper/?branch=master)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/zbateson/mb-wrapper/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/zbateson/mb-wrapper/?branch=master)
[![Build Status](https://github.com/zbateson/mb-wrapper/actions/workflows/tests.yml/badge.svg)](https://github.com/zbateson/mb-wrapper/actions/workflows/tests.yml)
[![Total Downloads](https://poser.pugx.org/zbateson/mb-wrapper/downloads)](https://packagist.org/packages/zbateson/mb-wrapper)
[![Latest Stable Version](https://poser.pugx.org/zbateson/mb-wrapper/version)](https://packagist.org/packages/zbateson/mb-wrapper)
The goals of this project are to be:
* Well written
* Tested where possible
* Support as wide a range of charset aliases as possible
To include it for use in your project, please install via composer:
```
composer require zbateson/mb-wrapper
```
## Php 7 Support Dropped
## Sponsors
As of mb-wrapper 2.0, support for php 7 has been dropped.
[![SecuMailer](https://mail-mime-parser.org/sponsors/logo-secumailer.png)](https://secumailer.com)
A huge thank you to [all my sponsors](https://github.com/sponsors/zbateson). <3
If this project's helped you, please consider [sponsoring me](https://github.com/sponsors/zbateson).
## Requirements
mb-wrapper requires PHP 8.0 or newer. Tested on PHP 8.0, 8.1, 8.2, and 8.3 on GitHub Actions.
## New in 2.0
If converting or performing an operation on a string fails in iconv, an UnsupportedCharsetException is now thrown.
PHP 8.1 or newer. Tested on PHP 8.1, 8.2, 8.3, 8.4, and 8.5.
## Description
MbWrapper is intended for use wherever mb_* or iconv_* is used. It scans supported charsets returned by mb_list_encodings(), and prefers mb_* functions, but will fallback to iconv if a charset isn't supported by the mb_ functions.
MbWrapper is intended for use wherever `mb_*` or `iconv_*` is used. It scans supported charsets returned by `mb_list_encodings()`, and prefers `mb_*` functions, but will fallback to `iconv` if a charset isn't supported by the `mb_*` functions.
A list of aliased charsets is maintained for both mb_* and iconv, where a supported charset exists for an alias. This is useful for mail and http parsing as other systems may report encodings not recognized by mb_* or iconv.
A list of aliased charsets is maintained for both `mb_*` and `iconv`, where a supported charset exists for an alias. This is useful for mail and http parsing as other systems may report encodings not recognized by `mb_*` or `iconv`.
Charset lookup is done by removing non-alphanumeric characters as well, so UTF8 will always be matched to UTF-8, etc...
Charset lookup is done by removing non-alphanumeric characters as well, so `UTF8` will always be matched to `UTF-8`, etc.
## Usage
The following wrapper methods are exposed:
* mb_convert_encoding, iconv with MbWrapper::convert
* mb_substr, iconv_substr with MbWrapper::getSubstr
* mb_strlen, iconv_strlen with MbWrapper::getLength
* mb_check_encoding, iconv (for verification) with MbWrapper::checkEncoding
* `mb_convert_encoding`, `iconv` with `MbWrapper::convert`
* `mb_substr`, `iconv_substr` with `MbWrapper::getSubstr`
* `mb_strlen`, `iconv_strlen` with `MbWrapper::getLength`
* `mb_check_encoding`, `iconv` (for verification) with `MbWrapper::checkEncoding`
```php
$mbWrapper = new \ZBateson\MbWrapper\MbWrapper();

View File

@@ -9,12 +9,12 @@
}
],
"require": {
"php": ">=8.0",
"php": ">=8.1",
"symfony/polyfill-mbstring": "^1.9",
"symfony/polyfill-iconv": "^1.9"
},
"require-dev": {
"phpunit/phpunit": "^9.6|^10.0",
"phpunit/phpunit": "^10.0|^11.0",
"friendsofphp/php-cs-fixer": "*",
"phpstan/phpstan": "*"
},

View File

@@ -1,9 +1,11 @@
<?php
/**
* This file is part of the ZBateson\MbWrapper project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MbWrapper;
/**
@@ -24,15 +26,15 @@ namespace ZBateson\MbWrapper;
class MbWrapper
{
/**
* @var array<string, string> aliased charsets supported by mb_convert_encoding.
* The alias is stripped of any non-alphanumeric characters (so CP367
* is equal to CP-367) when comparing.
* Some of these translations are already supported by
* mb_convert_encoding on "my" PHP 5.5.9, but may not be supported in
* other implementations or versions since they're not part of
* documented support.
* Aliased charsets supported by mb_convert_encoding.
* The alias is stripped of any non-alphanumeric characters (so CP367
* is equal to CP-367) when comparing.
* Some of these translations are already supported by
* mb_convert_encoding on "my" PHP 5.5.9, but may not be supported in
* other implementations or versions since they're not part of
* documented support.
*/
public static $mbAliases = [
public const MB_ALIASES = [
// supported but not included in mb_list_encodings for some reason...
'CP850' => 'CP850',
'GB2312' => 'GB18030',
@@ -186,9 +188,9 @@ class MbWrapper
];
/**
* @var array<string, string> aliased charsets supported by iconv.
* Aliased charsets supported by iconv.
*/
public static $iconvAliases = [
public const ICONV_ALIASES = [
// iconv aliases -- a lot of these may already be supported
'CESU8' => 'UTF8',
'CP154' => 'PT154',
@@ -278,21 +280,12 @@ class MbWrapper
'1258' => 'CP1258',
];
/**
* @var string[] cached lookups for quicker retrieval
*/
protected $mappedMbCharsets = [
'UTF8' => 'UTF-8',
'USASCII' => 'US-ASCII',
'ISO88591' => 'ISO-8859-1',
];
/**
* @var string[] An array of encodings supported by the mb_* extension, as
* returned by mb_list_encodings(), with the key set to the charset's
* name afte
*/
private static $mbListedEncodings;
private static ?array $mbListedEncodings = null;
/**
* Initializes the static mb_* encoding array.
@@ -311,19 +304,28 @@ class MbWrapper
* characters before being returned.
*
* @param string|string[] $charset
* @return string|string[]
* @return ($charset is string ? string : string[])
*/
private function getNormalizedCharset($charset)
private function getNormalizedCharset(string|array $charset) : string|array
{
$upper = null;
if (\is_array($charset)) {
$upper = \array_map('strtoupper', $charset);
$upper = \array_map(\strtoupper(...), $charset);
} else {
$upper = \strtoupper($charset);
}
return \preg_replace('/[^A-Z0-9]+/', '', $upper);
}
/**
* Strips iconv conversion modifiers (e.g. '//TRANSLIT', '//IGNORE') that are
* not part of a charset name.
*/
private function stripCharsetConversionModifier(string $cs) : string
{
$pos = \strpos($cs, '//');
return ($pos === false) ? $cs : \substr($cs, 0, $pos);
}
private function iconv(string $fromCharset, string $toCharset, string $str) : string
{
$ret = @\iconv($fromCharset, $toCharset . '//TRANSLIT//IGNORE', $str);
@@ -332,7 +334,7 @@ class MbWrapper
}
return $ret;
}
private function iconvStrlen(string $str, string $charset) : int
{
$ret = @\iconv_strlen($str, $charset . '//TRANSLIT//IGNORE');
@@ -341,7 +343,7 @@ class MbWrapper
}
return $ret;
}
private function iconvSubstr(string $str, string $charset, int $start, ?int $length = null) : string
{
$ret = @\iconv_substr($str, $start, $length, $charset . '//TRANSLIT//IGNORE');
@@ -429,7 +431,7 @@ class MbWrapper
/**
* Uses either mb_substr or iconv_substr to create and return a substring of
* the passed $str.
*
*
* If the offset provided in $start is greater than the length of the
* string, an empty string is returned.
*
@@ -459,30 +461,30 @@ class MbWrapper
*
* On success, the method will return the charset name as accepted by mb_*.
*
* @return string|bool
*/
private function getMbCharset(string $cs)
private function getMbCharset(string $cs) : string|false
{
$normalized = $this->getNormalizedCharset($cs);
$normalized = $this->getNormalizedCharset($this->stripCharsetConversionModifier($cs));
if (\array_key_exists($normalized, self::$mbListedEncodings)) {
return self::$mbListedEncodings[$normalized];
} elseif (\array_key_exists($normalized, self::$mbAliases)) {
return self::$mbAliases[$normalized];
} elseif (\array_key_exists($normalized, self::MB_ALIASES)) {
return self::MB_ALIASES[$normalized];
}
return false;
}
/**
* Looks up the passed charset in self::$iconvAliases, returning the mapped
* Looks up the passed charset in self::ICONV_ALIASES, returning the mapped
* charset if applicable. Otherwise returns charset.
*
* @return string the mapped charset (if mapped) or $cs otherwise
*/
private function getIconvAlias(string $cs) : string
{
$cs = $this->stripCharsetConversionModifier($cs);
$normalized = $this->getNormalizedCharset($cs);
if (\array_key_exists($normalized, self::$iconvAliases)) {
return static::$iconvAliases[$normalized];
if (\array_key_exists($normalized, self::ICONV_ALIASES)) {
return self::ICONV_ALIASES[$normalized];
}
return $cs;
}

View File

@@ -1,4 +1,5 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*

View File

@@ -9,7 +9,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
php: [8.3, 8.2, 8.1, 8.0]
php: [8.5, 8.4, 8.3, 8.2, 8.1]
stability: [prefer-stable]
name: P${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }}

View File

@@ -1,40 +1,41 @@
# zbateson/stream-decorators
Psr7 stream decorators for character set conversion and common mail format content encodings.
PSR-7 stream decorators for character set conversion and common mail format content encodings.
[![Tests](https://github.com/zbateson/stream-decorators/actions/workflows/tests.yml/badge.svg)](https://github.com/zbateson/stream-decorators/actions/workflows/tests.yml)
[![Code Coverage](https://scrutinizer-ci.com/g/zbateson/stream-decorators/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/zbateson/stream-decorators/?branch=master)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/zbateson/stream-decorators/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/zbateson/stream-decorators/?branch=master)
[![Build Status](https://github.com/zbateson/stream-decorators/actions/workflows/tests.yml/badge.svg)](https://github.com/zbateson/stream-decorators/actions/workflows/tests.yml)
[![Total Downloads](https://poser.pugx.org/zbateson/stream-decorators/downloads)](//packagist.org/packages/zbateson/stream-decorators)
[![Latest Stable Version](https://poser.pugx.org/zbateson/stream-decorators/v)](//packagist.org/packages/zbateson/stream-decorators)
The goals of this project are to be:
* Well written
* Standards-compliant but forgiving
* Tested where possible
To include it for use in your project, please install via composer:
```
composer require zbateson/stream-decorators
```
## Php 7 Support Dropped
## Sponsors
As of stream-decorators 2.0, support for php 7 has been dropped.
[![SecuMailer](https://mail-mime-parser.org/sponsors/logo-secumailer.png)](https://secumailer.com)
A huge thank you to [all my sponsors](https://github.com/sponsors/zbateson). <3
If this project's helped you, please consider [sponsoring me](https://github.com/sponsors/zbateson).
## Requirements
stream-decorators requires PHP 8.0 or newer. Tested on 8.0, 8.1, 8.2 and 8.3.
PHP 8.1 or newer. Tested on PHP 8.1, 8.2, 8.3, 8.4, and 8.5.
## New in 2.0 and 2.1
## Description
Support for guzzlehttp/psr7 1.9 dropped, min supported version is 2.0.
The library provides the following `Psr\Http\Message\StreamInterface` implementations:
zbateson/mb-wrapper has been updated to 2.0 as well, which throws an UnsupportedCharsetException converting from/to an unsupported charset, which changes the behaviour of CharsetStream.
Two new classes are introduced in 2.1, DecoratedCachingStream and a TellZeroStream.
* `Base64Stream` - decodes on read and encodes on write to base64
* `CharsetStream` - encodes from `$streamCharset` to `$stringCharset` on read, and vice-versa on write
* `ChunkSplitStream` - splits written characters into lines of `$lineLength` long (stream implementation of PHP's `chunk_split`)
* `DecoratedCachingStream` - a caching stream that writes to a decorated stream, and reads from the cached undecorated stream
* `NonClosingStream` - overrides `close()` and `detach()`, and simply unsets the attached stream without closing it
* `PregReplaceFilterStream` - calls `preg_replace` with passed arguments on every `read()` call
* `QuotedPrintableStream` - decodes on read and encodes on write to quoted-printable
* `SeekingLimitStream` - similar to GuzzleHttp's `LimitStream`, but maintains an internal current read position
* `TellZeroStream` - `tell()` always returns `0` -- used by `DecoratedCachingStream` to wrap a `BufferStream` in a `CachingStream`
* `UUStream` - decodes on read, encodes on write to uu-encoded
## Usage
@@ -46,32 +47,9 @@ $charsetStream = new ZBateson\StreamDecorators\CharsetStream($b64Stream, 'UTF-32
while (($line = GuzzleHttp\Psr7\Utils::readLine()) !== false) {
echo $line, "\r\n";
}
```
Note that CharsetStream, depending on the target encoding, may return multiple bytes when a single 'char' is read. If using php's 'fread', this will result in a warning:
'read x bytes more data than requested (xxxx read, xxxx max) - excess data will be lost
This is because the parameter to 'fread' is bytes, and so when CharsetStream returns, say, 4 bytes representing a single UTF-32 character, fread will truncate to the first byte when requesting '1' byte. It is recommended to **not** convert to a stream handle (with StreamWrapper) for this reason when using CharsetStream.
The library consists of the following Psr\Http\Message\StreamInterface implementations:
* ZBateson\StreamDecorators\Base64Stream - decodes on read and encodes on write to base64.
* ZBateson\StreamDecorators\CharsetStream - encodes from $streamCharset to $stringCharset on read, and vice-versa on write.
* ZBateson\StreamDecorators\ChunkSplitStream - splits written characters into lines of $lineLength long (stream implementation of php's chunk_split).
* ZBateson\StreamDecorators\DecoratedCachingStream - a caching stream that writes to a decorated stream, and reads from the cached undecorated stream, so for instance a stream could be passed, and decorated with a Base64Stream, and when read, the returned bytes would be base64 encoded.
* ZBateson\StreamDecorators\NonClosingStream - overrides close() and detach(), and simply unsets the attached stream without closing it.
* ZBateson\StreamDecorators\PregReplaceFilterStream - calls preg_replace on with passed arguments on every read() call.
* ZBateson\StreamDecorators\QuotedPrintableStream - decodes on read and encodes on write to quoted-printable.
* ZBateson\StreamDecorators\SeekingLimitStream - similar to GuzzleHttp's LimitStream, but maintains an internal current read position, seeking to it when read() is called, and seeking back to the wrapped stream's position after reading.
* ZBateson\StreamDecorators\TellZeroStream - tell() always returns '0' -- used by DecoratedCachingStream to wrap a BufferStream in a CachingStream. CachingStream calls tell() on its wrapped stream, and BufferStream throws an exception, so TellZeroStream is used to wrap the internal BufferStream to mitigate that.
* ZBateson\StreamDecorators\UUStream - decodes on read, encodes on write to uu-encoded.
QuotedPrintableStream, Base64Stream and UUStream's constructors take a single argument of a StreamInterface.
CharsetStreams's constructor also takes $streamCharset and $stringCharset as arguments respectively, ChunkSplitStream
optionally takes a $lineLength argument (defaults to 76) and a $lineEnding argument (defaults to CRLF).
PregReplaceFilterStream takes a $pattern argument and a $replacement argument. SeekingLimitStream takes optional
$limit and $offset parameters, similar to GuzzleHttp's LimitStream.
Note that `CharsetStream`, depending on the target encoding, may return multiple bytes when a single 'char' is read. If using PHP's `fread`, this will result in a warning. It is recommended to **not** convert to a stream handle (with `StreamWrapper`) when using `CharsetStream`.
## License

View File

@@ -9,12 +9,12 @@
}
],
"require": {
"php": ">=8.0",
"guzzlehttp/psr7": "^2.5",
"zbateson/mb-wrapper": "^2.0"
"php": ">=8.1",
"guzzlehttp/psr7": "^2.5 || ^3.0",
"zbateson/mb-wrapper": "^2.0 || ^3.0"
},
"require-dev": {
"phpunit/phpunit": "^9.6|^10.0",
"phpunit/phpunit": "^10.0 || ^11.0",
"friendsofphp/php-cs-fixer": "*",
"phpstan/phpstan": "*"
},

View File

@@ -47,7 +47,7 @@ class Base64Stream implements StreamInterface
/**
* @var BufferStream buffered bytes
*/
private BufferStream $buffer;
private readonly BufferStream $buffer;
/**
* @var string remainder of write operation if the bytes didn't align to 3
@@ -60,14 +60,8 @@ class Base64Stream implements StreamInterface
*/
private int $position = 0;
/**
* @var StreamInterface $stream
*/
private StreamInterface $stream;
public function __construct(StreamInterface $stream)
public function __construct(private readonly StreamInterface $stream)
{
$this->stream = $stream;
$this->buffer = new BufferStream();
}
@@ -98,7 +92,7 @@ class Base64Stream implements StreamInterface
* @param int $whence
* @throws RuntimeException
*/
public function seek($offset, $whence = SEEK_SET) : void
public function seek($offset, $whence = SEEK_SET) : never
{
throw new RuntimeException('Cannot seek a Base64Stream');
}

View File

@@ -24,18 +24,7 @@ class CharsetStream implements StreamInterface
/**
* @var MbWrapper the charset converter
*/
protected MbWrapper $converter;
/**
* @var string charset of the source stream
*/
protected string $streamCharset = 'ISO-8859-1';
/**
* @var string charset of strings passed in write operations, and returned
* in read operations.
*/
protected string $stringCharset = 'UTF-8';
protected readonly MbWrapper $converter;
/**
* @var int current read/write position
@@ -53,23 +42,18 @@ class CharsetStream implements StreamInterface
*/
private string $buffer = '';
/**
* @var StreamInterface $stream
*/
private StreamInterface $stream;
/**
* @param StreamInterface $stream Stream to decorate
* @param string $streamCharset The underlying stream's charset
* @param string $stringCharset The charset to encode strings to (or
* expected for write)
*/
public function __construct(StreamInterface $stream, string $streamCharset = 'ISO-8859-1', string $stringCharset = 'UTF-8')
{
$this->stream = $stream;
public function __construct(
private readonly StreamInterface $stream,
protected readonly string $streamCharset = 'ISO-8859-1',
protected readonly string $stringCharset = 'UTF-8',
) {
$this->converter = new MbWrapper();
$this->streamCharset = $streamCharset;
$this->stringCharset = $stringCharset;
}
/**
@@ -97,7 +81,7 @@ class CharsetStream implements StreamInterface
* @param int $whence
* @throws RuntimeException
*/
public function seek($offset, $whence = SEEK_SET) : void
public function seek($offset, $whence = SEEK_SET) : never
{
throw new RuntimeException('Cannot seek a CharsetStream');
}

View File

@@ -25,34 +25,18 @@ class ChunkSplitStream implements StreamInterface
* final $lineEnding on close (and so maintained instead of using
* tell() directly)
*/
private int $position;
/**
* @var int The number of characters in a line before inserting $lineEnding.
*/
private int $lineLength;
/**
* @var string The line ending characters to insert.
*/
private string $lineEnding;
private int $position = 0;
/**
* @var int The strlen() of $lineEnding
*/
private int $lineEndingLength;
private readonly int $lineEndingLength;
/**
* @var StreamInterface $stream
*/
private StreamInterface $stream;
public function __construct(StreamInterface $stream, int $lineLength = 76, string $lineEnding = "\r\n")
{
$this->stream = $stream;
$this->position = 0;
$this->lineLength = $lineLength;
$this->lineEnding = $lineEnding;
public function __construct(
private readonly StreamInterface $stream,
private readonly int $lineLength = 76,
private readonly string $lineEnding = "\r\n",
) {
$this->lineEndingLength = \strlen($this->lineEnding);
}

View File

@@ -33,13 +33,13 @@ class DecoratedCachingStream implements StreamInterface
/**
* @var StreamInterface the stream to read from and fill writeStream with
*/
private StreamInterface $readStream;
private readonly StreamInterface $readStream;
/**
* @var StreamInterface the underlying undecorated stream to read from,
* where $writeStream is being written to
*/
private StreamInterface $stream;
private readonly StreamInterface $stream;
/**
* @var StreamInterface decorated $stream that will be written to for
@@ -48,13 +48,6 @@ class DecoratedCachingStream implements StreamInterface
*/
private ?StreamInterface $writeStream;
/**
* @var int Minimum buffer read length. At least this many bytes will be
* read and cached into $writeStream on each call to read from
* $readStream
*/
private int $minBytesCache;
/**
* @param StreamInterface $stream Stream to cache. The cursor is assumed to
* be at the beginning of the stream.
@@ -65,13 +58,12 @@ class DecoratedCachingStream implements StreamInterface
public function __construct(
StreamInterface $stream,
callable $decorator,
int $minBytesCache = 16384
private readonly int $minBytesCache = 16384,
) {
$this->readStream = $stream;
$bufferStream = new TellZeroStream(new BufferStream());
$this->stream = new CachingStream($bufferStream);
$this->writeStream = $decorator(new NonClosingStream($bufferStream));
$this->minBytesCache = $minBytesCache;
}
public function getSize(): ?int
@@ -142,7 +134,7 @@ class DecoratedCachingStream implements StreamInterface
return false;
}
public function write($string): int
public function write($string): never
{
throw new \RuntimeException('Cannot write to a DecoratedCachingStream');
}
@@ -167,9 +159,9 @@ class DecoratedCachingStream implements StreamInterface
private function cacheEntireStream(): int
{
// as-is from CachingStream
$target = new FnStream(['write' => 'strlen']);
$target = new FnStream(['write' => strlen(...)]);
Utils::copyToStream($this, $target);
return $this->tell();
}
}
}

View File

@@ -51,7 +51,7 @@ class NonClosingStream implements StreamInterface
*/
public function close() : void
{
$this->stream = null; // @phpstan-ignore-line
$this->stream = null;
}
/**
@@ -61,7 +61,7 @@ class NonClosingStream implements StreamInterface
*/
public function detach()
{
$this->stream = null; // @phpstan-ignore-line
$this->stream = null;
return null;
}
}

View File

@@ -24,31 +24,16 @@ class PregReplaceFilterStream implements StreamInterface
{
use StreamDecoratorTrait;
/**
* @var string The regex pattern
*/
private string $pattern;
/**
* @var string The replacement
*/
private string $replacement;
/**
* @var BufferStream Buffered stream of input from the underlying stream
*/
private BufferStream $buffer;
private readonly BufferStream $buffer;
/**
* @var StreamInterface $stream
*/
private StreamInterface $stream;
public function __construct(StreamInterface $stream, string $pattern, string $replacement)
{
$this->stream = $stream;
$this->pattern = $pattern;
$this->replacement = $replacement;
public function __construct(
private readonly StreamInterface $stream,
private readonly string $pattern,
private readonly string $replacement,
) {
$this->buffer = new BufferStream();
}
@@ -67,7 +52,7 @@ class PregReplaceFilterStream implements StreamInterface
* @param int $whence
* @throws RuntimeException
*/
public function seek($offset, $whence = SEEK_SET) : void
public function seek($offset, $whence = SEEK_SET) : never
{
throw new RuntimeException('Cannot seek a PregReplaceFilterStream');
}

View File

@@ -34,7 +34,7 @@ class QuotedPrintableStream implements StreamInterface
* @var StreamInterface $stream
* @phpstan-ignore-next-line
*/
private StreamInterface $stream;
private readonly StreamInterface $stream;
/**
* Overridden to return the position in the target encoding.
@@ -61,7 +61,7 @@ class QuotedPrintableStream implements StreamInterface
* @param int $whence
* @throws RuntimeException
*/
public function seek($offset, $whence = SEEK_SET) : void
public function seek($offset, $whence = SEEK_SET) : never
{
throw new RuntimeException('Cannot seek a QuotedPrintableStream');
}
@@ -107,7 +107,7 @@ class QuotedPrintableStream implements StreamInterface
*/
private function decodeBlock(string $block) : string
{
if (\substr($block, -1) === '=') {
if (\str_ends_with($block, '=')) {
$block .= $this->readEncodedChars(2);
} elseif (\substr($block, -2, 1) === '=') {
$first = \substr($block, -1);

View File

@@ -37,11 +37,6 @@ class SeekingLimitStream implements StreamInterface
*/
private int $position = 0;
/**
* @var StreamInterface $stream
*/
private StreamInterface $stream;
/**
* @param StreamInterface $stream Stream to wrap
* @param int $limit Total number of bytes to allow to be read
@@ -49,9 +44,8 @@ class SeekingLimitStream implements StreamInterface
* @param int $offset Position to seek to before reading (only
* works on seekable streams).
*/
public function __construct(StreamInterface $stream, int $limit = -1, int $offset = 0)
public function __construct(private readonly StreamInterface $stream, int $limit = -1, int $offset = 0)
{
$this->stream = $stream;
$this->setLimit($limit);
$this->setOffset($offset);
}
@@ -123,17 +117,11 @@ class SeekingLimitStream implements StreamInterface
*/
public function seek($offset, $whence = SEEK_SET) : void
{
$pos = $offset;
switch ($whence) {
case SEEK_CUR:
$pos = $this->position + $offset;
break;
case SEEK_END:
$pos = $this->limit + $offset;
break;
default:
break;
}
$pos = match ($whence) {
SEEK_CUR => $this->position + $offset,
SEEK_END => $this->limit + $offset,
default => $offset,
};
$this->doSeek($pos);
}

View File

@@ -21,11 +21,12 @@ class TellZeroStream implements StreamInterface
/**
* @var StreamInterface
* @phpstan-ignore-next-line
*/
private StreamInterface $stream;
private readonly StreamInterface $stream;
public function tell() : int
{
return 0;
}
}
}

View File

@@ -26,43 +26,44 @@ class UUStream implements StreamInterface
use StreamDecoratorTrait;
/**
* @var string name of the UUEncoded file
* @var ?string name of the UUEncoded file
*/
protected $filename = null;
protected ?string $filename = null;
/**
* @var BufferStream of read and decoded bytes
*/
private $buffer;
private readonly BufferStream $buffer;
/**
* @var string remainder of write operation if the bytes didn't align to 3
* bytes
*/
private $remainder = '';
private string $remainder = '';
/**
* @var string bytes read past the last line ending, carried to the next read
*/
private string $lineRemainder = '';
/**
* @var int read/write position
*/
private $position = 0;
private int $position = 0;
/**
* @var bool set to true when 'write' is called
*/
private $isWriting = false;
/**
* @var StreamInterface $stream
*/
private $stream;
private bool $isWriting = false;
/**
* @param StreamInterface $stream Stream to decorate
* @param string $filename optional file name
*/
public function __construct(StreamInterface $stream, ?string $filename = null)
{
$this->stream = $stream;
public function __construct(
private readonly StreamInterface $stream,
?string $filename = null,
) {
$this->filename = $filename;
$this->buffer = new BufferStream();
}
@@ -92,7 +93,7 @@ class UUStream implements StreamInterface
* @param int $whence
* @throws RuntimeException
*/
public function seek($offset, $whence = SEEK_SET) : void
public function seek($offset, $whence = SEEK_SET) : never
{
throw new RuntimeException('Cannot seek a UUStream');
}
@@ -111,17 +112,23 @@ class UUStream implements StreamInterface
*/
private function readToEndOfLine(int $length) : string
{
$str = $this->stream->read($length);
$str = $this->lineRemainder . $this->stream->read($length);
$this->lineRemainder = '';
if ($str === '') {
return $str;
}
while (\substr($str, -1) !== "\n") {
$chr = $this->stream->read(1);
while (\strpos($str, "\n") === false) {
$chr = $this->stream->read($length);
if ($chr === '') {
break;
}
$str .= $chr;
}
$eol = \strrpos($str, "\n");
if ($eol !== false && $eol < \strlen($str) - 1) {
$this->lineRemainder = \substr($str, $eol + 1);
$str = \substr($str, 0, $eol + 1);
}
return $str;
}
@@ -139,10 +146,9 @@ class UUStream implements StreamInterface
$this->filename = $matches[1];
}
$ret = \preg_replace('/^\s*begin[^\r\n]+\s*$/im', '', $ret);
} else {
$ret = \preg_replace('/^\s*end\s*$/im', '', $ret);
}
return \convert_uudecode(\trim($ret));
$ret = \trim(\preg_replace('/^\s*end\s*$/im', '', $ret));
return ($ret === '') ? '' : \convert_uudecode($ret);
}
/**
@@ -168,7 +174,7 @@ class UUStream implements StreamInterface
*/
public function eof() : bool
{
return ($this->buffer->eof() && $this->stream->eof());
return ($this->buffer->eof() && $this->stream->eof() && $this->lineRemainder === '');
}
/**
@@ -264,7 +270,7 @@ class UUStream implements StreamInterface
/**
* Returns the filename set in the UUEncoded header (or null)
*/
public function getFilename() : string
public function getFilename() : ?string
{
return $this->filename;
}