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: <abc@example.com> gehört zur Mail mit Message-ID: <abc@example.com>. 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 <F3R2X.MB3@PROD.OUTLOOK.COM> 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.jsonist 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 <F3R2X.MB3@PROD.OUTLOOK.COM> zu f3r2x.mb3@prod.outlook.com, 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:
In_Reply_Toleer → eigene Wurzel:thread_root_id = Message_ID,thread_depth = 0- Vorgänger ist über
T34_mail_MAIL||parenterreichbar → erbethread_root_idund setzethread_depth = parent.depth + 1 - Vorgänger fehlt → Fallback auf erste ID aus
References(per ExecuteSQL gegenMessage_ID_norm) - 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 (<img src="cid:xyz">) 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 = "<abc@…>" ] 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-imap5.x undPHPMailer6.x - Apache als HTTP-Frontend
- SQLite für die Mail-↔-FileMaker-Verknüpfung
- Bearer-Token aus
.env, Account-Cache indata/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