[Mime] added classes for generating MIME messages

This commit is contained in:
Fabien Potencier 2019-01-17 18:07:31 +01:00
parent 91c5b14d8b
commit ee787d17b4
100 changed files with 8201 additions and 17 deletions

View File

@ -31,6 +31,7 @@
"symfony/contracts": "^1.0.2",
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-intl-icu": "~1.0",
"symfony/polyfill-intl-idn": "^1.10",
"symfony/polyfill-mbstring": "~1.0",
"symfony/polyfill-php72": "~1.5",
"symfony/polyfill-php73": "^1.8"

View File

@ -0,0 +1,100 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Mime;
use League\HTMLToMarkdown\HtmlConverter;
use Twig\Environment;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @experimental in 4.3
*/
final class Renderer
{
private $twig;
private $context;
private $converter;
public function __construct(Environment $twig, array $context = [])
{
$this->twig = $twig;
$this->context = $context;
if (class_exists(HtmlConverter::class)) {
$this->converter = new HtmlConverter([
'hard_break' => true,
'strip_tags' => true,
'remove_nodes' => 'head style',
]);
}
}
public function render(TemplatedEmail $email): TemplatedEmail
{
$email = clone $email;
$vars = array_merge($this->context, $email->getContext(), [
'email' => new WrappedTemplatedEmail($this->twig, $email),
]);
if ($template = $email->getTemplate()) {
$this->renderFull($email, $template, $vars);
}
if ($template = $email->getTextTemplate()) {
$email->text($this->twig->render($template, $vars));
}
if ($template = $email->getHtmlTemplate()) {
$email->html($this->twig->render($template, $vars));
}
// if text body is empty, compute one from the HTML body
if (!$email->getTextBody() && null !== $html = $email->getHtmlBody()) {
$email->text($this->convertHtmlToText(\is_resource($html) ? stream_get_contents($html) : $html));
}
return $email;
}
private function renderFull(TemplatedEmail $email, string $template, array $vars): void
{
$template = $this->twig->load($template);
if ($template->hasBlock('subject', $vars)) {
$email->subject($template->renderBlock('subject', $vars));
}
if ($template->hasBlock('text', $vars)) {
$email->text($template->renderBlock('text', $vars));
}
if ($template->hasBlock('html', $vars)) {
$email->html($template->renderBlock('html', $vars));
}
if ($template->hasBlock('config', $vars)) {
// we discard the output as we're only interested
// in the side effect of calling email methods
$template->renderBlock('config', $vars);
}
}
private function convertHtmlToText(string $html): string
{
if (null !== $this->converter) {
return $this->converter->convert($html);
}
return strip_tags($html);
}
}

View File

@ -0,0 +1,87 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Mime;
use Symfony\Component\Mime\Email;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @experimental in 4.3
*/
class TemplatedEmail extends Email
{
private $template;
private $htmlTemplate;
private $textTemplate;
private $context = [];
/**
* @return $this
*/
public function template(?string $template)
{
$this->template = $template;
return $this;
}
/**
* @return $this
*/
public function textTemplate(?string $template)
{
$this->textTemplate = $template;
return $this;
}
/**
* @return $this
*/
public function htmlTemplate(?string $template)
{
$this->htmlTemplate = $template;
return $this;
}
public function getTemplate(): ?string
{
return $this->template;
}
public function getTextTemplate(): ?string
{
return $this->textTemplate;
}
public function getHtmlTemplate(): ?string
{
return $this->htmlTemplate;
}
/**
* @return $this
*/
public function context(array $context)
{
$this->context = $context;
return $this;
}
public function getContext(): array
{
return $this->context;
}
}

View File

@ -0,0 +1,199 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Mime;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\NamedAddress;
use Twig\Environment;
/**
* @internal
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @experimental in 4.3
*/
final class WrappedTemplatedEmail
{
private $twig;
private $message;
public function __construct(Environment $twig, TemplatedEmail $message)
{
$this->twig = $twig;
$this->message = $message;
}
public function toName(): string
{
$to = $this->message->getTo()[0];
return $to instanceof NamedAddress ? $to->getName() : '';
}
public function image(string $image, string $contentType = null): string
{
$file = $this->twig->getLoader()->getSourceContext($image);
if ($path = $file->getPath()) {
$this->message->embedFromPath($path, $image, $contentType);
} else {
$this->message->embed($file->getCode(), $image, $contentType);
}
return 'cid:'.$image;
}
public function attach(string $file, string $name = null, string $contentType = null): void
{
$file = $this->twig->getLoader()->getSourceContext($file);
if ($path = $file->getPath()) {
$this->message->attachFromPath($path, $name, $contentType);
} else {
$this->message->attach($file->getCode(), $name, $contentType);
}
}
/**
* @return $this
*/
public function setSubject(string $subject)
{
$this->message->subject($subject);
return $this;
}
public function getSubject(): ?string
{
return $this->message->getSubject();
}
/**
* @return $this
*/
public function setReturnPath(string $address)
{
$this->message->returnPath($address);
return $this;
}
public function getReturnPath(): string
{
return $this->message->getReturnPath();
}
/**
* @return $this
*/
public function addFrom(string $address, string $name = null)
{
$this->message->addFrom($name ? new NamedAddress($address, $name) : new Address($address));
return $this;
}
/**
* @return (Address|NamedAddress)[]
*/
public function getFrom(): array
{
return $this->message->getFrom();
}
/**
* @return $this
*/
public function addReplyTo(string $address)
{
$this->message->addReplyTo($address);
return $this;
}
/**
* @return Address[]
*/
public function getReplyTo(): array
{
return $this->message->getReplyTo();
}
/**
* @return $this
*/
public function addTo(string $address, string $name = null)
{
$this->message->addTo($name ? new NamedAddress($address, $name) : new Address($address));
return $this;
}
/**
* @return (Address|NamedAddress)[]
*/
public function getTo(): array
{
return $this->message->getTo();
}
/**
* @return $this
*/
public function addCc(string $address, string $name = null)
{
$this->message->addCc($name ? new NamedAddress($address, $name) : new Address($address));
return $this;
}
/**
* @return (Address|NamedAddress)[]
*/
public function getCc(): array
{
return $this->message->getCc();
}
/**
* @return $this
*/
public function addBcc(string $address, string $name = null)
{
$this->message->addBcc($name ? new NamedAddress($address, $name) : new Address($address));
return $this;
}
/**
* @return (Address|NamedAddress)[]
*/
public function getBcc(): array
{
return $this->message->getBcc();
}
/**
* @return $this
*/
public function setPriority(int $priority)
{
$this->message->setPriority($priority);
return $this;
}
public function getPriority(): int
{
return $this->message->getPriority();
}
}

View File

