# 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 GetAsync(Guid userId, Guid senderId, CancellationToken ct = default); Task SetAsync(Guid userId, Guid senderId, SenderPolicyInputDto input, CancellationToken ct = default); Task ClearAsync(Guid userId, Guid senderId, CancellationToken ct = default); // Screener Task> 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-Age**. 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`.