first commit

This commit is contained in:
Sampanna Rimal
2024-08-27 17:48:06 +05:45
commit 53c0140f58
10839 changed files with 1125847 additions and 0 deletions

120
vendor/symfony/mime/Address.php vendored Normal file
View File

@ -0,0 +1,120 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime;
use Egulias\EmailValidator\EmailValidator;
use Egulias\EmailValidator\Validation\MessageIDValidation;
use Egulias\EmailValidator\Validation\RFCValidation;
use Symfony\Component\Mime\Encoder\IdnAddressEncoder;
use Symfony\Component\Mime\Exception\InvalidArgumentException;
use Symfony\Component\Mime\Exception\LogicException;
use Symfony\Component\Mime\Exception\RfcComplianceException;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class Address
{
/**
* A regex that matches a structure like 'Name <email@address.com>'.
* It matches anything between the first < and last > as email address.
* This allows to use a single string to construct an Address, which can be convenient to use in
* config, and allows to have more readable config.
* This does not try to cover all edge cases for address.
*/
private const FROM_STRING_PATTERN = '~(?<displayName>[^<]*)<(?<addrSpec>.*)>[^>]*~';
private static EmailValidator $validator;
private static IdnAddressEncoder $encoder;
private string $address;
private string $name;
public function __construct(string $address, string $name = '')
{
if (!class_exists(EmailValidator::class)) {
throw new LogicException(sprintf('The "%s" class cannot be used as it needs "%s". Try running "composer require egulias/email-validator".', __CLASS__, EmailValidator::class));
}
self::$validator ??= new EmailValidator();
$this->address = trim($address);
$this->name = trim(str_replace(["\n", "\r"], '', $name));
if (!self::$validator->isValid($this->address, class_exists(MessageIDValidation::class) ? new MessageIDValidation() : new RFCValidation())) {
throw new RfcComplianceException(sprintf('Email "%s" does not comply with addr-spec of RFC 2822.', $address));
}
}
public function getAddress(): string
{
return $this->address;
}
public function getName(): string
{
return $this->name;
}
public function getEncodedAddress(): string
{
self::$encoder ??= new IdnAddressEncoder();
return self::$encoder->encodeString($this->address);
}
public function toString(): string
{
return ($n = $this->getEncodedName()) ? $n.' <'.$this->getEncodedAddress().'>' : $this->getEncodedAddress();
}
public function getEncodedName(): string
{
if ('' === $this->getName()) {
return '';
}
return sprintf('"%s"', preg_replace('/"/u', '\"', $this->getName()));
}
public static function create(self|string $address): self
{
if ($address instanceof self) {
return $address;
}
if (!str_contains($address, '<')) {
return new self($address);
}
if (!preg_match(self::FROM_STRING_PATTERN, $address, $matches)) {
throw new InvalidArgumentException(sprintf('Could not parse "%s" to a "%s" instance.', $address, self::class));
}
return new self($matches['addrSpec'], trim($matches['displayName'], ' \'"'));
}
/**
* @param array<Address|string> $addresses
*
* @return Address[]
*/
public static function createArray(array $addresses): array
{
$addrs = [];
foreach ($addresses as $address) {
$addrs[] = self::create($address);
}
return $addrs;
}
}

View File

@ -0,0 +1,20 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
interface BodyRendererInterface
{
public function render(Message $message): void;
}

50
vendor/symfony/mime/CHANGELOG.md vendored Normal file
View File

@ -0,0 +1,50 @@
CHANGELOG
=========
6.3
---
* Support detection of related parts if `Content-Id` is used instead of the name
* Add `TextPart::getDisposition()`
6.2
---
* Add `File`
* Deprecate `Email::attachPart()`, use `addPart()` instead
* Deprecate calling `Message::setBody()` without arguments
6.1
---
* Add `DataPart::getFilename()` and `DataPart::getContentType()`
6.0
---
* Remove `Address::fromString()`, use `Address::create()` instead
* Remove `Serializable` interface from `RawMessage`
5.2.0
-----
* Add support for DKIM
* Deprecated `Address::fromString()`, use `Address::create()` instead
4.4.0
-----
* [BC BREAK] Removed `NamedAddress` (`Address` now supports a name)
* Added PHPUnit constraints
* Added `AbstractPart::asDebugString()`
* Added `Address::fromString()`
4.3.3
-----
* [BC BREAK] Renamed method `Headers::getAll()` to `Headers::all()`.
4.3.0
-----
* Introduced the component as experimental

211
vendor/symfony/mime/CharacterStream.php vendored Normal file
View File

@ -0,0 +1,211 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Xavier De Cock <xdecock@gmail.com>
*
* @internal
*/
final class CharacterStream
{
/** Pre-computed for optimization */
private const UTF8_LENGTH_MAP = [
"\x00" => 1, "\x01" => 1, "\x02" => 1, "\x03" => 1, "\x04" => 1, "\x05" => 1, "\x06" => 1, "\x07" => 1,
"\x08" => 1, "\x09" => 1, "\x0a" => 1, "\x0b" => 1, "\x0c" => 1, "\x0d" => 1, "\x0e" => 1, "\x0f" => 1,
"\x10" => 1, "\x11" => 1, "\x12" => 1, "\x13" => 1, "\x14" => 1, "\x15" => 1, "\x16" => 1, "\x17" => 1,
"\x18" => 1, "\x19" => 1, "\x1a" => 1, "\x1b" => 1, "\x1c" => 1, "\x1d" => 1, "\x1e" => 1, "\x1f" => 1,
"\x20" => 1, "\x21" => 1, "\x22" => 1, "\x23" => 1, "\x24" => 1, "\x25" => 1, "\x26" => 1, "\x27" => 1,
"\x28" => 1, "\x29" => 1, "\x2a" => 1, "\x2b" => 1, "\x2c" => 1, "\x2d" => 1, "\x2e" => 1, "\x2f" => 1,
"\x30" => 1, "\x31" => 1, "\x32" => 1, "\x33" => 1, "\x34" => 1, "\x35" => 1, "\x36" => 1, "\x37" => 1,
"\x38" => 1, "\x39" => 1, "\x3a" => 1, "\x3b" => 1, "\x3c" => 1, "\x3d" => 1, "\x3e" => 1, "\x3f" => 1,
"\x40" => 1, "\x41" => 1, "\x42" => 1, "\x43" => 1, "\x44" => 1, "\x45" => 1, "\x46" => 1, "\x47" => 1,
"\x48" => 1, "\x49" => 1, "\x4a" => 1, "\x4b" => 1, "\x4c" => 1, "\x4d" => 1, "\x4e" => 1, "\x4f" => 1,
"\x50" => 1, "\x51" => 1, "\x52" => 1, "\x53" => 1, "\x54" => 1, "\x55" => 1, "\x56" => 1, "\x57" => 1,
"\x58" => 1, "\x59" => 1, "\x5a" => 1, "\x5b" => 1, "\x5c" => 1, "\x5d" => 1, "\x5e" => 1, "\x5f" => 1,
"\x60" => 1, "\x61" => 1, "\x62" => 1, "\x63" => 1, "\x64" => 1, "\x65" => 1, "\x66" => 1, "\x67" => 1,
"\x68" => 1, "\x69" => 1, "\x6a" => 1, "\x6b" => 1, "\x6c" => 1, "\x6d" => 1, "\x6e" => 1, "\x6f" => 1,
"\x70" => 1, "\x71" => 1, "\x72" => 1, "\x73" => 1, "\x74" => 1, "\x75" => 1, "\x76" => 1, "\x77" => 1,
"\x78" => 1, "\x79" => 1, "\x7a" => 1, "\x7b" => 1, "\x7c" => 1, "\x7d" => 1, "\x7e" => 1, "\x7f" => 1,
"\x80" => 0, "\x81" => 0, "\x82" => 0, "\x83" => 0, "\x84" => 0, "\x85" => 0, "\x86" => 0, "\x87" => 0,
"\x88" => 0, "\x89" => 0, "\x8a" => 0, "\x8b" => 0, "\x8c" => 0, "\x8d" => 0, "\x8e" => 0, "\x8f" => 0,
"\x90" => 0, "\x91" => 0, "\x92" => 0, "\x93" => 0, "\x94" => 0, "\x95" => 0, "\x96" => 0, "\x97" => 0,
"\x98" => 0, "\x99" => 0, "\x9a" => 0, "\x9b" => 0, "\x9c" => 0, "\x9d" => 0, "\x9e" => 0, "\x9f" => 0,
"\xa0" => 0, "\xa1" => 0, "\xa2" => 0, "\xa3" => 0, "\xa4" => 0, "\xa5" => 0, "\xa6" => 0, "\xa7" => 0,
"\xa8" => 0, "\xa9" => 0, "\xaa" => 0, "\xab" => 0, "\xac" => 0, "\xad" => 0, "\xae" => 0, "\xaf" => 0,
"\xb0" => 0, "\xb1" => 0, "\xb2" => 0, "\xb3" => 0, "\xb4" => 0, "\xb5" => 0, "\xb6" => 0, "\xb7" => 0,
"\xb8" => 0, "\xb9" => 0, "\xba" => 0, "\xbb" => 0, "\xbc" => 0, "\xbd" => 0, "\xbe" => 0, "\xbf" => 0,
"\xc0" => 2, "\xc1" => 2, "\xc2" => 2, "\xc3" => 2, "\xc4" => 2, "\xc5" => 2, "\xc6" => 2, "\xc7" => 2,
"\xc8" => 2, "\xc9" => 2, "\xca" => 2, "\xcb" => 2, "\xcc" => 2, "\xcd" => 2, "\xce" => 2, "\xcf" => 2,
"\xd0" => 2, "\xd1" => 2, "\xd2" => 2, "\xd3" => 2, "\xd4" => 2, "\xd5" => 2, "\xd6" => 2, "\xd7" => 2,
"\xd8" => 2, "\xd9" => 2, "\xda" => 2, "\xdb" => 2, "\xdc" => 2, "\xdd" => 2, "\xde" => 2, "\xdf" => 2,
"\xe0" => 3, "\xe1" => 3, "\xe2" => 3, "\xe3" => 3, "\xe4" => 3, "\xe5" => 3, "\xe6" => 3, "\xe7" => 3,
"\xe8" => 3, "\xe9" => 3, "\xea" => 3, "\xeb" => 3, "\xec" => 3, "\xed" => 3, "\xee" => 3, "\xef" => 3,
"\xf0" => 4, "\xf1" => 4, "\xf2" => 4, "\xf3" => 4, "\xf4" => 4, "\xf5" => 4, "\xf6" => 4, "\xf7" => 4,
"\xf8" => 5, "\xf9" => 5, "\xfa" => 5, "\xfb" => 5, "\xfc" => 6, "\xfd" => 6, "\xfe" => 0, "\xff" => 0,
];
private string $data = '';
private int $dataSize = 0;
private array $map = [];
private int $charCount = 0;
private int $currentPos = 0;
private int $fixedWidth = 0;
/**
* @param resource|string $input
*/
public function __construct($input, ?string $charset = 'utf-8')
{
$charset = strtolower(trim($charset)) ?: 'utf-8';
if ('utf-8' === $charset || 'utf8' === $charset) {
$this->fixedWidth = 0;
$this->map = ['p' => [], 'i' => []];
} else {
$this->fixedWidth = match ($charset) {
// 16 bits
'ucs2',
'ucs-2',
'utf16',
'utf-16' => 2,
// 32 bits
'ucs4',
'ucs-4',
'utf32',
'utf-32' => 4,
// 7-8 bit charsets: (us-)?ascii, (iso|iec)-?8859-?[0-9]+, windows-?125[0-9], cp-?[0-9]+, ansi, macintosh,
// koi-?7, koi-?8-?.+, mik, (cork|t1), v?iscii
// and fallback
default => 1,
};
}
if (\is_resource($input)) {
$blocks = 16372;
while (false !== $read = fread($input, $blocks)) {
$this->write($read);
}
} else {
$this->write($input);
}
}
public function read(int $length): ?string
{
if ($this->currentPos >= $this->charCount) {
return null;
}
$length = ($this->currentPos + $length > $this->charCount) ? $this->charCount - $this->currentPos : $length;
if ($this->fixedWidth > 0) {
$len = $length * $this->fixedWidth;
$ret = substr($this->data, $this->currentPos * $this->fixedWidth, $len);
$this->currentPos += $length;
} else {
$end = $this->currentPos + $length;
$end = $end > $this->charCount ? $this->charCount : $end;
$ret = '';
$start = 0;
if ($this->currentPos > 0) {
$start = $this->map['p'][$this->currentPos - 1];
}
$to = $start;
for (; $this->currentPos < $end; ++$this->currentPos) {
if (isset($this->map['i'][$this->currentPos])) {
$ret .= substr($this->data, $start, $to - $start).'?';
$start = $this->map['p'][$this->currentPos];
} else {
$to = $this->map['p'][$this->currentPos];
}
}
$ret .= substr($this->data, $start, $to - $start);
}
return $ret;
}
public function readBytes(int $length): ?array
{
if (null !== $read = $this->read($length)) {
return array_map('ord', str_split($read, 1));
}
return null;
}
public function setPointer(int $charOffset): void
{
if ($this->charCount < $charOffset) {
$charOffset = $this->charCount;
}
$this->currentPos = $charOffset;
}
public function write(string $chars): void
{
$ignored = '';
$this->data .= $chars;
if ($this->fixedWidth > 0) {
$strlen = \strlen($chars);
$ignoredL = $strlen % $this->fixedWidth;
$ignored = $ignoredL ? substr($chars, -$ignoredL) : '';
$this->charCount += ($strlen - $ignoredL) / $this->fixedWidth;
} else {
$this->charCount += $this->getUtf8CharPositions($chars, $this->dataSize, $ignored);
}
$this->dataSize = \strlen($this->data) - \strlen($ignored);
}
private function getUtf8CharPositions(string $string, int $startOffset, string &$ignoredChars): int
{
$strlen = \strlen($string);
$charPos = \count($this->map['p']);
$foundChars = 0;
$invalid = false;
for ($i = 0; $i < $strlen; ++$i) {
$char = $string[$i];
$size = self::UTF8_LENGTH_MAP[$char];
if (0 == $size) {
/* char is invalid, we must wait for a resync */
$invalid = true;
continue;
}
if ($invalid) {
/* We mark the chars as invalid and start a new char */
$this->map['p'][$charPos + $foundChars] = $startOffset + $i;
$this->map['i'][$charPos + $foundChars] = true;
++$foundChars;
$invalid = false;
}
if (($i + $size) > $strlen) {
$ignoredChars = substr($string, $i);
break;
}
for ($j = 1; $j < $size; ++$j) {
$char = $string[$i + $j];
if ($char > "\x7F" && $char < "\xC0") {
// Valid - continue parsing
} else {
/* char is invalid, we must wait for a resync */
$invalid = true;
continue 2;
}
}
/* Ok we got a complete char here */
$this->map['p'][$charPos + $foundChars] = $startOffset + $i + $size;
$i += $j - 1;
++$foundChars;
}
return $foundChars;
}
}

View File

@ -0,0 +1,97 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Crypto;
/**
* A helper providing autocompletion for available DkimSigner options.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class DkimOptions
{
private array $options = [];
public function toArray(): array
{
return $this->options;
}
/**
* @return $this
*/
public function algorithm(string $algo): static
{
$this->options['algorithm'] = $algo;
return $this;
}
/**
* @return $this
*/
public function signatureExpirationDelay(int $show): static
{
$this->options['signature_expiration_delay'] = $show;
return $this;
}
/**
* @return $this
*/
public function bodyMaxLength(int $max): static
{
$this->options['body_max_length'] = $max;
return $this;
}
/**
* @return $this
*/
public function bodyShowLength(bool $show): static
{
$this->options['body_show_length'] = $show;
return $this;
}
/**
* @return $this
*/
public function headerCanon(string $canon): static
{
$this->options['header_canon'] = $canon;
return $this;
}
/**
* @return $this
*/
public function bodyCanon(string $canon): static
{
$this->options['body_canon'] = $canon;
return $this;
}
/**
* @return $this
*/
public function headersToIgnore(array $headers): static
{
$this->options['headers_to_ignore'] = $headers;
return $this;
}
}

View File

@ -0,0 +1,217 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Crypto;
use Symfony\Component\Mime\Exception\InvalidArgumentException;
use Symfony\Component\Mime\Exception\RuntimeException;
use Symfony\Component\Mime\Header\UnstructuredHeader;
use Symfony\Component\Mime\Message;
use Symfony\Component\Mime\Part\AbstractPart;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* RFC 6376 and 8301
*/
final class DkimSigner
{
public const CANON_SIMPLE = 'simple';
public const CANON_RELAXED = 'relaxed';
public const ALGO_SHA256 = 'rsa-sha256';
public const ALGO_ED25519 = 'ed25519-sha256'; // RFC 8463
private \OpenSSLAsymmetricKey $key;
private string $domainName;
private string $selector;
private array $defaultOptions;
/**
* @param string $pk The private key as a string or the path to the file containing the private key, should be prefixed with file:// (in PEM format)
* @param string $passphrase A passphrase of the private key (if any)
*/
public function __construct(string $pk, string $domainName, string $selector, array $defaultOptions = [], string $passphrase = '')
{
if (!\extension_loaded('openssl')) {
throw new \LogicException('PHP extension "openssl" is required to use DKIM.');
}
$this->key = openssl_pkey_get_private($pk, $passphrase) ?: throw new InvalidArgumentException('Unable to load DKIM private key: '.openssl_error_string());
$this->domainName = $domainName;
$this->selector = $selector;
$this->defaultOptions = $defaultOptions + [
'algorithm' => self::ALGO_SHA256,
'signature_expiration_delay' => 0,
'body_max_length' => \PHP_INT_MAX,
'body_show_length' => false,
'header_canon' => self::CANON_RELAXED,
'body_canon' => self::CANON_RELAXED,
'headers_to_ignore' => [],
];
}
public function sign(Message $message, array $options = []): Message
{
$options += $this->defaultOptions;
if (!\in_array($options['algorithm'], [self::ALGO_SHA256, self::ALGO_ED25519], true)) {
throw new InvalidArgumentException(sprintf('Invalid DKIM signing algorithm "%s".', $options['algorithm']));
}
$headersToIgnore['return-path'] = true;
$headersToIgnore['x-transport'] = true;
foreach ($options['headers_to_ignore'] as $name) {
$headersToIgnore[strtolower($name)] = true;
}
unset($headersToIgnore['from']);
$signedHeaderNames = [];
$headerCanonData = '';
$headers = $message->getPreparedHeaders();
foreach ($headers->getNames() as $name) {
foreach ($headers->all($name) as $header) {
if (isset($headersToIgnore[strtolower($header->getName())])) {
continue;
}
if ('' !== $header->getBodyAsString()) {
$headerCanonData .= $this->canonicalizeHeader($header->toString(), $options['header_canon']);
$signedHeaderNames[] = $header->getName();
}
}
}
[$bodyHash, $bodyLength] = $this->hashBody($message->getBody(), $options['body_canon'], $options['body_max_length']);
$params = [
'v' => '1',
'q' => 'dns/txt',
'a' => $options['algorithm'],
'bh' => base64_encode($bodyHash),
'd' => $this->domainName,
'h' => implode(': ', $signedHeaderNames),
'i' => '@'.$this->domainName,
's' => $this->selector,
't' => time(),
'c' => $options['header_canon'].'/'.$options['body_canon'],
];
if ($options['body_show_length']) {
$params['l'] = $bodyLength;
}
if ($options['signature_expiration_delay']) {
$params['x'] = $params['t'] + $options['signature_expiration_delay'];
}
$value = '';
foreach ($params as $k => $v) {
$value .= $k.'='.$v.'; ';
}
$value = trim($value);
$header = new UnstructuredHeader('DKIM-Signature', $value);
$headerCanonData .= rtrim($this->canonicalizeHeader($header->toString()."\r\n b=", $options['header_canon']));
if (self::ALGO_SHA256 === $options['algorithm']) {
if (!openssl_sign($headerCanonData, $signature, $this->key, \OPENSSL_ALGO_SHA256)) {
throw new RuntimeException('Unable to sign DKIM hash: '.openssl_error_string());
}
} else {
throw new \RuntimeException(sprintf('The "%s" DKIM signing algorithm is not supported yet.', self::ALGO_ED25519));
}
$header->setValue($value.' b='.trim(chunk_split(base64_encode($signature), 73, ' ')));
$headers->add($header);
return new Message($headers, $message->getBody());
}
private function canonicalizeHeader(string $header, string $headerCanon): string
{
if (self::CANON_RELAXED !== $headerCanon) {
return $header."\r\n";
}
$exploded = explode(':', $header, 2);
$name = strtolower(trim($exploded[0]));
$value = str_replace("\r\n", '', $exploded[1]);
$value = trim(preg_replace("/[ \t][ \t]+/", ' ', $value));
return $name.':'.$value."\r\n";
}
private function hashBody(AbstractPart $body, string $bodyCanon, int $maxLength): array
{
$hash = hash_init('sha256');
$relaxed = self::CANON_RELAXED === $bodyCanon;
$currentLine = '';
$emptyCounter = 0;
$isSpaceSequence = false;
$length = 0;
foreach ($body->bodyToIterable() as $chunk) {
$canon = '';
for ($i = 0, $len = \strlen($chunk); $i < $len; ++$i) {
switch ($chunk[$i]) {
case "\r":
break;
case "\n":
// previous char is always \r
if ($relaxed) {
$isSpaceSequence = false;
}
if ('' === $currentLine) {
++$emptyCounter;
} else {
$currentLine = '';
$canon .= "\r\n";
}
break;
case ' ':
case "\t":
if ($relaxed) {
$isSpaceSequence = true;
break;
}
// no break
default:
if ($emptyCounter > 0) {
$canon .= str_repeat("\r\n", $emptyCounter);
$emptyCounter = 0;
}
if ($isSpaceSequence) {
$currentLine .= ' ';
$canon .= ' ';
$isSpaceSequence = false;
}
$currentLine .= $chunk[$i];
$canon .= $chunk[$i];
}
}
if ($length + \strlen($canon) >= $maxLength) {
$canon = substr($canon, 0, $maxLength - $length);
$length += \strlen($canon);
hash_update($hash, $canon);
break;
}
$length += \strlen($canon);
hash_update($hash, $canon);
}
// Add trailing Line return if last line is non empty
if ('' !== $currentLine) {
hash_update($hash, "\r\n");
$length += \strlen("\r\n");
}
if (!$relaxed && 0 === $length) {
hash_update($hash, "\r\n");
$length = 2;
}
return [hash_final($hash, true), $length];
}
}

111
vendor/symfony/mime/Crypto/SMime.php vendored Normal file
View File

@ -0,0 +1,111 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Crypto;
use Symfony\Component\Mime\Exception\RuntimeException;
use Symfony\Component\Mime\Part\SMimePart;
/**
* @author Sebastiaan Stok <s.stok@rollerscapes.net>
*
* @internal
*/
abstract class SMime
{
protected function normalizeFilePath(string $path): string
{
if (!file_exists($path)) {
throw new RuntimeException(sprintf('File does not exist: "%s".', $path));
}
return 'file://'.str_replace('\\', '/', realpath($path));
}
protected function iteratorToFile(iterable $iterator, $stream): void
{
foreach ($iterator as $chunk) {
fwrite($stream, $chunk);
}
}
protected function convertMessageToSMimePart($stream, string $type, string $subtype): SMimePart
{
rewind($stream);
$headers = '';
while (!feof($stream)) {
$buffer = fread($stream, 78);
$headers .= $buffer;
// Detect ending of header list
if (preg_match('/(\r\n\r\n|\n\n)/', $headers, $match)) {
$headersPosEnd = strpos($headers, $headerBodySeparator = $match[0]);
break;
}
}
$headers = $this->getMessageHeaders(trim(substr($headers, 0, $headersPosEnd)));
fseek($stream, $headersPosEnd + \strlen($headerBodySeparator));
return new SMimePart($this->getStreamIterator($stream), $type, $subtype, $this->getParametersFromHeader($headers['content-type']));
}
protected function getStreamIterator($stream): iterable
{
while (!feof($stream)) {
yield str_replace("\n", "\r\n", str_replace("\r\n", "\n", fread($stream, 16372)));
}
}
private function getMessageHeaders(string $headerData): array
{
$headers = [];
$headerLines = explode("\r\n", str_replace("\n", "\r\n", str_replace("\r\n", "\n", $headerData)));
$currentHeaderName = '';
// Transform header lines into an associative array
foreach ($headerLines as $headerLine) {
// Empty lines between headers indicate a new mime-entity
if ('' === $headerLine) {
break;
}
// Handle headers that span multiple lines
if (!str_contains($headerLine, ':')) {
$headers[$currentHeaderName] .= ' '.trim($headerLine);
continue;
}
$header = explode(':', $headerLine, 2);
$currentHeaderName = strtolower($header[0]);
$headers[$currentHeaderName] = trim($header[1]);
}
return $headers;
}
private function getParametersFromHeader(string $header): array
{
$params = [];
preg_match_all('/(?P<name>[a-z-0-9]+)=(?P<value>"[^"]+"|(?:[^\s;]+|$))(?:\s+;)?/i', $header, $matches);
foreach ($matches['value'] as $pos => $paramValue) {
$params[$matches['name'][$pos]] = trim($paramValue, '"');
}
return $params;
}
}

View File

@ -0,0 +1,63 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Crypto;
use Symfony\Component\Mime\Exception\RuntimeException;
use Symfony\Component\Mime\Message;
/**
* @author Sebastiaan Stok <s.stok@rollerscapes.net>
*/
final class SMimeEncrypter extends SMime
{
private string|array $certs;
private int $cipher;
/**
* @param string|string[] $certificate The path (or array of paths) of the file(s) containing the X.509 certificate(s)
* @param int|null $cipher A set of algorithms used to encrypt the message. Must be one of these PHP constants: https://www.php.net/manual/en/openssl.ciphers.php
*/
public function __construct(string|array $certificate, ?int $cipher = null)
{
if (!\extension_loaded('openssl')) {
throw new \LogicException('PHP extension "openssl" is required to use SMime.');
}
if (\is_array($certificate)) {
$this->certs = array_map($this->normalizeFilePath(...), $certificate);
} else {
$this->certs = $this->normalizeFilePath($certificate);
}
$this->cipher = $cipher ?? \OPENSSL_CIPHER_AES_256_CBC;
}
public function encrypt(Message $message): Message
{
$bufferFile = tmpfile();
$outputFile = tmpfile();
$this->iteratorToFile($message->toIterable(), $bufferFile);
if (!@openssl_pkcs7_encrypt(stream_get_meta_data($bufferFile)['uri'], stream_get_meta_data($outputFile)['uri'], $this->certs, [], 0, $this->cipher)) {
throw new RuntimeException(sprintf('Failed to encrypt S/Mime message. Error: "%s".', openssl_error_string()));
}
$mimePart = $this->convertMessageToSMimePart($outputFile, 'application', 'pkcs7-mime');
$mimePart->getHeaders()
->addTextHeader('Content-Transfer-Encoding', 'base64')
->addParameterizedHeader('Content-Disposition', 'attachment', ['name' => 'smime.p7m'])
;
return new Message($message->getHeaders(), $mimePart);
}
}

View File

@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Crypto;
use Symfony\Component\Mime\Exception\RuntimeException;
use Symfony\Component\Mime\Message;
/**
* @author Sebastiaan Stok <s.stok@rollerscapes.net>
*/
final class SMimeSigner extends SMime
{
private string $signCertificate;
private string|array $signPrivateKey;
private int $signOptions;
private ?string $extraCerts;
/**
* @param string $certificate The path of the file containing the signing certificate (in PEM format)
* @param string $privateKey The path of the file containing the private key (in PEM format)
* @param string|null $privateKeyPassphrase A passphrase of the private key (if any)
* @param string|null $extraCerts The path of the file containing intermediate certificates (in PEM format) needed by the signing certificate
* @param int|null $signOptions Bitwise operator options for openssl_pkcs7_sign() (@see https://secure.php.net/manual/en/openssl.pkcs7.flags.php)
*/
public function __construct(string $certificate, string $privateKey, ?string $privateKeyPassphrase = null, ?string $extraCerts = null, ?int $signOptions = null)
{
if (!\extension_loaded('openssl')) {
throw new \LogicException('PHP extension "openssl" is required to use SMime.');
}
$this->signCertificate = $this->normalizeFilePath($certificate);
if (null !== $privateKeyPassphrase) {
$this->signPrivateKey = [$this->normalizeFilePath($privateKey), $privateKeyPassphrase];
} else {
$this->signPrivateKey = $this->normalizeFilePath($privateKey);
}
$this->signOptions = $signOptions ?? \PKCS7_DETACHED;
$this->extraCerts = $extraCerts ? realpath($extraCerts) : null;
}
public function sign(Message $message): Message
{
$bufferFile = tmpfile();
$outputFile = tmpfile();
$this->iteratorToFile($message->getBody()->toIterable(), $bufferFile);
if (!@openssl_pkcs7_sign(stream_get_meta_data($bufferFile)['uri'], stream_get_meta_data($outputFile)['uri'], $this->signCertificate, $this->signPrivateKey, [], $this->signOptions, $this->extraCerts)) {
throw new RuntimeException(sprintf('Failed to sign S/Mime message. Error: "%s".', openssl_error_string()));
}
return new Message($message->getHeaders(), $this->convertMessageToSMimePart($outputFile, 'multipart', 'signed'));
}
}

View File

@ -0,0 +1,37 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
/**
* Registers custom mime types guessers.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class AddMimeTypeGuesserPass implements CompilerPassInterface
{
/**
* @return void
*/
public function process(ContainerBuilder $container)
{
if ($container->has('mime_types')) {
$definition = $container->findDefinition('mime_types');
foreach ($container->findTaggedServiceIds('mime.mime_type_guesser', true) as $id => $attributes) {
$definition->addMethodCall('registerGuesser', [new Reference($id)]);
}
}
}
}

45
vendor/symfony/mime/DraftEmail.php vendored Normal file
View File

@ -0,0 +1,45 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime;
use Symfony\Component\Mime\Header\Headers;
use Symfony\Component\Mime\Part\AbstractPart;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
class DraftEmail extends Email
{
public function __construct(?Headers $headers = null, ?AbstractPart $body = null)
{
parent::__construct($headers, $body);
$this->getHeaders()->addTextHeader('X-Unsent', '1');
}
/**
* Override default behavior as draft emails do not require From/Sender/Date/Message-ID headers.
* These are added by the client that actually sends the email.
*/
public function getPreparedHeaders(): Headers
{
$headers = clone $this->getHeaders();
if (!$headers->has('MIME-Version')) {
$headers->addTextHeader('MIME-Version', '1.0');
}
$headers->remove('Bcc');
return $headers;
}
}

591
vendor/symfony/mime/Email.php vendored Normal file
View File

