update
This commit is contained in:
224
ts/mail/delivery/smtpclient/utils/helpers.ts
Normal file
224
ts/mail/delivery/smtpclient/utils/helpers.ts
Normal file
@ -0,0 +1,224 @@
|
||||
/**
|
||||
* SMTP Client Helper Functions
|
||||
* Protocol helper functions and utilities
|
||||
*/
|
||||
|
||||
import { SMTP_CODES, REGEX_PATTERNS, LINE_ENDINGS } from '../constants.js';
|
||||
import type { ISmtpResponse, ISmtpCapabilities } from '../interfaces.js';
|
||||
|
||||
/**
|
||||
* Parse SMTP server response
|
||||
*/
|
||||
export function parseSmtpResponse(data: string): ISmtpResponse {
|
||||
const lines = data.trim().split(/\r?\n/);
|
||||
const firstLine = lines[0];
|
||||
const match = firstLine.match(REGEX_PATTERNS.RESPONSE_CODE);
|
||||
|
||||
if (!match) {
|
||||
return {
|
||||
code: 500,
|
||||
message: 'Invalid server response',
|
||||
raw: data
|
||||
};
|
||||
}
|
||||
|
||||
const code = parseInt(match[1], 10);
|
||||
const separator = match[2];
|
||||
const message = lines.map(line => line.substring(4)).join(' ');
|
||||
|
||||
// Check for enhanced status code
|
||||
const enhancedMatch = message.match(REGEX_PATTERNS.ENHANCED_STATUS);
|
||||
const enhancedCode = enhancedMatch ? enhancedMatch[1] : undefined;
|
||||
|
||||
return {
|
||||
code,
|
||||
message: enhancedCode ? message.substring(enhancedCode.length + 1) : message,
|
||||
enhancedCode,
|
||||
raw: data
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse EHLO response and extract capabilities
|
||||
*/
|
||||
export function parseEhloResponse(response: string): ISmtpCapabilities {
|
||||
const lines = response.trim().split(/\r?\n/);
|
||||
const capabilities: ISmtpCapabilities = {
|
||||
extensions: new Set(),
|
||||
authMethods: new Set(),
|
||||
pipelining: false,
|
||||
starttls: false,
|
||||
eightBitMime: false
|
||||
};
|
||||
|
||||
for (const line of lines.slice(1)) { // Skip first line (greeting)
|
||||
const extensionLine = line.substring(4); // Remove "250-" or "250 "
|
||||
const parts = extensionLine.split(/\s+/);
|
||||
const extension = parts[0].toUpperCase();
|
||||
|
||||
capabilities.extensions.add(extension);
|
||||
|
||||
switch (extension) {
|
||||
case 'PIPELINING':
|
||||
capabilities.pipelining = true;
|
||||
break;
|
||||
case 'STARTTLS':
|
||||
capabilities.starttls = true;
|
||||
break;
|
||||
case '8BITMIME':
|
||||
capabilities.eightBitMime = true;
|
||||
break;
|
||||
case 'SIZE':
|
||||
if (parts[1]) {
|
||||
capabilities.maxSize = parseInt(parts[1], 10);
|
||||
}
|
||||
break;
|
||||
case 'AUTH':
|
||||
// Parse authentication methods
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
capabilities.authMethods.add(parts[i].toUpperCase());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format SMTP command with proper line ending
|
||||
*/
|
||||
export function formatCommand(command: string, ...args: string[]): string {
|
||||
const fullCommand = args.length > 0 ? `${command} ${args.join(' ')}` : command;
|
||||
return fullCommand + LINE_ENDINGS.CRLF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode authentication string for AUTH PLAIN
|
||||
*/
|
||||
export function encodeAuthPlain(username: string, password: string): string {
|
||||
const authString = `\0${username}\0${password}`;
|
||||
return Buffer.from(authString, 'utf8').toString('base64');
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode authentication string for AUTH LOGIN
|
||||
*/
|
||||
export function encodeAuthLogin(value: string): string {
|
||||
return Buffer.from(value, 'utf8').toString('base64');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate OAuth2 authentication string
|
||||
*/
|
||||
export function generateOAuth2String(username: string, accessToken: string): string {
|
||||
const authString = `user=${username}\x01auth=Bearer ${accessToken}\x01\x01`;
|
||||
return Buffer.from(authString, 'utf8').toString('base64');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if response code indicates success
|
||||
*/
|
||||
export function isSuccessCode(code: number): boolean {
|
||||
return code >= 200 && code < 300;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if response code indicates temporary failure
|
||||
*/
|
||||
export function isTemporaryFailure(code: number): boolean {
|
||||
return code >= 400 && code < 500;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if response code indicates permanent failure
|
||||
*/
|
||||
export function isPermanentFailure(code: number): boolean {
|
||||
return code >= 500;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape email address for SMTP commands
|
||||
*/
|
||||
export function escapeEmailAddress(email: string): string {
|
||||
return `<${email.trim()}>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract email address from angle brackets
|
||||
*/
|
||||
export function extractEmailAddress(email: string): string {
|
||||
const match = email.match(/^<(.+)>$/);
|
||||
return match ? match[1] : email.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique connection ID
|
||||
*/
|
||||
export function generateConnectionId(): string {
|
||||
return `smtp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format timeout duration for human readability
|
||||
*/
|
||||
export function formatTimeout(milliseconds: number): string {
|
||||
if (milliseconds < 1000) {
|
||||
return `${milliseconds}ms`;
|
||||
} else if (milliseconds < 60000) {
|
||||
return `${Math.round(milliseconds / 1000)}s`;
|
||||
} else {
|
||||
return `${Math.round(milliseconds / 60000)}m`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and normalize email data size
|
||||
*/
|
||||
export function validateEmailSize(emailData: string, maxSize?: number): boolean {
|
||||
const size = Buffer.byteLength(emailData, 'utf8');
|
||||
return !maxSize || size <= maxSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean sensitive data from logs
|
||||
*/
|
||||
export function sanitizeForLogging(data: any): any {
|
||||
if (typeof data !== 'object' || data === null) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const sanitized = { ...data };
|
||||
const sensitiveFields = ['password', 'pass', 'accessToken', 'refreshToken', 'clientSecret'];
|
||||
|
||||
for (const field of sensitiveFields) {
|
||||
if (field in sanitized) {
|
||||
sanitized[field] = '[REDACTED]';
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate exponential backoff delay
|
||||
*/
|
||||
export function calculateBackoffDelay(attempt: number, baseDelay: number = 1000): number {
|
||||
return Math.min(baseDelay * Math.pow(2, attempt - 1), 30000); // Max 30 seconds
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse enhanced status code
|
||||
*/
|
||||
export function parseEnhancedStatusCode(code: string): { class: number; subject: number; detail: number } | null {
|
||||
const match = code.match(/^(\d)\.(\d)\.(\d)$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
class: parseInt(match[1], 10),
|
||||
subject: parseInt(match[2], 10),
|
||||
detail: parseInt(match[3], 10)
|
||||
};
|
||||
}
|
212
ts/mail/delivery/smtpclient/utils/logging.ts
Normal file
212
ts/mail/delivery/smtpclient/utils/logging.ts
Normal file
@ -0,0 +1,212 @@
|
||||
/**
|
||||
* SMTP Client Logging Utilities
|
||||
* Client-side logging utilities for SMTP operations
|
||||
*/
|
||||
|
||||
import { logger } from '../../../../logger.js';
|
||||
import type { ISmtpResponse, ISmtpClientOptions } from '../interfaces.js';
|
||||
|
||||
export interface ISmtpClientLogData {
|
||||
component: string;
|
||||
host?: string;
|
||||
port?: number;
|
||||
secure?: boolean;
|
||||
command?: string;
|
||||
response?: ISmtpResponse;
|
||||
error?: Error;
|
||||
connectionId?: string;
|
||||
messageId?: string;
|
||||
duration?: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log SMTP client connection events
|
||||
*/
|
||||
export function logConnection(
|
||||
event: 'connecting' | 'connected' | 'disconnected' | 'error',
|
||||
options: ISmtpClientOptions,
|
||||
data?: Partial<ISmtpClientLogData>
|
||||
): void {
|
||||
const logData: ISmtpClientLogData = {
|
||||
component: 'smtp-client',
|
||||
event,
|
||||
host: options.host,
|
||||
port: options.port,
|
||||
secure: options.secure,
|
||||
...data
|
||||
};
|
||||
|
||||
switch (event) {
|
||||
case 'connecting':
|
||||
logger.info('SMTP client connecting', logData);
|
||||
break;
|
||||
case 'connected':
|
||||
logger.info('SMTP client connected', logData);
|
||||
break;
|
||||
case 'disconnected':
|
||||
logger.info('SMTP client disconnected', logData);
|
||||
break;
|
||||
case 'error':
|
||||
logger.error('SMTP client connection error', logData);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log SMTP command execution
|
||||
*/
|
||||
export function logCommand(
|
||||
command: string,
|
||||
response?: ISmtpResponse,
|
||||
options?: ISmtpClientOptions,
|
||||
data?: Partial<ISmtpClientLogData>
|
||||
): void {
|
||||
const logData: ISmtpClientLogData = {
|
||||
component: 'smtp-client',
|
||||
command,
|
||||
response,
|
||||
host: options?.host,
|
||||
port: options?.port,
|
||||
...data
|
||||
};
|
||||
|
||||
if (response && response.code >= 400) {
|
||||
logger.warn('SMTP command failed', logData);
|
||||
} else {
|
||||
logger.debug('SMTP command executed', logData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log authentication events
|
||||
*/
|
||||
export function logAuthentication(
|
||||
event: 'start' | 'success' | 'failure',
|
||||
method: string,
|
||||
options: ISmtpClientOptions,
|
||||
data?: Partial<ISmtpClientLogData>
|
||||
): void {
|
||||
const logData: ISmtpClientLogData = {
|
||||
component: 'smtp-client',
|
||||
event: `auth_${event}`,
|
||||
authMethod: method,
|
||||
host: options.host,
|
||||
port: options.port,
|
||||
...data
|
||||
};
|
||||
|
||||
switch (event) {
|
||||
case 'start':
|
||||
logger.debug('SMTP authentication started', logData);
|
||||
break;
|
||||
case 'success':
|
||||
logger.info('SMTP authentication successful', logData);
|
||||
break;
|
||||
case 'failure':
|
||||
logger.error('SMTP authentication failed', logData);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log TLS/STARTTLS events
|
||||
*/
|
||||
export function logTLS(
|
||||
event: 'starttls_start' | 'starttls_success' | 'starttls_failure' | 'tls_connected',
|
||||
options: ISmtpClientOptions,
|
||||
data?: Partial<ISmtpClientLogData>
|
||||
): void {
|
||||
const logData: ISmtpClientLogData = {
|
||||
component: 'smtp-client',
|
||||
event,
|
||||
host: options.host,
|
||||
port: options.port,
|
||||
...data
|
||||
};
|
||||
|
||||
if (event.includes('failure')) {
|
||||
logger.error('SMTP TLS operation failed', logData);
|
||||
} else {
|
||||
logger.info('SMTP TLS operation', logData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log email sending events
|
||||
*/
|
||||
export function logEmailSend(
|
||||
event: 'start' | 'success' | 'failure',
|
||||
recipients: string[],
|
||||
options: ISmtpClientOptions,
|
||||
data?: Partial<ISmtpClientLogData>
|
||||
): void {
|
||||
const logData: ISmtpClientLogData = {
|
||||
component: 'smtp-client',
|
||||
event: `send_${event}`,
|
||||
recipientCount: recipients.length,
|
||||
recipients: recipients.slice(0, 5), // Only log first 5 recipients for privacy
|
||||
host: options.host,
|
||||
port: options.port,
|
||||
...data
|
||||
};
|
||||
|
||||
switch (event) {
|
||||
case 'start':
|
||||
logger.info('SMTP email send started', logData);
|
||||
break;
|
||||
case 'success':
|
||||
logger.info('SMTP email send successful', logData);
|
||||
break;
|
||||
case 'failure':
|
||||
logger.error('SMTP email send failed', logData);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log performance metrics
|
||||
*/
|
||||
export function logPerformance(
|
||||
operation: string,
|
||||
duration: number,
|
||||
options: ISmtpClientOptions,
|
||||
data?: Partial<ISmtpClientLogData>
|
||||
): void {
|
||||
const logData: ISmtpClientLogData = {
|
||||
component: 'smtp-client',
|
||||
operation,
|
||||
duration,
|
||||
host: options.host,
|
||||
port: options.port,
|
||||
...data
|
||||
};
|
||||
|
||||
if (duration > 10000) { // Log slow operations (>10s)
|
||||
logger.warn('SMTP slow operation detected', logData);
|
||||
} else {
|
||||
logger.debug('SMTP operation performance', logData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log debug information (only when debug is enabled)
|
||||
*/
|
||||
export function logDebug(
|
||||
message: string,
|
||||
options: ISmtpClientOptions,
|
||||
data?: Partial<ISmtpClientLogData>
|
||||
): void {
|
||||
if (!options.debug) {
|
||||
return;
|
||||
}
|
||||
|
||||
const logData: ISmtpClientLogData = {
|
||||
component: 'smtp-client-debug',
|
||||
host: options.host,
|
||||
port: options.port,
|
||||
...data
|
||||
};
|
||||
|
||||
logger.debug(`[SMTP Client Debug] ${message}`, logData);
|
||||
}
|
154
ts/mail/delivery/smtpclient/utils/validation.ts
Normal file
154
ts/mail/delivery/smtpclient/utils/validation.ts
Normal file
@ -0,0 +1,154 @@
|
||||
/**
|
||||
* SMTP Client Validation Utilities
|
||||
* Input validation functions for SMTP client operations
|
||||
*/
|
||||
|
||||
import { REGEX_PATTERNS } from '../constants.js';
|
||||
import type { ISmtpClientOptions, ISmtpAuthOptions } from '../interfaces.js';
|
||||
|
||||
/**
|
||||
* Validate email address format
|
||||
*/
|
||||
export function validateEmailAddress(email: string): boolean {
|
||||
if (!email || typeof email !== 'string') {
|
||||
return false;
|
||||
}
|
||||
return REGEX_PATTERNS.EMAIL_ADDRESS.test(email.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate SMTP client options
|
||||
*/
|
||||
export function validateClientOptions(options: ISmtpClientOptions): string[] {
|
||||
const errors: string[] = [];
|
||||
|
||||
// Required fields
|
||||
if (!options.host || typeof options.host !== 'string') {
|
||||
errors.push('Host is required and must be a string');
|
||||
}
|
||||
|
||||
if (!options.port || typeof options.port !== 'number' || options.port < 1 || options.port > 65535) {
|
||||
errors.push('Port must be a number between 1 and 65535');
|
||||
}
|
||||
|
||||
// Optional field validation
|
||||
if (options.connectionTimeout !== undefined) {
|
||||
if (typeof options.connectionTimeout !== 'number' || options.connectionTimeout < 1000) {
|
||||
errors.push('Connection timeout must be a number >= 1000ms');
|
||||
}
|
||||
}
|
||||
|
||||
if (options.socketTimeout !== undefined) {
|
||||
if (typeof options.socketTimeout !== 'number' || options.socketTimeout < 1000) {
|
||||
errors.push('Socket timeout must be a number >= 1000ms');
|
||||
}
|
||||
}
|
||||
|
||||
if (options.maxConnections !== undefined) {
|
||||
if (typeof options.maxConnections !== 'number' || options.maxConnections < 1) {
|
||||
errors.push('Max connections must be a positive number');
|
||||
}
|
||||
}
|
||||
|
||||
if (options.maxMessages !== undefined) {
|
||||
if (typeof options.maxMessages !== 'number' || options.maxMessages < 1) {
|
||||
errors.push('Max messages must be a positive number');
|
||||
}
|
||||
}
|
||||
|
||||
// Validate authentication options
|
||||
if (options.auth) {
|
||||
errors.push(...validateAuthOptions(options.auth));
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate authentication options
|
||||
*/
|
||||
export function validateAuthOptions(auth: ISmtpAuthOptions): string[] {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (auth.method && !['PLAIN', 'LOGIN', 'OAUTH2', 'AUTO'].includes(auth.method)) {
|
||||
errors.push('Invalid authentication method');
|
||||
}
|
||||
|
||||
// For basic auth, require user and pass
|
||||
if ((auth.user || auth.pass) && (!auth.user || !auth.pass)) {
|
||||
errors.push('Both user and pass are required for basic authentication');
|
||||
}
|
||||
|
||||
// For OAuth2, validate required fields
|
||||
if (auth.oauth2) {
|
||||
const oauth = auth.oauth2;
|
||||
if (!oauth.user || !oauth.clientId || !oauth.clientSecret || !oauth.refreshToken) {
|
||||
errors.push('OAuth2 requires user, clientId, clientSecret, and refreshToken');
|
||||
}
|
||||
|
||||
if (oauth.user && !validateEmailAddress(oauth.user)) {
|
||||
errors.push('OAuth2 user must be a valid email address');
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate hostname format
|
||||
*/
|
||||
export function validateHostname(hostname: string): boolean {
|
||||
if (!hostname || typeof hostname !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Basic hostname validation (allow IP addresses and domain names)
|
||||
const hostnameRegex = /^(?:[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])?$|^(?:\d{1,3}\.){3}\d{1,3}$|^\[(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\]$/;
|
||||
return hostnameRegex.test(hostname);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate port number
|
||||
*/
|
||||
export function validatePort(port: number): boolean {
|
||||
return typeof port === 'number' && port >= 1 && port <= 65535;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize and validate domain name for EHLO
|
||||
*/
|
||||
export function validateAndSanitizeDomain(domain: string): string {
|
||||
if (!domain || typeof domain !== 'string') {
|
||||
return 'localhost';
|
||||
}
|
||||
|
||||
const sanitized = domain.trim().toLowerCase();
|
||||
if (validateHostname(sanitized)) {
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
return 'localhost';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate recipient list
|
||||
*/
|
||||
export function validateRecipients(recipients: string | string[]): string[] {
|
||||
const errors: string[] = [];
|
||||
const recipientList = Array.isArray(recipients) ? recipients : [recipients];
|
||||
|
||||
for (const recipient of recipientList) {
|
||||
if (!validateEmailAddress(recipient)) {
|
||||
errors.push(`Invalid email address: ${recipient}`);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate sender address
|
||||
*/
|
||||
export function validateSender(sender: string): boolean {
|
||||
return validateEmailAddress(sender);
|
||||
}
|
Reference in New Issue
Block a user