Skip to content

Gmail send: gmail_reply/gmail_send with attachments — new gmail.send scope, write consent, and the re-consent migration #713

Description

@serge-ivo

Part of the Gmail epic. This is the one that needs a new OAuth scope, and therefore the one with a migration.

Why

PAGS cannot send an email by any route. Verified by exhaustion, not by reading one file:

grep -rn -i "gmail.send\|gmail.compose\|messages/send\|mailchannels\|sendgrid\|resend\.\|smtp\|nodemailer" workers/api/src packages/*/src

returns exactly one hit, and it is an unrelated English word in a CLI prompt string (lib/connector-tool-prompt.ts:218, "Re-capture, wait, and resend."). There is no mail transport, no provider integration, and no Gmail send call.

The connector says so too — scopes: { read: true, write: false } (lib/connectors/connected-accounts.ts:91), which is currently accurate.

Scope

Declaration changes on GMAIL_CONNECTOR:

  • scopes.writetrue. Required for the tool to be reachable at all: assertScope refuses a write-scoped tool on a write:false connector before any handler runs —
// lib/connectors/client.ts:84-87functionassertScope(opts?: TokenOpts): void{// A read-only connector (no write scope) can never satisfy a write request.if(opts?.scope==="write"&&!connector.scopes.write){

Flipping it is also what brings send under the #90 per-instance write-consent gate, so this makes send consent-gated, not ungated.

  • add https://www.googleapis.com/auth/gmail.send to oauth.scopes. gmail.send is sufficient to reply in-thread — pass threadId and set In-Reply-To/References. gmail.modify is NOT needed and must not be requested.

Tools (both scope: "write", mutates: true):

  • gmail_reply — reply to a message id, in its thread, with optional attachments taken from instance file ids
  • gmail_send — a new message to explicit recipients

Both build RFC-2822 MIME and POST base64url to /messages/send. Threading is the part to get right: correct In-Reply-To and References from the parent's Message-ID, plus threadId on the request body, or the reply lands as a detached message in the recipient's client.

The migration — this is the real work

Adding a scope invalidates nothing stored, but every already-connected user holds a refresh token granted under gmail.readonly alone. Google will mint access tokens from it happily; the send call then fails at the API with an insufficient-scopes 403. gmailErrorReason (lib/gmail.ts:185) already digs Google's real message out, which is why a 403 here says "insufficient scopes" instead of a bare status — that groundwork is done.

Required handling:

  • persist the granted scope set at connect time, so "can this connection send?" is answerable without making a failing API call
  • GET /v1/email/status reports canSend
  • gmail_reply/gmail_send refuse up front with "Gmail is connected but was authorised for reading only — reconnect Gmail to grant send access", never a raw 403
  • reconnecting must re-prompt Google for consent (prompt=consent) or the user silently gets the old grant back

Safety

Sending mail as the owner is among the most consequential things an agent on this platform can do. Non-negotiable:

  • permissions.email remains a hard precondition, as with every Gmail tool
  • write consent for the gmail connector must be granted for the instance ([connectors] Connector consent + write-scope safety + admin visibility #90) — this is what the scopes.write flip enables
  • the tool is NOT creator-selectable by default; an agent from the catalog must not arrive able to send mail as its subscriber
  • recipients come from the message being replied to or from explicit input, never inferred

Done when

  • a reply lands in the sender's client threaded under the original, with the attachment intact
  • a connection granted before this issue gets a clear reconnect instruction rather than a 403
  • consent refusal, permission refusal and scope refusal each have a distinct, actionable message, covered by tests

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    backendBackend / Worker / API workconnectorsConnector + tool frameworkenhancementNew feature or requestsecuritySecurity hardening / audit finding

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions