E‑Mails effizient in Obsidian integrieren

Dieses Bild dient nur Illustrationszwecken! Auf dem Bild ist ein Schreibtisch mit mehreren Bürogeräten und -utensilien zu sehen. Ein großer Monitor zeigt Codezeilen an und ist von verschiedenen Notizen umgeben. Daneben steht ein Laptop, auf dem auch Dokumente angezeigt werden. Es gibt eine Schreibtischlampe, einen Kaffeebecher mit Dampf, einige Stifte in einem Behälter, ein Smartphone und einen Taschenrechner. Auf dem Tisch liegt außerdem ein kleines Notizbuch und ein Stück Gebäck. Der Arbeitsplatz wirkt organisiert und kreativ.

Lou Plummer beschreibt in sei­nem Artikel „How to Send an Email to Your Obsidian Vault”, wie er mit Hilfe von IFTTT, Dropbox und Hazel E‑Mails an sein Obsidian Vault „sen­det“. Dazu nutzt er eine spe­zi­el­le Mailbox in IFTTT, in der ein­ge­hen­de Mails einen Workflow aus­lö­sen, der den Inhalt der Mail in der per­sön­li­chen Dropbox als Textdatei spei­chert. Von dort wer­den die E‑Mails dann, in einem nächs­ten Schritt, lokal von Hazel nach Obsidian kopiert.

Der Artikel inspi­rier­te mich dazu, eine eige­ne Lösung zu ent­wi­ckeln. E‑Mails bie­ten eine prak­ti­sche Möglichkeit, Notizen zu erfas­sen, ins­be­son­de­re wenn z.B. am Arbeitsplatz der Zugang zum per­sön­li­chen Obsidian Vault nicht ver­füg­bar ist. Ich habe mir schon vor Jahren einen spe­zi­el­len E‑Mail-Account erstellt, den ich zum Übermitteln von Links, Notizen und Erinnerungen nut­ze. Lous Artikel brach­te mich nun auf die Idee, die­sen Mail-Account direkt mit mei­nem Obsidian Vault zu kop­peln. Zusätzlich erschien mir die­ser Workflow auch als eine gute Herausforderung, um mei­ne Python-Kenntnisse zu erwei­tern.
.
Hier sind die Anforderungen so einen sol­chen Workflow:

  • Er soll lokal auf mei­nem Hauptcomputer lau­fen, in mei­nem Fall auf mei­nem MacBook.
  • Er soll die E‑Mails des spe­zi­el­len Mail-Accounts direkt her­un­ter­la­den und in Obsidian als Markdown-Dateien spei­chern.
  • Es sol­len nur die unge­le­se­nen E‑Mails her­un­ter­ge­la­den wer­den, die anschlie­ßend den Status „gele­sen“ erhal­ten.
  • Die Konfiguration, also die Zugangsdaten zum Mail-Server und der Pfad zum Obsidian-Ordner, soll in einer exter­nen Konfigurationsdatei gespei­chert wer­den.

Warnhinweis: Beim auto­ma­ti­schen Konvertieren einer E‑Mail in eine Markdown-Datei, die dann in Obsidian auf­ge­ru­fen wird, besteht theo­re­tisch die Möglichkeit, dass Code aus­ge­führt wird. Nach mei­nem aktu­el­len Kenntnisstand ist die­ses Risiko jedoch gering. Ich bin offen für Hinweise, falls jemand ande­re Erfahrungen gemacht hat. Zudem möch­te man ver­mei­den, Spam in sei­nem Obsidian Vault zu spei­chern. Für mei­nen spe­zi­el­len E‑Mail-Account ist dies unwahr­schein­lich, da er nur mir bekannt ist und bis­her kei­nen Spam erhal­ten hat. Dennoch könn­ten Filter imple­men­tiert wer­den, um nur E‑Mails von bestimm­ten Absendern oder mit bestimm­ten Schlüsselwörtern im Betreff nach Obsidian wei­ter­zu­lei­ten. Derzeit ver­zich­te ich jedoch auf sol­che Maßnahmen.

Vorbereitung des Projekt-Setups

Da mei­ne Anforderungen den Einsatz exter­ner Python-Bibliotheken erfor­dern, emp­fiehlt es sich, das Skript in einer vir­tu­el­len Python-Umgebung zu erstel­len und aus­zu­füh­ren.

Der ers­te Schritt besteht dar­in, einen Projektordner zu erstel­len:

