Files
smartmta/test/test.e2e.inbound-smtp.node.ts

240 lines
6.3 KiB
TypeScript
Raw Normal View History

import { tap, expect } from '@git.zone/tstest/tapbundle';
import { UnifiedEmailServer } from '../ts/mail/routing/classes.unified.email.server.js';
import { RustSecurityBridge } from '../ts/security/classes.rustsecuritybridge.js';
import {
connectToSmtp,
waitForGreeting,
sendSmtpCommand,
performSmtpHandshake,
createConcurrentConnections,
createMimeMessage,
} from './helpers/utils.js';
const storageMap = new Map<string, string>();
const mockDcRouter = {
storageManager: {
get: async (key: string) => storageMap.get(key) || null,
set: async (key: string, value: string) => { storageMap.set(key, value); },
},
};
let server: UnifiedEmailServer;
let bridge: RustSecurityBridge;
let bridgeAvailable = false;
tap.test('setup - start server on port 10125', async () => {
RustSecurityBridge.resetInstance();
bridge = RustSecurityBridge.getInstance();
server = new UnifiedEmailServer(mockDcRouter, {
ports: [10125],
hostname: 'test.inbound.local',
domains: [
{
domain: 'testdomain.com',
dnsMode: 'forward',
},
],
routes: [
{
name: 'catch-all',
priority: 0,
match: {
recipients: '*@testdomain.com',
},
action: {
type: 'process',
},
},
],
});
try {
await server.start();
bridgeAvailable = true;
} catch (err) {
console.log(`SKIP: Server failed to start — ${(err as Error).message}`);
console.log('Build the Rust binary with: cd rust && cargo build --release');
}
});
tap.test('EHLO and capability discovery', async () => {
if (!bridgeAvailable) {
console.log('SKIP: bridge not running');
return;
}
const socket = await connectToSmtp('127.0.0.1', 10125, 10000);
const capabilities = await performSmtpHandshake(socket, 'test-client.local');
// Verify we received capabilities from the EHLO response
expect(capabilities.length).toBeGreaterThan(0);
// The server hostname should be in the first capability line
const firstLine = capabilities[0];
expect(firstLine).toBeTruthy();
socket.destroy();
});
tap.test('send valid email - full SMTP transaction', async () => {
if (!bridgeAvailable) {
console.log('SKIP: bridge not running');
return;
}
const socket = await connectToSmtp('127.0.0.1', 10125, 10000);
await waitForGreeting(socket, 10000);
// EHLO
await sendSmtpCommand(socket, 'EHLO test-client.local', '250', 10000);
// MAIL FROM
await sendSmtpCommand(socket, 'MAIL FROM:<sender@example.com>', '250', 10000);
// RCPT TO
await sendSmtpCommand(socket, 'RCPT TO:<user@testdomain.com>', '250', 10000);
// DATA
await sendSmtpCommand(socket, 'DATA', '354', 10000);
// Send MIME message
const mimeMessage = createMimeMessage({
from: 'sender@example.com',
to: 'user@testdomain.com',
subject: 'E2E Test Email',
text: 'This is an end-to-end test email.',
});
// Send the message data followed by the terminator
await new Promise<void>((resolve, reject) => {
let buffer = '';
const timer = setTimeout(() => {
socket.removeAllListeners('data');
reject(new Error('DATA response timeout'));
}, 10000);
const onData = (data: Buffer) => {
buffer += data.toString();
if (buffer.includes('250')) {
clearTimeout(timer);
socket.removeListener('data', onData);
resolve();
}
};
socket.on('data', onData);
socket.write(mimeMessage + '\r\n.\r\n');
});
// QUIT
try {
await sendSmtpCommand(socket, 'QUIT', '221', 5000);
} catch {
// Ignore QUIT errors
}
socket.destroy();
// Verify the email was queued for processing
const stats = server.deliveryQueue.getStats();
expect(stats.queueSize).toBeGreaterThan(0);
});
tap.test('multiple recipients', async () => {
if (!bridgeAvailable) {
console.log('SKIP: bridge not running');
return;
}
const socket = await connectToSmtp('127.0.0.1', 10125, 10000);
await waitForGreeting(socket, 10000);
await sendSmtpCommand(socket, 'EHLO test-client.local', '250', 10000);
await sendSmtpCommand(socket, 'MAIL FROM:<sender@example.com>', '250', 10000);
await sendSmtpCommand(socket, 'RCPT TO:<user1@testdomain.com>', '250', 10000);
await sendSmtpCommand(socket, 'RCPT TO:<user2@testdomain.com>', '250', 10000);
await sendSmtpCommand(socket, 'DATA', '354', 10000);
const mimeMessage = createMimeMessage({
from: 'sender@example.com',
to: 'user1@testdomain.com',
subject: 'Multi-recipient Test',
text: 'Testing multiple recipients.',
});
await new Promise<void>((resolve, reject) => {
let buffer = '';
const timer = setTimeout(() => {
socket.removeAllListeners('data');
reject(new Error('DATA response timeout'));
}, 10000);
const onData = (data: Buffer) => {
buffer += data.toString();
if (buffer.includes('250')) {
clearTimeout(timer);
socket.removeListener('data', onData);
resolve();
}
};
socket.on('data', onData);
socket.write(mimeMessage + '\r\n.\r\n');
});
socket.destroy();
});
tap.test('concurrent connections', async () => {
if (!bridgeAvailable) {
console.log('SKIP: bridge not running');
return;
}
const sockets = await createConcurrentConnections('127.0.0.1', 10125, 3, 10000);
expect(sockets.length).toEqual(3);
// Perform EHLO on each connection
for (const socket of sockets) {
await waitForGreeting(socket, 10000);
await sendSmtpCommand(socket, 'EHLO concurrent-client.local', '250', 10000);
}
// Close all connections
for (const socket of sockets) {
socket.destroy();
}
});
tap.test('RSET mid-session', async () => {
if (!bridgeAvailable) {
console.log('SKIP: bridge not running');
return;
}
const socket = await connectToSmtp('127.0.0.1', 10125, 10000);
await waitForGreeting(socket, 10000);
await sendSmtpCommand(socket, 'EHLO test-client.local', '250', 10000);
// Start a transaction
await sendSmtpCommand(socket, 'MAIL FROM:<sender@example.com>', '250', 10000);
// Reset the transaction
await sendSmtpCommand(socket, 'RSET', '250', 10000);
// Start a new transaction after RSET
await sendSmtpCommand(socket, 'MAIL FROM:<other@example.com>', '250', 10000);
socket.destroy();
});
tap.test('cleanup - stop server', async () => {
if (bridgeAvailable) {
await server.stop();
}
await tap.stopForcefully();
});
export default tap.start();