This commit is contained in:
2025-05-25 11:18:12 +00:00
parent 58f4a123d2
commit 5b33623c2d
15 changed files with 832 additions and 764 deletions

View File

@ -54,7 +54,10 @@ export class CommandHandler extends EventEmitter {
* Send MAIL FROM command
*/
public async sendMailFrom(connection: ISmtpConnection, fromAddress: string): Promise<ISmtpResponse> {
const command = `${SMTP_COMMANDS.MAIL_FROM}:<${fromAddress}>`;
// Handle empty return path for bounce messages
const command = fromAddress === ''
? `${SMTP_COMMANDS.MAIL_FROM}:<>`
: `${SMTP_COMMANDS.MAIL_FROM}:<${fromAddress}>`;
return this.sendCommand(connection, command);
}
@ -77,15 +80,19 @@ export class CommandHandler extends EventEmitter {
* Send email data content
*/
public async sendDataContent(connection: ISmtpConnection, emailData: string): Promise<ISmtpResponse> {
// Ensure email data ends with CRLF.CRLF
let data = emailData;
// Normalize line endings to CRLF
let data = emailData.replace(/\r\n/g, '\n').replace(/\r/g, '\n').replace(/\n/g, '\r\n');
// Ensure email data ends with CRLF
if (!data.endsWith(LINE_ENDINGS.CRLF)) {
data += LINE_ENDINGS.CRLF;
}
data += '.' + LINE_ENDINGS.CRLF;
// Perform dot stuffing (escape lines starting with a dot)
data = data.replace(/\n\./g, '\n..');
data = data.replace(/\r\n\./g, '\r\n..');
// Add termination sequence
data += '.' + LINE_ENDINGS.CRLF;
return this.sendRawData(connection, data);
}
@ -306,7 +313,7 @@ export class CommandHandler extends EventEmitter {
const response = parseSmtpResponse(this.responseBuffer);
this.responseBuffer = '';
if (isSuccessCode(response.code) || response.code >= 400) {
if (isSuccessCode(response.code) || (response.code >= 300 && response.code < 400) || response.code >= 400) {
this.pendingCommand.resolve(response);
} else {
this.pendingCommand.reject(new Error(`Command failed: ${response.message}`));

View File

@ -57,20 +57,27 @@ export class SmtpClient extends EventEmitter {
*/
public async sendMail(email: Email): Promise<ISmtpSendResult> {
const startTime = Date.now();
const fromAddress = email.from;
const recipients = Array.isArray(email.to) ? email.to : [email.to];
// Extract clean email addresses without display names for SMTP operations
const fromAddress = email.getFromAddress();
const recipients = email.getToAddresses();
const ccRecipients = email.getCcAddresses();
const bccRecipients = email.getBccAddresses();
// Combine all recipients for SMTP operations
const allRecipients = [...recipients, ...ccRecipients, ...bccRecipients];
// Validate email addresses
if (!validateSender(fromAddress)) {
throw new Error(`Invalid sender address: ${fromAddress}`);
}
const recipientErrors = validateRecipients(recipients);
const recipientErrors = validateRecipients(allRecipients);
if (recipientErrors.length > 0) {
throw new Error(`Invalid recipients: ${recipientErrors.join(', ')}`);
}
logEmailSend('start', recipients, this.options);
logEmailSend('start', allRecipients, this.options);
let connection: ISmtpConnection | null = null;
const result: ISmtpSendResult = {
@ -79,7 +86,7 @@ export class SmtpClient extends EventEmitter {
rejectedRecipients: [],
envelope: {
from: fromAddress,
to: recipients
to: allRecipients
}
};
@ -114,8 +121,8 @@ export class SmtpClient extends EventEmitter {
throw new Error(`MAIL FROM failed: ${mailFromResponse.message}`);
}
// Send RCPT TO for each recipient
for (const recipient of recipients) {
// Send RCPT TO for each recipient (includes TO, CC, and BCC)
for (const recipient of allRecipients) {
try {
const rcptResponse = await this.commandHandler.sendRcptTo(connection, recipient);
if (rcptResponse.code >= 400) {

View File

@ -8,12 +8,28 @@ import type { ISmtpClientOptions, ISmtpAuthOptions } from '../interfaces.js';
/**
* Validate email address format
* Supports RFC-compliant addresses including empty return paths for bounces
*/
export function validateEmailAddress(email: string): boolean {
if (!email || typeof email !== 'string') {
if (typeof email !== 'string') {
return false;
}
return REGEX_PATTERNS.EMAIL_ADDRESS.test(email.trim());
const trimmed = email.trim();
// Handle empty return path for bounce messages (RFC 5321)
if (trimmed === '' || trimmed === '<>') {
return true;
}
// Handle display name formats
const angleMatch = trimmed.match(/<([^>]+)>/);
if (angleMatch) {
return REGEX_PATTERNS.EMAIL_ADDRESS.test(angleMatch[1]);
}
// Regular email validation
return REGEX_PATTERNS.EMAIL_ADDRESS.test(trimmed);
}
/**