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:
parent
191c4160c1
commit
e1a7b3e8f7
342
readme.md
342
readme.md
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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':
|
||||
|
@ -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
|
||||
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';
|
||||
|
Loading…
x
Reference in New Issue
Block a user