Nette Mail 4.1.3: So Your Emails Actually Arrive
Sending an email is one line of code. Getting it delivered is a discipline where Gmail and Microsoft tighten the rules every year. Nette Mail 4.1.3 brings what providers demand today: OAuth sign-in, one-click unsubscribe and more modern DKIM. Plus one security patch that makes updating right away worth it.
And it's a backward compatible release: you rewrite nothing, just update and pick up the new features as you need them. Let's go through them in the order they will come after you: from the requirements of the big providers through security to developer convenience.
Passwords Are No Longer Enough: OAuth 2.0
Gmail and Microsoft 365 are gradually switching off username-and-password
sign-in for SMTP; app passwords are on their way out and the future belongs to
OAuth tokens. SmtpMailer therefore supports XOAUTH2
authentication:
$mailer = new Nette\Mail\SmtpMailer(
host: 'smtp.gmail.com',
username: 'franta@gmail.com',
password: '',
encryption: 'tls',
);
$mailer->setAccessToken($accessToken);
Access tokens expire, usually after an hour. Instead of a string you can pass a callback, which is invoked on every connection and always supplies a fresh token:
$mailer->setAccessToken(fn() => $oauth->getFreshToken());
Obtaining and refreshing the token stays with your OAuth library, that's its craft. Nette Mail handles just the SMTP part, and handles it completely: including properly closing the exchange when the server rejects the token, so the error shows you the real reason instead of a mysterious timeout.
One-Click Unsubscribe
Gmail and Yahoo require bulk senders to let recipients unsubscribe from a
newsletter with a single click right in the client. Technically this means a
pair of headers per RFC 8058 that must match exactly; the
List-Unsubscribe header alone does not satisfy the requirement.
This is exactly the kind of knowledge a library should hold, not your code:
$mail->setUnsubscribe('https://example.com/unsubscribe?token=xyz', 'unsubscribe@example.com');
The first parameter is a URL that unsubscribes the recipient in response to a bare HTTP POST, the second is an email address as a fallback for clients that cannot send a POST. Both headers are now also signed with DKIM, so nobody can swap the address the click goes to along the way.
DKIM: Ed25519 and One Patch
Nette Mail has been able to sign emails with DKIM for a long time.
Now, besides RSA, it also handles Ed25519 signatures per RFC 8463: you simply
pass the key and the library detects the type on its own: RSA comes in PEM
format, Ed25519 as raw bytes in base64. No new parameter, no configuration, you
just need the sodium extension.
There is also so-called oversigning: in the oversignHeaders
parameter you list headers that should be protected against a second copy being
appended to an already signed message. That's a favorite trick of spoofed
emails, because many clients display exactly the added From header.
With oversigning, such an attempt breaks the signature.
And the promised patch: the signature no longer includes the l=
tag with the body length. It sounded harmless, yet it allowed anyone to append
arbitrary content to a signed message without invalidating the signature. That
alone is a reason to update, even if none of the new features interest you.
CssInliner Learned the Cascade
Inlining CSS into style attributes is a necessity for emails,
because clients often ignore the <style> tag. Until now,
though, a simple rule applied: the last declaration wins. Take this
stylesheet:
p.intro { color: red; }
p { color: blue; }
A browser renders the paragraph with the intro class in red,
because the more specific selector wins. The inliner, however, colored it blue,
and the email differed from the website the stylesheet was written for. The more
carefully the styles were written, the further the result drifted.
Since version 4.1.3, CssInliner resolves conflicts with a real
CSS cascade, just like a browser: !important beats ordinary
declarations, an existing inline style beats selectors, then specificity decides
and only at the very end does order matter. Moreover, only the winning value is
written to the output, so style attributes are no longer bloated
with defeated declarations. The conversion to HTML attributes for Outlook got a
fix too: width: auto used to generate width="0" and
the column in Outlook collapsed to nothing; now the attribute simply isn't
created.
FileMailer and Other Small Things
The new FileMailer sends nothing: it writes every message as an
.eml file into a given directory. Open the file in any email client
and you see exactly what would have gone out. Ideal for tests and during
development:
$mailer = new Nette\Mail\FileMailer(__DIR__ . '/mails');
$mailer->send($mail);
Alongside all this, a number of smaller improvements arrived that you'll notice exactly when they matter:
- Addresses with an international domain like
jan@příklad.czare automatically converted to punycode, so ordinary SMTP servers won't reject them. - STARTTLS defaults to port 587 and negotiates exclusively TLS 1.2 and 1.3; when the handshake fails, you learn the real reason.
- A persistent connection that the server has meanwhile closed is quietly re-established before the next send, instead of every attempt ending with an error.
- Large attachments can no longer be silently truncated when the send buffer fills up, and timeouts also apply against a server that never stops responding.
FallbackMailerno longer falls back to mailers whose failure is permanent, for example due to rejected credentials.
How to Update
Version 4.1.3 is backward compatible, so the usual is enough:
composer update nette/mail
Sending an email is still one line of code. Making sure it actually arrives is now much more the library's job. You'll find a complete overview of all the options in the documentation.
Further reading
- Nette Http 3.4.0 Brings Protection Against SSRF Attacks
- Nette Utils 4.0: UTF-8, Finder and named arguments
- Latte 3.1: When a templating system truly understands HTML
- A Quarter Century of CSRF. Now the Browser Finally Handles It
- Nette Tester: HTTP testing has never been so easy
- A form inside a form? HTML says no, Nette 3.3 says yes
Sign in to submit a comment