Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code fc013a9f23 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.
2026-05-12 20:43:45 +00:00
2 changed files with 60 additions and 1 deletions
@@ -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);
}
}
@@ -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<ImapFlow>();
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<void> {
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);
}
}