symfony
symfony copied to clipboard
[Mime] [Email] Add encoders to text and html parts
| Q | A |
|---|---|
| Branch | 7.1 |
| Bug fix | no |
| New feature | yes |
| Deprecations | no |
| Issues | n/a |
| License | MIT |
Add encoders to text and html parts of Email object.
$email = new Email();
$email->text('My text content', 'utf-8', 'base64');
$email->html('<div>My HTML content</div>', 'utf-8', 'base64');
Each part of the sent mail will be encoded in Base 64 with defined charset in Content-Type header and base64 in Content-Transfer-Encoding header.
Hi @kumulo, and thank you for your PR! 😄 It's an excellent idea, but we risk having limits as it is.
I created a controller as in the example at https://symfony.com/doc/current/mailer.html#creating-sending-messages:
class MailerController extends AbstractController
{
#[Route('/text_html', name: 'text_html')]
public function textHtml(MailerInterface $mailer): Response
{
$email = (new Email())
->from('[email protected]')
->to('[email protected]')
->subject('Time for Symfony Mailer!')
->text('Sending emails is fun again!', 'utf-8', 'base64') // <-- encoding option !
->html('<p>See Twig integration for better HTML integration!</p>', 'utf-8', 'base64'); // <-- encoding option !
$mailer->send($email);
return new Response('text_html');
}
}
It's good! In the Symfony Profiler, I can see the following raw message (with base64 encoding):
From: [email protected]
To: [email protected]
Subject: Time for Symfony Mailer!
MIME-Version: 1.0
Date: Thu, 07 Mar 2024 21:14:48 +0000
Message-ID: <[email protected]>
Content-Type: multipart/alternative; boundary=RxGlMsDY
--RxGlMsDY
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: base64
U2VuZGluZyBlbWFpbHMgaXMgZnVuIGFnYWluIQ== // <-- base64 encoding !!!
--RxGlMsDY
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: base64
PHA+U2VlIFR3aWcgaW50ZWdyYXRpb24gZm9yIGJldHRlciBIVE1MIGludGVncmF0aW9uITwvcD4= // <-- base64 encoding !!!
--RxGlMsDY--
But, if I want to use TemplatedEmail() as in the example at https://symfony.com/doc/current/mailer.html#html-content, it's not possible to specify base64 encoding:
class MailerController extends AbstractController
{
#[Route('/templated_email', name: 'templated_email')]
public function templatedEmail(MailerInterface $mailer): Response
{
$email = (new TemplatedEmail())
->from('[email protected]')
->to(new Address('[email protected]'))
->subject('Thanks for signing up!')
->htmlTemplate('emails/signup.html.twig') // <-- no encoding option
->locale('de')
->context([
'expiration_date' => new \DateTime('+7 days'),
'username' => 'foo',
]);
$mailer->send($email);
return new Response('templated_email');
}
}
In this case, rendering is performed in this following method BodyRenderer::render() :
//src/Symfony/Bridge/Twig/Mime/BodyRenderer.php
...
if ($template = $message->getTextTemplate()) {
$message->text($this->twig->render($template, $vars)); // <-- no encoding option
}
if ($template = $message->getHtmlTemplate()) {
$message->html($this->twig->render($template, $vars)); // <-- no encoding option
}
...
Another little detail in Email(): text, textCharset, html and htmlCharset properties have accessors. If we follow this formalism, we can create accessors fortextEncoding and htmlEncoding. For Example:
// src/Symfony/Component/Mime/Email.php
...
public function getHtmlEncoding(): ?string
{
return $this->htmlEncoding;
}
...
I haven't yet found all the places where the base64 encoding option would be needed.
What are your thoughts on this? Do you have the time and opportunity to take your idea further? 😀
Hi @jprivet-dev , thanks for you feedback !
But, if I want to use
TemplatedEmail()as in the example at https://symfony.com/doc/current/mailer.html#html-content, it's not possible to specify base64 encoding:class MailerController extends AbstractController { #[Route('/templated_email', name: 'templated_email')] public function templatedEmail(MailerInterface $mailer): Response { $email = (new TemplatedEmail()) ->from('[email protected]') ->to(new Address('[email protected]')) ->subject('Thanks for signing up!') ->htmlTemplate('emails/signup.html.twig') // <-- no encoding option ->locale('de') ->context([ 'expiration_date' => new \DateTime('+7 days'), 'username' => 'foo', ]); $mailer->send($email); return new Response('templated_email'); } }In this case, rendering is performed in this following method
BodyRenderer::render()://src/Symfony/Bridge/Twig/Mime/BodyRenderer.php ... if ($template = $message->getTextTemplate()) { $message->text($this->twig->render($template, $vars)); // <-- no encoding option } if ($template = $message->getHtmlTemplate()) { $message->html($this->twig->render($template, $vars)); // <-- no encoding option } ...
Body rendering is very far from TemplatedEmail creation, so I've made some properties setters, you will be able to do this :
class MailerController extends AbstractController
{
#[Route('/templated_email', name: 'templated_email')]
public function templatedEmail(MailerInterface $mailer): Response
{
$email = (new TemplatedEmail())
->from('[email protected]')
->to(new Address('[email protected]'))
->subject('Thanks for signing up!')
->htmlTemplate('emails/signup.html.twig')
->htmlEncoding('base64') // <= Encoding option :)
->locale('de')
->context([
'expiration_date' => new \DateTime('+7 days'),
'username' => 'foo',
]);
$mailer->send($email);
return new Response('templated_email');
}
}
Another little detail in
Email():text,textCharset,htmlandhtmlCharsetproperties have accessors. If we follow this formalism, we can create accessors fortextEncodingandhtmlEncoding. For Example:// src/Symfony/Component/Mime/Email.php ... public function getHtmlEncoding(): ?string { return $this->htmlEncoding; } ...
Properties accessors are there now :)
Each part of the sent mail will be encoded in Base 64 with defined charset in
Content-Typeheader andbase64inContent-Transfer-Encodingheader.
I'm not sure to understand thewill be encoded in Base 64 .
Where is this done / by "who" ?
Each part of the sent mail will be encoded in Base 64 with defined charset in
Content-Typeheader andbase64inContent-Transfer-Encodingheader.I'm not sure to understand the
will be encoded in Base 64.Where is this done / by "who" ?
Hi @smnandre,
It's to encode text and html parts of the Email and TemplatedEmail.
Without encoding (by default), raw message is :
From: [email protected]
To: [email protected]
Subject: Time for Symfony Mailer!
MIME-Version: 1.0
Date: Thu, 07 Mar 2024 21:14:48 +0000
Message-ID: <[email protected]>
Content-Type: multipart/alternative; boundary=RxGlMsDY
--RxGlMsDY
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
Sending emails is fun again!
--RxGlMsDY
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable
<p>See Twig integration for better HTML integration!</p>
--RxGlMsDY--
With encoding it could be :
From: [email protected]
To: [email protected]
Subject: Time for Symfony Mailer!
MIME-Version: 1.0
Date: Thu, 07 Mar 2024 21:14:48 +0000
Message-ID: <[email protected]>
Content-Type: multipart/alternative; boundary=RxGlMsDY
--RxGlMsDY
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: base64
U2VuZGluZyBlbWFpbHMgaXMgZnVuIGFnYWluIQ== // <-- base64 encoding !!!
--RxGlMsDY
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: base64
PHA+U2VlIFR3aWcgaW50ZWdyYXRpb24gZm9yIGJldHRlciBIVE1MIGludGVncmF0aW9uITwvcD4= // <-- base64 encoding !!!
--RxGlMsDY--
Those two parts are TextPart objects, TextPart allows you to set the encoding of the part :
class TextPart extends AbstractPart
{
public function __construct($body, ?string $charset = 'utf-8', string $subtype = 'plain', ?string $encoding = null)
{
//...
}
}
For Email object, those parts are constructed by generateBody method for text and prepareParts method for html.
Email class allows you to modify charset of parts but not encoding, this PR adds this possibility.
Stop all! It's all so unnecessary! :smile:
After some investigation and a lot of help from @smnandre, it turns out that the base64 encoding option is already available in TextPart::chooseEncoding(), if the charset is null :
private function chooseEncoding(): string
{
if (null === $this->charset) {
return 'base64';
}
return 'quoted-printable';
}
And that's where the problem lies: you can't get a null charset using Email::text() or Email::html() :
class Email extends Message
{
...
private ?string $textCharset = null; // <-- textCharset can be null
private ?string $htmlCharset = null; // <-- htmlCharset can be null
...
public function text($body, string $charset = 'utf-8'): static
{
...
$this->textCharset = $charset; // <-- Impossible to set textCharset to null: default value is 'utf-8'
...
}
public function html($body, string $charset = 'utf-8'): static
{
...
$this->htmlCharset = $charset; // <-- Impossible to set htmlCharset to null: default value is 'utf-8'
...
}
}
In this context, the following code:
class MailerController extends AbstractController
{
#[Route('/']
public function textHtml(MailerInterface $mailer): Response
{
$email = (new Email())
->from('[email protected]')
->to('[email protected]')
->subject('Time for Symfony Mailer!')
->text('Sending emails is fun again!', null) // <-- Set charset to null to access base64 encoding
->html('<p>See Twig integration for better HTML integration!</p>', null); // <-- Set charset to null to access base64 encoding
$mailer->send($email);
return new Response('text_html');
}
}
Returns errors :
Symfony\Component\Mime\Email::text(): Argument #2 ($charset) must be of type string, null given
Symfony\Component\Mime\Email::html(): Argument #2 ($charset) must be of type string, null given
If the following changes are made:
class Email extends Message
{
public function text($body, ?string $charset = 'utf-8'): static // <-- add "?"
{
...
}
public function html($body, ?string $charset = 'utf-8'): static // <-- add "?"
{
...
}
}
Then the code in the controller will be able to run in base64 encoding :partying_face: :
From: [email protected]
To: [email protected]
Subject: Time for Symfony Mailer!
MIME-Version: 1.0
Date: Fri, 08 Mar 2024 17:27:28 +0000
Message-ID: <[email protected]>
Content-Type: multipart/alternative; boundary=htGvt9zJ
--htGvt9zJ
Content-Type: text/plain
Content-Transfer-Encoding: base64
U2VuZGluZyBlbWFpbHMgaXMgZnVuIGFnYWluIQ== // <-- base64 encoding
--htGvt9zJ
Content-Type: text/html
Content-Transfer-Encoding: base64
PHA+U2VlIFR3aWcgaW50ZWdyYXRpb24gZm9yIGJldHRlciBIVE1MIGludGVncmF0aW9uITwvcD4= // <-- base64 encoding
--htGvt9zJ--
However, the problem remains the same with TemplatedEmail(): how to indicate the use of base64 encoding?
class MailerController extends AbstractController
{
#[Route('/')]
public function templatedEmail(MailerInterface $mailer): Response
{
$email = (new TemplatedEmail())
->from('[email protected]')
->to(new Address('[email protected]'))
->subject('Thanks for signing up!')
->htmlTemplate('emails/signup.html.twig') // <-- no base64 encoding option
->locale('de')
->context([
'expiration_date' => new \DateTime('+7 days'),
'username' => 'foo',
]);
$mailer->send($email);
return new Response('templated_email');
}
}
Solution under investigation...
That was my first investigation but, IMO, charset and encoding are 2 different things and encoded content can be iso or utf. And indeed, in this case, accentuated characters (like éàè) are broken at display because utf8 is not specified.
It's to encode text and html parts of the
TemplatedEmail.
Does the charset represent the "current" charset ?
Here, $email->getEncoding() would returns 'base64' .. even if the content is not yet base64 encoded, right ?
So how would you do if someone wanted to pass some already base64-encoded text (genuine question, not saying this is a every-day-scenario either) ?
Rapid test with accentuated characters:
#[Route('/')]
public function textHtml(MailerInterface $mailer): Response
{
$email = (new Email())
->from('[email protected]')
->to('[email protected]')
->subject('Time for Symfony Mailer!')
->text('éàèéàèéàèéàèéàèéàè', null)
->html('<p>éàèéàèéàèéàèéàèéàè</p>', null);
$mailer->send($email);
return new Response('text_html');
}
Results:
From: [email protected]
To: [email protected]
Subject: Time for Symfony Mailer!
MIME-Version: 1.0
Date: Fri, 08 Mar 2024 22:22:29 +0000
Message-ID: <[email protected]>
Content-Type: multipart/alternative; boundary=M0Ye1N1G
--M0Ye1N1G
Content-Type: text/plain
Content-Transfer-Encoding: base64
w6nDoMOow6nDoMOow6nDoMOow6nDoMOow6nDoMOow6nDoMOo
--M0Ye1N1G
Content-Type: text/html
Content-Transfer-Encoding: base64
PHA+w6nDoMOow6nDoMOow6nDoMOow6nDoMOow6nDoMOow6nDoMOoPC9wPg==
--M0Ye1N1G--
Screenshot:
Everything looks good :thinking:
- However, I don't know in what context (Windows?) would an iso charset be necessary?
- Have you had problems encoding accented characters?
For me, charset can depends of the part : https://www.rfc-editor.org/rfc/rfc9110#section-8.3.2
Or maybe I misunderstood something in the RFC.
@jprivet-dev in your last exemple, is argument $charset can really be null ?
public function text($body, string $charset = 'utf-8'): static
Maybe signature has to be update as :
public function text($body, ?string $charset = 'utf-8'): static
And I think, charset reading depends of client :
For me, charset can depends of the part : https://www.rfc-editor.org/rfc/rfc9110#section-8.3.2
Or maybe I misunderstood something in the RFC.
Perhaps this is a subject that should be explored in greater depth to get closer to the RFC?
@jprivet-dev in your last exemple, is argument $charset can really be null ?
public function text($body, string $charset = 'utf-8'): staticMaybe signature has to be update as :
public function text($body, ?string $charset = 'utf-8'): static
Yes, that's exactly what I was suggesting and testing in my previous message :smile: :
class Email extends Message
{
public function text($body, ?string $charset = 'utf-8'): static // <-- add "?"
{
...
}
public function html($body, ?string $charset = 'utf-8'): static // <-- add "?"
{
...
}
}
However, the problem remains the same with TemplatedEmail(): how to indicate the use of base64 encoding?
However, the problem remains the same with TemplatedEmail(): how to indicate the use of base64 encoding?
I have add textEncoding() and htmlEncoding() methods in Email parent class, to have encoding setters and be able to do it with TemplatedEmail