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)
|
||||
};
|
||||
}
|
Reference in New Issue
Block a user