# 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.* --- ## 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.csv`** â **3.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: "[email protected]" [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: ```python "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: ```python 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: [email protected] 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