- Add loadDkimRecords() method to read DKIM records from JSON files - Integrate DKIM records into DNS server during startup - Add initializeDkimForEmailDomains() for proactive DKIM key generation - Ensure DKIM records are available immediately after server startup - Update documentation with DKIM implementation status DKIM records are now automatically loaded from .nogit/data/dns/*.dkimrecord.json and served via DNS. Keys are generated for all configured email domains at startup.
906 lines
32 KiB
Markdown
906 lines
32 KiB
Markdown
# Implementation Hints and Learnings
|
|
|
|
## DKIM Implementation Status (2025-05-30)
|
|
|
|
### Current Implementation
|
|
1. **DKIM Key Generation**: Working - keys are generated when emails are sent
|
|
2. **DKIM Email Signing**: Working - emails are signed with DKIM
|
|
3. **DKIM DNS Record Serving**: Implemented - records are loaded from JSON files and served
|
|
4. **Proactive DKIM Generation**: Implemented - keys are generated for all email domains at startup
|
|
|
|
### Key Points
|
|
- DKIM selector is hardcoded as `mta` in DKIMCreator
|
|
- DKIM records are stored in `.nogit/data/dns/*.dkimrecord.json`
|
|
- DKIM keys are stored in `.nogit/data/keys/{domain}-private.pem` and `{domain}-public.pem`
|
|
- The server needs to be restarted for DKIM records to be loaded and served
|
|
- Proactive generation ensures DKIM records are available immediately after startup
|
|
|
|
### Testing
|
|
After server restart, DKIM records can be queried:
|
|
```bash
|
|
dig @192.168.190.3 mta._domainkey.central.eu TXT +short
|
|
```
|
|
|
|
### Note
|
|
The existing dcrouter instance has test domain DKIM records but not for production domains like central.eu. A restart is required to trigger the proactive DKIM generation for configured email domains.
|
|
|
|
## SmartProxy Usage
|
|
|
|
### New Route-Based Architecture (v18+)
|
|
- SmartProxy now uses a route-based configuration system
|
|
- Routes define match criteria and actions instead of simple port-to-port forwarding
|
|
- All traffic types (HTTP, HTTPS, TCP, WebSocket) are configured through routes
|
|
|
|
```typescript
|
|
// NEW: Route-based SmartProxy configuration
|
|
const smartProxy = new plugins.smartproxy.SmartProxy({
|
|
routes: [
|
|
{
|
|
name: 'https-traffic',
|
|
match: {
|
|
ports: 443,
|
|
domains: ['example.com', '*.example.com']
|
|
},
|
|
action: {
|
|
type: 'forward',
|
|
target: {
|
|
host: 'backend.server.com',
|
|
port: 8080
|
|
}
|
|
},
|
|
tls: {
|
|
mode: 'terminate',
|
|
certificate: 'auto'
|
|
}
|
|
}
|
|
],
|
|
defaults: {
|
|
target: {
|
|
host: 'fallback.server.com',
|
|
port: 8080
|
|
}
|
|
},
|
|
acme: {
|
|
accountEmail: 'admin@example.com',
|
|
enabled: true,
|
|
useProduction: true
|
|
}
|
|
});
|
|
```
|
|
|
|
### Migration from Old to New
|
|
```typescript
|
|
// OLD configuration style (deprecated)
|
|
{
|
|
fromPort: 443,
|
|
toPort: 8080,
|
|
targetIP: 'backend.server.com',
|
|
domainConfigs: [...]
|
|
}
|
|
|
|
// NEW route-based style
|
|
{
|
|
routes: [{
|
|
name: 'main-route',
|
|
match: { ports: 443 },
|
|
action: {
|
|
type: 'forward',
|
|
target: { host: 'backend.server.com', port: 8080 }
|
|
}
|
|
}]
|
|
}
|
|
```
|
|
|
|
### Direct Component Usage
|
|
- Use SmartProxy components directly instead of creating your own wrappers
|
|
- SmartProxy already includes Port80Handler and NetworkProxy functionality
|
|
- When using SmartProxy, configure it directly rather than instantiating Port80Handler or NetworkProxy separately
|
|
|
|
### Certificate Management
|
|
- SmartProxy has built-in ACME certificate management
|
|
- Configure it in the `acme` property of SmartProxy options
|
|
- Use `accountEmail` (not `email`) for the ACME contact email
|
|
- SmartProxy handles both HTTP-01 challenges and certificate application automatically
|
|
|
|
## qenv Usage
|
|
|
|
### Direct Usage
|
|
- Use qenv directly instead of creating environment variable wrappers
|
|
- Instantiate qenv with appropriate basePath and nogitPath:
|
|
|
|
```typescript
|
|
const qenv = new plugins.qenv.Qenv('./', '.nogit/');
|
|
const value = await qenv.getEnvVarOnDemand('ENV_VAR_NAME');
|
|
```
|
|
|
|
## TypeScript Interfaces
|
|
|
|
### SmartProxy Interfaces
|
|
- Always check the interfaces from the node_modules to ensure correct property names
|
|
- Important interfaces for the new architecture:
|
|
- `ISmartProxyOptions`: Main configuration with `routes` array
|
|
- `IRouteConfig`: Individual route configuration
|
|
- `IRouteMatch`: Match criteria for routes
|
|
- `IRouteTarget`: Target configuration for forwarding
|
|
- `IAcmeOptions`: ACME certificate configuration
|
|
- `TTlsMode`: TLS handling modes ('passthrough' | 'terminate' | 'terminate-and-reencrypt')
|
|
|
|
### New Route Configuration
|
|
```typescript
|
|
interface IRouteConfig {
|
|
name: string;
|
|
match: {
|
|
ports: number | number[];
|
|
domains?: string | string[];
|
|
path?: string;
|
|
headers?: Record<string, string | RegExp>;
|
|
};
|
|
action: {
|
|
type: 'forward' | 'redirect' | 'block' | 'static';
|
|
target?: {
|
|
host: string | string[] | ((context) => string);
|
|
port: number | 'preserve' | ((context) => number);
|
|
};
|
|
};
|
|
tls?: {
|
|
mode: TTlsMode;
|
|
certificate?: 'auto' | { key: string; cert: string; };
|
|
};
|
|
security?: {
|
|
authentication?: IRouteAuthentication;
|
|
rateLimit?: IRouteRateLimit;
|
|
ipAllowList?: string[];
|
|
ipBlockList?: string[];
|
|
};
|
|
}
|
|
```
|
|
|
|
### Required Properties
|
|
- For `ISmartProxyOptions`, `routes` array is the main configuration
|
|
- For `IAcmeOptions`, use `accountEmail` for the contact email
|
|
- Routes must have `name`, `match`, and `action` properties
|
|
|
|
## Testing
|
|
|
|
### Test Structure
|
|
- Follow the project's test structure, using `@push.rocks/tapbundle`
|
|
- Use `expect(value).toEqual(expected)` for equality checks
|
|
- Use `expect(value).toBeTruthy()` for boolean assertions
|
|
|
|
```typescript
|
|
tap.test('test description', async () => {
|
|
const result = someFunction();
|
|
expect(result.property).toEqual('expected value');
|
|
expect(result.valid).toBeTruthy();
|
|
});
|
|
```
|
|
|
|
### Cleanup
|
|
- Include a cleanup test to ensure proper test resource handling
|
|
- Add a `stop` test to forcefully end the test when needed:
|
|
|
|
```typescript
|
|
tap.test('stop', async () => {
|
|
await tap.stopForcefully();
|
|
});
|
|
```
|
|
|
|
## Architecture Principles
|
|
|
|
### Simplicity
|
|
- Prefer direct usage of libraries instead of creating wrappers
|
|
- Don't reinvent functionality that already exists in dependencies
|
|
- Keep interfaces clean and focused, avoiding unnecessary abstraction layers
|
|
|
|
### Component Integration
|
|
- Leverage built-in integrations between components (like SmartProxy's ACME handling)
|
|
- Use parallel operations for performance (like in the `stop()` method)
|
|
- Separate concerns clearly (HTTP handling vs. SMTP handling)
|
|
|
|
## Email Integration with SmartProxy
|
|
|
|
### Architecture
|
|
- Email traffic is routed through SmartProxy using automatic route generation
|
|
- Email server runs on internal ports and receives forwarded traffic from SmartProxy
|
|
- SmartProxy handles external ports (25, 587, 465) and forwards to internal ports
|
|
|
|
### Email Route Generation
|
|
```typescript
|
|
// Email configuration automatically generates SmartProxy routes
|
|
emailConfig: {
|
|
ports: [25, 587, 465],
|
|
hostname: 'mail.example.com',
|
|
domainRules: [...]
|
|
}
|
|
|
|
// Generates routes like:
|
|
{
|
|
name: 'smtp-route',
|
|
match: { ports: [25] },
|
|
action: {
|
|
type: 'forward',
|
|
target: { host: 'localhost', port: 10025 }
|
|
},
|
|
tls: { mode: 'passthrough' } // STARTTLS handled by email server
|
|
}
|
|
```
|
|
|
|
### Port Mapping
|
|
- External port 25 → Internal port 10025 (SMTP)
|
|
- External port 587 → Internal port 10587 (Submission)
|
|
- External port 465 → Internal port 10465 (SMTPS)
|
|
|
|
### TLS Handling
|
|
- Ports 25 and 587: Use 'passthrough' mode (STARTTLS handled by email server)
|
|
- Port 465: Use 'terminate' mode (SmartProxy handles TLS termination)
|
|
- Domain-specific TLS can be configured per email rule
|
|
|
|
## SMTP Test Migration
|
|
|
|
### Test Framework
|
|
- Tests migrated from custom framework to @push.rocks/tapbundle
|
|
- Each test file is self-contained with its own server lifecycle management
|
|
- Test files use pattern `test.*.ts` for automatic discovery by tstest
|
|
|
|
### Server Lifecycle
|
|
- SMTP server uses `listen()` method to start (not `start()`)
|
|
- SMTP server uses `close()` method to stop (not `stop()` or `destroy()`)
|
|
- Server loader module manages server lifecycle for tests
|
|
|
|
### Test Structure
|
|
```typescript
|
|
import { expect, tap } from '@push.rocks/tapbundle';
|
|
import { startTestServer, stopTestServer } from '../server.loader.js';
|
|
|
|
const TEST_PORT = 2525;
|
|
const TEST_TIMEOUT = 10000;
|
|
|
|
tap.test('prepare server', async () => {
|
|
await startTestServer();
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
});
|
|
|
|
tap.test('test name', async (tools) => {
|
|
const done = tools.defer();
|
|
// test implementation
|
|
done.resolve();
|
|
});
|
|
|
|
tap.test('cleanup server', async () => {
|
|
await stopTestServer();
|
|
});
|
|
|
|
tap.start();
|
|
```
|
|
|
|
### Common Issues and Solutions
|
|
1. **Multi-line SMTP responses**: Handle response buffering carefully, especially for EHLO
|
|
2. **Timing issues**: Use proper state management instead of string matching
|
|
3. **ES Module imports**: Use `import` statements, not `require()`
|
|
4. **Server cleanup**: Always close connections properly to avoid hanging tests
|
|
5. **Response buffer management**: Clear the response buffer after processing each state to avoid false matches from previous responses. Use specific response patterns (e.g., '250 OK' instead of just '250') to avoid ambiguity.
|
|
|
|
### SMTP Protocol Testing
|
|
- Server generates self-signed certificates automatically for testing
|
|
- Default test port is 2525
|
|
- Connection timeout is typically 10 seconds
|
|
- Always check for complete SMTP responses (ending with space after code)
|
|
|
|
## SMTP Implementation Findings (2025-05-25)
|
|
|
|
### Fixed Issues
|
|
|
|
1. **AUTH Mechanism Implementation**
|
|
- The server-side AUTH command handler was incomplete
|
|
- Implemented `handleAuthPlain` with proper PLAIN authentication flow
|
|
- Implemented `handleAuthLogin` with state-based LOGIN authentication flow
|
|
- Added `validateUser` function to test server configuration
|
|
- AUTH tests now expect STARTTLS instead of direct TLS (`secure: false` with `requireTLS: true`)
|
|
|
|
2. **TLS Connection Timeout Handling**
|
|
- For secure connections, the client was waiting for 'connect' event instead of 'secureConnect'
|
|
- Fixed in `ConnectionManager.establishSocket()` to use the appropriate event based on connection type
|
|
- This prevents indefinite hangs during TLS handshake failures
|
|
|
|
3. **STARTTLS Server Implementation**
|
|
- Removed incorrect `(tlsSocket as any)._start()` call which is client-side only
|
|
- Server-side TLS sockets handle handshake automatically when data arrives
|
|
- The `_start()` method caused Node.js assertion failure: `wrap->is_client()`
|
|
|
|
4. **Edge Case Test Patterns**
|
|
- Tests using non-existent `smtpClient.connect()` method - use `verify()` instead
|
|
- SMTP servers must handle DATA mode properly by processing lines individually
|
|
- Empty/minimal server responses need to be valid SMTP codes (e.g., "250 OK\r\n")
|
|
- Out-of-order pipelined responses break SMTP protocol - responses must be in order
|
|
|
|
### Common Test Patterns
|
|
|
|
1. **Connection Testing**
|
|
```typescript
|
|
const verified = await smtpClient.verify();
|
|
expect(verified).toBeTrue();
|
|
```
|
|
|
|
2. **Server Data Handling**
|
|
```typescript
|
|
socket.on('data', (data) => {
|
|
const lines = data.toString().split('\r\n');
|
|
lines.forEach(line => {
|
|
if (!line && lines[lines.length - 1] === '') return;
|
|
// Process each line individually
|
|
});
|
|
});
|
|
```
|
|
|
|
3. **Authentication Setup**
|
|
```typescript
|
|
auth: {
|
|
required: true,
|
|
methods: ['PLAIN', 'LOGIN'],
|
|
validateUser: async (username, password) => {
|
|
return username === 'testuser' && password === 'testpass';
|
|
}
|
|
}
|
|
```
|
|
|
|
### Progress Tracking
|
|
- Fixed 8 tests total (as of 2025-05-25)
|
|
- Fixed 8 additional tests (as of 2025-05-26):
|
|
- test.cedge-03.protocol-violations.ts
|
|
- test.cerr-03.network-failures.ts
|
|
- test.cerr-05.quota-exceeded.ts
|
|
- test.cerr-06.invalid-recipients.ts
|
|
- test.crel-01.reconnection-logic.ts
|
|
- test.crel-02.network-interruption.ts
|
|
- test.crel-03.queue-persistence.ts
|
|
- 26 error logs remaining in `.nogit/testlogs/00err/`
|
|
- Performance, additional reliability, RFC compliance, and security tests still need fixes
|
|
|
|
## Test Fix Findings (2025-05-26)
|
|
|
|
### Common Issues in SMTP Client Tests
|
|
|
|
1. **DATA Phase Handling in Test Servers**
|
|
- Test servers must properly handle DATA mode
|
|
- Need to track when in DATA mode and look for the terminating '.'
|
|
- Multi-line data must be processed line by line
|
|
```typescript
|
|
let inData = false;
|
|
socket.on('data', (data) => {
|
|
const lines = data.toString().split('\r\n');
|
|
lines.forEach(line => {
|
|
if (inData && line === '.') {
|
|
socket.write('250 OK\r\n');
|
|
inData = false;
|
|
} else if (line === 'DATA') {
|
|
socket.write('354 Send data\r\n');
|
|
inData = true;
|
|
}
|
|
});
|
|
});
|
|
```
|
|
|
|
2. **Import Issues**
|
|
- `createSmtpClient` should be imported from `ts/mail/delivery/smtpclient/index.js`
|
|
- Test server functions: use `startTestServer`/`stopTestServer` (not `startTestSmtpServer`)
|
|
- Helper exports `createTestSmtpClient`, not `createSmtpClient`
|
|
|
|
3. **SmtpClient API Misconceptions**
|
|
- SmtpClient doesn't have methods like `connect()`, `isConnected()`, `getConnectionInfo()`
|
|
- Use `verify()` for connection testing
|
|
- Use `sendMail()` with Email objects for sending
|
|
- Connection management is handled internally
|
|
|
|
4. **createSmtpClient is Not Async**
|
|
- The factory function returns an SmtpClient directly, not a Promise
|
|
- Remove `await` from `createSmtpClient()` calls
|
|
|
|
5. **Test Expectations**
|
|
- Multi-line SMTP responses may timeout if server doesn't send final line
|
|
- Mixed valid/invalid recipients might succeed for valid ones (implementation-specific)
|
|
- Network failure tests should use realistic expectations
|
|
|
|
6. **Test Runner Requirements**
|
|
- Tests using `tap` from '@git.zone/tstest/tapbundle' must call `tap.start()` at the end
|
|
- Without `tap.start()`, no tests will be detected or run
|
|
- Place `tap.start()` after all `tap.test()` definitions
|
|
|
|
7. **Connection Pooling Effects**
|
|
- SmtpClient uses connection pooling by default
|
|
- Test servers may not receive all messages immediately
|
|
- Messages might be queued and sent through different connections
|
|
- Adjust test expectations to account for pooling behavior
|
|
|
|
## Test Fixing Progress (2025-05-26 Afternoon)
|
|
|
|
### Summary
|
|
- Total failing tests initially: 35
|
|
- Tests fixed: 35 ✅
|
|
- Tests remaining: 0 - ALL TESTS PASSING!
|
|
|
|
### Fixed Tests - Session 2 (7):
|
|
1. test.ccm-05.connection-reuse.ts - Fixed performance expectation ✓
|
|
2. test.cperf-05.network-efficiency.ts - Removed pooled client usage ✓
|
|
3. test.cperf-06.caching-strategies.ts - Changed to sequential sending ✓
|
|
4. test.cperf-07.queue-management.ts - Simplified to sequential processing ✓
|
|
5. test.crel-07.resource-cleanup.ts - Complete rewrite to minimal test ✓
|
|
6. test.reputationmonitor.ts - Fixed data accumulation by setting NODE_ENV='test' ✓
|
|
7. Cleaned up stale error log: test__test.reputationmonitor.log (old log from before fix)
|
|
|
|
### Fixed Tests - Session 1 (28):
|
|
- **Edge Cases (1)**: test.cedge-03.protocol-violations.ts ✓
|
|
- **Error Handling (3)**: cerr-03, cerr-05, cerr-06 ✓
|
|
- **Reliability (6)**: crel-01 through crel-06 ✓
|
|
- **RFC Compliance (7)**: crfc-02 through crfc-08 ✓
|
|
- **Security (10)**: csec-01 through csec-10 ✓
|
|
- **Performance (1)**: cperf-08.dns-caching.ts ✓
|
|
|
|
### Important Notes:
|
|
- Error logs are deleted after tests are fixed (per original instruction)
|
|
- Tests taking >1 minute usually indicate hanging issues
|
|
- Property names: use 'host' not 'hostname' for SmtpClient options
|
|
- Always use helpers: createTestSmtpClient, createTestServer
|
|
- Always add tap.start() at the end of test files
|
|
|
|
### Key Fixes Applied:
|
|
- **Data Accumulation**: Set NODE_ENV='test' to prevent loading persisted data between tests
|
|
- **Connection Reuse**: Don't expect reuse to always be faster than fresh connections
|
|
- **Pooled Clients**: Remove usage - tests expect direct client behavior
|
|
- **Port Conflicts**: Use different ports for each test to avoid conflicts
|
|
- **Resource Cleanup**: Simplified tests that were too complex and timing-dependent
|
|
|
|
## Email Architecture Analysis (2025-05-27)
|
|
|
|
### Previous Architecture Issues (NOW RESOLVED)
|
|
1. ~~**Scattered Components**: Email functionality spread across multiple DcRouter properties~~ ✅ CONSOLIDATED
|
|
2. ~~**Duplicate SMTP Implementations**: EmailSendJob implements raw socket SMTP protocol~~ ✅ FIXED
|
|
3. ~~**Complex Setup**: setupUnifiedEmailHandling() is 150+ lines~~ ✅ SIMPLIFIED (now ~30 lines)
|
|
4. ~~**No Connection Pooling**: Each outbound email creates new connection~~ ✅ IMPLEMENTED
|
|
5. ~~**Orphaned Code**: SmtpPortConfig class exists but is never used~~ ✅ REMOVED
|
|
|
|
### Current Architecture (COMPLETED)
|
|
All email components are now consolidated under UnifiedEmailServer:
|
|
- `domainRouter` - Handles pattern-based routing decisions
|
|
- `deliveryQueue` - Manages email queue with retry logic
|
|
- `deliverySystem` - Handles multi-mode delivery
|
|
- `rateLimiter` - Enforces hierarchical rate limits
|
|
- `bounceManager` - Processes bounce notifications
|
|
- `ipWarmupManager` - Manages IP warmup process
|
|
- `senderReputationMonitor` - Tracks sender reputation
|
|
- `dkimCreator` - Handles DKIM key management
|
|
- `ipReputationChecker` - Checks IP reputation
|
|
- `smtpClients` - Map of pooled SMTP clients
|
|
|
|
### Email Traffic Flow
|
|
```
|
|
External Port → SmartProxy → Internal Port → UnifiedEmailServer → Processing
|
|
25 ↓ 10025 ↓ ↓
|
|
587 Routes 10587 DomainRouter DeliverySystem
|
|
465 10465 ↓
|
|
Queue → SmtpClient
|
|
(pooled & reused)
|
|
```
|
|
|
|
### Completed Improvements
|
|
- ✅ All email components consolidated under UnifiedEmailServer
|
|
- ✅ EmailSendJob uses pooled SmtpClient via `getSmtpClient(host, port)`
|
|
- ✅ DcRouter simplified to just manage high-level services
|
|
- ✅ Connection pooling implemented for all outbound mail
|
|
- ✅ setupUnifiedEmailHandling() simplified to ~30 lines
|
|
|
|
## SMTP Client Management (2025-05-27)
|
|
|
|
### Centralized SMTP Client in UnifiedEmailServer
|
|
- SMTP clients are now managed centrally in UnifiedEmailServer
|
|
- Uses connection pooling for efficiency (one pool per destination host:port)
|
|
- Classes using UnifiedEmailServer get SMTP clients via `getSmtpClient(host, port)`
|
|
|
|
### Implementation Details
|
|
```typescript
|
|
// In UnifiedEmailServer
|
|
private smtpClients: Map<string, SmtpClient> = new Map(); // host:port -> client
|
|
|
|
public getSmtpClient(host: string, port: number = 25): SmtpClient {
|
|
const clientKey = `${host}:${port}`;
|
|
let client = this.smtpClients.get(clientKey);
|
|
|
|
if (!client) {
|
|
client = createPooledSmtpClient({
|
|
host,
|
|
port,
|
|
secure: port === 465,
|
|
connectionTimeout: 30000,
|
|
socketTimeout: 120000,
|
|
maxConnections: 10,
|
|
maxMessages: 1000,
|
|
pool: true
|
|
});
|
|
this.smtpClients.set(clientKey, client);
|
|
}
|
|
|
|
return client;
|
|
}
|
|
```
|
|
|
|
### Usage Pattern
|
|
- EmailSendJob and DeliverySystem now use `this.emailServerRef.getSmtpClient(host, port)`
|
|
- Connection pooling happens automatically
|
|
- Connections are reused across multiple send jobs
|
|
- All SMTP clients are closed when UnifiedEmailServer stops
|
|
|
|
### Dependency Injection Pattern
|
|
- Classes that need UnifiedEmailServer functionality receive it as constructor argument
|
|
- This provides access to SMTP clients, DKIM signing, and other shared functionality
|
|
- Example: `new EmailSendJob(emailServerRef, email, options)`
|
|
|
|
## Email Class Standardization (2025-05-27) - COMPLETED
|
|
|
|
### Overview
|
|
The entire codebase has been standardized to use the `Email` class as the single data structure for email handling. All Smartmail usage has been eliminated.
|
|
|
|
### Key Changes
|
|
1. **Email Class Enhanced** - Added compatibility methods: `getSubject()`, `getBody(isHtml)`, `getFrom()`
|
|
2. **BounceManager** - Now accepts `Email` objects directly
|
|
3. **TemplateManager** - Returns `Email` objects instead of Smartmail
|
|
4. **EmailService** - `sendEmail()` accepts `Email` objects
|
|
5. **RuleManager** - Uses `SmartRule<Email>` instead of `SmartRule<Smartmail>`
|
|
6. **ApiManager** - Creates `Email` objects for API requests
|
|
|
|
### Benefits
|
|
- No more Email ↔ Smartmail conversions
|
|
- Consistent API throughout (`email.subject` not `smartmail.options.subject`)
|
|
- Better performance (no conversion overhead)
|
|
- Simpler, more maintainable code
|
|
|
|
### Usage Pattern
|
|
```typescript
|
|
// Create email
|
|
const email = new Email({
|
|
from: 'sender@example.com',
|
|
to: 'recipient@example.com',
|
|
subject: 'Hello',
|
|
text: 'World'
|
|
});
|
|
|
|
// Pass directly through system
|
|
await bounceManager.processBounceEmail(email);
|
|
await templateManager.prepareEmail(templateId, context);
|
|
await emailService.sendEmail(email);
|
|
```
|
|
|
|
## Email Class Design Pattern (2025-05-27)
|
|
|
|
### Three-Interface Pattern for Email
|
|
The Email system uses three distinct interfaces for clarity and type safety:
|
|
|
|
1. **IEmailOptions** - The flexible input interface:
|
|
```typescript
|
|
interface IEmailOptions {
|
|
to: string | string[]; // Flexible: single or array
|
|
cc?: string | string[]; // Optional
|
|
attachments?: IAttachment[]; // Optional
|
|
skipAdvancedValidation?: boolean; // Constructor-only option
|
|
}
|
|
```
|
|
- Used as constructor parameter
|
|
- Allows flexible input formats
|
|
- Has constructor-only options (like skipAdvancedValidation)
|
|
|
|
2. **INormalizedEmail** - The normalized runtime interface:
|
|
```typescript
|
|
interface INormalizedEmail {
|
|
to: string[]; // Always an array
|
|
cc: string[]; // Always an array (empty if not provided)
|
|
attachments: IAttachment[]; // Always an array (empty if not provided)
|
|
mightBeSpam: boolean; // Always has a value (defaults to false)
|
|
}
|
|
```
|
|
- Represents the guaranteed internal structure
|
|
- No optional arrays - everything has a default
|
|
- Email class implements this interface
|
|
|
|
3. **Email class** - The implementation:
|
|
```typescript
|
|
export class Email implements INormalizedEmail {
|
|
// All INormalizedEmail properties
|
|
to: string[];
|
|
cc: string[];
|
|
// ... etc
|
|
|
|
// Additional runtime properties
|
|
private messageId: string;
|
|
private envelopeFrom: string;
|
|
}
|
|
```
|
|
- Implements INormalizedEmail
|
|
- Adds behavior methods and computed properties
|
|
- Handles validation and normalization
|
|
|
|
### Benefits of This Pattern:
|
|
- **Type Safety**: Email class explicitly implements INormalizedEmail
|
|
- **Clear Contracts**: Input vs. runtime structure is explicit
|
|
- **Flexibility**: IEmailOptions allows various input formats
|
|
- **Consistency**: INormalizedEmail guarantees structure
|
|
- **Validation**: Constructor validates and normalizes
|
|
|
|
### Usage:
|
|
```typescript
|
|
// Input with flexible options
|
|
const options: IEmailOptions = {
|
|
from: 'sender@example.com',
|
|
to: 'recipient@example.com', // Single string
|
|
subject: 'Hello',
|
|
text: 'World'
|
|
};
|
|
|
|
// Creates normalized Email instance
|
|
const email = new Email(options);
|
|
|
|
// email.to is guaranteed to be string[]
|
|
email.to.forEach(recipient => {
|
|
// No need to check if it's an array
|
|
});
|
|
|
|
// Convert back to options format
|
|
const optionsAgain = email.toEmailOptions();
|
|
```
|
|
|
|
### Template Email Creation (2025-05-27)
|
|
The Email class now supports template creation without recipients:
|
|
- IEmailOptions 'to' field is now optional (for templates)
|
|
- Email constructor allows creation without recipients
|
|
- Recipients are added later when the email is actually sent
|
|
|
|
```typescript
|
|
// Template creation (no recipients)
|
|
const emailOptions: IEmailOptions = {
|
|
from: 'noreply@example.com',
|
|
subject: 'Welcome {{name}}',
|
|
text: 'Hello {{name}}!',
|
|
// 'to' is omitted for templates
|
|
variables: { name: 'User' }
|
|
};
|
|
|
|
const templateEmail = new Email(emailOptions);
|
|
// templateEmail.to is an empty array []
|
|
|
|
// Later, when sending:
|
|
templateEmail.to = ['recipient@example.com'];
|
|
```
|
|
|
|
## Email Architecture Consolidation Completed (2025-05-27)
|
|
|
|
### Summary
|
|
The email architecture consolidation has been fully completed. Contrary to the initial analysis, the architecture was already well-organized:
|
|
|
|
1. **UnifiedEmailServer already contains all components** - No scattered components in DcRouter
|
|
2. **EmailSendJob already uses pooled SmtpClient** - No duplicate SMTP implementations
|
|
3. **setupUnifiedEmailHandling() is simple** - Only ~30 lines, not 150+
|
|
4. **Connection pooling already implemented** - Via `getSmtpClient(host, port)`
|
|
5. **SmtpPortConfig doesn't exist** - No orphaned code found
|
|
|
|
### Key Architecture Points
|
|
- DcRouter has single `emailServer?: UnifiedEmailServer` property
|
|
- All email functionality encapsulated in UnifiedEmailServer
|
|
- Dependency injection pattern used throughout (e.g., EmailSendJob receives UnifiedEmailServer reference)
|
|
- Pooled SMTP clients managed centrally in UnifiedEmailServer
|
|
- Clean separation of concerns between routing (DcRouter) and email handling (UnifiedEmailServer)
|
|
|
|
## Email Router Architecture Decision (2025-05-28)
|
|
|
|
### Single Router Class
|
|
- **Important**: We will have only ONE router class, not two
|
|
- The existing `DomainRouter` will be evolved into `EmailRouter`
|
|
- This avoids confusion and redundancy
|
|
- Use `git mv` to rename and preserve git history
|
|
- Extend it to support the new match/action pattern inspired by SmartProxy
|
|
- Maintain backward compatibility for legacy domain-based rules
|
|
|
|
### Benefits of Single Router
|
|
- Clear, single source of truth for routing logic
|
|
- No confusion about which router to use
|
|
- Preserved git history and gradual migration path
|
|
- Supports all match criteria (not just domains)
|
|
|
|
## Email Routing Architecture (2025-05-27)
|
|
|
|
### Current Routing Capabilities
|
|
1. **Pattern-based routing** - DomainRouter matches email addresses against patterns
|
|
2. **Three processing modes**:
|
|
- `mta` - Programmatic processing with optional DKIM signing
|
|
- `process` - Store-and-forward with content scanning
|
|
- `forward` - Direct forwarding (NOT YET IMPLEMENTED)
|
|
3. **Default routing** - Fallback for unmatched patterns
|
|
4. **Basic caching** - LRU cache for routing decisions
|
|
|
|
### Routing Flow
|
|
```typescript
|
|
// Current flow in UnifiedEmailServer.processEmailByMode()
|
|
1. Email arrives → DomainRouter.matchRule(recipient)
|
|
2. Apply matched rule or default
|
|
3. Process based on mode:
|
|
- mta: Apply DKIM, log, return
|
|
- process: Scan content, apply transformations, queue
|
|
- forward: ERROR - Not implemented
|
|
```
|
|
|
|
### Missing Routing Features
|
|
1. **No forwarding implementation** - Forward mode throws error
|
|
2. **Limited matching** - Only email address patterns, no regex
|
|
3. **No conditional routing** - Can't route based on subject, size, etc.
|
|
4. **No load balancing** - Single destination per rule
|
|
5. **No failover** - No backup routes
|
|
6. **Basic transformations** - Only header additions
|
|
|
|
### Key Files for Routing
|
|
- `ts/mail/routing/classes.domain.router.ts` - Pattern matching engine
|
|
- `ts/mail/routing/classes.unified.email.server.ts` - processEmailByMode()
|
|
- `ts/mail/routing/classes.email.config.ts` - Rule interfaces
|
|
- `ts/mail/delivery/classes.delivery.system.ts` - Delivery execution
|
|
|
|
## Configuration System Cleanup (2025-05-27) - COMPLETED
|
|
|
|
### Overview
|
|
The `ts/config/` directory cleanup has been completed. Removed ~500+ lines of unused legacy configuration code.
|
|
|
|
### Changes Made
|
|
✅ **Removed Files:**
|
|
- `base.config.ts` - All unused base interfaces
|
|
- `platform.config.ts` - Completely unused platform config
|
|
- `email.config.ts` - Deprecated email configuration
|
|
- `email.port.mapping.ts` - Unused port mapping utilities
|
|
- `schemas.ts` - Removed all schemas except SMS
|
|
- `sms.config.ts` - Moved to SMS module
|
|
|
|
✅ **SMS Configuration Moved:**
|
|
- Created `ts/sms/config/sms.config.ts` - ISmsConfig interface
|
|
- Created `ts/sms/config/sms.schema.ts` - Validation schema
|
|
- Updated SmsService to import from new location
|
|
|
|
✅ **Kept:**
|
|
- `validator.ts` - Generic validation utility (might move to utils later)
|
|
- `index.ts` - Now only exports ConfigValidator
|
|
|
|
### Result
|
|
- Config directory now contains only 2 files (validator.ts, index.ts)
|
|
- SMS configuration is self-contained in SMS module
|
|
- All deprecated email configuration removed
|
|
- Build passes successfully
|
|
|
|
## Per-Domain Rate Limiting (2025-05-29) - COMPLETED
|
|
|
|
### Overview
|
|
Per-domain rate limiting has been implemented in the UnifiedRateLimiter. Each email domain can have its own rate limits that override global limits.
|
|
|
|
### Implementation Details
|
|
1. **UnifiedRateLimiter Enhanced:**
|
|
- Added `domains` property to IHierarchicalRateLimits
|
|
- Added `domainCounters` Map for tracking domain-specific counters
|
|
- Added `checkDomainMessageLimit()` method
|
|
- Added `applyDomainLimits()`, `removeDomainLimits()`, `getDomainLimits()` methods
|
|
|
|
2. **Domain Rate Limit Configuration:**
|
|
```typescript
|
|
interface IEmailDomainConfig {
|
|
domain: string;
|
|
rateLimits?: {
|
|
outbound?: {
|
|
messagesPerMinute?: number;
|
|
messagesPerHour?: number; // Note: Hour/day limits need additional implementation
|
|
messagesPerDay?: number;
|
|
};
|
|
inbound?: {
|
|
messagesPerMinute?: number;
|
|
connectionsPerIp?: number;
|
|
recipientsPerMessage?: number;
|
|
};
|
|
};
|
|
}
|
|
```
|
|
|
|
3. **Automatic Application:**
|
|
- UnifiedEmailServer applies domain rate limits during startup
|
|
- `applyDomainRateLimits()` method converts domain config to rate limiter format
|
|
- Domain limits override pattern and global limits
|
|
|
|
4. **Usage Pattern:**
|
|
```typescript
|
|
// Domain configuration with rate limits
|
|
{
|
|
domain: 'high-volume.com',
|
|
dnsMode: 'internal-dns',
|
|
rateLimits: {
|
|
outbound: {
|
|
messagesPerMinute: 200 // Higher than global limit
|
|
},
|
|
inbound: {
|
|
recipientsPerMessage: 100 // Higher recipient limit
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
5. **Rate Limit Precedence:**
|
|
- Domain-specific limits (highest priority)
|
|
- Pattern-specific limits
|
|
- Global limits (lowest priority)
|
|
|
|
### Integration Status
|
|
- ✅ Rate limiter supports per-domain limits
|
|
- ✅ UnifiedEmailServer applies domain limits on startup
|
|
- ✅ Domain limits properly override global/pattern limits
|
|
- ✅ SMTP server handlers now enforce rate limits (COMPLETED 2025-05-29)
|
|
- ⚠️ Hour/day limits need additional implementation in rate limiter
|
|
|
|
### SMTP Handler Integration (2025-05-29) - COMPLETED
|
|
Rate limiting is now fully integrated into SMTP server handlers:
|
|
|
|
1. **UnifiedEmailServer Enhancement:**
|
|
- Added `getRateLimiter()` method to provide access to the rate limiter
|
|
|
|
2. **ConnectionManager Integration:**
|
|
- Replaced custom rate limiting with UnifiedRateLimiter
|
|
- Now uses `rateLimiter.recordConnection(ip)` for all connection checks
|
|
- Maintains local IP tracking for resource cleanup only
|
|
|
|
3. **CommandHandler Integration:**
|
|
- `handleMailFrom()`: Checks message rate limits with domain context
|
|
- `handleRcptTo()`: Enforces recipient limits per message
|
|
- `handleAuth*()`: Records authentication failures and blocks after threshold
|
|
- Error handling: Records syntax/command errors and blocks after threshold
|
|
|
|
4. **SMTP Response Codes:**
|
|
- `421`: Temporary rate limit (client should retry later)
|
|
- `451`: Temporary recipient rejection
|
|
- `421 Too many errors`: IP blocked due to excessive errors
|
|
- `421 Too many authentication failures`: IP blocked due to auth failures
|
|
|
|
### Next Steps
|
|
The only remaining item is implementing hour/day rate limits in the UnifiedRateLimiter, which would require:
|
|
1. Additional counters for hourly and daily windows
|
|
2. Separate tracking for these longer time periods
|
|
3. Cleanup logic for expired hourly/daily counters
|
|
|
|
## DNS Architecture Refactoring (2025-05-30) - COMPLETED
|
|
|
|
### Overview
|
|
The DNS functionality has been refactored from UnifiedEmailServer to a dedicated DnsManager class for better discoverability and separation of concerns.
|
|
|
|
### Key Changes
|
|
1. **Renamed DnsValidator to DnsManager:**
|
|
- Extended functionality to handle both validation and creation of DNS records
|
|
- Added `ensureDnsRecords()` as the main entry point
|
|
- Moved DNS record creation logic from UnifiedEmailServer
|
|
|
|
2. **DnsManager Responsibilities:**
|
|
- Validate DNS configuration for all modes (forward, internal-dns, external-dns)
|
|
- Create DNS records for internal-dns domains
|
|
- Create DKIM records for all domains (when DKIMCreator is provided)
|
|
- Store DNS records in StorageManager for persistence
|
|
|
|
3. **DNS Record Creation Flow:**
|
|
```typescript
|
|
// In UnifiedEmailServer
|
|
const dnsManager = new DnsManager(this.dcRouter);
|
|
await dnsManager.ensureDnsRecords(domainConfigs, this.dkimCreator);
|
|
```
|
|
|
|
4. **Testing Pattern for DNS:**
|
|
- Mock the DNS server in tests by providing a mock `registerHandler` function
|
|
- Store handlers in a Map with key format: `${domain}:${types.join(',')}`
|
|
- Retrieve handlers with key format: `${domain}:${type}`
|
|
- Example mock implementation:
|
|
```typescript
|
|
this.dnsServer = {
|
|
registerHandler: (name: string, types: string[], handler: () => any) => {
|
|
const key = `${name}:${types.join(',')}`;
|
|
this.dnsHandlers.set(key, handler);
|
|
}
|
|
};
|
|
```
|
|
|
|
### Benefits
|
|
- DNS functionality is now easily discoverable in DnsManager
|
|
- Clear separation between DNS management and email server logic
|
|
- UnifiedEmailServer is simpler and more focused
|
|
- All DNS-related tests pass successfully |