Skip to main content

Mailafhandeling en templates

Dit hoofdstuk beschrijft de database-intentie voor applicatiegestuurde e-mail vanuit OefenHub. Identity-providerinterne mailflows zoals wachtwoordreset, provider-e-mailverificatie en MFA-mail vallen buiten deze database-informatie.

De mailmodule ondersteunt de eerste concrete flow: externe OefenHub-uitnodigingsmails voor relatie-uitnodigingen naar e-mailadressen die nog niet bij een intern account horen. Verzoeken worden niet synchron via SMTP verstuurd in de gebruikersrequest, maar als TickerQ-deliverytaak gepland zodat retry en achtergrondverwerking centraal gebeuren.

10.1 Eigenaarschap

OnderdeelKeuze
ProjectOefenHub.Mail
DbContextMailDbContext
Schemamail
Primaire verantwoordelijkheidMailtemplates, templatehistory, mailaanvragen/verzendpogingen en technische mailstatus.

OefenHub.Mail is eigenaar van externe applicatiemail. OefenHub.Communication blijft eigenaar van interne systeemberichten, mailboxitems, privéthreads en badges.

10.2 Tabelsamenvatting

TabelnaamCategorieDoel / verantwoordelijkheidGerelateerde tabellen
EmailTemplatesMailBeheerbare mailtemplatebron met codegedreven referentie en veilige HTML-inhoud.EmailTemplateHistory, MailSendAttempts
EmailTemplateHistoryMailHistorie van templatewijzigingen door beheerders.EmailTemplates, soft link naar Users
MailSendAttemptsMailHerleidbare registratie van mailaanvragen/verzendpogingen, TickerQ-planning, technische foutstatus, bronworkflow en correlation zonder secrets.EmailTemplates, MailSendAttemptEvents, soft links naar veroorzakende domeinobjecten
MailSendAttemptEventsMailAppend-only deliveryhistorie per logisch mailbericht met veilige gebruikerscategorieën en technische beheerinformatie zonder secrets.MailSendAttempts

Exacte kolommen worden bij implementatie in EF-configuratie en migratie vastgelegd, maar onderstaande velden zijn de minimale datacontractuele intentie.

10.3 EmailTemplates

VeldnaamTypeDefaultPKFKVerwijst naarUniqueNullableIndexOpmerking
Iduniqueidentifiernewsequentialid/newidJN-JNJTechnische sleutel.
CodeReferencenvarchar(100)-NNCodegedreven templatecatalogusJNJUnieke technische referentie, bijvoorbeeld relationship-invitation.
Namenvarchar(200)-NN-NNNBeheerbare naam voor de beheer-UI.
SubjectTemplatenvarchar(400)-NN-NNNOnderwerp met toegestane placeholders.
BodyHtmlTemplatenvarchar(max)-NN-NNNVeilige HTML-body met toegestane placeholders.
IsActivebit1NN-NNJAlleen actieve templates mogen runtime gebruikt worden.
CreatedAtUtcdatetime2sysutcdatetimeNN-NNNAanmaakmoment.
UpdatedAtUtcdatetime2-NN-NJNLaatste wijziging.

10.4 EmailTemplateHistory

VeldnaamTypeDefaultPKFKVerwijst naarUniqueNullableIndexOpmerking
Iduniqueidentifiernewsequentialid/newidJN-JNJTechnische sleutel.
EmailTemplateIduniqueidentifier-NJmail.EmailTemplates.IdNNJHarde FK binnen mailmodule.
ChangedAtUtcdatetime2sysutcdatetimeNN-NNJWijzigingsmoment.
ChangedByUserIduniqueidentifier-NNidentity.Users.IdNJJSoft link naar beheerder; geen cross-schema harde FK.
CodeReferencenvarchar(100)-NNCodegedreven templatecatalogusNNJSnapshot van de templatecode.
Namenvarchar(200)-NN-NNNSnapshot van de beheernaam.
SubjectTemplatenvarchar(400)-NN-NNNSnapshot van het onderwerp.
BodyHtmlTemplatenvarchar(max)-NN-NNNSnapshot van de HTML-body.
ChangeReasonnvarchar(500)-NN-NNNKorte samenvatting.

10.5 MailSendAttempts

