From fc013a9f23cef1ffe3355452836ada5e9563f17a Mon Sep 17 00:00:00 2001 From: Sonarly Claude Code Date: Tue, 12 May 2026 20:43:45 +0000 Subject: [PATCH] fix(messaging): handle ImapFlow error events safely https://sonarly.com/issue/37138?type=bug An unhandled Fix: Implemented the crash fix by ensuring every runtime-created IMAP client has an attached `error` listener before connect/use, so async socket failures are handled instead of terminating the Node process. What changed: 1) `ImapClientProvider` now attaches `client.on('error', ...)` inside `createConnection()` before `connect()`. - This covers the shared provider used by import/outbound flows (`getClient()` consumers). - Added lifecycle awareness for shutdown to avoid treating expected disconnect-time socket errors as hard failures. 2) `ImapSmtpCaldavService.testImapConnection()` now also attaches `client.on('error', ...)`. - This closes the second `new ImapFlow(...)` site so test-connection path is also protected from unhandled emitter errors. These changes keep existing connect/auth error propagation behavior intact while preventing process-level crashes from unhandled IMAP emitter errors. --- .../imap-smtp-caldav-connection.service.ts | 24 ++++++++++++ .../imap/providers/imap-client.provider.ts | 37 ++++++++++++++++++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/services/imap-smtp-caldav-connection.service.ts b/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/services/imap-smtp-caldav-connection.service.ts index 5408778f972..7467ba3569f 100644 --- a/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/services/imap-smtp-caldav-connection.service.ts +++ b/packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/services/imap-smtp-caldav-connection.service.ts @@ -5,6 +5,7 @@ import { msg } from '@lingui/core/macro'; import { ImapFlow } from 'imapflow'; import { createTransport } from 'nodemailer'; import { ConnectedAccountProvider } from 'twenty-shared/types'; +import { MessageNetworkExceptionCode } from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-network.exception'; import { Repository } from 'typeorm'; import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; @@ -53,6 +54,16 @@ export class ImapSmtpCaldavService { }, }); + client.on('error', (error) => { + if (this.isExpectedSocketError(error)) { + this.logger.warn(`IMAP test client socket error: ${error.message}`); + + return; + } + + this.logger.error('IMAP test client emitted an async error', error.stack); + }); + try { await client.connect(); @@ -220,4 +231,17 @@ export class ImapSmtpCaldavService { authContext, ); } + + private isExpectedSocketError(error: Error): boolean { + if (!('code' in error)) { + return false; + } + + return [ + MessageNetworkExceptionCode.ECONNABORTED, + MessageNetworkExceptionCode.ECONNRESET, + MessageNetworkExceptionCode.ETIMEDOUT, + MessageNetworkExceptionCode.EHOSTUNREACH, + ].includes(error.code as MessageNetworkExceptionCode); + } } diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/providers/imap-client.provider.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/providers/imap-client.provider.ts index 65747593481..539109c9132 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/providers/imap-client.provider.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/providers/imap-client.provider.ts @@ -8,6 +8,7 @@ import { type ImapSmtpCaldavParams } from 'src/engine/core-modules/imap-smtp-cal import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service'; import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity'; import { MessageImportDriverExceptionCode } from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception'; +import { MessageNetworkExceptionCode } from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-network.exception'; import { parseImapAuthenticationError } from 'src/modules/messaging/message-import-manager/drivers/imap/utils/parse-imap-authentication-error.util'; type ConnectedAccountIdentifier = Pick< @@ -18,6 +19,7 @@ type ConnectedAccountIdentifier = Pick< @Injectable() export class ImapClientProvider { private readonly logger = new Logger(ImapClientProvider.name); + private readonly clientsInShutdown = new WeakSet(); private static readonly CONNECTION_TIMEOUT_MS = 30000; private static readonly GREETING_TIMEOUT_MS = 16000; @@ -42,11 +44,19 @@ export class ImapClientProvider { } async closeClient(client: ImapFlow): Promise { + this.clientsInShutdown.add(client); + try { await client.logout(); this.logger.log('Closed IMAP client'); } catch (error) { - this.logger.error(`Error closing IMAP client: ${error.message}`); + if (this.isExpectedSocketError(error)) { + this.logger.warn(`IMAP client closed with a socket error: ${error.message}`); + } else { + this.logger.error(`Error closing IMAP client: ${error.message}`); + } + } finally { + this.clientsInShutdown.delete(client); } } @@ -94,6 +104,18 @@ export class ImapClientProvider { greetingTimeout: ImapClientProvider.GREETING_TIMEOUT_MS, }); + client.on('error', (error) => { + const logContext = `IMAP client error for ${connectedAccount.handle}`; + + if (this.clientsInShutdown.has(client)) { + this.logger.warn(`${logContext} during client shutdown: ${error.message}`); + + return; + } + + this.logger.error(logContext, error.stack); + }); + try { await client.connect(); @@ -112,4 +134,17 @@ export class ImapClientProvider { throw error; } } + + private isExpectedSocketError(error: Error): boolean { + if (!('code' in error)) { + return false; + } + + return [ + MessageNetworkExceptionCode.ECONNABORTED, + MessageNetworkExceptionCode.ECONNRESET, + MessageNetworkExceptionCode.ETIMEDOUT, + MessageNetworkExceptionCode.EHOSTUNREACH, + ].includes(error.code as MessageNetworkExceptionCode); + } }