KeiSeiKit-1.0/_ts_packages/packages/gmail-adapter/src/client.ts
Parfii-bot c21943e40b feat(ts-packages): 6 TS packages — MCP server + 5 external-API adapters
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).
2026-04-22 12:45:19 +08:00

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 });
},
};
}