Nette Mail 4.1.3: So Your Emails Actually Arrive

2 hours ago by David Grudl  

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.cz are 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.
  • FallbackMailer no 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.

David Grudl Founder of Uměligence and creator of Nette Framework, the popular PHP framework. Since 2021, he's been fully immersed in artificial intelligence, teaching practical AI applications. He discusses weekly tech developments on Tech Guys with his co-hosts and writes for phpFashion and La Trine. He believes AI isn't science fiction—it's a practical tool for improving life today.