Claris FileMaker Pro
seen from United States
seen from China

seen from United States
seen from Russia
seen from India

seen from Malaysia

seen from China
seen from Germany

seen from Australia
seen from China
seen from China

seen from Singapore
seen from United States
seen from China
seen from Georgia
seen from China
seen from China

seen from Singapore
seen from China
seen from Singapore
Claris FileMaker Pro

Anya is live and ready to show you everything. Watch her strip, dance, and perform exclusive shows just for you. Interact in real-time and make your fantasies come true.
Free to watch âą No registration required âą HD streaming
Jessie Maple, a bacteriologist who took up filmmaking in the 1970s, became the first Black woman to join the camera operators union in New York and went on to direct trailblazing independent films. Maple turned first to journalism and then to film, working as a camera operator and a documentarian before releasing her first feature, âWillâ (1981), a family drama that she made for less than $12,000. Jessie Maple passed away at her home on 30 May 2023, in Atlanta. She was 86.
seit 1985
Die Post und das Konto
1985: Ich eröffne ein Konto bei der Post und kann jetzt mit der Postcard vom Automaten Geld beziehen. Besonders faszinierend: Auch aus Automaten in Spanien lÀsst sich mit derselben PIN (vierstellige Zahl) Bargeld holen, allerdings keine Schweizer Franken, sondern spanische Peseten.
1987: Ich komme aus dem Ausland zurĂŒck und lebe in einer Wohngemeinschaft. Ich kann mich vage daran erinnern, dass ich damals ein Konto bei der ZĂŒrcher Kantonalbank eröffnet habe, um die Miete ĂŒberweisen zu können. Aber ich kann mich nicht an das Vorgehen erinnern, das Molinarius und Tilman Otter im Techniktagebuch beschrieben haben. Ich habe nie auf der Post Geld bar abgehoben und auf einer Bank wieder eingezahlt oder umgekehrt. Das einzige Geld, das ich der Post bar einzahle, ist das FĂŒnflibergeld und das geht immer auf mein eigenes Konto. FĂŒr wiederkehrende Zahlungen richte ich DauerauftrĂ€ge ein; einmalige Zahlungen ĂŒberweise ich per Zahlungsauftrag. Vor dem Online-Zeitalter geht das so: Man entnimmt allen Rechnungen die beiliegenden Einzahlungsscheine, trennt die Girobelege ab, fĂŒllt noch nicht eingedruckte Felder manuell aus, zĂ€hlt alles zusammen, schreibt die Summe auf einen Zahlungsauftrag und steckt alle Belege zusammen mit dem Zahlungsauftrag in ein vorfrankiertes Couvert. Manchmal ist in den Vermischten Meldungen zu lesen, dass solche Couverts aus den BriefkĂ€sten gefischt und die Belege ausgetauscht werden. Ich selbst hatte mit dem Verfahren nie Probleme.
1995: Ich arbeite in einem alternativen Lokalradio, das ĂŒber die BeitrĂ€ge von rund 2'000 Vereinsmitgliedern finanziert wird. Wir stellen den Zahlungsverkehr um von âgrĂŒnenâ Einzahlungsscheinen auf âblaueâ. In diesem Zusammenhang höre ich zum ersten Mal des KĂŒrzel âESRâ (= Einzahlungsschein mit Referenznummer). ESR steht fĂŒr eine algorithmisch generierte Nummer auf dem Girobeleg des Einzahlungsscheins. Dank dieser Nummer lĂ€sst sich der Zahlungsverkehr automatisieren. Im Fall des Lokalradios mĂŒssen die Einzahlungen der Mitglieder nicht mehr manuell erfasst werden. Die Debitorenbuchhaltung wird massiv vereinfacht. Wir drucken nun individualisierte Einzahlungsscheine aus und erhalten von der Post regelmĂ€ssig Daten-Files, die wir in unsere Mitgliederverwaltung (eine 4D-Anwendung fĂŒr Apple) einlesen können. Wer bezahlt hat und wer noch nicht, lĂ€sst sich so ohne viel hĂ€ndische Arbeit kontrollieren. Probleme gibt es nur, wenn der Drucker die Referenznummer nicht sauber in das dafĂŒr vorgesehene Feld druckt.
ca. 1999: Aus Widerwillen gegenĂŒber dem Bankenwesen löse ich mein Bankkonto auf und regle meinen gesamten Verkehr ĂŒber das Konto der Postfinance. An Probleme beim Ăberweisen auf Bankkonten kann ich mich nicht erinnern. Das löst alles der ESR. Die Zahlungen gehen auf das Postkonto der jeweiligen Bank und von dort auf das Bankkonto der Rechnungsstellerin (siehe Abbildung).
2004: Ein Freund hat eine Möglichkeit (eine Filemaker-Anwendung fĂŒr Apple) entwickelt, mit der sich so genannte EZAG-Files erstellen lassen. EZAG steht fĂŒr âElektronischer Zahlungsauftragâ. Ich erfasse meine Zahlungen offline, exportiere das EZAG-File, logge mich im Yellownet, dem Portal der Postfinance, ein und lade dort das EZAG-File hoch. Damit erspare ich mir viel Ărger, den unstabile Internetverbindungen verursachen können, wenn man gerade dabei ist seine Zahlungen zu erfassen. Um mich im Yellownet einloggen zu können, muss ich meine Yellownet-Nummer eingeben, ein Passwort sowie ein Zahl aus einer Abstreichliste (siehe Abbildung).Â
Irgendwann wird dieses System umgestellt. Alle Postfinance-Kunden erhalten ein gelbes KĂ€stchen, in das man seine Postcard einschieben kann. Beim Einloggen gebe ich immer noch meine Yellownet-Nummer und ein Passwort ein, dann erhalte ich eine Nummer, die ich in das KĂ€stchen eingeben muss, worauf das KĂ€stchen das Passwort meiner Postcard einfordert, und mir dann eine Nummer ausgibt, die mir im Yellownet die TĂŒre zu meinem Konto öffnet.
2017: Ich habe arbeitgeberwechselbedingt keinen File-Maker mehr auf meinem Rechner und erfasse Zahlungen fortan online. Die Verbindungen sind inzwischen so stabil, dass das kein Problem mehr ist. Die App der Postfinance auf dem klugen Telefon nutze ich nur, um hin und wieder meinen Kontostand abzurufen.
(Franziska Nyffenegger)
Whether you need a simple composition retyped or a sermon transcribed, I will accommodate you to the specifics provided in accordance with the services offered.
# Volltext-Suche ĂŒber 100 000 Mails- und sie reagiert in 50 Millisekunden
Wie wir den klassischen FileMaker-WebViewer-Suchpatterns einen modernen Such-Backend untergeschoben haben, ohne die FileMaker-Datei selbst zu berĂŒhren. Mit einer SQLite-Datenbank, die schon im System lag. Mit einer Eingebauten von SQLite, die kaum jemand kennt. Und mit einer 2-Stunden-Implementierung, die jetzt fĂŒr DatenbestĂ€nde von 100 000 bis 10 Millionen Mails ausreicht, bei gleichbleibend sub-100-ms-Reaktion auf jeden Tastendruck.* --- ## Das Problem Im Mail-System eines Kunden lagen nach dem groĂen Legacy-Import knapp 96 000 Mails. Das vorhandene Suchpattern war ein bewĂ€hrter FileMaker-WebViewer-Trick: Eine **gespeicherte Berechnung** pro Datensatz baut ein HTML-Schnipsel (`
âŠ
`), eine zweite **ungespeicherte Berechnung** verknĂŒpft alle Schnipsel zu einer groĂen HTML-Seite mit Suchfeld, der WebViewer rendert sie als `data:`-URL, JavaScript filtert clientseitig per `data-search`-Attribut. Bei 5 000 DatensĂ€tzen ist das **magisch**: man tippt, die Liste filtert sich sofort, kein Server-Roundtrip, kein Lag. Bei 10 000 noch ok. Bei 92 000 stirbt es. Konkrete Zahlen aus der Diagnose: - `Liste()` ĂŒber 96 000 DatensĂ€tze: **10â30 Minuten** pro Aufruf (Oder FileMaker gibt auf) - Generiertes HTML: **~75 MB** als String - `data:`-URL: bei manchen WebKit-Versionen ĂŒber 64 MB einfach gekappt - 92 000 DOM-Knoten + JS `querySelectorAll` pro Tastendruck: **mehrere hundert Millisekunden** zĂ€hes Laggen Kurz: Das Pattern skaliert auf 5â10 k DatensĂ€tze. Bei 92 k bricht es. ## Die Optionen Drei klassische Wege: **A â Vorfilter.** Statt alle Mails zu zeigen, immer nur eine sinnvolle Teilmenge (letzte 90 Tage, Mails eines Kontakts). Reduziert auf <2 000 DatensĂ€tze, dann funktioniert das alte Pattern wieder. Pragmatisch, aber zwingt den User in eine starre Vorfilter-Logik. **B â Klassische FileMaker-Suche.** Suchfeld â ExecuteSQL â Ergebnismenge in Listenansicht. Skaliert auf Millionen, verliert aber das schöne Live-Filter-GefĂŒhl. **C â Server-side-Index.** Ein eigener Such-Endpoint im PHP-Backend, der gegen eine speziell optimierte Such-Datenbank fragt, JSON zurĂŒckliefert, vom WebViewer per Fetch konsumiert. Skaliert auf Millionen, behĂ€lt Live-Filter, kostet aber ein paar Stunden Aufbau. Variante C wĂ€re normalerweise Cadillac-Engineering. In dem Moment, als der Kunde fragte âhaben wir nicht schon eine SQLite im System?", wurde sie zur Anderthalb-Stunden-Lösung. ## Der entscheidende Aha-Moment Der PHP-Mailclient hatte schon eine SQLite-Datei. Genauer: `mailclient/data/links.sqlite`, eingerichtet fĂŒr die kleine VerknĂŒpfungstabelle `mail_links`, die Mail-Message-IDs auf FileMaker-EntitĂ€ten (Kontakte, Projekte, Konten) abbildet. PDO-Treiber konfiguriert, `db()`-Helper definiert, Schema-Initialisierung idempotent. Alles da. Und dann der zweite Aha: **SQLite hat seit Version 3.9 einen eingebauten Volltext-Index namens FTS5.** Eine virtuelle Tabelle, die Token-basierte Volltext-Suche ĂŒber Millionen DatensĂ€tze in <50 ms macht. Genau das, was Apple Spotlight nutzt. Kostenlos, ohne Plugin, ohne externen Index-Service. Ein schneller Check, ob FTS5 in der PHP-SQLite-Installation kompiliert ist: ```php $pdo = new PDO("sqlite::memory:"); $pdo->exec("CREATE VIRTUAL TABLE t USING fts5(x)"); $pdo->exec("INSERT INTO t VALUES ('hallo welt'), ('foo bar')"); $r = $pdo->query("SELECT x FROM t WHERE t MATCH 'hallo'")->fetchAll(PDO::FETCH_COLUMN); // â ['hallo welt'] ``` Ein âFTS5 verfĂŒgbar: ja" als Antwort, und das Projekt war von â2 Tage Aufwand" auf â2 Stunden" geschrumpft. ## Die Architektur ``` FileMaker WebViewer PHP-Mailclient SQLite +-------------------+ +-------------------+ +----------------+ | + JS | fetch(q=...) | /api/search_mails | SQL | mails_fts | | rendert Top 100 | <------------> | sucht im FTS5 | <-----> | (92k Mails) | +-------------------+ JSON +-------------------+ +----------------+ Tastendruck ^ | bei jedem neuen DS | /api/index_mail (von FM aufgerufen) ``` Drei neue PHP-Endpoints, eine FTS5-Tabelle, ein Python-Initial-Lauf, eine neue FileMaker-Berechnung mit ein paar Dutzend Zeilen JavaScript. Mehr nicht. ## Die FTS5-Tabelle ```sql CREATE VIRTUAL TABLE mails_fts USING fts5( pk_mail_id UNINDEXED, message_id UNINDEXED, betreff, body, absender, empfaenger, datum UNINDEXED, id_contacts UNINDEXED, id_account UNINDEXED, z_eingang UNINDEXED, thread_root_id UNINDEXED, tokenize = 'unicode61 remove_diacritics 2' ); ``` Zwei Details lohnen ErklĂ€rung: **`UNINDEXED`-Spalten** werden gespeichert, aber nicht in den Volltext-Index aufgenommen. Sie sind als reine Lookup- und Filter-Felder gedacht. So bleibt der Index kompakt: nur das, was wirklich durchsuchbar sein muss (`betreff`, `body`, `absender`, `empfaenger`), trifft den Tokenizer. **`remove_diacritics 2`** im Tokenizer ist der versteckte Held: er normalisiert alle Akzente und Umlaute. Eine Suche nach âmuller" findet âMĂŒller", âMueller" und âMĂŒller". Bei einem multilingualen Mail-Archiv mit deutschen, französischen, tĂŒrkischen Namen ist das nicht Nice-to-have, sondern FunktionalitĂ€t. ## Der Initial-Lauf Bei 96 000 Mails wĂ€re ein Datensatz-fĂŒr-Datensatz-Index-Update ĂŒber die FileMaker-Skript-Engine vermutlich ein Stundenprozess. Python machte es in **15 Sekunden**: ```python import sqlite3, csv conn = sqlite3.connect('mailclient/data/links.sqlite') conn.execute("PRAGMA journal_mode = WAL") conn.execute("PRAGMA synchronous = NORMAL") for csv_file in [mailausgang_csv, maileingang_csv]: batch = [] with open(csv_file, encoding='utf-8') as f: for row in csv.DictReader(f): batch.append(( row['Message_ID'], row['Betreff'], row['Body_Plain'], row['Absender_mailadresse'], row['Empfaenger'], # ⊠)) if len(batch) >= 1000: conn.executemany("INSERT INTO mails_fts (...) VALUES (?,?,...)", batch) conn.commit() batch = [] ``` 6 100 DatensĂ€tze pro Sekunde mit Batch-Inserts und WAL-Mode. Das Ergebnis: 91 935 Mails im Index, Datei-GröĂe der SQLite ~250 MB, Initial-Investment zum Aufbauen einmalig 15 Sekunden, danach fĂŒr immer verfĂŒgbar. ## Der Such-Endpoint PHP, gut 80 Zeilen Code. Der Kern: ```php $sql = " SELECT pk_mail_id, message_id, betreff, absender, empfaenger, datum, id_contacts, id_account, z_eingang, thread_root_id, snippet(mails_fts, 3, '', '', 'âŠ', 12) AS snippet FROM mails_fts WHERE mails_fts MATCH ? ORDER BY rank LIMIT 100 "; ``` `snippet()` ist eine eingebaute FTS5-Funktion: sie nimmt das Body-Feld (Index 3), markiert die Treffer-Worte mit ``-Tags und schneidet 12 Tokens links und rechts ringsum raus. Das Ergebnis sieht aus wie Gmail-Suchergebnisse: man tippt âheiratsurkunde", sieht ein paar Zeilen Body-Kontext mit gelb hervorgehobenem Treffer. `ORDER BY rank` sortiert nach FTS5-internem Relevanz-Algorithmus (BM25). Die hĂ€ufigste/beste Ăbereinstimmung erscheint oben. Kein Tuning, kein TF-IDF-Selbstbau nötig. Etwas mehr Sorgfalt verlangt das Mapping von **Benutzer-Eingaben in FTS5-Query-Syntax**: ```php $tokens = preg_split('/\s+/', $userInput); $clean = []; foreach ($tokens as $t) { $safe = preg_replace('/["\\\\:()*]/', '', $t); if (preg_match('/[@.\-]/', $safe)) { $clean[] = '"' . $safe . '"'; // E-Mail-Adresse als Phrase } else { $clean[] = $safe . '*'; // Prefix-Match fĂŒr "mueller" â "muellermann" } } $ftsQuery = implode(' ', $clean); ``` Drei Regeln: gefĂ€hrliche Sonderzeichen rausnehmen, Token mit `@`/`.` als Phrasen (sonst zerlegt FTS5 sie an Punkten), normale Wörter als Prefix-Match mit Stern (damit âmueller" sofort matcht, sobald 7 Zeichen getippt sind, ohne dass der User ENTER drĂŒcken muss). ## Das Frontend Im WebViewer: ```javascript let timer = null; function suche(q) { clearTimeout(timer); timer = setTimeout(() => doFetch(q), 200); } function doFetch(q) { if (!q || q.trim().length < 2) { document.getElementById('liste').innerHTML = '
Mindestens 2 ZeichenâŠ
'; return; } fetch(`${API}?q=${encodeURIComponent(q)}&limit=100&token=${TOKEN}`) .then(r => r.json()) .then(d => render(d)); } ``` Zwei Details: **Debouncing mit 200 ms.** Tippt der User âmueller", wird nicht fĂŒr jeden Buchstaben ein Request abgesetzt. Erst wenn 200 ms keine neue Eingabe kommt, geht der Fetch los. Das schont den Server und ist auf modernen Maschinen subjektiv noch immer âsofort". **Race-Condition-Schutz mit `lastReq`-ID.** Wenn der User schnell tippt und der erste Request langsamer zurĂŒckkommt als der zweite, wĂŒrde die Liste sonst kurz die alte Antwort zeigen. Ăber einen lokalen ZĂ€hler werden veraltete Antworten verworfen. ## Die CORS-Falle Beim ersten Test im WebViewer: nichts. âLoad failed". Ein typisches Browser-Mysterium, das eine Stunde Diagnose verschlingt, wenn man's nicht kennt. ErklĂ€rung: Der WebViewer lĂ€dt seine HTML-Seite per `data:`-URL. Aus Browser-Sicht hat eine `data:`-URL als Origin den Wert `null`. Macht der eingebettete JavaScript-Code dann einen `fetch()` an einen HTTP-Server, prĂŒft der Browser CORS (Cross-Origin Resource Sharing). Liefert der Server keine `Access-Control-Allow-Origin: *`-Header, wird die Antwort blockiert â der `fetch()` erhĂ€lt einen generischen âLoad failed"-Fehler ohne nĂ€here ErklĂ€rung. Fix: zwei Zeilen im `json_response()`-Helper: ```php header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET, POST, OPTIONS'); ``` Sofort lief alles. Eine dieser Sachen, die man einmal gelernt hat und nie wieder vergisst, aber bis dahin Stunden kosten kann. ## Performance â die echten Zahlen Ăber 96 935 Mails, gemessen direkt am Server, dann inklusive HTTP-Roundtrip aus dem WebViewer: | Such-Typ | SQL-Zeit | HTTP-Roundtrip | |---|---:|---:| | Volltext âmueller*" | 4 ms | 59 ms | | Volltext âheiratsurkunde" | 4 ms | 55 ms | | Volltext + Filter (z_eingang=1) | 5 ms | 34 ms | | Phrasen-Suche âaicher.com" | 4 ms | 42 ms | Der HTTP-Roundtrip ist dabei der Löwenanteil. Die eigentliche FTS5-Query liegt **unter 10 ms** â auch bei Millionen DatensĂ€tzen wĂŒrde sich daran wenig Ă€ndern. Vergleich mit dem alten Pattern: | | Liste-basierter WebViewer | FTS5-Backend | |---|---|---| | Initiales Rendering | 10â30 m | <100 ms | | Tastendruck-Reaktion | 100â500 ms (DOM-Filter) | 50â100 ms (HTTP) | | HTML-GröĂe initial | ~75 MB | ~1 KB pro Request | | Skalierungsgrenze | ~10 000 DS | 10 Mio+ | | Body-Volltext-Suche | begrenzt, kein Stemming | voll, mit Diacritics-Normalisierung | | Sortierung | nach Anlegen | nach Relevanz (BM25) | | Snippet-Hervorhebung | nein | ja, mit `` | | Sonderzeichen/Umlaut-Toleranz | nein | ja | ## Wartung Damit der Index nicht veraltet, muss er bei jeder neuen oder geĂ€nderten Mail aktualisiert werden: - **Mails per IMAP abholen**: das FileMaker-Skript ruft nach jedem neuen Datensatz `Mail indizieren` auf, der schickt den DS per POST an `/api/index_mail.php` - **Mails senden**: macht der PHP-Endpoint selbst nach erfolgreichem Versand - **Manuelle Ănderungen** in FileMaker (Betreff editiert, Body angepasst): per Skript-Trigger OnRecordCommit â `Mail indizieren` Bei Inkonsistenzen genĂŒgt ein Reset: Python-Script neu laufen lassen, leert den Index, baut ihn neu auf. 15 Sekunden. ## Bilanz Was am Ende steht: - **Live-Volltext-Suche ĂŒber 96 935 Mails** mit <100 ms Antwortzeit - **Sub-50-ms-Performance auf der DB-Ebene** â wĂŒrde auch bei 10 Mio DatensĂ€tzen noch unter 100 ms bleiben - **Snippet-Hervorhebung** mit Kontext-Wörtern um den Treffer herum - **Umlaut- und Akzent-tolerant**: âmuller" findet âMĂŒller" - **Filter-Kombinationen**: Volltext + Kontakt + Richtung + Thread (ĂŒber zusĂ€tzliche URL-Parameter) - **Skaliert** auf jeden Datenbestand, der realistisch in FileMaker liegt - **Wartung minimal**: PHP-Endpoint indiziert neue Mails automatisch, Python-Script setzt bei Bedarf alles zurĂŒck Das Pattern ist breit ĂŒbertragbar. Ăberall dort, wo FileMaker mit groĂen Datenmengen Live-Suche bieten soll â Kontakte, Aufgaben, Dokumente, Logs â ist FTS5 in einer Helper-SQLite die richtige Antwort. Die FM-Datei selbst bleibt unangetastet, die Suche lĂ€uft daneben, der WebViewer ist die BrĂŒcke. ## Was als NĂ€chstes kĂ€me Drei mögliche Erweiterungen, die diese Lösung von âfunktional" zu âbrillant" heben wĂŒrden: 1. **Server-side Pagination**: aktuell 100 Treffer pro Anfrage. Bei einer â1000 Treffer fĂŒr muller" könnte âMehr laden"-Button mit `offset`-Parameter ergĂ€nzt werden. 2. **Faceted Search**: Aggregations-Endpoint, der fĂŒr eine Such-Query liefert âX Treffer in Inbox, Y in Sent, Z bei Kontakt A". Bietet sofortige Verfeinerungs-Optionen. 3. **Synonyms-Wörterbuch**: fĂŒr GeschĂ€ftsbegriffe Synonyme definieren (âRechnung" matcht auch âInvoice"). FTS5 unterstĂŒtzt das ĂŒber Custom-Tokenizer. Aber: nichts davon braucht es heute. Sub-100-ms-Volltextsuche ĂŒber 96 000 Mails reicht fĂŒr die nĂ€chsten Jahre. Das Pattern ist offen, falls je gebraucht. --- **Stack:** - SQLite 3.x mit FTS5 (in praktisch jeder modernen PHP-Installation enthalten) - PHP 8.x, PDO-SQLite-Treiber - FileMaker WebViewer mit `data:`-URL + JavaScript-Fetch - Python 3 fĂŒr den einmaligen Initial-Index-Lauf **Code-Umfang nach Implementierung:** - `lib/helper.php`: +40 Zeilen (FTS5-Tabelle + `index_mail()`-Helper) - `api/search_mails.php`: ~100 Zeilen (komplett neu) - `api/index_mail.php`: ~40 Zeilen (komplett neu) - `/tmp/initial_index_mails.py`: ~80 Zeilen (einmalig) - FileMaker-Berechnung `wv_html_search_fts`: ~80 Zeilen JavaScript+HTML in einer FM-Calc - FileMaker-Skript `Mail indizieren`: ~12 Steps

