Héberger son serveur mail - 06 - Intégrer le serveur mail dans ton application

Connecter ton application a ton serveur mail. Envoi avec nodemailer, lecture IMAP avec imapflow, et templates HTML.

06 - Intégrer le serveur mail dans ton application

Ce que tu vas apprendre

  • Envoyer des emails depuis ton application avec nodemailer
  • La config SMTP pour la communication container-to-container
  • Construire des templates email avec fallback texte
  • Lire les emails en IMAP avec imapflow

Prerequisites

  • 03 - docker-mailserver : le serveur mail est en place
  • Un backend Node.js / Bun (les concepts s'appliquent a d'autres langages)

Envoyer des emails depuis ton app

Tu as un serveur mail qui tourne. Tu as un score de 9+/10 sur mail-tester.com. Maintenant il faut que ton application puisse envoyer des emails : confirmations d'inscription, formulaire de contact, notifications.

La lib standard pour ca en Node.js / Bun, c'est nodemailer. Elle est stable, bien documentee, et fait le job depuis des annees.

bashbun add nodemailer

La configuration SMTP

Voici le point qui m'a fait perdre une heure la première fois. Quand ton application tourne dans un container Docker sur le meme réseau que le mailserver, tu ne te connectes pas a mail.paltemps.fr. Tu te connectes au nom du container : mailserver.

typescriptimport nodemailer from "nodemailer";

const transporter = nodemailer.createTransport({
  host: "mailserver",
  port: 587,
  secure: false,
  auth: {
    user: "contact@paltemps.fr",
    pass: process.env.SMTP_PASSWORD,
  },
  tls: {
    rejectUnauthorized: false,
  },
});

Pourquoi rejectUnauthorized: false ? Parce que le certificat SSL est emis pour mail.paltemps.fr, mais ta connexion arrive depuis le hostname mailserver (le nom du container Docker). Le certificat ne matche pas le hostname de connexion, donc Node.js refuserait la connexion par défaut. Comme c'est un réseau interne Docker, le risque de MITM est quasi nul. C'est un compromis acceptable.

Si ca te derange (et je comprends), l'alternative c'est de te connecter via mail.paltemps.fr en passant par le réseau externe. Mais ca ajoute de la latence et ca n'apporte pas grand-chose en sécurité dans ce contexte.

Le .env correspondant :

bashSMTP_HOST=mailserver
SMTP_PORT=587
SMTP_USER=contact@paltemps.fr
SMTP_PASSWORD=ton-mot-de-passe-fort

Envoyer un email

typescriptasync function sendEmail(to: string, subject: string, html: string, text: string) {
  const info = await transporter.sendMail({
    from: "Nicolas <contact@paltemps.fr>",
    to,
    subject,
    html,
    text,
  });
  console.log("Email envoye:", info.messageId);
  return info;
}

Toujours inclure html et text. Le text c'est le fallback pour les clients mail qui n'affichent pas le HTML (ca existe encore) et pour les filtres anti-spam qui penalisent les emails HTML-only. Sur mail-tester.com, un email sans version texte te coûte -0.5 points. Facile a éviter.

Exemple : formulaire de contact

Sur paltemps.fr, le formulaire de contact envoie un email a contact@paltemps.fr :

typescriptapp.post("/api/contact", async (req, res) => {
  const { name, email, message } = req.body;

  const html = `
    <h2>Nouveau message de ${name}</h2>
    <p><strong>Email :</strong> ${email}</p>
    <p><strong>Message :</strong></p>
    <p>${message.replace(/\n/g, "<br>")}</p>
  `;

  const text = `Nouveau message de ${name}\nEmail: ${email}\nMessage:\n${message}`;

  await sendEmail(
    "contact@paltemps.fr",
    `Contact paltemps.fr - ${name}`,
    html,
    text
  );

  res.json({ ok: true });
});

Simple et direct. Pas besoin de service tiers comme SendGrid ou Mailgun. Ton serveur mail fait le travail.

Templates email plus propres

