Module — Envoi d'emails
Synchronisé avec le code. Mettre à jour à chaque changement structurel.
Rôle
Horizon envoie des emails par deux canaux parallèles, distincts par cible et par techno :
| Canal | Cible | Techno |
|---|---|---|
| SendGrid | Client final (B2C : site web, mobile) | API SendGrid + templates hébergés (id d-xxx) |
| FluentEmail + AWS SES | Utilisateurs back-office Buchard | FluentEmail + Razor .cshtml embedded |
Les deux canaux partagent une seule config de redirection hors-prod : Mailing:NonProductionRecipients.
Emplacement
- Canal SendGrid :
- Service :
src/Application/Services/SendGridService.cs(ISendGridService) - Templates hébergés sur le compte SendGrid (id mappés dans
appsettings.*.json > SendGrid:Templates) - Callers :
OnlinePaymentService,MailController(Areas/Api),LoginController(Areas/Api)
- Service :
- Canal FluentEmail/SES :
- Factory :
src/Application/Mailing/EmailFactory.cs(IEmailFactory) - Senders :
src/Application/Mailing/AwsSesSender.cs(prod-like) etFluentEmail.Core.Defaults.SaveToDiskSender(Dev/Integration) - Configuration DI :
src/Application/Mailing/MailingServices.cs > Configure(appelé parApplicationModule,Worker/Program,WorkerGlobe/Program) - Modèles :
src/Application/Mailing/Models/(CreatePassword, ResetPassword, ResetCustomerPassword, Notification — tous héritent deEmailModelBase) - Templates Razor :
src/Application/Templates/*.cshtml - Extensions d'envoi :
src/Application/Extensions/EmailFactoryExtensions.cs - Callers :
UserService,NotificationService,Identity/ForgotPassword.cshtml.cs
- Factory :
Canal SendGrid — méthodes et usages
| Méthode | Usage | Callers |
|---|---|---|
SendTemplateEmailAsync(email, data, templateId, bcc, pdfStream, pdfFileName) | Email template SendGrid + PDF optionnel | OnlinePaymentService (Confirmation booking, Gift, Club), MailController (WaitingList, TripOffer, SetCustomerPassword), LoginController (ForgotCustomerPassword) |
SendEmailAsync(email, subject, content) | Email raw HTML | Très peu utilisé (aucun caller actif identifié à date) |
Templates SendGrid (id dans appsettings:SendGrid:Templates) : SetCustomerPassword, ForgotCustomerPassword, Confirmation, WaitingList, Club, Gift, TripOffer.
Canal FluentEmail/SES — méthodes et usages
Extensions sur IEmailFactory (cf. EmailFactoryExtensions.cs) :
| Méthode | Template Razor | Caller actif |
|---|---|---|
SendDefinePasswordAsync | CreatePassword.cshtml | UserService:103 (création user back-office) |
SendResetPasswordAsync | ResetPassword.cshtml | Identity/ForgotPassword:51 |
SendResetCustomerPasswordAsync | ResetCustomerPassword.cshtml | (aucun — extension préparée, non utilisée) |
SendNotification (2 surcharges) | Notification.cshtml | NotificationService:92/105/115 |
SendEmailConfirmationAsync | HTML inline (pas de template) | (aucun caller actif) |
Résolution du template Razor dans EmailFactory.Prepare<TModel>(model) : recherche par locale {ModelName}_{fr-CH}.cshtml → {ModelName}_{fr}.cshtml → {ModelName}.cshtml. Les .cshtml sont embedded dans l'assembly Horizon.Application.
Comportement par environnement
Canal SendGrid
| Env | Mailing:NonProductionRecipients | Destinataire effectif | BCC |
|---|---|---|---|
| Development | configuré | les adresses configurées | droppés |
| Development | absent / [] | fallback geeks+buchard@spektrummedia.com | droppés |
| Integration / Staging | configuré | les adresses configurées | droppés |
| Integration / Staging | absent / [] | fallback geeks+buchard@spektrummedia.com | droppés |
| Production | n'importe quoi | vrai destinataire (setting ignoré) | conservés |
Canal FluentEmail/SES
L'ISender injecté change selon l'env (MailingServices.Configure) :
| Env | ISender injecté | Effet |
|---|---|---|
| Development | SaveToDiskSender | Email écrit sur disque dans {rootPath}/.dev/inbox/ — aucun envoi réseau |
| Integration | SaveToDiskSender | idem (statu quo historique) |
| Autre env (Staging custom, Production) | AwsSesSender | Envoi réel via AWS SES |
Quand AwsSesSender est actif, la redirection est appliquée à l'intérieur :
Env (avec AwsSesSender) | Mailing:NonProductionRecipients | Destinataire effectif | BCC |
|---|---|---|---|
| non-Production (ex : Staging) | configuré | les adresses configurées | vidés |
| non-Production (ex : Staging) | absent / [] | fallback geeks+buchard@spektrummedia.com | vidés |
| Production | n'importe quoi | vrais destinataires | conservés |
Configuration
{
"Mailing": {
"FromAddress": "no-reply@buchard.ch",
"FromName": "Buchard Voyages SA",
"NonProductionRecipients": [ "tester@spektrummedia.com" ], // partagé SendGrid + SES
"AwsSes": {
"AccessKeyId": "...",
"AccessKeySecret": "...",
"RegionEndpoint": "us-east-1" // optionnel, défaut us-east-1
}
},
"SendGrid": {
"Key": "SG.xxx",
"EmailFrom": "no-reply@buchard.ch",
"NameFrom": "Buchard Voyages SA",
"Templates": { "Confirmation": "d-xxx", "Gift": "d-yyy", "...": "..." }
}
}Mailing:NonProductionRecipients accepte une ou plusieurs adresses (tableau de strings) — partagée entre les deux canaux.
Règles métier / invariants
- Garde-fou Production : les deux services (
SendGridService,AwsSesSender) ignorentMailing:NonProductionRecipientsquandIHostEnvironment.IsProduction(). Impossible d'activer accidentellement la redirection en prod en posant la clé. - Fallback hardcodé : si on est hors-prod ET sans config,
FallbackRedirect = "geeks+buchard@spektrummedia.com"est utilisé. Aucun email test ne peut partir au vrai client par oubli de config. Constante en haut de chaque sender. - BCC droppés en redirection : sur les deux canaux, quand on redirige, les BCC sont enlevés. Évite de copier des tiers réels (commerciaux, partenaires) sur un mail de test.
SaveToDiskSenderneutralise tout : en Development et Integration, le canal FluentEmail/SES n'émet jamais sur le réseau, peu importe la config — l'isolation est faite au niveau du sender injecté.
Points d'attention / pièges
- ⚠ Le Worker ignore
ASPNETCORE_ENVIRONMENT— il fautDOTNET_ENVIRONMENT: le Worker est un generic host (Host.CreateDefaultBuilder, cf.Worker/WorkerHostBuilder.cs:32) qui ne lit que les variables préfixéesDOTNET_.ASPNETCORE_ENVIRONMENTn'est lue que par le host web. Un conteneur Worker avec seulementASPNETCORE_ENVIRONMENT=Stagingtourne donc en environnement Production (défaut .NET quand rien n'est défini) →IsProduction()= true → redirection désactivée. Conséquence : l'email de confirmation post-paiement (envoyé par le Worker viaBackgroundWorker.DoOneMinuteJob→OnlinePaymentService.CheckPendingPayments(), pas par le Web) part au vrai client + BCC réels (francois.buchard@buchard.ch/info@buchard.ch),Mailing:NonProductionRecipientsignoré. Incident constaté sur staging en août 2026. Toujours définirDOTNET_ENVIRONMENTsur les conteneurs Worker non-prod (les deux variables, comme dansWorker/Properties/launchSettings.json). La prod se comporte correctement « par accident » (le défaut est justement Production). Integrationn'envoie rien côté back-office : actuellementMailingServices.Configureaiguille Integration versSaveToDiskSender. Les notifications back-office en staging ne partent pas. Si on veut tester les vrais envois SES en staging, renommer l'env en autre chose que"Development"/"Integration"ET renseignerMailing:AwsSes:AccessKeyId/Secret.- Creds AWS SES vides en Integration (
appsettings.Integration.json) :AccessKeyIdetAccessKeySecretsont à"". Tant qu'on gardeSaveToDiskSenderce n'est pas un problème, mais bascule sur SES → exception AWS à l'envoi. - Asymétrie de niveau d'isolation entre les 2 canaux :
- SendGrid : redirection logique (le mail part vers SendGrid, qui l'envoie au destinataire redirigé).
- FluentEmail/SES en Dev/Integration : aucun envoi du tout (écriture disque).
- Conséquence : en Dev, un caller SendGrid produit un email visible dans Mailgun/destinataire de redirection ; un caller FluentEmail produit un fichier à fouiller dans
.dev/inbox/. Penser à ces deux endroits différents en debug.
- Clé SendGrid versionnée :
appsettings.development.jsonetWorker/appsettings.development.jsoncontiennentSendGrid:Keyen clair. À rotater + sortir du repo (user-secrets / env vars) dès que possible. - Templates SendGrid pas reproductibles localement : si l'id de template est cassé, il faut le corriger dans le dashboard SendGrid — pas dans le repo. Le fichier
appsettings:SendGrid:Templatesne contient que le mapping nom → id.
Conventions locales
- Ne PAS ajouter de logique de redirection ailleurs que dans
SendGridService/AwsSesSender. Si un nouveau service envoie des emails, il doit déléguer à l'un de ces deux canaux. - Le nom de l'env est sensible à la casse dans
MailingServices.Configure(is "Development" or "Integration").IHostEnvironment.IsProduction()est insensible à la casse mais matche uniquementProduction. - Pour ajouter un testeur à la redirection : éditer
Mailing:NonProductionRecipientsdans leappsettings.{env}.jsoncorrespondant. Une seule clé à modifier pour les 2 canaux.
Dépendances
- ←
booking(OnlinePaymentServiceenvoie la confirmation SendGrid après paiement) - ←
gift(envoi du bon cadeau SendGrid après paiement) - ←
notifications back-office(NotificationServicevia EmailFactory) - ←
auth(UserService,Identity/ForgotPassword,LoginControllerAPI client) - → SendGrid SaaS (templates dynamiques)
- → AWS SES (SMTP-like managé)

