Files
MijnBesluit/docs/E-mail.md
T
2026-09-27 14:21:31 +00:00

9.7 KiB

E-mail: hoe MijnBesluit post verstuurt

Zonder e-mail werkt MijnBesluit niet. Inloggen gaat met een code per mail, en uitnodigingen aan familie gaan per mail. Elke storing hier is dus een storing in het hart van de applicatie.

Hoe het nu loopt

Sinds september 2026 gaat alle uitgaande post via de eigen Kerio Connect op dezelfde server als de website. Het domein mijnbesluit.nl staat daar als domein ingericht, met de mailboxen noreply@mijnbesluit.nl en info@mijnbesluit.nl.

MijnBesluit (website + SocketServer)
   -> 127.0.0.1:25  (Kerio Connect, zelfde machine)
   -> poort 25 naar buiten  -> Gmail en de rest

De afzender is noreply@mijnbesluit.nl. Dat mag nu, want het domein staat op onze eigen server; bij Solcon kon het niet (zie "Wat er al een keer misging").

Waar de instellingen staan

In C:\MijnBesluitData\appsettings.secrets.json op de server, in het blok Smtp. Dat bestand wordt gelezen door zowel de website als de SocketServer, zodat er één plek is:

"Smtp": {
  "Host": "127.0.0.1",
  "Port": 25,
  "UseStartTls": false,
  "Username": "",
  "Password": "",
  "FromAddress": "noreply@mijnbesluit.nl",
  "FromName": "MijnBesluit",
  "WriteToDiskInsteadOfSending": false,
  "RedirectAllTo": ""
}

Waarom gebruikersnaam en wachtwoord leeg zijn

Kerio staat relayen toe voor de groep "Local clients", en 127.0.0.1 zit daarin. Post die van deze server zelf komt hoeft dus niet in te loggen. Dat is niet alleen eenvoudiger maar ook beter: er staat geen mailwachtwoord meer in dit bestand.

Vul hier niet alsnog een wachtwoord in. Klopt het niet, dan krijg je geen foutmelding maar traagheid - zie hieronder.

Na een wijziging moet je herstarten; de instellingen worden één keer bij het opstarten gelezen en daarna vastgehouden, ondanks reloadOnChange:

Restart-Service MijnBesluitSocket
Restart-WebAppPool mijnbesluit

Waarom 127.0.0.1 en niet localhost

localhost lost Windows eerst op naar het IPv6-adres ::1. Luistert Kerio daar niet, dan wacht .NET het verbindingstijdslimiet af voordat hij IPv4 probeert. Het resultaat: de mail komt wél aan, maar het antwoord aan de app duurt meer dan tien seconden en het proefscript geeft een time-out. Met een hard IPv4-adres is het binnen een seconde klaar.

Dit geldt net zo goed voor de database. MariaDB luistert standaard alleen op 127.0.0.1, dus Server=localhost in de connectiereeks kost bij elke nieuwe verbinding dezelfde verloren seconden - merkbaar als de eerste bezoeker na een herstart van de app-pool lang moet wachten. Zet ook daar Server=127.0.0.1. Meet het verschil zo:

Measure-Command { .\tools\SocketServer-proef.ps1 }                          # raakt de database niet
Measure-Command { .\tools\SocketServer-proef.ps1 -Email ... -Nieuw }        # raakt hem wel

Waarom geen TLS op die stap

Kerio presenteert op 587 nog een zelfondertekend certificaat. System.Net.Mail.SmtpClient weigert dat botweg (The remote certificate is invalid according to the validation procedure, met RemoteCertificateNameMismatch, RemoteCertificateChainErrors), en de controle uitzetten is in een applicatie die wachtwoorden bewaart het verkeerde ruilmiddel.

Over loopback is dat te verdedigen: het pakket verlaat de machine niet. Maar het hoort tijdelijk te zijn - zie "Wat hier nog niet goed aan is".

Let op: SmtpClient kan geen implicit TLS. Poort 465 werkt dus nooit; dat moet 587 met STARTTLS zijn zodra er een geldig certificaat is.

De DNS voor mijnbesluit.nl

Bij Strato. Alles hieronder is september 2026 ingericht; daarvóór had het domein helemaal geen TXT-records.

Naam Type Waarde
mijnbesluit.nl A 85.215.171.61
mijnbesluit.nl MX mail.kwik-bit.nl, prioriteit hoog
mijnbesluit.nl TXT v=spf1 ip4:85.215.171.61 ~all
mail._domainkey.mijnbesluit.nl TXT de DKIM-sleutel die Kerio toont
_dmarc.mijnbesluit.nl TXT v=DMARC1; p=none; rua=mailto:info@mijnbesluit.nl

Aandachtspunten:

  • In het MX-record hoort een naam, geen IP-adres. Een MX naar 85.215.171.61. wordt door mailservers die zich aan de regels houden geweigerd.
  • Strato's eigen SPF- en DMARC-knoppen staan op "geen", zodat er geen tweede record naast het onze komt. Twee SPF-records betekent dat geen van beide werkt.
  • De reverse lookup van 85.215.171.61 geeft mail.kwik-bit.nl, en die naam wijst terug naar hetzelfde IP. Die heen-en-terugcontrole is wat Gmail eist; hij hoeft niet bij het afzenderdomein te horen. De Internet hostname in Kerio moet gelijk zijn aan die PTR.

Bekend probleem: ns3 loopt achter

De twee nameservers van Strato geven niet hetzelfde antwoord: ns4.stratoserver.net heeft het DKIM-record, ns3.stratoserver.net valt terug op een wildcard-CNAME naar mijnbesluit.nl. DKIM slaagt daardoor ongeveer de helft van de keren. SPF staat op het domein zelf en heeft er geen last van, en SPF alleen is genoeg voor DMARC-uitlijning - de post komt dus gewoon aan.

Controleren:

$ns = (Resolve-DnsName mijnbesluit.nl -Type NS -Server 8.8.8.8).NameHost
foreach ($s in $ns) {
  "--- $s"
  Resolve-DnsName mail._domainkey.mijnbesluit.nl -Type TXT -Server $s -ErrorAction SilentlyContinue |
    Select-Object Type, @{n='Waarde';e={ if ($_.Strings) { ($_.Strings -join '').Substring(0,40) } else { $_.NameHost } }}
}

Staat ns3 er over een paar dagen nog steeds anders in, dan is dat een melding aan Strato: "mail._domainkey.mijnbesluit.nl geeft op ns3 een wildcard-CNAME en op ns4 het TXT-record."

Testen

Van binnen naar buiten, zodat je weet wáár het misgaat:

# 1. de hele keten: socket, database, mailserver
.\tools\SocketServer-proef.ps1 -Email iemand@example.nl -Nieuw

# 2. alleen de mailserver
$c = Get-Credential
Send-MailMessage -SmtpServer mail.kwik-bit.nl -Port 587 -UseSsl -Credential $c `
  -From "noreply@mijnbesluit.nl" -To "..." -Subject "Proef" -Body "Proef"

-Nieuw vraagt een registratiecode in plaats van een inlogcode, zodat je naar een adres kunt testen waar nog geen account bij hoort. Er wordt pas een account aangemaakt als de code wordt ingevuld.

Komt de mail aan bij Gmail, open hem dan via Origineel weergeven: daar staat of SPF, DKIM en DMARC geslaagd zijn. Dat is de enige echte controle.

Wat er al een keer misging

0. Een voorbeeldtekst als wachtwoord kost negen seconden per inlogcode

Het langste dat dit project ons heeft gekost, en het stond al die tijd in het instellingenbestand:

"Password": "het wachtwoord van die mailbox",

Dat is de voorbeeldtekst uit de handleiding, nooit vervangen. Het gevolg is níet een foutmelding. Kerio weigert de aanmelding, .NET probeert het opnieuw met een andere methode, en valt daarna terug op versturen zonder aanmelden - wat mag, want het IP-adres staat in "Local clients". De mail komt dus gewoon aan. Alleen duurt hij negen seconden, elke keer, en die staat de gebruiker bij het inloggen uit te zitten.

Wat het zo lastig maakte om te vinden: alles wat je van buitenaf meet is snel. Kerio doet 5 ms, met en zonder inloggen. Maar dat meet je met Windows PowerShell, en dat is .NET Framework 4.8 - een andere implementatie van SmtpClient dan de .NET 8 waar de applicatie op draait, die andere keuzes maakt bij het aanmelden. Appels met peren.

Lessen voor de volgende keer:

  • Controleer het instellingenbestand als eerste, niet als laatste.
  • Windows PowerShell is geen goede proef voor .NET-gedrag. Gebruik pwsh als je het na wilt doen.
  • De tijdmeting in SmtpEmailSender splitst het wachten in "wachten op een vrije draad" en "het SMTP-gesprek zelf". Die twee getallen wezen het aan; laat ze staan.

De applicatie waarschuwt nu bij het opstarten als het SMTP-wachtwoord spaties bevat - een echt wachtwoord doet dat zelden en een voorbeeldzin altijd.

1. De afzender moet bij het account horen

Solcon weigerde elk afzenderadres dat niet bij het inlogaccount hoorde:

Mailbox unavailable. The server response was: Verification failed for <noreply@mijnbesluit.nl>

Dat is met de eigen mailserver verdwenen.

2. Elke mail heeft een Message-ID nodig

550-5.7.1 Messages missing a valid Message-ID header are not accepted

.NET zet die koptekst niet zelf, en niet elke mailserver vult hem aan. SmtpEmailSender zet hem er daarom zelf op; zie Berichtkenmerk in de Application-laag.

3. Het domein moet zeggen wie er namens hem mag versturen

550-5.7.26 Gmail requires all senders to authenticate with either SPF or DKIM

Vandaar de records hierboven.

Wat hier nog niet goed aan is

Kerio heeft geen geldig certificaat. Zelfondertekend, op de verkeerde naam. Daardoor moet de applicatie over loopback zonder TLS praten, krijgt elke mailclient een waarschuwing, en kunnen andere mailservers de TLS-verbinding weigeren. De oplossing is een Let's Encrypt-certificaat voor mail.kwik-bit.nl met win-acme op die server - die kan via IIS op poort 80 valideren, en na afloop een script draaien dat het certificaat in Kerio zet. Kerio's eigen Let's Encrypt-knop werkt hier niet, want die wil poort 80 zelf hebben en die is van IIS.

Daarna kan het Smtp-blok terug naar mail.kwik-bit.nl op poort 587 met UseStartTls: true.

Je ziet niet of een mail aankomt. De applicatie weet alleen of Kerio hem heeft aangenomen. Wordt hij daarna alsnog geweigerd, dan komt die melding als bounce terug op noreply@ en ziet de gebruiker alleen "de code is verstuurd" terwijl er niets aankomt. De DMARC-rapporten naar info@mijnbesluit.nl geven daar wel zicht op, maar pas achteraf en per dag.

p=none in DMARC. Dat is de juiste stand om mee te beginnen: je krijgt rapporten zonder dat er post geweigerd wordt. Gaat het een paar weken goed, dan kan het naar p=quarantine en het SPF-record van ~all naar -all.