2021-02-25


Apple Notizen überall ansehen, bearbeiten und verwalten


Apple benutzt einen offenen Standard?

Das gibt es sogar wirklich. Wenn einen E-Mail Account in den Einstellungen eingetragen wird, kann man auch die Option aktivieren, dass Notizen angelegt werden können. Ab dem Moment kann man in der Notiz-App Notizen anlegen die auf einem E-Mail Server gespeichert werden.

Die meisten Features sind zwar leider ICloud exklusiv (TODO Listen, Bilder, Tabellen), wenn man sich mit Plain-Text und Listen zufrieden geben kann ist mit dem Tool gut bedient.

Was ich aber auch brauche ist eine Cross-Platform Lösung, Notizen nur am Mac oder iPhone bearbeiten zu können ist auf Dauer unpraktisch wenn man mal schnell etwas notieren will und gerade am Windows oder Linux Rechner sitzt. Da die Notizen wortwörtlich als E-Mail gespeichert werden, lassen sich diese auf den Plattformen zumindest mit einem E-Mail-Client ansehen, bearbeiten geht jedoch leider nicht.

Alternativen wie Joplin oder Evernote wollte ich erst einmal nicht testen, und einen Client für die anderen Betriebssysteme zu schreiben war ein ideales Projekt um mal etwas in Rust zu entwickeln, was ich mir schon lange vorgenommen habe.

=> Repo

Also war die TODO Liste für das Projekt:

  • Rust Basics lernen
  • Mitschneiden des Datenverkehrs von der Notes App von Apple um folgendes herauszufinden:
    • Welche IMAP Befehle werden benutzt
    • Wie werden Notizen erstellt / gelöscht / verschoben
    • Wie erkennen Clients, dass Notizen geändert wurden
    • Wie erkennen Clients Merge-Konflikte und wie reagieren sie darauf?
  • Eine Bibliothek schreiben die das Verhalten nachimplementiert
  • Ein Frontend schreiben, dass man als Anwender komfortabel benutzen kann

Wie funtioniert das

Das Datenformat

Der erste Schritt war einfach, in Thunderbird lässt man sich einfach den Quelltext einer Notiz anzeigen, wobei folgende Header zum Vorschein kamen:

Uniform-Type-Identifier: com.apple.mail-note
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable
Mime-Version: 1.0 (Mac OS X Notes 4.6 \(879.10\))
Date: Tue, 06 Apr 2021 10:29:00 +0000
X-Mail-Created-Date: Tue, 06 Apr 2021 10:29:00 +0000
From: user@email.de
Message-Id: <34EBAC1A-35AF-44B6-838E-1E7C7CA24EF3@email.de>
X-Universally-Unique-Identifier: 22B847EC-133D-4FD2-914F-D6FFBCAD2C55
Subject: Titel

Die wichtigsten Davon:

X-Universally-Unique-Identifier

Wie der Name sagt eine eindeutige ID der Notiz, diese ID ändert sich niemals. Es können aber mehrere E-Mails mit der selben UUID existieren, und zwar, wenn an mehreren Geräten die gleiche Notiz verändert wird. Erkennt eine Notiz-App beim Synchronisieren Duplikate, werden beide Notizen gespeichert und der Benutzer muss sie dann manuell mergen.

Message-ID

Ein eindeutiger Identifier einer bestimmten Version einer Notiz, durch diese ID ist es möglich zu erkennen, ob sich eine Notiz geändert hat. Dadurch lassen sich alle Regeln ableiten, die beim Synchronisieren gebraucht werden:

Lokale Message-IDEntferne Message-IDUpdate
NeuAltUpdate Lokal->Remote
AltNeuUpdate Remote->Lokal
NeuNeuMerge Konflikt, behalte beide Versionen

Der Inhalt der Email selbst ist HTML mit bestimmten Apple-Klassen mit der die "richtige" Notes App Listen und Texte formatiert werden. Sonderzeichen und HTML Steuerzeichen werden ganz normal escaped.

Kommunikation mit dem Mailserver

Um den Datenverkehr mitzuschneiden habe ich mit Docker einen lokalen Mailserver aufgesetzt und so konfiguriert, dass dieser unverschlüsselte Verbindungen zulässt. Danach habe ich mit der Notes App alle möglichen Aktionen ausgeführt und den Datenverkehr mit Wireshark mitgeschnitten.

