Maßgeschneiderte FileMaker-Lösungen & AddOns. FileMaker-experts.de

Wie wir ein 50-GB-Mailarchiv aus einem 12 Jahre alten FileMaker-System gerettet haben

Wie wir ein 50-GB-Mailarchiv aus einem 12 Jahre alten FileMaker-System gerettet haben

Eine Detektivgeschichte über CSV-Exporte ohne Header, drei verschiedene Encoding-Katastrophen, IDs aus drei Tabellen die alle „Kontakt" hießen und am Ende doch nicht das Gleiche meinten — und darüber, was eine moderne KI in einer solchen Migration konkret leistet. Was ein Senior-FileMaker-Entwickler in zwei Wochen allein hingekriegt hätte, war im Tandem in drei Tagen erledigt. Inklusive funktionierendem Threading.

Image3 opus. ---

Der Ausgangspunkt: ein Archiv ohne Karte

Bei einem Kunden lag eine FileMaker-Datenbank mit knapp 50 GB Mailarchiv auf der Platte. Aufgebaut über zwölf Jahre, durchgewuchert mit einem Plugin namens „Dacons MailIT", erweitert mit eigenen Korrespondenz-Tabellen, irgendwann nicht mehr richtig gepflegt. Der ursprüngliche Entwickler war nicht mehr greifbar. Das produktive System lief auf einer alten FileMaker-Version, auf die niemand mehr Zugriff hatte. Das einzige, was noch verfügbar war: zwei CSV-Exporte. Diese wurden aus einer anderen Perspektive der Altdatenbank exportiert. Feldnamen völlig unbekannt und aufgrund der Bezeichner nicht erklärbar.

  • Mailausgang.csv — 426 MB, 45 787 Datensätze, 72 Spalten ohne Header
  • MailEingang.csv3.5 GB, ca. 9.5 Millionen CSV-Zeilen, 70 Spalten ohne Header

Was sich nach „ein paar Mails importieren" anhörte, entpuppte sich als datenarchäologisches Projekt. Was folgt, ist die Geschichte, wie wir aus diesen rohen Exporten am Ende ~93 000 Mails mit funktionierendem Threading in der neuen Datenbank hatten — und warum eine KI in diesem Workflow den Unterschied zwischen drei Tagen und drei Wochen macht.

Erster Blick — und der erste Schock

Beide CSVs hatten keinen Header. Das heißt: 72 bzw. 70 Spalten von Werten, ohne dass jemand sagen konnte, was jede einzelne bedeutet. Manche Spalten waren immer leer. Manche enthielten die gleiche Konstante in allen Zeilen. Manche enthielten 2 KB lange „Wertelisten" aus dem alten FM-Schema, die irgendwie ins Datenfeld geraten waren.

Beispiel eines einzelnen Datensatzes:

"","","","","","","1","verlag degener & co\x0bmanfred\x0bHerr manfred musterman\x0b…",
"","unser zeichen: m-m","","45787","45787","16-3314","","Correspondence_Number","",
"OfferteLohnabrechnungKostenvoranschlagProjektAuftragsbestätigung…",
"45787","1","verlag degen & co\x0b…","","","16-3314 nachlassache [erblasser]",
…

\x0b ist ein Vertical Tab, in FileMaker ein interner Zeilenumbruch innerhalb eines Feldes. Die Adresse verlag degener & co\x0bmanfred\x0bHerr manfred dreiss\x0bam brühl 9\x0b91610 insingen\x0bdeutschland ist also eigentlich eine mehrzeilige Anschrift, die nur platt nebeneinander geschrieben aussieht.

Und der Datensatz-Trenner zwischen Zeilen war kein \n und auch kein \r\n, sondern \r allein — Mac-Klassik-CR, wie in den Neunzigern. Standard-Tools wie wc -l lieferten daher für eine 426-MB-Datei „0 Zeilen". Erfreulich.

Detektivarbeit Phase 1 — die Spalten erschließen