mkdir /Users/leif/Documents/Projects/mail2obsidian
cd /Users/leif/Documents/Projects/mail2obsidian

Anschließend wird die vir­tu­el­le Umgebung erstellt und akti­viert:

python3 -m venv env 
source env/bin/activate

Nach der Aktivierung der Umgebung ändert sich der Eingabe-Prompt im Terminal, um anzu­zei­gen, dass die Umgebung aktiv ist, z.B. so:

(env) > $

Einrichten der benötigten Bibliotheken

Zunächst benö­ti­ge ich eine Bibliothek, die den Zugriff auf den E‑Mail-Account ermög­licht. Nach ein­ge­hen­der Recherche habe ich mich für IMAPClient ent­schie­den. Zum Lesen der exter­nen Konfigurationsdatei ver­wen­de ich PyYAML, und für die Konvertierung der E‑Mails nut­ze ich mark­dow­ni­fy. Alle die­se Bibliotheken wer­den in die akti­ve Python-Umgebung instal­liert:

pip install imapclient pyyaml markdownify

Erstellen der Konfigurationsdatei

Ich habe mich für eine exter­ne Konfigurationsdatei ent­schie­den, um sicher­zu­stel­len, dass beim Veröffentlichen des Source-Codes kei­ne pri­va­ten Daten im Code ent­hal­ten sind. Zudem ist es prak­tisch, die­se Daten an einem zen­tra­len Ort abzu­le­gen, der leicht an neue Anforderungen ange­passt wer­den kann.

Derzeit wer­den in der Datei die Zugangsdaten zum Mail-Server und der Dateipfad zum Ordner in Obsidian, in dem die Mails gespei­chert wer­den, abge­legt. Als Datenstruktur habe ich das soge­nann­tes „geschach­tel­tes Dictionary” gewählt, mit zwei Hauptschlüsseln, denen die eigent­li­chen Schlüssel-Wert-Paare fol­gen. Diese Datenstruktur lässt sich mit der PyYAML-Bibliothek leicht ver­ar­bei­ten und in die Python-Datenstruktur umset­zen.

email: 
    server: "imap.example.com" 
    user: "your-email@example.com" 
    password: "your-password" 
output: 
    path: "/Users/username/Documents/ObsidianVault/98 emails"

Für Windows oder Linux muss der Dateipfad ent­spre­chend ange­passt wer­den, damit das Skript auch auf die­sen Plattformen funk­tio­niert.

Vorstellung des mail2obsidian .py Scriptes

Da das Abrufen der E‑Mails vom Server mit der Bibliothek mei­ne Fähigkeiten über­stieg, habe ich den Code zusam­men mit ChatGPT ent­wi­ckelt. Da ich dabei auch etwas ler­nen woll­te, ließ ich mir die ein­zel­nen Stellen aus­führ­lich erklä­ren. Diese Erklärungen, die mehr Iterationen als die Code-Entwicklung benö­tig­ten, fas­se ich an die­ser Stelle mit eige­nen Worten zusam­men. Wem dies etwas zu lang­at­mig ist, kann direkt zum gesam­ten Skript-Code sprin­gen.

1. Laden der Konfiguration aus einer YAML-Datei

with open('config.yaml', 'r') as file:
    config = yaml.safe_load(file)

EMAIL = config['email']['user']
PASSWORD = config['email']['password']
IMAP_SERVER = config['email']['server']
OUTPUT_PATH = config['output']['path']

Wie erwähnt, ist die Dateistruktur der Konfigurationsdatei ein „geschach­tel­tes Dictionary”, das es auch in der Programmiersprache Python gibt. In den ers­ten zwei Zeilen wird die Datei ein­ge­le­sen und die Daten mit dem Befehl yaml.safe_load(file) in der Variablen config gespei­chert, wobei die Datenstruktur erhal­ten bleibt.

Der Zugriff auf die Werte erfolgt dann über den Hauptschlüssel, email oder output und anschlie­ßend über den jewei­li­gen Schlüssel.

Mit dem with-Statement wird ein Kontextmanager erstellt, der sicher­stellt, dass die Datei nach dem Verlassen des Code-Blocks ord­nungs­ge­mäß geschlos­sen wird. Der Kontextmanager über­nimmt die Verwaltung von Ressourcen, wie das Öffnen und Schließen von Dateien, und sorgt dafür, dass die­se kor­rekt frei­ge­ge­ben wer­den, auch wenn inner­halb des Blocks eine Ausnahme auf­tritt.