@ -0,0 +1,174 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Tests\Mime;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\Twig\Mime\Renderer;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\Mime\Part\Multipart\AlternativePart;
use Symfony\Component\Mime\Part\Multipart\MixedPart;
use Symfony\Component\Mime\Part\Multipart\RelatedPart;
use Symfony\Component\Mime\Part\TextPart;
use Twig\Environment;
use Twig\Loader\ArrayLoader;
class RendererTest extends TestCase
{
public function testRenderTextOnly(): void
{
$email = $this->prepareEmail(null, 'Text', null);
$this->assertEquals('Text', $email->getBody()->bodyToString());
}
public function testRenderHtmlOnly(): void
{
$email = $this->prepareEmail(null, null, '<b>HTML</b>');
$body = $email->getBody();
$this->assertInstanceOf(AlternativePart::class, $body);
$this->assertEquals('HTML', $body->getParts()[0]->bodyToString());
$this->assertEquals('<b>HTML</b>', $body->getParts()[1]->bodyToString());
}
public function testRenderHtmlOnlyWithTextSet(): void
{
$email = $this->prepareEmail(null, null, '<b>HTML</b>');
$email->text('Text');
$body = $email->getBody();
$this->assertInstanceOf(AlternativePart::class, $body);
$this->assertEquals('Text', $body->getParts()[0]->bodyToString());
$this->assertEquals('<b>HTML</b>', $body->getParts()[1]->bodyToString());
}
public function testRenderTextAndHtml(): void
{
$email = $this->prepareEmail(null, 'Text', '<b>HTML</b>');
$body = $email->getBody();
$this->assertInstanceOf(AlternativePart::class, $body);
$this->assertEquals('Text', $body->getParts()[0]->bodyToString());
$this->assertEquals('<b>HTML</b>', $body->getParts()[1]->bodyToString());
}
public function testRenderFullOnly(): void
{
$email = $this->prepareEmail(<<<EOF
{% block subject %}Subject{% endblock %}
{% block text %}Text{% endblock %}
{% block html %}<b>HTML</b>{% endblock %}
EOF
, null, null);
$body = $email->getBody();
$this->assertInstanceOf(AlternativePart::class, $body);
$this->assertEquals('Subject', $email->getSubject());
$this->assertEquals('Text', $body->getParts()[0]->bodyToString());
$this->assertEquals('<b>HTML</b>', $body->getParts()[1]->bodyToString());
}
public function testRenderFullOnlyWithTextOnly(): void
{
$email = $this->prepareEmail(<<<EOF
{% block text %}Text{% endblock %}
EOF
, null, null);
$body = $email->getBody();
$this->assertInstanceOf(TextPart::class, $body);
$this->assertEquals('', $email->getSubject());
$this->assertEquals('Text', $body->bodyToString());
}
public function testRenderFullOnlyWithHtmlOnly(): void
{
$email = $this->prepareEmail(<<<EOF
{% block html %}<b>HTML</b>{% endblock %}
EOF
, null, null);
$body = $email->getBody();
$this->assertInstanceOf(AlternativePart::class, $body);
$this->assertEquals('', $email->getSubject());
$this->assertEquals('HTML', $body->getParts()[0]->bodyToString());
$this->assertEquals('<b>HTML</b>', $body->getParts()[1]->bodyToString());
}
public function testRenderFullAndText(): void
{
$email = $this->prepareEmail(<<<EOF
{% block text %}Text full{% endblock %}
{% block html %}<b>HTML</b>{% endblock %}
EOF
, 'Text', null);
$body = $email->getBody();
$this->assertInstanceOf(AlternativePart::class, $body);
$this->assertEquals('Text', $body->getParts()[0]->bodyToString());
$this->assertEquals('<b>HTML</b>', $body->getParts()[1]->bodyToString());
}
public function testRenderFullAndHtml(): void
{
$email = $this->prepareEmail(<<<EOF
{% block text %}Text full{% endblock %}
{% block html %}<b>HTML</b>{% endblock %}
EOF
, null, '<i>HTML</i>');
$body = $email->getBody();
$this->assertInstanceOf(AlternativePart::class, $body);
$this->assertEquals('Text full', $body->getParts()[0]->bodyToString());
$this->assertEquals('<i>HTML</i>', $body->getParts()[1]->bodyToString());
}
public function testRenderHtmlWithEmbeddedImages(): void
{
$email = $this->prepareEmail(null, null, '<img src="{{ email.image("image.jpg") }}" />');
$body = $email->getBody();
$this->assertInstanceOf(RelatedPart::class, $body);
$this->assertInstanceOf(AlternativePart::class, $body->getParts()[0]);
$this->assertStringMatchesFormat('<img src=3D"cid:%s@symfony" />', $body->getParts()[0]->getParts()[1]->bodyToString());
$this->assertEquals('Some image data', base64_decode($body->getParts()[1]->bodyToString()));
}
public function testRenderFullWithAttachments(): void
{
$email = $this->prepareEmail(<<<EOF
{% block text %}Text{% endblock %}
{% block config %}
{% do email.attach('document.txt') %}
{% endblock %}
EOF
, null, null);
$body = $email->getBody();
$this->assertInstanceOf(MixedPart::class, $body);
$this->assertEquals('Text', $body->getParts()[0]->bodyToString());
$this->assertEquals('Some text document...', base64_decode($body->getParts()[1]->bodyToString()));
}
private function prepareEmail(?string $full, ?string $text, ?string $html): TemplatedEmail
{
$twig = new Environment(new ArrayLoader([
'full' => $full,
'text' => $text,
'html' => $html,
'document.txt' => 'Some text document...',
'image.jpg' => 'Some image data',
]));
$renderer = new Renderer($twig);
$email = (new TemplatedEmail())->to('fabien@symfony.com')->from('helene@symfony.com');
if (null !== $full) {
$email->template('full');
}
if (null !== $text) {
$email->textTemplate('text');
}
if (null !== $html) {
$email->htmlTemplate('html');
}
return $renderer->render($email);
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Symfony\Bridge\Twig\Tests\Mime;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
class TemplatedEmailTest extends TestCase
{
public function test()
{
$email = new TemplatedEmail();
$email->context($context = ['product' => 'Symfony']);
$this->assertEquals($context, $email->getContext());
$email->template($template = 'full');
$this->assertEquals($template, $email->getTemplate());
$email->textTemplate($template = 'text');
$this->assertEquals($template, $email->getTextTemplate());
$email->htmlTemplate($template = 'html');
$this->assertEquals($template, $email->getHtmlTemplate());
}
}

View File

@ -27,6 +27,7 @@
"symfony/form": "^4.3",
"symfony/http-foundation": "~3.4|~4.0",
"symfony/http-kernel": "~3.4|~4.0",
"symfony/mime": "~4.3",
"symfony/polyfill-intl-icu": "~1.0",
"symfony/routing": "~3.4|~4.0",
"symfony/templating": "~3.4|~4.0",

View File

@ -0,0 +1,98 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime;
use Egulias\EmailValidator\EmailValidator;
use Egulias\EmailValidator\Validation\RFCValidation;
use Symfony\Component\Mime\Encoder\IdnAddressEncoder;
use Symfony\Component\Mime\Exception\InvalidArgumentException;
use Symfony\Component\Mime\Exception\LogicException;
use Symfony\Component\Mime\Exception\RfcComplianceException;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @experimental in 4.3
*/
class Address
{
private static $validator;
private static $encoder;
private $address;
public function __construct(string $address)
{
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));
}
if (null === self::$validator) {
self::$validator = new EmailValidator();
}
if (null === self::$encoder) {
self::$encoder = new IdnAddressEncoder();
}
if (!self::$validator->isValid($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
{
return $this->address;
}
public function getEncodedAddress(): string
{
return self::$encoder->encodeString($this->address);
}
public function toString(): string
{
return $this->getEncodedAddress();
}
/**
* @param Address|string $address
*/
public static function create($address): self
{
if ($address instanceof self) {
return $address;
}
if (\is_string($address)) {
return new self($address);
}
throw new InvalidArgumentException(sprintf('An address can be an instance of Address or a string ("%s") given).', \is_object($address) ? \get_class($address) : \gettype($address)));
}
/**
* @param (Address|string)[] $addresses
*
* @return Address[]
*/
public static function createArray(array $addresses): array
{
$addrs = [];
foreach ($addresses as $address) {
$addrs[] = self::create($address);
}
return $addrs;
}
}

View File

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

View File

@ -19,6 +19,8 @@ use Symfony\Component\DependencyInjection\Reference;
* Registers custom mime types guessers.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @experimental in 4.3
*/
class AddMimeTypeGuesserPass implements CompilerPassInterface
{

View File

@ -0,0 +1,604 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime;
use Symfony\Component\Mime\Exception\LogicException;
use Symfony\Component\Mime\Part\AbstractPart;
use Symfony\Component\Mime\Part\DataPart;
use Symfony\Component\Mime\Part\Multipart\AlternativePart;
use Symfony\Component\Mime\Part\Multipart\MixedPart;
use Symfony\Component\Mime\Part\Multipart\RelatedPart;
use Symfony\Component\Mime\Part\TextPart;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @experimental in 4.3
*/
class Email extends Message
{
const PRIORITY_HIGHEST = 1;
const PRIORITY_HIGH = 2;
const PRIORITY_NORMAL = 3;
const PRIORITY_LOW = 4;
const PRIORITY_LOWEST = 5;
private const PRIORITY_MAP = [
self::PRIORITY_HIGHEST => 'Highest',
self::PRIORITY_HIGH => 'High',
self::PRIORITY_NORMAL => 'Normal',
self::PRIORITY_LOW => 'Low',
self::PRIORITY_LOWEST => 'Lowest',
];
private $text;
private $textCharset;
private $html;
private $htmlCharset;
private $attachments = [];
/**
* @return $this
*/
public function subject(string $subject)
{
return $this->setHeaderBody('Text', 'Subject', $subject);
}
public function getSubject(): ?string
{
return $this->getHeaders()->getHeaderBody('Subject');
}
/**
* @return $this
*/
public function date(\DateTimeInterface $dateTime)
{
return $this->setHeaderBody('Date', 'Date', $dateTime);
}
public function getDate(): ?\DateTimeImmutable
{
return $this->getHeaders()->getHeaderBody('Date');
}
/**
* @param Address|string $address
*
* @return $this
*/
public function returnPath($address)
{
return $this->setHeaderBody('Path', 'Return-Path', Address::create($address));
}
public function getReturnPath(): ?Address
{
return $this->getHeaders()->getHeaderBody('Return-Path');
}
/**
* @param Address|string $address
*
* @return $this
*/
public function sender($address)
{
return $this->setHeaderBody('Mailbox', 'Sender', Address::create($address));
}
public function getSender(): ?Address
{
return $this->getHeaders()->getHeaderBody('Sender');
}
/**
* @param Address|NamedAddress|string $addresses
*
* @return $this
*/
public function addFrom(...$addresses)
{
return $this->addListAddressHeaderBody('From', $addresses);
}
/**
* @param Address|NamedAddress|string $addresses
*
* @return $this
*/
public function from(...$addresses)
{
return $this->setListAddressHeaderBody('From', $addresses);
}
/**
* @return (Address|NamedAddress)[]
*/
public function getFrom(): array
{
return $this->getHeaders()->getHeaderBody('From') ?: [];
}
/**
* @param Address|string $addresses
*
* @return $this
*/
public function addReplyTo(...$addresses)
{
return $this->addListAddressHeaderBody('Reply-To', $addresses);
}
/**
* @param Address|string $addresses
*
* @return $this
*/
public function replyTo(...$addresses)
{
return $this->setListAddressHeaderBody('Reply-To', $addresses);
}
/**
* @return Address[]
*/
public function getReplyTo(): array
{
return $this->getHeaders()->getHeaderBody('Reply-To') ?: [];
}
/**
* @param Address|NamedAddress|string $addresses
*
* @return $this
*/
public function addTo(...$addresses)
{
return $this->addListAddressHeaderBody('To', $addresses);
}
/**
* @param Address|NamedAddress|string $addresses
*
* @return $this
*/
public function to(...$addresses)
{
return $this->setListAddressHeaderBody('To', $addresses);
}
/**
* @return (Address|NamedAddress)[]
*/
public function getTo(): array
{
return $this->getHeaders()->getHeaderBody('To') ?: [];
}
/**
* @param Address|NamedAddress|string $addresses
*
* @return $this
*/
public function addCc(...$addresses)
{
return $this->addListAddressHeaderBody('Cc', $addresses);
}
/**
* @param Address|string $addresses
*
* @return $this
*/
public function cc(...$addresses)
{
return $this->setListAddressHeaderBody('Cc', $addresses);
}
/**
* @return (Address|NamedAddress)[]
*/
public function getCc(): array
{
return $this->getHeaders()->getHeaderBody('Cc') ?: [];
}
/**
* @param Address|NamedAddress|string $addresses
*
* @return $this
*/
public function addBcc(...$addresses)
{
return $this->addListAddressHeaderBody('Bcc', $addresses);
}
/**
* @param Address|string $addresses
*
* @return $this
*/
public function bcc(...$addresses)
{
return $this->setListAddressHeaderBody('Bcc', $addresses);
}
/**
* @return (Address|NamedAddress)[]
*/
public function getBcc(): array
{
return $this->getHeaders()->getHeaderBody('Bcc') ?: [];
}
/**
* Sets the priority of this message.
*
* The value is an integer where 1 is the highest priority and 5 is the lowest.
*
* @return $this
*/
public function priority(int $priority)
{
if ($priority > 5) {
$priority = 5;
} elseif ($priority < 1) {
$priority = 1;
}
return $this->setHeaderBody('Text', 'X-Priority', sprintf('%d (%s)', $priority, self::PRIORITY_MAP[$priority]));
}
/**
* Get the priority of this message.
*
* The returned value is an integer where 1 is the highest priority and 5
* is the lowest.
*/
public function getPriority(): int
{
list($priority) = sscanf($this->getHeaders()->getHeaderBody('X-Priority'), '%[1-5]');
return $priority ?? 3;
}
/**
* @param resource|string $body
*
* @return $this
*/
public function text($body, string $charset = 'utf-8')
{
$this->text = $body;
$this->textCharset = $charset;
return $this;
}
/**
* @return resource|string|null
*/
public function getTextBody()
{
return $this->text;
}
public function getTextCharset(): ?string
{
return $this->textCharset;
}
/**
* @param resource|string|null $body
*
* @return $this
*/
public function html($body, string $charset = 'utf-8')
{
$this->html = $body;
$this->htmlCharset = $charset;
return $this;
}
/**
* @return resource|string|null
*/
public function getHtmlBody()
{
return $this->html;
}
public function getHtmlCharset(): ?string
{
return $this->htmlCharset;
}
/**
* @param resource|string $body
*
* @return $this
*/
public function attach($body, string $name = null, string $contentType = null)
{
$this->attachments[] = ['body' => $body, 'name' => $name, 'content-type' => $contentType, 'inline' => false];
return $this;
}
/**
* @return $this
*/
public function attachFromPath(string $path, string $name = null, string $contentType = null)
{
$this->attachments[] = ['path' => $path, 'name' => $name, 'content-type' => $contentType, 'inline' => false];
return $this;
}
/**
* @param resource|string $body
*
* @return $this
*/
public function embed($body, string $name = null, string $contentType = null)
{
$this->attachments[] = ['body' => $body, 'name' => $name, 'content-type' => $contentType, 'inline' => true];
return $this;
}
/**
* @return $this
*/
public function embedFromPath(string $path, string $name = null, string $contentType = null)
{
$this->attachments[] = ['path' => $path, 'name' => $name, 'content-type' => $contentType, 'inline' => true];
return $this;
}
/**
* @return $this
*/
public function attachPart(AbstractPart $part)
{
$this->attachments[] = ['part' => $part];
return $this;
}
/**
* @return AbstractPart[]
*/
public function getAttachments(): array
{
$parts = [];
foreach ($this->attachments as $attachment) {
$parts[] = $this->createDataPart($attachment);
}
return $parts;
}
public function getBody(): AbstractPart
{
if (null !== $body = parent::getBody()) {
return $body;
}
return $this->generateBody();
}
/**
* Generates an AbstractPart based on the raw body of a message.
*
* The most "complex" part generated by this method is when there is text and HTML bodies
* with related images for the HTML part and some attachments:
*
* multipart/mixed
* |
* |------------> multipart/related
* | |
* | |------------> multipart/alternative
* | | |
* | | ------------> text/plain (with content)
* | | |
* | | ------------> text/html (with content)
* | |
* | ------------> image/png (with content)
* |
* ------------> application/pdf (with content)
*/
private function generateBody(): AbstractPart
{
if (null === $this->text && null === $this->html) {
throw new LogicException('A message must have a text and/or an HTML part.');
}
$part = null === $this->text ? null : new TextPart($this->text, $this->textCharset);
[$htmlPart, $attachmentParts, $inlineParts] = $this->prepareParts();
if (null !== $htmlPart) {
if (null !== $part) {
$part = new AlternativePart($part, $htmlPart);
} else {
$part = $htmlPart;
}
}
if ($inlineParts) {
$part = new RelatedPart($part, ...$inlineParts);
}
if ($attachmentParts) {
$part = new MixedPart($part, ...$attachmentParts);
}
return $part;
}
private function prepareParts(): ?array
{
$names = [];
$htmlPart = null;
$html = $this->html;
if (null !== $this->html) {
if (\is_resource($html)) {
if (stream_get_meta_data($html)['seekable'] ?? false) {
rewind($html);
}
$html = stream_get_contents($html);
}
$htmlPart = new TextPart($html, $this->htmlCharset, 'html');
preg_match_all('(<img\s+[^>]*src\s*=\s*(?:([\'"])cid:([^"]+)\\1|cid:([^>\s]+)))i', $html, $names);
$names = array_filter(array_unique(array_merge($names[2], $names[3])));
}
$attachmentParts = $inlineParts = [];
foreach ($this->attachments as $attachment) {
foreach ($names as $name) {
if (isset($attachment['part'])) {
continue;
}
if ($name !== $attachment['name']) {
continue;
}
if (isset($inlineParts[$name])) {
continue 2;
}
$attachment['inline'] = true;
$inlineParts[$name] = $part = $this->createDataPart($attachment);
$html = str_replace('cid:'.$name, 'cid:'.$part->getContentId(), $html);
continue 2;
}
$attachmentParts[] = $this->createDataPart($attachment);
}
if (null !== $htmlPart) {
$htmlPart = new TextPart($html, $this->htmlCharset, 'html');
}
return [$htmlPart, $attachmentParts, array_values($inlineParts)];
}
private function createDataPart(array $attachment): DataPart
{
if (isset($attachment['part'])) {
return $attachment['part'];
}
if (isset($attachment['body'])) {
$part = new DataPart($attachment['body'], $attachment['name'] ?? null, $attachment['content-type'] ?? null);
} else {
$part = DataPart::fromPath($attachment['path'] ?? '', $attachment['name'] ?? null, $attachment['content-type'] ?? null);
}
if ($attachment['inline']) {
$part->asInline();
}
return $part;
}
/**
* @return $this
*/
private function setHeaderBody(string $type, string $name, $body)
{
$this->getHeaders()->setHeaderBody($type, $name, $body);
return $this;
}
private function addListAddressHeaderBody($name, array $addresses)
{
if (!$to = $this->getHeaders()->get($name)) {
return $this->setListAddressHeaderBody($name, $addresses);
}
$to->addAddresses(Address::createArray($addresses));
return $this;
}
private function setListAddressHeaderBody($name, array $addresses)
{
$addresses = Address::createArray($addresses);
$headers = $this->getHeaders();
if ($to = $headers->get($name)) {
$to->setAddresses($addresses);
} else {
$headers->addMailboxListHeader($name, $addresses);
}
return $this;
}
public function __sleep()
{
$this->_headers = $this->getHeaders();
$this->_raw = false;
if (null !== $body = parent::getBody()) {
$r = new \ReflectionProperty(Message::class, 'body');
$r->setAccessible(true);
$this->_body = $r->getValue($this);
$this->_raw = true;
return ['_raw', '_headers', '_body'];
}
if (\is_resource($this->text)) {
if (stream_get_meta_data($this->text)['seekable'] ?? false) {
rewind($this->text);
}
$this->text = stream_get_contents($this->text);
}
if (\is_resource($this->html)) {
if (stream_get_meta_data($this->html)['seekable'] ?? false) {
rewind($this->html);
}
$this->html = stream_get_contents($this->html);
}
foreach ($this->attachments as $i => $attachment) {
if (isset($attachment['body']) && \is_resource($attachment['body'])) {
if (stream_get_meta_data($attachment['body'])['seekable'] ?? false) {
rewind($attachment['body']);
}
$this->attachments[$i]['body'] = stream_get_contents($attachment['body']);
}
}
return ['_raw', '_headers', 'text', 'textCharset', 'html', 'htmlCharset', 'attachments'];
}
public function __wakeup()
{
$r = new \ReflectionProperty(Message::class, 'headers');
$r->setAccessible(true);
$r->setValue($this, $this->_headers);
unset($this->_headers);
if ($this->_raw) {
$r = new \ReflectionProperty(Message::class, 'body');
$r->setAccessible(true);
$r->setValue($this, $this->_body);
unset($this->_body);
}
unset($this->_raw);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,56 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Encoder;
use Symfony\Component\Mime\Exception\AddressEncoderException;
/**
* An IDN email address encoder.
*
* Encodes the domain part of an address using IDN. This is compatible will all
* SMTP servers.
*
* This encoder does not support email addresses with non-ASCII characters in
* local-part (the substring before @). To send to such addresses, use
* Utf8AddressEncoder together with SmtpUtf8Handler. Your outbound SMTP server must support
* the SMTPUTF8 extension.
*
* @author Christian Schmidt
*
* @experimental in 4.3
*/
final class IdnAddressEncoder implements AddressEncoderInterface
{
/**
* Encodes the domain part of an address using IDN.
*
* @throws AddressEncoderException If local-part contains non-ASCII characters
*/
public function encodeString(string $address): string
{
$i = strrpos($address, '@');
if (false !== $i) {
$local = substr($address, 0, $i);
$domain = substr($address, $i + 1);
if (preg_match('/[^\x00-\x7F]/', $local)) {
throw new AddressEncoderException(sprintf('Non-ASCII characters not supported in local-part os "%s".', $address));
}
if (preg_match('/[^\x00-\x7F]/', $domain)) {
$address = sprintf('%s@%s', $local, idn_to_ascii($domain, 0, INTL_IDNA_VARIANT_UTS46));
}
}
return $address;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,10 +11,15 @@
namespace Symfony\Component\Mime;
use Symfony\Component\Mime\Exception\InvalidArgumentException;
use Symfony\Component\Mime\Exception\LogicException;
/**
* Guesses the MIME type with the binary "file" (only available on *nix).
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @experimental in 4.3
*/
class FileBinaryMimeTypeGuesser implements MimeTypeGuesserInterface
{
@ -61,11 +66,11 @@ class FileBinaryMimeTypeGuesser implements MimeTypeGuesserInterface
public function guessMimeType(string $path): ?string
{
if (!is_file($path) || !is_readable($path)) {
throw new \InvalidArgumentException(sprintf('The "%s" file does not exist or is not readable.', $path));
throw new InvalidArgumentException(sprintf('The "%s" file does not exist or is not readable.', $path));
}
if (!$this->isGuesserSupported()) {
throw new \LogicException(sprintf('The "%s" guesser is not supported.', __CLASS__));
throw new LogicException(sprintf('The "%s" guesser is not supported.', __CLASS__));
}
ob_start();

View File

@ -11,10 +11,15 @@
namespace Symfony\Component\Mime;
use Symfony\Component\Mime\Exception\InvalidArgumentException;
use Symfony\Component\Mime\Exception\LogicException;
/**
* Guesses the MIME type using the PECL extension FileInfo.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @experimental in 4.3
*/
class FileinfoMimeTypeGuesser implements MimeTypeGuesserInterface
{
@ -44,11 +49,11 @@ class FileinfoMimeTypeGuesser implements MimeTypeGuesserInterface
public function guessMimeType(string $path): ?string
{
if (!is_file($path) || !is_readable($path)) {
throw new \InvalidArgumentException(sprintf('The "%s" file does not exist or is not readable.', $path));
throw new InvalidArgumentException(sprintf('The "%s" file does not exist or is not readable.', $path));
}
if (!$this->isGuesserSupported()) {
throw new \LogicException(sprintf('The "%s" guesser is not supported.', __CLASS__));
throw new LogicException(sprintf('The "%s" guesser is not supported.', __CLASS__));
}
if (false === $finfo = new \finfo(FILEINFO_MIME_TYPE, $this->magicFile)) {

View File

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

View File

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

View File

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

View File

@ -0,0 +1,275 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Header;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Exception\LogicException;
use Symfony\Component\Mime\NamedAddress;
/**
* A collection of headers.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @experimental in 4.3
*/
final class Headers
{
private static $uniqueHeaders = [
'date', 'from', 'sender', 'reply-to', 'to', 'cc', 'bcc',
'message-id', 'in-reply-to', 'references', 'subject',
];
private $headers = [];
private $lineLength = 76;
public function __construct(HeaderInterface ...$headers)
{
foreach ($headers as $header) {
$this->add($header);
}
}
public function __clone()
{
foreach ($this->headers as $name => $collection) {
foreach ($collection as $i => $header) {
$this->headers[$name][$i] = clone $header;
}
}
}
public function setMaxLineLength(int $lineLength)
{
$this->lineLength = $lineLength;
foreach ($this->getAll() as $header) {
$header->setMaxLineLength($lineLength);
}
}
public function getMaxLineLength(): int
{
return $this->lineLength;
}
/**
* @param (NamedAddress|Address|string)[] $addresses
*
* @return $this
*/
public function addMailboxListHeader(string $name, array $addresses)
{
return $this->add(new MailboxListHeader($name, Address::createArray($addresses)));
}
/**
* @param NamedAddress|Address|string $address
*
* @return $this
*/
public function addMailboxHeader(string $name, $address)
{
return $this->add(new MailboxHeader($name, Address::create($address)));
}
/**
* @param string|array $ids
*
* @return $this
*/
public function addIdHeader(string $name, $ids)
{
return $this->add(new IdentificationHeader($name, $ids));
}
/**
* @param Address|string $path
*
* @return $this
*/
public function addPathHeader(string $name, $path)
{
return $this->add(new PathHeader($name, $path instanceof Address ? $path : new Address($path)));
}
/**
* @return $this
*/
public function addDateHeader(string $name, \DateTimeInterface $dateTime)
{
return $this->add(new DateHeader($name, $dateTime));
}
/**
* @return $this
*/
public function addTextHeader(string $name, string $value)
{
return $this->add(new UnstructuredHeader($name, $value));
}
/**
* @return $this
*/
public function addParameterizedHeader(string $name, string $value, array $params = [])
{
return $this->add(new ParameterizedHeader($name, $value, $params));
}
public function has(string $name): bool
{
return isset($this->headers[strtolower($name)]);
}
/**
* @return $this
*/
public function add(HeaderInterface $header)
{
static $map = [
'date' => DateHeader::class,
'from' => MailboxListHeader::class,
'sender' => MailboxHeader::class,
'reply-to' => MailboxListHeader::class,
'to' => MailboxListHeader::class,
'cc' => MailboxListHeader::class,
'bcc' => MailboxListHeader::class,
'message-id' => IdentificationHeader::class,
'in-reply-to' => IdentificationHeader::class,
'references' => IdentificationHeader::class,
'return-path' => PathHeader::class,
];
$header->setMaxLineLength($this->lineLength);
$name = strtolower($header->getName());
if (isset($map[$name]) && !$header instanceof $map[$name]) {
throw new LogicException(sprintf('The "%s" header must be an instance of "%s" (got "%s").', $header->getName(), $map[$name], \get_class($header)));
}
if (\in_array($name, self::$uniqueHeaders, true) && isset($this->headers[$name]) && \count($this->headers[$name]) > 0) {
throw new LogicException(sprintf('Impossible to set header "%s" as it\'s already defined and must be unique.', $header->getName()));
}
$this->headers[$name][] = $header;
return $this;
}
public function get(string $name): ?HeaderInterface
{
$name = strtolower($name);
if (!isset($this->headers[strtolower($name)])) {
return null;
}
$values = array_values($this->headers[$name]);
return array_shift($values);
}
public function getAll(string $name = null): iterable
{
if (null === $name) {
foreach ($this->headers as $name => $collection) {
foreach ($collection as $header) {
yield $name => $header;
}
}
} elseif (isset($this->headers[strtolower($name)])) {
foreach ($this->headers[strtolower($name)] as $header) {
yield $header;
}
}
}
public function getNames(): array
{
return array_keys($this->headers);
}
public function remove(string $name): void
{
unset($this->headers[strtolower($name)]);
}
public static function isUniqueHeader(string $name): bool
{
return \in_array($name, self::$uniqueHeaders, true);
}
public function toString(): string
{
$string = '';
foreach ($this->getAll() as $header) {
if ('' !== $header->getBodyAsString()) {
$string .= $header->toString()."\r\n";
}
}
return $string;
}
/**
* @internal
*/
public function getHeaderBody($name)
{
return $this->has($name) ? $this->get($name)->getBody() : null;
}
/**
* @internal
*/
public function setHeaderBody(string $type, string $name, $body): void
{
if ($this->has($name)) {
$this->get($name)->setBody($body);
} else {
$this->{'add'.$type.'Header'}($name, $body);
}
}
/**
* @internal
*/
public function getHeaderParameter(string $name, string $parameter): ?string
{
if (!$this->has($name)) {
return null;
}
$header = $this->get($name);
if (!$header instanceof ParameterizedHeader) {
throw new LogicException(sprintf('Unable to get parameter "%s" on header "%s" as the header is not of class "%s".', $parameter, $name, ParameterizedHeader::class));
}
return $header->getParameter($parameter);
}
/**
* @internal
*/
public function setHeaderParameter(string $name, string $parameter, $value): void
{
if (!$this->has($name)) {
throw new LogicException(sprintf('Unable to set parameter "%s" on header "%s" as the header is not defined.', $parameter, $name));
}
$header = $this->get($name);
if (!$header instanceof ParameterizedHeader) {
throw new LogicException(sprintf('Unable to set parameter "%s" on header "%s" as the header is not of class "%s".', $parameter, $name, ParameterizedHeader::class));
}
$header->setParameter($parameter, $value);
}
}

View File

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

View File

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

View File

@ -0,0 +1,139 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Header;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Exception\RfcComplianceException;
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
*/
public function __construct(string $name, array $addresses)
{
parent::__construct($name);
$this->setAddresses($addresses);
}
/**
* @param (NamedAddress|Address)[] $body
*
* @throws RfcComplianceException
*/
public function setBody($body)
{
$this->setAddresses($body);
}
/**
* @throws RfcComplianceException
*
* @return (NamedAddress|Address)[]
*/
public function getBody()
{
return $this->getAddresses();
}
/**
* Sets a list of addresses to be shown in this Header.
*
* @param (NamedAddress|Address)[] $addresses
*
* @throws RfcComplianceException
*/
public function setAddresses(array $addresses)
{
$this->addresses = [];
$this->addAddresses($addresses);
}
/**
* Sets a list of addresses to be shown in this Header.
*
* @param (NamedAddress|Address)[] $addresses
*
* @throws RfcComplianceException
*/
public function addAddresses(array $addresses)
{
foreach ($addresses as $address) {
$this->addAddress($address);
}
}
/**
* @throws RfcComplianceException
*/
public function addAddress(Address $address)
{
$this->addresses[] = $address;
}
/**
* @return (NamedAddress|Address)[]
*/
public function getAddresses(): array
{
return $this->addresses;
}
/**
* Gets the full mailbox list of this Header as an array of valid RFC 2822 strings.
*
* @throws RfcComplianceException
*
* @return string[]
*/
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.'>';
}
$strings[] = $str;
}
return $strings;
}
public function getBodyAsString(): string
{
return implode(', ', $this->getAddressStrings());
}
/**
* Redefine the encoding requirements for addresses.
*
* All "specials" must be encoded as the full header value will not be quoted
*
* @see RFC 2822 3.2.1
*/
protected function tokenNeedsEncoding(string $token): bool
{
return preg_match('/[()<>\[\]:;@\,."]/', $token) || parent::tokenNeedsEncoding($token);
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,134 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime;
use Symfony\Component\Mime\Exception\LogicException;
use Symfony\Component\Mime\Header\Headers;
use Symfony\Component\Mime\Part\AbstractPart;
use Symfony\Component\Mime\Part\TextPart;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @experimental in 4.3
*/
class Message extends RawMessage
{
private $headers;
private $body;
public function __construct(Headers $headers = null, AbstractPart $body = null)
{
$this->headers = $headers ? clone $headers : new Headers();
$this->body = $body;
}
public function __clone()
{
if (null !== $this->headers) {
$this->headers = clone $this->headers;
}
if (null !== $this->body) {
$this->body = clone $this->body;
}
}
/**
* @return $this
*/
public function setBody(AbstractPart $body = null)
{
$this->body = $body;
return $this;
}
public function getBody(): ?AbstractPart
{
return $this->body;
}
/**
* @return $this
*/
public function setHeaders(Headers $headers)
{
$this->headers = $headers;
return $this;
}
public function getHeaders(): Headers
{
return $this->headers;
}
public function getPreparedHeaders(): Headers
{
$headers = clone $this->headers;
if (!$headers->has('From')) {
throw new LogicException('An email must have a "From" header.');
}
$headers->addTextHeader('MIME-Version', '1.0');
if (!$headers->has('Date')) {
$headers->addDateHeader('Date', new \DateTimeImmutable());
}
// 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('Message-ID')) {
$headers->addIdHeader('Message-ID', $this->generateMessageId($sender->toString()));
}
// remove the Bcc field which should NOT be part of the sent message
$headers->remove('Bcc');
return $headers;
}
public function toString(): string
{
if (null === $body = $this->getBody()) {
$body = new TextPart('');
}
return $this->getPreparedHeaders()->toString().$body->toString();
}
public function toIterable(): iterable
{
if (null === $body = $this->getBody()) {
$body = new TextPart('');
}
yield $this->getPreparedHeaders()->toString();
foreach ($body->toIterable() as $chunk) {
yield $chunk;
}
}
private function generateMessageId(string $email): string
{
return bin2hex(random_bytes(16)).strstr($email, '@');
}
}

View File

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

View File

@ -15,6 +15,8 @@ namespace Symfony\Component\Mime;
* Guesses the MIME type of a file.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @experimental in 4.3
*/
interface MimeTypeGuesserInterface
{

View File

@ -11,6 +11,8 @@
namespace Symfony\Component\Mime;
use Symfony\Component\Mime\Exception\LogicException;
/**
* Manages MIME types and file extensions.
*
@ -31,6 +33,8 @@ namespace Symfony\Component\Mime;
* $guesser->registerGuesser(new FileinfoMimeTypeGuesser('/path/to/magic/file'));
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @experimental in 4.3
*/
final class MimeTypes implements MimeTypesInterface
{
@ -122,7 +126,7 @@ final class MimeTypes implements MimeTypesInterface
}
if (!$this->isGuesserSupported()) {
throw new \LogicException('Unable to guess the MIME type as no guessers are available (have you enable the php_fileinfo extension?).');
throw new LogicException('Unable to guess the MIME type as no guessers are available (have you enable the php_fileinfo extension?).');
}
return null;

View File

@ -13,6 +13,8 @@ namespace Symfony\Component\Mime;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @experimental in 4.3
*/
interface MimeTypesInterface extends MimeTypeGuesserInterface
{

View File

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

View File

@ -0,0 +1,100 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Part;
use Symfony\Component\Mime\Exception\LogicException;
use Symfony\Component\Mime\Header\Headers;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @experimental in 4.3
*/
abstract class AbstractMultipartPart extends AbstractPart
{
private $boundary;
private $parts = [];
public function __construct(AbstractPart ...$parts)
{
parent::__construct();
foreach ($parts as $part) {
$this->parts[] = $part;
}
}
/**
* @return AbstractPart[]
*/
public function getParts(): array
{
return $this->parts;
}
public function getMediaType(): string
{
return 'multipart';
}
public function getPreparedHeaders(): Headers
{
$headers = parent::getPreparedHeaders();
$headers->setHeaderParameter('Content-Type', 'boundary', $this->getBoundary());
return $headers;
}
public function bodyToString(): string
{
$parts = $this->getParts();
if (\count($parts) < 2) {
throw new LogicException(sprintf('A "%s" instance must have at least 2 parts.', __CLASS__));
}
$string = '';
foreach ($parts as $part) {
$string .= '--'.$this->getBoundary()."\r\n".$part->toString()."\r\n";
}
$string .= '--'.$this->getBoundary()."--\r\n";
return $string;
}
public function bodyToIterable(): iterable
{
$parts = $this->getParts();
if (\count($parts) < 2) {
throw new LogicException(sprintf('A "%s" instance must have at least 2 parts.', __CLASS__));
}
foreach ($parts as $part) {
yield '--'.$this->getBoundary()."\r\n";
foreach ($part->toIterable() as $chunk) {
yield $chunk;
}
yield "\r\n";
}
yield '--'.$this->getBoundary()."--\r\n";
}
private function getBoundary(): string
{
if (null === $this->boundary) {
$this->boundary = '_=_symfony_'.time().'_'.bin2hex(random_bytes(16)).'_=_';
}
return $this->boundary;
}
}

View File

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

View File

@ -0,0 +1,152 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Part;
use Symfony\Component\Mime\Exception\InvalidArgumentException;
use Symfony\Component\Mime\Header\Headers;
use Symfony\Component\Mime\MimeTypes;
/**
* @final
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @experimental in 4.3
*/
class DataPart extends TextPart
{
private static $mimeTypes;
private $filename;
private $mediaType;
private $cid;
private $handle;
/**
* @param resource|string $body
*/
public function __construct($body, string $filename = null, string $contentType = null, string $encoding = null)
{
if (null === $contentType) {
$contentType = 'application/octet-stream';
}
list($this->mediaType, $subtype) = explode('/', $contentType);
parent::__construct($body, null, $subtype, $encoding);
$this->filename = $filename;
$this->setName($filename);
$this->setDisposition('attachment');
}
public static function fromPath(string $path, string $name = null, string $contentType = null): self
{
// FIXME: if file is not readable, exception?
if (null === $contentType) {
$ext = strtolower(substr($path, strrpos($path, '.') + 1));
if (null === self::$mimeTypes) {
self::$mimeTypes = new MimeTypes();
}
$contentType = self::$mimeTypes->getMimeTypes($ext)[0] ?? 'application/octet-stream';
}
if (false === $handle = @fopen($path, 'r', false)) {
throw new InvalidArgumentException(sprintf('Unable to open path "%s"', $path));
}
$p = new self($handle, $name ?: basename($path), $contentType);
$p->handle = $handle;
return $p;
}
/**
* @return $this
*/
public function asInline()
{
return $this->setDisposition('inline');
}
public function getContentId(): string
{
return $this->cid ?: $this->cid = $this->generateContentId();
}
public function hasContentId(): bool
{
return null !== $this->cid;
}
public function getMediaType(): string
{
return $this->mediaType;
}
public function getPreparedHeaders(): Headers
{
$headers = parent::getPreparedHeaders();
if (null !== $this->cid) {
$headers->setHeaderBody('Id', 'Content-ID', $this->cid);
}
if (null !== $this->filename) {
$headers->setHeaderParameter('Content-Disposition', 'filename', $this->filename);
}
return $headers;
}
private function generateContentId(): string
{
return bin2hex(random_bytes(16)).'@symfony';
}
public function __destruct()
{
if (null !== $this->handle && \is_resource($this->handle)) {
fclose($this->handle);
}
}
public function __sleep()
{
// converts the body to a string
parent::__sleep();
$this->_parent = [];
foreach (['body', 'charset', 'subtype', 'disposition', 'name', 'encoding'] as $name) {
$r = new \ReflectionProperty(TextPart::class, $name);
$r->setAccessible(true);
$this->_parent[$name] = $r->getValue($this);
}
$this->_headers = $this->getHeaders();
return ['_headers', '_parent', 'filename', 'mediaType'];
}
public function __wakeup()
{
$r = new \ReflectionProperty(AbstractPart::class, 'headers');
$r->setAccessible(true);
$r->setValue($this, $this->_headers);
unset($this->_headers);
foreach (['body', 'charset', 'subtype', 'disposition', 'name', 'encoding'] as $name) {
$r = new \ReflectionProperty(TextPart::class, $name);
$r->setAccessible(true);
$r->setValue($this, $this->_parent[$name]);
}
unset($this->_parent);
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,92 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Part\Multipart;
use Symfony\Component\Mime\Exception\InvalidArgumentException;
use Symfony\Component\Mime\Part\AbstractMultipartPart;
use Symfony\Component\Mime\Part\DataPart;
use Symfony\Component\Mime\Part\TextPart;
/**
* Implements RFC 7578.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @experimental in 4.3
*/
final class FormDataPart extends AbstractMultipartPart
{
private $fields = [];
/**
* @param (string|array|DataPart)[] $fields
*/
public function __construct(array $fields = [])
{
parent::__construct();
foreach ($fields as $name => $value) {
if (!\is_string($value) && !\is_array($value) && !$value instanceof TextPart) {
throw new InvalidArgumentException(sprintf('A form field value can only be a string, an array, or an instance of TextPart ("%s" given).', \is_object($value) ? \get_class($value) : \gettype($value)));
}
$this->fields[$name] = $value;
}
// HTTP does not support \r\n in header values
$this->getHeaders()->setMaxLineLength(1000);
}
public function getMediaSubtype(): string
{
return 'form-data';
}
public function getParts(): array
{
return $this->prepareFields($this->fields);
}
private function prepareFields(array $fields): array
{
$values = [];
foreach ($fields as $name => $value) {
if (\is_array($value)) {
foreach ($value as $v) {
$values[] = $this->preparePart($name, $v);
}
} else {
$values[] = $this->preparePart($name, $value);
}
}
return $values;
}
private function preparePart($name, $value): TextPart
{
if (\is_string($value)) {
return $this->configurePart($name, new TextPart($value));
}
return $this->configurePart($name, $value);
}
private function configurePart(string $name, TextPart $part): TextPart
{
$part->setDisposition('form-data');
$part->setName($name);
// HTTP does not support \r\n in header values
$part->getHeaders()->setMaxLineLength(1000);
return $part;
}
}

View File

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

View File

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

View File

@ -0,0 +1,191 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Part;
use Symfony\Component\Mime\Encoder\Base64ContentEncoder;
use Symfony\Component\Mime\Encoder\ContentEncoderInterface;
use Symfony\Component\Mime\Encoder\QpContentEncoder;
use Symfony\Component\Mime\Exception\InvalidArgumentException;
use Symfony\Component\Mime\Header\Headers;
/**
* @final
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @experimental in 4.3
*/
class TextPart extends AbstractPart
{
private static $encoders = [];
private $body;
private $charset;
private $subtype;
private $disposition;
private $name;
protected $encoding;
/**
* @param resource|string $body
*/
public function __construct($body, ?string $charset = 'utf-8', $subtype = 'plain', string $encoding = null)
{
parent::__construct();
if (!\is_string($body) && !\is_resource($body)) {
throw new \TypeError(sprintf('The body of "%s" must be a string or a resource (got "%s").', self::class, \is_object($body) ? \get_class($body) : \gettype($body)));
}
$this->body = $body;
$this->charset = $charset;
$this->subtype = $subtype;
// FIXME: can also be 7BIT, 8BIT, ...
if (null === $encoding) {
$this->encoding = $this->chooseEncoding();
} else {
if ('quoted-printable' !== $encoding && 'base64' !== $encoding) {
throw new InvalidArgumentException(sprintf('The encoding must be one of "quoted-printable" or "base64" ("%s" given).', $encoding));
}
$this->encoding = $encoding;
}
}
public function getMediaType(): string
{
return 'text';
}
public function getMediaSubtype(): string
{
return $this->subtype;
}
/**
* @param string $disposition one of attachment, inline, or form-data
*
* @return $this
*/
public function setDisposition(string $disposition)
{
$this->disposition = $disposition;
return $this;
}
/**
* Sets the name of the file (used by FormDataPart).
*
* @return $this
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
public function getBody(): string
{
if (!\is_resource($this->body)) {
return $this->body;
}
if (stream_get_meta_data($this->body)['seekable'] ?? false) {
rewind($this->body);
}
return stream_get_contents($this->body) ?: '';
}
public function bodyToString(): string
{
return $this->getEncoder()->encodeString($this->getBody(), $this->charset);
}
public function bodyToIterable(): iterable
{
if (\is_resource($this->body)) {
if (stream_get_meta_data($this->body)['seekable'] ?? false) {
rewind($this->body);
}
foreach ($this->getEncoder()->encodeByteStream($this->body) as $chunk) {
yield $chunk;
}
} else {
yield $this->getEncoder()->encodeString($this->body);
}
}
public function getPreparedHeaders(): Headers
{
$headers = parent::getPreparedHeaders();
$headers->setHeaderBody('Parameterized', 'Content-Type', $this->getMediaType().'/'.$this->getMediaSubtype());
if ($this->charset) {
$headers->setHeaderParameter('Content-Type', 'charset', $this->charset);
}
if ($this->name) {
$headers->setHeaderParameter('Content-Type', 'name', $this->name);
}
$headers->setHeaderBody('Text', 'Content-Transfer-Encoding', $this->encoding);
if (!$headers->has('Content-Disposition') && null !== $this->disposition) {
$headers->setHeaderBody('Parameterized', 'Content-Disposition', $this->disposition);
if ($this->name) {
$headers->setHeaderParameter('Content-Disposition', 'name', $this->name);
}
}
return $headers;
}
protected function getEncoder(): ContentEncoderInterface
{
if ('quoted-printable' === $this->encoding) {
return self::$encoders[$this->encoding] ?? (self::$encoders[$this->encoding] = new QpContentEncoder());
}
return self::$encoders[$this->encoding] ?? (self::$encoders[$this->encoding] = new Base64ContentEncoder());
}
private function chooseEncoding(): string
{
if (null === $this->charset) {
return 'base64';
}
return 'quoted-printable';
}
public function __sleep()
{
// convert resources to strings for serialization
if (\is_resource($this->body)) {
$this->body = $this->getBody();
}
$this->_headers = $this->getHeaders();
return ['_headers', 'body', 'charset', 'subtype', 'disposition', 'name', 'encoding'];
}
public function __wakeup()
{
$r = new \ReflectionProperty(AbstractPart::class, 'headers');
$r->setAccessible(true);
$r->setValue($this, $this->_headers);
unset($this->_headers);
}
}

View File

@ -1,8 +1,11 @@
MIME Component
==============
The MIME component allows manipulating MIME types.
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 BC-break policy.
Resources
---------

View File

@ -0,0 +1,55 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @experimental in 4.3
*/
class RawMessage
{
private $message;
/**
* @param iterable|string $message
*/
public function __construct($message)
{
$this->message = $message;
}
public function toString(): string
{
if (\is_string($this->message)) {
return $this->message;
}
return $this->message = implode('', iterator_to_array($this->message));
}
public function toIterable(): iterable
{
if (\is_string($this->message)) {
yield $this->message;
return;
}
$message = '';
foreach ($this->message as $chunk) {
$message .= $chunk;
yield $chunk;
}
$this->message = $message;
}
}

View File

@ -18,7 +18,7 @@ abstract class AbstractMimeTypeGuesserTest extends TestCase
{
public static function tearDownAfterClass()
{
$path = __DIR__.'/Fixtures/to_delete';
$path = __DIR__.'/Fixtures/mimetypes/to_delete';
if (file_exists($path)) {
@chmod($path, 0666);
@unlink($path);
@ -33,7 +33,7 @@ abstract class AbstractMimeTypeGuesserTest extends TestCase
$this->markTestSkipped('Guesser is not supported');
}
$this->assertEquals('image/gif', $this->getGuesser()->guessMimeType(__DIR__.'/Fixtures/test'));
$this->assertEquals('image/gif', $this->getGuesser()->guessMimeType(__DIR__.'/Fixtures/mimetypes/test'));
}
public function testGuessImageWithDirectory()
@ -43,7 +43,7 @@ abstract class AbstractMimeTypeGuesserTest extends TestCase
}
$this->expectException('\InvalidArgumentException');
$this->getGuesser()->guessMimeType(__DIR__.'/Fixtures/directory');
$this->getGuesser()->guessMimeType(__DIR__.'/Fixtures/mimetypes/directory');
}
public function testGuessImageWithKnownExtension()
@ -52,7 +52,7 @@ abstract class AbstractMimeTypeGuesserTest extends TestCase
$this->markTestSkipped('Guesser is not supported');
}
$this->assertEquals('image/gif', $this->getGuesser()->guessMimeType(__DIR__.'/Fixtures/test.gif'));
$this->assertEquals('image/gif', $this->getGuesser()->guessMimeType(__DIR__.'/Fixtures/mimetypes/test.gif'));
}
public function testGuessFileWithUnknownExtension()
@ -61,7 +61,7 @@ abstract class AbstractMimeTypeGuesserTest extends TestCase
$this->markTestSkipped('Guesser is not supported');
}
$this->assertEquals('application/octet-stream', $this->getGuesser()->guessMimeType(__DIR__.'/Fixtures/.unknownextension'));
$this->assertEquals('application/octet-stream', $this->getGuesser()->guessMimeType(__DIR__.'/Fixtures/mimetypes/.unknownextension'));
}
public function testGuessWithIncorrectPath()
@ -71,7 +71,7 @@ abstract class AbstractMimeTypeGuesserTest extends TestCase
}
$this->expectException('\InvalidArgumentException');
$this->getGuesser()->guessMimeType(__DIR__.'/Fixtures/not_here');
$this->getGuesser()->guessMimeType(__DIR__.'/Fixtures/mimetypes/not_here');
}
public function testGuessWithNonReadablePath()
@ -88,7 +88,7 @@ abstract class AbstractMimeTypeGuesserTest extends TestCase
$this->markTestSkipped('This test will fail if run under superuser');
}
$path = __DIR__.'/Fixtures/to_delete';
$path = __DIR__.'/Fixtures/mimetypes/to_delete';
touch($path);
@chmod($path, 0333);

View File

@ -0,0 +1,61 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\NamedAddress;
class AddressTest extends TestCase
{
public function testConstructor()
{
$a = new Address('fabien@symfonï.com');
$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());
}
public function testConstructorWithInvalidAddress()
{
$this->expectException(\InvalidArgumentException::class);
new Address('fab pot@symfony.com');
}
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->assertEquals($a, Address::create('fabien@symfony.com'));
}
public function testCreateWrongArg()
{
$this->expectException(\InvalidArgumentException::class);
Address::create(new \stdClass());
}
public function testCreateArray()
{
$fabien = new Address('fabien@symfony.com');
$helene = new NamedAddress('helene@symfony.com', 'Helene');
$this->assertSame([$fabien, $helene], Address::createArray([$fabien, $helene]));
$this->assertEquals([$fabien], Address::createArray(['fabien@symfony.com']));
}
public function testCreateArrayWrongArg()
{
$this->expectException(\InvalidArgumentException::class);
Address::createArray([new \stdClass()]);
}
}

View File

@ -0,0 +1,87 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\CharacterStream;
class CharacterStreamTest extends TestCase
{
public function testReadCharactersAreInTact()
{
$stream = new CharacterStream(pack('C*', 0xD0, 0x94, 0xD0, 0xB6, 0xD0, 0xBE));
$stream->write(pack('C*',
0xD0, 0xBB,
0xD1, 0x8E,
0xD0, 0xB1,
0xD1, 0x8B,
0xD1, 0x85
));
$this->assertSame(pack('C*', 0xD0, 0x94), $stream->read(1));
$this->assertSame(pack('C*', 0xD0, 0xB6, 0xD0, 0xBE), $stream->read(2));
$this->assertSame(pack('C*', 0xD0, 0xBB), $stream->read(1));
$this->assertSame(pack('C*', 0xD1, 0x8E, 0xD0, 0xB1, 0xD1, 0x8B), $stream->read(3));
$this->assertSame(pack('C*', 0xD1, 0x85), $stream->read(1));
$this->assertNull($stream->read(1));
}
public function testCharactersCanBeReadAsByteArrays()
{
$stream = new CharacterStream(pack('C*', 0xD0, 0x94, 0xD0, 0xB6, 0xD0, 0xBE));
$stream->write(pack('C*',
0xD0, 0xBB,
0xD1, 0x8E,
0xD0, 0xB1,
0xD1, 0x8B,
0xD1, 0x85
));
$this->assertEquals([0xD0, 0x94], $stream->readBytes(1));
$this->assertEquals([0xD0, 0xB6, 0xD0, 0xBE], $stream->readBytes(2));
$this->assertEquals([0xD0, 0xBB], $stream->readBytes(1));
$this->assertEquals([0xD1, 0x8E, 0xD0, 0xB1, 0xD1, 0x8B], $stream->readBytes(3));
$this->assertEquals([0xD1, 0x85], $stream->readBytes(1));
$this->assertNull($stream->readBytes(1));
}
public function testRequestingLargeCharCountPastEndOfStream()
{
$stream = new CharacterStream(pack('C*', 0xD0, 0x94, 0xD0, 0xB6, 0xD0, 0xBE));
$this->assertSame(pack('C*', 0xD0, 0x94, 0xD0, 0xB6, 0xD0, 0xBE), $stream->read(100));
$this->assertNull($stream->read(1));
}
public function testRequestingByteArrayCountPastEndOfStream()
{
$stream = new CharacterStream(pack('C*', 0xD0, 0x94, 0xD0, 0xB6, 0xD0, 0xBE));
$this->assertEquals([0xD0, 0x94, 0xD0, 0xB6, 0xD0, 0xBE], $stream->readBytes(100));
$this->assertNull($stream->readBytes(1));
}
public function testPointerOffsetCanBeSet()
{
$stream = new CharacterStream(pack('C*', 0xD0, 0x94, 0xD0, 0xB6, 0xD0, 0xBE));
$this->assertSame(pack('C*', 0xD0, 0x94), $stream->read(1));
$stream->setPointer(0);
$this->assertSame(pack('C*', 0xD0, 0x94), $stream->read(1));
$stream->setPointer(2);
$this->assertSame(pack('C*', 0xD0, 0xBE), $stream->read(1));
}
public function testAlgorithmWithFixedWidthCharsets()
{
$stream = new CharacterStream(pack('C*', 0xD1, 0x8D, 0xD0, 0xBB, 0xD0, 0xB0));
$this->assertSame(pack('C*', 0xD1, 0x8D), $stream->read(1));
$this->assertSame(pack('C*', 0xD0, 0xBB), $stream->read(1));
$this->assertSame(pack('C*', 0xD0, 0xB0), $stream->read(1));
$this->assertNull($stream->read(1));
}
}

View File

@ -0,0 +1,385 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Tests;
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;
use Symfony\Component\Mime\Part\Multipart\RelatedPart;
use Symfony\Component\Mime\Part\TextPart;
class EmailTest extends TestCase
{
public function testSubject()
{
$e = new Email();
$e->subject('Subject');
$this->assertEquals('Subject', $e->getSubject());
}
public function testDate()
{
$e = new Email();
$e->date($d = new \DateTimeImmutable());
$this->assertSame($d, $e->getDate());
}
public function testReturnPath()
{
$e = new Email();
$e->returnPath('fabien@symfony.com');
$this->assertEquals(new Address('fabien@symfony.com'), $e->getReturnPath());
}
public function testSender()
{
$e = new Email();
$e->sender('fabien@symfony.com');
$this->assertEquals(new Address('fabien@symfony.com'), $e->getSender());
$e->sender($fabien = new Address('fabien@symfony.com'));
$this->assertSame($fabien, $e->getSender());
}
public function testFrom()
{
$e = new Email();
$helene = new Address('helene@symfony.com');
$thomas = new NamedAddress('thomas@symfony.com', 'Thomas');
$caramel = new Address('caramel@symfony.com');
$this->assertSame($e, $e->from('fabien@symfony.com', $helene, $thomas));
$v = $e->getFrom();
$this->assertCount(3, $v);
$this->assertEquals(new Address('fabien@symfony.com'), $v[0]);
$this->assertSame($helene, $v[1]);
$this->assertSame($thomas, $v[2]);
$this->assertSame($e, $e->addFrom('lucas@symfony.com', $caramel));
$v = $e->getFrom();
$this->assertCount(5, $v);
$this->assertEquals(new Address('fabien@symfony.com'), $v[0]);
$this->assertSame($helene, $v[1]);
$this->assertSame($thomas, $v[2]);
$this->assertEquals(new Address('lucas@symfony.com'), $v[3]);
$this->assertSame($caramel, $v[4]);
$e = new Email();
$e->addFrom('lucas@symfony.com', $caramel);
$this->assertCount(2, $e->getFrom());
$e = new Email();
$e->from('lucas@symfony.com');
$e->from($caramel);
$this->assertSame([$caramel], $e->getFrom());
}
public function testReplyTo()
{
$e = new Email();
$helene = new Address('helene@symfony.com');
$thomas = new NamedAddress('thomas@symfony.com', 'Thomas');
$caramel = new Address('caramel@symfony.com');
$this->assertSame($e, $e->replyTo('fabien@symfony.com', $helene, $thomas));
$v = $e->getReplyTo();
$this->assertCount(3, $v);
$this->assertEquals(new Address('fabien@symfony.com'), $v[0]);
$this->assertSame($helene, $v[1]);
$this->assertSame($thomas, $v[2]);
$this->assertSame($e, $e->addReplyTo('lucas@symfony.com', $caramel));
$v = $e->getReplyTo();
$this->assertCount(5, $v);
$this->assertEquals(new Address('fabien@symfony.com'), $v[0]);
$this->assertSame($helene, $v[1]);
$this->assertSame($thomas, $v[2]);
$this->assertEquals(new Address('lucas@symfony.com'), $v[3]);
$this->assertSame($caramel, $v[4]);
$e = new Email();
$e->addReplyTo('lucas@symfony.com', $caramel);
$this->assertCount(2, $e->getReplyTo());
$e = new Email();
$e->replyTo('lucas@symfony.com');
$e->replyTo($caramel);
$this->assertSame([$caramel], $e->getReplyTo());
}
public function testTo()
{
$e = new Email();
$helene = new Address('helene@symfony.com');
$thomas = new NamedAddress('thomas@symfony.com', 'Thomas');
$caramel = new Address('caramel@symfony.com');
$this->assertSame($e, $e->to('fabien@symfony.com', $helene, $thomas));
$v = $e->getTo();
$this->assertCount(3, $v);
$this->assertEquals(new Address('fabien@symfony.com'), $v[0]);
$this->assertSame($helene, $v[1]);
$this->assertSame($thomas, $v[2]);
$this->assertSame($e, $e->addTo('lucas@symfony.com', $caramel));
$v = $e->getTo();
$this->assertCount(5, $v);
$this->assertEquals(new Address('fabien@symfony.com'), $v[0]);
$this->assertSame($helene, $v[1]);
$this->assertSame($thomas, $v[2]);
$this->assertEquals(new Address('lucas@symfony.com'), $v[3]);
$this->assertSame($caramel, $v[4]);
$e = new Email();
$e->addTo('lucas@symfony.com', $caramel);
$this->assertCount(2, $e->getTo());
$e = new Email();
$e->to('lucas@symfony.com');
$e->to($caramel);
$this->assertSame([$caramel], $e->getTo());
}
public function testCc()
{
$e = new Email();
$helene = new Address('helene@symfony.com');
$thomas = new NamedAddress('thomas@symfony.com', 'Thomas');
$caramel = new Address('caramel@symfony.com');
$this->assertSame($e, $e->cc('fabien@symfony.com', $helene, $thomas));
$v = $e->getCc();
$this->assertCount(3, $v);
$this->assertEquals(new Address('fabien@symfony.com'), $v[0]);
$this->assertSame($helene, $v[1]);
$this->assertSame($thomas, $v[2]);
$this->assertSame($e, $e->addCc('lucas@symfony.com', $caramel));
$v = $e->getCc();
$this->assertCount(5, $v);
$this->assertEquals(new Address('fabien@symfony.com'), $v[0]);
$this->assertSame($helene, $v[1]);
$this->assertSame($thomas, $v[2]);
$this->assertEquals(new Address('lucas@symfony.com'), $v[3]);
$this->assertSame($caramel, $v[4]);
$e = new Email();
$e->addCc('lucas@symfony.com', $caramel);
$this->assertCount(2, $e->getCc());
$e = new Email();
$e->cc('lucas@symfony.com');
$e->cc($caramel);
$this->assertSame([$caramel], $e->getCc());
}
public function testBcc()
{
$e = new Email();
$helene = new Address('helene@symfony.com');
$thomas = new NamedAddress('thomas@symfony.com', 'Thomas');
$caramel = new Address('caramel@symfony.com');
$this->assertSame($e, $e->bcc('fabien@symfony.com', $helene, $thomas));
$v = $e->getBcc();
$this->assertCount(3, $v);
$this->assertEquals(new Address('fabien@symfony.com'), $v[0]);
$this->assertSame($helene, $v[1]);
$this->assertSame($thomas, $v[2]);
$this->assertSame($e, $e->addBcc('lucas@symfony.com', $caramel));
$v = $e->getBcc();
$this->assertCount(5, $v);
$this->assertEquals(new Address('fabien@symfony.com'), $v[0]);
$this->assertSame($helene, $v[1]);
$this->assertSame($thomas, $v[2]);
$this->assertEquals(new Address('lucas@symfony.com'), $v[3]);
$this->assertSame($caramel, $v[4]);
$e = new Email();
$e->addBcc('lucas@symfony.com', $caramel);
$this->assertCount(2, $e->getBcc());
$e = new Email();
$e->bcc('lucas@symfony.com');
$e->bcc($caramel);
$this->assertSame([$caramel], $e->getBcc());
}
public function testPriority()
{
$e = new Email();
$this->assertEquals(3, $e->getPriority());
$e->priority(1);
$this->assertEquals(1, $e->getPriority());
$e->priority(10);
$this->assertEquals(5, $e->getPriority());
$e->priority(-10);
$this->assertEquals(1, $e->getPriority());
}
public function testGenerateBodyThrowsWhenEmptyBody()
{
$this->expectException(\LogicException::class);
(new Email())->getBody();
}
public function testGetBody()
{
$e = new Email();
$e->setBody($text = new TextPart('text content'));
$this->assertEquals($text, $e->getBody());
}
public function testGenerateBody()
{
$text = new TextPart('text content');
$html = new TextPart('html content', 'utf-8', 'html');
$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->text('text content');
$this->assertEquals($text, $e->getBody());
$this->assertEquals('text content', $e->getTextBody());
$e = new Email();
$e->html('html content');
$this->assertEquals($html, $e->getBody());
$this->assertEquals('html content', $e->getHtmlBody());
$e = new Email();
$e->html('html content');
$e->text('text content');
$this->assertEquals(new AlternativePart($text, $html), $e->getBody());
$e = new Email();
$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->attach($file);
$e->text('text content');
$this->assertEquals(new MixedPart($text, $att), $e->getBody());
$e = new Email();
$e->attach($file);
$e->html('html content');
$this->assertEquals(new MixedPart($html, $att), $e->getBody());
$e = new Email();
$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->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->text('text content');
$e->attach($file);
$e->attach($image, 'test.gif');
$this->assertEquals(new MixedPart($text, $att, $img), $e->getBody());
$e = new Email();
$e->html($content = 'html content <img src="test.gif">');
$e->text('text content');
$e->attach($file);
$e->attach($image, 'test.gif');
$fullhtml = new TextPart($content, 'utf-8', 'html');
$this->assertEquals(new MixedPart(new AlternativePart($text, $fullhtml), $att, $img), $e->getBody());
$e = new Email();
$e->html($content = 'html content <img src="cid:test.gif">');
$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());
$this->assertInstanceOf(RelatedPart::class, $related[0]);
$this->assertEquals($att, $related[1]);
$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());
$content = 'html content <img src="cid:test.gif">';
$r = fopen('php://memory', 'r+', false);
fwrite($r, $content);
rewind($r);
$e = new Email();
$e->html($r);
// embedding the same image twice results in one image only in the email
$e->embed($image, 'test.gif');
$e->embed($image, 'test.gif');
$body = $e->getBody();
$this->assertInstanceOf(RelatedPart::class, $body);
// 2 parts only, not 3 (text + embedded image once)
$this->assertCount(2, $parts = $body->getParts());
$this->assertStringMatchesFormat('html content <img src=3D"cid:%s@symfony">', $parts[0]->bodyToString());
}
public function testAttachments()
{
$contents = file_get_contents($name = __DIR__.'/Fixtures/mimetypes/test', 'r');
$att = new DataPart($file = fopen($name, 'r'), 'test');
$inline = (new DataPart($contents, 'test'))->asInline();
$e = new Email();
$e->attach($file, 'test');
$e->embed($contents, 'test');
$this->assertEquals([$att, $inline], $e->getAttachments());
$att = DataPart::fromPath($name, 'test');
$inline = DataPart::fromPath($name, 'test')->asInline();
$e = new Email();
$e->attachFromPath($name);
$e->embedFromPath($name);
$this->assertEquals([$att->bodyToString(), $inline->bodyToString()], array_map(function (DataPart $a) { return $a->bodyToString(); }, $e->getAttachments()));
$this->assertEquals([$att->getPreparedHeaders(), $inline->getPreparedHeaders()], array_map(function (DataPart $a) { return $a->getPreparedHeaders(); }, $e->getAttachments()));
}
public function testSerialize()
{
$r = fopen('php://memory', 'r+', false);
fwrite($r, 'Text content');
rewind($r);
$e = new Email();
$e->from('fabien@symfony.com');
$e->text($r);
$e->html($r);
$contents = file_get_contents($name = __DIR__.'/Fixtures/mimetypes/test', 'r');
$file = fopen($name, 'r');
$e->attach($file, 'test');
$expected = clone $e;
$n = unserialize(serialize($e));
$this->assertEquals($expected->getHeaders(), $n->getHeaders());
$this->assertEquals($e->getBody(), $n->getBody());
}
}

View File

@ -0,0 +1,158 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Tests\Encoder;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\Encoder\Base64Encoder;
class Base64EncoderTest extends TestCase
{
/*
There's really no point in testing the entire base64 encoding to the
level QP encoding has been tested. base64_encode() has been in PHP for
years.
*/
public function testInputOutputRatioIs3to4Bytes()
{
/*
RFC 2045, 6.8
The encoding process represents 24-bit groups of input bits as output
strings of 4 encoded characters. Proceeding from left to right, a
24-bit input group is formed by concatenating 3 8bit input groups.
These 24 bits are then treated as 4 concatenated 6-bit groups, each
of which is translated into a single digit in the base64 alphabet.
*/
$encoder = new Base64Encoder();
$this->assertEquals('MTIz', $encoder->encodeString('123'), '3 bytes of input should yield 4 bytes of output');
$this->assertEquals('MTIzNDU2', $encoder->encodeString('123456'), '6 bytes in input should yield 8 bytes of output');
$this->assertEquals('MTIzNDU2Nzg5', $encoder->encodeString('123456789'), '%s: 9 bytes in input should yield 12 bytes of output');
}
public function testPadLength()
{
/*
RFC 2045, 6.8
Special processing is performed if fewer than 24 bits are available
at the end of the data being encoded. A full encoding quantum is
always completed at the end of a body. When fewer than 24 input bits
are available in an input group, zero bits are added (on the right)
to form an integral number of 6-bit groups. Padding at the end of
the data is performed using the "=" character. Since all base64
input is an integral number of octets, only the following cases can
arise: (1) the final quantum of encoding input is an integral
multiple of 24 bits; here, the final unit of encoded output will be
an integral multiple of 4 characters with no "=" padding, (2) the
final quantum of encoding input is exactly 8 bits; here, the final
unit of encoded output will be two characters followed by two "="
padding characters, or (3) the final quantum of encoding input is
exactly 16 bits; here, the final unit of encoded output will be three
characters followed by one "=" padding character.
*/
$encoder = new Base64Encoder();
for ($i = 0; $i < 30; ++$i) {
$input = pack('C', random_int(0, 255));
$this->assertRegExp('~^[a-zA-Z0-9/\+]{2}==$~', $encoder->encodeString($input), 'A single byte should have 2 bytes of padding');
}
for ($i = 0; $i < 30; ++$i) {
$input = pack('C*', random_int(0, 255), random_int(0, 255));
$this->assertRegExp('~^[a-zA-Z0-9/\+]{3}=$~', $encoder->encodeString($input), 'Two bytes should have 1 byte of padding');
}
for ($i = 0; $i < 30; ++$i) {
$input = pack('C*', random_int(0, 255), random_int(0, 255), random_int(0, 255));
$this->assertRegExp('~^[a-zA-Z0-9/\+]{4}$~', $encoder->encodeString($input), 'Three bytes should have no padding');
}
}
public function testMaximumLineLengthIs76Characters()
{
/*
The encoded output stream must be represented in lines of no more
than 76 characters each. All line breaks or other characters not
found in Table 1 must be ignored by decoding software.
*/
$input =
'abcdefghijklmnopqrstuvwxyz'.
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.
'1234567890'.
'abcdefghijklmnopqrstuvwxyz'.
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.
'1234567890'.
'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
$output =
'YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQk'.//38
'NERUZHSElKS0xNTk9QUVJTVFVWV1hZWjEyMzQ1'."\r\n".//76 *
'Njc4OTBhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3'.//38
'h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFla'."\r\n".//76 *
'MTIzNDU2Nzg5MEFCQ0RFRkdISUpLTE1OT1BRUl'.//38
'NUVVZXWFla'; //48
$encoder = new Base64Encoder();
$this->assertEquals($output, $encoder->encodeString($input), 'Lines should be no more than 76 characters');
}
public function testMaximumLineLengthCanBeSpecified()
{
$input =
'abcdefghijklmnopqrstuvwxyz'.
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.
'1234567890'.
'abcdefghijklmnopqrstuvwxyz'.
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.
'1234567890'.
'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
$output =
'YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQk'.//38
'NERUZHSElKS0'."\r\n".//50 *
'xNTk9QUVJTVFVWV1hZWjEyMzQ1Njc4OTBhYmNk'.//38
'ZWZnaGlqa2xt'."\r\n".//50 *
'bm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1'.//38
'BRUlNUVVZXWF'."\r\n".//50 *
'laMTIzNDU2Nzg5MEFCQ0RFRkdISUpLTE1OT1BR'.//38
'UlNUVVZXWFla'; //50 *
$encoder = new Base64Encoder();
$this->assertEquals($output, $encoder->encodeString($input, 'utf-8', 0, 50), 'Lines should be no more than 100 characters');
}
public function testFirstLineLengthCanBeDifferent()
{
$input =
'abcdefghijklmnopqrstuvwxyz'.
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.
'1234567890'.
'abcdefghijklmnopqrstuvwxyz'.
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.
'1234567890'.
'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
$output =
'YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQk'.//38
'NERUZHSElKS0xNTk9QU'."\r\n".//57 *
'VJTVFVWV1hZWjEyMzQ1Njc4OTBhYmNkZWZnaGl'.//38
'qa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLT'."\r\n".//76 *
'E1OT1BRUlNUVVZXWFlaMTIzNDU2Nzg5MEFCQ0R'.//38
'FRkdISUpLTE1OT1BRUlNUVVZXWFla'; //67
$encoder = new Base64Encoder();
$this->assertEquals($output, $encoder->encodeString($input, 'utf-8', 19), 'First line offset is 19 so first line should be 57 chars long');
}
}

View File

@ -0,0 +1,23 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Tests\Encoder;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\Encoder\Base64MimeHeaderEncoder;
class Base64MimeHeaderEncoderTest extends TestCase
{
public function testNameIsB()
{
$this->assertEquals('B', (new Base64MimeHeaderEncoder())->getName());
}
}

View File

@ -0,0 +1,213 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Tests\Encoder;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\Encoder\QpEncoder;
class QpEncoderTest extends TestCase
{
/* -- RFC 2045, 6.7 --
(1) (General 8bit representation) Any octet, except a CR or
LF that is part of a CRLF line break of the canonical
(standard) form of the data being encoded, may be
represented by an "=" followed by a two digit
hexadecimal representation of the octet's value. The
digits of the hexadecimal alphabet, for this purpose,
are "0123456789ABCDEF". Uppercase letters must be
used; lowercase letters are not allowed. Thus, for
example, the decimal value 12 (US-ASCII form feed) can
be represented by "=0C", and the decimal value 61 (US-
ASCII EQUAL SIGN) can be represented by "=3D". This
rule must be followed except when the following rules
allow an alternative encoding.
*/
public function testPermittedCharactersAreNotEncoded()
{
/* -- RFC 2045, 6.7 --
(2) (Literal representation) Octets with decimal values of
33 through 60 inclusive, and 62 through 126, inclusive,
MAY be represented as the US-ASCII characters which
correspond to those octets (EXCLAMATION POINT through
LESS THAN, and GREATER THAN through TILDE,
respectively).
*/
$encoder = new QpEncoder();
foreach (array_merge(range(33, 60), range(62, 126)) as $ordinal) {
$char = \chr($ordinal);
$this->assertSame($char, $encoder->encodeString($char));
}
}
public function testWhiteSpaceAtLineEndingIsEncoded()
{
/* -- RFC 2045, 6.7 --
(3) (White Space) Octets with values of 9 and 32 MAY be
represented as US-ASCII TAB (HT) and SPACE characters,
respectively, but MUST NOT be so represented at the end
of an encoded line. Any TAB (HT) or SPACE characters
on an encoded line MUST thus be followed on that line
by a printable character. In particular, an "=" at the
end of an encoded line, indicating a soft line break
(see rule #5) may follow one or more TAB (HT) or SPACE
characters. It follows that an octet with decimal
value 9 or 32 appearing at the end of an encoded line
must be represented according to Rule #1. This rule is
necessary because some MTAs (Message Transport Agents,
programs which transport messages from one user to
another, or perform a portion of such transfers) are
known to pad lines of text with SPACEs, and others are
known to remove "white space" characters from the end
of a line. Therefore, when decoding a Quoted-Printable
body, any trailing white space on a line must be
deleted, as it will necessarily have been added by
intermediate transport agents.
*/
$encoder = new QpEncoder();
$HT = \chr(0x09); // 9
$SPACE = \chr(0x20); // 32
// HT
$string = 'a'.$HT.$HT."\r\n".'b';
$this->assertEquals('a'.$HT.'=09'."\r\n".'b', $encoder->encodeString($string));
// SPACE
$string = 'a'.$SPACE.$SPACE."\r\n".'b';
$this->assertEquals('a'.$SPACE.'=20'."\r\n".'b', $encoder->encodeString($string));
}
public function testCRLFIsLeftAlone()
{
/*
(4) (Line Breaks) A line break in a text body, represented
as a CRLF sequence in the text canonical form, must be
represented by a (RFC 822) line break, which is also a
CRLF sequence, in the Quoted-Printable encoding. Since
the canonical representation of media types other than
text do not generally include the representation of
line breaks as CRLF sequences, no hard line breaks
(i.e. line breaks that are intended to be meaningful
and to be displayed to the user) can occur in the
quoted-printable encoding of such types. Sequences
like "=0D", "=0A", "=0A=0D" and "=0D=0A" will routinely
appear in non-text data represented in quoted-
printable, of course.
Note that many implementations may elect to encode the
local representation of various content types directly
rather than converting to canonical form first,
encoding, and then converting back to local
representation. In particular, this may apply to plain
text material on systems that use newline conventions
other than a CRLF terminator sequence. Such an
implementation optimization is permissible, but only
when the combined canonicalization-encoding step is
equivalent to performing the three steps separately.
*/
$encoder = new QpEncoder();
$string = 'a'."\r\n".'b'."\r\n".'c'."\r\n";
$this->assertEquals($string, $encoder->encodeString($string));
}
public function testLinesLongerThan76CharactersAreSoftBroken()
{
/*
(5) (Soft Line Breaks) The Quoted-Printable encoding
REQUIRES that encoded lines be no more than 76
characters long. If longer lines are to be encoded
with the Quoted-Printable encoding, "soft" line breaks
must be used. An equal sign as the last character on a
encoded line indicates such a non-significant ("soft")
line break in the encoded text.
*/
$encoder = new QpEncoder();
$input = str_repeat('a', 140);
$output = '';
for ($i = 0; $i < 140; ++$i) {
// we read 4 chars at a time (max is 75)
if (18 * 4 /* 72 */ == $i) {
$output .= "=\r\n";
}
$output .= 'a';
}
$this->assertEquals($output, $encoder->encodeString($input));
}
public function testMaxLineLengthCanBeSpecified()
{
$encoder = new QpEncoder();
$input = str_repeat('a', 100);
$output = '';
for ($i = 0; $i < 100; ++$i) {
// we read 4 chars at a time (max is 53)
if (13 * 4 /* 52 */ == $i) {
$output .= "=\r\n";
}
$output .= 'a';
}
$this->assertEquals($output, $encoder->encodeString($input, 'utf-8', 0, 54));
}
public function testBytesBelowPermittedRangeAreEncoded()
{
// According to Rule (1 & 2)
$encoder = new QpEncoder();
foreach (range(0, 32) as $ordinal) {
$char = \chr($ordinal);
$this->assertEquals(sprintf('=%02X', $ordinal), $encoder->encodeString($char));
}
}
public function testDecimalByte61IsEncoded()
{
// According to Rule (1 & 2)
$encoder = new QpEncoder();
$this->assertEquals('=3D', $encoder->encodeString('='));
}
public function testBytesAbovePermittedRangeAreEncoded()
{
// According to Rule (1 & 2)
$encoder = new QpEncoder();
foreach (range(127, 255) as $ordinal) {
$this->assertSame(sprintf('=%02X', $ordinal), $encoder->encodeString(\chr($ordinal), 'iso-8859-1'));
}
}
public function testFirstLineLengthCanBeDifferent()
{
$encoder = new QpEncoder();
$input = str_repeat('a', 140);
$output = '';
for ($i = 0; $i < 140; ++$i) {
// we read 4 chars at a time (max is 54 for the first line and 75 for the second one)
if (13 * 4 == $i || 13 * 4 + 18 * 4 == $i) {
$output .= "=\r\n";
}
$output .= 'a';
}
$this->assertEquals($output, $encoder->encodeString($input, 'utf-8', 22), 'First line should start at offset 22 so can only have max length 54');
}
public function testTextIsPreWrapped()
{
$encoder = new QpEncoder();
$input = str_repeat('a', 70)."\r\n".str_repeat('a', 70)."\r\n".str_repeat('a', 70);
$this->assertEquals($input, $encoder->encodeString($input));
}
}

View File

@ -0,0 +1,139 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Tests\Encoder;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\Encoder\QpMimeHeaderEncoder;
class QpMimeHeaderEncoderTest extends TestCase
{
public function testNameIsQ()
{
$encoder = new QpMimeHeaderEncoder();
$this->assertEquals('Q', $encoder->getName());
}
public function testSpaceAndTabNeverAppear()
{
/* -- RFC 2047, 4.
Only a subset of the printable ASCII characters may be used in
'encoded-text'. Space and tab characters are not allowed, so that
the beginning and end of an 'encoded-word' are obvious.
*/
$encoder = new QpMimeHeaderEncoder();
$this->assertNotRegExp('~[ \t]~', $encoder->encodeString("a \t b"), 'encoded-words in headers cannot contain LWSP as per RFC 2047.');
}
public function testSpaceIsRepresentedByUnderscore()
{
/* -- RFC 2047, 4.2.
(2) The 8-bit hexadecimal value 20 (e.g., ISO-8859-1 SPACE) may be
represented as "_" (underscore, ASCII 95.). (This character may
not pass through some internetwork mail gateways, but its use
will greatly enhance readability of "Q" encoded data with mail
readers that do not support this encoding.) Note that the "_"
always represents hexadecimal 20, even if the SPACE character
occupies a different code position in the character set in use.
*/
$encoder = new QpMimeHeaderEncoder();
$this->assertEquals('a_b', $encoder->encodeString('a b'), 'Spaces can be represented by more readable underscores as per RFC 2047.');
}
public function testEqualsAndQuestionAndUnderscoreAreEncoded()
{
/* -- RFC 2047, 4.2.
(3) 8-bit values which correspond to printable ASCII characters other
than "=", "?", and "_" (underscore), MAY be represented as those
characters. (But see section 5 for restrictions.) In
particular, SPACE and TAB MUST NOT be represented as themselves
within encoded words.
*/
$encoder = new QpMimeHeaderEncoder();
$this->assertEquals('=3D=3F=5F', $encoder->encodeString('=?_'), 'Chars =, ? and _ (underscore) may not appear as per RFC 2047.');
}
public function testParensAndQuotesAreEncoded()
{
/* -- RFC 2047, 5 (2).
A "Q"-encoded 'encoded-word' which appears in a 'comment' MUST NOT
contain the characters "(", ")" or "
*/
$encoder = new QpMimeHeaderEncoder();
$this->assertEquals('=28=22=29', $encoder->encodeString('(")'), 'Chars (, " (DQUOTE) and ) may not appear as per RFC 2047.');
}
public function testOnlyCharactersAllowedInPhrasesAreUsed()
{
/* -- RFC 2047, 5.
(3) As a replacement for a 'word' entity within a 'phrase', for example,
one that precedes an address in a From, To, or Cc header. The ABNF
definition for 'phrase' from RFC 822 thus becomes:
phrase = 1*( encoded-word / word )
In this case the set of characters that may be used in a "Q"-encoded
'encoded-word' is restricted to: <upper and lower case ASCII
letters, decimal digits, "!", "*", "+", "-", "/", "=", and "_"
(underscore, ASCII 95.)>. An 'encoded-word' that appears within a
'phrase' MUST be separated from any adjacent 'word', 'text' or
'special' by 'linear-white-space'.
*/
$allowedBytes = array_merge(
range(\ord('a'), \ord('z')), range(\ord('A'), \ord('Z')),
range(\ord('0'), \ord('9')),
[\ord('!'), \ord('*'), \ord('+'), \ord('-'), \ord('/')]
);
$encoder = new QpMimeHeaderEncoder();
foreach (range(0x00, 0xFF) as $byte) {
$char = pack('C', $byte);
$encodedChar = $encoder->encodeString($char, 'iso-8859-1');
if (\in_array($byte, $allowedBytes)) {
$this->assertEquals($char, $encodedChar, 'Character '.$char.' should not be encoded.');
} elseif (0x20 == $byte) {
// special case
$this->assertEquals('_', $encodedChar, 'Space character should be replaced.');
} else {
$this->assertEquals(sprintf('=%02X', $byte), $encodedChar, 'Byte '.$byte.' should be encoded.');
}
}
}
public function testEqualsNeverAppearsAtEndOfLine()
{
/* -- RFC 2047, 5 (3).
The 'encoded-text' in an 'encoded-word' must be self-contained;
'encoded-text' MUST NOT be continued from one 'encoded-word' to
another. This implies that the 'encoded-text' portion of a "B"
'encoded-word' will be a multiple of 4 characters long; for a "Q"
'encoded-word', any "=" character that appears in the 'encoded-text'
portion will be followed by two hexadecimal characters.
*/
$input = str_repeat('a', 140);
$output = '';
$seq = 0;
for (; $seq < 140; ++$seq) {
// compute the end of line (multiple of 4 chars)
if (18 * 4 === $seq) {
$output .= "\r\n"; // =\r\n
}
$output .= 'a';
}
$encoder = new QpMimeHeaderEncoder();
$this->assertEquals($output, $encoder->encodeString($input));
}
}

View File

@ -0,0 +1,129 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Tests\Encoder;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\Encoder\Rfc2231Encoder;
class Rfc2231EncoderTest extends TestCase
{
private $rfc2045Token = '/^[\x21\x23-\x27\x2A\x2B\x2D\x2E\x30-\x39\x41-\x5A\x5E-\x7E]+$/D';
/* --
This algorithm is described in RFC 2231, but is barely touched upon except
for mentioning bytes can be represented as their octet values (e.g. %20 for
the SPACE character).
The tests here focus on how to use that representation to always generate text
which matches RFC 2045's definition of "token".
*/
public function testEncodingAsciiCharactersProducesValidToken()
{
$string = '';
foreach (range(0x00, 0x7F) as $octet) {
$char = pack('C', $octet);
$string .= $char;
}
$encoder = new Rfc2231Encoder();
$encoded = $encoder->encodeString($string);
foreach (explode("\r\n", $encoded) as $line) {
$this->assertRegExp($this->rfc2045Token, $line, 'Encoder should always return a valid RFC 2045 token.');
}
}
public function testEncodingNonAsciiCharactersProducesValidToken()
{
$string = '';
foreach (range(0x80, 0xFF) as $octet) {
$char = pack('C', $octet);
$string .= $char;
}
$encoder = new Rfc2231Encoder();
$encoded = $encoder->encodeString($string);
foreach (explode("\r\n", $encoded) as $line) {
$this->assertRegExp($this->rfc2045Token, $line, 'Encoder should always return a valid RFC 2045 token.');
}
}
public function testMaximumLineLengthCanBeSet()
{
$string = '';
for ($x = 0; $x < 200; ++$x) {
$char = 'a';
$string .= $char;
}
$encoder = new Rfc2231Encoder();
$encoded = $encoder->encodeString($string, 'utf-8', 0, 75);
// 72 here and not 75 as we read 4 chars at a time
$this->assertEquals(
str_repeat('a', 72)."\r\n".
str_repeat('a', 72)."\r\n".
str_repeat('a', 56),
$encoded,
'Lines should be wrapped at each 72 characters'
);
}
public function testFirstLineCanHaveShorterLength()
{
$string = '';
for ($x = 0; $x < 200; ++$x) {
$char = 'a';
$string .= $char;
}
$encoder = new Rfc2231Encoder();
$encoded = $encoder->encodeString($string, 'utf-8', 24, 72);
$this->assertEquals(
str_repeat('a', 48)."\r\n".
str_repeat('a', 72)."\r\n".
str_repeat('a', 72)."\r\n".
str_repeat('a', 8),
$encoded,
'First line should be 24 bytes shorter than the others.'
);
}
public function testEncodingAndDecodingSamples()
{
$dir = realpath(__DIR__.'/../Fixtures/samples/charsets');
$sampleFp = opendir($dir);
while (false !== $encoding = readdir($sampleFp)) {
if ('.' == substr($encoding, 0, 1)) {
continue;
}
$encoder = new Rfc2231Encoder();
if (is_dir($dir.'/'.$encoding)) {
$fileFp = opendir($dir.'/'.$encoding);
while (false !== $sampleFile = readdir($fileFp)) {
if ('.' == substr($sampleFile, 0, 1)) {
continue;
}
$text = file_get_contents($dir.'/'.$encoding.'/'.$sampleFile);
$encodedText = $encoder->encodeString($text, $encoding);
$this->assertEquals(
urldecode(implode('', explode("\r\n", $encodedText))), $text,
'Encoded string should decode back to original string for sample '.$dir.'/'.$encoding.'/'.$sampleFile
);
}
closedir($fileFp);
}
}
closedir($sampleFp);
}
}

View File

Before

Width:  |  Height:  |  Size: 35 B

After

Width:  |  Height:  |  Size: 35 B

View File

Before

Width:  |  Height:  |  Size: 35 B

After

Width:  |  Height:  |  Size: 35 B

View File

@ -0,0 +1,11 @@
ISO-2022-JPは、インターネット上(特に電子メール)などで使われる日本の文字用の文字符号化方式。ISO/IEC 2022のエスケープシーケンスを利用して文字集合を切り替える7ビットのコードであることを特徴とする (アナウンス機能のエスケープシーケンスは省略される)。俗に「JISコード」と呼ばれることもある。
概要
日本語表記への利用が想定されている文字コードであり、日本語の利用されるネットワークにおいて、日本の規格を応用したものである。また文字集合としては、日本語で用いられる漢字、ひらがな、カタカナはもちろん、ラテン文字、ギリシア文字、キリル文字なども含んでおり、学術や産業の分野での利用も考慮たものとなっている。規格名に、ISOの日本語の言語コードであるjaではなく、国・地域名コードのJPが示されているゆえんである。
文字集合としてJIS X 0201のC0集合制御文字、JIS X 0201のラテン文字集合、ISO 646の国際基準版図形文字、JIS X 0208の1978年版JIS C 6226-1978と1983年および1990年版が利用できる。JIS X 0201の片仮名文字集合は利用できない。1986年以降、日本の電子メールで用いられてきたJUNETコードを、村井純・Mark Crispin・Erik van der Poelが1993年にRFC化したもの(RFC 1468)。後にJIS X 0208:1997の附属書2としてJISに規定された。MIMEにおける文字符号化方式の識別用の名前として IANA に登録されている。
なお、符号化の仕様についてはISO/IEC 2022#ISO-2022-JPも参照。
ISO-2022-JPと非標準的拡張使用
「JISコード」または「ISO-2022-JP」というコード名の規定下では、その仕様通りの使用が求められる。しかし、Windows OS上では、実際にはCP932コード (MicrosoftによるShift JISを拡張した亜種。ISO-2022-JP規定外文字が追加されている。による独自拡張の文字を断りなく使うアプリケーションが多い。この例としてInternet ExplorerやOutlook Expressがある。また、EmEditor、秀丸エディタやThunderbirdのようなMicrosoft社以外のWindowsアプリケーションでも同様の場合がある。この場合、ISO-2022-JPの範囲外の文字を使ってしまうと、異なる製品間では未定義不明文字として認識されるか、もしくは文字化けを起こす原因となる。そのため、Windows用の電子メールクライアントであっても独自拡張の文字を使用すると警告を出したり、あえて使えないように制限しているものも存在する。さらにはISO-2022-JPの範囲内であってもCP932は非標準文字FULLWIDTH TILDE等を持つので文字化けの原因になり得る。
また、符号化方式名をISO-2022-JPとしているのに、文字集合としてはJIS X 0212 (いわゆる補助漢字) やJIS X 0201の片仮名文字集合 (いわゆる半角カナ) をも符号化している例があるが、ISO-2022-JPではこれらの文字を許容していない。これらの符号化は独自拡張の実装であり、中にはISO/IEC 2022の仕様に準拠すらしていないものもある[2]。従って受信側の電子メールクライアントがこれらの独自拡張に対応していない場合、その文字あるいはその文字を含む行、時にはテキスト全体が文字化けすることがある。

View File

@ -0,0 +1,19 @@
Op mat eraus hinnen beschte, rou zënne schaddreg ké. Ké sin Eisen Kaffi prächteg, den haut esou Fielse wa, Well zielen d'Welt am dir. Aus grousse rëschten d'Stroos do, as dat Kléder gewëss d'Kàchen. Schied gehéiert d'Vioule net hu, rou ke zënter Säiten d'Hierz. Ze eise Fletschen mat, gei as gréng d'Lëtzebuerger. Wäit räich no mat.
Säiten d'Liewen aus en. Un gëtt bléit lossen wee, da wéi alle weisen Kolrettchen. Et deser d'Pan d'Kirmes vun, en wuel Benn rëschten méi. En get drem ménger beschte, da wär Stad welle. Nun Dach d'Pied do, mä gét ruffen gehéiert. Ze onser ugedon fir, d'Liewen Plett'len ech no, si Räis wielen bereet wat. Iwer spilt fir jo.
An hin däischter Margréitchen, eng ke Frot brommt, vu den Räis néierens. Da hir Hunn Frot nozegon, rout Fläiß Himmel zum si, net gutt Kaffi Gesträich fu. Vill lait Gaart sou wa, Land Mamm Schuebersonndeg rei do. Gei geet Minutt en, gei d'Leit beschte Kolrettchen et, Mamm fergiess un hun.
Et gutt Heck kommen oft, Lann rëscht rei um, Hunn rëscht schéinste ke der. En lait zielen schnéiwäiss hir, fu rou botze éiweg Minutt, rem fest gudden schaddreg en. Noper bereet Margréitchen mat op, dem denkt d'Leit d'Vioule no, oft ké Himmel Hämmel. En denkt blénken Fréijor net, Gart Schiet d'Natur no wou. No hin Ierd Frot d'Kirmes. Hire aremt un rou, ké den éiweg wielen Milliounen.
Mir si Hunn Blénkeg. Ké get ston derfir d'Kàchen. Haut d'Pan fu ons, dé frou löschteg d'Meereische rei. Sou op wuel Léift. Stret schlon grousse gin hu. Mä denkt d'Leit hinnen net, ké gét haut fort rëscht.
Koum d'Pan hannendrun ass ké, ké den brét Kaffi geplot. Schéi Hären d'Pied fu gét, do d'Mier néierens bei. Rëm päift Hämmel am, wee Engel beschéngt mä. Brommt klinzecht der ke, wa rout jeitzt dén. Get Zalot d'Vioule däischter da, jo fir Bänk päift duerch, bei d'Beem schéinen Plett'len jo. Den haut Faarwen ze, eng en Biereg Kirmesdag, um sin alles Faarwen d'Vioule.
Eng Hunn Schied et, wat wa Frot fest gebotzt. Bei jo bleiwe ruffen Klarinett. Un Feld klinzecht gét, rifft Margréitchen rem ke. Mir dé Noper duurch gewëss, ston sech kille sin en. Gei Stret d'Wise um, Haus Gart wee as. Monn ménger an blo, wat da Gart gefällt Hämmelsbrot.
Brommt geplot och ze, dat wa Räis Well Kaffi. Do get spilt prächteg, as wär kille bleiwe gewalteg. Onser frësch Margréitchen rem ke, blo en huet ugedon. Onser Hemecht wär de, hu eraus d'Sonn dat, eise deser hannendrun da och.
As durch Himmel hun, no fest iw'rem schéinste mir, Hunn séngt Hierz ke zum. Séngt iw'rem d'Natur zum an. Ke wär gutt Grénge. Kënnt gudden prächteg mä rei. Dé dir Blénkeg Klarinett Kolrettchen, da fort muerges d'Kanner wou, main Feld ruffen vu wéi. Da gin esou Zalot gewalteg, gét vill Hemecht blénken dé.
Haut gréng nun et, nei vu Bass gréng d'Gaassen. Fest d'Beem uechter si gin. Oft vu sinn wellen kréien. Et ass lait Zalot schéinen.

View File

@ -0,0 +1,22 @@
Код одно гринспана руководишь на. Его вы знания движение. Ты две начать
одиночку, сказать основатель удовольствием но миф. Бы какие система тем.
Полностью использует три мы, человек клоунов те нас, бы давать творческую
эзотерическая шеф.
Мог не помнить никакого сэкономленного, две либо какие пишите бы. Должен
компанию кто те, этот заключалась проектировщик не ты. Глупые периоды ты
для. Вам который хороший он. Те любых кремния концентрируются мог,
собирать принадлежите без вы.
Джоэла меньше хорошего вы миф, за тем году разработки. Даже управляющим
руководители был не. Три коде выпускать заботиться ну. То его система
удовольствием безостановочно, или ты главной процессорах. Мы без джоэл
знания получат, статьи остальные мы ещё.
Них русском касается поскольку по, образование должником
систематизированный ну мои. Прийти кандидата университет но нас, для бы
должны никакого, биг многие причин интервьюирования за.
Тем до плиту почему. Вот учёт такие одного бы, об биг разным внешних
промежуток. Вас до какому возможностей безответственный, были погодите бы
его, по них глупые долгий количества.

View File

@ -0,0 +1,45 @@
Αν ήδη διάβασε γλιτώσει μεταγλωτίσει, αυτήν θυμάμαι μου μα. Την κατάσταση χρησιμοποίησέ να! Τα διαφορά φαινόμενο διολισθήσεις πες, υψηλότερη προκαλείς περισσότερες όχι κι. Με ελέγχου γίνεται σας, μικρής δημιουργούν τη του. Τις τα γράψει εικόνες απαράδεκτη?
Να ότι πρώτοι απαραίτητο. Άμεση πετάνε κακόκεφος τον ώς, να χώρου πιθανότητες του. Το μέχρι ορίστε λιγότερους σας. Πω ναί φυσικά εικόνες.
Μου οι κώδικα αποκλειστικούς, λες το μάλλον συνεχώς. Νέου σημεία απίστευτα σας μα. Χρόνου μεταγλωτιστής σε νέα, τη τις πιάνει μπορούσες προγραμματιστές. Των κάνε βγαίνει εντυπωσιακό τα? Κρατάει τεσσαρών δυστυχώς της κι, ήδη υψηλότερη εξακολουθεί τα?
Ώρα πετάνε μπορούσε λιγότερους αν, τα απαράδεκτη συγχωνευτεί ροή. Τη έγραψες συνηθίζουν σαν. Όλα με υλικό στήλες χειρότερα. Ανώδυνη δουλέψει επί ως, αν διαδίκτυο εσωτερικών παράγοντες από. Κεντρικό επιτυχία πες το.
Πω ναι λέει τελειώσει, έξι ως έργων τελειώσει. Με αρχεία βουτήξουν ανταγωνιστής ώρα, πολύ γραφικά σελίδων τα στη. Όρο οέλεγχος δημιουργούν δε, ας θέλεις ελέγχου συντακτικό όρο! Της θυμάμαι επιδιόρθωση τα. Για μπορούσε περισσότερο αν, μέγιστη σημαίνει αποφάσισε τα του, άτομο αποτελέσει τι στα.
Τι στην αφήσεις διοίκηση στη. Τα εσφαλμένη δημιουργια επιχείριση έξι! Βήμα μαγικά εκτελέσει ανά τη. Όλη αφήσεις συνεχώς εμπορικά αν, το λες κόλπα επιτυχία. Ότι οι ζώνη κειμένων. Όρο κι ρωτάει γραμμής πελάτες, τελειώσει διολισθήσεις καθυστερούσε αν εγώ? Τι πετούν διοίκηση προβλήματα ήδη.
Τη γλιτώσει αποθηκευτικού μια. Πω έξι δημιουργια πιθανότητες, ως πέντε ελέγχους εκτελείται λες. Πως ερωτήσεις διοικητικό συγκεντρωμένοι οι, ας συνεχώς διοικητικό αποστηθίσει σαν. Δε πρώτες συνεχώς διολισθήσεις έχω, από τι κανένας βουτήξουν, γειτονιάς προσεκτικά ανταγωνιστής κι σαν.
Δημιουργια συνηθίζουν κλπ τι? Όχι ποσοστό διακοπής κι. Κλπ φακέλους δεδομένη εξοργιστικά θα? Υποψήφιο καθορίζουν με όλη, στα πήρε προσοχή εταιρείες πω, ώς τον συνάδελφος διοικητικό δημιουργήσεις! Δούλευε επιτίθενται σας θα, με ένας παραγωγικής ένα, να ναι σημεία μέγιστη απαράδεκτη?
Σας τεσσαρών συνεντεύξης τη, αρπάζεις σίγουρος μη για', επί τοπικές εντολές ακούσει θα? Ως δυστυχής μεταγλωτιστής όλη, να την είχαν σφάλμα απαραίτητο! Μην ώς άτομο διορθώσει χρησιμοποιούνταν. Δεν τα κόλπα πετάξαμε, μη που άγχος υόρκη άμεση, αφού δυστυχώς διακόψουμε όρο αν! Όλη μαγικά πετάνε επιδιορθώσεις δε, ροή φυσικά αποτελέσει πω.
Άπειρα παραπάνω φαινόμενο πω ώρα, σαν πόρτες κρατήσουν συνηθίζουν ως. Κι ώρα τρέξει είχαμε εφαρμογή. Απλό σχεδιαστής μεταγλωτιστής ας επί, τις τα όταν έγραψες γραμμής? Όλα κάνεις συνάδελφος εργαζόμενοι θα, χαρτιού χαμηλός τα ροή. Ως ναι όροφο έρθει, μην πελάτες αποφάσισε μεταφραστής με, να βιαστικά εκδόσεις αναζήτησης λες. Των φταίει εκθέσεις προσπαθήσεις οι, σπίτι αποστηθίσει ας λες?
Ώς που υπηρεσία απαραίτητο δημιουργείς. Μη άρα χαρά καθώς νύχτας, πω ματ μπουν είχαν. Άμεση δημιουργείς ώς ροή, γράψει γραμμής σίγουρος στα τι! Αν αφού πρώτοι εργαζόμενων ναί.
Άμεση διορθώσεις με δύο? Έχουν παράδειγμα των θα, μου έρθει θυμάμαι περισσότερο το. Ότι θα αφού χρειάζονται περισσότερες. Σαν συνεχώς περίπου οι.
Ώς πρώτης πετάξαμε λες, όρο κι πρώτες ζητήσεις δυστυχής. Ανά χρόνου διακοπή επιχειρηματίες ας, ώς μόλις άτομο χειρότερα όρο, κρατάει σχεδιαστής προσπαθήσεις νέο το. Πουλάς προσθέσει όλη πω, τύπου χαρακτηριστικό εγώ σε, πω πιο δούλευε αναζήτησης? Αναφορά δίνοντας σαν μη, μάθε δεδομένη εσωτερικών με ναι, αναφέρονται περιβάλλοντος ώρα αν. Και λέει απόλαυσε τα, που το όροφο προσπαθούν?
Πάντα χρόνου χρήματα ναι το, σαν σωστά θυμάμαι σκεφτείς τα. Μα αποτελέσει ανεπιθύμητη την, πιο το τέτοιο ατόμου, τη των τρόπο εργαλείων επιδιόρθωσης. Περιβάλλον παραγωγικής σου κι, κλπ οι τύπου κακόκεφους αποστηθίσει, δε των πλέον τρόποι. Πιθανότητες χαρακτηριστικών σας κι, γραφικά δημιουργήσεις μια οι, πω πολλοί εξαρτάται προσεκτικά εδώ. Σταματάς παράγοντες για' ώς, στις ρωτάει το ναι! Καρέκλα ζητήσεις συνδυασμούς τη ήδη!
Για μαγικά συνεχώς ακούσει το. Σταματάς προϊόντα βουτήξουν ώς ροή. Είχαν πρώτες οι ναι, μα λες αποστηθίσει ανακαλύπτεις. Όροφο άλγεβρα παραπάνω εδώ τη, πρόσληψη λαμβάνουν καταλάθος ήδη ας? Ως και εισαγωγή κρατήσουν, ένας κακόκεφους κι μας, όχι κώδικάς παίξουν πω. Πω νέα κρατάει εκφράσουν, τότε τελικών τη όχι, ας της τρέξει αλλάζοντας αποκλειστικούς.
Ένας βιβλίο σε άρα, ναι ως γράψει ταξινομεί διορθώσεις! Εδώ να γεγονός συγγραφείς, ώς ήδη διακόψουμε επιχειρηματίες? Ότι πακέτων εσφαλμένη κι, θα όρο κόλπα παραγωγικής? Αν έχω κεντρικό υψηλότερη, κι δεν ίδιο πετάνε παρατηρούμενη! Που λοιπόν σημαντικό μα, προκαλείς χειροκροτήματα ως όλα, μα επί κόλπα άγχος γραμμές! Δε σου κάνεις βουτήξουν, μη έργων επενδυτής χρησιμοποίησέ στα, ως του πρώτες διάσημα σημαντικό.
Βιβλίο τεράστιο προκύπτουν σαν το, σαν τρόπο επιδιόρθωση ας. Είχαν προσοχή προσπάθεια κι ματ, εδώ ως έτσι σελίδων συζήτηση. Και στην βγαίνει εσφαλμένη με, δυστυχής παράδειγμα δε μας, από σε υόρκη επιδιόρθωσης. Νέα πω νέου πιθανό, στήλες συγγραφείς μπαίνοντας μα για', το ρωτήσει κακόκεφους της? Μου σε αρέσει συγγραφής συγχωνευτεί, μη μου υόρκη ξέχασε διακοπής! Ώς επί αποφάσισε αποκλειστικούς χρησιμοποιώντας, χρήματα σελίδων ταξινομεί ναι με.
Μη ανά γραμμή απόλαυσε, πω ναι μάτσο διασφαλίζεται. Τη έξι μόλις εργάστηκε δημιουργούν, έκδοση αναφορά δυσκολότερο οι νέο. Σας ως μπορούσε παράδειγμα, αν ότι δούλευε μπορούσε αποκλειστικούς, πιο λέει βουτήξουν διορθώσει ως. Έχω τελευταία κακόκεφους ας, όσο εργαζόμενων δημιουργήσεις τα.
Του αν δουλέψει μπορούσε, πετούν χαμηλός εδώ ας? Κύκλο τύπους με που, δεν σε έχουν συνεχώς χειρότερα, τις τι απαράδεκτη συνηθίζουν? Θα μην τους αυτήν, τη ένα πήρε πακέτων, κι προκύπτουν περιβάλλον πως. Μα για δουλέψει απόλαυσε εφαμοργής, ώς εδώ σημαίνει μπορούσες, άμεση ακούσει προσοχή τη εδώ?
Στα δώσε αθόρυβες λιγότερους οι, δε αναγκάζονται αποκλειστικούς όλα! Ας μπουν διοικητικό μια, πάντα ελέγχου διορθώσεις ώς τον. Ότι πήρε κανόνα μα. Που άτομα κάνεις δημιουργίες τα, οι μας αφού κόλπα προγραμματιστής, αφού ωραίο προκύπτουν στα ως. Θέμα χρησιμοποιήσει αν όλα, του τα άλγεβρα σελίδων. Τα ότι ανώδυνη δυστυχώς συνδυασμούς, μας οι πάντα γνωρίζουμε ανταγωνιστής, όχι τα δοκιμάσεις σχεδιαστής! Στην συνεντεύξης επιδιόρθωση πιο τα, μα από πουλάς περιβάλλον παραγωγικής.
Έχουν μεταγλωτίσει σε σας, σε πάντα πρώτης μειώσει των, γράψει ρουτίνα δυσκολότερο ήδη μα? Ταξινομεί διορθώσεις να μας. Θα της προσπαθούν περιεχόμενα, δε έχω τοπικές στέλνοντάς. Ανά δε αλφα άμεση, κάποιο ρωτάει γνωρίζουμε πω στη, φράση μαγικά συνέχεια δε δύο! Αν είχαμε μειώσει ροή, μας μετράει καθυστερούσε επιδιορθώσεις μη. Χάος υόρκη κεντρικό έχω σε, ανά περίπου αναγκάζονται πω.
Όσο επιστρέφουν χρονοδιαγράμματα μη. Πως ωραίο κακόκεφος διαχειριστής ως, τις να διακοπής αναζήτησης. Κάποιο ποσοστό ταξινομεί επί τη? Μάθε άμεση αλλάζοντας δύο με, μου νέου πάντα να.
Πω του δυστυχώς πιθανότητες. Κι ρωτάει υψηλότερη δημιουργια ότι, πω εισαγωγή τελευταία απομόνωση ναι. Των ζητήσεις γνωρίζουμε ώς? Για' μη παραδοτέου αναφέρονται! Ύψος παραγωγικά ροή ως, φυσικά διάβασε εικόνες όσο σε? Δεν υόρκη διορθώσεις επεξεργασία θα, ως μέση σύστημα χρησιμοποιήσει τις.

View File

@ -0,0 +1,3 @@
रखति आवश्यकत प्रेरना मुख्यतह हिंदी किएलोग असक्षम कार्यलय करते विवरण किके मानसिक दिनांक पुर्व संसाध एवम् कुशलता अमितकुमार प्रोत्साहित जनित देखने उदेशीत विकसित बलवान ब्रौशर किएलोग विश्लेषण लोगो कैसे जागरुक प्रव्रुति प्रोत्साहित सदस्य आवश्यकत प्रसारन उपलब्धता अथवा हिंदी जनित दर्शाता यन्त्रालय बलवान अतित सहयोग शुरुआत सभीकुछ माहितीवानीज्य लिये खरिदे है।अभी एकत्रित सम्पर्क रिती मुश्किल प्राथमिक भेदनक्षमता विश्व उन्हे गटको द्वारा तकरीबन
विश्व द्वारा व्याख्या सके। आजपर वातावरण व्याख्यान पहोच। हमारी कीसे प्राथमिक विचारशिलता पुर्व करती कम्प्युटर भेदनक्षमता लिये बलवान और्४५० यायेका वार्तालाप सुचना भारत शुरुआत लाभान्वित पढाए संस्था वर्णित मार्गदर्शन चुनने

View File

@ -0,0 +1,80 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Tests\Header;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\Header\DateHeader;
class DateHeaderTest extends TestCase
{
/* --
The following tests refer to RFC 2822, section 3.6.1 and 3.3.
*/
public function testGetDateTime()
{
$header = new DateHeader('Date', $dateTime = new \DateTimeImmutable());
$this->assertSame($dateTime, $header->getDateTime());
}
public function testDateTimeCanBeSetBySetter()
{
$header = new DateHeader('Date', new \DateTimeImmutable());
$header->setDateTime($dateTime = new \DateTimeImmutable());
$this->assertSame($dateTime, $header->getDateTime());
}
public function testDateTimeIsConvertedToImmutable()
{
$dateTime = new \DateTime();
$header = new DateHeader('Date', $dateTime);
$this->assertInstanceOf('DateTimeImmutable', $header->getDateTime());
$this->assertEquals($dateTime->getTimestamp(), $header->getDateTime()->getTimestamp());
$this->assertEquals($dateTime->getTimezone(), $header->getDateTime()->getTimezone());
}
public function testDateTimeIsImmutable()
{
$header = new DateHeader('Date', $dateTime = new \DateTime('2000-01-01 12:00:00 Europe/Berlin'));
$dateTime->setDate(2002, 2, 2);
$this->assertEquals('Sat, 01 Jan 2000 12:00:00 +0100', $header->getDateTime()->format('r'));
$this->assertEquals('Sat, 01 Jan 2000 12:00:00 +0100', $header->getBodyAsString());
}
public function testDateTimeIsConvertedToRfc2822Date()
{
$header = new DateHeader('Date', $dateTime = new \DateTimeImmutable('2000-01-01 12:00:00 Europe/Berlin'));
$header->setDateTime($dateTime);
$this->assertEquals('Sat, 01 Jan 2000 12:00:00 +0100', $header->getBodyAsString());
}
public function testSetBody()
{
$header = new DateHeader('Date', $dateTime = new \DateTimeImmutable());
$header->setBody($dateTime);
$this->assertEquals($dateTime->format('r'), $header->getBodyAsString());
}
public function testGetBody()
{
$header = new DateHeader('Date', $dateTime = new \DateTimeImmutable());
$header->setDateTime($dateTime);
$this->assertEquals($dateTime, $header->getBody());
}
public function testToString()
{
$header = new DateHeader('Date', $dateTime = new \DateTimeImmutable('2000-01-01 12:00:00 Europe/Berlin'));
$header->setDateTime($dateTime);
$this->assertEquals('Date: Sat, 01 Jan 2000 12:00:00 +0100', $header->toString());
}
}

View File

@ -0,0 +1,234 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Tests\Header;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Header\Headers;
use Symfony\Component\Mime\Header\IdentificationHeader;
use Symfony\Component\Mime\Header\MailboxListHeader;
use Symfony\Component\Mime\Header\UnstructuredHeader;
class HeadersTest extends TestCase
{
public function testAddMailboxListHeaderDelegatesToFactory()
{
$headers = new Headers();
$headers->addMailboxListHeader('From', ['person@domain']);
$this->assertNotNull($headers->get('From'));
}
public function testAddDateHeaderDelegatesToFactory()
{
$dateTime = new \DateTimeImmutable();
$headers = new Headers();
$headers->addDateHeader('Date', $dateTime);
$this->assertNotNull($headers->get('Date'));
}
public function testAddTextHeaderDelegatesToFactory()
{
$headers = new Headers();
$headers->addTextHeader('Subject', 'some text');
$this->assertNotNull($headers->get('Subject'));
}
public function testAddParameterizedHeaderDelegatesToFactory()
{
$headers = new Headers();
$headers->addParameterizedHeader('Content-Type', 'text/plain', ['charset' => 'utf-8']);
$this->assertNotNull($headers->get('Content-Type'));
}
public function testAddIdHeaderDelegatesToFactory()
{
$headers = new Headers();
$headers->addIdHeader('Message-ID', 'some@id');
$this->assertNotNull($headers->get('Message-ID'));
}
public function testAddPathHeaderDelegatesToFactory()
{
$headers = new Headers();
$headers->addPathHeader('Return-Path', 'some@path');
$this->assertNotNull($headers->get('Return-Path'));
}
public function testHasReturnsFalseWhenNoHeaders()
{
$headers = new Headers();
$this->assertFalse($headers->has('Some-Header'));
}
public function testAddedMailboxListHeaderIsSeenByHas()
{
$headers = new Headers();
$headers->addMailboxListHeader('From', ['person@domain']);
$this->assertTrue($headers->has('From'));
}
public function testAddedDateHeaderIsSeenByHas()
{
$dateTime = new \DateTimeImmutable();
$headers = new Headers();
$headers->addDateHeader('Date', $dateTime);
$this->assertTrue($headers->has('Date'));
}
public function testAddedTextHeaderIsSeenByHas()
{
$headers = new Headers();
$headers->addTextHeader('Subject', 'some text');
$this->assertTrue($headers->has('Subject'));
}
public function testAddedParameterizedHeaderIsSeenByHas()
{
$headers = new Headers();
$headers->addParameterizedHeader('Content-Type', 'text/plain', ['charset' => 'utf-8']);
$this->assertTrue($headers->has('Content-Type'));
}
public function testAddedIdHeaderIsSeenByHas()
{
$headers = new Headers();
$headers->addIdHeader('Message-ID', 'some@id');
$this->assertTrue($headers->has('Message-ID'));
}
public function testAddedPathHeaderIsSeenByHas()
{
$headers = new Headers();
$headers->addPathHeader('Return-Path', 'some@path');
$this->assertTrue($headers->has('Return-Path'));
}
public function testNewlySetHeaderIsSeenByHas()
{
$headers = new Headers();
$headers->add(new UnstructuredHeader('X-Foo', 'bar'));
$this->assertTrue($headers->has('X-Foo'));
}
public function testHasCanDistinguishMultipleHeaders()
{
$headers = new Headers();
$headers->addTextHeader('X-Test', 'some@id');
$headers->addTextHeader('X-Test', 'other@id');
$this->assertTrue($headers->has('X-Test'));
}
public function testGet()
{
$header = new IdentificationHeader('Message-ID', 'some@id');
$headers = new Headers();
$headers->addIdHeader('Message-ID', 'some@id');
$this->assertEquals($header->toString(), $headers->get('Message-ID')->toString());
}
public function testGetReturnsNullIfHeaderNotSet()
{
$headers = new Headers();
$this->assertNull($headers->get('Message-ID'));
}
public function testGetAllReturnsAllHeadersMatchingName()
{
$header0 = new UnstructuredHeader('X-Test', 'some@id');
$header1 = new UnstructuredHeader('X-Test', 'other@id');
$header2 = new UnstructuredHeader('X-Test', 'more@id');
$headers = new Headers();
$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')));
}
public function testGetAllReturnsAllHeadersIfNoArguments()
{
$header0 = new IdentificationHeader('Message-ID', 'some@id');
$header1 = new UnstructuredHeader('Subject', 'thing');
$header2 = new MailboxListHeader('To', [new Address('person@example.org')]);
$headers = new Headers();
$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()));
}
public function testGetAllReturnsEmptyArrayIfNoneSet()
{
$headers = new Headers();
$this->assertEquals([], iterator_to_array($headers->getAll('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');
$headers->remove('X-Test');
$this->assertFalse($headers->has('X-Test'));
$this->assertFalse($headers->has('X-Test'));
}
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'));
}
public function testGetIsNotCaseSensitive()
{
$header = new IdentificationHeader('Message-ID', 'some@id');
$headers = new Headers();
$headers->addIdHeader('Message-ID', 'some@id');
$this->assertEquals($header, $headers->get('message-id'));
}
public function testGetAllIsNotCaseSensitive()
{
$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')));
}
public function testRemoveIsNotCaseSensitive()
{
$header = new IdentificationHeader('Message-ID', 'some@id');
$headers = new Headers();
$headers->addIdHeader('Message-ID', 'some@id');
$headers->remove('message-id');
$this->assertFalse($headers->has('Message-ID'));
}
public function testToStringJoinsHeadersTogether()
{
$headers = new Headers();
$headers->addTextHeader('Foo', 'bar');
$headers->addTextHeader('Zip', 'buttons');
$this->assertEquals("Foo: bar\r\nZip: buttons\r\n", $headers->toString());
}
public function testHeadersWithoutBodiesAreNotDisplayed()
{
$headers = new Headers();
$headers->addTextHeader('Foo', 'bar');
$headers->addTextHeader('Zip', '');
$this->assertEquals("Foo: bar\r\n", $headers->toString());
}
}

View File

@ -0,0 +1,179 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Tests\Header;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\Header\IdentificationHeader;
class IdentificationHeaderTest extends TestCase
{
public function testValueMatchesMsgIdSpec()
{
/* -- RFC 2822, 3.6.4.
message-id = "Message-ID:" msg-id CRLF
in-reply-to = "In-Reply-To:" 1*msg-id CRLF
references = "References:" 1*msg-id CRLF
msg-id = [CFWS] "<" id-left "@" id-right ">" [CFWS]
id-left = dot-atom-text / no-fold-quote / obs-id-left
id-right = dot-atom-text / no-fold-literal / obs-id-right
no-fold-quote = DQUOTE *(qtext / quoted-pair) DQUOTE
no-fold-literal = "[" *(dtext / quoted-pair) "]"
*/
$header = new IdentificationHeader('Message-ID', 'id-left@id-right');
$this->assertEquals('<id-left@id-right>', $header->getBodyAsString());
}
public function testIdCanBeRetrievedVerbatim()
{
$header = new IdentificationHeader('Message-ID', 'id-left@id-right');
$this->assertEquals('id-left@id-right', $header->getId());
}
public function testMultipleIdsCanBeSet()
{
$header = new IdentificationHeader('References', 'c@d');
$header->setIds(['a@b', 'x@y']);
$this->assertEquals(['a@b', 'x@y'], $header->getIds());
}
public function testSettingMultipleIdsProducesAListValue()
{
/* -- RFC 2822, 3.6.4.
The "References:" and "In-Reply-To:" field each contain one or more
unique message identifiers, optionally separated by CFWS.
.. SNIP ..
in-reply-to = "In-Reply-To:" 1*msg-id CRLF
references = "References:" 1*msg-id CRLF
*/
$header = new IdentificationHeader('References', ['a@b', 'x@y']);
$this->assertEquals('<a@b> <x@y>', $header->getBodyAsString());
}
public function testIdLeftCanBeQuoted()
{
/* -- RFC 2822, 3.6.4.
id-left = dot-atom-text / no-fold-quote / obs-id-left
*/
$header = new IdentificationHeader('References', '"ab"@c');
$this->assertEquals('"ab"@c', $header->getId());
$this->assertEquals('<"ab"@c>', $header->getBodyAsString());
}
public function testIdLeftCanContainAnglesAsQuotedPairs()
{
/* -- RFC 2822, 3.6.4.
no-fold-quote = DQUOTE *(qtext / quoted-pair) DQUOTE
*/
$header = new IdentificationHeader('References', '"a\\<\\>b"@c');
$this->assertEquals('"a\\<\\>b"@c', $header->getId());
$this->assertEquals('<"a\\<\\>b"@c>', $header->getBodyAsString());
}
public function testIdLeftCanBeDotAtom()
{
$header = new IdentificationHeader('References', 'a.b+&%$.c@d');
$this->assertEquals('a.b+&%$.c@d', $header->getId());
$this->assertEquals('<a.b+&%$.c@d>', $header->getBodyAsString());
}
/**
* @expectedException \Exception
* @expectedMessageException "a b c" is not valid id-left
*/
public function testInvalidIdLeftThrowsException()
{
$header = new IdentificationHeader('References', 'a b c@d');
}
public function testIdRightCanBeDotAtom()
{
/* -- RFC 2822, 3.6.4.
id-right = dot-atom-text / no-fold-literal / obs-id-right
*/
$header = new IdentificationHeader('References', 'a@b.c+&%$.d');
$this->assertEquals('a@b.c+&%$.d', $header->getId());
$this->assertEquals('<a@b.c+&%$.d>', $header->getBodyAsString());
}
public function testIdRightCanBeLiteral()
{
/* -- RFC 2822, 3.6.4.
no-fold-literal = "[" *(dtext / quoted-pair) "]"
*/
$header = new IdentificationHeader('References', 'a@[1.2.3.4]');
$this->assertEquals('a@[1.2.3.4]', $header->getId());
$this->assertEquals('<a@[1.2.3.4]>', $header->getBodyAsString());
}
public function testIdRigthIsIdnEncoded()
{
$header = new IdentificationHeader('References', 'a@ä');
$this->assertEquals('a@ä', $header->getId());
$this->assertEquals('<a@xn--4ca>', $header->getBodyAsString());
}
/**
* @expectedException \Exception
* @expectedMessageException "b c d" is not valid id-right
*/
public function testInvalidIdRightThrowsException()
{
$header = new IdentificationHeader('References', 'a@b c d');
}
/**
* @expectedException \Exception
* @expectedMessageException "abc" is does not contain @
*/
public function testMissingAtSignThrowsException()
{
/* -- RFC 2822, 3.6.4.
msg-id = [CFWS] "<" id-left "@" id-right ">" [CFWS]
*/
$header = new IdentificationHeader('References', 'abc');
}
public function testSetBody()
{
$header = new IdentificationHeader('Message-ID', 'c@d');
$header->setBody('a@b');
$this->assertEquals(['a@b'], $header->getIds());
}
public function testGetBody()
{
$header = new IdentificationHeader('Message-ID', 'a@b');
$this->assertEquals(['a@b'], $header->getBody());
}
public function testStringValue()
{
$header = new IdentificationHeader('References', ['a@b', 'x@y']);
$this->assertEquals('References: <a@b> <x@y>', $header->toString());
}
}

View File

@ -0,0 +1,79 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Tests\Header;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Header\MailboxHeader;
use Symfony\Component\Mime\NamedAddress;
class MailboxHeaderTest extends TestCase
{
public function testConstructor()
{
$header = new MailboxHeader('Sender', $address = new Address('fabien@symfony.com'));
$this->assertEquals($address, $header->getAddress());
$this->assertEquals($address, $header->getBody());
}
public function testAddress()
{
$header = new MailboxHeader('Sender', new Address('fabien@symfony.com'));
$header->setBody($address = new Address('helene@symfony.com'));
$this->assertEquals($address, $header->getAddress());
$this->assertEquals($address, $header->getBody());
$header->setAddress($address = new Address('thomas@symfony.com'));
$this->assertEquals($address, $header->getAddress());
$this->assertEquals($address, $header->getBody());
}
public function testgetBodyAsString()
{
$header = new MailboxHeader('Sender', new Address('fabien@symfony.com'));
$this->assertEquals('fabien@symfony.com', $header->getBodyAsString());
$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'));
$this->assertEquals('Fabien Potencier <fabien@symfony.com>', $header->getBodyAsString());
$header = new MailboxHeader('Sender', new NamedAddress('fabien@symfony.com', 'Fabien Potencier, "from Symfony"'));
$this->assertEquals('"Fabien Potencier, \"from Symfony\"" <fabien@symfony.com>', $header->getBodyAsString());
$header = new MailboxHeader('From', new NamedAddress('fabien@symfony.com', 'Fabien Potencier, \\escaped\\'));
$this->assertEquals('"Fabien Potencier, \\\\escaped\\\\" <fabien@symfony.com>', $header->getBodyAsString());
$name = 'P'.pack('C', 0x8F).'tencier';
$header = new MailboxHeader('Sender', new NamedAddress('fabien@symfony.com', 'Fabien '.$name));
$header->setCharset('iso-8859-1');
$this->assertEquals('Fabien =?'.$header->getCharset().'?Q?P=8Ftencier?= <fabien@symfony.com>', $header->getBodyAsString());
}
/**
* @expectedException \Symfony\Component\Mime\Exception\AddressEncoderException
*/
public function testUtf8CharsInLocalPartThrows()
{
$header = new MailboxHeader('Sender', new Address('fabïen@symfony.com'));
$header->getBodyAsString();
}
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'));
$this->assertEquals('Sender: Fabien Potencier <fabien@symfony.com>', $header->toString());
}
}

View File

@ -0,0 +1,133 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Tests\Header;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Header\MailboxListHeader;
use Symfony\Component\Mime\NamedAddress;
class MailboxListHeaderTest extends TestCase
{
// RFC 2822, 3.6.2 for all tests
public function testMailboxIsSetForAddress()
{
$header = new MailboxListHeader('From', [new Address('chris@swiftmailer.org')]);
$this->assertEquals(['chris@swiftmailer.org'], $header->getAddressStrings());
}
public function testMailboxIsRenderedForNameAddress()
{
$header = new MailboxListHeader('From', [new NamedAddress('chris@swiftmailer.org', 'Chris Corbyn')]);
$this->assertEquals(['Chris Corbyn <chris@swiftmailer.org>'], $header->getAddressStrings());
}
public function testAddressCanBeReturnedForAddress()
{
$header = new MailboxListHeader('From', $addresses = [new Address('chris@swiftmailer.org')]);
$this->assertEquals($addresses, $header->getAddresses());
}
public function testQuotesInNameAreQuoted()
{
$header = new MailboxListHeader('From', [new NamedAddress('chris@swiftmailer.org', 'Chris Corbyn, "DHE"')]);
$this->assertEquals(['"Chris Corbyn, \"DHE\"" <chris@swiftmailer.org>'], $header->getAddressStrings());
}
public function testEscapeCharsInNameAreQuoted()
{
$header = new MailboxListHeader('From', [new NamedAddress('chris@swiftmailer.org', 'Chris Corbyn, \\escaped\\')]);
$this->assertEquals(['"Chris Corbyn, \\\\escaped\\\\" <chris@swiftmailer.org>'], $header->getAddressStrings());
}
public function testUtf8CharsInDomainAreIdnEncoded()
{
$header = new MailboxListHeader('From', [new NamedAddress('chris@swïftmailer.org', 'Chris Corbyn')]);
$this->assertEquals(['Chris Corbyn <chris@xn--swftmailer-78a.org>'], $header->getAddressStrings());
}
/**
* @expectedException \Symfony\Component\Mime\Exception\AddressEncoderException
*/
public function testUtf8CharsInLocalPartThrows()
{
$header = new MailboxListHeader('From', [new NamedAddress('chrïs@swiftmailer.org', 'Chris Corbyn')]);
$header->getAddressStrings();
}
public function testGetMailboxesReturnsNameValuePairs()
{
$header = new MailboxListHeader('From', $addresses = [new NamedAddress('chris@swiftmailer.org', 'Chris Corbyn, DHE')]);
$this->assertEquals($addresses, $header->getAddresses());
}
public function testMultipleAddressesAsMailboxStrings()
{
$header = new MailboxListHeader('From', [new Address('chris@swiftmailer.org'), new Address('mark@swiftmailer.org')]);
$this->assertEquals(['chris@swiftmailer.org', 'mark@swiftmailer.org'], $header->getAddressStrings());
}
public function testNameIsEncodedIfNonAscii()
{
$name = 'C'.pack('C', 0x8F).'rbyn';
$header = new MailboxListHeader('From', [new NamedAddress('chris@swiftmailer.org', 'Chris '.$name)]);
$header->setCharset('iso-8859-1');
$addresses = $header->getAddressStrings();
$this->assertEquals('Chris =?'.$header->getCharset().'?Q?C=8Frbyn?= <chris@swiftmailer.org>', array_shift($addresses));
}
public function testEncodingLineLengthCalculations()
{
/* -- RFC 2047, 2.
An 'encoded-word' may not be more than 75 characters long, including
'charset', 'encoding', 'encoded-text', and delimiters.
*/
$name = 'C'.pack('C', 0x8F).'rbyn';
$header = new MailboxListHeader('From', [new NamedAddress('chris@swiftmailer.org', 'Chris '.$name)]);
$header->setCharset('iso-8859-1');
$addresses = $header->getAddressStrings();
$this->assertEquals('Chris =?'.$header->getCharset().'?Q?C=8Frbyn?= <chris@swiftmailer.org>', array_shift($addresses));
}
public function testGetValueReturnsMailboxStringValue()
{
$header = new MailboxListHeader('From', [new NamedAddress('chris@swiftmailer.org', 'Chris Corbyn')]);
$this->assertEquals('Chris Corbyn <chris@swiftmailer.org>', $header->getBodyAsString());
}
public function testGetValueReturnsMailboxStringValueForMultipleMailboxes()
{
$header = new MailboxListHeader('From', [new NamedAddress('chris@swiftmailer.org', 'Chris Corbyn'), new NamedAddress('mark@swiftmailer.org', 'Mark Corbyn')]);
$this->assertEquals('Chris Corbyn <chris@swiftmailer.org>, Mark Corbyn <mark@swiftmailer.org>', $header->getBodyAsString());
}
public function testSetBody()
{
$header = new MailboxListHeader('From', []);
$header->setBody($addresses = [new Address('chris@swiftmailer.org')]);
$this->assertEquals($addresses, $header->getAddresses());
}
public function testGetBody()
{
$header = new MailboxListHeader('From', $addresses = [new Address('chris@swiftmailer.org')]);
$this->assertEquals($addresses, $header->getBody());
}
public function testToString()
{
$header = new MailboxListHeader('From', [new NamedAddress('chris@example.org', 'Chris Corbyn'), new NamedAddress('mark@example.org', 'Mark Corbyn')]);
$this->assertEquals('From: Chris Corbyn <chris@example.org>, Mark Corbyn <mark@example.org>', $header->toString());
}
}

View File

@ -0,0 +1,274 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Tests\Header;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\Header\ParameterizedHeader;
class ParameterizedHeaderTest extends TestCase
{
private $charset = 'utf-8';
private $lang = 'en-us';
public function testValueIsReturnedVerbatim()
{
$header = new ParameterizedHeader('Content-Type', 'text/plain');
$this->assertEquals('text/plain', $header->getValue());
}
public function testParametersAreAppended()
{
/* -- RFC 2045, 5.1
parameter := attribute "=" value
attribute := token
; Matching of attributes
; is ALWAYS case-insensitive.
value := token / quoted-string
token := 1*<any (US-ASCII) CHAR except SPACE, CTLs,
or tspecials>
tspecials := "(" / ")" / "<" / ">" / "@" /
"," / ";" / ":" / "\" / <">
"/" / "[" / "]" / "?" / "="
; Must be in quoted-string,
; to use within parameter values
*/
$header = new ParameterizedHeader('Content-Type', 'text/plain');
$header->setParameters(['charset' => 'utf-8']);
$this->assertEquals('text/plain; charset=utf-8', $header->getBodyAsString());
}
public function testSpaceInParamResultsInQuotedString()
{
$header = new ParameterizedHeader('Content-Type', 'attachment');
$header->setParameters(['filename' => 'my file.txt']);
$this->assertEquals('attachment; filename="my file.txt"', $header->getBodyAsString());
}
public function testLongParamsAreBrokenIntoMultipleAttributeStrings()
{
/* -- RFC 2231, 3.
The asterisk character ("*") followed
by a decimal count is employed to indicate that multiple parameters
are being used to encapsulate a single parameter value. The count
starts at 0 and increments by 1 for each subsequent section of the
parameter value. Decimal values are used and neither leading zeroes
nor gaps in the sequence are allowed.
The original parameter value is recovered by concatenating the
various sections of the parameter, in order. For example, the
content-type field
Content-Type: message/external-body; access-type=URL;
URL*0="ftp://";
URL*1="cs.utk.edu/pub/moore/bulk-mailer/bulk-mailer.tar"
is semantically identical to
Content-Type: message/external-body; access-type=URL;
URL="ftp://cs.utk.edu/pub/moore/bulk-mailer/bulk-mailer.tar"
Note that quotes around parameter values are part of the value
syntax; they are NOT part of the value itself. Furthermore, it is
explicitly permitted to have a mixture of quoted and unquoted
continuation fields.
*/
$value = str_repeat('a', 180);
$header = new ParameterizedHeader('Content-Disposition', 'attachment');
$header->setParameters(['filename' => $value]);
$this->assertEquals(
'attachment; '.
'filename*0*=utf-8\'\''.str_repeat('a', 60).";\r\n ".
'filename*1*='.str_repeat('a', 60).";\r\n ".
'filename*2*='.str_repeat('a', 60),
$header->getBodyAsString()
);
}
public function testEncodedParamDataIncludesCharsetAndLanguage()
{
/* -- RFC 2231, 4.
Asterisks ("*") are reused to provide the indicator that language and
character set information is present and encoding is being used. A
single quote ("'") is used to delimit the character set and language
information at the beginning of the parameter value. Percent signs
("%") are used as the encoding flag, which agrees with RFC 2047.
Specifically, an asterisk at the end of a parameter name acts as an
indicator that character set and language information may appear at
the beginning of the parameter value. A single quote is used to
separate the character set, language, and actual value information in
the parameter value string, and an percent sign is used to flag
octets encoded in hexadecimal. For example:
Content-Type: application/x-stuff;
title*=us-ascii'en-us'This%20is%20%2A%2A%2Afun%2A%2A%2A
Note that it is perfectly permissible to leave either the character
set or language field blank. Note also that the single quote
delimiters MUST be present even when one of the field values is
omitted.
*/
$value = str_repeat('a', 20).pack('C', 0x8F).str_repeat('a', 10);
$header = new ParameterizedHeader('Content-Disposition', 'attachment');
$header->setCharset('iso-8859-1');
$header->setValue('attachment');
$header->setParameters(['filename' => $value]);
$header->setLanguage($this->lang);
$this->assertEquals(
'attachment; filename*='.$header->getCharset()."'".$this->lang."'".
str_repeat('a', 20).'%8F'.str_repeat('a', 10),
$header->getBodyAsString()
);
}
public function testMultipleEncodedParamLinesAreFormattedCorrectly()
{
/* -- RFC 2231, 4.1.
Character set and language information may be combined with the
parameter continuation mechanism. For example:
Content-Type: application/x-stuff
title*0*=us-ascii'en'This%20is%20even%20more%20
title*1*=%2A%2A%2Afun%2A%2A%2A%20
title*2="isn't it!"
Note that:
(1) Language and character set information only appear at
the beginning of a given parameter value.
(2) Continuations do not provide a facility for using more
than one character set or language in the same
parameter value.
(3) A value presented using multiple continuations may
contain a mixture of encoded and unencoded segments.
(4) The first segment of a continuation MUST be encoded if
language and character set information are given.
(5) If the first segment of a continued parameter value is
encoded the language and character set field delimiters
MUST be present even when the fields are left blank.
*/
$value = str_repeat('a', 20).pack('C', 0x8F).str_repeat('a', 60);
$header = new ParameterizedHeader('Content-Disposition', 'attachment');
$header->setValue('attachment');
$header->setCharset('utf-6');
$header->setParameters(['filename' => $value]);
$header->setLanguage($this->lang);
$this->assertEquals(
'attachment; filename*0*='.$header->getCharset()."'".$this->lang."'".
str_repeat('a', 20).'%8F'.str_repeat('a', 23).";\r\n ".
'filename*1*='.str_repeat('a', 37),
$header->getBodyAsString()
);
}
public function testToString()
{
$header = new ParameterizedHeader('Content-Type', 'text/html');
$header->setParameters(['charset' => 'utf-8']);
$this->assertEquals('Content-Type: text/html; charset=utf-8', $header->toString());
}
public function testValueCanBeEncodedIfNonAscii()
{
$value = 'fo'.pack('C', 0x8F).'bar';
$header = new ParameterizedHeader('X-Foo', $value);
$header->setCharset('iso-8859-1');
$header->setParameters(['lookslike' => 'foobar']);
$this->assertEquals('X-Foo: =?'.$header->getCharset().'?Q?fo=8Fbar?=; lookslike=foobar', $header->toString());
}
public function testValueAndParamCanBeEncodedIfNonAscii()
{
$value = 'fo'.pack('C', 0x8F).'bar';
$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());
}
public function testParamsAreEncodedWithEncodedWordsIfNoParamEncoderSet()
{
$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());
}
public function testLanguageInformationAppearsInEncodedWords()
{
/* -- RFC 2231, 5.
5. Language specification in Encoded Words
RFC 2047 provides support for non-US-ASCII character sets in RFC 822
message header comments, phrases, and any unstructured text field.
This is done by defining an encoded word construct which can appear
in any of these places. Given that these are fields intended for
display, it is sometimes necessary to associate language information
with encoded words as well as just the character set. This
specification extends the definition of an encoded word to allow the
inclusion of such information. This is simply done by suffixing the
character set specification with an asterisk followed by the language
tag. For example:
From: =?US-ASCII*EN?Q?Keith_Moore?= <moore@cs.utk.edu>
*/
$value = 'fo'.pack('C', 0x8F).'bar';
$header = new ParameterizedHeader('X-Foo', $value);
$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());
}
public function testSetBody()
{
$header = new ParameterizedHeader('Content-Type', 'text/html');
$header->setBody('text/plain');
$this->assertEquals('text/plain', $header->getValue());
}
public function testGetBody()
{
$header = new ParameterizedHeader('Content-Type', 'text/plain');
$this->assertEquals('text/plain', $header->getBody());
}
public function testSetParameter()
{
$header = new ParameterizedHeader('Content-Type', 'text/html');
$header->setParameters(['charset' => 'utf-8', 'delsp' => 'yes']);
$header->setParameter('delsp', 'no');
$this->assertEquals(['charset' => 'utf-8', 'delsp' => 'no'], $header->getParameters());
}
public function testGetParameter()
{
$header = new ParameterizedHeader('Content-Type', 'text/html');
$header->setParameters(['charset' => 'utf-8', 'delsp' => 'yes']);
$this->assertEquals('utf-8', $header->getParameter('charset'));
}
}

