Zum Inhalt springen
Bernhard Götzendorfer
Debugging & Lösungen

0 Euro Betriebskosten: ein lokaler Mail-Assistant mit Apple Foundation Models

Ein E-Mail-Assistant für macOS Mail.app, der lokal auf dem Gerät rechnet: PII-Redaction vor jedem LLM-Call und ein stiller macOS-26.5-Bug, der die schnelle SQLite-Query tot machte.

Dark-Sepia-Tuschezeichnung in Amber: ein Briefumschlag, der in einem geschlossenen Tresor lokal verarbeitet wird, als Andeutung von Datenschutz auf dem eigenen Gerät

TL;DR

Ich baue an einem E-Mail-Assistant für macOS, der eingehende Mails einordnet und Antwortentwürfe vorbereitet, ohne dafür Cloud-Tokens zu verbrennen. Der Trick ist nicht magisch: Apple Foundation Models rechnen on-device, Ollama springt lokal ein, und nur im Notfall geht etwas über einen Cloud-Fallback raus. Das ergibt 0 Euro laufende Betriebskosten und einen Datenschutz-Ansatz, der jeden Prompt vor dem LLM-Call von personenbezogenen Daten bereinigt. Dieser Artikel zeigt zwei Dinge ehrlich: wie der Local-First-Aufbau wirklich aussieht, und einen stillen Bug, der nach einem macOS-Update die schnelle SQLite-Query tot machte, ohne eine einzige Fehlermeldung zu werfen. Das Tool ist in Vorbereitung, kein Verkaufsversprechen.

Warum ich das überhaupt baue

E-Mail ist für viele Selbständige und kleine Teams der unsichtbare Zeitfresser. Klassifizieren, priorisieren, formulieren: Das sind genau die Aufgaben, die ein Sprachmodell gut kann. Der Reflex der meisten Anbieter ist, jede einzelne Mail an eine Cloud-API zu schicken. Das ist schnell gebaut, kostet aber zweierlei: laufende Token-Gebühren und Ihre Postfach-Inhalte, die das Gerät verlassen.

Für Sie als Entscheiderin ist die Kernaussage: sensible Postfach-Inhalte verlassen das Gerät nicht und laufende Kosten entstehen keine. Den technischen Teil dieses Artikels können Sie an Ihr Team weitergeben.

Mich hat beides gestört. Wer geschäftliche Mails verarbeitet, schickt nebenbei Rechnungsbeträge, IBANs, Namen von Geschäftspartnern und gelegentlich Sozialversicherungsnummern durch fremde Inferenz-Pipelines. Auch bei Zero-Retention-Versprechen ist das eine Vertrauensentscheidung, die ich für meine eigenen Daten nicht treffen wollte. Und die Token-Kosten skalieren mit dem Postfach-Volumen, also genau dort nach oben, wo der Assistent am nützlichsten wäre.

Also habe ich die Frage umgedreht: Wie viel von dieser Arbeit lässt sich vollständig lokal erledigen, sodass der Standardbetrieb 0 Euro kostet und keine Mail-Inhalte das Gerät verlassen? Die Antwort ist erfreulich viel. Diesen Grundgedanken habe ich in EU-Souveränität im KI-Tech-Stack schon einmal für ganze Stacks durchgespielt; hier ist die konkrete Umsetzung in einem einzelnen Tool.

Die Architektur in einem Satz

Ein Hintergrund-Daemon beobachtet die lokale Mail.app, ein kleines SwiftUI-Programm in der Menüleiste zeigt die Vorschläge, und das eigentliche Rechnen passiert auf dem Gerät. Konkret:

  • Daemon. Bun und TypeScript, gebunden an 127.0.0.1:7879. Er liest Mails, klassifiziert sie, schlägt Entwürfe vor und lernt aus Korrekturen.
  • Menüleisten-App. Swift 6 und SwiftUI als MenuBarExtra. Sie ist die einzige Oberfläche, die der Nutzer sieht.
  • LLM-Schicht. Apple Foundation Models on-device als Standard, Ollama mit einem lokalen Gemma-Modell als zweite lokale Option, ein Cloud-Fallback nur für den Notfall.

