Repository navigation
feat(email): REST Gmail/Graph mail, OAuth SMTP retry, and migration API updates - #8
amiteshwarp wants to merge 5 commits into
Conversation
…12.1-email-support - REST Gmail/Microsoft Graph inbound fetch and delivery (api_mode rest) - Channel::Email inbound helpers; migration PATCH + api_mode on create/update - OAuth token refresh merges provider_config; OAuth SMTP retry on send - BaseRefreshOauthTokenService, Gmail/Graph fetch services, mail delivery adapters Co-authored-by: Cursor <cursoragent@cursor.com>
scmmishra
left a comment
There was a problem hiding this comment.
I couldn't find the changes to auth flow to add the new scopes. Unless you wish to migrate users to the mail.google.com scope when re-authenticating it would be worth changing that
| pattr_initialize [:channel!, :interval] | ||
|
|
||
| GMAIL_API = 'https://gmail.googleapis.com/gmail/v1/users/me'.freeze | ||
| FETCHED_LABEL = 'CHATWOOT_FETCHED'.freeze |
There was a problem hiding this comment.
This label will be applied to the actual inbox of the customers as "CHATWOOT FETCHED". Is that acceptable? This will effectively expose chatwoot name to them
| class Gmail::FetchEmailService | ||
| pattr_initialize [:channel!, :interval] | ||
|
|
||
| GMAIL_API = 'https://gmail.googleapis.com/gmail/v1/users/me'.freeze |
There was a problem hiding this comment.
It's best to abstract out a GMail Client, that can be used elsewhere
| mail = Mail.read_from_string(Base64.urlsafe_decode64(raw_b64)) | ||
| return nil if channel.inbox.messages.exists?(source_id: mail.message_id) | ||
|
|
||
| add_label(message_id, label_id) |
There was a problem hiding this comment.
There's an issue here, add_label runs before Imap::ImapMailbox creates the Chatwoot message. If processing fails afterward, Gmail excludes the email from future syncs via -label:CHATWOOT_FETCHED, so the email can be silently lost.
This can be considered an acceptable risk, but worth documenting it
| @@ -0,0 +1,79 @@ | |||
| class Gmail::FetchEmailService | |||
There was a problem hiding this comment.
In general this shares a lot of it's boiler plate with Microsoft fetching service, would be worth abstracting it for long term maintainability
| mail = Mail.read_from_string(raw) | ||
| return nil if channel.inbox.messages.exists?(source_id: mail.message_id) | ||
|
|
||
| mark_fetched(message_id) |
There was a problem hiding this comment.
mark_fetched runs before the mailbox processing succeeds. A later processing failure leaves the Outlook message categorized as fetched, so future syncs skip it.
| def oauth_smtp_settings | ||
| return unless @inbox.email? && @channel.imap_enabled | ||
|
|
||
| if @channel.rest_api_mode? |
There was a problem hiding this comment.
The new model/job path explicitly supports REST API inbound with imap_enabled false, but this delivery branch is still gated by @channel.imap_enabled. In that configuration email_reply also fails its email_imap_enabled? && email_oauth_enabled? guard, so outbound replies return nil or fall back to global SMTP instead of using Gmail/Graph delivery.
Summary
This change adds an optional REST mail mode for Google and Microsoft email channels: inbound mail can be fetched via the Gmail API and Microsoft Graph instead of IMAP, and outbound mail can use matching REST delivery when that mode is active. It also hardens OAuth SMTP sending when access tokens are stale, fixes OAuth token refresh persistence on JSONB
provider_config, and ensures scheduled inbound fetch runs for channels that rely on REST (or OAuth mail APIs) without IMAP being enabled.Together, these updates support migrated or narrow-scope OAuth tokens where full IMAP/SMTP-style scopes are not available, while preserving existing behavior for channels that still use IMAP inbound + SMTP outbound.
Background and intent
Narrow OAuth scopes: Some integrations only have REST-oriented scopes (e.g. Gmail modify / Graph mail read-write-send) rather than broad legacy mail scopes. For those channels, the application must use provider REST APIs for read and send instead of assuming IMAP/SMTP.
Compatibility: Channels created through the standard in-app Google/Microsoft flows that already use IMAP + SMTP should behave as before when REST mode is not enabled.
Problems addressed
provider_config['access_token']was expired, eventually pushing the channel toward reauthorization/disconnect.nilin some paths.provider_config, causing races and dropped metadata.imap_enabled, so channels that only fetch via REST (or OAuth mail without IMAP) were skipped.What changed (by area)
Channel::Email(app/models/channel/email.rb)rest_api_mode?—provider_config['api_mode'] == 'rest'.gmail_api_inbound?/microsoft_graph_inbound?— Select REST inbound fetchers for Google/Microsoft when REST is set or when OAuth mail APIs apply without IMAP.inbound_fetch_enabled?— IMAP enabled OR REST mode OR OAuth API inbound without IMAP, so the scheduler and fetch job actually run for REST-only setups.Inbound jobs
Inboxes::FetchImapEmailInboxesJob— Usesinbound_fetch_enabled?instead of onlyimap_enabled.Inboxes::FetchImapEmailsJob— Dispatches toGmail::FetchEmailService/MicrosoftGraph::FetchEmailServicewhen the channel is in REST inbound mode; otherwise keeps the existing IMAP service chain.REST inbound implementations apply provider-specific idempotency (e.g. label/category) so messages are not fetched repeatedly.
Outbound mail
ConversationReplyMailerHelper— For REST mode, setsdelivery_methodto:gmail_apior:microsoft_graphwith{ channel: }options. For classic OAuth SMTP, usesoauth_access_tokenvia the refresh services as a read-only consumer of cached tokens (no destructive merge from the mailer).config/initializers/api_email_delivery.rb— Registers custom Action Mailer delivery methods.lib/mail/gmail_api_delivery.rbandlib/mail/microsoft_graph_delivery.rb— Send via provider REST APIs.OAuth refresh and SMTP retry
BaseRefreshOauthTokenService—update_channel_provider_configmerges new tokens into existingprovider_config(string keys) so flags likeapi_modesurvive refresh; clears cached indifferent access after refresh soaccess_tokenreturns the updated value.Email::OauthSmtpRetrier— OnNet::SMTPAuthenticationErrorfor Google/Microsoft email channels: refresh tokens, retry the send block once, clear stale reauthorization UI state when a send proves credentials work, and useauthorization_error!on persistent failure.Email::SendOnEmailService— Wrapsdeliver_nowinEmail::OauthSmtpRetrier.with_retry.Note for this codebase: Some upstream trees enqueue outgoing mail through a dedicated Sidekiq worker class; this branch delivers the relevant path through
Email::SendOnEmailService, so the retrier is wired there (not duplicated in a separate worker that does not exist on this line).Platform API — email channel migrations
Platform::Api::V1::EmailChannelMigrationsController— On create/update for Google/Microsoft, ensuresapi_mode: 'rest'is stamped intoprovider_configwhere applicable. Adds update support for migrated inboxes/channels (including optionalreauthorized!when credentials are rotated).config/routes.rb— Exposesupdateonemail_channel_migrationswithparam: :inbox_id.Tests and factories
Request specs for the migration API (create + update), job specs, model specs for fetch eligibility, Gmail/Graph fetch and mail delivery specs, refresh-token service coverage, and factory traits for REST-shaped channels.
Files touched (reference)
app/models/channel/email.rbapp/jobs/inboxes/fetch_imap_emails_job.rb,app/jobs/inboxes/fetch_imap_email_inboxes_job.rbapp/mailers/conversation_reply_mailer_helper.rb,lib/mail/gmail_api_delivery.rb,lib/mail/microsoft_graph_delivery.rb,config/initializers/api_email_delivery.rbapp/services/gmail/fetch_email_service.rb,app/services/microsoft_graph/fetch_email_service.rb,app/services/email/oauth_smtp_retrier.rb,app/services/email/send_on_email_service.rb,app/services/base_refresh_oauth_token_service.rbapp/controllers/platform/api/v1/email_channel_migrations_controller.rb,config/routes.rbspec/andspec/factories/channel/channel_email.rbDeploy / infra: If your environment previously relied on separate Helm or env-only changes for OAuth client configuration, confirm those values in your deployment pipeline; this application PR does not add Helm value files on this branch.
How to test
EMAIL_CHANNEL_MIGRATIONenabled, create channels via the platform migrations endpoint; confirmprovider_config['api_mode']isrestfor Google/Microsoft where designed.imap_enabledfalse but REST (or OAuth mail API) configuration present, confirm the inbox is still picked up byFetchImapEmailInboxesJoband processed.provider_config(includingapi_modewhen set).PATCH .../email_channel_migrations/:inbox_idand verify attributes and reauthorization state when rotatingprovider_config.Do not paste real tokens or client secrets into tickets or pull requests.
Base branch note for reviewers
This work sits on a branch that includes platform email channel migrations and related baseline not present on every long-lived
developtip. If the diff againstdevelopis noisy, switch the PR base to the integration or release branch that already contains that baseline so the review focuses on the mail/OAuth/migration changes above.