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:
342
readme.md
342
readme.md
@@ -97,13 +97,17 @@ const router = new DcRouter({
|
|||||||
emailConfig: {
|
emailConfig: {
|
||||||
ports: [25, 587, 465],
|
ports: [25, 587, 465],
|
||||||
hostname: 'mail.example.com',
|
hostname: 'mail.example.com',
|
||||||
domainRules: [
|
routes: [
|
||||||
{
|
{
|
||||||
pattern: '*@example.com',
|
name: 'local-mail',
|
||||||
mode: 'mta',
|
match: { recipients: '*@example.com' },
|
||||||
mtaOptions: {
|
action: {
|
||||||
domain: 'example.com',
|
type: 'process',
|
||||||
dkimSign: true
|
process: {
|
||||||
|
scan: true,
|
||||||
|
dkim: true,
|
||||||
|
queue: 'normal'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -203,10 +207,11 @@ interface IDcRouterOptions {
|
|||||||
emailConfig?: {
|
emailConfig?: {
|
||||||
ports: number[];
|
ports: number[];
|
||||||
hostname: string;
|
hostname: string;
|
||||||
domainRules: IDomainRule[];
|
routes: IEmailRoute[]; // Route-based configuration
|
||||||
defaultMode: EmailProcessingMode;
|
|
||||||
auth?: IAuthConfig;
|
auth?: IAuthConfig;
|
||||||
tls?: ITlsConfig;
|
tls?: ITlsConfig;
|
||||||
|
maxMessageSize?: number;
|
||||||
|
rateLimits?: IRateLimitConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
// DNS server configuration
|
// DNS server configuration
|
||||||
@@ -259,102 +264,186 @@ interface IRouteConfig {
|
|||||||
|
|
||||||
## Email System
|
## Email System
|
||||||
|
|
||||||
### Email Processing Modes
|
### Email Route Actions
|
||||||
|
|
||||||
#### **Forward Mode**
|
#### **Forward Action**
|
||||||
Routes emails to external SMTP servers with optional authentication and TLS.
|
Routes emails to external SMTP servers.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
{
|
{
|
||||||
pattern: '*@company.com',
|
name: 'forward-to-internal',
|
||||||
mode: 'forward',
|
match: { recipients: '*@company.com' },
|
||||||
target: {
|
action: {
|
||||||
server: 'internal-mail.company.com',
|
type: 'forward',
|
||||||
port: 25,
|
forward: {
|
||||||
useTls: true,
|
host: 'internal-mail.company.com',
|
||||||
auth: {
|
port: 25,
|
||||||
username: 'relay-user',
|
auth: {
|
||||||
password: 'relay-pass'
|
username: 'relay-user',
|
||||||
|
password: 'relay-pass'
|
||||||
|
},
|
||||||
|
addHeaders: {
|
||||||
|
'X-Forwarded-By': 'dcrouter'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### **MTA Mode**
|
#### **Process Action**
|
||||||
Full Mail Transfer Agent functionality with DKIM signing and delivery queues.
|
Full Mail Transfer Agent functionality with scanning and delivery queues.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
{
|
{
|
||||||
pattern: '*@notifications.company.com',
|
name: 'process-notifications',
|
||||||
mode: 'mta',
|
match: { recipients: '*@notifications.company.com' },
|
||||||
mtaOptions: {
|
action: {
|
||||||
domain: 'notifications.company.com',
|
type: 'process',
|
||||||
dkimSign: true,
|
process: {
|
||||||
dkimOptions: {
|
scan: true,
|
||||||
domainName: 'notifications.company.com',
|
dkim: true,
|
||||||
keySelector: 'mail',
|
queue: 'priority'
|
||||||
privateKey: fs.readFileSync('./dkim-private.key', 'utf8')
|
|
||||||
},
|
|
||||||
queueConfig: {
|
|
||||||
maxRetries: 3,
|
|
||||||
retryDelay: 300000
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### **Process Mode**
|
#### **Deliver Action**
|
||||||
Store-and-forward with content scanning and transformations.
|
Local delivery for mailbox storage.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
{
|
{
|
||||||
pattern: '*@marketing.company.com',
|
name: 'deliver-local',
|
||||||
mode: 'process',
|
match: { recipients: '*@marketing.company.com' },
|
||||||
contentScanning: true,
|
action: {
|
||||||
scanners: [
|
type: 'deliver'
|
||||||
{
|
}
|
||||||
type: 'spam',
|
}
|
||||||
threshold: 5.0,
|
```
|
||||||
action: 'tag'
|
|
||||||
},
|
#### **Reject Action**
|
||||||
{
|
Reject emails with custom SMTP responses.
|
||||||
type: 'virus',
|
|
||||||
action: 'reject'
|
```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',
|
### Common Email Routing Patterns
|
||||||
value: 'auto-processed'
|
|
||||||
}
|
#### **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
|
### Email Security Features
|
||||||
|
|
||||||
#### **DKIM, SPF, DMARC**
|
#### **Route Matching Patterns**
|
||||||
|
|
||||||
|
**Glob Pattern Matching**
|
||||||
```typescript
|
```typescript
|
||||||
// Automatic DKIM signing
|
// Email address patterns
|
||||||
const dkimOptions = {
|
match: { recipients: '*@example.com' } // All addresses at domain
|
||||||
domainName: 'example.com',
|
match: { recipients: 'admin@*' } // Admin at any domain
|
||||||
keySelector: 'mail',
|
match: { senders: ['*@trusted.com', '*@partner.com'] } // Multiple patterns
|
||||||
privateKey: dkimPrivateKey,
|
|
||||||
algorithm: 'rsa-sha256'
|
|
||||||
};
|
|
||||||
|
|
||||||
// SPF record validation
|
// CIDR IP matching
|
||||||
const spfPolicy = 'v=spf1 include:_spf.google.com ~all';
|
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
|
// Header matching
|
||||||
const dmarcPolicy = {
|
match: {
|
||||||
policy: 'quarantine',
|
headers: {
|
||||||
alignment: {
|
'X-Priority': 'high',
|
||||||
spf: 'relaxed',
|
'Subject': /urgent|emergency/i
|
||||||
dkim: 'strict'
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
// Size and content matching
|
||||||
|
match: {
|
||||||
|
sizeRange: { min: 1000, max: 5000000 }, // 1KB to 5MB
|
||||||
|
hasAttachments: true,
|
||||||
|
subject: /invoice|receipt/i
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### **Content Scanning**
|
#### **Content Scanning**
|
||||||
@@ -630,50 +719,64 @@ const router = new DcRouter({
|
|||||||
certPath: './certs/mail-cert.pem'
|
certPath: './certs/mail-cert.pem'
|
||||||
},
|
},
|
||||||
|
|
||||||
// Domain routing rules
|
// Email routing rules
|
||||||
domainRules: [
|
routes: [
|
||||||
// Transactional emails via MTA
|
// Relay from office network
|
||||||
{
|
{
|
||||||
pattern: '*@notifications.example.com',
|
name: 'office-relay',
|
||||||
mode: 'mta',
|
priority: 100,
|
||||||
mtaOptions: {
|
match: { clientIp: '192.168.0.0/16' },
|
||||||
domain: 'notifications.example.com',
|
action: {
|
||||||
dkimSign: true,
|
type: 'forward',
|
||||||
dkimOptions: {
|
forward: {
|
||||||
domainName: 'notifications.example.com',
|
host: 'internal-mail.example.com',
|
||||||
keySelector: 'mail',
|
port: 25
|
||||||
privateKey: dkimKey
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 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
|
// Internal emails forwarded to Exchange
|
||||||
{
|
{
|
||||||
pattern: '*@example.com',
|
name: 'internal-mail',
|
||||||
mode: 'forward',
|
priority: 25,
|
||||||
target: {
|
match: { recipients: '*@example.com' },
|
||||||
server: 'exchange.internal.example.com',
|
action: {
|
||||||
port: 25,
|
type: 'forward',
|
||||||
useTls: true
|
forward: {
|
||||||
|
host: 'exchange.internal.example.com',
|
||||||
|
port: 25
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Marketing emails with content scanning
|
// Default reject
|
||||||
{
|
{
|
||||||
pattern: '*@marketing.example.com',
|
name: 'default-reject',
|
||||||
mode: 'process',
|
match: { recipients: '*' },
|
||||||
contentScanning: true,
|
action: {
|
||||||
scanners: [
|
type: 'reject',
|
||||||
{ type: 'spam', threshold: 5.0, action: 'tag' },
|
reject: {
|
||||||
{ type: 'virus', action: 'reject' }
|
code: 550,
|
||||||
]
|
message: 'Relay denied'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
|
|
||||||
// Default fallback
|
|
||||||
defaultMode: 'forward',
|
|
||||||
defaultServer: 'backup-mail.example.com',
|
|
||||||
defaultPort: 25
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// DNS server for ACME challenges
|
// DNS server for ACME challenges
|
||||||
@@ -754,6 +857,33 @@ dig TXT mail._domainkey.your-domain.com
|
|||||||
dig TXT 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
|
#### DNS Issues
|
||||||
```bash
|
```bash
|
||||||
# Test DNS server
|
# Test DNS server
|
||||||
|
@@ -5,7 +5,6 @@ import * as paths from './paths.js';
|
|||||||
|
|
||||||
// Import the email server and its configuration
|
// Import the email server and its configuration
|
||||||
import { UnifiedEmailServer, type IUnifiedEmailServerOptions } from './mail/routing/classes.unified.email.server.js';
|
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 type { IEmailRoute } from './mail/routing/interfaces.js';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
// Import the email configuration helpers directly from mail/delivery
|
// Import the email configuration helpers directly from mail/delivery
|
||||||
@@ -596,6 +595,6 @@ export class DcRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Re-export email server types for convenience
|
// Re-export email server types for convenience
|
||||||
export type { IUnifiedEmailServerOptions, IDomainRule, EmailProcessingMode };
|
export type { IUnifiedEmailServerOptions };
|
||||||
|
|
||||||
export default DcRouter;
|
export default DcRouter;
|
||||||
|
@@ -10,7 +10,6 @@ import {
|
|||||||
} from '../../security/index.js';
|
} from '../../security/index.js';
|
||||||
import { UnifiedDeliveryQueue, type IQueueItem } from './classes.delivery.queue.js';
|
import { UnifiedDeliveryQueue, type IQueueItem } from './classes.delivery.queue.js';
|
||||||
import type { Email } from '../core/classes.email.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 { UnifiedEmailServer } from '../routing/classes.unified.email.server.js';
|
||||||
import type { SmtpClient } from './smtpclient/smtp-client.js';
|
import type { SmtpClient } from './smtpclient/smtp-client.js';
|
||||||
|
|
||||||
@@ -50,7 +49,7 @@ export interface IMultiModeDeliveryOptions {
|
|||||||
|
|
||||||
// Mode-specific handlers
|
// Mode-specific handlers
|
||||||
forwardHandler?: IDeliveryHandler;
|
forwardHandler?: IDeliveryHandler;
|
||||||
mtaHandler?: IDeliveryHandler;
|
deliveryHandler?: IDeliveryHandler;
|
||||||
processHandler?: IDeliveryHandler;
|
processHandler?: IDeliveryHandler;
|
||||||
|
|
||||||
// Rate limiting
|
// Rate limiting
|
||||||
@@ -136,7 +135,7 @@ export class MultiModeDeliverySystem extends EventEmitter {
|
|||||||
forwardHandler: options.forwardHandler || {
|
forwardHandler: options.forwardHandler || {
|
||||||
deliver: this.handleForwardDelivery.bind(this)
|
deliver: this.handleForwardDelivery.bind(this)
|
||||||
},
|
},
|
||||||
mtaHandler: options.mtaHandler || {
|
deliveryHandler: options.deliveryHandler || {
|
||||||
deliver: this.handleMtaDelivery.bind(this)
|
deliver: this.handleMtaDelivery.bind(this)
|
||||||
},
|
},
|
||||||
processHandler: options.processHandler || {
|
processHandler: options.processHandler || {
|
||||||
@@ -313,7 +312,7 @@ export class MultiModeDeliverySystem extends EventEmitter {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'mta':
|
case 'mta':
|
||||||
result = await this.options.mtaHandler.deliver(item);
|
result = await this.options.deliveryHandler.deliver(item);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'process':
|
case 'process':
|
||||||
|
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,6 +1,4 @@
|
|||||||
// Email routing components
|
// Email routing components
|
||||||
export * from './classes.domain.router.js';
|
|
||||||
export * from './classes.email.config.js';
|
|
||||||
export * from './classes.email.router.js';
|
export * from './classes.email.router.js';
|
||||||
export * from './classes.unified.email.server.js';
|
export * from './classes.unified.email.server.js';
|
||||||
export * from './classes.dnsmanager.js';
|
export * from './classes.dnsmanager.js';
|
||||||
|
Reference in New Issue
Block a user