View File

@ -0,0 +1,81 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Tests\Header;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Header\PathHeader;
class PathHeaderTest extends TestCase
{
public function testSingleAddressCanBeSetAndFetched()
{
$header = new PathHeader('Return-Path', $address = new Address('chris@swiftmailer.org'));
$this->assertEquals($address, $header->getAddress());
}
/**
* @expectedException \Exception
*/
public function testAddressMustComplyWithRfc2822()
{
$header = new PathHeader('Return-Path', new Address('chr is@swiftmailer.org'));
}
public function testValueIsAngleAddrWithValidAddress()
{
/* -- RFC 2822, 3.6.7.
return = "Return-Path:" path CRLF
path = ([CFWS] "<" ([CFWS] / addr-spec) ">" [CFWS]) /
obs-path
*/
$header = new PathHeader('Return-Path', new Address('chris@swiftmailer.org'));
$this->assertEquals('<chris@swiftmailer.org>', $header->getBodyAsString());
}
public function testAddressIsIdnEncoded()
{
$header = new PathHeader('Return-Path', new Address('chris@swïftmailer.org'));
$this->assertEquals('<chris@xn--swftmailer-78a.org>', $header->getBodyAsString());
}
/**
* @expectedException \Symfony\Component\Mime\Exception\AddressEncoderException
*/
public function testAddressMustBeEncodable()
{
$header = new PathHeader('Return-Path', new Address('chrïs@swiftmailer.org'));
$header->getBodyAsString();
}
public function testSetBody()
{
$header = new PathHeader('Return-Path', new Address('foo@example.com'));
$header->setBody($address = new Address('foo@bar.tld'));
$this->assertEquals($address, $header->getAddress());
}
public function testGetBody()
{
$header = new PathHeader('Return-Path', $address = new Address('foo@bar.tld'));
$this->assertEquals($address, $header->getBody());
}
public function testToString()
{
$header = new PathHeader('Return-Path', new Address('chris@swiftmailer.org'));
$this->assertEquals('Return-Path: <chris@swiftmailer.org>', $header->toString());
}
}

