Auto-héberger l'envoi du formulaire de contact Next.js
Suppression de Formspree et envoi direct des messages du formulaire Next.js vers une boîte Stalwart avec Nodemailer, SMTP TLS, Cloudflare Turnstile, rate limiting et journalisation.
Sommaire
- Prérequis
- Architecture
- Installation de Nodemailer
- Variables d'environnement
- Configuration systemd
- Modification de la route contact
- Authentification SMTP
- Déploiement Git
- Vérifications
- Troubleshooting
Prérequis
- Portfolio Next.js fonctionnel.
- Route serveur
/api/contactexistante. - Cloudflare Turnstile déjà configuré.
- Serveur Stalwart fonctionnel.
- Boîte
contact@mprost.frcréée. - SMTP TLS disponible sur
mail.mprost.fr:465. - Certificat TLS valide présenté par Stalwart.
- Déploiement GitHub Actions fonctionnel.
Architecture
L'objectif est de supprimer Formspree du chemin des messages.
Visiteur
|
Formulaire Next.js
|
/api/contact
|
Origin / Validation / Honeypot
|
Cloudflare Turnstile
|
Rate limiting
|
Nodemailer
|
SMTP TLS :465
|
mail.mprost.fr
|
Stalwart
|
contact@mprost.fr
L'adresse du visiteur est utilisée en Reply-To. L'expéditeur SMTP reste une adresse du domaine afin de conserver un envoi cohérent avec l'authentification du serveur mail.
Installation de Nodemailer
- Sur le poste de développement :
cd C:\sauvegarde-mprost
npm install nodemailer
- Vérifier que
package.jsonetpackage-lock.jsonont été modifiés.
Nodemailer utilise SMTP comme transport principal. Sur le port 465, la connexion utilise TLS dès le début avec secure: true. citeturn199search51turn199search55
Variables d'environnement
-
Créer ou compléter le fichier local
.env.localpour les tests locaux. -
Ne jamais envoyer ce fichier sur GitHub.
-
Utiliser un
.env.examplesans secret :
TURNSTILE_SECRET_KEY=
SMTP_HOST=
SMTP_PORT=465
SMTP_SECURE=true
SMTP_USER=
SMTP_PASSWORD=
CONTACT_TO=
- Sur le serveur, créer :
sudo nano /etc/mprost.env
- Ajouter :
TURNSTILE_SECRET_KEY=SECRET_TURNSTILE
SMTP_HOST=mail.mprost.fr
SMTP_PORT=465
SMTP_SECURE=true
SMTP_USER=contact@mprost.fr
SMTP_PASSWORD=APP_PASSWORD_OU_MOT_DE_PASSE_SMTP
CONTACT_TO=contact@mprost.fr
- Protéger le fichier :
sudo chown root:root /etc/mprost.env
sudo chmod 600 /etc/mprost.env
- Supprimer l'ancienne variable Formspree :
FORMSPREE_URL
Configuration systemd
- Modifier le service :
sudo nano /etc/systemd/system/mprost.service
- Vérifier que
EnvironmentFilese trouve bien dans la section[Service]:
[Unit]
Description=mprost.fr - Next.js
After=network.target
[Service]
Type=simple
WorkingDirectory=/var/www/mprost.fr
EnvironmentFile=/etc/mprost.env
ExecStart=/usr/bin/npm start
Restart=always
RestartSec=5
Environment=NODE_ENV=production
Environment=PORT=3000
[Install]
WantedBy=multi-user.target
- Recharger :
sudo systemctl daemon-reload
sudo systemctl restart mprost
- Vérifier :
sudo systemctl status mprost --no-pager
sudo systemctl show mprost --property=EnvironmentFiles
Modification de la route contact
- Importer Nodemailer :
import nodemailer from "nodemailer";
- Conserver la validation Turnstile côté serveur.
Cloudflare précise que le widget seul ne protège pas le formulaire : le backend doit valider le token via Siteverify. Les tokens sont à usage unique et expirent après cinq minutes. citeturn199search57
- Charger la configuration SMTP :
const smtpHost = process.env.SMTP_HOST;
const smtpPort = Number(process.env.SMTP_PORT ?? "465");
const smtpUser = process.env.SMTP_USER;
const smtpPassword = process.env.SMTP_PASSWORD;
const contactTo = process.env.CONTACT_TO ?? smtpUser;
- Créer le transport SMTP :
const transporter = nodemailer.createTransport({
host: smtpHost,
port: smtpPort,
secure: true,
auth: {
user: smtpUser,
pass: smtpPassword,
},
tls: {
servername: "mail.mprost.fr",
},
});
Le port 465 correspond à une connexion SMTP avec TLS implicite. citeturn199search51turn199search52
- Envoyer le message :
await transporter.sendMail({
from: `"Portfolio mprost.fr" <${smtpUser}>`,
to: contactTo,
replyTo: email,
subject: `Nouveau message du portfolio - ${name}`,
text: [
"Nouveau message reçu depuis le formulaire mprost.fr",
"",
`Nom : ${name}`,
`Adresse email : ${email}`,
"",
"Message :",
message,
].join("\n"),
});
- Journaliser le succès :
await logEvent("sent", req, { transport: "smtp" });
- Journaliser les erreurs SMTP sans inscrire le mot de passe ni le contenu du message.
Authentification SMTP
- Utiliser le compte :
contact@mprost.fr
-
Pour une application, créer de préférence un App Password dédié depuis le portail du compte Stalwart.
-
Limiter si possible ce credential à l'adresse du serveur Web :
192.168.1.210
- Stocker le secret uniquement dans :
/etc/mprost.env
- Redémarrer l'application après modification :
sudo systemctl restart mprost
Déploiement Git
- Ajouter les changements de code :
git add app/api/contact/route.ts package.json package-lock.json .env.example
- Créer le commit :
git commit -m "Remplacement de Formspree par Stalwart SMTP"
- Envoyer :
git push
- Le workflow GitHub Actions effectue ensuite :
git pull
npm ci
npm run build
systemctl restart mprost
Les secrets ne sont jamais envoyés sur GitHub.
Vérifications
- Vérifier Nodemailer :
cd /var/www/mprost.fr
npm list nodemailer
- Vérifier que le service charge le fichier d'environnement :
sudo systemctl show mprost --property=EnvironmentFiles
- Vérifier la connexion SMTP TLS :
openssl s_client -connect mail.mprost.fr:465 -servername mail.mprost.fr </dev/null 2>/dev/null | grep -E 'subject=|issuer=|Verify return code'
- Afficher les logs :
sudo journalctl -u mprost -f
-
Envoyer un message depuis le formulaire.
-
Résultat attendu :
"event":"sent"
"transport":"smtp"
- Vérifier que le message apparaît dans :
contact@mprost.fr
Troubleshooting
TURNSTILE_SECRET_KEY manquante
Symptôme :
TURNSTILE_SECRET_KEY manquante dans les variables d'environnement.
Vérifier :
sudo sed -E 's/=.*$/=[MASQUE]/' /etc/mprost.env
Vérifier également que :
EnvironmentFile=/etc/mprost.env
se trouve bien dans [Service] et non dans [Install].
Certificat SMTP auto-signé
Symptôme :
self-signed certificate
Solution : importer un certificat valide pour mail.mprost.fr dans Stalwart et conserver la validation TLS active.
Ne pas contourner l'erreur avec :
rejectUnauthorized=false
Authentification SMTP refusée
Symptôme :
535 5.7.8 Authentication credentials invalid
Vérifier :
SMTP_USER=contact@mprost.fr
SMTP_PASSWORD=...
Puis redémarrer :
sudo systemctl restart mprost
Pour les applications, utiliser de préférence un App Password Stalwart dédié.
Rate limit pendant les tests
Symptôme :
rate_limited_ip
Le formulaire limite volontairement les essais répétés. Diagnostiquer d'abord SMTP indépendamment du formulaire afin d'éviter de consommer inutilement la fenêtre de limitation.
Le formulaire affiche seulement une erreur générique
Suivre directement les logs du service :
sudo journalctl -u mprost -f
Les événements config_error, captcha_failed, smtp_error et sent permettent d'identifier l'étape qui échoue.