Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Mail Laravel Package

nette/mail

Nette Mail is a simple, flexible PHP library for sending emails. It supports SMTP, sendmail, and PHP mail, with a clean API for building messages, attachments, HTML bodies, and multiple recipients.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and pick your wiring path. composer require nette/mail pulls in only nette/utils ^4.1; you need PHP 8.2+ and ext-iconv. There are two integration styles:

  1. Standalone — instantiate Nette\Mail\SmtpMailer (or SendmailMailer) yourself. Good for a single script or when you are not on Nette DI.
  2. Nette DI — register Nette\Bridges\MailDI\MailExtension and configure everything in NEON. This is the path that gives you DKIM, Interceptor, and the Tracy panel for free.
extensions:
	mail: Nette\Bridges\MailDI\MailExtension

mail:
	smtp:
		host: smtp.example.com
		port: 587
		username: apikey
		password: %smtpPassword%
		encryption: tls        # note: renamed from 'secure' in v3.1.10
		timeout: 20
	dkim:
		domain: example.com
		selector: mail
		privateKey: %appDir%/../certs/dkim.key
		passPhrase: %dkimPass%

Where to look first. Read Message and MimePart in src/Mail/ — Message is the fluent builder and it is MimePart, which means everything you learn about headers applies to attachments too. Then read HtmlComposer, because since v4.1.2 it is the actual engine behind setHtmlBody() and the place where CSS inlining and image embedding live. docs/upgrading.md in the repo is a per-release list of public-facing changes — grep it before you upgrade.

First use case: a password-reset mail. This is the 20-line version you will write on day one:

$mail = new Nette\Mail\Message;
$mail->setFrom('Acme <[email protected]>')
	->addTo($user->email, $user->name)
	->setSubject('Reset your password')
	->setHtmlBody(__DIR__ . '/emails/reset.latte');   // or a string of HTML

(new Nette\Mail\SendmailMailer)->send($mail);

setHtmlBody() does more than store a string: it extracts <title> into the subject if you did not set one, generates the plain-text alternative, and embeds locally referenced images. You get a multipart message without writing MIME code.


Implementation Patterns

Wrap the mailer behind your own service

Do not sprinkle new SmtpMailer through controllers. Bind Nette\Mail\Mailer (the interface, renamed from IMailer in v3.1.0) once and inject it:

final class MailService
{
	public function __construct(
		private Nette\Mail\Mailer $mailer,
		private Nette\Application\LinkGenerator $links,
	) {}
}

Everything in the package — SmtpMailer, SendmailMailer, FallbackMailer, Interceptor — implements that interface, so swapping delivery is a config change, not a refactor. This is also your seam for tests.

Keep templates out of the mail layer

Message has no template engine. Render with Latte/Twig/Blade into a string, then hand the string (or a temp file) to setHtmlBody(). The <title> element is your subject source of truth if you prefer convention over an explicit setSubject():

$html = $this->latte->renderToString(__DIR__ . '/emails/order.latte', $params);
$mail->setHtmlBody($html, basePath: __DIR__ . '/emails/assets');

basePath is what makes src="logo.png" resolve. Omit it and images are silently not embedded.

Use HtmlComposer directly when you compose across sources

If your HTML arrives in pieces — a layout, a tenant stylesheet, blocks of personalization — HtmlComposer is the standalone pipeline you want. Note inlineCss() accumulates stylesheets across calls:

$composer = new Nette\Mail\HtmlComposer;
$composer->inlineCss($tenantCss);      // can be called repeatedly
$composer->inlineCss($brandOverrides);
$html = $composer->inlineCss($bodyHtml)  // returns processed HTML
	->embedImages(__DIR__ . '/assets');  // then embed

Message::setHtmlBody() remains a BC-friendly facade over the same pipeline, so migrating gradually is safe.

Attachments and inline images

$mail->addAttachment('/path/to/invoice.pdf', 'Invoice-2026-04.pdf');
$mail->addAttachment($pdfBinary, 'receipt.pdf');   // content given → filename untouched
$mail->addInlinePart(new Nette\Mail\Message(...)); // rarely needed; setHtmlBody handles most cases

In HTML bodies you can reference local files directly and they are converted to cid: parts automatically. The [[image.gif]] syntax from v2.4.0 also still works.

Multi-recipient / Bcc campaigns

$mail->addBcc('[email protected]');

If only Bcc recipients exist, v4.0.1 makes SmtpMailer emit To: undisclosed-recipients:; so strict servers do not reject the message. Do not assume To is populated — read recipients from getRecipients() when auditing.

Failover and retry

FallbackMailer wraps an ordered list of mailers and moves to the next one when the previous throws:

$mailer = new Nette\Mail\FallbackMailer([
	new Nette\Mail\SmtpMailer('smtp.primary...'),
	new Nette\Mail\SmtpMailer('smtp.backup...'),
]);

This is transport failover, not a queue. It runs synchronously inside the request. For anything user-visible, push the Message serialization onto a real queue job and send from a worker — otherwise an SMTP timeout becomes an HTTP timeout.

Deliverability as configuration, not code

Put DKIM in config and forget it. Two details matter for day-to-day: since v4.1.1 SendmailMailer signs the full message so Subject and To are covered (previously a real spam-folder cause at Gmail/Outlook), and SmtpMailer does not sign — the signer is wired into the send path, so verify your chosen transport produces a valid signature with dkimvalidator before go-live.

Make dev/staging structurally incapable of emailing customers

This is the single highest-value feature for team workflow:

