update
This commit is contained in:
166
test/suite/smtpclient_commands/test.ccmd-01.ehlo-helo-sending.ts
Normal file
166
test/suite/smtpclient_commands/test.ccmd-01.ehlo-helo-sending.ts
Normal 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();
|
@ -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();
|
276
test/suite/smtpclient_commands/test.ccmd-03.rcpt-to-multiple.ts
Normal file
276
test/suite/smtpclient_commands/test.ccmd-03.rcpt-to-multiple.ts
Normal 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();
|
274
test/suite/smtpclient_commands/test.ccmd-04.data-transmission.ts
Normal file
274
test/suite/smtpclient_commands/test.ccmd-04.data-transmission.ts
Normal 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();
|
281
test/suite/smtpclient_commands/test.ccmd-05.auth-mechanisms.ts
Normal file
281
test/suite/smtpclient_commands/test.ccmd-05.auth-mechanisms.ts
Normal 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();
|
@ -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();
|
352
test/suite/smtpclient_commands/test.ccmd-07.response-parsing.ts
Normal file
352
test/suite/smtpclient_commands/test.ccmd-07.response-parsing.ts
Normal 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();
|
290
test/suite/smtpclient_commands/test.ccmd-08.rset-command.ts
Normal file
290
test/suite/smtpclient_commands/test.ccmd-08.rset-command.ts
Normal 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();
|
340
test/suite/smtpclient_commands/test.ccmd-09.noop-command.ts
Normal file
340
test/suite/smtpclient_commands/test.ccmd-09.noop-command.ts
Normal 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();
|
382
test/suite/smtpclient_commands/test.ccmd-10.vrfy-expn.ts
Normal file
382
test/suite/smtpclient_commands/test.ccmd-10.vrfy-expn.ts
Normal 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();
|
364
test/suite/smtpclient_commands/test.ccmd-11.help-command.ts
Normal file
364
test/suite/smtpclient_commands/test.ccmd-11.help-command.ts
Normal 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();
|
Reference in New Issue
Block a user