View File

@ -0,0 +1,247 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Tests\Header;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\Header\UnstructuredHeader;
class UnstructuredHeaderTest extends TestCase
{
private $charset = 'utf-8';
public function testGetNameReturnsNameVerbatim()
{
$header = new UnstructuredHeader('Subject', '');
$this->assertEquals('Subject', $header->getName());
}
public function testGetValueReturnsValueVerbatim()
{
$header = new UnstructuredHeader('Subject', 'Test');
$this->assertEquals('Test', $header->getValue());
}
public function testBasicStructureIsKeyValuePair()
{
/* -- RFC 2822, 2.2
Header fields are lines composed of a field name, followed by a colon
(":"), followed by a field body, and terminated by CRLF.
*/
$header = new UnstructuredHeader('Subject', 'Test');
$this->assertEquals('Subject: Test', $header->toString());
}
public function testLongHeadersAreFoldedAtWordBoundary()
{
/* -- RFC 2822, 2.2.3
Each header field is logically a single line of characters comprising
the field name, the colon, and the field body. For convenience
however, and to deal with the 998/78 character limitations per line,
the field body portion of a header field can be split into a multiple
line representation; this is called "folding". The general rule is
that wherever this standard allows for folding white space (not
simply WSP characters), a CRLF may be inserted before any WSP.
*/
$value = 'The quick brown fox jumped over the fence, he was a very very '.
'scary brown fox with a bushy tail';
$header = new UnstructuredHeader('X-Custom-Header', $value);
/*
X-Custom-Header: The quick brown fox jumped over the fence, he was a very very
scary brown fox with a bushy tail
*/
$this->assertEquals(
'X-Custom-Header: The quick brown fox jumped over the fence, he was a'.
' very'."\r\n".//Folding
' very scary brown fox with a bushy tail',
$header->toString(), '%s: The header should have been folded at 76th char'
);
}
public function testPrintableAsciiOnlyAppearsInHeaders()
{
/* -- RFC 2822, 2.2.
A field name MUST be composed of printable US-ASCII characters (i.e.,
characters that have values between 33 and 126, inclusive), except
colon. A field body may be composed of any US-ASCII characters,
except for CR and LF.
*/
$nonAsciiChar = pack('C', 0x8F);
$header = new UnstructuredHeader('X-Test', $nonAsciiChar);
$this->assertRegExp('~^[^:\x00-\x20\x80-\xFF]+: [^\x80-\xFF\r\n]+$~s', $header->toString());
}
public function testEncodedWordsFollowGeneralStructure()
{
/* -- RFC 2047, 1.
Generally, an "encoded-word" is a sequence of printable ASCII
characters that begins with "=?", ends with "?=", and has two "?"s in
between.
*/
$nonAsciiChar = pack('C', 0x8F);
$header = new UnstructuredHeader('X-Test', $nonAsciiChar);
$this->assertRegExp('~^X-Test: \=?.*?\?.*?\?.*?\?=$~s', $header->toString());
}
public function testEncodedWordIncludesCharsetAndEncodingMethodAndText()
{
/* -- RFC 2047, 2.
An 'encoded-word' is defined by the following ABNF grammar. The
notation of RFC 822 is used, with the exception that white space
characters MUST NOT appear between components of an 'encoded-word'.
encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
*/
$nonAsciiChar = pack('C', 0x8F);
$header = new UnstructuredHeader('X-Test', $nonAsciiChar);
$header->setCharset('iso-8859-1');
$this->assertEquals('X-Test: =?'.$header->getCharset().'?Q?=8F?=', $header->toString());
}
public function testEncodedWordsAreUsedToEncodedNonPrintableAscii()
{
// SPACE and TAB permitted
$nonPrintableBytes = array_merge(range(0x00, 0x08), range(0x10, 0x19), [0x7F]);
foreach ($nonPrintableBytes as $byte) {
$char = pack('C', $byte);
$encodedChar = sprintf('=%02X', $byte);
$header = new UnstructuredHeader('X-A', $char);
$header->setCharset('iso-8859-1');
$this->assertEquals('X-A: =?'.$header->getCharset().'?Q?'.$encodedChar.'?=', $header->toString(), 'Non-printable ascii should be encoded');
}
}
public function testEncodedWordsAreUsedToEncode8BitOctets()
{
foreach (range(0x80, 0xFF) as $byte) {
$char = pack('C', $byte);
$encodedChar = sprintf('=%02X', $byte);
$header = new UnstructuredHeader('X-A', $char);
$header->setCharset('iso-8859-1');
$this->assertEquals('X-A: =?'.$header->getCharset().'?Q?'.$encodedChar.'?=', $header->toString(), '8-bit octets should be encoded');
}
}
public function testEncodedWordsAreNoMoreThan75CharsPerLine()
{
/* -- RFC 2047, 2.
An 'encoded-word' may not be more than 75 characters long, including
'charset', 'encoding', 'encoded-text', and delimiters.
... SNIP ...
While there is no limit to the length of a multiple-line header
field, each line of a header field that contains one or more
'encoded-word's is limited to 76 characters.
*/
$nonAsciiChar = pack('C', 0x8F);
//Note that multi-line headers begin with LWSP which makes 75 + 1 = 76
//Note also that =?utf-8?q??= is 12 chars which makes 75 - 12 = 63
//* X-Test: is 8 chars
$header = new UnstructuredHeader('X-Test', $nonAsciiChar);
$header->setCharset('iso-8859-1');
$this->assertEquals('X-Test: =?'.$header->getCharset().'?Q?=8F?=', $header->toString());
}
public function testFWSPIsUsedWhenEncoderReturnsMultipleLines()
{
/* --RFC 2047, 2.
If it is desirable to encode more text than will fit in an 'encoded-word' of
75 characters, multiple 'encoded-word's (separated by CRLF SPACE) may
be used.
*/
// Note that multi-line headers begin with LWSP which makes 75 + 1 = 76
// Note also that =?utf-8?q??= is 12 chars which makes 75 - 12 = 63
//* X-Test: is 8 chars
$header = new UnstructuredHeader('X-Test', pack('C', 0x8F).'line_one_here'."\r\n".'line_two_here');
$header->setCharset('iso-8859-1');
$this->assertEquals('X-Test: =?'.$header->getCharset().'?Q?=8Fline=5Fone=5Fhere?='."\r\n".' =?'.$header->getCharset().'?Q?line=5Ftwo=5Fhere?=', $header->toString());
}
public function testAdjacentWordsAreEncodedTogether()
{
/* -- RFC 2047, 5 (1)
Ordinary ASCII text and 'encoded-word's may appear together in the
same header field. However, an 'encoded-word' that appears in a
header field defined as '*text' MUST be separated from any adjacent
'encoded-word' or 'text' by 'linear-white-space'.
-- RFC 2047, 2.
IMPORTANT: 'encoded-word's are designed to be recognized as 'atom's
by an RFC 822 parser. As a consequence, unencoded white space
characters (such as SPACE and HTAB) are FORBIDDEN within an
'encoded-word'.
*/
// It would be valid to encode all words needed, however it's probably
// easiest to encode the longest amount required at a time
$word = 'w'.pack('C', 0x8F).'rd';
$text = 'start '.$word.' '.$word.' then '.$word;
// 'start', ' word word', ' and end', ' word'
$header = new UnstructuredHeader('X-Test', $text);
$header->setCharset('iso-8859-1');
$this->assertEquals('X-Test: start =?'.$header->getCharset().'?Q?'.
'w=8Frd_w=8Frd?= then =?'.$header->getCharset().'?Q?'.
'w=8Frd?=', $header->toString(),
'Adjacent encoded words should appear grouped with WSP encoded'
);
}
public function testLanguageInformationAppearsInEncodedWords()
{
/* -- RFC 2231, 5.
5. Language specification in Encoded Words
RFC 2047 provides support for non-US-ASCII character sets in RFC 822
message header comments, phrases, and any unstructured text field.
This is done by defining an encoded word construct which can appear
in any of these places. Given that these are fields intended for
display, it is sometimes necessary to associate language information
with encoded words as well as just the character set. This
specification extends the definition of an encoded word to allow the
inclusion of such information. This is simply done by suffixing the
character set specification with an asterisk followed by the language
tag. For example:
From: =?US-ASCII*EN?Q?Keith_Moore?= <moore@cs.utk.edu>
*/
$value = 'fo'.pack('C', 0x8F).'bar';
$header = new UnstructuredHeader('Subject', $value);
$header->setLanguage('en');
$header->setCharset('iso-8859-1');
$this->assertEquals('Subject: =?iso-8859-1*en?Q?fo=8Fbar?=', $header->toString());
}
public function testSetBody()
{
$header = new UnstructuredHeader('X-Test', '');
$header->setBody('test');
$this->assertEquals('test', $header->getValue());
}
public function testGetBody()
{
$header = new UnstructuredHeader('Subject', 'test');
$this->assertEquals('test', $header->getBody());
}
}

