Mail

Wysyłanie
E-maile

Nette Mail

Planujesz wysyłać e-maile, na przykład newslettery albo potwierdzenia zamówień? Nette Framework daje potrzebne narzędzia z bardzo przyjaznym API. Pokażemy Ci:

  • jak utworzyć e-mail wraz z załącznikami
  • jak go wysłać
  • jak łączyć e-maile z szablonami

Instalacja

Pobierz i zainstaluj bibliotekę za pomocą Composera:

composer require nette/mail

Tworzenie e-maili

E-mail to obiekt Nette\Mail\Message. Utwórzmy go tak:

$mail = new Nette\Mail\Message;
$mail->setFrom('John <john@example.com>')
	->addTo('peter@example.com')
	->addTo('jack@example.com')
	->setSubject('Order Confirmation')
	->setBody("Hello,\nYour order has been accepted.");

Wszystkie podane parametry muszą być w kodowaniu UTF-8.

Adresy z domeną umiędzynarodowioną, jak jan@příklad.cz, konwertowane są automatycznie na postać ASCII zwaną punycode, której wymagają serwery pocztowe; potrzebne jest do tego rozszerzenie intl.

Oprócz podawania odbiorców metodą addTo() możesz podać też odbiorców kopii metodą addCc() albo odbiorców kopii ukrytej metodą addBcc(). Wszystkie te metody, wraz z setFrom(), przyjmują adresata na trzy sposoby:

$mail->setFrom('john.doe@example.com');
$mail->setFrom('john.doe@example.com', 'John Doe');
$mail->setFrom('John Doe <john.doe@example.com>');

Ciało e-maila napisane w HTML przekazuje się metodą setHtmlBody():

$mail->setHtmlBody('<p>Hello,</p><p>Your order has been accepted.</p>');

Nie musisz tworzyć alternatywy tekstowej; Nette wygeneruje ją za Ciebie automatycznie. A jeśli e-mail nie ma ustawionego tematu, spróbuje wziąć go z elementu <title>.

Obrazki da się też wyjątkowo łatwo osadzić w ciele HTML. Wystarczy przekazać jako drugi parametr ścieżkę, gdzie obrazki fizycznie się znajdują, a Nette automatycznie dołączy je do e-maila:

// automatycznie dodaje do e-maila /path/to/images/background.gif
$mail->setHtmlBody(
	'<b>Hello</b> <img src="background.gif">',
	'/path/to/images',
);

Algorytm osadzania obrazków szuka tych wzorców: <img src=...>, <body background=...>, url(...) wewnątrz atrybutu HTML style oraz specjalnej składni [[...]].

Czy wysyłanie e-maili mogłoby być jeszcze łatwiejsze?

E-maile są jak pocztówki. Nigdy nie wysyłaj e-mailem haseł ani innych danych uwierzytelniających.

Pozostałe opcje

Obiekt Message pozwala też ustawić adres do odpowiedzi, ścieżkę powrotną dla wiadomości odbitych i priorytet wiadomości:

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

Priorytet to jedna ze stałych Message::High, Message::Normal albo Message::Low.

Wypisanie jednym kliknięciem

Gmail i Yahoo wymagają, żeby poczta masowa, jak newslettery, oferowała wypisanie jednym kliknięciem bezpośrednio w kliencie pocztowym. Zajmuje się tym para nagłówków zdefiniowanych przez RFC 8058, którą poprawnie ustawia za Ciebie metoda setUnsubscribe():

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

URL musi wypisać odbiorcę w odpowiedzi na samo żądanie HTTP POST, bez dalszego potwierdzania. Drugi parametr może podać adres e-mail jako rozwiązanie zapasowe dla klientów, które nie potrafią wysłać POST-a; działa też samodzielnie: $mail->setUnsubscribe(email: 'unsubscribe@example.com').

Załączniki

Do e-maili możesz oczywiście dołączać pliki. Służy do tego metoda addAttachment(string $file, ?string $content = null, ?string $contentType = null).

// dołącza do e-maila plik /path/to/example.zip pod nazwą example.zip
$mail->addAttachment('/path/to/example.zip');

// dołącza plik /path/to/example.zip pod nazwą info.zip
$mail->addAttachment('info.zip', file_get_contents('/path/to/example.zip'));

// dołącza plik example.txt o treści "Hello John!"
$mail->addAttachment('example.txt', 'Hello John!');

Plik możesz też osadzić bezpośrednio w ciele HTML za pomocą addEmbeddedFile(). Zwraca utworzoną część MIME, do której Content-ID odwołujesz się w HTML-u (to dokładnie ten mechanizm, którego automatyczne osadzanie obrazków używa wewnętrznie):

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

Szablony

Jeśli wysyłasz e-maile HTML, świetną opcją jest pisanie ich w systemie szablonów Latte. Jak to zrobić?

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

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

Plik email.latte:

<html>
<head>
	<meta charset="utf-8">
	<title>Order Confirmation</title>
	<style>
	body {
		background: url("background.png")
	}
	</style>
</head>
<body>
	<p>Hello,</p>

	<p>Your order number {$orderId} has been accepted.</p>
</body>
</html>

Nette automatycznie osadza wszystkie obrazki, ustawia temat na podstawie elementu <title> i generuje dla HTML-a alternatywę tekstową.

Użycie w Nette Application

Jeśli używasz e-maili razem z Nette Application, czyli z presenterami, możesz chcieć tworzyć w szablonach odnośniki atrybutem n:href albo tagiem {link}. Latte domyślnie ich nie zna, ale bardzo łatwo je dodać. Odnośniki potrafi tworzyć obiekt Nette\Application\LinkGenerator, a uzyskasz go, pozwalając sobie go przekazać przez wstrzykiwanie zależności:

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;
	}
}

W szablonie tworzysz potem odnośniki tak, jak jesteś przyzwyczajony. Wszystkie odnośniki utworzone przez LinkGenerator będą absolutne.

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

Inlining CSS

Nette\Mail\CssInliner konwertuje reguły CSS na inline'owe atrybuty style, żeby e-maile renderowały się spójnie we wszystkich klientach. Generuje też atrybuty HTML dla kompatybilności z Outlookiem.

Wymaga PHP 8.4 albo nowszego i rozszerzenia dom.

Większość klientów pocztowych ma ograniczone wsparcie dla tagów <style> albo ignoruje je całkowicie. Żeby zapewnić poprawne renderowanie, reguły CSS trzeba przekonwertować na inline'owe atrybuty style na poszczególnych elementach. Wystarczy przepuścić swój HTML przez inline():

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

Jeśli na przykład HTML zawiera:

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

Wynik po inliningu będzie taki (tag <style> zostaje zachowany, ale dla zwięzłości go tu pomijamy):

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

Tag <style> jest zawsze zachowywany w wyjściu, więc zapytania @media i inne reguły, których nie da się zinlinować, działają dalej.

Oprócz wyciągania stylów z tagów <style> możesz dostarczyć CSS także metodą addCss(). CSS trzeba zinlinować, zanim przekażesz 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);

Gdy wiele reguł celuje w tę samą właściwość elementu, o zwycięzcy decyduje kaskada CSS, tak samo jak w przeglądarce: deklaracje !important biją zwykłe, istniejący inline'owy atrybut style bije dowolny selektor, selektor bardziej szczegółowy bije mniej szczegółowy, a przy remisie wygrywa reguła późniejsza. Reguły z tagów <style> przetwarzane są przed tymi dodanymi przez addCss(), a wypisywana jest tylko wartość zwycięska.

Reguły at, jak @media czy @font-face, są przy inliningu pomijane. Zwróć uwagę, że pseudoklas jak :hover nie da się sensownie zinlinować, bo style inline nie wspierają stanów dynamicznych.

Atrybuty HTML dla Outlooka

Desktopowe wersje Microsoft Outlook używają silnika renderującego Worda, który nie rozumie wielu właściwości CSS. Żeby zapewnić kompatybilność, CssInliner automatycznie generuje z reguł CSS odpowiadające atrybuty HTML obok stylów inline:

Właściwość CSS Atrybut HTML Stosowana do
background-color bgcolor <table>, <td>, <th>, <body><tr>
width width <table>, <td>, <th><img>
height height <table>, <td>, <th><img>
border-spacing cellspacing <table>

Dla width, height i cellspacing jednostka px jest automatycznie usuwana (np. width: 600px staje się width="600"), procent zachowuje swój %, a wartości, których atrybut nie potrafi wyrazić, jak auto czy calc(), nie tworzą żadnego atrybutu. Ustawiane są jednocześnie styl inline i atrybut HTML, więc e-mail renderuje się poprawnie zarówno w nowoczesnych klientach, jak i w Outlooku.

Atrybuty HTML generowane są wyłącznie z reguł CSS przetworzonych przez CssInliner, a nie z atrybutów style obecnych już w oryginalnym HTML-u.

Wysyłanie e-maili

Mailer to klasa odpowiedzialna za wysyłanie e-maili. Implementuje interfejs Nette\Mail\Mailer, a do dyspozycji jest kilka gotowych mailerów, które przedstawimy.

Framework automatycznie dodaje do kontenera DI usługę Nette\Mail\Mailer na podstawie konfiguracji, którą uzyskasz, pozwalając sobie ją przekazać przez wstrzykiwanie zależności.

SendmailMailer

Domyślnym mailerem jest SendmailMailer, który używa funkcji PHP mail. Przykład użycia:

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

Jeśli chcesz ustawić returnPath, a Twój serwer i tak go nadpisuje, użyj $mailer->commandArgs = '-fmy@email.com'.

Domyślnie SendmailMailer przekazuje adres nadawcy funkcji mail() jako nadawcę koperty (argument -f). Możesz to wyłączyć przez $mailer->setEnvelopeSender(false).

SmtpMailer

Do wysyłania poczty przez serwer SMTP użyj SmtpMailer.

$mailer = new Nette\Mail\SmtpMailer(
	host: 'smtp.gmail.com',
	username: 'john@gmail.com',
	password: '*****', // Twoje hasło
	encryption: 'ssl', // albo 'tls'
);
$mailer->send($mail);

Konstruktorowi można przekazać następujące dodatkowe parametry:

  • port – jeśli nie jest ustawiony, używana jest wartość domyślna: 465 dla ssl, 587 dla tls, w przeciwnym razie 25
  • timeout – timeout połączenia SMTP
  • persistent – użycie połączenia trwałego
  • clientHost – podaje nagłówek host klienta
  • streamOptions – pozwala ustawić opcje kontekstu SSL dla połączenia

Uwierzytelnianie OAuth 2.0

Gmail i Microsoft 365 wycofują uwierzytelnianie hasłem dla SMTP i wymagają zamiast niego tokenu dostępowego OAuth 2.0 (mechanizm XOAUTH2). Przekaż token metodą setAccessToken(); nazwa użytkownika zostaje, hasło pozostawia się puste:

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

Ponieważ tokeny dostępowe wygasają, możesz przekazać zamiast tego callback; wywoływany jest przy każdym połączeniu, więc zawsze może dostarczyć świeży token. Uzyskanie i odświeżanie tokenu pozostaje po Twojej stronie albo po stronie Twojej biblioteki OAuth:

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

FallbackMailer

Ten mailer nie wysyła e-maili bezpośrednio, tylko pośredniczy w wysyłaniu przez zestaw mailerów. Jeśli jeden mailer zawiedzie, ponawia próbę kolejnym. Jeśli zawiedzie ostatni, zaczyna znowu od pierwszego.

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

Pozostałe parametry konstruktora to liczba ponowień (domyślnie 3) i czas oczekiwania między nimi w milisekundach (domyślnie 1000). Jeśli wszystkie mailery zawiodą w każdej próbie, rzucany jest Nette\Mail\FallbackMailerException, którego właściwość $failures trzyma zebrane wyjątki.

Mailer, którego awaria jest trwała, na przykład serwer SMTP odrzucający dane uwierzytelniające, jest wyłączany z dalszych prób: ponawianie nie może zmienić wyniku.

Kolejny mailer możesz dodać później metodą addMailer() i zarejestrować zdarzenie $onFailure, wywoływane po każdej nieudanej próbie:

$mailer->onFailure[] = function ($mailer, $exception, $failedMailer, $mail) {
	// np. zalogowanie nieudanej próby
};

FileMailer

Ten mailer nic nie wysyła: zapisuje każdą wiadomość jako plik .eml w podanym katalogu. Pliki otwierają się w dowolnym kliencie pocztowym, więc możesz sprawdzić dokładnie, co zostałoby wysłane, co przydaje się w testach i podczas tworzenia.

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

Debugowanie e-maili

Przy tworzeniu albo na serwerze stagingowym nie chcesz, żeby testowy e-mail wymknął się do prawdziwego klienta. Są dwa sposoby, żeby mieć pewność, że to się nigdy nie stanie.

Zalecane rozwiązanie lokalne to uruchomienie na swojej maszynie lekkiego łapacza SMTP, jak Mailpit albo MailHog. Przyjmują każdą wiadomość, pokazują ją w interfejsie webowym i nigdy niczego nie przekazują dalej; wystarczy skierować Nette Mail na 127.0.0.1:1025:

mail:
	smtp: true
	host: 127.0.0.1
	port: 1025

Dla stagingu albo środowisk, w których nie możesz uruchomić lokalnego łapacza, Nette Mail ma wbudowane przekierowanie. Ustaw cel w konfiguracji, a każdy odbiorca To, Cc i Bcc zostanie nim zastąpiony. Nette Mail zachowuje oryginały w nagłówkach X-Original-*, żebyś widział, do kogo e-mail był przeznaczony, a do tematu możesz doklejać znacznik:

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

Skrócona postać redirect: dev@example.com działa, gdy nie potrzebujesz prefiksu tematu. W trybie debug automatycznie podpina się panel Tracy Bara wypisujący wszystkie wysłane e-maile.

Wewnętrznie zajmuje się tym Nette\Mail\Interceptor, który udostępnia też zdarzenie $onSent dla własnych słuchaczy (logi audytowe, metryki, …).

DKIM

DKIM (DomainKeys Identified Mail) to technologia zwiększająca wiarygodność e-maili, która pomaga też wykrywać podrobione wiadomości. Wysyłana wiadomość podpisywana jest kluczem prywatnym domeny nadawcy, a podpis ten zapisywany jest w nagłówku e-maila. Serwer odbiorcy porównuje ten podpis z kluczem publicznym zapisanym w rekordach DNS domeny. Jeśli podpis się zgadza, dowodzi to, że e-mail faktycznie pochodzi z domeny nadawcy i że wiadomość nie została po drodze zmodyfikowana.

Mailera do podpisywania e-maili możesz ustawić bezpośrednio w konfiguracji. Jeśli nie używasz dependency injection, używa się go tak:

$signer = new Nette\Mail\DkimSigner(
	domain: 'yourdomain.com',
	selector: 'dkim', // selektor z rekordu DNS
	privateKey: file_get_contents('/path/to/dkim.key'), // ścieżka do Twojego klucza prywatnego
	passPhrase: 'your_passphrase', // hasło do klucza prywatnego, jeśli jest
);

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

Kluczem prywatnym może być albo klucz RSA w formacie PEM, albo klucz Ed25519 (RFC 8463) jako surowe bajty zakodowane base64; typ wykrywany jest z samego klucza. Podpisywanie Ed25519 wymaga rozszerzenia sodium.

W parametrze oversignHeaders możesz wypisać nagłówki chronione przed doklejeniem drugiej kopii do już podpisanej wiadomości, co jest sztuczką stosowaną przez podrobione e-maile; zwykłym kandydatem jest From.

Konfiguracja

Przegląd opcji konfiguracyjnych dla Nette Mail. Jeśli nie używasz całego frameworku, tylko tej biblioteki, przeczytaj, jak wczytać konfigurację.

Domyślnie do wysyłania e-maili używany jest Nette\Mail\SendmailMailer, który nie wymaga dalszej konfiguracji. Możemy jednak przełączyć go na Nette\Mail\SmtpMailer:

mail:
	# użyj SmtpMailer
	smtp: true       # (bool) domyślnie false

	host: ...        # (string) nazwa hosta serwera SMTP
	port: ...        # (int) port serwera SMTP
	username: ...    # (string) nazwa użytkownika do uwierzytelniania SMTP
	password: ...    # (string) hasło do uwierzytelniania SMTP
	timeout: ...     # (int) timeout połączenia SMTP
	encryption: ...  # (ssl|tls|null) domyślnie null (alias 'secure')
	clientHost: ...  # (string) nazwa hosta klienta, domyślnie $_SERVER['HTTP_HOST'] albo 'localhost'
	persistent: ...  # (bool) użycie połączenia trwałego, domyślnie false

	# opcje kontekstu strumienia dla połączenia SMTP, domyślnie stream_context_get_default()
	context:
		ssl:         # wszystkie opcje na https://www.php.net/manual/en/context.ssl.php
			allow_self_signed: ...
			...
		http:        # lista opcji na https://www.php.net/manual/en/context.http.php
			header: ...
			...

Weryfikację certyfikatu SSL możesz wyłączyć opcją context › ssl › verify_peer: false. Zdecydowanie odradzamy to robić, bo czyni to aplikację podatną. Zamiast tego dodaj certyfikaty do magazynu zaufania.

Żeby zwiększyć wiarygodność, możemy podpisywać e-maile technologią DKIM:

mail:
	dkim:
		domain: myweb.com                  # Twoja domena
		selector: lovenette                # selektor DKIM
		privateKey: %appDir%/cert/dkim.key # ścieżka do pliku Twojego klucza prywatnego
		passPhrase: ...                    # hasło do klucza prywatnego, jeśli potrzebne

Opcje przekierowywania wszystkich e-maili i włączania panelu debugowego opisuje sekcja Debugowanie e-maili:

mail:
	# przekierowuje wszystkie e-maile na jeden adres
	redirect: dev@example.com

	# włącza (true) albo wyłącza (false) panel Tracy i przechwytywanie e-maili
	debugger: ...    # (bool) domyślnie null, czyli auto w trybie debug

Usługi DI

Do kontenera DI dodawane są te usługi:

Nazwa Typ Opis
mail.mailer Nette\Mail\Mailer klasa wysyłająca e-maile
mail.signer Nette\Mail\Signer podpisywanie DKIM

Jeśli aktualizujesz do nowszej wersji, zajrzyj na stronę aktualizacji.

wersja: 4.x