Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
3d7cc62
docs: name the event pipeline (ADR-010) + email tools design
arthware-dev Jun 20, 2026
e02c25e
docs: email runs as a container, not a host service
arthware-dev Jun 20, 2026
fbaf199
feat: email as a capture source (pipeline core)
arthware-dev Jun 20, 2026
02f0cfb
feat: parse Maildir RFC822 emails for ingestion
arthware-dev Jun 21, 2026
3842a8b
docs: email vault paths, thread-folding, summary gate + transport fin…
arthware-dev Jun 21, 2026
894cec6
test: pin email IMAP ingestion end-to-end against GreenMail
arthware-dev Jun 21, 2026
b6b9bf3
feat: IMAP mail fetcher with thread-keying and dedup
arthware-dev Jun 21, 2026
fe30bdd
feat: fold an email conversation into one thread file
arthware-dev Jun 22, 2026
eae9435
docs: diagram the email thread-fold path; mark threading shipped
arthware-dev Jun 22, 2026
eff877c
docs: gateway/archivist split, m.thread mapping, twofold event shape
arthware-dev Jun 22, 2026
721a7fe
docs: mail bot talks for config; ingest plumbing belongs in MicroBot
arthware-dev Jun 22, 2026
f6ad136
docs: email completeness = raw_content body + attachments pasted to room
arthware-dev Jun 22, 2026
cdf336c
refactor: move email parse + IMAP fetch into the framework (stack.*)
arthware-dev Jun 22, 2026
103410a
feat: MicroBot.post_source_message — twofold threaded ingest helper
arthware-dev Jun 22, 2026
1a7d7cc
feat: mail bot — fetch email, post it threaded into a room
arthware-dev Jun 22, 2026
7ca86a7
feat: archivist files email posted by the mail bot
arthware-dev Jun 22, 2026
f5f9c83
feat: render stack.toml [mail] into the mail bot's config
arthware-dev Jun 22, 2026
1bd2f1a
feat: mail bot introduces itself on join; archivist ignores bot chatter
arthware-dev Jun 22, 2026
4573f72
fix: provision the mail bot's Matrix password on up
arthware-dev Jun 22, 2026
16b94d9
feat: cleaner mail message layout in the room
arthware-dev Jun 22, 2026
21dcfd9
fix: start the mail poll loop on every launch, not once ever
arthware-dev Jun 22, 2026
70c27f9
fix: keep an email thread in one vault file; convert HTML mail to mar…
arthware-dev Jun 22, 2026
236f03c
feat: tag email with sender, mailbox, and folder; stop filing the bot…
arthware-dev Jun 22, 2026
3e1325d
feat: defang email links to plain non-clickable text (anti-phishing)
arthware-dev Jun 23, 2026
3c5a999
feat: harden capture classifier against prompt injection from untrust…
arthware-dev Jun 23, 2026
bbf16e3
feat: UID-incremental IMAP polling so steady-state fetch is cheap
arthware-dev Jun 23, 2026
cf81bd9
feat: optional since-date floor to backfill existing IMAP mail
arthware-dev Jun 23, 2026
fe40b1b
feat: date-prefix email thread filenames for chronological sort
arthware-dev Jun 23, 2026
b4647a8
fix: date email with no Date header by INTERNALDATE, not processing time
arthware-dev Jun 23, 2026
68a9fc5
feat: widening the since floor re-scans older mail
arthware-dev Jun 23, 2026
851d169
feat: post email attachments into the room for the archivist to file
arthware-dev Jun 23, 2026
26bc09c
feat: file email attachments on behalf of the source, not the bot
arthware-dev Jun 23, 2026
acfdaad
feat: scope email to the bucket of the room it lands in
arthware-dev Jun 23, 2026
cb0c073
feat: scope email by room membership (bots excluded), incl. DMs
arthware-dev Jun 23, 2026
f0ef487
docs: explain email in the user guide
arthware-dev Jun 23, 2026
c1a1090
docs: email setup in the admin guide
arthware-dev Jun 23, 2026
cd45269
feat: bots apply their bot.toml display name on launch; rename Mail -…
arthware-dev Jun 23, 2026
c2a9a72
feat: stack core mail — test IMAP config and list mailbox folders
arthware-dev Jun 24, 2026
2f69773
feat: stack core mail shows a masked credential preview
arthware-dev Jun 24, 2026
0859ad6
feat: stack core mail labels output as the bound config + restart hint
arthware-dev Jun 24, 2026
e5ec7a0
fix: render HTML email tables instead of raw pipes
arthware-dev Jun 24, 2026
d64d1f6
feat: compact email card in the timeline, full body in the thread
arthware-dev Jun 24, 2026
d8627b1
feat: filter_noise drops newsletters/automated mail before the brain
arthware-dev Jun 24, 2026
f5fb16f
docs: email troubleshooting + README capability line; SSL is the default
arthware-dev Jun 24, 2026
2ee6d28
fix: filter email display chrome and restrict attachment types
arthware-dev Jun 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ The reference implementation runs on a Mac Studio M1 in our living room at Lake
- **Your family wiki**: an Obsidian-compatible second brain, generated from the documents, notes, and voice memos you file (memory).
- **Local AI engine** on Apple Metal GPU: voice transcription, text-to-speech, document classification (oMLX + Whisper + Piper).
- **A bot runtime in chat** that automates the small stuff: filing receipts, transcribing voice memos, status reports.
- **Email into the family brain**: point a mailbox at a chat room and new mail (with its attachments) lands there for the archivist to file, newsletters and marketing filtered out (IMAP, read-only, private).
- **One CLI to operate it all**: `./stack up <thing>` and it is running.

