486 lines
15 KiB
TypeScript
486 lines
15 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';
|
|
const TEST_PORT = 2525;
|
|
|
|
let testServer: ITestServer;
|
|
|
|
tap.test('setup - start test server', async () => {
|
|
testServer = await startTestServer({ port: TEST_PORT });
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
});
|
|
|
|
tap.test('DSN - Extension advertised in EHLO', async (tools) => {
|
|
const done = tools.defer();
|
|
|
|
const socket = net.createConnection({
|
|
host: 'localhost',
|
|
port: TEST_PORT,
|
|
timeout: 30000
|
|
});
|
|
|
|
let dataBuffer = '';
|
|
|
|
socket.on('data', (data) => {
|
|
dataBuffer += data.toString();
|
|
console.log('Server response:', data.toString());
|
|
|
|
if (dataBuffer.includes('220 ') && !dataBuffer.includes('EHLO')) {
|
|
socket.write('EHLO testclient\r\n');
|
|
dataBuffer = '';
|
|
} else if (dataBuffer.includes('250')) {
|
|
// Check if DSN extension is advertised
|
|
const dsnSupported = dataBuffer.toLowerCase().includes('dsn');
|
|
console.log('DSN extension advertised:', dsnSupported);
|
|
|
|
// Parse extensions
|
|
const lines = dataBuffer.split('\r\n');
|
|
const extensions = lines
|
|
.filter(line => line.startsWith('250-') || (line.startsWith('250 ') && lines.indexOf(line) > 0))
|
|
.map(line => line.substring(4).split(' ')[0].toUpperCase());
|
|
|
|
console.log('Server extensions:', extensions);
|
|
|
|
socket.write('QUIT\r\n');
|
|
socket.end();
|
|
done.resolve();
|
|
}
|
|
});
|
|
|
|
socket.on('error', (err) => {
|
|
console.error('Socket error:', err);
|
|
done.reject(err);
|
|
});
|
|
|
|
await done.promise;
|
|
});
|
|
|
|
tap.test('DSN - Success notification request', async (tools) => {
|
|
const done = tools.defer();
|
|
|
|
const socket = net.createConnection({
|
|
host: 'localhost',
|
|
port: TEST_PORT,
|
|
timeout: 30000
|
|
});
|
|
|
|
let dataBuffer = '';
|
|
let step = 'greeting';
|
|
let completed = false;
|
|
|
|
socket.on('data', (data) => {
|
|
dataBuffer += data.toString();
|
|
console.log('Server response:', data.toString());
|
|
|
|
if (step === 'greeting' && dataBuffer.includes('220 ')) {
|
|
step = 'ehlo';
|
|
socket.write('EHLO testclient\r\n');
|
|
dataBuffer = '';
|
|
} else if (step === 'ehlo' && dataBuffer.includes('250')) {
|
|
step = 'mail';
|
|
// MAIL FROM with DSN parameters
|
|
const envId = `dsn-success-${Date.now()}`;
|
|
socket.write(`MAIL FROM:<sender@example.com> RET=FULL ENVID=${envId}\r\n`);
|
|
dataBuffer = '';
|
|
} else if (step === 'mail') {
|
|
const accepted = dataBuffer.includes('250');
|
|
const notSupported = dataBuffer.includes('501') || dataBuffer.includes('555');
|
|
|
|
console.log(`MAIL FROM with DSN: ${accepted ? 'accepted' : notSupported ? 'not supported' : 'error'}`);
|
|
|
|
if (accepted || notSupported) {
|
|
step = 'rcpt';
|
|
// Plain MAIL FROM if DSN not supported
|
|
if (notSupported) {
|
|
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
|
dataBuffer = '';
|
|
} else {
|
|
// RCPT TO with NOTIFY parameter
|
|
socket.write('RCPT TO:<recipient@example.com> NOTIFY=SUCCESS\r\n');
|
|
dataBuffer = '';
|
|
}
|
|
}
|
|
} else if (step === 'rcpt') {
|
|
const accepted = dataBuffer.includes('250');
|
|
const notSupported = dataBuffer.includes('501') || dataBuffer.includes('555');
|
|
|
|
if (notSupported) {
|
|
// DSN not supported, try plain RCPT TO
|
|
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
|
step = 'rcpt_plain';
|
|
dataBuffer = '';
|
|
} else if (accepted) {
|
|
step = 'data';
|
|
socket.write('DATA\r\n');
|
|
dataBuffer = '';
|
|
}
|
|
} else if (step === 'rcpt_plain' && dataBuffer.includes('250')) {
|
|
step = 'data';
|
|
socket.write('DATA\r\n');
|
|
dataBuffer = '';
|
|
} else if (step === 'data' && dataBuffer.includes('354')) {
|
|
const email = [
|
|
`From: sender@example.com`,
|
|
`To: recipient@example.com`,
|
|
`Subject: DSN Test - Success Notification`,
|
|
`Date: ${new Date().toUTCString()}`,
|
|
`Message-ID: <dsn-success-${Date.now()}@example.com>`,
|
|
'',
|
|
'This email tests DSN success notification.',
|
|
'The server should send a success DSN if supported.',
|
|
'.',
|
|
''
|
|
].join('\r\n');
|
|
|
|
socket.write(email);
|
|
step = 'sent';
|
|
dataBuffer = '';
|
|
} else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
|
|
if (!completed) {
|
|
completed = true;
|
|
console.log('Email with DSN success request accepted');
|
|
expect(true).toEqual(true);
|
|
|
|
socket.write('QUIT\r\n');
|
|
socket.end();
|
|
done.resolve();
|
|
}
|
|
}
|
|
});
|
|
|
|
socket.on('error', (err) => {
|
|
console.error('Socket error:', err);
|
|
done.reject(err);
|
|
});
|
|
|
|
await done.promise;
|
|
});
|
|
|
|
tap.test('DSN - Multiple notification types', async (tools) => {
|
|
const done = tools.defer();
|
|
|
|
const socket = net.createConnection({
|
|
host: 'localhost',
|
|
port: TEST_PORT,
|
|
timeout: 30000
|
|
});
|
|
|
|
let dataBuffer = '';
|
|
let step = 'greeting';
|
|
let completed = false;
|
|
|
|
socket.on('data', (data) => {
|
|
dataBuffer += data.toString();
|
|
console.log('Server response:', data.toString());
|
|
|
|
if (step === 'greeting' && dataBuffer.includes('220 ')) {
|
|
step = 'ehlo';
|
|
socket.write('EHLO testclient\r\n');
|
|
dataBuffer = '';
|
|
} else if (step === 'ehlo' && dataBuffer.includes('250')) {
|
|
step = 'mail';
|
|
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
|
dataBuffer = '';
|
|
} else if (step === 'mail' && dataBuffer.includes('250')) {
|
|
step = 'rcpt';
|
|
// Request multiple notification types
|
|
socket.write('RCPT TO:<recipient@example.com> NOTIFY=SUCCESS,FAILURE,DELAY\r\n');
|
|
dataBuffer = '';
|
|
} else if (step === 'rcpt') {
|
|
const accepted = dataBuffer.includes('250');
|
|
const notSupported = dataBuffer.includes('501') || dataBuffer.includes('555');
|
|
|
|
console.log(`Multiple NOTIFY types: ${accepted ? 'accepted' : notSupported ? 'not supported' : 'error'}`);
|
|
|
|
if (notSupported) {
|
|
// Try plain RCPT TO
|
|
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
|
step = 'rcpt_plain';
|
|
dataBuffer = '';
|
|
} else if (accepted) {
|
|
step = 'data';
|
|
socket.write('DATA\r\n');
|
|
dataBuffer = '';
|
|
}
|
|
} else if (step === 'rcpt_plain' && dataBuffer.includes('250')) {
|
|
step = 'data';
|
|
socket.write('DATA\r\n');
|
|
dataBuffer = '';
|
|
} else if (step === 'data' && dataBuffer.includes('354')) {
|
|
const email = [
|
|
`From: sender@example.com`,
|
|
`To: recipient@example.com`,
|
|
`Subject: DSN Test - Multiple Notifications`,
|
|
`Date: ${new Date().toUTCString()}`,
|
|
`Message-ID: <dsn-multi-${Date.now()}@example.com>`,
|
|
'',
|
|
'Testing multiple DSN notification types.',
|
|
'.',
|
|
''
|
|
].join('\r\n');
|
|
|
|
socket.write(email);
|
|
step = 'sent';
|
|
dataBuffer = '';
|
|
} else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
|
|
if (!completed) {
|
|
completed = true;
|
|
console.log('Email with multiple DSN types accepted');
|
|
expect(true).toEqual(true);
|
|
|
|
socket.write('QUIT\r\n');
|
|
socket.end();
|
|
done.resolve();
|
|
}
|
|
}
|
|
});
|
|
|
|
socket.on('error', (err) => {
|
|
console.error('Socket error:', err);
|
|
done.reject(err);
|
|
});
|
|
|
|
await done.promise;
|
|
});
|
|
|
|
tap.test('DSN - Never notify', async (tools) => {
|
|
const done = tools.defer();
|
|
|
|
const socket = net.createConnection({
|
|
host: 'localhost',
|
|
port: TEST_PORT,
|
|
timeout: 30000
|
|
});
|
|
|
|
let dataBuffer = '';
|
|
let step = 'greeting';
|
|
let completed = false;
|
|
|
|
socket.on('data', (data) => {
|
|
dataBuffer += data.toString();
|
|
console.log('Server response:', data.toString());
|
|
|
|
if (step === 'greeting' && dataBuffer.includes('220 ')) {
|
|
step = 'ehlo';
|
|
socket.write('EHLO testclient\r\n');
|
|
dataBuffer = '';
|
|
} else if (step === 'ehlo' && dataBuffer.includes('250')) {
|
|
step = 'mail';
|
|
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
|
dataBuffer = '';
|
|
} else if (step === 'mail' && dataBuffer.includes('250')) {
|
|
step = 'rcpt';
|
|
// Request no notifications
|
|
socket.write('RCPT TO:<recipient@example.com> NOTIFY=NEVER\r\n');
|
|
dataBuffer = '';
|
|
} else if (step === 'rcpt') {
|
|
const accepted = dataBuffer.includes('250');
|
|
const notSupported = dataBuffer.includes('501') || dataBuffer.includes('555');
|
|
|
|
console.log(`NOTIFY=NEVER: ${accepted ? 'accepted' : notSupported ? 'not supported' : 'error'}`);
|
|
expect(accepted || notSupported).toEqual(true);
|
|
|
|
if (notSupported) {
|
|
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
|
step = 'rcpt_plain';
|
|
dataBuffer = '';
|
|
} else if (accepted) {
|
|
step = 'data';
|
|
socket.write('DATA\r\n');
|
|
dataBuffer = '';
|
|
}
|
|
} else if (step === 'rcpt_plain' && dataBuffer.includes('250')) {
|
|
step = 'data';
|
|
socket.write('DATA\r\n');
|
|
dataBuffer = '';
|
|
} else if (step === 'data' && dataBuffer.includes('354')) {
|
|
const email = [
|
|
`From: sender@example.com`,
|
|
`To: recipient@example.com`,
|
|
`Subject: DSN Test - Never Notify`,
|
|
`Date: ${new Date().toUTCString()}`,
|
|
`Message-ID: <dsn-never-${Date.now()}@example.com>`,
|
|
'',
|
|
'This email should not generate any DSN.',
|
|
'.',
|
|
''
|
|
].join('\r\n');
|
|
|
|
socket.write(email);
|
|
step = 'sent';
|
|
dataBuffer = '';
|
|
} else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
|
|
if (!completed) {
|
|
completed = true;
|
|
console.log('Email with NOTIFY=NEVER accepted');
|
|
expect(true).toEqual(true);
|
|
|
|
socket.write('QUIT\r\n');
|
|
socket.end();
|
|
done.resolve();
|
|
}
|
|
}
|
|
});
|
|
|
|
socket.on('error', (err) => {
|
|
console.error('Socket error:', err);
|
|
done.reject(err);
|
|
});
|
|
|
|
await done.promise;
|
|
});
|
|
|
|
tap.test('DSN - Original recipient tracking', async (tools) => {
|
|
const done = tools.defer();
|
|
|
|
const socket = net.createConnection({
|
|
host: 'localhost',
|
|
port: TEST_PORT,
|
|
timeout: 30000
|
|
});
|
|
|
|
let dataBuffer = '';
|
|
let step = 'greeting';
|
|
let completed = false;
|
|
|
|
socket.on('data', (data) => {
|
|
dataBuffer += data.toString();
|
|
console.log('Server response:', data.toString());
|
|
|
|
if (step === 'greeting' && dataBuffer.includes('220 ')) {
|
|
step = 'ehlo';
|
|
socket.write('EHLO testclient\r\n');
|
|
dataBuffer = '';
|
|
} else if (step === 'ehlo' && dataBuffer.includes('250')) {
|
|
step = 'mail';
|
|
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
|
dataBuffer = '';
|
|
} else if (step === 'mail' && dataBuffer.includes('250')) {
|
|
step = 'rcpt';
|
|
// Include original recipient for tracking
|
|
socket.write('RCPT TO:<recipient@example.com> NOTIFY=FAILURE ORCPT=rfc822;original@example.com\r\n');
|
|
dataBuffer = '';
|
|
} else if (step === 'rcpt') {
|
|
const accepted = dataBuffer.includes('250');
|
|
const notSupported = dataBuffer.includes('501') || dataBuffer.includes('555');
|
|
|
|
console.log(`ORCPT parameter: ${accepted ? 'accepted' : notSupported ? 'not supported' : 'error'}`);
|
|
|
|
if (notSupported) {
|
|
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
|
step = 'rcpt_plain';
|
|
dataBuffer = '';
|
|
} else if (accepted) {
|
|
step = 'data';
|
|
socket.write('DATA\r\n');
|
|
dataBuffer = '';
|
|
}
|
|
} else if (step === 'rcpt_plain' && dataBuffer.includes('250')) {
|
|
step = 'data';
|
|
socket.write('DATA\r\n');
|
|
dataBuffer = '';
|
|
} else if (step === 'data' && dataBuffer.includes('354')) {
|
|
const email = [
|
|
`From: sender@example.com`,
|
|
`To: recipient@example.com`,
|
|
`Subject: DSN Test - Original Recipient`,
|
|
`Date: ${new Date().toUTCString()}`,
|
|
`Message-ID: <dsn-orcpt-${Date.now()}@example.com>`,
|
|
'',
|
|
'This email tests ORCPT parameter for tracking.',
|
|
'.',
|
|
''
|
|
].join('\r\n');
|
|
|
|
socket.write(email);
|
|
step = 'sent';
|
|
dataBuffer = '';
|
|
} else if (step === 'sent' && dataBuffer.includes('250 ') && dataBuffer.includes('message queued')) {
|
|
if (!completed) {
|
|
completed = true;
|
|
console.log('Email with ORCPT tracking accepted');
|
|
expect(true).toEqual(true);
|
|
|
|
socket.write('QUIT\r\n');
|
|
socket.end();
|
|
done.resolve();
|
|
}
|
|
}
|
|
});
|
|
|
|
socket.on('error', (err) => {
|
|
console.error('Socket error:', err);
|
|
done.reject(err);
|
|
});
|
|
|
|
await done.promise;
|
|
});
|
|
|
|
tap.test('DSN - Return parameter handling', async (tools) => {
|
|
const done = tools.defer();
|
|
|
|
const socket = net.createConnection({
|
|
host: 'localhost',
|
|
port: TEST_PORT,
|
|
timeout: 30000
|
|
});
|
|
|
|
let dataBuffer = '';
|
|
let step = 'greeting';
|
|
|
|
socket.on('data', (data) => {
|
|
dataBuffer += data.toString();
|
|
console.log('Server response:', data.toString());
|
|
|
|
if (step === 'greeting' && dataBuffer.includes('220 ')) {
|
|
step = 'ehlo';
|
|
socket.write('EHLO testclient\r\n');
|
|
dataBuffer = '';
|
|
} else if (step === 'ehlo' && dataBuffer.includes('250')) {
|
|
step = 'mail_hdrs';
|
|
// Test RET=HDRS
|
|
socket.write('MAIL FROM:<sender@example.com> RET=HDRS\r\n');
|
|
dataBuffer = '';
|
|
} else if (step === 'mail_hdrs') {
|
|
const accepted = dataBuffer.includes('250');
|
|
const notSupported = dataBuffer.includes('501') || dataBuffer.includes('555');
|
|
|
|
console.log(`RET=HDRS: ${accepted ? 'accepted' : notSupported ? 'not supported' : 'error'}`);
|
|
|
|
if (accepted || notSupported) {
|
|
// Reset and test RET=FULL
|
|
socket.write('RSET\r\n');
|
|
step = 'reset';
|
|
dataBuffer = '';
|
|
}
|
|
} else if (step === 'reset' && dataBuffer.includes('250')) {
|
|
step = 'mail_full';
|
|
socket.write('MAIL FROM:<sender@example.com> RET=FULL\r\n');
|
|
dataBuffer = '';
|
|
} else if (step === 'mail_full') {
|
|
const accepted = dataBuffer.includes('250');
|
|
const notSupported = dataBuffer.includes('501') || dataBuffer.includes('555');
|
|
|
|
console.log(`RET=FULL: ${accepted ? 'accepted' : notSupported ? 'not supported' : 'error'}`);
|
|
expect(accepted || notSupported).toEqual(true);
|
|
|
|
socket.write('QUIT\r\n');
|
|
socket.end();
|
|
done.resolve();
|
|
}
|
|
});
|
|
|
|
socket.on('error', (err) => {
|
|
console.error('Socket error:', err);
|
|
done.reject(err);
|
|
});
|
|
|
|
await done.promise;
|
|
});
|
|
|
|
tap.test('cleanup - stop test server', async () => {
|
|
await stopTestServer(testServer);
|
|
});
|
|
|
|
export default tap.start(); |