2. Sicherstellen, dass der Ausgabeordner existiert

os.makedirs(OUTPUT_PATH, exist_ok=True)

Dieser Aufruf stellt sicher, dass der Pfad für das Speichern der Markdown-Datei exis­tiert. Dabei wird ver­sucht, das Verzeichnis zu erstel­len. Normalerweise wür­de der Befehl einen Fehler mel­den, falls das Verzeichnis bereits vor­han­den ist. Mit dem Zusatz exist_ok=True wird die Fehlermeldung jedoch unter­drückt, und das vor­han­de­ne Verzeichnis bleibt unver­än­dert.

3. Verbindung zum E‑Mail-Server herstellen

with imapclient.IMAPClient(IMAP_SERVER) as server:
    server.login(EMAIL, PASSWORD)
    server.select_folder('INBOX')

In die­sem Block wird die Verbindung zum Mail-Server auf­ge­baut und der Fokus auf die INBOX gesetzt. Falls der Server ein ande­res Postfach für neue Mails nutzt, muss der Name ent­spre­chend ange­passt wer­den, was in der Regel kein Problem dar­stellt.

Auch hier wird mit einem with-Statement ein Kontextmanager erstellt, der die Ressourcenverwaltung über­nimmt. Er sorgt dafür, dass die Verbindung zum Mail-Server wäh­rend der fol­gen­den Verarbeitungsschritte auf­recht­erhal­ten bleibt.

4. Ungelesene E‑Mails suchen

messages = server.search(['UNSEEN'])

Mit die­sem Aufruf wird eine Liste aller IDs (UIDs) der unge­le­se­nen E‑Mails aus der INBOX erstellt. Diese Liste wird im nächs­ten Schritt ver­wen­det, um die E‑Mails nach­ein­an­der zu ver­ar­bei­ten.

5. Schleife für die Verarbeitung der E‑Mails

for uid, message_data in server.fetch(messages, 'RFC822').items():

Um die for-Schleife zu ver­ste­hen, benö­tig­te ich etwas län­ger, da ich bis­her nicht mit einem sol­chen Konstrukt gear­bei­tet hat­te. Die Iteration in der Schleife wird nicht expli­zit in der for-Schleife defi­niert, son­dern ergibt sich aus dem Ergebnis des Aufrufs:

server.fetch(messages, 'RFC822')

Die Variable messages ent­hält die Liste der IDs der unge­le­se­nen E‑Mails, z.B.:

messages = [101, 102, 103]  # IDs ungelesener E-Mail-Server

Der Aufruf server.fetch(messages, 'RFC822') ruft für jede ID in messages die E‑Mail-Daten ab, wobei das Format ‘RFC822’ ver­langt wird, und gibt die­se als Dictionary zurück:

{
    101: {b'RFC822': b'Raw Email Data for UID 101'},
    102: {b'RFC822': b'Raw Email Data for UID 102'},
    103: {b'RFC822': b'Raw Email Data for UID 103'}
}

Hierbei ist die ID 101 der Schlüssel zur ers­ten unge­le­se­nen E‑Mail, und der Wert {b’RFC822’: b’Raw Email Data for UID 101′} reprä­sen­tiert den message_body im RAW-Format. Dieser ist nicht so klar les­bar wie im Beispiel und kann kodiert sein, z.B. mit Base64 besteht aus min­des­tens zwei Bereichen, dem E‑Mail-Header Daten und dem E‑Mail-Body.

Das .items() am Ende des Aufrufs erzeugt eine Abfolge von Schlüssel-Wert-Paaren, durch die die for-Schleife ite­riert. Dabei wird die ID in der Variablen uid und der E‑Mail-Body in message_data für die wei­te­re Verarbeitung gespei­chert.

6. Umwandeln der E‑Mail-Daten in ein lesbares Format

email_message = email.message_from_bytes(message_data[b'RFC822'])

In der Schleife wird die E‑Mail in der Variablen email_message gespei­chert. Die Methode message_from_bytes(message_data[b'RFC822']) wan­delt die rohen E‑Mail-Daten in ein Python-E-Mail-Objekt um. Dieses Objekt bie­tet eine kla­re Struktur, mit der das Skript ein­fach auf die benö­tig­ten Informationen zugrei­fen kann. Diese Struktur besteht aus den fol­gen­den Bereichen:

