Systeem-mailboxen

Dit document legt uit waarom systeem-mailboxen bestaan, hoe ze zich verhouden tot tenant-mailboxen en welke beslissingen achter het ontwerp zitten. Voor het concreet koppelen, zie de how-to. Voor velden en endpoints, zie de reference.

Probleem

Tot voor kort verstuurde elke tenant zijn eigen e-mails via de mailprovider die in zijn eigen instellingen staat (mail-setting per tenant). Voor de meeste mails is dat goed: de afzender is dan de organisatie waar de respondent een relatie mee heeft.

Voor het monitor-domein werkt dat niet. Monitor-campagne-mails (uitnodiging, eerste herinnering, tweede herinnering, plus rapportages en bevestigingen) moeten systeembreed altijd vanuit één centraal adres komen, met één herkenbare afzender en één centrale inbox voor antwoorden. Wisselende afzenders per tenant verlagen herkenbaarheid en response.

Concept

Een systeem-mailbox is een mailbox die niet aan een tenant hangt, maar aan een vaste sleutel (een key, bijvoorbeeld 'monitor'). Records leven op de admin-connectie, los van tenant-databases. Het stelsel ondersteunt meerdere systeem-mailboxen onder verschillende keys, maar vandaag bestaat alleen monitor.

Een mail kiest zijn route via een optioneel veld op het bericht: systemMailboxKey. Is dat veld leeg, dan loopt de mail via de tenant-mailbox (huidige gedrag, ongewijzigd). Is het gevuld, dan haalt de mailer-factory de bijbehorende systeem-mailbox-config op en stuurt via die mailbox. Het veld wordt geërfd vanuit de message-template: een template met systemMailboxKey: 'monitor' produceert messages met diezelfde key.

Waarom een aparte registry, niet een vlag op de tenant-setting

De alternatieven waren:

  • Eén globale “default” mailprovider als fallback: gaf geen scheiding tussen “alle mails systeem-breed” en “monitor-mails systeem-breed”. Wij wilden expliciet kunnen kiezen.
  • Eén speciale tenant: organisatorisch verwarrend en koppelt mailbox-config aan tenant-onderhoud.
  • Hardcoded 'monitor@...' in de monitor-services: maakt de keuze onzichtbaar in instellingen en blokkeert toekomstige systeem-mailboxen.

De gekozen oplossing (kleine registry per key, optioneel veld op template/message) blijft dicht bij de bestaande mailer-architectuur en laat het stelsel additief uitbreiden.

Verhouding tot het bestaande mailstelsel

Het tenant-mailpad blijft volledig intact. Alleen MailProviderService.send heeft een vertakking aan de bovenkant:

message.systemMailboxKey?
  ja  → mailerFactory.getMailerForSystemMailbox(key)
  nee → mailerFactory.getMailer()  (huidige tenant-mailer)

Beide takken eindigen in dezelfde Mailer.send(message) interface. Onder water worden systeem-mailboxen altijd via Microsoft Graph verstuurd (app-only). Voor SendGrid is geen systeem-pad gebouwd, dat is niet nodig voor het monitor-doel.

Caching werkt gespiegeld: de factory cachet per key en invalideert na een config-wijziging of na een nieuwe consent.

Microsoft vereist admin-consent op de Azure app-registratie voordat een mailbox via Graph mag versturen. De systeem-flow gebruikt dezelfde app-registratie als de tenant-flow (AZURE_CLIENT_ID), maar:

  • het state-veld bevat { systemMailboxKey } in plaats van een tenantUuid,
  • de redirect-URI eindigt op /system/mailboxes/azure/callback,
  • de consent wordt opgeslagen op het SystemMailbox-record (admin-DB), niet op een tenant-setting.

De callback is publiek (Microsoft moet kunnen redirecten zonder sessie), maar valideert de state en weigert een onbekende key.

Foutgedrag, geen fallback

Bewust is er geen fallback naar de tenant-mailer. Een monitor-mail die niet door de systeem-mailbox kan, blijft staan op status ERROR. Reden: de afspraak “altijd vanuit één adres” zou worden geschonden als we bij outage stilzwijgend op een tenant-afzender terugvallen.

Concreet:

  • Als het record ontbreekt of incomplete is, throwt de factory en de message gaat naar ERROR. De bestaande BullMQ-retry pakt later opnieuw op.
  • Als de Graph-call een 401 of 403 geeft (consent ingetrokken Azure-side), zet de service consentStatus.granted op false en logt de fout op het record. De UI toont dan een banner.
  • Andere transient-fouten (rate limit, 5xx) worden gelogd op consentStatus.lastErrorAt/Message. Retry doet de rest.

Kernregels

  • Eén key, één Azure-mailbox. Geen multi-tenant-rotatie binnen één systeem-mailbox.
  • App-only OAuth. Geen user-delegated tokens, dus geen verloop op gebruikersniveau.
  • Templates dragen de keuze, niet de campagne-services. Wie een nieuwe monitor-template aanmaakt en wil dat hij via de systeem-mailbox gaat, tagt de template met systemMailboxKey.
  • Expliciete override wint. Geeft de caller van messageService.create zelf een systemMailboxKey mee, dan wint die boven de template-waarde.

Verwante documenten