Mail

odesílání
emailů

Nette Mail

Chystáte se odesílat e-maily, například newslettery nebo potvrzení objednávek? Nette Framework poskytuje potřebné nástroje s velmi příjemným API. Ukážeme si:

  • jak e-mail vytvořit včetně příloh
  • jak ho odeslat
  • jak spojit e-maily a šablony

Instalace

Knihovnu stáhnete a nainstalujete pomocí nástroje Composer:

composer require nette/mail

Vytvoření e-mailu

E-mail je objektem třídy Nette\Mail\Message. Vytvoříme jej třeba takto:

$mail = new Nette\Mail\Message;
$mail->setFrom('Franta <franta@example.com>')
	->addTo('petr@example.com')
	->addTo('jirka@example.com')
	->setSubject('Potvrzení objednávky')
	->setBody("Dobrý den,\nvaše objednávka byla přijata.");

Všechny zadávané parametry musí být v UTF-8.

Adresy s mezinárodními znaky v doméně, jako jan@příklad.cz, se automaticky převedou do ASCII podoby zvané punycode, kterou poštovní servery vyžadují; je k tomu potřeba rozšíření intl.

Kromě uvedení příjemce metodou addTo() lze uvést i příjemce kopie addCc(), nebo příjemce skryté kopie addBcc(). Ve všech těchto metodách včetně setFrom() můžeme adresáta zapsat třemi způsoby:

$mail->setFrom('franta@example.com');
$mail->setFrom('franta@example.com', 'Franta');
$mail->setFrom('Franta <franta@example.com>');

Tělo e-mailu zapsaného v HTML se předává metodou setHtmlBody():

$mail->setHtmlBody('<p>Dobrý den,</p><p>vaše objednávka byla přijata.</p>');

Textovou alternativu nemusíte vytvářet, Nette ji vygeneruje automaticky za vás. A pokud e-mail nemá nastavený subjekt, pokusí se jej převzít z elementu <title>.

Do HTML těla lze také neobyčejně snadno vkládat obrázky. Stačí jako druhý parametr předat cestu, kde se obrázky fyzicky nacházejí, a Nette je automaticky zahrne do e-mailu:

// automaticky přidá /path/to/images/background.gif do e-mailu
$mail->setHtmlBody(
	'<b>Hello</b> <img src="background.gif">',
	'/path/to/images',
);

Algoritmus vkládající obrázky vyhledává tyto vzory: <img src=...>, <body background=...>, url(...) uvnitř HTML atributu style a speciální syntaxi [[...]].

Může být odesílání e-mailů ještě jednodušší?

E-mail je něco jako pohlednice. Nikdy e-mailem neposílejte hesla ani jiné přístupové údaje.

Další volby

Objekt Message umožňuje nastavit i adresu pro odpověď (reply-to), návratovou adresu pro nedoručené zprávy (return path) a prioritu zprávy:

$mail->addReplyTo('reply@example.com', 'Podpora')
	->setReturnPath('bounces@example.com')
	->setPriority(Nette\Mail\Message::High);

Priorita je jedna z konstant Message::High, Message::Normal nebo Message::Low.

Odhlášení jedním kliknutím

Gmail a Yahoo vyžadují, aby hromadná pošta jako newslettery šla odhlásit jediným kliknutím přímo v e-mailovém klientu. Zajišťuje to dvojice hlaviček podle RFC 8058, které za vás správně nastaví metoda setUnsubscribe():

$mail->setUnsubscribe('https://example.com/unsubscribe?token=xyz');

URL musí příjemce odhlásit v reakci na samotný HTTP POST požadavek, bez jakéhokoli dalšího potvrzování. Druhým parametrem lze uvést e-mailovou adresu jako fallback pro klienty, které POST odeslat neumějí; funguje i samostatně: $mail->setUnsubscribe(email: 'unsubscribe@example.com').

Přílohy

Do e-mailu lze samozřejmě vkládat přílohy. Slouží k tomu metoda addAttachment(string $file, ?string $content = null, ?string $contentType = null).

