This commit is contained in:
2025-05-24 16:19:19 +00:00
parent 3d669ed9dd
commit 4e4c7df558
50 changed files with 17232 additions and 340 deletions

View File

@ -0,0 +1,166 @@
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';
let testServer: ITestServer;
let smtpClient: SmtpClient;
tap.test('setup - start SMTP server for command tests', async () => {
testServer = await startTestServer({
port: 2540,
tlsEnabled: false,
authRequired: false
});
expect(testServer.port).toEqual(2540);
});
tap.test('CCMD-01: EHLO/HELO - should send EHLO with custom domain', async () => {
const startTime = Date.now();
try {
// Create SMTP client with custom domain
smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
domain: 'mail.example.com', // Custom EHLO domain
connectionTimeout: 5000,
debug: true
});
// Verify connection (which sends EHLO)
const isConnected = await smtpClient.verify();
expect(isConnected).toBeTrue();
const duration = Date.now() - startTime;
console.log(`✅ EHLO command sent with custom domain in ${duration}ms`);
} catch (error) {
const duration = Date.now() - startTime;
console.error(`❌ EHLO command failed after ${duration}ms:`, error);
throw error;
}
});
tap.test('CCMD-01: EHLO/HELO - should use default domain when not specified', async () => {
const defaultClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
// No domain specified - should use default
connectionTimeout: 5000,
debug: true
});
const isConnected = await defaultClient.verify();
expect(isConnected).toBeTrue();
await defaultClient.close();
console.log('✅ EHLO sent with default domain');
});
tap.test('CCMD-01: EHLO/HELO - should handle international domains', async () => {
const intlClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
domain: 'mail.例え.jp', // International domain
connectionTimeout: 5000,
debug: true
});
const isConnected = await intlClient.verify();
expect(isConnected).toBeTrue();
await intlClient.close();
console.log('✅ EHLO sent with international domain');
});
tap.test('CCMD-01: EHLO/HELO - should fall back to HELO if needed', async () => {
// Most modern servers support EHLO, but client should handle HELO fallback
const heloClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
domain: 'legacy.example.com',
connectionTimeout: 5000,
debug: true
});
// The client should handle EHLO/HELO automatically
const isConnected = await heloClient.verify();
expect(isConnected).toBeTrue();
await heloClient.close();
console.log('✅ EHLO/HELO fallback mechanism working');
});
tap.test('CCMD-01: EHLO/HELO - should parse server capabilities', async () => {
const capClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await capClient.verify();
// After EHLO, client should have server capabilities
// This is internal to the client, but we can verify by attempting
// operations that depend on capabilities
const poolStatus = capClient.getPoolStatus();
expect(poolStatus.total).toBeGreaterThanOrEqual(1);
await capClient.close();
console.log('✅ Server capabilities parsed from EHLO response');
});
tap.test('CCMD-01: EHLO/HELO - should handle very long domain names', async () => {
const longDomain = 'very-long-subdomain.with-many-parts.and-labels.example.com';
const longDomainClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
domain: longDomain,
connectionTimeout: 5000
});
const isConnected = await longDomainClient.verify();
expect(isConnected).toBeTrue();
await longDomainClient.close();
console.log('✅ Long domain name handled correctly');
});
tap.test('CCMD-01: EHLO/HELO - should reconnect with EHLO after disconnect', async () => {
// First connection
await smtpClient.verify();
expect(smtpClient.isConnected()).toBeTrue();
// Close connection
await smtpClient.close();
expect(smtpClient.isConnected()).toBeFalse();
// Reconnect - should send EHLO again
const isReconnected = await smtpClient.verify();
expect(isReconnected).toBeTrue();
console.log('✅ EHLO sent correctly on reconnection');
});
tap.test('cleanup - close SMTP client', async () => {
if (smtpClient && smtpClient.isConnected()) {
await smtpClient.close();
}
});
tap.test('cleanup - stop SMTP server', async () => {
await stopTestServer(testServer);
});
tap.start();

View File

