feat(storage): add comprehensive tests for StorageManager with memory, filesystem, and custom function backends
Some checks failed
CI / Type Check & Lint (push) Failing after 3s
CI / Build Test (Current Platform) (push) Failing after 3s
CI / Build All Platforms (push) Failing after 3s

feat(email): implement EmailSendJob class for robust email delivery with retry logic and MX record resolution

feat(mail): restructure mail module exports for simplified access to core and delivery functionalities
This commit is contained in:
2025-10-28 19:46:17 +00:00
parent 6523c55516
commit 17f5661636
271 changed files with 61736 additions and 6222 deletions

View File

@@ -0,0 +1,168 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.ts';
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.ts';
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.ts';
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,
pool: true, // Enable pooling to maintain connections
debug: true
});
// verify() creates a temporary connection and closes it
const verifyResult = await capClient.verify();
expect(verifyResult).toBeTrue();
// After verify(), the pool might be empty since verify() closes its connection
// Instead, let's send an actual email to test capabilities
const poolStatus = capClient.getPoolStatus();
// Pool starts empty
expect(poolStatus.total).toEqual(0);
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 - verify() creates and closes its own connection
const firstVerify = await smtpClient.verify();
expect(firstVerify).toBeTrue();
// After verify(), no connections should be in the pool
expect(smtpClient.isConnected()).toBeFalse();
// Second verify - should send EHLO again
const secondVerify = await smtpClient.verify();
expect(secondVerify).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);
});
export default tap.start();

View File

@@ -0,0 +1,277 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.ts';
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.ts';
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.ts';
import { Email } from '../../../ts/mail/core/classes.email.ts';
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 - auth requires TLS per RFC 8314
const authServer = await startTestServer({
port: 2542,
tlsEnabled: true,
authRequired: true
});
const authClient = createSmtpClient({
host: authServer.hostname,
port: authServer.port,
secure: false, // Use STARTTLS instead of direct TLS
requireTLS: true, // Require TLS upgrade
tls: {
rejectUnauthorized: false // Accept self-signed cert for testing
},
auth: {
user: 'testuser',
pass: 'testpass'
},
connectionTimeout: 5000,
debug: true
});
try {
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');
} catch (error) {
console.error('AUTH test error:', error);
throw error;
} finally {
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);
});
export default tap.start();

View File

@@ -0,0 +1,283 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.ts';
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.ts';
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.ts';
import { Email } from '../../../ts/mail/core/classes.email.ts';
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'
];
// Filter out invalid recipients before creating the email
const validRecipients = mixedRecipients.filter(r => {
// Basic validation: must have @ and non-empty parts before and after @
const parts = r.split('@');
return parts.length === 2 && parts[0].length > 0 && parts[1].length > 0;
});
const email = new Email({
from: 'sender@example.com',
to: validRecipients,
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);
});
export default tap.start();

View File

@@ -0,0 +1,274 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.ts';
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.ts';
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.ts';
import { Email } from '../../../ts/mail/core/classes.email.ts';
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);
});
export default tap.start();

View File