@ -0,0 +1,591 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime;
use Symfony\Component\Mime\Exception\LogicException;
use Symfony\Component\Mime\Part\AbstractPart;
use Symfony\Component\Mime\Part\DataPart;
use Symfony\Component\Mime\Part\File;
use Symfony\Component\Mime\Part\Multipart\AlternativePart;
use Symfony\Component\Mime\Part\Multipart\MixedPart;
use Symfony\Component\Mime\Part\Multipart\RelatedPart;
use Symfony\Component\Mime\Part\TextPart;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class Email extends Message
{
public const PRIORITY_HIGHEST = 1;
public const PRIORITY_HIGH = 2;
public const PRIORITY_NORMAL = 3;
public const PRIORITY_LOW = 4;
public const PRIORITY_LOWEST = 5;
private const PRIORITY_MAP = [
self::PRIORITY_HIGHEST => 'Highest',
self::PRIORITY_HIGH => 'High',
self::PRIORITY_NORMAL => 'Normal',
self::PRIORITY_LOW => 'Low',
self::PRIORITY_LOWEST => 'Lowest',
];
/**
* @var resource|string|null
*/
private $text;
private ?string $textCharset = null;
/**
* @var resource|string|null
*/
private $html;
private ?string $htmlCharset = null;
private array $attachments = [];
private ?AbstractPart $cachedBody = null; // Used to avoid wrong body hash in DKIM signatures with multiple parts (e.g. HTML + TEXT) due to multiple boundaries.
/**
* @return $this
*/
public function subject(string $subject): static
{
return $this->setHeaderBody('Text', 'Subject', $subject);
}
public function getSubject(): ?string
{
return $this->getHeaders()->getHeaderBody('Subject');
}
/**
* @return $this
*/
public function date(\DateTimeInterface $dateTime): static
{
return $this->setHeaderBody('Date', 'Date', $dateTime);
}
public function getDate(): ?\DateTimeImmutable
{
return $this->getHeaders()->getHeaderBody('Date');
}
/**
* @return $this
*/
public function returnPath(Address|string $address): static
{
return $this->setHeaderBody('Path', 'Return-Path', Address::create($address));
}
public function getReturnPath(): ?Address
{
return $this->getHeaders()->getHeaderBody('Return-Path');
}
/**
* @return $this
*/
public function sender(Address|string $address): static
{
return $this->setHeaderBody('Mailbox', 'Sender', Address::create($address));
}
public function getSender(): ?Address
{
return $this->getHeaders()->getHeaderBody('Sender');
}
/**
* @return $this
*/
public function addFrom(Address|string ...$addresses): static
{
return $this->addListAddressHeaderBody('From', $addresses);
}
/**
* @return $this
*/
public function from(Address|string ...$addresses): static
{
if (!$addresses) {
throw new LogicException('"from()" must be called with at least one address.');
}
return $this->setListAddressHeaderBody('From', $addresses);
}
/**
* @return Address[]
*/
public function getFrom(): array
{
return $this->getHeaders()->getHeaderBody('From') ?: [];
}
/**
* @return $this
*/
public function addReplyTo(Address|string ...$addresses): static
{
return $this->addListAddressHeaderBody('Reply-To', $addresses);
}
/**
* @return $this
*/
public function replyTo(Address|string ...$addresses): static
{
return $this->setListAddressHeaderBody('Reply-To', $addresses);
}
/**
* @return Address[]
*/
public function getReplyTo(): array
{
return $this->getHeaders()->getHeaderBody('Reply-To') ?: [];
}
/**
* @return $this
*/
public function addTo(Address|string ...$addresses): static
{
return $this->addListAddressHeaderBody('To', $addresses);
}
/**
* @return $this
*/
public function to(Address|string ...$addresses): static
{
return $this->setListAddressHeaderBody('To', $addresses);
}
/**
* @return Address[]
*/
public function getTo(): array
{
return $this->getHeaders()->getHeaderBody('To') ?: [];
}
/**
* @return $this
*/
public function addCc(Address|string ...$addresses): static
{
return $this->addListAddressHeaderBody('Cc', $addresses);
}
/**
* @return $this
*/
public function cc(Address|string ...$addresses): static
{
return $this->setListAddressHeaderBody('Cc', $addresses);
}
/**
* @return Address[]
*/
public function getCc(): array
{
return $this->getHeaders()->getHeaderBody('Cc') ?: [];
}
/**
* @return $this
*/
public function addBcc(Address|string ...$addresses): static
{
return $this->addListAddressHeaderBody('Bcc', $addresses);
}
/**
* @return $this
*/
public function bcc(Address|string ...$addresses): static
{
return $this->setListAddressHeaderBody('Bcc', $addresses);
}
/**
* @return Address[]
*/
public function getBcc(): array
{
return $this->getHeaders()->getHeaderBody('Bcc') ?: [];
}
/**
* Sets the priority of this message.
*
* The value is an integer where 1 is the highest priority and 5 is the lowest.
*
* @return $this
*/
public function priority(int $priority): static
{
if ($priority > 5) {
$priority = 5;
} elseif ($priority < 1) {
$priority = 1;
}
return $this->setHeaderBody('Text', 'X-Priority', sprintf('%d (%s)', $priority, self::PRIORITY_MAP[$priority]));
}
/**
* Get the priority of this message.
*
* The returned value is an integer where 1 is the highest priority and 5
* is the lowest.
*/
public function getPriority(): int
{
[$priority] = sscanf($this->getHeaders()->getHeaderBody('X-Priority') ?? '', '%[1-5]');
return $priority ?? 3;
}
/**
* @param resource|string|null $body
*
* @return $this
*/
public function text($body, string $charset = 'utf-8'): static
{
if (null !== $body && !\is_string($body) && !\is_resource($body)) {
throw new \TypeError(sprintf('The body must be a string, a resource or null (got "%s").', get_debug_type($body)));
}
$this->cachedBody = null;
$this->text = $body;
$this->textCharset = $charset;
return $this;
}
/**
* @return resource|string|null
*/
public function getTextBody()
{
return $this->text;
}
public function getTextCharset(): ?string
{
return $this->textCharset;
}
/**
* @param resource|string|null $body
*
* @return $this
*/
public function html($body, string $charset = 'utf-8'): static
{
if (null !== $body && !\is_string($body) && !\is_resource($body)) {
throw new \TypeError(sprintf('The body must be a string, a resource or null (got "%s").', get_debug_type($body)));
}
$this->cachedBody = null;
$this->html = $body;
$this->htmlCharset = $charset;
return $this;
}
/**
* @return resource|string|null
*/
public function getHtmlBody()
{
return $this->html;
}
public function getHtmlCharset(): ?string
{
return $this->htmlCharset;
}
/**
* @param resource|string $body
*
* @return $this
*/
public function attach($body, ?string $name = null, ?string $contentType = null): static
{
return $this->addPart(new DataPart($body, $name, $contentType));
}
/**
* @return $this
*/
public function attachFromPath(string $path, ?string $name = null, ?string $contentType = null): static
{
return $this->addPart(new DataPart(new File($path), $name, $contentType));
}
/**
* @param resource|string $body
*
* @return $this
*/
public function embed($body, ?string $name = null, ?string $contentType = null): static
{
return $this->addPart((new DataPart($body, $name, $contentType))->asInline());
}
/**
* @return $this
*/
public function embedFromPath(string $path, ?string $name = null, ?string $contentType = null): static
{
return $this->addPart((new DataPart(new File($path), $name, $contentType))->asInline());
}
/**
* @return $this
*
* @deprecated since Symfony 6.2, use addPart() instead
*/
public function attachPart(DataPart $part): static
{
@trigger_deprecation('symfony/mime', '6.2', 'The "%s()" method is deprecated, use "addPart()" instead.', __METHOD__);
return $this->addPart($part);
}
/**
* @return $this
*/
public function addPart(DataPart $part): static
{
$this->cachedBody = null;
$this->attachments[] = $part;
return $this;
}
/**
* @return DataPart[]
*/
public function getAttachments(): array
{
return $this->attachments;
}
public function getBody(): AbstractPart
{
if (null !== $body = parent::getBody()) {
return $body;
}
return $this->generateBody();
}
/**
* @return void
*/
public function ensureValidity()
{
$this->ensureBodyValid();
if ('1' === $this->getHeaders()->getHeaderBody('X-Unsent')) {
throw new LogicException('Cannot send messages marked as "draft".');
}
parent::ensureValidity();
}
private function ensureBodyValid(): void
{
if (null === $this->text && null === $this->html && !$this->attachments) {
throw new LogicException('A message must have a text or an HTML part or attachments.');
}
}
/**
* Generates an AbstractPart based on the raw body of a message.
*
* The most "complex" part generated by this method is when there is text and HTML bodies
* with related images for the HTML part and some attachments:
*
* multipart/mixed
* |
* |------------> multipart/related
* | |
* | |------------> multipart/alternative
* | | |
* | | ------------> text/plain (with content)
* | | |
* | | ------------> text/html (with content)
* | |
* | ------------> image/png (with content)
* |
* ------------> application/pdf (with content)
*/
private function generateBody(): AbstractPart
{
if (null !== $this->cachedBody) {
return $this->cachedBody;
}
$this->ensureBodyValid();
[$htmlPart, $otherParts, $relatedParts] = $this->prepareParts();
$part = null === $this->text ? null : new TextPart($this->text, $this->textCharset);
if (null !== $htmlPart) {
if (null !== $part) {
$part = new AlternativePart($part, $htmlPart);
} else {
$part = $htmlPart;
}
}
if ($relatedParts) {
$part = new RelatedPart($part, ...$relatedParts);
}
if ($otherParts) {
if ($part) {
$part = new MixedPart($part, ...$otherParts);
} else {
$part = new MixedPart(...$otherParts);
}
}
return $this->cachedBody = $part;
}
private function prepareParts(): ?array
{
$names = [];
$htmlPart = null;
$html = $this->html;
if (null !== $html) {
$htmlPart = new TextPart($html, $this->htmlCharset, 'html');
$html = $htmlPart->getBody();
$regexes = [
'<img\s+[^>]*src\s*=\s*(?:([\'"])cid:(.+?)\\1|cid:([^>\s]+))',
'<\w+\s+[^>]*background\s*=\s*(?:([\'"])cid:(.+?)\\1|cid:([^>\s]+))',
];
$tmpMatches = [];
foreach ($regexes as $regex) {
preg_match_all('/'.$regex.'/i', $html, $tmpMatches);
$names = array_merge($names, $tmpMatches[2], $tmpMatches[3]);
}
$names = array_filter(array_unique($names));
}
$otherParts = $relatedParts = [];
foreach ($this->attachments as $part) {
foreach ($names as $name) {
if ($name !== $part->getName() && (!$part->hasContentId() || $name !== $part->getContentId())) {
continue;
}
if (isset($relatedParts[$name])) {
continue 2;
}
if ($name !== $part->getContentId()) {
$html = str_replace('cid:'.$name, 'cid:'.$part->getContentId(), $html, $count);
}
$relatedParts[$name] = $part;
$part->setName($part->getContentId())->asInline();
continue 2;
}
$otherParts[] = $part;
}
if (null !== $htmlPart) {
$htmlPart = new TextPart($html, $this->htmlCharset, 'html');
}
return [$htmlPart, $otherParts, array_values($relatedParts)];
}
/**
* @return $this
*/
private function setHeaderBody(string $type, string $name, $body): static
{
$this->getHeaders()->setHeaderBody($type, $name, $body);
return $this;
}
/**
* @return $this
*/
private function addListAddressHeaderBody(string $name, array $addresses): static
{
if (!$header = $this->getHeaders()->get($name)) {
return $this->setListAddressHeaderBody($name, $addresses);
}
$header->addAddresses(Address::createArray($addresses));
return $this;
}
/**
* @return $this
*/
private function setListAddressHeaderBody(string $name, array $addresses): static
{
$addresses = Address::createArray($addresses);
$headers = $this->getHeaders();
if ($header = $headers->get($name)) {
$header->setAddresses($addresses);
} else {
$headers->addMailboxListHeader($name, $addresses);
}
return $this;
}
/**
* @internal
*/
public function __serialize(): array
{
if (\is_resource($this->text)) {
$this->text = (new TextPart($this->text))->getBody();
}
if (\is_resource($this->html)) {
$this->html = (new TextPart($this->html))->getBody();
}
return [$this->text, $this->textCharset, $this->html, $this->htmlCharset, $this->attachments, parent::__serialize()];
}
/**
* @internal
*/
public function __unserialize(array $data): void
{
[$this->text, $this->textCharset, $this->html, $this->htmlCharset, $this->attachments, $parentData] = $data;
parent::__unserialize($parentData);
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Encoder;
use Symfony\Component\Mime\Exception\AddressEncoderException;
/**
* @author Christian Schmidt
*/
interface AddressEncoderInterface
{
/**
* Encodes an email address.
*
* @throws AddressEncoderException if the email cannot be represented in
* the encoding implemented by this class
*/
public function encodeString(string $address): string;
}

View File

@ -0,0 +1,45 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Encoder;
use Symfony\Component\Mime\Exception\RuntimeException;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class Base64ContentEncoder extends Base64Encoder implements ContentEncoderInterface
{
public function encodeByteStream($stream, int $maxLineLength = 0): iterable
{
if (!\is_resource($stream)) {
throw new \TypeError(sprintf('Method "%s" takes a stream as a first argument.', __METHOD__));
}
$filter = stream_filter_append($stream, 'convert.base64-encode', \STREAM_FILTER_READ, [
'line-length' => 0 >= $maxLineLength || 76 < $maxLineLength ? 76 : $maxLineLength,
'line-break-chars' => "\r\n",
]);
if (!\is_resource($filter)) {
throw new RuntimeException('Unable to set the base64 content encoder to the filter.');
}
while (!feof($stream)) {
yield fread($stream, 16372);
}
stream_filter_remove($filter);
}
public function getName(): string
{
return 'base64';
}
}

View File

@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Encoder;
/**
* @author Chris Corbyn
*/
class Base64Encoder implements EncoderInterface
{
/**
* Takes an unencoded string and produces a Base64 encoded string from it.
*
* Base64 encoded strings have a maximum line length of 76 characters.
* If the first line needs to be shorter, indicate the difference with
* $firstLineOffset.
*/
public function encodeString(string $string, ?string $charset = 'utf-8', int $firstLineOffset = 0, int $maxLineLength = 0): string
{
if (0 >= $maxLineLength || 76 < $maxLineLength) {
$maxLineLength = 76;
}
$encodedString = base64_encode($string);
$firstLine = '';
if (0 !== $firstLineOffset) {
$firstLine = substr($encodedString, 0, $maxLineLength - $firstLineOffset)."\r\n";
$encodedString = substr($encodedString, $maxLineLength - $firstLineOffset);
}
return $firstLine.trim(chunk_split($encodedString, $maxLineLength, "\r\n"));
}
}

View File

@ -0,0 +1,43 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Encoder;
/**
* @author Chris Corbyn
*/
final class Base64MimeHeaderEncoder extends Base64Encoder implements MimeHeaderEncoderInterface
{
public function getName(): string
{
return 'B';
}
/**
* Takes an unencoded string and produces a Base64 encoded string from it.
*
* If the charset is iso-2022-jp, it uses mb_encode_mimeheader instead of
* default encodeString, otherwise pass to the parent method.
*/
public function encodeString(string $string, ?string $charset = 'utf-8', int $firstLineOffset = 0, int $maxLineLength = 0): string
{
if ('iso-2022-jp' === strtolower($charset)) {
$old = mb_internal_encoding();
mb_internal_encoding('utf-8');
$newstring = mb_encode_mimeheader($string, 'iso-2022-jp', $this->getName(), "\r\n");
mb_internal_encoding($old);
return $newstring;
}
return parent::encodeString($string, $charset, $firstLineOffset, $maxLineLength);
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Encoder;
/**
* @author Chris Corbyn
*/
interface ContentEncoderInterface extends EncoderInterface
{
/**
* Encodes the stream to a Generator.
*
* @param resource $stream
*/
public function encodeByteStream($stream, int $maxLineLength = 0): iterable;
/**
* Gets the MIME name of this content encoding scheme.
*/
public function getName(): string;
}

View File

@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Encoder;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class EightBitContentEncoder implements ContentEncoderInterface
{
public function encodeByteStream($stream, int $maxLineLength = 0): iterable
{
while (!feof($stream)) {
yield fread($stream, 16372);
}
}
public function getName(): string
{
return '8bit';
}
public function encodeString(string $string, ?string $charset = 'utf-8', int $firstLineOffset = 0, int $maxLineLength = 0): string
{
return $string;
}
}

View File

@ -0,0 +1,26 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Encoder;
/**
* @author Chris Corbyn
*/
interface EncoderInterface
{
/**
* Encode a given string to produce an encoded string.
*
* @param int $firstLineOffset if first line needs to be shorter
* @param int $maxLineLength - 0 indicates the default length for this encoding
*/
public function encodeString(string $string, ?string $charset = 'utf-8', int $firstLineOffset = 0, int $maxLineLength = 0): string;
}

View File

@ -0,0 +1,44 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Encoder;
/**
* An IDN email address encoder.
*
* Encodes the domain part of an address using IDN. This is compatible will all
* SMTP servers.
*
* Note: It leaves the local part as is. In case there are non-ASCII characters
* in the local part then it depends on the SMTP Server if this is supported.
*
* @author Christian Schmidt
*/
final class IdnAddressEncoder implements AddressEncoderInterface
{
/**
* Encodes the domain part of an address using IDN.
*/
public function encodeString(string $address): string
{
$i = strrpos($address, '@');
if (false !== $i) {
$local = substr($address, 0, $i);
$domain = substr($address, $i + 1);
if (preg_match('/[^\x00-\x7F]/', $domain)) {
$address = sprintf('%s@%s', $local, idn_to_ascii($domain, \IDNA_DEFAULT | \IDNA_USE_STD3_RULES | \IDNA_CHECK_BIDI | \IDNA_CHECK_CONTEXTJ | \IDNA_NONTRANSITIONAL_TO_ASCII, \INTL_IDNA_VARIANT_UTS46));
}
}
return $address;
}
}

View File

@ -0,0 +1,23 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Encoder;
/**
* @author Chris Corbyn
*/
interface MimeHeaderEncoderInterface
{
/**
* Get the MIME name of this content encoding scheme.
*/
public function getName(): string;
}

View File

@ -0,0 +1,55 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Encoder;
/**
* @author Lars Strojny
*/
final class QpContentEncoder implements ContentEncoderInterface
{
public function encodeByteStream($stream, int $maxLineLength = 0): iterable
{
if (!\is_resource($stream)) {
throw new \TypeError(sprintf('Method "%s" takes a stream as a first argument.', __METHOD__));
}
// we don't use PHP stream filters here as the content should be small enough
yield $this->encodeString(stream_get_contents($stream), 'utf-8', 0, $maxLineLength);
}
public function getName(): string
{
return 'quoted-printable';
}
public function encodeString(string $string, ?string $charset = 'utf-8', int $firstLineOffset = 0, int $maxLineLength = 0): string
{
return $this->standardize(quoted_printable_encode($string));
}
/**
* Make sure CRLF is correct and HT/SPACE are in valid places.
*/
private function standardize(string $string): string
{
// transform CR or LF to CRLF
$string = preg_replace('~=0D(?!=0A)|(?<!=0D)=0A~', '=0D=0A', $string);
// transform =0D=0A to CRLF
$string = str_replace(["\t=0D=0A", ' =0D=0A', '=0D=0A'], ["=09\r\n", "=20\r\n", "\r\n"], $string);
return match (\ord(substr($string, -1))) {
0x09 => substr_replace($string, '=09', -1),
0x20 => substr_replace($string, '=20', -1),
default => $string,
};
}
}

View File

@ -0,0 +1,192 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Encoder;
use Symfony\Component\Mime\CharacterStream;
/**
* @author Chris Corbyn
*/
class QpEncoder implements EncoderInterface
{
/**
* Pre-computed QP for HUGE optimization.
*/
private const QP_MAP = [
0 => '=00', 1 => '=01', 2 => '=02', 3 => '=03', 4 => '=04',
5 => '=05', 6 => '=06', 7 => '=07', 8 => '=08', 9 => '=09',
10 => '=0A', 11 => '=0B', 12 => '=0C', 13 => '=0D', 14 => '=0E',
15 => '=0F', 16 => '=10', 17 => '=11', 18 => '=12', 19 => '=13',
20 => '=14', 21 => '=15', 22 => '=16', 23 => '=17', 24 => '=18',
25 => '=19', 26 => '=1A', 27 => '=1B', 28 => '=1C', 29 => '=1D',
30 => '=1E', 31 => '=1F', 32 => '=20', 33 => '=21', 34 => '=22',
35 => '=23', 36 => '=24', 37 => '=25', 38 => '=26', 39 => '=27',
40 => '=28', 41 => '=29', 42 => '=2A', 43 => '=2B', 44 => '=2C',
45 => '=2D', 46 => '=2E', 47 => '=2F', 48 => '=30', 49 => '=31',
50 => '=32', 51 => '=33', 52 => '=34', 53 => '=35', 54 => '=36',
55 => '=37', 56 => '=38', 57 => '=39', 58 => '=3A', 59 => '=3B',
60 => '=3C', 61 => '=3D', 62 => '=3E', 63 => '=3F', 64 => '=40',
65 => '=41', 66 => '=42', 67 => '=43', 68 => '=44', 69 => '=45',
70 => '=46', 71 => '=47', 72 => '=48', 73 => '=49', 74 => '=4A',
75 => '=4B', 76 => '=4C', 77 => '=4D', 78 => '=4E', 79 => '=4F',
80 => '=50', 81 => '=51', 82 => '=52', 83 => '=53', 84 => '=54',
85 => '=55', 86 => '=56', 87 => '=57', 88 => '=58', 89 => '=59',
90 => '=5A', 91 => '=5B', 92 => '=5C', 93 => '=5D', 94 => '=5E',
95 => '=5F', 96 => '=60', 97 => '=61', 98 => '=62', 99 => '=63',
100 => '=64', 101 => '=65', 102 => '=66', 103 => '=67', 104 => '=68',
105 => '=69', 106 => '=6A', 107 => '=6B', 108 => '=6C', 109 => '=6D',
110 => '=6E', 111 => '=6F', 112 => '=70', 113 => '=71', 114 => '=72',
115 => '=73', 116 => '=74', 117 => '=75', 118 => '=76', 119 => '=77',
120 => '=78', 121 => '=79', 122 => '=7A', 123 => '=7B', 124 => '=7C',
125 => '=7D', 126 => '=7E', 127 => '=7F', 128 => '=80', 129 => '=81',
130 => '=82', 131 => '=83', 132 => '=84', 133 => '=85', 134 => '=86',
135 => '=87', 136 => '=88', 137 => '=89', 138 => '=8A', 139 => '=8B',
140 => '=8C', 141 => '=8D', 142 => '=8E', 143 => '=8F', 144 => '=90',
145 => '=91', 146 => '=92', 147 => '=93', 148 => '=94', 149 => '=95',
150 => '=96', 151 => '=97', 152 => '=98', 153 => '=99', 154 => '=9A',
155 => '=9B', 156 => '=9C', 157 => '=9D', 158 => '=9E', 159 => '=9F',
160 => '=A0', 161 => '=A1', 162 => '=A2', 163 => '=A3', 164 => '=A4',
165 => '=A5', 166 => '=A6', 167 => '=A7', 168 => '=A8', 169 => '=A9',
170 => '=AA', 171 => '=AB', 172 => '=AC', 173 => '=AD', 174 => '=AE',
175 => '=AF', 176 => '=B0', 177 => '=B1', 178 => '=B2', 179 => '=B3',
180 => '=B4', 181 => '=B5', 182 => '=B6', 183 => '=B7', 184 => '=B8',
185 => '=B9', 186 => '=BA', 187 => '=BB', 188 => '=BC', 189 => '=BD',
190 => '=BE', 191 => '=BF', 192 => '=C0', 193 => '=C1', 194 => '=C2',
195 => '=C3', 196 => '=C4', 197 => '=C5', 198 => '=C6', 199 => '=C7',
200 => '=C8', 201 => '=C9', 202 => '=CA', 203 => '=CB', 204 => '=CC',
205 => '=CD', 206 => '=CE', 207 => '=CF', 208 => '=D0', 209 => '=D1',
210 => '=D2', 211 => '=D3', 212 => '=D4', 213 => '=D5', 214 => '=D6',
215 => '=D7', 216 => '=D8', 217 => '=D9', 218 => '=DA', 219 => '=DB',
220 => '=DC', 221 => '=DD', 222 => '=DE', 223 => '=DF', 224 => '=E0',
225 => '=E1', 226 => '=E2', 227 => '=E3', 228 => '=E4', 229 => '=E5',
230 => '=E6', 231 => '=E7', 232 => '=E8', 233 => '=E9', 234 => '=EA',
235 => '=EB', 236 => '=EC', 237 => '=ED', 238 => '=EE', 239 => '=EF',
240 => '=F0', 241 => '=F1', 242 => '=F2', 243 => '=F3', 244 => '=F4',
245 => '=F5', 246 => '=F6', 247 => '=F7', 248 => '=F8', 249 => '=F9',
250 => '=FA', 251 => '=FB', 252 => '=FC', 253 => '=FD', 254 => '=FE',
255 => '=FF',
];
private static array $safeMapShare = [];
/**
* A map of non-encoded ascii characters.
*
* @var string[]
*
* @internal
*/
protected array $safeMap = [];
public function __construct()
{
$id = static::class;
if (!isset(self::$safeMapShare[$id])) {
$this->initSafeMap();
self::$safeMapShare[$id] = $this->safeMap;
} else {
$this->safeMap = self::$safeMapShare[$id];
}
}
protected function initSafeMap(): void
{
foreach (array_merge([0x09, 0x20], range(0x21, 0x3C), range(0x3E, 0x7E)) as $byte) {
$this->safeMap[$byte] = \chr($byte);
}
}
/**
* Takes an unencoded string and produces a QP encoded string from it.
*
* QP encoded strings have a maximum line length of 76 characters.
* If the first line needs to be shorter, indicate the difference with
* $firstLineOffset.
*/
public function encodeString(string $string, ?string $charset = 'utf-8', int $firstLineOffset = 0, int $maxLineLength = 0): string
{
if ($maxLineLength > 76 || $maxLineLength <= 0) {
$maxLineLength = 76;
}
$thisLineLength = $maxLineLength - $firstLineOffset;
$lines = [];
$lNo = 0;
$lines[$lNo] = '';
$currentLine = &$lines[$lNo++];
$size = $lineLen = 0;
$charStream = new CharacterStream($string, $charset);
// Fetching more than 4 chars at one is slower, as is fetching fewer bytes
// Conveniently 4 chars is the UTF-8 safe number since UTF-8 has up to 6
// bytes per char and (6 * 4 * 3 = 72 chars per line) * =NN is 3 bytes
while (null !== $bytes = $charStream->readBytes(4)) {
$enc = $this->encodeByteSequence($bytes, $size);
$i = strpos($enc, '=0D=0A');
$newLineLength = $lineLen + (false === $i ? $size : $i);
if ($currentLine && $newLineLength >= $thisLineLength) {
$lines[$lNo] = '';
$currentLine = &$lines[$lNo++];
$thisLineLength = $maxLineLength;
$lineLen = 0;
}
$currentLine .= $enc;
if (false === $i) {
$lineLen += $size;
} else {
// 6 is the length of '=0D=0A'.
$lineLen = $size - strrpos($enc, '=0D=0A') - 6;
}
}
return $this->standardize(implode("=\r\n", $lines));
}
/**
* Encode the given byte array into a verbatim QP form.
*/
private function encodeByteSequence(array $bytes, int &$size): string
{
$ret = '';
$size = 0;
foreach ($bytes as $b) {
if (isset($this->safeMap[$b])) {
$ret .= $this->safeMap[$b];
++$size;
} else {
$ret .= self::QP_MAP[$b];
$size += 3;
}
}
return $ret;
}
/**
* Make sure CRLF is correct and HT/SPACE are in valid places.
*/
private function standardize(string $string): string
{
$string = str_replace(["\t=0D=0A", ' =0D=0A', '=0D=0A'], ["=09\r\n", "=20\r\n", "\r\n"], $string);
return match ($end = \ord(substr($string, -1))) {
0x09,
0x20 => substr_replace($string, self::QP_MAP[$end], -1),
default => $string,
};
}
}

View File

@ -0,0 +1,40 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Encoder;
/**
* @author Chris Corbyn
*/
final class QpMimeHeaderEncoder extends QpEncoder implements MimeHeaderEncoderInterface
{
protected function initSafeMap(): void
{
foreach (array_merge(
range(0x61, 0x7A), range(0x41, 0x5A),
range(0x30, 0x39), [0x20, 0x21, 0x2A, 0x2B, 0x2D, 0x2F]
) as $byte) {
$this->safeMap[$byte] = \chr($byte);
}
}
public function getName(): string
{
return 'Q';
}
public function encodeString(string $string, ?string $charset = 'utf-8', int $firstLineOffset = 0, int $maxLineLength = 0): string
{
return str_replace([' ', '=20', "=\r\n"], ['_', '_', "\r\n"],
parent::encodeString($string, $charset, $firstLineOffset, $maxLineLength)
);
}
}

View File

@ -0,0 +1,50 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Encoder;
use Symfony\Component\Mime\CharacterStream;
/**
* @author Chris Corbyn
*/
final class Rfc2231Encoder implements EncoderInterface
{
/**
* Takes an unencoded string and produces a string encoded according to RFC 2231 from it.
*/
public function encodeString(string $string, ?string $charset = 'utf-8', int $firstLineOffset = 0, int $maxLineLength = 0): string
{
$lines = [];
$lineCount = 0;
$lines[] = '';
$currentLine = &$lines[$lineCount++];
if (0 >= $maxLineLength) {
$maxLineLength = 75;
}
$charStream = new CharacterStream($string, $charset);
$thisLineLength = $maxLineLength - $firstLineOffset;
while (null !== $char = $charStream->read(4)) {
$encodedChar = rawurlencode($char);
if ('' !== $currentLine && \strlen($currentLine.$encodedChar) > $thisLineLength) {
$lines[] = '';
$currentLine = &$lines[$lineCount++];
$thisLineLength = $maxLineLength;
}
$currentLine .= $encodedChar;
}
return implode("\r\n", $lines);
}
}

View File

@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Exception;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class AddressEncoderException extends RfcComplianceException
{
}

View File

@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Exception;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
interface ExceptionInterface extends \Throwable
{
}

View File

@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Exception;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}

View File

@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Exception;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class LogicException extends \LogicException implements ExceptionInterface
{
}

View File

@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Exception;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class RfcComplianceException extends \InvalidArgumentException implements ExceptionInterface
{
}

View File

@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Exception;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}

View File

@ -0,0 +1,87 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime;
use Symfony\Component\Mime\Exception\InvalidArgumentException;
use Symfony\Component\Mime\Exception\LogicException;
/**
* Guesses the MIME type with the binary "file" (only available on *nix).
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FileBinaryMimeTypeGuesser implements MimeTypeGuesserInterface
{
private string $cmd;
/**
* The $cmd pattern must contain a "%s" string that will be replaced
* with the file name to guess.
*
* The command output must start with the MIME type of the file.
*
* @param string $cmd The command to run to get the MIME type of a file
*/
public function __construct(string $cmd = 'file -b --mime -- %s 2>/dev/null')
{
$this->cmd = $cmd;
}
public function isGuesserSupported(): bool
{
static $supported = null;
if (null !== $supported) {
return $supported;
}
if ('\\' === \DIRECTORY_SEPARATOR || !\function_exists('passthru') || !\function_exists('escapeshellarg')) {
return $supported = false;
}
ob_start();
passthru('command -v file', $exitStatus);
$binPath = trim(ob_get_clean());
return $supported = 0 === $exitStatus && '' !== $binPath;
}
public function guessMimeType(string $path): ?string
{
if (!is_file($path) || !is_readable($path)) {
throw new InvalidArgumentException(sprintf('The "%s" file does not exist or is not readable.', $path));
}
if (!$this->isGuesserSupported()) {
throw new LogicException(sprintf('The "%s" guesser is not supported.', __CLASS__));
}
ob_start();
// need to use --mime instead of -i. see #6641
passthru(sprintf($this->cmd, escapeshellarg((str_starts_with($path, '-') ? './' : '').$path)), $return);
if ($return > 0) {
ob_end_clean();
return null;
}
$type = trim(ob_get_clean());
if (!preg_match('#^([a-z0-9\-]+/[a-z0-9\-\+\.]+)#i', $type, $match)) {
// it's not a type, but an error message
return null;
}
return $match[1];
}
}

View File

@ -0,0 +1,63 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime;
use Symfony\Component\Mime\Exception\InvalidArgumentException;
use Symfony\Component\Mime\Exception\LogicException;
/**
* Guesses the MIME type using the PECL extension FileInfo.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FileinfoMimeTypeGuesser implements MimeTypeGuesserInterface
{
private ?string $magicFile;
/**
* @param string|null $magicFile A magic file to use with the finfo instance
*
* @see http://www.php.net/manual/en/function.finfo-open.php
*/
public function __construct(?string $magicFile = null)
{
$this->magicFile = $magicFile;
}
public function isGuesserSupported(): bool
{
return \function_exists('finfo_open');
}
public function guessMimeType(string $path): ?string
{
if (!is_file($path) || !is_readable($path)) {
throw new InvalidArgumentException(sprintf('The "%s" file does not exist or is not readable.', $path));
}
if (!$this->isGuesserSupported()) {
throw new LogicException(sprintf('The "%s" guesser is not supported.', __CLASS__));
}
if (false === $finfo = new \finfo(\FILEINFO_MIME_TYPE, $this->magicFile)) {
return null;
}
$mimeType = $finfo->file($path);
if ($mimeType && 0 === (\strlen($mimeType) % 2)) {
$mimeStart = substr($mimeType, 0, \strlen($mimeType) >> 1);
$mimeType = $mimeStart.$mimeStart === $mimeType ? $mimeStart : $mimeType;
}
return $mimeType;
}
}