Header-Bereich:

Der Header-Bereich besteht aus Schlüssel-Wert-Paaren. In vie­len E‑Mail-Programmen kann man sich die­se Header-Daten anzei­gen las­sen, z.B. auf dem Mac im Mail-Programm unter dem Menüpunkt Darstellung → E-Mail → Alle Header. In dem Skript wer­de ich nur, den Betreff, den Absender und das Datum aus­wer­ten:

  • Subject (Betreff)
  • From (Absender)
  • To (Empfänger)
  • Date (Datum der E‑Mail)
  • Weitere Felder wie CC, BCC, Reply-To

Auf die­se Felder kann rela­tiv ein­fach zuge­grif­fen wer­den, da sie in einem E‑Mail-Objekt struk­tu­riert vor­lie­gen. Allerdings ist es oft not­wen­dig, die Felder zu deko­die­ren, da E‑Mails nicht nur druck­ba­re ASCII-Zeichen ent­hal­ten.

Payload (Inhalt):

Die Payload ent­hält den eigent­li­chen Inhalt der E‑Mail. Sie kann aus einem oder meh­re­ren „Parts” bestehen, die wie­der­um deko­diert sein kön­nen:

  • Text/plain: Der Klartext-Inhalt.
  • Text/html: HTML-Version der Nachricht.
  • Anhänge: Dateien, Bilder oder ande­re ein­ge­bet­te­te Inhalte, die oft kodiert sind, z.B. mit Base64, um sie kor­rekt in das E‑Mail-Format ein­zu­bin­den

7. Verarbeitung des Header-Bereichs

subject, encoding = decode_header(email_message['Subject'])[0]
if isinstance(subject, bytes):
    subject = subject.decode(encoding if encoding else 'utf-8')

Im ers­ten Schritt wird aus dem Header der Betreff, also das Subject, extra­hiert. Aus dem Betreff soll spä­ter der Dateiname erstellt wer­den. Wie bereits erwähnt, kön­nen der Header und das Subject kodiert sein, daher wer­den sie zunächst in UTF‑8 deko­diert. Das Ergebnis wird in der Variablen subject gespei­chert.

Zusätzlich wer­den der Absender der E‑Mail und das Datum für die Notiz-Properties aus­ge­le­sen und in den Variablen sender und send_date gespei­chert.

sender = email_message.get('From', 'Unknown sender')
        date_header = email_message.get('Date', 'Unknown date')
        try:
            send_date = parsedate_to_datetime(date_header).strftime('%d.%m.%Y %H:%M:%S') if date_header != 'Unknown date' else date_header
        except Exception:
            send_date = "Invalid date"

Das Datum wird dabei in ein bei uns gebräuch­li­ches Format umge­wan­delt. Falls die­se Daten nicht aus­ge­le­sen wer­den kön­nen, wird ein Standardwert in den Variablen gespei­chert.

8. Erstellen eines sicheren Dateinamens

filename = f"{subject}.md"
filename = (
    filename.replace('/', '_')
            .replace('\\', '_')
            .replace(':', '_')
            .replace('*', '_')                        
            .replace('?', '_')
            .replace('"', '_')
            .replace('<', '_')
            .replace('>', '_')
            .replace('|', '_')
)

Da im Betreff einer E‑Mail Zeichen vor­kom­men kön­nen, die in Dateinamen pro­ble­ma­tisch sind, wer­den die­se Zeichen in der Variablen filename durch ein _ ersetzt, nach­dem ihr der Wert von subject zuge­ord­net wur­de.

Ich benut­ze bei der Zuordnung zur Variablen filename einen f‑String, der es mir ermög­licht, die Variable subject direkt in geschweif­te Klammern {} ein­zu­fü­gen, um den Dateinamen zu erstel­len.

Zum Ersetzen der kri­ti­schen Zeichen ver­wen­de ich ein Konstrukt namens Method Chaining, das es mir ermög­licht, die replace-Methode mit den unter­schied­li­chen Zeichen hin­ter­ein­an­der anzu­wen­den.

9. Initialisierung des Notiz-Variablen

        # Initialize email content with YAML frontmatter
        email_content = f"""---
subject: "{subject}"
from: "{sender}"
send_date: "{send_date}"
---

"""

