This commit is contained in:
2025-05-26 04:09:29 +00:00
parent 84196f9b13
commit 5a45d6cd45
19 changed files with 2691 additions and 4472 deletions

View File

@ -193,7 +193,10 @@ export class ConnectionManager extends EventEmitter {
reject(new Error(`Connection timeout after ${timeout}ms`));
}, timeout);
socket.once('connect', () => {
// For TLS connections, we need to wait for 'secureConnect' instead of 'connect'
const successEvent = this.options.secure ? 'secureConnect' : 'connect';
socket.once(successEvent, () => {
clearTimeout(timeoutHandler);
resolve(socket);
});

View File

@ -45,6 +45,12 @@ export class CommandHandler implements ICommandHandler {
return;
}
// Check if we're in the middle of an AUTH LOGIN sequence
if ((session as any).authLoginState) {
await this.handleAuthLoginResponse(socket, session, commandLine);
return;
}
// Handle raw data chunks from connection manager during DATA mode
if (commandLine.startsWith('__RAW_DATA__')) {
const rawData = commandLine.substring('__RAW_DATA__'.length);
@ -780,8 +786,167 @@ export class CommandHandler implements ICommandHandler {
return;
}
// Simple response for now - authentication would be implemented in the security handler
this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} Authentication not implemented yet`);
// Parse AUTH command
const parts = args.trim().split(/\s+/);
const method = parts[0]?.toUpperCase();
const initialResponse = parts[1];
// Check if method is supported
const supportedMethods = this.smtpServer.getOptions().auth.methods.map(m => m.toUpperCase());
if (!method || !supportedMethods.includes(method)) {
this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} Unsupported authentication method`);
return;
}
// Handle different authentication methods
switch (method) {
case 'PLAIN':
this.handleAuthPlain(socket, session, initialResponse);
break;
case 'LOGIN':
this.handleAuthLogin(socket, session, initialResponse);
break;
default:
this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} ${method} authentication not implemented`);
}
}
/**
* Handle AUTH PLAIN authentication
* @param socket - Client socket
* @param session - Session
* @param initialResponse - Optional initial response
*/
private async handleAuthPlain(socket: plugins.net.Socket | plugins.tls.TLSSocket, session: ISmtpSession, initialResponse?: string): Promise<void> {
try {
let credentials: string;
if (initialResponse) {
// Credentials provided with AUTH PLAIN command
credentials = initialResponse;
} else {
// Request credentials
this.sendResponse(socket, '334');
// Wait for credentials
credentials = await this.waitForAuthResponse(socket);
}
// Decode PLAIN credentials (base64 encoded: authzid\0authcid\0password)
const decoded = Buffer.from(credentials, 'base64').toString('utf8');
const parts = decoded.split('\0');
if (parts.length !== 3) {
this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} Invalid credentials format`);
return;
}
const [authzid, authcid, password] = parts;
const username = authcid || authzid; // Use authcid if provided, otherwise authzid
// Authenticate using security handler
const authenticated = await this.smtpServer.getSecurityHandler().authenticate({
username,
password,
mechanism: 'PLAIN'
});
if (authenticated) {
session.authenticated = true;
session.username = username;
this.sendResponse(socket, `${SmtpResponseCode.AUTHENTICATION_SUCCESSFUL} Authentication successful`);
} else {
this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} Authentication failed`);
}
} catch (error) {
SmtpLogger.error(`AUTH PLAIN error: ${error instanceof Error ? error.message : String(error)}`);
this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} Authentication error`);
}
}
/**
* Handle AUTH LOGIN authentication
* @param socket - Client socket
* @param session - Session
* @param initialResponse - Optional initial response
*/
private async handleAuthLogin(socket: plugins.net.Socket | plugins.tls.TLSSocket, session: ISmtpSession, initialResponse?: string): Promise<void> {
try {
if (initialResponse) {
// Username provided with AUTH LOGIN command
const username = Buffer.from(initialResponse, 'base64').toString('utf8');
(session as any).authLoginState = 'waiting_password';
(session as any).authLoginUsername = username;
// Request password
this.sendResponse(socket, '334 UGFzc3dvcmQ6'); // Base64 for "Password:"
} else {
// Request username
(session as any).authLoginState = 'waiting_username';
this.sendResponse(socket, '334 VXNlcm5hbWU6'); // Base64 for "Username:"
}
} catch (error) {
SmtpLogger.error(`AUTH LOGIN error: ${error instanceof Error ? error.message : String(error)}`);
this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} Authentication error`);
delete (session as any).authLoginState;
delete (session as any).authLoginUsername;
}
}
/**
* Handle AUTH LOGIN response
* @param socket - Client socket
* @param session - Session
* @param response - Response from client
*/
private async handleAuthLoginResponse(socket: plugins.net.Socket | plugins.tls.TLSSocket, session: ISmtpSession, response: string): Promise<void> {
const trimmedResponse = response.trim();
// Check for cancellation
if (trimmedResponse === '*') {
this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} Authentication cancelled`);
delete (session as any).authLoginState;
delete (session as any).authLoginUsername;
return;
}
try {
if ((session as any).authLoginState === 'waiting_username') {
// We received the username
const username = Buffer.from(trimmedResponse, 'base64').toString('utf8');
(session as any).authLoginUsername = username;
(session as any).authLoginState = 'waiting_password';
// Request password
this.sendResponse(socket, '334 UGFzc3dvcmQ6'); // Base64 for "Password:"
} else if ((session as any).authLoginState === 'waiting_password') {
// We received the password
const password = Buffer.from(trimmedResponse, 'base64').toString('utf8');
const username = (session as any).authLoginUsername;
// Clear auth state
delete (session as any).authLoginState;
delete (session as any).authLoginUsername;
// Authenticate using security handler
const authenticated = await this.smtpServer.getSecurityHandler().authenticate({
username,
password,
mechanism: 'LOGIN'
});
if (authenticated) {
session.authenticated = true;
session.username = username;
this.sendResponse(socket, `${SmtpResponseCode.AUTHENTICATION_SUCCESSFUL} Authentication successful`);
} else {
this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} Authentication failed`);
}
}
} catch (error) {
SmtpLogger.error(`AUTH LOGIN response error: ${error instanceof Error ? error.message : String(error)}`);
this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} Authentication error`);
delete (session as any).authLoginState;
delete (session as any).authLoginUsername;
}
}
/**