Hoe werkt de mail?

Pidgey

De Harambee mail server is naast de website en maarifa een van de grotere nerds-projecten in de recente Harambee Nerds-geschiedenis.

Hij is geboren op een regenachtige nacht op dinsdag 18 april 2017 om 03:37 uur, waar Vincent Smit en Yuri van Midden een flinke nacht doorhaalden om het te fixen en uiteindelijk dropten, in deze commit. De jonge jenever van de firma Zuidam en verschillende Klok bieren vloeiden daarna rijkelijk.

Prominente features van de mail-server:

  • Op IMAP-niveau bij het inloggen de toegangsrechten geven op de gedeelde mailboxen van de commissies waar de persoon deel van uit maakt
  • Mail doorsturen die binnenkomt voor teams, commissies, trainers en bestuurders, naar de privé-mailadressen van de betreffende personen
  • Spam-filter
  • IMAP en SMTP toegang

Maar HoE WeRKt Ie dAn??!

Well, laten we dit monster dat e-mail heet maar eens even gaan uitleggen.

Whale hello there

Hoe werkt mail normaal gesproken?#

E-mail is wel een van de oudere standaarden van het moderne en klassieke world wide web. Aanvankelijk werkte het zo:

  1. Jij stuurt een mail naar bob@alice.com
  2. Jouw SMTP (Simple Mail Transfer Protocol) server maakt een telnet-verbinding (ja echt) naar alice.com, op poort 25
  3. De ontvangende SMTP-server op alice.com zegt: "hoi"
  4. Jouw SMTP-server zegt: "hoi, ik heb mail voor bob, hier is de inhoud"
  5. De server op alice.com zegt: "k thx bye"
  6. Einde.

Tegenwoordig komt hier nog een laag overheen die TLS regelt zodat de verbinding in elk geval beveiligd is, maar in essentie komt het hier nog steeds op neer.

Zodra de server op alice.com de inhoud van de mail heeft ontvangen, slaat hij het op op de server. Het programma dat dit opslaan regelt is Dovecot.

Het programma dat de exchange en communicaties doet met andere servers, is Postfix.

Eindgebruikers kunnen een mail-client (Outlook, Thunderbird, Roundcube, etc.) gebruiken om die mail vervolgens uit te lezen. Die programma's maken verbinding via IMAP (Internet Message Access Protocol), waarna Dovecot de directories en e-mails serveert.

Simple! But wait...#

Nu hoor ik je denken: "maar als elke server maar gewoon kan zeggen: "hoi ik heb mail" en alle ontvangende servers zeggen altijd "k thx bye", komt er dan niet vet veel spam, en kun je dan niet identiteiten forgen enzo omdat je kan zeggen "hoi ik ben president@wittehuis.gov"? Hoe de f werkt dat dan?"

Ja. Ja dat klopt ja.

Daarom zijn er over de vele jaren dat e-mail een ding is talloze pleisters geplakt om e-mail veiliger te maken.

NaamWat is het?
DMARCDMARC is een verificatieprotocol voor e-mail. Het is ontworpen om de beheerders van e-maildomeinen de mogelijkheid te geven hun domein te beschermen tegen ongeoorloofd gebruik, beter bekend als e-mail spoofing.
DKIM (DomainKeys Identified Mail)Verifieert met public key verificatie dat de server garant staat voor de verstuurde mail. Deze keys publiceer je via DNS en zijn voor elke andere server dus te verifiëren.
SPF (Sender Policy Framework)Instrueert wat mailservers moeten doen als de DKIM verificatie faalt: bijvoorbeeld "mail weigeren". Dit wordt ook gepubliceerd via DNS.
PostgreyVertraagt inkomende mails van verzenders die nog niet eerder zijn gezien. Dit noem je greylisten, en doordat het lang duurt voordat mail afgeleverd wordt, wordt gehoopt dat spammers niet vaker proberen attempts te doen.
SRS (Sender Rewriting Scheme)Als inkomende mail van bijvoorbeeld @nevobo.nl binnenkomt, en die wordt hoor de Harambee server doorgestuurd naar jouw @hotmail.com, stuurt de server verantwoordelijk voor @harambee.nl in feite mails namens @volleybal.nl. Jouw @hotmail.com server ziet dat, en klassificeert dit waarschijnlijk als spam. Daarvoor is het Sender Rewriting Scheme, die de afzender van de mail verandert naar een @harambee.nl adres, zodat mails wél geaccepteerd worden. Door middel van wat trucjes, werkt antwoorden op deze gemanipuleerde afzender nog wel.
SpamAssassinSpamfilter dat met machine learning spam klassificeert
ClamAVAntivirus

