Files
Inboxintel/docs/ARCHITECTURE.md
T
2026-06-30 15:53:32 +02:00

5.1 KiB

InboxIntel Architecture

Layering (Clean Architecture)

The solution enforces a one-directional dependency flow so the core stays testable and framework-agnostic.

InboxIntel.Domain holds entities (Email, MailThread, Sender, MailDomain, Attachment, Label/EmailLabel, SyncState, AnalyticsAggregate, WidgetLayout, UnsubscribeItem, User) and enums. It references nothing except Npgsql (for the NpgsqlTsVector full-text type).

InboxIntel.Application defines the contracts the rest of the system programs against: IGmailService, ISyncService, IAnalyticsService, ISearchService, ICleanupService, IUnsubscribeService, IAiService/IAiProvider, IExportService, plus IAppDbContext, DTOs, FluentValidation validators, and the GmailQueryParser.

InboxIntel.Infrastructure implements those contracts: the EF Core AppDbContext and entity configurations, the Gmail REST client (GmailApiService + GmailClientFactory + GmailMessageParser), the SyncService and GmailSyncWorker, analytics/search/cleanup/unsubscribe services, the AI providers (Null/Ollama/OpenAI) and AiService, the ExportService (PDF/CSV/JSON), and the DataProtectionTokenProtector.

InboxIntel.Api is the composition root: Serilog, Google OAuth2 + cookie auth, API versioning, CORS, controllers, and startup migration.

Request flow

A browser calls /api/v1/... with the auth cookie. The controller (no business logic) resolves ICurrentUser to get the tenant id and calls an Application interface. The Infrastructure implementation runs EF Core queries / Gmail calls and returns DTOs. Validators run via the FluentValidation pipeline before handlers execute.

Authentication

Google OAuth2 is the only login method. The Google handler runs with AccessType=offline to obtain a refresh token; GoogleAuthEvents.OnCreatingTicketAsync upserts the User, encrypts the refresh token with the Data Protection API, and stamps the internal user id (inboxintel:uid) as a claim. A 7-day cookie carries the session. GmailClientFactory decrypts the refresh token per call and lets the Google client library refresh access tokens automatically.

Data model & scale

Every owned row carries a UserId (multi-user ready though single-user today). Composite indexes back the hot paths at 100k+ emails: (UserId, GmailMessageId) unique, (UserId, SenderId), (UserId, SentAtUtc), (UserId, IsUnread), (UserId, Category). Sender/domain rollup counters are maintained during sync for instant grouping. Full-text search uses a stored, generated tsvector column over subject+body with a GIN index, queried through EF.Functions.PlainToTsQuery. Daily AnalyticsAggregate rows let dashboard widgets render without scanning the email table.

Gmail sync

Full sync pages every message id, fetches+parses each, and checkpoints the page token and counts to SyncState after each page — so an interrupted run resumes instead of restarting. Incremental sync replays Gmail's history feed from the stored historyId watermark, applying adds and deletes. All Gmail calls run through a Polly pipeline: exponential backoff with jitter on 429/5xx, capped by GmailSync:MaxRetries. A BackgroundService (GmailSyncWorker) runs a daily incremental sync per user and refreshes aggregates, fully off the request path. In production this can be swapped for Hangfire without touching callers.

Cleanup & unsubscribe safety

CleanupService.PreviewAsync always precedes execution and returns the affected count, total size, and a sample. Destructive actions (Trash, HardDelete) are rejected unless Confirmed == true — enforced both by a validator and again inside the service as defence in depth. The unsubscribe pipeline detects List-Unsubscribe headers during sync, groups opportunities per sender, ranks them by volume, and only processes a user-confirmed queue. One-click (List-Unsubscribe-Post) targets are POSTed; mailto: targets are surfaced for the user — the app never auto-sends mail.

AI layer

Toggleable via Ai:Mode (Disabled / LocalOllama / CloudOpenAi), selected at DI time. IAiProvider abstracts chat completion; AiService builds classification, inbox summaries, cleanup suggestions, and natural-language→Gmail-query features on top. Critically, the AI layer only reads and suggests — destructive actions always route back through the confirmed cleanup/unsubscribe flows.

Frontend

React + Vite SPA. The dashboard uses react-grid-layout for draggable/resizable widgets with hide/show toggles; the layout is persisted per user via PUT /widgetlayout. Widgets render through Chart.js (volume line, attachment doughnut) and a custom CSS heatmap. The axios client sends the session cookie and redirects to the Google login flow on 401.

Extension points (left intentionally open)

Gmail MIME parsing handles the common multipart/plain cases; richer HTML-body unsubscribe-URL scraping and attachment-by-attachment download are stubbed for extension. AI prompts are minimal and meant to be tuned. The background worker uses a simple hourly tick; production deployments may prefer Hangfire with a cron schedule.