Lær e-mailautomatisering med Pythons imaplib. Guide til tilslutning til IMAP-servere, søgning, hentning, parsing af e-mails, vedhæftede filer og postkassestyring.
Python IMAP-klient: En omfattende guide til e-mailhentning og postkassestyring
E-mail forbliver en hjørnesten i digital kommunikation for virksomheder og enkeltpersoner verden over. Men at administrere en stor mængde e-mails kan være en tidskrævende og gentagende opgave. Fra behandling af fakturaer og filtrering af meddelelser til arkivering af vigtige samtaler kan den manuelle indsats hurtigt blive overvældende. Det er her, programmeret automatisering skinner igennem, og Python leverer med sit rige standardbibliotek kraftfulde værktøjer til at tage kontrol over din indbakke.
Denne omfattende guide vil føre dig gennem processen med at opbygge en Python IMAP-klient fra bunden ved hjælp af det indbyggede imaplib
-bibliotek. Du lærer ikke kun, hvordan du henter e-mails, men også hvordan du parser deres indhold, downloader vedhæftede filer og administrerer din postkasse ved at markere meddelelser som læst, flytte dem eller slette dem. Ved slutningen af denne artikel vil du være udstyret til at automatisere dine mest kedelige e-mailopgaver, hvilket sparer dig tid og øger din produktivitet.
Forståelse af protokollerne: IMAP vs. POP3 vs. SMTP
Før du dykker ned i koden, er det vigtigt at forstå de grundlæggende protokoller, der styrer e-mail. Du vil ofte høre tre akronymer: SMTP, POP3 og IMAP. De tjener hver især et særskilt formål.
- SMTP (Simple Mail Transfer Protocol): Dette er protokollen for afsendelse af e-mail. Tænk på SMTP som postvæsenet, der henter dit brev og leverer det til modtagerens postkasseserver. Når dit Python-script sender en e-mail, bruger det SMTP.
- POP3 (Post Office Protocol 3): Dette er en protokol til hentning af e-mail. POP3 er designet til at forbinde til en server, downloade alle nye meddelelser til din lokale klient og derefter, som standard, slette dem fra serveren. Det er som at gå på posthuset, hente al din post og tage den med hjem; når den er hjemme hos dig, er den ikke længere på posthuset. Denne model er mindre almindelig i dag på grund af dens begrænsninger i en verden med flere enheder.
- IMAP (Internet Message Access Protocol): Dette er den moderne protokol til adgang til og administration af e-mail. I modsætning til POP3 efterlader IMAP meddelelserne på serveren og synkroniserer status (læst, ulæst, markeret, slettet) på tværs af alle tilsluttede klienter. Når du læser en e-mail på din telefon, vises den som læst på din bærbare computer. Denne servercentrerede model er perfekt til automatisering, fordi dit script kan interagere med postkassen som en anden klient, og de ændringer, det foretager, vil blive afspejlet overalt. For denne guide vil vi udelukkende fokusere på IMAP.
Kom godt i gang med Pythons imaplib
Pythons standardbibliotek inkluderer imaplib
, et modul der leverer alle de nødvendige værktøjer til at kommunikere med en IMAP-server. Ingen eksterne pakker er påkrævet for at komme i gang.
Forudsætninger
- Python installeret: Sørg for at have en nyere version af Python (3.6 eller nyere) installeret på dit system.
- En e-mailkonto med IMAP aktiveret: De fleste moderne e-mailudbydere (Gmail, Outlook, Yahoo osv.) understøtter IMAP. Du skal muligvis aktivere det i din kontos indstillinger.
Sikkerhed først: Brug app-adgangskoder, ikke din primære adgangskode
Dette er det mest kritiske trin for sikkerhed. Undlad at hardkode din primære e-mailkontos adgangskode i dit script. Hvis din kode nogensinde kompromitteres, er hele din konto i fare. De fleste store e-mailudbydere, der bruger totrinsgodkendelse (2FA), kræver, at du genererer en "app-adgangskode".
En app-adgangskode er en unik, 16-cifret adgangskode, der giver en specifik applikation tilladelse til at få adgang til din konto uden at skulle bruge din primære adgangskode eller 2FA-koder. Du kan generere en og tilbagekalde den når som helst uden at påvirke din primære adgangskode.
- For Gmail: Gå til dine Google-kontoindstillinger -> Sikkerhed -> Totrinsbekræftelse -> App-adgangskoder.
- For Outlook/Microsoft: Gå til dit Microsoft-kontos sikkerhedsoversigt -> Avancerede sikkerhedsindstillinger -> App-adgangskoder.
- For andre udbydere: Søg i deres dokumentation efter "app-adgangskode" eller "applikationsspecifik adgangskode".
Når den er genereret, skal du behandle denne app-adgangskode som enhver anden legitimationsoplysning. En god praksis er at gemme den i en miljøvariabel eller et sikkert hemmelighedsstyringssystem i stedet for direkte i din kildekode.
Den grundlæggende forbindelse
Lad os skrive vores første stykke kode for at etablere en sikker forbindelse til en IMAP-server, logge ind og derefter logge ud på en ordentlig måde. Vi vil bruge imaplib.IMAP4_SSL
for at sikre, at vores forbindelse er krypteret.
import imaplib
import os
# --- Legitimationsoplysninger ---
# Det er bedst at indlæse disse fra miljøvariabler eller en konfigurationsfil
# Til dette eksempel definerer vi dem her. Erstat med dine oplysninger.
EMAIL_ACCOUNT = "din_e-mail@eksempel.com"
APP_PASSWORD = "din_16-cifrede_app_adgangskode"
IMAP_SERVER = "imap.eksempel.com" # f.eks. "imap.gmail.com"
# --- Forbind til IMAP-serveren ---
# Vi bruger en try...finally blok for at sikre, at vi logger ud på en ordentlig måde
conn = None
try:
# Opret forbindelse med SSL for en sikker forbindelse
conn = imaplib.IMAP4_SSL(IMAP_SERVER)
# Log ind på kontoen
status, messages = conn.login(EMAIL_ACCOUNT, APP_PASSWORD)
if status == 'OK':
print("Succesfuldt logget ind!")
# Vi tilføjer mere logik her senere
else:
print(f"Login mislykkedes: {messages}")
finally:
if conn:
# Log altid ud og luk forbindelsen
conn.logout()
print("Logget ud og forbindelse lukket.")
Dette script etablerer et fundament. Den try...finally
-blokken er afgørende, fordi den garanterer, at conn.logout()
kaldes, hvilket lukker sessionen med serveren, selvom der opstår en fejl under vores operationer.
Navigering i din postkasse
Når du er logget ind, kan du begynde at interagere med postkasserne (ofte kaldet mapper) på din konto.
Visning af alle postkasser
For at se hvilke postkasser der er tilgængelige, kan du bruge metoden conn.list()
. Outputtet kan være lidt rodet, så lidt parsing er nødvendig for at få en ren liste over navne.
# Inde i 'try'-blokken efter et succesfuldt login:
status, mailbox_list = conn.list()
if status == 'OK':
print("Tilgængelige postkasser:")
for mailbox in mailbox_list:
# Den rå postkasseindgang er en byte-streng, der skal afkodes
# Den er ofte formateret som: (\\HasNoChildren) \"/\" \"INBOX\"
# Vi kan udføre en grundlæggende parsing for at rydde den op
parts = mailbox.decode().split(' \"/\" ')
if len(parts) == 2:
mailbox_name = parts[1].strip('"')
print(f"- {mailbox_name}")
Dette vil udskrive en liste som 'INBOX', 'Sendt', '[Gmail]/Spam' osv., afhængigt af din e-mailudbyder.
Valg af en postkasse
Før du kan søge eller hente e-mails, skal du vælge en postkasse at arbejde med. Det mest almindelige valg er 'INBOX'. Metoden conn.select()
gør en postkasse aktiv. Du kan også åbne den i skrivebeskyttet tilstand, hvis du ikke har til hensigt at foretage ændringer (som at markere e-mails som læst).
# Vælg 'INBOX' at arbejde med.
# Brug readonly=True, hvis du ikke ønsker at ændre e-mail-flag (f.eks. fra UNSEEN til SEEN)
status, messages = conn.select('INBOX', readonly=False)
if status == 'OK':
total_messages = int(messages[0])
print(f"INDBAKKE valgt. Samlet antal meddelelser: {total_messages}")
else:
print(f"Kunne ikke vælge INDBAKKE: {messages}")
Når du vælger en postkasse, returnerer serveren det samlede antal meddelelser, den indeholder. Alle efterfølgende kommandoer til søgning og hentning vil gælde for denne valgte postkasse.
Søgning og hentning af e-mails
Dette er kernen i e-mailhentning. Processen involverer to trin: Først søgning efter meddelelser, der matcher specifikke kriterier, for at få deres unikke ID'er, og for det andet hentning af indholdet af disse meddelelser ved hjælp af deres ID'er.
Kraften i `search()`
Metoden search()
er utrolig alsidig. Den returnerer ikke e-mails selv, men snarere en liste over meddelelsessekvensnumre (ID'er), der matcher din forespørgsel. Disse ID'er er specifikke for den aktuelle session og den valgte postkasse.
Her er nogle af de mest almindelige søgekriterier:
'ALL'
: Alle meddelelser i postkassen.'UNSEEN'
: Meddelelser, der endnu ikke er læst.'SEEN'
: Meddelelser, der er læst.'FROM \"sender@example.com\"'
: Meddelelser fra en specifik afsender.'TO \"recipient@example.com\"'
: Meddelelser sendt til en specifik modtager.'SUBJECT \"Din emnelinje\"'
: Meddelelser med et specifikt emne.'BODY \"et søgeord i brødteksten\"'
: Meddelelser, der indeholder en bestemt streng i brødteksten.'SINCE \"01-Jan-2024\"'
: Meddelelser modtaget på eller efter en specifik dato.'BEFORE \"31-Jan-2024\"'
: Meddelelser modtaget før en specifik dato.
Du kan også kombinere kriterier. For eksempel, for at finde alle ulæste e-mails fra en specifik afsender med et bestemt emne, ville du søge efter '(UNSEEN FROM \"alerts@example.com\" SUBJECT \"System Alarm\")'
.
Lad os se det i aktion:
# Søg efter alle ulæste e-mails i INDBAKKEN
status, message_ids = conn.search(None, 'UNSEEN')
if status == 'OK':
# message_ids er en liste af byte-strenge, f.eks. [b'1 2 3']
# Vi skal opdele den i individuelle ID'er
email_id_list = message_ids[0].split()
if email_id_list:
print(f"Fandt {len(email_id_list)} ulæste e-mails.")
else:
print("Ingen ulæste e-mails fundet.")
else:
print("Søgning mislykkedes.")
Hentning af e-mailindhold med `fetch()`
Nu hvor du har meddelelses-ID'erne, kan du bruge metoden fetch()
til at hente de faktiske e-maildata. Du skal specificere, hvilke dele af e-mailen du ønsker.
'RFC822'
: Dette henter hele det rå e-mailindhold, inklusive alle headers og body-dele. Det er den mest almindelige og omfattende mulighed.'BODY[]'
: Et synonym for `RFC822`.'ENVELOPE'
: Henter nøgleheaderinformation som Dato, Emne, Fra, Til og Svar-til. Dette er hurtigere, hvis du kun behøver metadata.'BODY[HEADER]'
: Henter kun headers.
Lad os hente det fulde indhold af den første ulæste e-mail, vi fandt:
if email_id_list:
first_email_id = email_id_list[0]
# Hent e-maildata for det givne ID
# 'RFC822' er en standard, der specificerer formatet for tekstmeddelelser
status, msg_data = conn.fetch(first_email_id, '(RFC822)')
if status == 'OK':
for response_part in msg_data:
# Fetch-kommandoen returnerer en tuple, hvor den anden del er e-mailindholdet
if isinstance(response_part, tuple):
raw_email = response_part[1]
# Nu har vi de rå e-maildata som bytes
# Næste trin er at parse dem
print("Succesfuldt hentet en e-mail.")
# Vi vil behandle \`raw_email\` i næste afsnit
else:
print("Hentning mislykkedes.")
Parsing af e-mailindhold med modulet email
De rå data returneret af fetch()
er en bytestreng formateret i henhold til RFC 822-standarden. Den er ikke let læselig. Pythons indbyggede email
-modul er specielt designet til at parse disse rå meddelelser til en brugervenlig objektstruktur.
Oprettelse af et Message
-objekt
Det første trin er at konvertere den rå bytestreng til et Message
-objekt ved hjælp af email.message_from_bytes()
.
import email
from email.header import decode_header
# Antager at \`raw_email\` indeholder byte-data fra fetch-kommandoen
email_message = email.message_from_bytes(raw_email)
Udpakning af nøgleinformation (Headers)
Når du har Message
-objektet, kan du få adgang til dets headers som en ordbog.
# Hent emne, fra, til og dato
subject = email_message["Subject"]
from_ = email_message["From"]
to_ = email_message["To"]
date_ = email_message["Date"]
# E-mail headers kan indeholde ikke-ASCII-tegn, så vi skal afkode dem
def decode_email_header(header):
decoded_parts = decode_header(header)
header_str = ""
for part, encoding in decoded_parts:
if isinstance(part, bytes):
# Hvis der er en kodning, brug den. Ellers standard til utf-8.
header_str += part.decode(encoding or 'utf-8')
else:
header_str += part
return header_str
subject = decode_email_header(subject)
from_ = decode_email_header(from_)
print(f"Emne: {subject}")
print(f"Fra: {from_}")
Hjælpefunktionen decode_email_header
er vigtig, fordi headers ofte er kodet til at håndtere internationale tegnsæt. Simpel adgang til email_message["Subject"]
kan give dig en streng med forvirrende tegnsammensætninger, hvis du ikke afkoder den korrekt.
Håndtering af e-mailbrødtekster og vedhæftede filer
Moderne e-mails er ofte "multipart", hvilket betyder, at de indeholder forskellige versioner af indholdet (som almindelig tekst og HTML) og kan også inkludere vedhæftede filer. Vi skal gennemgå disse dele for at finde det, vi leder efter.
Metoden msg.is_multipart()
fortæller os, om en e-mail har flere dele, og msg.walk()
giver en nem måde at iterere gennem dem på.
def process_email_body(msg):
body = ""
attachments = []
if msg.is_multipart():
# Iterer gennem e-maildele
for part in msg.walk():
content_type = part.get_content_type()
content_disposition = str(part.get("Content-Disposition"))
try:
# Hent e-mailbrødteksten
if content_type == "text/plain" and "attachment" not in content_disposition:
payload = part.get_payload(decode=True)
charset = part.get_content_charset() or 'utf-8'
body = payload.decode(charset)
# Hent vedhæftede filer
elif "attachment" in content_disposition:
filename = part.get_filename()
if filename:
# Afkod filnavn om nødvendigt
decoded_filename = decode_email_header(filename)
attachments.append({
'filename': decoded_filename,
'data': part.get_payload(decode=True)
})
except Exception as e:
print(f"Fejl under behandling af del: {e}")
else:
# Ikke en multipart-meddelelse, hent blot payload
payload = msg.get_payload(decode=True)
charset = msg.get_content_charset() or 'utf-8'
body = payload.decode(charset)
return body, attachments
# Brug funktionen med vores hentede meddelelse
email_body, email_attachments = process_email_body(email_message)
print("\n--- E-mailbrødtekst ---")
print(email_body)
if email_attachments:
print("\n--- Vedhæftede filer ---")
for att in email_attachments:
print(f"Filnavn: {att['filename']}")
# Eksempel på lagring af en vedhæftet fil
with open(att['filename'], 'wb') as f:
f.write(att['data'])
print(f"Vedhæftet fil gemt: {att['filename']}")
Denne funktion skelner intelligent mellem brødteksten i almindelig tekst og filvedhæftninger ved at inspicere den Content-Type
og Content-Disposition
headers for hver del.
Avanceret postkassestyring
Hentning af e-mails er kun halvdelen af kampen. Ægte automatisering involverer at ændre meddelelsernes status på serveren. Kommandoen store()
er dit primære værktøj til dette.
Markering af e-mails (læst, ulæst, flaget)
Du kan tilføje, fjerne eller erstatte flag på en meddelelse. Det mest almindelige flag er \\Seen
, som styrer læst/ulæst status.
- Marker som læst:
conn.store(msg_id, '+FLAGS', '\\Seen')
- Marker som ulæst:
conn.store(msg_id, '-FLAGS', '\\Seen')
- Flag/Stjernemarker en e-mail:
conn.store(msg_id, '+FLAGS', '\\Flagged')
- Fjern flag fra en e-mail:
conn.store(msg_id, '-FLAGS', '\\Flagged')
Kopiering og flytning af e-mails
Der er ingen direkte "flyt"-kommando i IMAP. Flytning af en e-mail er en totrinsproces:
- Kopier meddelelsen til destinationspostkassen ved hjælp af
conn.copy()
. - Marker den originale meddelelse til sletning ved hjælp af
\\Deleted
-flaget.
# Antager at \`msg_id\` er ID'et på den e-mail, der skal flyttes
# 1. Kopier til postkassen 'Arkiv'
status, _ = conn.copy(msg_id, 'Archive')
if status == 'OK':
print(f"Meddelelse {msg_id.decode()} kopieret til Arkiv.")
# 2. Marker originalen til sletning
conn.store(msg_id, '+FLAGS', '\\Deleted')
print(f"Meddelelse {msg_id.decode()} markeret til sletning.")
Permanent sletning af e-mails
Markering af en meddelelse med \\Deleted
fjerner den ikke øjeblikkeligt. Det skjuler den blot fra visning i de fleste e-mailklienter. For permanent at fjerne alle meddelelser i den aktuelt valgte postkasse, der er markeret til sletning, skal du kalde metoden expunge()
.
Advarsel: expunge()
er irreversibel. Når den er kaldt, er dataene væk for altid.
# Dette vil permanent slette alle meddelelser med \\Deleted-flaget
status, response = conn.expunge()
if status == 'OK':
print(f"{len(response)} meddelelser slettet permanent.")
En afgørende bivirkning af expunge()
er, at den kan omnummerere meddelelses-ID'erne for alle efterfølgende meddelelser i postkassen. Af denne grund er det bedst at identificere alle meddelelser, du ønsker at behandle, udføre dine handlinger (som kopiering og markering til sletning) og derefter kalde expunge()
én gang helt i slutningen af din session.
Sætter det hele sammen: Et praktisk eksempel
Lad os oprette et komplet script, der udfører en virkelighedsnær opgave: Scan indbakken for ulæste e-mails fra "invoices@mycorp.com", download eventuelle PDF-vedhæftninger, og flyt den behandlede e-mail til en postkasse kaldet "Processed-Invoices".
import imaplib
import email
from email.header import decode_header
import os
# --- Konfiguration ---
EMAIL_ACCOUNT = "din_e-mail@eksempel.com"
APP_PASSWORD = "din_16-cifrede_app_adgangskode"
IMAP_SERVER = "imap.gmail.com"
TARGET_SENDER = "invoices@mycorp.com"
DESTINATION_MAILBOX = "Processed-Invoices"
DOWNLOAD_DIR = "invoices"
# Opret download-mappe, hvis den ikke eksisterer
if not os.path.isdir(DOWNLOAD_DIR):
os.mkdir(DOWNLOAD_DIR)
def decode_email_header(header):
# (Samme funktion som defineret tidligere)
decoded_parts = decode_header(header)
header_str = ""
for part, encoding in decoded_parts:
if isinstance(part, bytes):
header_str += part.decode(encoding or 'utf-8')
else:
header_str += part
return header_str
conn = None
try:
# --- Forbind og log ind ---
conn = imaplib.IMAP4_SSL(IMAP_SERVER)
conn.login(EMAIL_ACCOUNT, APP_PASSWORD)
print("Login succesfuld.")
# --- Vælg INDBAKKE ---
conn.select('INBOX')
print("INDBAKKE valgt.")
# --- Søg efter e-mails ---
search_criteria = f'(UNSEEN FROM \"{TARGET_SENDER}\")'
status, message_ids = conn.search(None, search_criteria)
if status != 'OK':
raise Exception("Søgning mislykkedes")
email_id_list = message_ids[0].split()
if not email_id_list:
print("Ingen nye fakturaer fundet.")
else:
print(f"Fandt {len(email_id_list)} nye fakturaer at behandle.")
# --- Behandl hver e-mail ---
for email_id in email_id_list:
print(f"\nBehandler e-mail ID: {email_id.decode()}")
# Hent e-mailen
status, msg_data = conn.fetch(email_id, '(RFC822)')
if status != 'OK':
print(f"Kunne ikke hente e-mail ID {email_id.decode()}")
continue
raw_email = msg_data[0][1]
email_message = email.message_from_bytes(raw_email)
subject = decode_email_header(email_message["Subject"])
print(f" Emne: {subject}")
# Se efter vedhæftede filer
for part in email_message.walk():
if part.get_content_maintype() == 'multipart':
continue
if part.get('Content-Disposition') is None:
continue
filename = part.get_filename()
if filename and filename.lower().endswith('.pdf'):
decoded_filename = decode_email_header(filename)
filepath = os.path.join(DOWNLOAD_DIR, decoded_filename)
# Gem den vedhæftede fil
with open(filepath, 'wb') as f:
f.write(part.get_payload(decode=True))
print(f" -> Vedhæftet fil downloadet: {decoded_filename}")
# --- Flyt den behandlede e-mail ---
# 1. Kopier til destinationspostkassen
status, _ = conn.copy(email_id, DESTINATION_MAILBOX)
if status == 'OK':
# 2. Marker originalen til sletning
conn.store(email_id, '+FLAGS', '\\Deleted')
print(f" E-mail flyttet til '{DESTINATION_MAILBOX}'.")
# --- Rens op og ryd op ---
if email_id_list:
conn.expunge()
print("\nSlettede e-mails permanent fjernet.")
except Exception as e:
print(f"Der opstod en fejl: {e}")
finally:
if conn:
conn.logout()
print("Logget ud.")
Bedste praksis og fejlhåndtering
Når du bygger robuste automatiseringsscripts, skal du overveje følgende bedste praksis:
- Robust fejlhåndtering: Indpak din kode i
try...except
-blokke for at fange potentielle problemer som loginfejl (imaplib.IMAP4.error
), netværksproblemer eller parsingfejl. - Konfigurationsstyring: Hardkod aldrig legitimationsoplysninger. Brug miljøvariabler (
os.getenv()
), en konfigurationsfil (f.eks. INI eller YAML) eller en dedikeret hemmelighedsadministrator. - Logning: I stedet for
print()
-udsagn, brug Pythonslogging
-modul. Det giver dig mulighed for at kontrollere detaljegraden af dit output, skrive til filer og tilføje tidsstempler, hvilket er uvurderligt for fejlfinding af scripts, der kører uden opsyn. - Hastighedsbegrænsning: Vær en god internetborger. Afspørg ikke e-mailserveren overdrevent. Hvis du ofte skal tjekke for ny post, overvej intervaller på flere minutter i stedet for sekunder.
- Tegnkodninger: E-mail er en global standard, og du vil støde på forskellige tegnkodninger. Forsøg altid at bestemme tegnsættet fra e-maildelen (
part.get_content_charset()
) og have en fallback (som 'utf-8') for at undgåUnicodeDecodeError
.
Konklusion
Du har nu rejst gennem hele livscyklussen for interaktion med en e-mailserver ved hjælp af Pythons imaplib
. Vi har dækket etablering af en sikker forbindelse, visning af postkasser, udførelse af kraftfulde søgninger, hentning og parsing af komplekse multipart-e-mails, download af vedhæftede filer og styring af meddelelsesstatusser på serveren.
Kraften i denne viden er enorm. Du kan bygge systemer til automatisk at kategorisere supportbilletter, parse data fra daglige rapporter, arkivere nyhedsbreve, udløse handlinger baseret på advarsels-e-mails og meget mere. Indbakken, engang en kilde til manuelt arbejde, kan blive en kraftfuld, automatiseret datakilde for dine applikationer og arbejdsgange.
Hvilke e-mailopgaver vil du automatisere først? Mulighederne er kun begrænset af din fantasi. Start i det små, byg videre på eksemplerne i denne guide, og genvind din tid fra dybderne af din indbakke.