Tracking: Sieve-driven email publishing to permissioned ATProto spaces
openTracking: Sieve-driven email publishing to permissioned ATProto spaces
Design intent
Enable user-directed email workflows: a user authors a Sieve script that matches emails they want to share, and on match the email is saved (JMAP JSON form) as a record in a permissioned ATProto data space using the user’s credentials.
Example: [email protected] publishes all inbound mail as records in a shared
space; an external help-desk app syncs the space, creates cases, assigns workers.
This issue tracks the overarching design. It is the basis for a follow-up design session to decompose into smaller tasks. Not all decisions need to be locked here.
Proposal (recommended architecture)
Signal + bridge publisher. No custom Sieve action, no Stalwart fork.
- User sieve (untrusted, delivery-time) stays portable standard Sieve and only
signals:
fileintoa designated mailbox and/oraddflag. - A sovrn publisher daemon watches via JMAP (push/poll on the signal mailbox),
fetches
Email/getJMAP JSON via the service credential (+ Impersonate), and writesat.sovrn.mail.archive(name TBD) into the target permissioned space using the user’s delegated PDS grant. - “With whom” is controlled by space membership (
com.atproto.simplespacemember list), not by raw addresses in Sieve.
Rationale:
- Untrusted user scripts cannot
exec/HTTP by design; only admin trusted scripts can. Giving users trusted-script power breaks isolation. - A
vnd.sovrn.*Sieve action would require forkingstalwartlabs/sieve(grammar + bytecode + runtime + host loops) with AGPL + upkeep cost. Prefer HTTP-level integration against unmodified Stalwart (seedocs/05-stalwart-integration.mdlicensing posture). - Pre-queue (milter/MTA hook) would publish spam/rejected mail; post-delivery JMAP watch is the correct stage.
User-control requirements (locked direction)
- Match logic is 100% user Sieve. Bridge never re-interprets it.
- User picks a human-meaningful mailbox/space name (e.g.
Shared/<name>orSpace/<name>, exact prefix TBD) whose purpose is obvious from the name they chose. The name maps to an opaque spaceskeyunderneath soat://URIs leak no names. - Revocation is user-side: edit script, remove
fileinto, leave space, or revoke the PDS grant. All stop publishing without operator action. - App-view validates the script on save: only allowlisted publish targets permitted; audit-log every publish-target change.
Follow-up decisions (to lock before spec)
- Signal naming + mapping (user input: user controls the name).
Decide
Shared/<name>vsSpace/<name>, allowed charset/length, mappingname -> skey, rename semantics, per-user vs per-domain namespaces. - Redaction / payload shape (user input: curated subset first). Define the redaction list as part of this work. Initial direction: keep only a curated subset (messageId, from/to/date/subject, thread refs, text body snippet?, attachment metadata not content?) rather than full JMAP JSON. Decide size caps, attachment/blob-ref policy, header allowlist/blocklist, what the help-desk MVP needs.
- Credential scope (user input: likely standard permissionSet).
Decide: new OAuth scope profile (
repo:at.sovrn.mail.archive?action=create) vs bundling into the standard permissionSet. Covers incremental re-auth UX, refresh storage (device_grants-like), TTL, revocation fan-out. Seedocs/02-identity-and-auth.mdscopes +docs/04-spaces.mdcredential chain. - Space topology. Per-share space (
skey= share ID) vs reuse of domain space? Recommendation: per-share space to isolate help-desk membership. Needs new space type (e.g.at.sovrn.space.inbox) + record lexicon (e.g.at.sovrn.mail.archive). - Author/reader invariant. Confirm
authorDid == mailbox-owner DIDfilter on the reader side; allowlist model for the external app. - Publisher mechanics. JMAP EventSource/WS vs poll fallback, journal + content-hash dedupe (Message-ID -> rkey), retry/backoff, per-user rate caps, loop guard header, failure never blocks delivery.
- Erasure propagation. Mailbox/record delete -> space record delete +
member-rotation story per
docs/04-spaces.mderasure section; residual-copy disclosure. - Sieve save-time validation UX. Where the allowlisted-target check lives (app-view XRPC vs manage path), error copy, audit log schema.
Candidate decomposition (for design session)
- S-e1: space + record lexicons (
at.sovrn.space.inbox,at.sovrn.mail.archive). - S-e2: permission/scope decision + grant storage + re-auth UX.
- S-e3: Sieve signal contract + save-time validation + audit log.
- S-e4: publisher daemon (watch -> fetch -> redact -> publish -> dedupe/retry).
- S-e5: redaction list + payload fixtures.
- S-e6: erasure + revocation e2e test.
- S-e7: help-desk reference consumer doc.
References
docs/04-spaces.md(permissioned spaces, member-list v1, publisher/reader).docs/02-identity-and-auth.md(DPoP-bound tokens unusable cross-service; brokered grants).docs/05-stalwart-integration.md(Registry/JMAP-only control plane, no fork).- Prior finding: Stalwart untrusted Sieve has no
pipe/exec; trusted-onlyexec/query/http_headernot suitable for user-authored scripts.