// vloží do e-mailu soubor /path/to/example.zip pod názvem example.zip
$mail->addAttachment('/path/to/example.zip');

// vloží do e-mailu soubor /path/to/example.zip pojmenovaný info.zip
$mail->addAttachment('info.zip', file_get_contents('/path/to/example.zip'));

// vloží do e-mailu soubor example.txt s obsahem "Hello John!"
$mail->addAttachment('example.txt', 'Hello John!');

Soubor lze vložit i přímo do HTML těla metodou addEmbeddedFile(). Vrací vytvořenou MIME část, jejíž Content-ID v HTML odkážete (přesně tento mechanismus interně používá automatické vkládání obrázků):

$file = $mail->addEmbeddedFile('/path/to/logo.png');
$mail->setHtmlBody('<img src="cid:' . trim($file->getHeader('Content-ID'), '<>') . '">');

Šablony

Pokud posíláte HTML e-maily, přímo se nabízí je zapisovat v šablonovacím systému Latte. Jak na to?

$latte = new Latte\Engine;
$params = [
	'orderId' => 123,
];

$mail = new Nette\Mail\Message;
$mail->setFrom('Franta <franta@example.com>')
	->addTo('petr@example.com')
	->setHtmlBody(
		$latte->renderToString('/path/to/email.latte', $params),
		'/path/to/images',
	);

Soubor email.latte:

<html>
<head>
	<meta charset="utf-8">
	<title>Potvrzení objednávky</title>
	<style>
	body {
		background: url("background.png")
	}
	</style>
</head>
<body>
	<p>Dobrý den,</p>

	<p>Vaše objednávka číslo {$orderId} byla přijata.</p>
</body>
</html>

Nette automaticky vloží všechny obrázky, nastaví subject podle elementu <title> a vygeneruje textovou alternativu k HTML.

Použití v Nette Application

Pokud e-maily používáte společeně s Nette Application, tj. s presentery, můžete chtít v šablonách vytvářet odkazy pomocí atributu n:href nebo značky {link}. Ty Latte v základu nezná, ale je velmi snadné je doplnit. Vytvářet odkazy umí objekt Nette\Application\LinkGenerator, ke kterému se dostanete tak, že si jej necháte předat pomocí dependency injection:

use Nette;

class MailSender
{
	public function __construct(
		private Nette\Application\LinkGenerator $linkGenerator,
		private Nette\Bridges\ApplicationLatte\TemplateFactory $templateFactory,
	) {
	}

	private function createTemplate(): Nette\Application\UI\Template
	{
		$template = $this->templateFactory->createTemplate();
		$template->getLatte()->addProvider('uiControl', $this->linkGenerator);
		return $template;
	}

	public function createEmail(): Nette\Mail\Message
	{
		$template = $this->createTemplate();
		$html = $template->renderToString('/path/to/email.latte', $params);

		$mail = new Nette\Mail\Message;
		$mail->setHtmlBody($html);
		// ...
		return $mail;
	}
}

V šabloně potom vytváříme odkazy tak, jak jsme zvyklí. Všechny odkazy vytvořene přes LinkGenerator budou absolutní.

<a n:href="Presenter:action">Odkaz</a>

Inlinování CSS

Nette\Mail\CssInliner převádí CSS pravidla na inline atributy style, aby se e-maily zobrazovaly správně ve všech klientech. Zároveň generuje HTML atributy pro kompatibilitu s Outlookem.

Vyžaduje PHP 8.4 nebo novější a rozšíření dom.

Většina e-mailových klientů má omezenou podporu značky <style> nebo ji zcela ignoruje. Pro správné zobrazení je proto potřeba převést CSS pravidla na inline atributy style u jednotlivých elementů. Stačí HTML předat metodě inline():

$inliner = new Nette\Mail\CssInliner;
$html = $inliner->inline($html);

Pokud HTML obsahuje například:

