import { Injectable, Logger } from '@nestjs/common'; import { readFileSync } from 'fs'; import { join } from 'path'; import * as Handlebars from 'handlebars'; import { AppConfigService } from '../config/config.service'; /** Ticket #28 — quel template d'email envoyer après validation de compte */ export type ValidationAccountEmailKind = 'parent' | 'am'; @Injectable() export class MailService { private readonly logger = new Logger(MailService.name); /** Cache des templates Handlebars compilés (fichiers .hbs) */ private readonly compiledTemplates = new Map(); constructor(private readonly configService: AppConfigService) {} private templateBasename(kind: ValidationAccountEmailKind): string { const map: Record = { parent: 'account-validated-parent', am: 'account-validated-am', }; return map[kind]; } private getCompiledTemplate(kind: ValidationAccountEmailKind): Handlebars.TemplateDelegate { let compiled = this.compiledTemplates.get(kind); if (!compiled) { const filePath = join(__dirname, 'templates', `${this.templateBasename(kind)}.hbs`); const source = readFileSync(filePath, 'utf8'); compiled = Handlebars.compile(source); this.compiledTemplates.set(kind, compiled); } return compiled; } /** * Email post-validation : lien création MDP (token existant ou régénéré côté appelant). * Ticket #28 — app_name, app_url, expéditeur via ConfigService (sendEmail). */ async sendValidatedAccountPasswordSetupEmail( recipient: { email: string; prenom: string; nom: string; token: string; numeroDossier?: string | null; }, kind: ValidationAccountEmailKind, ): Promise { const appName = this.configService.get('app_name', "P'titsPas"); const appUrl = (this.configService.get('app_url', 'https://app.ptits-pas.fr') || '').replace(/\/+$/, ''); const createPasswordUrl = `${appUrl}/create-password?token=${encodeURIComponent(recipient.token)}`; const data: Record = { prenom: recipient.prenom || '', nom: recipient.nom || '', appName, appUrl, createPasswordUrl, numeroDossier: recipient.numeroDossier || '', }; const html = this.getCompiledTemplate(kind)(data) as string; const subjects: Record = { parent: `${appName} — Compte parent validé : créez votre mot de passe`, am: `${appName} — Inscription validée : créez votre mot de passe`, }; await this.sendEmail(recipient.email, subjects[kind], html); } /** * Envoi d'un email générique * @param to Destinataire * @param subject Sujet * @param html Contenu HTML * @param text Contenu texte (optionnel) */ async sendEmail(to: string, subject: string, html: string, text?: string): Promise { try { // Récupération de la configuration SMTP const smtpHost = this.configService.get('smtp_host'); const smtpPort = this.configService.get('smtp_port'); const smtpSecure = this.configService.get('smtp_secure'); const smtpAuthRequired = this.configService.get('smtp_auth_required'); const smtpUser = this.configService.get('smtp_user'); const smtpPassword = this.configService.get('smtp_password'); const emailFromName = this.configService.get('email_from_name'); const emailFromAddress = this.configService.get('email_from_address'); // Import dynamique de nodemailer const nodemailer = await import('nodemailer'); // Configuration du transporteur const transportConfig: any = { host: smtpHost, port: smtpPort, secure: smtpSecure, }; if (smtpAuthRequired && smtpUser && smtpPassword) { transportConfig.auth = { user: smtpUser, pass: smtpPassword, }; } const transporter = nodemailer.createTransport(transportConfig); // Envoi de l'email await transporter.sendMail({ from: `"${emailFromName}" <${emailFromAddress}>`, to, subject, text: text || html.replace(/<[^>]*>?/gm, ''), // Fallback texte simple html, }); this.logger.log(`📧 Email envoyé à ${to} : ${subject}`); } catch (error) { this.logger.error(`❌ Erreur lors de l'envoi de l'email à ${to}`, error); throw error; } } /** * Envoi de l'email de bienvenue pour un gestionnaire * @param to Email du gestionnaire * @param prenom Prénom * @param nom Nom * @param token Token de création de mot de passe (si applicable) ou mot de passe temporaire (si applicable) * @note Pour l'instant, on suppose que le gestionnaire doit définir son mot de passe via "Mot de passe oublié" ou un lien d'activation * Mais le ticket #17 parle de "Flag changement_mdp_obligatoire = TRUE", ce qui implique qu'on lui donne un mot de passe temporaire ou qu'on lui envoie un lien. * Le ticket #24 parle de "API Création mot de passe" via token. * Pour le ticket #17, on crée le gestionnaire avec un mot de passe (hashé). * Si on suit le ticket #35 (Frontend), on saisit un mot de passe. * Donc on envoie juste un email de confirmation de création de compte. */ async sendGestionnaireWelcomeEmail(to: string, prenom: string, nom: string): Promise { const appName = this.configService.get('app_name', 'P\'titsPas'); const appUrl = this.configService.get('app_url', 'https://app.ptits-pas.fr'); const subject = `Bienvenue sur ${appName}`; const html = `

Bienvenue ${prenom} ${nom} !

Votre compte gestionnaire sur ${appName} a été créé avec succès.

Vous pouvez dès à présent vous connecter avec l'adresse email ${to} et le mot de passe qui vous a été communiqué.

Lors de votre première connexion, il vous sera demandé de modifier votre mot de passe pour des raisons de sécurité.


Cet email a été envoyé automatiquement. Merci de ne pas y répondre.

`; await this.sendEmail(to, subject, html); } /** * Accusé de réception inscription (parent ou assistante maternelle) : * demande enregistrée + n° de dossier. En attente de validation ; pas de lien création MDP à ce stade. */ async sendRegistrationPendingEmail( to: string, prenom: string, nom: string, numeroDossier: string, ): Promise { const appName = this.configService.get('app_name', "P'titsPas"); const appUrl = this.configService.get('app_url', 'https://app.ptits-pas.fr'); const safe = (s: string) => (s || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); const subject = `Votre demande d'inscription sur ${appName} — dossier ${safe(numeroDossier)}`; const html = `

Bonjour ${safe(prenom)} ${safe(nom)},

Nous avons bien enregistré votre demande de création de compte sur ${safe(appName)}.

Numéro de dossier : ${safe(numeroDossier)}

Votre dossier est en attente de validation par notre équipe. Vous recevrez un email lorsqu’il aura été traité.


Cet email a été envoyé automatiquement. Merci de ne pas y répondre.

`; await this.sendEmail(to, subject, html); } /** * Email de refus de dossier avec lien reprise (token). * Ticket #110 – Refus sans suppression */ async sendRefusEmail( to: string, prenom: string, nom: string, comment: string | undefined, token: string, ): Promise { const appName = this.configService.get('app_name', "P'titsPas"); const appUrl = this.configService.get('app_url', 'https://app.ptits-pas.fr'); const repriseLink = `${appUrl}/reprise?token=${encodeURIComponent(token)}`; const subject = `Votre dossier – compléments demandés`; const commentBlock = comment ? `

Message du gestionnaire :

${comment.replace(//g, '>')}

` : ''; const html = `

Bonjour ${prenom} ${nom},

Votre dossier d'inscription sur ${appName} n'a pas pu être validé en l'état.

${commentBlock}

Vous pouvez corriger les éléments indiqués et soumettre à nouveau votre dossier en cliquant sur le lien ci-dessous.

Ce lien est valable 7 jours. Si vous n'avez pas demandé cette reprise, vous pouvez ignorer cet email.


Cet email a été envoyé automatiquement. Merci de ne pas y répondre.

`; await this.sendEmail(to, subject, html); } /** * Ticket #127 — lien application `/reset-password?token=` (pas create-password). */ async sendPasswordResetEmail(recipient: { email: string; prenom: string; nom: string; token: string; }): Promise { const appName = this.configService.get('app_name', "P'titsPas"); const appUrl = (this.configService.get('app_url', 'https://app.ptits-pas.fr') || '').replace(/\/+$/, ''); const resetPasswordUrl = `${appUrl}/reset-password?token=${encodeURIComponent(recipient.token)}`; const filePath = join(__dirname, 'templates', 'password-reset.hbs'); const source = readFileSync(filePath, 'utf8'); const compiled = Handlebars.compile(source); const html = compiled({ prenom: recipient.prenom || '', nom: recipient.nom || '', appName, resetPasswordUrl, }) as string; const subject = `${appName} — Réinitialisation de votre mot de passe`; await this.sendEmail(recipient.email, subject, html); } }