Das Problem
Manuelle E-Mail-Sortierung ist Zeitverschwendung. Regeln basieren auf starren Kriterien (Absender, Betreff), missen aber Kontext. Newsletter-Filter erwischen Werbung, aber nicht "wichtige" Updates. Resultat: tägliches Durchforsten des Postfachs – oder verpasste E-Mails.
Architektur
Der Datenfluss: IMAP holt E-Mails → LLM kategorisiert → SQLite trackt Zustand → IMAP verschiebt in Ordner.
Code-Deep-Dive
IMAP-Verbindung mit Retry-Logik
IONOS-Server sind instabil. Exponentieller Backoff, SSL-Kontext-Optimierung, sauberes State-Management.
def connect_to_account(self, account: Dict, max_retries: int = 3):
for attempt in range(max_retries):
try:
socket.setdefaulttimeout(60)
if account['use_ssl']:
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
ssl_context.set_ciphers(
'HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA'
)
imap = imaplib.IMAP4_SSL(
account['server'],
account['port'],
ssl_context=ssl_context
)
else:
imap = imaplib.IMAP4(account['server'], account['port'])
imap.login(account['username'], account['password'])
# Verbindungstest
result = imap.capability()
if result[0] != 'OK':
raise Exception(f"IMAP-Capability-Test fehlgeschlagen")
return imap
except (socket.error, socket.timeout, imaplib.IMAP4.error) as e:
wait_time = (2 ** attempt) * 5 # 5, 10, 20 Sekunden
time.sleep(wait_time)
return None
LLM-Prompt für Kategorisierung
Der Prompt enthält persönlichen Kontext, detaillierte Regeln pro Kategorie, und strikte Format-Anweisungen.
prompt_template: "Du bist ein E-Mail-Kategorisierer.
Deine Aufgabe ist es, E-Mails in eine der folgenden
Kategorien einzuordnen:
{categories_with_rules}
!!!! Antworte NUR mit dem Namen der Kategorie,
ohne weitere Erklärungen !!!!
PERSÖNLICHER KONTEXT:
{personal_context}
Regeln:
1. Antworte NUR mit dem Namen der Kategorie
2. Normale E-Mails bleiben im Hauptpostfach
3. Werbung von großen Tech-Firmen ist 'Werbung'
4. Werbung von anderen Absendern ist 'Spam'
5. Wenn Links verdächtig sind → 'Spam'"
Content-Hash für Duplikate
MD5 über Betreff + Absender + Content (erste 500 Zeichen). Schnell, zuverlässig, kollisions-resistent für diesen Use-Case.
def create_email_hash(self, subject: str, from_address: str,
content: str) -> str:
"""Erstellt einen Hash zur Duplikat-Erkennung."""
email_content = f"{subject}|{from_address}|{content[:500]}"
return hashlib.md5(
email_content.encode('utf-8')
).hexdigest()
def is_email_duplicate(self, email_hash: str) -> Optional[dict]:
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT account_name, subject, from_address,
category, processed_at
FROM processed_emails
WHERE email_hash = ?
''', (email_hash,))
result = cursor.fetchone()
# ... parse to dict
SQLite Schema
CREATE TABLE IF NOT EXISTS processed_emails (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_name TEXT NOT NULL,
email_uid TEXT NOT NULL,
email_hash TEXT NOT NULL,
subject TEXT,
from_address TEXT,
category TEXT,
processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(account_name, email_uid)
);
CREATE INDEX idx_account_uid
ON processed_emails(account_name, email_uid);
CREATE INDEX idx_email_hash
ON processed_emails(email_hash);
Kategorien-Konfiguration
categories:
- name: Wichtig
description: Wichtige E-Mails, sofortige Aufmerksamkeit
- name: Projekte
description: E-Mails bezüglich laufender Projekte
- name: Newsletter
description: Newsletter und regelmäßige Updates
- name: Werbung
description: Werbemails und Angebote
detailed_rules: |
Newsletter von Google, Microsoft, Apple, Amazon
Seriöse Angebote von bekannten Marken
Produktupdates von etablierten Services
- name: Spam
description: Verdächtige oder unerwünschte E-Mails
- name: Rechnungen
description: Rechnungen und Zahlungsbestätigungen
- name: Termine
description: Termine und Einladungen
- name: Archiv
description: E-Mails, die archiviert werden sollen
- name: Volt-Herzogenrath
description: Wahlkampftermine, Anfragen etc
9 Kategorien
Jede Kategorie ist trainiert mit spezifischen Regeln. Das LLM bekommt den vollständigen Kontext.
Lessons Learned
- IMAP-Instabilität bei IONOS Nach 10 E-Mails wird die Verbindung instabil. Lösung: Verbindung alle 10 Mails neu aufbauen mit exponential backoff.
- Encoding-Probleme E-Mails kommen mit ISO-8859-1, UTF-8, Windows-1252. `errors='replace'` in decode() ist Pflicht.
- LLM-Halluzinationen Das Modell erfindet manchmal Kategorien. Lösung: Strict validation gegen erlaubte Kategorien, Fallback zu "Archiv".
- Duplikate vs. Threads Gleicher Betreff ≠ gleiche E-Mail. MD5 über Content + Absender + Betreff, nicht nur Betreff.
- Timeouts sind kritisch 60 Sekunden Socket-Timeout, sonst hängt der Prozess bei instabilen Verbindungen.
- SQLite ohne WAL-Mode Bei Standard-Journal-Mode können lange Transaktionen blockieren. Kurze Connections, schnelle Commits.