dcrouter/test/suite/smtpserver_email-processing/test.ep-01.basic-email-sending.ts
2025-05-25 19:05:43 +00:00

338 lines
12 KiB
TypeScript

import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as net from 'net';
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'
import type { ITestServer } from '../../helpers/server.loader.js';
// Test configuration
const TEST_PORT = 2525;
const TEST_TIMEOUT = 15000;
let testServer: ITestServer;
// Setup
tap.test('setup - start SMTP server', async () => {
testServer = await startTestServer({ port: TEST_PORT });
await new Promise(resolve => setTimeout(resolve, 1000));
});
// Test: Complete email sending flow
tap.test('Basic Email Sending - should send email through complete SMTP flow', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: TEST_TIMEOUT
});
let receivedData = '';
let currentStep = 'connecting';
const fromAddress = 'sender@example.com';
const toAddress = 'recipient@example.com';
const emailContent = `Subject: Production Test Email\r\nFrom: ${fromAddress}\r\nTo: ${toAddress}\r\nDate: ${new Date().toUTCString()}\r\n\r\nThis is a test email sent during production testing.\r\nTest ID: EP-01\r\nTimestamp: ${Date.now()}\r\n`;
const steps: string[] = [];
socket.on('data', (data) => {
receivedData += data.toString();
if (currentStep === 'connecting' && receivedData.includes('220')) {
steps.push('CONNECT');
currentStep = 'ehlo';
socket.write('EHLO test.example.com\r\n');
} else if (currentStep === 'ehlo' && receivedData.includes('250')) {
steps.push('EHLO');
currentStep = 'mail_from';
socket.write(`MAIL FROM:<${fromAddress}>\r\n`);
} else if (currentStep === 'mail_from' && receivedData.includes('250')) {
steps.push('MAIL FROM');
currentStep = 'rcpt_to';
socket.write(`RCPT TO:<${toAddress}>\r\n`);
} else if (currentStep === 'rcpt_to' && receivedData.includes('250')) {
steps.push('RCPT TO');
currentStep = 'data';
socket.write('DATA\r\n');
} else if (currentStep === 'data' && receivedData.includes('354')) {
steps.push('DATA');
currentStep = 'email_content';
socket.write(emailContent);
socket.write('\r\n.\r\n'); // End of data marker
} else if (currentStep === 'email_content' && receivedData.includes('250')) {
steps.push('CONTENT');
currentStep = 'quit';
socket.write('QUIT\r\n');
} else if (currentStep === 'quit' && receivedData.includes('221')) {
steps.push('QUIT');
socket.destroy();
// Verify all steps completed
expect(steps).toInclude('CONNECT');
expect(steps).toInclude('EHLO');
expect(steps).toInclude('MAIL FROM');
expect(steps).toInclude('RCPT TO');
expect(steps).toInclude('DATA');
expect(steps).toInclude('CONTENT');
expect(steps).toInclude('QUIT');
expect(steps.length).toEqual(7);
done.resolve();
} else if (receivedData.match(/\r\n5\d{2}\s/)) {
// Server error (5xx response codes)
socket.destroy();
done.reject(new Error(`Email sending failed at step ${currentStep}: ${receivedData}`));
}
});
socket.on('error', (error) => {
done.reject(error);
});
socket.on('timeout', () => {
socket.destroy();
done.reject(new Error(`Connection timeout at step: ${currentStep}`));
});
await done.promise;
});
// Test: Send email with attachments (MIME)
tap.test('Basic Email Sending - should send email with MIME attachment', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: TEST_TIMEOUT
});
let receivedData = '';
let currentStep = 'connecting';
const fromAddress = 'sender@example.com';
const toAddress = 'recipient@example.com';
const boundary = '----=_Part_0_1234567890';
const emailContent = `Subject: Email with Attachment\r\nFrom: ${fromAddress}\r\nTo: ${toAddress}\r\nMIME-Version: 1.0\r\nContent-Type: multipart/mixed; boundary="${boundary}"\r\n\r\n--${boundary}\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\nThis email contains an attachment.\r\n\r\n--${boundary}\r\nContent-Type: text/plain; name="test.txt"\r\nContent-Disposition: attachment; filename="test.txt"\r\nContent-Transfer-Encoding: base64\r\n\r\nVGhpcyBpcyBhIHRlc3QgZmlsZS4=\r\n\r\n--${boundary}--\r\n`;
socket.on('data', (data) => {
receivedData += data.toString();
if (currentStep === 'connecting' && receivedData.includes('220')) {
currentStep = 'ehlo';
socket.write('EHLO test.example.com\r\n');
} else if (currentStep === 'ehlo' && receivedData.includes('250')) {
currentStep = 'mail_from';
socket.write(`MAIL FROM:<${fromAddress}>\r\n`);
} else if (currentStep === 'mail_from' && receivedData.includes('250')) {
currentStep = 'rcpt_to';
socket.write(`RCPT TO:<${toAddress}>\r\n`);
} else if (currentStep === 'rcpt_to' && receivedData.includes('250')) {
currentStep = 'data';
socket.write('DATA\r\n');
} else if (currentStep === 'data' && receivedData.includes('354')) {
currentStep = 'email_content';
socket.write(emailContent);
socket.write('\r\n.\r\n'); // End of data marker
} else if (currentStep === 'email_content' && receivedData.includes('250')) {
socket.write('QUIT\r\n');
setTimeout(() => {
socket.destroy();
expect(receivedData).toInclude('250');
done.resolve();
}, 100);
}
});
socket.on('error', (error) => {
done.reject(error);
});
socket.on('timeout', () => {
socket.destroy();
done.reject(new Error(`Connection timeout at step: ${currentStep}`));
});
await done.promise;
});
// Test: Send HTML email
tap.test('Basic Email Sending - should send HTML email', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: TEST_TIMEOUT
});
let receivedData = '';
let currentStep = 'connecting';
const fromAddress = 'sender@example.com';
const toAddress = 'recipient@example.com';
const boundary = '----=_Part_0_987654321';
const emailContent = `Subject: HTML Email Test\r\nFrom: ${fromAddress}\r\nTo: ${toAddress}\r\nMIME-Version: 1.0\r\nContent-Type: multipart/alternative; boundary="${boundary}"\r\n\r\n--${boundary}\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\nThis is the plain text version.\r\n\r\n--${boundary}\r\nContent-Type: text/html; charset=UTF-8\r\n\r\n<html><body><h1>HTML Email</h1><p>This is the <strong>HTML</strong> version.</p></body></html>\r\n\r\n--${boundary}--\r\n`;
socket.on('data', (data) => {
receivedData += data.toString();
if (currentStep === 'connecting' && receivedData.includes('220')) {
currentStep = 'ehlo';
socket.write('EHLO test.example.com\r\n');
} else if (currentStep === 'ehlo' && receivedData.includes('250')) {
currentStep = 'mail_from';
socket.write(`MAIL FROM:<${fromAddress}>\r\n`);
} else if (currentStep === 'mail_from' && receivedData.includes('250')) {
currentStep = 'rcpt_to';
socket.write(`RCPT TO:<${toAddress}>\r\n`);
} else if (currentStep === 'rcpt_to' && receivedData.includes('250')) {
currentStep = 'data';
socket.write('DATA\r\n');
} else if (currentStep === 'data' && receivedData.includes('354')) {
currentStep = 'email_content';
socket.write(emailContent);
socket.write('\r\n.\r\n'); // End of data marker
} else if (currentStep === 'email_content' && receivedData.includes('250')) {
socket.write('QUIT\r\n');
setTimeout(() => {
socket.destroy();
expect(receivedData).toInclude('250');
done.resolve();
}, 100);
}
});
socket.on('error', (error) => {
done.reject(error);
});
socket.on('timeout', () => {
socket.destroy();
done.reject(new Error(`Connection timeout at step: ${currentStep}`));
});
await done.promise;
});
// Test: Send email with custom headers
tap.test('Basic Email Sending - should send email with custom headers', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: TEST_TIMEOUT
});
let receivedData = '';
let currentStep = 'connecting';
const fromAddress = 'sender@example.com';
const toAddress = 'recipient@example.com';
const emailContent = `Subject: Custom Headers Test\r\nFrom: ${fromAddress}\r\nTo: ${toAddress}\r\nX-Custom-Header: CustomValue\r\nX-Priority: 1\r\nX-Mailer: SMTP Test Suite\r\nReply-To: noreply@example.com\r\nOrganization: Test Organization\r\n\r\nThis email contains custom headers.\r\n`;
socket.on('data', (data) => {
receivedData += data.toString();
if (currentStep === 'connecting' && receivedData.includes('220')) {
currentStep = 'ehlo';
socket.write('EHLO test.example.com\r\n');
} else if (currentStep === 'ehlo' && receivedData.includes('250')) {
currentStep = 'mail_from';
socket.write(`MAIL FROM:<${fromAddress}>\r\n`);
} else if (currentStep === 'mail_from' && receivedData.includes('250')) {
currentStep = 'rcpt_to';
socket.write(`RCPT TO:<${toAddress}>\r\n`);
} else if (currentStep === 'rcpt_to' && receivedData.includes('250')) {
currentStep = 'data';
socket.write('DATA\r\n');
} else if (currentStep === 'data' && receivedData.includes('354')) {
currentStep = 'email_content';
socket.write(emailContent);
socket.write('\r\n.\r\n'); // End of data marker
} else if (currentStep === 'email_content' && receivedData.includes('250')) {
socket.write('QUIT\r\n');
setTimeout(() => {
socket.destroy();
expect(receivedData).toInclude('250');
done.resolve();
}, 100);
}
});
socket.on('error', (error) => {
done.reject(error);
});
socket.on('timeout', () => {
socket.destroy();
done.reject(new Error(`Connection timeout at step: ${currentStep}`));
});
await done.promise;
});
// Test: Minimal email (only required headers)
tap.test('Basic Email Sending - should send minimal email', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: TEST_TIMEOUT
});
let receivedData = '';
let currentStep = 'connecting';
const fromAddress = 'sender@example.com';
const toAddress = 'recipient@example.com';
// Minimal email - just a body, no headers
const emailContent = 'This is a minimal email with no headers.\r\n';
socket.on('data', (data) => {
receivedData += data.toString();
if (currentStep === 'connecting' && receivedData.includes('220')) {
currentStep = 'ehlo';
socket.write('EHLO test.example.com\r\n');
} else if (currentStep === 'ehlo' && receivedData.includes('250')) {
currentStep = 'mail_from';
socket.write(`MAIL FROM:<${fromAddress}>\r\n`);
} else if (currentStep === 'mail_from' && receivedData.includes('250')) {
currentStep = 'rcpt_to';
socket.write(`RCPT TO:<${toAddress}>\r\n`);
} else if (currentStep === 'rcpt_to' && receivedData.includes('250')) {
currentStep = 'data';
socket.write('DATA\r\n');
} else if (currentStep === 'data' && receivedData.includes('354')) {
currentStep = 'email_content';
socket.write(emailContent);
socket.write('\r\n.\r\n'); // End of data marker
} else if (currentStep === 'email_content' && receivedData.includes('250')) {
socket.write('QUIT\r\n');
setTimeout(() => {
socket.destroy();
expect(receivedData).toInclude('250');
done.resolve();
}, 100);
}
});
socket.on('error', (error) => {
done.reject(error);
});
socket.on('timeout', () => {
socket.destroy();
done.reject(new Error(`Connection timeout at step: ${currentStep}`));
});
await done.promise;
});
// Teardown
tap.test('teardown - stop SMTP server', async () => {
await stopTestServer(testServer);
});
// Start the test
export default tap.start();