feat(storage): add comprehensive tests for StorageManager with memory, filesystem, and custom function backends
feat(email): implement EmailSendJob class for robust email delivery with retry logic and MX record resolution feat(mail): restructure mail module exports for simplified access to core and delivery functionalities
This commit is contained in:
@@ -0,0 +1,438 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.ts';
|
||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.ts';
|
||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.ts';
|
||||
import { Email } from '../../../ts/mail/core/classes.email.ts';
|
||||
import * as net from 'net';
|
||||
|
||||
let testServer: ITestServer;
|
||||
|
||||
tap.test('setup test SMTP server', async () => {
|
||||
testServer = await startTestServer({
|
||||
port: 2571,
|
||||
tlsEnabled: false,
|
||||
authRequired: false
|
||||
});
|
||||
expect(testServer).toBeTruthy();
|
||||
expect(testServer.port).toEqual(2571);
|
||||
});
|
||||
|
||||
tap.test('CEDGE-02: Commands with extra spaces', async () => {
|
||||
// Create server that accepts commands with extra spaces
|
||||
const spaceyServer = net.createServer((socket) => {
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
let inData = false;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const lines = data.toString().split('\r\n');
|
||||
|
||||
lines.forEach(line => {
|
||||
if (!line && lines[lines.length - 1] === '') return; // Skip empty trailing line
|
||||
|
||||
console.log(`Server received: "${line}"`);
|
||||
|
||||
if (inData) {
|
||||
if (line === '.') {
|
||||
socket.write('250 Message accepted\r\n');
|
||||
inData = false;
|
||||
}
|
||||
// Otherwise it's email data, ignore
|
||||
} else if (line.match(/^EHLO\s+/i)) {
|
||||
socket.write('250-mail.example.com\r\n');
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line.match(/^MAIL\s+FROM:/i)) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line.match(/^RCPT\s+TO:/i)) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
inData = true;
|
||||
} else if (line === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
} else if (line) {
|
||||
socket.write('500 Command not recognized\r\n');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
spaceyServer.listen(0, '127.0.0.1', () => resolve());
|
||||
});
|
||||
|
||||
const spaceyPort = (spaceyServer.address() as net.AddressInfo).port;
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: spaceyPort,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
const verified = await smtpClient.verify();
|
||||
expect(verified).toBeTrue();
|
||||
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Test with extra spaces',
|
||||
text: 'Testing command formatting'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
expect(result.success).toBeTrue();
|
||||
console.log('✅ Server handled commands with extra spaces');
|
||||
|
||||
await smtpClient.close();
|
||||
spaceyServer.close();
|
||||
});
|
||||
|
||||
tap.test('CEDGE-02: Mixed case commands', async () => {
|
||||
// Create server that accepts mixed case commands
|
||||
const mixedCaseServer = net.createServer((socket) => {
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
let inData = false;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const lines = data.toString().split('\r\n');
|
||||
|
||||
lines.forEach(line => {
|
||||
if (!line && lines[lines.length - 1] === '') return;
|
||||
|
||||
const upperLine = line.toUpperCase();
|
||||
console.log(`Server received: "${line}"`);
|
||||
|
||||
if (inData) {
|
||||
if (line === '.') {
|
||||
socket.write('250 Message accepted\r\n');
|
||||
inData = false;
|
||||
}
|
||||
} else if (upperLine.startsWith('EHLO')) {
|
||||
socket.write('250-mail.example.com\r\n');
|
||||
socket.write('250 8BITMIME\r\n');
|
||||
} else if (upperLine.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (upperLine.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (upperLine === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
inData = true;
|
||||
} else if (upperLine === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
mixedCaseServer.listen(0, '127.0.0.1', () => resolve());
|
||||
});
|
||||
|
||||
const mixedPort = (mixedCaseServer.address() as net.AddressInfo).port;
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: mixedPort,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
const verified = await smtpClient.verify();
|
||||
expect(verified).toBeTrue();
|
||||
console.log('✅ Server accepts mixed case commands');
|
||||
|
||||
await smtpClient.close();
|
||||
mixedCaseServer.close();
|
||||
});
|
||||
|
||||
tap.test('CEDGE-02: Commands with missing parameters', async () => {
|
||||
// Create server that handles incomplete commands
|
||||
const incompleteServer = net.createServer((socket) => {
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const lines = data.toString().split('\r\n');
|
||||
|
||||
lines.forEach(line => {
|
||||
if (!line && lines[lines.length - 1] === '') return;
|
||||
|
||||
console.log(`Server received: "${line}"`);
|
||||
|
||||
if (line.startsWith('EHLO')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line === 'MAIL FROM:' || line === 'MAIL FROM') {
|
||||
// Missing email address
|
||||
socket.write('501 Syntax error in parameters\r\n');
|
||||
} else if (line === 'RCPT TO:' || line === 'RCPT TO') {
|
||||
// Missing recipient
|
||||
socket.write('501 Syntax error in parameters\r\n');
|
||||
} else if (line === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
} else if (line) {
|
||||
socket.write('500 Command not recognized\r\n');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
incompleteServer.listen(0, '127.0.0.1', () => resolve());
|
||||
});
|
||||
|
||||
const incompletePort = (incompleteServer.address() as net.AddressInfo).port;
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: incompletePort,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// This should succeed as the client sends proper commands
|
||||
const verified = await smtpClient.verify();
|
||||
expect(verified).toBeTrue();
|
||||
console.log('✅ Client sends properly formatted commands');
|
||||
|
||||
await smtpClient.close();
|
||||
incompleteServer.close();
|
||||
});
|
||||
|
||||
tap.test('CEDGE-02: Commands with extra parameters', async () => {
|
||||
// Create server that handles commands with extra parameters
|
||||
const extraParamsServer = net.createServer((socket) => {
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
let inData = false;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const lines = data.toString().split('\r\n');
|
||||
|
||||
lines.forEach(line => {
|
||||
if (!line && lines[lines.length - 1] === '') return;
|
||||
|
||||
console.log(`Server received: "${line}"`);
|
||||
|
||||
if (inData) {
|
||||
if (line === '.') {
|
||||
socket.write('250 Message accepted\r\n');
|
||||
inData = false;
|
||||
}
|
||||
} else if (line.startsWith('EHLO')) {
|
||||
// Accept EHLO with any parameter
|
||||
socket.write('250-mail.example.com\r\n');
|
||||
socket.write('250-SIZE 10240000\r\n');
|
||||
socket.write('250 8BITMIME\r\n');
|
||||
} else if (line.match(/^MAIL FROM:.*SIZE=/i)) {
|
||||
// Accept SIZE parameter
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
inData = true;
|
||||
} else if (line === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
extraParamsServer.listen(0, '127.0.0.1', () => resolve());
|
||||
});
|
||||
|
||||
const extraPort = (extraParamsServer.address() as net.AddressInfo).port;
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: extraPort,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Test with parameters',
|
||||
text: 'Testing extra parameters'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
expect(result.success).toBeTrue();
|
||||
console.log('✅ Server handled commands with extra parameters');
|
||||
|
||||
await smtpClient.close();
|
||||
extraParamsServer.close();
|
||||
});
|
||||
|
||||
tap.test('CEDGE-02: Invalid command sequences', async () => {
|
||||
// Create server that enforces command sequence
|
||||
const sequenceServer = net.createServer((socket) => {
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
let state = 'GREETING';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const lines = data.toString().split('\r\n');
|
||||
|
||||
lines.forEach(line => {
|
||||
if (!line && lines[lines.length - 1] === '') return;
|
||||
|
||||
console.log(`Server received: "${line}" in state ${state}`);
|
||||
|
||||
if (state === 'DATA' && line !== '.') {
|
||||
// In DATA state, ignore everything except the terminating period
|
||||
return;
|
||||
}
|
||||
|
||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
||||
state = 'READY';
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line.startsWith('MAIL FROM:')) {
|
||||
if (state !== 'READY') {
|
||||
socket.write('503 Bad sequence of commands\r\n');
|
||||
} else {
|
||||
state = 'MAIL';
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (line.startsWith('RCPT TO:')) {
|
||||
if (state !== 'MAIL' && state !== 'RCPT') {
|
||||
socket.write('503 Bad sequence of commands\r\n');
|
||||
} else {
|
||||
state = 'RCPT';
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
} else if (line === 'DATA') {
|
||||
if (state !== 'RCPT') {
|
||||
socket.write('503 Bad sequence of commands\r\n');
|
||||
} else {
|
||||
state = 'DATA';
|
||||
socket.write('354 Start mail input\r\n');
|
||||
}
|
||||
} else if (line === '.' && state === 'DATA') {
|
||||
state = 'READY';
|
||||
socket.write('250 Message accepted\r\n');
|
||||
} else if (line === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
} else if (line === 'RSET') {
|
||||
state = 'READY';
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
sequenceServer.listen(0, '127.0.0.1', () => resolve());
|
||||
});
|
||||
|
||||
const sequencePort = (sequenceServer.address() as net.AddressInfo).port;
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: sequencePort,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Client should handle proper command sequencing
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Test sequence',
|
||||
text: 'Testing command sequence'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
expect(result.success).toBeTrue();
|
||||
console.log('✅ Client maintains proper command sequence');
|
||||
|
||||
await smtpClient.close();
|
||||
sequenceServer.close();
|
||||
});
|
||||
|
||||
tap.test('CEDGE-02: Malformed email addresses', async () => {
|
||||
// Test how client handles various email formats
|
||||
const emailServer = net.createServer((socket) => {
|
||||
socket.write('220 mail.example.com ESMTP\r\n');
|
||||
let inData = false;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const lines = data.toString().split('\r\n');
|
||||
|
||||
lines.forEach(line => {
|
||||
if (!line && lines[lines.length - 1] === '') return;
|
||||
|
||||
console.log(`Server received: "${line}"`);
|
||||
|
||||
if (inData) {
|
||||
if (line === '.') {
|
||||
socket.write('250 Message accepted\r\n');
|
||||
inData = false;
|
||||
}
|
||||
} else if (line.startsWith('EHLO')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line.startsWith('MAIL FROM:')) {
|
||||
// Accept any sender format
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line.startsWith('RCPT TO:')) {
|
||||
// Accept any recipient format
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line === 'DATA') {
|
||||
socket.write('354 Start mail input\r\n');
|
||||
inData = true;
|
||||
} else if (line === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
emailServer.listen(0, '127.0.0.1', () => resolve());
|
||||
});
|
||||
|
||||
const emailPort = (emailServer.address() as net.AddressInfo).port;
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: emailPort,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Test with properly formatted email
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Test email formats',
|
||||
text: 'Testing email address handling'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
expect(result.success).toBeTrue();
|
||||
console.log('✅ Client properly formats email addresses');
|
||||
|
||||
await smtpClient.close();
|
||||
emailServer.close();
|
||||
});
|
||||
|
||||
tap.test('cleanup test SMTP server', async () => {
|
||||
if (testServer) {
|
||||
await stopTestServer(testServer);
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
Reference in New Issue
Block a user