Die Mail-Anbindung läuft bewusst über Apple Mail.app als einzige Oberfläche. Outlook-, Gmail- oder iCloud-Konten spiegeln sich ohnehin in Mail.app, wenn man sie dort als zweites Konto hinterlegt. Damit muss ich nicht ein Dutzend Provider-APIs pflegen, sondern lese die lokale Mail-Datenbank.

Local-First heißt nicht "kein Internet". Es heißt: Die Standardpfade rechnen auf dem Gerät, und alles, was doch raus muss, ist die bewusste Ausnahme.

Punkt 1: 0 Euro Betrieb durch eine Fallback-Kette

Das wirtschaftliche Versprechen "0 Euro Betrieb" steht und fällt mit einer einzigen Designentscheidung: Welcher Backend rechnet zuerst, und was passiert, wenn es ausfällt?

Die Reihenfolge ist eine konfigurierbare Fallback-Kette. Standardmäßig rechnet Apple Foundation Models on-device. Antwortet die On-Device-Bridge nicht, läuft in einen Timeout oder gibt einen HTTP-Fehler zurück, wandert der Call zur nächsten Stufe. Vereinfacht sieht die Dispatch-Logik so aus:

async function dispatchWithFallback(prompt: string, settings: Settings) {
  const chain = ['foundation', ...settings.fallbackChain]; // z.B. ['foundation','openrouter','ollama']
  let lastError: unknown;
  for (const backend of chain) {
    try {
      return await runBackend(backend, prompt);
    } catch (err) {
      if (!isRecoverable(err)) throw err; // echte Fehler nicht verschlucken
      lastError = err; // bridge-not-running | timeout | http -> nächste Stufe
    }
  }
  throw lastError;
}

Der entscheidende Punkt: Die erste Stufe, Apple Foundation Models, kostet nichts. Sie ist Teil von macOS und rechnet lokal auf dem Gerät. Die zweite lokale Option, Ollama mit einem Gemma-Modell, kostet ebenfalls nichts außer Strom. Die Cloud-Stufe existiert nur als Sicherheitsnetz für Geräte ohne On-Device-KI oder für den seltenen Fall, dass ein längerer Entwurf die lokalen Modelle überfordert.

Für den Normalbetrieb auf einem aktuellen Mac heißt das: Jede eingehende Mail wird lokal klassifiziert, jeder Entwurf lokal formuliert, und die monatliche Rechnung bleibt bei null. Das ist kein Hexenwerk, sondern eine Konsequenz daraus, dass Apple die Inferenz inzwischen ins Betriebssystem gelegt hat. Wer den Aufbau eines solchen lokalen Assistenten Schritt für Schritt nachvollziehen will, findet die Methode später im Artikel verlinkt.

Punkt 2: PII-Redaction vor jedem LLM-Call

Der zweite erzählenswerte Teil ist eine Entscheidung, die ich von Anfang an nicht verhandelbar gemacht habe: Bevor irgendein Text an irgendein Modell geht, läuft er durch eine Redaction-Stufe, die personenbezogene Daten erkennt und ersetzt.

Konkret betrifft das IBANs, Kreditkartennummern, österreichische Sozialversicherungsnummern und Telefonnummern. Das Muster ist einfach: Eine redact()-Funktion läuft über jeden Prompt, unabhängig vom Backend.

function redact(text: string): string {
  return text
    .replace(IBAN_RE, '[IBAN]')
    .replace(CREDIT_CARD_RE, '[CC]')
    .replace(AT_SVNR_RE, '[SVNR]')
    .replace(PHONE_RE, '[TEL]');
}

// Vor jedem Dispatch, nicht nur auf dem Cloud-Pfad:
const safePrompt = redact(systemPrompt) + redact(userPrompt);

Der naheliegende Einwand lautet: Wenn die On-Device-Modelle das Gerät ohnehin nicht verlassen, warum dann auch deren Prompts bereinigen? Aus zwei Gründen. Erstens als Defense-in-Depth: Wenn die Fallback-Kette doch einmal auf den Cloud-Pfad fällt, ist der Text bereits sauber, statt sich auf eine Verzweigung zu verlassen, die irgendwann brechen könnte. Zweitens für einen einheitlichen Audit-Pfad: Ich will sagen können, dass kein Prompt das System ohne Redaction durchläuft, ohne jedes Mal nachschauen zu müssen, welches Backend gerade aktiv war.

