mirror of
https://github.com/Sea-Haven-Industries/sh-mcp.git
synced 2026-10-03 10:23:19 +00:00
150 lines
4.3 KiB
TypeScript
150 lines
4.3 KiB
TypeScript
|
|
/**
|
||
|
|
* 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<string, EmailThread>;
|
||
|
|
}
|
||
|
|
|
||
|
|
function seedInboxes(): Map<string, SeedInbox> {
|
||
|
|
const map = new Map<string, SeedInbox>();
|
||
|
|
|
||
|
|
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<string, EmailThread>([
|
||
|
|
[
|
||
|
|
'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<string, EmailThread>([
|
||
|
|
[
|
||
|
|
'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<string, SeedInbox>;
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
this.inboxes = seedInboxes();
|
||
|
|
}
|
||
|
|
|
||
|
|
async searchInbox(params: SearchInboxParams): Promise<EmailMessage[]> {
|
||
|
|
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<EmailThread> {
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
}
|