2025-05-23 19:49:25 +00:00
|
|
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
2025-05-23 19:03:44 +00:00
|
|
|
import * as net from 'net';
|
2025-05-23 19:49:25 +00:00
|
|
|
import { startTestServer, stopTestServer, type ITestServer } from '../server.loader.js';
|
2025-05-23 19:03:44 +00:00
|
|
|
|
2025-05-23 19:49:25 +00:00
|
|
|
const TEST_PORT = 2525;
|
2025-05-23 19:03:44 +00:00
|
|
|
const TEST_TIMEOUT = 30000;
|
|
|
|
|
|
|
|
tap.test('Plain Connection - should establish basic TCP connection', async (tools) => {
|
|
|
|
const done = tools.defer();
|
|
|
|
|
|
|
|
// Start test server
|
2025-05-23 19:49:25 +00:00
|
|
|
const testServer = await startTestServer();
|
2025-05-23 19:03:44 +00:00
|
|
|
|
2025-05-23 19:49:25 +00:00
|
|
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 1000));try {
|
2025-05-23 19:03:44 +00:00
|
|
|
const socket = net.createConnection({
|
|
|
|
host: 'localhost',
|
|
|
|
port: TEST_PORT,
|
|
|
|
timeout: TEST_TIMEOUT
|
|
|
|
});
|
|
|
|
|
|
|
|
const connected = await new Promise<boolean>((resolve) => {
|
|
|
|
socket.once('connect', () => resolve(true));
|
|
|
|
socket.once('error', () => resolve(false));
|
|
|
|
setTimeout(() => resolve(false), 5000);
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(connected).toBeTrue();
|
|
|
|
|
|
|
|
if (connected) {
|
|
|
|
console.log('Plain connection established:');
|
|
|
|
console.log('- Local:', `${socket.localAddress}:${socket.localPort}`);
|
|
|
|
console.log('- Remote:', `${socket.remoteAddress}:${socket.remotePort}`);
|
|
|
|
|
|
|
|
// Close connection
|
|
|
|
socket.destroy();
|
|
|
|
}
|
|
|
|
|
|
|
|
} finally {
|
2025-05-23 19:49:25 +00:00
|
|
|
await stopTestServer();
|
2025-05-23 19:03:44 +00:00
|
|
|
done.resolve();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
tap.test('Plain Connection - should receive SMTP banner on plain connection', async (tools) => {
|
|
|
|
const done = tools.defer();
|
|
|
|
|
|
|
|
// Start test server
|
2025-05-23 19:49:25 +00:00
|
|
|
const testServer = await startTestServer();
|
|
|
|
|
2025-05-23 19:03:44 +00:00
|
|
|
|
2025-05-23 19:49:25 +00:00
|
|
|
await new Promise(resolve => setTimeout(resolve, 1000));try {
|
2025-05-23 19:03:44 +00:00
|
|
|
const socket = net.createConnection({
|
|
|
|
host: 'localhost',
|
|
|
|
port: TEST_PORT,
|
|
|
|
timeout: TEST_TIMEOUT
|
|
|
|
});
|
|
|
|
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
|
|
socket.once('connect', () => resolve());
|
|
|
|
socket.once('error', reject);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Get banner
|
|
|
|
const banner = await new Promise<string>((resolve) => {
|
|
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
|
|
});
|
|
|
|
|
|
|
|
console.log('Received banner:', banner.trim());
|
|
|
|
|
|
|
|
expect(banner).toInclude('220');
|
|
|
|
expect(banner).toInclude('ESMTP');
|
|
|
|
|
|
|
|
// Clean up
|
|
|
|
socket.write('QUIT\r\n');
|
|
|
|
socket.end();
|
|
|
|
|
|
|
|
} finally {
|
2025-05-23 19:49:25 +00:00
|
|
|
await stopTestServer();
|
2025-05-23 19:03:44 +00:00
|
|
|
done.resolve();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
tap.test('Plain Connection - should complete full SMTP transaction on plain connection', async (tools) => {
|
|
|
|
const done = tools.defer();
|
|
|
|
|
|
|
|
// Start test server
|
2025-05-23 19:49:25 +00:00
|
|
|
const testServer = await startTestServer();
|
2025-05-23 19:03:44 +00:00
|
|
|
|
2025-05-23 19:49:25 +00:00
|
|
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 1000));try {
|
2025-05-23 19:03:44 +00:00
|
|
|
const socket = net.createConnection({
|
|
|
|
host: 'localhost',
|
|
|
|
port: TEST_PORT,
|
|
|
|
timeout: TEST_TIMEOUT
|
|
|
|
});
|
|
|
|
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
|
|
socket.once('connect', () => resolve());
|
|
|
|
socket.once('error', reject);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Get banner
|
|
|
|
await new Promise<string>((resolve) => {
|
|
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
|
|
});
|
|
|
|
|
|
|
|
// Send EHLO
|
|
|
|
socket.write('EHLO testhost\r\n');
|
|
|
|
|
|
|
|
const ehloResponse = await new Promise<string>((resolve) => {
|
|
|
|
let data = '';
|
|
|
|
const handler = (chunk: Buffer) => {
|
|
|
|
data += chunk.toString();
|
|
|
|
if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) {
|
|
|
|
socket.removeListener('data', handler);
|
|
|
|
resolve(data);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
socket.on('data', handler);
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(ehloResponse).toInclude('250');
|
|
|
|
console.log('EHLO successful on plain connection');
|
|
|
|
|
|
|
|
// Send MAIL FROM
|
|
|
|
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
|
|
|
|
|
|
|
const mailResponse = await new Promise<string>((resolve) => {
|
|
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(mailResponse).toInclude('250');
|
|
|
|
|
|
|
|
// Send RCPT TO
|
|
|
|
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
|
|
|
|
|
|
|
const rcptResponse = await new Promise<string>((resolve) => {
|
|
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(rcptResponse).toInclude('250');
|
|
|
|
|
|
|
|
// Send DATA
|
|
|
|
socket.write('DATA\r\n');
|
|
|
|
|
|
|
|
const dataResponse = await new Promise<string>((resolve) => {
|
|
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(dataResponse).toInclude('354');
|
|
|
|
|
|
|
|
// Send email content
|
|
|
|
const emailContent =
|
|
|
|
'From: sender@example.com\r\n' +
|
|
|
|
'To: recipient@example.com\r\n' +
|
|
|
|
'Subject: Plain Connection Test\r\n' +
|
|
|
|
'\r\n' +
|
|
|
|
'This email was sent over a plain connection.\r\n' +
|
|
|
|
'.\r\n';
|
|
|
|
|
|
|
|
socket.write(emailContent);
|
|
|
|
|
|
|
|
const finalResponse = await new Promise<string>((resolve) => {
|
|
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(finalResponse).toInclude('250');
|
|
|
|
console.log('Email sent successfully over plain connection');
|
|
|
|
|
|
|
|
// Clean up
|
|
|
|
socket.write('QUIT\r\n');
|
|
|
|
|
|
|
|
const quitResponse = await new Promise<string>((resolve) => {
|
|
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(quitResponse).toInclude('221');
|
|
|
|
socket.end();
|
|
|
|
|
|
|
|
} finally {
|
2025-05-23 19:49:25 +00:00
|
|
|
await stopTestServer();
|
2025-05-23 19:03:44 +00:00
|
|
|
done.resolve();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
tap.test('Plain Connection - should handle multiple plain connections', async (tools) => {
|
|
|
|
const done = tools.defer();
|
|
|
|
|
|
|
|
// Start test server
|
2025-05-23 19:49:25 +00:00
|
|
|
const testServer = await startTestServer();
|
|
|
|
|
2025-05-23 19:03:44 +00:00
|
|
|
|
2025-05-23 19:49:25 +00:00
|
|
|
await new Promise(resolve => setTimeout(resolve, 1000));try {
|
2025-05-23 19:03:44 +00:00
|
|
|
const connectionCount = 3;
|
|
|
|
const connections: net.Socket[] = [];
|
|
|
|
|
|
|
|
// Create multiple connections
|
|
|
|
for (let i = 0; i < connectionCount; i++) {
|
|
|
|
const socket = net.createConnection({
|
|
|
|
host: 'localhost',
|
|
|
|
port: TEST_PORT,
|
|
|
|
timeout: TEST_TIMEOUT
|
|
|
|
});
|
|
|
|
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
|
|
socket.once('connect', () => {
|
|
|
|
connections.push(socket);
|
|
|
|
resolve();
|
|
|
|
});
|
|
|
|
socket.once('error', reject);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Get banner
|
|
|
|
const banner = await new Promise<string>((resolve) => {
|
|
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(banner).toInclude('220');
|
|
|
|
console.log(`Connection ${i + 1} established`);
|
|
|
|
}
|
|
|
|
|
|
|
|
expect(connections.length).toBe(connectionCount);
|
|
|
|
console.log(`All ${connectionCount} plain connections established successfully`);
|
|
|
|
|
|
|
|
// Clean up all connections
|
|
|
|
for (const socket of connections) {
|
|
|
|
socket.write('QUIT\r\n');
|
|
|
|
socket.end();
|
|
|
|
}
|
|
|
|
|
|
|
|
} finally {
|
2025-05-23 19:49:25 +00:00
|
|
|
await stopTestServer();
|
2025-05-23 19:03:44 +00:00
|
|
|
done.resolve();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
tap.test('Plain Connection - should work on standard SMTP port 25', async (tools) => {
|
|
|
|
const done = tools.defer();
|
|
|
|
|
|
|
|
// Test port 25 (standard SMTP port)
|
|
|
|
const SMTP_PORT = 25;
|
|
|
|
|
|
|
|
// Note: Port 25 might require special permissions or might be blocked
|
|
|
|
// We'll test the connection but handle failures gracefully
|
|
|
|
const socket = net.createConnection({
|
|
|
|
host: 'localhost',
|
|
|
|
port: SMTP_PORT,
|
|
|
|
timeout: 5000
|
|
|
|
});
|
|
|
|
|
|
|
|
const result = await new Promise<{connected: boolean, error?: string}>((resolve) => {
|
|
|
|
socket.once('connect', () => {
|
|
|
|
socket.destroy();
|
|
|
|
resolve({ connected: true });
|
|
|
|
});
|
|
|
|
|
|
|
|
socket.once('error', (err) => {
|
|
|
|
resolve({
|
|
|
|
connected: false,
|
|
|
|
error: err.message
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
socket.destroy();
|
|
|
|
resolve({
|
|
|
|
connected: false,
|
|
|
|
error: 'Connection timeout'
|
|
|
|
});
|
|
|
|
}, 5000);
|
|
|
|
});
|
|
|
|
|
|
|
|
if (result.connected) {
|
|
|
|
console.log('Successfully connected to port 25 (standard SMTP)');
|
|
|
|
} else {
|
|
|
|
console.log(`Could not connect to port 25: ${result.error}`);
|
|
|
|
console.log('This is expected if port 25 is blocked or requires privileges');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Test passes regardless - port 25 connectivity is environment-dependent
|
|
|
|
expect(true).toBeTrue();
|
|
|
|
|
|
|
|
done.resolve();
|
|
|
|
});
|
|
|
|
|
|
|
|
tap.start();
|