update
This commit is contained in:
@@ -17,7 +17,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^2.5.2",
|
"@git.zone/tsbuild": "^2.5.2",
|
||||||
"@git.zone/tsrun": "^1.2.8",
|
"@git.zone/tsrun": "^1.3.3",
|
||||||
"@git.zone/tstest": "^1.9.0",
|
"@git.zone/tstest": "^1.9.0",
|
||||||
"@git.zone/tswatch": "^2.0.1",
|
"@git.zone/tswatch": "^2.0.1",
|
||||||
"@types/node": "^22.15.21"
|
"@types/node": "^22.15.21"
|
||||||
|
2
pnpm-lock.yaml
generated
2
pnpm-lock.yaml
generated
@@ -94,7 +94,7 @@ importers:
|
|||||||
specifier: ^2.5.2
|
specifier: ^2.5.2
|
||||||
version: 2.5.2
|
version: 2.5.2
|
||||||
'@git.zone/tsrun':
|
'@git.zone/tsrun':
|
||||||
specifier: ^1.2.8
|
specifier: ^1.3.3
|
||||||
version: 1.3.3
|
version: 1.3.3
|
||||||
'@git.zone/tstest':
|
'@git.zone/tstest':
|
||||||
specifier: ^1.9.0
|
specifier: ^1.9.0
|
||||||
|
258
test/test.mta.ts
258
test/test.mta.ts
@@ -1,258 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import * as plugins from '../ts/plugins.js';
|
|
||||||
import * as paths from '../ts/paths.js';
|
|
||||||
import { SmtpClient } from '../ts/mail/delivery/classes.smtp.client.js';
|
|
||||||
import type { ISmtpClientOptions } from '../ts/mail/delivery/classes.smtp.client.js';
|
|
||||||
import { Email } from '../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests for the SMTP client class
|
|
||||||
*/
|
|
||||||
tap.test('verify SMTP client initialization', async () => {
|
|
||||||
// Create test configuration
|
|
||||||
const options: ISmtpClientOptions = {
|
|
||||||
host: 'smtp.example.com',
|
|
||||||
port: 587,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 10000,
|
|
||||||
domain: 'test.example.com'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create MTA instance
|
|
||||||
const mta = new MailTransferAgent(options);
|
|
||||||
|
|
||||||
// Verify instance was created correctly
|
|
||||||
expect(mta).toBeTruthy();
|
|
||||||
expect(mta.isConnected()).toBeFalsy(); // Should start disconnected
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('test MTA configuration update', async () => {
|
|
||||||
// Create test configuration
|
|
||||||
const options: IMtaConnectionOptions = {
|
|
||||||
host: 'smtp.example.com',
|
|
||||||
port: 587,
|
|
||||||
secure: false
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create MTA instance
|
|
||||||
const mta = new MailTransferAgent(options);
|
|
||||||
|
|
||||||
// Update configuration
|
|
||||||
mta.updateOptions({
|
|
||||||
host: 'new-smtp.example.com',
|
|
||||||
port: 465,
|
|
||||||
secure: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Can't directly test private fields, but we can verify it doesn't throw
|
|
||||||
expect(() => mta.updateOptions({
|
|
||||||
tls: {
|
|
||||||
rejectUnauthorized: false
|
|
||||||
}
|
|
||||||
})).not.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mocked SMTP server for testing
|
|
||||||
class MockSmtpServer {
|
|
||||||
private responses: Map<string, string>;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.responses = new Map();
|
|
||||||
|
|
||||||
// Default responses
|
|
||||||
this.responses.set('connect', '220 smtp.example.com ESMTP ready');
|
|
||||||
this.responses.set('EHLO', '250-smtp.example.com\r\n250-PIPELINING\r\n250-SIZE 10240000\r\n250-STARTTLS\r\n250-AUTH PLAIN LOGIN\r\n250 HELP');
|
|
||||||
this.responses.set('MAIL FROM', '250 OK');
|
|
||||||
this.responses.set('RCPT TO', '250 OK');
|
|
||||||
this.responses.set('DATA', '354 Start mail input; end with <CRLF>.<CRLF>');
|
|
||||||
this.responses.set('data content', '250 OK: message accepted');
|
|
||||||
this.responses.set('QUIT', '221 Bye');
|
|
||||||
}
|
|
||||||
|
|
||||||
public setResponse(command: string, response: string): void {
|
|
||||||
this.responses.set(command, response);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getResponse(command: string): string {
|
|
||||||
if (command.startsWith('MAIL FROM')) {
|
|
||||||
return this.responses.get('MAIL FROM') || '250 OK';
|
|
||||||
} else if (command.startsWith('RCPT TO')) {
|
|
||||||
return this.responses.get('RCPT TO') || '250 OK';
|
|
||||||
} else if (command.startsWith('EHLO') || command.startsWith('HELO')) {
|
|
||||||
return this.responses.get('EHLO') || '250 OK';
|
|
||||||
} else if (command === 'DATA') {
|
|
||||||
return this.responses.get('DATA') || '354 Start mail input; end with <CRLF>.<CRLF>';
|
|
||||||
} else if (command.includes('Content-Type')) {
|
|
||||||
return this.responses.get('data content') || '250 OK: message accepted';
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
return this.responses.get('QUIT') || '221 Bye';
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.responses.get(command) || '250 OK';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This test validates the MTA capabilities without connecting to a real server
|
|
||||||
* It uses a simple mock that only checks method signatures and properties
|
|
||||||
*/
|
|
||||||
tap.test('verify MTA email delivery functionality with mock', async () => {
|
|
||||||
// Create a mock SMTP server
|
|
||||||
const mockServer = new MockSmtpServer();
|
|
||||||
|
|
||||||
// Create a test email
|
|
||||||
const testEmail = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Test Email',
|
|
||||||
text: 'This is a test email'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create MTA options
|
|
||||||
const options: IMtaConnectionOptions = {
|
|
||||||
host: 'smtp.example.com',
|
|
||||||
port: 587,
|
|
||||||
secure: false,
|
|
||||||
domain: 'test.example.com',
|
|
||||||
auth: {
|
|
||||||
user: 'testuser',
|
|
||||||
pass: 'testpass'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create MTA instance
|
|
||||||
const mta = new MailTransferAgent(options);
|
|
||||||
|
|
||||||
// Mock the connect method
|
|
||||||
mta['connect'] = async function() {
|
|
||||||
// @ts-ignore: setting private property for testing
|
|
||||||
this.connected = true;
|
|
||||||
// @ts-ignore: setting private property for testing
|
|
||||||
this.socket = {
|
|
||||||
write: (data: string, callback: () => void) => {
|
|
||||||
callback();
|
|
||||||
},
|
|
||||||
on: () => {},
|
|
||||||
once: () => {},
|
|
||||||
removeListener: () => {},
|
|
||||||
destroy: () => {},
|
|
||||||
setTimeout: () => {}
|
|
||||||
};
|
|
||||||
// @ts-ignore: setting private property for testing
|
|
||||||
this.supportedExtensions = new Set(['PIPELINING', 'SIZE', 'STARTTLS', 'AUTH']);
|
|
||||||
|
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mock the sendCommand method
|
|
||||||
mta['sendCommand'] = async function(command: string) {
|
|
||||||
return Promise.resolve(mockServer.getResponse(command));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mock the readResponse method
|
|
||||||
mta['readResponse'] = async function() {
|
|
||||||
return Promise.resolve(mockServer.getResponse('connect'));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Test sending an email
|
|
||||||
try {
|
|
||||||
const result = await mta.sendMail(testEmail);
|
|
||||||
|
|
||||||
// Verify the result
|
|
||||||
expect(result).toBeTruthy();
|
|
||||||
expect(result.success).toEqual(true);
|
|
||||||
expect(result.acceptedRecipients).toEqual(['recipient@example.com']);
|
|
||||||
expect(result.rejectedRecipients).toEqual([]);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
// This should not happen
|
|
||||||
expect(error).toBeUndefined();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test closing the connection
|
|
||||||
await mta.close();
|
|
||||||
expect(mta.isConnected()).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('test MTA error handling with mock', async () => {
|
|
||||||
// Create a mock SMTP server
|
|
||||||
const mockServer = new MockSmtpServer();
|
|
||||||
|
|
||||||
// Set error response for RCPT TO
|
|
||||||
mockServer.setResponse('RCPT TO', '550 No such user here');
|
|
||||||
|
|
||||||
// Create a test email
|
|
||||||
const testEmail = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['unknown@example.com'],
|
|
||||||
subject: 'Test Email',
|
|
||||||
text: 'This is a test email'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create MTA instance
|
|
||||||
const mta = new MailTransferAgent({
|
|
||||||
host: 'smtp.example.com',
|
|
||||||
port: 587,
|
|
||||||
secure: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mock the connect method
|
|
||||||
mta['connect'] = async function() {
|
|
||||||
// @ts-ignore: setting private property for testing
|
|
||||||
this.connected = true;
|
|
||||||
// @ts-ignore: setting private property for testing
|
|
||||||
this.socket = {
|
|
||||||
write: (data: string, callback: () => void) => {
|
|
||||||
callback();
|
|
||||||
},
|
|
||||||
on: () => {},
|
|
||||||
once: () => {},
|
|
||||||
removeListener: () => {},
|
|
||||||
destroy: () => {},
|
|
||||||
setTimeout: () => {}
|
|
||||||
};
|
|
||||||
// @ts-ignore: setting private property for testing
|
|
||||||
this.supportedExtensions = new Set(['PIPELINING', 'SIZE', 'STARTTLS', 'AUTH']);
|
|
||||||
|
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Mock the sendCommand method
|
|
||||||
mta['sendCommand'] = async function(command: string) {
|
|
||||||
const response = mockServer.getResponse(command);
|
|
||||||
|
|
||||||
// Simulate an error response for RCPT TO
|
|
||||||
if (command.startsWith('RCPT TO') && response.startsWith('550')) {
|
|
||||||
const error = new Error(response);
|
|
||||||
error['context'] = {
|
|
||||||
data: {
|
|
||||||
statusCode: '550'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve(response);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Test sending an email that will fail
|
|
||||||
const result = await mta.sendMail(testEmail);
|
|
||||||
|
|
||||||
// Verify the result shows failure
|
|
||||||
expect(result).toBeTruthy();
|
|
||||||
expect(result.success).toEqual(false);
|
|
||||||
expect(result.acceptedRecipients).toEqual([]);
|
|
||||||
expect(result.rejectedRecipients).toEqual(['unknown@example.com']);
|
|
||||||
expect(result.error).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Final clean-up test
|
|
||||||
tap.test('clean up after tests', async () => {
|
|
||||||
// No-op - just to make sure everything is cleaned up properly
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('stop', async () => {
|
|
||||||
await tap.stopForcefully();
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
168
test/test.smtp.server.ts
Normal file
168
test/test.smtp.server.ts
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
|
import * as plugins from '../ts/plugins.js';
|
||||||
|
import * as paths from '../ts/paths.js';
|
||||||
|
import { SMTPServer } from '../ts/mail/delivery/classes.smtpserver.js';
|
||||||
|
import { UnifiedEmailServer } from '../ts/mail/routing/classes.unified.email.server.js';
|
||||||
|
import { Email } from '../ts/mail/core/classes.email.js';
|
||||||
|
import type { ISmtpServerOptions } from '../ts/mail/delivery/interfaces.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for the SMTP server class
|
||||||
|
*/
|
||||||
|
tap.test('verify SMTP server initialization', async () => {
|
||||||
|
// Mock email server
|
||||||
|
const mockEmailServer = {
|
||||||
|
processEmailByMode: async () => new Email({
|
||||||
|
from: 'test@example.com',
|
||||||
|
to: 'recipient@example.com',
|
||||||
|
subject: 'Test Email',
|
||||||
|
text: 'This is a test email'
|
||||||
|
})
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
// Create test configuration
|
||||||
|
const options: ISmtpServerOptions = {
|
||||||
|
port: 2525, // Use a high port for testing
|
||||||
|
hostname: 'test.example.com',
|
||||||
|
key: '-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAxzYIwlfnr7AK2v6E+c2oYD7nAIXIIvDuvVvZ8R9kyxXIzTXB\nj5D1AgntqKS3bFR1XT8hCVeXjuLKPBvXbhVjG15gXlXxpNiFi1ZcphJvs4zB/Vh7\nZv2ALt3anSIwsJ2rZA/R/GqdJPkHvYf/GMTDLw0YllR0YOevErnRIIM5S58Lj2nT\nCr5v5hK1Gl9mWwRkFQKkWVl2UXt/JX6C7Z6UyJXMZSnoG0Kw6GQje41K5r0Zdzrh\nrGfmb9wSDUn9sZGX6il+oMiYz7UgQkPEzGUZEJxKJwxy8ZgPdSgbvYq4WwPwbBUJ\nlpw0gt5i6HOS7CphRama+zAf5LvfSLoLXSP5JwIDAQABAoIBAQC8C5Ge6wS4LuH9\ntbZFPwjdGHXL+QT2fOFxPBrE7PkeY8UXD7G5Yei6iqqCxJh8nhLQ3DoayhZM69hO\nePOV1Z/LDERCnGel15WKQ1QJ1HZ+JQXnfQrE1Mi9QrXO5bVFtnXIr0mZ+AzwoUmn\nK5fYCvaL3xDZPDzOYL5kZG2hQKgbywGKZoQx16G0dSEhlAHbK9z6XmPRrbUKGzB8\nqV7QGbL7BUTQs5JW/8LpkYr5C0q5THtUVb9mHNR3jPf9WTPQ0D3lxcbLS4PQ8jQ/\nL/GcuHGmsXhe2Unw3w2wpuJKPeHKz4rBNIvaSjIZl9/dIKM88JYQTiIGKErxsC0e\nkczQMp6BAoGBAO0zUN8H7ynXGNNtK/tJo0lI3qg1ZKgr+0CU2L5eU8Bn1oJ1JkCI\nWD3p36NdECx5tGexm9U6MN+HzKYUjnQ6LKzbHQGLZqzF5IL5axXgCn8w4BM+6Ixm\ny8kQgsTKlKRMXIn8RZCmXNnc7v0FhBgpDxPmm7ZUuOPrInd8Ph4mEsePAoGBANb4\n3/izAHnLEp3/sTOZpfWBnDcvEHCG7/JAX0TDRW1FpXiTHpvDV1j3XU3EvLl7WRJ1\nB+B8h/Z6kQtUUxQ3I+zxuQIkQYI8qPu+xhQ8gb5AIO5CMX09+xKUgYjQtm7kYs7W\nL0LD9u3hkGsJk2wfVvMJKb3OSIHeTwRzFCzGX995AoGADkLB8eu/FKAIfwRPCHVE\nsfwMtqjkj2XJ9FeNcRQ5g/Tf8OGnCGEzBwXb05wJVrXUgXp4dBaqYTdAKj8uLEvd\nmi9t/LzR+33cGUdAQHItxcKbsMv00TyNRQUvZFZ7ZEY8aBkv5uZfvJHZ5iQ8C7+g\nHGXNfVGXGPutz/KN6X25CLECgYEAjVLK0MkXzLxCYJRDIhB1TpQVXjpxYUP2Vxls\nSSxfeYqkJPgNvYiHee33xQ8+TP1y9WzkWh+g2AbGmwTuKKL6CvQS9gKVvqqaFB7y\nKrkR13MTPJKvHHdQYKGQqQGgHKh0kGFCC0+PoVwtYs/XU1KpZCE16nNgXrOvTYNN\nHxESa+kCgYB7WOcawTp3WdKP8JbolxIfxax7Kd4QkZhY7dEb4JxBBYXXXpv/NHE9\npcJw4eKDyY+QE2AHPu3+fQYzXopaaTGRpB+ynEfYfD2hW+HnOWfWu/lFJbiwBn/S\nwRsYzSWiLtNplKNFRrsSoMWlh8GOTUpZ7FMLXWhE4rE9NskQBbYq8g==\n-----END RSA PRIVATE KEY-----',
|
||||||
|
cert: '-----BEGIN CERTIFICATE-----\nMIIDazCCAlOgAwIBAgIUcmAewXEYwtzbZmZAJ5inMogKSbowDQYJKoZIhvcNAQEL\nBQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM\nGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDAyMjAwODM4MzRaFw0yNTAy\nMTkwODM4MzRaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw\nHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB\nAQUAA4IBDwAwggEKAoIBAQDHNgjCV+evsAra/oT5zahgPucAhcgi8O69W9nxH2TL\nFcjNNcGPkPUCCe2opLdsVHVdPyEJV5eO4so8G9duFWMbXmBeVfGk2IWLVlymEm+z\njMH9WHtm/YAu3dqdIjCwnatED9H8ap0k+Qd9h/8YxMMvDRiWVHRg568SudEggzlL\nnwuPadMKvm/mErUaX2ZbBGQVAqRZWXZRe38lfoLtnpTIlcxlKegbQrDoZCN7jUrm\nvRl3OuGsZ+Zv3BINSf2xkZfqKX6gyJjPtSBCQ8TMZRkQnEonDHLxmA91KBu9irhb\nA/BsFQmWnDSC3mLoc5LsKmFFqZr7MB/ku99IugtdI/knAgMBAAGjUzBRMB0GA1Ud\nDgQWBBQryyWLuN22OqU1r9HIt2tMLBk42DAfBgNVHSMEGDAWgBQryyWLuN22OqU1\nr9HIt2tMLBk42DAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAe\nCeXQZlXJ2xLnDoOoKY3BpodErNmAwygGYxwDCU0xPbpUMPrQhLI80JlZmfy58gT/\n0ZbULS+srShfEsFnBLmzWLGXDvA/IKCQyTmCQwbPeELGXF6h4URMb+lQL7WL9tY0\nuUg2dA+7CtYokIrOkGqUitPK3yvVhxugkf51WIgKMACZDibOQSWrV5QO2vHOAaO9\nePzRGGl3+Ebmcs3+5w1fI6OLsIZH10lfEnC83C0lO8tIJlGsXMQkCjAcX22rT0rc\nAcxLm07H4EwMwgOAJUkuDjD3y4+KH91jKWF8bhaLZooFB8lccNnaCRiuZRnXlvmf\nM7uVlLGwlj5R9iHd+0dP\n-----END CERTIFICATE-----'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create SMTP server instance
|
||||||
|
const smtpServer = new SMTPServer(mockEmailServer, options);
|
||||||
|
|
||||||
|
// Verify instance was created correctly
|
||||||
|
expect(smtpServer).toBeTruthy();
|
||||||
|
|
||||||
|
// Test that the listen method exists and is callable
|
||||||
|
expect(typeof smtpServer.listen === 'function').toBeTruthy();
|
||||||
|
|
||||||
|
// Test that the close method exists
|
||||||
|
expect(typeof smtpServer.close === 'function').toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('verify SMTP server listen method', async () => {
|
||||||
|
// Mock email server
|
||||||
|
const mockEmailServer = {
|
||||||
|
processEmailByMode: async () => new Email({
|
||||||
|
from: 'test@example.com',
|
||||||
|
to: 'recipient@example.com',
|
||||||
|
subject: 'Test Email',
|
||||||
|
text: 'This is a test email'
|
||||||
|
})
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
// Create test configuration with a different port
|
||||||
|
const options: ISmtpServerOptions = {
|
||||||
|
port: 2526, // Use a different port for this test
|
||||||
|
hostname: 'test.example.com',
|
||||||
|
key: '-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAxzYIwlfnr7AK2v6E+c2oYD7nAIXIIvDuvVvZ8R9kyxXIzTXB\nj5D1AgntqKS3bFR1XT8hCVeXjuLKPBvXbhVjG15gXlXxpNiFi1ZcphJvs4zB/Vh7\nZv2ALt3anSIwsJ2rZA/R/GqdJPkHvYf/GMTDLw0YllR0YOevErnRIIM5S58Lj2nT\nCr5v5hK1Gl9mWwRkFQKkWVl2UXt/JX6C7Z6UyJXMZSnoG0Kw6GQje41K5r0Zdzrh\nrGfmb9wSDUn9sZGX6il+oMiYz7UgQkPEzGUZEJxKJwxy8ZgPdSgbvYq4WwPwbBUJ\nlpw0gt5i6HOS7CphRama+zAf5LvfSLoLXSP5JwIDAQABAoIBAQC8C5Ge6wS4LuH9\ntbZFPwjdGHXL+QT2fOFxPBrE7PkeY8UXD7G5Yei6iqqCxJh8nhLQ3DoayhZM69hO\nePOV1Z/LDERCnGel15WKQ1QJ1HZ+JQXnfQrE1Mi9QrXO5bVFtnXIr0mZ+AzwoUmn\nK5fYCvaL3xDZPDzOYL5kZG2hQKgbywGKZoQx16G0dSEhlAHbK9z6XmPRrbUKGzB8\nqV7QGbL7BUTQs5JW/8LpkYr5C0q5THtUVb9mHNR3jPf9WTPQ0D3lxcbLS4PQ8jQ/\nL/GcuHGmsXhe2Unw3w2wpuJKPeHKz4rBNIvaSjIZl9/dIKM88JYQTiIGKErxsC0e\nkczQMp6BAoGBAO0zUN8H7ynXGNNtK/tJo0lI3qg1ZKgr+0CU2L5eU8Bn1oJ1JkCI\nWD3p36NdECx5tGexm9U6MN+HzKYUjnQ6LKzbHQGLZqzF5IL5axXgCn8w4BM+6Ixm\ny8kQgsTKlKRMXIn8RZCmXNnc7v0FhBgpDxPmm7ZUuOPrInd8Ph4mEsePAoGBANb4\n3/izAHnLEp3/sTOZpfWBnDcvEHCG7/JAX0TDRW1FpXiTHpvDV1j3XU3EvLl7WRJ1\nB+B8h/Z6kQtUUxQ3I+zxuQIkQYI8qPu+xhQ8gb5AIO5CMX09+xKUgYjQtm7kYs7W\nL0LD9u3hkGsJk2wfVvMJKb3OSIHeTwRzFCzGX995AoGADkLB8eu/FKAIfwRPCHVE\nsfwMtqjkj2XJ9FeNcRQ5g/Tf8OGnCGEzBwXb05wJVrXUgXp4dBaqYTdAKj8uLEvd\nmi9t/LzR+33cGUdAQHItxcKbsMv00TyNRQUvZFZ7ZEY8aBkv5uZfvJHZ5iQ8C7+g\nHGXNfVGXGPutz/KN6X25CLECgYEAjVLK0MkXzLxCYJRDIhB1TpQVXjpxYUP2Vxls\nSSxfeYqkJPgNvYiHee33xQ8+TP1y9WzkWh+g2AbGmwTuKKL6CvQS9gKVvqqaFB7y\nKrkR13MTPJKvHHdQYKGQqQGgHKh0kGFCC0+PoVwtYs/XU1KpZCE16nNgXrOvTYNN\nHxESa+kCgYB7WOcawTp3WdKP8JbolxIfxax7Kd4QkZhY7dEb4JxBBYXXXpv/NHE9\npcJw4eKDyY+QE2AHPu3+fQYzXopaaTGRpB+ynEfYfD2hW+HnOWfWu/lFJbiwBn/S\nwRsYzSWiLtNplKNFRrsSoMWlh8GOTUpZ7FMLXWhE4rE9NskQBbYq8g==\n-----END RSA PRIVATE KEY-----',
|
||||||
|
cert: '-----BEGIN CERTIFICATE-----\nMIIDazCCAlOgAwIBAgIUcmAewXEYwtzbZmZAJ5inMogKSbowDQYJKoZIhvcNAQEL\nBQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM\nGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDAyMjAwODM4MzRaFw0yNTAy\nMTkwODM4MzRaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw\nHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB\nAQUAA4IBDwAwggEKAoIBAQDHNgjCV+evsAra/oT5zahgPucAhcgi8O69W9nxH2TL\nFcjNNcGPkPUCCe2opLdsVHVdPyEJV5eO4so8G9duFWMbXmBeVfGk2IWLVlymEm+z\njMH9WHtm/YAu3dqdIjCwnatEA9H8ap0k+Qd9h/8YxMMvDRiWVHRg568SudEggzlL\nnwuPadMKvm/mErUaX2ZbBGQVAqRZWXZRe38lfoLtnpTIlcxlKegbQrDoZCN7jUrm\nvRl3OuGsZ+Zv3BINSf2xkZfqKX6gyJjPtSBCQ8TMZRkQnEonDHLxmA91KBu9irhb\nA/BsFQmWnDSC3mLoc5LsKmFFqZr7MB/ku99IugtdI/knAgMBAAGjUzBRMB0GA1Ud\nDgQWBBQryyWLuN22OqU1r9HIt2tMLBk42DAfBgNVHSMEGDAWgBQryyWLuN22OqU1\nr9HIt2tMLBk42DAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAe\nCeXQZlXJ2xLnDoOoKY3BpodErNmAwygGYxwDCU0xPbpUMPrQhLI80JlZmfy58gT/\n0ZbULS+srShfEsFnBLmzWLGXDvA/IKCQyTmCQwbPeELGXF6h4URMb+lQL7WL9tY0\nuUg2dA+7CtYokIrOkGqUitPK3yvVhxugkf51WIgKMACZDibOQSWrV5QO2vHOAaO9\nePzRGGl3+Ebmcs3+5w1fI6OLsIZH10lfEnC83C0lO8tIJlGsXMQkCjAcX22rT0rc\nAcxLm07H4EwMwgOAJUkuDjD3y4+KH91jKWF8bhaLZooFB8lccNnaCRiuZRnXlvmf\nM7uVlLGwlj5R9iHd+0dP\n-----END CERTIFICATE-----',
|
||||||
|
connectionTimeout: 5000 // Short timeout for tests
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create SMTP server instance
|
||||||
|
const smtpServer = new SMTPServer(mockEmailServer, options);
|
||||||
|
|
||||||
|
// Mock net.Server.listen and net.Server.close to avoid actual networking
|
||||||
|
const originalListen = smtpServer.server.listen;
|
||||||
|
const originalClose = smtpServer.server.close;
|
||||||
|
|
||||||
|
smtpServer.server.listen = function(port, callback) {
|
||||||
|
// Call the original without actually binding
|
||||||
|
if (callback) callback();
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
smtpServer.server.close = function(callback) {
|
||||||
|
if (callback) callback(null);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Test listen method
|
||||||
|
await smtpServer.listen();
|
||||||
|
|
||||||
|
// Should get here without error
|
||||||
|
expect(true).toBeTruthy();
|
||||||
|
|
||||||
|
// Test close method
|
||||||
|
await smtpServer.close();
|
||||||
|
|
||||||
|
// Should get here without error
|
||||||
|
expect(true).toBeTruthy();
|
||||||
|
} finally {
|
||||||
|
// Restore original methods
|
||||||
|
smtpServer.server.listen = originalListen;
|
||||||
|
smtpServer.server.close = originalClose;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('verify SMTP server error handling', async () => {
|
||||||
|
// Mock email server
|
||||||
|
const mockEmailServer = {
|
||||||
|
processEmailByMode: async () => new Email({
|
||||||
|
from: 'test@example.com',
|
||||||
|
to: 'recipient@example.com',
|
||||||
|
subject: 'Test Email',
|
||||||
|
text: 'This is a test email'
|
||||||
|
})
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
// Create test configuration with an invalid port
|
||||||
|
const options: ISmtpServerOptions = {
|
||||||
|
port: 0, // Invalid port
|
||||||
|
hostname: 'test.example.com',
|
||||||
|
key: '-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAxzYIwlfnr7AK2v6E+c2oYD7nAIXIIvDuvVvZ8R9kyxXIzTXB\nj5D1AgntqKS3bFR1XT8hCVeXjuLKPBvXbhVjG15gXlXxpNiFi1ZcphJvs4zB/Vh7\nZv2ALt3anSIwsJ2rZA/R/GqdJPkHvYf/GMTDLw0YllR0YOevErnRIIM5S58Lj2nT\nCr5v5hK1Gl9mWwRkFQKkWVl2UXt/JX6C7Z6UyJXMZSnoG0Kw6GQje41K5r0Zdzrh\nrGfmb9wSDUn9sZGX6il+oMiYz7UgQkPEzGUZEJxKJwxy8ZgPdSgbvYq4WwPwbBUJ\nlpw0gt5i6HOS7CphRama+zAf5LvfSLoLXSP5JwIDAQABAoIBAQC8C5Ge6wS4LuH9\ntbZFPwjdGHXL+QT2fOFxPBrE7PkeY8UXD7G5Yei6iqqCxJh8nhLQ3DoayhZM69hO\nePOV1Z/LDERCnGel15WKQ1QJ1HZ+JQXnfQrE1Mi9QrXO5bVFtnXIr0mZ+AzwoUmn\nK5fYCvaL3xDZPDzOYL5kZG2hQKgbywGKZoQx16G0dSEhlAHbK9z6XmPRrbUKGzB8\nqV7QGbL7BUTQs5JW/8LpkYr5C0q5THtUVb9mHNR3jPf9WTPQ0D3lxcbLS4PQ8jQ/\nL/GcuHGmsXhe2Unw3w2wpuJKPeHKz4rBNIvaSjIZl9/dIKM88JYQTiIGKErxsC0e\nkczQMp6BAoGBAO0zUN8H7ynXGNNtK/tJo0lI3qg1ZKgr+0CU2L5eU8Bn1oJ1JkCI\nWD3p36NdECx5tGexm9U6MN+HzKYUjnQ6LKzbHQGLZqzF5IL5axXgCn8w4BM+6Ixm\ny8kQgsTKlKRMXIn8RZCmXNnc7v0FhBgpDxPmm7ZUuOPrInd8Ph4mEsePAoGBANb4\n3/izAHnLEp3/sTOZpfWBnDcvEHCG7/JAX0TDRW1FpXiTHpvDV1j3XU3EvLl7WRJ1\nB+B8h/Z6kQtUUxQ3I+zxuQIkQYI8qPu+xhQ8gb5AIO5CMX09+xKUgYjQtm7kYs7W\nL0LD9u3hkGsJk2wfVvMJKb3OSIHeTwRzFCzGX995AoGADkLB8eu/FKAIfwRPCHVE\nsfwMtqjkj2XJ9FeNcRQ5g/Tf8OGnCGEzBwXb05wJVrXUgXp4dBaqYTdAKj8uLEvd\nmi9t/LzR+33cGUdAQHItxcKbsMv00TyNRQUvZFZ7ZEY8aBkv5uZfvJHZ5iQ8C7+g\nHGXNfVGXGPutz/KN6X25CLECgYEAjVLK0MkXzLxCYJRDIhB1TpQVXjpxYUP2Vxls\nSSxfeYqkJPgNvYiHee33xQ8+TP1y9WzkWh+g2AbGmwTuKKL6CvQS9gKVvqqaFB7y\nKrkR13MTPJKvHHdQYKGQqQGgHKh0kGFCC0+PoVwtYs/XU1KpZCE16nNgXrOvTYNN\nHxESa+kCgYB7WOcawTp3WdKP8JbolxIfxax7Kd4QkZhY7dEb4JxBBYXXXpv/NHE9\npcJw4eKDyY+QE2AHPu3+fQYzXopaaTGRpB+ynEfYfD2hW+HnOWfWu/lFJbiwBn/S\nwRsYzSWiLtNplKNFRrsSoMWlh8GOTUpZ7FMLXWhE4rE9NskQBbYq8g==\n-----END RSA PRIVATE KEY-----',
|
||||||
|
cert: '-----BEGIN CERTIFICATE-----\nMIIDazCCAlOgAwIBAgIUcmAewXEYwtzbZmZAJ5inMogKSbowDQYJKoZIhvcNAQEL\nBQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM\nGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDAyMjAwODM4MzRaFw0yNTAy\nMTkwODM4MzRaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw\nHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB\nAQUAA4IBDwAwggEKAoIBAQDHNgjCV+evsAra/oT5zahgPucAhcgi8O69W9nxH2TL\nFcjNNcGPkPUCCe2opLdsVHVdPyEJV5eO4so8G9duFWMbXmBeVfGk2IWLVlymEm+z\njMH9WHtm/YAu3dqdIjCwnatEA9H8ap0k+Qd9h/8YxMMvDRiWVHRg568SudEggzlL\nnwuPadMKvm/mErUaX2ZbBGQVAqRZWXZRe38lfoLtnpTIlcxlKegbQrDoZCN7jUrm\nvRl3OuGsZ+Zv3BINSf2xkZfqKX6gyJjPtSBCQ8TMZRkQnEonDHLxmA91KBu9irhb\nA/BsFQmWnDSC3mLoc5LsKmFFqZr7MB/ku99IugtdI/knAgMBAAGjUzBRMB0GA1Ud\nDgQWBBQryyWLuN22OqU1r9HIt2tMLBk42DAfBgNVHSMEGDAWgBQryyWLuN22OqU1\nr9HIt2tMLBk42DAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAe\nCeXQZlXJ2xLnDoOoKY3BpodErNmAwygGYxwDCU0xPbpUMPrQhLI80JlZmfy58gT/\n0ZbULS+srShfEsFnBLmzWLGXDvA/IKCQyTmCQwbPeELGXF6h4URMb+lQL7WL9tY0\nuUg2dA+7CtYokIrOkGqUitPK3yvVhxugkf51WIgKMACZDibOQSWrV5QO2vHOAaO9\nePzRGGl3+Ebmcs3+5w1fI6OLsIZH10lfEnC83C0lO8tIJlGsXMQkCjAcX22rT0rc\nAcxLm07H4EwMwgOAJUkuDjD3y4+KH91jKWF8bhaLZooFB8lccNnaCRiuZRnXlvmf\nM7uVlLGwlj5R9iHd+0dP\n-----END CERTIFICATE-----'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create SMTP server instance
|
||||||
|
const smtpServer = new SMTPServer(mockEmailServer, options);
|
||||||
|
|
||||||
|
// Mock server.listen to simulate an error
|
||||||
|
const originalListen = smtpServer.server.listen;
|
||||||
|
const originalOn = smtpServer.server.on;
|
||||||
|
|
||||||
|
let errorCallback: (err: Error) => void;
|
||||||
|
|
||||||
|
smtpServer.server.listen = function(port, callback) {
|
||||||
|
// Don't call the callback - instead trigger the error event
|
||||||
|
setTimeout(() => {
|
||||||
|
if (errorCallback) {
|
||||||
|
errorCallback(new Error('EACCES: Permission denied'));
|
||||||
|
}
|
||||||
|
}, 10);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
smtpServer.server.on = function(event: string, callback: any) {
|
||||||
|
if (event === 'error') {
|
||||||
|
errorCallback = callback;
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// This should fail with an error
|
||||||
|
try {
|
||||||
|
await smtpServer.listen();
|
||||||
|
// Should not reach here
|
||||||
|
expect(false).toBeTruthy();
|
||||||
|
} catch (error) {
|
||||||
|
// Expect an error
|
||||||
|
expect(error).toBeTruthy();
|
||||||
|
expect(error.message.includes('EACCES')).toBeTruthy();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// Restore original methods
|
||||||
|
smtpServer.server.listen = originalListen;
|
||||||
|
smtpServer.server.on = originalOn as any;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('stop', async () => {
|
||||||
|
await tap.stopForcefully();
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
@@ -21,7 +21,8 @@ import { SmtpState } from './interfaces.js';
|
|||||||
export class SMTPServer {
|
export class SMTPServer {
|
||||||
public emailServerRef: UnifiedEmailServer;
|
public emailServerRef: UnifiedEmailServer;
|
||||||
private smtpServerOptions: ISmtpServerOptions;
|
private smtpServerOptions: ISmtpServerOptions;
|
||||||
private server: plugins.net.Server;
|
// Making server protected so tests can access it
|
||||||
|
protected server: plugins.net.Server;
|
||||||
private sessions: Map<plugins.net.Socket | plugins.tls.TLSSocket, ISmtpSession>;
|
private sessions: Map<plugins.net.Socket | plugins.tls.TLSSocket, ISmtpSession>;
|
||||||
private sessionTimeouts: Map<string, NodeJS.Timeout>;
|
private sessionTimeouts: Map<string, NodeJS.Timeout>;
|
||||||
private hostname: string;
|
private hostname: string;
|
||||||
@@ -55,6 +56,51 @@ export class SMTPServer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the SMTP server and listen on the specified port
|
||||||
|
* @returns A promise that resolves when the server is listening
|
||||||
|
*/
|
||||||
|
public listen(): Promise<void> {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
if (!this.smtpServerOptions.port) {
|
||||||
|
return reject(new Error('SMTP server port not specified'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const port = this.smtpServerOptions.port;
|
||||||
|
|
||||||
|
this.server.listen(port, () => {
|
||||||
|
logger.log('info', `SMTP server listening on port ${port}`);
|
||||||
|
console.log(`SMTP server started on port ${port}`);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.server.on('error', (err) => {
|
||||||
|
logger.log('error', `SMTP server error: ${err.message}`);
|
||||||
|
console.error(`Failed to start SMTP server: ${err.message}`);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the SMTP server
|
||||||
|
* @returns A promise that resolves when the server has stopped
|
||||||
|
*/
|
||||||
|
public close(): Promise<void> {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
this.server.close((err) => {
|
||||||
|
if (err) {
|
||||||
|
logger.log('error', `Error closing SMTP server: ${err.message}`);
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log('info', 'SMTP server stopped');
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean up idle sessions
|
* Clean up idle sessions
|
||||||
* @private
|
* @private
|
||||||
|
@@ -205,6 +205,16 @@ export interface ISmtpServerOptions {
|
|||||||
*/
|
*/
|
||||||
methods: ('PLAIN' | 'LOGIN' | 'OAUTH2')[];
|
methods: ('PLAIN' | 'LOGIN' | 'OAUTH2')[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Socket timeout in milliseconds (default: 5 minutes)
|
||||||
|
*/
|
||||||
|
socketTimeout?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initial connection timeout in milliseconds (default: 30 seconds)
|
||||||
|
*/
|
||||||
|
connectionTimeout?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Reference in New Issue
Block a user