Die Variable email_content wird genutzt, um den Inhalt der Notiz zusam­men­zu­bau­en. Dabei wird die Variable bei der Initialisierung mit den Property-Daten belegt. Hierzu wird, wie schon beim Notiznamen, ein f‑string genutzt, der die Einbettung von Python-Ausdrücken ermög­licht, deren Ergebnisse direkt in den String ein­ge­setzt wer­den. Der Frontmatter-Block wird jeweils von --- umschlos­sen.

10. Verarbeitung der E‑Mail-Payload

# Extract email body
        for part in email_message.walk():
            if part.get_content_type() == "text/html":
                # Found HTML content; convert to Markdown and stop searching
                charset = part.get_content_charset() or 'utf-8'
                payload = part.get_payload(decode=True)
                if payload:
                    html_content = payload.decode(charset, errors='replace')
                    email_content += md(html_content)  # Convert HTML to Markdown
                    break  # Prioritize HTML, so we stop here
            elif part.get_content_type() == "text/plain" and email_content.strip() == f"---\n{subject}\n---":
                # Only use if no HTML was found
                charset = part.get_content_charset() or 'utf-8'
                payload = part.get_payload(decode=True)
                if payload:
                    email_content += payload.decode(charset, errors='replace')

        # If email_content is still empty, add a default message
        if email_content.strip() == f"---\n{subject}\n---":
            email_content += "No content available."

Wie oben erwähnt, befin­det sich der Inhalt der Mail in der soge­nann­ten Payload und muss von dort extra­hiert wer­den. Da die­se Payload aus meh­re­ren Teilen (Parts) besteht, durch­läuft die­se Schleife die ein­zel­nen Teile. Der ers­te if-Block if part.get_content_type() == "text/html": über­prüft, ob der aktu­el­le Teil HTML-Inhalt ist. Wenn dies der Fall ist, wird mit der Methode get_content_charset() im Ausdruck charset = part.get_content_charset() or 'utf-8' ver­sucht, die Kodierung des Inhalts zu ermit­teln. Wenn kei­ne Kodierung ermit­telt wer­den kann, wird stan­dard­mä­ßig utf-8 ver­wen­det. Mit payload = part.get_payload(decode=True) wird nun der eigent­li­che Inhalt deko­diert. Durch das Setzen von decode=True wird sicher­ge­stellt, dass der Inhalt als Rohbytes zurück­ge­ge­ben wird, nach­dem etwa­ige Transferkodierungen wie base64 ent­fernt wor­den sind. Damit ist die­ser Inhalt jedoch immer noch kein les­ba­rer String, son­dern ein soge­nann­ter Byte-String. Ein Beispiel für so einen Byte-String wäre b"M\xc3\xbcller Stra\xc3\x9fe", der utf-8 deko­diert Müller Straße ergibt.

Bevor die Konvertierung die­ses Byte-Strings des HTML-Inhalts in Markdown erfolgt, wird zunächst sicher­heits­hal­ber geprüft, ob der Inhalt tat­säch­lich deko­diert wur­de (payload ist nicht None oder leer). Auf die­se Weise wird ver­mie­den, dass wei­te­re Verarbeitungsschritte auf einem nicht vor­han­de­nen oder lee­ren Inhalt statt­fin­den, was zu Fehlern füh­ren könn­te.

11. Konvertierung und Speichern des HTML-Inhaltes

html_content = payload.decode(charset, errors='replace')
email_content += md(html_content)  # Convert HTML to Markdown
break  # Prioritize HTML, so we stop here

In der ers­ten Zeile wird der Inhalt nun end­gül­tig in les­ba­res HTML kon­ver­tiert, wobei even­tu­el­le nicht in utf-8 vor­han­de­ne Zeichen (errors='replace') durch ein Ersatzzeichen wie  ersetzt wer­den. Dies stellt sicher, dass der Dekodierungsvorgang nicht abbricht und der String wei­ter­hin ver­ar­bei­tet wer­den kann, auch wenn eini­ge Zeichen nicht kor­rekt dar­ge­stellt wer­den.

Mit email_content += md(html_content) wird der HTML-Inhalt in Markdown kon­ver­tiert und an die Variable email_content hin­ter den bereits zuvor gespei­cher­ten Frontmatter ange­hängt (+=).

Das anschlie­ßen­de break been­det die Schleife. Damit wird der elif-Teil der Bedingung nur ver­ar­bei­tet, wenn die Mail kei­nen HTML-Part ent­hält.

12. Text-Inhalt verarbeiten und Speichern

