fix(mail): migrate filesystem helpers to fsUtils, update DKIM and mail APIs, harden SMTP client, and bump dependencies
This commit is contained in:
@@ -24,6 +24,9 @@ export class CommandHandler extends EventEmitter {
|
||||
private responseBuffer: string = '';
|
||||
private pendingCommand: { resolve: Function; reject: Function; command: string } | null = null;
|
||||
private commandTimeout: NodeJS.Timeout | null = null;
|
||||
|
||||
// Maximum buffer size to prevent memory exhaustion from rogue servers
|
||||
private static readonly MAX_BUFFER_SIZE = 1024 * 1024; // 1MB max
|
||||
|
||||
constructor(options: ISmtpClientOptions) {
|
||||
super();
|
||||
@@ -144,63 +147,82 @@ export class CommandHandler extends EventEmitter {
|
||||
reject(new Error('Another command is already pending'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
this.pendingCommand = { resolve, reject, command };
|
||||
|
||||
// Set command timeout
|
||||
const timeout = 30000; // 30 seconds
|
||||
this.commandTimeout = setTimeout(() => {
|
||||
this.pendingCommand = null;
|
||||
this.commandTimeout = null;
|
||||
reject(new Error(`Command timeout: ${command}`));
|
||||
}, timeout);
|
||||
|
||||
|
||||
// Set up data handler
|
||||
const dataHandler = (data: Buffer) => {
|
||||
this.handleIncomingData(data.toString());
|
||||
};
|
||||
|
||||
|
||||
// Set up socket close/error handlers to reject pending promises
|
||||
const closeHandler = () => {
|
||||
if (this.pendingCommand) {
|
||||
this.pendingCommand.reject(new Error('Socket closed during command'));
|
||||
}
|
||||
};
|
||||
|
||||
const errorHandler = (err: Error) => {
|
||||
if (this.pendingCommand) {
|
||||
this.pendingCommand.reject(err);
|
||||
}
|
||||
};
|
||||
|
||||
connection.socket.on('data', dataHandler);
|
||||
|
||||
// Clean up function
|
||||
connection.socket.once('close', closeHandler);
|
||||
connection.socket.once('error', errorHandler);
|
||||
|
||||
// Clean up function - removes all listeners and clears buffer
|
||||
const cleanup = () => {
|
||||
connection.socket.removeListener('data', dataHandler);
|
||||
connection.socket.removeListener('close', closeHandler);
|
||||
connection.socket.removeListener('error', errorHandler);
|
||||
if (this.commandTimeout) {
|
||||
clearTimeout(this.commandTimeout);
|
||||
this.commandTimeout = null;
|
||||
}
|
||||
// Clear response buffer to prevent corrupted data for next command
|
||||
this.responseBuffer = '';
|
||||
};
|
||||
|
||||
// Send command
|
||||
const formattedCommand = command.endsWith(LINE_ENDINGS.CRLF) ? command : formatCommand(command);
|
||||
|
||||
logCommand(command, undefined, this.options);
|
||||
logDebug(`Sending command: ${command}`, this.options);
|
||||
|
||||
connection.socket.write(formattedCommand, (error) => {
|
||||
if (error) {
|
||||
cleanup();
|
||||
this.pendingCommand = null;
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Override resolve/reject to include cleanup
|
||||
|
||||
// Override resolve/reject to include cleanup BEFORE setting timeout
|
||||
const originalResolve = resolve;
|
||||
const originalReject = reject;
|
||||
|
||||
|
||||
this.pendingCommand.resolve = (response: ISmtpResponse) => {
|
||||
cleanup();
|
||||
this.pendingCommand = null;
|
||||
logCommand(command, response, this.options);
|
||||
originalResolve(response);
|
||||
};
|
||||
|
||||
|
||||
this.pendingCommand.reject = (error: Error) => {
|
||||
cleanup();
|
||||
this.pendingCommand = null;
|
||||
originalReject(error);
|
||||
};
|
||||
|
||||
// Set command timeout - uses wrapped reject that includes cleanup
|
||||
const timeout = 30000; // 30 seconds
|
||||
this.commandTimeout = setTimeout(() => {
|
||||
if (this.pendingCommand) {
|
||||
this.pendingCommand.reject(new Error(`Command timeout: ${command}`));
|
||||
}
|
||||
}, timeout);
|
||||
|
||||
// Send command
|
||||
const formattedCommand = command.endsWith(LINE_ENDINGS.CRLF) ? command : formatCommand(command);
|
||||
|
||||
logCommand(command, undefined, this.options);
|
||||
logDebug(`Sending command: ${command}`, this.options);
|
||||
|
||||
connection.socket.write(formattedCommand, (error) => {
|
||||
if (error) {
|
||||
if (this.pendingCommand) {
|
||||
this.pendingCommand.reject(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -213,55 +235,74 @@ export class CommandHandler extends EventEmitter {
|
||||
reject(new Error('Another command is already pending'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
this.pendingCommand = { resolve, reject, command: 'DATA_CONTENT' };
|
||||
|
||||
// Set data timeout
|
||||
const timeout = 60000; // 60 seconds for data
|
||||
this.commandTimeout = setTimeout(() => {
|
||||
this.pendingCommand = null;
|
||||
this.commandTimeout = null;
|
||||
reject(new Error('Data transmission timeout'));
|
||||
}, timeout);
|
||||
|
||||
|
||||
// Set up data handler
|
||||
const dataHandler = (chunk: Buffer) => {
|
||||
this.handleIncomingData(chunk.toString());
|
||||
};
|
||||
|
||||
|
||||
// Set up socket close/error handlers to reject pending promises
|
||||
const closeHandler = () => {
|
||||
if (this.pendingCommand) {
|
||||
this.pendingCommand.reject(new Error('Socket closed during data transmission'));
|
||||
}
|
||||
};
|
||||
|
||||
const errorHandler = (err: Error) => {
|
||||
if (this.pendingCommand) {
|
||||
this.pendingCommand.reject(err);
|
||||
}
|
||||
};
|
||||
|
||||
connection.socket.on('data', dataHandler);
|
||||
|
||||
// Clean up function
|
||||
connection.socket.once('close', closeHandler);
|
||||
connection.socket.once('error', errorHandler);
|
||||
|
||||
// Clean up function - removes all listeners and clears buffer
|
||||
const cleanup = () => {
|
||||
connection.socket.removeListener('data', dataHandler);
|
||||
connection.socket.removeListener('close', closeHandler);
|
||||
connection.socket.removeListener('error', errorHandler);
|
||||
if (this.commandTimeout) {
|
||||
clearTimeout(this.commandTimeout);
|
||||
this.commandTimeout = null;
|
||||
}
|
||||
// Clear response buffer to prevent corrupted data for next command
|
||||
this.responseBuffer = '';
|
||||
};
|
||||
|
||||
// Override resolve/reject to include cleanup
|
||||
|
||||
// Override resolve/reject to include cleanup BEFORE setting timeout
|
||||
const originalResolve = resolve;
|
||||
const originalReject = reject;
|
||||
|
||||
|
||||
this.pendingCommand.resolve = (response: ISmtpResponse) => {
|
||||
cleanup();
|
||||
this.pendingCommand = null;
|
||||
originalResolve(response);
|
||||
};
|
||||
|
||||
|
||||
this.pendingCommand.reject = (error: Error) => {
|
||||
cleanup();
|
||||
this.pendingCommand = null;
|
||||
originalReject(error);
|
||||
};
|
||||
|
||||
|
||||
// Set data timeout - uses wrapped reject that includes cleanup
|
||||
const timeout = 60000; // 60 seconds for data
|
||||
this.commandTimeout = setTimeout(() => {
|
||||
if (this.pendingCommand) {
|
||||
this.pendingCommand.reject(new Error('Data transmission timeout'));
|
||||
}
|
||||
}, timeout);
|
||||
|
||||
// Send data
|
||||
connection.socket.write(data, (error) => {
|
||||
if (error) {
|
||||
cleanup();
|
||||
this.pendingCommand = null;
|
||||
reject(error);
|
||||
if (this.pendingCommand) {
|
||||
this.pendingCommand.reject(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -274,17 +315,34 @@ export class CommandHandler extends EventEmitter {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = 30000; // 30 seconds
|
||||
let timeoutHandler: NodeJS.Timeout;
|
||||
|
||||
let resolved = false;
|
||||
|
||||
const cleanup = () => {
|
||||
if (resolved) return;
|
||||
resolved = true;
|
||||
clearTimeout(timeoutHandler);
|
||||
connection.socket.removeListener('data', dataHandler);
|
||||
connection.socket.removeListener('close', closeHandler);
|
||||
connection.socket.removeListener('error', errorHandler);
|
||||
this.responseBuffer = '';
|
||||
};
|
||||
|
||||
const dataHandler = (data: Buffer) => {
|
||||
if (resolved) return;
|
||||
|
||||
// Check buffer size
|
||||
if (this.responseBuffer.length + data.length > CommandHandler.MAX_BUFFER_SIZE) {
|
||||
cleanup();
|
||||
reject(new Error('Greeting response too large'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.responseBuffer += data.toString();
|
||||
|
||||
|
||||
if (this.isCompleteResponse(this.responseBuffer)) {
|
||||
clearTimeout(timeoutHandler);
|
||||
connection.socket.removeListener('data', dataHandler);
|
||||
|
||||
const response = parseSmtpResponse(this.responseBuffer);
|
||||
this.responseBuffer = '';
|
||||
|
||||
cleanup();
|
||||
|
||||
if (isSuccessCode(response.code)) {
|
||||
resolve(response);
|
||||
} else {
|
||||
@@ -292,13 +350,28 @@ export class CommandHandler extends EventEmitter {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const closeHandler = () => {
|
||||
if (resolved) return;
|
||||
cleanup();
|
||||
reject(new Error('Socket closed while waiting for greeting'));
|
||||
};
|
||||
|
||||
const errorHandler = (err: Error) => {
|
||||
if (resolved) return;
|
||||
cleanup();
|
||||
reject(err);
|
||||
};
|
||||
|
||||
timeoutHandler = setTimeout(() => {
|
||||
connection.socket.removeListener('data', dataHandler);
|
||||
if (resolved) return;
|
||||
cleanup();
|
||||
reject(new Error('Greeting timeout'));
|
||||
}, timeout);
|
||||
|
||||
|
||||
connection.socket.on('data', dataHandler);
|
||||
connection.socket.once('close', closeHandler);
|
||||
connection.socket.once('error', errorHandler);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -306,13 +379,19 @@ export class CommandHandler extends EventEmitter {
|
||||
if (!this.pendingCommand) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Check buffer size to prevent memory exhaustion from rogue servers
|
||||
if (this.responseBuffer.length + data.length > CommandHandler.MAX_BUFFER_SIZE) {
|
||||
this.pendingCommand.reject(new Error('Response too large'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.responseBuffer += data;
|
||||
|
||||
|
||||
if (this.isCompleteResponse(this.responseBuffer)) {
|
||||
const response = parseSmtpResponse(this.responseBuffer);
|
||||
this.responseBuffer = '';
|
||||
|
||||
|
||||
if (isSuccessCode(response.code) || (response.code >= 300 && response.code < 400) || response.code >= 400) {
|
||||
this.pendingCommand.resolve(response);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user