View File

@ -0,0 +1,288 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Header;
use Symfony\Component\Mime\Encoder\QpMimeHeaderEncoder;
/**
* An abstract base MIME Header.
*
* @author Chris Corbyn
*/
abstract class AbstractHeader implements HeaderInterface
{
public const PHRASE_PATTERN = '(?:(?:(?:(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))*(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))|(?:(?:[ \t]*(?:\r\n))?[ \t])))?[a-zA-Z0-9!#\$%&\'\*\+\-\/=\?\^_`\{\}\|~]+(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))*(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))|(?:(?:[ \t]*(?:\r\n))?[ \t])))?)|(?:(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))*(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))|(?:(?:[ \t]*(?:\r\n))?[ \t])))?"((?:(?:[ \t]*(?:\r\n))?[ \t])?(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21\x23-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])))*(?:(?:[ \t]*(?:\r\n))?[ \t])?"(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))*(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))|(?:(?:[ \t]*(?:\r\n))?[ \t])))?))+?)';
private static QpMimeHeaderEncoder $encoder;
private string $name;
private int $lineLength = 76;
private ?string $lang = null;
private string $charset = 'utf-8';
public function __construct(string $name)
{
$this->name = $name;
}
/**
* @return void
*/
public function setCharset(string $charset)
{
$this->charset = $charset;
}
public function getCharset(): ?string
{
return $this->charset;
}
/**
* Set the language used in this Header.
*
* For example, for US English, 'en-us'.
*
* @return void
*/
public function setLanguage(string $lang)
{
$this->lang = $lang;
}
public function getLanguage(): ?string
{
return $this->lang;
}
public function getName(): string
{
return $this->name;
}
/**
* @return void
*/
public function setMaxLineLength(int $lineLength)
{
$this->lineLength = $lineLength;
}
public function getMaxLineLength(): int
{
return $this->lineLength;
}
public function toString(): string
{
return $this->tokensToString($this->toTokens());
}
/**
* Produces a compliant, formatted RFC 2822 'phrase' based on the string given.
*
* @param string $string as displayed
* @param bool $shorten the first line to make remove for header name
*/
protected function createPhrase(HeaderInterface $header, string $string, string $charset, bool $shorten = false): string
{
// Treat token as exactly what was given
$phraseStr = $string;
// If it's not valid
if (!preg_match('/^'.self::PHRASE_PATTERN.'$/D', $phraseStr)) {
// .. but it is just ascii text, try escaping some characters
// and make it a quoted-string
if (preg_match('/^[\x00-\x08\x0B\x0C\x0E-\x7F]*$/D', $phraseStr)) {
foreach (['\\', '"'] as $char) {
$phraseStr = str_replace($char, '\\'.$char, $phraseStr);
}
$phraseStr = '"'.$phraseStr.'"';
} else {
// ... otherwise it needs encoding
// Determine space remaining on line if first line
if ($shorten) {
$usedLength = \strlen($header->getName().': ');
} else {
$usedLength = 0;
}
$phraseStr = $this->encodeWords($header, $string, $usedLength);
}
} elseif (str_contains($phraseStr, '(')) {
foreach (['\\', '"'] as $char) {
$phraseStr = str_replace($char, '\\'.$char, $phraseStr);
}
$phraseStr = '"'.$phraseStr.'"';
}
return $phraseStr;
}
/**
* Encode needed word tokens within a string of input.
*/
protected function encodeWords(HeaderInterface $header, string $input, int $usedLength = -1): string
{
$value = '';
$tokens = $this->getEncodableWordTokens($input);
foreach ($tokens as $token) {
// See RFC 2822, Sect 2.2 (really 2.2 ??)
if ($this->tokenNeedsEncoding($token)) {
// Don't encode starting WSP
$firstChar = substr($token, 0, 1);
switch ($firstChar) {
case ' ':
case "\t":
$value .= $firstChar;
$token = substr($token, 1);
}
if (-1 == $usedLength) {
$usedLength = \strlen($header->getName().': ') + \strlen($value);
}
$value .= $this->getTokenAsEncodedWord($token, $usedLength);
} else {
$value .= $token;
}
}
return $value;
}
protected function tokenNeedsEncoding(string $token): bool
{
return (bool) preg_match('~[\x00-\x08\x10-\x19\x7F-\xFF\r\n]~', $token);
}
/**
* Splits a string into tokens in blocks of words which can be encoded quickly.
*
* @return string[]
*/
protected function getEncodableWordTokens(string $string): array
{
$tokens = [];
$encodedToken = '';
// Split at all whitespace boundaries
foreach (preg_split('~(?=[\t ])~', $string) as $token) {
if ($this->tokenNeedsEncoding($token)) {
$encodedToken .= $token;
} else {
if ('' !== $encodedToken) {
$tokens[] = $encodedToken;
$encodedToken = '';
}
$tokens[] = $token;
}
}
if ('' !== $encodedToken) {
$tokens[] = $encodedToken;
}
return $tokens;
}
/**
* Get a token as an encoded word for safe insertion into headers.
*/
protected function getTokenAsEncodedWord(string $token, int $firstLineOffset = 0): string
{
self::$encoder ??= new QpMimeHeaderEncoder();
// Adjust $firstLineOffset to account for space needed for syntax
$charsetDecl = $this->charset;
if (null !== $this->lang) {
$charsetDecl .= '*'.$this->lang;
}
$encodingWrapperLength = \strlen('=?'.$charsetDecl.'?'.self::$encoder->getName().'??=');
if ($firstLineOffset >= 75) {
// Does this logic need to be here?
$firstLineOffset = 0;
}
$encodedTextLines = explode("\r\n",
self::$encoder->encodeString($token, $this->charset, $firstLineOffset, 75 - $encodingWrapperLength)
);
if ('iso-2022-jp' !== strtolower($this->charset)) {
// special encoding for iso-2022-jp using mb_encode_mimeheader
foreach ($encodedTextLines as $lineNum => $line) {
$encodedTextLines[$lineNum] = '=?'.$charsetDecl.'?'.self::$encoder->getName().'?'.$line.'?=';
}
}
return implode("\r\n ", $encodedTextLines);
}
/**
* Generates tokens from the given string which include CRLF as individual tokens.
*
* @return string[]
*/
protected function generateTokenLines(string $token): array
{
return preg_split('~(\r\n)~', $token, -1, \PREG_SPLIT_DELIM_CAPTURE);
}
/**
* Generate a list of all tokens in the final header.
*/
protected function toTokens(?string $string = null): array
{
$string ??= $this->getBodyAsString();
$tokens = [];
// Generate atoms; split at all invisible boundaries followed by WSP
foreach (preg_split('~(?=[ \t])~', $string) as $token) {
$newTokens = $this->generateTokenLines($token);
foreach ($newTokens as $newToken) {
$tokens[] = $newToken;
}
}
return $tokens;
}
/**
* Takes an array of tokens which appear in the header and turns them into
* an RFC 2822 compliant string, adding FWSP where needed.
*
* @param string[] $tokens
*/
private function tokensToString(array $tokens): string
{
$lineCount = 0;
$headerLines = [];
$headerLines[] = $this->name.': ';
$currentLine = &$headerLines[$lineCount++];
// Build all tokens back into compliant header
foreach ($tokens as $i => $token) {
// Line longer than specified maximum or token was just a new line
if (("\r\n" === $token)
|| ($i > 0 && \strlen($currentLine.$token) > $this->lineLength)
&& '' !== $currentLine) {
$headerLines[] = '';
$currentLine = &$headerLines[$lineCount++];
}
// Append token to the line
if ("\r\n" !== $token) {
$currentLine .= $token;
}
}
// Implode with FWS (RFC 2822, 2.2.3)
return implode("\r\n", $headerLines);
}
}