@@ -0,0 +1,306 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.ts';
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.ts';
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.ts';
import { Email } from '../../../ts/mail/core/classes.email.ts';
let authServer: ITestServer;
tap.test('setup - start SMTP server with authentication', async () => {
authServer = await startTestServer({
port: 2580,
tlsEnabled: true, // Enable STARTTLS capability
authRequired: true
});
expect(authServer.port).toEqual(2580);
expect(authServer.config.authRequired).toBeTrue();
});
tap.test('CCMD-05: AUTH - should fail without credentials', async () => {
const noAuthClient = createSmtpClient({
host: authServer.hostname,
port: authServer.port,
secure: false, // Start plain, upgrade with STARTTLS
tls: {
rejectUnauthorized: false // Accept self-signed certs for testing
},
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'
});
const result = await noAuthClient.sendMail(email);
expect(result.success).toBeFalse();
expect(result.error).toBeInstanceOf(Error);
expect(result.error?.message).toContain('Authentication required');
console.log('✅ Authentication required error:', result.error?.message);
await noAuthClient.close();
});
tap.test('CCMD-05: AUTH - should authenticate with PLAIN mechanism', async () => {
const plainAuthClient = createSmtpClient({
host: authServer.hostname,
port: authServer.port,
secure: false, // Start plain, upgrade with STARTTLS
tls: {
rejectUnauthorized: false // Accept self-signed certs for testing
},
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, // Start plain, upgrade with STARTTLS
tls: {
rejectUnauthorized: false // Accept self-signed certs for testing
},
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, // Start plain, upgrade with STARTTLS
tls: {
rejectUnauthorized: false // Accept self-signed certs for testing
},
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 () => {
const badAuthClient = createSmtpClient({
host: authServer.hostname,
port: authServer.port,
secure: false, // Start plain, upgrade with STARTTLS
tls: {
rejectUnauthorized: false // Accept self-signed certs for testing
},
connectionTimeout: 5000,
auth: {
user: 'wronguser',
pass: 'wrongpass'
}
});
const isConnected = await badAuthClient.verify();
expect(isConnected).toBeFalse();
console.log('✅ Invalid credentials rejected');
await badAuthClient.close();
});
tap.test('CCMD-05: AUTH - should handle special characters in credentials', async () => {
const specialAuthClient = createSmtpClient({
host: authServer.hostname,
port: authServer.port,
secure: false, // Start plain, upgrade with STARTTLS
tls: {
rejectUnauthorized: false // Accept self-signed certs for testing
},
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: false, // Use STARTTLS
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, // Start plain, upgrade with STARTTLS
tls: {
rejectUnauthorized: false // Accept self-signed certs for testing
},
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, // Start plain, upgrade with STARTTLS
tls: {
rejectUnauthorized: false // Accept self-signed certs for testing
},
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);
// Debug output to understand failures
results.forEach((result, index) => {
if (!result.success) {
console.log(`❌ Email ${index} failed:`, result.error?.message);
}
});
const successCount = results.filter(r => r.success).length;
console.log(`📧 Sent ${successCount} of ${results.length} emails successfully`);
const poolStatus = pooledAuthClient.getPoolStatus();
console.log('📊 Auth pool status:', poolStatus);
// Check that at least one email was sent (connection pooling might limit concurrent sends)
expect(successCount).toBeGreaterThan(0);
await pooledAuthClient.close();
console.log('✅ Authentication works with connection pooling');
});
tap.test('cleanup - stop auth server', async () => {
await stopTestServer(authServer);
});
export default tap.start();

View File

@@ -0,0 +1,233 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.ts';
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.ts';
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.ts';
import { Email } from '../../../ts/mail/core/classes.email.ts';
let testServer: ITestServer;
tap.test('setup test SMTP server', async () => {
testServer = await startTestServer({
port: 2546,
tlsEnabled: false,
authRequired: false
});
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
});
// The SmtpClient handles pipelining internally
// We can verify the server supports it by checking a successful send
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Pipelining Test',
text: 'Testing pipelining support'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
// Server logs show PIPELINING is advertised
console.log('✅ Server supports PIPELINING (advertised in EHLO response)');
await smtpClient.close();
});
tap.test('CCMD-06: Basic command pipelining', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
// Send email with multiple recipients to test pipelining
const email = new Email({
from: 'sender@example.com',
to: ['recipient1@example.com', 'recipient2@example.com'],
subject: 'Multi-recipient Test',
text: 'Testing pipelining with multiple recipients'
});
const startTime = Date.now();
const result = await smtpClient.sendMail(email);
const elapsed = Date.now() - startTime;
expect(result.success).toBeTrue();
expect(result.acceptedRecipients.length).toEqual(2);
console.log(`✅ Sent to ${result.acceptedRecipients.length} recipients in ${elapsed}ms`);
console.log('Pipelining improves performance by sending multiple commands without waiting');
await smtpClient.close();
});
tap.test('CCMD-06: Pipelining with DATA command', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
// Send a normal email - pipelining is handled internally
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'DATA Command Test',
text: 'Testing pipelining up to DATA command'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ Commands pipelined up to DATA successfully');
console.log('DATA command requires synchronous handling as per RFC');
await smtpClient.close();
});
tap.test('CCMD-06: Pipelining error handling', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
// Send email with mix of valid and potentially problematic recipients
const email = new Email({
from: 'sender@example.com',
to: [
'valid1@example.com',
'valid2@example.com',
'valid3@example.com'
],
subject: 'Error Handling Test',
text: 'Testing pipelining error handling'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log(`✅ Handled ${result.acceptedRecipients.length} recipients`);
console.log('Pipelining handles errors gracefully');
await smtpClient.close();
});
tap.test('CCMD-06: Pipelining performance comparison', async () => {
// Create two clients - both use pipelining by default when available
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000
});
// Test with multiple recipients
const email = new Email({
from: 'sender@example.com',
to: [
'recipient1@example.com',
'recipient2@example.com',
'recipient3@example.com',
'recipient4@example.com',
'recipient5@example.com'
],
subject: 'Performance Test',
text: 'Testing performance with multiple recipients'
});
const startTime = Date.now();
const result = await smtpClient.sendMail(email);
const elapsed = Date.now() - startTime;
expect(result.success).toBeTrue();
expect(result.acceptedRecipients.length).toEqual(5);
console.log(`✅ Sent to ${result.acceptedRecipients.length} recipients in ${elapsed}ms`);
console.log('Pipelining provides significant performance improvements');
await smtpClient.close();
});
tap.test('CCMD-06: Pipelining with multiple recipients', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
// Send to many recipients
const recipients = Array.from({ length: 10 }, (_, i) => `recipient${i + 1}@example.com`);
const email = new Email({
from: 'sender@example.com',
to: recipients,
subject: 'Many Recipients Test',
text: 'Testing pipelining with many recipients'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
expect(result.acceptedRecipients.length).toEqual(recipients.length);
console.log(`✅ Successfully sent to ${result.acceptedRecipients.length} recipients`);
console.log('Pipelining efficiently handles multiple RCPT TO commands');
await smtpClient.close();
});
tap.test('CCMD-06: Pipelining limits and buffering', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
// Test with a reasonable number of recipients
const recipients = Array.from({ length: 50 }, (_, i) => `user${i + 1}@example.com`);
const email = new Email({
from: 'sender@example.com',
to: recipients.slice(0, 20), // Use first 20 for TO
cc: recipients.slice(20, 35), // Next 15 for CC
bcc: recipients.slice(35), // Rest for BCC
subject: 'Buffering Test',
text: 'Testing pipelining limits and buffering'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
const totalRecipients = email.to.length + email.cc.length + email.bcc.length;
console.log(`✅ Handled ${totalRecipients} total recipients`);
console.log('Pipelining respects server limits and buffers appropriately');
await smtpClient.close();
});
tap.test('cleanup test SMTP server', async () => {
await stopTestServer(testServer);
expect(testServer).toBeTruthy();
});
export default tap.start();

View File

@@ -0,0 +1,243 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.ts';
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.ts';
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.ts';
import { Email } from '../../../ts/mail/core/classes.email.ts';
let testServer: ITestServer;
tap.test('setup test SMTP server', async () => {
testServer = await startTestServer({
port: 2547,
tlsEnabled: false,
authRequired: false
});
expect(testServer).toBeTruthy();
expect(testServer.port).toBeGreaterThan(0);
});
tap.test('CCMD-07: Parse successful send responses', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Response Test',
text: 'Testing response parsing'
});
const result = await smtpClient.sendMail(email);
// Verify successful response parsing
expect(result.success).toBeTrue();
expect(result.response).toBeTruthy();
expect(result.messageId).toBeTruthy();
// The response should contain queue ID
expect(result.response).toInclude('queued');
console.log(`✅ Parsed success response: ${result.response}`);
await smtpClient.close();
});
tap.test('CCMD-07: Parse multiple recipient responses', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
// Send to multiple recipients
const email = new Email({
from: 'sender@example.com',
to: ['recipient1@example.com', 'recipient2@example.com', 'recipient3@example.com'],
subject: 'Multi-recipient Test',
text: 'Testing multiple recipient response parsing'
});
const result = await smtpClient.sendMail(email);
// Verify parsing of multiple recipient responses
expect(result.success).toBeTrue();
expect(result.acceptedRecipients.length).toEqual(3);
expect(result.rejectedRecipients.length).toEqual(0);
console.log(`✅ Accepted ${result.acceptedRecipients.length} recipients`);
console.log('Multiple RCPT TO responses parsed correctly');
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
});
// Test with invalid email to trigger error
try {
const email = new Email({
from: '', // Empty from should trigger error
to: 'recipient@example.com',
subject: 'Error Test',
text: 'Testing error response'
});
await smtpClient.sendMail(email);
expect(false).toBeTrue(); // Should not reach here
} catch (error: any) {
expect(error).toBeInstanceOf(Error);
expect(error.message).toBeTruthy();
console.log(`✅ Error response parsed: ${error.message}`);
}
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
});
// Normal send - server advertises ENHANCEDSTATUSCODES
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Enhanced Status Test',
text: 'Testing enhanced status code parsing'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
// Server logs show it advertises ENHANCEDSTATUSCODES in EHLO
console.log('✅ Server advertises ENHANCEDSTATUSCODES capability');
console.log('Enhanced status codes are parsed automatically');
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: 5000,
debug: true
});
// Measure response time
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Timing Test',
text: 'Testing response timing'
});
const startTime = Date.now();
const result = await smtpClient.sendMail(email);
const elapsed = Date.now() - startTime;
expect(result.success).toBeTrue();
expect(elapsed).toBeGreaterThan(0);
expect(elapsed).toBeLessThan(5000); // Should complete within 5 seconds
console.log(`✅ Response received and parsed in ${elapsed}ms`);
console.log('Client handles response timing appropriately');
await smtpClient.close();
});
tap.test('CCMD-07: Parse envelope information', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
const from = 'sender@example.com';
const to = ['recipient1@example.com', 'recipient2@example.com'];
const cc = ['cc@example.com'];
const bcc = ['bcc@example.com'];
const email = new Email({
from,
to,
cc,
bcc,
subject: 'Envelope Test',
text: 'Testing envelope parsing'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
expect(result.envelope).toBeTruthy();
expect(result.envelope.from).toEqual(from);
expect(result.envelope.to).toBeArray();
// Envelope should include all recipients (to, cc, bcc)
const totalRecipients = to.length + cc.length + bcc.length;
expect(result.envelope.to.length).toEqual(totalRecipients);
console.log(`✅ Envelope parsed with ${result.envelope.to.length} recipients`);
console.log('Envelope information correctly extracted from responses');
await smtpClient.close();
});
tap.test('CCMD-07: Parse connection state responses', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
// Test verify() which checks connection state
const isConnected = await smtpClient.verify();
expect(isConnected).toBeTrue();
console.log('✅ Connection verified through greeting and EHLO responses');
// Send email to test active connection
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'State Test',
text: 'Testing connection state'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ Connection state maintained throughout session');
console.log('Response parsing handles connection state correctly');
await smtpClient.close();
});
tap.test('cleanup test SMTP server', async () => {
await stopTestServer(testServer);
expect(testServer).toBeTruthy();
});
export default tap.start();

