MVP scope: campaigns CRUD, audience filter from contacts, Tiptap editor, bulk-send via mana-mail extension, per-recipient tracking (open/click/ unsubscribe), DSGVO-compliant footer, DNS-check. Key decisions made up-front: - Tracking endpoints live in mana-mail (public, token-HMAC signed) — not in apps/api, because mana-mail already owns SMTP + auth plumbing - Per-recipient state stays Postgres-only; no Dexie mirror (could be millions of events for big lists, no cross-device benefit) - Tiptap over Unlayer/Lexical: MIT, Svelte wrapper exists, extension- based so bundle stays lean via tree-shaking - juice for CSS-inlining runs server-side — keeps the client bundle light and concentrates email-compat knowledge in one place - Explicitly NOT zero-knowledge compatible; server needs plaintext recipient lists to send. Warning in onboarding. - 10 milestones, ~17 days MVP. M1-M4 builds the core send path, M5-M8 adds tracking + DSGVO + deliverability. Related: docs/reports/clubdesk-vs-mana-comparison.md §7.2 Paket D. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
25 KiB
Broadcast / Newsletter — Module Plan
Status (2026-04-20)
M0 Planning — dieser Plan. Noch kein Code.
Nächster Schritt: M1 Skelett (Modul registriert, Dexie-Table, leere ListView).
Ziel
Ein Modul, mit dem der Nutzer E-Mail-Kampagnen an Kontakte verschickt: Newsletter, Ankündigungen, Serienmails. Kernfrage: "Wer bekommt welche Mail wann — und wie hat sie performt?"
Zielgruppen (in Reihenfolge der Priorität):
- Creator / Solo-Blogger — periodischer Newsletter an Abonnenten, Substack-Alternative
- Solo-Freelancer — Kunden-Updates, Projekt-Ankündigungen
- Kleine Unternehmen — Kampagnen, Aktionen
- Vereine (später) — Mitgliederinfos, Mahnungen, Einladungen (verbindet sich später mit Invoices + Events)
Abgrenzung
In scope (MVP):
- Kampagnen CRUD (Entwurf → Geplant → Gesendet)
- Empfängerliste aus
contactsvia Tag/Filter-basierter Segmentierung - Rich-Text-Editor (Tiptap) mit Basis-Formatierung, Bild-Einbettung, Link-Rewriting
- Versand über mana-mail mit neuem
/v1/mail/bulk-sendEndpoint - Per-Empfänger-Tracking:
sent/delivered/bounced/opened/clicked/unsubscribed - Open-Tracking via 1×1 Pixel
- Click-Tracking via Link-Rewrite → Redirect-Endpoint
- Unsubscribe-Link (One-Click, DSGVO-konform, kein Login)
- Templates (Built-in Defaults + User-Custom)
- Basis-Statistik pro Kampagne: Öffnungsrate, Klickrate, Bounce-Rate, Unsubscribe-Rate
Out of scope (Phase 2+):
- A/B-Testing
- Drip-Kampagnen / Automations (später über
rituals) - Heatmaps / Click-Maps
- Revenue Attribution
- Behavioral Triggers
- Transaktionale Mails (die bleiben in Stalwart direkt via JMAP)
- SMS
- Shop-Integration
- Double-Opt-In-Flow (MVP: angenommen, Empfänger wurden außerhalb geworben; Phase 2 fügt DOI-Landing-Pages hinzu)
Abgrenzung zu mail:
mail= 1:1-Kommunikation (wie Apple Mail / Gmail)broadcast= 1:N-Kommunikation (Newsletter, Ankündigungen, Serienmails)- Share: beide nutzen Stalwart als Backend, aber
broadcastbraucht zusätzliche Infrastruktur (Tracking, Bulk-Orchestrierung, DSGVO-Consent-Logs)
Abgrenzung zu invoices:
- Invoices sendet 1:1 transaktionale Mails via
mailto:(weil Stalwart aktuell keine Attachments kann) - Broadcast sendet 1:N Marketing-/Info-Mails direkt serverseitig
- Later: Invoices könnte
broadcastnutzen für "Rechnung an alle Mitgliedergruppe" (Vereins-Variante, Phase 3)
Namensschema
- Modul-/appId:
broadcast - UI-Label: "Broadcasts" (oder "Newsletter & Broadcasts")
- Route:
/broadcasts
Begründung: "Newsletter" impliziert periodisch; "Broadcast" deckt auch einmalige Ankündigungen ab. User-Copy kann trotzdem "Newsletter" sagen, wo das passender ist.
Architektur-Entscheidungen
Tracking-Infrastruktur: neuer Endpoint-Cluster in mana-mail
mana-mail ist der logische Ort — es besitzt schon den SMTP-Stack (Stalwart), die Auth-Integration, und eine Service-Key-basierte internal-Route.
Drei neue öffentliche (kein Auth, weil Empfänger sie anklicken) Endpoints:
GET /v1/mail/track/open/:token→ 1×1 transparentes GIF, loggt OpenGET /v1/mail/track/click/:token?url=<encoded>→ 302-Redirect zum Ziel, loggt ClickGET /v1/mail/track/unsubscribe/:token→ HTML-Bestätigungsseite ("bist du sicher?"), POST-Endpoint setzt Empfänger aufunsubscribed
Ein JWT-authed Endpoint für den Bulk-Versand:
POST /v1/mail/bulk-send— nimmt Kampagnen-Payload + Empfängerliste entgegen, verarbeitet serverseitig, streamt Status-Updates zurück via SSE (oder 202-Accepted + Polling)
Token-Design: {campaignId}:{recipientId}:{nonce} signiert mit HMAC-SHA256 über einen Broadcast-spezifischen Server-Secret. Kurz (base64url, ~50 chars), stateless validierbar, nicht vorhersagbar.
Versand-Orchestrierung: serverseitig, asynchron
mana-mail nimmt den Bulk-Auftrag entgegen, persistiert ihn als Job, arbeitet ihn im Hintergrund ab:
- Rate-Limiting (max X Mails/Minute pro User, verhindert Spam-Listings)
- Retry bei transienten Fehlern (SMTP 4xx)
- Bounce-Handling (SMTP 5xx → permanent, markiert Empfänger als
bounced) - Per-Empfänger Status wird zurückgemeldet (via WebSocket oder Polling auf
/v1/mail/bulk-send/:jobId/status)
Für MVP: synchroner Loop. Der Nutzer wartet 30 Sekunden für 100 Empfänger — akzeptabel. Echte Async-Jobs + Retry sind Phase 2, wenn Listen wachsen.
Client vs. Server für Tracking-Events
Tracking-Events (Open, Click, Unsubscribe) entstehen auf dem Server (Empfänger öffnen die Mail in Gmail, nicht in Mana). Sie müssen:
- Serverseitig persistiert werden (Postgres, per-campaign + per-recipient)
- Zum User-Client synchronisiert werden (damit das Dashboard Öffnungsraten zeigt)
Zwei Optionen:
- A) mana-mail schreibt in eigene Postgres-Tabelle, Webapp liest über authenticated API (
GET /v1/mail/campaigns/:id/events) - B) mana-mail pusht Events in den normalen Sync-Stream (mana-sync), Webapp bekommt sie wie alle anderen Daten
Gewählt: Option A. Tracking-Events sind viele (potenziell Millionen), und sie müssen nie ein anderes Client-Device beschreiben. Kein Mehrwert durch Sync-Overhead. API-Polling via liveQuery reicht.
Rich-Text-Editor: Tiptap
Bereits im Mana-Stack anerkannt (wird im Notes-Modul schon erwogen). Alternativen verworfen:
- Unlayer — hochwertig aber closed-source + SaaS-License für Embed
- Lexical (Facebook) — React-only
- ProseMirror direkt — zu low-level
- Plain Markdown + Preview — okay für v0, aber Bilder-Einbetten wird Krampf
Tiptap 2 hat Svelte-Wrapper (@tiptap/svelte). Extension-basiert → wir nehmen nur was wir brauchen (Starterkit + Image + Link + Placeholder).
HTML-Generierung & Inlining
E-Mail-HTML ≠ Web-HTML. Viele Clients (Outlook, Gmail) rendern Flexbox/Grid/CSS-Variables nicht. Wir brauchen:
- Inlining — Style-Rules werden in
style=""-Attribute aufgelöst - Table-basiertes Layout (Altlast aus Outlook-Zeiten) — für komplexe Layouts; MVP hält sich an einfache single-column
- Dark-Mode-Compat — Standard-CSS bypass wird in einigen Clients ausgehebelt; zunächst light-only
Library-Wahl: juice (npm, ~20KB) für CSS-Inlining. Ausgeführt serverseitig in mana-mail beim Versand, nicht client-side — dann kann das Webapp-Bundle schlank bleiben.
DSGVO & Compliance
Critical Path für EU/CH-Deploy:
- Einwilligung: jeder Empfänger muss explizit zugestimmt haben. Im MVP ist das out-of-scope; User trägt die Verantwortung. Dokumentiert in Settings-Seite mit Warnung.
- Double-Opt-In (DOI): Phase 2 — Landing-Page + Confirm-Mail-Flow.
- Unsubscribe-Link in jeder Mail: Pflicht. Ein-Klick-Abmeldung, kein Login.
- List-Unsubscribe-Header (RFC 8058):
List-Unsubscribe: <mailto:unsubscribe@…>, <https://…>+List-Unsubscribe-Post: List-Unsubscribe=One-Click→ Gmail/Apple-Mail-nativer Abmelden-Knopf. - Consent-Audit-Log: wann/wie kam der Kontakt auf die Liste? (Phase 2)
- Data-Processing-Agreement (AVV): User-Settings-Seite erzeugt ausfüllbares PDF.
- Right-to-be-forgotten: hash-basierte Dedupe + harter Delete beim Unsubscribe (nicht nur soft-delete).
MVP-Pragmatik: Punkte 1-4 sind Must-Have. 5-7 sind Phase 2.
Deliverability
Newsletter fallen häufiger ins Spam als 1:1-Mail. Checkliste:
- SPF — Stalwart hat's, User-Domain muss eintragen
- DKIM — Stalwart signiert, User-Domain muss eintragen
- DMARC — User-Domain braucht Policy (
p=noneanfangs reicht) - Bounce-Handling — harte Bounces → Empfänger dauerhaft ausschließen
- Warm-up — erste Kampagne an ≤ 100 Empfänger, dann Ausweitung
- Reputation — User-Feedback-Loop mit Gmail/Hotmail (Phase 2)
Für MVP: Settings-Onboarding zeigt SPF/DKIM/DMARC-Check mit DNS-Einträgen zum Kopieren. Wenn nicht konfiguriert, warnt der Send-Flow.
Modul-Struktur
apps/mana/apps/web/src/lib/modules/broadcast/
├── types.ts # LocalCampaign, LocalTemplate, LocalRecipientList, LocalSendStatus
├── collections.ts # Dexie tables, no seed (leere Liste bei First-Run)
├── queries.ts # useAllCampaigns, useCampaign(id), useTemplates, useSendStats(id)
├── stores/
│ ├── campaigns.svelte.ts # create, update, schedule, cancel, send, duplicate, delete
│ ├── templates.svelte.ts # createTemplate, updateTemplate, deleteTemplate
│ └── settings.svelte.ts # From-Name, Reply-To, Footer, DNS-Check-Status
├── audience/
│ ├── segment-builder.ts # Pure: Filter-Ausdrücke parsen + auf Contacts matchen
│ └── AudienceBuilder.svelte # UI: Tag-Chips + Feld-Filter + Preview-Liste
├── editor/
│ ├── Editor.svelte # Tiptap-Wrapper mit Newsletter-spezifischen Extensions
│ ├── ImageUpload.svelte # Via uload, wie bei invoices/logo
│ └── extensions/ # Tiptap-Extensions für Merge-Tags ({{vorname}}), Divider, Button
├── preview/
│ ├── DesktopPreview.svelte # iframe mit gerendertem HTML
│ ├── MobilePreview.svelte # iframe, schmal, mit User-Agent-Mobile-Hack
│ └── PlainTextPreview.svelte # Pflicht-Alternative für Non-HTML-Clients
├── components/
│ ├── CampaignCard.svelte # Listenzeile: Betreff + Status + Empfänger + Datum
│ ├── StatusBadge.svelte # draft / scheduled / sending / sent / cancelled
│ ├── StatsPanel.svelte # Open-Rate, Click-Rate, Bounces als Kennzahlen
│ └── DnsCheckBanner.svelte # Warnung wenn SPF/DKIM/DMARC fehlt
├── views/
│ ├── ListView.svelte # Kampagnen-Liste mit Status-Filter + Stats-Cards
│ ├── DetailView.svelte # Kampagnen-Detail mit Preview + Stats
│ ├── ComposeView.svelte # Compose: Audience → Content → Review → Send
│ └── TemplateManager.svelte # Template-Bibliothek
├── tools.ts # AI-Tools
├── constants.ts # DEFAULT_TEMPLATES, STATUS_LABELS, TRACK_PIXEL_URL
├── module.config.ts # appId: 'broadcast', tables: [...]
└── index.ts
Daten-Schema
Dexie-Tabellen (client, encrypted)
export type CampaignStatus =
| 'draft'
| 'scheduled' // zeitgesteuert, noch nicht versendet
| 'sending' // aktiv im Versand
| 'sent' // komplett abgeschlossen
| 'cancelled';
export interface LocalCampaign extends BaseRecord {
name: string; // encrypted — interner Arbeitstitel
subject: string; // encrypted — E-Mail-Betreff
preheader?: string | null; // encrypted — die Zeile nach dem Betreff in Gmail-Liste
fromName: string; // encrypted — "Till Ilmatar"
fromEmail: string; // encrypted — muss zu verifizierter Domain gehören
replyTo?: string | null; // encrypted
content: { // encrypted — Tiptap-JSON + generiertes HTML
tiptap: object; // Roh-JSON vom Editor
html?: string; // Gerenderter HTML-Output (Cache, regeneriert bei save)
plainText?: string; // Fallback für Non-HTML-Clients
};
templateId?: string | null; // plaintext — FK zu broadcastTemplates, optional
audience: { // encrypted — Segment-Definition als serialisierter AST
filters: Array<{
field: 'tag' | 'email' | 'custom';
op: 'has' | 'not-has' | 'eq' | 'contains';
value: string;
}>;
estimatedCount: number; // cached, plaintext (für Listen-Preview)
};
scheduledAt?: string | null; // plaintext — ISO timestamp für Scheduled-Send
sentAt?: string | null; // plaintext
status: CampaignStatus; // plaintext
serverJobId?: string | null; // plaintext — verknüpft mit mana-mail-Job
stats?: { // plaintext — cached, wird per API aktualisiert
totalRecipients: number;
sent: number;
delivered: number;
bounced: number;
opened: number;
clicked: number;
unsubscribed: number;
lastSyncedAt: string;
};
}
export interface LocalBroadcastTemplate extends BaseRecord {
name: string; // encrypted
description?: string | null; // encrypted
subject?: string | null; // encrypted — Vorlage für Betreff
content: { tiptap: object }; // encrypted
isBuiltIn: boolean; // plaintext — built-in templates come from constants.ts
thumbnailUrl?: string | null; // plaintext — mana-media
}
export interface LocalBroadcastSettings extends BaseRecord {
// Sender-Profil (Default für neue Kampagnen)
defaultFromName: string; // encrypted
defaultFromEmail: string; // encrypted
defaultReplyTo?: string | null; // encrypted
defaultFooter?: string | null; // encrypted — erscheint unter jeder Kampagne
// DNS-Check-Status (last-checked)
dnsCheck?: { // plaintext
domain: string;
spf: 'ok' | 'missing' | 'wrong';
dkim: 'ok' | 'missing' | 'wrong';
dmarc: 'ok' | 'missing' | 'wrong' | 'weak';
checkedAt: string;
} | null;
// Compliance / DSGVO
legalAddress: string; // encrypted — Pflicht im Footer (ImpressumsPflicht)
unsubscribeLandingCopy?: string | null;// encrypted — Copy für Abmelde-Seite
}
Per-Recipient-Status bleibt serverseitig — siehe Backend-Schema unten. Kein Dexie-Mirror (könnte bei 10k+ Empfängern teuer werden).
Postgres (mana-mail-seitig, neuer broadcast Schema)
-- Server-seitiger Spiegel einer gesendeten Kampagne (für Tracking-Events)
CREATE TABLE broadcast.campaigns (
id TEXT PRIMARY KEY, -- matches LocalCampaign.id
user_id TEXT NOT NULL,
subject TEXT, -- für Audit; wird nicht re-sendet
sent_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE broadcast.sends (
id TEXT PRIMARY KEY, -- UUID
campaign_id TEXT REFERENCES broadcast.campaigns(id) ON DELETE CASCADE,
recipient_email TEXT NOT NULL,
recipient_contact_id TEXT, -- FK zu contacts, optional
tracking_token TEXT NOT NULL UNIQUE, -- für Open/Click/Unsubscribe-Links
status TEXT NOT NULL, -- queued | sent | delivered | bounced | failed
sent_at TIMESTAMPTZ,
bounced_at TIMESTAMPTZ,
bounce_reason TEXT
);
CREATE TABLE broadcast.events (
id BIGSERIAL PRIMARY KEY,
send_id TEXT REFERENCES broadcast.sends(id) ON DELETE CASCADE,
kind TEXT NOT NULL, -- open | click | unsubscribe
occurred_at TIMESTAMPTZ DEFAULT NOW(),
ip_hash TEXT, -- HMAC — für Dedup, nicht PII
user_agent_hash TEXT, -- HMAC — für Dedup
link_url TEXT, -- nur bei kind=click
metadata JSONB
);
CREATE INDEX ON broadcast.events (send_id, kind);
CREATE INDEX ON broadcast.events (occurred_at DESC);
Encryption-Registry
broadcastCampaigns: entry<LocalCampaign>([
'name', 'subject', 'preheader', 'fromName', 'fromEmail', 'replyTo',
'content', 'audience',
]),
broadcastTemplates: entry<LocalBroadcastTemplate>([
'name', 'description', 'subject', 'content',
]),
broadcastSettings: entry<LocalBroadcastSettings>([
'defaultFromName', 'defaultFromEmail', 'defaultReplyTo', 'defaultFooter',
'legalAddress', 'unsubscribeLandingCopy',
]),
Backend-Erweiterungen (mana-mail)
Neue Routes
services/mana-mail/src/routes/
├── broadcast-send.ts # POST /v1/mail/bulk-send (JWT auth)
├── broadcast-stats.ts # GET /v1/mail/campaigns/:id/events (JWT auth)
├── broadcast-track.ts # GET /v1/mail/track/... (PUBLIC, no auth)
└── broadcast-dns.ts # GET /v1/mail/dns-check?domain=... (JWT auth)
Neue Services
services/mana-mail/src/services/
├── broadcast-orchestrator.ts # Bulk-Send-Loop mit Rate-Limiting
├── html-inliner.ts # juice-Wrapper für inline-css
├── tracking-token.ts # HMAC-Sign/Verify
├── bounce-handler.ts # SMTP-Response-Parser
└── dns-check.ts # SPF/DKIM/DMARC-Verifier
Neue Env-Vars
BROADCAST_TRACKING_SECRET=... # HMAC-Secret für Tokens
BROADCAST_MAX_RECIPIENTS_PER_HOUR=500
BROADCAST_MAX_RECIPIENTS_PER_CAMPAIGN=5000
BROADCAST_UNSUBSCRIBE_LANDING_URL=https://mana.how/unsubscribe
Tracking-Pipeline
Open-Tracking (Pixel)
Beim Rendern der Mail wird vor </body> ein 1×1-Bild eingefügt:
<img src="https://mail.mana.how/v1/mail/track/open/{token}"
width="1" height="1" alt="" style="display:block">
Server-Endpoint:
- Prüft Token (HMAC), findet
send_id - INSERT in
broadcast.eventsmitkind='open', IP/UA-Hash - Dedupe per (send_id, ip_hash, user_agent_hash, day) — sonst zählt "User öffnet 5×" als 5 Opens
- Response: 1×1 transparent GIF,
Cache-Control: no-store
Click-Tracking (Link-Rewrite)
Beim HTML-Rendering werden alle <a href="X">-Links umgeschrieben zu:
<a href="https://mail.mana.how/v1/mail/track/click/{token}?url=<encoded>">
Server-Endpoint:
- Prüft Token, findet
send_id - INSERT in
broadcast.eventsmitkind='click',link_url=decoded - 302-Redirect zum Original-URL
- Bei ungültigem Token: 302 zum Original (graceful fail — User soll nicht stehen bleiben)
Known issue: User, die die Mail als "als Text anzeigen" öffnen, bekommen kein Tracking. Akzeptabel.
Unsubscribe
Zwei Pfade:
- Link im Mail-Footer → GET
/track/unsubscribe/:token→ HTML-Seite mit Bestätigen-Button - List-Unsubscribe-Header → Gmail-Native → POST direkt
Beide landen im gleichen Handler: UPDATE broadcast.sends SET status='unsubscribed' WHERE tracking_token = ?, plus Event-Log. Bei zukünftigen Kampagnen automatisch ausgeschlossen.
UI-Konzept
Landing (/broadcasts)
- Kennzahlen-Karten oben: Kampagnen dieses Jahr, durchschnittliche Öffnungsrate, Unsubscribes
- Status-Chips: Alle | Entwurf | Geplant | Gesendet | Abgebrochen
- Kampagnen-Liste: Karte pro Kampagne mit Betreff + Empfänger-Count + Sent-Datum + Mini-Stats (%)
- FAB: "+ Neue Kampagne" → öffnet ComposeView
Compose-Flow (4 Schritte mit Stepper)
- Audience — Filter-Builder mit Live-Preview der Empfängerliste
- Content — Editor mit Live-Preview (Desktop + Mobile Tabs)
- Preflight — Review: Absender, Betreff, Empfänger-Count, DNS-Check, Unsubscribe-Link-Check, Spam-Score-Preview
- Send — "Jetzt senden" ODER "Später senden" (Datepicker)
DetailView
- Header: Betreff + Status + Sent-At + Empfänger-Count
- Stats-Panel (für
sentKampagnen): Open-Rate, Click-Rate, Bounce-Rate, Unsubscribe-Rate als Prozent-Balken - Preview: iframe mit finalem HTML
- Empfänger-Liste: tabellarisch, filterbar (alle / bounced / unsubscribed)
- Klick-Liste (für Performance-Analyse): Top-geklickte Links mit Count
Settings
- Absender-Defaults (Name, E-Mail, Reply-To)
- Legal-Address (Impressum-Pflicht)
- Default-Footer
- DNS-Check-Widget — klickbar "jetzt prüfen" → ruft
/v1/mail/dns-check, zeigt SPF/DKIM/DMARC-Status mit Copy-Paste-DNS-Records
Milestones
| Milestone | Umfang | Aufwand |
|---|---|---|
| M1 Skelett | Modul registriert, Dexie-Tables, leere ListView + ComposeView-Stub, Settings-Placeholder | 1 Tag |
| M2 Audience + Editor | AudienceBuilder (Tag-Filter), Tiptap-Editor mit Bild-Upload, Live-Count der Empfänger | 3 Tage |
| M3 HTML-Render | Tiptap-JSON → HTML mit Footer + Unsubscribe-Link, Desktop/Mobile-Preview, PlainText-Fallback | 2 Tage |
| M4 Bulk-Send (synchron) | /v1/mail/bulk-send in mana-mail (sync loop), Tracking-Token-Signing, HTML-Inlining via juice |
3 Tage |
| M5 Open + Click Tracking | Track-Endpoints, Pixel + Link-Rewrite, Postgres-Schema, Stats-API | 2 Tage |
| M6 Unsubscribe | Landing-Seite, List-Unsubscribe-Header, Empfänger-Exclusion bei nächster Kampagne | 1 Tag |
| M7 Stats-Dashboard | StatsPanel mit Raten, Empfänger-Liste filterbar, Klick-Liste | 1.5 Tage |
| M8 DNS-Check + Settings | DNS-Verifier, Settings-UI mit Copy-Paste-Records, Legal-Address-Pflicht | 1 Tag |
| M9 AI-Tools | suggest_subject_line, segment_audience, draft_campaign_from_notes |
1.5 Tage |
| M10 Dashboard-Widget + Tests | "Offene Kampagnen + YTD-Stats" als Widget, Unit-Tests für Pure-Helpers | 1 Tag |
Summe MVP (M1–M10): ~17 Arbeitstage → realistisch ~4 Wochen mit Polish + Testing.
AI-Integration
Nach M4-Basis-Versand. Tools werden in @mana/shared-ai's AI_TOOL_CATALOG registriert:
| Tool | Policy | Beschreibung |
|---|---|---|
create_campaign |
propose | Entwurf aus Kurzbeschreibung — Subject, Audience-Filter, Body-Grobstruktur |
suggest_subject_line |
auto | 5 Varianten eines Betreffs, optimiert für Öffnungsrate (kurz, aktiv, konkret) |
segment_audience |
auto | "Kontakte mit Tag X, aber nicht Y" — parse Natural-Language-Query zu Filter-AST |
draft_campaign_from_notes |
propose | Nimmt eine note als Input, generiert Newsletter-Outline |
list_campaigns |
auto | Status + Zeitraum, für Planner-Kontext |
get_campaign_stats |
auto | Raten für eine spezifische Kampagne |
schedule_campaign |
propose | Entwurf → Scheduled mit Timestamp |
Mission-Beispiel: "Jeden Freitag meine Notizen der Woche zu einem Newsletter-Entwurf machen" — auto-Call list_notes(thisWeek), dann propose draft_campaign_from_notes.
Offene Fragen
-
Sender-Domain: Mana als SaaS braucht eine eigene Versand-Domain (z.B.
mail.mana.how). Wenn User eigene Domain (news.kunde.ch) nutzt, müssen wir DKIM/SPF für deren Domain co-signen — das ist Stalwart-Config-Arbeit. MVP: nur mana-Domain, DNS-Check für Custom-Domain kommt M8. -
Spam-Score Preview (M3 Preflight): Nutzen wir
SpamAssassinoder eine SaaS (Mail-Tester.com API)? SpamAssassin selbst hosten ist Wartungsaufwand. Für MVP: einfache Regex-basierte Hinweise ("Betreff ist zu lang", "zu viele Ausrufezeichen") + externe manuelle Prüfung empfehlen. -
Image-Hosting: Bilder in Mails müssen von einer öffentlichen URL kommen. Nutzen wir uload wie bei invoices/logo? Dann müssen uload-URLs CDN-cached + nicht hinter Auth sein. Klären vor M2.
-
Webhooks für Bounce-Handling: Stalwart kann Bounce-Notifications als Webhook senden. Alternative: periodisches IMAP-Polling der
bounces@-Mailbox. Einfacher = IMAP-Poll, robuster = Webhook. -
Rate-Limits: wie viele Mails pro Stunde sind OK für unser Stalwart-Setup? Muss man mit dem Hoster (Nine/Cloudflare/…) abklären. Konservativ: 100/h pro User, 500/h global, im MVP.
-
Zero-Knowledge-Mode: Newsletter-Content ist by-design nicht sensitiv (geht ja an N Empfänger raus). Aber: Empfänger-Listen SIND sensitiv. Im ZK-Mode müsste der Server die Empfängerliste entschlüsseln können, um Mails zu versenden — das bricht ZK. Lösung: Broadcast-Modul ist explizit nicht ZK-kompatibel, warnt im Onboarding.
-
Tiptap-Bundle-Size: Tiptap core + Starterkit + Image + Link ≈ 100 KB gzipped. Akzeptabel. Wir lazy-laden den Editor nur in ComposeView.
-
Empfänger aus mehreren Quellen: Nur contacts für MVP. Phase 2: manuelle Listen (CSV-Import), Phase 3: externe Integrationen (Mailchimp-Import).
Abhängigkeiten
contacts— Empfängerlisten (vorhanden)mana-mail— muss Bulk-Send + Track-Endpoints bekommen (neue Routes)mana-media(uload) — für Bild-Einbettung im Editor (vorhanden)rituals— Phase 2 für wiederkehrende Kampagnen (vorhanden)notes— AI-Tooldraft_campaign_from_notes(vorhanden)- npm:
@tiptap/core,@tiptap/starter-kit,@tiptap/extension-image,@tiptap/extension-link,juice(server-seitig in mana-mail)
Erfolgs-Kriterien
- Tag 1 nach MVP-Launch: Till verschickt Newsletter an 5 Test-Empfänger, sieht Open-Stats im Dashboard
- Woche 2: 3 externe Alpha-Tester nutzen das Modul für echte Kampagnen
- Monat 1: Eine Kampagne mit ≥ 100 Empfängern verschickt, Open-Rate ≥ 25% (Consumer-Benchmark)
- Monat 3: Mission "Jeden Freitag Newsletter-Entwurf aus Notizen" läuft automatisch für mindestens einen User
Phase 2 (nach MVP)
- Double-Opt-In mit Landing-Pages
- Consent-Audit-Log (wer hat wann zugestimmt, woher)
- A/B-Testing (zwei Subjects, 20% Test, automatischer Gewinner-Versand)
- Drip-Kampagnen via
rituals-Integration - Empfänger-CSV-Import
- Externe Imports (Mailchimp/Substack)
- Bounce-Webhook statt IMAP-Poll
- Custom-Domain-DKIM-Signing
Phase 3 (Vereinsvariante)
- Mitglieder-Gruppen als Audience-Quelle (nach Paket A Rollen-/Rechtematrix)
- "Invoices + Newsletter"-Kombi: Mitgliederrechnung mit Anschreiben-Mail
- Vereinssatzungen als Anhang bei Jahresmailing