Root causes + fixes:
- Archive leaked Sent mail: filter was just 'not in inbox'. Added ExcludeGmailLabels;
Archive now excludes SENT/DRAFT/SPAM/TRASH/CHAT by label.
- Read Later / Pinned / Unlabelled returned EVERYTHING (no folder case -> default all).
Pinned = IsImportant; Read Later = new local IsReadLater marker (+toggle endpoints);
Unlabelled = HasUserLabels=false.
- Trash & Spam always empty: IncludeSpamTrash=false + sync never set IsTrashed nor created
EmailLabel rows. Now fetches spam/trash, sets IsTrashed from the TRASH label, and links
every message to its Gmail labels (also fixes Sent/Spam/category-by-label/Unlabelled).
- Old Mail: now strictly older than 6 months (was 1 year).
Guardrail: sync upsert is now idempotent (drops prior copy before reinsert) so re-sync
can't duplicate a message or leave stale labels. 4 new filter tests; 65 backend + frontend
green; migration is a single non-destructive column add.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Real CI at .gitea/workflows/ci.yml (Gitea reads .gitea/workflows, not the empty
untracked .git/workflows/ci.yml cruft): backend dotnet build + dotnet test (39 tests)
and frontend npm ci + build, on pushes to main/develop and all PRs. Establishes the
merge gate recommended in the Phase 7 git-workflow review.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 5 re-validation caught a functional regression: the V-10 clamp in
SearchService.SearchAsync also capped CleanupService's internal target resolution
(pageSize 10000 -> 200), silently limiting bulk cleanup-by-query to 200 emails.
The clamp belongs at the user-facing trust boundary, not the shared service: move
MaxPageSize (200) enforcement into SearchController (both the POST body and GET query
paths). Internal callers of ISearchService now request large pages unhindered, while
user requests are still bounded. Adds a regression test proving SearchService returns
a 250-row page uncapped. No security regressions per Phase 5. All 39 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolves the Critical destructive-action safety gap (and the CSRF-adjacent risk of
frictionless Gmail mutation):
- Bulk Trash now requires an explicit confirmation dialog (count + 30-day-recovery
note) instead of firing on one click.
- Every bulk action surfaces a success / partial-failure / error toast.
- Rows are removed from the list only when the server confirms the whole batch
succeeded; partial failures leave the list intact so the user can retry, instead
of optimistically hiding failed items.
Built on the new Dialog/Toast primitives (also restyles the toolbar to the new
system). Real cross-session Undo is deferred to the activity-log backend (specced).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Defense-in-depth tenant isolation: every user-owned entity (Email, Sender,
MailThread, MailDomain, Attachment, Label, SyncState, AnalyticsAggregate,
WidgetLayout, UnsubscribeItem) gets a global query filter restricting reads to the
authenticated user. AppDbContext takes an optional ICurrentUser; CurrentUserId is
Guid.Empty for background workers / design-time, which DISABLES the filter so sync
and tooling (which already scope by an explicit userId) are unaffected. On the HTTP
attack surface a forgotten manual `WHERE UserId ==` can no longer leak another
tenant's rows.
Phase 1 confirmed no active IDOR; this is preventive, and prioritised now because the
upcoming automation engine will add many new queries.
Also: moved the Npgsql-only tsvector FTS mapping out of EmailConfiguration into
AppDbContext.OnModelCreating, guarded by Database.IsRelational() (Ignored otherwise),
so non-relational test providers work — honouring the existing Email.SearchVector
comment. Production (Npgsql) model is unchanged; no migration needed.
Adds 3 cross-user tenant-isolation integration tests. All 38 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backend security fixes from the Phase 1 register / Phase 2 roadmap (PR1 + V-01):
- V-01 SSRF: new SafeHttpGuard validates outbound unsubscribe URLs (scheme allowlist
+ DNS-resolve-and-block private/loopback/link-local/ULA/metadata ranges), wired into
UnsubscribeService; the "unsubscribe" HttpClient now disables auto-redirect so a
validated external URL can't 3xx into an internal target. +33 unit tests.
- V-04: session cookie SecurePolicy=Always in non-dev (SameAsRequest in dev).
- V-06: UseExceptionHandler/ProblemDetails in prod; Cleanup/Unsubscribe no longer
echo ex.Message to clients (logged server-side, generic message returned).
- V-08: ForwardedHeaders trusted only from configurable KnownNetworks (default private
ranges) + ForwardLimit, instead of trusting any client. New ForwardedHeaders config.
- V-09: returnUrl validated with Url.IsLocalUrl (no open redirect via OAuth flow).
- V-10: SearchService clamps Page/PageSize (<=200); Analytics clamps take/days.
- V-11: baseline security headers (nosniff, X-Frame-Options DENY, Referrer-Policy,
COOP) + HSTS in prod.
- V-13: /app/info discloses only devMode to anonymous callers unless dev mode is on.
- V-12: API container runs as non-root 'app' user (keys dir pre-owned).
- V-03: Postgres + API ports bound to 127.0.0.1; POSTGRES_PASSWORD now required (no
weak default fallback).
API compatibility preserved (clamps not rejections; error-body shape changes only on
failure paths). No DB migrations. Build + all 33 unit tests green. V-15 (MailKit
NU1902) persists across versions and the SMTP path is default-off — tracked, not bumped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Install Tailwind v3, Radix primitives, cva/tailwind-merge/clsx, lucide-react.
Add HSL design tokens (light + dark) with the Indigo #5b5bf0 accent isolated
to a single --primary token (swappable for a future accent picker), darkMode
'class', anti-flash inline script, useTheme hook, and cn() helper. styles.css
still loads for not-yet-migrated pages. Build green.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Email breach endpoints need no API key (2 req/s, cached), so Privacy Monitor
ships enabled by default. HIBP kept as a swappable key-based alternative;
provider chosen via Privacy:Provider config.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Heuristic classifier falls back to AI only when it can't determine a category;
unsubscribe confidence blends method reliability with an AI safety opinion and
surfaces it in the Unsubscribe Manager; emails get an on-demand AI one-line
summary in the detail pane. All AI calls degrade gracefully when AI is disabled
or the provider errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a periodic inbox-summary email per opted-in user. SMTP is
configured via appsettings (Smtp section); the digest is built from
existing analytics (health score, top senders, recommendations) and
sent by a new hourly DigestWorker, mirroring the GmailSyncWorker
pattern. Frequency and send hour are configurable (Digest section).
Backend: IEmailSender (SmtpEmailSender, MailKit), IDigestService,
DigestWorker, new User.DigestEnabled/LastDigestSentUtc fields +
migration, SettingsController for the per-user toggle and a
send-now test endpoint, all scoped to the authenticated UserId.
Frontend: SettingsApi + a bell/no-bell toggle button in the topbar.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lets users name and pin a search query from the SearchResults page;
saved searches persist to localStorage and show as quick links in a new
sidebar section, similar to Favorites. Clicking re-runs the query.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>