chat-adapter-gmail
v0.1.0
Published
Gmail adapter for Chat SDK — turn an inbox into a first-class Chat SDK channel via Pub/Sub push.
Maintainers
Readme
chat-adapter-gmail
A Chat SDK adapter for Gmail. It turns a mailbox into a first-class Chat SDK channel: incoming mail arrives as normalized messages via Cloud Pub/Sub push, and your handler can reply — or save drafts — as the connected account through the Gmail API.
A Gmail thread maps to a Chat SDK thread, an email maps to a message, and a reply
maps to postMessage. The adapter exposes capabilities only; whether to
auto-send a reply or leave a draft for review is your application's decision,
not the adapter's.
Installation
chat is a peer dependency — install it alongside the adapter:
npm install chat-adapter-gmail chatQuick start
import { Chat } from "chat";
import { createGmailAdapter } from "chat-adapter-gmail";
const gmail = createGmailAdapter({
auth: {
clientId: process.env.GMAIL_CLIENT_ID!,
clientSecret: process.env.GMAIL_CLIENT_SECRET!,
redirectUri: process.env.GMAIL_REDIRECT_URI!,
},
topicName: "projects/my-project/topics/gmail-push",
watchLabelIds: ["Label_123"], // only act on mail with this label
pushAudience: process.env.GMAIL_PUSH_AUDIENCE,
pushServiceAccountEmail: process.env.GMAIL_PUSH_SERVICE_ACCOUNT,
// tokenStore / cursorStore: supply persistent stores in production.
});
const chat = new Chat({ adapters: { gmail } });
// Every incoming email is a DM. Here we DRAFT a reply for review (safest
// default); swap `createDraft` for `thread.post(...)` to actually send as you.
chat.onDirectMessage(async (thread, message) => {
if (message.author.isMe) return; // never reply to ourselves
const reply = `Thanks for your email! You wrote:\n\n> ${message.text}`;
await gmail.createDraft(thread.id, { markdown: reply });
await thread.subscribe(); // catch follow-ups in this thread
});
// Initialize adapters, then start receiving push notifications.
// Renew the watch at least every 7 days (see Platform setup).
await chat.initialize();
await gmail.startWatch();Expose the webhook from any HTTP entry point — the adapter takes a standard
Request and returns a Response, so it runs unchanged on Node, Next.js, or
serverless:
// Next.js App Router — app/api/webhook/gmail/route.ts
import { after } from "next/server";
export async function POST(request: Request) {
return gmail.handleWebhook(request, { waitUntil: (p) => after(() => p) });
}Environment variables
createGmailAdapter() falls back to these when the matching option is omitted:
| Variable | Required | Example |
| ----------------------------- | -------- | --------------------------------------------- |
| GMAIL_CLIENT_ID | Yes | 1234.apps.googleusercontent.com |
| GMAIL_CLIENT_SECRET | Yes | GOCSPX-xxxxxxxx |
| GMAIL_REDIRECT_URI | Yes | http://localhost:3000/oauth2callback |
| GMAIL_PUBSUB_TOPIC | For push | projects/my-project/topics/gmail-push |
| GMAIL_WATCH_LABEL_IDS | No | Label_123,Label_456 (comma-separated) |
| GMAIL_PUSH_AUDIENCE | No* | https://example.com/api/webhook/gmail |
| GMAIL_PUSH_SERVICE_ACCOUNT | No* | [email protected] |
* Strongly recommended in production. When neither is set, the adapter skips Pub/Sub token verification (and logs a warning) — fine for local development, unsafe for a public endpoint.
Configuration reference
| Field | Type | Default | Description |
| ------------------------- | -------------- | ------------- | --------------------------------------------------------------------------- |
| auth.clientId | string | env | OAuth 2.0 client ID. |
| auth.clientSecret | string | env | OAuth 2.0 client secret. |
| auth.redirectUri | string | env | Redirect URI registered on the OAuth client. |
| topicName | string | env | Fully-qualified Pub/Sub topic Gmail publishes to. Required for startWatch. |
| watchLabelIds | string[] | ["INBOX"] | Only surface messages carrying one of these label IDs. |
| pushAudience | string | env | Expected OIDC aud claim (your webhook URL). |
| pushServiceAccountEmail | string | env | Expected OIDC email claim (the push service account). |
| tokenStore | TokenStore | in-memory | Persists the OAuth refresh token. |
| cursorStore | CursorStore | in-memory | Persists the Gmail historyId cursor. |
| name | string | "gmail" | Adapter name used in thread IDs / routing. |
Production note: the in-memory stores lose state on restart. Implement
TokenStoreandCursorStoreagainst your database or secrets manager so the refresh token and history cursor survive restarts.
Platform setup
- Create OAuth credentials. In the Google Cloud console, enable the Gmail API, configure the OAuth consent screen, and create an OAuth client ID (type Web application). Note the client ID/secret and add your redirect URI.
- Create a Pub/Sub topic. Create a topic (e.g.
gmail-push) and grant Gmail permission to publish to it:gcloud pubsub topics create gmail-push gcloud pubsub topics add-iam-policy-binding gmail-push \ --member="serviceAccount:[email protected]" \ --role="roles/pubsub.publisher" - Create a push subscription pointing at your webhook, with an OIDC token so
the adapter can verify it:
Setgcloud pubsub subscriptions create gmail-push-sub \ --topic=gmail-push \ --push-endpoint="https://example.com/api/webhook/gmail" \ --push-auth-service-account="[email protected]"pushAudienceto the push endpoint URL andpushServiceAccountEmailto that service account. - Connect an account. Run the one-time OAuth flow to mint a refresh token,
then save it in your
TokenStore:GMAIL_CLIENT_ID=... GMAIL_CLIENT_SECRET=... \ GMAIL_REDIRECT_URI=http://localhost:3000/oauth2callback \ node --experimental-strip-types scripts/auth.ts - (Optional) Scope with a label. Create a Gmail filter that applies a label
to the mail you want handled, and pass that label's ID as
watchLabelIds. - Start (and renew) the watch. Call
startWatch()on boot. Gmail watches expire within 7 days, so callrenewWatch()on a daily schedule (cron).
Features
- ✅ Real-time incoming mail via Cloud Pub/Sub push (
handleWebhook). - ✅ Pub/Sub OIDC token verification (audience + service-account checks).
- ✅ Send replies as the connected account, correctly threaded
(
In-Reply-To/References+ GmailthreadId). - ✅
createDraft()capability for review-before-send workflows. - ✅ Label-scoped watching; self-sent mail is filtered to prevent reply loops.
- ✅ Fetch thread history (
fetchMessages/fetchThread), parse plain-text and HTML bodies, strip quoted replies, and surface attachment metadata. - ✅ Markdown ⇄ HTML email formatting.
Limitations (email has no equivalent — these throw NotImplementedError or
are no-ops): editing sent messages, reactions, typing indicators, modals,
ephemeral messages. Outgoing attachments are not yet supported. historyId
cursor and OAuth token persistence require stores you provide.
License
MIT
