inbox-check.ts
Language TypeScript4.1 KB100 lines
/*** Does the Gmail service account actually work? Ask Google directly.** Verifying the credential through the Worker is slow or impossible:* `wrangler dev` cannot see secrets set with `secret put`, so it always reports* "not set"; `wrangler dev --remote` can, but its preview-session API is* unreliable on the pinned wrangler; and a deployed Worker only reads Gmail* when its cron fires. So the one question that matters during setup (is the* delegation right?) needs its own fast answer.** This asks Google the same question the Worker will, through the same client* code, with no Cloudflare in the middle.** Read-only; writes nothing. It lists one message per mailbox to prove the* impersonation works, prints no subjects and no senders, and stores nothing.** pnpm inbox:check ~/Downloads/meridian-dashboard-inbox-abc123.json** Or, if you have already deleted the key file and put it in your environment:** pnpm inbox:check*/import { readFileSync } from 'node:fs'import { INBOX_MAILBOXES } from '../config/inbox.js'import { GMAIL_SCOPE, GMAIL_SCOPES, readMailbox, type ServiceAccount } from '@meridian/gmail-client'import { errorMessage } from '@meridian/shared'function credentials(): ServiceAccount {const file = process.argv[2]if (file) {let parsed: { client_email?: string; private_key?: string }try {parsed = JSON.parse(readFileSync(file, 'utf8')) as typeof parsed} catch (err) {console.error(`Could not read ${file}: ${errorMessage(err)}`)process.exit(1)}if (!parsed.client_email || !parsed.private_key) {console.error(`${file} has no client_email / private_key. Is it the service account key JSON?`)process.exit(1)}return { email: parsed.client_email, privateKey: parsed.private_key }}const email = process.env.GMAIL_SA_EMAILconst privateKey = process.env.GMAIL_SA_KEYif (!email || !privateKey) {console.error('Usage: pnpm inbox:check <path-to-service-account-key.json>')console.error(' or: set GMAIL_SA_EMAIL and GMAIL_SA_KEY and run with no argument.')process.exit(1)}return { email, privateKey }}const sa = credentials()console.log(`\nService account: ${sa.email}`)console.log(`Scopes: ${GMAIL_SCOPE}\n`)let ok = 0const failures: string[] = []for (const m of INBOX_MAILBOXES) {// One message is enough to prove impersonation. Nothing about it is printed.const r = await readMailbox(m.email, sa, { maxMessages: 1 })if (r.ok) {ok++console.log(` OK ${m.email.padEnd(28)} (${r.messages.length} message read)`)} else {failures.push(m.email)console.log(` FAILED ${m.email.padEnd(28)} ${r.error}`)}}console.log(`\n${ok} of ${INBOX_MAILBOXES.length} mailboxes readable.`)if (failures.length === INBOX_MAILBOXES.length) {// Every mailbox failing is a different diagnosis from one failing, and the// difference is worth stating rather than leaving to be inferred from a wall// of identical errors.console.log(`\nAll of them failed, which points at the delegation rather than at any\n` +`individual mailbox. Check, in this order:\n` +` 1. Google Admin -> Security -> Access and data control -> API controls\n` +` -> Domain-wide delegation has an entry for this service account's\n` +` UNIQUE ID (the long number, not the email).\n` +` 2. Its scopes are exactly (that field is COMMA-separated, unlike\n` +` the OAuth parameter above): ${GMAIL_SCOPES.join(',')}\n` +` 3. It has been a few minutes: Admin changes propagate slowly.\n` +` 4. The Gmail API is enabled on the Cloud project.`,)} else if (failures.length) {console.log(`\nSome mailboxes read and some did not, so the delegation is working.`)console.log(`The failures are about those addresses: ${failures.join(', ')}.`)console.log(`A mailbox that does not exist, is suspended, or is a group rather`)console.log(`than a mailbox will fail this way.`)} else if (process.argv[2]) {console.log(`\nAll good. Delete the key file now: it opens every mailbox in the domain:`)console.log(` rm ${process.argv[2]}`)}