338 lines
12 KiB
TypeScript
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(); |