<table align="center">
Expand Down
63 changes: 63 additions & 0 deletions docs/admin-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,53 @@ Auto-generated passwords for service accounts. Created once, reused on every `./
./stack config --secrets # include generated passwords
```

### Email (`[mail]`)

> Needs the `core` stacklet running; `docs` + `code` to file what arrives.

famstack can watch IMAP mailboxes and deliver new mail into chat rooms. The mail bot (part of `core`) fetches; the archivist files the message and its attachments. Configure mailboxes in `stack.toml`:

```toml
[mail]
poll_interval = 120 # seconds between checks
filter_noise = true # drop newsletters/automated mail (default true)

[[mail.accounts]]
name = "family" # used for the secret key and provenance tags
imap_host = "imap.example.org"
imap_port = 993 # 993 = IMAP over SSL (the default)
imap_user = "family@example.org"
ssl = true # default; set false only for a local/test server
folder = "INBOX"
room = "!roomid:yourserver" # where this mailbox delivers
since = "2026-01-01" # optional backfill floor (omit = whole folder)
```

The password never goes in `stack.toml` or chat. Put it in `.stack/secrets.toml`, keyed by the account name uppercased:

```toml
mail__FAMILY_IMAP_PASSWORD = "<app-password>"
```

Then `./stack restart core` to apply.

**Check the connection and find folder names:**
```bash
./stack core mail # all accounts: log in, count INBOX, list folders
./stack core mail --account work # just one
```
This logs in read-only and prints the server's **real folder names** (Gmail's `[Gmail]/All Mail`, a localized `Gesendet`, nested paths), which often differ from the webmail labels. Use it to confirm the `folder` value points where you expect, and to debug a wrong host/password before pointing the bot at the mailbox.

Key things to know:

- **Invite both bots into the room.** `@mail-bot` delivers, `@archivist-bot` files. Type the handle into Element's invite box; fresh bot accounts don't show up in directory search.
- **Where mail files = who is in the room.** A room with two or more people files under the shared bucket; a private room or DM (one person) files under that person; a `Topic: ...` room files under the topic. Route work mail and family mail to different rooms to keep them separate.
- **App passwords.** Gmail, iCloud and most providers need an app-specific password with IMAP enabled, not your login password.
- **Backfill with `since`.** It is a floor by the date the server received a message. Start narrow (last week) to check it works, then widen (the whole year); widening re-scans and already-filed mail is skipped. Omit `since` to pull the entire folder on first run.
- **Noise filtering.** `filter_noise` (default on) drops automated and marketing mail before it reaches the room: newsletters and mailing lists (a `List-Unsubscribe` or `List-Id` header), `Precedence: bulk`, auto-replies (`Auto-Submitted`), and machine senders (`noreply@`, `mailer-daemon@`, `bounce@`). Detection is header-only, never body text, so personal mail that merely mentions "unsubscribe" is not dropped. Set `filter_noise = false` to ingest everything.
- **Read-only.** The fetcher never marks mail read or deletes it. It remembers what it has handled by Message-ID and IMAP UID, so restarts don't re-file.
- **Links are defanged.** URLs in mail are filed as non-clickable plain text, so a phishing link can't be tapped by reflex.