@ -0,0 +1,266 @@
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 MAIL FROM tests', async () => {
testServer = await startTestServer({
port: 2541,
tlsEnabled: false,
authRequired: false,
size: 10 * 1024 * 1024 // 10MB size limit
});
expect(testServer.port).toEqual(2541);
});
tap.test('setup - create SMTP client', async () => {
smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
const isConnected = await smtpClient.verify();
expect(isConnected).toBeTrue();
});
tap.test('CCMD-02: MAIL FROM - should send basic MAIL FROM command', async () => {
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Basic MAIL FROM Test',
text: 'Testing basic MAIL FROM command'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
expect(result.envelope?.from).toEqual('sender@example.com');
console.log('✅ Basic MAIL FROM command sent successfully');
});
tap.test('CCMD-02: MAIL FROM - should handle display names correctly', async () => {
const email = new Email({
from: 'John Doe <john.doe@example.com>',
to: 'Jane Smith <jane.smith@example.com>',
subject: 'Display Name Test',
text: 'Testing MAIL FROM with display names'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
// Envelope should contain only email address, not display name
expect(result.envelope?.from).toEqual('john.doe@example.com');
console.log('✅ Display names handled correctly in MAIL FROM');
});
tap.test('CCMD-02: MAIL FROM - should handle SIZE parameter if server supports it', async () => {
// Send a larger email to test SIZE parameter
const largeContent = 'x'.repeat(1000000); // 1MB of content
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'SIZE Parameter Test',
text: largeContent
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ SIZE parameter handled for large email');
});
tap.test('CCMD-02: MAIL FROM - should handle international email addresses', async () => {
const email = new Email({
from: 'user@例え.jp',
to: 'recipient@example.com',
subject: 'International Domain Test',
text: 'Testing international domains in MAIL FROM'
});
try {
const result = await smtpClient.sendMail(email);
if (result.success) {
console.log('✅ International domain accepted');
expect(result.envelope?.from).toContain('@');
}
} catch (error) {
// Some servers may not support international domains
console.log(' Server does not support international domains');
}
});
tap.test('CCMD-02: MAIL FROM - should handle empty return path (bounce address)', async () => {
const email = new Email({
from: '<>', // Empty return path for bounces
to: 'recipient@example.com',
subject: 'Bounce Message Test',
text: 'This is a bounce message with empty return path'
});
try {
const result = await smtpClient.sendMail(email);
if (result.success) {
console.log('✅ Empty return path accepted for bounce');
expect(result.envelope?.from).toEqual('');
}
} catch (error) {
console.log(' Server rejected empty return path');
}
});
tap.test('CCMD-02: MAIL FROM - should handle special characters in local part', async () => {
const specialEmails = [
'user+tag@example.com',
'first.last@example.com',
'user_name@example.com',
'user-name@example.com'
];
for (const fromEmail of specialEmails) {
const email = new Email({
from: fromEmail,
to: 'recipient@example.com',
subject: 'Special Character Test',
text: `Testing special characters in: ${fromEmail}`
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
expect(result.envelope?.from).toEqual(fromEmail);
console.log(`✅ Special character email accepted: ${fromEmail}`);
}
});
tap.test('CCMD-02: MAIL FROM - should reject invalid sender addresses', async () => {
const invalidSenders = [
'no-at-sign',
'@example.com',
'user@',
'user@@example.com',
'user@.com',
'user@example.',
'user with spaces@example.com'
];
let rejectedCount = 0;
for (const invalidSender of invalidSenders) {
try {
const email = new Email({
from: invalidSender,
to: 'recipient@example.com',
subject: 'Invalid Sender Test',
text: 'This should fail'
});
await smtpClient.sendMail(email);
} catch (error) {
rejectedCount++;
console.log(`✅ Invalid sender rejected: ${invalidSender}`);
}
}
expect(rejectedCount).toBeGreaterThan(0);
});
tap.test('CCMD-02: MAIL FROM - should handle 8BITMIME parameter', async () => {
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'UTF-8 Test with special characters',
text: 'This email contains UTF-8 characters: 你好世界 🌍',
html: '<p>UTF-8 content: <strong>你好世界</strong> 🌍</p>'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ 8BITMIME content handled correctly');
});
tap.test('CCMD-02: MAIL FROM - should handle AUTH parameter if authenticated', async () => {
// Create authenticated client
const authServer = await startTestServer({
port: 2542,
authRequired: true
});
const authClient = createSmtpClient({
host: authServer.hostname,
port: authServer.port,
secure: false,
auth: {
user: 'testuser',
pass: 'testpass'
},
connectionTimeout: 5000
});
const email = new Email({
from: 'authenticated@example.com',
to: 'recipient@example.com',
subject: 'AUTH Parameter Test',
text: 'Sent with authentication'
});
const result = await authClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ AUTH parameter handled in MAIL FROM');
await authClient.close();
await stopTestServer(authServer);
});
tap.test('CCMD-02: MAIL FROM - should handle very long email addresses', async () => {
// RFC allows up to 320 characters total (64 + @ + 255)
const longLocal = 'a'.repeat(64);
const longDomain = 'subdomain.' + 'a'.repeat(60) + '.example.com';
const longEmail = `${longLocal}@${longDomain}`;
const email = new Email({
from: longEmail,
to: 'recipient@example.com',
subject: 'Long Email Address Test',
text: 'Testing maximum length email addresses'
});
try {
const result = await smtpClient.sendMail(email);
if (result.success) {
console.log('✅ Long email address accepted');
expect(result.envelope?.from).toEqual(longEmail);
}
} catch (error) {
console.log(' Server enforces email length limits');
}
});
tap.test('cleanup - close SMTP client', async () => {
if (smtpClient && smtpClient.isConnected()) {
await smtpClient.close();
}
});
tap.test('cleanup - stop SMTP server', async () => {
await stopTestServer(testServer);
});
tap.start();

View File

@ -0,0 +1,276 @@
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 RCPT TO tests', async () => {
testServer = await startTestServer({
port: 2543,
tlsEnabled: false,
authRequired: false,
maxRecipients: 10 // Set recipient limit
});
expect(testServer.port).toEqual(2543);
});
tap.test('setup - create SMTP client', async () => {
smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
const isConnected = await smtpClient.verify();
expect(isConnected).toBeTrue();
});
tap.test('CCMD-03: RCPT TO - should send to single recipient', async () => {
const email = new Email({
from: 'sender@example.com',
to: 'single@example.com',
subject: 'Single Recipient Test',
text: 'Testing single RCPT TO command'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
expect(result.acceptedRecipients).toContain('single@example.com');
expect(result.acceptedRecipients.length).toEqual(1);
expect(result.envelope?.to).toContain('single@example.com');
console.log('✅ Single RCPT TO command successful');
});
tap.test('CCMD-03: RCPT TO - should send to multiple TO recipients', async () => {
const recipients = [
'recipient1@example.com',
'recipient2@example.com',
'recipient3@example.com'
];
const email = new Email({
from: 'sender@example.com',
to: recipients,
subject: 'Multiple Recipients Test',
text: 'Testing multiple RCPT TO commands'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
expect(result.acceptedRecipients.length).toEqual(3);
recipients.forEach(recipient => {
expect(result.acceptedRecipients).toContain(recipient);
});
console.log(`✅ Sent to ${result.acceptedRecipients.length} recipients`);
});
tap.test('CCMD-03: RCPT TO - should handle CC recipients', async () => {
const email = new Email({
from: 'sender@example.com',
to: 'primary@example.com',
cc: ['cc1@example.com', 'cc2@example.com'],
subject: 'CC Recipients Test',
text: 'Testing RCPT TO with CC recipients'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
expect(result.acceptedRecipients.length).toEqual(3);
expect(result.acceptedRecipients).toContain('primary@example.com');
expect(result.acceptedRecipients).toContain('cc1@example.com');
expect(result.acceptedRecipients).toContain('cc2@example.com');
console.log('✅ CC recipients handled correctly');
});
tap.test('CCMD-03: RCPT TO - should handle BCC recipients', async () => {
const email = new Email({
from: 'sender@example.com',
to: 'visible@example.com',
bcc: ['hidden1@example.com', 'hidden2@example.com'],
subject: 'BCC Recipients Test',
text: 'Testing RCPT TO with BCC recipients'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
expect(result.acceptedRecipients.length).toEqual(3);
expect(result.acceptedRecipients).toContain('visible@example.com');
expect(result.acceptedRecipients).toContain('hidden1@example.com');
expect(result.acceptedRecipients).toContain('hidden2@example.com');
// BCC recipients should be in envelope but not in headers
expect(result.envelope?.to.length).toEqual(3);
console.log('✅ BCC recipients handled correctly');
});
tap.test('CCMD-03: RCPT TO - should handle mixed TO, CC, and BCC', async () => {
const email = new Email({
from: 'sender@example.com',
to: ['to1@example.com', 'to2@example.com'],
cc: ['cc1@example.com', 'cc2@example.com'],
bcc: ['bcc1@example.com', 'bcc2@example.com'],
subject: 'Mixed Recipients Test',
text: 'Testing all recipient types together'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
expect(result.acceptedRecipients.length).toEqual(6);
console.log('✅ Mixed recipient types handled correctly');
console.log(` TO: 2, CC: 2, BCC: 2 = Total: ${result.acceptedRecipients.length}`);
});
tap.test('CCMD-03: RCPT TO - should handle recipient limit', async () => {
// Create more recipients than server allows
const manyRecipients = [];
for (let i = 0; i < 15; i++) {
manyRecipients.push(`recipient${i}@example.com`);
}
const email = new Email({
from: 'sender@example.com',
to: manyRecipients,
subject: 'Recipient Limit Test',
text: 'Testing server recipient limits'
});
const result = await smtpClient.sendMail(email);
// Server should accept up to its limit
if (result.rejectedRecipients.length > 0) {
console.log(`✅ Server enforced recipient limit:`);
console.log(` Accepted: ${result.acceptedRecipients.length}`);
console.log(` Rejected: ${result.rejectedRecipients.length}`);
expect(result.acceptedRecipients.length).toBeLessThanOrEqual(10);
} else {
// Server accepted all
expect(result.acceptedRecipients.length).toEqual(15);
console.log(' Server accepted all recipients');
}
});
tap.test('CCMD-03: RCPT TO - should handle invalid recipients gracefully', async () => {
const mixedRecipients = [
'valid1@example.com',
'invalid@address@with@multiple@ats.com',
'valid2@example.com',
'no-domain@',
'valid3@example.com'
];
const email = new Email({
from: 'sender@example.com',
to: mixedRecipients.filter(r => r.includes('@') && r.split('@').length === 2),
subject: 'Mixed Valid/Invalid Recipients',
text: 'Testing partial recipient acceptance'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
expect(result.acceptedRecipients).toContain('valid1@example.com');
expect(result.acceptedRecipients).toContain('valid2@example.com');
expect(result.acceptedRecipients).toContain('valid3@example.com');
console.log('✅ Valid recipients accepted, invalid filtered');
});
tap.test('CCMD-03: RCPT TO - should handle duplicate recipients', async () => {
const email = new Email({
from: 'sender@example.com',
to: ['user@example.com', 'user@example.com'],
cc: ['user@example.com'],
bcc: ['user@example.com'],
subject: 'Duplicate Recipients Test',
text: 'Testing duplicate recipient handling'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
// Check if duplicates were removed
const uniqueAccepted = [...new Set(result.acceptedRecipients)];
console.log(`✅ Duplicate handling: ${result.acceptedRecipients.length} total, ${uniqueAccepted.length} unique`);
});
tap.test('CCMD-03: RCPT TO - should handle special characters in recipient addresses', async () => {
const specialRecipients = [
'user+tag@example.com',
'first.last@example.com',
'user_name@example.com',
'user-name@example.com',
'"quoted.user"@example.com'
];
const email = new Email({
from: 'sender@example.com',
to: specialRecipients.filter(r => !r.includes('"')), // Skip quoted for Email class
subject: 'Special Characters Test',
text: 'Testing special characters in recipient addresses'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
expect(result.acceptedRecipients.length).toBeGreaterThan(0);
console.log(`✅ Special character recipients accepted: ${result.acceptedRecipients.length}`);
});
tap.test('CCMD-03: RCPT TO - should maintain recipient order', async () => {
const orderedRecipients = [
'first@example.com',
'second@example.com',
'third@example.com',
'fourth@example.com'
];
const email = new Email({
from: 'sender@example.com',
to: orderedRecipients,
subject: 'Recipient Order Test',
text: 'Testing if recipient order is maintained'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
expect(result.envelope?.to.length).toEqual(orderedRecipients.length);
// Check order preservation
orderedRecipients.forEach((recipient, index) => {
expect(result.envelope?.to[index]).toEqual(recipient);
});
console.log('✅ Recipient order maintained in envelope');
});
tap.test('cleanup - close SMTP client', async () => {
if (smtpClient && smtpClient.isConnected()) {
await smtpClient.close();
}
});
tap.test('cleanup - stop SMTP server', async () => {
await stopTestServer(testServer);
});
tap.start();

View File

@ -0,0 +1,274 @@
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 DATA command tests', async () => {
testServer = await startTestServer({
port: 2544,
tlsEnabled: false,
authRequired: false,
size: 10 * 1024 * 1024 // 10MB message size limit
});
expect(testServer.port).toEqual(2544);
});
tap.test('setup - create SMTP client', async () => {
smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
socketTimeout: 30000, // Longer timeout for data transmission
debug: true
});
const isConnected = await smtpClient.verify();
expect(isConnected).toBeTrue();
});
tap.test('CCMD-04: DATA - should transmit simple text email', async () => {
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Simple DATA Test',
text: 'This is a simple text email transmitted via DATA command.'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
expect(result.response).toBeTypeofString();
console.log('✅ Simple text email transmitted successfully');
console.log('📧 Server response:', result.response);
});
tap.test('CCMD-04: DATA - should handle dot stuffing', async () => {
// Lines starting with dots should be escaped
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Dot Stuffing Test',
text: 'This email tests dot stuffing:\n.This line starts with a dot\n..So does this one\n...And this one'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ Dot stuffing handled correctly');
});
tap.test('CCMD-04: DATA - should transmit HTML email', async () => {
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'HTML Email Test',
text: 'This is the plain text version',
html: `
<html>
<head>
<title>HTML Email Test</title>
</head>
<body>
<h1>HTML Email</h1>
<p>This is an <strong>HTML</strong> email with:</p>
<ul>
<li>Lists</li>
<li>Formatting</li>
<li>Links: <a href="https://example.com">Example</a></li>
</ul>
</body>
</html>
`
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ HTML email transmitted successfully');
});
tap.test('CCMD-04: DATA - should handle large message body', async () => {
// Create a large message (1MB)
const largeText = 'This is a test line that will be repeated many times.\n'.repeat(20000);
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Large Message Test',
text: largeText
});
const startTime = Date.now();
const result = await smtpClient.sendMail(email);
const duration = Date.now() - startTime;
expect(result.success).toBeTrue();
console.log(`✅ Large message (${Math.round(largeText.length / 1024)}KB) transmitted in ${duration}ms`);
});
tap.test('CCMD-04: DATA - should handle binary attachments', async () => {
// Create a binary attachment
const binaryData = Buffer.alloc(1024);
for (let i = 0; i < binaryData.length; i++) {
binaryData[i] = i % 256;
}
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Binary Attachment Test',
text: 'This email contains a binary attachment',
attachments: [{
filename: 'test.bin',
content: binaryData,
contentType: 'application/octet-stream'
}]
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ Binary attachment transmitted successfully');
});
tap.test('CCMD-04: DATA - should handle special characters and encoding', async () => {
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Special Characters Test "Quotes" & More',
text: 'Special characters: © ® ™ € £ ¥ • … « » " " ' '',
html: '<p>Unicode: 你好世界 🌍 🚀 ✉️</p>'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ Special characters and Unicode handled correctly');
});
tap.test('CCMD-04: DATA - should handle line length limits', async () => {
// RFC 5321 specifies 1000 character line limit (including CRLF)
const longLine = 'a'.repeat(990); // Leave room for CRLF and safety
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('✅ Long lines handled within RFC limits');
});
tap.test('CCMD-04: DATA - should handle empty message body', async () => {
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Empty Body Test',
text: '' // Empty body
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ Empty message body handled correctly');
});
tap.test('CCMD-04: DATA - should handle CRLF line endings', async () => {
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'CRLF Test',
text: 'Line 1\r\nLine 2\r\nLine 3\nLine 4 (LF only)\r\nLine 5'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ Mixed line endings normalized to CRLF');
});
tap.test('CCMD-04: DATA - should handle message headers correctly', async () => {
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
cc: 'cc@example.com',
subject: 'Header Test',
text: 'Testing header transmission',
priority: 'high',
headers: {
'X-Custom-Header': 'custom-value',
'X-Mailer': 'SMTP Client Test Suite',
'Reply-To': 'replies@example.com'
}
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ All headers transmitted in DATA command');
});
tap.test('CCMD-04: DATA - should handle timeout for slow transmission', async () => {
// Create a very large message to test timeout handling
const hugeText = 'x'.repeat(5 * 1024 * 1024); // 5MB
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Timeout Test',
text: hugeText
});
// Should complete within socket timeout
const startTime = Date.now();
const result = await smtpClient.sendMail(email);
const duration = Date.now() - startTime;
expect(result.success).toBeTrue();
expect(duration).toBeLessThan(30000); // Should complete within socket timeout
console.log(`✅ Large data transmission completed in ${duration}ms`);
});
tap.test('CCMD-04: DATA - should handle server rejection after DATA', async () => {
// Some servers might reject after seeing content
const email = new Email({
from: 'spam@spammer.com',
to: 'recipient@example.com',
subject: 'Potential Spam Test',
text: 'BUY NOW! SPECIAL OFFER! CLICK HERE!',
mightBeSpam: true // Flag as potential spam
});
const result = await smtpClient.sendMail(email);
// Test server might accept or reject
if (result.success) {
console.log(' Test server accepted potential spam (normal for test)');
} else {
console.log('✅ Server can reject messages after DATA inspection');
}
});
tap.test('cleanup - close SMTP client', async () => {
if (smtpClient && smtpClient.isConnected()) {
await smtpClient.close();
}
});
tap.test('cleanup - stop SMTP server', async () => {
await stopTestServer(testServer);
});
tap.start();

View File

@ -0,0 +1,281 @@
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 authServer: ITestServer;
tap.test('setup - start SMTP server with authentication', async () => {
authServer = await startTestServer({
port: 2580,
tlsEnabled: false,
authRequired: true
});
expect(authServer.port).toEqual(2580);
expect(authServer.config.authRequired).toBeTrue();
});
tap.test('CCMD-05: AUTH - should fail without credentials', async () => {
let errorCaught = false;
try {
const noAuthClient = createSmtpClient({
host: authServer.hostname,
port: authServer.port,
secure: false,
connectionTimeout: 5000
// No auth provided
});
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'No Auth Test',
text: 'Should fail without authentication'
});
await noAuthClient.sendMail(email);
} catch (error: any) {
errorCaught = true;
expect(error).toBeInstanceOf(Error);
console.log('✅ Authentication required error:', error.message);
}
expect(errorCaught).toBeTrue();
});
tap.test('CCMD-05: AUTH - should authenticate with PLAIN mechanism', async () => {
const plainAuthClient = createSmtpClient({
host: authServer.hostname,
port: authServer.port,
secure: false,
connectionTimeout: 5000,
auth: {
user: 'testuser',
pass: 'testpass',
method: 'PLAIN'
},
debug: true
});
const isConnected = await plainAuthClient.verify();
expect(isConnected).toBeTrue();
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'PLAIN Auth Test',
text: 'Sent with PLAIN authentication'
});
const result = await plainAuthClient.sendMail(email);
expect(result.success).toBeTrue();
await plainAuthClient.close();
console.log('✅ PLAIN authentication successful');
});
tap.test('CCMD-05: AUTH - should authenticate with LOGIN mechanism', async () => {
const loginAuthClient = createSmtpClient({
host: authServer.hostname,
port: authServer.port,
secure: false,
connectionTimeout: 5000,
auth: {
user: 'testuser',
pass: 'testpass',
method: 'LOGIN'
},
debug: true
});
const isConnected = await loginAuthClient.verify();
expect(isConnected).toBeTrue();
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'LOGIN Auth Test',
text: 'Sent with LOGIN authentication'
});
const result = await loginAuthClient.sendMail(email);
expect(result.success).toBeTrue();
await loginAuthClient.close();
console.log('✅ LOGIN authentication successful');
});
tap.test('CCMD-05: AUTH - should auto-select authentication method', async () => {
const autoAuthClient = createSmtpClient({
host: authServer.hostname,
port: authServer.port,
secure: false,
connectionTimeout: 5000,
auth: {
user: 'testuser',
pass: 'testpass'
// No method specified - should auto-select
}
});
const isConnected = await autoAuthClient.verify();
expect(isConnected).toBeTrue();
await autoAuthClient.close();
console.log('✅ Auto-selected authentication method');
});
tap.test('CCMD-05: AUTH - should handle invalid credentials', async () => {
let authFailed = false;
try {
const badAuthClient = createSmtpClient({
host: authServer.hostname,
port: authServer.port,
secure: false,
connectionTimeout: 5000,
auth: {
user: 'wronguser',
pass: 'wrongpass'
}
});
await badAuthClient.verify();
} catch (error: any) {
authFailed = true;
expect(error).toBeInstanceOf(Error);
console.log('✅ Invalid credentials rejected:', error.message);
}
expect(authFailed).toBeTrue();
});
tap.test('CCMD-05: AUTH - should handle special characters in credentials', async () => {
const specialAuthClient = createSmtpClient({
host: authServer.hostname,
port: authServer.port,
secure: false,
connectionTimeout: 5000,
auth: {
user: 'user@domain.com',
pass: 'p@ssw0rd!#$%'
}
});
// Server might accept or reject based on implementation
try {
await specialAuthClient.verify();
await specialAuthClient.close();
console.log('✅ Special characters in credentials handled');
} catch (error) {
console.log(' Test server rejected special character credentials');
}
});
tap.test('CCMD-05: AUTH - should prefer secure auth over TLS', async () => {
// Start TLS-enabled server
const tlsAuthServer = await startTestServer({
port: 2581,
tlsEnabled: true,
authRequired: true
});
const tlsAuthClient = createSmtpClient({
host: tlsAuthServer.hostname,
port: tlsAuthServer.port,
secure: true,
connectionTimeout: 5000,
auth: {
user: 'testuser',
pass: 'testpass'
},
tls: {
rejectUnauthorized: false
}
});
const isConnected = await tlsAuthClient.verify();
expect(isConnected).toBeTrue();
await tlsAuthClient.close();
await stopTestServer(tlsAuthServer);
console.log('✅ Secure authentication over TLS');
});
tap.test('CCMD-05: AUTH - should maintain auth state across multiple sends', async () => {
const persistentAuthClient = createSmtpClient({
host: authServer.hostname,
port: authServer.port,
secure: false,
connectionTimeout: 5000,
auth: {
user: 'testuser',
pass: 'testpass'
}
});
await persistentAuthClient.verify();
// Send multiple emails without re-authenticating
for (let i = 0; i < 3; i++) {
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: `Persistent Auth Test ${i + 1}`,
text: `Email ${i + 1} using same auth session`
});
const result = await persistentAuthClient.sendMail(email);
expect(result.success).toBeTrue();
}
await persistentAuthClient.close();
console.log('✅ Authentication state maintained across sends');
});
tap.test('CCMD-05: AUTH - should handle auth with connection pooling', async () => {
const pooledAuthClient = createSmtpClient({
host: authServer.hostname,
port: authServer.port,
secure: false,
pool: true,
maxConnections: 3,
connectionTimeout: 5000,
auth: {
user: 'testuser',
pass: 'testpass'
}
});
// Send concurrent emails with pooled authenticated connections
const promises = [];
for (let i = 0; i < 5; i++) {
const email = new Email({
from: 'sender@example.com',
to: `recipient${i}@example.com`,
subject: `Pooled Auth Test ${i}`,
text: 'Testing auth with connection pooling'
});
promises.push(pooledAuthClient.sendMail(email));
}
const results = await Promise.all(promises);
results.forEach(result => {
expect(result.success).toBeTrue();
});
const poolStatus = pooledAuthClient.getPoolStatus();
console.log('📊 Auth pool status:', poolStatus);
await pooledAuthClient.close();
console.log('✅ Authentication works with connection pooling');
});
tap.test('cleanup - stop auth server', async () => {
await stopTestServer(authServer);
});
tap.start();

View File

@ -0,0 +1,328 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestSmtpServer } from '../../helpers/server.loader.js';
import { createSmtpClient } from '../../helpers/smtp.client.js';
let testServer: any;
tap.test('setup test SMTP server', async () => {
testServer = await startTestSmtpServer({
features: ['PIPELINING'] // Ensure server advertises PIPELINING
});
expect(testServer).toBeTruthy();
expect(testServer.port).toBeGreaterThan(0);
});
tap.test('CCMD-06: Check PIPELINING capability', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Send EHLO to get capabilities
const ehloResponse = await smtpClient.sendCommand('EHLO testclient.example.com');
expect(ehloResponse).toInclude('250');
// Check if PIPELINING is advertised
const supportsPipelining = ehloResponse.includes('PIPELINING');
console.log(`Server supports PIPELINING: ${supportsPipelining}`);
if (supportsPipelining) {
expect(ehloResponse).toInclude('PIPELINING');
}
await smtpClient.close();
});
tap.test('CCMD-06: Basic command pipelining', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
enablePipelining: true,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Send EHLO first
await smtpClient.sendCommand('EHLO testclient.example.com');
// Pipeline multiple commands
console.log('Sending pipelined commands...');
const startTime = Date.now();
// Send commands without waiting for responses
const promises = [
smtpClient.sendCommand('MAIL FROM:<sender@example.com>'),
smtpClient.sendCommand('RCPT TO:<recipient1@example.com>'),
smtpClient.sendCommand('RCPT TO:<recipient2@example.com>')
];
// Wait for all responses
const responses = await Promise.all(promises);
const elapsed = Date.now() - startTime;
console.log(`Pipelined commands completed in ${elapsed}ms`);
// Verify all responses are successful
responses.forEach((response, index) => {
expect(response).toInclude('250');
console.log(`Response ${index + 1}: ${response.trim()}`);
});
// Reset for cleanup
await smtpClient.sendCommand('RSET');
await smtpClient.close();
});
tap.test('CCMD-06: Pipelining with DATA command', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
enablePipelining: true,
connectionTimeout: 10000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Pipeline commands up to DATA
console.log('Pipelining commands before DATA...');
const setupPromises = [
smtpClient.sendCommand('MAIL FROM:<sender@example.com>'),
smtpClient.sendCommand('RCPT TO:<recipient@example.com>')
];
const setupResponses = await Promise.all(setupPromises);
setupResponses.forEach(response => {
expect(response).toInclude('250');
});
// DATA command should not be pipelined
const dataResponse = await smtpClient.sendCommand('DATA');
expect(dataResponse).toInclude('354');
// Send message data
const messageData = [
'Subject: Test Pipelining',
'From: sender@example.com',
'To: recipient@example.com',
'',
'This is a test message sent with pipelining.',
'.'
].join('\r\n');
const messageResponse = await smtpClient.sendCommand(messageData);
expect(messageResponse).toInclude('250');
await smtpClient.close();
});
tap.test('CCMD-06: Pipelining error handling', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
enablePipelining: true,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Pipeline commands with an invalid one
console.log('Testing pipelining with invalid command...');
const mixedPromises = [
smtpClient.sendCommand('MAIL FROM:<sender@example.com>'),
smtpClient.sendCommand('RCPT TO:<invalid-email>'), // Invalid format
smtpClient.sendCommand('RCPT TO:<valid@example.com>')
];
const responses = await Promise.allSettled(mixedPromises);
// Check responses
responses.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Command ${index + 1} response: ${result.value.trim()}`);
if (index === 1) {
// Invalid email might get rejected
expect(result.value).toMatch(/[45]\d\d/);
}
} else {
console.log(`Command ${index + 1} failed: ${result.reason}`);
}
});
// Reset
await smtpClient.sendCommand('RSET');
await smtpClient.close();
});
tap.test('CCMD-06: Pipelining performance comparison', async () => {
// Test without pipelining
const clientNoPipeline = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
enablePipelining: false,
connectionTimeout: 10000,
debug: false
});
await clientNoPipeline.connect();
await clientNoPipeline.sendCommand('EHLO testclient.example.com');
const startNoPipeline = Date.now();
// Send commands sequentially
await clientNoPipeline.sendCommand('MAIL FROM:<sender@example.com>');
await clientNoPipeline.sendCommand('RCPT TO:<recipient1@example.com>');
await clientNoPipeline.sendCommand('RCPT TO:<recipient2@example.com>');
await clientNoPipeline.sendCommand('RCPT TO:<recipient3@example.com>');
await clientNoPipeline.sendCommand('RSET');
const timeNoPipeline = Date.now() - startNoPipeline;
await clientNoPipeline.close();
// Test with pipelining
const clientPipeline = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
enablePipelining: true,
connectionTimeout: 10000,
debug: false
});
await clientPipeline.connect();
await clientPipeline.sendCommand('EHLO testclient.example.com');
const startPipeline = Date.now();
// Send commands pipelined
await Promise.all([
clientPipeline.sendCommand('MAIL FROM:<sender@example.com>'),
clientPipeline.sendCommand('RCPT TO:<recipient1@example.com>'),
clientPipeline.sendCommand('RCPT TO:<recipient2@example.com>'),
clientPipeline.sendCommand('RCPT TO:<recipient3@example.com>'),
clientPipeline.sendCommand('RSET')
]);
const timePipeline = Date.now() - startPipeline;
await clientPipeline.close();
console.log(`Sequential: ${timeNoPipeline}ms, Pipelined: ${timePipeline}ms`);
console.log(`Speedup: ${(timeNoPipeline / timePipeline).toFixed(2)}x`);
// Pipelining should be faster (but might not be in local testing)
expect(timePipeline).toBeLessThanOrEqual(timeNoPipeline * 1.1); // Allow 10% margin
});
tap.test('CCMD-06: Pipelining with multiple recipients', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
enablePipelining: true,
connectionTimeout: 10000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Create many recipients
const recipientCount = 10;
const recipients = Array.from({ length: recipientCount },
(_, i) => `recipient${i + 1}@example.com`
);
console.log(`Pipelining ${recipientCount} recipients...`);
// Pipeline MAIL FROM and all RCPT TO commands
const commands = [
smtpClient.sendCommand('MAIL FROM:<sender@example.com>'),
...recipients.map(rcpt => smtpClient.sendCommand(`RCPT TO:<${rcpt}>`))
];
const startTime = Date.now();
const responses = await Promise.all(commands);
const elapsed = Date.now() - startTime;
console.log(`Sent ${commands.length} pipelined commands in ${elapsed}ms`);
// Verify all succeeded
responses.forEach((response, index) => {
expect(response).toInclude('250');
});
// Calculate average time per command
const avgTime = elapsed / commands.length;
console.log(`Average time per command: ${avgTime.toFixed(2)}ms`);
await smtpClient.sendCommand('RSET');
await smtpClient.close();
});
tap.test('CCMD-06: Pipelining limits and buffering', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
enablePipelining: true,
pipelineMaxCommands: 5, // Limit pipeline size
connectionTimeout: 10000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Try to pipeline more than the limit
const commandCount = 8;
const commands = [
smtpClient.sendCommand('MAIL FROM:<sender@example.com>'),
...Array.from({ length: commandCount - 1 }, (_, i) =>
smtpClient.sendCommand(`RCPT TO:<recipient${i + 1}@example.com>`)
)
];
console.log(`Attempting to pipeline ${commandCount} commands with limit of 5...`);
const responses = await Promise.all(commands);
// All should still succeed, even if sent in batches
responses.forEach(response => {
expect(response).toInclude('250');
});
console.log('All commands processed successfully despite pipeline limit');
await smtpClient.sendCommand('RSET');
await smtpClient.close();
});
tap.test('cleanup test SMTP server', async () => {
if (testServer) {
await testServer.stop();
}
});
export default tap.start();

View File

@ -0,0 +1,352 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestSmtpServer } from '../../helpers/server.loader.js';
import { createSmtpClient } from '../../helpers/smtp.client.js';
let testServer: any;
tap.test('setup test SMTP server', async () => {
testServer = await startTestSmtpServer();
expect(testServer).toBeTruthy();
expect(testServer.port).toBeGreaterThan(0);
});
tap.test('CCMD-07: Parse single-line responses', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Test various single-line responses
const testCases = [
{ command: 'NOOP', expectedCode: '250', expectedText: /OK/ },
{ command: 'RSET', expectedCode: '250', expectedText: /Reset/ },
{ command: 'HELP', expectedCode: '214', expectedText: /Help/ }
];
for (const test of testCases) {
const response = await smtpClient.sendCommand(test.command);
// Parse response code and text
const codeMatch = response.match(/^(\d{3})\s+(.*)$/m);
expect(codeMatch).toBeTruthy();
if (codeMatch) {
const [, code, text] = codeMatch;
expect(code).toEqual(test.expectedCode);
expect(text).toMatch(test.expectedText);
console.log(`${test.command}: ${code} ${text}`);
}
}
await smtpClient.close();
});
tap.test('CCMD-07: Parse multi-line responses', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// EHLO typically returns multi-line response
const ehloResponse = await smtpClient.sendCommand('EHLO testclient.example.com');
// Parse multi-line response
const lines = ehloResponse.split('\r\n').filter(line => line.length > 0);
let capabilities: string[] = [];
let finalCode = '';
lines.forEach((line, index) => {
const multiLineMatch = line.match(/^(\d{3})-(.*)$/); // 250-CAPABILITY
const finalLineMatch = line.match(/^(\d{3})\s+(.*)$/); // 250 CAPABILITY
if (multiLineMatch) {
const [, code, capability] = multiLineMatch;
expect(code).toEqual('250');
capabilities.push(capability);
} else if (finalLineMatch) {
const [, code, capability] = finalLineMatch;
expect(code).toEqual('250');
finalCode = code;
capabilities.push(capability);
}
});
expect(finalCode).toEqual('250');
expect(capabilities.length).toBeGreaterThan(0);
console.log('Parsed capabilities:', capabilities);
// Common capabilities to check for
const commonCapabilities = ['PIPELINING', 'SIZE', '8BITMIME'];
const foundCapabilities = commonCapabilities.filter(cap =>
capabilities.some(c => c.includes(cap))
);
console.log('Found common capabilities:', foundCapabilities);
await smtpClient.close();
});
tap.test('CCMD-07: Parse error response codes', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Test various error conditions
const errorTests = [
{
command: 'RCPT TO:<recipient@example.com>', // Without MAIL FROM
expectedCodeRange: [500, 599],
description: 'RCPT without MAIL FROM'
},
{
command: 'INVALID_COMMAND',
expectedCodeRange: [500, 502],
description: 'Invalid command'
},
{
command: 'MAIL FROM:<invalid email format>',
expectedCodeRange: [501, 553],
description: 'Invalid email format'
}
];
for (const test of errorTests) {
try {
const response = await smtpClient.sendCommand(test.command);
const codeMatch = response.match(/^(\d{3})/);
if (codeMatch) {
const code = parseInt(codeMatch[1]);
console.log(`${test.description}: ${code} ${response.trim()}`);
expect(code).toBeGreaterThanOrEqual(test.expectedCodeRange[0]);
expect(code).toBeLessThanOrEqual(test.expectedCodeRange[1]);
}
} catch (error) {
console.log(`${test.description}: Error caught - ${error.message}`);
}
}
await smtpClient.sendCommand('RSET');
await smtpClient.close();
});
tap.test('CCMD-07: Parse enhanced status codes', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Send commands that might return enhanced status codes
await smtpClient.sendCommand('MAIL FROM:<sender@example.com>');
// Try to send to a potentially problematic address
const response = await smtpClient.sendCommand('RCPT TO:<postmaster@[127.0.0.1]>');
// Parse for enhanced status codes (X.Y.Z format)
const enhancedMatch = response.match(/\b(\d\.\d+\.\d+)\b/);
if (enhancedMatch) {
const [, enhancedCode] = enhancedMatch;
console.log(`Found enhanced status code: ${enhancedCode}`);
// Parse enhanced code components
const [classCode, subjectCode, detailCode] = enhancedCode.split('.').map(Number);
expect(classCode).toBeGreaterThanOrEqual(2);
expect(classCode).toBeLessThanOrEqual(5);
expect(subjectCode).toBeGreaterThanOrEqual(0);
expect(detailCode).toBeGreaterThanOrEqual(0);
// Interpret the enhanced code
const classDescriptions = {
2: 'Success',
3: 'Temporary Failure',
4: 'Persistent Transient Failure',
5: 'Permanent Failure'
};
console.log(`Enhanced code ${enhancedCode} means: ${classDescriptions[classCode] || 'Unknown'}`);
} else {
console.log('No enhanced status code found in response');
}
await smtpClient.sendCommand('RSET');
await smtpClient.close();
});
tap.test('CCMD-07: Parse response timing and delays', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 10000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Measure response times for different commands
const timingTests = [
'NOOP',
'HELP',
'MAIL FROM:<sender@example.com>',
'RSET'
];
const timings: { command: string; time: number; code: string }[] = [];
for (const command of timingTests) {
const startTime = Date.now();
const response = await smtpClient.sendCommand(command);
const elapsed = Date.now() - startTime;
const codeMatch = response.match(/^(\d{3})/);
const code = codeMatch ? codeMatch[1] : 'unknown';
timings.push({ command, time: elapsed, code });
}
// Analyze timings
console.log('\nCommand response times:');
timings.forEach(t => {
console.log(` ${t.command}: ${t.time}ms (${t.code})`);
});
const avgTime = timings.reduce((sum, t) => sum + t.time, 0) / timings.length;
console.log(`Average response time: ${avgTime.toFixed(2)}ms`);
// All commands should respond quickly (under 1 second)
timings.forEach(t => {
expect(t.time).toBeLessThan(1000);
});
await smtpClient.close();
});
tap.test('CCMD-07: Parse continuation responses', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 10000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
await smtpClient.sendCommand('MAIL FROM:<sender@example.com>');
await smtpClient.sendCommand('RCPT TO:<recipient@example.com>');
// DATA command returns a continuation response (354)
const dataResponse = await smtpClient.sendCommand('DATA');
// Parse continuation response
const contMatch = dataResponse.match(/^(\d{3})[\s-](.*)$/);
expect(contMatch).toBeTruthy();
if (contMatch) {
const [, code, text] = contMatch;
expect(code).toEqual('354');
expect(text).toMatch(/mail input|end with/i);
console.log(`Continuation response: ${code} ${text}`);
}
// Send message data
const messageData = 'Subject: Test\r\n\r\nTest message\r\n.';
const finalResponse = await smtpClient.sendCommand(messageData);
// Parse final response
const finalMatch = finalResponse.match(/^(\d{3})/);
expect(finalMatch).toBeTruthy();
expect(finalMatch![1]).toEqual('250');
await smtpClient.close();
});
tap.test('CCMD-07: Parse response text variations', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Different servers may have different response text
const response = await smtpClient.sendCommand('EHLO testclient.example.com');
// Extract server identification from first line
const firstLineMatch = response.match(/^250[\s-](.+?)(?:\r?\n|$)/);
if (firstLineMatch) {
const serverIdent = firstLineMatch[1];
console.log(`Server identification: ${serverIdent}`);
// Check for common patterns
const patterns = [
{ pattern: /ESMTP/, description: 'Extended SMTP' },
{ pattern: /ready|ok|hello/i, description: 'Greeting' },
{ pattern: /\d+\.\d+/, description: 'Version number' },
{ pattern: /[a-zA-Z0-9.-]+/, description: 'Hostname' }
];
patterns.forEach(p => {
if (p.pattern.test(serverIdent)) {
console.log(` Found: ${p.description}`);
}
});
}
// Test QUIT response variations
const quitResponse = await smtpClient.sendCommand('QUIT');
const quitMatch = quitResponse.match(/^(\d{3})\s+(.*)$/);
if (quitMatch) {
const [, code, text] = quitMatch;
expect(code).toEqual('221');
// Common QUIT response patterns
const quitPatterns = ['bye', 'closing', 'goodbye', 'terminating'];
const foundPattern = quitPatterns.some(p => text.toLowerCase().includes(p));
console.log(`QUIT response: ${text} (matches pattern: ${foundPattern})`);
}
});
tap.test('cleanup test SMTP server', async () => {
if (testServer) {
await testServer.stop();
}
});
export default tap.start();

View File

@ -0,0 +1,290 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestSmtpServer } from '../../helpers/server.loader.js';
import { createSmtpClient } from '../../helpers/smtp.client.js';
let testServer: any;
tap.test('setup test SMTP server', async () => {
testServer = await startTestSmtpServer();
expect(testServer).toBeTruthy();
expect(testServer.port).toBeGreaterThan(0);
});
tap.test('CCMD-08: Basic RSET command', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Send RSET command
const rsetResponse = await smtpClient.sendCommand('RSET');
// Verify response
expect(rsetResponse).toInclude('250');
expect(rsetResponse).toMatch(/reset|ok/i);
console.log(`RSET response: ${rsetResponse.trim()}`);
await smtpClient.close();
});
tap.test('CCMD-08: RSET after MAIL FROM', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Start transaction
const mailResponse = await smtpClient.sendCommand('MAIL FROM:<sender@example.com>');
expect(mailResponse).toInclude('250');
// Reset transaction
const rsetResponse = await smtpClient.sendCommand('RSET');
expect(rsetResponse).toInclude('250');
// Verify transaction was reset by trying RCPT TO without MAIL FROM
const rcptResponse = await smtpClient.sendCommand('RCPT TO:<recipient@example.com>');
expect(rcptResponse).toMatch(/[45]\d\d/); // Should fail
await smtpClient.close();
});
tap.test('CCMD-08: RSET after multiple recipients', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Build up a transaction with multiple recipients
await smtpClient.sendCommand('MAIL FROM:<sender@example.com>');
await smtpClient.sendCommand('RCPT TO:<recipient1@example.com>');
await smtpClient.sendCommand('RCPT TO:<recipient2@example.com>');
await smtpClient.sendCommand('RCPT TO:<recipient3@example.com>');
console.log('Transaction built with 3 recipients');
// Reset the transaction
const rsetResponse = await smtpClient.sendCommand('RSET');
expect(rsetResponse).toInclude('250');
// Start a new transaction to verify reset
const newMailResponse = await smtpClient.sendCommand('MAIL FROM:<newsender@example.com>');
expect(newMailResponse).toInclude('250');
const newRcptResponse = await smtpClient.sendCommand('RCPT TO:<newrecipient@example.com>');
expect(newRcptResponse).toInclude('250');
console.log('Successfully started new transaction after RSET');
await smtpClient.sendCommand('RSET');
await smtpClient.close();
});
tap.test('CCMD-08: RSET during DATA phase', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Start transaction
await smtpClient.sendCommand('MAIL FROM:<sender@example.com>');
await smtpClient.sendCommand('RCPT TO:<recipient@example.com>');
// Enter DATA phase
const dataResponse = await smtpClient.sendCommand('DATA');
expect(dataResponse).toInclude('354');
// Try RSET during DATA (should fail or be queued)
// Most servers will interpret this as message content
await smtpClient.sendCommand('RSET');
// Complete the DATA phase
const endDataResponse = await smtpClient.sendCommand('.');
expect(endDataResponse).toInclude('250');
// Now RSET should work
const rsetResponse = await smtpClient.sendCommand('RSET');
expect(rsetResponse).toInclude('250');
await smtpClient.close();
});
tap.test('CCMD-08: Multiple RSET commands', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Send multiple RSET commands
for (let i = 0; i < 5; i++) {
const rsetResponse = await smtpClient.sendCommand('RSET');
expect(rsetResponse).toInclude('250');
console.log(`RSET ${i + 1}: ${rsetResponse.trim()}`);
}
// Should still be able to start a transaction
const mailResponse = await smtpClient.sendCommand('MAIL FROM:<sender@example.com>');
expect(mailResponse).toInclude('250');
await smtpClient.sendCommand('RSET');
await smtpClient.close();
});
tap.test('CCMD-08: RSET with pipelining', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
enablePipelining: true,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Pipeline commands including RSET
const pipelinedCommands = [
smtpClient.sendCommand('MAIL FROM:<sender@example.com>'),
smtpClient.sendCommand('RCPT TO:<recipient@example.com>'),
smtpClient.sendCommand('RSET'),
smtpClient.sendCommand('MAIL FROM:<newsender@example.com>'),
smtpClient.sendCommand('RCPT TO:<newrecipient@example.com>')
];
const responses = await Promise.all(pipelinedCommands);
// Check responses
expect(responses[0]).toInclude('250'); // MAIL FROM
expect(responses[1]).toInclude('250'); // RCPT TO
expect(responses[2]).toInclude('250'); // RSET
expect(responses[3]).toInclude('250'); // New MAIL FROM
expect(responses[4]).toInclude('250'); // New RCPT TO
console.log('Successfully pipelined commands with RSET');
await smtpClient.sendCommand('RSET');
await smtpClient.close();
});
tap.test('CCMD-08: RSET state verification', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Build complex state
await smtpClient.sendCommand('MAIL FROM:<sender@example.com> SIZE=1000');
await smtpClient.sendCommand('RCPT TO:<recipient1@example.com>');
await smtpClient.sendCommand('RCPT TO:<recipient2@example.com>');
console.log('Built transaction state with SIZE parameter and 2 recipients');
// Reset
const rsetResponse = await smtpClient.sendCommand('RSET');
expect(rsetResponse).toInclude('250');
// Verify all state is cleared
// 1. Can't add recipients without MAIL FROM
const rcptResponse = await smtpClient.sendCommand('RCPT TO:<test@example.com>');
expect(rcptResponse).toMatch(/[45]\d\d/);
// 2. Can start fresh transaction
const newMailResponse = await smtpClient.sendCommand('MAIL FROM:<different@example.com>');
expect(newMailResponse).toInclude('250');
// 3. Previous recipients are not remembered
const dataResponse = await smtpClient.sendCommand('DATA');
expect(dataResponse).toMatch(/[45]\d\d/); // Should fail - no recipients
await smtpClient.sendCommand('RSET');
await smtpClient.close();
});
tap.test('CCMD-08: RSET performance impact', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: false // Quiet for performance test
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
const iterations = 20;
const times: number[] = [];
for (let i = 0; i < iterations; i++) {
// Build transaction
await smtpClient.sendCommand('MAIL FROM:<sender@example.com>');
await smtpClient.sendCommand('RCPT TO:<recipient@example.com>');
// Measure RSET time
const startTime = Date.now();
await smtpClient.sendCommand('RSET');
const elapsed = Date.now() - startTime;
times.push(elapsed);
}
// Analyze RSET performance
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
const minTime = Math.min(...times);
const maxTime = Math.max(...times);
console.log(`RSET performance over ${iterations} iterations:`);
console.log(` Average: ${avgTime.toFixed(2)}ms`);
console.log(` Min: ${minTime}ms`);
console.log(` Max: ${maxTime}ms`);
// RSET should be fast
expect(avgTime).toBeLessThan(100);
await smtpClient.close();
});
tap.test('cleanup test SMTP server', async () => {
if (testServer) {
await testServer.stop();
}
});
export default tap.start();

View File

@ -0,0 +1,340 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestSmtpServer } from '../../helpers/server.loader.js';
import { createSmtpClient } from '../../helpers/smtp.client.js';
let testServer: any;
tap.test('setup test SMTP server', async () => {
testServer = await startTestSmtpServer();
expect(testServer).toBeTruthy();
expect(testServer.port).toBeGreaterThan(0);
});
tap.test('CCMD-09: Basic NOOP command', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Send NOOP command
const noopResponse = await smtpClient.sendCommand('NOOP');
// Verify response
expect(noopResponse).toInclude('250');
console.log(`NOOP response: ${noopResponse.trim()}`);
await smtpClient.close();
});
tap.test('CCMD-09: NOOP during transaction', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Start a transaction
await smtpClient.sendCommand('MAIL FROM:<sender@example.com>');
await smtpClient.sendCommand('RCPT TO:<recipient@example.com>');
// Send NOOP - should not affect transaction
const noopResponse = await smtpClient.sendCommand('NOOP');
expect(noopResponse).toInclude('250');
// Continue transaction - should still work
const dataResponse = await smtpClient.sendCommand('DATA');
expect(dataResponse).toInclude('354');
// Send message
const messageResponse = await smtpClient.sendCommand('Subject: Test\r\n\r\nTest message\r\n.');
expect(messageResponse).toInclude('250');
console.log('Transaction completed successfully after NOOP');
await smtpClient.close();
});
tap.test('CCMD-09: Multiple NOOP commands', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Send multiple NOOPs rapidly
const noopCount = 10;
const responses: string[] = [];
console.log(`Sending ${noopCount} NOOP commands...`);
for (let i = 0; i < noopCount; i++) {
const response = await smtpClient.sendCommand('NOOP');
responses.push(response);
}
// All should succeed
responses.forEach((response, index) => {
expect(response).toInclude('250');
});
console.log(`All ${noopCount} NOOP commands succeeded`);
await smtpClient.close();
});
tap.test('CCMD-09: NOOP for keep-alive', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 10000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
console.log('Using NOOP for keep-alive over 10 seconds...');
// Send NOOP every 2 seconds for 10 seconds
const keepAliveInterval = 2000;
const duration = 10000;
const iterations = duration / keepAliveInterval;
for (let i = 0; i < iterations; i++) {
await new Promise(resolve => setTimeout(resolve, keepAliveInterval));
const startTime = Date.now();
const response = await smtpClient.sendCommand('NOOP');
const elapsed = Date.now() - startTime;
expect(response).toInclude('250');
console.log(`Keep-alive NOOP ${i + 1}: ${elapsed}ms`);
}
// Connection should still be active
expect(smtpClient.isConnected()).toBeTruthy();
await smtpClient.close();
});
tap.test('CCMD-09: NOOP with parameters', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// RFC 5321 allows NOOP to have parameters (which are ignored)
const noopVariants = [
'NOOP',
'NOOP test',
'NOOP hello world',
'NOOP 12345',
'NOOP check connection'
];
for (const command of noopVariants) {
const response = await smtpClient.sendCommand(command);
expect(response).toInclude('250');
console.log(`"${command}" -> ${response.trim()}`);
}
await smtpClient.close();
});
tap.test('CCMD-09: NOOP timing analysis', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: false // Quiet for timing
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Measure NOOP response times
const measurements = 20;
const times: number[] = [];
for (let i = 0; i < measurements; i++) {
const startTime = Date.now();
await smtpClient.sendCommand('NOOP');
const elapsed = Date.now() - startTime;
times.push(elapsed);
}
// Calculate statistics
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
const minTime = Math.min(...times);
const maxTime = Math.max(...times);
// Calculate standard deviation
const variance = times.reduce((sum, time) => sum + Math.pow(time - avgTime, 2), 0) / times.length;
const stdDev = Math.sqrt(variance);
console.log(`NOOP timing analysis (${measurements} samples):`);
console.log(` Average: ${avgTime.toFixed(2)}ms`);
console.log(` Min: ${minTime}ms`);
console.log(` Max: ${maxTime}ms`);
console.log(` Std Dev: ${stdDev.toFixed(2)}ms`);
// NOOP should be very fast
expect(avgTime).toBeLessThan(50);
// Check for consistency (low standard deviation)
expect(stdDev).toBeLessThan(avgTime * 0.5); // Less than 50% of average
await smtpClient.close();
});
tap.test('CCMD-09: NOOP during DATA phase', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Setup transaction
await smtpClient.sendCommand('MAIL FROM:<sender@example.com>');
await smtpClient.sendCommand('RCPT TO:<recipient@example.com>');
// Enter DATA phase
const dataResponse = await smtpClient.sendCommand('DATA');
expect(dataResponse).toInclude('354');
// During DATA phase, NOOP will be treated as message content
await smtpClient.sendCommand('Subject: Test with NOOP');
await smtpClient.sendCommand('');
await smtpClient.sendCommand('This message contains the word NOOP');
await smtpClient.sendCommand('NOOP'); // This is message content, not a command
await smtpClient.sendCommand('End of message');
// End DATA phase
const endResponse = await smtpClient.sendCommand('.');
expect(endResponse).toInclude('250');
// Now NOOP should work as a command again
const noopResponse = await smtpClient.sendCommand('NOOP');
expect(noopResponse).toInclude('250');
console.log('NOOP works correctly after DATA phase');
await smtpClient.close();
});
tap.test('CCMD-09: NOOP in pipelined commands', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
enablePipelining: true,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Pipeline NOOP with other commands
console.log('Pipelining NOOP with other commands...');
const pipelinedCommands = [
smtpClient.sendCommand('NOOP'),
smtpClient.sendCommand('MAIL FROM:<sender@example.com>'),
smtpClient.sendCommand('NOOP'),
smtpClient.sendCommand('RCPT TO:<recipient@example.com>'),
smtpClient.sendCommand('NOOP'),
smtpClient.sendCommand('RSET'),
smtpClient.sendCommand('NOOP')
];
const responses = await Promise.all(pipelinedCommands);
// All commands should succeed
responses.forEach((response, index) => {
expect(response).toInclude('250');
});
console.log('All pipelined commands including NOOPs succeeded');
await smtpClient.close();
});
tap.test('CCMD-09: NOOP error scenarios', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Try NOOP before EHLO/HELO (some servers might reject)
const earlyNoop = await smtpClient.sendCommand('NOOP');
console.log(`NOOP before EHLO: ${earlyNoop.trim()}`);
// Most servers allow it, but check response
expect(earlyNoop).toMatch(/[25]\d\d/);
// Now do proper handshake
await smtpClient.sendCommand('EHLO testclient.example.com');
// Test malformed NOOP (though it should be accepted)
const malformedTests = [
'NOOP\t\ttabs',
'NOOP multiple spaces',
'noop lowercase',
'NoOp MixedCase'
];
for (const command of malformedTests) {
try {
const response = await smtpClient.sendCommand(command);
console.log(`"${command}" -> ${response.trim()}`);
// Most servers are lenient
} catch (error) {
console.log(`"${command}" -> Error: ${error.message}`);
}
}
await smtpClient.close();
});
tap.test('cleanup test SMTP server', async () => {
if (testServer) {
await testServer.stop();
}
});
export default tap.start();

View File

@ -0,0 +1,382 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestSmtpServer } from '../../helpers/server.loader.js';
import { createSmtpClient } from '../../helpers/smtp.client.js';
let testServer: any;
tap.test('setup test SMTP server', async () => {
testServer = await startTestSmtpServer({
features: ['VRFY', 'EXPN'] // Enable VRFY and EXPN support
});
expect(testServer).toBeTruthy();
expect(testServer.port).toBeGreaterThan(0);
});
tap.test('CCMD-10: VRFY command basic usage', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Test VRFY with various addresses
const testAddresses = [
'user@example.com',
'postmaster',
'admin@example.com',
'nonexistent@example.com'
];
for (const address of testAddresses) {
const response = await smtpClient.sendCommand(`VRFY ${address}`);
console.log(`VRFY ${address}: ${response.trim()}`);
// Response codes:
// 250 - Address valid
// 251 - Address valid but not local
// 252 - Cannot verify but will accept
// 550 - Address not found
// 502 - Command not implemented
// 252 - Cannot VRFY user
expect(response).toMatch(/^[25]\d\d/);
if (response.startsWith('250') || response.startsWith('251')) {
console.log(` -> Address verified: ${address}`);
} else if (response.startsWith('252')) {
console.log(` -> Cannot verify: ${address}`);
} else if (response.startsWith('550')) {
console.log(` -> Address not found: ${address}`);
} else if (response.startsWith('502')) {
console.log(` -> VRFY not implemented`);
}
}
await smtpClient.close();
});
tap.test('CCMD-10: EXPN command basic usage', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Test EXPN with mailing lists
const testLists = [
'all',
'staff',
'users@example.com',
'mailinglist'
];
for (const list of testLists) {
const response = await smtpClient.sendCommand(`EXPN ${list}`);
console.log(`EXPN ${list}: ${response.trim()}`);
// Response codes:
// 250 - Expansion successful (may be multi-line)
// 252 - Cannot expand
// 550 - List not found
// 502 - Command not implemented
expect(response).toMatch(/^[25]\d\d/);
if (response.startsWith('250')) {
// Multi-line response possible
const lines = response.split('\r\n');
console.log(` -> List expanded to ${lines.length - 1} entries`);
} else if (response.startsWith('252')) {
console.log(` -> Cannot expand list: ${list}`);
} else if (response.startsWith('550')) {
console.log(` -> List not found: ${list}`);
} else if (response.startsWith('502')) {
console.log(` -> EXPN not implemented`);
}
}
await smtpClient.close();
});
tap.test('CCMD-10: VRFY with full names', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Test VRFY with full names
const fullNameTests = [
'John Doe',
'"Smith, John" <john.smith@example.com>',
'Mary Johnson <mary@example.com>',
'Robert "Bob" Williams'
];
for (const name of fullNameTests) {
const response = await smtpClient.sendCommand(`VRFY ${name}`);
console.log(`VRFY "${name}": ${response.trim()}`);
// Check if response includes email address
const emailMatch = response.match(/<([^>]+)>/);
if (emailMatch) {
console.log(` -> Resolved to: ${emailMatch[1]}`);
}
}
await smtpClient.close();
});
tap.test('CCMD-10: VRFY/EXPN security considerations', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Many servers disable VRFY/EXPN for security
console.log('\nTesting security responses:');
// Check if commands are disabled
const vrfyResponse = await smtpClient.sendCommand('VRFY postmaster');
const expnResponse = await smtpClient.sendCommand('EXPN all');
if (vrfyResponse.startsWith('502') || vrfyResponse.startsWith('252')) {
console.log('VRFY is disabled or restricted (security best practice)');
}
if (expnResponse.startsWith('502') || expnResponse.startsWith('252')) {
console.log('EXPN is disabled or restricted (security best practice)');
}
// Test potential information disclosure
const probeAddresses = [
'root',
'admin',
'administrator',
'webmaster',
'hostmaster',
'abuse'
];
let disclosureCount = 0;
for (const addr of probeAddresses) {
const response = await smtpClient.sendCommand(`VRFY ${addr}`);
if (response.startsWith('250') || response.startsWith('251')) {
disclosureCount++;
console.log(`Information disclosed for: ${addr}`);
}
}
console.log(`Total addresses disclosed: ${disclosureCount}/${probeAddresses.length}`);
await smtpClient.close();
});
tap.test('CCMD-10: VRFY/EXPN during transaction', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Start a mail transaction
await smtpClient.sendCommand('MAIL FROM:<sender@example.com>');
await smtpClient.sendCommand('RCPT TO:<recipient@example.com>');
// VRFY/EXPN during transaction should not affect it
const vrfyResponse = await smtpClient.sendCommand('VRFY user@example.com');
console.log(`VRFY during transaction: ${vrfyResponse.trim()}`);
const expnResponse = await smtpClient.sendCommand('EXPN mailinglist');
console.log(`EXPN during transaction: ${expnResponse.trim()}`);
// Continue transaction
const dataResponse = await smtpClient.sendCommand('DATA');
expect(dataResponse).toInclude('354');
await smtpClient.sendCommand('Subject: Test\r\n\r\nTest message\r\n.');
console.log('Transaction completed successfully after VRFY/EXPN');
await smtpClient.close();
});
tap.test('CCMD-10: VRFY with special characters', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Test addresses with special characters
const specialAddresses = [
'user+tag@example.com',
'first.last@example.com',
'user%remote@example.com',
'"quoted string"@example.com',
'user@[192.168.1.1]',
'user@sub.domain.example.com'
];
for (const addr of specialAddresses) {
const response = await smtpClient.sendCommand(`VRFY ${addr}`);
console.log(`VRFY special address "${addr}": ${response.trim()}`);
}
await smtpClient.close();
});
tap.test('CCMD-10: EXPN multi-line response', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// EXPN might return multiple addresses
const response = await smtpClient.sendCommand('EXPN all-users');
if (response.startsWith('250')) {
const lines = response.split('\r\n').filter(line => line.length > 0);
console.log('EXPN multi-line response:');
lines.forEach((line, index) => {
if (line.includes('250-')) {
// Continuation line
const address = line.substring(4);
console.log(` Member ${index + 1}: ${address}`);
} else if (line.includes('250 ')) {
// Final line
const address = line.substring(4);
console.log(` Member ${index + 1}: ${address} (last)`);
}
});
}
await smtpClient.close();
});
tap.test('CCMD-10: VRFY/EXPN rate limiting', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: false // Quiet for rate test
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Send many VRFY commands rapidly
const requestCount = 20;
const startTime = Date.now();
let successCount = 0;
let rateLimitHit = false;
console.log(`Sending ${requestCount} VRFY commands rapidly...`);
for (let i = 0; i < requestCount; i++) {
const response = await smtpClient.sendCommand(`VRFY user${i}@example.com`);
if (response.startsWith('421') || response.startsWith('450')) {
rateLimitHit = true;
console.log(`Rate limit hit at request ${i + 1}`);
break;
} else if (response.match(/^[25]\d\d/)) {
successCount++;
}
}
const elapsed = Date.now() - startTime;
const rate = (successCount / elapsed) * 1000;
console.log(`Completed ${successCount} requests in ${elapsed}ms`);
console.log(`Rate: ${rate.toFixed(2)} requests/second`);
if (rateLimitHit) {
console.log('Server implements rate limiting (good security practice)');
}
await smtpClient.close();
});
tap.test('CCMD-10: VRFY/EXPN error handling', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Test error cases
const errorTests = [
{ command: 'VRFY', description: 'VRFY without parameter' },
{ command: 'EXPN', description: 'EXPN without parameter' },
{ command: 'VRFY @', description: 'VRFY with invalid address' },
{ command: 'EXPN ""', description: 'EXPN with empty string' },
{ command: 'VRFY ' + 'x'.repeat(500), description: 'VRFY with very long parameter' }
];
for (const test of errorTests) {
try {
const response = await smtpClient.sendCommand(test.command);
console.log(`${test.description}: ${response.trim()}`);
// Should get error response
expect(response).toMatch(/^[45]\d\d/);
} catch (error) {
console.log(`${test.description}: Caught error - ${error.message}`);
}
}
await smtpClient.close();
});
tap.test('cleanup test SMTP server', async () => {
if (testServer) {
await testServer.stop();
}
});
export default tap.start();

View File

@ -0,0 +1,364 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestSmtpServer } from '../../helpers/server.loader.js';
import { createSmtpClient } from '../../helpers/smtp.client.js';
let testServer: any;
tap.test('setup test SMTP server', async () => {
testServer = await startTestSmtpServer();
expect(testServer).toBeTruthy();
expect(testServer.port).toBeGreaterThan(0);
});
tap.test('CCMD-11: Basic HELP command', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Send HELP without parameters
const helpResponse = await smtpClient.sendCommand('HELP');
// HELP typically returns 214 or 211
expect(helpResponse).toMatch(/^21[14]/);
console.log('HELP response:');
console.log(helpResponse);
// Check if it's multi-line
const lines = helpResponse.split('\r\n').filter(line => line.length > 0);
if (lines.length > 1) {
console.log(`Multi-line help with ${lines.length} lines`);
}
await smtpClient.close();
});
tap.test('CCMD-11: HELP with specific commands', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Test HELP for specific commands
const commands = [
'HELO',
'EHLO',
'MAIL',
'RCPT',
'DATA',
'RSET',
'NOOP',
'QUIT',
'VRFY',
'EXPN',
'HELP',
'AUTH',
'STARTTLS'
];
for (const cmd of commands) {
const response = await smtpClient.sendCommand(`HELP ${cmd}`);
console.log(`\nHELP ${cmd}:`);
if (response.startsWith('214') || response.startsWith('211')) {
// Extract help text
const helpText = response.replace(/^21[14][\s-]/, '');
console.log(` ${helpText.trim()}`);
} else if (response.startsWith('502')) {
console.log(` Command not implemented`);
} else if (response.startsWith('504')) {
console.log(` Command parameter not implemented`);
} else {
console.log(` ${response.trim()}`);
}
}
await smtpClient.close();
});
tap.test('CCMD-11: HELP response format variations', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Test different HELP queries
const queries = [
'', // No parameter
'MAIL FROM', // Command with space
'RCPT TO', // Another with space
'UNKNOWN', // Unknown command
'mail', // Lowercase
'MaIl' // Mixed case
];
for (const query of queries) {
const cmd = query ? `HELP ${query}` : 'HELP';
const response = await smtpClient.sendCommand(cmd);
console.log(`\n"${cmd}":`);
// Parse response code
const codeMatch = response.match(/^(\d{3})/);
if (codeMatch) {
const code = codeMatch[1];
console.log(` Response code: ${code}`);
// Common codes:
// 211 - System status
// 214 - Help message
// 502 - Command not implemented
// 504 - Command parameter not implemented
if (code === '214' || code === '211') {
// Check if response mentions the queried command
if (query && response.toLowerCase().includes(query.toLowerCase())) {
console.log(` Help specifically mentions "${query}"`);
}
}
}
}
await smtpClient.close();
});
tap.test('CCMD-11: HELP during transaction', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Start a transaction
await smtpClient.sendCommand('MAIL FROM:<sender@example.com>');
await smtpClient.sendCommand('RCPT TO:<recipient@example.com>');
// HELP should not affect transaction
console.log('\nHELP during transaction:');
const helpResponse = await smtpClient.sendCommand('HELP DATA');
expect(helpResponse).toMatch(/^21[14]/);
// Continue transaction
const dataResponse = await smtpClient.sendCommand('DATA');
expect(dataResponse).toInclude('354');
await smtpClient.sendCommand('Subject: Test\r\n\r\nTest message\r\n.');
console.log('Transaction completed successfully after HELP');
await smtpClient.close();
});
tap.test('CCMD-11: HELP command availability check', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Check HELP before EHLO
console.log('\nTesting HELP before EHLO:');
const earlyHelp = await smtpClient.sendCommand('HELP');
console.log(`Response: ${earlyHelp.substring(0, 50)}...`);
// HELP should work even before EHLO
expect(earlyHelp).toMatch(/^[25]\d\d/);
// Now do EHLO and check features
const ehloResponse = await smtpClient.sendCommand('EHLO testclient.example.com');
// Check if HELP is advertised (not common but possible)
if (ehloResponse.includes('HELP')) {
console.log('Server explicitly advertises HELP support');
}
await smtpClient.close();
});
tap.test('CCMD-11: HELP with invalid parameters', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Test HELP with various invalid inputs
const invalidTests = [
'HELP ' + 'X'.repeat(100), // Very long parameter
'HELP <>', // Special characters
'HELP MAIL RCPT DATA', // Multiple commands
'HELP\t\tTABS', // Tabs
'HELP\r\nINJECTION' // Injection attempt
];
for (const cmd of invalidTests) {
try {
const response = await smtpClient.sendCommand(cmd);
console.log(`\n"${cmd.substring(0, 30)}...": ${response.substring(0, 50)}...`);
// Should still get a valid SMTP response
expect(response).toMatch(/^\d{3}/);
} catch (error) {
console.log(`Command rejected: ${error.message}`);
}
}
await smtpClient.close();
});
tap.test('CCMD-11: HELP response parsing', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Get general HELP
const helpResponse = await smtpClient.sendCommand('HELP');
// Parse help content
if (helpResponse.match(/^21[14]/)) {
// Extract command list if present
const commandMatches = helpResponse.match(/\b(HELO|EHLO|MAIL|RCPT|DATA|RSET|NOOP|QUIT|VRFY|EXPN|HELP|AUTH|STARTTLS)\b/g);
if (commandMatches) {
const uniqueCommands = [...new Set(commandMatches)];
console.log('\nCommands mentioned in HELP:');
uniqueCommands.forEach(cmd => console.log(` - ${cmd}`));
// Verify common commands are mentioned
const essentialCommands = ['MAIL', 'RCPT', 'DATA', 'QUIT'];
const mentionedEssentials = essentialCommands.filter(cmd =>
uniqueCommands.includes(cmd)
);
console.log(`\nEssential commands mentioned: ${mentionedEssentials.length}/${essentialCommands.length}`);
}
// Check for URLs or references
const urlMatch = helpResponse.match(/https?:\/\/[^\s]+/);
if (urlMatch) {
console.log(`\nHelp includes URL: ${urlMatch[0]}`);
}
// Check for RFC references
const rfcMatch = helpResponse.match(/RFC\s*\d+/gi);
if (rfcMatch) {
console.log(`\nRFC references: ${rfcMatch.join(', ')}`);
}
}
await smtpClient.close();
});
tap.test('CCMD-11: HELP command localization', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Some servers might support localized help
// Test with Accept-Language style parameter (non-standard)
const languages = ['en', 'es', 'fr', 'de'];
for (const lang of languages) {
const response = await smtpClient.sendCommand(`HELP ${lang}`);
console.log(`\nHELP ${lang}: ${response.substring(0, 60)}...`);
// Most servers will treat this as unknown command
// But we're testing how they handle it
}
await smtpClient.close();
});
tap.test('CCMD-11: HELP performance', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: false // Quiet for performance test
});
await smtpClient.connect();
await smtpClient.sendCommand('EHLO testclient.example.com');
// Measure HELP response times
const iterations = 10;
const times: number[] = [];
for (let i = 0; i < iterations; i++) {
const startTime = Date.now();
await smtpClient.sendCommand('HELP');
const elapsed = Date.now() - startTime;
times.push(elapsed);
}
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
const minTime = Math.min(...times);
const maxTime = Math.max(...times);
console.log(`\nHELP command performance (${iterations} iterations):`);
console.log(` Average: ${avgTime.toFixed(2)}ms`);
console.log(` Min: ${minTime}ms`);
console.log(` Max: ${maxTime}ms`);
// HELP should be fast (static response)
expect(avgTime).toBeLessThan(100);
await smtpClient.close();
});
tap.test('cleanup test SMTP server', async () => {
if (testServer) {
await testServer.stop();
}
});
export default tap.start();