Semantic-search slice 1 (infra). Adds the pgvector plumbing the backfill worker and
hybrid search will use:
- swap the Postgres image to pgvector/pgvector:pg16 (drop-in for pg16 data)
- Pgvector + Pgvector.EntityFrameworkCore (0.2.0, EF8-compatible); UseVector() on the
runtime + design-time contexts
- Email.Embedding vector(768) column (nomic-embed-text dims), nullable, with an HNSW
cosine index; ignored under the InMemory test provider
- migration: CREATE EXTENSION vector + column + HNSW index
Verified against a real pgvector container: extension, HNSW, and cosine search work, and
the full EF round-trip (store a Pgvector.Vector, CosineDistance operator, nearest-first
ordering) applies all migrations and passes. No vulnerable packages. Build + all 41 tests
pass. Column stays null until Ollama generates embeddings (search falls back to lexical).
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>
Wires the shared useListKeyboardNav hook into SenderEmails so j/k/e/#
work there too, matching FolderView and SearchResults.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
'/' focuses the search bar from anywhere (Layout-level listener).
j/k move focus up/down the email list in FolderView and SearchResults;
e archives the focused email, # trashes it. All shortcuts are ignored
while an input/textarea/select has focus, so typing is never hijacked.
A small hint footer in the bottom-right reminds users of the shortcuts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Select multiple rows (checkbox per row) in FolderView, SearchResults,
and the Senders email panel. A toolbar appears with mark read/unread,
star, archive, and trash, applied to the whole selection via
CleanupService.ExecuteAsync (already scoped to UserId).
Shared via useSelection hook and BulkToolbar component to avoid
duplicating selection state across the three list views.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SyncStatus widget polls /sync/status and shows a live progress bar in
the topbar while syncing, or last-synced time / error otherwise.
Unsubscribe page reworked into a proper queue: status filter tabs
(All/Pending/Succeeded/Failed), select-all, sorted by volume, status
badges, and a result toast after processing.
GET /email/{id} returns full BodyText; Senders detail view fetches
and renders it instead of just the snippet.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Left panel: scrollable sender list with filter, email count, unread badge.
Right panel: infinite-scroll email list, quick actions, click row to view
detail (subject, meta, snippet, open in Gmail). Back button returns to list.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Smart folders with zero matching emails are not rendered once
sidebar counts have loaded. Folders still show while counts are
pending (avoids flash of empty sidebar on load).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New EmailCategory enum values: Travel, Subscriptions, Parcels,
Recruitment, Events, SecurityAlerts, Healthcare, Education,
NewsMedia, PropertyUtilities, Charity, Government, CryptoInvesting,
FamilySchool.
HeuristicClassifier: locale-agnostic rules using global brand
domains + TLD patterns (.gov.*, .edu, .ac.*) + English subject
keywords — works for international users without relying on
country-specific domains (e.g. gov.uk).
Priority order ensures SecurityAlerts and Government take
precedence over Finance, and Travel/RideSharing/FoodDelivery
fire before Finance to prevent receipt mis-classification.
AnalyticsService: maps all 14 new slugs to category counts.
client.js: folderToRequest() handles all 14 new slugs.
Layout.jsx: SMART_FOLDERS extended with icons and labels.
widgets.jsx: CAT_SLUG updated for dashboard drillthrough.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Added 6 new EmailCategory enum values
- HeuristicClassifier rewritten with domain and subject-keyword rules:
- RideSharing: Uber, Lyft, Bolt, Free Now, Grab etc.
- FoodDelivery: DoorDash, Uber Eats, Deliveroo, Just Eat, Grubhub etc.
- Gaming: Steam, Epic, EA, Xbox, PlayStation, Nintendo, Blizzard, Riot etc.
- Shopping: Amazon, eBay, Etsy, ASOS, Zalando, Shein etc.
- SeasonalSales: subject keywords (sale, % off, promo, black friday etc.)
- Wellness: Strava, Garmin, Peloton, MyFitnessPal, Headspace, Nike etc. + subject hints
- Priority ordering prevents ride-share/food receipts misclassifying as Finance
- SidebarCounts maps all new slugs to their EmailCategory
- frontend folderToRequest() and CAT_SLUG map all new categories
- Existing emails will pick up new categories on next sync
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ✉✕ button appears on rows where the email has a List-Unsubscribe header
- One-click POST / HTTP GET executed automatically; shows ✓ on success
- mailto: targets open the user mail client (we never auto-send email)
- POST /email/{id}/unsubscribe: detects sender, then processes — UserId scoped
- EmailSummaryDto gains IsStarred, HasListUnsubscribe, SupportsOneClick fields
- Both SearchService and CleanupService updated to populate new DTO fields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- EmailRow shared component with hover action buttons: mark read/unread, star/unstar, trash
- Optimistic UI — local state patches immediately on click; row fades and becomes non-interactive during the API call
- Trashed rows disappear from the current folder view via onRemove callback
- Backend: EmailController single-email endpoints (POST /email/{id}/read|unread|star|unstar|trash|untrash)
- Star/Unstar added to CleanupActionType enum and CleanupService (maps to STARRED Gmail label via BatchModifyAsync)
- UserId scope enforced in ResolveTargetsAsync — a user can only act on their own emails
- action buttons use stopPropagation so clicking them does not open Gmail
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Search form in topbar routes to /app/search?q=...
- SearchResults page with infinite scroll, same row style as FolderView
- Supports Gmail-like operators: from:, is:unread, has:attachment, after:, before:
- SearchApi.query() calls GET /search which runs through the existing GmailQueryParser
- Input sanitised via encodeURIComponent on submit; external links use noopener/noreferrer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace page buttons with IntersectionObserver sentinel — scrolling near
the bottom automatically fetches and appends the next page of results.
Shows a loading indicator while fetching and a total count footer when
all emails are loaded.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New FolderView page at /app/folder/:slug renders a paginated email table
- Sidebar links now route to their respective folder views
- Extended SearchRequestDto with IsInInbox, IsStarred, IsTrashed, GmailLabel, Category, MinSizeBytes
- SearchService applies the new filters; GmailLabel does a label-id lookup (SENT/DRAFT/SPAM)
- Frontend folderToRequest() maps every slug to the correct search payload
- Email rows show sender, subject+snippet, attachment icon, size, date; bold for unread
- Pagination controls for large folders
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>