Replace legacy domain-rule based routing with flexible route-based system that supports: - Multi-criteria matching (recipients, senders, IPs, authentication) - Four action types (forward, process, deliver, reject) - Moved DKIM signing to delivery phase for signature validity - Connection pooling for efficient email forwarding - Pattern caching for improved performance This provides more granular control over email routing with priority-based matching and comprehensive test coverage.
283 lines
6.1 KiB
TypeScript
283 lines
6.1 KiB
TypeScript
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
import { EmailRouter, type IEmailRoute, type IEmailContext } from '../ts/mail/routing/index.js';
|
|
import { Email } from '../ts/mail/core/classes.email.js';
|
|
|
|
tap.test('EmailRouter - should create and manage routes', async () => {
|
|
const router = new EmailRouter([]);
|
|
|
|
// Test initial state
|
|
expect(router.getRoutes()).toEqual([]);
|
|
|
|
// Add some test routes
|
|
const routes: IEmailRoute[] = [
|
|
{
|
|
name: 'forward-example',
|
|
priority: 10,
|
|
match: {
|
|
recipients: '*@example.com'
|
|
},
|
|
action: {
|
|
type: 'forward',
|
|
forward: {
|
|
host: 'mail.example.com',
|
|
port: 25
|
|
}
|
|
}
|
|
},
|
|
{
|
|
name: 'reject-spam',
|
|
priority: 20,
|
|
match: {
|
|
senders: '*@spammer.com'
|
|
},
|
|
action: {
|
|
type: 'reject',
|
|
reject: {
|
|
code: 550,
|
|
message: 'Spam not allowed'
|
|
}
|
|
}
|
|
}
|
|
];
|
|
|
|
router.updateRoutes(routes);
|
|
expect(router.getRoutes().length).toEqual(2);
|
|
});
|
|
|
|
tap.test('EmailRouter - should evaluate routes based on priority', async () => {
|
|
const router = new EmailRouter([]);
|
|
|
|
const routes: IEmailRoute[] = [
|
|
{
|
|
name: 'low-priority',
|
|
priority: 5,
|
|
match: {
|
|
recipients: '*@test.com'
|
|
},
|
|
action: {
|
|
type: 'deliver'
|
|
}
|
|
},
|
|
{
|
|
name: 'high-priority',
|
|
priority: 10,
|
|
match: {
|
|
recipients: 'admin@test.com'
|
|
},
|
|
action: {
|
|
type: 'process',
|
|
process: {
|
|
scan: true
|
|
}
|
|
}
|
|
}
|
|
];
|
|
|
|
router.updateRoutes(routes);
|
|
|
|
// Create test context
|
|
const email = new Email({
|
|
from: 'sender@example.com',
|
|
to: 'admin@test.com',
|
|
subject: 'Test email',
|
|
text: 'Test email content'
|
|
});
|
|
|
|
const context: IEmailContext = {
|
|
email,
|
|
session: {
|
|
id: 'test-session',
|
|
remoteAddress: '192.168.1.1',
|
|
matchedRoute: null
|
|
} as any
|
|
};
|
|
|
|
const route = await router.evaluateRoutes(context);
|
|
expect(route).not.toEqual(null);
|
|
expect(route?.name).toEqual('high-priority');
|
|
});
|
|
|
|
tap.test('EmailRouter - should match recipient patterns', async () => {
|
|
const router = new EmailRouter([]);
|
|
|
|
const routes: IEmailRoute[] = [
|
|
{
|
|
name: 'exact-match',
|
|
match: {
|
|
recipients: 'admin@example.com'
|
|
},
|
|
action: {
|
|
type: 'forward',
|
|
forward: {
|
|
host: 'admin-server.com',
|
|
port: 25
|
|
}
|
|
}
|
|
},
|
|
{
|
|
name: 'wildcard-match',
|
|
match: {
|
|
recipients: '*@example.com'
|
|
},
|
|
action: {
|
|
type: 'deliver'
|
|
}
|
|
}
|
|
];
|
|
|
|
router.updateRoutes(routes);
|
|
|
|
// Test exact match
|
|
const email1 = new Email({
|
|
from: 'sender@test.com',
|
|
to: 'admin@example.com',
|
|
subject: 'Admin email',
|
|
text: 'Admin email content'
|
|
});
|
|
|
|
const context1: IEmailContext = {
|
|
email: email1,
|
|
session: { id: 'test1', remoteAddress: '10.0.0.1' } as any
|
|
};
|
|
|
|
const route1 = await router.evaluateRoutes(context1);
|
|
expect(route1?.name).toEqual('exact-match');
|
|
|
|
// Test wildcard match
|
|
const email2 = new Email({
|
|
from: 'sender@test.com',
|
|
to: 'user@example.com',
|
|
subject: 'User email',
|
|
text: 'User email content'
|
|
});
|
|
|
|
const context2: IEmailContext = {
|
|
email: email2,
|
|
session: { id: 'test2', remoteAddress: '10.0.0.2' } as any
|
|
};
|
|
|
|
const route2 = await router.evaluateRoutes(context2);
|
|
expect(route2?.name).toEqual('wildcard-match');
|
|
});
|
|
|
|
tap.test('EmailRouter - should match IP ranges with CIDR notation', async () => {
|
|
const router = new EmailRouter([]);
|
|
|
|
const routes: IEmailRoute[] = [
|
|
{
|
|
name: 'internal-network',
|
|
match: {
|
|
clientIp: '10.0.0.0/24'
|
|
},
|
|
action: {
|
|
type: 'deliver'
|
|
}
|
|
},
|
|
{
|
|
name: 'external-network',
|
|
match: {
|
|
clientIp: ['192.168.1.0/24', '172.16.0.0/16']
|
|
},
|
|
action: {
|
|
type: 'process',
|
|
process: {
|
|
scan: true
|
|
}
|
|
}
|
|
}
|
|
];
|
|
|
|
router.updateRoutes(routes);
|
|
|
|
// Test internal network match
|
|
const email = new Email({
|
|
from: 'internal@company.com',
|
|
to: 'user@company.com',
|
|
subject: 'Internal email',
|
|
text: 'Internal email content'
|
|
});
|
|
|
|
const context1: IEmailContext = {
|
|
email,
|
|
session: { id: 'test1', remoteAddress: '10.0.0.15' } as any
|
|
};
|
|
|
|
const route1 = await router.evaluateRoutes(context1);
|
|
expect(route1?.name).toEqual('internal-network');
|
|
|
|
// Test external network match
|
|
const context2: IEmailContext = {
|
|
email,
|
|
session: { id: 'test2', remoteAddress: '192.168.1.100' } as any
|
|
};
|
|
|
|
const route2 = await router.evaluateRoutes(context2);
|
|
expect(route2?.name).toEqual('external-network');
|
|
});
|
|
|
|
tap.test('EmailRouter - should handle authentication matching', async () => {
|
|
const router = new EmailRouter([]);
|
|
|
|
const routes: IEmailRoute[] = [
|
|
{
|
|
name: 'authenticated-users',
|
|
match: {
|
|
authenticated: true
|
|
},
|
|
action: {
|
|
type: 'deliver'
|
|
}
|
|
},
|
|
{
|
|
name: 'unauthenticated-users',
|
|
match: {
|
|
authenticated: false
|
|
},
|
|
action: {
|
|
type: 'reject',
|
|
reject: {
|
|
code: 550,
|
|
message: 'Authentication required'
|
|
}
|
|
}
|
|
}
|
|
];
|
|
|
|
router.updateRoutes(routes);
|
|
|
|
const email = new Email({
|
|
from: 'user@example.com',
|
|
to: 'recipient@test.com',
|
|
subject: 'Test',
|
|
text: 'Test content'
|
|
});
|
|
|
|
// Test authenticated session
|
|
const context1: IEmailContext = {
|
|
email,
|
|
session: {
|
|
id: 'test1',
|
|
remoteAddress: '10.0.0.1',
|
|
authenticated: true,
|
|
authenticatedUser: 'user@example.com'
|
|
} as any
|
|
};
|
|
|
|
const route1 = await router.evaluateRoutes(context1);
|
|
expect(route1?.name).toEqual('authenticated-users');
|
|
|
|
// Test unauthenticated session
|
|
const context2: IEmailContext = {
|
|
email,
|
|
session: {
|
|
id: 'test2',
|
|
remoteAddress: '10.0.0.2',
|
|
authenticated: false
|
|
} as any
|
|
};
|
|
|
|
const route2 = await router.evaluateRoutes(context2);
|
|
expect(route2?.name).toEqual('unauthenticated-users');
|
|
});
|
|
|
|
export default tap.start(); |