Pour des emails plus elabores (confirmation d'inscription, reset de mot de passe), construire le HTML inline devient vite penible. Une approche qui marche bien c'est d'utiliser des template strings avec un layout de base :

typescriptfunction emailLayout(content: string): string {
  return `
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="font-family: -apple-system, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
  ${content}
  <hr style="border: none; border-top: 1px solid #e0e0e0; margin: 30px 0;">
  <p style="color: #666; font-size: 12px;">paltemps.fr</p>
</body>
</html>`;
}

Le CSS inline c'est moche mais c'est obligatoire. La plupart des clients mail (Gmail, Outlook) strippent les balises <style>. Le inline est la seule chose qui passe partout. La vie est injuste.

Lire les emails en IMAP

Si tu veux construire un webmail ou lire les emails reçus depuis ton app, imapflow est une bonne option :

bashbun add imapflow
typescriptimport { ImapFlow } from "imapflow";

async function fetchRecentEmails(account: string, password: string) {
  const client = new ImapFlow({
    host: "mail.paltemps.fr",
    port: 993,
    secure: true,
    auth: {
      user: account,
      pass: password,
    },
  });

  await client.connect();

  const lock = await client.getMailboxLock("INBOX");
  try {
    const messages = [];
    for await (const message of client.fetch("1:10", {
      envelope: true,
      source: true,
    })) {
      messages.push({
        subject: message.envelope.subject,
        from: message.envelope.from,
        date: message.envelope.date,
      });
    }
    return messages;
  } finally {
    lock.release();
    await client.logout();
  }
}

Ici on se connecte a mail.paltemps.fr (pas mailserver) parce que le certificat SSL sur le port 993 est emis pour ce domaine. Si ton app tourne dans le meme réseau Docker, tu peux utiliser mailserver comme host mais tu devras gerer le mismatch de certificat comme pour SMTP.

Pour un webmail, pense au connection pooling. Ouvrir et fermer une connexion IMAP a chaque requête HTTP c'est lent. Garde les sessions ouvertes et réutilisé-les. Les mots de passe IMAP en mémoire doivent etre chiffres. J'en parle dans ma serie sur la cryptographie et la gestion des clés.

Les erreurs courantes

"Connection refused" sur le port 587. Ton container applicatif n'est probablement pas sur le meme réseau Docker que le mailserver. Verifie avec docker network ls et assure-toi que les deux services partagent un réseau.

"Invalid login" avec les bons identifiants. Le compte doit exister sur le mailserver. Verifie avec docker exec mailserver setup email list. Le mot de passe est sensible a la casse.

Emails envoyes mais jamais reçus. Regarde les logs Postfix : docker logs mailserver --tail 100 | grep postfix. Tu verras si l'email a ete accepte, relay, ou rejete. Souvent c'est un problème de DNS chez le destinataire ou un rejet base sur la reputation de ton IP.

Timeout sur la connexion SMTP. Si tu utilises le port 465 (SMTPS) au lieu du 587 (STARTTLS), mets secure: true dans la config nodemailer. Les deux ports marchent mais le handshake est différent.

La config Docker Compose complète

Pour que la communication container-to-container fonctionne, les services doivent etre sur le meme réseau :

yamlservices:
  api:
    build: .
    environment:
      - SMTP_HOST=mailserver
      - SMTP_PORT=587
    networks:
      - app_network

  mailserver:
    image: ghcr.io/docker-mailserver/docker-mailserver:latest
    networks:
      - app_network

networks:
  app_network:

Avec PERMIT_DOCKER=connected-networks dans la config du mailserver (qu'on a mis dans l'article 03), les containers du meme réseau peuvent envoyer des emails via le mailserver. Simple et propre.

Et voila. Tu as un serveur mail self-hosted, correctement configure du DNS jusqu'a l'intégration applicative. Plus de dépendance a des services tiers pour envoyer tes emails.


Navigation : Precedent : 05 - Delivrabilite | Suivant : 07 - Glossaire


Sources

Retrouve d'autres articles techniques sur paltemps.fr.

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.