Clicking an email now opens a collapsible, horizontally resizable
reading pane beside the list instead of a new tab. Extracts the shared
EmailDetail component (full detail fetch + lazy AI summary + Open in
Gmail) and adds list skeleton + empty states via the ui primitives.
- New components/EmailDetail.jsx (emailId/email + onClose)
- New split.css (flex master-detail, native resize, <768px overlay)
- FolderView/SearchResults: selectedEmail state, onOpen wiring,
Skeleton loading rows, EmptyState zero-states
- main.jsx imports split.css
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>
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>
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>