Skip to content
Permalink
Browse files

feature #30416 Mime messages (fabpot)

This PR was merged into the 4.3-dev branch.

Discussion
----------

Mime messages

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | many on Swiftmailer
| License       | MIT
| Doc PR        | upcoming

As announced today at SymfonyLive Lille, here is the new MIME component.

This PR is one step towards the new Symfony Mailer (announced at Symfony London). It started as a fork of Swiftmailer, but soon enough I rewrote almost everything to make it (hopefully) better and more flexible. I've removed all the complexity of Swiftmailer when it comes to multiparts for instance.

Some big differences with Swiftmailer:

* Way less complexity (no crazy dependency injection when not needed, less interfaces, no cache)
* Plain data object and no state (out are the observers for charset and encoding, in are POPO and serializable objects)
* No magic regarding multipart management, but a nice wrapper for the most common use cases
  swiftmailer/swiftmailer#434
  swiftmailer/swiftmailer#775
  swiftmailer/swiftmailer#946
  swiftmailer/swiftmailer#615
  swiftmailer/swiftmailer#184
  swiftmailer/swiftmailer#56
  and probably many others
* More Symfony-like
* Messages are built on-demand and we do not mess up with your headers/body (Swiftmailer add headers and change yours, but here, we generate needed headers when converting the message as a string, they are not stored -- it means for instance that generating an Email twice will give you 2 different Date headers)
* and probably more that I don't remember right now

I've also kept some nice features from Swiftmailer like support for any charset.

More information on the slides:

https://speakerdeck.com/fabpot/2-new-symfony-components-httpclient-and-mime

Commits
-------

ee787d1 [Mime] added classes for generating MIME messages
  • Loading branch information...
