diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..ebb9287 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitignore export-ignore diff --git a/Address.php b/Address.php index 86a8042..b0dcbd0 100644 --- a/Address.php +++ b/Address.php @@ -20,17 +20,25 @@ /** * @author Fabien Potencier - * - * @experimental in 4.3 */ -class Address +final class Address { + /** + * A regex that matches a structure like 'Name '. + * 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 = '~(?[^<]*)<(?.*)>[^>]*~'; + private static $validator; private static $encoder; private $address; + private $name; - public function __construct(string $address) + 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)); @@ -40,11 +48,12 @@ public function __construct(string $address) self::$validator = new EmailValidator(); } - if (!self::$validator->isValid($address, new RFCValidation())) { + $this->address = trim($address); + $this->name = trim(str_replace(["\n", "\r"], '', $name)); + + if (!self::$validator->isValid($this->address, new RFCValidation())) { throw new RfcComplianceException(sprintf('Email "%s" does not comply with addr-spec of RFC 2822.', $address)); } - - $this->address = $address; } public function getAddress(): string @@ -52,6 +61,11 @@ public function getAddress(): string return $this->address; } + public function getName(): string + { + return $this->name; + } + public function getEncodedAddress(): string { if (null === self::$encoder) { @@ -63,7 +77,7 @@ public function getEncodedAddress(): string public function toString(): string { - return $this->getEncodedAddress(); + return ($n = $this->getName()) ? $n.' <'.$this->getEncodedAddress().'>' : $this->getEncodedAddress(); } /** @@ -95,4 +109,17 @@ public static function createArray(array $addresses): array return $addrs; } + + public static function fromString(string $string): self + { + if (false === strpos($string, '<')) { + return new self($string, ''); + } + + if (!preg_match(self::FROM_STRING_PATTERN, $string, $matches)) { + throw new InvalidArgumentException(sprintf('Could not parse "%s" to a "%s" instance.', $string, static::class)); + } + + return new self($matches['addrSpec'], trim($matches['displayName'], ' \'"')); + } } diff --git a/BodyRendererInterface.php b/BodyRendererInterface.php index 8fcc401..d692172 100644 --- a/BodyRendererInterface.php +++ b/BodyRendererInterface.php @@ -13,8 +13,6 @@ /** * @author Fabien Potencier - * - * @experimental in 4.3 */ interface BodyRendererInterface { diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6148360 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ +CHANGELOG +========= + +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 diff --git a/CharacterStream.php b/CharacterStream.php index 9b80b2e..749066f 100644 --- a/CharacterStream.php +++ b/CharacterStream.php @@ -16,8 +16,6 @@ * @author Xavier De Cock * * @internal - * - * @experimental in 4.3 */ final class CharacterStream { @@ -116,7 +114,6 @@ public function read(int $length): ?string if ($this->currentPos >= $this->charCount) { return null; } - $ret = null; $length = ($this->currentPos + $length > $this->charCount) ? $this->charCount - $this->currentPos : $length; if ($this->fixedWidth > 0) { $len = $length * $this->fixedWidth; @@ -177,7 +174,7 @@ public function write(string $chars): void $this->dataSize = \strlen($this->data) - \strlen($ignored); } - private function getUtf8CharPositions(string $string, int $startOffset, &$ignoredChars): int + private function getUtf8CharPositions(string $string, int $startOffset, string &$ignoredChars): int { $strlen = \strlen($string); $charPos = \count($this->map['p']); diff --git a/Crypto/SMime.php b/Crypto/SMime.php new file mode 100644 index 0000000..55941be --- /dev/null +++ b/Crypto/SMime.php @@ -0,0 +1,111 @@ + + * + * 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 + * + * @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 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 (false === strpos($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[a-z-0-9]+)=(?P"[^"]+"|(?:[^\s;]+|$))(?:\s+;)?/i', $header, $matches); + + foreach ($matches['value'] as $pos => $paramValue) { + $params[$matches['name'][$pos]] = trim($paramValue, '"'); + } + + return $params; + } +} diff --git a/Crypto/SMimeEncrypter.php b/Crypto/SMimeEncrypter.php new file mode 100644 index 0000000..d6961a6 --- /dev/null +++ b/Crypto/SMimeEncrypter.php @@ -0,0 +1,63 @@ + + * + * 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 + */ +final class SMimeEncrypter extends SMime +{ + private $certs; + private $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($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); + } +} diff --git a/Crypto/SMimeSigner.php b/Crypto/SMimeSigner.php new file mode 100644 index 0000000..243aaf1 --- /dev/null +++ b/Crypto/SMimeSigner.php @@ -0,0 +1,71 @@ + + * + * 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 + */ +final class SMimeSigner extends SMime +{ + private $signCertificate; + private $signPrivateKey; + private $signOptions; + private $extraCerts; + + /** + * @var string|null + */ + private $privateKeyPassphrase; + + /** + * @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; + $this->privateKeyPassphrase = $privateKeyPassphrase; + } + + 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')); + } +} diff --git a/DependencyInjection/AddMimeTypeGuesserPass.php b/DependencyInjection/AddMimeTypeGuesserPass.php index 78450b9..e24beb0 100644 --- a/DependencyInjection/AddMimeTypeGuesserPass.php +++ b/DependencyInjection/AddMimeTypeGuesserPass.php @@ -19,8 +19,6 @@ * Registers custom mime types guessers. * * @author Fabien Potencier - * - * @experimental in 4.3 */ class AddMimeTypeGuesserPass implements CompilerPassInterface { diff --git a/Email.php b/Email.php index 7812372..7ecea47 100644 --- a/Email.php +++ b/Email.php @@ -21,8 +21,6 @@ /** * @author Fabien Potencier - * - * @experimental in 4.3 */ class Email extends Message { @@ -103,7 +101,7 @@ public function getSender(): ?Address } /** - * @param Address|NamedAddress|string $addresses + * @param Address|string ...$addresses * * @return $this */ @@ -113,7 +111,7 @@ public function addFrom(...$addresses) } /** - * @param Address|NamedAddress|string $addresses + * @param Address|string ...$addresses * * @return $this */ @@ -123,7 +121,7 @@ public function from(...$addresses) } /** - * @return (Address|NamedAddress)[] + * @return Address[] */ public function getFrom(): array { @@ -131,7 +129,7 @@ public function getFrom(): array } /** - * @param Address|string $addresses + * @param Address|string ...$addresses * * @return $this */ @@ -141,7 +139,7 @@ public function addReplyTo(...$addresses) } /** - * @param Address|string $addresses + * @param Address|string ...$addresses * * @return $this */ @@ -159,7 +157,7 @@ public function getReplyTo(): array } /** - * @param Address|NamedAddress|string $addresses + * @param Address|string ...$addresses * * @return $this */ @@ -169,7 +167,7 @@ public function addTo(...$addresses) } /** - * @param Address|NamedAddress|string $addresses + * @param Address|string ...$addresses * * @return $this */ @@ -179,7 +177,7 @@ public function to(...$addresses) } /** - * @return (Address|NamedAddress)[] + * @return Address[] */ public function getTo(): array { @@ -187,7 +185,7 @@ public function getTo(): array } /** - * @param Address|NamedAddress|string $addresses + * @param Address|string ...$addresses * * @return $this */ @@ -197,7 +195,7 @@ public function addCc(...$addresses) } /** - * @param Address|string $addresses + * @param Address|string ...$addresses * * @return $this */ @@ -207,7 +205,7 @@ public function cc(...$addresses) } /** - * @return (Address|NamedAddress)[] + * @return Address[] */ public function getCc(): array { @@ -215,7 +213,7 @@ public function getCc(): array } /** - * @param Address|NamedAddress|string $addresses + * @param Address|string ...$addresses * * @return $this */ @@ -225,7 +223,7 @@ public function addBcc(...$addresses) } /** - * @param Address|string $addresses + * @param Address|string ...$addresses * * @return $this */ @@ -235,7 +233,7 @@ public function bcc(...$addresses) } /** - * @return (Address|NamedAddress)[] + * @return Address[] */ public function getBcc(): array { @@ -401,6 +399,15 @@ public function getBody(): AbstractPart return $this->generateBody(); } + public function ensureValidity() + { + if (null === $this->text && null === $this->html && !$this->attachments) { + throw new LogicException('A message must have a text or an HTML part or attachments.'); + } + + parent::ensureValidity(); + } + /** * Generates an AbstractPart based on the raw body of a message. * @@ -423,12 +430,11 @@ public function getBody(): AbstractPart */ private function generateBody(): AbstractPart { - if (null === $this->text && null === $this->html) { - throw new LogicException('A message must have a text and/or an HTML part.'); - } + $this->ensureValidity(); - $part = null === $this->text ? null : new TextPart($this->text, $this->textCharset); [$htmlPart, $attachmentParts, $inlineParts] = $this->prepareParts(); + + $part = null === $this->text ? null : new TextPart($this->text, $this->textCharset); if (null !== $htmlPart) { if (null !== $part) { $part = new AlternativePart($part, $htmlPart); @@ -442,7 +448,11 @@ private function generateBody(): AbstractPart } if ($attachmentParts) { - $part = new MixedPart($part, ...$attachmentParts); + if ($part) { + $part = new MixedPart($part, ...$attachmentParts); + } else { + $part = new MixedPart(...$attachmentParts); + } } return $part; @@ -520,22 +530,22 @@ private function setHeaderBody(string $type, string $name, $body) return $this; } - private function addListAddressHeaderBody($name, array $addresses) + private function addListAddressHeaderBody(string $name, array $addresses) { - if (!$to = $this->getHeaders()->get($name)) { + if (!$header = $this->getHeaders()->get($name)) { return $this->setListAddressHeaderBody($name, $addresses); } - $to->addAddresses(Address::createArray($addresses)); + $header->addAddresses(Address::createArray($addresses)); return $this; } - private function setListAddressHeaderBody($name, array $addresses) + private function setListAddressHeaderBody(string $name, array $addresses) { $addresses = Address::createArray($addresses); $headers = $this->getHeaders(); - if ($to = $headers->get($name)) { - $to->setAddresses($addresses); + if ($header = $headers->get($name)) { + $header->setAddresses($addresses); } else { $headers->addMailboxListHeader($name, $addresses); } diff --git a/Encoder/AddressEncoderInterface.php b/Encoder/AddressEncoderInterface.php index 5d6ea3c..de477d8 100644 --- a/Encoder/AddressEncoderInterface.php +++ b/Encoder/AddressEncoderInterface.php @@ -15,8 +15,6 @@ /** * @author Christian Schmidt - * - * @experimental in 4.3 */ interface AddressEncoderInterface { diff --git a/Encoder/Base64ContentEncoder.php b/Encoder/Base64ContentEncoder.php index e9c352e..338490b 100644 --- a/Encoder/Base64ContentEncoder.php +++ b/Encoder/Base64ContentEncoder.php @@ -15,8 +15,6 @@ /** * @author Fabien Potencier - * - * @experimental in 4.3 */ final class Base64ContentEncoder extends Base64Encoder implements ContentEncoderInterface { diff --git a/Encoder/Base64Encoder.php b/Encoder/Base64Encoder.php index 25dae67..7106478 100644 --- a/Encoder/Base64Encoder.php +++ b/Encoder/Base64Encoder.php @@ -13,8 +13,6 @@ /** * @author Chris Corbyn - * - * @experimental in 4.3 */ class Base64Encoder implements EncoderInterface { diff --git a/Encoder/Base64MimeHeaderEncoder.php b/Encoder/Base64MimeHeaderEncoder.php index 8baee5b..5c06f6d 100644 --- a/Encoder/Base64MimeHeaderEncoder.php +++ b/Encoder/Base64MimeHeaderEncoder.php @@ -13,8 +13,6 @@ /** * @author Chris Corbyn - * - * @experimental in 4.3 */ final class Base64MimeHeaderEncoder extends Base64Encoder implements MimeHeaderEncoderInterface { diff --git a/Encoder/ContentEncoderInterface.php b/Encoder/ContentEncoderInterface.php index b44e1a4..a45ad04 100644 --- a/Encoder/ContentEncoderInterface.php +++ b/Encoder/ContentEncoderInterface.php @@ -13,8 +13,6 @@ /** * @author Chris Corbyn - * - * @experimental in 4.3 */ interface ContentEncoderInterface extends EncoderInterface { diff --git a/Encoder/EightBitContentEncoder.php b/Encoder/EightBitContentEncoder.php index 94b838c..8283120 100644 --- a/Encoder/EightBitContentEncoder.php +++ b/Encoder/EightBitContentEncoder.php @@ -13,8 +13,6 @@ /** * @author Fabien Potencier - * - * @experimental in 4.3 */ final class EightBitContentEncoder implements ContentEncoderInterface { diff --git a/Encoder/EncoderInterface.php b/Encoder/EncoderInterface.php index 3c2ef19..bbf6d48 100644 --- a/Encoder/EncoderInterface.php +++ b/Encoder/EncoderInterface.php @@ -13,8 +13,6 @@ /** * @author Chris Corbyn - * - * @experimental in 4.3 */ interface EncoderInterface { diff --git a/Encoder/IdnAddressEncoder.php b/Encoder/IdnAddressEncoder.php index 8936047..1c5e32c 100644 --- a/Encoder/IdnAddressEncoder.php +++ b/Encoder/IdnAddressEncoder.php @@ -25,8 +25,6 @@ * the SMTPUTF8 extension. * * @author Christian Schmidt - * - * @experimental in 4.3 */ final class IdnAddressEncoder implements AddressEncoderInterface { diff --git a/Encoder/MimeHeaderEncoderInterface.php b/Encoder/MimeHeaderEncoderInterface.php index f575665..fff2c78 100644 --- a/Encoder/MimeHeaderEncoderInterface.php +++ b/Encoder/MimeHeaderEncoderInterface.php @@ -13,8 +13,6 @@ /** * @author Chris Corbyn - * - * @experimental in 4.3 */ interface MimeHeaderEncoderInterface { diff --git a/Encoder/QpContentEncoder.php b/Encoder/QpContentEncoder.php index ef2de46..e0b8605 100644 --- a/Encoder/QpContentEncoder.php +++ b/Encoder/QpContentEncoder.php @@ -13,8 +13,6 @@ /** * @author Lars Strojny - * - * @experimental in 4.3 */ final class QpContentEncoder implements ContentEncoderInterface { diff --git a/Encoder/QpEncoder.php b/Encoder/QpEncoder.php index 4ffbaed..ff9b0cc 100644 --- a/Encoder/QpEncoder.php +++ b/Encoder/QpEncoder.php @@ -15,8 +15,6 @@ /** * @author Chris Corbyn - * - * @experimental in 4.3 */ class QpEncoder implements EncoderInterface { diff --git a/Encoder/QpMimeHeaderEncoder.php b/Encoder/QpMimeHeaderEncoder.php index 0413959..d1d3837 100644 --- a/Encoder/QpMimeHeaderEncoder.php +++ b/Encoder/QpMimeHeaderEncoder.php @@ -13,8 +13,6 @@ /** * @author Chris Corbyn - * - * @experimental in 4.3 */ final class QpMimeHeaderEncoder extends QpEncoder implements MimeHeaderEncoderInterface { diff --git a/Encoder/Rfc2231Encoder.php b/Encoder/Rfc2231Encoder.php index 4743a72..aa3e062 100644 --- a/Encoder/Rfc2231Encoder.php +++ b/Encoder/Rfc2231Encoder.php @@ -15,8 +15,6 @@ /** * @author Chris Corbyn - * - * @experimental in 4.3 */ final class Rfc2231Encoder implements EncoderInterface { diff --git a/Exception/AddressEncoderException.php b/Exception/AddressEncoderException.php index 73ef7f3..51ee2e0 100644 --- a/Exception/AddressEncoderException.php +++ b/Exception/AddressEncoderException.php @@ -13,8 +13,6 @@ /** * @author Fabien Potencier - * - * @experimental in 4.3 */ class AddressEncoderException extends RfcComplianceException { diff --git a/Exception/ExceptionInterface.php b/Exception/ExceptionInterface.php index 7dbcdc7..1193390 100644 --- a/Exception/ExceptionInterface.php +++ b/Exception/ExceptionInterface.php @@ -13,8 +13,6 @@ /** * @author Fabien Potencier - * - * @experimental in 4.3 */ interface ExceptionInterface extends \Throwable { diff --git a/Exception/InvalidArgumentException.php b/Exception/InvalidArgumentException.php index 59d04e2..e89ebae 100644 --- a/Exception/InvalidArgumentException.php +++ b/Exception/InvalidArgumentException.php @@ -13,8 +13,6 @@ /** * @author Fabien Potencier - * - * @experimental in 4.3 */ class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface { diff --git a/Exception/LogicException.php b/Exception/LogicException.php index 07cb044..0508ee7 100644 --- a/Exception/LogicException.php +++ b/Exception/LogicException.php @@ -13,8 +13,6 @@ /** * @author Fabien Potencier - * - * @experimental in 4.3 */ class LogicException extends \LogicException implements ExceptionInterface { diff --git a/Exception/RfcComplianceException.php b/Exception/RfcComplianceException.php index 5dc4cf5..26e4a50 100644 --- a/Exception/RfcComplianceException.php +++ b/Exception/RfcComplianceException.php @@ -13,8 +13,6 @@ /** * @author Fabien Potencier - * - * @experimental in 4.3 */ class RfcComplianceException extends \InvalidArgumentException implements ExceptionInterface { diff --git a/Exception/RuntimeException.php b/Exception/RuntimeException.php index 84b11fb..fb018b0 100644 --- a/Exception/RuntimeException.php +++ b/Exception/RuntimeException.php @@ -13,8 +13,6 @@ /** * @author Fabien Potencier - * - * @experimental in 4.3 */ class RuntimeException extends \RuntimeException implements ExceptionInterface { diff --git a/FileBinaryMimeTypeGuesser.php b/FileBinaryMimeTypeGuesser.php index a25ebe4..fe1e0cd 100644 --- a/FileBinaryMimeTypeGuesser.php +++ b/FileBinaryMimeTypeGuesser.php @@ -18,8 +18,6 @@ * Guesses the MIME type with the binary "file" (only available on *nix). * * @author Bernhard Schussek - * - * @experimental in 4.3 */ class FileBinaryMimeTypeGuesser implements MimeTypeGuesserInterface { @@ -33,7 +31,7 @@ class FileBinaryMimeTypeGuesser implements MimeTypeGuesserInterface * * @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') + public function __construct(string $cmd = 'file -b --mime -- %s 2>/dev/null') { $this->cmd = $cmd; } @@ -76,7 +74,7 @@ public function guessMimeType(string $path): ?string ob_start(); // need to use --mime instead of -i. see #6641 - passthru(sprintf($this->cmd, escapeshellarg($path)), $return); + passthru(sprintf($this->cmd, escapeshellarg((0 === strpos($path, '-') ? './' : '').$path)), $return); if ($return > 0) { ob_end_clean(); @@ -85,7 +83,7 @@ public function guessMimeType(string $path): ?string $type = trim(ob_get_clean()); - if (!preg_match('#^([a-z0-9\-]+/[a-z0-9\-\.]+)#i', $type, $match)) { + if (!preg_match('#^([a-z0-9\-]+/[a-z0-9\-\+\.]+)#i', $type, $match)) { // it's not a type, but an error message return null; } diff --git a/FileinfoMimeTypeGuesser.php b/FileinfoMimeTypeGuesser.php index 81c62ee..b91a4ff 100644 --- a/FileinfoMimeTypeGuesser.php +++ b/FileinfoMimeTypeGuesser.php @@ -18,8 +18,6 @@ * Guesses the MIME type using the PECL extension FileInfo. * * @author Bernhard Schussek - * - * @experimental in 4.3 */ class FileinfoMimeTypeGuesser implements MimeTypeGuesserInterface { diff --git a/Header/AbstractHeader.php b/Header/AbstractHeader.php index 517ee8d..548c192 100644 --- a/Header/AbstractHeader.php +++ b/Header/AbstractHeader.php @@ -17,8 +17,6 @@ * An abstract base MIME Header. * * @author Chris Corbyn - * - * @experimental in 4.3 */ abstract class AbstractHeader implements HeaderInterface { diff --git a/Header/DateHeader.php b/Header/DateHeader.php index 1e1a979..a7385d4 100644 --- a/Header/DateHeader.php +++ b/Header/DateHeader.php @@ -15,8 +15,6 @@ * A Date MIME Header. * * @author Chris Corbyn - * - * @experimental in 4.3 */ final class DateHeader extends AbstractHeader { @@ -37,10 +35,7 @@ public function setBody($body) $this->setDateTime($body); } - /** - * @return \DateTimeImmutable - */ - public function getBody() + public function getBody(): \DateTimeImmutable { return $this->getDateTime(); } diff --git a/Header/HeaderInterface.php b/Header/HeaderInterface.php index b0638bc..4546947 100644 --- a/Header/HeaderInterface.php +++ b/Header/HeaderInterface.php @@ -15,8 +15,6 @@ * A MIME Header. * * @author Chris Corbyn - * - * @experimental in 4.3 */ interface HeaderInterface { diff --git a/Header/Headers.php b/Header/Headers.php index c0e45e0..9de506e 100644 --- a/Header/Headers.php +++ b/Header/Headers.php @@ -13,14 +13,11 @@ use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Exception\LogicException; -use Symfony\Component\Mime\NamedAddress; /** * A collection of headers. * * @author Fabien Potencier - * - * @experimental in 4.3 */ final class Headers { @@ -51,7 +48,7 @@ public function __clone() public function setMaxLineLength(int $lineLength) { $this->lineLength = $lineLength; - foreach ($this->getAll() as $header) { + foreach ($this->all() as $header) { $header->setMaxLineLength($lineLength); } } @@ -62,21 +59,21 @@ public function getMaxLineLength(): int } /** - * @param (NamedAddress|Address|string)[] $addresses + * @param (Address|string)[] $addresses * * @return $this */ - public function addMailboxListHeader(string $name, array $addresses) + public function addMailboxListHeader(string $name, array $addresses): self { return $this->add(new MailboxListHeader($name, Address::createArray($addresses))); } /** - * @param NamedAddress|Address|string $address + * @param Address|string $address * * @return $this */ - public function addMailboxHeader(string $name, $address) + public function addMailboxHeader(string $name, $address): self { return $this->add(new MailboxHeader($name, Address::create($address))); } @@ -86,7 +83,7 @@ public function addMailboxHeader(string $name, $address) * * @return $this */ - public function addIdHeader(string $name, $ids) + public function addIdHeader(string $name, $ids): self { return $this->add(new IdentificationHeader($name, $ids)); } @@ -96,7 +93,7 @@ public function addIdHeader(string $name, $ids) * * @return $this */ - public function addPathHeader(string $name, $path) + public function addPathHeader(string $name, $path): self { return $this->add(new PathHeader($name, $path instanceof Address ? $path : new Address($path))); } @@ -104,7 +101,7 @@ public function addPathHeader(string $name, $path) /** * @return $this */ - public function addDateHeader(string $name, \DateTimeInterface $dateTime) + public function addDateHeader(string $name, \DateTimeInterface $dateTime): self { return $this->add(new DateHeader($name, $dateTime)); } @@ -112,7 +109,7 @@ public function addDateHeader(string $name, \DateTimeInterface $dateTime) /** * @return $this */ - public function addTextHeader(string $name, string $value) + public function addTextHeader(string $name, string $value): self { return $this->add(new UnstructuredHeader($name, $value)); } @@ -120,7 +117,7 @@ public function addTextHeader(string $name, string $value) /** * @return $this */ - public function addParameterizedHeader(string $name, string $value, array $params = []) + public function addParameterizedHeader(string $name, string $value, array $params = []): self { return $this->add(new ParameterizedHeader($name, $value, $params)); } @@ -133,7 +130,7 @@ public function has(string $name): bool /** * @return $this */ - public function add(HeaderInterface $header) + public function add(HeaderInterface $header): self { static $map = [ 'date' => DateHeader::class, @@ -177,7 +174,7 @@ public function get(string $name): ?HeaderInterface return array_shift($values); } - public function getAll(string $name = null): iterable + public function all(string $name = null): iterable { if (null === $name) { foreach ($this->headers as $name => $collection) { @@ -220,7 +217,7 @@ public function toString(): string public function toArray(): array { $arr = []; - foreach ($this->getAll() as $header) { + foreach ($this->all() as $header) { if ('' !== $header->getBodyAsString()) { $arr[] = $header->toString(); } diff --git a/Header/IdentificationHeader.php b/Header/IdentificationHeader.php index 0695b68..8a94574 100644 --- a/Header/IdentificationHeader.php +++ b/Header/IdentificationHeader.php @@ -18,8 +18,6 @@ * An ID MIME Header for something like Message-ID or Content-ID (one or more addresses). * * @author Chris Corbyn - * - * @experimental in 4.3 */ final class IdentificationHeader extends AbstractHeader { @@ -46,10 +44,7 @@ public function setBody($body) $this->setId($body); } - /** - * @return array - */ - public function getBody() + public function getBody(): array { return $this->getIds(); } diff --git a/Header/MailboxHeader.php b/Header/MailboxHeader.php index c4f48f3..b58c825 100644 --- a/Header/MailboxHeader.php +++ b/Header/MailboxHeader.php @@ -13,14 +13,11 @@ use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Exception\RfcComplianceException; -use Symfony\Component\Mime\NamedAddress; /** * A Mailbox MIME Header for something like Sender (one named address). * * @author Fabien Potencier - * - * @experimental in 4.3 */ final class MailboxHeader extends AbstractHeader { @@ -45,10 +42,8 @@ public function setBody($body) /** * @throws RfcComplianceException - * - * @return Address */ - public function getBody() + public function getBody(): Address { return $this->getAddress(); } @@ -61,9 +56,6 @@ public function setAddress(Address $address) $this->address = $address; } - /** - * @return Address - */ public function getAddress(): Address { return $this->address; @@ -72,7 +64,7 @@ public function getAddress(): Address public function getBodyAsString(): string { $str = $this->address->getEncodedAddress(); - if ($this->address instanceof NamedAddress && $name = $this->address->getName()) { + if ($name = $this->address->getName()) { $str = $this->createPhrase($this, $name, $this->getCharset(), true).' <'.$str.'>'; } diff --git a/Header/MailboxListHeader.php b/Header/MailboxListHeader.php index 51af2da..1d00fdb 100644 --- a/Header/MailboxListHeader.php +++ b/Header/MailboxListHeader.php @@ -13,21 +13,18 @@ use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Exception\RfcComplianceException; -use Symfony\Component\Mime\NamedAddress; /** * A Mailbox list MIME Header for something like From, To, Cc, and Bcc (one or more named addresses). * * @author Chris Corbyn - * - * @experimental in 4.3 */ final class MailboxListHeader extends AbstractHeader { private $addresses = []; /** - * @param (NamedAddress|Address)[] $addresses + * @param Address[] $addresses */ public function __construct(string $name, array $addresses) { @@ -37,7 +34,7 @@ public function __construct(string $name, array $addresses) } /** - * @param (NamedAddress|Address)[] $body + * @param Address[] $body * * @throws RfcComplianceException */ @@ -49,9 +46,9 @@ public function setBody($body) /** * @throws RfcComplianceException * - * @return (NamedAddress|Address)[] + * @return Address[] */ - public function getBody() + public function getBody(): array { return $this->getAddresses(); } @@ -59,7 +56,7 @@ public function getBody() /** * Sets a list of addresses to be shown in this Header. * - * @param (NamedAddress|Address)[] $addresses + * @param Address[] $addresses * * @throws RfcComplianceException */ @@ -72,7 +69,7 @@ public function setAddresses(array $addresses) /** * Sets a list of addresses to be shown in this Header. * - * @param (NamedAddress|Address)[] $addresses + * @param Address[] $addresses * * @throws RfcComplianceException */ @@ -92,7 +89,7 @@ public function addAddress(Address $address) } /** - * @return (NamedAddress|Address)[] + * @return Address[] */ public function getAddresses(): array { @@ -111,8 +108,8 @@ public function getAddressStrings(): array $strings = []; foreach ($this->addresses as $address) { $str = $address->getEncodedAddress(); - if ($address instanceof NamedAddress && $name = $address->getName()) { - $str = $this->createPhrase($this, $name, $this->getCharset(), empty($strings)).' <'.$str.'>'; + if ($name = $address->getName()) { + $str = $this->createPhrase($this, $name, $this->getCharset(), !$strings).' <'.$str.'>'; } $strings[] = $str; } diff --git a/Header/ParameterizedHeader.php b/Header/ParameterizedHeader.php index 5813dcf..d8e5001 100644 --- a/Header/ParameterizedHeader.php +++ b/Header/ParameterizedHeader.php @@ -15,8 +15,6 @@ /** * @author Chris Corbyn - * - * @experimental in 4.3 */ final class ParameterizedHeader extends UnstructuredHeader { @@ -38,7 +36,7 @@ public function __construct(string $name, string $value, array $parameters = []) $this->setParameter($k, $v); } - if ('content-disposition' === strtolower($name)) { + if ('content-type' !== strtolower($name)) { $this->encoder = new Rfc2231Encoder(); } } diff --git a/Header/PathHeader.php b/Header/PathHeader.php index 6d16500..5101ad0 100644 --- a/Header/PathHeader.php +++ b/Header/PathHeader.php @@ -18,8 +18,6 @@ * A Path Header, such a Return-Path (one address). * * @author Chris Corbyn - * - * @experimental in 4.3 */ final class PathHeader extends AbstractHeader { @@ -42,10 +40,7 @@ public function setBody($body) $this->setAddress($body); } - /** - * @return Address - */ - public function getBody() + public function getBody(): Address { return $this->getAddress(); } diff --git a/Header/UnstructuredHeader.php b/Header/UnstructuredHeader.php index afb9615..2085ddf 100644 --- a/Header/UnstructuredHeader.php +++ b/Header/UnstructuredHeader.php @@ -15,8 +15,6 @@ * A Simple MIME Header. * * @author Chris Corbyn - * - * @experimental in 4.3 */ class UnstructuredHeader extends AbstractHeader { diff --git a/Message.php b/Message.php index db6e13f..5b4e67f 100644 --- a/Message.php +++ b/Message.php @@ -18,8 +18,6 @@ /** * @author Fabien Potencier - * - * @experimental in 4.3 */ class Message extends RawMessage { @@ -34,9 +32,7 @@ public function __construct(Headers $headers = null, AbstractPart $body = null) public function __clone() { - if (null !== $this->headers) { - $this->headers = clone $this->headers; - } + $this->headers = clone $this->headers; if (null !== $this->body) { $this->body = clone $this->body; @@ -88,16 +84,12 @@ public function getPreparedHeaders(): Headers } // determine the "real" sender - $senders = $headers->get('From')->getAddresses(); - $sender = $senders[0]; - if ($headers->has('Sender')) { - $sender = $headers->get('Sender')->getAddress(); - } elseif (\count($senders) > 1) { - $headers->addMailboxHeader('Sender', $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($sender->getAddress())); + $headers->addIdHeader('Message-ID', $this->generateMessageId()); } // remove the Bcc field which should NOT be part of the sent message @@ -125,22 +117,33 @@ public function toIterable(): iterable yield from $body->toIterable(); } - private function generateMessageId(string $email): string + public function ensureValidity() { - return bin2hex(random_bytes(16)).strstr($email, '@'); + if (!$this->headers->has('From')) { + throw new LogicException('An email must have a "From" header.'); + } + + parent::ensureValidity(); + } + + public function generateMessageId(): string + { + if ($this->headers->has('Sender')) { + $sender = $this->headers->get('Sender')->getAddress(); + } elseif ($this->headers->has('From')) { + $sender = $this->headers->get('From')->getAddresses()[0]; + } else { + throw new LogicException('An email must have a "From" or a "Sender" header to compute a Messsage ID.'); + } + + return bin2hex(random_bytes(16)).strstr($sender->getAddress(), '@'); } - /** - * @internal - */ public function __serialize(): array { return [$this->headers, $this->body]; } - /** - * @internal - */ public function __unserialize(array $data): void { [$this->headers, $this->body] = $data; diff --git a/MessageConverter.php b/MessageConverter.php index b139f1c..a810cb7 100644 --- a/MessageConverter.php +++ b/MessageConverter.php @@ -20,8 +20,6 @@ /** * @author Fabien Potencier - * - * @experimental in 4.3 */ final class MessageConverter { diff --git a/MimeTypeGuesserInterface.php b/MimeTypeGuesserInterface.php index b7d27f5..68b0505 100644 --- a/MimeTypeGuesserInterface.php +++ b/MimeTypeGuesserInterface.php @@ -15,8 +15,6 @@ * Guesses the MIME type of a file. * * @author Fabien Potencier - * - * @experimental in 4.3 */ interface MimeTypeGuesserInterface { diff --git a/MimeTypes.php b/MimeTypes.php index eea75fa..268658d 100644 --- a/MimeTypes.php +++ b/MimeTypes.php @@ -33,8 +33,6 @@ * $guesser->registerGuesser(new FileinfoMimeTypeGuesser('/path/to/magic/file')); * * @author Fabien Potencier - * - * @experimental in 4.3 */ final class MimeTypes implements MimeTypesInterface { @@ -1253,6 +1251,7 @@ public function guessMimeType(string $path): ?string 'image/psd' => ['psd'], 'image/rle' => ['rle'], 'image/sgi' => ['sgi'], + 'image/svg' => ['svg'], 'image/svg+xml' => ['svg', 'svgz'], 'image/svg+xml-compressed' => ['svgz'], 'image/tiff' => ['tiff', 'tif'], @@ -2433,12 +2432,12 @@ public function guessMimeType(string $path): ?string 'odc' => ['application/vnd.oasis.opendocument.chart'], 'odf' => ['application/vnd.oasis.opendocument.formula'], 'odft' => ['application/vnd.oasis.opendocument.formula-template'], - 'odg' => ['vnd.oasis.opendocument.graphics', 'application/vnd.oasis.opendocument.graphics'], + 'odg' => ['application/vnd.oasis.opendocument.graphics'], 'odi' => ['application/vnd.oasis.opendocument.image'], 'odm' => ['application/vnd.oasis.opendocument.text-master'], - 'odp' => ['vnd.oasis.opendocument.presentation', 'application/vnd.oasis.opendocument.presentation'], - 'ods' => ['vnd.oasis.opendocument.spreadsheet', 'application/vnd.oasis.opendocument.spreadsheet'], - 'odt' => ['vnd.oasis.opendocument.text', 'application/vnd.oasis.opendocument.text'], + 'odp' => ['application/vnd.oasis.opendocument.presentation'], + 'ods' => ['application/vnd.oasis.opendocument.spreadsheet'], + 'odt' => ['application/vnd.oasis.opendocument.text'], 'oga' => ['audio/ogg', 'audio/vorbis', 'audio/x-flac+ogg', 'audio/x-ogg', 'audio/x-oggflac', 'audio/x-speex+ogg', 'audio/x-vorbis', 'audio/x-vorbis+ogg'], 'ogg' => ['audio/ogg', 'audio/vorbis', 'audio/x-flac+ogg', 'audio/x-ogg', 'audio/x-oggflac', 'audio/x-speex+ogg', 'audio/x-vorbis', 'audio/x-vorbis+ogg', 'video/ogg', 'video/x-ogg', 'video/x-theora', 'video/x-theora+ogg'], 'ogm' => ['video/x-ogm', 'video/x-ogm+ogg'], @@ -2810,7 +2809,7 @@ public function guessMimeType(string $path): ?string 'sv4crc' => ['application/x-sv4crc'], 'svc' => ['application/vnd.dvb.service'], 'svd' => ['application/vnd.svd'], - 'svg' => ['image/svg+xml'], + 'svg' => ['image/svg+xml', 'image/svg'], 'svgz' => ['image/svg+xml', 'image/svg+xml-compressed'], 'svh' => ['text/x-svhdr'], 'swa' => ['application/x-director'], diff --git a/MimeTypesInterface.php b/MimeTypesInterface.php index bdf2042..9fbd2cc 100644 --- a/MimeTypesInterface.php +++ b/MimeTypesInterface.php @@ -13,8 +13,6 @@ /** * @author Fabien Potencier - * - * @experimental in 4.3 */ interface MimeTypesInterface extends MimeTypeGuesserInterface { diff --git a/NamedAddress.php b/NamedAddress.php deleted file mode 100644 index 0d58708..0000000 --- a/NamedAddress.php +++ /dev/null @@ -1,44 +0,0 @@ - - * - * 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 - * - * @experimental in 4.3 - */ -final class NamedAddress extends Address -{ - private $name; - - public function __construct(string $address, string $name) - { - parent::__construct($address); - - $this->name = $name; - } - - public function getName(): string - { - return $this->name; - } - - public function getEncodedNamedAddress(): string - { - return ($n = $this->getName()) ? $n.' <'.$this->getEncodedAddress().'>' : $this->getEncodedAddress(); - } - - public function toString(): string - { - return $this->getEncodedNamedAddress(); - } -} diff --git a/Part/AbstractMultipartPart.php b/Part/AbstractMultipartPart.php index 34a94d2..48b8620 100644 --- a/Part/AbstractMultipartPart.php +++ b/Part/AbstractMultipartPart.php @@ -15,8 +15,6 @@ /** * @author Fabien Potencier - * - * @experimental in 4.3 */ abstract class AbstractMultipartPart extends AbstractPart { @@ -76,6 +74,20 @@ public function bodyToIterable(): iterable 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 { if (null === $this->boundary) { diff --git a/Part/AbstractPart.php b/Part/AbstractPart.php index 29eaa1e..93892d9 100644 --- a/Part/AbstractPart.php +++ b/Part/AbstractPart.php @@ -15,8 +15,6 @@ /** * @author Fabien Potencier - * - * @experimental in 4.3 */ abstract class AbstractPart { @@ -52,6 +50,11 @@ public function toIterable(): iterable yield from $this->bodyToIterable(); } + public function asDebugString(): string + { + return $this->getMediaType().'/'.$this->getMediaSubtype(); + } + abstract public function bodyToString(): string; abstract public function bodyToIterable(): iterable; diff --git a/Part/DataPart.php b/Part/DataPart.php index 1cfb1e6..423185f 100644 --- a/Part/DataPart.php +++ b/Part/DataPart.php @@ -17,8 +17,6 @@ /** * @author Fabien Potencier - * - * @experimental in 4.3 */ class DataPart extends TextPart { @@ -105,6 +103,16 @@ public function getPreparedHeaders(): Headers return $headers; } + public function asDebugString(): string + { + $str = parent::asDebugString(); + if (null !== $this->filename) { + $str .= ' filename: '.$this->filename; + } + + return $str; + } + private function generateContentId(): string { return bin2hex(random_bytes(16)).'@symfony'; @@ -117,6 +125,9 @@ public function __destruct() } } + /** + * @return array + */ public function __sleep() { // converts the body to a string diff --git a/Part/MessagePart.php b/Part/MessagePart.php index 64a5340..1b5c23e 100644 --- a/Part/MessagePart.php +++ b/Part/MessagePart.php @@ -18,8 +18,6 @@ * @final * * @author Fabien Potencier - * - * @experimental in 4.3 */ class MessagePart extends DataPart { diff --git a/Part/Multipart/AlternativePart.php b/Part/Multipart/AlternativePart.php index ad316a4..fd75423 100644 --- a/Part/Multipart/AlternativePart.php +++ b/Part/Multipart/AlternativePart.php @@ -15,8 +15,6 @@ /** * @author Fabien Potencier - * - * @experimental in 4.3 */ final class AlternativePart extends AbstractMultipartPart { diff --git a/Part/Multipart/DigestPart.php b/Part/Multipart/DigestPart.php index 6199e5b..27537f1 100644 --- a/Part/Multipart/DigestPart.php +++ b/Part/Multipart/DigestPart.php @@ -16,8 +16,6 @@ /** * @author Fabien Potencier - * - * @experimental in 4.3 */ final class DigestPart extends AbstractMultipartPart { diff --git a/Part/Multipart/FormDataPart.php b/Part/Multipart/FormDataPart.php index 75d69a8..6838620 100644 --- a/Part/Multipart/FormDataPart.php +++ b/Part/Multipart/FormDataPart.php @@ -20,8 +20,6 @@ * Implements RFC 7578. * * @author Fabien Potencier - * - * @experimental in 4.3 */ final class FormDataPart extends AbstractMultipartPart { @@ -58,16 +56,25 @@ public function getParts(): array private function prepareFields(array $fields): array { $values = []; - array_walk_recursive($fields, function ($item, $key) use (&$values) { - if (!\is_array($item)) { - $values[] = $this->preparePart($key, $item); + + $prepare = function ($item, $key, $root = null) use (&$values, &$prepare) { + $fieldName = $root ? sprintf('%s[%s]', $root, $key) : $key; + + if (\is_array($item)) { + array_walk($item, $prepare, $fieldName); + + return; } - }); + + $values[] = $this->preparePart($fieldName, $item); + }; + + array_walk($fields, $prepare); return $values; } - private function preparePart($name, $value): TextPart + private function preparePart(string $name, $value): TextPart { if (\is_string($value)) { return $this->configurePart($name, new TextPart($value, 'utf-8', 'plain', '8bit')); diff --git a/Part/Multipart/MixedPart.php b/Part/Multipart/MixedPart.php index eaa869f..c8d7028 100644 --- a/Part/Multipart/MixedPart.php +++ b/Part/Multipart/MixedPart.php @@ -15,8 +15,6 @@ /** * @author Fabien Potencier - * - * @experimental in 4.3 */ final class MixedPart extends AbstractMultipartPart { diff --git a/Part/Multipart/RelatedPart.php b/Part/Multipart/RelatedPart.php index 2d55630..08fdd5f 100644 --- a/Part/Multipart/RelatedPart.php +++ b/Part/Multipart/RelatedPart.php @@ -16,8 +16,6 @@ /** * @author Fabien Potencier - * - * @experimental in 4.3 */ final class RelatedPart extends AbstractMultipartPart { diff --git a/Part/SMimePart.php b/Part/SMimePart.php new file mode 100644 index 0000000..1dfc1ae --- /dev/null +++ b/Part/SMimePart.php @@ -0,0 +1,116 @@ + + * + * 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 + */ +class SMimePart extends AbstractPart +{ + private $body; + private $type; + private $subtype; + private $parameters; + + /** + * @param iterable|string $body + */ + public function __construct($body, string $type, string $subtype, array $parameters) + { + parent::__construct(); + + if (!\is_string($body) && !is_iterable($body)) { + throw new \TypeError(sprintf('The body of "%s" must be a string or a iterable (got "%s").', self::class, \is_object($body) ? \get_class($body) : \gettype($body))); + } + + $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->setAccessible(true); + $r->setValue($this, $this->_headers); + unset($this->_headers); + } +} diff --git a/Part/TextPart.php b/Part/TextPart.php index 6a04185..a41d91d 100644 --- a/Part/TextPart.php +++ b/Part/TextPart.php @@ -20,8 +20,6 @@ /** * @author Fabien Potencier - * - * @experimental in 4.3 */ class TextPart extends AbstractPart { @@ -146,6 +144,19 @@ public function getPreparedHeaders(): Headers 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) { @@ -168,6 +179,9 @@ private function chooseEncoding(): string return 'quoted-printable'; } + /** + * @return array + */ public function __sleep() { // convert resources to strings for serialization diff --git a/README.md b/README.md index 3288246..4d565c9 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,6 @@ MIME Component The MIME component allows manipulating MIME messages. -**This Component is experimental**. -[Experimental features](https://symfony.com/doc/current/contributing/code/experimental.html) -are not covered by Symfony's -[Backward Compatibility Promise](https://symfony.com/doc/current/contributing/code/bc.html). - Resources --------- diff --git a/RawMessage.php b/RawMessage.php index 16b090c..79a27e9 100644 --- a/RawMessage.php +++ b/RawMessage.php @@ -11,10 +11,10 @@ namespace Symfony\Component\Mime; +use Symfony\Component\Mime\Exception\LogicException; + /** * @author Fabien Potencier - * - * @experimental in 4.3 */ class RawMessage implements \Serializable { @@ -53,10 +53,17 @@ public function toIterable(): iterable $this->message = $message; } + /** + * @throws LogicException if the message is not valid + */ + public function ensureValidity() + { + } + /** * @internal */ - final public function serialize() + final public function serialize(): string { return serialize($this->__serialize()); } @@ -69,17 +76,11 @@ final public function unserialize($serialized) $this->__unserialize(unserialize($serialized)); } - /** - * @internal - */ public function __serialize(): array { return [$this->message]; } - /** - * @internal - */ public function __unserialize(array $data): void { [$this->message] = $data; diff --git a/Resources/bin/update_mime_types.php b/Resources/bin/update_mime_types.php index 0311d0d..74a9449 100644 --- a/Resources/bin/update_mime_types.php +++ b/Resources/bin/update_mime_types.php @@ -108,10 +108,6 @@ 'mp4' => ['video/mp4'], 'mpeg' => ['video/mpeg'], 'mpg' => ['video/mpeg'], - 'odg' => ['vnd.oasis.opendocument.graphics'], - 'odp' => ['vnd.oasis.opendocument.presentation'], - 'ods' => ['vnd.oasis.opendocument.spreadsheet'], - 'odt' => ['vnd.oasis.opendocument.text'], 'ogg' => ['audio/ogg'], 'pdf' => ['application/pdf'], 'php' => ['application/x-php'], diff --git a/Test/Constraint/EmailAddressContains.php b/Test/Constraint/EmailAddressContains.php new file mode 100644 index 0000000..58ef360 --- /dev/null +++ b/Test/Constraint/EmailAddressContains.php @@ -0,0 +1,74 @@ + + * + * 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 $headerName; + private $expectedValue; + + public function __construct(string $headerName, string $expectedValue) + { + $this->headerName = $headerName; + $this->expectedValue = $expectedValue; + } + + /** + * {@inheritdoc} + */ + public function toString(): string + { + return sprintf('contains address "%s" with value "%s"', $this->headerName, $this->expectedValue); + } + + /** + * @param RawMessage $message + * + * {@inheritdoc} + */ + protected function matches($message): bool + { + if (RawMessage::class === \get_class($message)) { + 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->Address()->getAddress(); + } elseif ($header instanceof MailboxListHeader) { + foreach ($header->getAddresses() as $address) { + if ($this->expectedValue === $address->getAddress()) { + return true; + } + } + + return false; + } + + throw new \LogicException(sprintf('Unable to test a message address on a non-address header.')); + } + + /** + * @param RawMessage $message + * + * {@inheritdoc} + */ + protected function failureDescription($message): string + { + return sprintf('the Email %s (value is %s)', $this->toString(), $message->getHeaders()->get($this->headerName)->getBodyAsString()); + } +} diff --git a/Test/Constraint/EmailAttachmentCount.php b/Test/Constraint/EmailAttachmentCount.php new file mode 100644 index 0000000..b219f28 --- /dev/null +++ b/Test/Constraint/EmailAttachmentCount.php @@ -0,0 +1,59 @@ + + * + * 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 EmailAttachmentCount extends Constraint +{ + private $expectedValue; + private $transport; + + public function __construct(int $expectedValue, string $transport = null) + { + $this->expectedValue = $expectedValue; + $this->transport = $transport; + } + + /** + * {@inheritdoc} + */ + public function toString(): string + { + return sprintf('has sent "%d" attachment(s)', $this->expectedValue); + } + + /** + * @param RawMessage $message + * + * {@inheritdoc} + */ + protected function matches($message): bool + { + if (RawMessage::class === \get_class($message) || Message::class === \get_class($message)) { + throw new \LogicException('Unable to test a message attachment on a RawMessage or Message instance.'); + } + + return $this->expectedValue === \count($message->getAttachments()); + } + + /** + * @param RawMessage $message + * + * {@inheritdoc} + */ + protected function failureDescription($message): string + { + return 'the Email '.$this->toString(); + } +} diff --git a/Test/Constraint/EmailHasHeader.php b/Test/Constraint/EmailHasHeader.php new file mode 100644 index 0000000..a29f835 --- /dev/null +++ b/Test/Constraint/EmailHasHeader.php @@ -0,0 +1,57 @@ + + * + * 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 $headerName; + + public function __construct(string $headerName) + { + $this->headerName = $headerName; + } + + /** + * {@inheritdoc} + */ + public function toString(): string + { + return sprintf('has header "%s"', $this->headerName); + } + + /** + * @param RawMessage $message + * + * {@inheritdoc} + */ + protected function matches($message): bool + { + if (RawMessage::class === \get_class($message)) { + throw new \LogicException('Unable to test a message header on a RawMessage instance.'); + } + + return $message->getHeaders()->has($this->headerName); + } + + /** + * @param RawMessage $message + * + * {@inheritdoc} + */ + protected function failureDescription($message): string + { + return 'the Email '.$this->toString(); + } +} diff --git a/Test/Constraint/EmailHeaderSame.php b/Test/Constraint/EmailHeaderSame.php new file mode 100644 index 0000000..bc7e330 --- /dev/null +++ b/Test/Constraint/EmailHeaderSame.php @@ -0,0 +1,59 @@ + + * + * 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 EmailHeaderSame extends Constraint +{ + private $headerName; + private $expectedValue; + + public function __construct(string $headerName, string $expectedValue) + { + $this->headerName = $headerName; + $this->expectedValue = $expectedValue; + } + + /** + * {@inheritdoc} + */ + public function toString(): string + { + return sprintf('has header "%s" with value "%s"', $this->headerName, $this->expectedValue); + } + + /** + * @param RawMessage $message + * + * {@inheritdoc} + */ + protected function matches($message): bool + { + if (RawMessage::class === \get_class($message)) { + throw new \LogicException('Unable to test a message header on a RawMessage instance.'); + } + + return $this->expectedValue === $message->getHeaders()->get($this->headerName)->getBodyAsString(); + } + + /** + * @param RawMessage $message + * + * {@inheritdoc} + */ + protected function failureDescription($message): string + { + return sprintf('the Email %s (value is %s)', $this->toString(), $message->getHeaders()->get($this->headerName)->getBodyAsString()); + } +} diff --git a/Test/Constraint/EmailHtmlBodyContains.php b/Test/Constraint/EmailHtmlBodyContains.php new file mode 100644 index 0000000..8965195 --- /dev/null +++ b/Test/Constraint/EmailHtmlBodyContains.php @@ -0,0 +1,56 @@ + + * + * 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; + +final class EmailHtmlBodyContains extends Constraint +{ + private $expectedText; + + public function __construct(string $expectedText) + { + $this->expectedText = $expectedText; + } + + /** + * {@inheritdoc} + */ + public function toString(): string + { + return sprintf('contains "%s"', $this->expectedText); + } + + /** + * {@inheritdoc} + * + * @param RawMessage $message + */ + protected function matches($message): bool + { + if (RawMessage::class === \get_class($message) || Message::class === \get_class($message)) { + throw new \LogicException('Unable to test a message HTML body on a RawMessage or Message instance.'); + } + + return false !== mb_strpos($message->getHtmlBody(), $this->expectedText); + } + + /** + * {@inheritdoc} + * + * @param RawMessage $message + */ + protected function failureDescription($message): string + { + return 'the Email HTML body '.$this->toString(); + } +} diff --git a/Test/Constraint/EmailTextBodyContains.php b/Test/Constraint/EmailTextBodyContains.php new file mode 100644 index 0000000..b5e87f9 --- /dev/null +++ b/Test/Constraint/EmailTextBodyContains.php @@ -0,0 +1,56 @@ + + * + * 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; + +final class EmailTextBodyContains extends Constraint +{ + private $expectedText; + + public function __construct(string $expectedText) + { + $this->expectedText = $expectedText; + } + + /** + * {@inheritdoc} + */ + public function toString(): string + { + return sprintf('contains "%s"', $this->expectedText); + } + + /** + * {@inheritdoc} + * + * @param RawMessage $message + */ + protected function matches($message): bool + { + if (RawMessage::class === \get_class($message) || Message::class === \get_class($message)) { + throw new \LogicException('Unable to test a message text body on a RawMessage or Message instance.'); + } + + return false !== mb_strpos($message->getTextBody(), $this->expectedText); + } + + /** + * {@inheritdoc} + * + * @param RawMessage $message + */ + protected function failureDescription($message): string + { + return 'the Email text body '.$this->toString(); + } +} diff --git a/Tests/AbstractMimeTypeGuesserTest.php b/Tests/AbstractMimeTypeGuesserTest.php index f9f5ec5..70e419c 100644 --- a/Tests/AbstractMimeTypeGuesserTest.php +++ b/Tests/AbstractMimeTypeGuesserTest.php @@ -16,7 +16,7 @@ abstract class AbstractMimeTypeGuesserTest extends TestCase { - public static function tearDownAfterClass() + public static function tearDownAfterClass(): void { $path = __DIR__.'/Fixtures/mimetypes/to_delete'; if (file_exists($path)) { @@ -27,6 +27,21 @@ public static function tearDownAfterClass() abstract protected function getGuesser(): MimeTypeGuesserInterface; + public function testGuessWithLeadingDash() + { + if (!$this->getGuesser()->isGuesserSupported()) { + $this->markTestSkipped('Guesser is not supported'); + } + + $cwd = getcwd(); + chdir(__DIR__.'/Fixtures/mimetypes'); + try { + $this->assertEquals('image/gif', $this->getGuesser()->guessMimeType('-test')); + } finally { + chdir($cwd); + } + } + public function testGuessImageWithoutExtension() { if (!$this->getGuesser()->isGuesserSupported()) { diff --git a/Tests/AddressTest.php b/Tests/AddressTest.php index dd7d3ce..50d5780 100644 --- a/Tests/AddressTest.php +++ b/Tests/AddressTest.php @@ -13,7 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Mime\Address; -use Symfony\Component\Mime\NamedAddress; +use Symfony\Component\Mime\Exception\InvalidArgumentException; class AddressTest extends TestCase { @@ -23,6 +23,12 @@ public function testConstructor() $this->assertEquals('fabien@symfonï.com', $a->getAddress()); $this->assertEquals('fabien@xn--symfon-nwa.com', $a->toString()); $this->assertEquals('fabien@xn--symfon-nwa.com', $a->getEncodedAddress()); + + $a = new Address('fabien@symfonï.com', 'Fabien'); + $this->assertEquals('Fabien', $a->getName()); + $this->assertEquals('fabien@symfonï.com', $a->getAddress()); + $this->assertEquals('Fabien ', $a->toString()); + $this->assertEquals('fabien@xn--symfon-nwa.com', $a->getEncodedAddress()); } public function testConstructorWithInvalidAddress() @@ -34,7 +40,7 @@ public function testConstructorWithInvalidAddress() public function testCreate() { $this->assertSame($a = new Address('fabien@symfony.com'), Address::create($a)); - $this->assertSame($b = new NamedAddress('helene@symfony.com', 'Helene'), Address::create($b)); + $this->assertSame($b = new Address('helene@symfony.com', 'Helene'), Address::create($b)); $this->assertEquals($a, Address::create('fabien@symfony.com')); } @@ -47,7 +53,7 @@ public function testCreateWrongArg() public function testCreateArray() { $fabien = new Address('fabien@symfony.com'); - $helene = new NamedAddress('helene@symfony.com', 'Helene'); + $helene = new Address('helene@symfony.com', 'Helene'); $this->assertSame([$fabien, $helene], Address::createArray([$fabien, $helene])); $this->assertEquals([$fabien], Address::createArray(['fabien@symfony.com'])); @@ -58,4 +64,93 @@ public function testCreateArrayWrongArg() $this->expectException(\InvalidArgumentException::class); Address::createArray([new \stdClass()]); } + + /** + * @dataProvider nameEmptyDataProvider + */ + public function testNameEmpty(string $name) + { + $mail = 'mail@example.org'; + $this->assertSame($mail, (new Address($mail, $name))->toString()); + } + + public function nameEmptyDataProvider(): array + { + return [[''], [' '], [" \r\n "]]; + } + + /** + * @dataProvider fromStringProvider + */ + public function testFromString($string, $displayName, $addrSpec) + { + $address = Address::fromString($string); + $this->assertEquals($displayName, $address->getName()); + $this->assertEquals($addrSpec, $address->getAddress()); + $fromToStringAddress = Address::fromString($address->toString()); + $this->assertEquals($displayName, $fromToStringAddress->getName()); + $this->assertEquals($addrSpec, $fromToStringAddress->getAddress()); + } + + public function testFromStringFailure() + { + $this->expectException(InvalidArgumentException::class); + Address::fromString('Jane Doe ', + '', + 'example@example.com', + ], + [ + 'Jane Doe ', + 'Jane Doe', + 'example@example.com', + ], + [ + 'Jane Doe', + 'Jane Doe', + 'example@example.com', + ], + [ + '\'Jane Doe\' ', + 'Jane Doe', + 'example@example.com', + ], + [ + '"Jane Doe" ', + 'Jane Doe', + 'example@example.com', + ], + [ + 'Jane Doe <"ex', + 'Jane Doe', + '"exle"@example.com>', + 'Jane Doe', + '"exle"@example.com', + ], + [ + 'Jane Doe > <"exle"@example.com>', + 'Jane Doe >', + '"exle"@example.com', + ], + [ + 'Jane Doe discarded', + 'Jane Doe', + 'example@example.com', + ], + ]; + } } diff --git a/Tests/Crypto/SMimeEncryptorTest.php b/Tests/Crypto/SMimeEncryptorTest.php new file mode 100644 index 0000000..cad87bf --- /dev/null +++ b/Tests/Crypto/SMimeEncryptorTest.php @@ -0,0 +1,106 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Tests\Crypto; + +use Symfony\Component\Mime\Crypto\SMimeEncrypter; +use Symfony\Component\Mime\Crypto\SMimeSigner; +use Symfony\Component\Mime\Email; +use Symfony\Component\Mime\Message; + +/** + * @requires extension openssl + */ +class SMimeEncryptorTest extends SMimeTestCase +{ + public function testEncryptMessage() + { + $message = (new Email()) + ->date(new \DateTime('2019-04-07 10:36:30', new \DateTimeZone('Europe/Paris'))) + ->to('fabien@symfony.com') + ->subject('Testing') + ->from('noreply@example.com') + ->text('El Barto was not here'); + + $message->getHeaders()->addIdHeader('Message-ID', 'some@id'); + + $encrypter = new SMimeEncrypter($this->samplesDir.'encrypt.crt'); + $encryptedMessage = $encrypter->encrypt($message); + + $this->assertMessageIsEncryptedProperly($encryptedMessage, $message); + } + + public function testEncryptSignedMessage() + { + $message = (new Email()) + ->date(new \DateTime('2019-04-07 10:36:30', new \DateTimeZone('Europe/Paris'))) + ->to('fabien@symfony.com') + ->bcc('luna@symfony.com') + ->subject('Testing') + ->from('noreply@example.com') + ->text('El Barto was not here'); + + $message->getHeaders()->addIdHeader('Message-ID', 'some@id'); + + $signer = new SMimeSigner($this->samplesDir.'sign.crt', $this->samplesDir.'sign.key'); + $signedMessage = $signer->sign($message); + + $encrypter = new SMimeEncrypter($this->samplesDir.'encrypt.crt'); + $encryptedMessage = $encrypter->encrypt($signedMessage); + + $this->assertMessageIsEncryptedProperly($encryptedMessage, $signedMessage); + } + + public function testEncryptMessageWithMultipleCerts() + { + $message = (new Email()) + ->date(new \DateTime('2019-04-07 10:36:30', new \DateTimeZone('Europe/Paris'))) + ->to('fabien@symfony.com') + ->subject('Testing') + ->from('noreply@example.com') + ->text('El Barto was not here'); + + $message2 = (new Email()) + ->date(new \DateTime('2019-04-07 10:36:30', new \DateTimeZone('Europe/Paris'))) + ->to('luna@symfony.com') + ->subject('Testing') + ->from('noreply@example.com') + ->text('El Barto was not here'); + + $message->getHeaders()->addIdHeader('Message-ID', 'some@id'); + $message2->getHeaders()->addIdHeader('Message-ID', 'some@id2'); + + $encrypter = new SMimeEncrypter(['fabien@symfony.com' => $this->samplesDir.'encrypt.crt', 'luna@symfony.com' => $this->samplesDir.'encrypt2.crt']); + + $this->assertMessageIsEncryptedProperly($encrypter->encrypt($message), $message); + $this->assertMessageIsEncryptedProperly($encrypter->encrypt($message2), $message2); + } + + private function assertMessageIsEncryptedProperly(Message $message, Message $originalMessage): void + { + $messageFile = $this->generateTmpFilename(); + file_put_contents($messageFile, $message->toString()); + + $outputFile = $this->generateTmpFilename(); + + $this->assertMessageHeaders($message, $originalMessage); + $this->assertTrue( + openssl_pkcs7_decrypt( + $messageFile, + $outputFile, + 'file://'.$this->samplesDir.'encrypt.crt', + 'file://'.$this->samplesDir.'encrypt.key' + ), + sprintf('Decryption of the message failed. Internal error "%s".', openssl_error_string()) + ); + $this->assertEquals(str_replace("\r", '', $originalMessage->toString()), str_replace("\r", '', file_get_contents($outputFile))); + } +} diff --git a/Tests/Crypto/SMimeSignerTest.php b/Tests/Crypto/SMimeSignerTest.php new file mode 100644 index 0000000..0a86c3c --- /dev/null +++ b/Tests/Crypto/SMimeSignerTest.php @@ -0,0 +1,166 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Tests\Crypto; + +use Symfony\Component\Mime\Crypto\SMimeEncrypter; +use Symfony\Component\Mime\Crypto\SMimeSigner; +use Symfony\Component\Mime\Email; +use Symfony\Component\Mime\Header\Headers; +use Symfony\Component\Mime\Message; +use Symfony\Component\Mime\Part\TextPart; + +/** + * @requires extension openssl + */ +class SMimeSignerTest extends SMimeTestCase +{ + public function testSignedMessage() + { + $message = new Message( + (new Headers()) + ->addDateHeader('Date', new \DateTime('2019-04-07 10:36:30', new \DateTimeZone('Europe/Paris'))) + ->addMailboxListHeader('From', ['fabien@symfony.com']), + new TextPart('content') + ); + + $signer = new SMimeSigner($this->samplesDir.'sign.crt', $this->samplesDir.'sign.key'); + $signedMessage = $signer->sign($message); + + $this->assertMessageSignatureIsValid($signedMessage, $message); + } + + public function testSignEncryptedMessage() + { + $message = (new Email()) + ->date(new \DateTime('2019-04-07 10:36:30', new \DateTimeZone('Europe/Paris'))) + ->to('fabien@symfony.com') + ->subject('Testing') + ->from('noreply@example.com') + ->text('El Barto was not here'); + + $message->getHeaders()->addIdHeader('Message-ID', 'some@id'); + + $encrypter = new SMimeEncrypter($this->samplesDir.'encrypt.crt'); + $encryptedMessage = $encrypter->encrypt($message); + + $signer = new SMimeSigner($this->samplesDir.'sign.crt', $this->samplesDir.'sign.key'); + $signedMessage = $signer->sign($encryptedMessage); + + $this->assertMessageSignatureIsValid($signedMessage, $message); + } + + public function testSignedMessageWithPassphrase() + { + $message = new Message( + (new Headers()) + ->addDateHeader('Date', new \DateTime('2019-04-07 10:36:30', new \DateTimeZone('Europe/Paris'))) + ->addMailboxListHeader('From', ['fabien@symfony.com']), + new TextPart('content') + ); + + $signer = new SMimeSigner($this->samplesDir.'sign3.crt', $this->samplesDir.'sign3.key', 'symfony-rocks'); + $signedMessage = $signer->sign($message); + + $this->assertMessageSignatureIsValid($signedMessage, $message); + } + + public function testProperSerialiable() + { + $message = (new Email()) + ->date(new \DateTime('2019-04-07 10:36:30', new \DateTimeZone('Europe/Paris'))) + ->to('fabien@symfony.com') + ->subject('Testing') + ->from('noreply@example.com') + ->text('El Barto was not here'); + + $message->getHeaders()->addIdHeader('Message-ID', 'some@id'); + + $signer = new SMimeSigner($this->samplesDir.'sign.crt', $this->samplesDir.'sign.key'); + $signedMessage = $signer->sign($message); + + $restoredMessage = unserialize(serialize($signedMessage)); + + self::assertSame($this->iterableToString($signedMessage->toIterable()), $this->iterableToString($restoredMessage->toIterable())); + self::assertSame($signedMessage->toString(), $restoredMessage->toString()); + + $this->assertMessageSignatureIsValid($restoredMessage, $message); + } + + public function testSignedMessageWithBcc() + { + $message = (new Email()) + ->date(new \DateTime('2019-04-07 10:36:30', new \DateTimeZone('Europe/Paris'))) + ->addBcc('fabien@symfony.com', 's.stok@rollerscapes.net') + ->subject('I am your sign of fear') + ->from('noreply@example.com') + ->text('El Barto was not here'); + + $signer = new SMimeSigner($this->samplesDir.'sign.crt', $this->samplesDir.'sign.key'); + $signedMessage = $signer->sign($message); + + $this->assertMessageSignatureIsValid($signedMessage, $message); + } + + public function testSignedMessageWithAttachments() + { + $message = new Email((new Headers()) + ->addDateHeader('Date', new \DateTime('2019-04-07 10:36:30', new \DateTimeZone('Europe/Paris'))) + ->addMailboxListHeader('From', ['fabien@symfony.com']) + ); + $message->html($content = 'html content '); + $message->text('text content'); + $message->attach(fopen(__DIR__.'/../Fixtures/mimetypes/test', 'r')); + $message->attach(fopen(__DIR__.'/../Fixtures/mimetypes/test.gif', 'r'), 'test.gif'); + + $signer = new SMimeSigner($this->samplesDir.'sign.crt', $this->samplesDir.'sign.key'); + + $signedMessage = $signer->sign($message); + $this->assertMessageSignatureIsValid($signedMessage, $message); + } + + public function testSignedMessageExtraCerts() + { + $message = new Message( + (new Headers()) + ->addDateHeader('Date', new \DateTime('2019-04-07 10:36:30', new \DateTimeZone('Europe/Paris'))) + ->addMailboxListHeader('From', ['fabien@symfony.com']), + new TextPart('content') + ); + + $signer = new SMimeSigner( + $this->samplesDir.'sign.crt', + $this->samplesDir.'sign.key', + null, + $this->samplesDir.'intermediate.crt', + PKCS7_DETACHED + ); + $signedMessage = $signer->sign($message); + + $this->assertMessageSignatureIsValid($signedMessage, $message); + } + + private function assertMessageSignatureIsValid(Message $message, Message $originalMessage): void + { + $messageFile = $this->generateTmpFilename(); + $messageString = $message->toString(); + file_put_contents($messageFile, $messageString); + + $this->assertMessageHeaders($message, $originalMessage); + $this->assertTrue(openssl_pkcs7_verify($messageFile, 0, $this->generateTmpFilename(), [$this->samplesDir.'ca.crt']), sprintf('Verification of the message %s failed. Internal error "%s".', $messageFile, openssl_error_string())); + + if (false === strpos($messageString, 'enveloped-data')) { + // Tamper to ensure it actually verified + file_put_contents($messageFile, str_replace('Content-Transfer-Encoding: ', 'Content-Transfer-Encoding: ', $messageString)); + $this->assertFalse(openssl_pkcs7_verify($messageFile, 0, $this->generateTmpFilename(), [$this->samplesDir.'ca.crt']), sprintf('Verification of the message failed. Internal error "%s".', openssl_error_string())); + } + } +} diff --git a/Tests/Crypto/SMimeTestCase.php b/Tests/Crypto/SMimeTestCase.php new file mode 100644 index 0000000..0bd3c13 --- /dev/null +++ b/Tests/Crypto/SMimeTestCase.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Tests\Crypto; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Mime\Exception\RuntimeException; +use Symfony\Component\Mime\Message; +use Symfony\Component\Mime\RawMessage; + +abstract class SMimeTestCase extends TestCase +{ + protected $samplesDir; + + protected function setUp(): void + { + $this->samplesDir = str_replace('\\', '/', realpath(__DIR__.'/../').'/_data/'); + } + + protected function generateTmpFilename(): string + { + return stream_get_meta_data(tmpfile())['uri']; + } + + protected function normalizeFilePath(string $path): string + { + if (!file_exists($path)) { + throw new RuntimeException(sprintf('File does not exist: %s', $path)); + } + + return str_replace('\\', '/', realpath($path)); + } + + protected function iterableToString(iterable $iterable): string + { + $string = ''; + + // Can't use iterator_to_array as the generators are merged internally, + // leading to overwritten keys + foreach ($iterable as $chunk) { + $string .= $chunk; + } + + return $string; + } + + protected function assertMessageHeaders(Message $message, RawMessage $originalMessage): void + { + $messageString = $message->toString(); + self::assertStringNotContainsString('Bcc: ', $messageString, '', true); + + if (!$originalMessage instanceof Message) { + return; + } + + if ($originalMessage->getHeaders()->has('Bcc')) { + self::assertEquals($originalMessage->getHeaders()->get('Bcc'), $message->getHeaders()->get('Bcc')); + } + + if ($originalMessage->getHeaders()->has('Subject')) { + self::assertEquals($originalMessage->getHeaders()->get('Subject'), $message->getPreparedHeaders()->get('Subject')); + self::assertStringContainsString('Subject:', $messageString, '', true); + } + } +} diff --git a/Tests/EmailTest.php b/Tests/EmailTest.php index 1d45cab..bfd3005 100644 --- a/Tests/EmailTest.php +++ b/Tests/EmailTest.php @@ -14,7 +14,6 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Email; -use Symfony\Component\Mime\NamedAddress; use Symfony\Component\Mime\Part\DataPart; use Symfony\Component\Mime\Part\Multipart\AlternativePart; use Symfony\Component\Mime\Part\Multipart\MixedPart; @@ -58,7 +57,7 @@ public function testFrom() { $e = new Email(); $helene = new Address('helene@symfony.com'); - $thomas = new NamedAddress('thomas@symfony.com', 'Thomas'); + $thomas = new Address('thomas@symfony.com', 'Thomas'); $caramel = new Address('caramel@symfony.com'); $this->assertSame($e, $e->from('fabien@symfony.com', $helene, $thomas)); @@ -91,7 +90,7 @@ public function testReplyTo() { $e = new Email(); $helene = new Address('helene@symfony.com'); - $thomas = new NamedAddress('thomas@symfony.com', 'Thomas'); + $thomas = new Address('thomas@symfony.com', 'Thomas'); $caramel = new Address('caramel@symfony.com'); $this->assertSame($e, $e->replyTo('fabien@symfony.com', $helene, $thomas)); @@ -124,7 +123,7 @@ public function testTo() { $e = new Email(); $helene = new Address('helene@symfony.com'); - $thomas = new NamedAddress('thomas@symfony.com', 'Thomas'); + $thomas = new Address('thomas@symfony.com', 'Thomas'); $caramel = new Address('caramel@symfony.com'); $this->assertSame($e, $e->to('fabien@symfony.com', $helene, $thomas)); @@ -157,7 +156,7 @@ public function testCc() { $e = new Email(); $helene = new Address('helene@symfony.com'); - $thomas = new NamedAddress('thomas@symfony.com', 'Thomas'); + $thomas = new Address('thomas@symfony.com', 'Thomas'); $caramel = new Address('caramel@symfony.com'); $this->assertSame($e, $e->cc('fabien@symfony.com', $helene, $thomas)); @@ -190,7 +189,7 @@ public function testBcc() { $e = new Email(); $helene = new Address('helene@symfony.com'); - $thomas = new NamedAddress('thomas@symfony.com', 'Thomas'); + $thomas = new Address('thomas@symfony.com', 'Thomas'); $caramel = new Address('caramel@symfony.com'); $this->assertSame($e, $e->bcc('fabien@symfony.com', $helene, $thomas)); @@ -252,58 +251,62 @@ public function testGenerateBody() $att = new DataPart($file = fopen(__DIR__.'/Fixtures/mimetypes/test', 'r')); $img = new DataPart($image = fopen(__DIR__.'/Fixtures/mimetypes/test.gif', 'r'), 'test.gif'); - $e = new Email(); + $e = (new Email())->from('me@example.com'); $e->text('text content'); $this->assertEquals($text, $e->getBody()); $this->assertEquals('text content', $e->getTextBody()); - $e = new Email(); + $e = (new Email())->from('me@example.com'); $e->html('html content'); $this->assertEquals($html, $e->getBody()); $this->assertEquals('html content', $e->getHtmlBody()); - $e = new Email(); + $e = (new Email())->from('me@example.com'); $e->html('html content'); $e->text('text content'); $this->assertEquals(new AlternativePart($text, $html), $e->getBody()); - $e = new Email(); + $e = (new Email())->from('me@example.com'); $e->html('html content', 'iso-8859-1'); $e->text('text content', 'iso-8859-1'); $this->assertEquals('iso-8859-1', $e->getTextCharset()); $this->assertEquals('iso-8859-1', $e->getHtmlCharset()); $this->assertEquals(new AlternativePart(new TextPart('text content', 'iso-8859-1'), new TextPart('html content', 'iso-8859-1', 'html')), $e->getBody()); - $e = new Email(); + $e = (new Email())->from('me@example.com'); $e->attach($file); $e->text('text content'); $this->assertEquals(new MixedPart($text, $att), $e->getBody()); - $e = new Email(); + $e = (new Email())->from('me@example.com'); $e->attach($file); $e->html('html content'); $this->assertEquals(new MixedPart($html, $att), $e->getBody()); - $e = new Email(); + $e = (new Email())->from('me@example.com'); + $e->attach($file); + $this->assertEquals(new MixedPart($att), $e->getBody()); + + $e = (new Email())->from('me@example.com'); $e->html('html content'); $e->text('text content'); $e->attach($file); $this->assertEquals(new MixedPart(new AlternativePart($text, $html), $att), $e->getBody()); - $e = new Email(); + $e = (new Email())->from('me@example.com'); $e->html('html content'); $e->text('text content'); $e->attach($file); $e->attach($image, 'test.gif'); $this->assertEquals(new MixedPart(new AlternativePart($text, $html), $att, $img), $e->getBody()); - $e = new Email(); + $e = (new Email())->from('me@example.com'); $e->text('text content'); $e->attach($file); $e->attach($image, 'test.gif'); $this->assertEquals(new MixedPart($text, $att, $img), $e->getBody()); - $e = new Email(); + $e = (new Email())->from('me@example.com'); $e->html($content = 'html content '); $e->text('text content'); $e->attach($file); @@ -311,13 +314,11 @@ public function testGenerateBody() $fullhtml = new TextPart($content, 'utf-8', 'html'); $this->assertEquals(new MixedPart(new AlternativePart($text, $fullhtml), $att, $img), $e->getBody()); - $e = new Email(); + $e = (new Email())->from('me@example.com'); $e->html($content = 'html content '); $e->text('text content'); $e->attach($file); $e->attach($image, 'test.gif'); - $fullhtml = new TextPart($content, 'utf-8', 'html'); - $inlinedimg = (new DataPart($image, 'test.gif'))->asInline(); $body = $e->getBody(); $this->assertInstanceOf(MixedPart::class, $body); $this->assertCount(2, $related = $body->getParts()); @@ -326,14 +327,14 @@ public function testGenerateBody() $this->assertCount(2, $parts = $related[0]->getParts()); $this->assertInstanceOf(AlternativePart::class, $parts[0]); $generatedHtml = $parts[0]->getParts()[1]; - $this->assertContains('cid:'.$parts[1]->getContentId(), $generatedHtml->getBody()); + $this->assertStringContainsString('cid:'.$parts[1]->getContentId(), $generatedHtml->getBody()); $content = 'html content '; $r = fopen('php://memory', 'r+', false); fwrite($r, $content); rewind($r); - $e = new Email(); + $e = (new Email())->from('me@example.com'); $e->html($r); // embedding the same image twice results in one image only in the email $e->embed($image, 'test.gif'); @@ -374,7 +375,7 @@ public function testSerialize() $e->from('fabien@symfony.com'); $e->text($r); $e->html($r); - $contents = file_get_contents($name = __DIR__.'/Fixtures/mimetypes/test', 'r'); + $name = __DIR__.'/Fixtures/mimetypes/test'; $file = fopen($name, 'r'); $e->attach($file, 'test'); $expected = clone $e; diff --git a/Tests/Fixtures/mimetypes/-test b/Tests/Fixtures/mimetypes/-test new file mode 100644 index 0000000..b636f4b Binary files /dev/null and b/Tests/Fixtures/mimetypes/-test differ diff --git a/Tests/Header/HeadersTest.php b/Tests/Header/HeadersTest.php index 2f4a1dd..e2eb75a 100644 --- a/Tests/Header/HeadersTest.php +++ b/Tests/Header/HeadersTest.php @@ -141,7 +141,7 @@ public function testGetReturnsNullIfHeaderNotSet() $this->assertNull($headers->get('Message-ID')); } - public function testGetAllReturnsAllHeadersMatchingName() + public function testAllReturnsAllHeadersMatchingName() { $header0 = new UnstructuredHeader('X-Test', 'some@id'); $header1 = new UnstructuredHeader('X-Test', 'other@id'); @@ -150,10 +150,10 @@ public function testGetAllReturnsAllHeadersMatchingName() $headers->addTextHeader('X-Test', 'some@id'); $headers->addTextHeader('X-Test', 'other@id'); $headers->addTextHeader('X-Test', 'more@id'); - $this->assertEquals([$header0, $header1, $header2], iterator_to_array($headers->getAll('X-Test'))); + $this->assertEquals([$header0, $header1, $header2], iterator_to_array($headers->all('X-Test'))); } - public function testGetAllReturnsAllHeadersIfNoArguments() + public function testAllReturnsAllHeadersIfNoArguments() { $header0 = new IdentificationHeader('Message-ID', 'some@id'); $header1 = new UnstructuredHeader('Subject', 'thing'); @@ -162,19 +162,17 @@ public function testGetAllReturnsAllHeadersIfNoArguments() $headers->addIdHeader('Message-ID', 'some@id'); $headers->addTextHeader('Subject', 'thing'); $headers->addMailboxListHeader('To', [new Address('person@example.org')]); - $this->assertEquals(['message-id' => $header0, 'subject' => $header1, 'to' => $header2], iterator_to_array($headers->getAll())); + $this->assertEquals(['message-id' => $header0, 'subject' => $header1, 'to' => $header2], iterator_to_array($headers->all())); } - public function testGetAllReturnsEmptyArrayIfNoneSet() + public function testAllReturnsEmptyArrayIfNoneSet() { $headers = new Headers(); - $this->assertEquals([], iterator_to_array($headers->getAll('Received'))); + $this->assertEquals([], iterator_to_array($headers->all('Received'))); } public function testRemoveRemovesAllHeadersWithName() { - $header0 = new UnstructuredHeader('X-Test', 'some@id'); - $header1 = new UnstructuredHeader('X-Test', 'other@id'); $headers = new Headers(); $headers->addIdHeader('X-Test', 'some@id'); $headers->addIdHeader('X-Test', 'other@id'); @@ -185,7 +183,6 @@ public function testRemoveRemovesAllHeadersWithName() public function testHasIsNotCaseSensitive() { - $header = new IdentificationHeader('Message-ID', 'some@id'); $headers = new Headers(); $headers->addIdHeader('Message-ID', 'some@id'); $this->assertTrue($headers->has('message-id')); @@ -199,17 +196,16 @@ public function testGetIsNotCaseSensitive() $this->assertEquals($header, $headers->get('message-id')); } - public function testGetAllIsNotCaseSensitive() + public function testAllIsNotCaseSensitive() { $header = new IdentificationHeader('Message-ID', 'some@id'); $headers = new Headers(); $headers->addIdHeader('Message-ID', 'some@id'); - $this->assertEquals([$header], iterator_to_array($headers->getAll('message-id'))); + $this->assertEquals([$header], iterator_to_array($headers->all('message-id'))); } public function testRemoveIsNotCaseSensitive() { - $header = new IdentificationHeader('Message-ID', 'some@id'); $headers = new Headers(); $headers->addIdHeader('Message-ID', 'some@id'); $headers->remove('message-id'); diff --git a/Tests/Header/IdentificationHeaderTest.php b/Tests/Header/IdentificationHeaderTest.php index 7d94d4d..7d274ab 100644 --- a/Tests/Header/IdentificationHeaderTest.php +++ b/Tests/Header/IdentificationHeaderTest.php @@ -99,13 +99,11 @@ public function testIdLeftCanBeDotAtom() $this->assertEquals('', $header->getBodyAsString()); } - /** - * @expectedException \Exception - * @expectedMessageException "a b c" is not valid id-left - */ public function testInvalidIdLeftThrowsException() { - $header = new IdentificationHeader('References', 'a b c@d'); + $this->expectException('Exception'); + $this->expectExceptionMessage('Email "a b c@d" does not comply with addr-spec of RFC 2822.'); + new IdentificationHeader('References', 'a b c@d'); } public function testIdRightCanBeDotAtom() @@ -137,25 +135,21 @@ public function testIdRigthIsIdnEncoded() $this->assertEquals('', $header->getBodyAsString()); } - /** - * @expectedException \Exception - * @expectedMessageException "b c d" is not valid id-right - */ public function testInvalidIdRightThrowsException() { - $header = new IdentificationHeader('References', 'a@b c d'); + $this->expectException('Exception'); + $this->expectExceptionMessage('Email "a@b c d" does not comply with addr-spec of RFC 2822.'); + new IdentificationHeader('References', 'a@b c d'); } - /** - * @expectedException \Exception - * @expectedMessageException "abc" is does not contain @ - */ public function testMissingAtSignThrowsException() { + $this->expectException('Exception'); + $this->expectExceptionMessage('Email "abc" does not comply with addr-spec of RFC 2822.'); /* -- RFC 2822, 3.6.4. msg-id = [CFWS] "<" id-left "@" id-right ">" [CFWS] */ - $header = new IdentificationHeader('References', 'abc'); + new IdentificationHeader('References', 'abc'); } public function testSetBody() diff --git a/Tests/Header/MailboxHeaderTest.php b/Tests/Header/MailboxHeaderTest.php index 11a7ac9..cca27db 100644 --- a/Tests/Header/MailboxHeaderTest.php +++ b/Tests/Header/MailboxHeaderTest.php @@ -14,7 +14,6 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Header\MailboxHeader; -use Symfony\Component\Mime\NamedAddress; class MailboxHeaderTest extends TestCase { @@ -44,26 +43,24 @@ public function testgetBodyAsString() $header->setAddress(new Address('fabien@sïmfony.com')); $this->assertEquals('fabien@xn--smfony-iwa.com', $header->getBodyAsString()); - $header = new MailboxHeader('Sender', new NamedAddress('fabien@symfony.com', 'Fabien Potencier')); + $header = new MailboxHeader('Sender', new Address('fabien@symfony.com', 'Fabien Potencier')); $this->assertEquals('Fabien Potencier ', $header->getBodyAsString()); - $header = new MailboxHeader('Sender', new NamedAddress('fabien@symfony.com', 'Fabien Potencier, "from Symfony"')); + $header = new MailboxHeader('Sender', new Address('fabien@symfony.com', 'Fabien Potencier, "from Symfony"')); $this->assertEquals('"Fabien Potencier, \"from Symfony\"" ', $header->getBodyAsString()); - $header = new MailboxHeader('From', new NamedAddress('fabien@symfony.com', 'Fabien Potencier, \\escaped\\')); + $header = new MailboxHeader('From', new Address('fabien@symfony.com', 'Fabien Potencier, \\escaped\\')); $this->assertEquals('"Fabien Potencier, \\\\escaped\\\\" ', $header->getBodyAsString()); $name = 'P'.pack('C', 0x8F).'tencier'; - $header = new MailboxHeader('Sender', new NamedAddress('fabien@symfony.com', 'Fabien '.$name)); + $header = new MailboxHeader('Sender', new Address('fabien@symfony.com', 'Fabien '.$name)); $header->setCharset('iso-8859-1'); $this->assertEquals('Fabien =?'.$header->getCharset().'?Q?P=8Ftencier?= ', $header->getBodyAsString()); } - /** - * @expectedException \Symfony\Component\Mime\Exception\AddressEncoderException - */ public function testUtf8CharsInLocalPartThrows() { + $this->expectException('Symfony\Component\Mime\Exception\AddressEncoderException'); $header = new MailboxHeader('Sender', new Address('fabïen@symfony.com')); $header->getBodyAsString(); } @@ -73,7 +70,7 @@ public function testToString() $header = new MailboxHeader('Sender', new Address('fabien@symfony.com')); $this->assertEquals('Sender: fabien@symfony.com', $header->toString()); - $header = new MailboxHeader('Sender', new NamedAddress('fabien@symfony.com', 'Fabien Potencier')); + $header = new MailboxHeader('Sender', new Address('fabien@symfony.com', 'Fabien Potencier')); $this->assertEquals('Sender: Fabien Potencier ', $header->toString()); } } diff --git a/Tests/Header/MailboxListHeaderTest.php b/Tests/Header/MailboxListHeaderTest.php index a2a2805..4cace96 100644 --- a/Tests/Header/MailboxListHeaderTest.php +++ b/Tests/Header/MailboxListHeaderTest.php @@ -14,7 +14,6 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Header\MailboxListHeader; -use Symfony\Component\Mime\NamedAddress; class MailboxListHeaderTest extends TestCase { @@ -28,7 +27,7 @@ public function testMailboxIsSetForAddress() public function testMailboxIsRenderedForNameAddress() { - $header = new MailboxListHeader('From', [new NamedAddress('chris@swiftmailer.org', 'Chris Corbyn')]); + $header = new MailboxListHeader('From', [new Address('chris@swiftmailer.org', 'Chris Corbyn')]); $this->assertEquals(['Chris Corbyn '], $header->getAddressStrings()); } @@ -40,34 +39,32 @@ public function testAddressCanBeReturnedForAddress() public function testQuotesInNameAreQuoted() { - $header = new MailboxListHeader('From', [new NamedAddress('chris@swiftmailer.org', 'Chris Corbyn, "DHE"')]); + $header = new MailboxListHeader('From', [new Address('chris@swiftmailer.org', 'Chris Corbyn, "DHE"')]); $this->assertEquals(['"Chris Corbyn, \"DHE\"" '], $header->getAddressStrings()); } public function testEscapeCharsInNameAreQuoted() { - $header = new MailboxListHeader('From', [new NamedAddress('chris@swiftmailer.org', 'Chris Corbyn, \\escaped\\')]); + $header = new MailboxListHeader('From', [new Address('chris@swiftmailer.org', 'Chris Corbyn, \\escaped\\')]); $this->assertEquals(['"Chris Corbyn, \\\\escaped\\\\" '], $header->getAddressStrings()); } public function testUtf8CharsInDomainAreIdnEncoded() { - $header = new MailboxListHeader('From', [new NamedAddress('chris@swïftmailer.org', 'Chris Corbyn')]); + $header = new MailboxListHeader('From', [new Address('chris@swïftmailer.org', 'Chris Corbyn')]); $this->assertEquals(['Chris Corbyn '], $header->getAddressStrings()); } - /** - * @expectedException \Symfony\Component\Mime\Exception\AddressEncoderException - */ public function testUtf8CharsInLocalPartThrows() { - $header = new MailboxListHeader('From', [new NamedAddress('chrïs@swiftmailer.org', 'Chris Corbyn')]); + $this->expectException('Symfony\Component\Mime\Exception\AddressEncoderException'); + $header = new MailboxListHeader('From', [new Address('chrïs@swiftmailer.org', 'Chris Corbyn')]); $header->getAddressStrings(); } public function testGetMailboxesReturnsNameValuePairs() { - $header = new MailboxListHeader('From', $addresses = [new NamedAddress('chris@swiftmailer.org', 'Chris Corbyn, DHE')]); + $header = new MailboxListHeader('From', $addresses = [new Address('chris@swiftmailer.org', 'Chris Corbyn, DHE')]); $this->assertEquals($addresses, $header->getAddresses()); } @@ -80,7 +77,7 @@ public function testMultipleAddressesAsMailboxStrings() public function testNameIsEncodedIfNonAscii() { $name = 'C'.pack('C', 0x8F).'rbyn'; - $header = new MailboxListHeader('From', [new NamedAddress('chris@swiftmailer.org', 'Chris '.$name)]); + $header = new MailboxListHeader('From', [new Address('chris@swiftmailer.org', 'Chris '.$name)]); $header->setCharset('iso-8859-1'); $addresses = $header->getAddressStrings(); $this->assertEquals('Chris =?'.$header->getCharset().'?Q?C=8Frbyn?= ', array_shift($addresses)); @@ -94,7 +91,7 @@ public function testEncodingLineLengthCalculations() */ $name = 'C'.pack('C', 0x8F).'rbyn'; - $header = new MailboxListHeader('From', [new NamedAddress('chris@swiftmailer.org', 'Chris '.$name)]); + $header = new MailboxListHeader('From', [new Address('chris@swiftmailer.org', 'Chris '.$name)]); $header->setCharset('iso-8859-1'); $addresses = $header->getAddressStrings(); $this->assertEquals('Chris =?'.$header->getCharset().'?Q?C=8Frbyn?= ', array_shift($addresses)); @@ -102,13 +99,13 @@ public function testEncodingLineLengthCalculations() public function testGetValueReturnsMailboxStringValue() { - $header = new MailboxListHeader('From', [new NamedAddress('chris@swiftmailer.org', 'Chris Corbyn')]); + $header = new MailboxListHeader('From', [new Address('chris@swiftmailer.org', 'Chris Corbyn')]); $this->assertEquals('Chris Corbyn ', $header->getBodyAsString()); } public function testGetValueReturnsMailboxStringValueForMultipleMailboxes() { - $header = new MailboxListHeader('From', [new NamedAddress('chris@swiftmailer.org', 'Chris Corbyn'), new NamedAddress('mark@swiftmailer.org', 'Mark Corbyn')]); + $header = new MailboxListHeader('From', [new Address('chris@swiftmailer.org', 'Chris Corbyn'), new Address('mark@swiftmailer.org', 'Mark Corbyn')]); $this->assertEquals('Chris Corbyn , Mark Corbyn ', $header->getBodyAsString()); } @@ -127,7 +124,7 @@ public function testGetBody() public function testToString() { - $header = new MailboxListHeader('From', [new NamedAddress('chris@example.org', 'Chris Corbyn'), new NamedAddress('mark@example.org', 'Mark Corbyn')]); + $header = new MailboxListHeader('From', [new Address('chris@example.org', 'Chris Corbyn'), new Address('mark@example.org', 'Mark Corbyn')]); $this->assertEquals('From: Chris Corbyn , Mark Corbyn ', $header->toString()); } } diff --git a/Tests/Header/ParameterizedHeaderTest.php b/Tests/Header/ParameterizedHeaderTest.php index aa82658..e41d038 100644 --- a/Tests/Header/ParameterizedHeaderTest.php +++ b/Tests/Header/ParameterizedHeaderTest.php @@ -205,16 +205,25 @@ public function testValueAndParamCanBeEncodedIfNonAscii() $header = new ParameterizedHeader('X-Foo', $value); $header->setCharset('iso-8859-1'); $header->setParameters(['says' => $value]); - $this->assertEquals('X-Foo: =?'.$header->getCharset().'?Q?fo=8Fbar?=; says="=?'.$header->getCharset().'?Q?fo=8Fbar?="', $header->toString()); + $this->assertEquals('X-Foo: =?'.$header->getCharset().'?Q?fo=8Fbar?=; says*='.$header->getCharset()."''fo%8Fbar", $header->toString()); } - public function testParamsAreEncodedWithEncodedWordsIfNoParamEncoderSet() + public function testParamsAreEncodedIfNonAscii() { $value = 'fo'.pack('C', 0x8F).'bar'; $header = new ParameterizedHeader('X-Foo', 'bar'); $header->setCharset('iso-8859-1'); $header->setParameters(['says' => $value]); - $this->assertEquals('X-Foo: bar; says="=?'.$header->getCharset().'?Q?fo=8Fbar?="', $header->toString()); + $this->assertEquals('X-Foo: bar; says*='.$header->getCharset()."''fo%8Fbar", $header->toString()); + } + + public function testParamsAreEncodedWithLegacyEncodingEnabled() + { + $value = 'fo'.pack('C', 0x8F).'bar'; + $header = new ParameterizedHeader('Content-Type', 'bar'); + $header->setCharset('iso-8859-1'); + $header->setParameters(['says' => $value]); + $this->assertEquals('Content-Type: bar; says="=?'.$header->getCharset().'?Q?fo=8Fbar?="', $header->toString()); } public function testLanguageInformationAppearsInEncodedWords() @@ -234,6 +243,18 @@ public function testLanguageInformationAppearsInEncodedWords() tag. For example: From: =?US-ASCII*EN?Q?Keith_Moore?= + + -- RFC 2047, 5. Use of encoded-words in message headers + ... + + An 'encoded-word' MUST NOT be used in parameter of a MIME + Content-Type or Content-Disposition field, or in any structured + field body except within a 'comment' or 'phrase'. + + -- RFC 2047, Appendix - changes since RFC 1522 + ... + + clarify that encoded-words are allowed in '*text' fields in both + RFC822 headers and MIME body part headers, but NOT as parameter + values. */ $value = 'fo'.pack('C', 0x8F).'bar'; @@ -241,7 +262,7 @@ public function testLanguageInformationAppearsInEncodedWords() $header->setCharset('iso-8859-1'); $header->setLanguage('en'); $header->setParameters(['says' => $value]); - $this->assertEquals('X-Foo: =?'.$header->getCharset().'*en?Q?fo=8Fbar?=; says="=?'.$header->getCharset().'*en?Q?fo=8Fbar?="', $header->toString()); + $this->assertEquals('X-Foo: =?'.$header->getCharset().'*en?Q?fo=8Fbar?=; says*='.$header->getCharset()."'en'fo%8Fbar", $header->toString()); } public function testSetBody() diff --git a/Tests/Header/PathHeaderTest.php b/Tests/Header/PathHeaderTest.php index 8f41959..a8386f8 100644 --- a/Tests/Header/PathHeaderTest.php +++ b/Tests/Header/PathHeaderTest.php @@ -23,12 +23,10 @@ public function testSingleAddressCanBeSetAndFetched() $this->assertEquals($address, $header->getAddress()); } - /** - * @expectedException \Exception - */ public function testAddressMustComplyWithRfc2822() { - $header = new PathHeader('Return-Path', new Address('chr is@swiftmailer.org')); + $this->expectException('Exception'); + new PathHeader('Return-Path', new Address('chr is@swiftmailer.org')); } public function testValueIsAngleAddrWithValidAddress() @@ -51,11 +49,9 @@ public function testAddressIsIdnEncoded() $this->assertEquals('', $header->getBodyAsString()); } - /** - * @expectedException \Symfony\Component\Mime\Exception\AddressEncoderException - */ public function testAddressMustBeEncodable() { + $this->expectException('Symfony\Component\Mime\Exception\AddressEncoderException'); $header = new PathHeader('Return-Path', new Address('chrïs@swiftmailer.org')); $header->getBodyAsString(); } diff --git a/Tests/MessageTest.php b/Tests/MessageTest.php index dbeb0a5..bd5d7ca 100644 --- a/Tests/MessageTest.php +++ b/Tests/MessageTest.php @@ -17,7 +17,6 @@ use Symfony\Component\Mime\Header\MailboxListHeader; use Symfony\Component\Mime\Header\UnstructuredHeader; use Symfony\Component\Mime\Message; -use Symfony\Component\Mime\NamedAddress; use Symfony\Component\Mime\Part\TextPart; class MessageTest extends TestCase @@ -68,7 +67,7 @@ public function testGetPreparedHeaders() $message = new Message(); $message->getHeaders()->addMailboxListHeader('From', ['fabien@symfony.com']); $h = $message->getPreparedHeaders(); - $this->assertCount(4, iterator_to_array($h->getAll())); + $this->assertCount(4, iterator_to_array($h->all())); $this->assertEquals(new MailboxListHeader('From', [new Address('fabien@symfony.com')]), $h->get('From')); $this->assertEquals(new UnstructuredHeader('MIME-Version', '1.0'), $h->get('mime-version')); $this->assertTrue($h->has('Message-Id')); @@ -94,9 +93,9 @@ public function testGetPreparedHeadersWithNoFrom() public function testGetPreparedHeadersWithNamedFrom() { $message = new Message(); - $message->getHeaders()->addMailboxListHeader('From', [new NamedAddress('fabien@symfony.com', 'Fabien')]); + $message->getHeaders()->addMailboxListHeader('From', [new Address('fabien@symfony.com', 'Fabien')]); $h = $message->getPreparedHeaders(); - $this->assertEquals(new MailboxListHeader('From', [new NamedAddress('fabien@symfony.com', 'Fabien')]), $h->get('From')); + $this->assertEquals(new MailboxListHeader('From', [new Address('fabien@symfony.com', 'Fabien')]), $h->get('From')); $this->assertTrue($h->has('Message-Id')); } diff --git a/Tests/MimeTypesTest.php b/Tests/MimeTypesTest.php index c5ff262..a736dbe 100644 --- a/Tests/MimeTypesTest.php +++ b/Tests/MimeTypesTest.php @@ -47,6 +47,8 @@ public function testGetExtensions() $mt = new MimeTypes(); $this->assertSame(['mbox'], $mt->getExtensions('application/mbox')); $this->assertSame(['ai', 'eps', 'ps'], $mt->getExtensions('application/postscript')); + $this->assertContains('svg', $mt->getExtensions('image/svg+xml')); + $this->assertContains('svg', $mt->getExtensions('image/svg')); $this->assertSame([], $mt->getExtensions('application/whatever-symfony')); } @@ -56,6 +58,8 @@ public function testGetMimeTypes() $this->assertSame(['application/mbox'], $mt->getMimeTypes('mbox')); $this->assertContains('application/postscript', $mt->getMimeTypes('ai')); $this->assertContains('application/postscript', $mt->getMimeTypes('ps')); + $this->assertContains('image/svg+xml', $mt->getMimeTypes('svg')); + $this->assertContains('image/svg', $mt->getMimeTypes('svg')); $this->assertSame([], $mt->getMimeTypes('symfony')); } } diff --git a/Tests/NamedAddressTest.php b/Tests/NamedAddressTest.php deleted file mode 100644 index 7284019..0000000 --- a/Tests/NamedAddressTest.php +++ /dev/null @@ -1,27 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Mime\Tests; - -use PHPUnit\Framework\TestCase; -use Symfony\Component\Mime\NamedAddress; - -class NamedAddressTest extends TestCase -{ - public function testConstructor() - { - $a = new NamedAddress('fabien@symfonï.com', 'Fabien'); - $this->assertEquals('Fabien', $a->getName()); - $this->assertEquals('fabien@symfonï.com', $a->getAddress()); - $this->assertEquals('Fabien ', $a->toString()); - $this->assertEquals('fabien@xn--symfon-nwa.com', $a->getEncodedAddress()); - } -} diff --git a/Tests/Part/MessagePartTest.php b/Tests/Part/MessagePartTest.php index 3855e08..21a4eb0 100644 --- a/Tests/Part/MessagePartTest.php +++ b/Tests/Part/MessagePartTest.php @@ -23,9 +23,9 @@ class MessagePartTest extends TestCase public function testConstructor() { $p = new MessagePart((new Email())->from('fabien@symfony.com')->text('content')); - $this->assertContains('content', $p->getBody()); - $this->assertContains('content', $p->bodyToString()); - $this->assertContains('content', implode('', iterator_to_array($p->bodyToIterable()))); + $this->assertStringContainsString('content', $p->getBody()); + $this->assertStringContainsString('content', $p->bodyToString()); + $this->assertStringContainsString('content', implode('', iterator_to_array($p->bodyToIterable()))); $this->assertEquals('message', $p->getMediaType()); $this->assertEquals('rfc822', $p->getMediaSubType()); } diff --git a/Tests/Part/Multipart/FormDataPartTest.php b/Tests/Part/Multipart/FormDataPartTest.php index 127fce0..a74ecea 100644 --- a/Tests/Part/Multipart/FormDataPartTest.php +++ b/Tests/Part/Multipart/FormDataPartTest.php @@ -26,7 +26,7 @@ public function testConstructor() $b = new TextPart('content'); $c = DataPart::fromPath($file = __DIR__.'/../../Fixtures/mimetypes/test.gif'); $f = new FormDataPart([ - 'foo' => $content = 'very very long content that will not be cut even if the length i way more than 76 characters, ok?', + 'foo' => $content = 'very very long content that will not be cut even if the length is way more than 76 characters, ok?', 'bar' => clone $b, 'baz' => clone $c, ]); @@ -47,6 +47,34 @@ public function testConstructor() $this->assertEquals([$t, $b, $c], $f->getParts()); } + public function testNestedArrayParts() + { + $p1 = new TextPart('content', 'utf-8', 'plain', '8bit'); + $f = new FormDataPart([ + 'foo' => clone $p1, + 'bar' => [ + 'baz' => [ + clone $p1, + 'qux' => clone $p1, + ], + ], + ]); + + $this->assertEquals('multipart', $f->getMediaType()); + $this->assertEquals('form-data', $f->getMediaSubtype()); + + $p1->setName('foo'); + $p1->setDisposition('form-data'); + + $p2 = clone $p1; + $p2->setName('bar[baz][0]'); + + $p3 = clone $p1; + $p3->setName('bar[baz][qux]'); + + $this->assertEquals([$p1, $p2, $p3], $f->getParts()); + } + public function testToString() { $p = DataPart::fromPath($file = __DIR__.'/../../Fixtures/mimetypes/test.gif'); diff --git a/Tests/_data/ca.crt b/Tests/_data/ca.crt new file mode 100644 index 0000000..bca02b3 --- /dev/null +++ b/Tests/_data/ca.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDFDCCAfwCCQDaMw8tuy1dgDANBgkqhkiG9w0BAQsFADBMMRcwFQYDVQQDDA5T +eW1mb255TWltZSBDQTEUMBIGA1UECgwLU3ltZm9ueU1pbWUxDjAMBgNVBAcMBVBh +cmlzMQswCQYDVQQGEwJGUjAeFw0xOTA0MTkxNDIwMTFaFw0yMzA0MTgxNDIwMTFa +MEwxFzAVBgNVBAMMDlN5bWZvbnlNaW1lIENBMRQwEgYDVQQKDAtTeW1mb255TWlt +ZTEOMAwGA1UEBwwFUGFyaXMxCzAJBgNVBAYTAkZSMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAnvxOWE8qOVkuYbTu6u4Oao2n91FPF6umrcF8mq0uD2G0 +dtOJuFaR7FeElmJnHfWvqvesCigXyA7kpdVBFGhEo83SGYTbPSGzehWDc7Kvc321 +UPvNb61T2Ekdo+5ufrpbzlOPtTTaVL98dFEZntYNM3CXnnSSdeKz38NlHHV3QsDZ +crQRMxHrYi2bgkhxVoAY03ZQRbb95rEE1cfyGZ0x6VSBrVC2nnEUT2vopwny/vy+ +QSn3oga+ucMkxJdoD8MA13Zh5I4Uiozl82xoWH/zmVrqrrO2lNBv7WYOnwbv6MSr +5kCE3Kcqzs8qAGv62GYyS4exIMEZsbbPv3cvp9hgYQIDAQABMA0GCSqGSIb3DQEB +CwUAA4IBAQBuJtPqAX6ApOymDux9sRqxx5FMIIEX2TmanSSSLesP0AVVLv8Am8/p +Xs8N9e49KoQhnQ3FmdtwY6IV6f3yIMnZxmkXZoUi4zCkSZd/+2iap1c51zV1b6NC +4C5LZtdWzhons4jOmtmxaMSy08oPPYv1wXATjjfHvqqYa/7axLY1mqbxLYC437Fv +H5zkdzQM2qXpIgtCjlXfOd/L9Az5DTSH4UvWiiocRdmnxGP+nMEOuUUvLzokJSeq +Otw4gjxczF8NQ/g/io6iG3w4OfjgRrCpuMv/l3eYClC7vDXOX9S172CpzaD/qkHM +NFxckxTgT4ylmivmHZWym4xS1bkAAAsd +-----END CERTIFICATE----- diff --git a/Tests/_data/ca.key b/Tests/_data/ca.key new file mode 100644 index 0000000..4832a1d --- /dev/null +++ b/Tests/_data/ca.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAnvxOWE8qOVkuYbTu6u4Oao2n91FPF6umrcF8mq0uD2G0dtOJ +uFaR7FeElmJnHfWvqvesCigXyA7kpdVBFGhEo83SGYTbPSGzehWDc7Kvc321UPvN +b61T2Ekdo+5ufrpbzlOPtTTaVL98dFEZntYNM3CXnnSSdeKz38NlHHV3QsDZcrQR +MxHrYi2bgkhxVoAY03ZQRbb95rEE1cfyGZ0x6VSBrVC2nnEUT2vopwny/vy+QSn3 +oga+ucMkxJdoD8MA13Zh5I4Uiozl82xoWH/zmVrqrrO2lNBv7WYOnwbv6MSr5kCE +3Kcqzs8qAGv62GYyS4exIMEZsbbPv3cvp9hgYQIDAQABAoIBAD/Y1WGzkSJsxSqp +7dTc+18hOlYhCiFYZtyaun6nk7rLoxyhQUqNQZbnYrC+HekzNHP1eNqvVTWbfYl3 +heY7JW2fB4QGDcGUGi6qGxtIpBs+XaWDKfJyahyO6F9gLnGoR5wphKnh6thj+ggA +Vciq76w7yDfzWqoK+++d2ao/JkDg7YrpOQonfceYgjTtiXXFDV4cm4GgKr7gWolt +AqZbHcbH6pmbwRduT+g5QjsYmYPven/ji6Mr2eTFwE0qjlwj4LHlEWuKgpwAnLc/ +jzdnx3UjRGTbiMnbxrv4sHApW9Bb02aRWHVG3axkxWFyWefKuGRvXUZAguGvMpeq +Ng6Jc9ECgYEA0YpHxa32IFRzkOC7Vs79uKWmkbiYZihSrAyCG7Xo5rxTtB5HUcB+ +qIrFU2t2OSDffrRS5C6Ewpw3kBgYElsoYyqL/h1Kb+SQzZVwgK3PAF9p4mcgzyCU +Q25Nqy2CyX3gZblQMK6ui5aI7ZC3WE2wl8fxAneZOtHEEw2e0DaiUP0CgYEAwjx+ +gQr+NHFbDSfhh1IdIz+kGBgR+TS0OIjE2/Mb5IUfDzMsWGo0JEpTH1ma+e7VrxCC +9o47dvz5PXlHAuxsgLEXN7NEPqhiluAbTG/YEpsYeqftqKJsFROmFa3TDeEp3LGz +2OVY/uZjxNVVfljS/weGhOXGfATwQQoAUFbEzDUCgYEAznRsmvz4EIqlAw4qBzIT +EydDozg6EA2Sxynb1+m3+/96iXF727TKFs4D9llfNpKJIpIRSfn7nLPGmxbiQNPI +S0zUeh/qA600bxraqi6WUkuwS/5IeUwkSPwZUpuYzWZU/mVD+XNjTu2XJFr+Cuch +I6tAb6nfM/ESO6Oj4oqyCxECgYBsXr4iF1UPQ3OOmoKtMnZJVVejjcJxbSNkK4LS +SQh17oQOwflq9w5SdRl9c0wRSFz2iNrY3zB0Sd5xmvmwuuIqxyNyE1XvM5mWHkF8 +2yYN83Sr8oeZv81X0ReoHsyTgN4PYSI70HJf/YEKsBA8JyjJ25QFEAI27bZyQzc7 +m72/RQKBgEtyibh8X7DC9B3oVMZAX1BJJDzDSH1RyRaoa+7nARSl90qJD877NZ8o +jteoRFNJJzruADouffK+lTlMtwdfQJQW4wGYGiyr1S5dKXNsPmcnKCj7HbvBphVA +oCzZi3txFcOmH4IZ5HA0VxvGViQwV7fyl5ch7XVqSFOeFaa6lIF5 +-----END RSA PRIVATE KEY----- diff --git a/Tests/_data/ca.srl b/Tests/_data/ca.srl new file mode 100644 index 0000000..8543646 --- /dev/null +++ b/Tests/_data/ca.srl @@ -0,0 +1 @@ +C51E36445BB0C79B diff --git a/Tests/_data/create-cert.sh b/Tests/_data/create-cert.sh new file mode 100755 index 0000000..3f36d2f --- /dev/null +++ b/Tests/_data/create-cert.sh @@ -0,0 +1,52 @@ +#!/bin/sh + +openssl genrsa -out ca.key 2048 +openssl req -x509 -new -nodes -key ca.key -days 1460 -subj '/CN=SymfonyMime CA/O=SymfonyMime/L=Paris/C=FR' -out ca.crt +openssl x509 -in ca.crt -clrtrust -out ca.crt + +## Sign + +openssl genrsa -out sign.key 2048 +openssl req -new -key sign.key -subj '/CN=fabien@symfony.com/O=SymfonyMime/L=Paris/C=FR/emailAddress=fabien@symfony.com' -out sign.csr +openssl x509 -req -in sign.csr -CA ca.crt -CAkey ca.key -out sign.crt -days 1460 -addtrust emailProtection +openssl x509 -in sign.crt -clrtrust -out sign.crt + +rm sign.csr + +openssl genrsa -out intermediate.key 2048 +openssl req -new -key intermediate.key -subj '/CN=SymfonyMime Intermediate/O=SymfonyMime/L=Paris/C=FR' -out intermediate.csr +openssl x509 -req -in intermediate.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out intermediate.crt -days 1460 +openssl x509 -in intermediate.crt -clrtrust -out intermediate.crt + +rm intermediate.csr + +openssl genrsa -out sign2.key 2048 +openssl req -new -key sign2.key -subj '/CN=SymfonyMime-User2/O=SymfonyMime/L=Paris/C=FR' -out sign2.csr +openssl x509 -req -in sign2.csr -CA intermediate.crt -CAkey intermediate.key -set_serial 01 -out sign2.crt -days 1460 -addtrust emailProtection +openssl x509 -in sign2.crt -clrtrust -out sign2.crt + +rm sign2.csr + +### Sign with passphrase +openssl genrsa -aes256 -passout pass:symfony-rocks -out sign3.key 2048 +openssl req -new -key sign3.key -passin pass:symfony-rocks -subj '/CN=SymfonyMime-User3/O=SymfonyMime/L=Paris/C=FR' -out sign3.csr +openssl x509 -req -in sign3.csr -CA ca.crt -CAkey ca.key -out sign3.crt -days 1460 -addtrust emailProtection +openssl x509 -in sign3.crt -clrtrust -out sign3.crt + +rm sign3.csr + +## Encrypt + +openssl genrsa -out encrypt.key 2048 +openssl req -new -key encrypt.key -subj '/CN=SymfonyMime-User/O=SymfonyMime/L=Paris/C=FR' -out encrypt.csr +openssl x509 -req -in encrypt.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out encrypt.crt -days 1460 -addtrust emailProtection +openssl x509 -in encrypt.crt -clrtrust -out encrypt.crt + +rm encrypt.csr + +openssl genrsa -out encrypt2.key 2048 +openssl req -new -key encrypt2.key -subj '/CN=SymfonyMime-User2/O=SymfonyMime/L=Paris/C=FR' -out encrypt2.csr +openssl x509 -req -in encrypt2.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out encrypt2.crt -days 1460 -addtrust emailProtection +openssl x509 -in encrypt2.crt -clrtrust -out encrypt2.crt + +rm encrypt2.csr diff --git a/Tests/_data/encrypt.crt b/Tests/_data/encrypt.crt new file mode 100644 index 0000000..e8a5a7c --- /dev/null +++ b/Tests/_data/encrypt.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDFjCCAf4CCQDFHjZEW7DHmjANBgkqhkiG9w0BAQUFADBMMRcwFQYDVQQDDA5T +eW1mb255TWltZSBDQTEUMBIGA1UECgwLU3ltZm9ueU1pbWUxDjAMBgNVBAcMBVBh +cmlzMQswCQYDVQQGEwJGUjAeFw0xOTA0MTkxNDIwMTdaFw0yMzA0MTgxNDIwMTda +ME4xGTAXBgNVBAMMEFN5bWZvbnlNaW1lLVVzZXIxFDASBgNVBAoMC1N5bWZvbnlN +aW1lMQ4wDAYDVQQHDAVQYXJpczELMAkGA1UEBhMCRlIwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQCxnMT1TGmWhBp4K6IKztiplKVsdoYvi8JsflTpBHiw +/tLB3ikytItSADuqb/aEX/upgpvPQNJWa0Gf7f9yOQ0CekhTsNtP+o7UA9LtGrcI +lM1szBoaVhjpBgBAyP5OXcK7pOSRmUgp+vD/I9TRdRdzwwoJzvb35gpWGNZJ3WF0 +k9z4KqjdJDpQ7QBcEwZXVr8z5VnQ3gl8olY0AyN9Dh6B52uejGd1fBHf5v+hAR+5 +A0AAOOsTCa4kSXU2KaX9fNd0z/oK+GowfYtfrcCCVLaA6rmEGATQ9meGb54VBFVY +xarMX0ZY+0C3r8a9h8dJ9qxisMWksKLW8mE97/CclNHlAgMBAAEwDQYJKoZIhvcN +AQEFBQADggEBAAP4r76F+5EF+wgOvDlDU+KYXI4LfAy/yIvI5cDOLh65iAwgSWKX +HQPBDzPbQoJaTwj4XPwc4Ygrk7yftgcdYXRm5GWs5pp7DvSfskaX7TSuvNHt0M2A +gAo/rPH5BXp0/C+zgcmFVL067uhB10YHgsrX1ppLFPOsWvXNGAsKA4Qt2pxquI/g +UpNoucZ45Y1+idUq99jQr7sXdL3o5o1LLUdI64vrV/y2AYhUGn+NJvz1bXsp5NIV +jfBaYrAdZ4BMOF6gDMaJekI4PMcoH9sJFr1OIcKnk+UlGir+gAuaQGjKjOKjhB2r +KpZ7PMSJTC+bJYl3KVoIjBJ9/Bf1yjygb38= +-----END CERTIFICATE----- diff --git a/Tests/_data/encrypt.key b/Tests/_data/encrypt.key new file mode 100644 index 0000000..b7d1e91 --- /dev/null +++ b/Tests/_data/encrypt.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAsZzE9UxploQaeCuiCs7YqZSlbHaGL4vCbH5U6QR4sP7Swd4p +MrSLUgA7qm/2hF/7qYKbz0DSVmtBn+3/cjkNAnpIU7DbT/qO1APS7Rq3CJTNbMwa +GlYY6QYAQMj+Tl3Cu6TkkZlIKfrw/yPU0XUXc8MKCc729+YKVhjWSd1hdJPc+Cqo +3SQ6UO0AXBMGV1a/M+VZ0N4JfKJWNAMjfQ4egedrnoxndXwR3+b/oQEfuQNAADjr +EwmuJEl1Niml/XzXdM/6CvhqMH2LX63AglS2gOq5hBgE0PZnhm+eFQRVWMWqzF9G +WPtAt6/GvYfHSfasYrDFpLCi1vJhPe/wnJTR5QIDAQABAoIBADhp4uVG8AKu0vl4 +Ym+sY4T5gdGBk/1mFsr/FVkt4mfViHurZMqGLfpNuKXaCiLhmb2tjm+11xk72AxE +O+670DYJQQ/UDNTKcLNGw6gr5BcFrHnyGhhjYGYjUdFCBgQ+I6wWI8NbPGCZJBLl +/qLI3joWqQmUgz0aBA50tRuhBWNRS9lNDfoPpibzFNjkxMzb3X3KdbsTfpH7Ocj/ +bOBuS1mnsm/xh30RzN0w/2yIzpxX4XvGy5eftMLWZY22NkbnDgGbGHDvNR3mjKkZ +8QF4Urx86VnfTnA6f0m/QS3YXEWk7RxUEGjzwIRv+6FcY+mFEsehWnly2KP3TG3S +65Z2SgECgYEA6Fe2NdjRbzBCukGa4/fZyrCggQq6Pr448QcyQEenf/X9CulroPtH +OiiIDuU6mCOBQVHp1FiCtZQT9hTfMszrhy7AMtJQncmQkMcbslEd8JgvDj4Jw64u +HcnKupNxfVew+at2u+GA4w88ntXxrcNl8Mde7aPnytAyUWPGsbKcCpkCgYEAw7Jy +yR2KFW0YhIePL1cEzA5J62Yy0yM8MJXHXshy3v1qU9LKsdVk7fbB9UojEnEGcu2R +T2sW2wQqxIo442KUmStisTtQ8pyAOyQyfzIVRSlHTh4BKlDp8SMuGdnOibavttHK +q/RgeOiXXG0Yfpf3sKDHSmQv7TGlsI07O6Z0vS0CgYEAgbc+jk+PlgEer/girrXY +jTYRVhoUIyV2ivKWlpaqqGFAtg/dvBGuEYVBePd3wCrKZhqCbsA/sXqLrm62shkA +QgfS3EzZH07CfGH9T4/EJGgClXQDZZFgQ9c+bO4WhYEo2CtnbbuXhq0iDheqB3Y4 ++rWEhS5mIbAc952598mdHrkCgYARF5jm7+mLjYfCq4RaAiOtHuJd6QMvZbhwFeTf +5moCB+gtgg+qEJVMI201W1BM4ApMJ2u1oAjTAD4sBFaLpaSM7DkmeaPMTNb2U2cF +rP4mmEBeFkjLxV1pbkUshNWBOa+HLDOjaSiz5ryxmeW1yNgdWS2O1clJ0jhCf1NZ +FmTD0QKBgGYjCX6vZl+aEchm3Ie18Vp8cNUu9CYAiDiDzEwgxgiTfRCIJywWjv5v +ll4lqtMgsrDrmI8fBFq4BKytMFvgPqW0sI7U4Fu1vArFeyTwCgfR8VMO7L+qvbWE +MKKKTeO8aTjTiNJ3b/eIkFDAKU+yQOhVR3VqbduqEeVtxRgzMOIh +-----END RSA PRIVATE KEY----- diff --git a/Tests/_data/encrypt2.crt b/Tests/_data/encrypt2.crt new file mode 100644 index 0000000..d3ba774 --- /dev/null +++ b/Tests/_data/encrypt2.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDFzCCAf8CCQDFHjZEW7DHmzANBgkqhkiG9w0BAQUFADBMMRcwFQYDVQQDDA5T +eW1mb255TWltZSBDQTEUMBIGA1UECgwLU3ltZm9ueU1pbWUxDjAMBgNVBAcMBVBh +cmlzMQswCQYDVQQGEwJGUjAeFw0xOTA0MTkxNDIwMjFaFw0yMzA0MTgxNDIwMjFa +ME8xGjAYBgNVBAMMEVN5bWZvbnlNaW1lLVVzZXIyMRQwEgYDVQQKDAtTeW1mb255 +TWltZTEOMAwGA1UEBwwFUGFyaXMxCzAJBgNVBAYTAkZSMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEAvGX70bC9IIjPKIGN3FKR3wNHD5UdXhgEWpMDuQeA +gZ01LRc+tTactRMsuI3lTXCGOmU+kXpT03GcUEB4sP4ykSw04umDn8UbZ0o9WfzW +8c2Es3iYY/sDr4f7KUMaGqrARZrA9mJM4jvT49lOVWoiyzZ2Jgx2gDtFyCEW1b+0 +Hqnx4zjhBlCfe6XLpGgEtMwZ9tcmV96BBmlNVNHJbjiSqrsE97FTxxXzQgAmYDRc +qVAZicNcoNlDo/nV9A0n5ygA2Mgx6LF0HUAjf9YRXvRQ4BARtDJV9q/dzu5zxolS +mZOWxdlaCkTbeITGmRJNRl6BJiQu5kFRmzTO/Egt6bd5DwIDAQABMA0GCSqGSIb3 +DQEBBQUAA4IBAQAO6gTF27+s2CaCFE9VOHsqr/+9Rj3jYXefPD1NR4VU7fARXOGA +dgXW4PhNs2yfgBG2YJwK0uHRsLLwosh6KXZeyBm5XGT8QnzGVj/pZFJKuY0iIK9y +v4liJkLRKfUNPNEW214c3wcgd7chSOM6eV8rJFtnNyju4LnfnnNGFT2w48rccAyU +ZsL3BsQ40b/RUqBB12rNoKRyzmLVhdkTU/gTPYAVz9VQqtGXmYrqYQNuyenOYWV9 +ttQHUD7jszGNtyjNKMmo422QMZzTx38YJ+aR5PfW/arkW3RJPpSn5ClbnH1TSmCd +oFHODRxroV7eu+L2fQMmHtcbXKCTWg7lfvgW +-----END CERTIFICATE----- diff --git a/Tests/_data/encrypt2.key b/Tests/_data/encrypt2.key new file mode 100644 index 0000000..2f58e19 --- /dev/null +++ b/Tests/_data/encrypt2.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAvGX70bC9IIjPKIGN3FKR3wNHD5UdXhgEWpMDuQeAgZ01LRc+ +tTactRMsuI3lTXCGOmU+kXpT03GcUEB4sP4ykSw04umDn8UbZ0o9WfzW8c2Es3iY +Y/sDr4f7KUMaGqrARZrA9mJM4jvT49lOVWoiyzZ2Jgx2gDtFyCEW1b+0Hqnx4zjh +BlCfe6XLpGgEtMwZ9tcmV96BBmlNVNHJbjiSqrsE97FTxxXzQgAmYDRcqVAZicNc +oNlDo/nV9A0n5ygA2Mgx6LF0HUAjf9YRXvRQ4BARtDJV9q/dzu5zxolSmZOWxdla +CkTbeITGmRJNRl6BJiQu5kFRmzTO/Egt6bd5DwIDAQABAoIBACtOojFUmFUXPc+I +4GxKCsAiB768P1D24mFTtCJfaBnjYmroEgEj+afiLYCLFa/UcvaPeW+FmCldz1nf +SB8ff85BRDL5DMm4TJFUzn+WEG7rGFsNGLK66+D4uDKG+0QwBhy58ytv8056BD43 +ILuftznRXh1m9gKKHYNgn9goxiXZ816WYgtNDyYw8kgb9v79VKQRqePW60ZcgquD +THmJWz3eO1Ewurbr4PfrhDtCUcHzz0GRDY0QATCUrAfyl4YbzHrsjnxy4fRq4dXb +01eYZjoD4wchbWOrIMV72urF/KGWYljwOQjwgRgY9q54VrM/Rgga6jj4gWNXLPwn +LN1+/AECgYEA9mnNjNMxAn5sOJkhN430DAv/MSheu0yrNsY3iMjrLJcoVpTxvmos +NxpjWETZFA0T4Yae1VtQjn3LGo1PWJ7j7bCyYdwxuVMHYjCTB107xYg977pMBa37 +6yoN9aZ97p/FeFrOmIoRCO1Xlyu32nhMVBtVCw8TRCks0VuE1gJ84gECgYEAw7pe +7iUaPFzPHzxQc53BcWNEnKSvsNYbiEHLad+kbH76VeVWyf8M6ZSdVE38h7wawYfN +UXPmB2+7ESvvWnXV8zKJCRPMt9ytzH7UxJVCPepvTO8rWPLldUJS3a7sbB9pFFGc +WkPvnKGsnqf6mtj7IO/O4SluR7M9qosg0lxQOw8CgYBzOeqKrb8/QUrt9H1Z8yFp ++LoujIgv4Zw2kt4pMnr2cQDF7ARXXGKsqcRG5Hr2K19emIrxji/PUfeFxQqTkElZ +PsVLiaIe3TqYqco3KVvn9NuxnFYsWb1xrEq20lIVIdU/gIcXQYjRudq5sBHbMWHP ++q/76eLCftacV8V4JdWsAQKBgHYOsjfetUZ3jI8AqF40Z3vnLnl1dGurmYvEc9d2 +iAzRQloRLRpF9xnlBEjXiVyt/02Ahj19NOCDakhfQc5EiTpZ3wJUqQS13TcdwWSZ +ywzhnSTAllrel7z0tlr0qbJF9/HDkBV6KMtHUYGZPLWt7zvcqeJyRQyGdsmphbCc +8d/NAoGBAJ1ahF7h9ezHs60EJ3AxDoA3SHfv9DM5lyz4Kahr7gxBeENkdD6vP+JC +E5LVlen6SgdNg2LDY6rcYm8uHT8Agf234FcTiq4IbiKuRu/QbdPXjN/LrXPFmjgw +hPaPjrm2jSDBdaHlO/wFUq2Oo8hSlWrqHiUCTLg/TfkGegYL+RUc +-----END RSA PRIVATE KEY----- diff --git a/Tests/_data/intermediate.crt b/Tests/_data/intermediate.crt new file mode 100644 index 0000000..cf9c422 --- /dev/null +++ b/Tests/_data/intermediate.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDFjCCAf4CAQEwDQYJKoZIhvcNAQEFBQAwTDEXMBUGA1UEAwwOU3ltZm9ueU1p +bWUgQ0ExFDASBgNVBAoMC1N5bWZvbnlNaW1lMQ4wDAYDVQQHDAVQYXJpczELMAkG +A1UEBhMCRlIwHhcNMTkwNDE5MTQyMDEzWhcNMjMwNDE4MTQyMDEzWjBWMSEwHwYD +VQQDDBhTeW1mb255TWltZSBJbnRlcm1lZGlhdGUxFDASBgNVBAoMC1N5bWZvbnlN +aW1lMQ4wDAYDVQQHDAVQYXJpczELMAkGA1UEBhMCRlIwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDMvRxMxQyecc1bTVZeRCSjBTEmHFZQ2Taqmk5UwO2T +UGsk4nJRnpFHKqSQJgUX8Lj/Y1sjEM+IWrzVAhCPvsAc5x5mU2smylRKzkTCBUkH +UBrbqRBAHVeWu0W58E6D6zo6kweGD7fX+gtMeJY/pcib9tlGPUaVST1TaXYbZD8f +dWD3cN+32FAUyH+TgCtOwYAcwpQ+Npe8X1P/JHIixZz9Eb2WvtiyYqnEDLhKdS9b +zrUZsknkxUguMdd3n3kOO8scd6PTI8k699hOewtDkR7LPDelxhhIazo3kClQV24f +Dd6ktX8L/sGCG7+YTTRpKB47fdEVtiZjLlyZ0K8ih8BZAgMBAAEwDQYJKoZIhvcN +AQEFBQADggEBAIn7oIEeFGCeAUto5PHv3/hHTqLMZZI+VgSxC7zCKBkH59S+ua/s +8HUPRVbBk8qtApz0kL+p4LeUr0mQIQUXSKeyvp6jplMnrgZ1NXck1D9x14oBesiS +q8aVEfwH2DsyJi/0UE4boIeSlk9I0Jh1JSN2jX+zSF0RYYPrTOJKqBfu3QgLgt9s +PbsgOAcHhmWdwDRdFyu/Ok0pieqcHM3TMOV1DPU1aXKtzkCMOHHWfR2bXnIuw1aT +7koX52/3nq9xQ/17ly7iiZAgTWXC9mlnbgO/izWb2WdXHoLkFPrl8IPi3Enf+lo5 +xbpVMU82bgYtgM/Sm2RYV0vUZ9kp50SYy4M= +-----END CERTIFICATE----- diff --git a/Tests/_data/intermediate.key b/Tests/_data/intermediate.key new file mode 100644 index 0000000..b622a6b --- /dev/null +++ b/Tests/_data/intermediate.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAzL0cTMUMnnHNW01WXkQkowUxJhxWUNk2qppOVMDtk1BrJOJy +UZ6RRyqkkCYFF/C4/2NbIxDPiFq81QIQj77AHOceZlNrJspUSs5EwgVJB1Aa26kQ +QB1XlrtFufBOg+s6OpMHhg+31/oLTHiWP6XIm/bZRj1GlUk9U2l2G2Q/H3Vg93Df +t9hQFMh/k4ArTsGAHMKUPjaXvF9T/yRyIsWc/RG9lr7YsmKpxAy4SnUvW861GbJJ +5MVILjHXd595DjvLHHej0yPJOvfYTnsLQ5Eeyzw3pcYYSGs6N5ApUFduHw3epLV/ +C/7Bghu/mE00aSgeO33RFbYmYy5cmdCvIofAWQIDAQABAoIBAQDMIfWYeZOWWsM0 +uExX2rtoquGRLQnGvHwr54QYLu/xRGo/sWPoCyCwg0zmyHGlqAbb4/VXZgh13HqQ +KunWWIr1hl6iCaQ5XdxjZXvasyhYGT9eKhegxWCyUfA4bufp0dwR0MzclslnltAz +I7wyo5n8H0gNJ0U7zXVOuEThFLd3JWg2oJLfnjgWYi1sq0sA3VTKX2L4iwY+vnlN +d/i4Q09jorR38YZzEFjirc6YgOYHmEa8rx5oAZxEViRyAS/yvPTfvxB6HwxYbF4e +EB95VHNft490diDm0RHO8vaw4G8jpP7r8ObXiqwAoKxb8+AHNy765DmbyXp89eFU +SSRNYgQxAoGBAPjTYiq3jU1ykyyhci/y+T1ix+Jhjy/5WKbogKYalEDFbErEChM7 +zMjQNo92Vl4CgN+CpcY/SBVIZozbsT2nJWCN8FaQnZjNK4BlndN30bCp+Mu3sBDl +jZaOdm4Svif9kXPooG7wTcxadvGb+pRNy8zmZCWej1y9/13/FQdh+L1dAoGBANKk +UJ0wJf8F4jR5PC2Db9JlluVHfPP39d1eGGS/FzfbSbmRZImU2eZI1ItWE0whYF06 +WMULqzdRLdcft/SHux8d832ZyHLqc5t4Xip5QE+XEk26S2ZCcjoOd2Ez4NIN5YNm +veac9udX7oiVX8cKn3zGxyrEHLjIB+XW5Yq+KeMtAoGAEQkv9GrCwuWwS+L11XCW +PeywcMBrNEanGi5a+IRjWBfsNSY85lo2yBzxT1szyJX1SthAD1Wv0r01QDmeZfE2 +ruio5tRZ5edOLilG5/6RHb5VaWU3KcD9s6wnUZv45vYGamAn89CCExayhBJA0ryM +0oeHncfAWwIrJL1dLDc594UCgYEAuSahjWlzHJUJXmJqWP89XUzatDKATNpaDPjW +rEejmv9v8GMyYhSq69Z8rPU+BR8ZWxkcSieVmgwLJRrGUXS1MAbdrjtsjEY01CWb +b+4gb1U1S4lDGWGykgGBQbmeFkUMxtGafojeJj+OdhQGmihmRAFds+Op82owNwEL +x0ab/wkCgYEAuCJQiyRzO/X/jq6CdaClDhCiMW6z5JZced6VZS+6s7aA5vmhcl8f +TAZp7BeHqGscPHfpzY6P7lZmqQL2OWURFFjgJYbRWZtXOYGH8ZhBsGA3uAIwyses +2XxUbSYZ5jNvp7r3GwqIsFFth4QRtGEBMdUgCS2mZkRsW6A5NXmeiKA= +-----END RSA PRIVATE KEY----- diff --git a/Tests/_data/sign.crt b/Tests/_data/sign.crt new file mode 100644 index 0000000..3cdb0cd --- /dev/null +++ b/Tests/_data/sign.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDOzCCAiMCCQDFHjZEW7DHnjANBgkqhkiG9w0BAQUFADBMMRcwFQYDVQQDDA5T +eW1mb255TWltZSBDQTEUMBIGA1UECgwLU3ltZm9ueU1pbWUxDjAMBgNVBAcMBVBh +cmlzMQswCQYDVQQGEwJGUjAeFw0xOTA1MTIxMjA0MzdaFw0yMzA1MTExMjA0Mzda +MHMxGzAZBgNVBAMMEmZhYmllbkBzeW1mb255LmNvbTEUMBIGA1UECgwLU3ltZm9u +eU1pbWUxDjAMBgNVBAcMBVBhcmlzMQswCQYDVQQGEwJGUjEhMB8GCSqGSIb3DQEJ +ARYSZmFiaWVuQHN5bWZvbnkuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAqCDgfKFvJ1NdE0MmSjiVA6Z8ydmZZBsfAE57q9+2bjepW5qwVmL/Igvz +hjjeWFiFIDuLhKkBFZmDR22pkGm5yZRQY8DDXB3Oz0qXr3tVwY4/Iiq+AQhSQfPw +xA11ahRkU14U1CfPc+XdN+Bfv+iwcX8itlz/auHF5BnwFMWcE0A6UC9/70owayia +rVMEWKuYxHrG89t6p3CgKxBG4gF7uxZhy80qVfJWG5ZcCH57xwD/hgQ0We23H89M +G4cpYDX8FZfjzeaVEikOJ9/RK3P6pb5EHtfsO42s2G+j6MnrVTTIA9g326VLW3Vf +3xIrWpGQbwwvm9wiARhEUV+o7QmdXwIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQB+ +mQRLFCkuKfq+pPr9a5p5HfxBbVrNveAOEGRsC83aD4vtWT4X2NoE1MkW2n/AgXtv +AmF/duynnRurKGH8k0Fkw5fMEE+GChwwmJk6UxIV5oO6khk05QAua+5dI2c6rf0z +xanXQksuQDjynnUKNbwyMGAUBcWPlpBeyUKThNWGyRgVuM/7nihI77Rqm2WGHRac +RcCoosNXG0othSWzz0hsxuqsPneO0hGAf3UZI/b+gJk9/SJelUvIRStHBoQRB7YZ +y7kXDwcQZS/IkIGDyWxV1KIpZ0Ban5+0awEG1ShUyepy1dV/24frwu3VOgRN4jv4 +2CGR71B5H0zIRjazNERL +-----END CERTIFICATE----- diff --git a/Tests/_data/sign.key b/Tests/_data/sign.key new file mode 100644 index 0000000..68d1d57 --- /dev/null +++ b/Tests/_data/sign.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAqCDgfKFvJ1NdE0MmSjiVA6Z8ydmZZBsfAE57q9+2bjepW5qw +VmL/IgvzhjjeWFiFIDuLhKkBFZmDR22pkGm5yZRQY8DDXB3Oz0qXr3tVwY4/Iiq+ +AQhSQfPwxA11ahRkU14U1CfPc+XdN+Bfv+iwcX8itlz/auHF5BnwFMWcE0A6UC9/ +70owayiarVMEWKuYxHrG89t6p3CgKxBG4gF7uxZhy80qVfJWG5ZcCH57xwD/hgQ0 +We23H89MG4cpYDX8FZfjzeaVEikOJ9/RK3P6pb5EHtfsO42s2G+j6MnrVTTIA9g3 +26VLW3Vf3xIrWpGQbwwvm9wiARhEUV+o7QmdXwIDAQABAoIBAQCD/gOfdLGqAwVw +SOh3noJGcl9HrKCC+dPVzsfCwIgdcW9xLjlAKMo59X4DIwRUAXLKQlUfGfty9Kke +25YifQ5RljGijsQQvooNLXd2WfKSWVVxQnMWpmzFwHiFwjcqx8WXuaXKhVKVn6GT +63/gTxKul+wtlUckpwlQMZjNBfKpHR56GWTpvvMiEhssGvt601rRj9Dk+f/nWTJo +Qp2Ka2O616/2jF/BuDj7nsK5ePvDotHf4dlRsLLHk8hoqpamByQsepLltvGu4p7U +bAYGOkYJyPtBuLyoYxOBtJrrewUsxo7jEuTfO9j7JHaYqG99QDEWaGlqXGR3481x +emFDVDnBAoGBANaq6mYCkNb84EaR0OzVdj89b5wcECds3UNvGy/h8fx5X9s1Xal2 +EVG976nY4r2wEGKnwCb0qhh94BUmFoxF0iPe3dwlLz9H2ymmWu1v36CMKc92f0UM +3TzeaWrBHdk72PZE8nBTuKkQabboH9LHf2EUrnjNwoY0p9a4uZbLPNi/AoGBAMiA +BKg59MI6GN/9mp+n9D7jdl6ZqhLfYJFGPADRnRPIS89TKywACVNwYCpC2wBRHJ6W +F05BQW2U5ohF2w4n4UODwBadQhP1dg/l40ZO5+BL98Dx8g3bwItCTe25bYtAS0wI +dULj/AR4zdMQCdrh2zzWwPfoNT+6gXfI8V40nsNhAoGBAIvFBw9aVlIUnjZ0lLLP +nck5SCU9xGrXIA3bFrmLhNKdeIMy8QP4Yvh1Ecnl9GQLce+6R4tVvDZsJu2+Oeol +P9ipMI05DNVIBPPOY9+6+sD+4e45uk4MPTR3n+2pRbT+mZpnc+8dI9u4WwyDgMzt +pgtguuTfG+vj9vAAoJ4FQF3jAoGAbmSuK8HdVaOPVqTXodhjzsyGvAd3cPS0wsgc ++YZwKhg6RWjReGR8vgg9qocs9buzOk4BfwDG+YLme1mbBuxGR1ofRVRIsZyQ6Kf2 +vxtq6EBrpTyRvbelCAf1yFI0UluQGcj+Z1oHxJ6PFQrbojyA7bqAfP7JctFJv55P +50Kpt4ECgYEAqjYa3J1YJsK+xTMEG1mAzPZcGG09/Od5Zc7XRY8SkaKwW6eRRu7G +Eq0RuXLW5wxM32sIJyhNqTSchWcntqV/cwvLDmI+JFg3gJ9fNzEdRyHGccBDe7ad +bdR/9n23NVBfaLd6lLszFUQW8efmbzI0HQO1USKRTsFm2TkkcHlafts= +-----END RSA PRIVATE KEY----- diff --git a/Tests/_data/sign2.crt b/Tests/_data/sign2.crt new file mode 100644 index 0000000..c107dfd --- /dev/null +++ b/Tests/_data/sign2.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDGTCCAgECAQEwDQYJKoZIhvcNAQEFBQAwVjEhMB8GA1UEAwwYU3ltZm9ueU1p +bWUgSW50ZXJtZWRpYXRlMRQwEgYDVQQKDAtTeW1mb255TWltZTEOMAwGA1UEBwwF +UGFyaXMxCzAJBgNVBAYTAkZSMB4XDTE5MDQxOTE0MjAxNloXDTIzMDQxODE0MjAx +NlowTzEaMBgGA1UEAwwRU3ltZm9ueU1pbWUtVXNlcjIxFDASBgNVBAoMC1N5bWZv +bnlNaW1lMQ4wDAYDVQQHDAVQYXJpczELMAkGA1UEBhMCRlIwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCuXCpZPt3ikKzhKePg4ra9ynLMyaRZI1PLHJ9G +01XjAJhz1w1yRk7N+AhtEHQK86UwAQLHTOt6XZU62Ifh2yWWkuhDCnO7leQFtJnr +GAzuvXwUfK/Fa+gqmhf0HU5QAnSMmH7w3ViprT2YyoP9aa4G5sD8/DoHBejV4oCE +QFevUuaeKov9rWo81pkREBM8CVkghFIdbbj/gegAmmK2SApkvATx7JCWh3oPtSJ8 +CCuPwLtE9aDfdT7LyuI8x+O8MHVeFB3LvBTOlzPPs43/N8RU7WX1/VTpREIyWC7J +I2bF8V6qLdYknYWm8VMlBlCWj73SuZreUWYesxUFmLrRgLeZAgMBAAEwDQYJKoZI +hvcNAQEFBQADggEBAFQlQzsZfdZ8Z5uZVRM2JG7Ga70cBMd/wS9J/We1ECujgGJD ++smJCONNHmobZswy3EoMaHlUDvUA35gTvEkA+XMXItEfJLPY75j9zRdOZWYI0Y+G +XWt4Bhrh7Dswtci8NUs8TPqJlmLMYJFFEbnxdZr+o2/KIkdVoCjpXM7fa4GLBnD3 +aM59/yclNFCghxGhCYF+nEOoIIet35lxsTC3Pmo/5nDI9fOgjt6yYeiWOM7eHIOJ +G37mWWFODhLnzlA6uRPCjkMzRZnJYiSx7/kJkxqsPJVzIH3vCgHzRnt7JYoKCxqE +nvM0FdQ9+HG4VKggElSdVbKAgt8XjGHeSmVPd+M= +-----END CERTIFICATE----- diff --git a/Tests/_data/sign2.key b/Tests/_data/sign2.key new file mode 100644 index 0000000..ae4ffcb --- /dev/null +++ b/Tests/_data/sign2.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEArlwqWT7d4pCs4Snj4OK2vcpyzMmkWSNTyxyfRtNV4wCYc9cN +ckZOzfgIbRB0CvOlMAECx0zrel2VOtiH4dsllpLoQwpzu5XkBbSZ6xgM7r18FHyv +xWvoKpoX9B1OUAJ0jJh+8N1Yqa09mMqD/WmuBubA/Pw6BwXo1eKAhEBXr1LmniqL +/a1qPNaZERATPAlZIIRSHW24/4HoAJpitkgKZLwE8eyQlod6D7UifAgrj8C7RPWg +33U+y8riPMfjvDB1XhQdy7wUzpczz7ON/zfEVO1l9f1U6URCMlguySNmxfFeqi3W +JJ2FpvFTJQZQlo+90rma3lFmHrMVBZi60YC3mQIDAQABAoIBAGL4/Cz2q5rdBtU1 +Ix5XcuXe0jV+zGSw0fK8h4j7k4gsoV04GHDiif8OqTHHoidJUF4kZMBe4FfwYTIr +EU7aR8bmEyNi/njfx7SZZLl3SHgIZTN354qICxyLpcczD24JRsE8Gup8qsR+CzX8 +1tl1MIzIVYoFXqb36sfmL49iuqNQ2qyrH+OnDKHhBYPrHt8FuhZ1XYyRhtqgjJAD +YMYE6+vA4gwN4Ajk/a9XE05awQTUiLcaXtMxVGB1fejK3xnN/HGOFR8xH5Qi0aSf +vMaJ7rh4fwYjRPQTBVPnm8HA7CVf0dyoCBjK63OJvd+RVj3w4t4/3BeV9VsDVRiT +PFEoJwECgYEA1RZcMkz+Os67e3Ot9NOFReup2PmFhh6PvrqJd0oAlTc/+f6fUI6G +INJNhuCCVg96cw76gfvdN4a3EPgyV708Y16m4EhHpu7jMtGprnbX3Y5H8NjGs62n +ziKw6sCa25xdXFV2c0M0uQVUJAk7sWxpDU3OWkxpmMRPdhbawEdKVyECgYEA0Xk7 +exS2+SRkrveMM2jj6dVZLQ00Tj0WQOovKloUXp9K8ACzXVdgcMeBSQhzQFN1SwAZ +EdEvR9c6dizRHTnfXK+sSMURjMw0e9Fca9vs43oYOW0bIzAzpRJrkh2ZlwDlDuZ0 +ST25nUpCKn7s6eq3XIIs0vNuJNaW2KKIoHqBaXkCgYAHdxkTyg6+ELAQyyS1BxQM +Nw1kRJmg8UEn9XELdNRAZgcfwwPh1pxsWfHNX+AxE6m+ji/IjgJaB6YyOf/Jgx+y +e4ZtJRsdhhD/nsjLC+7UHD/4+B89/D98wUphbw3906SRr4zOzPPz53PjL0+gD6Q+ +ixNHppWsfHQsNvDC+7xnAQKBgHWMi7V5HWjYZGvPXOzomqV45S8j7stM+nT5NfiV +TkL/LwVZz029H9CKFGIQjOR3MSYiau8VrWuqOxNf+QVmmZKgvpSjikKxwW4OQcgB +RYEt3fQz5vurLAAhQx5e3/beOKxQ5MbJDaVXq6O/UGHAJp+SKWdD1fZ0OXheVT+B +H6g5AoGAZBjDmmAca4SMT5nC4Ueh6PlYt0Yr4pMTdkrYTkGxzak6VnRlGg8rmiCP +LSO9vaCoriMSouhVKdBdTjD6lwYxxt4O2HY8D3RgqvU8T7uiI58l2XVdCbs0cBGE +IlNC0CjIiHPWuVhCQk5eLk4WLDvbLq6Oc+8J4BOFupvq3KrGKik= +-----END RSA PRIVATE KEY----- diff --git a/Tests/_data/sign3.crt b/Tests/_data/sign3.crt new file mode 100644 index 0000000..3f907cf --- /dev/null +++ b/Tests/_data/sign3.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDFzCCAf8CCQDFHjZEW7DHnDANBgkqhkiG9w0BAQUFADBMMRcwFQYDVQQDDA5T +eW1mb255TWltZSBDQTEUMBIGA1UECgwLU3ltZm9ueU1pbWUxDjAMBgNVBAcMBVBh +cmlzMQswCQYDVQQGEwJGUjAeFw0xOTA0MjcwOTQ2MDRaFw0yMzA0MjYwOTQ2MDRa +ME8xGjAYBgNVBAMMEVN5bWZvbnlNaW1lLVVzZXIzMRQwEgYDVQQKDAtTeW1mb255 +TWltZTEOMAwGA1UEBwwFUGFyaXMxCzAJBgNVBAYTAkZSMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEAyywWINcIgcxT8GUXTx/5Xa7rkwoMh3gwNcuOhbRg +08xupcsHAe3f6pPosqm5xweaPcw2xI+yrkXxmXEE0LU4qi7qgtBZt2GqYKcFEtZw +fh0YV4b2NHSO/CFTY25AtwW9wUW+GHq6yYRqrjVZnP9mEokNclPNS4QWSyaNuIav +jCB9xdDXjIc24zM4TeM5LaaFoH4qZGhp3MkjRadnMrrYsp9a3XKrYlQ5lqaXyVlC +/aKeBZy5qckoFj0Lep6LV2xlipX4+d5tcZ2FATkXjJK834WRzFMAVhYxqFyj/RIY +IoEk/AdYeg3b1HK19flNNQbpHPayiSg3l90ecbaN5vqgLQIDAQABMA0GCSqGSIb3 +DQEBBQUAA4IBAQBd+sTqEPXn5MLXMNRsV9HuP4VkX79K7ThA4kXW0fj7Bp+Mpfvg +LLUXRVcyzAKz2RgxyNIKHPr3u6OxHXbtGL5IgdH74uCR4MN+srKpLiGAMNjtvWBr +sGG3pIfpw4sFfVkj5zLFH9MLVSkKFu7Ub2KfOh310AnSnMOJpjy8a0MqY+iOcpj7 +ioOdPHaSQX7DZrECKozOzcfqryYBOwkbwrh1juhDYzy3WtgxZe1FRl3O3FKLtmAc +J4At8HbMDOBH/fMR4o4B1miP6K9QWc66hsAsgY3GWrmiBf67sf1u6kNeNPBGcviW +fQndtjrUUmwkAAQaFYjUnVEcfx55p8TrLhRC +-----END CERTIFICATE----- diff --git a/Tests/_data/sign3.key b/Tests/_data/sign3.key new file mode 100644 index 0000000..b8b51bd --- /dev/null +++ b/Tests/_data/sign3.key @@ -0,0 +1,30 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-256-CBC,5485FF78699F76CB93A05FF9CE7FB4FD + +iaLWaPXmnAweLnqtXLk/NwQOUNTRIV1yN9gHo0aR4fWy3GDPa08AJz+pYuDULQDY +RCejCBHeSqH4UFbbRHkYHsOdpjEUWhOy07TesajkW3Kl3At1T7aa3cCjhYSpeWkc +4wqzbs5byRd/mzp4G17WjJnohBXBdxCea5WzgoGL4ZeX7nqgxwpnh51VvuOBk+vY +W6QHwpaq+lFa/G7lcoWJxmtQd3/O8apjZB9xT65II7ZeMsIP9NRLitoH7IX+GteU +MdVgTMWVyxhEko5SXlqpZUJJ/C7CReHipI8hkfub0zsQj4FkZ/jJa3X18MQFNQZE +YhhJOhGsv3U65J3PRkxEYeK9fIO8V4e+YeaJb3iItmgh4zfdfw0BieC08xYhuA5f +uEzkxqrk9nWaFHtam5/skmFcE5uH3raMLTHDYkFXxqMRQamddlONuZ4Z+oAdr+VM +zGf4oI7Dsie+PEzvbQNfphrI89TOkH8IhT6HATFKvyLG21lKgBMwFOdJg5B4zcuJ +bh/8cqDrP5byxjDe96fOBWoH2g72f4T9SrOmpgdhEL9AOiLuBnVMQDVLthMY1xxg +SuYxrmtPKn3eaeWHpv8RJSToFF4fl2zJm/HKEAd8ghAJa330/CnNCCQSqUrdE0xp +xjVURJvPZxzMS9UEQhXPyYp66GYs/uLuSTjaQwymFgc/l2UMm3zjHhjyn6S382u7 +ih+AM7y8+pIV2n8CbqoG4XR8UEr9w/ayeDSbFKGZhiqhACW8iZATgVOhmeoH7ON3 +ib7FTNHsyr2eEZ97JeKpbG9E3sFEfHJ51d0PScKmzENGP9LNjMzWb3uot0zP0q0s +kzulVIHnkh48gRICPIKeOL894gAL4PmRViP0x7chXk/xULueVb7T8WmCLYI+Kjaz +X/yGCpPmrBFNAiamrHMSh4qUE+zm7OI2jNOu87tRxcXid8O9wQZDZKaE19HZJ84j +bGghr9pKhIXdn3Z4hSyBqkBnUXNPc1g1er4DryluZ/+wKUvDkZpVmygYvQM1598b +p/xkRxlGE3BZiLX1++wIjMJ2xmwLKGiJOR0BexXJWYz0pD22YUSto7lwmGLm3oxg +MYIFqEgaYFc0MsZ7PRiMcovTUr/c2yHMpyveLHSLMFVmxfGKGZ+JcalCDFhkuZem +DcMQ0bs0DjmJZvC18SYH+iym1JXHnkVSUjsYWuVGGp7zArwlyP3W1MOhHcYDVcEl +TikNqrpgl04ti4qcdtH3TrPufMHq7Y9SM0dY1SUctyaO5yD52ozWriPuS1kTtkaL +GwvJhqvkPyO89bK/We0dIv5KoZPFhEWFwkNMNBIT1GI8XfQZTgi5ZJI3DSr1q022 +zvs5FruPwN2qFsvpmiakl4GhoIs8zwqqCDAO5JXhnSEZeEIB2Hsld7pRDAg3D8Me +T10ZOqM3XRzXPfi5zls81KhkrCh/RHiXEnMseT7aLYxvUHQM+Ktr9ar9Zv42BF+r +c5WFWDs58THPODiKYqPHpUBuIV2moTSsSWGzyvQF5TLw9/rtfrwCiBOiaqzO2odo +h7zLceAmxJfcQ6gIaAy8t5JgAUq5Uwkk0WK2Z3RyJAejdxghQI8XNxkmvu/pM5al +-----END RSA PRIVATE KEY----- diff --git a/composer.json b/composer.json index f1d2f09..4cf3c42 100644 --- a/composer.json +++ b/composer.json @@ -21,8 +21,11 @@ "symfony/polyfill-mbstring": "^1.0" }, "require-dev": { - "egulias/email-validator": "^2.0", - "symfony/dependency-injection": "~3.4|^4.1" + "egulias/email-validator": "^2.1.10", + "symfony/dependency-injection": "^3.4|^4.1|^5.0" + }, + "conflict": { + "symfony/mailer": "<4.4" }, "autoload": { "psr-4": { "Symfony\\Component\\Mime\\": "" }, @@ -33,7 +36,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "4.3-dev" + "dev-master": "4.4-dev" } } } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy