glastree_on_gitea

This commit is contained in:
2026-05-26 08:14:29 +02:00
commit 0bed099d05
9556 changed files with 1186307 additions and 0 deletions
@@ -0,0 +1,86 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Header\Part;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use ZBateson\MbWrapper\MbWrapper;
/**
* Holds a group of addresses and a group name.
*
* @author Zaahid Bateson
*/
class AddressGroupPart extends NameValuePart
{
/**
* @var AddressPart[] an array of AddressParts
*/
protected array $addresses;
/**
* Creates an AddressGroupPart out of the passed array of AddressParts/
* AddressGroupParts and name.
*
* @param HeaderPart[] $nameParts
* @param AddressPart[]|AddressGroupPart[] $addressesAndGroupParts
*/
public function __construct(
LoggerInterface $logger,
MbWrapper $charsetConverter,
array $nameParts,
array $addressesAndGroupParts
) {
parent::__construct(
$logger,
$charsetConverter,
$nameParts,
$addressesAndGroupParts
);
$this->addresses = \array_merge(...\array_map(
fn ($p) => ($p instanceof AddressGroupPart) ? $p->getAddresses() : [$p],
$addressesAndGroupParts
));
// for backwards compatibility
$this->value = $this->name;
}
/**
* Return the AddressGroupPart's array of addresses.
*
* @return AddressPart[] An array of address parts.
*/
public function getAddresses() : array
{
return $this->addresses;
}
/**
* Returns the AddressPart at the passed index or null.
*
* @param int $index The 0-based index.
* @return ?AddressPart The address.
*/
public function getAddress(int $index) : ?AddressPart
{
if (!isset($this->addresses[$index])) {
return null;
}
return $this->addresses[$index];
}
protected function validate() : void
{
if ($this->name === null || \mb_strlen($this->name) === 0) {
$this->addError('Address group doesn\'t have a name', LogLevel::ERROR);
}
if (empty($this->addresses)) {
$this->addError('Address group doesn\'t have any email addresses defined in it', LogLevel::NOTICE);
}
}
}
@@ -0,0 +1,57 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Header\Part;
use Psr\Log\LogLevel;
/**
* Holds a single address or name/address pair.
*
* The name part of the address may be mime-encoded, but the email address part
* can't be mime-encoded. Any whitespace in the email address part is stripped
* out.
*
* A convenience method, getEmail, is provided for clarity -- but getValue
* returns the email address as well.
*
* @author Zaahid Bateson
*/
class AddressPart extends NameValuePart
{
protected function getValueFromParts(array $parts) : string
{
return \implode('', \array_map(
function($p) {
if ($p instanceof AddressPart) {
return $p->getValue();
} elseif ($p instanceof QuotedLiteralPart && $p->getValue() !== '') {
return '"' . \preg_replace('/(["\\\])/', '\\\$1', $p->getValue()) . '"';
}
return \preg_replace('/\s+/', '', $p->getValue());
},
$parts
));
}
/**
* Returns the email address.
*
* @return string The email address.
*/
public function getEmail() : string
{
return $this->value;
}
protected function validate() : void
{
if (empty($this->value)) {
$this->addError('Address doesn\'t contain an email address', LogLevel::ERROR);
}
}
}
@@ -0,0 +1,77 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Header\Part;
use Psr\Log\LoggerInterface;
use ZBateson\MbWrapper\MbWrapper;
/**
* Represents a mime header comment -- text in a structured mime header
* value existing within parentheses.
*
* @author Zaahid Bateson
*/
class CommentPart extends ContainerPart
{
/**
* @var HeaderPartFactory used to create intermediate parts.
*/
protected HeaderPartFactory $partFactory;
/**
* @var string the contents of the comment
*/
protected string $comment;
public function __construct(
LoggerInterface $logger,
MbWrapper $charsetConverter,
HeaderPartFactory $partFactory,
array $children
) {
$this->partFactory = $partFactory;
parent::__construct($logger, $charsetConverter, $children);
$this->comment = $this->value;
$this->value = '';
$this->isSpace = true;
$this->canIgnoreSpacesBefore = true;
$this->canIgnoreSpacesAfter = true;
}
protected function getValueFromParts(array $parts) : string
{
$partFactory = $this->partFactory;
return parent::getValueFromParts(\array_map(
function($p) use ($partFactory) {
if ($p instanceof CommentPart) {
return $partFactory->newQuotedLiteralPart([$partFactory->newToken('(' . $p->getComment() . ')')]);
} elseif ($p instanceof QuotedLiteralPart) {
return $partFactory->newQuotedLiteralPart([$partFactory->newToken('"' . \str_replace('(["\\])', '\$1', $p->getValue()) . '"')]);
}
return $p;
},
$parts
));
}
/**
* Returns the comment's text.
*/
public function getComment() : string
{
return $this->comment;
}
/**
* Returns an empty string.
*/
public function getValue() : string
{
return '';
}
}
@@ -0,0 +1,128 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Header\Part;
use Psr\Log\LoggerInterface;
use ZBateson\MailMimeParser\ErrorBag;
use ZBateson\MbWrapper\MbWrapper;
/**
* Base HeaderPart for a part that consists of other parts.
*
* The base container part constructs a string value out of the passed parts by
* concatenating their values, discarding whitespace between parts that can be
* ignored (in general allows for a single space but removes extras.)
*
* A ContainerPart can also contain any number of child comment parts. The
* CommentParts in this and all child parts can be returned by calling
* getComments.
*
* @author Zaahid Bateson
*/
class ContainerPart extends HeaderPart
{
/**
* @var HeaderPart[] parts that were used to create this part, collected for
* proper error reporting and validation.
*/
protected $children = [];
public function __construct(
LoggerInterface $logger,
MbWrapper $charsetConverter,
array $children
) {
ErrorBag::__construct($logger);
$this->charsetConverter = $charsetConverter;
$this->children = $children;
$str = (!empty($children)) ? $this->getValueFromParts($children) : '';
parent::__construct(
$logger,
$this->charsetConverter,
$str
);
}
/**
* Filters out ignorable space tokens.
*
* Spaces are removed if parts on either side of it have their
* canIgnoreSpaceAfter/canIgnoreSpaceBefore properties set to true.
*
* @param HeaderPart[] $parts
* @return HeaderPart[]
*/
protected function filterIgnoredSpaces(array $parts) : array
{
$ends = (object) ['isSpace' => true, 'canIgnoreSpacesAfter' => true, 'canIgnoreSpacesBefore' => true, 'value' => ''];
$spaced = \array_merge($parts, [$ends]);
$filtered = \array_slice(\array_reduce(
\array_slice(\array_keys($spaced), 0, -1),
function($carry, $key) use ($spaced, $ends) {
$p = $spaced[$key];
$l = \end($carry);
$a = $spaced[$key + 1];
if ($p->isSpace && $a === $ends) {
// trim
if ($l->isSpace) {
\array_pop($carry);
}
return $carry;
} elseif ($p->isSpace && ($l->isSpace || ($l->canIgnoreSpacesAfter && $a->canIgnoreSpacesBefore))) {
return $carry;
}
return \array_merge($carry, [$p]);
},
[$ends]
), 1);
return $filtered;
}
/**
* Creates the string value representation of this part constructed from the
* child parts passed to it.
*
* The default implementation filters out ignorable whitespace between
* parts, and concatenates parts calling 'getValue'.
*
* @param HeaderParts[] $parts
*/
protected function getValueFromParts(array $parts) : string
{
return \array_reduce($this->filterIgnoredSpaces($parts), fn ($c, $p) => $c . $p->getValue(), '');
}
/**
* Returns the child parts this container part consists of.
*
* @return IHeaderPart[]
*/
public function getChildParts() : array
{
return $this->children;
}
public function getComments() : array
{
return \array_merge(...\array_filter(\array_map(
fn ($p) => ($p instanceof CommentPart) ? [$p] : $p->getComments(),
$this->children
)));
}
/**
* Returns this part's children, same as getChildParts().
*
* @return ErrorBag
*/
protected function getErrorBagChildren() : array
{
return $this->children;
}
}
@@ -0,0 +1,72 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Header\Part;
use DateTime;
use Exception;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use ZBateson\MbWrapper\MbWrapper;
/**
* Represents the value of a date header, parsing the date into a \DateTime
* object.
*
* @author Zaahid Bateson
*/
class DatePart extends ContainerPart
{
/**
* @var DateTime the parsed date, or null if the date could not be parsed
*/
protected ?DateTime $date = null;
/**
* Tries parsing the passed token as an RFC 2822 date, and failing that into
* an RFC 822 date, and failing that, tries to parse it by calling
* new DateTime($value).
*
* @param HeaderPart[] $children
*/
public function __construct(
LoggerInterface $logger,
MbWrapper $charsetConverter,
array $children
) {
// parent::__construct converts character encoding -- may cause problems sometimes.
parent::__construct($logger, $charsetConverter, $children);
$this->value = $dateToken = \trim($this->value);
// Missing "+" in timezone definition. eg: Thu, 13 Mar 2014 15:02:47 0000 (not RFC compliant)
// Won't result in an Exception, but in a valid DateTime in year `0000` - therefore we need to check this first:
if (\preg_match('# [0-9]{4}$#', $dateToken)) {
$dateToken = \preg_replace('# ([0-9]{4})$#', ' +$1', $dateToken);
// @see https://bugs.php.net/bug.php?id=42486
} elseif (\preg_match('#UT$#', $dateToken)) {
$dateToken = $dateToken . 'C';
}
try {
$this->date = new DateTime($dateToken);
} catch (Exception $e) {
$this->addError(
"Unable to parse date from header: \"{$dateToken}\"",
LogLevel::ERROR,
$e
);
}
}
/**
* Returns a DateTime object or null if it can't be parsed.
*/
public function getDateTime() : ?DateTime
{
return $this->date;
}
}
@@ -0,0 +1,116 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Header\Part;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use ZBateson\MailMimeParser\ErrorBag;
use ZBateson\MailMimeParser\Header\IHeaderPart;
use ZBateson\MbWrapper\MbWrapper;
use ZBateson\MbWrapper\UnsupportedCharsetException;
/**
* Abstract base class representing a single part of a parsed header.
*
* @author Zaahid Bateson
*/
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
*/
protected bool $canIgnoreSpacesBefore = false;
/**
* @var bool set to true to ignore spaces after this part
*/
protected bool $canIgnoreSpacesAfter = false;
/**
* True if the part is a space token
*/
protected bool $isSpace = false;
public function __construct(LoggerInterface $logger, MbWrapper $charsetConverter, string $value)
{
parent::__construct($logger);
$this->charsetConverter = $charsetConverter;
$this->value = $value;
}
/**
* Returns the part's representative value after any necessary processing
* has been performed. For the raw value, call getRawValue().
*/
public function getValue() : string
{
return $this->value;
}
/**
* Returns the value of the part (which is a string).
*
* @return string the value
*/
public function __toString() : string
{
return $this->value;
}
/**
* Ensures the encoding of the passed string is set to UTF-8.
*
* The method does nothing if the passed $from charset is UTF-8 already, or
* if $force is set to false and mb_check_encoding for $str returns true
* for 'UTF-8'.
*
* @return string utf-8 string
*/
protected function convertEncoding(string $str, string $from = 'ISO-8859-1', bool $force = false) : string
{
if ($from !== 'UTF-8') {
// mime header part decoding will force it. This is necessary for
// UTF-7 because mb_check_encoding will return true
if ($force || !($this->charsetConverter->checkEncoding($str, 'UTF-8'))) {
try {
return $this->charsetConverter->convert($str, $from, 'UTF-8');
} catch (UnsupportedCharsetException $ce) {
$this->addError('Unable to convert charset', LogLevel::ERROR, $ce);
return $this->charsetConverter->convert($str, 'ISO-8859-1', 'UTF-8');
}
}
}
return $str;
}
public function getComments() : array
{
return [];
}
/**
* Default implementation returns an empty array.
*/
protected function getErrorBagChildren() : array
{
return [];
}
}
@@ -0,0 +1,176 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Header\Part;
use Psr\Log\LoggerInterface;
use ZBateson\MailMimeParser\Header\IHeaderPart;
use ZBateson\MbWrapper\MbWrapper;
/**
* Constructs and returns IHeaderPart objects.
*
* @author Zaahid Bateson
*/
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;
}
/**
* Creates and returns a default IHeaderPart for this factory, allowing
* subclass factories for specialized IHeaderParts.
*
* The default implementation returns a new Token
*/
public function newInstance(string $value) : IHeaderPart
{
return $this->newToken($value);
}
/**
* Initializes and returns a new Token.
*/
public function newToken(string $value, bool $isLiteral = false, bool $preserveSpaces = false) : Token
{
return new Token($this->logger, $this->charsetConverter, $value, $isLiteral, $preserveSpaces);
}
/**
* Initializes and returns a new SubjectToken.
*/
public function newSubjectToken(string $value) : SubjectToken
{
return new SubjectToken($this->logger, $this->charsetConverter, $value);
}
/**
* Initializes and returns a new MimeToken.
*/
public function newMimeToken(string $value) : MimeToken
{
return new MimeToken($this->logger, $this->charsetConverter, $value);
}
/**
* Initializes and returns a new ContainerPart.
*
* @param HeaderPart[] $children
*/
public function newContainerPart(array $children) : ContainerPart
{
return new ContainerPart($this->logger, $this->charsetConverter, $children);
}
/**
* Instantiates and returns a SplitParameterPart.
*
* @param ParameterPart[] $children
*/
public function newSplitParameterPart(array $children) : SplitParameterPart
{
return new SplitParameterPart($this->logger, $this->charsetConverter, $this, $children);
}
/**
* Initializes and returns a new QuotedLiteralPart.
*
* @param HeaderPart[] $parts
*/
public function newQuotedLiteralPart(array $parts) : QuotedLiteralPart
{
return new QuotedLiteralPart($this->logger, $this->charsetConverter, $parts);
}
/**
* Initializes and returns a new CommentPart.
*
* @param HeaderPart[] $children
*/
public function newCommentPart(array $children) : CommentPart
{
return new CommentPart($this->logger, $this->charsetConverter, $this, $children);
}
/**
* Initializes and returns a new AddressPart.
*
* @param HeaderPart[] $nameParts
* @param HeaderPart[] $emailParts
*/
public function newAddress(array $nameParts, array $emailParts) : AddressPart
{
return new AddressPart($this->logger, $this->charsetConverter, $nameParts, $emailParts);
}
/**
* Initializes and returns a new AddressGroupPart
*
* @param HeaderPart[] $nameParts
* @param AddressPart[]|AddressGroupPart[] $addressesAndGroups
*/
public function newAddressGroupPart(array $nameParts, array $addressesAndGroups) : AddressGroupPart
{
return new AddressGroupPart($this->logger, $this->charsetConverter, $nameParts, $addressesAndGroups);
}
/**
* Initializes and returns a new DatePart
*
* @param HeaderPart[] $children
*/
public function newDatePart(array $children) : DatePart
{
return new DatePart($this->logger, $this->charsetConverter, $children);
}
/**
* Initializes and returns a new ParameterPart.
*
* @param HeaderPart[] $nameParts
*/
public function newParameterPart(array $nameParts, ContainerPart $valuePart) : ParameterPart
{
return new ParameterPart($this->logger, $this->charsetConverter, $nameParts, $valuePart);
}
/**
* Initializes and returns a new ReceivedPart.
*
* @param HeaderPart[] $children
*/
public function newReceivedPart(string $name, array $children) : ReceivedPart
{
return new ReceivedPart($this->logger, $this->charsetConverter, $name, $children);
}
/**
* Initializes and returns a new ReceivedDomainPart.
*
* @param HeaderPart[] $children
*/
public function newReceivedDomainPart(string $name, array $children) : ReceivedDomainPart
{
return new ReceivedDomainPart(
$this->logger,
$this->charsetConverter,
$name,
$children
);
}
}
@@ -0,0 +1,109 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Header\Part;
use Psr\Log\LoggerInterface;
use ZBateson\MbWrapper\MbWrapper;
/**
* Represents a single mime header part token, with the possibility of it being
* MIME-Encoded as per RFC-2047.
*
* MimeToken automatically decodes the value if it's encoded.
*
* @author Zaahid Bateson
*/
class MimeToken extends Token
{
/**
* @var string regex pattern matching a mime-encoded part
*/
public const MIME_PART_PATTERN = '=\?[^?=]+\?[QBqb]\?[^\?]+\?=';
/**
* @var string regex pattern used when parsing parameterized headers
*/
public const MIME_PART_PATTERN_NO_QUOTES = '=\?[^\?=]+\?[QBqb]\?[^\?"]+\?=';
/**
* @var ?string the language code if any, or null otherwise
*/
protected ?string $language = null;
/**
* @var ?string the charset if any, or null otherwise
*/
protected ?string $charset = null;
public function __construct(LoggerInterface $logger, MbWrapper $charsetConverter, string $value)
{
parent::__construct($logger, $charsetConverter, $value);
$this->value = $this->decodeMime(\preg_replace('/\r|\n/', '', $this->value));
$pattern = self::MIME_PART_PATTERN;
$this->canIgnoreSpacesBefore = (bool) \preg_match("/^\s*{$pattern}|\s+/", $this->rawValue);
$this->canIgnoreSpacesAfter = (bool) \preg_match("/{$pattern}\s*|\s+\$/", $this->rawValue);
}
/**
* Finds and replaces mime parts with their values.
*
* The method splits the token value into an array on mime-part-patterns,
* either replacing a mime part with its value by calling iconv_mime_decode
* or converts the encoding on the text part by calling convertEncoding.
*/
protected function decodeMime(string $value) : string
{
if (\preg_match('/^=\?([A-Za-z\-_0-9]+)\*?([A-Za-z\-_0-9]+)?\?([QBqb])\?([^\?]*)\?=$/', $value, $matches)) {
return $this->decodeMatchedEntity($matches);
}
return $this->convertEncoding($value);
}
/**
* Decodes a matched mime entity part into a string and returns it, after
* adding the string into the languages array.
*
* @param string[] $matches
*/
private function decodeMatchedEntity(array $matches) : string
{
$body = $matches[4];
if (\strtoupper($matches[3]) === 'Q') {
$body = \quoted_printable_decode(\str_replace('_', '=20', $body));
} else {
$body = \base64_decode($body);
}
$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);
}
/**
* Returns the language code for the mime part.
*/
public function getLanguage() : ?string
{
return $this->language;
}
/**
* Returns the charset for the encoded part.
*/
public function getCharset() : ?string
{
return $this->charset;
}
public function getRawValue() : string
{
return $this->rawValue;
}
}
@@ -0,0 +1,27 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Header\Part;
use ZBateson\MailMimeParser\Header\IHeaderPart;
/**
* Extends HeaderPartFactory to instantiate MimeTokens for its
* newInstance method.
*
* @author Zaahid Bateson
*/
class MimeTokenPartFactory extends HeaderPartFactory
{
/**
* Creates and returns a MimeToken.
*/
public function newInstance(string $value) : IHeaderPart
{
return $this->newMimeToken($value);
}
}
@@ -0,0 +1,65 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Header\Part;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use ZBateson\MailMimeParser\ErrorBag;
use ZBateson\MbWrapper\MbWrapper;
/**
* Represents a name/value pair part of a header.
*
* @author Zaahid Bateson
*/
class NameValuePart extends ContainerPart
{
/**
* @var string the name of the part
*/
protected string $name;
public function __construct(
LoggerInterface $logger,
MbWrapper $charsetConverter,
array $nameParts,
array $valueParts
) {
ErrorBag::__construct($logger);
$this->charsetConverter = $charsetConverter;
$this->name = (!empty($nameParts)) ? $this->getNameFromParts($nameParts) : '';
parent::__construct($logger, $charsetConverter, $valueParts);
\array_unshift($this->children, ...$nameParts);
}
/**
* Creates the string 'name' representation of this part constructed from
* the child name parts passed to it.
*
* @param HeaderParts[] $parts
*/
protected function getNameFromParts(array $parts) : string
{
return \array_reduce($this->filterIgnoredSpaces($parts), fn ($c, $p) => $c . $p->getValue(), '');
}
/**
* Returns the name of the name/value part.
*/
public function getName() : string
{
return $this->name;
}
protected function validate() : void
{
if ($this->value === '') {
$this->addError('NameValuePart value is empty', LogLevel::NOTICE);
}
}
}
@@ -0,0 +1,119 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Header\Part;
use Psr\Log\LoggerInterface;
use ZBateson\MbWrapper\MbWrapper;
/**
* Represents a name/value parameter part of a header.
*
* @author Zaahid Bateson
*/
class ParameterPart extends NameValuePart
{
/**
* @var string the RFC-1766 language tag if set.
*/
protected ?string $language = null;
/**
* @var string charset of content if set.
*/
protected ?string $charset = null;
/**
* @var int the zero-based index of the part if part of a 'continuation' in
* an RFC-2231 split parameter.
*/
protected ?int $index = null;
/**
* @var bool true if the part is an RFC-2231 encoded part, and the value
* needs to be decoded.
*/
protected bool $encoded = false;
/**
* @param HeaderPart[] $nameParts
*/
public function __construct(
LoggerInterface $logger,
MbWrapper $charsetConverter,
array $nameParts,
ContainerPart $valuePart
) {
parent::__construct($logger, $charsetConverter, $nameParts, $valuePart->children);
}
protected function getNameFromParts(array $parts) : string
{
$name = parent::getNameFromParts($parts);
if (\preg_match('~^\s*([^\*]+)\*(\d*)(\*)?$~', $name, $matches)) {
$name = $matches[1];
$this->index = ($matches[2] !== '') ? (int) ($matches[2]) : null;
$this->encoded = (($matches[2] === '') || !empty($matches[3]));
}
return $name;
}
protected function decodePartValue(string $value, ?string $charset = null) : string
{
if ($charset !== null) {
return $this->convertEncoding(\rawurldecode($value), $charset, true);
}
return $this->convertEncoding(\rawurldecode($value));
}
protected function getValueFromParts(array $parts) : string
{
$value = parent::getValueFromParts($parts);
if ($this->encoded && \preg_match('~^([^\']*)\'?([^\']*)\'?(.*)$~', $value, $matches)) {
$this->charset = (!empty($matches[1]) && !empty($matches[3])) ? $matches[1] : $this->charset;
$this->language = (!empty($matches[2])) ? $matches[2] : $this->language;
$ev = (empty($matches[3])) ? $matches[1] : $matches[3];
// only if it's not part of a SplitParameterPart
if ($this->index === null) {
// subsequent parts are decoded as a SplitParameterPart since only
// the first part are supposed to have charset/language fields
return $this->decodePartValue($ev, $this->charset);
}
return $ev;
}
return $value;
}
/**
* Returns the charset if the part is an RFC-2231 part with a charset set.
*/
public function getCharset() : ?string
{
return $this->charset;
}
/**
* Returns the RFC-1766 (or subset) language tag, if the parameter is an
* RFC-2231 part with a language tag set.
*
* @return ?string the language if set, or null if not
*/
public function getLanguage() : ?string
{
return $this->language;
}
public function isUrlEncoded() : bool
{
return $this->encoded;
}
public function getIndex() : ?int
{
return $this->index;
}
}
@@ -0,0 +1,46 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Header\Part;
/**
* A quoted literal header string part. The value of the part is stripped of CR
* and LF characters, and whitespace between two adjacent MimeTokens is removed.
*
* @author Zaahid Bateson
*/
class QuotedLiteralPart extends ContainerPart
{
/**
* Strips spaces found between two adjacent MimeToken parts.
* Other whitespace is returned as-is.
*
* @param HeaderPart[] $parts
* @return HeaderPart[]
*/
protected function filterIgnoredSpaces(array $parts) : array
{
$filtered = \array_reduce(
\array_keys($parts),
function($carry, $key) use ($parts) {
$cur = $parts[$key];
$last = ($carry !== null) ? \end($carry) : null;
$next = (count($parts) > $key + 1) ? $parts[$key + 1] : null;
if ($last !== null && $next !== null && $cur->isSpace && (
$last->canIgnoreSpacesAfter
&& $next->canIgnoreSpacesBefore
&& $last instanceof MimeToken
&& $next instanceof MimeToken
)) {
return $carry;
}
return \array_merge($carry ?? [], [$cur]);
}
);
return $filtered;
}
}
@@ -0,0 +1,103 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Header\Part;
use Psr\Log\LoggerInterface;
use ZBateson\MbWrapper\MbWrapper;
/**
* Holds extra information about a parsed Received header part, for FROM and BY
* parts, namely: ehlo name, hostname, and address.
*
* The parsed parts would be mapped as follows:
*
* FROM ehlo name (hostname [address]), for example: FROM computer (domain.com
* [1.2.3.4]) would contain "computer" for getEhloName(), domain.com for
* getHostname and 1.2.3.4 for getAddress().
*
* This doesn't change if the ehlo name is an address, it is still returned in
* getEhloName(), and not in getAddress(). Additionally square brackets are not
* stripped from getEhloName() if its an address. For example: "FROM [1.2.3.4]"
* would return "[1.2.3.4]" in a call to getEhloName().
*
* For further information on how the header's parsed, check the documentation
* for {@see \ZBateson\MailMimeParser\Header\Consumer\Received\DomainConsumer}.
*
* @author Zaahid Bateson
*/
class ReceivedDomainPart extends ReceivedPart
{
/**
* @var string The name used to identify the server in the EHLO line.
*/
protected ?string $ehloName = null;
/**
* @var string The hostname.
*/
protected ?string $hostname = null;
/**
* @var string The address.
*/
protected ?string $address = null;
/**
* @param HeaderPart[] $children
*/
public function __construct(
LoggerInterface $logger,
MbWrapper $charsetConverter,
string $name,
array $children
) {
parent::__construct($logger, $charsetConverter, $name, $children);
$this->ehloName = ($this->value !== '') ? $this->value : null;
$cps = $this->getComments();
$commentPart = (!empty($cps)) ? $cps[0] : null;
$pattern = '~^(\[(IPv[64])?(?P<addr1>[a-f\d\.\:]+)\])?\s*(helo=)?(?P<name>[a-z0-9\-]+[a-z0-9\-\.]+)?\s*(\[(IPv[64])?(?P<addr2>[a-f\d\.\:]+)\])?$~i';
if ($commentPart !== null && \preg_match($pattern, $commentPart->getComment(), $matches)) {
$this->value .= ' (' . $commentPart->getComment() . ')';
$this->hostname = (!empty($matches['name'])) ? $matches['name'] : null;
$this->address = (!empty($matches['addr1'])) ? $matches['addr1'] : ((!empty($matches['addr2'])) ? $matches['addr2'] : null);
}
}
/**
* Returns the name used to identify the server in the first part of the
* extended-domain line.
*
* Note that this is not necessarily the name used in the EHLO line to an
* SMTP server, since implementations differ so much, not much can be
* guaranteed except the position it was parsed in.
*/
public function getEhloName() : ?string
{
return $this->ehloName;
}
/**
* Returns the hostname of the server, or whatever string in the hostname
* position when parsing (but never an address).
*/
public function getHostname() : ?string
{
return $this->hostname;
}
/**
* Returns the address of the server, or whatever string that looks like an
* address in the address position when parsing (but never a hostname).
*/
public function getAddress() : ?string
{
return $this->address;
}
}
@@ -0,0 +1,37 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Header\Part;
use Psr\Log\LoggerInterface;
use ZBateson\MbWrapper\MbWrapper;
/**
* Represents one parameter in a parsed 'Received' header, e.g. the FROM or VIA
* part.
*
* Note that FROM and BY actually get parsed into a sub-class,
* ReceivedDomainPart which keeps track of other sub-parts that can be parsed
* from them.
*
* @author Zaahid Bateson
*/
class ReceivedPart extends NameValuePart
{
/**
* @param HeaderPart[] $children
*/
public function __construct(
LoggerInterface $logger,
MbWrapper $charsetConverter,
string $name,
array $children
) {
parent::__construct($logger, $charsetConverter, [], $children);
$this->name = $name;
}
}
@@ -0,0 +1,102 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Header\Part;
use Psr\Log\LoggerInterface;
use ZBateson\MbWrapper\MbWrapper;
/**
* Holds a running value for an RFC-2231 split header parameter.
*
* ParameterConsumer creates SplitParameterTokens when a split header parameter
* is first found, and adds subsequent split parts to an already created one if
* the parameter name matches.
*
* @author Zaahid Bateson
*/
class SplitParameterPart extends ParameterPart
{
/**
* @var HeaderPartFactory used to create combined MimeToken parts.
*/
protected HeaderPartFactory $partFactory;
/**
* Initializes a SplitParameterToken.
*
* @param ParameterPart[] $children
*/
public function __construct(
LoggerInterface $logger,
MbWrapper $charsetConverter,
HeaderPartFactory $headerPartFactory,
array $children
) {
$this->partFactory = $headerPartFactory;
NameValuePart::__construct($logger, $charsetConverter, [$children[0]], $children);
$this->children = $children;
}
protected function getNameFromParts(array $parts) : string
{
return $parts[0]->getName();
}
private function getMimeTokens(string $value) : array
{
$pattern = MimeToken::MIME_PART_PATTERN;
// remove whitespace between two adjacent mime encoded parts
$normed = \preg_replace("/($pattern)\\s+(?=$pattern)/", '$1', $value);
// with PREG_SPLIT_DELIM_CAPTURE, matched and unmatched parts are returned
$aMimeParts = \preg_split("/($pattern)/", $normed, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
return \array_map(
fn ($p) => (\preg_match("/$pattern/", $p)) ? $this->partFactory->newMimeToken($p) : $this->partFactory->newToken($p, true, true),
$aMimeParts
);
}
private function combineAdjacentUnencodedParts(array $parts) : array
{
$runningValue = '';
$returnedParts = [];
foreach ($parts as $part) {
if (!$part->encoded) {
$runningValue .= $part->value;
continue;
}
if (!empty($runningValue)) {
$returnedParts = \array_merge($returnedParts, $this->getMimeTokens($runningValue));
$runningValue = '';
}
$returnedParts[] = $part;
}
if (!empty($runningValue)) {
$returnedParts = \array_merge($returnedParts, $this->getMimeTokens($runningValue));
}
return $returnedParts;
}
protected function getValueFromParts(array $parts) : string
{
$sorted = $parts;
\usort($sorted, fn ($a, $b) => $a->index <=> $b->index);
$first = $sorted[0];
$this->language = $first->language;
$charset = $this->charset = $first->charset;
$combined = $this->combineAdjacentUnencodedParts($sorted);
return \implode('', \array_map(
fn ($p) => ($p instanceof ParameterPart && $p->encoded)
? $this->decodePartValue($p->getValue(), ($p->charset === null) ? $charset : $p->charset)
: $p->getValue(),
$combined
));
}
}
@@ -0,0 +1,40 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Header\Part;
use Psr\Log\LoggerInterface;
use ZBateson\MbWrapper\MbWrapper;
/**
* Specialized token for subjects that preserves whitespace, except for new
* lines.
*
* New lines are either discarded if followed by a whitespace as should happen
* with folding whitespace, or replaced by a single space character if somehow
* aren't followed by whitespace.
*
* @author Zaahid Bateson
*/
class SubjectToken extends Token
{
public function __construct(
LoggerInterface $logger,
MbWrapper $charsetConverter,
string $value
) {
parent::__construct($logger, $charsetConverter, $value, true);
$this->value = \preg_replace(['/(\r|\n)+(\s)\s*/', '/(\r|\n)+/'], ['$2', ' '], $value);
$this->isSpace = (\preg_match('/^\s*$/m', $this->value) === 1);
$this->canIgnoreSpacesBefore = $this->canIgnoreSpacesAfter = $this->isSpace;
}
public function getValue() : string
{
return $this->convertEncoding($this->value);
}
}
@@ -0,0 +1,66 @@
<?php
/**
* This file is part of the ZBateson\MailMimeParser project.
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*/
namespace ZBateson\MailMimeParser\Header\Part;
use Psr\Log\LoggerInterface;
use ZBateson\MbWrapper\MbWrapper;
/**
* Holds a string value token that will require additional processing by a
* consumer prior to returning to a client.
*
* A Token is meant to hold a value for further processing -- for instance when
* consuming an address list header (like From or To) -- before it's known what
* type of IHeaderPart it is (could be an email address, could be a name, or
* could be a group.)
*
* @author Zaahid Bateson
*/
class Token extends HeaderPart
{
/**
* @var string the raw value of the part.
*/
protected string $rawValue;
public function __construct(
LoggerInterface $logger,
MbWrapper $charsetConverter,
string $value,
bool $isLiteral = false,
bool $preserveSpaces = false
) {
parent::__construct($logger, $charsetConverter, $value);
$this->rawValue = $value;
if (!$isLiteral) {
$this->value = \preg_replace(['/(\r|\n)+(\s)/', '/(\r|\n)+/'], ['$2', ' '], $value);
if (!$preserveSpaces) {
$this->value = \preg_replace('/^\s+$/m', ' ', $this->value);
}
}
$this->isSpace = ($this->value === '' || (!$isLiteral && \preg_match('/^\s*$/m', $this->value) === 1));
$this->canIgnoreSpacesBefore = $this->canIgnoreSpacesAfter = $this->isSpace;
}
/**
* Returns the part's representative value after any necessary processing
* has been performed. For the raw value, call getRawValue().
*/
public function getValue() : string
{
return $this->convertEncoding($this->value);
}
/**
* Returns the part's raw value.
*/
public function getRawValue() : string
{
return $this->rawValue;
}
}