Skip to content

fix(email): use Gmail REST API for sending on OAuth accounts#570

Merged
zbigniewsobiecki merged 1 commit intodevfrom
feat/gmail-rest-api-send
Feb 27, 2026
Merged

fix(email): use Gmail REST API for sending on OAuth accounts#570
zbigniewsobiecki merged 1 commit intodevfrom
feat/gmail-rest-api-send

Conversation

@zbigniewsobiecki
Copy link
Copy Markdown
Member

Problem

SMTP port 465 is blocked in the worker container, causing ReplyToEmail and SendEmail gadget calls on Gmail OAuth accounts to silently hang for 30 s before the gadget timeout fires. Confirmed in the car-dealership email-joke run: all three ReplyToEmail calls timed out at exactly 30 000 ms with a cryptic "Gadget exceeded timeout" error.

Root cause: nodemailer.createTransport() has no connection-level timeout, so the gadget outer timer is the only kill-switch. IMAP (port 993) is not affected.

Solution

For authMethod === 'oauth' accounts, route sendEmail() and replyToEmail() through the Gmail REST API (users.messages.send) instead of SMTP. Password/SMTP accounts are completely unaffected.

  • MIME building: uses nodemailer's streamTransport mode — in-process, zero network — to produce correctly-encoded RFC 822 bytes without any custom MIME logic
  • Auth: OAuth2Client.setCredentials() passes the existing access token directly; no re-auth needed (token refresh is already handled in oauth.ts)
  • Threading: replyViaGmailApi() reads the original message via IMAP (unchanged), then builds correct In-Reply-To and References headers before sending via REST

Changes

File Change
src/email/gmail/send.ts New — sendViaGmailApi() and replyViaGmailApi()
src/email/client.ts Branch on authMethod in sendEmail() and replyToEmail()
tests/unit/email/gmail/send.test.ts New — 8 unit tests
package.json / package-lock.json Add @googleapis/gmail

Tests

8 new unit tests cover:

  • ✅ Success path: correct messageId and accepted array
  • ✅ Raw payload is valid base64url and passed to Gmail API
  • ✅ Reply-to-sender: single recipient, Re: prefix added
  • ✅ Reply-all: self excluded, original To + CC included
  • ✅ No double Re: prefix when subject already starts with Re:
  • ✅ Error propagation from the Gmail API (both send and reply)

Verification

# Re-trigger on prod after deploy:
cascade runs trigger --project car-dealership --agent-type email-joke
# Wait ~2 min, then:
cascade runs list --project car-dealership --agent-type email-joke --limit 3
cascade runs logs <new-run-id>
# Expect: ReplyToEmail gadget results succeed (no 30 s timeout), status=completed

🤖 Generated with Claude Code

SMTP port 465 is blocked in the worker container, causing all
ReplyToEmail/SendEmail gadget calls on Gmail OAuth accounts to silently
hang for 30 s before the gadget timeout fires — confirmed in the
car-dealership email-joke run (three ReplyToEmail calls, each 30 000 ms).

Root cause: nodemailer's createTransport() has no connection-level
timeout, so the gadget outer timer was the only kill-switch, yielding a
cryptic "Gadget exceeded timeout" error with no actionable detail.

Fix: for OAuth accounts (authMethod === 'oauth'), route sendEmail() and
replyToEmail() through the Gmail REST API (messages.send) instead of
SMTP. IMAP operations (SearchEmails, ReadEmail, MarkEmailAsSeen) are
unchanged — port 993 is not blocked.

Implementation:
- src/email/gmail/send.ts (new): sendViaGmailApi() and replyViaGmailApi()
  use nodemailer streamTransport (in-process, no network) to build
  RFC 822 bytes, then POST to gmail.users.messages.send via
  @googleapis/gmail. OAuth2Client passes the existing access token
  directly — no re-auth needed (token refresh is already handled in
  oauth.ts before this point).
- src/email/client.ts: branch on creds.authMethod at the top of both
  send functions; password/SMTP accounts are completely unaffected.
- @googleapis/gmail added as a production dependency (~1 MB, pulls in
  google-auth-library transitively).

Tests: 8 new unit tests in tests/unit/email/gmail/send.test.ts covering
success paths, base64url encoding, reply-to-sender, reply-all (self
excluded), Re: prefix deduplication, and error propagation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@zbigniewsobiecki zbigniewsobiecki merged commit 4a826c5 into dev Feb 27, 2026
6 checks passed
@zbigniewsobiecki zbigniewsobiecki deleted the feat/gmail-rest-api-send branch February 27, 2026 14:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant