L'application BizTalk STM.ICUCourriel.EAI fournit une couche d'intégration pour l'envoi de courriels et le suivi des rejets (EAI = Enterprise Application Integration) pour la STM (Société de transport de Montréal). Ses responsabilités sont :
L'application est composée de deux assemblages BizTalk déployés comme une seule application BizTalk :
| Assemblage | Responsabilité |
|---|---|
STM.ICUCourriel.EAI.EnvoiCourriel |
Envoi et suivi des courriels |
STM.ICUCourriel.EAI.TraiteRejet |
Réconciliation des rejets/suppressions |
| Système | Rôle | Transport dans BizTalk |
|---|---|---|
| Applications appelantes (internes) | Publication des demandes d'envoi de courriels | POST HTTP vers l'emplacement de réception WCF-WebHttp /icucourriel/api.svc/courriel |
| Exchange Online (EWS / SMTP) | Envoie les courriels pour les sources mappées au code de service SMTP |
Adaptateur BizTalk Static-DotNetSMTP, hôte ${SMTPHostName}:${SMTPPort} |
| SendGrid | Envoie les courriels pour toutes les autres sources ; fournit aussi les API de liste de suppression (rebonds, blocages, invalides, spam) | Port d'envoi WCF-WebHttp STM.ICUCourriel.EAI_SendGrid, URL de base ${UrlSendGrid} |
| Microsoft Entra ID (Azure AD) | Émet des jetons OAuth 2.0 pour l'interrogation de suppression Exchange Online | Port d'envoi WCF-WebHttp STM.ICUCourriel.EAI_OAuthToken.WCF-WebHttp, point de terminaison https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token |
| Base de données SQL ICU | Stocke et récupère les enregistrements de traitement de courriels ; interrogée via l'adaptateur WCF-SQL | Ports d'envoi STM.ICUCourriel.EAI_TraitementCourriel.WCF-SQL (écriture) et STM.ICUCourriel.EAI_TraitementCourrielResponse.WCF-SQL (lecture/mise à jour) |
| Système de fichiers (dossier de dépôt déclencheur) | Fichiers XML déclencheurs qui initient le lot de traitement de rejets | Emplacement de réception FILE ${RepertoireDeclenchement}\*.xml (interrogé toutes les 60 s) |
| Système de fichiers (dossier de résultats) | Destination des messages de sortie ResultatExecution |
Port d'envoi FILE STM.ICUCourriel.EAI_ResultatExecution, chemin ${ResultatExecutionRepertoire}\%MessageID%.xml |
| Nom | Protocole | Adresse | Pipeline | Notes |
|---|---|---|---|---|
STM.ICUCourriel.EAI_EnvoiCourriel.WCF-WebHttp |
HTTP/HTTPS | /icucourriel/api.svc — opération EnvoyerCourriel POST /courriel |
Rcv_Json (désassembleur JSON personnalisé) |
Auth Basic sur TLS ; AuthorizationManager personnalisé ; max 200 appels simultanés ; max message 206 536 octets |
STM.ICUCourriel.EAI_Declencheur.FILE |
FILE | ${RepertoireDeclenchement}\*.xml |
PassThruReceive |
Interroge toutes les 60 s, taille de lot 20 |
STM.ICUCourriel.EAI_PurgerTraitmentCourriel.Schedule |
Adaptateur Schedule | Quotidien à 03:00 | Rcv_Nullify |
Exécute PurgerTraitementCourriel 100 via ScheduledTaskAdapter ; nettoie la table de suivi |
| Nom | Protocole | Adresse / Point de terminaison | Notes |
|---|---|---|---|
STM.ICUCourriel.EAI_SMTP |
Static-DotNetSMTP (unidirectionnel) | ${SMTPHostName}:${SMTPPort} |
Pipeline Snd_GenCourrielSMTP ; réessai 3×5 s ; achemine les messages en échec |
STM.ICUCourriel.EAI_SendGrid |
WCF-WebHttp (bidirectionnel) | ${UrlSendGrid} |
Opérations : POST /mail/send, GET /suppression/blocks, GET /suppression/bounces, GET /suppression/invalid_emails, GET /suppression/spam_reports ; en-tête Auth Authorization: Bearer ; TLS personnalisé + WCFHttpHeaderBehavior ; réessai 3×5 s |
STM.ICUCourriel.EAI_OAuthToken.WCF-WebHttp |
WCF-WebHttp (bidirectionnel) | https://login.microsoftonline.com — POST /{tenant-id}/oauth2/v2.0/token |
Content-Type application/x-www-form-urlencoded ; pipeline personnalisé Snd_wwwFormUrlEncoded ; pipeline de réponse Rcv_JsonToken avec désassembleur JSON personnalisé (espace de noms https://www.stm.info/ICUCourriel/MSGraph/Email/Response/1) ; aucun réessai |
STM.ICUCourriel.EAI_TraitementCourriel.WCF-SQL |
WCF-SQL (unidirectionnel, livraison ordonnée) | ${IcuDbAdress} |
Actions : SauvegarderTraitementCourriel, MettreAJourTraitementCourriel ; délai d'ouverture/envoi 10 min |
STM.ICUCourriel.EAI_TraitementCourrielResponse.WCF-SQL |
WCF-SQL (bidirectionnel, intention ReadOnly) | ${IcuDbAdress} |
Actions : MettreAJourCourrielStatus, selEAICourriel, updEAICourriel ; délai d'ouverture/envoi 10 min ; isolation Serializable |
STM.ICUCourriel.EAI_ResultatExecution |
FILE (unidirectionnel) | ${ResultatExecutionRepertoire}\%MessageID%.xml |
Pipeline Snd_Nullify ; filtre sur BTS.MessageType = ResultatExecution ET Medium = Courriel |
Orchestration : STM.ICUCourriel.EAI.EnvoiCourriel.EnvoyerCourriel
Liaison de port BizTalk : MessageBox (liaison directe) — s'active sur tout message de type STM.ICUSchema.EAI.Schema.MessageUsager.Courriel arrivant du port de réception STM.ICUCourriel.EAI_EnvoiCourriel.
POST HTTP entrant vers /icucourriel/api.svc/courriel portant une charge utile JSON désérialisée en MessageUsagerCourriel (STM.ICUSchema.EAI.Schema.MessageUsager.Courriel).
DEBUT_TRAITEMENT_INFO).Source depuis la propriété de contexte du message STM.ICUSchema.EAI.Schema.Source.EnvoyerCourrielHelper.VerifierFiltreDestinataire(MessageUsagerCourriel) :Service = EnvoyerCourrielHelper.ObtenirServiceCourriel(Source) — retourne "SMTP" ou une autre valeur (p. ex. "SENDGRID").NbEnvoi = EnvoyerCourrielHelper.ObtenirNombreEnvoiParCourriel(MessageUsagerCourriel, Service).IdxEnvoi de 1 à NbEnvoi :MessageSousCourriel = EnvoyerCourrielHelper.ObtenirSousCourriel(MessageUsagerCourriel, IdxEnvoi, Service) (copie toutes les propriétés de contexte du message (*)).Service == "SMTP" → appel EnvoyerEwsCourriel(MessageSousCourriel, out Statut, out ExtMessageId)EnvoyerSendGridCourriel(MessageSousCourriel, out ExtMessageId, out Statut, Source)IdxEnvoi."Impossible d'envoyer un courriel (Filtre)".ExtMessageId.Statut = "Filtre", Service = "Aucun".SauvegarderTraitementCourriel(MessageUsagerCourriel, Service, ExtMessageId, Statut) pour persister l'enregistrement filtré.ResultatExecution = EnvoyerCourrielHelper.CreerResultatExecution(MessageUsagerCourriel, Statut).ResultatExecution vers ResultatExecutionFiltrePort (liaison directe MessageBox → acheminé vers le port d'envoi FILE par filtre d'abonnement sur le type de message + medium = Courriel).FIN_TRAITEMENT_INFO).scope/catch. Les exceptions non gérées se propagent au MessageBox BizTalk et résultent en une instance d'orchestration suspendue."Filtre" (lorsque filtré), ou la valeur retournée par les sous-orchestrations appelées ("Envoyé" ou "Echec").STM.ICUSchema.EAI.Schema.MessageUsager.Courriel — message canonique de courriel entrant (champ système source : STM.ICUSchema.EAI.Schema.Source)STM.ICUSchema.EAI.Schema.ResultatExecution — message de résultat d'exécutionOrchestration : STM.ICUCourriel.EAI.EnvoiCourriel.EnvoyerEwsCourriel
Invocable : oui (appelée de façon synchrone depuis EnvoyerCourriel quand Service == "SMTP")
Paramètres en entrée : courriel (MessageUsager.Courriel) ; en sortie : Statut (string), MessageId (string)
Appelée de façon synchrone depuis EnvoyerCourriel.
Service = "SMTP".Scope_EnvoyerEwsCourriel :courrielEWS (type System.Xml.XmlDocument) par copie depuis courriel avec les propriétés de contexte.courrielEWS vers SMTPPort → lié à STM.ICUCourriel.EAI_SMTP (adaptateur Static-DotNetSMTP). Il s'agit d'un envoi unidirectionnel ; BizTalk pilote la livraison SMTP réelle via le pipeline Snd_GenCourrielSMTP.MessageId = EnvoyerCourrielHelper.ObtenriGuid().Statut = "Envoyé".SauvegarderTraitementCourriel(courriel, Service, MessageId, Statut) — persiste l'enregistrement de succès.Catch_SMTPException (System.Exception) :SYSTEM_EXCEPTION_ERREUR, détail de l'exception).MessageId = courriel(STM.ICUSchema.EAI.Schema.IDMessage).Statut = "Echec".SauvegarderTraitementCourriel(courriel, Service, MessageId, Statut) — persiste l'enregistrement d'échec.System.Exception, le bloc catch enregistre le statut "Echec" et appelle SauvegarderTraitementCourriel ; l'orchestration retourne ensuite Statut = "Echec" à l'appelant.synchronized (IsSynchronized=True), ce qui dans BizTalk fournit un accès exclusif à l'état partagé mais ne fournit pas de transactions de base de données.STM.ICUSchema.EAI.Schema.MessageUsager.CourrielSystem.Xml.XmlDocument (courrielEWS — XML brut envoyé à l'adaptateur SMTP)STM.ICUCourriel.EAI.EnvoiCourriel.Schema.TypedProcedure_dbo.SauvegarderTraitementCourriel (via la sous-orchestration SauvegarderTraitementCourriel)Orchestration : STM.ICUCourriel.EAI.EnvoiCourriel.EnvoyerSendGridCourriel
Invocable : oui (appelée de façon synchrone depuis EnvoyerCourriel quand Service != "SMTP")
Paramètres en entrée : courriel (MessageUsager.Courriel), Source (string) ; en sortie : MessageId (string), Statut (string)
Appelée de façon synchrone depuis EnvoyerCourriel.
Service = "SENDGRID".Scope_SendGridCourriel :ReplyTo : si la propriété de contexte SMTP.From existe sur courriel, l'utiliser ; sinon chaîne vide.msgSendGridRequest (type SendGridCourrielSchema) :EnvoyerSendGridCourrielHelper.ObtenirSendGridMessage(courriel, Source, ReplyTo).msgSendGridRequest(STM.WCFHttpHeaderBehavior.EAI.Schema.value) = EnvoyerSendGridCourrielHelper.ObtenirSendGridAPIKey().msgSendGridRequest vers SendGridSendMailPort (opération SendMail, POST /mail/send).msgSendGridResponse (type System.Xml.XmlDocument) depuis SendGridSendMailPort.MessageId depuis le contexte WCF.InboundHttpHeaders : EnvoyerSendGridCourrielHelper.ObtenirSendGridMessageId(msgSendGridResponse(WCF.InboundHttpHeaders)) — lit l'en-tête de réponse X-Message-Id."X-Message-Id:" + MessageId.Statut = "Envoyé".SauvegarderTraitementCourriel(courriel, Service, MessageId, Statut).CatchException_SendGrid (System.Exception) :MessageId = courriel(STM.ICUSchema.EAI.Schema.IDMessage).Statut = "Echec".SauvegarderTraitementCourriel(courriel, Service, MessageId, Statut).SendGridSendMailPortType est requestresponse).ObtenirSendGridAPIKey()), non stockée comme propriété de contexte de message — le mécanisme réel de stockage du secret est opaque depuis le code d'orchestration.Authorization: Bearer setatruntime dans les HttpHeaders du port d'envoi indique que le jeton est injecté dans le contexte du message WCF par le comportement de point de terminaison STM.WCFHttpHeaderBehavior.EAI.Inspector en utilisant la valeur promue dans STM.WCFHttpHeaderBehavior.EAI.Schema.value.STM.ICUSchema.EAI.Schema.MessageUsager.CourrielSTM.ICUCourriel.EAI.EnvoiCourriel.Schema.SendGridCourrielSchema — Corps de l'API SendGrid v3 Mail Send :personalizations[].item (to, cc, bcc, substitutions)from (email, name)reply_to (email, name)subjectcontent[] (type, value)attachments[] (content, filename)categories[]System.Xml.XmlDocument (réponse brute SendGrid)Orchestration : STM.ICUCourriel.EAI.EnvoiCourriel.SauvegarderTraitementCourriel
Appelée par : EnvoyerEwsCourriel, EnvoyerSendGridCourriel et EnvoyerCourriel (chemin filtre)
Cette orchestration est référencée dans tout le code, mais son fichier source .odx n'était pas inclus dans l'ensemble analysé. Son contrat est connu depuis le schéma et les sites d'appel :
courriel (MessageUsager.Courriel), Service (string), ExtMessageId/MessageId (string), Statut (string)TypedProcedure/dbo/SauvegarderTraitementCourrielcourriels (ArrayOftypeCourriel), idMessage (max 100), enteteCourriel, source (max 100), externalMessageId (max 50), status (max 50), serviceCourriel (max 50), sujetReturnValue (int)STM.ICUCourriel.EAI_TraitementCourriel.WCF-SQL (unidirectionnel, livraison ordonnée)Orchestration : STM.ICUCourriel.EAI.TraiteRejet.TraiterRejets
Liaison de port BizTalk : Réception depuis STM.ICUCourriel.EAI_Declencheur (adaptateur FILE).
Arrivée de tout fichier *.xml dans ${RepertoireDeclenchement} (interrogé toutes les 60 secondes). Le contenu du fichier n'est pas consommé sémantiquement — il sert purement de déclencheur planificateur.
msgDeclencheur (type System.Xml.XmlDocument).msgSelEAICourriel :cdInterface = "traiterRejets"..cdInterface.dtExecution = System.DateTime.Now.msgSelEAICourriel vers selEAICourrielPort → STM.ICUCourriel.EAI_TraitementCourrielResponse.WCF-SQL, action TypedProcedure/dbo/selEAICourriel.msgSelEAICourrielResponse — le jeu de résultats SQL des courriels en attente de réconciliation de rejets, filtré par cdInterface = "traiterRejets".msgSelEAICourrielResponse) :MettreAJourRejetServeurExchange — réconcilier les NDR ExchangeMettreAJourBouncesSendGrid — récupérer les rebonds de l'API SendGrid et mettre à jour la BDMettreAJourBlocksSendGrid — récupérer les blocages de l'API SendGrid et mettre à jour la BDMettreAJourInvalidsSendGrid — récupérer les adresses invalides de l'API SendGrid et mettre à jour la BDMettreAJourSpamsSendGrid — récupérer les rapports de spam de l'API SendGrid et mettre à jour la BDmsgUpdEAICourriel :.cdInterface et dtDerniereExecution = dtExecution (l'horodatage capturé à l'étape 3).msgUpdEAICourriel vers updEAICourrielPort → STM.ICUCourriel.EAI_TraitementCourrielResponse.WCF-SQL, action TypedProcedure/dbo/updEAICourriel.msgUpdEAICourrielResponse.scope/catch dans cette orchestration. Tout échec dans les branches parallèles ou les appels SQL suspend l'instance d'orchestration.STM.ICUCourriel.EAI.TraiteRejet.Schema.TypedProcedure_dbo.selEAICourriel — entrée de requête BD (champ cdInterface)STM.ICUCourriel.EAI.TraiteRejet.Schema.TypedProcedure_dbo.selEAICourrielResponse — jeu de résultats de requête BD (enregistrements de courriels en attente de réconciliation)STM.ICUCourriel.EAI.TraiteRejet.Schema.TypedProcedure_dbo.updEAICourriel — entrée de mise à jour BD (cdInterface, dtDerniereExecution)STM.ICUCourriel.EAI.TraiteRejet.Schema.TypedProcedure_dbo.updEAICourrielResponseOrchestration : STM.ICUCourriel.EAI.TraiteRejet.ObtenirTokenOAuth
Invocable : oui (appelée depuis les sous-orchestrations de rejet qui interrogent Exchange Online)
Paramètres en sortie : token (string)
Appelée de façon synchrone depuis les sous-orchestrations de réconciliation de rejets (spécifiquement MettreAJourRejetServeurExchange).
Scope_OAuthToken :msgOAuthTokenRequest (type System.Xml.XmlDocument) :TraiterRejetsHelper.ObtenirTokenMessage().tenantId : msgOAuthTokenRequest(STM.ICUCourriel.EAI.TraiteRejet.Schema.tenantId) = TraiterRejetsHelper.ObtenirTenantId().OAuthTokenPort → STM.ICUCourriel.EAI_OAuthToken.WCF-WebHttp, opération POST /{tenant-id}/oauth2/v2.0/token.{tenant-id} est remplie au moment de l'envoi depuis la propriété de contexte tenantId promue (via BtsVariablePropertyMapping).application/x-www-form-urlencoded (pipeline Snd_wwwFormUrlEncoded).msgOAuthTokenResponse (type System.Xml.XmlDocument) — réponse de jeton JSON désérialisée par le pipeline Rcv_JsonToken.token = TraiterRejetsHelper.ExtraireAccessToken(msgOAuthTokenResponse).Catch_DeliveryFailureException (Microsoft.XLANGs.BaseTypes.DeliveryFailureException) :token = String.Empty.Catch_Exception (System.Exception) :token = String.Empty.DeliveryFailureException (échec de livraison BizTalk) et System.Exception (erreur générale) sont capturées. Dans les deux cas, token est mis à chaîne vide — les appelants reçoivent un jeton vide et sont censés le gérer, bien qu'aucune garde explicite en aval ne soit visible dans les orchestrations analysées.TraiterRejetsHelper.ObtenirTenantId() et promu comme propriété de contexte BizTalk. La valeur réelle n'est pas stockée dans les fichiers .odx.STM.ICUCourriel.EAI.TraiteRejet.Schema.ExchangeOnlinePropertySchema — schéma de propriété promue avec champ tenantId (xs:token), espace de noms https://STM.ICUCourriel.EAI.TraiteRejet.Schema.ExchangeOnlinePropertySchemaSystem.Xml.XmlDocument (requête et réponse)ObtenirServiceCourriel(Source) détermine si un message va vers EWS/SMTP ou SendGrid en fonction du champ Source. Cette logique de mappage réside entièrement dans une classe helper compilée (EnvoyerCourrielHelper) — elle n'est pas visible dans les schémas ou les orchestrations, créant une règle de routage opaque qui ne peut être modifiée sans redéployer l'assemblage.
ObtenirSendGridAPIKey() est un appel de méthode helper sans surface de configuration visible. La clé est probablement stockée dans une application affiliée SSO BizTalk, un fichier de configuration ou une base de données. Il s'agit d'une lacune d'audit de sécurité.
Quand ObtenirTokenOAuth échoue, il retourne token = "". Il n'y a pas de garde dans les orchestrations appelantes pour sauter l'étape d'interrogation Exchange si le jeton est vide. Cela cause probablement une erreur HTTP 401 en aval contre l'API Exchange, résultant en une instance d'orchestration suspendue.
Ni EnvoyerEwsCourriel ni EnvoyerSendGridCourriel ne réessaient en cas d'échec au niveau de l'orchestration. Le seul réessai disponible est le nombre de réessais du port d'envoi BizTalk (3 réessais, intervalle de 5 secondes pour SendGrid ; 3 réessais pour SMTP). Les instances en échec enregistrent simplement Statut = "Echec" sans escalade.
Les cinq branches parallèles dans TraiterRejets (Exchange, Bounces, Blocks, Invalids, Spam) ne partagent aucun try/catch. Un échec dans n'importe quelle branche fera suspendre l'instance complète de l'orchestration TraiterRejets, bloquant la mise à jour de l'horodatage updEAICourriel pour tous les canaux.
Le lot de rejets est déclenché par le dépôt de fichier dans ${RepertoireDeclenchement}. Il n'y a aucune garantie intégrée d'exécution unique, aucune lettre morte pour le fichier déclencheur, et aucun historique des fichiers déclencheurs.
ObtenriGuid() (et non ObtenirGuid())Le nom de méthode ObtenriGuid (manque le second 'i' dans Obtenir) apparaît dans deux orchestrations (EnvoyerCourriel et EnvoyerEwsCourriel). Il s'agit d'une faute de frappe dans la classe helper compilée qui a été perpétuée en production.
Le message de résultat d'exécution est acheminé vers un port d'envoi FILE. Il s'agit d'un patron résiduel qui ne fournit aucune sémantique de file d'attente durable et dépend de la disponibilité du système de fichiers partagé.
Scope_EnvoyerEwsCourriel est synchronized, ce qui signifie qu'il utilise la portée synchronisée à longue durée de BizTalk. Cela ne fournit pas de rollback ACID ; cela ne fait que sérialiser l'accès aux objets partagés en mémoire. Dans un scénario multi-instance, cela ajoute de la contention sans fournir de sécurité transactionnelle.
L'emplacement de réception HTTP plafonne les messages à environ 202 Ko. Les courriels avec de grosses pièces jointes seraient rejetés au niveau de la couche de réception.
| Composant | Type | Objectif |
|---|---|---|
Assemblage STM.ICUCourriel.EAI.Helper |
Helper BizTalk personnalisé | Contient EnvoyerCourrielHelper, EnvoyerSendGridCourrielHelper, TraiterRejetsHelper, AuthorizationManager personnalisé, Logger |
Assemblage STM.ICUSchema.EAI |
Assemblage de schéma partagé | Définit MessageUsager.Courriel, ResultatExecution, propriétés de contexte Source, IDMessage, Medium |
Assemblage STM.WCFHttpHeaderBehavior.EAI |
Comportement de point de terminaison WCF | Injecte la clé API SendGrid comme en-tête HTTP Authorization au moment de l'envoi |
Assemblage STM.TLSEndpointBehavior.EAI |
Comportement de point de terminaison WCF | Configure le TLS mutuel sur les connexions WCF-WebHttp sortantes (SendGrid, OAuth) |
Assemblage STM.JsonAssembler.EAI |
Composant de pipeline personnalisé | Désassembleur JSON utilisé dans les pipelines Rcv_Json et Rcv_JsonToken |
ScheduledTaskAdapter |
Adaptateur BizTalk | Pilote la purge quotidienne de la table TraitementCourriel |
Base de données SQL ICU (${IcuDbAdress} / ${IcuDbAdoAdress}) |
SQL Server | Stocke les enregistrements de traitement de courriels ; expose les procédures stockées : SauvegarderTraitementCourriel, MettreAJourTraitementCourriel, MettreAJourCourrielStatus, selEAICourriel, updEAICourriel, PurgerTraitementCourriel |
| Exchange Online (EWS) | SaaS externe | Livre les courriels pour les sources routées SMTP |
| SendGrid | SaaS externe | Livre les courriels pour les sources non SMTP ; fournit les API de suppression |
| Microsoft Entra ID | SaaS externe | Émet des jetons OAuth 2.0 pour les appels à l'API Exchange Online |
| Variable | Usage |
|---|---|
${BtsProcessingHostInstance} |
Hôte d'orchestration |
${BtsSendHostInstance} |
Hôte d'adaptateur d'envoi |
${BtsIsolatedHostInstanceName} |
Hôte de réception isolé (HTTP) |
${BtsReceiveHostInstanceNameNonHa} |
Hôte de réception non HA (adaptateur schedule) |
${BtsHostInstanceNTGroupName} |
Groupe NT pour les hôtes approuvés |
${SMTPHostName}, ${SMTPPort} |
Relais Exchange/SMTP |
${UrlSendGrid} |
URL de base de l'API SendGrid |
${IcuDbAdress} |
Chaîne de connexion WCF-SQL |
${IcuDbAdoAdress} |
Chaîne de connexion ADO.NET (purge par adaptateur schedule) |
${ResultatExecutionRepertoire} |
Répertoire de sortie pour les résultats d'exécution |
${RepertoireDeclenchement} |
Répertoire de dépôt pour les fichiers déclencheurs de lot de rejets |
Le diagramme montre la topologie BizTalk complète pour STM.ICUCourriel.EAI. Les deux assemblages BizTalk (EnvoiCourriel et TraiteRejet) sont présentés comme des grappes d'orchestrations séparées.
graph LR
subgraph InternalCallers["Appelants internes"]
IC[Clients HTTP internes]
FS_IN["Déclencheur système de fichiers\n${RepertoireDeclenchement}/*.xml"]
SCHED["Adaptateur Schedule\npurge quotidienne 03:00"]
end
subgraph ReceiveLocations["Emplacements de réception"]
RL1["EAI_EnvoiCourriel.WCF-WebHttp\n/icucourriel/api.svc POST /courriel\nAuth Basic · Rcv_Json · max 202 Ko"]
RL2["EAI_Declencheur.FILE\n${RepertoireDeclenchement}/*.xml\ninterrogation 60s · lot 20"]
RL3["EAI_PurgerTraitmentCourriel.Schedule\nAdaptateur Schedule · 03:00 quotidien"]
end
subgraph MessageBox["BizTalk MessageBox (pub/sub)"]
MB[(MessageBox\nSQL Server)]
end
subgraph EnvoiCourrielAssembly["Assemblage : STM.ICUCourriel.EAI.EnvoiCourriel"]
O1["EnvoyerCourriel\nFiltre → routage par Source\n→ boucle de lot"]
O2["EnvoyerEwsCourriel\nLivraison SMTP\n(portée synchronisée)"]
O3["EnvoyerSendGridCourriel\nLivraison SendGrid"]
O4["SauvegarderTraitementCourriel\nHelper de persistance SQL"]
end
subgraph TraiteRejetAssembly["Assemblage : STM.ICUCourriel.EAI.TraiteRejet"]
O5["TraiterRejets\nCoordinateur\n(fan-out parallèle × 5)"]
O6["ObtenirTokenOAuth\nAcquisition de jeton Entra ID"]
O7["MettreAJourRejetServeurExchange\nRéconciliation NDR Exchange"]
O8["MettreAJourBouncesSendGrid"]
O9["MettreAJourBlocksSendGrid"]
O10["MettreAJourInvalidsSendGrid"]
O11["MettreAJourSpamsSendGrid"]
end
subgraph SendPorts["Ports d'envoi"]
SP1["EAI_SMTP\nStatic-DotNetSMTP\n${SMTPHostName}:${SMTPPort}"]
SP2["EAI_SendGrid.WCF-WebHttp\n${UrlSendGrid}\nPOST /mail/send\nGET /suppression/*"]
SP3["EAI_OAuthToken.WCF-WebHttp\nlogin.microsoftonline.com\nPOST /oauth2/v2.0/token"]
SP4["EAI_TraitementCourriel.WCF-SQL\nSauvegarder / MettreAJour\n(unidirectionnel, ordonné)"]
SP5["EAI_TraitementCourrielResponse.WCF-SQL\nMettreAJourStatus / selEAICourriel\nupdEAICourriel (bidirectionnel)"]
SP6["EAI_ResultatExecution\nFILE · ${ResultatExecutionRepertoire}"]
end
subgraph ExternalSystems["Systèmes externes"]
SMTP_SVC[Exchange Online\nEWS / SMTP]
SG[SendGrid\nSaaS courriel]
ENTRA[Microsoft Entra ID\noauth2/v2.0/token]
SQL[(Base de données SQL ICU\n${IcuDbAdress})]
FS_OUT[Système de fichiers\n${ResultatExecutionRepertoire}]
end
IC -->|POST /courriel JSON| RL1
FS_IN -->|déclencheur *.xml| RL2
SCHED -->|minuterie| RL3
RL1 --> MB
RL2 --> MB
RL3 --> MB
MB -->|MessageUsagerCourriel| O1
MB -->|declencheur| O5
O1 -->|Service=SMTP| O2
O1 -->|Service!=SMTP| O3
O1 -->|chemin Filtre| O4
O2 -->|appel| O4
O3 -->|appel| O4
O5 -->|parallèle| O7
O5 -->|parallèle| O8
O5 -->|parallèle| O9
O5 -->|parallèle| O10
O5 -->|parallèle| O11
O7 -->|appel| O6
O2 --> SP1
O3 --> SP2
O6 --> SP3
O8 --> SP2
O9 --> SP2
O10 --> SP2
O11 --> SP2
O4 --> SP4
O5 --> SP5
O1 --> SP6
SP1 -->|SMTP/TLS| SMTP_SVC
SP2 -->|HTTPS| SG
SP3 -->|HTTPS| ENTRA
SP4 -->|WCF-SQL| SQL
SP5 -->|WCF-SQL| SQL
SP6 --> FS_OUT
RL3 -->|purge ADO| SQL
style O6 fill:#f0ad4e,color:#000
style SP6 fill:#ccc,color:#000
Cette séquence couvre le flux principal d'envoi de courriels (le chemin le plus complexe — envoi SendGrid multi-destinataires), suivi du déclenchement de la réconciliation des rejets.
sequenceDiagram
autonumber
actor Caller as Appelant interne
participant RL as Emplacement de réception BizTalk<br/>(EAI_EnvoiCourriel)
participant MB as BizTalk MessageBox
participant EnvC as EnvoyerCourriel<br/>orchestration
participant EnvSG as EnvoyerSendGridCourriel<br/>sous-orchestration
participant EnvEWS as EnvoyerEwsCourriel<br/>sous-orchestration
participant SaveDB as SauvegarderTraitementCourriel<br/>sous-orchestration
participant SG as API SendGrid
participant SMTP as Exchange / SMTP
participant SQL as Base de données SQL ICU
participant FS as Répertoire de résultats
Caller->>RL: POST /icucourriel/api.svc/courriel (JSON, auth Basic)
RL->>MB: Publication MessageUsagerCourriel (désérialisé)
MB->>EnvC: Activation (liaison directe sur MessageUsager.Courriel)
EnvC->>EnvC: ObtenirServiceCourriel(Source)\n→ "SMTP" ou "SENDGRID"
EnvC->>EnvC: ObtenirNombreEnvoiParCourriel(message, Service)
alt Destinataire filtré
EnvC->>SaveDB: appel(Service=Aucun, Statut=Filtre)
SaveDB->>SQL: exec SauvegarderTraitementCourriel
EnvC->>MB: Publication ResultatExecution(Filtre)
MB->>FS: Le port d'envoi FILE écrit le XML
else Routage SMTP (par sous-message dans la boucle)
loop IdxEnvoi 1..NbEnvoi
EnvC->>EnvC: ObtenirSousCourriel(message, idx, SMTP)
EnvC->>EnvEWS: appel(MessageSousCourriel)
EnvEWS->>SMTP: Envoi via l'adaptateur Static-DotNetSMTP
alt Succès
EnvEWS->>SaveDB: appel(Statut=Envoyé, MessageId=nouveauGUID)
SaveDB->>SQL: exec SauvegarderTraitementCourriel
else System.Exception
EnvEWS->>SaveDB: appel(Statut=Echec, MessageId=courriel.IDMessage)
SaveDB->>SQL: exec SauvegarderTraitementCourriel
end
end
EnvC->>MB: Publication ResultatExecution
MB->>FS: Le port d'envoi FILE écrit le XML
else Routage SendGrid (par sous-message dans la boucle)
loop IdxEnvoi 1..NbEnvoi
EnvC->>EnvC: ObtenirSousCourriel(message, idx, SENDGRID)
EnvC->>EnvSG: appel(MessageSousCourriel, Source)
EnvSG->>EnvSG: ObtenirSendGridAPIKey() → en-tête
EnvSG->>SG: POST /mail/send {personalizations, from, subject, content}
SG-->>EnvSG: 202 Accepted, en-tête X-Message-Id
alt Succès
EnvSG->>SaveDB: appel(Statut=Envoyé, MessageId=X-Message-Id)
SaveDB->>SQL: exec SauvegarderTraitementCourriel
else System.Exception
EnvSG->>SaveDB: appel(Statut=Echec, MessageId=courriel.IDMessage)
SaveDB->>SQL: exec SauvegarderTraitementCourriel
end
end
EnvC->>MB: Publication ResultatExecution
MB->>FS: Le port d'envoi FILE écrit le XML
end
Note over MB,SQL: La réconciliation des rejets s'exécute sur un déclencheur fichier séparé
participant FT as Dépôt fichier déclencheur
participant TR as TraiterRejets<br/>orchestration
participant OA as ObtenirTokenOAuth<br/>sous-orchestration
FT->>MB: Dépôt de fichier dans ${RepertoireDeclenchement}
MB->>TR: Activation
TR->>SQL: exec selEAICourriel(cdInterface=traiterRejets)
SQL-->>TR: enregistrements de courriels en attente
par 5 branches parallèles
TR->>OA: appel (pour la branche Exchange)
OA->>ENTRA as Entra ID: POST /oauth2/v2.0/token
ENTRA-->>OA: access_token
OA-->>TR: token
TR->>SMTP: Requête NDR (API Exchange Graph)
and
TR->>SG: GET /suppression/bounces
and
TR->>SG: GET /suppression/blocks
and
TR->>SG: GET /suppression/invalid_emails
and
TR->>SG: GET /suppression/spam_reports
end
TR->>SQL: exec updEAICourriel(cdInterface, dtDerniereExecution)
| Orchestration | Complexité | Effort (jours) | Bloquant | Équivalent Azure |
|---|---|---|---|---|
EnvoyerCourriel |
Moyenne | 3 | Nécessite la décompilation de EnvoyerCourrielHelper — la table de routage ObtenirServiceCourriel(Source) est opaque ; impossible d'écrire la logique de routage sans elle |
Workflow Logic Apps Standard avec état |
EnvoyerEwsCourriel |
Moyenne | 2 | Confirmer si l'adaptateur SMTP Exchange Online est encore utilisé ; si oui, porter vers l'API Graph sendMail — nécessite un enregistrement d'application Entra avec Mail.Send |
Action HTTP Logic Apps → API Graph |
EnvoyerSendGridCourriel |
Basse | 2 | Récupérer la clé API SendGrid du SSO BizTalk avant la migration ; confirmer la construction du corps ObtenirSendGridMessage |
Action HTTP Logic Apps → API SendGrid v3 |
SauvegarderTraitementCourriel |
Basse | 1 | La signature de la procédure SQL est entièrement visible ; aucun bloquant | Appel SQL en ligne dans Logic App via connecteur Azure SQL |
TraiterRejets |
Haute | 4 | Cinq branches parallèles ; l'isolation des erreurs doit être repensée — le code actuel n'a aucune gestion d'exception par branche. Nécessite une décision des opérations STM sur le suivi indépendant des horodatages | Azure Durable Function fan-out/fan-in |
ObtenirTokenOAuth |
Basse | 0,5 | Peut être entièrement remplacé par l'identité managée — aucune migration de code nécessaire si la permission Graph Mail.Send est accordée |
L'identité managée remplace l'acquisition manuelle de jeton |
MettreAJourRejetServeurExchange |
Moyenne | 2 | Le patron de requête NDR Exchange doit être documenté (filtre de requête API Graph, boîte aux lettres à interroger) — non visible dans le code source BizTalk | Activity Durable Function |
MettreAJourBouncesSendGrid / MettreAJourBlocksSendGrid / MettreAJourInvalidsSendGrid / MettreAJourSpamsSendGrid |
Basse (chacune) | 1 par | Les points de terminaison et la pagination de l'API de suppression SendGrid sont bien documentés ; aucune décompilation d'assemblage nécessaire | Activities Durable Function (4 × 1 jour) |
Composants de pipeline JSON personnalisés (Rcv_Json, Rcv_JsonToken, Snd_wwwFormUrlEncoded) |
Basse | 1 | Décompiler pour vérifier l'absence de transformation non standard ; remplacer par System.Text.Json |
Analyse JSON native Logic Apps |
AuthorizationManager (auth HTTP Basic) |
Basse | 1 | Nécessite l'extraction de la liste d'appelants autorisés depuis l'assemblage compilé pour configurer correctement la politique JWT APIM | Politique validate-jwt APIM (Entra ID) |
| Inventaire des variables de déploiement | Moyenne | 1 | Sept variables de configuration ne sont pas dans le code source — récupérer des scripts de déploiement | Azure App Settings / références Key Vault |
Effort total estimé : 22 jours-développeur (développeur unique ; ObtenirTokenOAuth peut être parallélisé avec TraiterRejets une fois l'échafaudage Durable Function en place).
Compléter ces étapes avant d'écrire tout code Azure pour cette intégration.
STM.ICUCourriel.EAI.Helper.dll avec ILSpy ou dnSpy. Exporter toutes les classes : EnvoyerCourrielHelper, EnvoyerSendGridCourrielHelper, TraiterRejetsHelper, AuthorizationManager, Logger. Il s'agit de l'élément à plus haut risque — toute la logique de routage, de filtrage et de récupération de clés réside ici.EnvoyerCourrielHelper, extraire et documenter la table de mappage ObtenirServiceCourriel(Source). C'est la règle qui décide SMTP vs SendGrid par source appelante. Elle ne peut pas être réimplémentée sans ces données.EnvoyerCourrielHelper, extraire ObtenirNombreEnvoiParCourriel — comprendre ce qui déclenche une boucle multi-sous-messages (est-ce le nombre de destinataires ? le nombre de pièces jointes ? autre chose ?).EnvoyerSendGridCourrielHelper, extraire ObtenirSendGridMessage pour comprendre comment le corps de la requête SendGrid v3 est construit à partir de MessageUsagerCourriel. Vérifier le mappage des champs personalizations, from, reply_to, attachments.TraiterRejetsHelper, extraire ObtenirTokenMessage, ObtenirTenantId, ExtraireAccessToken pour comprendre le corps de la requête de jeton OAuth (client_id, client_secret, scope). Ces valeurs doivent être migrées vers Key Vault.STM.ICUCourriel.EAI.Helper.Securite.AuthorizationManager pour extraire la liste d'appelants autorisés. Cela devient l'audience JWT APIM ou la liste blanche de clés d'abonnement.STM.ICUCourriel. La clé API SendGrid y est stockée (ou dans un fichier de configuration — vérifier les deux). Extraire et stocker dans un classeur de secrets sécurisé.${SMTPHostName}, ${SMTPPort}, ${UrlSendGrid}, ${IcuDbAdress}, ${IcuDbAdoAdress}, ${ResultatExecutionRepertoire}, ${RepertoireDeclenchement}. Les obtenir des scripts de déploiement BizTalk (.btdfproj ou fichiers de propriétés MSBuild).${RepertoireDeclenchement} — compter combien de fichiers déclencheurs sont actuellement présents et s'ils sont en cours de traitement. Noter la cadence des dépôts de fichiers (manuel, planifié ou depuis un autre système). Cela détermine comment calibrer le remplacement par déclencheur minuterie.${ResultatExecutionRepertoire} — déterminer si un système en aval lit les fichiers XML de résultats. Si oui, planifier un remplacement par callback Service Bus ou HTTP avant de décommissionner le port d'envoi FILE.${IcuDbAdress} et scripter toutes les procédures stockées : SauvegarderTraitementCourriel, MettreAJourTraitementCourriel, MettreAJourCourrielStatus, selEAICourriel, updEAICourriel, PurgerTraitementCourriel. Sauvegarder le DDL dans le contrôle de source.selEAICourriel : comprendre ce que retourne cdInterface = "traiterRejets" — noms et types de colonnes. Cela devient l'entrée de la Durable Function pour les 5 activités de rejet.SauvegarderTraitementCourriel est idempotent sur idMessage. Sinon, les sessions Service Bus (livraison ordonnée) sont obligatoires pour prévenir les écritures dupliquées.updEAICourriel peut être appelé par activité (par type de rejet) plutôt qu'une seule fois à la fin, pour corriger le problème actuel d'horodatage tout-ou-rien.Static-DotNetSMTP achemine encore activement des courriels ou s'il a déjà été remplacé par SendGrid. Vérifier le mappage source ObtenirServiceCourriel pour toute source configurée comme "SMTP".Mail.Send (ou utiliser l'identité managée). Documenter la ou les boîtes aux lettres d'expéditeur utilisées par BizTalk.MettreAJourRejetServeurExchange — interroge-t-il la boîte de réception d'une boîte aux lettres Exchange pour les messages rebondis, ou appelle-t-il un point de terminaison d'API Exchange ? Le code source BizTalk n'expose pas cela clairement ; nécessite la décompilation du helper de rejet.TraiterRejets est acceptable. Dans BizTalk, un échec bloque le lot entier. Dans Azure, vous voudrez peut-être laisser les 4 autres branches continuer — il s'agit d'un changement de comportement nécessitant une approbation.ObtenriGuid() dans le code de production — confirmer que le GUID généré est véritablement aléatoire (équivalent UUID v4) pour que le code porté puisse utiliser Guid.NewGuid().ToString() sans problème de compatibilité de données.feat/la-send-email, feat/fn-process-rejections)..env.local (ignoré par git) avec tous les placeholders d'identifiants extraits. Ne jamais committer de vrais secrets.