View File

@ -0,0 +1,81 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\Email;
use Symfony\Component\Mime\Message;
use Symfony\Component\Mime\MessageConverter;
class MessageConverterTest extends TestCase
{
public function testToEmail()
{
$file = file_get_contents(__DIR__.'/Fixtures/mimetypes/test.gif');
$email = (new Email())->from('fabien@symfony.com');
$this->assertSame($email, MessageConverter::toEmail($email));
$this->assertConversion((clone $email)->text('text content'));
$this->assertConversion((clone $email)->html('HTML content <img src="cid:test.jpg" />'));
$this->assertConversion((clone $email)
->text('text content')
->html('HTML content <img src="cid:test.jpg" />')
);
$this->assertConversion((clone $email)
->text('text content')
->html('HTML content <img src="cid:test.jpg" />')
->embed($file, 'test.jpg', 'image/gif')
);
$this->assertConversion((clone $email)
->text('text content')
->html('HTML content <img src="cid:test.jpg" />')
->attach($file, 'test_attached.jpg', 'image/gif')
);
$this->assertConversion((clone $email)
->text('text content')
->html('HTML content <img src="cid:test.jpg" />')
->embed($file, 'test.jpg', 'image/gif')
->attach($file, 'test_attached.jpg', 'image/gif')
);
$this->assertConversion((clone $email)
->text('text content')
->attach($file, 'test_attached.jpg', 'image/gif')
);
$this->assertConversion((clone $email)
->html('HTML content <img src="cid:test.jpg" />')
->attach($file, 'test_attached.jpg', 'image/gif')
);
$this->assertConversion((clone $email)
->html('HTML content <img src="cid:test.jpg" />')
->embed($file, 'test.jpg', 'image/gif')
);
$this->assertConversion((clone $email)
->text('text content')
->embed($file, 'test_attached.jpg', 'image/gif')
);
}
private function assertConversion(Email $expected)
{
$r = new \ReflectionMethod($expected, 'generateBody');
$r->setAccessible(true);
$message = new Message($expected->getHeaders(), $r->invoke($expected));
$converted = MessageConverter::toEmail($message);
if ($expected->getHtmlBody()) {
$this->assertStringMatchesFormat(str_replace('cid:test.jpg', 'cid:%s', $expected->getHtmlBody()), $converted->getHtmlBody());
$expected->html('HTML content');
$converted->html('HTML content');
}
$this->assertEquals($expected, $converted);
}
}

