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 prossl, 587 protls, jinak 25timeout– timeout pro SMTP spojenípersistent– použít persistent spojeníclientHost– nastavení hlavičky Host klientastreamOptions– 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.