elif part.get_content_type() == "text/plain" and email_content.strip() == f"---\n{subject}\n---":
                # Only use if no HTML was found
                charset = part.get_content_charset() or 'utf-8'
                payload = part.get_payload(decode=True)
                if payload:
                    email_content += payload.decode(charset, errors='replace')

Dieser Teil der Bedingung wird nur ver­ar­bei­tet, wenn kein HTML-Part gefun­den wur­de und an die Variable email_content ange­hängt wer­den soll. Daher wird geprüft, ob die Variable email_content aus­schließ­lich den Inhalt des Property-Bereichs der Notiz ent­hält und ob es einen Text-Part (text/plain) gibt. Die strip-Methode ent­fernt dabei alle vor­an­ge­stell­ten und nach­ge­stell­ten Leerzeichen oder Zeilenumbrüche, um eine genaue Übereinstimmung zu gewähr­leis­ten.

Die Verarbeitung des Textes erfolgt dann ähn­lich wie beim HTML-Part, nur dass die deko­dier­te Payload direkt, also ohne Konvertierung, an die Variable email_content ange­hängt wird.

Abschluss

if email_content.strip() == f"---\n{subject}\n---":
            email_content += "No content available."

        # Save the email content to a Markdown file
        file_path = os.path.join(OUTPUT_PATH, filename)
        with open(file_path, 'w', encoding='utf-8') as f:
            f.write(email_content)

        # Mark the email as read
        server.add_flags(uid, '\\Seen')

print("Unread emails have been converted to Markdown files.")

Am Ende des Skriptes wird noch ein­mal geprüft, ob an die Variable email_content über­haupt E‑Mail-Inhalt ange­hängt wur­de. Wenn nicht, wird ein Hinweis, dass kein E‑Mail-Inhalt gele­sen wer­den konn­te, zu den Property-Daten in die Variable hin­zu­ge­fügt.

Danach wird der Dateipfad zum Obsidian Vault zusam­men mit dem Dateinamen der Notiz in die Variable file_path geschrie­ben. Anschließend wird die­se Datei erstellt und der Inhalt von email_content hin­ein­ge­schrie­ben. Am Ende wird die Mail auf dem Server mit dem Flag \\Seen, also als gele­sen, ver­se­hen, womit die Schleife mit der Verarbeitung einer Mail aus der Liste messages been­det ist. Diese Schleife wird so oft durch­lau­fen, bis die Liste abge­ar­bei­tet ist.

Am Ende wird eine Meldung in die Konsole geschrie­ben.

Der gesamte Skript-Code

import imapclient
import email
from email.header import decode_header
from email.utils import parsedate_to_datetime
import os
from markdownify import markdownify as md
import yaml

# Lädt die Konfiguration aus der YAML Datei
with open('config.yaml', 'r') as file:
    config = yaml.safe_load(file)

EMAIL = config['email']['user']
PASSWORD = config['email']['password']
IMAP_SERVER = config['email']['server']
OUTPUT_PATH = config['output']['path']

# Sicherstellen, dass der Ausgabeordner existiert
os.makedirs(OUTPUT_PATH, exist_ok=True)