Goed. Dit is wat theorie, en wat verder?

De Harambee case#

Op de Univeristeit is poort 25 voor mail submissions geblokeerd. Dat betekent dat in de normale situatie de Harambee-server geen mail kan ontvangen.

VDX#

Gelukkig hebben we hier een omweg voor gevonden middels VDX. Zij ontvangen onze mail op poort 25 en stuurt het vervolgens via poort 2525 door naar de server op harambee.utwente.nl. Die poort is in onze docker configuratie gemapt op poort 25 in de container, dus alles werkt zoals het zou moeten.

Dus:

Mail voor @harambee.nl ---|pt:25|---> VDX ---|pt:2525|---> harambee.utwente.nl

Afwijkende situatie tov normale mailservers#

Harambee heeft talloze mailadressen. Bijvoorbeeld voor teams, commissies, oud-besturen, noem maar op. De mail die binnenkomt, moet doorgestuurd worden naar de privé-mailadressen van de betreffende personen, maar ook nog opgeslagen worden op de server zelf. Zo kunnen commissieleden de mailboxen van hun commissies inzien.

Als je inlogt wil je daarom alle gedeelde mailboxen van de commissies waar jij deel van uitmaakt inzien. Maar als jij een mail beantwoordt, wil je dat de commissiegenoten ook kunnen zien dat jij dat hebt gedaan. Maar als jij een mail opent en op 'gelezen' zet, moet deze mail voor je commissiegenoten nog wel op 'ongelezen' staan. Hoe dan?

Begin bij 't begin#

De mail-server draait op de docker image van Thomas Vial. Deze full-stack mail-server image heeft veel basisfeatures zoals Postfix en Dovecot, maar ook antivirus, spamassassing, en alle technieken die nodig zijn om uitgaande mail vanuit Harambee zo weinig mogelijk in de spam te laten belanden.

LDAP#

Voor het doorsturen van e-mails hebben we voornamelijk een Harambee adresboek nodig. Dit wordt geregeld met LDAP en heeft versimpeld de volgende structuur:

|- teams
| |- heren 1 [heren1_1920@harambee.nl, mailbox: no] [members: Yuri, Sander, ...]
| |- heren 2 [heren2_1920@harambee.nl, mailbox: no] [members: Peter, Rob, ...]
| |- etc...
|
|- commissies
| |- bestuur [bestuur@harambee.nl, mailbox: yes] [members: Vincent, Rob, ...]
| |- nerds [nerds@harambee.nl, mailbox: yes] [members: Peter, Yuri, ...]
| |- etc...
|
|- personen
| |- Peter vd Slot
| | email: peter@outlook.com
| | alias: peter.v.d.slot@harambee.nl
| | maildrop: peter@outlook.com
| | memberOf: [nerds, h2]
| |
| |- Sander Oosterveld
| | email: sanders@gmail.com
| | alias: sander.oosterveld@harambee.nl
| | maildrop: sanders@gmail.com
| | memberOf: [nerds, h1]
| |
| |- Rob Warnaar
| | email: rob@hotmail.com
| | alias: rob.warnaar@harambee.nl
| | maildrop: rob@hotmail.com
| | memberOf: [bestuur, h2]
| |
| |- Vincent Smit
| | email: vistent@yahoo.com
| | alias: vincent.smit@harambee.nl
| | maildrop: vistent@yahoo.com
| | memberOf: [nerds]
| |
| |- Yuri van Midden
| | email: admin@yuri.nl
| | alias: yuri.van.midden@harambee.nl
| | maildrop: admin@yuri.nl
| | memberOf: [nerds, h1]
| |
| |- etc...
|
|- etc...

