ImportSingleMessageAsync now fetches the message + connection through the
provider-neutral seam (Email.GetMessageAsync/GetConnectionAsync), mapping the
neutral ExternalAttachmentId onto CorrespondenceAttachmentMetadata. The
controller's import path no longer touches Gmail directly.
OAuth lifecycle, the rich connection-status DTO, and Gmail candidate ranking
stay on IGmailOAuthService until a second provider (Microsoft/IMAP) forces the
contract shape. 135/135 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GmailController now resolves the "gmail" provider from IEmailProviderRegistry and
uses the provider-neutral seam for its read paths — message search (SearchAsync)
and thread listing (ListThreadMessagesAsync) across ImportThread, RelinkThread,
CreateSuggestedJob, RefreshLinkedThreads and the messages endpoint. OAuth
(connect/callback), connection status and Gmail-specific candidate ranking stay
on IGmailOAuthService until they are generalised.
An optional constructor param keeps direct construction (tests) working via a
fallback single-Gmail registry, so the mocked Gmail service is exercised through
GmailProvider. Behaviour is preserved (neutral DTOs mirror the Gmail shapes).
This makes the seam a real consumer and sets up MicrosoftGraphProvider /
ImapProvider / a manual free-text provider to slot in next.
Build clean; backend suite 135/135 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CreateSuggestedJob ran one AnyAsync per message in the thread to decide
imported-vs-skip — an N+1 that scales with thread length. Replace it with a
single query that loads the already-imported ExternalMessageIds for the job,
then check in memory (identical skip/import behaviour), mirroring the batched
pattern RelinkThread already uses.
Build clean; backend suite 135/135 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>