# Verbindung zum E-Mail-Server herstellen 
with imapclient.IMAPClient(IMAP_SERVER) as server:
    server.login(EMAIL, PASSWORD)
    server.select_folder('INBOX')

    # Ungelesene E-Mails suchen
    messages = server.search(['UNSEEN'])

    # E-Mails abrufen und verarbeiten
    for uid, message_data in server.fetch(messages, 'RFC822').items():
        email_message = email.message_from_bytes(message_data[b'RFC822'])

        # E-Mail-Betreff dekodieren
        subject, encoding = decode_header(email_message['Subject'])[0]
        if isinstance(subject, bytes):
            subject = subject.decode(encoding if encoding else 'utf-8')

        # Extrahiert und decodiert subject, send-date und sender
        sender = email_message.get('From', 'Unknown sender')
        date_header = email_message.get('Date', 'Unknown date')
        try:
            send_date = parsedate_to_datetime(date_header).strftime('%d.%m.%Y %H:%M:%S') if date_header != 'Unknown date' else date_header
        except Exception:
            send_date = "Invalid date"

        # Sicheren Dateinamen erstellen
        filename = f"{subject}.md"
        filename = (
            filename.replace('/', '_')
                    .replace('\\', '_')
                    .replace(':', '_')
                    .replace('*', '_')                        
                    .replace('?', '_')
                    .replace('"', '_')
                    .replace('<', '_')
                    .replace('>', '_')
                    .replace('|', '_')
        )

        # Initialisiert Variable für den E-Mail Inhalt mit Frontmatter YAML
        email_content = f"""---
subject: "{subject}"
from: "{sender}"
send_date: "{send_date}"
---

"""

        # E-Mail-Text extrahieren
        for part in email_message.walk():
            if part.get_content_type() == "text/html":
                # Bei gefundenen HTML-Part; Konvertiert zu Markdown and stoppt da Suchen nach weiteren Parts
                charset = part.get_content_charset() or 'utf-8'
                payload = part.get_payload(decode=True)
                if payload:
                    html_content = payload.decode(charset, errors='replace')
                    email_content += md(html_content)  # Konvertiert das HTML to Markdown
                    break  # Wenn HTML-Inhalt gefunden wird, wird die Schleife verlassen, dadurch wird HTML-Inhalt prioriziert
            elif part.get_content_type() == "text/plain" and email_content.strip() == f"---\n{subject}\n---":
                # Wird nur drucchlaufen, wenn kein HTML-Part gefunden wurde
                charset = part.get_content_charset() or 'utf-8'
                payload = part.get_payload(decode=True)
                if payload:
                    email_content += payload.decode(charset, errors='replace')

        # Fall weder HTML noch Text gefunden wird, wird ein Hinweis in die Variabel geschrieben
        if email_content.strip() == f"---\n{subject}\n---":
            email_content += "No content available."

        # Mail-Inhalt wird in die Notiz-Datei geschrieben und am definierten Ort gespeichert.
        file_path = os.path.join(OUTPUT_PATH, filename)
        with open(file_path, 'w', encoding='utf-8') as f:
            f.write(email_content)

        # Markiert die verarbeitete Mail als gelesen
        server.add_flags(uid, '\\Seen')

print("Unread emails have been converted to Markdown files.")

Zusammenfassung und Ausblick

Dieses Skript ist als Ausgangspunkt für wei­te­re Ideen gedacht und ist sicher­lich nicht “fer­tig”. Ich habe bereits eine ers­te funk­tio­nie­ren­de Version als Reaktion auf Lous Beitrag vor eini­gen Wochen auf mei­nem eng­li­schen Blog ver­öf­fent­licht, in der bei­spiels­wei­se noch nicht die Metadaten in das Frontmatter geschrie­ben wur­den. Durch das Schreiben an die­sem Beitrag habe ich auch wie­der­um wei­te­re Ideen für Erweiterungen bekom­men.

Derzeit ist das Skript so kon­zi­piert, dass es manu­ell auf­ge­ru­fen wird. Das macht für mich auch Sinn, da ich mir nicht stän­dig E‑Mails schi­cke und den Workflow bei Bedarf dann ein­fach manu­ell auf­ru­fe. Die Ausführung des Skriptes kann jedoch auch auto­ma­ti­siert erfol­gen. Auf dem Mac wäre der launchd-Service der geeig­ne­te Ort, um die­ses Skript auto­ma­tisch, z.B. bei jedem Login, zu star­ten.

Eine wei­te­re Idee, die ich bereits im Text erwähnt habe, besteht dar­in, nur E‑Mails von bestimm­ten Absendern oder mit defi­nier­ten Stichwörtern im Betreff in den Obsidian Vault zu über­neh­men. Mit die­sem Ansatz könn­te auch ein E‑Mail-Account inte­griert wer­den, der für ande­re Zwecke genutzt wird.

Momentan wer­den Bilder und ande­re Anhänge igno­riert. Auch die Konvertierung des HTML-Parts in Markdown ist nicht ganz opti­mal, ins­be­son­de­re wenn man bei­spiels­wei­se Teile von einer Webseite in die E‑Mail kopiert. Es könn­te sich loh­nen, die Konvertierung zu ver­bes­sern, mög­li­cher­wei­se durch die Verwendung einer ande­ren Konvertierungsbibliothek, um bes­se­re Ergebnisse zu erzie­len.

Es gibt also noch viel Potenzial, die­sen Workflow zu erwei­tern und zu ver­bes­sern. Ich wür­de mich freu­en, wenn die­se Idee auf­ge­grif­fen und wei­ter­ent­wi­ckelt wird. Ideen und Hinweise sind ger­ne in den Kommentaren will­kom­men.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert