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.
This commit is contained in:
+24
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+36
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user