View File

@ -0,0 +1,141 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Header\Headers;
use Symfony\Component\Mime\Header\MailboxListHeader;
use Symfony\Component\Mime\Header\UnstructuredHeader;
use Symfony\Component\Mime\Message;
use Symfony\Component\Mime\Part\TextPart;
class MessageTest extends TestCase
{
public function testConstruct()
{
$m = new Message();
$this->assertNull($m->getBody());
$this->assertEquals(new Headers(), $m->getHeaders());
$m = new Message($h = (new Headers())->addDateHeader('Date', new \DateTime()), $b = new TextPart('content'));
$this->assertSame($b, $m->getBody());
$this->assertEquals($h, $m->getHeaders());
$m = new Message();
$m->setBody($b);
$m->setHeaders($h);
$this->assertSame($b, $m->getBody());
$this->assertSame($h, $m->getHeaders());
}
public function testGetPreparedHeadersThrowsWhenNoFrom()
{
$this->expectException(\LogicException::class);
(new Message())->getPreparedHeaders();
}
public function testGetPreparedHeadersCloneHeaders()
{
$message = new Message();
$message->getHeaders()->addMailboxListHeader('From', ['fabien@symfony.com']);
$this->assertNotSame($message->getPreparedHeaders(), $message->getHeaders());
}
public function testGetPreparedHeadersSetRequiredHeaders()
{
$message = new Message();
$message->getHeaders()->addMailboxListHeader('From', ['fabien@symfony.com']);
$headers = $message->getPreparedHeaders();
$this->assertTrue($headers->has('MIME-Version'));
$this->assertTrue($headers->has('Message-ID'));
$this->assertTrue($headers->has('Date'));
$this->assertFalse($headers->has('Bcc'));
}
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->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'));
$this->assertTrue($h->has('Date'));
$message = new Message();
$message->getHeaders()->addMailboxListHeader('From', ['fabien@symfony.com']);
$message->getHeaders()->addDateHeader('Date', $n = new \DateTimeImmutable());
$this->assertEquals($n, $message->getPreparedHeaders()->get('Date')->getDateTime());
$message = new Message();
$message->getHeaders()->addMailboxListHeader('From', ['fabien@symfony.com']);
$message->getHeaders()->addMailboxListHeader('Bcc', ['fabien@symfony.com']);
$this->assertNull($message->getPreparedHeaders()->get('Bcc'));
}
public function testGetPreparedHeadersWithNoFrom()
{
$this->expectException(\LogicException::class);
(new Message())->getPreparedHeaders();
}
public function testGetPreparedHeadersHasSenderWhenNeeded()
{
$message = new Message();
$message->getHeaders()->addMailboxListHeader('From', ['fabien@symfony.com']);
$this->assertNull($message->getPreparedHeaders()->get('Sender'));
$message = new Message();
$message->getHeaders()->addMailboxListHeader('From', ['fabien@symfony.com', 'lucas@symfony.com']);
$this->assertEquals('fabien@symfony.com', $message->getPreparedHeaders()->get('Sender')->getAddress()->getAddress());
$message = new Message();
$message->getHeaders()->addMailboxListHeader('From', ['fabien@symfony.com', 'lucas@symfony.com']);
$message->getHeaders()->addMailboxHeader('Sender', 'thomas@symfony.com');
$this->assertEquals('thomas@symfony.com', $message->getPreparedHeaders()->get('Sender')->getAddress()->getAddress());
}
public function testToString()
{
$message = new Message();
$message->getHeaders()->addMailboxListHeader('From', ['fabien@symfony.com']);
$expected = <<<EOF
From: fabien@symfony.com
MIME-Version: 1.0
Date: %s
Message-ID: <%s@symfony.com>
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
EOF;
$this->assertStringMatchesFormat($expected, str_replace("\r\n", "\n", $message->toString()));
$this->assertStringMatchesFormat($expected, str_replace("\r\n", "\n", implode('', iterator_to_array($message->toIterable()))));
$message = new Message(null, new TextPart('content'));
$message->getHeaders()->addMailboxListHeader('From', ['fabien@symfony.com']);
$expected = <<<EOF
From: fabien@symfony.com
MIME-Version: 1.0
Date: %s
Message-ID: <%s@symfony.com>
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
content
EOF;
$this->assertStringMatchesFormat($expected, str_replace("\r\n", "\n", $message->toString()));
$this->assertStringMatchesFormat($expected, str_replace("\r\n", "\n", implode('', iterator_to_array($message->toIterable()))));
}
}

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\Mime\Tests;
use Symfony\Component\Mime\Exception\RuntimeException;
use Symfony\Component\Mime\MimeTypeGuesserInterface;
use Symfony\Component\Mime\MimeTypes;
@ -35,10 +36,10 @@ class MimeTypesTest extends AbstractMimeTypeGuesserTest
public function guessMimeType(string $mimeType): ?string
{
throw new \RuntimeException('Should never be called.');
throw new RuntimeException('Should never be called.');
}
});
$this->assertEquals('image/gif', $guesser->guessMimeType(__DIR__.'/Fixtures/test'));
$this->assertEquals('image/gif', $guesser->guessMimeType(__DIR__.'/Fixtures/mimetypes/test'));
}
public function testGetExtensions()