mail:
	redirect: [email protected]
	debugger: true          # forces MailTracy panel on/off explicitly

Interceptor rewrites To/Cc/Bcc and read-receipt headers to the fixed address, preserves the originals in X-Original-To / X-Original-Cc / X-Original-Bcc, and can prefix the subject. MailTracy\MailPanel subscribes to Interceptor::$onSent and lists every message with status in the debug bar. Adopt this in every non-prod environment on day one.

Testing

Two clean options:

// 1. Capture via the interceptor event
Nette\Mail\Interceptor::$onSent[] = fn($mail) => $captured[] = $mail;

// 2. Implement the Mailer interface with a NullMailer / ArrayMailer in tests
final class ArrayMailer implements Nette\Mail\Mailer
{
	public array $sent = [];
	public function send(Nette\Mail\Message $message): void { $this->sent[] = $message; }
}

Because Message extends MimePart, you can assert on $mail->getHeader('Subject'), getRecipients(), and $mail->generateMessage() (the raw MIME string) without touching a socket.


Gotchas and Tips

Display-name quoting is subtle — and was broken twice. v4.2.1 fixed a regression introduced by the DKIM hardening in v4.1.1: any non-ASCII name (i.e. every name with diacritics) was being wrapped in a quoted-string before MIME encoding, so the quotes ended up inside the base64 payload and recipients saw "Objednávky domena.cz" literally. Quoting is now restricted to characters that can restructure an address list (, ; : < > @ " \). Practical rule: pin ≥ 4.2.1 if you send to Czech/Slovak/German names, and if a customer reports stray quotes in the From field, check the version before touching your code.

setHtmlBody() has side effects you might not expect. It trims each line of the derived plain-text version (v3.1.7), propagates link target attributes into the text part (v2.3.3), and embeds images. If you already generate your own text alternative, call setBody() explicitly after setHtmlBody() to override it.

Inline-image paths are a classic silent failure. Filenames containing spaces or parentheses (holiday photos/beach.jpg, screenshot (1).png) broke embedding until v4.1.1. Very large base64 data: URIs (>500 kB) used to blow PCRE's backtrack limit and abort the whole send — also fixed in v4.1.1, but if you are on 4.1.0 or older, pre-shrink hero images rather than debugging a mysterious exception.

CssInliner needs PHP 8.4 + ext-dom. Selector matching uses Dom\HTMLDocument. On older PHP the inliner degrades rather than failing hard, and since v4.1.1 it skips selectors it cannot evaluate — so :hover, ::marker, and :checked do not take down the whole message. <style> tags are deliberately preserved so @media queries keep working in clients that support them; the inline style attributes are the Outlook fallback. Also emits Outlook-compatible HTML attributes (bgcolor, width, align). Do not "clean up" the leftover <style> block.

DKIM config quirks. The privateKey path is not a dynamic parameter (v3.1.2) — it is resolved statically, so do not try to build it from request context. testMode was removed from DkimSigner (v3.1.4); use getTime() if you need to control the signature timestamp. Ed25519 keys need ext-sodium, RSA needs ext-openssl.

Config key renames will bite on upgrade. secure → encryption (v3.1.10). In v4.0.0 the SmtpMailer and DkimSigner constructors changed $options array to individual parameters — a real BC break if you instantiated them directly. v4.0 also removed magic property access on Message in favour of getters. If you are upgrading from 3.x, budget an afternoon and read docs/upgrading.md.

Bcc-only messages. Covered above, but worth repeating because it is the kind of bug that only shows on some servers: pass only addBcc() and rely on v4.0.1's undisclosed-recipients header. On older versions you must add a dummy To.

Custom Message-ID is preserved (v3.1.6). If you need threading or idempotency, set it yourself before sending — do not expect the library to regenerate it.

Slow SMTP servers produced empty responses, patched twice (v3.1.11 and again v4.0.2). Set an explicit timeout in config and do not run sends inside a web request without one.

mail() may not exist. The library checks for the function's existence before SendmailMailer uses it (v2.4.6), but in containers it is often disabled — prefer SMTP and treat sendmail as the fallback. The -f option in the sendmail command can be turned off (v4.0.3) if your MTA rejects it.

Persistent SMTP connections are configurable via MailExtension (v2.4.6). They help in workers, hurt nothing in one-shot requests, but watch for stale sockets in long-running queue consumers — the connection does not self-heal, so recycle the process.

Extension points. For anything the library does not cover, implement Nette\Mail\Mailer (wrap an ESP SDK: SES, Postmark, SendGrid) and keep using Message for construction — that preserves message-building correctness while letting you swap transport. For custom MIME behaviour, subclass MimePart; for headers, $mail->setHeader('X-Thing', 'value') covers 95% of cases.

Know what it is not. There is no queue, no bounce/webhook handling, no templating, no bulk/batch API, and no analytics. It is a message builder plus three transports and a signer. The right architecture is Message (owned) → queue job → Mailer (swappable). That is also what makes ESP portability a config change rather than a rewrite.

Verify the license before shipping. Metadata reports NOASSERTION, which will trip a procurement or legal scan even though upstream is permissive. Get the actual LICENSE file reviewed and recorded before this becomes a dependency of a revenue path. Combined with the modest ecosystem signal (roughly 490 stars, zero declared dependents), plan to own the upgrade treadmill yourself — the v4.x line has already shipped encoding regressions in 4.1.1 and 4.2.1, so pin exact versions and re-run your own send tests on every bump.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log