Files
Inboxintel/docs/specs/feature-sender-policy.md
T
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

7.0 KiB

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 AutomationRules. 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.

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.
    • ApproveDisposition = Allow; remove InboxIntel/Screener, restore to inbox for held mail; future mail flows normally.
    • BlockDisposition = 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

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 AutomationRules 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.