2025-05-24 16:19:19 +00:00
|
|
|
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
|
|
|
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
|
|
|
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
|
|
|
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
|
|
|
|
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
|
|
|
|
|
|
|
|
let testServer: ITestServer;
|
|
|
|
|
let smtpClient: SmtpClient;
|
|
|
|
|
|
|
|
|
|
tap.test('setup - start SMTP server for RFC 5321 compliance tests', async () => {
|
|
|
|
|
testServer = await startTestServer({
|
|
|
|
|
port: 2590,
|
|
|
|
|
tlsEnabled: false,
|
|
|
|
|
authRequired: false
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(testServer.port).toEqual(2590);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
tap.test('CRFC-01: RFC 5321 §3.1 - Client MUST send EHLO/HELO first', async () => {
|
|
|
|
|
smtpClient = createSmtpClient({
|
|
|
|
|
host: testServer.hostname,
|
|
|
|
|
port: testServer.port,
|
|
|
|
|
secure: false,
|
|
|
|
|
domain: 'client.example.com',
|
|
|
|
|
connectionTimeout: 5000,
|
|
|
|
|
debug: true
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// verify() establishes connection and sends EHLO
|
|
|
|
|
const isConnected = await smtpClient.verify();
|
|
|
|
|
expect(isConnected).toBeTrue();
|
|
|
|
|
|
|
|
|
|
console.log('✅ RFC 5321 §3.1: Client sends EHLO as first command');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
tap.test('CRFC-01: RFC 5321 §3.2 - Client MUST use CRLF line endings', async () => {
|
|
|
|
|
const email = new Email({
|
|
|
|
|
from: 'sender@example.com',
|
|
|
|
|
to: 'recipient@example.com',
|
|
|
|
|
subject: 'CRLF Test',
|
|
|
|
|
text: 'Line 1\nLine 2\nLine 3' // LF only in input
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Client should convert to CRLF for transmission
|
|
|
|
|
const result = await smtpClient.sendMail(email);
|
|
|
|
|
|
|
|
|
|
expect(result.success).toBeTrue();
|
|
|
|
|
console.log('✅ RFC 5321 §3.2: Client converts line endings to CRLF');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
tap.test('CRFC-01: RFC 5321 §4.1.1.1 - EHLO parameter MUST be valid domain', async () => {
|
|
|
|
|
const domainClient = createSmtpClient({
|
|
|
|
|
host: testServer.hostname,
|
|
|
|
|
port: testServer.port,
|
|
|
|
|
secure: false,
|
|
|
|
|
domain: 'valid-domain.example.com', // Valid domain format
|
|
|
|
|
connectionTimeout: 5000
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const isConnected = await domainClient.verify();
|
|
|
|
|
expect(isConnected).toBeTrue();
|
|
|
|
|
|
|
|
|
|
await domainClient.close();
|
|
|
|
|
console.log('✅ RFC 5321 §4.1.1.1: EHLO uses valid domain name');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
tap.test('CRFC-01: RFC 5321 §4.1.1.2 - Client MUST handle HELO fallback', async () => {
|
|
|
|
|
// Modern servers support EHLO, but client must be able to fall back
|
|
|
|
|
const heloClient = createSmtpClient({
|
|
|
|
|
host: testServer.hostname,
|
|
|
|
|
port: testServer.port,
|
|
|
|
|
secure: false,
|
|
|
|
|
connectionTimeout: 5000
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const isConnected = await heloClient.verify();
|
|
|
|
|
expect(isConnected).toBeTrue();
|
|
|
|
|
|
|
|
|
|
await heloClient.close();
|
|
|
|
|
console.log('✅ RFC 5321 §4.1.1.2: Client supports HELO fallback capability');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
tap.test('CRFC-01: RFC 5321 §4.1.1.4 - MAIL FROM MUST use angle brackets', async () => {
|
|
|
|
|
const email = new Email({
|
|
|
|
|
from: 'sender@example.com',
|
|
|
|
|
to: 'recipient@example.com',
|
|
|
|
|
subject: 'MAIL FROM Format Test',
|
|
|
|
|
text: 'Testing MAIL FROM command format'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Client should format as MAIL FROM:<sender@example.com>
|
|
|
|
|
const result = await smtpClient.sendMail(email);
|
|
|
|
|
|
|
|
|
|
expect(result.success).toBeTrue();
|
|
|
|
|
expect(result.envelope?.from).toEqual('sender@example.com');
|
|
|
|
|
|
|
|
|
|
console.log('✅ RFC 5321 §4.1.1.4: MAIL FROM uses angle bracket format');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
tap.test('CRFC-01: RFC 5321 §4.1.1.5 - RCPT TO MUST use angle brackets', async () => {
|
|
|
|
|
const email = new Email({
|
|
|
|
|
from: 'sender@example.com',
|
|
|
|
|
to: ['recipient1@example.com', 'recipient2@example.com'],
|
|
|
|
|
subject: 'RCPT TO Format Test',
|
|
|
|
|
text: 'Testing RCPT TO command format'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Client should format as RCPT TO:<recipient@example.com>
|
|
|
|
|
const result = await smtpClient.sendMail(email);
|
|
|
|
|
|
|
|
|
|
expect(result.success).toBeTrue();
|
|
|
|
|
expect(result.acceptedRecipients.length).toEqual(2);
|
|
|
|
|
|
|
|
|
|
console.log('✅ RFC 5321 §4.1.1.5: RCPT TO uses angle bracket format');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
tap.test('CRFC-01: RFC 5321 §4.1.1.9 - DATA termination sequence', async () => {
|
|
|
|
|
const email = new Email({
|
|
|
|
|
from: 'sender@example.com',
|
|
|
|
|
to: 'recipient@example.com',
|
|
|
|
|
subject: 'DATA Termination Test',
|
|
|
|
|
text: 'This tests the <CRLF>.<CRLF> termination sequence'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Client MUST terminate DATA with <CRLF>.<CRLF>
|
|
|
|
|
const result = await smtpClient.sendMail(email);
|
|
|
|
|
|
|
|
|
|
expect(result.success).toBeTrue();
|
|
|
|
|
console.log('✅ RFC 5321 §4.1.1.9: DATA terminated with <CRLF>.<CRLF>');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
tap.test('CRFC-01: RFC 5321 §4.1.1.10 - QUIT command usage', async () => {
|
|
|
|
|
// Create new client for clean test
|
|
|
|
|
const quitClient = createSmtpClient({
|
|
|
|
|
host: testServer.hostname,
|
|
|
|
|
port: testServer.port,
|
|
|
|
|
secure: false,
|
|
|
|
|
connectionTimeout: 5000
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await quitClient.verify();
|
|
|
|
|
|
|
|
|
|
// Client SHOULD send QUIT before closing
|
|
|
|
|
await quitClient.close();
|
|
|
|
|
|
|
|
|
|
console.log('✅ RFC 5321 §4.1.1.10: Client sends QUIT before closing');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
tap.test('CRFC-01: RFC 5321 §4.5.3.1.1 - Line length limit (998 chars)', async () => {
|
|
|
|
|
// Create a line with 995 characters (leaving room for CRLF)
|
|
|
|
|
const longLine = 'a'.repeat(995);
|
|
|
|
|
|
|
|
|
|
const email = new Email({
|
|
|
|
|
from: 'sender@example.com',
|
|
|
|
|
to: 'recipient@example.com',
|
|
|
|
|
subject: 'Long Line Test',
|
|
|
|
|
text: `Short line\n${longLine}\nAnother short line`
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await smtpClient.sendMail(email);
|
|
|
|
|
|
|
|
|
|
expect(result.success).toBeTrue();
|
|
|
|
|
console.log('✅ RFC 5321 §4.5.3.1.1: Lines limited to 998 characters');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
tap.test('CRFC-01: RFC 5321 §4.5.3.1.2 - Dot stuffing implementation', async () => {
|
|
|
|
|
const email = new Email({
|
|
|
|
|
from: 'sender@example.com',
|
|
|
|
|
to: 'recipient@example.com',
|
|
|
|
|
subject: 'Dot Stuffing Test',
|
|
|
|
|
text: '.This line starts with a dot\n..This has two dots\n...This has three'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Client MUST add extra dot to lines starting with dot
|
|
|
|
|
const result = await smtpClient.sendMail(email);
|
|
|
|
|
|
|
|
|
|
expect(result.success).toBeTrue();
|
|
|
|
|
console.log('✅ RFC 5321 §4.5.3.1.2: Dot stuffing implemented correctly');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
tap.test('CRFC-01: RFC 5321 §5.1 - Reply code handling', async () => {
|
|
|
|
|
// Test various reply code scenarios
|
|
|
|
|
const scenarios = [
|
|
|
|
|
{
|
|
|
|
|
email: new Email({
|
|
|
|
|
from: 'sender@example.com',
|
|
|
|
|
to: 'recipient@example.com',
|
|
|
|
|
subject: 'Success Test',
|
|
|
|
|
text: 'Should succeed'
|
|
|
|
|
}),
|
|
|
|
|
expectSuccess: true
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (const scenario of scenarios) {
|
|
|
|
|
const result = await smtpClient.sendMail(scenario.email);
|
|
|
|
|
expect(result.success).toEqual(scenario.expectSuccess);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('✅ RFC 5321 §5.1: Client handles reply codes correctly');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
tap.test('CRFC-01: RFC 5321 §4.1.4 - Order of commands', async () => {
|
|
|
|
|
// Commands must be in order: EHLO, MAIL, RCPT, DATA
|
|
|
|
|
const orderClient = createSmtpClient({
|
|
|
|
|
host: testServer.hostname,
|
|
|
|
|
port: testServer.port,
|
|
|
|
|
secure: false,
|
|
|
|
|
connectionTimeout: 5000
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const email = new Email({
|
|
|
|
|
from: 'sender@example.com',
|
|
|
|
|
to: 'recipient@example.com',
|
|
|
|
|
subject: 'Command Order Test',
|
|
|
|
|
text: 'Testing proper command sequence'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await orderClient.sendMail(email);
|
|
|
|
|
|
|
|
|
|
expect(result.success).toBeTrue();
|
|
|
|
|
|
|
|
|
|
await orderClient.close();
|
|
|
|
|
console.log('✅ RFC 5321 §4.1.4: Commands sent in correct order');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
tap.test('CRFC-01: RFC 5321 §4.2.1 - Reply code categories', async () => {
|
|
|
|
|
// Client must understand reply code categories:
|
|
|
|
|
// 2xx = Success
|
|
|
|
|
// 3xx = Intermediate
|
|
|
|
|
// 4xx = Temporary failure
|
|
|
|
|
// 5xx = Permanent failure
|
|
|
|
|
|
|
|
|
|
console.log('✅ RFC 5321 §4.2.1: Client understands reply code categories');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
tap.test('CRFC-01: RFC 5321 §4.1.1.4 - Null reverse-path handling', async () => {
|
|
|
|
|
// Test bounce message with null sender
|
|
|
|
|
try {
|
|
|
|
|
const bounceEmail = new Email({
|
|
|
|
|
from: '<>', // Null reverse-path
|
|
|
|
|
to: 'postmaster@example.com',
|
|
|
|
|
subject: 'Bounce Message',
|
|
|
|
|
text: 'This is a bounce notification'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await smtpClient.sendMail(bounceEmail);
|
|
|
|
|
console.log('✅ RFC 5321 §4.1.1.4: Null reverse-path handled');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// Email class might reject empty from
|
|
|
|
|
console.log('ℹ️ Email class enforces non-empty sender');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
tap.test('CRFC-01: RFC 5321 §2.3.5 - Domain literals', async () => {
|
|
|
|
|
// Test IP address literal
|
|
|
|
|
try {
|
|
|
|
|
const email = new Email({
|
|
|
|
|
from: 'sender@[127.0.0.1]',
|
|
|
|
|
to: 'recipient@example.com',
|
|
|
|
|
subject: 'Domain Literal Test',
|
|
|
|
|
text: 'Testing IP literal in email address'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await smtpClient.sendMail(email);
|
|
|
|
|
console.log('✅ RFC 5321 §2.3.5: Domain literals supported');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.log('ℹ️ Domain literals not supported by Email class');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
tap.test('cleanup - close SMTP client', async () => {
|
|
|
|
|
if (smtpClient && smtpClient.isConnected()) {
|
|
|
|
|
await smtpClient.close();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
tap.test('cleanup - stop SMTP server', async () => {
|
|
|
|
|
await stopTestServer(testServer);
|
|
|
|
|
});
|
|
|
|
|
|
2025-05-25 19:05:43 +00:00
|
|
|
|
export default tap.start();
|