Files
Inboxintel/docs/specs/feature-sender-policy.md
cesnimda be6cbf90d7 docs: full specs for automation engine, sender policy, activity log, privacy monitor, UI overhaul
Clean.Email-parity feature build-out plus a Stripe/Notion-style UI rebuild on
Tailwind + shadcn-style primitives. Locks the hybrid-automation, Gmail-only,
light+dark, incremental-rollout decisions and lays out the backend/frontend
build sequence.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 23:04:52 +02:00

149 lines
7.0 KiB
Markdown

# Spec: Sender Policy — Block, Whitelist, Screener, Pause, Read-Later, Keep-Newest, Deliver-To
Per-sender ongoing behaviors. These are toggles the user sets from the Senders UI (or
the Screener queue), distinct from the condition-based `AutomationRule`s. They share the
same execution engine and `AutomationAction` queue/log (`feature-rules-engine.md`).
## 1. Domain model
### 1.1 `SenderPolicy` (new entity)
One row per sender that has any non-default policy. Created lazily.
```csharp
public class SenderPolicy : AuditableEntity
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid UserId { get; set; }
public Guid SenderId { get; set; }
public Sender? Sender { get; set; }
public SenderDisposition Disposition { get; set; } = SenderDisposition.None;
// Independent toggles (a sender can be Read-Later AND Keep-Newest, etc.)
public bool ReadLater { get; set; } // route new mail to InboxIntel/Read Later, skip inbox
public bool Paused { get; set; } // hold new mail in InboxIntel/Paused, skip inbox
public int? KeepNewestCount { get; set; } // trash all-but-newest-N (destructive → proposed)
public int? TrashOlderThanDays { get; set; } // per-sender trash-by-age (destructive → proposed)
public string? DeliverToLabel { get; set; } // auto-apply this label to new mail
public bool DeliverToSkipInbox { get; set; } // move vs. tag for Deliver-To
public DateTimeOffset? UpdatedUtc { get; set; }
}
public enum SenderDisposition
{
None = 0,
Allow = 1, // Whitelist — exempt from ALL automation, always reaches inbox
Block = 2, // auto-trash new mail (destructive → proposed)
Screening= 3, // unknown sender quarantined pending approve/block
}
```
> `Allow` (Whitelist) is checked first in the engine and short-circuits every other
> rule/policy for that sender. `Block` proposes Trash on each new message. `Screening`
> applies the `InboxIntel/Screener` label + skip-inbox and surfaces in the Screener queue.
### 1.2 Managed labels (created via `EnsureLabelAsync`)
| Feature | Label | On apply |
|---------|-------|----------|
| Screener | `InboxIntel/Screener` | + skip inbox |
| Pause | `InboxIntel/Paused` | + skip inbox |
| Read-Later | `InboxIntel/Read Later` | + skip inbox |
| Deliver-To | user-chosen (any Gmail label) | optional skip inbox |
Whitelist/Block need no label (exempt / trash).
## 2. Screener semantics
The Screener catches mail from **first-seen senders**.
- A sender is "known" if the user has ever received mail from them **before** the
screener was enabled, or has explicitly Allowed/Blocked them. Establish a baseline
`ScreenerEnabledUtc` on the `User` when the user turns Screener on, so existing
contacts aren't all quarantined retroactively.
- During automation, a candidate email from a sender with **no prior history before
`ScreenerEnabledUtc`** and **no disposition** → create `SenderPolicy { Disposition =
Screening }`, apply `InboxIntel/Screener` + skip inbox, log `AutomationAction(Source=Screener,
Applied)` (this is a *safe* action — just labeling/skip-inbox, nothing destroyed).
- **Screener queue UI**: lists screening senders with a sample subject + count.
- **Approve** → `Disposition = Allow`; remove `InboxIntel/Screener`, restore to inbox
for held mail; future mail flows normally.
- **Block** → `Disposition = Block`; propose Trash for held mail; future mail auto-proposed for trash.
- Screener is **opt-in** (a `User.ScreenerEnabled` flag, default false) because it
actively reroutes mail.
`User` additions (migration): `ScreenerEnabled` (bool), `ScreenerEnabledUtc` (DateTimeOffset?).
## 3. Pause vs Read-Later vs Deliver-To
All three are **safe** (label + optional skip-inbox), so they auto-apply:
- **Pause**: temporarily stop a sender cluttering the inbox without unsubscribing. New
mail → `InboxIntel/Paused` + skip inbox. **Resume** removes the policy and (optionally)
re-inboxes held mail.
- **Read-Later**: newsletters you want to read on your own time → `InboxIntel/Read Later`
+ skip inbox. A "Read Later" view in-app lists them.
- **Deliver-To**: auto-file a sender's mail under a chosen label (e.g. "Receipts"),
optionally skipping the inbox.
## 4. Block & Keep-Newest & per-sender Trash-by-Age
Destructive → **proposed**, surfaced in the Review queue:
- **Block**: each new message from a blocked sender → propose Trash. (We never
hard-delete; Gmail Trash auto-purges after 30 days.)
- **Keep-Newest** / **per-sender Trash-by-Age**: evaluated in the daily full sweep
(`feature-rules-engine.md` §5), proposing Trash for the cull set.
## 5. Application layer
```csharp
public interface ISenderPolicyService
{
Task<SenderPolicyDto> GetAsync(Guid userId, Guid senderId, CancellationToken ct = default);
Task<SenderPolicyDto> SetAsync(Guid userId, Guid senderId, SenderPolicyInputDto input, CancellationToken ct = default);
Task ClearAsync(Guid userId, Guid senderId, CancellationToken ct = default);
// Screener
Task<IReadOnlyList<ScreenerEntryDto>> GetScreenerQueueAsync(Guid userId, CancellationToken ct = default);
Task ApproveSenderAsync(Guid userId, Guid senderId, CancellationToken ct = default); // → Allow
Task BlockSenderAsync(Guid userId, Guid senderId, CancellationToken ct = default); // → Block
Task SetScreenerEnabledAsync(Guid userId, bool enabled, CancellationToken ct = default);
}
```
The engine reads `SenderPolicy` rows alongside `AutomationRule`s in `RunAsync`. Policy
evaluation order: **Allow (exempt) → Block → Pause → Read-Later → Deliver-To →
Keep-Newest/Trash-by-Ae**. A whitelisted sender exits immediately.
## 6. API (`SenderPolicyController` or extend an existing senders controller)
| Method | Route | Purpose |
|--------|-------|---------|
| GET | `/senders/{id}/policy` | current policy |
| PUT | `/senders/{id}/policy` | set disposition/toggles |
| DELETE | `/senders/{id}/policy` | clear |
| GET | `/screener` | screener queue |
| POST | `/screener/{senderId}/approve` | whitelist |
| POST | `/screener/{senderId}/block` | block + propose trash |
| PUT | `/screener/enabled` | enable/disable screener |
## 7. Frontend
- **Senders page**: each sender row gets a policy menu (DropdownMenu): Whitelist, Block,
Pause, Read-Later, Deliver-To→(label picker), Keep-Newest→(N), Trash-by-Age→(days).
Active policies shown as small badges on the row.
- **Screener page** (`/app/screener`): queue of screening senders, Approve/Block per row,
bulk approve/block, and a master enable toggle with an explainer.
- **Read Later view** (`/app/read-later`): mail tagged `InboxIntel/Read Later`.
## 8. Security & safety
- [ ] Policy rows verified to belong to `UserId` before any read/write.
- [ ] Block/Keep-Newest/Trash-by-Age only ever **propose** (hybrid rule) — never auto-trash.
- [ ] Whitelist exemption enforced before any other policy/rule, with a test.
- [ ] Screener baseline (`ScreenerEnabledUtc`) prevents retroactive mass-quarantine.
- [ ] Resume/Approve restores inbox state from `AutomationAction.UndoStateJson`.