Mailsammlung

Einleitung

Wer kennt das nicht: Im Laufe der Jahre mit demselben Mail-Account sammeln sich Mails und oft liegen noch irgendwo wichtige Informationen oder Kontakte, die sich eben nur in diesem Berg befinden. Befindet sich der Account bei Googlemail, kann nman alles bequem durchsuchen. Aber ich habe meine Mails auf dem Laptop und will nicht alles hochladen.

Wir starten gleich mit dem Projekt. Informationen, die ich im Laufe des Projekts rechercieren musste, stelle ich als Einschub oder separaten Artikel zur Verfügung. Das Github-Repo mit dem kompletten Code ist unten verlinkt.

Ziel

Was schrieb mir der Hausmeister wegen dem Wasserschaden? Diese Frage lässt sich mit der normalen Volltextsuche oft nicht beantworten – zumindest nicht zuverlässig.

E-Mails enthalten nicht immer exakt die Wörter, nach denen wir suchen. Manchmal steht statt „Wasserschaden“ nur „Rohrbruch“ oder „Feuchtigkeit im Keller“. Statt „Hausmeister“ vielleicht „Herr Keller“ oder „Technischer Dienst“. Wer seine Mails dennoch semantisch durchsuchen will, braucht mehr als nur grep – nämlich Embeddings.

In diesem Artikel bauen wir ein kleines semantisches Suchsystem, das:

  • E-Mails aus Thunderbird lokal verarbeitet
  • Embeddings aus den Mailtexten erzeugt
  • einen Vektorindex zur schnellen Ähnlichkeitssuche verwendet
  • relevante Mails zu einer Frage findet
  • ein kleines Sprachmodell lokal über Ollama nutzt, um eine Antwort zu formulieren

Und das Ganze läuft lokal, offline, transparent – mit freier Software.

Projekt einrichten

Ich richte meine Python-Projekte inzwischen gerne mit uv ein. Dieses Rust-Tool ist extrem schnell, unkompliziert und setzt die komplette Projektstruktur einschließlich .venv auf. Das Kompilieren der CUDA-Unterstützung funktionierte bei mir nur mit Python 3.9:

1
2
3
4
5
6
sudo dnf install -y uv python3.9
# oder uv python install 3.9
uv init mailsearch --python 3.9 && cd mailsearch
uv add sentence-transformers faiss-cpu numpy matplotlib umap-learn
[...]
uv run main.py

Mails aus Thunderbird extrahieren

Thunderbird speichert alle E-Mails im MBOX-Format. Diese Dateien befinden sich in der Regel unter:

  • Linux/macOS: ~/.thunderbird/<profil>.default-release/Mail/Local Folders/Inbox
  • Windows: C:\Users\<Benutzer>\AppData\Roaming\Thunderbird\...

Wir extrahieren die Mails per Python:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import mailbox, json

mbox = mailbox.mbox("/path/to/INBOX")

mails = []
for msg in mbox:
    mails.append({
        "id": msg.get("message-id"),
        "from": msg.get("from"),
        "to": msg.get("to"),
        "subject": msg.get("subject"),
        "date": msg.get("date"),
        "body": msg.get_payload(decode=True).decode(errors="ignore")
    })

with open("mails.json", "w") as f:
    json.dump(mails, f, indent=2)

Bei kaputten Mails kann die Extraktion des Payloads fehlschlagen – eine robustere Version des Skripts findet sich im Git-Projekt.

💡

Hardware-Beschleunigung

Um zu prüfen, ob Deine Umgebung CUDA (NVIDIA-GPU) oder Apple MPS (Metal Performance Shaders) unterstützt, kannst Du folgendes ausführen:

1
2
3
4
5
6
7
import torch
print("CUDA verfügbar:", torch.cuda.is_available())
print("CUDA Version:", torch.version.cuda)
print("GPU:", torch.cuda.get_device_name(0)
    if torch.cuda.is_available()
    else "-")
print("MPS (Apple GPU):", torch.backends.mps.is_available())

Für CUDA musst Du einen passenden PyTorch-Build installiert haben, z. B. mit --index-url https://download.pytorch.org/whl/cu121. Auf Apple Silicon wird MPS genutzt.

Mail-Inhalte vektorisieren

Für die semantische Suche nutzen wir ein kleines, effizientes Modell aus sentence-transformers:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from sentence_transformers import SentenceTransformer
import numpy as np

