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 moderne et responsive

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

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: `${frontendBaseUrl}/public/lead-forms/${formId}`,
},
{
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: `${frontendBaseUrl}/public/lead-forms/${formId}`,
},
{
label: "Gérer les formulaires",
url: `${frontendBaseUrl}/settings/lead-forms`,
},
],
});

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