Skip to content

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 :

CanalCibleTechno
SendGridClient final (B2C : site web, mobile)API SendGrid + templates hébergés (id d-xxx)
FluentEmail + AWS SESUtilisateurs back-office BuchardFluentEmail + 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)
  • Canal FluentEmail/SES :
    • Factory : src/Application/Mailing/EmailFactory.cs (IEmailFactory)
    • Senders : src/Application/Mailing/AwsSesSender.cs (prod-like) et FluentEmail.Core.Defaults.SaveToDiskSender (Dev/Integration)
    • Configuration DI : src/Application/Mailing/MailingServices.cs > Configure (appelé par ApplicationModule, Worker/Program, WorkerGlobe/Program)
    • Modèles : src/Application/Mailing/Models/ (CreatePassword, ResetPassword, ResetCustomerPassword, Notification — tous héritent de EmailModelBase)
    • Templates Razor : src/Application/Templates/*.cshtml
    • Extensions d'envoi : src/Application/Extensions/EmailFactoryExtensions.cs
    • Callers : UserService, NotificationService, Identity/ForgotPassword.cshtml.cs

Canal SendGrid — méthodes et usages

MéthodeUsageCallers
SendTemplateEmailAsync(email, data, templateId, bcc, pdfStream, pdfFileName)Email template SendGrid + PDF optionnelOnlinePaymentService (Confirmation booking, Gift, Club), MailController (WaitingList, TripOffer, SetCustomerPassword), LoginController (ForgotCustomerPassword)
SendEmailAsync(email, subject, content)Email raw HTMLTrè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éthodeTemplate RazorCaller actif
SendDefinePasswordAsyncCreatePassword.cshtmlUserService:103 (création user back-office)
SendResetPasswordAsyncResetPassword.cshtmlIdentity/ForgotPassword:51
SendResetCustomerPasswordAsyncResetCustomerPassword.cshtml(aucun — extension préparée, non utilisée)
SendNotification (2 surcharges)Notification.cshtmlNotificationService:92/105/115
SendEmailConfirmationAsyncHTML 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

EnvMailing:NonProductionRecipientsDestinataire effectifBCC
Developmentconfiguréles adresses configuréesdroppés
Developmentabsent / []fallback geeks+buchard@spektrummedia.comdroppés
Integration / Stagingconfiguréles adresses configuréesdroppés
Integration / Stagingabsent / []fallback geeks+buchard@spektrummedia.comdroppés
Productionn'importe quoivrai destinataire (setting ignoré)conservés

Canal FluentEmail/SES

L'ISender injecté change selon l'env (MailingServices.Configure) :

EnvISender injectéEffet
DevelopmentSaveToDiskSenderEmail écrit sur disque dans {rootPath}/.dev/inbox/ — aucun envoi réseau
IntegrationSaveToDiskSenderidem (statu quo historique)
Autre env (Staging custom, Production)AwsSesSenderEnvoi réel via AWS SES

Quand AwsSesSender est actif, la redirection est appliquée à l'intérieur :

Env (avec AwsSesSender)Mailing:NonProductionRecipientsDestinataire effectifBCC
non-Production (ex : Staging)configuréles adresses configuréesvidés
non-Production (ex : Staging)absent / []fallback geeks+buchard@spektrummedia.comvidés
Productionn'importe quoivrais destinatairesconservés

Configuration

jsonc
{
  "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) ignorent Mailing:NonProductionRecipients quand IHostEnvironment.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.
  • SaveToDiskSender neutralise 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 faut DOTNET_ENVIRONMENT : le Worker est un generic host (Host.CreateDefaultBuilder, cf. Worker/WorkerHostBuilder.cs:32) qui ne lit que les variables préfixées DOTNET_. ASPNETCORE_ENVIRONMENT n'est lue que par le host web. Un conteneur Worker avec seulement ASPNETCORE_ENVIRONMENT=Staging tourne 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 via BackgroundWorker.DoOneMinuteJobOnlinePaymentService.CheckPendingPayments(), pas par le Web) part au vrai client + BCC réels (francois.buchard@buchard.ch / info@buchard.ch), Mailing:NonProductionRecipients ignoré. Incident constaté sur staging en août 2026. Toujours définir DOTNET_ENVIRONMENT sur les conteneurs Worker non-prod (les deux variables, comme dans Worker/Properties/launchSettings.json). La prod se comporte correctement « par accident » (le défaut est justement Production).
  • Integration n'envoie rien côté back-office : actuellement MailingServices.Configure aiguille Integration vers SaveToDiskSender. 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 renseigner Mailing:AwsSes:AccessKeyId/Secret.
  • Creds AWS SES vides en Integration (appsettings.Integration.json) : AccessKeyId et AccessKeySecret sont à "". Tant qu'on garde SaveToDiskSender ce 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.json et Worker/appsettings.development.json contiennent SendGrid:Key en 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:Templates ne 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 uniquement Production.
  • Pour ajouter un testeur à la redirection : éditer Mailing:NonProductionRecipients dans le appsettings.{env}.json correspondant. Une seule clé à modifier pour les 2 canaux.

Dépendances

  • booking (OnlinePaymentService envoie la confirmation SendGrid après paiement)
  • gift (envoi du bon cadeau SendGrid après paiement)
  • notifications back-office (NotificationService via EmailFactory)
  • auth (UserService, Identity/ForgotPassword, LoginController API client)
  • → SendGrid SaaS (templates dynamiques)
  • → AWS SES (SMTP-like managé)

Contributors

No contributors

Changelog

No recent changes