Mail

Envoi de
Emails

Nette Mail

Vous prévoyez d'envoyer des e-mails, par exemple des newsletters ou des confirmations de commande ? Nette Framework vous fournit les outils nécessaires avec une API très agréable. Nous allons voir :

  • comment créer un e-mail, pièces jointes comprises
  • comment l'envoyer
  • comment combiner e-mails et templates

Installation

Téléchargez et installez la bibliothèque à l'aide de Composer :

composer require nette/mail

Création d'e-mails

Un e-mail est un objet Nette\Mail\Message. Créons-en un ainsi :

$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.");

Tous les paramètres indiqués doivent être encodés en UTF-8.

Les adresses avec un domaine internationalisé, comme jan@příklad.cz, sont automatiquement converties dans la forme ASCII appelée punycode, exigée par les serveurs de messagerie ; cela nécessite l'extension intl.

Outre les destinataires indiqués avec addTo(), vous pouvez ajouter des destinataires en copie avec addCc(), ou en copie cachée avec addBcc(). Toutes ces méthodes, setFrom() comprise, acceptent le destinataire de trois façons :

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

Le corps d'un e-mail écrit en HTML se passe à la méthode setHtmlBody() :

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

Vous n'avez pas besoin de créer l'alternative texte, Nette la génère automatiquement pour vous. Et si l'e-mail n'a pas de sujet, il essaiera de le reprendre depuis l'élément <title>.

Les images s'intègrent au corps HTML avec une facilité déconcertante. Il suffit de passer en second paramètre le chemin où les images se trouvent physiquement, et Nette les joindra automatiquement à l'e-mail :

// automatically adds /path/to/images/background.gif to the email
$mail->setHtmlBody(
	'<b>Hello</b> <img src="background.gif">',
	'/path/to/images',
);

L'algorithme d'intégration des images recherche ces motifs : <img src=...>, <body background=...>, url(...) à l'intérieur de l'attribut HTML style, et la syntaxe spéciale [[...]].

L'envoi d'e-mails pourrait-il être encore plus simple ?

Les e-mails sont comme des cartes postales. N'envoyez jamais de mots de passe ni d'autres identifiants par e-mail.

Autres options

L'objet Message permet aussi de définir une adresse de réponse, un chemin de retour pour les messages rejetés et la priorité du message :

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

La priorité est l'une des constantes Message::High, Message::Normal ou Message::Low.

Désabonnement en un clic

Gmail et Yahoo exigent que les envois de masse comme les newsletters permettent le désabonnement en un seul clic directement dans le client de messagerie. C'est le rôle d'une paire d'en-têtes définis par la RFC 8058, que la méthode setUnsubscribe() met en place correctement pour vous :

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

L'URL doit désabonner le destinataire en réponse à une simple requête HTTP POST, sans autre confirmation. Le second paramètre peut fournir une adresse e-mail de repli pour les clients incapables d'envoyer un POST ; il fonctionne aussi seul : $mail->setUnsubscribe(email: 'unsubscribe@example.com').

Pièces jointes

Vous pouvez bien sûr joindre des fichiers aux e-mails. Utilisez pour cela la méthode addAttachment(string $file, ?string $content = null, ?string $contentType = null).

// attaches the file /path/to/example.zip to the email with the name example.zip
$mail->addAttachment('/path/to/example.zip');

// attaches the file /path/to/example.zip named info.zip
$mail->addAttachment('info.zip', file_get_contents('/path/to/example.zip'));

// attaches the file example.txt with the content "Hello John!"
$mail->addAttachment('example.txt', 'Hello John!');

Vous pouvez aussi intégrer un fichier directement dans le corps HTML avec addEmbeddedFile(). Elle renvoie la partie MIME créée, dont vous référencez le Content-ID dans le HTML (c'est exactement le mécanisme utilisé en interne par l'intégration automatique des images) :

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

Templates

Si vous envoyez des e-mails HTML, les écrire dans le système de templates Latte est une excellente option. Comment faire ?

$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',
	);

Fichier 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 intègre automatiquement toutes les images, définit le sujet d'après l'élément <title> et génère l'alternative texte du HTML.

Utilisation dans Nette Application

Si vous utilisez les e-mails avec Nette Application, c'est-à-dire avec des presenters, vous voudrez peut-être créer des liens dans les templates à l'aide de l'attribut n:href ou du tag {link}. Latte ne les connaît pas par défaut, mais il est très simple de les lui ajouter. L'objet Nette\Application\LinkGenerator sait créer des liens et vous vous le faites passer par injection de dépendances :

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

Dans le template, vous créez ensuite les liens comme d'habitude. Tous les liens créés via LinkGenerator seront absolus.

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

Inlining du CSS

Nette\Mail\CssInliner convertit les règles CSS en attributs style inline, pour que les e-mails s'affichent de la même façon dans tous les clients. Il génère aussi des attributs HTML pour la compatibilité avec Outlook.

Nécessite PHP 8.4 ou supérieur et l'extension dom.

La plupart des clients de messagerie prennent mal en charge les balises <style>, voire les ignorent complètement. Pour garantir un rendu correct, les règles CSS doivent être converties en attributs style inline sur les différents éléments. Il suffit de faire passer votre HTML par inline() :

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

Par exemple, si le HTML contient :

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

Le résultat après inlining sera (la balise <style> est conservée, mais omise ici par souci de concision) :

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

La balise <style> est toujours conservée dans la sortie, si bien que les requêtes @media et les autres règles qui ne peuvent pas être inlinées continuent de fonctionner.

Outre l'extraction des styles depuis les balises <style>, vous pouvez aussi fournir du CSS par la méthode addCss(). Il faut inliner le CSS avant de passer le HTML à 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);

Quand plusieurs règles visent la même propriété d'un élément, le vainqueur est décidé par la cascade CSS, exactement comme dans un navigateur : les déclarations !important l'emportent sur les normales, un attribut style inline existant l'emporte sur n'importe quel sélecteur, un sélecteur plus spécifique l'emporte sur un moins spécifique, et à égalité c'est la règle la plus tardive qui gagne. Les règles issues des balises <style> sont traitées avant celles ajoutées par addCss(), et seule la valeur gagnante est écrite.

Les at-rules comme @media ou @font-face sont ignorées lors de l'inlining. Notez que les pseudo-classes comme :hover ne peuvent pas être inlinées de façon sensée, car les styles inline ne connaissent pas les états dynamiques.

Attributs HTML pour Outlook

Les versions bureau de Microsoft Outlook utilisent le moteur de rendu de Word, qui ne comprend pas beaucoup de propriétés CSS. Pour assurer la compatibilité, CssInliner génère automatiquement, à côté des styles inline, les attributs HTML correspondant aux règles CSS :

Propriété CSS Attribut HTML Appliqué à
background-color bgcolor <table>, <td>, <th>, <body><tr>
width width <table>, <td>, <th><img>
height height <table>, <td>, <th><img>
border-spacing cellspacing <table>

Pour width, height et cellspacing, l'unité px est automatiquement retirée (par ex. width: 600px devient width="600"), un pourcentage garde son %, et les valeurs qu'un attribut ne sait pas exprimer, comme auto ou calc(), ne produisent aucun attribut. Le style inline et l'attribut HTML sont posés ensemble, si bien que l'e-mail s'affiche correctement aussi bien dans les clients modernes que dans Outlook.

Les attributs HTML ne sont générés qu'à partir des règles CSS traitées par CssInliner, pas à partir des attributs style déjà présents dans le HTML d'origine.

Envoi d'e-mails

Le mailer est une classe chargée d'envoyer les e-mails. Elle implémente l'interface Nette\Mail\Mailer, et plusieurs mailers tout faits sont disponibles ; nous allons les présenter.

Le framework ajoute automatiquement au conteneur DI un service Nette\Mail\Mailer bâti d'après la Configuration, que vous vous faites passer par injection de dépendances.

SendmailMailer

Le mailer par défaut est SendmailMailer, qui utilise la fonction PHP mail. Exemple d'utilisation :

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

Si vous voulez définir le returnPath et que votre serveur l'écrase malgré tout, utilisez $mailer->commandArgs = '-fmy@email.com'.

Par défaut, SendmailMailer passe l'adresse de l'expéditeur à la fonction mail() comme expéditeur d'enveloppe (l'argument -f). Vous pouvez le désactiver avec $mailer->setEnvelopeSender(false).

SmtpMailer

Pour envoyer le courrier via un serveur SMTP, utilisez SmtpMailer.

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

Les paramètres supplémentaires suivants peuvent être passés au constructeur :

  • port – s'il n'est pas défini, la valeur par défaut est utilisée : 465 pour ssl, 587 pour tls, sinon 25
  • timeout – délai d'attente de la connexion SMTP
  • persistent – utiliser une connexion persistante
  • clientHost – indiquer l'en-tête host du client
  • streamOptions – permet de définir les options du contexte SSL de la connexion

Authentification OAuth 2.0

Gmail et Microsoft 365 abandonnent l'authentification par mot de passe pour SMTP et exigent à la place un jeton d'accès OAuth 2.0 (le mécanisme XOAUTH2). Passez le jeton à la méthode setAccessToken() ; le nom d'utilisateur reste, le mot de passe est laissé vide :

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

Comme les jetons d'accès expirent, vous pouvez passer un callback à la place ; il est appelé à chaque connexion et peut donc toujours fournir un jeton frais. L'obtention et le renouvellement du jeton restent à votre charge ou à celle de votre bibliothèque OAuth :

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

FallbackMailer

Ce mailer n'envoie pas les e-mails lui-même, il fait passer l'envoi par un ensemble de mailers. Si l'un d'eux échoue, il retente avec le suivant. Si le dernier échoue, il repart du premier.

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

Les autres paramètres du constructeur sont le nombre de tentatives (par défaut 3) et le temps d'attente entre elles en millisecondes (par défaut 1000). Si tous les mailers échouent à chaque tentative, une Nette\Mail\FallbackMailerException est levée, dont la propriété $failures contient les exceptions collectées.

Un mailer dont l'échec est définitif, par exemple parce que le serveur SMTP refuse les identifiants, est écarté des tentatives suivantes : retenter ne changerait rien.

Vous pouvez ajouter un autre mailer plus tard avec addMailer() et vous abonner à l'événement $onFailure, appelé après chaque tentative échouée :

$mailer->onFailure[] = function ($mailer, $exception, $failedMailer, $mail) {
	// e.g. log the failed attempt
};

FileMailer

Ce mailer n'envoie rien : il écrit chaque message dans un fichier .eml du répertoire indiqué. Les fichiers s'ouvrent dans n'importe quel client de messagerie, vous voyez donc exactement ce qui aurait été envoyé, ce qui est bien pratique dans les tests et pendant le développement.

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

Débogage des e-mails

Pendant le développement ou sur un serveur de préproduction, vous ne voulez pas qu'un e-mail de test file vers un vrai client. Il existe deux façons de s'en prémunir.

L'installation locale recommandée consiste à faire tourner sur votre machine un attrapeur SMTP léger comme Mailpit ou MailHog. Ils acceptent tous les messages, les affichent dans une interface web et ne transmettent jamais rien : vous pointez simplement Nette Mail vers 127.0.0.1:1025 :

mail:
	smtp: true
	host: 127.0.0.1
	port: 1025

Pour la préproduction ou les environnements où vous ne pouvez pas faire tourner d'attrapeur local, Nette Mail dispose d'une redirection intégrée. Indiquez la destination dans la configuration et chaque destinataire To, Cc et Bcc est remplacé par celle-ci. Nette Mail conserve les originaux dans des en-têtes X-Original-*, vous voyez donc à qui l'e-mail était destiné, et vous pouvez préfixer le sujet d'un marqueur :

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

La forme raccourcie redirect: dev@example.com suffit quand vous n'avez pas besoin de préfixe de sujet. En mode debug, un panneau de la barre Tracy listant tous les e-mails envoyés s'attache automatiquement.

En interne, cela est assuré par Nette\Mail\Interceptor, qui expose aussi un événement $onSent pour vos propres écouteurs (journaux d'audit, métriques, …).

DKIM

DKIM (DomainKeys Identified Mail) est une technologie qui augmente la fiabilité des e-mails et aide aussi à détecter les messages usurpés. Le message envoyé est signé avec la clé privée du domaine de l'expéditeur, et cette signature est stockée dans l'en-tête de l'e-mail. Le serveur du destinataire compare cette signature avec la clé publique enregistrée dans les enregistrements DNS du domaine. Si la signature correspond, cela prouve que l'e-mail provient bien du domaine de l'expéditeur et que le message n'a pas été modifié en chemin.

Vous pouvez régler la signature des e-mails directement dans la Configuration. Si vous n'utilisez pas l'injection de dépendances, cela s'emploie ainsi :

$signer = new Nette\Mail\DkimSigner(
	domain: 'yourdomain.com',
	selector: 'dkim', // selector from DNS record
	privateKey: file_get_contents('/path/to/dkim.key'), // path to your private key
	passPhrase: 'your_passphrase', // passphrase for the private key, if any
);

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

La clé privée peut être soit une clé RSA au format PEM, soit une clé Ed25519 (RFC 8463) sous forme d'octets bruts encodés en base64 ; le type est détecté depuis la clé elle-même. La signature Ed25519 nécessite l'extension sodium.

Dans le paramètre oversignHeaders, vous pouvez lister les en-têtes à protéger contre l'ajout d'une seconde copie au message déjà signé, une ruse employée par les e-mails usurpés ; le candidat habituel est From.

Configuration

Vue d'ensemble des options de configuration de Nette Mail. Si vous n'utilisez pas tout le framework, mais seulement cette bibliothèque, lisez comment charger la configuration.

Par défaut, c'est Nette\Mail\SendmailMailer qui envoie les e-mails, et il ne demande aucune configuration supplémentaire. Nous pouvons toutefois passer à Nette\Mail\SmtpMailer :

mail:
	# use SmtpMailer
	smtp: true       # (bool) defaults to false

	host: ...        # (string) SMTP server hostname
	port: ...        # (int) SMTP server port
	username: ...    # (string) username for SMTP authentication
	password: ...    # (string) password for SMTP authentication
	timeout: ...     # (int) timeout for SMTP connection
	encryption: ...  # (ssl|tls|null) defaults to null (alias 'secure')
	clientHost: ...  # (string) client hostname, defaults to $_SERVER['HTTP_HOST'] or 'localhost'
	persistent: ...  # (bool) use persistent connection, defaults to false

	# stream context options for the SMTP connection, defaults to stream_context_get_default()
	context:
		ssl:         # all options at https://www.php.net/manual/en/context.ssl.php
			allow_self_signed: ...
			...
		http:        # options list at https://www.php.net/manual/en/context.http.php
			header: ...
			...

Vous pouvez désactiver la vérification du certificat SSL avec l'option context › ssl › verify_peer: false. Nous le déconseillons fortement, car cela rend l'application vulnérable. Préférez ajouter les certificats au magasin de confiance.

Pour gagner en fiabilité, nous pouvons signer les e-mails avec la technologie DKIM :

mail:
	dkim:
		domain: myweb.com                  # your domain
		selector: lovenette                # DKIM selector
		privateKey: %appDir%/cert/dkim.key # path to your private key file
		passPhrase: ...                    # passphrase for the private key, if needed

Les options de redirection de tous les e-mails et d'activation du panneau de débogage sont décrites dans la section Débogage des e-mails :

mail:
	# redirects all emails to a single address
	redirect: dev@example.com

	# enables (true) or disables (false) the Tracy panel and email interception
	debugger: ...    # (bool) defaults to null, meaning auto in debug mode

Services DI

Ces services sont ajoutés au conteneur DI :

Nom Type Description
mail.mailer Nette\Mail\Mailer classe d'envoi des e-mails
mail.signer Nette\Mail\Signer signature DKIM

Si vous passez à une version plus récente, consultez la page mise à niveau.

version: 4.x