View File

@ -0,0 +1,62 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Header;
/**
* A Date MIME Header.
*
* @author Chris Corbyn
*/
final class DateHeader extends AbstractHeader
{
private \DateTimeImmutable $dateTime;
public function __construct(string $name, \DateTimeInterface $date)
{
parent::__construct($name);
$this->setDateTime($date);
}
/**
* @param \DateTimeInterface $body
*/
public function setBody(mixed $body): void
{
$this->setDateTime($body);
}
public function getBody(): \DateTimeImmutable
{
return $this->getDateTime();
}
public function getDateTime(): \DateTimeImmutable
{
return $this->dateTime;
}
/**
* Set the date-time of the Date in this Header.
*
* If a DateTime instance is provided, it is converted to DateTimeImmutable.
*/
public function setDateTime(\DateTimeInterface $dateTime): void
{
$this->dateTime = \DateTimeImmutable::createFromInterface($dateTime);
}
public function getBodyAsString(): string
{
return $this->dateTime->format(\DateTimeInterface::RFC2822);
}
}

View File

@ -0,0 +1,72 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Header;
/**
* A MIME Header.
*
* @author Chris Corbyn
*/
interface HeaderInterface
{
/**
* Sets the body.
*
* The type depends on the Header concrete class.
*
* @return void
*/
public function setBody(mixed $body);
/**
* Gets the body.
*
* The return type depends on the Header concrete class.
*/
public function getBody(): mixed;
/**
* @return void
*/
public function setCharset(string $charset);
public function getCharset(): ?string;
/**
* @return void
*/
public function setLanguage(string $lang);
public function getLanguage(): ?string;
public function getName(): string;
/**
* @return void
*/
public function setMaxLineLength(int $lineLength);
public function getMaxLineLength(): int;
/**
* Gets this Header rendered as a compliant string.
*/
public function toString(): string;
/**
* Gets the header's body, prepared for folding into a final header value.
*
* This is not necessarily RFC 2822 compliant since folding white space is
* not added at this stage (see {@link toString()} for that).
*/
public function getBodyAsString(): string;
}

316
vendor/symfony/mime/Header/Headers.php vendored Normal file
View File

@ -0,0 +1,316 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Header;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Exception\LogicException;
/**
* A collection of headers.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class Headers
{
private const UNIQUE_HEADERS = [
'date', 'from', 'sender', 'reply-to', 'to', 'cc', 'bcc',
'message-id', 'in-reply-to', 'references', 'subject',
];
private const HEADER_CLASS_MAP = [
'date' => DateHeader::class,
'from' => MailboxListHeader::class,
'sender' => MailboxHeader::class,
'reply-to' => MailboxListHeader::class,
'to' => MailboxListHeader::class,
'cc' => MailboxListHeader::class,
'bcc' => MailboxListHeader::class,
'message-id' => IdentificationHeader::class,
'in-reply-to' => [UnstructuredHeader::class, IdentificationHeader::class], // `In-Reply-To` and `References` are less strict than RFC 2822 (3.6.4) to allow users entering the original email's ...
'references' => [UnstructuredHeader::class, IdentificationHeader::class], // ... `Message-ID`, even if that is no valid `msg-id`
'return-path' => PathHeader::class,
];
/**
* @var HeaderInterface[][]
*/
private array $headers = [];
private int $lineLength = 76;
public function __construct(HeaderInterface ...$headers)
{
foreach ($headers as $header) {
$this->add($header);
}
}
public function __clone()
{
foreach ($this->headers as $name => $collection) {
foreach ($collection as $i => $header) {
$this->headers[$name][$i] = clone $header;
}
}
}
public function setMaxLineLength(int $lineLength): void
{
$this->lineLength = $lineLength;
foreach ($this->all() as $header) {
$header->setMaxLineLength($lineLength);
}
}
public function getMaxLineLength(): int
{
return $this->lineLength;
}
/**
* @param array<Address|string> $addresses
*
* @return $this
*/
public function addMailboxListHeader(string $name, array $addresses): static
{
return $this->add(new MailboxListHeader($name, Address::createArray($addresses)));
}
/**
* @return $this
*/
public function addMailboxHeader(string $name, Address|string $address): static
{
return $this->add(new MailboxHeader($name, Address::create($address)));
}
/**
* @return $this
*/
public function addIdHeader(string $name, string|array $ids): static
{
return $this->add(new IdentificationHeader($name, $ids));
}
/**
* @return $this
*/
public function addPathHeader(string $name, Address|string $path): static
{
return $this->add(new PathHeader($name, $path instanceof Address ? $path : new Address($path)));
}
/**
* @return $this
*/
public function addDateHeader(string $name, \DateTimeInterface $dateTime): static
{
return $this->add(new DateHeader($name, $dateTime));
}
/**
* @return $this
*/
public function addTextHeader(string $name, string $value): static
{
return $this->add(new UnstructuredHeader($name, $value));
}
/**
* @return $this
*/
public function addParameterizedHeader(string $name, string $value, array $params = []): static
{
return $this->add(new ParameterizedHeader($name, $value, $params));
}
/**
* @return $this
*/
public function addHeader(string $name, mixed $argument, array $more = []): static
{
$headerClass = self::HEADER_CLASS_MAP[strtolower($name)] ?? UnstructuredHeader::class;
if (\is_array($headerClass)) {
$headerClass = $headerClass[0];
}
$parts = explode('\\', $headerClass);
$method = 'add'.ucfirst(array_pop($parts));
if ('addUnstructuredHeader' === $method) {
$method = 'addTextHeader';
} elseif ('addIdentificationHeader' === $method) {
$method = 'addIdHeader';
} elseif ('addMailboxListHeader' === $method && !\is_array($argument)) {
$argument = [$argument];
}
return $this->$method($name, $argument, $more);
}
public function has(string $name): bool
{
return isset($this->headers[strtolower($name)]);
}
/**
* @return $this
*/
public function add(HeaderInterface $header): static
{
self::checkHeaderClass($header);
$header->setMaxLineLength($this->lineLength);
$name = strtolower($header->getName());
if (\in_array($name, self::UNIQUE_HEADERS, true) && isset($this->headers[$name]) && \count($this->headers[$name]) > 0) {
throw new LogicException(sprintf('Impossible to set header "%s" as it\'s already defined and must be unique.', $header->getName()));
}
$this->headers[$name][] = $header;
return $this;
}
public function get(string $name): ?HeaderInterface
{
$name = strtolower($name);
if (!isset($this->headers[$name])) {
return null;
}
$values = array_values($this->headers[$name]);
return array_shift($values);
}
public function all(?string $name = null): iterable
{
if (null === $name) {
foreach ($this->headers as $name => $collection) {
foreach ($collection as $header) {
yield $name => $header;
}
}
} elseif (isset($this->headers[strtolower($name)])) {
foreach ($this->headers[strtolower($name)] as $header) {
yield $header;
}
}
}
public function getNames(): array
{
return array_keys($this->headers);
}
public function remove(string $name): void
{
unset($this->headers[strtolower($name)]);
}
public static function isUniqueHeader(string $name): bool
{
return \in_array(strtolower($name), self::UNIQUE_HEADERS, true);
}
/**
* @throws LogicException if the header name and class are not compatible
*/
public static function checkHeaderClass(HeaderInterface $header): void
{
$name = strtolower($header->getName());
$headerClasses = self::HEADER_CLASS_MAP[$name] ?? [];
if (!\is_array($headerClasses)) {
$headerClasses = [$headerClasses];
}
if (!$headerClasses) {
return;
}
foreach ($headerClasses as $c) {
if ($header instanceof $c) {
return;
}
}
throw new LogicException(sprintf('The "%s" header must be an instance of "%s" (got "%s").', $header->getName(), implode('" or "', $headerClasses), get_debug_type($header)));
}
public function toString(): string
{
$string = '';
foreach ($this->toArray() as $str) {
$string .= $str."\r\n";
}
return $string;
}
public function toArray(): array
{
$arr = [];
foreach ($this->all() as $header) {
if ('' !== $header->getBodyAsString()) {
$arr[] = $header->toString();
}
}
return $arr;
}
public function getHeaderBody(string $name): mixed
{
return $this->has($name) ? $this->get($name)->getBody() : null;
}
/**
* @internal
*/
public function setHeaderBody(string $type, string $name, mixed $body): void
{
if ($this->has($name)) {
$this->get($name)->setBody($body);
} else {
$this->{'add'.$type.'Header'}($name, $body);
}
}
public function getHeaderParameter(string $name, string $parameter): ?string
{
if (!$this->has($name)) {
return null;
}
$header = $this->get($name);
if (!$header instanceof ParameterizedHeader) {
throw new LogicException(sprintf('Unable to get parameter "%s" on header "%s" as the header is not of class "%s".', $parameter, $name, ParameterizedHeader::class));
}
return $header->getParameter($parameter);
}
/**
* @internal
*/
public function setHeaderParameter(string $name, string $parameter, ?string $value): void
{
if (!$this->has($name)) {
throw new LogicException(sprintf('Unable to set parameter "%s" on header "%s" as the header is not defined.', $parameter, $name));
}
$header = $this->get($name);
if (!$header instanceof ParameterizedHeader) {
throw new LogicException(sprintf('Unable to set parameter "%s" on header "%s" as the header is not of class "%s".', $parameter, $name, ParameterizedHeader::class));
}
$header->setParameter($parameter, $value);
}
}

View File

@ -0,0 +1,107 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Header;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Exception\RfcComplianceException;
/**
* An ID MIME Header for something like Message-ID or Content-ID (one or more addresses).
*
* @author Chris Corbyn
*/
final class IdentificationHeader extends AbstractHeader
{
private array $ids = [];
private array $idsAsAddresses = [];
public function __construct(string $name, string|array $ids)
{
parent::__construct($name);
$this->setId($ids);
}
/**
* @param string|string[] $body a string ID or an array of IDs
*
* @throws RfcComplianceException
*/
public function setBody(mixed $body): void
{
$this->setId($body);
}
public function getBody(): array
{
return $this->getIds();
}
/**
* Set the ID used in the value of this header.
*
* @param string|string[] $id
*
* @throws RfcComplianceException
*/
public function setId(string|array $id): void
{
$this->setIds(\is_array($id) ? $id : [$id]);
}
/**
* Get the ID used in the value of this Header.
*
* If multiple IDs are set only the first is returned.
*/
public function getId(): ?string
{
return $this->ids[0] ?? null;
}
/**
* Set a collection of IDs to use in the value of this Header.
*
* @param string[] $ids
*
* @throws RfcComplianceException
*/
public function setIds(array $ids): void
{
$this->ids = [];
$this->idsAsAddresses = [];
foreach ($ids as $id) {
$this->idsAsAddresses[] = new Address($id);
$this->ids[] = $id;
}
}
/**
* Get the list of IDs used in this Header.
*
* @return string[]
*/
public function getIds(): array
{
return $this->ids;
}
public function getBodyAsString(): string
{
$addrs = [];
foreach ($this->idsAsAddresses as $address) {
$addrs[] = '<'.$address->toString().'>';
}
return implode(' ', $addrs);
}
}

View File

@ -0,0 +1,85 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Header;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Exception\RfcComplianceException;
/**
* A Mailbox MIME Header for something like Sender (one named address).
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class MailboxHeader extends AbstractHeader
{
private Address $address;
public function __construct(string $name, Address $address)
{
parent::__construct($name);
$this->setAddress($address);
}
/**
* @param Address $body
*
* @throws RfcComplianceException
*/
public function setBody(mixed $body): void
{
$this->setAddress($body);
}
/**
* @throws RfcComplianceException
*/
public function getBody(): Address
{
return $this->getAddress();
}
/**
* @throws RfcComplianceException
*/
public function setAddress(Address $address): void
{
$this->address = $address;
}
public function getAddress(): Address
{
return $this->address;
}
public function getBodyAsString(): string
{
$str = $this->address->getEncodedAddress();
if ($name = $this->address->getName()) {
$str = $this->createPhrase($this, $name, $this->getCharset(), true).' <'.$str.'>';
}
return $str;
}
/**
* Redefine the encoding requirements for an address.
*
* All "specials" must be encoded as the full header value will not be quoted
*
* @see RFC 2822 3.2.1
*/
protected function tokenNeedsEncoding(string $token): bool
{
return preg_match('/[()<>\[\]:;@\,."]/', $token) || parent::tokenNeedsEncoding($token);
}
}

View File

@ -0,0 +1,136 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Header;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Exception\RfcComplianceException;
/**
* A Mailbox list MIME Header for something like From, To, Cc, and Bcc (one or more named addresses).
*
* @author Chris Corbyn
*/
final class MailboxListHeader extends AbstractHeader
{
private array $addresses = [];
/**
* @param Address[] $addresses
*/
public function __construct(string $name, array $addresses)
{
parent::__construct($name);
$this->setAddresses($addresses);
}
/**
* @param Address[] $body
*
* @throws RfcComplianceException
*/
public function setBody(mixed $body): void
{
$this->setAddresses($body);
}
/**
* @return Address[]
*
* @throws RfcComplianceException
*/
public function getBody(): array
{
return $this->getAddresses();
}
/**
* Sets a list of addresses to be shown in this Header.
*
* @param Address[] $addresses
*
* @throws RfcComplianceException
*/
public function setAddresses(array $addresses): void
{
$this->addresses = [];
$this->addAddresses($addresses);
}
/**
* Sets a list of addresses to be shown in this Header.
*
* @param Address[] $addresses
*
* @throws RfcComplianceException
*/
public function addAddresses(array $addresses): void
{
foreach ($addresses as $address) {
$this->addAddress($address);
}
}
/**
* @throws RfcComplianceException
*/
public function addAddress(Address $address): void
{
$this->addresses[] = $address;
}
/**
* @return Address[]
*/
public function getAddresses(): array
{
return $this->addresses;
}
/**
* Gets the full mailbox list of this Header as an array of valid RFC 2822 strings.
*
* @return string[]
*
* @throws RfcComplianceException
*/
public function getAddressStrings(): array
{
$strings = [];
foreach ($this->addresses as $address) {
$str = $address->getEncodedAddress();
if ($name = $address->getName()) {
$str = $this->createPhrase($this, $name, $this->getCharset(), !$strings).' <'.$str.'>';
}
$strings[] = $str;
}
return $strings;
}
public function getBodyAsString(): string
{
return implode(', ', $this->getAddressStrings());
}
/**
* Redefine the encoding requirements for addresses.
*
* All "specials" must be encoded as the full header value will not be quoted
*
* @see RFC 2822 3.2.1
*/
protected function tokenNeedsEncoding(string $token): bool
{
return preg_match('/[()<>\[\]:;@\,."]/', $token) || parent::tokenNeedsEncoding($token);
}
}

View File

@ -0,0 +1,191 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Header;
use Symfony\Component\Mime\Encoder\Rfc2231Encoder;
/**
* @author Chris Corbyn
*/
final class ParameterizedHeader extends UnstructuredHeader
{
/**
* RFC 2231's definition of a token.
*
* @var string
*/
public const TOKEN_REGEX = '(?:[\x21\x23-\x27\x2A\x2B\x2D\x2E\x30-\x39\x41-\x5A\x5E-\x7E]+)';
private ?Rfc2231Encoder $encoder = null;
private array $parameters = [];
public function __construct(string $name, string $value, array $parameters = [])
{
parent::__construct($name, $value);
foreach ($parameters as $k => $v) {
$this->setParameter($k, $v);
}
if ('content-type' !== strtolower($name)) {
$this->encoder = new Rfc2231Encoder();
}
}
public function setParameter(string $parameter, ?string $value): void
{
$this->setParameters(array_merge($this->getParameters(), [$parameter => $value]));
}
public function getParameter(string $parameter): string
{
return $this->getParameters()[$parameter] ?? '';
}
/**
* @param string[] $parameters
*/
public function setParameters(array $parameters): void
{
$this->parameters = $parameters;
}
/**
* @return string[]
*/
public function getParameters(): array
{
return $this->parameters;
}
public function getBodyAsString(): string
{
$body = parent::getBodyAsString();
foreach ($this->parameters as $name => $value) {
if (null !== $value) {
$body .= '; '.$this->createParameter($name, $value);
}
}
return $body;
}
/**
* Generate a list of all tokens in the final header.
*
* This doesn't need to be overridden in theory, but it is for implementation
* reasons to prevent potential breakage of attributes.
*/
protected function toTokens(?string $string = null): array
{
$tokens = parent::toTokens(parent::getBodyAsString());
// Try creating any parameters
foreach ($this->parameters as $name => $value) {
if (null !== $value) {
// Add the semi-colon separator
$tokens[\count($tokens) - 1] .= ';';
$tokens = array_merge($tokens, $this->generateTokenLines(' '.$this->createParameter($name, $value)));
}
}
return $tokens;
}
/**
* Render an RFC 2047 compliant header parameter from the $name and $value.
*/
private function createParameter(string $name, string $value): string
{
$origValue = $value;
$encoded = false;
// Allow room for parameter name, indices, "=" and DQUOTEs
$maxValueLength = $this->getMaxLineLength() - \strlen($name.'=*N"";') - 1;
$firstLineOffset = 0;
// If it's not already a valid parameter value...
if (!preg_match('/^'.self::TOKEN_REGEX.'$/D', $value)) {
// TODO: text, or something else??
// ... and it's not ascii
if (!preg_match('/^[\x00-\x08\x0B\x0C\x0E-\x7F]*$/D', $value)) {
$encoded = true;
// Allow space for the indices, charset and language
$maxValueLength = $this->getMaxLineLength() - \strlen($name.'*N*="";') - 1;
$firstLineOffset = \strlen($this->getCharset()."'".$this->getLanguage()."'");
}
if (\in_array($name, ['name', 'filename'], true) && 'form-data' === $this->getValue() && 'content-disposition' === strtolower($this->getName()) && preg_match('//u', $value)) {
// WHATWG HTML living standard 4.10.21.8 2 specifies:
// For field names and filenames for file fields, the result of the
// encoding in the previous bullet point must be escaped by replacing
// any 0x0A (LF) bytes with the byte sequence `%0A`, 0x0D (CR) with `%0D`
// and 0x22 (") with `%22`.
// The user agent must not perform any other escapes.
$value = str_replace(['"', "\r", "\n"], ['%22', '%0D', '%0A'], $value);
if (\strlen($value) <= $maxValueLength) {
return $name.'="'.$value.'"';
}
$value = $origValue;
}
}
// Encode if we need to
if ($encoded || \strlen($value) > $maxValueLength) {
if (null !== $this->encoder) {
$value = $this->encoder->encodeString($origValue, $this->getCharset(), $firstLineOffset, $maxValueLength);
} else {
// We have to go against RFC 2183/2231 in some areas for interoperability
$value = $this->getTokenAsEncodedWord($origValue);
$encoded = false;
}
}
$valueLines = $this->encoder ? explode("\r\n", $value) : [$value];
// Need to add indices
if (\count($valueLines) > 1) {
$paramLines = [];
foreach ($valueLines as $i => $line) {
$paramLines[] = $name.'*'.$i.$this->getEndOfParameterValue($line, true, 0 === $i);
}
return implode(";\r\n ", $paramLines);
} else {
return $name.$this->getEndOfParameterValue($valueLines[0], $encoded, true);
}
}
/**
* Returns the parameter value from the "=" and beyond.
*
* @param string $value to append
*/
private function getEndOfParameterValue(string $value, bool $encoded = false, bool $firstLine = false): string
{
$forceHttpQuoting = 'form-data' === $this->getValue() && 'content-disposition' === strtolower($this->getName());
if ($forceHttpQuoting || !preg_match('/^'.self::TOKEN_REGEX.'$/D', $value)) {
$value = '"'.$value.'"';
}
$prepend = '=';
if ($encoded) {
$prepend = '*=';
if ($firstLine) {
$prepend = '*='.$this->getCharset()."'".$this->getLanguage()."'";
}
}
return $prepend.$value;
}
}

View File

@ -0,0 +1,62 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Header;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Exception\RfcComplianceException;
/**
* A Path Header, such a Return-Path (one address).
*
* @author Chris Corbyn
*/
final class PathHeader extends AbstractHeader
{
private Address $address;
public function __construct(string $name, Address $address)
{
parent::__construct($name);
$this->setAddress($address);
}
/**
* @param Address $body
*
* @throws RfcComplianceException
*/
public function setBody(mixed $body): void
{
$this->setAddress($body);
}
public function getBody(): Address
{
return $this->getAddress();
}
public function setAddress(Address $address): void
{
$this->address = $address;
}
public function getAddress(): Address
{
return $this->address;
}
public function getBodyAsString(): string
{
return '<'.$this->address->toString().'>';
}
}

View File

@ -0,0 +1,70 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Header;
/**
* A Simple MIME Header.
*
* @author Chris Corbyn
*/
class UnstructuredHeader extends AbstractHeader
{
private string $value;
public function __construct(string $name, string $value)
{
parent::__construct($name);
$this->setValue($value);
}
/**
* @param string $body
*
* @return void
*/
public function setBody(mixed $body)
{
$this->setValue($body);
}
public function getBody(): string
{
return $this->getValue();
}
/**
* Get the (unencoded) value of this header.
*/
public function getValue(): string
{
return $this->value;
}
/**
* Set the (unencoded) value of this header.
*
* @return void
*/
public function setValue(string $value)
{
$this->value = $value;
}
/**
* Get the value of this header prepared for rendering.
*/
public function getBodyAsString(): string
{
return $this->encodeWords($this, $this->value);
}
}

View File

@ -0,0 +1,23 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\HtmlToTextConverter;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class DefaultHtmlToTextConverter implements HtmlToTextConverterInterface
{
public function convert(string $html, string $charset): string
{
return strip_tags(preg_replace('{<(head|style)\b.*?</\1>}is', '', $html));
}
}

View File

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\HtmlToTextConverter;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
interface HtmlToTextConverterInterface
{
/**
* Converts an HTML representation of a Message to a text representation.
*
* The output must use the same charset as the HTML one.
*/
public function convert(string $html, string $charset): string;
}

View File

@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\HtmlToTextConverter;
use League\HTMLToMarkdown\HtmlConverter;
use League\HTMLToMarkdown\HtmlConverterInterface;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class LeagueHtmlToMarkdownConverter implements HtmlToTextConverterInterface
{
public function __construct(
private HtmlConverterInterface $converter = new HtmlConverter([
'hard_break' => true,
'strip_tags' => true,
'remove_nodes' => 'head style',
]),
) {
}
public function convert(string $html, string $charset): string
{
return $this->converter->convert($html);
}
}

19
vendor/symfony/mime/LICENSE vendored Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2010-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

169
vendor/symfony/mime/Message.php vendored Normal file
View File

@ -0,0 +1,169 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime;
use Symfony\Component\Mime\Exception\LogicException;
use Symfony\Component\Mime\Header\Headers;
use Symfony\Component\Mime\Part\AbstractPart;
use Symfony\Component\Mime\Part\TextPart;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class Message extends RawMessage
{
private Headers $headers;
private ?AbstractPart $body;
public function __construct(?Headers $headers = null, ?AbstractPart $body = null)
{
$this->headers = $headers ? clone $headers : new Headers();
$this->body = $body;
}
public function __clone()
{
$this->headers = clone $this->headers;
if (null !== $this->body) {
$this->body = clone $this->body;
}
}
/**
* @return $this
*/
public function setBody(?AbstractPart $body = null): static
{
if (1 > \func_num_args()) {
trigger_deprecation('symfony/mime', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__);
}
$this->body = $body;
return $this;
}
public function getBody(): ?AbstractPart
{
return $this->body;
}
/**
* @return $this
*/
public function setHeaders(Headers $headers): static
{
$this->headers = $headers;
return $this;
}
public function getHeaders(): Headers
{
return $this->headers;
}
public function getPreparedHeaders(): Headers
{
$headers = clone $this->headers;
if (!$headers->has('From')) {
if (!$headers->has('Sender')) {
throw new LogicException('An email must have a "From" or a "Sender" header.');
}
$headers->addMailboxListHeader('From', [$headers->get('Sender')->getAddress()]);
}
if (!$headers->has('MIME-Version')) {
$headers->addTextHeader('MIME-Version', '1.0');
}
if (!$headers->has('Date')) {
$headers->addDateHeader('Date', new \DateTimeImmutable());
}
// determine the "real" sender
if (!$headers->has('Sender') && \count($froms = $headers->get('From')->getAddresses()) > 1) {
$headers->addMailboxHeader('Sender', $froms[0]);
}
if (!$headers->has('Message-ID')) {
$headers->addIdHeader('Message-ID', $this->generateMessageId());
}
// remove the Bcc field which should NOT be part of the sent message
$headers->remove('Bcc');
return $headers;
}
public function toString(): string
{
if (null === $body = $this->getBody()) {
$body = new TextPart('');
}
return $this->getPreparedHeaders()->toString().$body->toString();
}
public function toIterable(): iterable
{
if (null === $body = $this->getBody()) {
$body = new TextPart('');
}
yield $this->getPreparedHeaders()->toString();
yield from $body->toIterable();
}
/**
* @return void
*/
public function ensureValidity()
{
if (!$this->headers->has('To') && !$this->headers->has('Cc') && !$this->headers->has('Bcc')) {
throw new LogicException('An email must have a "To", "Cc", or "Bcc" header.');
}
if (!$this->headers->has('From') && !$this->headers->has('Sender')) {
throw new LogicException('An email must have a "From" or a "Sender" header.');
}
parent::ensureValidity();
}
public function generateMessageId(): string
{
if ($this->headers->has('Sender')) {
$sender = $this->headers->get('Sender')->getAddress();
} elseif ($this->headers->has('From')) {
if (!$froms = $this->headers->get('From')->getAddresses()) {
throw new LogicException('A "From" header must have at least one email address.');
}
$sender = $froms[0];
} else {
throw new LogicException('An email must have a "From" or a "Sender" header.');
}
return bin2hex(random_bytes(16)).strstr($sender->getAddress(), '@');
}
public function __serialize(): array
{
return [$this->headers, $this->body];
}
public function __unserialize(array $data): void
{
[$this->headers, $this->body] = $data;
}
}

122
vendor/symfony/mime/MessageConverter.php vendored Normal file
View File

@ -0,0 +1,122 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime;
use Symfony\Component\Mime\Exception\RuntimeException;
use Symfony\Component\Mime\Part\DataPart;
use Symfony\Component\Mime\Part\Multipart\AlternativePart;
use Symfony\Component\Mime\Part\Multipart\MixedPart;
use Symfony\Component\Mime\Part\Multipart\RelatedPart;
use Symfony\Component\Mime\Part\TextPart;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class MessageConverter
{
/**
* @throws RuntimeException when unable to convert the message to an email
*/
public static function toEmail(Message $message): Email
{
if ($message instanceof Email) {
return $message;
}
// try to convert to a "simple" Email instance
$body = $message->getBody();
if ($body instanceof TextPart) {
return self::createEmailFromTextPart($message, $body);
}
if ($body instanceof AlternativePart) {
return self::createEmailFromAlternativePart($message, $body);
}
if ($body instanceof RelatedPart) {
return self::createEmailFromRelatedPart($message, $body);
}
if ($body instanceof MixedPart) {
$parts = $body->getParts();
if ($parts[0] instanceof RelatedPart) {
$email = self::createEmailFromRelatedPart($message, $parts[0]);
} elseif ($parts[0] instanceof AlternativePart) {
$email = self::createEmailFromAlternativePart($message, $parts[0]);
} elseif ($parts[0] instanceof TextPart) {
$email = self::createEmailFromTextPart($message, $parts[0]);
} else {
throw new RuntimeException(sprintf('Unable to create an Email from an instance of "%s" as the body is too complex.', get_debug_type($message)));
}
return self::addParts($email, \array_slice($parts, 1));
}
throw new RuntimeException(sprintf('Unable to create an Email from an instance of "%s" as the body is too complex.', get_debug_type($message)));
}
private static function createEmailFromTextPart(Message $message, TextPart $part): Email
{
if ('text' === $part->getMediaType() && 'plain' === $part->getMediaSubtype()) {
return (new Email(clone $message->getHeaders()))->text($part->getBody(), $part->getPreparedHeaders()->getHeaderParameter('Content-Type', 'charset') ?: 'utf-8');
}
if ('text' === $part->getMediaType() && 'html' === $part->getMediaSubtype()) {
return (new Email(clone $message->getHeaders()))->html($part->getBody(), $part->getPreparedHeaders()->getHeaderParameter('Content-Type', 'charset') ?: 'utf-8');
}
throw new RuntimeException(sprintf('Unable to create an Email from an instance of "%s" as the body is too complex.', get_debug_type($message)));
}
private static function createEmailFromAlternativePart(Message $message, AlternativePart $part): Email
{
$parts = $part->getParts();
if (
2 === \count($parts)
&& $parts[0] instanceof TextPart && 'text' === $parts[0]->getMediaType() && 'plain' === $parts[0]->getMediaSubtype()
&& $parts[1] instanceof TextPart && 'text' === $parts[1]->getMediaType() && 'html' === $parts[1]->getMediaSubtype()
) {
return (new Email(clone $message->getHeaders()))
->text($parts[0]->getBody(), $parts[0]->getPreparedHeaders()->getHeaderParameter('Content-Type', 'charset') ?: 'utf-8')
->html($parts[1]->getBody(), $parts[1]->getPreparedHeaders()->getHeaderParameter('Content-Type', 'charset') ?: 'utf-8')
;
}
throw new RuntimeException(sprintf('Unable to create an Email from an instance of "%s" as the body is too complex.', get_debug_type($message)));
}
private static function createEmailFromRelatedPart(Message $message, RelatedPart $part): Email
{
$parts = $part->getParts();
if ($parts[0] instanceof AlternativePart) {
$email = self::createEmailFromAlternativePart($message, $parts[0]);
} elseif ($parts[0] instanceof TextPart) {
$email = self::createEmailFromTextPart($message, $parts[0]);
} else {
throw new RuntimeException(sprintf('Unable to create an Email from an instance of "%s" as the body is too complex.', get_debug_type($message)));
}
return self::addParts($email, \array_slice($parts, 1));
}
private static function addParts(Email $email, array $parts): Email
{
foreach ($parts as $part) {
if (!$part instanceof DataPart) {
throw new RuntimeException(sprintf('Unable to create an Email from an instance of "%s" as the body is too complex.', get_debug_type($email)));
}
$email->addPart($part);
}
return $email;
}
}

View File

@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime;
/**
* Guesses the MIME type of a file.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface MimeTypeGuesserInterface
{
/**
* Returns true if this guesser is supported.
*/
public function isGuesserSupported(): bool;
/**
* Guesses the MIME type of the file with the given path.
*
* @throws \LogicException If the guesser is not supported
* @throws \InvalidArgumentException If the file does not exist or is not readable
*/
public function guessMimeType(string $path): ?string;
}

3655
vendor/symfony/mime/MimeTypes.php vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
interface MimeTypesInterface extends MimeTypeGuesserInterface
{
/**
* Gets the extensions for the given MIME type in decreasing order of preference.
*
* @return string[]
*/
public function getExtensions(string $mimeType): array;
/**
* Gets the MIME types for the given extension in decreasing order of preference.
*
* @return string[]
*/
public function getMimeTypes(string $ext): array;
}

View File

@ -0,0 +1,95 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Part;
use Symfony\Component\Mime\Header\Headers;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
abstract class AbstractMultipartPart extends AbstractPart
{
private ?string $boundary = null;
private array $parts = [];
public function __construct(AbstractPart ...$parts)
{
parent::__construct();
foreach ($parts as $part) {
$this->parts[] = $part;
}
}
/**
* @return AbstractPart[]
*/
public function getParts(): array
{
return $this->parts;
}
public function getMediaType(): string
{
return 'multipart';
}
public function getPreparedHeaders(): Headers
{
$headers = parent::getPreparedHeaders();
$headers->setHeaderParameter('Content-Type', 'boundary', $this->getBoundary());
return $headers;
}
public function bodyToString(): string
{
$parts = $this->getParts();
$string = '';
foreach ($parts as $part) {
$string .= '--'.$this->getBoundary()."\r\n".$part->toString()."\r\n";
}
$string .= '--'.$this->getBoundary()."--\r\n";
return $string;
}
public function bodyToIterable(): iterable
{
$parts = $this->getParts();
foreach ($parts as $part) {
yield '--'.$this->getBoundary()."\r\n";
yield from $part->toIterable();
yield "\r\n";
}
yield '--'.$this->getBoundary()."--\r\n";
}
public function asDebugString(): string
{
$str = parent::asDebugString();
foreach ($this->getParts() as $part) {
$lines = explode("\n", $part->asDebugString());
$str .= "\n".array_shift($lines);
foreach ($lines as $line) {
$str .= "\n |".$line;
}
}
return $str;
}
private function getBoundary(): string
{
return $this->boundary ??= strtr(base64_encode(random_bytes(6)), '+/', '-_');
}
}

View File

@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Part;
use Symfony\Component\Mime\Header\Headers;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
abstract class AbstractPart
{
private Headers $headers;
public function __construct()
{
$this->headers = new Headers();
}
public function getHeaders(): Headers
{
return $this->headers;
}
public function getPreparedHeaders(): Headers
{
$headers = clone $this->headers;
$headers->setHeaderBody('Parameterized', 'Content-Type', $this->getMediaType().'/'.$this->getMediaSubtype());
return $headers;
}
public function toString(): string
{
return $this->getPreparedHeaders()->toString()."\r\n".$this->bodyToString();
}
public function toIterable(): iterable
{
yield $this->getPreparedHeaders()->toString();
yield "\r\n";
yield from $this->bodyToIterable();
}
public function asDebugString(): string
{
return $this->getMediaType().'/'.$this->getMediaSubtype();
}
abstract public function bodyToString(): string;
abstract public function bodyToIterable(): iterable;
abstract public function getMediaType(): string;
abstract public function getMediaSubtype(): string;
}

168
vendor/symfony/mime/Part/DataPart.php vendored Normal file
View File

@ -0,0 +1,168 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Part;
use Symfony\Component\Mime\Exception\InvalidArgumentException;
use Symfony\Component\Mime\Header\Headers;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class DataPart extends TextPart
{
/** @internal */
protected array $_parent;
private ?string $filename = null;
private string $mediaType;
private ?string $cid = null;
/**
* @param resource|string|File $body Use a File instance to defer loading the file until rendering
*/
public function __construct($body, ?string $filename = null, ?string $contentType = null, ?string $encoding = null)
{
if ($body instanceof File && !$filename) {
$filename = $body->getFilename();
}
$contentType ??= $body instanceof File ? $body->getContentType() : 'application/octet-stream';
[$this->mediaType, $subtype] = explode('/', $contentType);
parent::__construct($body, null, $subtype, $encoding);
if (null !== $filename) {
$this->filename = $filename;
$this->setName($filename);
}
$this->setDisposition('attachment');
}
public static function fromPath(string $path, ?string $name = null, ?string $contentType = null): self
{
return new self(new File($path), $name, $contentType);
}
/**
* @return $this
*/
public function asInline(): static
{
return $this->setDisposition('inline');
}
/**
* @return $this
*/
public function setContentId(string $cid): static
{
if (!str_contains($cid, '@')) {
throw new InvalidArgumentException(sprintf('Invalid cid "%s".', $cid));
}
$this->cid = $cid;
return $this;
}
public function getContentId(): string
{
return $this->cid ?: $this->cid = $this->generateContentId();
}
public function hasContentId(): bool
{
return null !== $this->cid;
}
public function getMediaType(): string
{
return $this->mediaType;
}
public function getPreparedHeaders(): Headers
{
$headers = parent::getPreparedHeaders();
if (null !== $this->cid) {
$headers->setHeaderBody('Id', 'Content-ID', $this->cid);
}
if (null !== $this->filename) {
$headers->setHeaderParameter('Content-Disposition', 'filename', $this->filename);
}
return $headers;
}
public function asDebugString(): string
{
$str = parent::asDebugString();
if (null !== $this->filename) {
$str .= ' filename: '.$this->filename;
}
return $str;
}
public function getFilename(): ?string
{
return $this->filename;
}
public function getContentType(): string
{
return implode('/', [$this->getMediaType(), $this->getMediaSubtype()]);
}
private function generateContentId(): string
{
return bin2hex(random_bytes(16)).'@symfony';
}
public function __sleep(): array
{
// converts the body to a string
parent::__sleep();
$this->_parent = [];
foreach (['body', 'charset', 'subtype', 'disposition', 'name', 'encoding'] as $name) {
$r = new \ReflectionProperty(TextPart::class, $name);
$this->_parent[$name] = $r->getValue($this);
}
$this->_headers = $this->getHeaders();
return ['_headers', '_parent', 'filename', 'mediaType'];
}
/**
* @return void
*/
public function __wakeup()
{
$r = new \ReflectionProperty(AbstractPart::class, 'headers');
$r->setValue($this, $this->_headers);
unset($this->_headers);
if (!\is_array($this->_parent)) {
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
foreach (['body', 'charset', 'subtype', 'disposition', 'name', 'encoding'] as $name) {
if (null !== $this->_parent[$name] && !\is_string($this->_parent[$name]) && !$this->_parent[$name] instanceof File) {
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
$r = new \ReflectionProperty(TextPart::class, $name);
$r->setValue($this, $this->_parent[$name]);
}
unset($this->_parent);
}
}

51
vendor/symfony/mime/Part/File.php vendored Normal file
View File

@ -0,0 +1,51 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Part;
use Symfony\Component\Mime\MimeTypes;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class File
{
private static MimeTypes $mimeTypes;
public function __construct(
private string $path,
private ?string $filename = null,
) {
}
public function getPath(): string
{
return $this->path;
}
public function getContentType(): string
{
$ext = strtolower(pathinfo($this->path, \PATHINFO_EXTENSION));
self::$mimeTypes ??= new MimeTypes();
return self::$mimeTypes->getMimeTypes($ext)[0] ?? 'application/octet-stream';
}
public function getSize(): int
{
return filesize($this->path);
}
public function getFilename(): string
{
return $this->filename ??= basename($this->getPath());
}
}

View File

@ -0,0 +1,72 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Part;
use Symfony\Component\Mime\Message;
use Symfony\Component\Mime\RawMessage;
/**
* @final
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class MessagePart extends DataPart
{
private RawMessage $message;
public function __construct(RawMessage $message)
{
if ($message instanceof Message) {
$name = $message->getHeaders()->getHeaderBody('Subject').'.eml';
} else {
$name = 'email.eml';
}
parent::__construct('', $name);
$this->message = $message;
}
public function getMediaType(): string
{
return 'message';
}
public function getMediaSubtype(): string
{
return 'rfc822';
}
public function getBody(): string
{
return $this->message->toString();
}
public function bodyToString(): string
{
return $this->getBody();
}
public function bodyToIterable(): iterable
{
return $this->message->toIterable();
}
public function __sleep(): array
{
return ['message'];
}
public function __wakeup(): void
{
$this->__construct($this->message);
}
}

View File

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Part\Multipart;
use Symfony\Component\Mime\Part\AbstractMultipartPart;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class AlternativePart extends AbstractMultipartPart
{
public function getMediaSubtype(): string
{
return 'alternative';
}
}

View File

@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Part\Multipart;
use Symfony\Component\Mime\Part\AbstractMultipartPart;
use Symfony\Component\Mime\Part\MessagePart;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class DigestPart extends AbstractMultipartPart
{
public function __construct(MessagePart ...$parts)
{
parent::__construct(...$parts);
}
public function getMediaSubtype(): string
{
return 'digest';
}
}

View File

@ -0,0 +1,108 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Part\Multipart;
use Symfony\Component\Mime\Exception\InvalidArgumentException;
use Symfony\Component\Mime\Part\AbstractMultipartPart;
use Symfony\Component\Mime\Part\DataPart;
use Symfony\Component\Mime\Part\TextPart;
/**
* Implements RFC 7578.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
final class FormDataPart extends AbstractMultipartPart
{
private array $fields = [];
/**
* @param array<string|array|DataPart> $fields
*/
public function __construct(array $fields = [])
{
parent::__construct();
$this->fields = $fields;
// HTTP does not support \r\n in header values
$this->getHeaders()->setMaxLineLength(\PHP_INT_MAX);
}
public function getMediaSubtype(): string
{
return 'form-data';
}
public function getParts(): array
{
return $this->prepareFields($this->fields);
}
private function prepareFields(array $fields): array
{
$values = [];
$prepare = function ($item, $key, $root = null) use (&$values, &$prepare) {
if (null === $root && \is_int($key) && \is_array($item)) {
if (1 !== \count($item)) {
throw new InvalidArgumentException(sprintf('Form field values with integer keys can only have one array element, the key being the field name and the value being the field value, %d provided.', \count($item)));
}
$key = key($item);
$item = $item[$key];
}
$fieldName = null !== $root ? sprintf('%s[%s]', $root, $key) : $key;
if (\is_array($item)) {
array_walk($item, $prepare, $fieldName);
return;
}
if (!\is_string($item) && !$item instanceof TextPart) {
throw new InvalidArgumentException(sprintf('The value of the form field "%s" can only be a string, an array, or an instance of TextPart, "%s" given.', $fieldName, get_debug_type($item)));
}
$values[] = $this->preparePart($fieldName, $item);
};
array_walk($fields, $prepare);
return $values;
}
private function preparePart(string $name, string|TextPart $value): TextPart
{
if (\is_string($value)) {
return $this->configurePart($name, new TextPart($value, 'utf-8', 'plain', '8bit'));
}
return $this->configurePart($name, $value);
}
private function configurePart(string $name, TextPart $part): TextPart
{
static $r;
$r ??= new \ReflectionProperty(TextPart::class, 'encoding');
$part->setDisposition('form-data');
$part->setName($name);
// HTTP does not support \r\n in header values
$part->getHeaders()->setMaxLineLength(\PHP_INT_MAX);
$r->setValue($part, '8bit');
return $part;
}
}

View File

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Part\Multipart;
use Symfony\Component\Mime\Part\AbstractMultipartPart;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class MixedPart extends AbstractMultipartPart
{
public function getMediaSubtype(): string
{
return 'mixed';
}
}

View File

@ -0,0 +1,55 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Part\Multipart;
use Symfony\Component\Mime\Part\AbstractMultipartPart;
use Symfony\Component\Mime\Part\AbstractPart;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class RelatedPart extends AbstractMultipartPart
{
private AbstractPart $mainPart;
public function __construct(AbstractPart $mainPart, AbstractPart $part, AbstractPart ...$parts)
{
$this->mainPart = $mainPart;
$this->prepareParts($part, ...$parts);
parent::__construct($part, ...$parts);
}
public function getParts(): array
{
return array_merge([$this->mainPart], parent::getParts());
}
public function getMediaSubtype(): string
{
return 'related';
}
private function generateContentId(): string
{
return bin2hex(random_bytes(16)).'@symfony';
}
private function prepareParts(AbstractPart ...$parts): void
{
foreach ($parts as $part) {
if (!$part->getHeaders()->has('Content-ID')) {
$part->getHeaders()->setHeaderBody('Id', 'Content-ID', $this->generateContentId());
}
}
}
}

111
vendor/symfony/mime/Part/SMimePart.php vendored Normal file
View File

@ -0,0 +1,111 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Part;
use Symfony\Component\Mime\Header\Headers;
/**
* @author Sebastiaan Stok <s.stok@rollerscapes.net>
*/
class SMimePart extends AbstractPart
{
/** @internal */
protected Headers $_headers;
private iterable|string $body;
private string $type;
private string $subtype;
private array $parameters;
public function __construct(iterable|string $body, string $type, string $subtype, array $parameters)
{
parent::__construct();
$this->body = $body;
$this->type = $type;
$this->subtype = $subtype;
$this->parameters = $parameters;
}
public function getMediaType(): string
{
return $this->type;
}
public function getMediaSubtype(): string
{
return $this->subtype;
}
public function bodyToString(): string
{
if (\is_string($this->body)) {
return $this->body;
}
$body = '';
foreach ($this->body as $chunk) {
$body .= $chunk;
}
$this->body = $body;
return $body;
}
public function bodyToIterable(): iterable
{
if (\is_string($this->body)) {
yield $this->body;
return;
}
$body = '';
foreach ($this->body as $chunk) {
$body .= $chunk;
yield $chunk;
}
$this->body = $body;
}
public function getPreparedHeaders(): Headers
{
$headers = clone parent::getHeaders();
$headers->setHeaderBody('Parameterized', 'Content-Type', $this->getMediaType().'/'.$this->getMediaSubtype());
foreach ($this->parameters as $name => $value) {
$headers->setHeaderParameter('Content-Type', $name, $value);
}
return $headers;
}
public function __sleep(): array
{
// convert iterables to strings for serialization
if (is_iterable($this->body)) {
$this->body = $this->bodyToString();
}
$this->_headers = $this->getHeaders();
return ['_headers', 'body', 'type', 'subtype', 'parameters'];
}
public function __wakeup(): void
{
$r = new \ReflectionProperty(AbstractPart::class, 'headers');
$r->setValue($this, $this->_headers);
unset($this->_headers);
}
}

244
vendor/symfony/mime/Part/TextPart.php vendored Normal file
View File

@ -0,0 +1,244 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Part;
use Symfony\Component\Mime\Encoder\Base64ContentEncoder;
use Symfony\Component\Mime\Encoder\ContentEncoderInterface;
use Symfony\Component\Mime\Encoder\EightBitContentEncoder;
use Symfony\Component\Mime\Encoder\QpContentEncoder;
use Symfony\Component\Mime\Exception\InvalidArgumentException;
use Symfony\Component\Mime\Header\Headers;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class TextPart extends AbstractPart
{
/** @internal */
protected Headers $_headers;
private static array $encoders = [];
/** @var resource|string|File */
private $body;
private ?string $charset;
private string $subtype;
private ?string $disposition = null;
private ?string $name = null;
private string $encoding;
private ?bool $seekable = null;
/**
* @param resource|string|File $body Use a File instance to defer loading the file until rendering
*/
public function __construct($body, ?string $charset = 'utf-8', string $subtype = 'plain', ?string $encoding = null)
{
parent::__construct();
if (!\is_string($body) && !\is_resource($body) && !$body instanceof File) {
throw new \TypeError(sprintf('The body of "%s" must be a string, a resource, or an instance of "%s" (got "%s").', self::class, File::class, get_debug_type($body)));
}
if ($body instanceof File) {
$path = $body->getPath();
if ((is_file($path) && !is_readable($path)) || is_dir($path)) {
throw new InvalidArgumentException(sprintf('Path "%s" is not readable.', $path));
}
}
$this->body = $body;
$this->charset = $charset;
$this->subtype = $subtype;
$this->seekable = \is_resource($body) ? stream_get_meta_data($body)['seekable'] && 0 === fseek($body, 0, \SEEK_CUR) : null;
if (null === $encoding) {
$this->encoding = $this->chooseEncoding();
} else {
if ('quoted-printable' !== $encoding && 'base64' !== $encoding && '8bit' !== $encoding) {
throw new InvalidArgumentException(sprintf('The encoding must be one of "quoted-printable", "base64", or "8bit" ("%s" given).', $encoding));
}
$this->encoding = $encoding;
}
}
public function getMediaType(): string
{
return 'text';
}
public function getMediaSubtype(): string
{
return $this->subtype;
}
/**
* @param string $disposition one of attachment, inline, or form-data
*
* @return $this
*/
public function setDisposition(string $disposition): static
{
$this->disposition = $disposition;
return $this;
}
/**
* @return ?string null or one of attachment, inline, or form-data
*/
public function getDisposition(): ?string
{
return $this->disposition;
}
/**
* Sets the name of the file (used by FormDataPart).
*
* @return $this
*/
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
/**
* Gets the name of the file.
*/
public function getName(): ?string
{
return $this->name;
}
public function getBody(): string
{
if ($this->body instanceof File) {
return file_get_contents($this->body->getPath());
}
if (null === $this->seekable) {
return $this->body;
}
if ($this->seekable) {
rewind($this->body);
}
return stream_get_contents($this->body) ?: '';
}
public function bodyToString(): string
{
return $this->getEncoder()->encodeString($this->getBody(), $this->charset);
}
public function bodyToIterable(): iterable
{
if ($this->body instanceof File) {
$path = $this->body->getPath();
if (false === $handle = @fopen($path, 'r', false)) {
throw new InvalidArgumentException(sprintf('Unable to open path "%s".', $path));
}
yield from $this->getEncoder()->encodeByteStream($handle);
} elseif (null !== $this->seekable) {
if ($this->seekable) {
rewind($this->body);
}
yield from $this->getEncoder()->encodeByteStream($this->body);
} else {
yield $this->getEncoder()->encodeString($this->body);
}
}
public function getPreparedHeaders(): Headers
{
$headers = parent::getPreparedHeaders();
$headers->setHeaderBody('Parameterized', 'Content-Type', $this->getMediaType().'/'.$this->getMediaSubtype());
if ($this->charset) {
$headers->setHeaderParameter('Content-Type', 'charset', $this->charset);
}
if ($this->name && 'form-data' !== $this->disposition) {
$headers->setHeaderParameter('Content-Type', 'name', $this->name);
}
$headers->setHeaderBody('Text', 'Content-Transfer-Encoding', $this->encoding);
if (!$headers->has('Content-Disposition') && null !== $this->disposition) {
$headers->setHeaderBody('Parameterized', 'Content-Disposition', $this->disposition);
if ($this->name) {
$headers->setHeaderParameter('Content-Disposition', 'name', $this->name);
}
}
return $headers;
}
public function asDebugString(): string
{
$str = parent::asDebugString();
if (null !== $this->charset) {
$str .= ' charset: '.$this->charset;
}
if (null !== $this->disposition) {
$str .= ' disposition: '.$this->disposition;
}
return $str;
}
private function getEncoder(): ContentEncoderInterface
{
if ('8bit' === $this->encoding) {
return self::$encoders[$this->encoding] ??= new EightBitContentEncoder();
}
if ('quoted-printable' === $this->encoding) {
return self::$encoders[$this->encoding] ??= new QpContentEncoder();
}
return self::$encoders[$this->encoding] ??= new Base64ContentEncoder();
}
private function chooseEncoding(): string
{
if (null === $this->charset) {
return 'base64';
}
return 'quoted-printable';
}
public function __sleep(): array
{
// convert resources to strings for serialization
if (null !== $this->seekable) {
$this->body = $this->getBody();
$this->seekable = null;
}
$this->_headers = $this->getHeaders();
return ['_headers', 'body', 'charset', 'subtype', 'disposition', 'name', 'encoding'];
}
/**
* @return void
*/
public function __wakeup()
{
$r = new \ReflectionProperty(AbstractPart::class, 'headers');
$r->setValue($this, $this->_headers);
unset($this->_headers);
}
}

13
vendor/symfony/mime/README.md vendored Normal file
View File

@ -0,0 +1,13 @@
MIME Component
==============
The MIME component allows manipulating MIME messages.
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/mime.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)