View File

@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\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 <fabien@xn--symfon-nwa.com>', $a->toString());
$this->assertEquals('fabien@xn--symfon-nwa.com', $a->getEncodedAddress());
}
}

View File

@ -0,0 +1,149 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Tests\Part;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\Header\Headers;
use Symfony\Component\Mime\Header\IdentificationHeader;
use Symfony\Component\Mime\Header\ParameterizedHeader;
use Symfony\Component\Mime\Header\UnstructuredHeader;
use Symfony\Component\Mime\Part\DataPart;
class DataPartTest extends TestCase
{
public function testConstructor()
{
$p = new DataPart('content');
$this->assertEquals('content', $p->getBody());
$this->assertEquals(base64_encode('content'), $p->bodyToString());
$this->assertEquals(base64_encode('content'), implode('', iterator_to_array($p->bodyToIterable())));
// bodyToIterable() can be called several times
$this->assertEquals(base64_encode('content'), implode('', iterator_to_array($p->bodyToIterable())));
$this->assertEquals('application', $p->getMediaType());
$this->assertEquals('octet-stream', $p->getMediaSubType());
$p = new DataPart('content', null, 'text/html');
$this->assertEquals('text', $p->getMediaType());
$this->assertEquals('html', $p->getMediaSubType());
}
public function testConstructorWithResource()
{
$f = fopen('php://memory', 'r+', false);
fwrite($f, 'content');
rewind($f);
$p = new DataPart($f);
$this->assertEquals('content', $p->getBody());
$this->assertEquals(base64_encode('content'), $p->bodyToString());
$this->assertEquals(base64_encode('content'), implode('', iterator_to_array($p->bodyToIterable())));
fclose($f);
}
public function testConstructorWithNonStringOrResource()
{
$this->expectException(\TypeError::class);
new DataPart(new \stdClass());
}
public function testHeaders()
{
$p = new DataPart('content');
$this->assertEquals(new Headers(
new ParameterizedHeader('Content-Type', 'application/octet-stream'),
new UnstructuredHeader('Content-Transfer-Encoding', 'base64'),
new ParameterizedHeader('Content-Disposition', 'attachment')
), $p->getPreparedHeaders());
$p = new DataPart('content', 'photo.jpg', 'text/html');
$this->assertEquals(new Headers(
new ParameterizedHeader('Content-Type', 'text/html', ['name' => 'photo.jpg']),
new UnstructuredHeader('Content-Transfer-Encoding', 'base64'),
new ParameterizedHeader('Content-Disposition', 'attachment', ['name' => 'photo.jpg', 'filename' => 'photo.jpg'])
), $p->getPreparedHeaders());
}
public function testAsInline()
{
$p = new DataPart('content', 'photo.jpg', 'text/html');
$p->asInline();
$this->assertEquals(new Headers(
new ParameterizedHeader('Content-Type', 'text/html', ['name' => 'photo.jpg']),
new UnstructuredHeader('Content-Transfer-Encoding', 'base64'),
new ParameterizedHeader('Content-Disposition', 'inline', ['name' => 'photo.jpg', 'filename' => 'photo.jpg'])
), $p->getPreparedHeaders());
}
public function testAsInlineWithCID()
{
$p = new DataPart('content', 'photo.jpg', 'text/html');
$p->asInline();
$cid = $p->getContentId();
$this->assertEquals(new Headers(
new ParameterizedHeader('Content-Type', 'text/html', ['name' => 'photo.jpg']),
new UnstructuredHeader('Content-Transfer-Encoding', 'base64'),
new ParameterizedHeader('Content-Disposition', 'inline', ['name' => 'photo.jpg', 'filename' => 'photo.jpg']),
new IdentificationHeader('Content-ID', $cid)
), $p->getPreparedHeaders());
}
public function testFromPath()
{
$p = DataPart::fromPath($file = __DIR__.'/../Fixtures/mimetypes/test.gif');
$content = file_get_contents($file);
$this->assertEquals($content, $p->getBody());
$this->assertEquals(base64_encode($content), $p->bodyToString());
$this->assertEquals(base64_encode($content), implode('', iterator_to_array($p->bodyToIterable())));
$this->assertEquals('image', $p->getMediaType());
$this->assertEquals('gif', $p->getMediaSubType());
$this->assertEquals(new Headers(
new ParameterizedHeader('Content-Type', 'image/gif', ['name' => 'test.gif']),
new UnstructuredHeader('Content-Transfer-Encoding', 'base64'),
new ParameterizedHeader('Content-Disposition', 'attachment', ['name' => 'test.gif', 'filename' => 'test.gif'])
), $p->getPreparedHeaders());
}
public function testFromPathWithMeta()
{
$p = DataPart::fromPath($file = __DIR__.'/../Fixtures/mimetypes/test.gif', 'photo.gif', 'image/jpeg');
$content = file_get_contents($file);
$this->assertEquals($content, $p->getBody());
$this->assertEquals(base64_encode($content), $p->bodyToString());
$this->assertEquals(base64_encode($content), implode('', iterator_to_array($p->bodyToIterable())));
$this->assertEquals('image', $p->getMediaType());
$this->assertEquals('jpeg', $p->getMediaSubType());
$this->assertEquals(new Headers(
new ParameterizedHeader('Content-Type', 'image/jpeg', ['name' => 'photo.gif']),
new UnstructuredHeader('Content-Transfer-Encoding', 'base64'),
new ParameterizedHeader('Content-Disposition', 'attachment', ['name' => 'photo.gif', 'filename' => 'photo.gif'])
), $p->getPreparedHeaders());
}
public function testHasContentId()
{
$p = new DataPart('content');
$this->assertFalse($p->hasContentId());
$p->getContentId();
$this->assertTrue($p->hasContentId());
}
public function testSerialize()
{
$r = fopen('php://memory', 'r+', false);
fwrite($r, 'Text content');
rewind($r);
$p = new DataPart($r);
$p->getHeaders()->addTextHeader('foo', 'bar');
$expected = clone $p;
$this->assertEquals($expected->toString(), unserialize(serialize($p))->toString());
}
}