Hier kommt die erste Stelle, an der eine KI dramatisch beschleunigt. Statt sich durch 72 Spalten manuell durchzuarbeiten und für jede zu raten, was sie sein könnte, lief eine Python-Stichprobe über 500 Datensätze:

  • Wie viele Werte sind in jeder Spalte gefüllt?
  • Wie viele unique?
  • Sehen die Werte aus wie Zahlen, Datums, E-Mail-Adressen, freier Text?
  • Welche Spalten haben eindeutige Werte (PK-Kandidaten)?
  • Welche haben sich wiederholende kleine Zahlen (FK-Kandidaten)?

Das Ergebnis war ein systematischer Steckbrief pro Spalte. Aus dem ließ sich rückschließen:

[14] Aktenzeichen (Projektcode):  "16-3314"
[26] Versanddatum:                "13/04/2016"
[40] Mail-PK (eindeutig 17–244)
[44] FK-Kandidat (nur 1 oder 2)
[45] FK-Kandidat (62 unique, Range 1–60)   ← id_kontakt?
[53] Empfänger-Mail:              "degen@degen-verlag.de"
[59] Eindeutig pro DS, 2–3314    ← könnte id_customer sein
[64] Betreff
[65/67] Body (Plain)

Der Aha-Moment kam mit einer einfachen Hypothese: Hängt Spalte [59] mit dem Aktenzeichen zusammen? Aktenzeichen 16-3314 und Spalte [59] 3314 — das war ein verdächtiger Zufall. Test über 10 Stichproben:

DS  1: Aktenz. "16-3314" → [59] = "3314" ✓ MATCH
DS  2: Aktenz. "16-0002" → [59] = "2"    ✓ MATCH
DS  3: Aktenz. "16-0003" → [59] = "3"    ✓ MATCH
…

Bingo. Das Aktenzeichen ist JJ-CCCCC, wobei JJ das Jahr und CCCCC die Customer-ID ist. In zwei Minuten geknackt, was in der alten Dokumentation nicht stand und niemand mehr wusste.

Detektivarbeit Phase 2 — die Encoding-Hölle

Eine frühere Version des Exports (mailausgang_clean.csv, von einer anderen Migration übrig geblieben) sah auf den ersten Blick gut aus — 14 saubere Spalten mit Header. Aber dann das hier:

"Mit freundlichen Grüssen
Jörg Muster
Universit√§t Bern"

Das ist Mojibake: UTF-8-Bytes, die als MacRoman gelesen und wieder als UTF-8 gespeichert wurden. Aus ü (UTF-8 \xc3\xbc) wird beim Doppel-Encoding √º. Aus ä wird √§, aus ß wird √ü.

Die Reverse-Operation:

"Grüssen".encode('mac_roman').decode('utf-8')
# → 'Grüssen'

Diese eine Zeile — encode('mac_roman').decode('utf-8') — hat 41 133 Datensätze gerettet. Sie ist nicht trivial: man muss wissen, dass alte FileMaker-Versionen auf Macs intern MacRoman als Charset benutzten, dass aber die meisten Tools heute UTF-8 erwarten. Das Reverse-Pattern ist Sachkunde, kein Lehrbuchwissen. Ein Entwickler, der nicht regelmäßig mit alten Mac-Daten arbeitet, kann da Stunden bis Tage drauf verlieren.

Aber das war nur die erste Encoding-Falle. Die nächste war versteckter.

Die BOM-Falle

Die clean.csv ließ sich nicht parsen. csv.DictReader lieferte Datensätze, aber das Feld Mail_ID war immer leer. Ein Hexdump auf die ersten Bytes:

\xef\xbb\xbf M a i l _ I D , I D _ C O N T A C T ,…

Da waren drei Bytes vor dem Header, die der CSV-Reader nicht erkannte: UTF-8 BOM (Byte Order Mark). Die erste Header-Spalte hieß also nicht Mail_ID, sondern Mail_ID. Jeder Lookup auf row['Mail_ID'] lief ins Leere.

Fix: open(file, encoding='utf-8-sig') — das -sig frisst das BOM automatisch.

Die Modifikationsdatum-Falle

Beim ersten Import-Test der MailEingang-Mails zeigten alle Mails Empfangsdatum 2026 — also in der Zukunft. Eine Mail von Hiltrud Jacob, die laut Inhalt aus 2016 stammte, hatte als Empfangen_ts den Wert „2026-01-04 20:26:17".

