Persoenliches Office.js Add-in fuer Outlook auf macOS, das geoeffnete Mails inklusive Anhaengen per Klick als Markdown in einen Obsidian-Vault schreibt. Beim Setup wird pro Outlook-Account ein Vault plus Standard-Zielordner konfiguriert; dorthin landet jede geslingte Mail.
Ein Klick im Outlook-Ribbon ("Sling") nimmt die aktuell geoeffnete Mail, holt Body und Anhaenge, schreibt eine Markdown-Datei in den konfigurierten Obsidian-Vault und legt die Anhaenge in denselben Ordner. Die Mail landet immer unter dem im Setup definierten Default-Ordner, z.B. 01 Inbox/YYYY-MM-DD Betreff/. Wikilinks zu den Anhaengen werden automatisch in die Markdown-Datei eingebettet (Bilder als ![[…]]-Embed, andere Dateien als [[…]]-Link). Der Vorgang ist bewusst manuell, es gibt kein Auto-Sling.
Note
Zielordner-Auswahl beim Slingen ist derzeit nicht aktiv. Der Ribbon-Button ist eine ExecuteFunction, die direkt in den Default-Ordner slingt. Eine vollstaendige Picker-Oberflaeche (suchbare Vault-Ordnerliste, taskpane.ts + Helper-Endpunkt /folders) liegt im Code vor, ist aber nicht ins Manifest verdrahtet — es gibt keinen ShowTaskpane-Button, der sie oeffnet (der ShowTaskpane-Weg scheiterte am aggressiven Outlook-Mac-Caching, siehe „Bekannte Einschraenkungen"). Sortierung in andere Ordner passiert aktuell in Obsidian nach dem Slingen.
Vorlage war SlingMD (Windows-VSTO). Sling-Mac ist drastisch reduziert auf den Mac-Use-Case: nur Mail-Read-Surface, nur Slingen.
Editierbare Excalidraw-Quelle: docs/architecture.excalidraw.
Drei Komponenten:
| Komponente | Aufgabe |
|---|---|
Office.js Add-in (add-in/) |
XML-Manifest plus commands.ts. Wird in Outlook for Mac geladen, liefert den Sling-Button im Message-Read-Ribbon. |
Node.js Helper-Daemon (helper/) |
Zwei HTTPS-Server auf https://localhost:7331 (API, nimmt Sling-Requests entgegen) und https://localhost:3000 (Static Files, liefert commands.html, taskpane.html, Icons an Outlook). Laeuft persistent ueber launchd. |
| Obsidian Vault | Ziel-Verzeichnis im Dateisystem. Wird vom Helper direkt beschrieben. |
Beide Server nutzen das gleiche TLS-Zertifikat aus ~/.office-addin-dev-certs/ (von office-addin-dev-certs install erzeugt), damit Outlook die Endpunkte als trusted akzeptiert.
- Nutzer klickt im Ribbon einer geoeffneten Mail auf Sling.
- Outlook ruft die Funktion
slingMailauscommands.tsauf (registriert viaglobalThis["slingMail"]). slingMailholt Betreff,from,to, Body (body.getAsyncals HTML) sowie alle nicht-inline File-Anhaenge (getAttachmentContentAsync, Base64).- Payload wird per
POST https://localhost:7331/slingan den Helper geschickt. - Der Helper konvertiert das HTML mit Turndown nach Markdown, baut die Frontmatter, legt den Ziel-Ordner an, schreibt die Markdown-Datei und alle Anhaenge.
- Helper antwortet mit
{ path, attachments }. Add-in zeigt eine Outlook-Notification mit dem Pfad an.
Warning
Nicht in einen iCloud-synchronisierten Ordner klonen (~/Documents, ~/Desktop bei aktiviertem "Schreibtisch & Dokumente"-Sync, oder iCloud Drive direkt).
iCloud lagert bei "Mac-Speicher optimieren" einzelne Dateien als Platzhalter aus. Trifft Nodes synchroner Datei-Read (express.static im Helper bzw. require() beim Laden) auf so eine ausgelagerte Datei, gibt macOS errno -11 (EDEADLK) zurueck statt sie rechtzeitig zu materialisieren. Folge: der Static-Server liefert commands.js als HTTP 500, der Sling-Button laedt seinen Handler nicht, und das Add-in wirkt kaputt — intermittierend und schwer zu diagnostizieren.
Empfohlen: ein nicht-synchronisiertes Verzeichnis wie ~/Developer/sling-mac. Alle Pfade unten und in launchd/ch.owlist.sling-mac-helper.plist gehen von ~/Developer/sling-mac aus — beim Verschieben eines bestehenden Setups die Plist-Pfade entsprechend anpassen und den Daemon neu laden.
- macOS
- Node.js 22+ (siehe launchd-Plist:
/opt/homebrew/opt/node@22/bin/node) - Outlook for Mac
- Microsoft-365-Account mit Berechtigung, Add-ins zu sideloaden (oder ein eigenes Tenant Admin Center)
- Ein lokal vorhandener Obsidian-Vault
git clone <repo-url> ~/Developer/sling-mac # NICHT nach ~/Documents (iCloud) — siehe Warnung oben
cd ~/Developer/sling-mac
cd helper
npm install
npm run build
cd ../add-in
npm install
npm run buildcd add-in
npx office-addin-dev-certs installDas erzeugt ~/.office-addin-dev-certs/localhost.crt und localhost.key. Beide werden vom Helper-Server gelesen und sind im macOS-Keychain als trusted hinterlegt.
cp launchd/ch.owlist.sling-mac-helper.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/ch.owlist.sling-mac-helper.plistLogs liegen unter /tmp/sling-mac-helper.log und /tmp/sling-mac-helper.error.log.
Das Helper-Setup fragt interaktiv nach der Outlook-E-Mail-Adresse, dem Vault-Pfad und dem Standard-Zielordner innerhalb des Vaults:
cd helper
npm run setupBeispiel-Dialog:
Sling-Mac Setup
Outlook E-Mail-Adresse: deine-mail@example.com
Vault-Pfad [/Users/deinuser/Obsidian]: /Users/deinuser/Obsidian/SecondBrain
Standard-Sling-Ordner (relativ zum Vault) [01 Inbox]: 01 Inbox
Das Setup schreibt diese Angaben nach ~/.sling-mac.json. Der vaultPath ist der Schreibort fuer geslingte Mails. (Er ist ausserdem die Quelle fuer die Ordnerliste der noch nicht aktiven Picker-Oberflaeche — siehe Hinweis ganz oben.)
add-in/manifest.xml ueber das M365 Admin Center hochladen oder via "Integrierte Apps" / "Eigenes Add-In hinzufuegen" in Outlook for Mac sideloaden.
Die Helper-Konfiguration lebt in ~/.sling-mac.json:
{
"accounts": {
"deine-mail@example.com": {
"vaultPath": "/Users/deinuser/Obsidian/SecondBrain",
"defaultFolder": "01 Inbox"
}
}
}| Feld | Bedeutung |
|---|---|
accounts |
Map: E-Mail-Adresse → Account-Konfiguration. Die Adresse wird vom Add-in via Office.context.mailbox.userProfile.emailAddress mitgeschickt. |
vaultPath |
Absoluter Pfad zum Obsidian-Vault-Root. (Aus diesem Vault liest der Helper auch die Ordner fuer die noch nicht aktive Picker-Oberflaeche.) |
defaultFolder |
Zielordner relativ zum Vault. Jede geslingte Mail landet unter <vaultPath>/<defaultFolder>/<YYYY-MM-DD Betreff>/ — beim Slingen gibt es derzeit keine Ordner-Auswahl. |
Multi-Account: pro E-Mail-Adresse ein eigener Block mit eigenem Vault und Default-Ordner. Wird beim Slingen keine passende Adresse gefunden, faellt der Helper auf den ersten Account in der Map zurueck (siehe getAccountConfig in helper/src/config.ts).
- Eine Mail in Outlook for Mac oeffnen.
- Im Ribbon auf Sling klicken.
- Outlook schreibt die Mail in den im Setup definierten Default-Ordner und zeigt eine Info-Notification mit dem geschriebenen Pfad an. Bei Anhaengen wird die Anzahl mit angezeigt. Fehler erscheinen als Error-Notification.
Note
Dieser Abschnitt beschreibt eine im Code vorhandene, aber nicht ins Manifest verdrahtete Funktion. Aktuell slingt der Button immer in den Default-Ordner (siehe Hinweis ganz oben). Die folgende Beschreibung gilt erst, wenn der Picker via ShowTaskpane aktiviert wird.
Die Picker-Oberflaeche (add-in/src/taskpane/taskpane.ts) wuerde rechts in Outlook die aktuell geoeffnete Mail und darunter eine suchbare Ordnerliste zeigen. Diese Liste kommt aus dem Vault, der beim Setup als vaultPath gespeichert wurde (Helper-Endpunkt /folders). Sichtbar waeren die obersten Vault-Ordner und deren direkte Unterordner, z.B.:
00 Kontext
00 Kontext/Workflows
01 Inbox
02 Projekte
02 Projekte/CAS Vibe Engineering
04 Ressourcen
Der im Setup definierte defaultFolder ist vorausgewaehlt. Wer einen anderen Zielordner anklickt und dann Sling ausloest, wuerde die Mail in diesen Ordner schreiben (taskpane.ts sendet targetFolder). Warum es derzeit nicht aktiv ist: siehe „Bekannte Einschraenkungen".
Die anonymisierten Excalidraw-Skizzen zeigen die beiden relevanten Outlook-Momente (die zweite zeigt die geplante, noch nicht aktive Picker-Oberflaeche):
Editierbare Excalidraw-Quelle: docs/sling-mac-outlook-menu.excalidraw.
Editierbare Excalidraw-Quelle: docs/sling-mac-folder-picker.excalidraw.
Pro geslingter Mail wird ein eigener Ordner angelegt:
01 Inbox/
└── 2026-05-11 Betreff der Mail/
├── 2026-05-11 Betreff der Mail.md
├── anhang1.pdf
└── bild.png
Wikilinks im Markdown referenzieren die Anhaenge ohne Pfadpraefix, da sie im selben Ordner liegen. Bilder werden als Obsidian-Embed (![[…]]) eingebunden, andere Dateien als normaler Wikilink ([[…]]).
Der Dateiname wird aus Datum plus bereinigtem Betreff gebaut (alles ausser Buchstaben, Ziffern, deutschen Umlauten, Bindestrich und Leerzeichen wird entfernt, auf 80 Zeichen gekuerzt).
---
tags: [inbox, mail]
source: sling-mac
subject: "Betreff der Mail"
from: absender@example.com
date: 2026-05-11
---
# Betreff der Mail
**Von:** Absender Name <absender@example.com>
**An:** Empfaenger Name <empfaenger@example.com>
**Datum:** 2026-05-11
---
<Body als Markdown, konvertiert mit Turndown>
## Anhaenge
![[bild.png]]
[[anhang1.pdf]]Der ## Anhaenge-Block wird nur angehaengt, wenn Anhaenge gespeichert wurden.
Bekannte Fehlerbilder mit Diagnose- und Fix-Befehlen: siehe docs/TROUBLESHOOTING.md und die Schnell-Übersicht in docs/KNOWN_ISSUES.md.
Schnell-Check, wenn das Add-in nicht funktioniert:
launchctl list | grep -i sling # Daemon lebt?
lsof -nP -iTCP:7331 -sTCP:LISTEN # Port 7331 IPv4 + IPv6?
curl -k -I https://127.0.0.1:7331/health # Erreichbar via IPv4?
curl -k -I https://127.0.0.1:3000/taskpane.html # Static-Server via IPv4?Wenn der Daemon nur an [::1] lauscht oder IPv4-curl Connection refused liefert: auf v0.1.3+ updaten (Dual-Stack-Fix).
- Ordner-Picker ist nicht aktiv (toter Code). Helper-Endpunkt
/foldersund die Picker-UItaskpane.tssind vollstaendig implementiert, aber das Manifest exponiert nur einenExecuteFunction-Button (slingMail), dertargetFolder: ""hardcodiert — also immer den Default-Ordner nimmt. Es gibt keinenShowTaskpane-Button, der die Oberflaeche oeffnet. Grund: DerShowTaskpane-Weg liess sich auf Outlook for Mac nicht stabil ausliefern (siehe naechster Punkt). Reaktivierung =ShowTaskpane-Control ins Manifest + neue Add-in-ID. - Outlook-Mac-Caching bleibt zaeh. Nach Manifest-Aenderungen behaelt Outlook bzw. das M365 Admin Center alte Add-in-Metadaten hartnaeckig — bei der
ShowTaskpane-Umstellung lief sogar nach Remove+Re-Add und neuer GUID die alteExecuteFunctionweiter (genau deshalb wurde der Picker descoped). Hilft in der Praxis: neue Add-in-ID bzw. erneutes Sideloading. - Ordner-Picker waere bewusst flach. Der Helper liest nur die erste und zweite Ordnerebene des Vaults (
/folders), damit die Liste schnell und uebersichtlich bliebe. - Nur Mail-Read-Surface. Das Manifest haengt am
MessageReadCommandSurface. Kein Compose-Support, kein Slingen aus dem Editor heraus. - Anhaenge werden via
getAttachmentContentAsyncgeholt. Funktioniert mit der hier deklariertenReadWriteMailbox-Permission, ist aber auf denAttachmentType.Fileund nicht-inline Anhaenge beschraenkt. - Conversation-Threading wird zwar im Payload mitgeschickt (
conversationId), aktuell aber nicht zur Gruppierung verwendet. Jede Mail bekommt einen eigenen Ordner.
Notizen aus dem Bauen, falls jemand (oder ich selbst spaeter) das Setup nachbauen will:
- Webpack
scriptLoading: "blocking"ist Pflicht fuer Office.js Function Files. Mit dem DefaultdeferwirdslingMailnicht rechtzeitig registriert und Outlook meldet die Funktion als unbekannt. makeEwsRequestAsyncbrauchtReadWriteMailbox. Das M365 Admin Center verankert diese Permission unzuverlaessig — nach Manifest-Updates oft erst, wenn die Add-in-ID neu vergeben wird.getCallbackTokenAsync({isRest: true})schlaegt ohne Azure-AD-App-Registration fehl. Fuer dieses Setup nicht gebraucht, da der Body viabody.getAsyncund Anhaenge viagetAttachmentContentAsyncgeholt werden — keine direkten Graph-/EWS-Calls aus dem Add-in.- Static Server liefert
Cache-Control: no-store. Sonst werdencommands.jsundtaskpane.htmlvon Outlook hartnaeckig gecached, was Debugging unangenehm macht. - Zwei Ports sind kein Zufall. Port 3000 (Static) und Port 7331 (API) sind beide HTTPS mit dem gleichen Dev-Cert. Port 3000 dient als Same-Origin fuer Manifest-referenzierte HTML-/JS-Files; Port 7331 ist die eigentliche API.
- Die Picker-Endpunkte sind absichtlich auf beiden Ports gemountet, falls man den TaskPane-Weg spaeter doch reaktiviert (
pickerRouterwird inserver.tszweimal verwendet).
sling-mac/
├── add-in/ # Office.js Add-in (TypeScript, Webpack)
│ ├── src/
│ │ ├── commands/
│ │ │ ├── commands.ts # slingMail-Funktion (aktiv)
│ │ │ └── commands.html # Function-File-Wrapper
│ │ ├── taskpane/
│ │ │ ├── taskpane.ts # nicht aktiv im Manifest-Pfad
│ │ │ └── taskpane.html # nicht aktiv
│ │ └── picker.html # nicht aktiv (Dialog-Variante)
│ ├── assets/ # Icon-Set (16/32/64/80/128)
│ ├── manifest.xml # Office-Add-in-Manifest
│ ├── webpack.config.js
│ ├── tsconfig.json
│ └── package.json
├── helper/ # Node.js HTTPS-Helper-Server
│ ├── src/
│ │ ├── server.ts # Express, Turndown, beide Ports
│ │ └── config.ts # ~/.sling-mac.json-Loader
│ ├── scripts/setup.ts # interaktives Anlegen von ~/.sling-mac.json
│ ├── tsconfig.json
│ └── package.json
├── launchd/
│ └── ch.owlist.sling-mac-helper.plist # Helper-Daemon (serviert API 7331 + Static 3000)
└── README.md
- Teams-Obsidian-Bridge — Schwesterprojekt, separates Repo. Buy-First-Ansatz fuer Teams-Inhalte (existierende Tools evaluieren statt von Grund auf bauen).
Persoenliches Tool. Privates Repo. Kein Public Release geplant. Versionierung ueber GitHub-Releases/Tags (ab v0.1.1), ansonsten keine Garantien, keine Support-Verpflichtung.


