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 pourssl, 587 pourtls, sinon 25timeout– délai d'attente de la connexion SMTPpersistent– utiliser une connexion persistanteclientHost– indiquer l'en-tête host du clientstreamOptions– 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.