Aller au contenu principal

Templates d'Emails Système - Aaperture

Vue d'ensemble​

Tous les emails système envoyés aux utilisateurs (depuis Aaperture) utilisent désormais un template harmonisé avec :

  • Header : Logo Aaperture (embarqué en pièce jointe avec CID, ou texte "Aaperture" en fallback)
  • Footer : Liens utiles (Accès au compte, Politique de confidentialité, Conditions d'utilisation)
  • Style cohérent : Design premium, clair et responsive avec palette chaude Aaperture

Structure du Template​

Le template système est généré par EmailTemplatesService.wrapSystemNotificationEmail() et inclut :

  1. Header : Logo Aaperture centré
    • Logo embarqué : Si le fichier aaperture-logo.png est disponible, il est envoyé comme pièce jointe avec un Content-ID (CID) et référencé dans l'HTML via cid:logo@aaperture
    • Fallback texte : Si le logo n'est pas trouvé, un simple texte "Aaperture" stylisé est affiché (aucun lien externe, aucune image cassée)
    • Le logo est toujours embarqué quand disponible, jamais référencé via une URL externe
  2. Contenu : Corps de l'email (HTML)
  3. Actions : Boutons d'action contextuels (optionnels)
  4. Footer :
    • Liens standards : Accéder à mon compte, Politique de confidentialité, Conditions d'utilisation
    • Liens additionnels personnalisés (optionnels)
    • Texte de pied de page avec branding Aaperture

Direction visuelle​

Le rendu des emails système suit désormais le même langage que les surfaces premium web :

  • fond extérieur beige clair et carte centrale ivoire
  • header et footer légèrement teintés, sans bleu SaaS
  • titres serif Playfair Display
  • CTA principal foncé et éditorial
  • blocs d'information chauds (#faf6ef) avec bord gauche bronze

Utilisation​

Méthode de base​

await this.emailService.sendNotificationEmail({
subject: "Votre sujet",
html: "<p>Contenu de l'email en HTML</p>",
text: "Contenu en texte brut",
to: "user@example.com",
});

Le wrapper est appliqué automatiquement.

Avec liens d'action​

Pour ajouter des boutons d'action contextuels :

await this.emailService.sendNotificationEmail({
subject: "Votre formulaire n'a pas reçu de soumissions",
html: "<p>Votre formulaire...</p>",
text: "Votre formulaire...",
to: user.email,
actionLinks: [
{
label: "Voir et tester le formulaire",
url: canonicalPublicLeadFormUrl,
},
{
label: "Gérer les formulaires",
url: `${frontendBaseUrl}/settings/lead-forms`,
},
],
});

Pour ajouter des liens personnalisés dans le footer :

await this.emailService.sendNotificationEmail({
subject: "Notification",
html: "<p>Contenu...</p>",
text: "Contenu...",
to: user.email,
additionalFooterLinks: [
{
label: "Documentation",
url: "https://docs.aaperture.com",
},
],
});

Skip wrapper (rare)​

Si vous avez déjà un HTML complet avec header/footer, vous pouvez désactiver le wrapper :

await this.emailService.sendNotificationEmail({
subject: "Email avec template complet",
html: "<!DOCTYPE html>...", // Template complet déjà formaté
text: "...",
to: user.email,
skipWrapper: true, // Désactive le wrapper automatique
});

Exemples d'emails système​

Email d'alerte Lead Form​

// backend/src/lead-forms/lead-forms-monitoring.service.ts
await this.emailService.sendNotificationEmail({
html: `
<h1>Votre formulaire n'a reçu aucune soumission récemment</h1>
<p>Bonjour,</p>
<p>Votre formulaire <strong>"${formName}"</strong> n'a reçu aucune soumission récemment.</p>
<p>Merci de vérifier l'intégration sur votre site ou de le repartager.</p>
<div style="background-color: #f9fafb; border-left: 4px solid #2563eb; padding: 16px; margin: 24px 0; border-radius: 4px;">
<p style="margin: 0 0 8px 0; font-weight: 500;">💡 Astuce :</p>
<p style="margin: 0;">Testez votre formulaire pour vérifier qu'il fonctionne correctement.</p>
</div>
<p>Marquez l'alerte comme vérifiée depuis votre dashboard après vérification.</p>
`,
subject: `Aucune nouvelle soumission pour "${formName}"`,
text: `...`,
to: ownerEmail,
actionLinks: [
{
label: "Voir et tester le formulaire",
url: canonicalPublicLeadFormUrl,
},
{
label: "Gérer les formulaires",
url: `${frontendBaseUrl}/settings/lead-forms`,
},
],
});

Important:

  • For lead forms, booking, and client portal links exposed to end users, do not concatenate FRONTEND_BASE_URL inline when a tenant-aware canonical URL exists.
  • Prefer the dedicated URL builders from PublicHostService.
  • FRONTEND_BASE_URL remains the correct base for authenticated app dashboard links such as /settings/....

Email de notification Stripe​

// backend/src/stripe/stripe-notification.service.ts
await this.emailService.sendNotificationEmail({
html: `
<h1>Votre abonnement a été activé !</h1>
<p>Hello ${user.display_name || user.email},</p>
<p>Votre abonnement à <strong>${planName}</strong> a été activé avec succès.</p>
<p>Merci pour votre abonnement !</p>
`,
subject: `Subscription Activated - ${planName}`,
text: `...`,
to: user.email,
actionLinks: [
{
label: "Gérer mon abonnement",
url: `${frontendBaseUrl}/users/me/subscription`,
},
],
});

Bonnes Pratiques​

1. Contenu HTML​

  • Utilisez des balises HTML sémantiques (<h1>, <h2>, <p>, etc.)
  • Évitez les styles inline complexes (le template fournit déjà les styles de base)
  • Pour les informations importantes, utilisez des divs avec background-color: #f9fafb et border-left

2. Liens d'action​

  • Limitez à 2-3 boutons d'action maximum
  • Utilisez des labels clairs et actionnables
  • Le premier lien devrait être l'action principale
  • Les liens standards sont déjà présents (Compte, Politique de confidentialité, Conditions d'utilisation)
  • Utilisez additionalFooterLinks uniquement pour des liens spécifiques au contexte
  • Les liens pointent vers des pages publiques existantes (/privacy-policy, /terms-of-use)

4. Internationalisation​

  • Vérifiez la locale de l'utilisateur (user.locale)
  • Adaptez le contenu (FR/EN) en conséquence

5. URLs Frontend​

Toujours utiliser FRONTEND_BASE_URL depuis la config :

const frontendBaseUrl =
this.configService.get<string>("FRONTEND_BASE_URL") ?? "http://localhost:5173";

Services concernés​

Les emails suivants utilisent maintenant le nouveau template :

  • ✅ lead-forms-monitoring.service.ts - Alertes d'inactivité
  • ✅ stripe-notification.service.ts - Notifications Stripe
  • ✅ booking.controller.ts - Notifications de réservation (vers users)
  • ✅ gdpr-requests.service.ts - Vérification RGPD
  • ✅ backup-notification.service.ts - Notifications de backup
  • ✅ migrations.scheduler.ts - Notifications de migration
  • ✅ client-access-email.service.ts - Messages du portail client
  • ✅ security-alerts.service.ts - Alertes de sécurité
  • ✅ invitations.service.ts - Emails d'invitation utilisateur

Email d'invitation utilisateur​

L'email d'invitation est envoyé automatiquement lors de la création d'une invitation. Il contient :

  • Le lien d'invitation (avec token) pour l'inscription par email
  • Le code d'invitation à 6 caractères (requis pour l'inscription Google OAuth si requireInvitationCode est activé)
  • Les instructions pour s'inscrire via email ou Google OAuth
  • La date d'expiration de l'invitation

Templates : backend/src/email-templates/templates/{locale}/user-invitation.html Variables disponibles :

  • {inviter_name} : Nom de l'utilisateur qui invite
  • {invitation_link} : Lien d'inscription avec token
  • {invitation_code} : Code d'invitation à 6 caractères
  • {expires_at} : Date d'expiration formatée

Important : Si requireInvitationCode est activé dans les paramètres admin, le code d'invitation est requis pour toutes les nouvelles inscriptions (email et Google OAuth).

Traduction des emails système​

⚠️ IMPORTANT : Tous les emails système doivent être traduits en français (fr-FR) et en anglais (en-US).

Organisation des traductions​

Les traductions sont organisées dans backend/src/communication/system-emails-i18n.ts pour faciliter la maintenance et éviter la duplication de code.

Règles de traduction :

  1. Tous les emails système doivent utiliser les traductions - Ne jamais hardcoder les textes directement dans le code
  2. Obtenir la locale de l'utilisateur - Utiliser getUserEmailLocale() pour récupérer la locale depuis la base de données
  3. Utiliser getSystemEmailTranslation() - Pour obtenir les traductions avec support des paramètres dynamiques
  4. Organisation par module - Les clés de traduction sont organisées par module/service (ex: stripe.*, booking.*, gdpr.*)
  5. Paramètres dynamiques - Utiliser {paramName} dans les traductions pour les valeurs dynamiques (noms, dates, montants, etc.)

Comment utiliser les traductions​

Exemple complet​

import {
getSystemEmailTranslation,
getUserEmailLocale,
type SystemEmailLocale,
} from "../communication/system-emails-i18n.js";

async sendSubscriptionCreatedEmail(userId: string, planName: string): Promise<void> {
try {
const user = await this.usersService.findById(userId);
if (!user) {
return;
}

// 1. Obtenir la locale de l'utilisateur
const locale = await getUserEmailLocale(this.db, userId);

// 2. Construire les traductions avec paramètres
const subject = getSystemEmailTranslation(
"stripe.subscription.activated.title",
locale,
{ planName },
);

const greeting = getSystemEmailTranslation("common.greeting", locale, {
name: user.display_name || user.email,
});

const content = getSystemEmailTranslation(
"stripe.subscription.activated.content",
locale,
{ planName },
);

const thanks = getSystemEmailTranslation("stripe.subscription.activated.thanks", locale);

const actionLabel = getSystemEmailTranslation(
"stripe.actions.manage_subscription",
locale,
);

// 3. Construire le HTML
const html = `
<h1>${getSystemEmailTranslation("stripe.subscription.activated.title", locale, { planName: `<strong>${planName}</strong>` })}</h1>
<p>${greeting}</p>
<p>${content}</p>
<p>${thanks}</p>
`;

// 4. Construire le texte brut (sans HTML)
const text = `${getSystemEmailTranslation(
"stripe.subscription.activated.title",
locale,
{ planName },
)}\n\n${greeting}\n\n${content}\n\n${thanks}`;

// 5. Envoyer l'email avec les traductions
await this.emailService.sendNotificationEmail({
html,
subject,
text,
to: user.email,
actionLinks: [
{
label: actionLabel,
url: `${this.frontendBaseUrl}/users/me/subscription`,
},
],
});
} catch (error) {
// ... error handling
}
}

Emails système sans utilisateur spécifique​

Pour les emails envoyés aux administrateurs (backups, migrations, alertes sécurité), utiliser une locale par défaut ou la locale configurée :

// Pour les emails système (backups, migrations)
const locale: SystemEmailLocale = "fr-FR"; // ou récupérer depuis une config

// Pour les emails liés à un utilisateur
const locale = await getUserEmailLocale(this.db, userId);

Ajouter une nouvelle traduction​

  1. Ajouter la clé dans system-emails-i18n.ts :

    "mon_module.ma_clé": {
    "fr-FR": "Texte en français avec {param}",
    "en-US": "Text in English with {param}",
    },
  2. Utiliser la clé dans votre service :

    const text = getSystemEmailTranslation("mon_module.ma_clé", locale, { param: "valeur" });

Vérification des traductions​

Avant de merger :

  • Tous les emails système utilisent getSystemEmailTranslation()
  • Toutes les clés de traduction existent pour fr-FR et en-US
  • La locale de l'utilisateur est récupérée correctement
  • Les paramètres dynamiques sont passés correctement
  • Les tests passent avec les deux locales

Migration​

Pour migrer un email existant :

  1. Identifiez le contenu HTML existant
  2. Retirez le wrapper HTML existant (header/footer)
  3. Utilisez sendNotificationEmail au lieu de sendGenericEmail
  4. Ajoutez actionLinks si pertinent
  5. Testez l'email envoyé

Comment le logo est intégré​

Le logo Aaperture est intégré dans les emails de manière robuste et fiable :

  1. Recherche du fichier : Le système recherche aaperture-logo.png dans plusieurs emplacements (par ordre de priorité) :

    • Priorité 1 : dist/assets/aaperture-logo.png (après build, copié par nest-cli.json)
    • Priorité 2 : src/assets/aaperture-logo.png (emplacement source, développement)
    • Priorité 3 : assets/aaperture-logo.png (alternative)
    • Priorité 4-9 : Autres chemins alternatifs selon l'environnement (frontend/public/assets, etc.)
  2. Attachement CID : Si le logo est trouvé :

    • Il est lu depuis le système de fichiers
    • Il est attaché à l'email avec un Content-ID unique : logo@aaperture
    • L'HTML référence l'image via <img src="cid:logo@aaperture" />
    • Aucune URL externe : L'image est complètement embarquée dans l'email
  3. Fallback texte : Si le logo n'est pas trouvé :

    • Un <div> stylisé avec le texte "Aaperture" est affiché à la place
    • Aucune image cassée, aucun lien externe
    • L'email reste fonctionnel et professionnel

Processus de build​

Le logo doit être disponible lors de l'exécution du backend. Pour cela :

Configuration de base​

Le logo est maintenant stocké dans backend/src/assets/aaperture-logo.png et est automatiquement copié dans dist/assets/ par nest-cli.json lors du build :

// backend/nest-cli.json
{
"compilerOptions": {
"assets": [
"**/*.json",
"**/*.yaml",
"**/*.yml",
"**/*.html",
"assets/**/*" // Copie automatique du dossier assets
]
}
}

Développement local​

  1. Le logo doit être présent dans backend/src/assets/aaperture-logo.png
  2. Lors du build, nest-cli.json copie automatiquement le logo vers dist/assets/
  3. Le service EmailTemplatesService cherche d'abord dans dist/assets/, puis dans src/assets/ si le build n'a pas encore été fait
npm run build  # nest-cli.json copie automatiquement src/assets/ vers dist/assets/

Build Docker​

Le Dockerfile gère automatiquement la copie du logo depuis différents contextes de build :

  1. GitLab CI : Build depuis le contexte racine pour accéder à frontend/public/assets/

    docker build -f backend/Dockerfile -t $IMAGE .
  2. Dockerfile : Utilise un script shell pour détecter le contexte de build et copier le logo

    • Copie d'abord tout le contexte dans /build-context/
    • Détecte si les fichiers sont dans backend/ (build depuis racine) ou localement (build depuis backend/)
    • Copie le logo depuis frontend/public/assets/ vers src/assets/ comme fallback si nécessaire
    • Le build NestJS copie ensuite automatiquement src/assets/ vers dist/assets/ via nest-cli.json
    • Si le logo n'est pas trouvé, un message est loggé mais le build continue (fallback texte sera utilisé)

Vérification​

Pour vérifier que le logo est bien copié après le build :

# Vérifier que le logo existe dans dist/assets/
ls -la backend/dist/assets/aaperture-logo.png

# Ou dans le conteneur Docker
docker exec <container> ls -la /app/dist/assets/aaperture-logo.png

Logs et debugging​

Si le logo n'est pas trouvé, des logs sont émis :

  • Warning (template service) : Logo non trouvé, utilisation du fallback texte
  • Error (sending service) : Si un CID est référencé dans l'HTML mais le logo n'a pas pu être chargé

Ces logs permettent d'identifier rapidement les problèmes de configuration.

Fichiers concernés​

  • Template : backend/src/communication/email-templates.service.ts

    • getLogoAttachment() : Recherche et lit le logo
    • wrapSystemNotificationEmail() : Génère le HTML conditionnel (image CID ou texte)
  • Envoi : backend/src/communication/email-sending.service.ts

    • sendEmail() : Détecte les références CID et attache le logo
  • Build :

    • backend/nest-cli.json : Configuration pour copier src/assets/ vers dist/assets/
    • backend/src/assets/aaperture-logo.png : Emplacement source du logo
    • backend/Dockerfile : Script shell pour copier le logo lors du build Docker
    • .gitlab-ci.yml : Build avec contexte racine

Notes techniques​

  • Le wrapper vérifie automatiquement si le contenu est déjà un document HTML complet
  • Le template est responsive et compatible avec les clients email (Outlook, Gmail, etc.)
  • Support du mode sombre via media queries CSS
  • Compatible avec MSO (Microsoft Outlook) via commentaires conditionnels
  • Le logo est toujours embarqué (pièce jointe CID) ou remplacé par du texte, jamais via URL externe