VeldnaamTypeDefaultPKFKVerwijst naarUniqueNullableIndexOpmerking
Iduniqueidentifiernewsequentialid/newidJN-JNJTechnische sleutel.
TemplateCodeReferencenvarchar(100)-NNCodegedreven templatecatalogusNNJSnapshot van templatecode voor query/debug.
AddressKeynvarchar(50)-NNMail-configuratiecontextNNJBijvoorbeeld Invites; bepaalt runtime de afzenderconfiguratie. Geen afzenderadres- of wachtwoordsnapshot.
ToEmailnvarchar(320)-NN-NNJOntvanger zoals gebruikt voor verzending.
Subjectnvarchar(400)-NN-NNNGerenderd onderwerp op aanvraagmoment.
BodyHtmlnvarchar(max)-NN-NNNGerenderde HTML-body op aanvraagmoment.
Statusnvarchar(30)QueuedNNGesloten statussetNNJQueued, Sending, Sent, Failed, Canceled. Canceled betekent dat de mailaanvraag niet veilig kon worden gekoppeld aan een deliveryjob en daarom niet verzonden mag worden.
CorrelationIdnvarchar(100)-NNtechnische correlatieNNJKoppelt de oorspronkelijke gebruikersactie/workflow aan de TickerQ-delivery en technische logging. Meerdere mailpogingen uit dezelfde uitnodigingsactie kunnen zo gezamenlijk worden teruggevonden.
SourceEntityTypenvarchar(100)-NNbronworkflowNJJGenerieke bronkoppeling, bijvoorbeeld RelationshipInvitation.
SourceEntityIdnvarchar(100)-NNbronworkflow-idNJJBronentity-id als string, bijvoorbeeld RelationshipInvitation.Id.
Purposenvarchar(100)-NNmaildoelNJJDoel binnen bronworkflow, bijvoorbeeld InitialInvitation of Reminder.

Een reminder voor een relatie-uitnodiging maakt altijd een nieuwe MailSendAttempts-rij met Purpose = Reminder; het oorspronkelijke initial-invitation-mailattempt wordt niet hergebruikt. De bronkoppeling blijft SourceEntityType = RelationshipInvitation en SourceEntityId = RelationshipInvitations.Id, zodat de detailsweergave alle initiële en herinneringsmails veilig kan samenvatten. | RequestedByUserId | uniqueidentifier | - | N | N | identity.Users.Id | N | J | J | Soft link naar uitnodigende gebruiker. | | QueuedAtUtc | datetimeoffset | sysutcdatetime | N | N | - | N | N | N | Aanvraagmoment. | | ScheduledForUtc | datetimeoffset | sysutcdatetime | N | N | TickerQ planning | N | N | J | Earliest execution time voor de deliveryjob. | | LastAttemptAtUtc | datetimeoffset | - | N | N | - | N | J | N | Laatste verzendpoging. | | SentAtUtc | datetimeoffset | - | N | N | - | N | J | N | Succesvol verzonden moment. | | AttemptCount | int | 0 | N | N | - | N | N | N | Aantal SMTP-pogingen door de deliveryservice. | | LastError | nvarchar(max) | - | N | N | - | N | J | N | Veilige fouttype-informatie, geen credentials of providerpayloads. |

10.5A MailSendAttemptEvents

TECH-REL-MAIL-001 voegt een append-only eventtabel toe naast MailSendAttempts. MailSendAttempts blijft de actuele samenvatting van één logisch mailbericht; MailSendAttemptEvents bevat de tijdlijn van queue-, delivery-, retry- en annuleringsgebeurtenissen. Relatieverzoekdetails gebruiken hiervoor een mail-readmodelcontract en lezen niet rechtstreeks mailtabellen uit.

VeldnaamTypeDefaultPKFKVerwijst naarUniqueNullableIndexOpmerking
Iduniqueidentifiernewsequentialid/newidJN-JNJTechnische sleutel.
MailSendAttemptIduniqueidentifier-NJmail.MailSendAttempts.IdNNJLogisch mailbericht waarvoor dit event geldt.
AttemptNumberint-NN-NNJSMTP-/delivery-uitvoeringspoging binnen dezelfde mailattempt. Geen max-retrywaarde.
EventTypenvarchar(50)-NNGesloten eventsetNNJBijvoorbeeld Queued, SendStarted, SendSucceeded, SendFailed, Canceled, SourceContextInvalid.
OccurredAtUtcdatetimeoffsetsysutcdatetimeNN-NNJTijdstip van het event.
TechnicalErrorCodenvarchar(100)-NNTechnische foutcategorieNJJStabiele foutcode voor logging/beheer, zonder secrets.
TechnicalErrorTypenvarchar(100)-NNTechnische foutcategorieNJNAlleen voor beheer/diagnose, niet voor gewone gebruikers.
UserMessageCodenvarchar(100)-NNVeilige gebruikerscategorieNNJWordt door readmodels vertaald naar veilige melding zoals E-mail aangeboden aan de mailserver..

Zichtbaarheid voor gewone gebruikers wordt niet als IsUserVisible in de tabel opgeslagen. Het mail-readmodel bepaalt welke eventtypes en UserMessageCode-waarden veilig getoond mogen worden.