Anya is live and ready to show you everything. Watch her strip, dance, and perform exclusive shows just for you. Interact in real-time and make your fantasies come true.
Free to watch âą No registration required âą HD streaming
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.* --- ## 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
Wie wir die FileMaker-Mail-Lösung von einem Plugin befreit haben
# Wie wir die FileMaker-Mail-Lösung von einem Plugin befreit haben *Eine kleine Migrations-Geschichte: von Dacons MailIT zu einer eigenen PHP-Mail-Pipeline mit echtem Threading, ohne Plugin-Lizenzen, ohne lokalen Mini-Webserver, ohne Vendor-Lock-in. Was als âder Kunde will Konversations-Ansicht" begann, wurde zur kompletten Neuverdrahtung der Mail-Schicht. Mit ĂŒberraschend wenig Code und einigen Erkenntnissen, die wir hier teilen.* --- ## Ausgangslage: ein Plugin, das alles entscheidet Der Kunde hatte eine FileMaker-Mailtabelle mit gut 49.000 DatensĂ€tzen. Eingehende und ausgehende Mails, AnhĂ€nge in zwölf globalen Container-Slots, VerknĂŒpfungen zu Kontakten, Projekten und Konten alles solide gewachsen. Und alles durchwoben mit dem Plugin **Dacons MailIT**. MailIT ist ein in die Jahre gekommenes, aber durchaus mĂ€chtiges Plugin. Es bringt IMAP, POP3, SMTP, sogar einen kleinen lokalen Webserver mit (fĂŒr die Inline-Bilder-Darstellung im WebViewer) und das in einer fertigen Beispieldatenbank, die viele Kunden direkt als Basis ĂŒbernehmen. Das war hier auch passiert: die DB-Struktur stammte aus der âMailit Ticket"-Demo. Der Wunsch des Kunden klang harmlos: **âIch möchte, dass zusammengehörige Mails als Konversation angezeigt werden."** Klingt nach âein bisschen Sortierung". Wurde zum Anlass fĂŒr eine vollstĂ€ndige Plugin-Befreiung. ## Die Diagnose: Threading geht so nicht Threading auf Mail-Ebene heiĂt: aus losen DatensĂ€tzen wird eine Konversation. Eine eingehende Mail mit `In-Reply-To: ` gehört zur Mail mit `Message-ID: `. Mehrere Antworten auf eine Antwort bauen einen Baum. Mail-Clients wie Apple Mail oder Outlook lösen das seit zwanzig Jahren â in FileMaker bauen wir uns das selbst. Das Schema hatte schon `Message_ID` und `In_Reply_To`. Reicht fĂŒr eine Stufe RĂŒckwĂ€rts. FĂŒr tiefe Konversationen reicht das nicht â dafĂŒr braucht's den `References`-Header, der nach RFC 5322 die ganze VorgĂ€nger-Kette mitschleppt. Und der war nicht da. Plugin lieferte ihn auch nicht. AuĂerdem: das Match-Feld in der DB hieĂ `Message_id_suche` und war definiert als `LiesAlsZahl ( Message_ID )`. Bei einer Message-ID wie `` ergibt das **`3`**. Dreiundsiebzig Mails matchen damit munter zusammen. Bug seit Jahren unentdeckt â weil bisher niemand drĂŒber gestolpert ist, dass es nicht funktioniert. ## Die Idee: ein PHP-Mailclient gibt's schon Wir hatten in einem anderen Projekt einen kleinen PHP-Mailclient liegen â IMAP ĂŒber `webklex/php-imap`, SMTP ĂŒber PHPMailer, eine SQLite-Tabelle fĂŒr die VerknĂŒpfung Message-ID â FileMaker-EntitĂ€t, ein paar HTML-Seiten fĂŒr den WebViewer. War als Prototyp gedacht (dann als AddOn), lag aber zu rund 70 Prozent funktionsfĂ€hig herum. Die Idee schrieb sich von selbst: - **MailIT komplett raus.** Keine Plugin-Lizenz mehr, keine MailIT-Templates, keine âPlug-in not installed"-Dialoge. - **PHP ĂŒbernimmt die Mail-Mechanik.** Senden, Abholen, AnhĂ€nge, Folder-Listing â alles per JSON-API. - **FileMaker bleibt die Heimat der Daten.** Anzeige, Beziehungen, Threading-Logik, VerknĂŒpfungen mit dem CRM â bleibt nativ. Die Architektur: ``` FileMaker (PRJ_CRM.fmp12) PHP-Mailclient +------------------+ +--------------------------+ | TAB_Mail | ââ JSON-Import ââ | /api/list_inbox_messages | | TAB_MailAccount | ââ JSON-Send âââ | /api/send_mail | | Skripte/Layouts | ââ Anhang holen â | /attachment.php | +------------------+ +--------------------------+ â â âââ data/accounts.json âââ sync âââ TAB_MailAccount â IMAP/SMTP Server ``` ## Die zentrale Architektur-Entscheidung: Stateless, aber pragmatisch Klassisch hat man zwei Wahlmöglichkeiten: **A) Reines Stateless** â jeder API-Aufruf bringt die Server-Credentials mit. Sauber, aber FileMaker mĂŒsste bei jedem Klick die Account-Daten extrahieren, in ein JSON packen und mitschicken. Bei einem WebViewer-Aufruf (`view.php?...`) geht das nicht â der WebViewer kann nur GET-URLs absetzen, ohne JSON-Body. **B) Session-basiert** â Login-Endpoint, Session-Token, ablaufende Tokens. Sicher, aber erheblich mehr Code, vor allem auf FileMaker-Seite. Wir entschieden uns fĂŒr eine dritte Variante, die in der Praxis am unauffĂ€lligsten ist: **Account-Cache in PHP, gefĂŒllt aus FileMaker.** FileMaker pflegt die Mail-Accounts in einer eigenen Tabelle `TAB_MailAccount` (mit IMAP-Host, SMTP-Port, Username, Passwort, allem). Beim Speichern eines Accounts in FileMaker wird per Skript einmalig `POST /api/sync_account.php` aufgerufen. Der PHP-Server cached die Account-Daten in einer `data/accounts.json`. Ab dann reicht in jedem API-Aufruf eine `account_id` â PHP resolvt sie selbst. Vorteile: - FileMaker muss Credentials nur **einmal** synchronisieren, nicht bei jedem Klick. - HTML-Views (`view.php`, `attachment.php`) funktionieren mit GET-Parametern. - Die Quelle der Wahrheit bleibt FileMaker â `accounts.json` ist nur Cache. - Wechsel des Mail-Passworts: Wert in FileMaker Ă€ndern, Sync-Knopf, fertig. Kein Code-Deploy, kein PHP-Anfassen. FĂŒr die Authentifizierung selbst genĂŒgt ein **statischer Bearer-Token**, der in einer `.env` auf PHP-Seite und einer Custom Function `CF_API_Token` auf FileMaker-Seite hinterlegt ist. JWT wĂ€re Overkill fĂŒr eine Backoffice-Lösung. ## Threading: das eigentliche HerzstĂŒck Der ursprĂŒngliche Wunsch â die Konversationen â bekam jetzt das passende Datenmodell. Vier neue Felder in `TAB_Mail`: | Feld | Zweck | |---|---| | `References` | Komma-getrennte Liste aller VorgĂ€nger-Message-IDs aus dem RFC-5322-Header | | `thread_root_id` | Die Message-ID der Wurzel des Threads â alle Mails desselben Threads tragen denselben Wert | | `thread_depth` | EinrĂŒckungstiefe, 0 = Wurzel, sonst VorgĂ€nger + 1 | | `thread_seq` | Sortierreihenfolge innerhalb des Threads (fĂŒr spĂ€ter) | Plus drei **normalisierte Match-Felder** fĂŒr die Self-Joins: ``` Kleinbuchstaben ( Trimme ( Substitute ( Message_ID ; [ "<" ; "" ] ; [ ">" ; "" ] ; [ " " ; "" ] ; [ "¶" ; "" ] ) ) ) ``` `Message_ID_norm`, `In_Reply_To_norm`, `thread_root_id_norm` â alle indiziert. Damit wird `` zu `[email protected]`, sauber gegen GroĂschreibung, Klammern, Whitespace. Drei Self-Join-TOs: | Tabellenokkurrenz | Beziehung | Liefert | |---|---|---| | `T34_mail_MAIL\|\|parent` | `In_Reply_To_norm = Message_ID_norm` | direkten VorgĂ€nger (max. 1 DS) | | `T34_mail_MAIL\|\|replies` | `Message_ID_norm = In_Reply_To_norm` | alle Direkt-Antworten (0..n) | | `T34_mail_MAIL\|\|thread` | `thread_root_id_norm = thread_root_id_norm` | alle Mails desselben Threads | Das **Skript âMail einreihen"** lĂ€uft nach jedem neuen Datensatz und kennt vier Regeln, in dieser Reihenfolge: 1. `In_Reply_To` leer â eigene Wurzel: `thread_root_id = Message_ID`, `thread_depth = 0` 2. VorgĂ€nger ist ĂŒber `T34_mail_MAIL||parent` erreichbar â erbe `thread_root_id` und setze `thread_depth = parent.depth + 1` 3. VorgĂ€nger fehlt â Fallback auf erste ID aus `References` (per ExecuteSQL gegen `Message_ID_norm`) 4. Komplett verwaist â eigene Wurzel Was hier passiert, ist klassische Hierarchie-Synthese auf Datensatz-Ebene. Der Witz: das Skript lĂ€uft **ohne Mehrfach-DurchgĂ€nge** auf einem importierten Datensatz, weil der VorgĂ€nger bei chronologischer Reihenfolge (Ă€lteste zuerst) bereits korrekt einsortiert war. FĂŒr den seltenen Fall, dass eine Antwort vor ihrem VorgĂ€nger ankommt, gibt's das `Threading_Backfill`-Skript, das iterativ bis zur Konvergenz lĂ€uft. ## Die schönste Ăberraschung: kein Webserver nötig MailIT hatte einen lokalen HTTP-Server, der bei jedem FileMaker-Start auf einem konfigurierbaren Port hochfuhr. Zweck: Inline-Bilder in HTML-Mails (``) im WebViewer anzeigen können. Der WebViewer braucht eine URL â MailIT lieferte sie ĂŒber `localhost:Port`. Das war wackelig: Port-Konflikte, fehlende Berechtigungen, der berĂŒhmte âFailed to start local web server"-Dialog. In unserer neuen Lösung war dieser ganze Webserver-Komplex schlicht **ĂŒberflĂŒssig**. Wir haben einen echten Webserver, auf dem der PHP-Mailclient ohnehin lĂ€uft. Inline-Bilder werden entweder direkt als `data:`-URLs ins HTML eingebettet oder ĂŒber `attachment.php` ausgeliefert â der WebViewer holt sie sich wie jedes andere Bild. Allein dieses Detail wĂŒrde eine Plugin-Migration rechtfertigen. Es waren zwei `Email_Send_*`-Felder weniger im Schema, eine ganze Skript-Sektion âStart/Stop Web Server" entfernt, und keine mysteriösen Port-Konflikte mehr. ## Eine ehrliche Falle: Auto-Eingabe-Berechnungen Drei Tage lang lief das System scheinbar perfekt. Dann fiel auf: nach jedem Mail-Abruf wurden die seit gestern gesendeten Mails wieder mit-importiert. Doppelt, dreifach. Der Doppel-Match auf `Message_ID_norm` griff nicht. Diagnose: das `Message_ID_norm`-Feld war bei neu angelegten DatensĂ€tzen **leer**. Warum? In FileMaker hat eine Auto-Eingabe-Berechnung standardmĂ€Ăig die Option **âVorhandenen Feldwert nicht ersetzen"** angehakt. Das klingt harmlos, ist aber tĂŒckisch: beim Anlegen eines neuen Datensatzes wird die Berechnung **einmal** ausgefĂŒhrt â mit leerem Quellfeld. Ergebnis: leeres Norm-Feld. Wenn das Skript danach `Feldwert setzen [ Message_ID = "" ]` macht, greift die Berechnung **nicht mehr**, weil der Wert âschon vorhanden" ist (auch wenn er leer war). Lösung: HĂ€kchen raus. Dann rechnet FM die Auto-Eingabe bei jeder Ănderung am Quellfeld neu. Klassische Falle, von der man als FileMaker-Entwickler nie genug Schaden nehmen kann. ## Was am Ende stand Nach gut anderthalb Tagen Migration: - **MailIT komplett raus.** Keine Plugin-AbhĂ€ngigkeit, keine LizenzgebĂŒhren pro Client, keine plugin-spezifischen Skripte mehr. - **Plugin-frei auch im Mehr-Client-Betrieb.** Der PHP-Mailclient lĂ€uft einmal zentral. Mehrere FileMaker-Clients teilen ihn â auch FileMaker Server, WebDirect oder iOS Go funktionieren ohne Anpassung. - **Echtes Threading** mit Self-Joins, EinrĂŒckung, Reply-Indikatoren (â©/âȘ/âł) und einer Detail-Ansicht, die die ganze Konversation chronologisch zeigt. - **Multi-Account-fĂ€hig** ohne Code-Ănderung. Accounts kommen aus FileMaker, neue PostfĂ€cher brauchen nur einen Datensatz. - **OAuth-ready** â die Architektur ist offen fĂŒr XOAUTH2 (Gmail, Microsoft 365), wenn ein Kunde das braucht. Heute nicht aktiv, aber morgen kein Architektur-Eingriff mehr. - **Mehrere To-EmpfĂ€nger** funktionieren sauber, ein- wie ausgehend. - **Robuste Anhang-Verarbeitung** ĂŒber eindeutige `imap_folder + imap_uid + att_index`-Adressierung statt fragiler Message-ID-Suche. ## Lessons Learned Drei Sachen wĂŒrde ich aus dieser Migration mitnehmen: **1. Plugin-Befreiung lohnt sich oft erst auf den zweiten Blick.** Der ursprĂŒngliche Anlass (Threading) hĂ€tte auch innerhalb von MailIT mit KrĂŒcken gelöst werden können. Aber sobald man den Schritt einmal geht, fallen reihenweise andere Probleme weg, die man als ânormal" akzeptiert hatte: der Webserver-Dialog, die Lizenzpflege, die Limitierung auf einen Account, die fehlende OAuth-Option. Eine Migration zahlt sich oft erst durch die AufrĂ€umarbeit aus, die man nebenbei mitmacht. **2. Eine PHP-Bridge ist im FileMaker-Kosmos unterschĂ€tzt.** âAus URL einfĂŒgen" mit cURL-Optionen ist mĂ€chtig â Bearer-Tokens, POST mit JSON-Body, alles geht. Wer mit FileMaker eine Datenquelle anbinden will, die kein klassisches Plugin hat (Stripe, GitHub, eine interne API), sollte den PHP-Sandwich-Ansatz im Hinterkopf haben. Schneller geschrieben als ein eigenes Plugin, einfacher zu testen, und unabhĂ€ngig von der FileMaker-Version. **3. Normalisierte Match-Felder gehören in jedes Schema mit Fremd-IDs.** Egal ob Mail-IDs, Telefonnummern, IBANs, ISBN: das Original-Feld bleibt, ein `_norm`-Feld als indizierte Auto-Eingabe-Berechnung daneben, und der Match lĂ€uft darĂŒber. Spart langfristig die seltsamen Bugs, bei denen ein User mal mit GroĂbuchstaben tippt und nichts gefunden wird. --- **Stack im Ăberblick:** - FileMaker 22 mit deutscher Lokalisierung - PHP 8.3 mit `webklex/php-imap` 5.x und `PHPMailer` 6.x - Apache als HTTP-Frontend - SQLite fĂŒr die Mail-â-FileMaker-VerknĂŒpfung - Bearer-Token aus `.env`, Account-Cache in `data/accounts.json` - Keine Browser-Frameworks, kein npm, kein Build-Schritt **Code-Umfang nach der Migration:** - `mailclient/`: rund 1 200 Zeilen PHP, davon etwa die HĂ€lfte aus dem Prototyp ĂŒbernommen - FileMaker: 5 neue Skripte (`Mails abholen`, `Mail senden`, `Mail beantworten`, `Mail einreihen`, `Threading Backfill`), eine neue Tabelle (`TAB_MailAccount`), 7 neue Felder, 3 neue Beziehungen - Aufwand inklusive Schema-Refactor, PHP-Anpassungen und Tests: ungefĂ€hr zwölf Stunden, verteilt ĂŒber drei Arbeitstage
Hierarchische Darstellung im FileMaker WebViewer, ganz ohne Script
# Hierarchische Darstellung im FileMaker WebViewer, ganz ohne Script **Artikel mit Einzelteilen in einem WebViewer anzeigen, aufklappbar, farblich hervorgehoben, und das Ganze nur mit zwei Formelfeldern.** --- Wer in FileMaker schon mal versucht hat, Daten aus zwei Tabellen in einem Portal darzustellen, kennt das Problem: Ein Portal zeigt immer nur Datensaetze einer Bezugstabelle. Artikel und deren Einzelteile in einer gemeinsamen, hierarchischen Liste? Geht nicht, zumindest nicht mit Bordmitteln (Geht schon, der Aufwand ist aber hoch). Die Ausgangslage, aus Artikel möchte ich die zugehörigen Katalogteile sehen. Die gaengigen Workarounds wie Hilfstabellen, Virtual Lists oder Anzeigetabellen funktionieren, bringen aber alle ihren eigenen Overhead mit. Wir haben uns fuer einen anderen Weg entschieden: **HTML im WebViewer, komplett generiert durch FileMaker-Formelfelder**. ## Das Ergebnis Der WebViewer zeigt eine kompakte, aufklappbare Stueckliste: - **Artikel** als farbige Header-Zeilen mit Bezeichnung, Menge, Preis und Kategorie - **Einzelteile** darunter eingerueckt, per Klick auf- und zuklappbar - **Farbliche Zuordnung**: Artikel des aktuell ausgewaehlten Produkts erscheinen in Gold, alle anderen in dezentem Grau Das Ganze aktualisiert sich automatisch, ohne Script-Trigger, ohne Ladevorgang. ## Die Idee Statt Daten per Script in einen WebViewer zu schaufeln, lassen wir FileMaker das HTML direkt in einem Formelfeld zusammenbauen. Der WebViewer zeigt dieses Feld einfach an. Aendert sich ein Datensatz, berechnet FileMaker das Feld neu, der WebViewer zeigt den aktuellen Stand. Dafuer braucht es genau zwei Formelfelder und einen WebViewer auf dem Layout. ## Aufbau ### Formelfeld 1: Einzelteile als HTML In der Artikel-Tabelle liegt ein ungespeichertes Formelfeld. Es holt sich per `List()` die Bezeichnungen und Mengen aller zugehoerigen Einzelteile und gibt sie als fertige HTML-Divs zurueck: ``` While ( [ ~bez = List ( Einzelteile::bezeichnung ) ; ~men = List ( Einzelteile::menge ) ; ~anz = ValueCount ( ~bez ) ; ~i = 1 ; ~html = "" ] ; ~i <= ~anz ; [ ~html = ~html & "
" & GetValue ( ~bez ; ~i ) & "" & GetAsNumber ( GetValue ( ~men ; ~i ) ) & "x" & "
" ; ~i = ~i + 1 ] ; ~html ) ``` Das Ergebnis ist ein Textblock wie: ```html
Schraube M6x204x
Duebel S84x
``` Ein wichtiger Punkt: Der Output darf keine Zeilenumbrueche enthalten. `List()` im uebergeordneten Feld trennt nach Absatzmarken â mehrzeilige Werte wuerden das Mapping durcheinanderbringen. ### Formelfeld 2: Das komplette HTML In der Angebots-Tabelle sitzt das zweite Formelfeld. Es sammelt per `List()` alle Artikeldaten ein â Bezeichnung, Menge, Preise, Kategorie und das HTML-Feld der Einzelteile â und baut daraus ein vollstaendiges HTML-Dokument mit eingebettetem CSS und JavaScript. Der Kern ist eine `While`-Schleife, die jeden Artikel durchgeht: ``` // Fuer jeden Artikel pruefen: gehoert er zum aktiven Produkt? ~cls = If ( ~prod = ~aktivID ; "aktiv" ; "inaktiv" ) ``` Aktive Artikel bekommen die CSS-Klasse `aktiv` und damit den goldenen Hintergrund. Inaktive bleiben grau. Gleichzeitig werden aktive Artikel standardmaessig aufgeklappt angezeigt, inaktive zugeklappt. ### WebViewer Der WebViewer auf dem Layout bekommt als URL einfach: ``` "data:text/html," & Angebot::wv_html_artikel_teile ``` Keine externe Datei, kein Script, kein Nachladen. ## Das Auf- und Zuklappen Ein kleiner JavaScript-Block im HTML sorgt dafuer, dass Klicks auf die Artikel-Zeile die zugehoerigen Einzelteile ein- oder ausblenden. Der Pfeil dreht sich dabei mit â eine CSS-Transition auf `transform: rotate(90deg)`, gesteuert ueber eine Klasse. ```javascript function toggle(el) { var gruppe = el.closest('.gruppe'); var teile = gruppe.querySelector('.teile'); var pfeil = el.querySelector('.toggle'); if (teile) teile.classList.toggle('offen'); if (pfeil) pfeil.classList.toggle('offen'); } ``` Die Animation laeuft ueber `max-height` mit einer CSS-Transition â schlank und ohne externe Bibliotheken. ## Warum kein Script? Der naheliegende Weg waere: Ein Script baut JSON auf, uebergibt es per `Perform JavaScript in Web Viewer` an eine `loadData()`-Funktion im HTML. Das funktioniert, hat aber einen Nachteil: Man braucht Script-Trigger. Bei jedem Datensatzwechsel, nach jedem Aendern, nach jedem Loeschen muss das Script erneut laufen. Mit Formelfeldern passiert das automatisch. FileMaker wertet ungespeicherte Formelfelder bei jedem Zugriff neu aus. Der WebViewer zeigt immer den aktuellen Stand, ohne dass jemand ein Script anstossen muss. Weiterer Hintergrund, da ich ĂŒber Referenzen arbeite, werden niemals alle Formeln berechnet und FileMaker lĂ€uft weiterhin flĂŒssig. ## Was man noch draufsetzen kann Die Basis steht, aber das HTML im WebViewer laesst sich beliebig erweitern: **Rueckgaben an FileMaker:** Mit `FileMaker.PerformScript()` im JavaScript kann ein Klick auf einen Artikel oder ein Einzelteil ein FileMaker-Script ausloesen. So lassen sich Detail-Ansichten oeffnen, Datensaetze ansteuern oder Bearbeitungsdialoge starten â der WebViewer wird damit zur interaktiven Oberflaeche. **Farbcodes visuell darstellen:** Statt den Farbcode als Text anzuzeigen, kann man ihn als farbigen Punkt rendern. Ein kleiner CSS-Kreis mit dynamischer `background-color`, gespeist aus einer Zuordnungstabelle oder direkt als Hex-Wert. **Suchfeld mit Live-Filter:** Ein Eingabefeld im WebViewer, das per JavaScript die angezeigten Artikel und Teile in Echtzeit filtert. Keine erneute Berechnung noetig â das HTML ist schon da, JavaScript blendet nur aus. **Drag & Drop fuer Sortierung:** Mit etwas mehr JavaScript lassen sich Artikel per Drag & Drop umsortieren und die neue Reihenfolge per Callback an FileMaker zurueckgeben. **Inline-Editing:** Mengen oder Preise direkt im WebViewer aendern, ohne in ein FileMaker-Feld wechseln zu muessen. Der geaenderte Wert geht per `FileMaker.PerformScript()` zurueck in die Datenbank. ## Fazit Der WebViewer ist in FileMaker oft ein unterschaetztes Werkzeug. Fuer hierarchische Darstellungen, die mit Portal-Bordmitteln nicht machbar sind, bietet er eine elegante Loesung. Der Ansatz ueber Formelfelder haelt die Komplexitaet niedrig: kein Script-Management, keine Lade-Logik, keine Synchronisationsprobleme. Zwei Formelfelder, ein WebViewer, fertig. --- *Ronny Schrader â MaRo-Programmierung GbR* *FileMaker-Entwicklung seit ueber 30 Jahren*