Total 1465 LOC + 616 test LOC, 78/78 tests pass. - @keisei/mcp-server (25 tests) — Rust-CLI bridge via execa, stdio+HTTP, HMAC auth, kei() meta-tool - @keisei/telegram-adapter (16 tests) — grammy Bot, 7 tools - @keisei/recall-adapter (8 tests) — Zoom via Recall.ai, 5 tools - @keisei/grok-adapter (6 tests) — xAI OpenAI-compatible, 2 tools - @keisei/gmail-adapter (11 tests) — googleapis OAuth2, 6 tools (new — LBM gap) - @keisei/youtube-adapter (12 tests) — YouTube Data API v3, 5 tools (new — LBM gap) RULE 0.2 exception #4 (TS for MCP/API layer documented in _ts_packages/README.md). RULE 0.8 — env vars only (TELEGRAM_BOT_TOKEN, XAI_API_KEY, GMAIL_*, YOUTUBE_API_KEY). Strict TypeScript: strict + exactOptionalPropertyTypes + noUncheckedIndexedAccess. Genesis-scan clean (0 hits).
101 lines
3.7 KiB
TypeScript
101 lines
3.7 KiB
TypeScript
// Gmail API client via googleapis. One class that owns an OAuth2 client
|
|
// plus a gmail.users surface. All methods return plain data (no gapi types
|
|
// leak outward). Tests inject a mock surface via the `gmailSurface` param.
|
|
|
|
import { google } from "googleapis";
|
|
import type { MessageSummary } from "./types.js";
|
|
|
|
export interface GmailClientConfig {
|
|
clientId: string;
|
|
clientSecret: string;
|
|
refreshToken: string;
|
|
gmailSurface?: GmailSurface;
|
|
}
|
|
|
|
// Narrow shape we actually use. Everything googleapis exposes is optional.
|
|
export interface GmailSurface {
|
|
list: (q: string | undefined, max: number) => Promise<Array<{ id?: string | null; threadId?: string | null }>>;
|
|
get: (id: string) => Promise<{ id?: string | null; threadId?: string | null; snippet?: string | null; payload?: { headers?: Array<{ name?: string | null; value?: string | null }> | null } | null }>;
|
|
modify: (id: string, addIds: string[], removeIds: string[]) => Promise<void>;
|
|
trash: (id: string) => Promise<void>;
|
|
}
|
|
|
|
export class GmailClient {
|
|
private readonly surface: GmailSurface;
|
|
|
|
constructor(cfg: GmailClientConfig) {
|
|
this.surface = cfg.gmailSurface ?? buildDefaultSurface(cfg);
|
|
}
|
|
|
|
async listUnread(max: number): Promise<MessageSummary[]> {
|
|
const ids = await this.surface.list("is:unread", max);
|
|
return Promise.all(ids.map(async (row) => this.summarize(row.id ?? "")));
|
|
}
|
|
|
|
async search(query: string, max: number): Promise<MessageSummary[]> {
|
|
const ids = await this.surface.list(query, max);
|
|
return Promise.all(ids.map(async (row) => this.summarize(row.id ?? "")));
|
|
}
|
|
|
|
async getMessage(id: string): Promise<MessageSummary> {
|
|
return this.summarize(id);
|
|
}
|
|
|
|
async labelMessage(id: string, label: string): Promise<void> {
|
|
await this.surface.modify(id, [label], []);
|
|
}
|
|
|
|
async archive(id: string): Promise<void> {
|
|
await this.surface.modify(id, [], ["INBOX"]);
|
|
}
|
|
|
|
async trash(id: string): Promise<void> {
|
|
await this.surface.trash(id);
|
|
}
|
|
|
|
private async summarize(id: string): Promise<MessageSummary> {
|
|
if (!id) return { id: "" };
|
|
const msg = await this.surface.get(id);
|
|
const headers = msg.payload?.headers ?? [];
|
|
const pick = (name: string): string | undefined => headers.find((h) => h.name?.toLowerCase() === name)?.value ?? undefined;
|
|
return {
|
|
id: msg.id ?? id,
|
|
threadId: msg.threadId ?? undefined,
|
|
subject: pick("subject"),
|
|
from: pick("from"),
|
|
date: pick("date"),
|
|
snippet: msg.snippet ?? undefined,
|
|
};
|
|
}
|
|
}
|
|
|
|
function buildDefaultSurface(cfg: GmailClientConfig): GmailSurface {
|
|
if (!cfg.clientId || !cfg.clientSecret || !cfg.refreshToken) {
|
|
throw new Error("GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, GMAIL_REFRESH_TOKEN all required");
|
|
}
|
|
const oauth = new google.auth.OAuth2(cfg.clientId, cfg.clientSecret);
|
|
oauth.setCredentials({ refresh_token: cfg.refreshToken });
|
|
const gmail = google.gmail({ version: "v1", auth: oauth });
|
|
return {
|
|
list: async (q, max) => {
|
|
const params: { userId: string; maxResults: number; q?: string } = { userId: "me", maxResults: max };
|
|
if (q !== undefined) params.q = q;
|
|
const res = await gmail.users.messages.list(params);
|
|
const items = res.data.messages ?? [];
|
|
return items.map((m: { id?: string | null; threadId?: string | null }) => ({
|
|
id: m.id ?? null,
|
|
threadId: m.threadId ?? null,
|
|
}));
|
|
},
|
|
get: async (id) => {
|
|
const res = await gmail.users.messages.get({ userId: "me", id, format: "metadata" });
|
|
return res.data;
|
|
},
|
|
modify: async (id, addIds, removeIds) => {
|
|
await gmail.users.messages.modify({ userId: "me", id, requestBody: { addLabelIds: addIds, removeLabelIds: removeIds } });
|
|
},
|
|
trash: async (id) => {
|
|
await gmail.users.messages.trash({ userId: "me", id });
|
|
},
|
|
};
|
|
}
|