2025-05-21 13:42:12 +00:00

194 lines
5.4 KiB
TypeScript

/**
* SMTP Validation Utilities
* Provides validation functions for SMTP server
*/
import { SmtpState } from '../interfaces.js';
import { SMTP_PATTERNS } from '../constants.js';
/**
* Validates an email address
* @param email - Email address to validate
* @returns Whether the email address is valid
*/
export function isValidEmail(email: string): boolean {
if (!email || typeof email !== 'string') {
return false;
}
return SMTP_PATTERNS.EMAIL.test(email);
}
/**
* Validates the MAIL FROM command syntax
* @param args - Arguments string from the MAIL FROM command
* @returns Object with validation result and extracted data
*/
export function validateMailFrom(args: string): {
isValid: boolean;
address?: string;
params?: Record<string, string>;
errorMessage?: string;
} {
if (!args) {
return { isValid: false, errorMessage: 'Missing arguments' };
}
const match = args.match(SMTP_PATTERNS.MAIL_FROM);
if (!match) {
return { isValid: false, errorMessage: 'Invalid syntax' };
}
const [, address, paramsString] = match;
if (!isValidEmail(address)) {
return { isValid: false, errorMessage: 'Invalid email address' };
}
// Parse parameters if they exist
const params: Record<string, string> = {};
if (paramsString) {
let paramMatch;
const paramRegex = SMTP_PATTERNS.PARAM;
paramRegex.lastIndex = 0; // Reset the regex
while ((paramMatch = paramRegex.exec(paramsString)) !== null) {
const [, name, value = ''] = paramMatch;
params[name.toUpperCase()] = value;
}
}
return { isValid: true, address, params };
}
/**
* Validates the RCPT TO command syntax
* @param args - Arguments string from the RCPT TO command
* @returns Object with validation result and extracted data
*/
export function validateRcptTo(args: string): {
isValid: boolean;
address?: string;
params?: Record<string, string>;
errorMessage?: string;
} {
if (!args) {
return { isValid: false, errorMessage: 'Missing arguments' };
}
const match = args.match(SMTP_PATTERNS.RCPT_TO);
if (!match) {
return { isValid: false, errorMessage: 'Invalid syntax' };
}
const [, address, paramsString] = match;
if (!isValidEmail(address)) {
return { isValid: false, errorMessage: 'Invalid email address' };
}
// Parse parameters if they exist
const params: Record<string, string> = {};
if (paramsString) {
let paramMatch;
const paramRegex = SMTP_PATTERNS.PARAM;
paramRegex.lastIndex = 0; // Reset the regex
while ((paramMatch = paramRegex.exec(paramsString)) !== null) {
const [, name, value = ''] = paramMatch;
params[name.toUpperCase()] = value;
}
}
return { isValid: true, address, params };
}
/**
* Validates the EHLO command syntax
* @param args - Arguments string from the EHLO command
* @returns Object with validation result and extracted data
*/
export function validateEhlo(args: string): {
isValid: boolean;
hostname?: string;
errorMessage?: string;
} {
if (!args) {
return { isValid: false, errorMessage: 'Missing domain name' };
}
const match = args.match(SMTP_PATTERNS.EHLO);
if (!match) {
return { isValid: false, errorMessage: 'Invalid syntax' };
}
const hostname = match[1];
// Check for invalid characters in hostname
if (hostname.includes('@') || hostname.includes('<')) {
return { isValid: false, errorMessage: 'Invalid domain name format' };
}
return { isValid: true, hostname };
}
/**
* Validates command in the current SMTP state
* @param command - SMTP command
* @param currentState - Current SMTP state
* @returns Whether the command is valid in the current state
*/
export function isValidCommandSequence(command: string, currentState: SmtpState): boolean {
const upperCommand = command.toUpperCase();
// Some commands are valid in any state
if (upperCommand === 'QUIT' || upperCommand === 'RSET' || upperCommand === 'NOOP' || upperCommand === 'HELP') {
return true;
}
// State-specific validation
switch (currentState) {
case SmtpState.GREETING:
return upperCommand === 'EHLO' || upperCommand === 'HELO';
case SmtpState.AFTER_EHLO:
return upperCommand === 'MAIL' || upperCommand === 'STARTTLS' || upperCommand === 'AUTH';
case SmtpState.MAIL_FROM:
case SmtpState.RCPT_TO:
if (upperCommand === 'RCPT') {
return true;
}
return currentState === SmtpState.RCPT_TO && upperCommand === 'DATA';
case SmtpState.DATA:
// In DATA state, only the data content is accepted, not commands
return false;
case SmtpState.DATA_RECEIVING:
// In DATA_RECEIVING state, only the data content is accepted, not commands
return false;
case SmtpState.FINISHED:
// After data is received, only new transactions or session end
return upperCommand === 'MAIL' || upperCommand === 'QUIT' || upperCommand === 'RSET';
default:
return false;
}
}
/**
* Validates if a hostname is valid according to RFC 5321
* @param hostname - Hostname to validate
* @returns Whether the hostname is valid
*/
export function isValidHostname(hostname: string): boolean {
if (!hostname || typeof hostname !== 'string') {
return false;
}
// Basic hostname validation
// This is a simplified check, full RFC compliance would be more complex
return /^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$/.test(hostname);
}