E-mailadressen verifiëren in Python
Een e-mailadres verifiëren in Python is eenvoudig voor de makkelijke delen en verrassend lastig voor het deel dat het meest telt. Deze gids loopt door syntaxchecks en DNS-lookups die je lokaal kunt uitvoeren, legt precies uit waarom ruwe SMTP-probes in de praktijk mislukken en laat zien hoe je met een REST API een resultaat op mailboxniveau krijgt.
Gebruik Python om onjuist opgemaakte invoer te weigeren en eventueel DNS/MX-checks te cachen. Gebruik een verificatie-API voor het resultaat op mailboxniveau, want ruwe smtplib probes worden makkelijk afgeknepen en geven vaak dubbelzinnige antwoorden.
Wat het "verifiëren" van een e-mail echt betekent
E-mailverificatie is het proces waarmee je bevestigt dat een adres echt bestaat en mail kan ontvangen voordat je er iets naartoe stuurt. Het bestaat uit een reeks checks, elk moeilijker dan de vorige: is het adres correct opgemaakt, accepteert het domein überhaupt mail, en bestaat de specifieke mailbox. In Python zijn de eerste twee goed te doen. Bij de derde gaan de meeste zelfgebouwde oplossingen ongemerkt onderuit, en begrijpen waarom is het nuttigste in dit artikel.
Het plan hieronder is gelaagd. Voer de goedkope, snelle checks lokaal uit om duidelijk kapotte invoer eruit te gooien, en roep daarna een verificatie-API aan voor het antwoord op mailboxniveau over of het adres bruikbaar is. Zo weiger je onzin direct zonder API-call, en krijg je een duidelijker resultaat voor alles wat overblijft.
Stap 1: syntaxvalidatie
De eerste check bevestigt dat het adres correct is opgebouwd. Het is verleidelijk om hiervoor een reguliere expressie te schrijven, maar e-mailsyntax is veel ingewikkelder dan hij lijkt, en zelfgeschreven regexes zijn bijna altijd te streng of te los. De standaardbibliotheek hiervoor is email-validator, die de relevante standaarden correct implementeert en veel in productie wordt gebruikt.
Installeer hem met pip:
pip install email-validator
Valideer daarna een adres. De bibliotheek gooit EmailNotValidError bij alles wat onjuist is opgemaakt en geeft voor geldige invoer een genormaliseerd object terug:
from email_validator import validate_email, EmailNotValidError
def check_syntax(address):
try:
result = validate_email(address, check_deliverability=False)
return True, result.normalized
except EmailNotValidError as error:
return False, str(error)
ok, value = check_syntax("jane.doe@example.com")
print(ok, value) # True jane.doe@example.com
ok, value = check_syntax("jane.doe@@example")
print(ok, value) # False, with a description of the problem
Door check_deliverability=False in te stellen blijft deze stap pure syntax zonder netwerkcalls, dus snel genoeg om bij elke toetsaanslag of formulierverzending uit te voeren. Sla altijd de normalized waarde op die de bibliotheek teruggeeft in plaats van de ruwe invoer van de gebruiker, omdat die het adres canoniek maakt voor consistente opslag.
Stap 2: domein- en MX-checks
Een correct opgemaakt adres is nog steeds nutteloos als het domein geen e-mail kan ontvangen. De volgende check vraagt de DNS-records van het domein op en zoekt naar MX-records, die de mailservers aanwijzen die voor dat domein verantwoordelijk zijn. Een domein zonder MX-records kan geen mail ontvangen, dus elk adres op dat domein kun je direct weigeren.
Het email-validator bibliotheek kan dit voor je doen als je check_deliverability=True meegeeft, waarmee de MX-lookup onderdeel wordt van de validatie:
from email_validator import validate_email, EmailNotValidError
def check_domain(address):
try:
result = validate_email(address, check_deliverability=True)
return True, result.domain
except EmailNotValidError as error:
return False, str(error)
Wil je de DNS-query zelf uitvoeren, bijvoorbeeld om resultaten te cachen voor veel adressen op hetzelfde domein, gebruik dan direct de dnspython bibliotheek:
pip install dnspython
import dns.exception
import dns.resolver
def has_mx_record(domain):
try:
records = dns.resolver.resolve(domain, "MX")
return len(records) > 0
except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN):
return False
except dns.exception.DNSException:
return False # Treat resolver errors as inconclusive.
print(has_mx_record("gmail.com")) # True
print(has_mx_record("no-such-domain-xyz.invalid")) # False
Dit is echt nuttig. Het verwijdert adressen op domeinen die verlopen zijn, nooit hebben bestaan of simpelweg geen mail verwerken. Maar let op wat het je nog steeds niet vertelt: of someone@gmail.com een echte mailbox is. Gmail heeft MX-records, dus elk Gmail-adres komt door deze check, geldig of niet.
Stap 3: de mailboxcheck, en waarom ruwe SMTP tekortschiet
Om de mailbox zelf te bevestigen, moet je met de ontvangende mailserver praten. In principe maak je verbinding via SMTP, identificeer je jezelf, geef je een RCPT TO commando met het doeladres en lees je de responscode: een positieve code suggereert dat de mailbox bestaat, een weigering dat hij niet bestaat. Python's ingebouwde smtplib kan dit, en de code ziet er bedrieglijk eenvoudig uit:
import smtplib
import dns.resolver
def naive_smtp_check(address):
domain = address.split("@")[1]
mx_host = str(dns.resolver.resolve(domain, "MX")[0].exchange)
server = smtplib.SMTP(timeout=10)
server.connect(mx_host)
server.helo("example.com")
server.mail("probe@example.com")
code, message = server.rcpt(address)
server.quit()
# 250 and 251 generally mean the mailbox was accepted.
return code in (250, 251)
Dit werkt in een tutorial en faalt in productie. De redenen zijn de moeite waard om in detail te kennen, omdat ze verklaren waarom geen enkele serieuze verifier op zo'n script vertrouwt.
IP-reputatie en throttling
Mailboxproviders herkennen verificatieprobes actief. Voer een handvol RCPT TO checks uit vanaf één server en providers zoals Gmail en Outlook knijpen dat IP binnen een paar pogingen af of blokkeren het. Eenmaal afgeknepen geeft de server dubbelzinnige antwoorden, of de mailbox nu bestaat of niet, waardoor je resultaten betekenisloos worden. Precies daarom gebeurt echte verificatie vanaf grote pools van goed opgewarmde, goed beheerde IP-adressen.
Greylisting
Veel servers gebruiken greylisting en geven bewust een tijdelijke 4xx fout terug de eerste keer dat een onbekende afzender contact maakt. Legitieme mailservers proberen het later opnieuw; spammers meestal niet. Een naïef script dat niet opnieuw probeert, leest die tijdelijke fout als weigering en markeert een prima mailbox als ongeldig. Dat is een fout-negatief dat puur door ongeduld ontstaat.
Catch-all-domeinen
Een catch-all-domein accepteert mail voor elk mogelijk adres, dus RCPT TO geeft een positieve code voor realperson@domain.com en de random-string-9f3a@domain.com zonder onderscheid. Een ruwe SMTP-check kan op deze domeinen een echte mailbox niet onderscheiden van een niet-bestaande. Om een catch-all te detecteren, stuur je een tweede probe naar een adres dat zeker niet bestaat en kijk je of ook dat wordt geaccepteerd. Dat is catch-all-detectie, geen simpel ja of nee.
Dubbelzinnige antwoorden van grote providers
Sommige grote providers hebben hun RCPT TO gedrag bewust dubbelzinnig gemaakt. Bepaalde providers accepteren elk adres tijdens de handshake en bouncen pas later, dus de SMTP-response bevat simpelweg niet de informatie waar je om vraagt.
Daarom laten de beheerders van email-validator SMTP-checks bewust weg uit de bibliotheek. Hun documentatie zegt het ronduit: het heeft geen zin om zelf contact te leggen met een SMTP-server, omdat servers er goed in zijn niet te verraden of een adres afleverbaar is, en een adres dat mail lijkt te accepteren kan daarna alsnog bouncen. De eerlijke conclusie: syntax- en MX-checks kun je goed in je eigen code doen, een betrouwbare mailboxcheck niet.
Stap 4: verifiëren met de VeriMails API
De praktische oplossing is de mailboxcheck over te laten aan een verificatiedienst met infrastructuur die daarvoor gebouwd is, en die dienst vanuit Python aan te roepen met één HTTP-request. VeriMails biedt een REST API die de volledige pipeline uitvoert: syntax, MX, DNS, een live SMTP-handshake, catch-all-detectie, wegwerpdetectie en rolgebaseerde detectie, en één gestructureerd resultaat teruggeeft.
Met de standaard requests bibliotheek ziet een verificatiecall er zo uit. Voor details over het endpoint, zie de VeriMails API-documentatie:
import requests
def verify_email(address, api_key):
response = requests.get(
"https://api.verimails.com/v1/verify",
params={"email": address},
headers={"Authorization": f"Bearer {api_key}"},
timeout=10,
)
response.raise_for_status()
return response.json()
result = verify_email("jane.doe@example.com", api_key="YOUR_API_KEY")
print(result["status"]) # e.g. "deliverable", "undeliverable", "catch_all"
print(result["disposable"]) # True if a throwaway address
print(result["role_based"]) # True for addresses such as info@ or support@
Daarna kun je op het resultaat handelen. Een aanmeldformulier kan deliverable adressen accepteren, undeliverable en de disposable adressen weigeren en een eigen beleid bepalen voor catch_all:
def is_acceptable_for_signup(address, api_key):
result = verify_email(address, api_key)
if result["disposable"]:
return False, "Disposable addresses are not allowed."
if result["status"] == "undeliverable":
return False, "That address does not appear to exist."
return True, "Address verified."
Omdat elke call een duidelijk resultaat geeft, is het snel genoeg om binnen een formulierverzending uit te voeren zonder de gebruiker te laten wachten. Voor een bestaande lijst adressen die al in je database staat, past bulk-CSV-verificatie beter, omdat één bestand uploaden veel efficiënter is dan het endpoint voor losse adressen duizenden keren in een lus aanroepen.
Het aanbevolen patroon voor een Python-applicatie combineert beide lagen: houd de lokale email-validator syntaxcheck om onzin direct te weigeren voordat je een credit uitgeeft, en roep daarna de API aan voor het gezaghebbende deliverability-resultaat op alles wat erdoor komt. Zo krijg je snelle feedback op duidelijke fouten en een betrouwbaar oordeel over echte adressen.
Beleidstabel voor applicaties
| Resultaat | Beleid bij aanmelding | Beleid bij import of CRM |
|---|---|---|
deliverable | Accepteer en sla het genormaliseerde adres op | Houd in het actieve segment |
undeliverable | Weiger met een duidelijke melding om het te corrigeren | Onderdruk vóór elke campagne |
catch_all | Accepteer of beoordeel op basis van het accountrisico | Segmenteer apart en verstuur voorzichtig |
disposable | Weiger bij de meeste producten | Onderdruk in outreach-lijsten |
role_based | Sta alleen toe als gedeelde inboxen acceptabel zijn | Beoordeel vóór cold outreach |
API-calls versus lijstjobs
Gebruik de API waar een directe beslissing telt: aanmelding, checkout, leadformulieren, uitnodigingen en accountwijzigingen. Gebruik bulkverificatie voor een CSV-export, CRM-opschoning of campagnepubliek waar duizenden adressen dezelfde beoordeling nodig hebben. Beide workflows moeten dezelfde suppressieregels voeden, zodat een adres dat in de ene route is geweigerd niet later vanuit een andere tool wordt gemaild.
Alles bij elkaar
Een nette implementatie heeft drie lagen. Ten eerste een email-validator syntaxcheck met check_deliverability=False, lokaal en direct uitgevoerd om onjuist opgemaakte invoer te weigeren. Ten tweede een optionele MX-check met dnspython als je dode domeinen wilt filteren voordat je de API aanroept, al dekt de API dit ook af. Ten derde een call naar de VeriMails API voor het resultaat op mailboxniveau, inclusief catch-all-, wegwerp- en rolgebaseerde detectie die je zelf niet betrouwbaar kunt nabootsen.
Onthoud vooral de taakverdeling. Syntax en DNS kun je echt zelf in Python afhandelen. De live mailboxcheck hangt af van IP-reputatie, greylisting-gedrag en catch-all-detectie die een enkel script niet aankan, en daarom laat de standaard validatiebibliotheek SMTP-probes bewust weg. Gebruik lokale checks voor snelheid, een API voor verificatie op mailboxniveau, en de prijspagina om het plan te kiezen dat bij je verwachte volume past.
Veelgestelde vragen
Gerelateerde artikelen
Begin met schone data
100 gratis credits bij aanmelding. Geen creditcard nodig. Breng het advies vandaag nog in de praktijk.
Gratis starten