Eine Einschränkung benenne ich ehrlich, weil sie wichtig ist: Roh-Bildbytes lassen sich nicht so redacten wie Text. Ein Screenshot mit einer IBAN bleibt eine IBAN. Die Konsequenz: Anhänge mit reinen Bildinhalten werden hart auf den On-Device-Pfad gezwungen und gehen niemals an einen Cloud-Backend. Text aus PDFs oder Audio-Transkripten reitet dagegen auf dem bereinigten Kanal mit.

Punkt 3: der stille Bug, der mich am meisten gelehrt hat

Jetzt zum Teil, der nach Debugging klingt, weil er es war. Und die unangenehmste Sorte Bug: einer, der keine Fehlermeldung wirft.

Der schnelle Pfad

Zur Einordnung: Es gibt zwei Wege, Mails aus Mail.app zu lesen. Der bequeme Weg ist AppleScript, das stabil funktioniert, aber für jede Abfrage einen Skript-Aufruf braucht und entsprechend langsam ist. Der schnelle Weg liest direkt den lokalen SQLite-Envelope-Index, den Mail.app pflegt. In meinen Messungen ist der SQLite-Pfad zwei bis drei Größenordnungen schneller als AppleScript. Diese Beschleunigung ist kein Luxus, sie macht den Unterschied zwischen "Posteingang in einer Sekunde durchgesehen" und "der Lüfter dreht hoch".

Damit das System nie hart bricht, gibt es eine bewusste Fallback-Stufe: Wenn die schnelle SQLite-Query aus irgendeinem Grund nicht liefert, fällt der Code auf den langsamen AppleScript-Boden zurück. Funktional bleibt alles korrekt, nur eben langsamer. Genau dieser Sicherheitsmechanismus hat den Bug versteckt.

Das Symptom

Nach einem macOS-Update auf 26.5 fiel mir auf, dass die Postfach-Auflistung wieder zäh geworden war. Keine Fehlermeldung, kein Crash, keine roten Logs. Die App lief, las Mails, schlug Entwürfe vor. Nur eben spürbar langsamer. Wenn ich nicht zufällig auf die Geschwindigkeit geachtet hätte, wäre es mir womöglich wochenlang nicht aufgefallen.

Der Grund für die Unsichtbarkeit ist genau die Fallback-Stufe von oben: Die schnelle SQLite-Query schlug fehl, der Code fiel sauber auf den langsamen AppleScript-Boden zurück, und nach außen sah alles funktionsfähig aus. Ein still totgelegter schneller Pfad ist tückischer als ein Crash, weil nichts Sie auf das Problem stößt.

Die Wurzel: ein Schema-Drift

Die eigentliche Ursache lag im Datenbank-Schema von Mail.app. Mit macOS 26.5 hatte Apple die interne Struktur des Envelope-Index verändert, ohne dass es in irgendeiner Dokumentation auftauchte. Drei Dinge hatten sich verschoben:

  • Spalten verschwanden. Felder, auf die meine Query namentlich zugriff, hießen plötzlich anders oder existierten nicht mehr in der erwarteten Tabelle.
  • Eine Beziehungsrichtung kehrte sich um. Ein Fremdschlüssel, über den ich zwei Tabellen verband, zeigte in der neuen Schema-Version in die andere Richtung.
  • Der Join lief ins Leere. In der Summe lieferte die Query null Treffer statt eines Fehlers, und null Treffer ist für die Fallback-Logik dasselbe Signal wie "lies lieber per AppleScript".

Das ist die Lehre, die ich mitnehme: Eine Query gegen eine fremde, undokumentierte Datenbank ist eine Abhängigkeit, die jederzeit unter Ihnen wegbrechen kann, und zwar leise. Das Betriebssystem-Update hat nichts versprochen und nichts gebrochen, im formalen Sinn. Es hat nur eine interne Struktur verändert, von der mein Code eine Annahme hatte.

Der Fix

Repariert habe ich es mit zwei Maßnahmen. Erstens habe ich die Query an das neue Schema angepasst: die korrigierte Join-Richtung, die umbenannten Spalten über robustere Ausdrücke abgefangen, und fehlende Werte mit COALESCE defensiv mit Leerstrings ersetzt, statt auf ihre Existenz zu vertrauen.