<style>
p { margin: 0; color: #333; }
a { color: #a0704e; }
</style>
<p>Hello <a href="#">world</a></p>

Výsledek po inlinování bude (značka <style> se zachová, zde ji vynecháváme pro stručnost):

<p style="margin: 0; color: #333">Hello <a href="#" style="color: #a0704e">world</a></p>

Značka <style> zůstane ve výstupu vždy zachována, takže @media dotazy a další pravidla, která nelze inlinovat, budou nadále fungovat.

Kromě extrakce stylů ze značek <style> lze CSS dodat i metodou addCss(). Inlinování je třeba provést před předáním HTML do setHtmlBody():

$latte = new Latte\Engine;
$params = [
	'orderId' => 123,
];

$html = $latte->renderToString('/path/to/email.latte', $params);
$html = (new Nette\Mail\CssInliner)
	->addCss(file_get_contents('/path/to/email.css'))
	->inline($html);

$mail = new Nette\Mail\Message;
$mail->setHtmlBody($html);

Pokud na stejnou vlastnost elementu cílí více pravidel, o vítězi rozhoduje CSS kaskáda stejně jako v prohlížeči: deklarace s !important porazí obyčejné, existující inline atribut style porazí jakýkoli selektor, specifičtější selektor porazí méně specifický a při shodě vyhrává pozdější pravidlo. Pravidla ze značek <style> se zpracují před pravidly z addCss() a do výsledku se zapíše jen vítězná hodnota.

At-rules jako @media nebo @font-face se při inlinování přeskakují. Pseudo-třídy jako :hover nelze smysluplně inlinovat, protože inline styly nepodporují dynamické stavy.

HTML atributy pro Outlook

Desktopové verze Microsoft Outlooku používají vykreslovací jádro Wordu, které nerozumí mnoha CSS vlastnostem. Pro zajištění kompatibility CssInliner automaticky generuje odpovídající HTML atributy z CSS pravidel vedle inline stylů:

CSS vlastnost HTML atribut Aplikuje se na
background-color bgcolor <table>, <td>, <th>, <body><tr>
width width <table>, <td>, <th><img>
height height <table>, <td>, <th><img>
border-spacing cellspacing <table>

U width, height a cellspacing se automaticky odstraní jednotka px (např. width: 600px se převede na width="600"), procenta si % ponechají a hodnoty, které atribut vyjádřit neumí, jako auto nebo calc(), žádný atribut nevytvoří. Inline styl i HTML atribut se nastavují společně, takže se e-mail zobrazí správně v moderních klientech i v Outlooku.

HTML atributy se generují pouze z CSS pravidel zpracovaných třídou CssInliner, nikoliv z atributů style již přítomných v původním HTML.

Odeslání e-mailu

Mailer je třída zajišťující odesílání e-mailů. Implementuje rozhraní Nette\Mail\Mailer a k dispozici je několik předpřipravených mailerů, které si představíme.

Framework automaticky přidává do DI kontejneru službu typu Nette\Mail\Mailer sestavenou na základě konfigurace, a ke které se dostanete tak, že si ji necháte předat pomocí dependency injection.

SendmailMailer

Výchozí mailer je SendmailMailer, který používá PHP funkci mail. Příklad použití:

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

Pokud chcete nastavit returnPath a server vám ho pořád přepisuje, použijte $mailer->commandArgs = '-fMuj@email.cz'.

Ve výchozím stavu SendmailMailer předává adresu odesílatele funkci mail() jako envelope sender (argument -f). To lze vypnout metodou $mailer->setEnvelopeSender(false).

SmtpMailer

K odeslání pošty přes SMTP server slouží SmtpMailer.

$mailer = new Nette\Mail\SmtpMailer(
	host: 'smtp.gmail.com',
	username: 'franta@gmail.com',
	password: '*****',
	encryption: 'ssl',
);
$mailer->send($mail);

Konstruktoru lze předat tyto další parametry:

  • port – pokud není nastaven, použije se výchozí: 465 pro ssl, 587 pro tls, jinak 25
  • timeout – timeout pro SMTP spojení
  • persistent – použít persistent spojení
  • clientHost – nastavení hlavičky Host klienta
  • streamOptions – umožňuje nastavit SSL context options pro spojení

Přihlášení přes OAuth 2.0

Gmail a Microsoft 365 postupně ruší přihlašování heslem pro SMTP a místo něj vyžadují OAuth 2.0 access token (mechanismus XOAUTH2). Token předáte metodou setAccessToken(); uživatelské jméno zůstává, heslo se nechá prázdné:

$mailer = new Nette\Mail\SmtpMailer(
	host: 'smtp.gmail.com',
	username: 'franta@gmail.com',
	password: '',
	encryption: 'tls',
);
$mailer->setAccessToken($accessToken);

Protože access tokeny expirují, můžete místo řetězce předat callback; ten se zavolá při každém připojení, takže vždy dodá čerstvý token. Získání a obnovování tokenu zůstává na vás nebo vaší OAuth knihovně:

$mailer->setAccessToken(fn() => $oauthProvider->getFreshToken());

FallbackMailer

E-maily přímo neodesílá, ale odesílání zprostředkovává přes sadu mailerů. V případě, že jeden mailer selže, zopakuje pokus u dalšího. Pokud selže i poslední, začíná znovu od prvního.

$mailer = new Nette\Mail\FallbackMailer([
	$smtpMailer,
	$backupSmtpMailer,
	$sendmailMailer,
]);
$mailer->send($mail);

Dalšími parametry konstruktoru jsou počet opakování (výchozí 3) a čekací doba mezi nimi v milisekundách (výchozí 1000). Pokud selžou všechny mailery ve všech pokusech, vyhodí se Nette\Mail\FallbackMailerException, jejíž vlastnost $failures obsahuje nasbírané výjimky.

Mailer, jehož selhání je trvalé, například když SMTP server odmítne přihlašovací údaje, se z dalších pokusů vyřadí, protože opakování by na výsledku nic nezměnilo.

Další mailer lze přidat dodatečně metodou addMailer() a zaregistrovat událost $onFailure, která se zavolá po každém neúspěšném pokusu:

$mailer->onFailure[] = function ($mailer, $exception, $failedMailer, $mail) {
	// např. zalogování neúspěšného pokusu
};

FileMailer

Tento mailer nic neodesílá: každou zprávu zapíše jako soubor .eml do zadaného adresáře. Soubory otevřete v libovolném e-mailovém klientu, takže přesně vidíte, co by se odeslalo; to se hodí v testech a při vývoji.

$mailer = new Nette\Mail\FileMailer('/path/to/mails');
$mailer->send($mail);

Ladění e-mailů

Při vývoji nebo na staging serveru nechcete, aby se nějaký testovací e-mail náhodou dostal k reálnému zákazníkovi. Existují dva způsoby, jak tomu zabránit.

Pro lokální vývoj doporučujeme spustit u sebe lehký SMTP zachytávač jako Mailpit nebo MailHog. Přijmou každou zprávu, ukáží ji ve webovém UI a nikdy nic nepředají dál – Nette Mailu jen nastavíte 127.0.0.1:1025:

mail:
	smtp: true
	host: 127.0.0.1
	port: 1025

Pro staging nebo prostředí, kde lokální zachytávač spustit nejde, má Nette Mail vestavěný redirect. Nastavte cílovou adresu v konfiguraci a každý příjemce v To, Cc i Bcc bude nahrazen touto adresou. Nette Mail uloží originály do hlaviček X-Original-*, takže pořád vidíte, komu měl e-mail dorazit, a do předmětu lze přidat značku:

mail:
	redirect:
		to: dev@example.com
		subjectPrefix: '[debug]'   # volitelný

Zkrácený zápis redirect: dev@example.com postačí, když nepotřebujete prefix v předmětu. V debug módu se automaticky připojí panel v Tracy Baru s přehledem všech odeslaných e-mailů.

Interně tohle zajišťuje Nette\Mail\Interceptor, který nabízí ještě událost $onSent pro vlastní listenery (audit log, metriky, …).

DKIM

DKIM (DomainKeys Identified Mail) je technologie pro zvýšení důvěryhodnosti e-mailů, která také napomáhá odhalení podvržených zpráv. Odeslaná zpráva je podepsána privátním klíčem domény odesílatele a tento podpis je uložen v hlavičce e-mailu. Server příjemce porovná tento podpis s veřejným klíčem uloženým v DNS záznamech domény. Tím, že podpis odpovídá, je prokázáno, že e-mail skutečně pochází z odesílatelovy domény a že během přenosu zprávy nedošlo k její úpravě.

Podepisování e-mailů můžete maileru nastavit přímo v konfiguraci. Pokud nepoužíváte dependency injection, používá se tímto způsobem:

$signer = new Nette\Mail\DkimSigner(
	domain: 'nette.org',
	selector: 'dkim',
	privateKey: file_get_contents('../dkim/dkim.key'),
	passPhrase: '****',
);

$mailer = new Nette\Mail\SendmailMailer; // nebo SmtpMailer
$mailer->setSigner($signer);
$mailer->send($mail);

Privátní klíč může být buď RSA klíč ve formátu PEM, nebo klíč Ed25519 (RFC 8463) v podobě surových bajtů zakódovaných do base64; typ se pozná z klíče samotného. Podepisování Ed25519 vyžaduje rozšíření sodium.

V parametru oversignHeaders lze vyjmenovat hlavičky, které se mají chránit proti připojení druhé kopie k již podepsané zprávě, což je trik podvržených e-mailů; obvyklým kandidátem je From.

Konfigurace

Přehled konfiguračních voleb pro Nette Mail. Pokud nepoužívate celý framework, ale jen tuto knihovnu, přečtěte si, jak konfiguraci načíst.

Pro odesílání e-mailů se standardně používá mailer Nette\Mail\SendmailMailer, který se dále nekonfiguruje. Můžeme jej však přepnout na Nette\Mail\SmtpMailer:

mail:
	# použije SmtpMailer
	smtp: true       # (bool) výchozí je false

	host: ...        # (string)
	port: ...        # (int)
	username: ...    # (string)
	password: ...    # (string)
	timeout: ...     # (int)
	encryption: ...  # (ssl|tls|null) výchozí je null (má alias 'secure')
	clientHost: ...  # (string) výchozí je $_SERVER['HTTP_HOST'] nebo 'localhost'
	persistent: ...  # (bool) výchozí je false

	# kontext pro připojení k SMTP serveru, výchozí je stream_context_get_default()
	context:
		ssl:         # přehled voleb na https://www.php.net/manual/en/context.ssl.php
			allow_self_signed: ...
			...
		http:        # přehled voleb na https://www.php.net/manual/en/context.http.php
			header: ...
			...

Pomocí volby context › ssl › verify_peer: false lze vypnout ověřování SSL certifikátů. Důrazně nedoporučujeme tohle dělat, protože se aplikace stane zranitelnou. Místo toho přidejte certifikáty do uložiště.

Pro zvýšení důvěryhodnosti můžeme e-maily podpisovat pomocí technologie DKIM:

mail:
	dkim:
		domain: myweb.com
		selector: lovenette
		privateKey: %appDir%/cert/dkim.priv
		passPhrase: ...

Volby pro přesměrování všech e-mailů a zapnutí ladicího panelu popisuje sekce Ladění e-mailů:

mail:
	# přesměruje všechny e-maily na jednu adresu
	redirect: dev@example.com

	# zapne (true) nebo vypne (false) Tracy panel a odchytávání e-mailů
	debugger: ...    # (bool) výchozí je null, tj. auto v debug módu

Služby DI

Tyto služby se přidávají do DI kontejneru:

Název Typ Popis
mail.mailer Nette\Mail\Mailer třída odesílající e-maily
mail.signer Nette\Mail\Signer DKIM podepisování

Pokud aktualizujete balíček na novější verzi, podívejte se na stránku upgrade.

verze: 4.x 3.x 2.x