229 lines
9.7 KiB
Markdown
229 lines
9.7 KiB
Markdown
# 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:
|
|
|
|
```json
|
|
"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`:
|
|
|
|
```powershell
|
|
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:
|
|
|
|
```powershell
|
|
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:
|
|
|
|
```powershell
|
|
$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:
|
|
|
|
```powershell
|
|
# 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:
|
|
|
|
```json
|
|
"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`.
|