fabpot committed Mar 2, 2019
2 parents 91c5b14 + ee787d1 commit 9d578bd7437fd59474a8fa9677a78e212202dad0
Showing with 8,201 additions and 17 deletions.
  1. +1 −0 composer.json
  2. +100 −0 src/Symfony/Bridge/Twig/Mime/Renderer.php
  3. +87 −0 src/Symfony/Bridge/Twig/Mime/TemplatedEmail.php
  4. +199 −0 src/Symfony/Bridge/Twig/Mime/WrappedTemplatedEmail.php
  5. +174 −0 src/Symfony/Bridge/Twig/Tests/Mime/RendererTest.php
  6. +25 −0 src/Symfony/Bridge/Twig/Tests/Mime/TemplatedEmailTest.php
  7. +1 −0 src/Symfony/Bridge/Twig/composer.json
  8. +98 −0 src/Symfony/Component/Mime/Address.php
  9. +224 −0 src/Symfony/Component/Mime/CharacterStream.php
  10. +2 −0 src/Symfony/Component/Mime/DependencyInjection/AddMimeTypeGuesserPass.php
  11. +604 −0 src/Symfony/Component/Mime/Email.php
  12. +30 −0 src/Symfony/Component/Mime/Encoder/AddressEncoderInterface.php
  13. +50 −0 src/Symfony/Component/Mime/Encoder/Base64ContentEncoder.php
  14. +43 −0 src/Symfony/Component/Mime/Encoder/Base64Encoder.php
  15. +45 −0 src/Symfony/Component/Mime/Encoder/Base64MimeHeaderEncoder.php
  16. +32 −0 src/Symfony/Component/Mime/Encoder/ContentEncoderInterface.php
  17. +28 −0 src/Symfony/Component/Mime/Encoder/EncoderInterface.php
  18. +56 −0 src/Symfony/Component/Mime/Encoder/IdnAddressEncoder.php
  19. +25 −0 src/Symfony/Component/Mime/Encoder/MimeHeaderEncoderInterface.php
  20. +66 −0 src/Symfony/Component/Mime/Encoder/QpContentEncoder.php
  21. +199 −0 src/Symfony/Component/Mime/Encoder/QpEncoder.php
  22. +42 −0 src/Symfony/Component/Mime/Encoder/QpMimeHeaderEncoder.php
  23. +52 −0 src/Symfony/Component/Mime/Encoder/Rfc2231Encoder.php
  24. +21 −0 src/Symfony/Component/Mime/Exception/AddressEncoderException.php
  25. +21 −0 src/Symfony/Component/Mime/Exception/ExceptionInterface.php
  26. +21 −0 src/Symfony/Component/Mime/Exception/InvalidArgumentException.php
  27. +21 −0 src/Symfony/Component/Mime/Exception/LogicException.php
  28. +21 −0 src/Symfony/Component/Mime/Exception/RfcComplianceException.php
  29. +21 −0 src/Symfony/Component/Mime/Exception/RuntimeException.php
  30. +7 −2 src/Symfony/Component/Mime/FileBinaryMimeTypeGuesser.php
  31. +7 −2 src/Symfony/Component/Mime/FileinfoMimeTypeGuesser.php
  32. +281 −0 src/Symfony/Component/Mime/Header/AbstractHeader.php
  33. +71 −0 src/Symfony/Component/Mime/Header/DateHeader.php
  34. +67 −0 src/Symfony/Component/Mime/Header/HeaderInterface.php
  35. +275 −0 src/Symfony/Component/Mime/Header/Headers.php
  36. +115 −0 src/Symfony/Component/Mime/Header/IdentificationHeader.php
  37. +93 −0 src/Symfony/Component/Mime/Header/MailboxHeader.php
  38. +139 −0 src/Symfony/Component/Mime/Header/MailboxListHeader.php
  39. +176 −0 src/Symfony/Component/Mime/Header/ParameterizedHeader.php
  40. +67 −0 src/Symfony/Component/Mime/Header/PathHeader.php
  41. +73 −0 src/Symfony/Component/Mime/Header/UnstructuredHeader.php
  42. +134 −0 src/Symfony/Component/Mime/Message.php
  43. +132 −0 src/Symfony/Component/Mime/MessageConverter.php
  44. +2 −0 src/Symfony/Component/Mime/MimeTypeGuesserInterface.php
  45. +5 −1 src/Symfony/Component/Mime/MimeTypes.php
  46. +2 −0 src/Symfony/Component/Mime/MimeTypesInterface.php
  47. +44 −0 src/Symfony/Component/Mime/NamedAddress.php
  48. +100 −0 src/Symfony/Component/Mime/Part/AbstractMultipartPart.php
  49. +64 −0 src/Symfony/Component/Mime/Part/AbstractPart.php
  50. +152 −0 src/Symfony/Component/Mime/Part/DataPart.php
  51. +64 −0 src/Symfony/Component/Mime/Part/MessagePart.php
  52. +27 −0 src/Symfony/Component/Mime/Part/Multipart/AlternativePart.php
  53. +33 −0 src/Symfony/Component/Mime/Part/Multipart/DigestPart.php
  54. +92 −0 src/Symfony/Component/Mime/Part/Multipart/FormDataPart.php
  55. +27 −0 src/Symfony/Component/Mime/Part/Multipart/MixedPart.php
  56. +57 −0 src/Symfony/Component/Mime/Part/Multipart/RelatedPart.php
  57. +191 −0 src/Symfony/Component/Mime/Part/TextPart.php
  58. +4 −1 src/Symfony/Component/Mime/README.md
  59. +55 −0 src/Symfony/Component/Mime/RawMessage.php
  60. +7 −7 src/Symfony/Component/Mime/Tests/AbstractMimeTypeGuesserTest.php
  61. +61 −0 src/Symfony/Component/Mime/Tests/AddressTest.php
  62. +87 −0 src/Symfony/Component/Mime/Tests/CharacterStreamTest.php
  63. +385 −0 src/Symfony/Component/Mime/Tests/EmailTest.php
  64. +158 −0 src/Symfony/Component/Mime/Tests/Encoder/Base64EncoderTest.php
  65. +23 −0 src/Symfony/Component/Mime/Tests/Encoder/Base64MimeHeaderEncoderTest.php
  66. +213 −0 src/Symfony/Component/Mime/Tests/Encoder/QpEncoderTest.php
  67. +139 −0 src/Symfony/Component/Mime/Tests/Encoder/QpMimeHeaderEncoderTest.php
  68. +129 −0 src/Symfony/Component/Mime/Tests/Encoder/Rfc2231EncoderTest.php
  69. 0 src/Symfony/Component/Mime/Tests/Fixtures/{ → mimetypes}/.unknownextension
  70. 0 src/Symfony/Component/Mime/Tests/Fixtures/{ → mimetypes}/directory/.empty
  71. 0 src/Symfony/Component/Mime/Tests/Fixtures/{ → mimetypes}/other-file.example
  72. BIN src/Symfony/Component/Mime/Tests/Fixtures/{ → mimetypes}/test
  73. BIN src/Symfony/Component/Mime/Tests/Fixtures/{ → mimetypes}/test.gif
  74. +11 −0 src/Symfony/Component/Mime/Tests/Fixtures/samples/charsets/iso-2022-jp/one.txt
  75. +19 −0 src/Symfony/Component/Mime/Tests/Fixtures/samples/charsets/iso-8859-1/one.txt
  76. +22 −0 src/Symfony/Component/Mime/Tests/Fixtures/samples/charsets/utf-8/one.txt
  77. +45 −0 src/Symfony/Component/Mime/Tests/Fixtures/samples/charsets/utf-8/three.txt
  78. +3 −0 src/Symfony/Component/Mime/Tests/Fixtures/samples/charsets/utf-8/two.txt
  79. +80 −0 src/Symfony/Component/Mime/Tests/Header/DateHeaderTest.php
  80. +234 −0 src/Symfony/Component/Mime/Tests/Header/HeadersTest.php
  81. +179 −0 src/Symfony/Component/Mime/Tests/Header/IdentificationHeaderTest.php
  82. +79 −0 src/Symfony/Component/Mime/Tests/Header/MailboxHeaderTest.php
  83. +133 −0 src/Symfony/Component/Mime/Tests/Header/MailboxListHeaderTest.php
  84. +274 −0 src/Symfony/Component/Mime/Tests/Header/ParameterizedHeaderTest.php
  85. +81 −0 src/Symfony/Component/Mime/Tests/Header/PathHeaderTest.php
  86. +247 −0 src/Symfony/Component/Mime/Tests/Header/UnstructuredHeaderTest.php
  87. +81 −0 src/Symfony/Component/Mime/Tests/MessageConverterTest.php
  88. +141 −0 src/Symfony/Component/Mime/Tests/MessageTest.php
  89. +3 −2 src/Symfony/Component/Mime/Tests/MimeTypesTest.php
  90. +27 −0 src/Symfony/Component/Mime/Tests/NamedAddressTest.php
  91. +149 −0 src/Symfony/Component/Mime/Tests/Part/DataPartTest.php
  92. +42 −0 src/Symfony/Component/Mime/Tests/Part/MessagePartTest.php
  93. +25 −0 src/Symfony/Component/Mime/Tests/Part/Multipart/AlternativePartTest.php
  94. +28 −0 src/Symfony/Component/Mime/Tests/Part/Multipart/DigestPartTest.php
  95. +44 −0 src/Symfony/Component/Mime/Tests/Part/Multipart/FormDataPartTest.php
  96. +25 −0 src/Symfony/Component/Mime/Tests/Part/Multipart/MixedPartTest.php
  97. +30 −0 src/Symfony/Component/Mime/Tests/Part/Multipart/RelatedPartTest.php
  98. +92 −0 src/Symfony/Component/Mime/Tests/Part/TextPartTest.php
  99. +35 −0 src/Symfony/Component/Mime/Tests/RawMessageTest.php
  100. +5 −2 src/Symfony/Component/Mime/composer.json
@@ -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"
@@ -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);
}
}
@@ -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;
}
}
@@ -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();
}
}
Oops, something went wrong.

0 comments on commit 9d578bd

Please sign in to comment.
You can’t perform that action at this time.