Hierin kunnen we zien dat commissies personen hebben, handig voor het doorsturen van mail. We zien ook dat een persoon memberOf verschillende groepen is, handig voor de toegangsrechten tot mailboxen.

Ten slotte zien we dat groepen wel of niet een mailbox kunnen hebben, daarmee bepalen we of we de mail die binnenkomt op dat adres bewaren.

In de docker-compose.yml zien we de volgende config, die in de basis weergeeft hoe onze server omgaat met authenticatie:

- LDAP_QUERY_FILTER_USER=(&(mail=%s)(employeeType=active))
- LDAP_QUERY_FILTER_GROUP=(&(mail=%s)(objectClass=mailAccount))
# Alias: ontvang op x@harambee.nl, stuur door naar x@outlook.com.
# Er wordt gequeryt op het _mail_-veld in LDAP, en doorgestuurd naar het _mailDrop_ veld
- LDAP_QUERY_FILTER_ALIAS=(&(|(mail=%s)(mailalias=%s))(objectClass=mailAccount))
- LDAP_QUERY_FILTER_DOMAIN=(&(mailbox=*@%s)(objectClass=mailAccount))
# Wie mogen inloggen op de Dovecot server?
- DOVECOT_USER_FILTER=(&(objectClass=inetOrgPerson)(|(mail=%u)(mailAlias=%u))(employeeType=active))
- DOVECOT_PASS_FILTER=(&(objectClass=inetOrgPerson)(|(mail=%u)(mailAlias=%u))(employeeType=active))
- DOVECOT_USER_ATTRS=
# Mapping van LDAP-attributen naar Dovecot-attributen
- DOVECOT_PASS_ATTRS=mail=user,userPassword=password

Postfix#

Verder zien we in Postfix de volgende config in ldap-aliases.cf (in de Docker repository te vinden):

query_filter = (&(|(mail=%s)(mailalias=%s))(objectClass=mailAccount))
result_attribute = mailbox, maildrop

De inkomende mailadressen worden gequeryt naar LDAP. Als een LDAP-object gevonden wordt, wordt uit dat object het mailbox en maildrop teruggegeven als doorstuuradres. Bijvoorbeeld:

  1. Mail komt binnen voor nerds@harambee.nl
  2. Het volgende LDAP-object wordt gevonden, door bovenstaande filter:
    ℹ️ OBJECT
    cn = NerdCie
    mail = nerds@harambee.nl
    mailbox = nerds@harambee.nl
    maildrop = peter@outlook.com,admin@yuri.nl,...
    members = Peter,Yuri,...
  3. De result attribute mailbox en maildrop worden gepopulate met
  4. De server stuurt de mails door naar deze adressen.

Waarom ook nog een keer naar nerds@harambee.nl sturen? Dit is nodig om Dovecot aan het werk te zetten om de mail op te slaan.

Als je even scrollt naar het LDAP-structuurvoorbeeld hierboven, zie je dat dit ook werkt voor persoonlijke Harambee-mailaliassen, bijvoorbeeld: rob.warnaar@harambee.nl resolvet in de maildrop naar rob@warnaar.nl.

Hoe werkt dit met uitgaande mail? Precies hetzelfde! Als je vanaf Harambee een mail stuurt naar een Harambee-adres, bombardeer je hem in feite op hetzelfde systeem af.

Verder laten we de standaard-config van Postfix in de docker image nagenoeg ongemoeid, omdat die goed genoeg is.

Dovecot#

Nu we met Postfix mail kunnen ontvangen en sturen, moeten we een manier vinden om de e-mails op te slaan. Dat gaat als volgt:

  1. Postfix stuurt de inkomende mail naar Dovecot.
  2. Dovecot slaat het op in /var/mail/public/nerds@harambee.nl/Maildir

Dat is best eenvoudig. Wat minder eenvoudig is, is mensen deze mailboxen te laten inzien. Als je inlogt ben je als user bijvoorbeeld peter@outlook.com, wat niet correspondeert met nerds@harambee.nl. Daar moeten we iets op verzinnen.

Namespaces#

Dovecot heeft by default de standaard mappen voor een mailbox wel geregeld voor gebruikers. Dat zijn bijvoorbeeld Inbox, Sent, Junk. Echter bij Harambee willen we wat meer mappen, voor onder andere de commissies. Dat regelen we met Dovecot Namespaces.

Een namespace ziet er bijvoorbeeld als volgt uit:

namespace nerds@harambee.nl {
type = public
separator = /
prefix = Harambeemail/Nerdcie/
location = maildir:/var/mail/public/nerds@harambee.nl/Maildir:INDEXPVT=~/Maildir/public/nerds@harambee.nl
ignore_on_failure = yes
subscriptions = no
mailbox Spam {
auto = create # autocreate Spam, but don't autosubscribe
special_use = \Junk
autoexpunge = 14d
}
}

Hier zien we dat een Namespace van een commissie de volgende eigenschappen heeft:

  1. public - wat betekent dat het een publieke map is, dus voor ALLE gebruikers beschikbaar

  2. prefix = Harambeemail/Nerdcie - de map komt voor mailprogramma's beschikbaar in:

    Harambeemail > Nerdcie

  3. location = maildir:/var/mail/public/nerds@harambee.nl/Maildir:INDEXPVT=~/Maildir/public/nerds@harambee.nl - de meest interessante regel: de opslagdirectory van de binnengekomen mail is hier aangegeven (dus waar de server de mails die al zijn opgeslagen kan vinden).

    Verder zien we INDEXPVT=~/Maildir/public/nerds@harambee.nl staan. Dit geeft aan de Gelezen IMAP-flag opgeslagen wordt op de volgende locatie:

    de Home-directory van de user (~) > Maildir > public > nerds@harambee.nl.

    Hierdoor worden de mails die geopend worden door commissie-lid A niet als Gelezen bestempeld voor commissiegenoot B.

    Dit gebeurt alléén voor de Gelezen-flag, flags zoals Beantwoord, Doorgestuurd, etcetera. worden wél centraal opgeslagen. Als commissielid A dus op een mail reageert is dat te zien voor commissielid B.

  4. subscriptions = no - deze namespace is niet verantwoordelijk voor het bijhouden van de IMAP-abonnementen van een user.

  5. mailbox Spam - self explanatory.

Nu we weten hoe namespaces eruit zien, willen we ze graag genereren voor alle LDAP-objecten die het attribuut mailbox hebben. We willen immers mailboxen voor alle commissies en commissiefuncties die dat vereisen en dergelijke. Dat kan Dovecot helaas niet zelf voor ons regelen, dus gebruiken we daar Python voor.

Het Python-script dat gebruikt wordt voor het genereren van de namespaces is te vinden in de Docker repository in generate-namespaces.py. Daarin gebeurt nog iets, namelijk het genereren van Access Control List regels voor de mailboxen.

ACL trucje#

Hiervoor werd benoemd dat de public namespaces beschikbaar waren voor ALLE Dovecot users. Dat zou betekenen dat elke ingelogde gebruiker toegang kreeg tot alle mailboxen. Dat moeten we voorkomen.

In het voorgenoemde Python script wordt daarom een file met de naam dovecot-acl gedropt in de map van de publieke mailbox met de volgende inhoud:

group=<mailbox> lrwsipek\n

Dit betekent essentially dat de ingelogde Dovecot user die group-name MOET hebben in zijn environment variabelen (komt later terug). Als dat zo is krijgt de user de volgende rechten: lookup, read, write, insert, post, expunge, create

Pfieuw, mailbox veilig gesteld voor pottenkijkersoogjes.

Script: generate-namespaces.py#

Dit script doet de volgende dingen:

  1. Maak een lege file.
  2. Vul met een default namespace die voor alle users hetzelfde is.
  3. Query alle LDAP objecten met een mailbox attribuut.
  4. Maak voor elk object een namespace (zie template hierboven) en voeg toe in de file.
  5. Voor elke gegenereerde namespace: plaats de ACL-file in de publieke mailbox map.
  6. Sla de namespaces file op.
  7. Herlaad Dovecot om toe te passen.

Dit script wordt elk uur gerund in de mail-container, zodat nieuwe commissies of functionarissen snel een mailbox krijgen.

De inner workings van dit script is ook gedocumenteerd in het script zelf.

--

PAUZE! Als je het al tot hier volhoudt: lekker bezig 🥳🥳💃

Episch!

En dooooooorrrr

Gebruikers laten inloggen enzo#

Nu hebben we de namespaces die we nodig hebben om een vereniging te runnen, nu nog de mensen de toegang verschaffen.

Inloggen werkt als volgt:

  1. Een user logt in. De credentials worden aan de LDAP getoetst.
  2. De user krijgt te groepen toegewezen waar hij deel van uitmaakt volgens het memberOf-attribuut van LDAP.
  3. De user kan zich abonneren op de commissiemappen waar diegene toegang tot heeft (dit gaat helaas nog niet vanzelf).

Technisch werkt het iets ingewikkelder.

Een user in Dovecot heeft net als een Linux-user een bepaalde environment. Daarin zit bijvoorbeeld opgeslagen wat de home-directory is, of in welke ACL groups een user zit. Door deze gegevens aan te passen kunnen we de e-mailomgeving van de Dovecot-gebruiker manipuleren.

Dovecot kan pre-login scripts runnen. Dat houdt min of meer in dat zodra een gebruiker inlogt een script wordt gerund, wederom Python to the rescue!

Script: get-user-groups-acl.py#

Laten we inloggen als user rob@warnaar.nl. Het script doet het volgende:

  1. Query de USER os environment variabele.

    USER = rob@warnaar.nl

  2. Query de LDAP voor mail=user.

    USER = rob@warnaar.nl

    ℹ️ LDAP OBJECT
    cn = Rob Warnaar
    mail = rob@warnaar.nl
    employeeType = active
    memberOf = bestuur, nerds
  3. Query de mailadressen van de objecten in memberOf. Dit gaat basically door deze objecten als search-query te doen in LDAP (dat kan) en als resultaat-attribuut mailbox te vragen.

    USER = rob@warnaar.nl

    memberOf = bestuur, nerds --> uitgebreid tot: bestuur@harambee.nl, nerds@harambee.nl

  4. Sla deze mailadressen op in de ACL_GROUPS environment variabele in een comma-separated list.

    USER = rob@warnaar.nl

    ACL_GROUPS = bestuur@harambee.nl,nerds@harambee.nl

  5. Definieer nog even de home-directory van onze Dovecot user, zodat de Gelezen vlaggetjes opgeslagen kunnen worden.

    USER = rob@warnaar.nl

    ACL_GROUPS = bestuur@harambee.nl,nerds@harambee.nl

    HOME = /var/mail/private/rob@warnaar.nl

  6. Beëindig het script en call de Dovecot IMAP handler.

Met deze environment variabelen gevuld kan de user inloggen en zich abbonneren op de mappen die in zijn ACL_GROUPS staan.

Dit in feite alles wat nodig is om de Harambee-mail te laten werken.

De docker image#

De image draait verder met de volgende configuratie, gekopiëerd uit de docker-compose.yml van 19-02-2022:

#
# Mailserver
#
harambee_mail:
build: ./images/mail
restart: unless-stopped
hostname: mail
domainname: ${MAIL_DOMAIN}
container_name: harambee_mail
ports:
- "${MAIL_DELIVERY_PORT}:25" # Inkomende mail poort
- "143:143" # IMAP poort
- "465:465" # IMAP poort
- "587:587" # Submission poort
- "993:993" # Submission poort
- "4190:4190" # Sieve (e-mail sorteer regels) admin
volumes:
- ${DATA_TANK}/mail:/var/mail # De opslaglocatie van de e-mail
- mailstate:/var/mail-state
- ./config/mail:/tmp/docker-mailserver/
- /etc/localtime:/etc/localtime:ro
- ${PROXY_SSL_CERT_PATH}/star_harambee_utwente_nl.crt:/tmp/ssl/cert/public.crt:ro
- ${PROXY_SSL_KEY_PATH}/ssl-cert-harambee.key:/tmp/ssl/private/private.key:ro
- ./images/mail/config/policyd-spf.conf:/etc/postfix-policyd-spf-python/policyd-spf.conf:ro
environment:
- TZ=Europe/Amsterdam
- DMS_DEBUG=1
- ONE_DIR=1
- PERMIT_DOCKER=network
- POSTFIX_INET_PROTOCOLS=ipv4
- ENABLE_AMAVIS=1
- ENABLE_SPAMASSASSIN=1
- ENABLE_CLAMAV=1
- ENABLE_FAIL2BAN=1
- ENABLE_POSTGREY=1
- ENABLE_MANAGESIEVE=1
- ENABLE_LDAP=1
- ENABLE_UPDATE_CHECK=1
- UPDATE_CHECK_INTERVAL=7d
- ENABLE_SRS=1
- SRS_SECRET=${MAIL_SRS_SECRET}
- SPOOF_PROTECTION=1
- POSTFIX_MESSAGE_SIZE_LIMIT=40960000
- OVERRRIDE_HOSTNAME=harambee.nl
- POSTMASTER_ADDRESS=nerds@harambee.nl
- SPAMASSASSIN_SPAM_TO_INBOX=1
- MOVE_SPAM_TO_JUNK=1
- SA_SPAM_SUBJECT=*SPAM*
- LDAP_SERVER_HOST=harambee_ldap
- LDAP_SEARCH_BASE=dc=harambee,dc=utwente,dc=nl
- LDAP_BIND_DN=${MAIL_LDAP_BIND_DN}
- LDAP_BIND_PW=${MAIL_LDAP_BIND_PASSWORD}
- LDAP_QUERY_FILTER_USER=(&(mail=%s)(employeeType=active))
- LDAP_QUERY_FILTER_GROUP=(&(mail=%s)(objectClass=mailAccount))
- LDAP_QUERY_FILTER_ALIAS=(&(|(mail=%s)(mailalias=%s))(objectClass=mailAccount))
- LDAP_QUERY_FILTER_DOMAIN=(&(mailbox=*@%s)(objectClass=mailAccount))
- DOVECOT_USER_FILTER=(&(objectClass=inetOrgPerson)(|(mail=%u)(mailAlias=%u))(employeeType=active))
- DOVECOT_PASS_FILTER=(&(objectClass=inetOrgPerson)(|(mail=%u)(mailAlias=%u))(employeeType=active))
- DOVECOT_USER_ATTRS=
- DOVECOT_PASS_ATTRS=mail=user,userPassword=password
- TLS_LEVEL=modern # >=TLSv1.2
- SSL_TYPE=manual
- SSL_CERT_PATH=/tmp/ssl/cert/public.crt
- SSL_KEY_PATH=/tmp/ssl/private/private.key
cap_add:
- NET_ADMIN
- SYS_PTRACE
networks:
- web
- public
- mail
depends_on:
- harambee_ldap

Autodiscover#

Omdat de mailserver verder een gewone mailserver is die via IMAP te bereiken is, kan je er ook mee verbinden met je eigen mail-client. Hiervoor is een autodiscovery container opgetuigd, die de meest gangbare mail-clients van informatie kan voorzien.

Deze is te bereiken op https://autoconfig.harambee.utwente.nl.

Mail-clients doen een aanvraag bij de auto-discovery service van het domein van het mailadres waarmee wordt ingelogd. Het zou dus logisch zijn dat rob@warnaar.nl niet bij onze mailserver uitkomt. Dat is ook terecht.

Waarom hebben we dit dan toch? Omdat je op de mailserver óók kan inloggen met je Harambee-alias! 🤯🤯 rob.warnaar@harambee.nl komt namelijk wél uit op onze server, drops mic🎤.

Roundcube#

Roundcube is een mail-client die server-side draait in PHP. Omdat het een ordinaire mail-client is wordt die ook zo geconfigureerd. Hij maakt namelijk by default verbinding met de mail-server die je ook gebruikt voor bijvoorbeeld Outlook: harambee.utwente.nl via STARTTLS. De authenticatie gaat ook via IMAP, dus niet direct via LDAP.

Een toevoeging die is gedaan aan Roundcube is dat de LDAP directory is toegevoegd als adresboek. Besturen en commissieleden kunnen dus makkelijk mailen naar personen en andere commissies. In die adresboeken is alleen de persoonlijke Harambee-alias meegenomen. Bestuurders of commissieleden kunnen dus alleen die alias zien, en nooit iemand's privé mailadres.