91
vendor/symfony/mime/RawMessage.php vendored Normal file
View File

@ -0,0 +1,91 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime;
use Symfony\Component\Mime\Exception\LogicException;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class RawMessage
{
private iterable|string $message;
private bool $isGeneratorClosed;
public function __construct(iterable|string $message)
{
$this->message = $message;
}
public function toString(): string
{
if (\is_string($this->message)) {
return $this->message;
}
$message = '';
foreach ($this->message as $chunk) {
$message .= $chunk;
}
return $this->message = $message;
}
public function toIterable(): iterable
{
if ($this->isGeneratorClosed ?? false) {
trigger_deprecation('symfony/mime', '6.4', 'Sending an email with a closed generator is deprecated and will throw in 7.0.');
// throw new LogicException('Unable to send the email as its generator is already closed.');
}
if (\is_string($this->message)) {
yield $this->message;
return;
}
if ($this->message instanceof \Generator) {
$message = '';
foreach ($this->message as $chunk) {
$message .= $chunk;
yield $chunk;
}
$this->isGeneratorClosed = !$this->message->valid();
$this->message = $message;
return;
}
foreach ($this->message as $chunk) {
yield $chunk;
}
}
/**
* @return void
*
* @throws LogicException if the message is not valid
*/
public function ensureValidity()
{
}
public function __serialize(): array
{
return [$this->toString()];
}
public function __unserialize(array $data): void
{
[$this->message] = $data;
}
}

View File

@ -0,0 +1,168 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if ('cli' !== \PHP_SAPI) {
throw new Exception('This script must be run from the command line.');
}
// load new map
$data = json_decode(file_get_contents('https://cdn.jsdelivr.net/gh/jshttp/mime-db/db.json'), true);
$new = [];
foreach ($data as $mimeType => $mimeTypeInformation) {
if (!array_key_exists('extensions', $mimeTypeInformation)) {
continue;
}
$new[$mimeType] = $mimeTypeInformation['extensions'];
}
$xml = simplexml_load_string(file_get_contents('https://gitlab.freedesktop.org/xdg/shared-mime-info/-/raw/master/data/freedesktop.org.xml.in'));
foreach ($xml as $node) {
$exts = [];
foreach ($node->glob as $glob) {
$pattern = (string) $glob['pattern'];
if ('*' != $pattern[0] || '.' != $pattern[1]) {
continue;
}
$exts[] = substr($pattern, 2);
}
if (!$exts) {
continue;
}
$mt = strtolower((string) $node['type']);
$new[$mt] = array_merge($new[$mt] ?? [], $exts);
foreach ($node->alias as $alias) {
$mt = strtolower((string) $alias['type']);
$new[$mt] = array_merge($new[$mt] ?? [], $exts);
}
}
// load current map
$data = file_get_contents($output = __DIR__.'/../../MimeTypes.php');
$current = [];
$pre = '';
$post = '';
foreach (explode("\n", $data) as $line) {
if (!preg_match("{^ '([^']+/[^']+)' => \['(.+)'\],$}", $line, $matches)) {
if (!$current) {
$pre .= $line."\n";
} else {
$post .= $line."\n";
}
continue;
}
$current[$matches[1]] = explode("', '", $matches[2]);
}
$data = $pre;
// reverse map
// we prefill the extensions with some preferences for content-types
$exts = [
'asice' => ['application/vnd.etsi.asic-e+zip'],
'bz2' => ['application/x-bz2'],
'csv' => ['text/csv'],
'ecma' => ['application/ecmascript'],
'flv' => ['video/x-flv'],
'gif' => ['image/gif'],
'gz' => ['application/x-gzip'],
'htm' => ['text/html'],
'html' => ['text/html'],
'jar' => ['application/x-java-archive'],
'jpg' => ['image/jpeg'],
'js' => ['text/javascript'],
'keynote' => ['application/vnd.apple.keynote'],
'key' => ['application/vnd.apple.keynote'],
'm3u' => ['audio/x-mpegurl'],
'm4a' => ['audio/mp4'],
'md' => ['text/markdown', 'text/x-markdown'],
'mdb' => ['application/x-msaccess'],
'mid' => ['audio/midi'],
'mov' => ['video/quicktime'],
'mp3' => ['audio/mpeg'],
'ogg' => ['audio/ogg'],
'pdf' => ['application/pdf'],
'php' => ['application/x-php'],
'png' => ['image/png'],
'ppt' => ['application/vnd.ms-powerpoint'],
'rar' => ['application/x-rar-compressed'],
'hqx' => ['application/stuffit'],
'sit' => ['application/x-stuffit', 'application/stuffit'],
'svg' => ['image/svg+xml'],
'tar' => ['application/x-tar'],
'tif' => ['image/tiff'],
'ttf' => ['application/x-font-truetype'],
'vcf' => ['text/x-vcard'],
'wav' => ['audio/wav'],
'wma' => ['audio/x-ms-wma'],
'wmv' => ['audio/x-ms-wmv'],
'xls' => ['application/vnd.ms-excel'],
'yaml' => ['application/yaml'],
'yml' => ['application/yaml'],
'zip' => ['application/zip'],
];
// we merge the 2 maps (we never remove old mime types)
$map = array_replace_recursive($current, $new);
foreach ($exts as $ext => $types) {
foreach ($types as $mt) {
if (!isset($map[$mt])) {
$map += [$mt => [$ext]];
}
}
}
ksort($map);
foreach ($map as $mimeType => $extensions) {
foreach ($exts as $ext => $types) {
if (in_array($mimeType, $types, true)) {
array_unshift($extensions, $ext);
}
}
$data .= sprintf(" '%s' => ['%s'],\n", $mimeType, implode("', '", array_unique($extensions)));
}
$data .= $post;
foreach ($map as $mimeType => $extensions) {
foreach ($extensions as $extension) {
if ('application/octet-stream' === $mimeType && 'bin' !== $extension) {
continue;
}
$exts[$extension][] = $mimeType;
}
}
ksort($exts);
$updated = '';
$state = 0;
foreach (explode("\n", $data) as $line) {
if (!preg_match("{^ '([^'/]+)' => \['(.+)'\],$}", $line, $matches)) {
if (1 === $state) {
$state = 2;
foreach ($exts as $ext => $mimeTypes) {
$updated .= sprintf(" '%s' => ['%s'],\n", $ext, implode("', '", array_unique($mimeTypes)));
}
}
$updated .= $line."\n";
continue;
}
$state = 1;
}
$updated = preg_replace('{Updated from upstream on .+?\.}', sprintf('Updated from upstream on %s.', date('Y-m-d')), $updated, -1);
file_put_contents($output, rtrim($updated, "\n")."\n");
echo "Done.\n";

View File

@ -0,0 +1,67 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Test\Constraint;
use PHPUnit\Framework\Constraint\Constraint;
use Symfony\Component\Mime\Header\MailboxHeader;
use Symfony\Component\Mime\Header\MailboxListHeader;
use Symfony\Component\Mime\RawMessage;
final class EmailAddressContains extends Constraint
{
private string $headerName;
private string $expectedValue;
public function __construct(string $headerName, string $expectedValue)
{
$this->headerName = $headerName;
$this->expectedValue = $expectedValue;
}
public function toString(): string
{
return sprintf('contains address "%s" with value "%s"', $this->headerName, $this->expectedValue);
}
/**
* @param RawMessage $message
*/
protected function matches($message): bool
{
if (RawMessage::class === $message::class) {
throw new \LogicException('Unable to test a message address on a RawMessage instance.');
}
$header = $message->getHeaders()->get($this->headerName);
if ($header instanceof MailboxHeader) {
return $this->expectedValue === $header->getAddress()->getAddress();
} elseif ($header instanceof MailboxListHeader) {
foreach ($header->getAddresses() as $address) {
if ($this->expectedValue === $address->getAddress()) {
return true;
}
}
return false;
}
throw new \LogicException('Unable to test a message address on a non-address header.');
}
/**
* @param RawMessage $message
*/
protected function failureDescription($message): string
{
return sprintf('the Email %s (value is %s)', $this->toString(), $message->getHeaders()->get($this->headerName)->getBodyAsString());
}
}

View File

@ -0,0 +1,53 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Test\Constraint;
use PHPUnit\Framework\Constraint\Constraint;
use Symfony\Component\Mime\Message;
use Symfony\Component\Mime\RawMessage;
final class EmailAttachmentCount extends Constraint
{
private int $expectedValue;
private ?string $transport;
public function __construct(int $expectedValue, ?string $transport = null)
{
$this->expectedValue = $expectedValue;
$this->transport = $transport;
}
public function toString(): string
{
return sprintf('has sent "%d" attachment(s)', $this->expectedValue);
}
/**
* @param RawMessage $message
*/
protected function matches($message): bool
{
if (RawMessage::class === $message::class || Message::class === $message::class) {
throw new \LogicException('Unable to test a message attachment on a RawMessage or Message instance.');
}
return $this->expectedValue === \count($message->getAttachments());
}
/**
* @param RawMessage $message
*/
protected function failureDescription($message): string
{
return 'the Email '.$this->toString();
}
}

View File

@ -0,0 +1,50 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Test\Constraint;
use PHPUnit\Framework\Constraint\Constraint;
use Symfony\Component\Mime\RawMessage;
final class EmailHasHeader extends Constraint
{
private string $headerName;
public function __construct(string $headerName)
{
$this->headerName = $headerName;
}
public function toString(): string
{
return sprintf('has header "%s"', $this->headerName);
}
/**
* @param RawMessage $message
*/
protected function matches($message): bool
{
if (RawMessage::class === $message::class) {
throw new \LogicException('Unable to test a message header on a RawMessage instance.');
}
return $message->getHeaders()->has($this->headerName);
}
/**
* @param RawMessage $message
*/
protected function failureDescription($message): string
{
return 'the Email '.$this->toString();
}
}

View File

@ -0,0 +1,62 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Test\Constraint;
use PHPUnit\Framework\Constraint\Constraint;
use Symfony\Component\Mime\Header\UnstructuredHeader;
use Symfony\Component\Mime\RawMessage;
final class EmailHeaderSame extends Constraint
{
private string $headerName;
private string $expectedValue;
public function __construct(string $headerName, string $expectedValue)
{
$this->headerName = $headerName;
$this->expectedValue = $expectedValue;
}
public function toString(): string
{
return sprintf('has header "%s" with value "%s"', $this->headerName, $this->expectedValue);
}
/**
* @param RawMessage $message
*/
protected function matches($message): bool
{
if (RawMessage::class === $message::class) {
throw new \LogicException('Unable to test a message header on a RawMessage instance.');
}
return $this->expectedValue === $this->getHeaderValue($message);
}
/**
* @param RawMessage $message
*/
protected function failureDescription($message): string
{
return sprintf('the Email %s (value is %s)', $this->toString(), $this->getHeaderValue($message) ?? 'null');
}
private function getHeaderValue($message): ?string
{
if (null === $header = $message->getHeaders()->get($this->headerName)) {
return null;
}
return $header instanceof UnstructuredHeader ? $header->getValue() : $header->getBodyAsString();
}
}

View File

@ -0,0 +1,51 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Test\Constraint;
use PHPUnit\Framework\Constraint\Constraint;
use Symfony\Component\Mime\Message;
use Symfony\Component\Mime\RawMessage;
final class EmailHtmlBodyContains extends Constraint
{
private string $expectedText;
public function __construct(string $expectedText)
{
$this->expectedText = $expectedText;
}
public function toString(): string
{
return sprintf('contains "%s"', $this->expectedText);
}
/**
* @param RawMessage $message
*/
protected function matches($message): bool
{
if (RawMessage::class === $message::class || Message::class === $message::class) {
throw new \LogicException('Unable to test a message HTML body on a RawMessage or Message instance.');
}
return str_contains($message->getHtmlBody(), $this->expectedText);
}
/**
* @param RawMessage $message
*/
protected function failureDescription($message): string
{
return 'the Email HTML body '.$this->toString();
}
}

View File

@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Test\Constraint;
use PHPUnit\Framework\Constraint\Constraint;
use Symfony\Component\Mime\Email;
final class EmailSubjectContains extends Constraint
{
public function __construct(
private readonly string $expectedSubjectValue,
) {
}
public function toString(): string
{
return sprintf('contains subject with value "%s"', $this->expectedSubjectValue);
}
protected function matches($other): bool
{
if (!$other instanceof Email) {
throw new \LogicException('Can only test a message subject on an Email instance.');
}
return str_contains((string) $other->getSubject(), $this->expectedSubjectValue);
}
protected function failureDescription($other): string
{
$message = 'The email subject '.$this->toString();
if ($other instanceof Email) {
$message .= sprintf('. The subject was: "%s"', $other->getSubject() ?? '<empty>');
}
return $message;
}
}

View File

@ -0,0 +1,51 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Test\Constraint;
use PHPUnit\Framework\Constraint\Constraint;
use Symfony\Component\Mime\Message;
use Symfony\Component\Mime\RawMessage;
final class EmailTextBodyContains extends Constraint
{
private string $expectedText;
public function __construct(string $expectedText)
{
$this->expectedText = $expectedText;
}
public function toString(): string
{
return sprintf('contains "%s"', $this->expectedText);
}
/**
* @param RawMessage $message
*/
protected function matches($message): bool
{
if (RawMessage::class === $message::class || Message::class === $message::class) {
throw new \LogicException('Unable to test a message text body on a RawMessage or Message instance.');
}
return str_contains($message->getTextBody(), $this->expectedText);
}
/**
* @param RawMessage $message
*/
protected function failureDescription($message): string
{
return 'the Email text body '.$this->toString();
}
}

48
vendor/symfony/mime/composer.json vendored Normal file
View File

@ -0,0 +1,48 @@
{
"name": "symfony/mime",
"type": "library",
"description": "Allows manipulating MIME messages",
"keywords": ["mime", "mime-type"],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=8.1",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-intl-idn": "^1.10",
"symfony/polyfill-mbstring": "^1.0"
},
"require-dev": {
"egulias/email-validator": "^2.1.10|^3.1|^4",
"league/html-to-markdown": "^5.0",
"phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
"symfony/dependency-injection": "^5.4|^6.0|^7.0",
"symfony/process": "^5.4|^6.4|^7.0",
"symfony/property-access": "^5.4|^6.0|^7.0",
"symfony/property-info": "^5.4|^6.0|^7.0",
"symfony/serializer": "^6.3.2|^7.0"
},
"conflict": {
"egulias/email-validator": "~3.0.0",
"phpdocumentor/reflection-docblock": "<3.2.2",
"phpdocumentor/type-resolver": "<1.4.0",
"symfony/mailer": "<5.4",
"symfony/serializer": "<6.3.2"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Mime\\": "" },
"exclude-from-classmap": [
"/Tests/"
]
},
"minimum-stability": "dev"
}