Um neue Notizen anzulegen oder zu verändern wird der Imap APPEND Command benutzt. Dieser Befehl speichert die E-Mail in ein vorher mit dem SELECT Command ausgewählte Mailbox.

Danach werden noch Metadaten der E-Mail mit dem STORE Command verändert, in dem Fall wird das "Seen" Flag gesetzt, sodass die E-Mail in keinem E-Mail Programm als gelesen markiert wird.

Hat man mit dem APPEND Command eine bereits vorhandene Notiz aktualisiert muss die bereits vorhandene Notiz markiert und gelöscht werden. Dies geschieht auch mit dem STORE Command, mit dem das "Deleted" Flag gesetzt wird. Kurz danach wird der EXPUNGE Befehl abgesetzt, der alle mit dem "Deleted" Flag markierten E-Mails löscht.

Dies funktioniert jedoch nur, wenn es genau eine Notiz gibt, aber es können ja wie bereits erwähnt mehrere Notizen geben. Gibt es z.B. serverseitig 3 verschiedene Versionen einer Notiz, und der Benutzer hat alle 3 Konflikte aufgelöst und will nun die aktualisierte Version mit dem Server synchronisieren, müssen gleich 3 alte Notizen vom Server gelöscht werden.

Ein Lösungsansatz ist es mit dem UID SEARCH Command nach allen Notizen mit einer bestimmen UUID zu suchen, zurück kommt dann eine Liste aus UIDs (Nichts zu verwecheln mit dem uuid header), aus der man dann nur noch die neueste UID herausfiltern muss, um nicht die aktuellste Version der Notiz zu löschen. Die alten Notizen kann man dann mit dem STORE command flaggen und danach wieder expungen.

Das Rust Projekt

Das Projekt habe ich in 3 verschiedene Unterprojekte aufgeteilt:

  • Einer Bibliothek, die die Synchronisationslogik, sowie die Kommunikation mit dem Mail-Server implementiert und die lokalen Datenspeicherung verwaltet.
  • Einem Kommandozeilen Tool, mit dem man mit der Bibliothek interagieren kann, und mit einfachen Befehlen Notizen verwalten kann
  • Einer CLI-UI mit der man etwas komfortabler Notizen ansehen und verändern, sowie alle Notizen durchsuchen kann.

Herzstück der IMAP Implementierung ist das Imap Crate.

Die Notizen werden mit Diesel in einer Sqlite Datenbank gespeichert

Die Notizen werden vor dem Speichern in die Datenbank von HTML in Markdown konvertiert, damit man sie mit einem normalen Text-Editor bearbeiten kann. Die Konvertierung ist destruktiv und HTML spezifische Dekorationen wie Textfarben, Größe und Schriftart gehen dadurch leider verloren.

Nach dem Synchronisieren lassen sich Notizen nach belieben bearbeiten, löschen oder neu anlegen, ohne mit dem Server verbunden zu sein. Mit diesem wird sich nur mit dem "sync" command verbunden, der dann beide Seiten auf den aktuellen Stand bringt.

Kommt es zu Merge-Konflikten werden die betroffenen Notizen entsprechend markiert und müssen vor der nächsten Synchronisation aufgelöst werden. Notizen mit nicht aufgelösten Konflikten werden nicht synchronisiert.

Notizen kann man mit dem "Merge" Command zusammenführen, dafür wird ein neues, temporäres Textdokument mit einem Diff erstellt, der der User in einem Editor dann auflösen kann. Nach Beendigung werden die alten beiden Versionen gelöscht und eine neue Notiz mit dem zusammengeführten Inhalt erstellt.

Mit dem "Delete" Befehl werden die Notizen nur zum Löschen markiert, und erst beim nächsten Synchronisieren gelöscht, überlegt es sich davor noch einmal anders kann man mit dem "Undelete" Befehl die Notiz wieder demarkieren.

Grafische Oberflächen

Ich plane noch eine "richtige" grafische Oberfläche, aber bin mir momentan noch unsicher welche GUI Library ich verwenden will.

Merge Konflikte behandeln

soon(tm)