diff --git a/src/channels/email/smtp-imap/SmtpImapChannelHost.ts b/src/channels/email/smtp-imap/SmtpImapChannelHost.ts index fbf64c7..8d61be2 100644 --- a/src/channels/email/smtp-imap/SmtpImapChannelHost.ts +++ b/src/channels/email/smtp-imap/SmtpImapChannelHost.ts @@ -24,6 +24,7 @@ import { smtpImapSendBodySchema, smtpImapSendResponseSchema } from '../../../htt import type { SmtpImapSendResponse } from '../../../http/contracts/smtp-imap-outgoing'; import { NotFoundError } from '../../../errors'; import { SYSTEM_CONTEXT } from '../../../services/RequestContext'; +import { OAuth2TokenRefreshService } from '../../../services/OAuth2TokenRefreshService'; import { extractConversationIdFromMessageId, extractConversationIdFromReferences } from '../shared/MessageIdUtils'; const DEFAULT_SESSION_TIMEOUT_MS = 24 * 60 * 60 * 1000; @@ -50,6 +51,7 @@ export class SmtpImapChannelHost { @inject(ProjectService) private readonly projectService: ProjectService, @inject(UserService) private readonly userService: UserService, @inject(SecretRefUtils) private readonly secretRefUtils: SecretRefUtils, + @inject(OAuth2TokenRefreshService) private readonly oauth2TokenRefreshService: OAuth2TokenRefreshService, ) {} static getOpenAPIPaths(): RouteConfig[] { @@ -123,6 +125,18 @@ export class SmtpImapChannelHost { } const { fromAddress, smtp, threadingStrategy, oauth2 } = configResult.data; + if (oauth2?.accessToken && oauth2.accessTokenExpiry && Date.now() >= oauth2.accessTokenExpiry) { + logger.info({ channelProviderId }, 'SMTP/IMAP outgoing: OAuth2 token expired, refreshing inline'); + await this.oauth2TokenRefreshService.refreshProvider(channelProviderId); + const refreshedRawConfig = await this.secretRefUtils.resolveObject(providerRecord.config as Record); + const refreshedResult = smtpImapChannelProviderConfigSchema.safeParse(refreshedRawConfig); + if (refreshedResult.success) { + configResult.data = refreshedResult.data; + } + } + + const { oauth2: refreshedOAuth2 } = configResult.data; + let resolvedStageId = body.stageId ?? queryStageId; if (!resolvedStageId) { const project = await this.projectService.getProjectById(projectId, SYSTEM_CONTEXT); @@ -165,7 +179,7 @@ export class SmtpImapChannelHost { smtp.secure, smtp.auth.user, smtp.auth.pass, - oauth2?.accessToken, + refreshedOAuth2?.accessToken, ); try { diff --git a/src/services/ImapInboundService.ts b/src/services/ImapInboundService.ts index 0e9f405..5db8298 100644 --- a/src/services/ImapInboundService.ts +++ b/src/services/ImapInboundService.ts @@ -5,6 +5,7 @@ import { eq, and } from 'drizzle-orm'; import { db } from '../db'; import { providers, apiKeys } from '../db/schema'; import { SmtpImapChannelHost } from '../channels/email/smtp-imap/SmtpImapChannelHost'; +import { OAuth2TokenRefreshService } from './OAuth2TokenRefreshService'; import { smtpImapChannelProviderConfigSchema } from './providers/channel/SmtpImapChannelProvider'; import { SecretRefUtils } from './secrets/SecretRefUtils'; import { logger } from '../utils/logger'; @@ -430,6 +431,12 @@ export class ImapInboundService { const config = configResult.data; + if (config.oauth2?.accessToken && config.oauth2.accessTokenExpiry && Date.now() >= config.oauth2.accessTokenExpiry) { + logger.info({ providerId }, 'IMAP reload: OAuth2 token expired, refreshing inline'); + await container.resolve(OAuth2TokenRefreshService).refreshProvider(providerId); + return; + } + const apiKeyRecord = await this.findProjectApiKey(config.projectId); if (!apiKeyRecord) { logger.warn({ providerId, projectId: config.projectId }, 'No API key found for project, skipping IMAP reload'); @@ -487,6 +494,12 @@ export class ImapInboundService { const config = configResult.data; + if (config.oauth2?.accessToken && config.oauth2.accessTokenExpiry && Date.now() >= config.oauth2.accessTokenExpiry) { + logger.info({ providerId: provider.id }, 'IMAP discovery: OAuth2 token expired, refreshing inline'); + await container.resolve(OAuth2TokenRefreshService).refreshProvider(provider.id); + continue; + } + const apiKeyRecord = await this.findProjectApiKey(config.projectId); if (!apiKeyRecord) { logger.warn({ providerId: provider.id, projectId: config.projectId }, 'No API key found for project, skipping IMAP inbound');