model = SentenceTransformer("all-MiniLM-L6-v2")

with open("mails.json") as f:
    mails = json.load(f)

texts = [m["subject"] + ": " + m["body"] for m in mails]
vectors = model.encode(texts, convert_to_numpy=True)

Das Modell findet sich danach im Cache unter $HOME/.cache/huggingface/ und wird von den nachfolgenden Skripten verwendet.

Vektorindex mit FAISS bauen

Der nächste Schritt ist die Vektorisierung und die Erstellung des Indexes. Der folgende Codeausschnitt verdeutlicht das. Das komplette Skript ist im Git-Repo, dort wird der Index auch als Datei gespeichert.

1
2
3
4
5
6
7
import faiss

index = faiss.IndexFlatL2(vectors.shape[1])
index.add(vectors)

# Referenzen für spätere Rückverweise
mail_ids = [m["id"] for m in mails]
ℹ️

Cosinus-Ähnlichkeit

Die Cosinus-Ähnlichkeit misst, wie ähnlich zwei Vektoren in ihrer Richtung sind – unabhängig von ihrer Länge. Sie ist die gängigste Metrik für Embeddings.

$$ \text{cosine\_similarity}(\vec{a}, \vec{b}) = \frac{\vec{a} \cdot \vec{b}}{\|\vec{a}\| \cdot \|\vec{b}\|} $$

dabei ist $|\vec{a}|$ die euklidische Norm von $\vec{a}$

$$\|\vec{a}\| = \sqrt{\sum_{i=1}^n a_i^2}$$

– also einfach der Pythagoras im $n$-dimensionalem. Das Ergebnis bei Embeddings ist in $[0, 1]$, da alle Vektoren positiv sind.

In Python gibt es fertige Bibliotheksfunktionen dafür:

1
2
3
from sklearn.metrics.pairwise import cosine_similarity
sim = cosine_similarity([vec1], [vec2])
print(sim[0][0])

Suche starten

Sobald der Index vorhanden ist, können wir diesen für eine einfache Suche verwenden:

1
2
3
4
5
6
7
8
query = "Was schrieb mir der Hausmeister wegen dem Wasserschaden?"
qvec = model.encode([query])

D, I = index.search(np.array(qvec), k=3)
found = [mails[i] for i in I[0]]

for mail in found:
    print(mail["subject"])

Antwort formulieren mit Ollama

🦙

Ollama kurz erklärt

Ollama ist ein Tool zum lokalen Ausführen von Sprachmodellen (LLMs), z. B. phi, mistral, llama3. So richtest Du es ein:

1
2
3
4
5
6
7
curl -fsSL https://ollama.com/install.sh | sh

# Beispielmodell holen:
ollama pull phi

# Testlauf:
ollama run phi

Danach können wir das Modell aus Python mit dem REST-API von Ollama ansprechen. Wir übergeben die aufgrund des Kontexts gefundenen Mail mit dem Promt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import httpx

def ollama_prompt(question, mails):
    context = "\n\n".join(f"- {m['subject']}: {m['body']}" for m in mails)
    prompt = f"""
    Du bist ein hilfreiches KI-Modell.
    Beantworte die folgende Frage basierend auf den E-Mails.

    {context}

    Frage: {question}
    Antwort:
    """

    resp = httpx.post(
        "http://localhost:11434/api/generate",
        json={"model": "phi", "prompt": prompt, "stream": False},
        timeout=60
    )
    return resp.json().get("response", "(keine Antwort)")
ℹ️

Retrieval-Augmented Generation (RAG)

Die Anfrage wird per Embedding mit den Mails im Vektorraum verglichen. Nur die relevantesten Mails werden als Kontext in den Prompt eingefügt. Ollama formuliert basierend darauf eine Antwort.

Das Sprachmodell hat also keinen Zugriff auf alle Mails, sondern bekommt gezielt Kontext mitgeliefert.

Fazit

Wir haben ein einfaches, aber mächtiges Tool gebaut, um E-Mails semantisch zu durchsuchen – ganz ohne Cloud, Gmail oder Microsoft 365. Die Kombination aus lokalem Embedding-Modell und Ollama ist leichtgewichtig, schnell und privat.

Quellen und Links

Git-Repository zum Artikel