194 lines
5.4 KiB
TypeScript
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);
|
|
} |