gmail-draft
Backs Save to my Gmail drafts on the email draft card in Ask SABLE. The browser calls
POST /api/gmail-draft, which forwards the person's own session here.
{ "to": ["Paul Robinson <paul.robinson@globe.com>"], "cc": [], "subject": "Next steps", "body": "Hi Paul, ...", "draft_id": "uuid" }
It creates one draft in the signed-in person's own mailbox and returns { ok, gmail_draft_id, open_url, mailbox }.
It never sends: the only Gmail call is drafts.create, and a test fails if any other Gmail
endpoint appears in the code.
Checks, in order
- A signed-in, active staff member (admin, lead or consultant). Portal users are refused.
GMAIL_DRAFTS_ENABLED=true(an Edge secret). Otherwise412, and the card suggests Copy.- The mailbox is the person's own
@indigotrigger.comaddress from SABLE. Nothing in the request can choose another mailbox. The web route also refuses while View as is set to someone else. - A valid draft: every address is printable ASCII and parses, at least one To, at most 20 recipients, subject up to 250 characters, body up to 20,000 characters. Header values can't carry a line break, so nothing on the card can add a header such as Bcc.
- The
GMAIL_COMPOSE_SA_JSONkey is set (503otherwise). - At most 30 drafts per person per hour (
429). A request first records anauditrow (gmail_draft_created, statuspending) and then counts, so requests sent at once cannot slip past the limit. The row is removed if the draft isn't made, and updated tocreatedwhen it is. It holds the recipient count and subject, never the body.
One-time setup
Google has no draft-only permission. gmail.compose also allows sending mail as anyone in the
domain. SABLE's code only creates drafts, but anyone holding the key below could send. That is why it
uses a service account of its own, holding only this one permission, with its key in a single Edge
secret. Never add gmail.compose to SABLE's shared Workspace service account: its key is also kept in
other Edge secrets and a Vault row.
Google side (a Google Workspace super admin: Chris or Lindsey).
- Open console.cloud.google.com, choose the Indigo Trigger project that holds SABLE's
existing service account, and go to IAM & Admin → Service Accounts → Create service account.
Name it
sable-gmail-drafts. It needs no project roles: skip that step and choose Done. The project must have the Gmail API enabled (APIs & Services → Library); it already is if SABLE's Gmail sync uses this project. - Open the new account. On Details, copy the Unique ID (a long number): this is its client ID.
- On Keys, choose Add key → Create new key → JSON. A file downloads. Hand it to a SABLE admin
through a password manager, never email or chat, then delete your copy. If key creation is
blocked, the organisation policy
iam.disableServiceAccountKeyCreationneeds an exception for this project. - Open admin.google.com and go to Security → Access and data control → API controls → Manage Domain Wide Delegation → Add new.
- Client ID: the Unique ID from step 2. OAuth scopes:
https://www.googleapis.com/auth/gmail.composeand nothing else. Choose Authorize. This is a new entry: leave SABLE's existing entry (Drive, Calendar and Gmail read scopes:drive.readonly,calendar.readonly,calendar.events,gmail.readonly) exactly as it is.
SABLE side (a SABLE admin).
- In Supabase, Project settings → Edge Functions → Secrets, add
GMAIL_COMPOSE_SA_JSONwith the whole contents of the JSON key file, then delete the file. - Add
GMAIL_DRAFTS_ENABLED=true. - Test: draft an email in Ask SABLE, choose Save to my Gmail drafts, and open the link. A new delegation can take from a few minutes up to 24 hours to apply; until then the button reports "SABLE isn't allowed to write Gmail drafts yet".
To turn it off: set GMAIL_DRAFTS_ENABLED to anything else. To remove the permission
completely: delete the delegation entry from step 5 and the service account from step 1, and remove
GMAIL_COMPOSE_SA_JSON.
Where the code lives
supabase/functions/gmail-draft/index.ts(Google calls) andlogic.js(checks and the message)web/api/gmail-draft.js(forwarding route) andweb/email-draft-card.js(the button)