/** * InMemoryGmailClient โ€” deterministic in-memory GmailClient for local dev. * * Holds a small set of FAKE inboxes/threads in memory, partitioned by userSub * so a user only ever sees their own mail. searchInbox does a simple * case-insensitive substring match against subject/snippet; getThreadDetail * throws if the thread is not found or not owned by the caller. No network. * * This is for build-plan ยง4 "in-memory dev clients (tools return real data * locally)". */ import type { EmailMessage, EmailThread, GetThreadDetailParams, GmailClient, SearchInboxParams, } from './client.js'; interface SeedInbox { messages: EmailMessage[]; threads: Map; } function seedInboxes(): Map { const map = new Map(); map.set('lauren@seahavenind.com', { messages: [ { id: 'msg-l-1', threadId: 'thr-l-1', subject: 'Invoice #4821 from Coastal Supply', from: 'billing@coastalsupply.example', to: 'lauren@seahavenind.com', date: '2026-06-22T09:15:00Z', snippet: 'Please find attached invoice #4821 for the dock hardware order.', }, { id: 'msg-l-2', threadId: 'thr-l-2', subject: 'Re: Site walkthrough scheduling', from: 'adam@seahavenind.com', to: 'lauren@seahavenind.com', date: '2026-06-23T14:02:00Z', snippet: 'Friday afternoon works for the marina pier walkthrough.', }, ], threads: new Map([ [ 'thr-l-1', { threadId: 'thr-l-1', subject: 'Invoice #4821 from Coastal Supply', messages: [ { id: 'msg-l-1', from: 'billing@coastalsupply.example', to: 'lauren@seahavenind.com', date: '2026-06-22T09:15:00Z', body: 'Please find attached invoice #4821 for the dock hardware order. Net 30 terms apply.', }, ], }, ], [ 'thr-l-2', { threadId: 'thr-l-2', subject: 'Re: Site walkthrough scheduling', messages: [ { id: 'msg-l-2', from: 'adam@seahavenind.com', to: 'lauren@seahavenind.com', date: '2026-06-23T14:02:00Z', body: 'Friday afternoon works for the marina pier walkthrough. I will bring the punch list.', }, ], }, ], ]), }); map.set('adam@seahavenind.com', { messages: [ { id: 'msg-a-1', threadId: 'thr-a-1', subject: 'Payment confirmation โ€” check #2087', from: 'ap@seahavenind.com', to: 'adam@seahavenind.com', date: '2026-06-21T11:40:00Z', snippet: 'Check #2087 to Harbor Electric has cleared.', }, ], threads: new Map([ [ 'thr-a-1', { threadId: 'thr-a-1', subject: 'Payment confirmation โ€” check #2087', messages: [ { id: 'msg-a-1', from: 'ap@seahavenind.com', to: 'adam@seahavenind.com', date: '2026-06-21T11:40:00Z', body: 'Check #2087 to Harbor Electric has cleared. The payment dashboard is updated.', }, ], }, ], ]), }); return map; } /** In-memory GmailClient. Inboxes are partitioned by userSub. */ export class InMemoryGmailClient implements GmailClient { private readonly inboxes: Map; constructor() { this.inboxes = seedInboxes(); } async searchInbox(params: SearchInboxParams): Promise { const inbox = this.inboxes.get(params.userSub); if (!inbox) { return []; } const q = params.query.toLowerCase(); const matches = inbox.messages.filter( (m) => m.subject.toLowerCase().includes(q) || m.snippet.toLowerCase().includes(q), ); return matches.slice(0, params.maxResults); } async getThreadDetail(params: GetThreadDetailParams): Promise { const inbox = this.inboxes.get(params.userSub); const thread = inbox?.threads.get(params.threadId); if (!thread) { throw new Error(`Thread ${params.threadId} not found for user ${params.userSub}`); } return thread; } }