View File

@@ -0,0 +1,333 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.ts';
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.ts';
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.ts';
import { Email } from '../../../ts/mail/core/classes.email.ts';
let testServer: ITestServer;
tap.test('setup test SMTP server', async () => {
testServer = await startTestServer({
port: 2548,
tlsEnabled: false,
authRequired: false
});
expect(testServer).toBeTruthy();
expect(testServer.port).toBeGreaterThan(0);
});
tap.test('CCMD-08: Client handles transaction reset internally', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
// Send first email
const email1 = new Email({
from: 'sender1@example.com',
to: 'recipient1@example.com',
subject: 'First Email',
text: 'This is the first email'
});
const result1 = await smtpClient.sendMail(email1);
expect(result1.success).toBeTrue();
// Send second email - client handles RSET internally if needed
const email2 = new Email({
from: 'sender2@example.com',
to: 'recipient2@example.com',
subject: 'Second Email',
text: 'This is the second email'
});
const result2 = await smtpClient.sendMail(email2);
expect(result2.success).toBeTrue();
console.log('✅ Client handles transaction reset between emails');
console.log('RSET is used internally to ensure clean state');
await smtpClient.close();
});
tap.test('CCMD-08: Clean state after failed recipient', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
// Send email with multiple recipients - if one fails, RSET ensures clean state
const email = new Email({
from: 'sender@example.com',
to: [
'valid1@example.com',
'valid2@example.com',
'valid3@example.com'
],
subject: 'Multi-recipient Email',
text: 'Testing state management'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
// All recipients should be accepted
expect(result.acceptedRecipients.length).toEqual(3);
console.log('✅ State remains clean with multiple recipients');
console.log('Internal RSET ensures proper transaction handling');
await smtpClient.close();
});
tap.test('CCMD-08: Multiple emails in sequence', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
// Send multiple emails in sequence
const emails = [
{
from: 'sender1@example.com',
to: 'recipient1@example.com',
subject: 'Email 1',
text: 'First email'
},
{
from: 'sender2@example.com',
to: 'recipient2@example.com',
subject: 'Email 2',
text: 'Second email'
},
{
from: 'sender3@example.com',
to: 'recipient3@example.com',
subject: 'Email 3',
text: 'Third email'
}
];
for (const emailData of emails) {
const email = new Email(emailData);
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
}
console.log('✅ Successfully sent multiple emails in sequence');
console.log('RSET ensures clean state between each transaction');
await smtpClient.close();
});
tap.test('CCMD-08: Connection pooling with clean state', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 2,
connectionTimeout: 5000,
debug: true
});
// Send emails concurrently
const promises = Array.from({ length: 5 }, (_, i) => {
const email = new Email({
from: `sender${i}@example.com`,
to: `recipient${i}@example.com`,
subject: `Pooled Email ${i}`,
text: `This is pooled email ${i}`
});
return smtpClient.sendMail(email);
});
const results = await Promise.all(promises);
// Check results and log any failures
results.forEach((result, index) => {
console.log(`Email ${index}: ${result.success ? '✅' : '❌'} ${!result.success ? result.error?.message : ''}`);
});
// With connection pooling, at least some emails should succeed
const successCount = results.filter(r => r.success).length;
console.log(`Successfully sent ${successCount} of ${results.length} emails`);
expect(successCount).toBeGreaterThan(0);
console.log('✅ Connection pool maintains clean state');
console.log('RSET ensures each pooled connection starts fresh');
await smtpClient.close();
});
tap.test('CCMD-08: Error recovery with state reset', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
// First, try with invalid sender (should fail early)
try {
const badEmail = new Email({
from: '', // Invalid
to: 'recipient@example.com',
subject: 'Bad Email',
text: 'This should fail'
});
await smtpClient.sendMail(badEmail);
} catch (error) {
// Expected to fail
console.log('✅ Invalid email rejected as expected');
}
// Now send a valid email - should work fine
const goodEmail = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Good Email',
text: 'This should succeed'
});
const result = await smtpClient.sendMail(goodEmail);
expect(result.success).toBeTrue();
console.log('✅ State recovered after error');
console.log('RSET ensures clean state after failures');
await smtpClient.close();
});
tap.test('CCMD-08: Verify command maintains session', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
// verify() creates temporary connection
const verified1 = await smtpClient.verify();
expect(verified1).toBeTrue();
// Send email after verify
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'After Verify',
text: 'Email after verification'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
// verify() again
const verified2 = await smtpClient.verify();
expect(verified2).toBeTrue();
console.log('✅ Verify operations maintain clean session state');
console.log('Each operation ensures proper state management');
await smtpClient.close();
});
tap.test('CCMD-08: Rapid sequential sends', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
// Send emails rapidly
const count = 10;
const startTime = Date.now();
for (let i = 0; i < count; i++) {
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: `Rapid Email ${i}`,
text: `Rapid test email ${i}`
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
}
const elapsed = Date.now() - startTime;
const avgTime = elapsed / count;
console.log(`✅ Sent ${count} emails in ${elapsed}ms`);
console.log(`Average time per email: ${avgTime.toFixed(2)}ms`);
console.log('RSET maintains efficiency in rapid sends');
await smtpClient.close();
});
tap.test('CCMD-08: State isolation between clients', async () => {
// Create two separate clients
const client1 = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000
});
const client2 = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000
});
// Send from both clients
const email1 = new Email({
from: 'client1@example.com',
to: 'recipient1@example.com',
subject: 'From Client 1',
text: 'Email from client 1'
});
const email2 = new Email({
from: 'client2@example.com',
to: 'recipient2@example.com',
subject: 'From Client 2',
text: 'Email from client 2'
});
// Send concurrently
const [result1, result2] = await Promise.all([
client1.sendMail(email1),
client2.sendMail(email2)
]);
expect(result1.success).toBeTrue();
expect(result2.success).toBeTrue();
console.log('✅ Each client maintains isolated state');
console.log('RSET ensures no cross-contamination');
await client1.close();
await client2.close();
});
tap.test('cleanup test SMTP server', async () => {
await stopTestServer(testServer);
expect(testServer).toBeTruthy();
});
export default tap.start();