Schaute man genauer hin: Es gab in der CSV zwei verschiedene Zeitstempel-Felder. Feld [4] mit „12/04/2016" — Original-Empfangsdatum. Feld [62] mit „04/01/2026 20:26:17" — das letzte Modifikationsdatum in MailIT, also der letzte Touch (z.B. ein „Mail als gelesen markiert" oder eine spätere Bearbeitung).

Der erste Konverter-Lauf nutzte [62]. Falsch. Hätte vermutlich ein menschlicher Entwickler bei der ersten Stichprobe auch entdeckt, aber: man muss wissen, dass MailIT zwei verschiedene Datumsfelder pflegt. Auch das ist Sachkunde, die durch systematische Stichproben („zeig mir 3 echte Mails, lass mich sehen was wo steht") schneller aufgedeckt wird als durch manuelles Klicken.

Die ID-Brücke, die keine war

Beide CSV-Exporte hatten Kontakt-IDs. Aber sie meinten verschiedene Tabellen.

Mailausgang MailEingang
Spalte [45] ID_CONTACT [8] id_kontakt
Wertebereich 2 000 – 15 000 1 – 146
Verweist auf Korrespondenz-Tabelle (vermutlich) MailIT-Adressbuch

Die naheliegende Annahme — beide IDs zeigen auf dieselbe zentrale Kontakttabelle — war falsch. Die Wertebereiche überlappten sich nicht einmal. Eine ID 51 in MailEingang ist nicht „derselbe Kontakt" wie ID 51 in Mailausgang. Es waren schlicht zwei verschiedene Adressbücher.

Daraus folgte: Eine direkte ID-Brücke zwischen Eingang und Ausgang gab es nicht. Aber: Die E-Mail-Adresse des Gegenübers war in beiden Archiven vorhanden. Wenn man die als Brücke nahm — kombiniert mit dem normalisierten Betreff (also Betreff ohne „Re:", „Aw:", „Fwd:") — bekam man eine heuristische Cross-Archive-Verbindung.

Die 9.5-Millionen-Zeilen-Datei mit den 47.000 Mails

Die MailEingang.csv war 3.5 GB groß. Bei den ersten Stichproben sah es so aus, als wären nur etwa 1 % der Zeilen echte Mails (mit Header), der Rest „Müll-Datensätze" mit nur Betreff und Datum. Hochrechnung auf 47 000 Zeilen ergab ~560 echte Mails.

Aber ein vollständiger Stream-Durchlauf zeigte was ganz anderes:

Gesamt: 9 536 233 Zeilen, davon 47 678 echte Mails (0.50 %)

Die Datei hat nicht 47 000 Zeilen, sondern 9.5 Millionen. FileMaker hat beim Export pro Datensatz offenbar mehrere Hilfs-Zeilen produziert (vermutlich für Wertelisten oder Such-Indizes), und die echten Mails sind über die ganze Datei verstreut.

Hier eine wichtige technische Notiz: Python kann das in 35 Sekunden durchstreamen, wenn man weiß, wie:

csv.field_size_limit(sys.maxsize)
with open(path, 'r', encoding='utf-8', errors='replace', newline='') as f:
    for row in csv.reader(f):
        if len(row) > 41 and row[41].strip():
            # echte Mail
            ...

Drei Details, die zwingend nötig sind:

  1. csv.field_size_limit(sys.maxsize) — sonst wirft Python einen FieldOverflow bei den 23-KB-Headers
  2. errors='replace' — sonst stürzt der Reader an einem einzelnen kaputten Byte ab
  3. newline='' — sonst übernimmt Python die Zeilenumbruch-Logik und macht aus \r\n ein \n, was die FM-internen \r zerstört

Das sind drei Zeilen, die zusammen vier Stunden Debugging ersparen.

Threading — die schönste Disziplin

Das eigentliche Geschmacks-Ziel des Kunden war ursprünglich nicht der Mail-Import, sondern Threading: Mails, die thematisch zusammengehören, sollen als Konversation gruppiert sichtbar werden. Das ist im RFC 5322 sauber gelöst — Mail-Header Message-ID, In-Reply-To, References ergeben einen klaren Baum.

Aber:

  • Mailausgang.csv hatte keine Mail-Header. Keine Message-ID, kein In-Reply-To, nichts. Die alte Korrespondenz-Datenbank hatte das nie gespeichert.
  • MailEingang.csv hatte echte Header in Feld [41] — und davon hatten 60 % einen In-Reply-To, 62 % References.

Daraus folgten drei verschiedene Threading-Strategien:

1. Mailausgang — heuristisch über Kontakt + Betreff Jede Mail-Gruppe mit demselben id_contacts und demselben normalisierten Betreff wurde zu einem Thread. Älteste Mail = Wurzel. → 8 800 Threads, 27 800 Mails gruppiert (63 %).

2. MailEingang — echtes RFC-Threading Per In_Reply_To rekursiv die Wurzel suchen, mit Memoization für Performance. → 2 600 Threads, 6 100 Mails gruppiert (13 %, weil viele Vorgänger im anderen Archiv liegen).

3. Cross-Archive-Threading — die Königsdisziplin Gruppen-Key: (Gegenüber-E-Mail-Adresse, normalisierter Betreff). Damit verbinden sich die ausgehenden Mails des Kunden mit den eingehenden Antworten, obwohl die ausgehenden nur synthetische Message-IDs haben. → 11 000 Threads, 69 300 Mails in Threads (75 % aller Legacy-Daten).

Ein konkretes Beispiel aus dem Top-Thread:

Kontakt: sabine@aars.com   Betreff: "heiratsurkunde"
921 Mails über 10 Jahre (2016–2025)
   4 ausgehend, 917 eingehend

Eine zehn Jahre lange Geschäftskorrespondenz mit einer Notarin/Standesbeamtin, die jetzt erstmals als zusammenhängender Thread sichtbar ist.

Die FileMaker-Falle, die niemand sieht

In FileMaker gibt es eine kleine Checkbox in den Auto-Eingabe-Optionen eines Feldes: „Vorhandenen Feldwert nicht ersetzen (während des Imports oder beim Setzen über Skript)". Sie ist standardmäßig aktiv.

Das klingt harmlos. Aber:

  1. Datensatz wird neu angelegt → Auto-Eingabe-Berechnung läuft einmal mit leerem Quellfeld → ergibt leeren Wert
  2. Skript setzt nachträglich Message_ID → Auto-Eingabe läuft nicht mehr, weil der Wert (leer) „schon da war"
  3. Message_ID_norm bleibt dauerhaft leer
  4. Threading-Match per Message_ID_norm greift nie

Diese Falle hat im Verlauf des Projekts zu mehreren Stunden Fehlersuche geführt — beim ersten Test wurden alle frisch gesendeten Mails beim nächsten Abruf wieder als neu importiert, weil der Match nicht griff. Die Lösung ist trivial (Häkchen weg), aber das Wissen darüber, dass dieses Häkchen das ist, was schiefgeht, ist nichts, was in der Standard-Dokumentation auftaucht. Es ist tribal knowledge der FileMaker-Community.

Was die KI hier konkret leistet

Ehrliche Bilanz nach drei Tagen:

Was der Mensch lieferte:

  • Den Domain-Kontext (Kunden-Geschäftslogik, was sind FK-Tabellen, wo soll das Ergebnis hin)
  • Alle FileMaker-Entscheidungen (Schema, Beziehungsgrafik, Skripte, Layouts)
  • Die Hypothesen (z.B. „könnte die Conversation eine Obertabelle sein?")
  • Den Pragmatismus („das reicht so, Mojibake-Reste fixen wir später")

Was die KI lieferte:

  • Pattern-Erkennung in unstrukturierten CSVs in Sekunden, nicht Stunden
  • Encoding-Sachkunde (MacRoman-Roundtrip, BOM-Stripping, FileMaker-spezifische Steuerzeichen)
  • Python-Skripte on-the-fly für Einmal-Aufgaben (Streaming, Header-Parsing, Cross-Reference)
  • Cross-Reference-Analysen zwischen den Archiven (welche IDs überlappen?)
  • Drei verschiedene Threading-Strategien sauber implementiert
  • Iteratives Debugging mit klaren Tests („zeig mir die erste echte Mail, schau ob Datum stimmt")
  • Doku & Blogpost am Ende — bei laufender Erinnerung an alle Sackgassen und Aha-Momente

Wäre der Entwickler ohne KI rangegangen: Ich schätze konservativ 5–10 Arbeitstage, mit deutlich mehr Frustrations-Spitzen. Insbesondere die Encoding-Forensik (MacRoman-Mojibake erkennen) und die statistische Spalten-Identifikation (welche Spalte ist welche FK?) sind ohne Tools mit großer NLP- und Pattern-Recognition-Erfahrung mühsam.

Mit KI: drei Tage, von denen die letzten beiden hauptsächlich für Tests und Feintuning draufgingen.

Was am Ende stand

Anzahl
Mailausgang-Datensätze importiert 44 257
MailEingang-Datensätze importiert 47 678
Gesamt-Mails im neuen System 91 935
Davon mit FK-Zuordnung (Kontakt + Customer) ~86 000
Mails in Konversations-Threads 69 300 (75 %)
Cross-Archive-Threads (Ausgang ↔ Eingang verbunden) 3 054
Verlorene Datensätze 0
Manuelle Klicks im Migrationsprozess ~30

Plus die Sicherheit, dass künftige Mails (per IMAP über die neue PHP-Mail-Pipeline) mit echtem RFC-Threading reinkommen und mit dem Legacy-Archiv kohärent zusammenleben.

Lessons Learned

1. Bei alten Datenexporten gibt es immer mehr Encoding-Probleme als man denkt. UTF-8-BOM, MacRoman-Mojibake, Windows-1252-Mojibake, FM-interne Vertical Tabs — das sind nicht „Exoten", das sind die Regel. Wer mit Legacy-Daten arbeitet, sollte die Reverse-Patterns parat haben oder eine KI dabei haben, die sie kennt.

2. „Streamingfähig denken" gewinnt. 3.5 GB CSV in 35 Sekunden durchstreamen statt 30 Minuten zu warten ist eine Frage von drei richtig gesetzten Python-Parametern. Wer das nicht weiß, optimiert tagelang an einer Bulk-Load-Lösung herum.

3. Heuristische Brücken sind oft besser als perfekte Spezifikation. Es gab keinen klaren ID-Mapping zwischen den Archiven. Die Lösung war nicht, eine perfekte Brücke zu rekonstruieren, sondern eine pragmatische heuristische über E-Mail-Adresse und Betreff. Sie ist nicht 100 % korrekt, aber sie macht aus 37 % Threading-Quote 75 %. Ein „gut genug" zur richtigen Zeit ist mehr wert als ein „perfekt" nie.

4. Iteratives Vorgehen mit kleinen Test-Imports rettet Stunden. Test mit 100 Datensätzen, prüfen, justieren, dann erst die 47 000. Wer beim ersten Versuch alle 47 000 importiert und dann merkt, das Empfangsdatum ist Modifikationsdatum, hat richtig schlechte Laune.

5. KI ist im Datenmigrations-Kontext ein massiver Hebel. Nicht weil sie irgendwas Magisches macht, sondern weil sie drei Dinge zusammenbringt, die selten zusammenkommen: Pattern-Recognition über große Datenmengen, breite Encoding/Format/Tool-Sachkunde, und die Geduld, dieselbe stupide Frage zum 47. Mal zu beantworten („zeig mir das Feld nochmal").


Stack:

  • Python 3 (csv, re, base64) — Streaming-Konverter
  • Git/Dropbox — Datenversionierung
  • FileMaker 22 — Zieldatenbank
  • PHP-Mailclient — eigene Pipeline für laufende Mails (siehe separater Blog-Post zur Plugin-Migration)

Zeitaufwand:

  • Tag 1: PHP-Migration MailIT → eigener Mailclient
  • Tag 2: Threading-Schema in FileMaker
  • Tag 3: Legacy-Import (das hier beschriebene)

Datenmenge:

  • 50 GB FileMaker-Originaldatei (nicht zugänglich)
  • 3.9 GB CSV-Exporte (zugänglich)
  • 161 + 345 MB FM-importbereite CSVs (am Ende)
  • 91 935 Mails in der neuen Datenbank