---

## Day-to-day operations
Expand Down Expand Up @@ -751,6 +798,22 @@ Shows free space. If the photo library filled the disk, move `data_dir` to an ex

Removes everything. Only use this if you really mean it.

### Email: can't connect, or mail isn't filed

Run the diagnostic first. It logs in and lists the real folders:

```bash
./stack core mail
```

- **"connection failed" / "authentication failed".** Use an **app password** (not your login password) with IMAP enabled at the provider, and check `imap_host`/`imap_port` (993 for SSL). The `credential:` line reads `(EMPTY …)` when the secret-store key doesn't match the account name; it must be `mail__<NAME>_IMAP_PASSWORD` (account name uppercased) in `.stack/secrets.toml`.
- **Mail shows in the room but never gets filed.** The **archivist must be a member of that room**. Invite `@archivist-bot` by typing the handle into Element's invite box (bot accounts don't show in directory search).
- **Edited `stack.toml` but nothing changed.** `stack core mail` and the bot read the *rendered* config, which only refreshes on `./stack restart core`. Restart after any `[mail]` change.
- **Wrong or empty folder.** IMAP folder names differ from the webmail labels (Gmail's `[Gmail]/All Mail`, a localized `Gesendet`). `stack core mail` lists the exact names to copy into `folder`.
- **A whole inbox of old mail flooded in.** Set a `since` date floor and start narrow (last week), then widen it. With `since` unset, the first poll takes the whole folder.
- **Wanted mail is missing.** `filter_noise` (default on) drops newsletters and automated mail; set `filter_noise = false` to keep everything.
- **Threads render oddly on mobile.** Element X needs **beta features** enabled (Settings, then Labs) for threads.

### Something else

```bash
Expand Down
97 changes: 97 additions & 0 deletions docs/adr/adr-010-event-pipeline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# ADR-010: Event Pipeline

## Status
Accepted

## Context
famstack keeps growing inputs that follow the same shape: a document arrives in
Paperless, a URL or note is pasted in Matrix, a voice memo is recorded, and
(next) email is fetched over IMAP. Each one is captured, surfaced in a Matrix
room, filed into the vault, and later consumed — by the wiki deriver, the
search/Q&A loop, reminders, and the planned Family Agent.

Looking at this from a birds-eye view, four boxes recur:

1. **Sources** — an inbound channel produces a `SourceContent` (text + title +
origin URI). Documents, captures, soon email.
2. **Ledger** — every state-changing filing emits a `dev.famstack.event`
envelope on the Matrix timeline (`build_document_event` / `build_capture_event`).
3. **Surface / routing** — rooms bind to intent via `dev.famstack.capture`
room state carrying a `kind` (topic, documents, soon mailbox); filings post
to the bound room.
4. **Projections / processors** — consumers fold the stream into derived state
and actions: the wiki deriver, the tasks rollup, the agent, reminders.

This looks like event sourcing, which raised the question of whether to (a)
adopt event sourcing as a formal architecture and (b) build a generic
Channel/Processor framework now that we have nearly three sources.

The risk on both: the repo's own guidance (`agent/plan.md`, `dev.md`) warns
that generalising across consumers before a second/third concrete caller forces
the abstraction is the canonical premature-framework failure mode. The
archivist's tool loop was deliberately left un-generalised for exactly this
reason.

## Decision
Name the architecture **the event pipeline** and treat it as a *content
pipeline with an audit ledger and derived projections* — event-sourcing-
flavored, but not textbook event sourcing.

- **The vault (Forgejo git) is the working source of truth** for reads and
hand-edits; its git history is the durable record. The `dev.famstack.event`
stream is the audit/notify ledger, not the system of record. We do not adopt
a formal event store or CQRS — that ceremony does not pay off for a
single-Mac, single-family system.
- **The machine-derived vault is reproducible from its sources, indexed by the
ledger.** Everything the machine produced — document mirrors, capture
entries, derived wiki/topic/tasks pages — can be rebuilt by replaying filings
against their sources. The ledger says *what* was filed and *where its source
is*; the source holds the content. **Reprocessing replays the source, never
the vault file** — and the source of a chat-originated filing is the *whole
thread*: a URL/note capture reprocesses **its originating Matrix message plus
the reply chain rooted on it** (the corrections), folded in timeline order; a
document re-fetches Paperless by `paperless_id`, an email re-fetches IMAP by
`message-id`. The vault entry is the fold of original + corrections
(`_collect_correction_chain` already walks this thread for live corrections;
rebuild reuses the same walk). The only content not reproducible this way is
**user hand-edits** (plus `facts.toml`, ontology evolution) — preserved in
git, the irreducible human delta. This is "from sources, indexed by the
ledger" — *not* "from Matrix alone"; events stay thin, and full-content
disaster recovery is the backup stacklet's job, not the ledger's.
- **Extract only the thin seams that already repeat**, nothing more:
1. the `SourceContent` ingestion contract,
2. the `dev.famstack.event` envelope,
3. the `dev.famstack.capture` room-binding-by-`kind`.
A new source should need only: an extractor that yields `SourceContent`, a
`kind` for routing, and (if it acts outward) a tool.
- **Defer the framework.** Do not build a generic Channel/Processor abstraction
layer yet. The next concrete source (email — see
[../design/agent/email-tools.md](../design/agent/email-tools.md)) is the
third instance; build it against the three seams above and let it prove or
strain them. Formalise the abstraction only if a real second consumer or a
second decision branch forces it — on evidence, not aesthetics.

## Consequences
- There is one named mental model for how inputs flow through famstack; new
features describe themselves as "a source" / "a processor" / "a binding".
- Adding a source is cheap and uniform: extractor → `SourceContent` → classify
→ vault + event, plus a `kind` for room routing.
- We keep the useful parts of event sourcing (append-only ledger, replayable
deriver, idempotent keyed writes) without its operational cost.
- The framework is intentionally absent. Until a third instance earns it, some
per-source divergence (Paperless write-back, sender routing, email account
binding + outbound) stays as concrete code rather than forced into a shared
interface — accepted as the cheaper risk than a premature abstraction.
- **Reprocessing is defined as replaying the source, not patching the vault.**
This closes a current gap: capture reprocess reads the vault file today; the
reproducibility guarantee requires it to re-derive from the originating Matrix
message instead. Documents (Paperless re-fetch) and email (IMAP re-fetch)
already work this way.
- The reproducibility guarantee is kept honest by **one rebuild test**:
fabricated sources → ingest → vault; wipe the machine-derived files; rebuild;
assert equality modulo timestamps. Every new source must pass it — if you
can't replay it from its source, you're not done.
- We deliberately stop short of *full* event sourcing (rebuild-from-log alone,
time-travel, event-store infra). If those are ever needed, this ADR is the
place that records we scoped reproducibility to "from sources + ledger" on
purpose, and the decision would be revisited explicitly.
4 changes: 4 additions & 0 deletions docs/agent/dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ famstack/
4. **Convention over configuration.** If a file exists with the documented name (`hooks/on_install.py`, `cli/foo.py`, `caddy.snippet`, `bot/bot.toml`), it is picked up. No registration step.
5. **The `stack` CLI is the sanctioned agent interface.** Bots, hooks, and external automations call `./stack <id> <cmd>` - they do not import from `lib/`. Commands have stable exit codes, JSON output, and idempotent semantics.

## Reprocessing replays the source

Reprocessing re-derives from the source, never from the vault file. For a chat filing the source is the **whole thread** - the original message **plus its reply chain** (corrections) - folded in timeline order. Machine-derived vault state must stay reproducible this way; only user hand-edits are irreducible. See [adr-010](../adr/adr-010-event-pipeline.md).

## Stacklet anatomy

Required:
Expand Down
Loading
Loading