/** * Gmail client interface + thin implementation. * * The real Google API call is clearly stubbed/guarded behind the interface so * that nothing in this module imports the Google client SDK at module load time. * All consumers (tools.ts and tests) inject a GmailClient — no live network * traffic ever occurs during import or unit tests. * * Per §2.4 of the design: * - This module acts AS the signed-in user via a per-user Google OAuth token. * - The GoogleTokenProvider is injected; the implementation fetches a * short-lived access token on demand and never caches it. * - The refresh token is NEVER returned or logged. * - The client is instantiated with the minimal scope for the requested tool * only (gmail.readonly) — never the union of a user's scopes. */ export interface EmailMessage { id: string; threadId: string; subject: string; from: string; to: string; date: string; /** Plain-text snippet — safe for log output. */ snippet: string; } export interface EmailThread { threadId: string; subject: string; messages: Array<{ id: string; from: string; to: string; date: string; /** Full decoded plain-text body. */ body: string; }>; } export interface SearchInboxParams { /** Gmail query string, e.g. "from:vendor@example.com subject:invoice" */ query: string; /** Maximum number of results to return (1–50). */ maxResults: number; /** Sub of the calling user — used to look up the per-user Google token. */ userSub: string; } export interface GetThreadDetailParams { threadId: string; userSub: string; } /** * The interface all callers (tools + tests) program against. * Tests inject a mock; production code injects GmailApiClient. */ export interface GmailClient { searchInbox(params: SearchInboxParams): Promise; getThreadDetail(params: GetThreadDetailParams): Promise; } /** * Provides a short-lived Google access token for a given user sub. * The real implementation reads the encrypted refresh token from DynamoDB * (KMS-CMK, ABAC-partitioned by sub) and exchanges it for an access token. * Tests inject a mock that returns a hard-coded dummy token. * * IMPORTANT: The refresh token MUST NOT be returned or surfaced anywhere * outside this provider. Access tokens are minted per request; do not cache. */ export interface GoogleTokenProvider { getAccessToken(userSub: string): Promise; } /** * Production Gmail client. * * The actual HTTP call to the Gmail REST API is guarded behind the * STUB comment below. To complete the real implementation: * 1. npm install googleapis (or use undici/fetch directly) * 2. Build a google.auth.OAuth2 client from the access token returned * by tokenProvider.getAccessToken() * 3. Call gmail.users.messages.list / gmail.users.threads.get * * The stub throws so that accidental real calls fail loudly in tests. */ export class GmailApiClient implements GmailClient { // Stored for use by the real implementation once the Gmail SDK call is wired. private readonly _tokenProvider: GoogleTokenProvider; constructor(tokenProvider: GoogleTokenProvider) { this._tokenProvider = tokenProvider; // Mark as intentionally stored-but-unused until the real SDK call is wired. void this._tokenProvider; } async searchInbox(params: SearchInboxParams): Promise { // TODO: Replace this stub with the real Gmail API call. // When implementing, fetch a short-lived access token first: // const accessToken = await this.tokenProvider.getAccessToken(params.userSub); // Example (googleapis): // const auth = new google.auth.OAuth2(); // auth.setCredentials({ access_token: accessToken }); // const gmail = google.gmail({ version: 'v1', auth }); // const res = await gmail.users.messages.list({ // userId: 'me', // q: params.query, // maxResults: params.maxResults, // }); // return parseMessageList(res.data); void params; // suppress unused-param warning; remove when real call is wired throw new Error( 'GmailApiClient.searchInbox is not implemented — inject a GmailClient mock in tests and wire the real SDK call here for production.', ); } async getThreadDetail(params: GetThreadDetailParams): Promise { // TODO: Replace this stub with the real Gmail API call. // When implementing, fetch a short-lived access token first: // const accessToken = await this.tokenProvider.getAccessToken(params.userSub); // Example (googleapis): // const auth = new google.auth.OAuth2(); // auth.setCredentials({ access_token: accessToken }); // const gmail = google.gmail({ version: 'v1', auth }); // const res = await gmail.users.threads.get({ // userId: 'me', // id: params.threadId, // format: 'full', // }); // return parseThread(res.data); void params; // suppress unused-param warning; remove when real call is wired throw new Error( 'GmailApiClient.getThreadDetail is not implemented — inject a GmailClient mock in tests and wire the real SDK call here for production.', ); } }