-- defensiv gegen Schema-Drift: fehlende Spalte -> Leerstring statt NULL-Bruch
SELECT COALESCE(mb.url, '') AS mailbox_url, ...
FROM messages m
JOIN summaries s ON s.message = m.ROWID   -- korrigierte Join-Richtung
LEFT JOIN mailboxes mb ON mb.ROWID = m.mailbox

Zweitens, und das ist der wichtigere Teil, habe ich die Verifikation verändert. Ich verlasse mich nicht mehr darauf, dass "null Treffer" automatisch "lies per AppleScript" bedeutet. Stattdessen prüfe ich den schnellen Pfad gegen den realen Index und stelle sicher, dass er tatsächlich Treffer liefert, bevor ich ihm vertraue. Verifiziert habe ich gegen ein echtes Postfach mit mehreren hundert Nachrichten, nicht gegen einen synthetischen Testdatensatz, weil genau dieser Realdaten-Abgleich den Schema-Drift überhaupt erst sichtbar gemacht hätte.

Diese zweite Maßnahme ist die eigentliche Lektion. Der Fix der Query war Handwerk. Die Verifikation gegen Realdaten ist das, was den nächsten stillen Bug dieser Art früher fängt. Ich habe denselben Reflex schon in meinem gescheiterten lokalen Coding-Setup gebraucht: Ein Status, der "alles grün" sagt, ist kein Beweis, dass das System tatsächlich arbeitet.

Was an diesem Bug verallgemeinerbar ist

Drei Dinge nehme ich für jedes Tool mit, das gegen fremde Systeme arbeitet.

Stille Fallbacks brauchen einen Alarm. Ein Fallback, der sauber funktioniert, ist genau deshalb gefährlich, weil er ein Problem verschluckt. Wer einen schnellen und einen langsamen Pfad hat, sollte protokollieren, wenn der langsame greift, sonst merkt niemand, dass der schnelle gestorben ist.

Undokumentierte Abhängigkeiten sind tickende Annahmen. Der SQLite-Index von Mail.app ist kein veröffentlichtes API. Wer ihn liest, gewinnt enorm an Geschwindigkeit und kauft sich zugleich das Risiko ein, dass ein Update die Struktur verschiebt. Diese Abwägung kann man treffen, aber man muss sie bewusst treffen und den Bruchfall einplanen.

Verifikation schlägt Status. "100 Prozent geladen", "Query lief durch", "keine Fehler im Log" sind Status-Signale, keine Funktions-Beweise. Der einzige verlässliche Beleg war, die Query gegen echte Daten zu fahren und die Treffer zu zählen.

Wo das Tool steht

Ehrlich gesagt: Das ist ein Werkzeug in Vorbereitung, kein fertiges Produkt mit Verkaufsknopf. Es läuft bei mir produktiv, ist als signiertes und notarisiertes Programm mit automatischem Update-Pfad gebaut, und es macht genau das, wofür ich es brauche. Aber es ist ein Solo-Build, der noch reift, und ich kommuniziere es bewusst so, statt ein Versprechen zu geben, das ich für andere Postfächer noch nicht halten kann.

Was ich aus diesem Build sicher sagen kann: Ein lokaler, kostenfreier Mail-Assistant ist heute technisch machbar, weil die On-Device-Inferenz von macOS gut genug für Klassifikation und Entwürfe geworden ist. KI ist hier ein Werkzeug, mächtig aber nicht magisch, und der schwierige Teil war nicht das Modell, sondern die unsichtbare Datenbank darunter.

Wenn Sie das selbst betreiben wollen

Wenn Sie selbst einen Assistenten betreiben wollen, der lokal rechnet, Postfach-Inhalte nicht unkontrolliert nach außen gibt und im Sinne der DSGVO sauber bleibt: Genau diesen sicheren Betrieb eines self-hosted Assistenten habe ich im Kurs auf agenticbuilders.at/24-7-assistent Schritt für Schritt aufbereitet, inklusive der Frage, was man nie öffentlich binden sollte. Und falls Sie ein konkretes Vorhaben haben, bei dem Datensouveränität und lokaler Betrieb zusammenkommen sollen: Ich arbeite bewusst nur mit 1-2 Kunden gleichzeitig und nehme mir entsprechend Zeit für eine ehrliche Einschätzung über Kontakt.