# 01 — Provider Abstraction (Part 1) A unified layer so Gmail, Outlook/Graph, and future IMAP look identical to the rest of the app. **No provider-specific logic in Domain**; specifics live in Infrastructure adapters. ## Layering ``` Domain : Account, EmailMessage, EmailThread, Label (provider-agnostic) Application : IEmailProvider (contract) · ISyncOrchestrator · DTOs Infrastructure : GmailProvider · OutlookProvider · ImapProvider (adapters) ProviderFactory (ProviderType → adapter) · ITokenStore ``` ## The contract ```csharp public enum ProviderType { Google, Microsoft, Imap } public interface IEmailProvider { ProviderType Type { get; } ProviderCapabilities Capabilities { get; } // read, modifyFlags, folders, delta, send? // Auth (details in 02-auth-and-signin.md) Task ExchangeCodeAsync(string code, CancellationToken ct); Task RefreshAsync(TokenSet current, CancellationToken ct); Task GetIdentityAsync(TokenSet tokens, CancellationToken ct); // sub + email // Sync (pull-based, incremental) Task SyncAsync(SyncCursor cursor, TokenSet tokens, CancellationToken ct); Task FetchMessageAsync(string providerMessageId, TokenSet tokens, CancellationToken ct); // Mutations (only if Capabilities allow; mirrors current gmail.modify scope) Task ApplyFlagAsync(string providerMessageId, MailFlagChange change, TokenSet tokens, CancellationToken ct); } ``` - `SyncPage` = `{ IReadOnlyList upserts, IReadOnlyList deletes, SyncCursor next, bool hasMore }`. - `RawMessage` is the **provider-shaped** payload; a **normaliser** maps it to the domain `EmailMessage`. The rest of the app never sees `RawMessage`. - Capabilities let the UI/engine **degrade gracefully** (e.g., an IMAP server without CONDSTORE falls back to full-scan sync; no `send` today for any provider). ## Provider implementations | Provider | API | Incremental cursor | Threading | Folders/Labels | Notes | |----------|-----|--------------------|-----------|----------------|-------| | **Gmail** | Gmail REST | `historyId` (History API) | `threadId` | labels | Reuses existing client; read + modify (no send), as today | | **Outlook/365** | Microsoft Graph | **delta query** `@odata.deltaLink` | `conversationId` | mailFolders | OAuth via Microsoft identity platform | | **IMAP** | IMAP4rev1 | `UIDVALIDITY`+`UIDNEXT`, `HIGHESTMODSEQ` (CONDSTORE/QRESYNC) | heuristic (References/In-Reply-To) | folders | Fallback = periodic UID scan if no CONDSTORE; MailKit already a dependency | ## Normalisation (the unified model) Each adapter maps provider fields → domain via a `IMessageNormaliser`: | Domain field | Gmail | Graph | IMAP | |--------------|-------|-------|------| | `ProviderMessageId` | message id | message id | `UIDVALIDITY:UID` | | `ProviderThreadId` | threadId | conversationId | derived (References) | | flags (unread/star/important/trashed/inbox) | labelIds | isRead/flag/folder | `\Seen \Flagged`, folder | | labels/folders | labels | mailFolders | folders | | sent/received, from, subject, snippet, body, attachments, size | headers/parts | message resource | RFC822 parse (MailKit) | - **Threads are per-account** (each provider defines its own). *Cross-provider* thread linking is a later semantic/AI feature (see blueprint [07](../07-ai-feature-catalogue.md)), not part of core normalisation. ## Sync engine - **`ISyncOrchestrator`** replaces the Gmail-specific worker: for each active `Account`, it loads the `SyncCursor`, calls `provider.SyncAsync`, **upserts** normalised messages (idempotent on `(AccountId, ProviderMessageId)`), applies deletes, and **persists the next cursor** atomically. - Runs as the existing hosted-worker pattern (`AccountSyncWorker`), one logical job per account, bounded concurrency, Polly backoff, resumable. - **Token refresh:** `SyncAsync`/mutations get a valid `TokenSet` from `ITokenStore`, which refreshes on expiry/401 and **re-encrypts** at rest; a failed refresh flips the account to `reauth_needed` (surfaced in UI, see [07](07-ux-flows.md)) — never crashes sync. - **New mail** triggers AI enrichment + embedding jobs (blueprint [08](../08-technical-architecture.md)). ## Search across providers Because all providers normalise into **one `email_messages` store scoped by `UserId`**, search (structured + FTS + semantic) **already spans every account a user has connected** — no per-provider search code. An optional `AccountId` facet lets users scope to one mailbox. ## Extensibility Adding a provider = one `IEmailProvider` adapter + one normaliser + register in `ProviderFactory` + a feature flag to enable it. **Zero changes to Domain, search, or AI.**