Envoyer un courriel
Composer un gabarit React Email, le rendre en HTML et l'envoyer via Resend ou un relais SMTP.
Tout envoi de courriel passe par core/email. Le module fournit l'envoi et un gabarit de base ; les gabarits métier vivent avec leur feature ou leur module.
Configuration minimale
Deux fournisseurs au choix, LRVO_EMAIL_PROVIDER tranche. Par défaut, c'est Resend.
LRVO_EMAIL_PROVIDER=resend
LRVO_EMAIL_RESEND_APIKEY=re_…
LRVO_EMAIL_FROM_ADDRESS=[email protected]
LRVO_EMAIL_FROM_NAME="Mon site"Avec un relais SMTP existant, boîte d'hébergeur ou serveur d'entreprise :
LRVO_EMAIL_PROVIDER=smtp
LRVO_EMAIL_SMTP_HOST=mail.exemple.com
LRVO_EMAIL_SMTP_PORT=587
LRVO_EMAIL_SMTP_USER=[email protected]
LRVO_EMAIL_SMTP_PASSWORD=…
LRVO_EMAIL_FROM_ADDRESS=[email protected]
LRVO_EMAIL_FROM_NAME="Mon site"Sur le port 587, la connexion est chiffrée par STARTTLS dès que le relais l'annonce. Sur le port 465, le TLS est implicite et détecté sans configuration. Le reste du code ne change pas : l'API est la même quel que soit le fournisseur.
Sans ces variables, sendEmail échoue et retourne { success: false, error } sans lever.
Envoi direct
import { sendEmail } from '@lrvo/core/email';
const result = await sendEmail({
to: '[email protected]',
subject: 'Bienvenue',
html: '<p>Bonjour</p>',
});
if (!result.success) {
// l'erreur est déjà journalisée côté serveur
}sendEmail ne lève jamais. Une promesse ignorée ici est un envoi perdu en silence : vérifier result.success, ou au minimum attendre la promesse.
Créer un gabarit
Les gabarits vivent avec leur feature. Ceux réutilisables à l'échelle de l'application vont dans src/core/email/templates.
// src/features/orders/emails/OrderConfirmedEmail.js
import { BaseLayout } from '@lrvo/core/email/templates';
import { Text, Button } from '@lrvo/core/email/components';
export const OrderConfirmedEmail = ({ name, orderNumber, orderUrl }) => (
<BaseLayout
preview={`Commande ${orderNumber} confirmée`}
title="Commande confirmée"
supportSection
>
<Text>Bonjour {name}, votre commande {orderNumber} est confirmée.</Text>
<Button href={orderUrl}>Voir la commande</Button>
</BaseLayout>
);BaseLayout fournit la structure commune : logo ou nom de l'application, titre, contenu, pied avec copyright et lien de support. supportSection affiche l'adresse de LRVO_SUPPORT_EMAIL.
Le gabarit écrit les mêmes jetons que l'interface, bg-primary text-primary-fg pour un bouton, text-fg-muted pour une mention. BaseLayout les résout en hex dans les valeurs claires du thème du projet, parce que les clients de messagerie ignorent les variables CSS. Pour une bordure en style inline, lire getEmailColors() au rendu. Voir Courriel.
Les composants viennent de @lrvo/core/email/components, qui réexporte react-email. Un module n'a donc pas à déclarer @react-email/components dans son package.json, et une seule version circule dans le projet.
Rendre et envoyer
// src/features/orders/emails/sendOrderConfirmed.js
import { sendEmail } from '@lrvo/core/email';
import { getPublicBaseUrl } from '@lrvo/core/shared/config';
export async function sendOrderConfirmed({ to, name, order }) {
const { render, OrderConfirmedEmail } = await loadEmailKit();
const element = (
<OrderConfirmedEmail
name={name}
orderNumber={order.number}
orderUrl={`${getPublicBaseUrl()}/commandes/${order.id}`}
/>
);
return sendEmail({
to,
subject: `Commande ${order.number} confirmée`,
html: await render(element),
text: await render(element, { plainText: true }),
});
}loadEmailKit() est l'import paresseux décrit plus bas dans Pièges. render(element, { plainText: true }) tire l'alternative texte du même arbre : aucune seconde version à maintenir.
Les liens doivent être absolus. Un client de messagerie n'a pas d'origine : un href="/commandes/42" ne mène nulle part. Construire l'URL avec getPublicBaseUrl().
Envoi en lot
import { sendBatchEmails } from '@lrvo/core/email';
await sendBatchEmails([
{ to: '[email protected]', subject: 'Sujet A', html: htmlA },
{ to: '[email protected]', subject: 'Sujet B', html: htmlB },
]);Une seule requête vers Resend, ou une seule connexion SMTP réutilisée, au lieu de N. À préférer dès qu'on envoie le même type de message à plusieurs destinataires.
Depuis un module
Identique. Le module importe @lrvo/core/email et garde ses gabarits chez lui.
// @lrvo/module-blog — src/emails/templates/NewPostEmail.js
import { BaseLayout } from '@lrvo/core/email/templates';
import { Text, Button } from '@lrvo/core/email/components';Un module n'ajoute donc aucune dépendance pour envoyer un courriel : react-email lui arrive par le core, comme react-pdf par core/pdf.
Le rendu et l'envoi sont du code serveur : ils vivent dans un .server.js ou dans un handler de route, jamais dans un composant client.
Pièges
Le SDK Resend est en v6. Les champs sont en camelCase : replyTo, scheduledAt. Le snake_case de la v3 n'est plus reconnu et est silencieusement ignoré. Node 20 ou plus est requis.
Ne pas envoyer depuis une boucle sans attendre. Chaque appel non attendu est une promesse orpheline, et un relais comme Resend applique ses propres limites de débit. Utiliser sendBatchEmails, ou attendre chaque envoi.
Le lot SMTP est séquentiel. Sans verbe batch dans le protocole, les messages partent l'un après l'autre : le temps total croît avec le nombre de destinataires, ce qui peut dépasser le temps imparti d'une fonction serverless sur un gros lot. Découper le lot, ou passer par une tâche planifiée.
Deux champs sont propres à Resend. tags est ignoré en SMTP, et attachments[].path y lève : le fournisseur SMTP n'ira pas chercher une URL distante, il faut fournir content. Un module qui doit fonctionner sous les deux fournisseurs s'en tient à content.
Le mot de passe SMTP ne part jamais en clair. Si le relais n'offre ni TLS implicite ni STARTTLS, l'envoi échoue au lieu d'authentifier. C'est volontaire et non contournable : un relais local de développement n'a pas besoin d'identifiants.
Si la fonction d'envoi est atteignable depuis le boot, charger le rendu à la demande. Un fichier importé par la chaîne d'initializeLrvo() ne doit pas importer @lrvo/core/email/components au niveau racine : ce graphe embarque le compilateur Tailwind de react-email, qui lit globalThis.localStorage à son évaluation et fait apparaître un ExperimentalWarning Node au démarrage d'un site qui n'envoie aucun courriel. La contrainte remonte aux gabarits, qui importent ces composants : eux aussi passent par l'import paresseux, d'où l'index.js qui les regroupe.
let kitPromise = null;
function loadEmailKit() {
kitPromise ??= (async () => {
const [components, templates] = await Promise.all([
import('@lrvo/core/email/components'),
import('./templates/index.js'),
]);
return { ...components, ...templates };
})();
return kitPromise;
}
export async function sendOrderConfirmed({ to, name, order }) {
const { render, OrderConfirmedEmail } = await loadEmailKit();
// …
}Mémoïser la promesse et non le résultat : deux envois concurrents partagent alors le même import au lieu d'en déclencher deux.
Tester en développement. Resend n'envoie qu'aux adresses vérifiées tant que le domaine ne l'est pas. Un result.success à false en développement vient le plus souvent de là. Un moyen d'y échapper : pointer le fournisseur smtp sur une boîte de capture locale, qui accepte tout et n'envoie rien.
LRVO_EMAIL_PROVIDER=smtp
LRVO_EMAIL_SMTP_HOST=127.0.0.1
LRVO_EMAIL_SMTP_PORT=1025Courriels du core
features/auth envoie déjà six types de messages : vérification d'adresse, réinitialisation de mot de passe, confirmation de changement de mot de passe, confirmation et notification de changement d'adresse, invitation. Voir Authentification. Il n'est pas nécessaire de les réécrire.