fix(email): use Gmail REST API for sending on OAuth accounts#570
Merged
zbigniewsobiecki merged 1 commit intodevfrom Feb 27, 2026
Merged
fix(email): use Gmail REST API for sending on OAuth accounts#570zbigniewsobiecki merged 1 commit intodevfrom
zbigniewsobiecki merged 1 commit intodevfrom
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
SMTP port 465 is blocked in the worker container, causing
ReplyToEmailandSendEmailgadget 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 threeReplyToEmailcalls 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, routesendEmail()andreplyToEmail()through the Gmail REST API (users.messages.send) instead of SMTP. Password/SMTP accounts are completely unaffected.streamTransportmode — in-process, zero network — to produce correctly-encoded RFC 822 bytes without any custom MIME logicOAuth2Client.setCredentials()passes the existing access token directly; no re-auth needed (token refresh is already handled inoauth.ts)replyViaGmailApi()reads the original message via IMAP (unchanged), then builds correctIn-Reply-ToandReferencesheaders before sending via RESTChanges
src/email/gmail/send.tssendViaGmailApi()andreplyViaGmailApi()src/email/client.tsauthMethodinsendEmail()andreplyToEmail()tests/unit/email/gmail/send.test.tspackage.json/package-lock.json@googleapis/gmailTests
8 new unit tests cover:
messageIdandacceptedarrayRe:prefix addedTo+CCincludedRe:prefix when subject already starts withRe:Verification
🤖 Generated with Claude Code