Complete email router implementation and documentation

- Cleaned up interface definitions to only include implemented features
- Updated readme.md with comprehensive route-based configuration examples
- Added common email routing patterns and troubleshooting guide
- Removed legacy DomainRouter and IDomainRule interfaces
- Updated all imports and exports to use new EmailRouter system
- Verified build and core functionality tests pass

The match/action pattern implementation is now complete and production-ready.
This commit is contained in:
Philipp Kunz 2025-05-28 14:12:50 +00:00
parent 191c4160c1
commit e1a7b3e8f7
5 changed files with 240 additions and 483 deletions

342
readme.md
View File

@ -97,13 +97,17 @@ const router = new DcRouter({
emailConfig: {
ports: [25, 587, 465],
hostname: 'mail.example.com',
domainRules: [
routes: [
{
pattern: '*@example.com',
mode: 'mta',
mtaOptions: {
domain: 'example.com',
dkimSign: true
name: 'local-mail',
match: { recipients: '*@example.com' },
action: {
type: 'process',
process: {
scan: true,
dkim: true,
queue: 'normal'
}
}
}
],
@ -203,10 +207,11 @@ interface IDcRouterOptions {
emailConfig?: {
ports: number[];
hostname: string;
domainRules: IDomainRule[];
defaultMode: EmailProcessingMode;
routes: IEmailRoute[]; // Route-based configuration
auth?: IAuthConfig;
tls?: ITlsConfig;
maxMessageSize?: number;
rateLimits?: IRateLimitConfig;
};
// DNS server configuration
@ -259,102 +264,186 @@ interface IRouteConfig {
## Email System
### Email Processing Modes
### Email Route Actions
#### **Forward Mode**
Routes emails to external SMTP servers with optional authentication and TLS.
#### **Forward Action**
Routes emails to external SMTP servers.
```typescript
{
pattern: '*@company.com',
mode: 'forward',
target: {
server: 'internal-mail.company.com',
port: 25,
useTls: true,
auth: {
username: 'relay-user',
password: 'relay-pass'
name: 'forward-to-internal',
match: { recipients: '*@company.com' },
action: {
type: 'forward',
forward: {
host: 'internal-mail.company.com',
port: 25,
auth: {
username: 'relay-user',
password: 'relay-pass'
},
addHeaders: {
'X-Forwarded-By': 'dcrouter'
}
}
}
}
```
#### **MTA Mode**
Full Mail Transfer Agent functionality with DKIM signing and delivery queues.
#### **Process Action**
Full Mail Transfer Agent functionality with scanning and delivery queues.
```typescript
{
pattern: '*@notifications.company.com',
mode: 'mta',
mtaOptions: {
domain: 'notifications.company.com',
dkimSign: true,
dkimOptions: {
domainName: 'notifications.company.com',
keySelector: 'mail',
privateKey: fs.readFileSync('./dkim-private.key', 'utf8')
},
queueConfig: {
maxRetries: 3,
retryDelay: 300000
name: 'process-notifications',
match: { recipients: '*@notifications.company.com' },
action: {
type: 'process',
process: {
scan: true,
dkim: true,
queue: 'priority'
}
}
}
```
#### **Process Mode**
Store-and-forward with content scanning and transformations.
#### **Deliver Action**
Local delivery for mailbox storage.
```typescript
{
pattern: '*@marketing.company.com',
mode: 'process',
contentScanning: true,
scanners: [
{
type: 'spam',
threshold: 5.0,
action: 'tag'
},
{
type: 'virus',
action: 'reject'
name: 'deliver-local',
match: { recipients: '*@marketing.company.com' },
action: {
type: 'deliver'
}
}
```
#### **Reject Action**
Reject emails with custom SMTP responses.
```typescript
{
name: 'reject-spam',
match: {
senders: '*@spam-domain.com',
sizeRange: { min: 1000000 } // > 1MB
},
action: {
type: 'reject',
reject: {
code: 550,
message: 'Message rejected due to policy'
}
],
transformations: [
{
type: 'addHeader',
header: 'X-Marketing-Campaign',
value: 'auto-processed'
}
]
}
}
```
### Common Email Routing Patterns
#### **IP-Based Relay**
Allow internal networks to relay through the server:
```typescript
{
name: 'office-relay',
priority: 100,
match: { clientIp: ['192.168.0.0/16', '10.0.0.0/8'] },
action: {
type: 'forward',
forward: { host: 'internal-mail.company.com', port: 25 }
}
}
```
#### **Domain-Based Routing**
Route different domains to different servers:
```typescript
{
name: 'partner-domain',
match: { recipients: '*@partner.com' },
action: {
type: 'forward',
forward: { host: 'partner-mail.com', port: 587 }
}
}
```
#### **Authentication-Based Processing**
Different handling for authenticated vs unauthenticated senders:
```typescript
{
name: 'authenticated-users',
match: { authenticated: true },
action: {
type: 'process',
process: { scan: false, dkim: true, queue: 'priority' }
}
},
{
name: 'unauthenticated-reject',
match: { authenticated: false },
action: {
type: 'reject',
reject: { code: 550, message: 'Authentication required' }
}
}
```
#### **Content-Based Filtering**
Filter based on size, subject, or headers:
```typescript
{
name: 'large-email-reject',
match: { sizeRange: { min: 25000000 } }, // > 25MB
action: {
type: 'reject',
reject: { code: 552, message: 'Message too large' }
}
},
{
name: 'priority-emails',
match: {
headers: { 'X-Priority': 'high' },
subject: /urgent|emergency/i
},
action: {
type: 'process',
process: { queue: 'priority' }
}
}
```
### Email Security Features
#### **DKIM, SPF, DMARC**
#### **Route Matching Patterns**
**Glob Pattern Matching**
```typescript
// Automatic DKIM signing
const dkimOptions = {
domainName: 'example.com',
keySelector: 'mail',
privateKey: dkimPrivateKey,
algorithm: 'rsa-sha256'
};
// Email address patterns
match: { recipients: '*@example.com' } // All addresses at domain
match: { recipients: 'admin@*' } // Admin at any domain
match: { senders: ['*@trusted.com', '*@partner.com'] } // Multiple patterns
// SPF record validation
const spfPolicy = 'v=spf1 include:_spf.google.com ~all';
// CIDR IP matching
match: { clientIp: '192.168.0.0/16' } // Private subnet
match: { clientIp: ['10.0.0.0/8', '172.16.0.0/12'] } // Multiple ranges
// DMARC policy enforcement
const dmarcPolicy = {
policy: 'quarantine',
alignment: {
spf: 'relaxed',
dkim: 'strict'
// Header matching
match: {
headers: {
'X-Priority': 'high',
'Subject': /urgent|emergency/i
}
};
}
// Size and content matching
match: {
sizeRange: { min: 1000, max: 5000000 }, // 1KB to 5MB
hasAttachments: true,
subject: /invoice|receipt/i
}
```
#### **Content Scanning**
@ -630,50 +719,64 @@ const router = new DcRouter({
certPath: './certs/mail-cert.pem'
},
// Domain routing rules
domainRules: [
// Transactional emails via MTA
// Email routing rules
routes: [
// Relay from office network
{
pattern: '*@notifications.example.com',
mode: 'mta',
mtaOptions: {
domain: 'notifications.example.com',
dkimSign: true,
dkimOptions: {
domainName: 'notifications.example.com',
keySelector: 'mail',
privateKey: dkimKey
name: 'office-relay',
priority: 100,
match: { clientIp: '192.168.0.0/16' },
action: {
type: 'forward',
forward: {
host: 'internal-mail.example.com',
port: 25
}
}
},
// Transactional emails via processing
{
name: 'notifications',
priority: 50,
match: { recipients: '*@notifications.example.com' },
action: {
type: 'process',
process: {
scan: true,
dkim: true,
queue: 'priority'
}
}
},
// Internal emails forwarded to Exchange
{
pattern: '*@example.com',
mode: 'forward',
target: {
server: 'exchange.internal.example.com',
port: 25,
useTls: true
name: 'internal-mail',
priority: 25,
match: { recipients: '*@example.com' },
action: {
type: 'forward',
forward: {
host: 'exchange.internal.example.com',
port: 25
}
}
},
// Marketing emails with content scanning
// Default reject
{
pattern: '*@marketing.example.com',
mode: 'process',
contentScanning: true,
scanners: [
{ type: 'spam', threshold: 5.0, action: 'tag' },
{ type: 'virus', action: 'reject' }
]
name: 'default-reject',
match: { recipients: '*' },
action: {
type: 'reject',
reject: {
code: 550,
message: 'Relay denied'
}
}
}
],
// Default fallback
defaultMode: 'forward',
defaultServer: 'backup-mail.example.com',
defaultPort: 25
]
},
// DNS server for ACME challenges
@ -754,6 +857,33 @@ dig TXT mail._domainkey.your-domain.com
dig TXT your-domain.com
```
#### Email Routing Issues
**Route Not Matching**
- Check route priority order (higher priority = evaluated first)
- Verify glob patterns: `*@example.com` matches domain, `admin@*` matches user
- Test CIDR notation: `192.168.0.0/16` includes all 192.168.x.x addresses
- Confirm authentication state matches your expectations
**Common Route Patterns**
```typescript
// Debug route to log all traffic
{
name: 'debug-all',
priority: 1000,
match: { recipients: '*' },
action: { type: 'process', process: { scan: false } }
}
// Catch-all reject (should be lowest priority)
{
name: 'default-reject',
priority: 0,
match: { recipients: '*' },
action: { type: 'reject', reject: { code: 550, message: 'No route' } }
}
```
#### DNS Issues
```bash
# Test DNS server

View File

@ -5,7 +5,6 @@ import * as paths from './paths.js';
// Import the email server and its configuration
import { UnifiedEmailServer, type IUnifiedEmailServerOptions } from './mail/routing/classes.unified.email.server.js';
import type { IDomainRule, EmailProcessingMode } from './mail/routing/classes.email.config.js';
import type { IEmailRoute } from './mail/routing/interfaces.js';
import { logger } from './logger.js';
// Import the email configuration helpers directly from mail/delivery
@ -596,6 +595,6 @@ export class DcRouter {
}
// Re-export email server types for convenience
export type { IUnifiedEmailServerOptions, IDomainRule, EmailProcessingMode };
export type { IUnifiedEmailServerOptions };
export default DcRouter;

View File

@ -10,7 +10,6 @@ import {
} from '../../security/index.js';
import { UnifiedDeliveryQueue, type IQueueItem } from './classes.delivery.queue.js';
import type { Email } from '../core/classes.email.js';
import type { IDomainRule } from '../routing/classes.email.config.js';
import type { UnifiedEmailServer } from '../routing/classes.unified.email.server.js';
import type { SmtpClient } from './smtpclient/smtp-client.js';
@ -50,7 +49,7 @@ export interface IMultiModeDeliveryOptions {
// Mode-specific handlers
forwardHandler?: IDeliveryHandler;
mtaHandler?: IDeliveryHandler;
deliveryHandler?: IDeliveryHandler;
processHandler?: IDeliveryHandler;
// Rate limiting
@ -136,7 +135,7 @@ export class MultiModeDeliverySystem extends EventEmitter {
forwardHandler: options.forwardHandler || {
deliver: this.handleForwardDelivery.bind(this)
},
mtaHandler: options.mtaHandler || {
deliveryHandler: options.deliveryHandler || {
deliver: this.handleMtaDelivery.bind(this)
},
processHandler: options.processHandler || {
@ -313,7 +312,7 @@ export class MultiModeDeliverySystem extends EventEmitter {
break;
case 'mta':
result = await this.options.mtaHandler.deliver(item);
result = await this.options.deliveryHandler.deliver(item);
break;
case 'process':

View File

@ -1,369 +0,0 @@
import * as plugins from '../../plugins.js';
import { EventEmitter } from 'node:events';
import { type IDomainRule, type EmailProcessingMode } from './classes.email.config.js';
/**
* Options for the domain-based router
*/
export interface IDomainRouterOptions {
// Domain rules with glob pattern matching
domainRules: IDomainRule[];
// Default handling for unmatched domains
defaultMode: EmailProcessingMode;
defaultServer?: string;
defaultPort?: number;
defaultTls?: boolean;
// Pattern matching options
caseSensitive?: boolean;
priorityOrder?: 'most-specific' | 'first-match';
// Cache settings for pattern matching
enableCache?: boolean;
cacheSize?: number;
}
/**
* Result of a pattern match operation
*/
export interface IPatternMatchResult {
rule: IDomainRule;
exactMatch: boolean;
wildcardMatch: boolean;
specificity: number; // Higher is more specific
}
/**
* A pattern matching and routing class for email domains
*/
export class DomainRouter extends EventEmitter {
private options: IDomainRouterOptions;
private patternCache: Map<string, IDomainRule | null> = new Map();
/**
* Create a new domain router
* @param options Router options
*/
constructor(options: IDomainRouterOptions) {
super();
this.options = {
// Default options
caseSensitive: false,
priorityOrder: 'most-specific',
enableCache: true,
cacheSize: 1000,
...options
};
}
/**
* Match an email address against defined rules
* @param email Email address to match
* @returns The matching rule or null if no match
*/
public matchRule(email: string): IDomainRule | null {
// Check cache first if enabled
if (this.options.enableCache && this.patternCache.has(email)) {
return this.patternCache.get(email) || null;
}
// Normalize email if case-insensitive
const normalizedEmail = this.options.caseSensitive ? email : email.toLowerCase();
// Get all matching rules
const matches = this.getAllMatchingRules(normalizedEmail);
if (matches.length === 0) {
// Cache the result (null) if caching is enabled
if (this.options.enableCache) {
this.addToCache(email, null);
}
return null;
}
// Sort by specificity or order
let matchedRule: IDomainRule;
if (this.options.priorityOrder === 'most-specific') {
// Sort by specificity (most specific first)
const sortedMatches = matches.sort((a, b) => {
const aSpecificity = this.calculateSpecificity(a.pattern);
const bSpecificity = this.calculateSpecificity(b.pattern);
return bSpecificity - aSpecificity;
});
matchedRule = sortedMatches[0];
} else {
// First match in the list
matchedRule = matches[0];
}
// Cache the result if caching is enabled
if (this.options.enableCache) {
this.addToCache(email, matchedRule);
}
return matchedRule;
}
/**
* Calculate pattern specificity
* Higher is more specific
* @param pattern Pattern to calculate specificity for
*/
private calculateSpecificity(pattern: string): number {
let specificity = 0;
// Exact match is most specific
if (!pattern.includes('*')) {
return 100;
}
// Count characters that aren't wildcards
specificity += pattern.replace(/\*/g, '').length;
// Position of wildcards affects specificity
if (pattern.startsWith('*@')) {
// Wildcard in local part
specificity += 10;
} else if (pattern.includes('@*')) {
// Wildcard in domain part
specificity += 20;
}
return specificity;
}
/**
* Check if email matches a specific pattern
* @param email Email address to check
* @param pattern Pattern to check against
* @returns True if matching, false otherwise
*/
public matchesPattern(email: string, pattern: string): boolean {
// Normalize if case-insensitive
const normalizedEmail = this.options.caseSensitive ? email : email.toLowerCase();
const normalizedPattern = this.options.caseSensitive ? pattern : pattern.toLowerCase();
// Exact match
if (normalizedEmail === normalizedPattern) {
return true;
}
// Convert glob pattern to regex
const regexPattern = this.globToRegExp(normalizedPattern);
return regexPattern.test(normalizedEmail);
}
/**
* Convert a glob pattern to a regular expression
* @param pattern Glob pattern
* @returns Regular expression
*/
private globToRegExp(pattern: string): RegExp {
// Escape special regex characters except * and ?
let regexString = pattern
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
.replace(/\*/g, '.*')
.replace(/\?/g, '.');
return new RegExp(`^${regexString}$`);
}
/**
* Get all rules that match an email address
* @param email Email address to match
* @returns Array of matching rules
*/
public getAllMatchingRules(email: string): IDomainRule[] {
return this.options.domainRules.filter(rule => this.matchesPattern(email, rule.pattern));
}
/**
* Add a new routing rule
* @param rule Domain rule to add
*/
public addRule(rule: IDomainRule): void {
// Validate the rule
this.validateRule(rule);
// Add the rule
this.options.domainRules.push(rule);
// Clear cache since rules have changed
this.clearCache();
// Emit event
this.emit('ruleAdded', rule);
}
/**
* Validate a domain rule
* @param rule Rule to validate
*/
private validateRule(rule: IDomainRule): void {
// Pattern is required
if (!rule.pattern) {
throw new Error('Domain rule pattern is required');
}
// Mode is required
if (!rule.mode) {
throw new Error('Domain rule mode is required');
}
// Forward mode requires target
if (rule.mode === 'forward' && !rule.target) {
throw new Error('Forward mode requires target configuration');
}
// Forward mode target requires server
if (rule.mode === 'forward' && rule.target && !rule.target.server) {
throw new Error('Forward mode target requires server');
}
}
/**
* Update an existing rule
* @param pattern Pattern to update
* @param updates Updates to apply
* @returns True if rule was found and updated, false otherwise
*/
public updateRule(pattern: string, updates: Partial<IDomainRule>): boolean {
const ruleIndex = this.options.domainRules.findIndex(r => r.pattern === pattern);
if (ruleIndex === -1) {
return false;
}
// Get current rule
const currentRule = this.options.domainRules[ruleIndex];
// Create updated rule
const updatedRule: IDomainRule = {
...currentRule,
...updates
};
// Validate the updated rule
this.validateRule(updatedRule);
// Update the rule
this.options.domainRules[ruleIndex] = updatedRule;
// Clear cache since rules have changed
this.clearCache();
// Emit event
this.emit('ruleUpdated', updatedRule);
return true;
}
/**
* Remove a rule
* @param pattern Pattern to remove
* @returns True if rule was found and removed, false otherwise
*/
public removeRule(pattern: string): boolean {
const initialLength = this.options.domainRules.length;
this.options.domainRules = this.options.domainRules.filter(r => r.pattern !== pattern);
const removed = initialLength > this.options.domainRules.length;
if (removed) {
// Clear cache since rules have changed
this.clearCache();
// Emit event
this.emit('ruleRemoved', pattern);
}
return removed;
}
/**
* Get rule by pattern
* @param pattern Pattern to find
* @returns Rule with matching pattern or null if not found
*/
public getRule(pattern: string): IDomainRule | null {
return this.options.domainRules.find(r => r.pattern === pattern) || null;
}
/**
* Get all rules
* @returns Array of all domain rules
*/
public getRules(): IDomainRule[] {
return [...this.options.domainRules];
}
/**
* Update options
* @param options New options
*/
public updateOptions(options: Partial<IDomainRouterOptions>): void {
this.options = {
...this.options,
...options
};
// Clear cache if cache settings changed
if ('enableCache' in options || 'cacheSize' in options) {
this.clearCache();
}
// Emit event
this.emit('optionsUpdated', this.options);
}
/**
* Add an item to the pattern cache
* @param email Email address
* @param rule Matching rule or null
*/
private addToCache(email: string, rule: IDomainRule | null): void {
// If cache is disabled, do nothing
if (!this.options.enableCache) {
return;
}
// Add to cache
this.patternCache.set(email, rule);
// Check if cache size exceeds limit
if (this.patternCache.size > (this.options.cacheSize || 1000)) {
// Remove oldest entry (first in the Map)
const firstKey = this.patternCache.keys().next().value;
this.patternCache.delete(firstKey);
}
}
/**
* Clear pattern matching cache
*/
public clearCache(): void {
this.patternCache.clear();
this.emit('cacheCleared');
}
/**
* Update all domain rules at once
* @param rules New set of domain rules to replace existing ones
*/
public updateRules(rules: IDomainRule[]): void {
// Validate all rules
rules.forEach(rule => this.validateRule(rule));
// Replace all rules
this.options.domainRules = [...rules];
// Clear cache since rules have changed
this.clearCache();
// Emit event
this.emit('rulesUpdated', rules);
}
}

View File

@ -1,6 +1,4 @@
// Email routing components
export * from './classes.domain.router.js';
export * from './classes.email.config.js';
export * from './classes.email.router.js';
export * from './classes.unified.email.server.js';
export * from './classes.dnsmanager.js';