Portfolio 2026-11-01

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

  1. Prérequis
  2. Architecture
  3. Installation de Nodemailer
  4. Variables d'environnement
  5. Configuration systemd
  6. Modification de la route contact
  7. Authentification SMTP
  8. Déploiement Git
  9. Vérifications
  10. Troubleshooting

Prérequis

  • Portfolio Next.js fonctionnel.
  • Route serveur /api/contact existante.
  • Cloudflare Turnstile déjà configuré.
  • Serveur Stalwart fonctionnel.
  • Boîte contact@mprost.fr créé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

  1. Sur le poste de développement :
cd C:\sauvegarde-mprost
npm install nodemailer
  1. Vérifier que package.json et package-lock.json ont é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. citeturn199search51turn199search55


Variables d'environnement

  1. Créer ou compléter le fichier local .env.local pour les tests locaux.

  2. Ne jamais envoyer ce fichier sur GitHub.

  3. Utiliser un .env.example sans secret :

TURNSTILE_SECRET_KEY=
SMTP_HOST=
SMTP_PORT=465
SMTP_SECURE=true
SMTP_USER=
SMTP_PASSWORD=
CONTACT_TO=
  1. Sur le serveur, créer :
sudo nano /etc/mprost.env
  1. 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
  1. Protéger le fichier :
sudo chown root:root /etc/mprost.env
sudo chmod 600 /etc/mprost.env
  1. Supprimer l'ancienne variable Formspree :
FORMSPREE_URL

Configuration systemd

  1. Modifier le service :
sudo nano /etc/systemd/system/mprost.service
  1. Vérifier que EnvironmentFile se 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
  1. Recharger :
sudo systemctl daemon-reload
sudo systemctl restart mprost
  1. Vérifier :
sudo systemctl status mprost --no-pager
sudo systemctl show mprost --property=EnvironmentFiles

Modification de la route contact

  1. Importer Nodemailer :
import nodemailer from "nodemailer";
  1. 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. citeturn199search57

  1. 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;
  1. 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. citeturn199search51turn199search52

  1. 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"),
});
  1. Journaliser le succès :
await logEvent("sent", req, { transport: "smtp" });
  1. Journaliser les erreurs SMTP sans inscrire le mot de passe ni le contenu du message.

Authentification SMTP

  1. Utiliser le compte :
contact@mprost.fr
  1. Pour une application, créer de préférence un App Password dédié depuis le portail du compte Stalwart.

  2. Limiter si possible ce credential à l'adresse du serveur Web :

192.168.1.210
  1. Stocker le secret uniquement dans :
/etc/mprost.env
  1. Redémarrer l'application après modification :
sudo systemctl restart mprost

Déploiement Git

  1. Ajouter les changements de code :
git add app/api/contact/route.ts package.json package-lock.json .env.example
  1. Créer le commit :
git commit -m "Remplacement de Formspree par Stalwart SMTP"
  1. Envoyer :
git push
  1. 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

  1. Vérifier Nodemailer :
cd /var/www/mprost.fr
npm list nodemailer
  1. Vérifier que le service charge le fichier d'environnement :
sudo systemctl show mprost --property=EnvironmentFiles
  1. 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'
  1. Afficher les logs :
sudo journalctl -u mprost -f
  1. Envoyer un message depuis le formulaire.

  2. Résultat attendu :

"event":"sent"
"transport":"smtp"
  1. 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.