10.6 Business rules

  • CodeReference is uniek en codegedreven. Beheer mag de technische referentie niet vrij toevoegen of verwijderen.
  • SMTP-adressen en wachtwoorden komen uit configuratie/user secrets en worden nooit in mail.* opgeslagen. AddressKey is de enige persistente verwijzing naar de gekozen afzenderconfiguratie; delivery gebruikt altijd de actuele configuratie achter die sleutel.
  • SMTP-transport gebruikt MailKit zodat zowel implicit SSL/SMTPS (Mail:EnableSsl=true, Mail:EnableStartTls=false, typisch poort 465) als expliciete STARTTLS (Mail:EnableSsl=false, Mail:EnableStartTls=true, typisch poort 587) veilig configureerbaar zijn. Mail:EnableSsl en Mail:EnableStartTls zijn wederzijds exclusief en worden bij applicatiestart gevalideerd; een ongeldige combinatie is een configuratiefout die de applicatie bewust niet laat doorstarten.
  • Mail:SmtpTimeoutSeconds begrenst de connect/auth/send-stap. Een timeout wordt als tijdelijke delivery-fout geregistreerd en door TickerQ-retrybeleid opnieuw geprobeerd.
  • Templatehistory bevat geen SMTP-wachtwoorden, tokens of secrets.
  • Uitnodigingsmails naar onbekende relatieontvangers delen de volledige naam van de uitnodiger alleen na expliciete bevestiging in de UI.
  • Wanneer de gebruiker externe uitnodiging annuleert, wordt geen relatie-uitnodiging voor onbekende adressen opgeslagen.
  • Een mailverzoek is geen autorisatiebewijs. Links uit e-mail leiden naar reguliere routes die opnieuw server-side autorisatie en uitnodigingsstatus controleren.
  • Retry of herverzending moet idempotent zijn en mag geen dubbele relatie-uitnodigingen veroorzaken.
  • Wanneer een mailattempt een bronkoppeling heeft, controleert de deliveryjob vóór SMTP-submit of de broncontext nog geldig is. Voor RelationshipInvitation betekent dit: alleen verzenden zolang het verzoek nog Pending is.
  • Maildelivery loopt via TickerQ. Eén uitnodigingsmail wordt zo snel mogelijk gepland; meerdere mails worden met 30 seconden interval gepland om SMTP-bursts te voorkomen.
  • Een mailqueue-aanvraag is pas succesvol wanneer zowel het MailSendAttempts-record is opgeslagen als de bijbehorende TickerQ-deliveryjob is gepland. De aanroepende domeinflow moet synchrone mailvoorwaarden zoals template, senderconfiguratie en placeholderrendering vooraf valideren. Wanneer jobplanning daarna alsnog niet veilig lukt, wordt de mailattempt geannuleerd en mag de aanroepende domeinflow geen misleidende successtatus tonen.
  • CorrelationId blijft over de request-, relatie-uitnodiging-, mailqueue- en deliveryketen gelijk of herleidbaar, zodat support en logging later kunnen reconstrueren welke actie welke mailpogingen veroorzaakte.

10.7 Relatie met relatiebeheer

Voor een bestaand intern account wordt de relatie-uitnodiging via relationships.RelationshipInvitations en een intern communication.SystemMessages-record afgehandeld.

Voor een onbekend e-mailadres ontstaat pas na expliciete externe-mailbevestiging een zichtbaar RelationshipInvitations-record met ToUserId = null. Vóór deze opslag valideert de webflow de synchrone mailvoorwaarden via OefenHub.Mail: actieve template, senderconfiguratie en placeholderrendering. Daarna wordt de uitnodiging aangemaakt en wordt de mail als MailSendAttempts vastgelegd en via TickerQ gepland. Als de queue of jobplanning daarna alsnog faalt, wordt het zojuist aangemaakte externe relatieverzoek ingetrokken/verborgen en toont de gebruikersflow geen misleidende succesmelding.

Aanvulling - Reminderkanaal na claimen

mail.MailSendAttempts blijft alleen het externe mailkanaal vastleggen. Wanneer een externe relatie-uitnodiging later aan een bestaande OefenHub-gebruiker wordt gekoppeld, worden nieuwe herinneringen niet opnieuw als mailattempt aangemaakt maar als intern systeembericht. Alleen uitnodigingen zonder gekoppelde ToUserId gebruiken nog een nieuwe mailattempt met Purpose = Reminder.

Deze regel voorkomt dat OefenHub externe e-mail blijft sturen naar een gebruiker die inmiddels via het interne berichtensysteem bereikbaar is.