View File

@ -0,0 +1,42 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Tests\Part;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\Email;
use Symfony\Component\Mime\Header\Headers;
use Symfony\Component\Mime\Header\ParameterizedHeader;
use Symfony\Component\Mime\Header\UnstructuredHeader;
use Symfony\Component\Mime\Part\MessagePart;
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->assertEquals('message', $p->getMediaType());
$this->assertEquals('rfc822', $p->getMediaSubType());
}
public function testHeaders()
{
$p = new MessagePart((new Email())->from('fabien@symfony.com')->text('content')->subject('Subject'));
$this->assertEquals(new Headers(
new ParameterizedHeader('Content-Type', 'message/rfc822', ['name' => 'Subject.eml']),
new UnstructuredHeader('Content-Transfer-Encoding', 'base64'),
new ParameterizedHeader('Content-Disposition', 'attachment', ['name' => 'Subject.eml', 'filename' => 'Subject.eml'])
), $p->getPreparedHeaders());
}
}

View File

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Tests\Part\Multipart;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\Part\Multipart\AlternativePart;
class AlternativePartTest extends TestCase
{
public function testConstructor()
{
$a = new AlternativePart();
$this->assertEquals('multipart', $a->getMediaType());
$this->assertEquals('alternative', $a->getMediaSubtype());
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Tests\Part\Multipart;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\Message;
use Symfony\Component\Mime\Part\MessagePart;
use Symfony\Component\Mime\Part\Multipart\DigestPart;
class DigestPartTest extends TestCase
{
public function testConstructor()
{
$r = new DigestPart($a = new MessagePart(new Message()), $b = new MessagePart(new Message()));
$this->assertEquals('multipart', $r->getMediaType());
$this->assertEquals('digest', $r->getMediaSubtype());
$this->assertEquals([$a, $b], $r->getParts());
}
}

View File

@ -0,0 +1,44 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Tests\Part\Multipart;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\Part\DataPart;
use Symfony\Component\Mime\Part\Multipart\FormDataPart;
use Symfony\Component\Mime\Part\TextPart;
class FormDataPartTest extends TestCase
{
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?',
'bar' => clone $b,
'baz' => clone $c,
]);
$this->assertEquals('multipart', $f->getMediaType());
$this->assertEquals('form-data', $f->getMediaSubtype());
$t = new TextPart($content);
$t->setDisposition('form-data');
$t->setName('foo');
$t->getHeaders()->setMaxLineLength(1000);
$b->setDisposition('form-data');
$b->setName('bar');
$b->getHeaders()->setMaxLineLength(1000);
$c->setDisposition('form-data');
$c->setName('baz');
$c->getHeaders()->setMaxLineLength(1000);
$this->assertEquals([$t, $b, $c], $f->getParts());
}
}

View File

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Tests\Part\Multipart;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\Part\Multipart\MixedPart;
class MixedPartTest extends TestCase
{
public function testConstructor()
{
$a = new MixedPart();
$this->assertEquals('multipart', $a->getMediaType());
$this->assertEquals('mixed', $a->getMediaSubtype());
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Tests\Part\Multipart;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\Part\Multipart\RelatedPart;
use Symfony\Component\Mime\Part\TextPart;
class RelatedPartTest extends TestCase
{
public function testConstructor()
{
$r = new RelatedPart($a = new TextPart('content'), $b = new TextPart('HTML content', 'utf-8', 'html'), $c = new TextPart('HTML content again', 'utf-8', 'html'));
$this->assertEquals('multipart', $r->getMediaType());
$this->assertEquals('related', $r->getMediaSubtype());
$this->assertEquals([$a, $b, $c], $r->getParts());
$this->assertFalse($a->getHeaders()->has('Content-ID'));
$this->assertTrue($b->getHeaders()->has('Content-ID'));
$this->assertTrue($c->getHeaders()->has('Content-ID'));
}
}

View File

@ -0,0 +1,92 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Tests\Part;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\Header\Headers;
use Symfony\Component\Mime\Header\ParameterizedHeader;
use Symfony\Component\Mime\Header\UnstructuredHeader;
use Symfony\Component\Mime\Part\TextPart;
class TextPartTest extends TestCase
{
public function testConstructor()
{
$p = new TextPart('content');
$this->assertEquals('content', $p->getBody());
$this->assertEquals('content', $p->bodyToString());
$this->assertEquals('content', implode('', iterator_to_array($p->bodyToIterable())));
// bodyToIterable() can be called several times
$this->assertEquals('content', implode('', iterator_to_array($p->bodyToIterable())));
$this->assertEquals('text', $p->getMediaType());
$this->assertEquals('plain', $p->getMediaSubType());
$p = new TextPart('content', null, 'html');
$this->assertEquals('html', $p->getMediaSubType());
}
public function testConstructorWithResource()
{
$f = fopen('php://memory', 'r+', false);
fwrite($f, 'content');
rewind($f);
$p = new TextPart($f);
$this->assertEquals('content', $p->getBody());
$this->assertEquals('content', $p->bodyToString());
$this->assertEquals('content', implode('', iterator_to_array($p->bodyToIterable())));
fclose($f);
}
public function testConstructorWithNonStringOrResource()
{
$this->expectException(\TypeError::class);
new TextPart(new \stdClass());
}
public function testHeaders()
{
$p = new TextPart('content');
$this->assertEquals(new Headers(
new ParameterizedHeader('Content-Type', 'text/plain', ['charset' => 'utf-8']),
new UnstructuredHeader('Content-Transfer-Encoding', 'quoted-printable')
), $p->getPreparedHeaders());
$p = new TextPart('content', 'iso-8859-1');
$this->assertEquals(new Headers(
new ParameterizedHeader('Content-Type', 'text/plain', ['charset' => 'iso-8859-1']),
new UnstructuredHeader('Content-Transfer-Encoding', 'quoted-printable')
), $p->getPreparedHeaders());
}
public function testEncoding()
{
$p = new TextPart('content', 'utf-8', 'plain', 'base64');
$this->assertEquals(base64_encode('content'), $p->bodyToString());
$this->assertEquals(base64_encode('content'), implode('', iterator_to_array($p->bodyToIterable())));
$this->assertEquals(new Headers(
new ParameterizedHeader('Content-Type', 'text/plain', ['charset' => 'utf-8']),
new UnstructuredHeader('Content-Transfer-Encoding', 'base64')
), $p->getPreparedHeaders());
}
public function testSerialize()
{
$r = fopen('php://memory', 'r+', false);
fwrite($r, 'Text content');
rewind($r);
$p = new TextPart($r);
$p->getHeaders()->addTextHeader('foo', 'bar');
$expected = clone $p;
$this->assertEquals($expected->toString(), unserialize(serialize($p))->toString());
}
}

View File

@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Mime\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mime\RawMessage;
class RawMessageTest extends TestCase
{
public function testToString()
{
$message = new RawMessage('string');
$this->assertEquals('string', $message->toString());
$this->assertEquals('string', implode('', iterator_to_array($message->toIterable())));
// calling methods more than once work
$this->assertEquals('string', $message->toString());
$this->assertEquals('string', implode('', iterator_to_array($message->toIterable())));
$message = new RawMessage(new \ArrayObject(['some', ' ', 'string']));
$this->assertEquals('some string', $message->toString());
$this->assertEquals('some string', implode('', iterator_to_array($message->toIterable())));
// calling methods more than once work
$this->assertEquals('some string', $message->toString());
$this->assertEquals('some string', implode('', iterator_to_array($message->toIterable())));
}
}

View File

@ -1,7 +1,7 @@
{
"name": "symfony/mime",
"type": "library",
"description": "A ",
"description": "A library to manipulate MIME messages",
"keywords": ["mime", "mime-type"],
"homepage": "https://symfony.com",
"license": "MIT",
@ -16,9 +16,12 @@
}
],
"require": {
"php": "^7.1.3"
"php": "^7.1.3",
"symfony/polyfill-intl-idn": "^1.10",
"symfony/polyfill-mbstring": "^1.0"
},
"require-dev": {
"egulias/email-validator": "^2.0",
"symfony/dependency-injection": "~3.4|^4.1"
},
"autoload": {