View File

@@ -0,0 +1,339 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.ts';
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.ts';
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.ts';
import { Email } from '../../../ts/mail/core/classes.email.ts';
let testServer: ITestServer;
let smtpClient: SmtpClient;
tap.test('setup test SMTP server', async () => {
testServer = await startTestServer({
port: 2549,
tlsEnabled: false,
authRequired: false
});
expect(testServer).toBeTruthy();
expect(testServer.port).toBeGreaterThan(0);
});
tap.test('CCMD-09: Connection keepalive test', async () => {
// NOOP is used internally for keepalive - test that connections remain active
smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 10000,
greetingTimeout: 5000,
socketTimeout: 10000
});
// Send an initial email to establish connection
const email1 = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Initial connection test',
text: 'Testing connection establishment'
});
await smtpClient.sendMail(email1);
console.log('First email sent successfully');
// Wait 5 seconds (connection should stay alive with internal NOOP)
await new Promise(resolve => setTimeout(resolve, 5000));
// Send another email on the same connection
const email2 = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Keepalive test',
text: 'Testing connection after delay'
});
await smtpClient.sendMail(email2);
console.log('Second email sent successfully after 5 second delay');
});
tap.test('CCMD-09: Multiple emails in sequence', async () => {
// Test that client can handle multiple emails without issues
// Internal NOOP commands may be used between transactions
const emails = [];
for (let i = 0; i < 5; i++) {
emails.push(new Email({
from: 'sender@example.com',
to: [`recipient${i}@example.com`],
subject: `Sequential email ${i + 1}`,
text: `This is email number ${i + 1}`
}));
}
console.log('Sending 5 emails in sequence...');
for (let i = 0; i < emails.length; i++) {
await smtpClient.sendMail(emails[i]);
console.log(`Email ${i + 1} sent successfully`);
// Small delay between emails
await new Promise(resolve => setTimeout(resolve, 500));
}
console.log('All emails sent successfully');
});
tap.test('CCMD-09: Rapid email sending', async () => {
// Test rapid email sending without delays
// Internal connection management should handle this properly
const emailCount = 10;
const emails = [];
for (let i = 0; i < emailCount; i++) {
emails.push(new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: `Rapid email ${i + 1}`,
text: `Rapid fire email number ${i + 1}`
}));
}
console.log(`Sending ${emailCount} emails rapidly...`);
const startTime = Date.now();
// Send all emails as fast as possible
for (const email of emails) {
await smtpClient.sendMail(email);
}
const elapsed = Date.now() - startTime;
console.log(`All ${emailCount} emails sent in ${elapsed}ms`);
console.log(`Average: ${(elapsed / emailCount).toFixed(2)}ms per email`);
});
tap.test('CCMD-09: Long-lived connection test', async () => {
// Test that connection stays alive over extended period
// SmtpClient should use internal keepalive mechanisms
console.log('Testing connection over 10 seconds with periodic emails...');
const testDuration = 10000;
const emailInterval = 2500;
const iterations = Math.floor(testDuration / emailInterval);
for (let i = 0; i < iterations; i++) {
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: `Keepalive test ${i + 1}`,
text: `Testing connection keepalive - email ${i + 1}`
});
const startTime = Date.now();
await smtpClient.sendMail(email);
const elapsed = Date.now() - startTime;
console.log(`Email ${i + 1} sent in ${elapsed}ms`);
if (i < iterations - 1) {
await new Promise(resolve => setTimeout(resolve, emailInterval));
}
}
console.log('Connection remained stable over 10 seconds');
});
tap.test('CCMD-09: Connection pooling behavior', async () => {
// Test connection pooling with different email patterns
// Internal NOOP may be used to maintain pool connections
const testPatterns = [
{ count: 3, delay: 0, desc: 'Burst of 3 emails' },
{ count: 2, delay: 1000, desc: '2 emails with 1s delay' },
{ count: 1, delay: 3000, desc: '1 email after 3s delay' }
];
for (const pattern of testPatterns) {
console.log(`\nTesting: ${pattern.desc}`);
if (pattern.delay > 0) {
await new Promise(resolve => setTimeout(resolve, pattern.delay));
}
for (let i = 0; i < pattern.count; i++) {
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: `${pattern.desc} - Email ${i + 1}`,
text: 'Testing connection pooling behavior'
});
await smtpClient.sendMail(email);
}
console.log(`Completed: ${pattern.desc}`);
}
});
tap.test('CCMD-09: Email sending performance', async () => {
// Measure email sending performance
// Connection management (including internal NOOP) affects timing
const measurements = 20;
const times: number[] = [];
console.log(`Measuring performance over ${measurements} emails...`);
for (let i = 0; i < measurements; i++) {
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: `Performance test ${i + 1}`,
text: 'Measuring email sending performance'
});
const startTime = Date.now();
await smtpClient.sendMail(email);
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(`\nPerformance analysis (${measurements} emails):`);
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`);
// First email might be slower due to connection establishment
const avgWithoutFirst = times.slice(1).reduce((a, b) => a + b, 0) / (times.length - 1);
console.log(` Average (excl. first): ${avgWithoutFirst.toFixed(2)}ms`);
// Performance should be reasonable
expect(avgTime).toBeLessThan(200);
});
tap.test('CCMD-09: Email with NOOP in content', async () => {
// Test that NOOP as email content doesn't affect delivery
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Email containing NOOP',
text: `This email contains SMTP commands as content:
NOOP
HELO test
MAIL FROM:<test@example.com>
These should be treated as plain text, not commands.
The word NOOP appears multiple times in this email.
NOOP is used internally by SMTP for keepalive.`
});
await smtpClient.sendMail(email);
console.log('Email with NOOP content sent successfully');
// Send another email to verify connection still works
const email2 = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Follow-up email',
text: 'Verifying connection still works after NOOP content'
});
await smtpClient.sendMail(email2);
console.log('Follow-up email sent successfully');
});
tap.test('CCMD-09: Concurrent email sending', async () => {
// Test concurrent email sending
// Connection pooling and internal management should handle this
const concurrentCount = 5;
const emails = [];
for (let i = 0; i < concurrentCount; i++) {
emails.push(new Email({
from: 'sender@example.com',
to: [`recipient${i}@example.com`],
subject: `Concurrent email ${i + 1}`,
text: `Testing concurrent email sending - message ${i + 1}`
}));
}
console.log(`Sending ${concurrentCount} emails concurrently...`);
const startTime = Date.now();
// Send all emails concurrently
try {
await Promise.all(emails.map(email => smtpClient.sendMail(email)));
const elapsed = Date.now() - startTime;
console.log(`All ${concurrentCount} emails sent concurrently in ${elapsed}ms`);
} catch (error) {
// Concurrent sending might not be supported - that's OK
console.log('Concurrent sending not supported, falling back to sequential');
for (const email of emails) {
await smtpClient.sendMail(email);
}
}
});
tap.test('CCMD-09: Connection recovery test', async () => {
// Test connection recovery and error handling
// SmtpClient should handle connection issues gracefully
// Create a new client with shorter timeouts for testing
const testClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 3000,
socketTimeout: 3000
});
// Send initial email
const email1 = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Connection test 1',
text: 'Testing initial connection'
});
await testClient.sendMail(email1);
console.log('Initial email sent');
// Simulate long delay that might timeout connection
console.log('Waiting 5 seconds to test connection recovery...');
await new Promise(resolve => setTimeout(resolve, 5000));
// Try to send another email - client should recover if needed
const email2 = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Connection test 2',
text: 'Testing connection recovery'
});
try {
await testClient.sendMail(email2);
console.log('Email sent successfully after delay - connection recovered');
} catch (error) {
console.log('Connection recovery failed (this might be expected):', error.message);
}
});
tap.test('cleanup test SMTP server', async () => {
if (testServer) {
await stopTestServer(testServer);
}
});
export default tap.start();

View File

@@ -0,0 +1,457 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.ts';
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.ts';
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.ts';
import { Email } from '../../../ts/mail/core/classes.email.ts';
import { EmailValidator } from '../../../ts/mail/core/classes.emailvalidator.ts';
let testServer: ITestServer;
let smtpClient: SmtpClient;
tap.test('setup test SMTP server', async () => {
testServer = await startTestServer({
port: 2550,
tlsEnabled: false,
authRequired: false
});
expect(testServer).toBeTruthy();
expect(testServer.port).toBeGreaterThan(0);
smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000
});
});
tap.test('CCMD-10: Email address validation', async () => {
// Test email address validation which is what VRFY conceptually does
const validator = new EmailValidator();
const testAddresses = [
{ address: 'user@example.com', expected: true },
{ address: 'postmaster@example.com', expected: true },
{ address: 'admin@example.com', expected: true },
{ address: 'user.name+tag@example.com', expected: true },
{ address: 'test@sub.domain.example.com', expected: true },
{ address: 'invalid@', expected: false },
{ address: '@example.com', expected: false },
{ address: 'not-an-email', expected: false },
{ address: '', expected: false },
{ address: 'user@', expected: false }
];
console.log('Testing email address validation (VRFY equivalent):\n');
for (const test of testAddresses) {
const isValid = validator.isValidFormat(test.address);
expect(isValid).toEqual(test.expected);
console.log(`Address: "${test.address}" - Valid: ${isValid} (expected: ${test.expected})`);
}
// Test sending to valid addresses
const validEmail = new Email({
from: 'sender@example.com',
to: ['user@example.com'],
subject: 'Address validation test',
text: 'Testing address validation'
});
await smtpClient.sendMail(validEmail);
console.log('\nEmail sent successfully to validated address');
});
tap.test('CCMD-10: Multiple recipient handling (EXPN equivalent)', async () => {
// Test multiple recipients which is conceptually similar to mailing list expansion
console.log('Testing multiple recipient handling (EXPN equivalent):\n');
// Create email with multiple recipients (like a mailing list)
const multiRecipientEmail = new Email({
from: 'sender@example.com',
to: [
'user1@example.com',
'user2@example.com',
'user3@example.com'
],
cc: [
'cc1@example.com',
'cc2@example.com'
],
bcc: [
'bcc1@example.com'
],
subject: 'Multi-recipient test (mailing list)',
text: 'Testing email distribution to multiple recipients'
});
const toAddresses = multiRecipientEmail.getToAddresses();
const ccAddresses = multiRecipientEmail.getCcAddresses();
const bccAddresses = multiRecipientEmail.getBccAddresses();
console.log(`To recipients: ${toAddresses.length}`);
toAddresses.forEach(addr => console.log(` - ${addr}`));
console.log(`\nCC recipients: ${ccAddresses.length}`);
ccAddresses.forEach(addr => console.log(` - ${addr}`));
console.log(`\nBCC recipients: ${bccAddresses.length}`);
bccAddresses.forEach(addr => console.log(` - ${addr}`));
console.log(`\nTotal recipients: ${toAddresses.length + ccAddresses.length + bccAddresses.length}`);
// Send the email
await smtpClient.sendMail(multiRecipientEmail);
console.log('\nEmail sent successfully to all recipients');
});
tap.test('CCMD-10: Email addresses with display names', async () => {
// Test email addresses with display names (full names)
console.log('Testing email addresses with display names:\n');
const fullNameTests = [
{ from: '"John Doe" <john@example.com>', expectedAddress: 'john@example.com' },
{ from: '"Smith, John" <john.smith@example.com>', expectedAddress: 'john.smith@example.com' },
{ from: 'Mary Johnson <mary@example.com>', expectedAddress: 'mary@example.com' },
{ from: '<bob@example.com>', expectedAddress: 'bob@example.com' }
];
for (const test of fullNameTests) {
const email = new Email({
from: test.from,
to: ['recipient@example.com'],
subject: 'Display name test',
text: `Testing from: ${test.from}`
});
const fromAddress = email.getFromAddress();
console.log(`Full: "${test.from}"`);
console.log(`Extracted: "${fromAddress}"`);
expect(fromAddress).toEqual(test.expectedAddress);
await smtpClient.sendMail(email);
console.log('Email sent successfully\n');
}
});
tap.test('CCMD-10: Email validation security', async () => {
// Test security aspects of email validation
console.log('Testing email validation security considerations:\n');
// Test common system/role addresses that should be handled carefully
const systemAddresses = [
'root@example.com',
'admin@example.com',
'administrator@example.com',
'webmaster@example.com',
'hostmaster@example.com',
'abuse@example.com',
'postmaster@example.com',
'noreply@example.com'
];
const validator = new EmailValidator();
console.log('Checking if addresses are role accounts:');
for (const addr of systemAddresses) {
const validationResult = await validator.validate(addr, { checkRole: true, checkMx: false });
console.log(` ${addr}: ${validationResult.details?.role ? 'Role account' : 'Not a role account'} (format valid: ${validationResult.details?.formatValid})`);
}
// Test that we don't expose information about which addresses exist
console.log('\nTesting information disclosure prevention:');
try {
// Try sending to a non-existent address
const testEmail = new Email({
from: 'sender@example.com',
to: ['definitely-does-not-exist-12345@example.com'],
subject: 'Test',
text: 'Test'
});
await smtpClient.sendMail(testEmail);
console.log('Server accepted email (does not disclose non-existence)');
} catch (error) {
console.log('Server rejected email:', error.message);
}
console.log('\nSecurity best practice: Servers should not disclose address existence');
});
tap.test('CCMD-10: Validation during email sending', async () => {
// Test that validation doesn't interfere with email sending
console.log('Testing validation during email transaction:\n');
const validator = new EmailValidator();
// Create a series of emails with validation between them
const emails = [
{
from: 'sender1@example.com',
to: ['recipient1@example.com'],
subject: 'First email',
text: 'Testing validation during transaction'
},
{
from: 'sender2@example.com',
to: ['recipient2@example.com', 'recipient3@example.com'],
subject: 'Second email',
text: 'Multiple recipients'
},
{
from: '"Test User" <sender3@example.com>',
to: ['recipient4@example.com'],
subject: 'Third email',
text: 'Display name test'
}
];
for (let i = 0; i < emails.length; i++) {
const emailData = emails[i];
// Validate addresses before sending
console.log(`Email ${i + 1}:`);
const fromAddr = emailData.from.includes('<') ? emailData.from.match(/<([^>]+)>/)?.[1] || emailData.from : emailData.from;
console.log(` From: ${emailData.from} - Valid: ${validator.isValidFormat(fromAddr)}`);
for (const to of emailData.to) {
console.log(` To: ${to} - Valid: ${validator.isValidFormat(to)}`);
}
// Create and send email
const email = new Email(emailData);
await smtpClient.sendMail(email);
console.log(` Sent successfully\n`);
}
console.log('All emails sent successfully with validation');
});
tap.test('CCMD-10: Special characters in email addresses', async () => {
// Test email addresses with special characters
console.log('Testing email addresses with special characters:\n');
const validator = new EmailValidator();
const specialAddresses = [
{ address: 'user+tag@example.com', shouldBeValid: true, description: 'Plus addressing' },
{ address: 'first.last@example.com', shouldBeValid: true, description: 'Dots in local part' },
{ address: 'user_name@example.com', shouldBeValid: true, description: 'Underscore' },
{ address: 'user-name@example.com', shouldBeValid: true, description: 'Hyphen' },
{ address: '"quoted string"@example.com', shouldBeValid: true, description: 'Quoted string' },
{ address: 'user@sub.domain.example.com', shouldBeValid: true, description: 'Subdomain' },
{ address: 'user@example.co.uk', shouldBeValid: true, description: 'Multi-part TLD' },
{ address: 'user..name@example.com', shouldBeValid: false, description: 'Double dots' },
{ address: '.user@example.com', shouldBeValid: false, description: 'Leading dot' },
{ address: 'user.@example.com', shouldBeValid: false, description: 'Trailing dot' }
];
for (const test of specialAddresses) {
const isValid = validator.isValidFormat(test.address);
console.log(`${test.description}:`);
console.log(` Address: "${test.address}"`);
console.log(` Valid: ${isValid} (expected: ${test.shouldBeValid})`);
if (test.shouldBeValid && isValid) {
// Try sending an email with this address
try {
const email = new Email({
from: 'sender@example.com',
to: [test.address],
subject: 'Special character test',
text: `Testing special characters in: ${test.address}`
});
await smtpClient.sendMail(email);
console.log(` Email sent successfully`);
} catch (error) {
console.log(` Failed to send: ${error.message}`);
}
}
console.log('');
}
});
tap.test('CCMD-10: Large recipient lists', async () => {
// Test handling of large recipient lists (similar to EXPN multi-line)
console.log('Testing large recipient lists:\n');
// Create email with many recipients
const recipientCount = 20;
const toRecipients = [];
const ccRecipients = [];
for (let i = 1; i <= recipientCount; i++) {
if (i <= 10) {
toRecipients.push(`user${i}@example.com`);
} else {
ccRecipients.push(`user${i}@example.com`);
}
}
console.log(`Creating email with ${recipientCount} total recipients:`);
console.log(` To: ${toRecipients.length} recipients`);
console.log(` CC: ${ccRecipients.length} recipients`);
const largeListEmail = new Email({
from: 'sender@example.com',
to: toRecipients,
cc: ccRecipients,
subject: 'Large distribution list test',
text: `This email is being sent to ${recipientCount} recipients total`
});
// Show extracted addresses
const allTo = largeListEmail.getToAddresses();
const allCc = largeListEmail.getCcAddresses();
console.log('\nExtracted addresses:');
console.log(`To (first 3): ${allTo.slice(0, 3).join(', ')}...`);
console.log(`CC (first 3): ${allCc.slice(0, 3).join(', ')}...`);
// Send the email
const startTime = Date.now();
await smtpClient.sendMail(largeListEmail);
const elapsed = Date.now() - startTime;
console.log(`\nEmail sent to all ${recipientCount} recipients in ${elapsed}ms`);
console.log(`Average: ${(elapsed / recipientCount).toFixed(2)}ms per recipient`);
});
tap.test('CCMD-10: Email validation performance', async () => {
// Test validation performance
console.log('Testing email validation performance:\n');
const validator = new EmailValidator();
const testCount = 1000;
// Generate test addresses
const testAddresses = [];
for (let i = 0; i < testCount; i++) {
testAddresses.push(`user${i}@example${i % 10}.com`);
}
// Time validation
const startTime = Date.now();
let validCount = 0;
for (const address of testAddresses) {
if (validator.isValidFormat(address)) {
validCount++;
}
}
const elapsed = Date.now() - startTime;
const rate = (testCount / elapsed) * 1000;
console.log(`Validated ${testCount} addresses in ${elapsed}ms`);
console.log(`Rate: ${rate.toFixed(0)} validations/second`);
console.log(`Valid addresses: ${validCount}/${testCount}`);
// Test rapid email sending to see if there's rate limiting
console.log('\nTesting rapid email sending:');
const emailCount = 10;
const sendStartTime = Date.now();
let sentCount = 0;
for (let i = 0; i < emailCount; i++) {
try {
const email = new Email({
from: 'sender@example.com',
to: [`recipient${i}@example.com`],
subject: `Rate test ${i + 1}`,
text: 'Testing rate limits'
});
await smtpClient.sendMail(email);
sentCount++;
} catch (error) {
console.log(`Rate limit hit at email ${i + 1}: ${error.message}`);
break;
}
}
const sendElapsed = Date.now() - sendStartTime;
const sendRate = (sentCount / sendElapsed) * 1000;
console.log(`Sent ${sentCount}/${emailCount} emails in ${sendElapsed}ms`);
console.log(`Rate: ${sendRate.toFixed(2)} emails/second`);
});
tap.test('CCMD-10: Email validation error handling', async () => {
// Test error handling for invalid email addresses
console.log('Testing email validation error handling:\n');
const validator = new EmailValidator();
const errorTests = [
{ address: null, description: 'Null address' },
{ address: undefined, description: 'Undefined address' },
{ address: '', description: 'Empty string' },
{ address: ' ', description: 'Whitespace only' },
{ address: '@', description: 'Just @ symbol' },
{ address: 'user@', description: 'Missing domain' },
{ address: '@domain.com', description: 'Missing local part' },
{ address: 'user@@domain.com', description: 'Double @ symbol' },
{ address: 'user@domain@com', description: 'Multiple @ symbols' },
{ address: 'user space@domain.com', description: 'Space in local part' },
{ address: 'user@domain .com', description: 'Space in domain' },
{ address: 'x'.repeat(256) + '@domain.com', description: 'Very long local part' },
{ address: 'user@' + 'x'.repeat(256) + '.com', description: 'Very long domain' }
];
for (const test of errorTests) {
console.log(`${test.description}:`);
console.log(` Input: "${test.address}"`);
// Test validation
let isValid = false;
try {
isValid = validator.isValidFormat(test.address as any);
} catch (error) {
console.log(` Validation threw: ${error.message}`);
}
if (!isValid) {
console.log(` Correctly rejected as invalid`);
} else {
console.log(` WARNING: Accepted as valid!`);
}
// Try to send email with invalid address
if (test.address) {
try {
const email = new Email({
from: 'sender@example.com',
to: [test.address],
subject: 'Error test',
text: 'Testing invalid address'
});
await smtpClient.sendMail(email);
console.log(` WARNING: Email sent with invalid address!`);
} catch (error) {
console.log(` Email correctly rejected: ${error.message}`);
}
}
console.log('');
}
});
tap.test('cleanup test SMTP server', async () => {
if (testServer) {
await stopTestServer(testServer);
}
});
export default tap.start();

View File

@@ -0,0 +1,409 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.ts';
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.ts';
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.ts';
import { Email } from '../../../ts/mail/core/classes.email.ts';
let testServer: ITestServer;
let smtpClient: SmtpClient;
tap.test('setup test SMTP server', async () => {
testServer = await startTestServer({
port: 2551,
tlsEnabled: false,
authRequired: false
});
expect(testServer).toBeTruthy();
expect(testServer.port).toBeGreaterThan(0);
});
tap.test('CCMD-11: Server capabilities discovery', async () => {
// Test server capabilities which is what HELP provides info about
smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000
});
console.log('Testing server capabilities discovery (HELP equivalent):\n');
// Send a test email to see server capabilities in action
const testEmail = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Capability test',
text: 'Testing server capabilities'
});
await smtpClient.sendMail(testEmail);
console.log('Email sent successfully - server supports basic SMTP commands');
// Test different configurations to understand server behavior
const capabilities = {
basicSMTP: true,
multiplRecipients: false,
largeMessages: false,
internationalDomains: false
};
// Test multiple recipients
try {
const multiEmail = new Email({
from: 'sender@example.com',
to: ['recipient1@example.com', 'recipient2@example.com', 'recipient3@example.com'],
subject: 'Multi-recipient test',
text: 'Testing multiple recipients'
});
await smtpClient.sendMail(multiEmail);
capabilities.multiplRecipients = true;
console.log('✓ Server supports multiple recipients');
} catch (error) {
console.log('✗ Multiple recipients not supported');
}
console.log('\nDetected capabilities:', capabilities);
});
tap.test('CCMD-11: Error message diagnostics', async () => {
// Test error messages which HELP would explain
console.log('Testing error message diagnostics:\n');
const errorTests = [
{
description: 'Invalid sender address',
email: {
from: 'invalid-sender',
to: ['recipient@example.com'],
subject: 'Test',
text: 'Test'
}
},
{
description: 'Empty recipient list',
email: {
from: 'sender@example.com',
to: [],
subject: 'Test',
text: 'Test'
}
},
{
description: 'Null subject',
email: {
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: null as any,
text: 'Test'
}
}
];
for (const test of errorTests) {
console.log(`Testing: ${test.description}`);
try {
const email = new Email(test.email);
await smtpClient.sendMail(email);
console.log(' Unexpectedly succeeded');
} catch (error) {
console.log(` Error: ${error.message}`);
console.log(` This would be explained in HELP documentation`);
}
console.log('');
}
});
tap.test('CCMD-11: Connection configuration help', async () => {
// Test different connection configurations
console.log('Testing connection configurations:\n');
const configs = [
{
name: 'Standard connection',
config: {
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000
},
shouldWork: true
},
{
name: 'With greeting timeout',
config: {
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
greetingTimeout: 3000
},
shouldWork: true
},
{
name: 'With socket timeout',
config: {
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
socketTimeout: 10000
},
shouldWork: true
}
];
for (const testConfig of configs) {
console.log(`Testing: ${testConfig.name}`);
try {
const client = createSmtpClient(testConfig.config);
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Config test',
text: `Testing ${testConfig.name}`
});
await client.sendMail(email);
console.log(` ✓ Configuration works`);
} catch (error) {
console.log(` ✗ Error: ${error.message}`);
}
}
});
tap.test('CCMD-11: Protocol flow documentation', async () => {
// Document the protocol flow (what HELP would explain)
console.log('SMTP Protocol Flow (as HELP would document):\n');
const protocolSteps = [
'1. Connection established',
'2. Server sends greeting (220)',
'3. Client sends EHLO',
'4. Server responds with capabilities',
'5. Client sends MAIL FROM',
'6. Server accepts sender (250)',
'7. Client sends RCPT TO',
'8. Server accepts recipient (250)',
'9. Client sends DATA',
'10. Server ready for data (354)',
'11. Client sends message content',
'12. Client sends . to end',
'13. Server accepts message (250)',
'14. Client can send more or QUIT'
];
console.log('Standard SMTP transaction flow:');
protocolSteps.forEach(step => console.log(` ${step}`));
// Demonstrate the flow
console.log('\nDemonstrating flow with actual email:');
const email = new Email({
from: 'demo@example.com',
to: ['recipient@example.com'],
subject: 'Protocol flow demo',
text: 'Demonstrating SMTP protocol flow'
});
await smtpClient.sendMail(email);
console.log('✓ Protocol flow completed successfully');
});
tap.test('CCMD-11: Command availability matrix', async () => {
// Test what commands are available (HELP info)
console.log('Testing command availability:\n');
// Test various email features to determine support
const features = {
plainText: { supported: false, description: 'Plain text emails' },
htmlContent: { supported: false, description: 'HTML emails' },
attachments: { supported: false, description: 'File attachments' },
multipleRecipients: { supported: false, description: 'Multiple recipients' },
ccRecipients: { supported: false, description: 'CC recipients' },
bccRecipients: { supported: false, description: 'BCC recipients' },
customHeaders: { supported: false, description: 'Custom headers' },
priorities: { supported: false, description: 'Email priorities' }
};
// Test plain text
try {
await smtpClient.sendMail(new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Plain text test',
text: 'Plain text content'
}));
features.plainText.supported = true;
} catch (e) {}
// Test HTML
try {
await smtpClient.sendMail(new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'HTML test',
html: '<p>HTML content</p>'
}));
features.htmlContent.supported = true;
} catch (e) {}
// Test multiple recipients
try {
await smtpClient.sendMail(new Email({
from: 'sender@example.com',
to: ['recipient1@example.com', 'recipient2@example.com'],
subject: 'Multiple recipients test',
text: 'Test'
}));
features.multipleRecipients.supported = true;
} catch (e) {}
// Test CC
try {
await smtpClient.sendMail(new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
cc: ['cc@example.com'],
subject: 'CC test',
text: 'Test'
}));
features.ccRecipients.supported = true;
} catch (e) {}
// Test BCC
try {
await smtpClient.sendMail(new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
bcc: ['bcc@example.com'],
subject: 'BCC test',
text: 'Test'
}));
features.bccRecipients.supported = true;
} catch (e) {}
console.log('Feature support matrix:');
Object.entries(features).forEach(([key, value]) => {
console.log(` ${value.description}: ${value.supported ? '✓ Supported' : '✗ Not supported'}`);
});
});
tap.test('CCMD-11: Error code reference', async () => {
// Document error codes (HELP would explain these)
console.log('SMTP Error Code Reference (as HELP would provide):\n');
const errorCodes = [
{ code: '220', meaning: 'Service ready', type: 'Success' },
{ code: '221', meaning: 'Service closing transmission channel', type: 'Success' },
{ code: '250', meaning: 'Requested action completed', type: 'Success' },
{ code: '251', meaning: 'User not local; will forward', type: 'Success' },
{ code: '354', meaning: 'Start mail input', type: 'Intermediate' },
{ code: '421', meaning: 'Service not available', type: 'Temporary failure' },
{ code: '450', meaning: 'Mailbox unavailable', type: 'Temporary failure' },
{ code: '451', meaning: 'Local error in processing', type: 'Temporary failure' },
{ code: '452', meaning: 'Insufficient storage', type: 'Temporary failure' },
{ code: '500', meaning: 'Syntax error', type: 'Permanent failure' },
{ code: '501', meaning: 'Syntax error in parameters', type: 'Permanent failure' },
{ code: '502', meaning: 'Command not implemented', type: 'Permanent failure' },
{ code: '503', meaning: 'Bad sequence of commands', type: 'Permanent failure' },
{ code: '550', meaning: 'Mailbox not found', type: 'Permanent failure' },
{ code: '551', meaning: 'User not local', type: 'Permanent failure' },
{ code: '552', meaning: 'Storage allocation exceeded', type: 'Permanent failure' },
{ code: '553', meaning: 'Mailbox name not allowed', type: 'Permanent failure' },
{ code: '554', meaning: 'Transaction failed', type: 'Permanent failure' }
];
console.log('Common SMTP response codes:');
errorCodes.forEach(({ code, meaning, type }) => {
console.log(` ${code} - ${meaning} (${type})`);
});
// Test triggering some errors
console.log('\nDemonstrating error handling:');
// Invalid email format
try {
await smtpClient.sendMail(new Email({
from: 'invalid-email-format',
to: ['recipient@example.com'],
subject: 'Test',
text: 'Test'
}));
} catch (error) {
console.log(`Invalid format error: ${error.message}`);
}
});
tap.test('CCMD-11: Debugging assistance', async () => {
// Test debugging features (HELP assists with debugging)
console.log('Debugging assistance features:\n');
// Create client with debug enabled
const debugClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
console.log('Sending email with debug mode enabled:');
console.log('(Debug output would show full SMTP conversation)\n');
const debugEmail = new Email({
from: 'debug@example.com',
to: ['recipient@example.com'],
subject: 'Debug test',
text: 'Testing with debug mode'
});
// The debug output will be visible in the console
await debugClient.sendMail(debugEmail);
console.log('\nDebug mode helps troubleshoot:');
console.log('- Connection issues');
console.log('- Authentication problems');
console.log('- Message formatting errors');
console.log('- Server response codes');
console.log('- Protocol violations');
});
tap.test('CCMD-11: Performance benchmarks', async () => {
// Performance info (HELP might mention performance tips)
console.log('Performance benchmarks:\n');
const messageCount = 10;
const startTime = Date.now();
for (let i = 0; i < messageCount; i++) {
const email = new Email({
from: 'perf@example.com',
to: ['recipient@example.com'],
subject: `Performance test ${i + 1}`,
text: 'Testing performance'
});
await smtpClient.sendMail(email);
}
const totalTime = Date.now() - startTime;
const avgTime = totalTime / messageCount;
console.log(`Sent ${messageCount} emails in ${totalTime}ms`);
console.log(`Average time per email: ${avgTime.toFixed(2)}ms`);
console.log(`Throughput: ${(1000 / avgTime).toFixed(2)} emails/second`);
console.log('\nPerformance tips:');
console.log('- Use connection pooling for multiple emails');
console.log('- Enable pipelining when supported');
console.log('- Batch recipients when possible');
console.log('- Use appropriate timeouts');
console.log('- Monitor connection limits');
});
tap.test('cleanup test SMTP server', async () => {
if (testServer) {
await stopTestServer(testServer);
}
});
export default tap.start();