4 Commits

Author SHA1 Message Date
7908cbaefa v5.0.0
Some checks failed
CI / Type Check & Lint (push) Failing after 4s
CI / Build Test (Current Platform) (push) Failing after 4s
CI / Build All Platforms (push) Failing after 4s
Publish to npm / npm-publish (push) Failing after 5s
Release / build-and-release (push) Failing after 4s
2026-02-11 07:55:28 +00:00
526dcb4dac BREAKING CHANGE(mail): remove DMARC and DKIM verifier implementations and MTA error classes; introduce DkimManager and EmailActionExecutor; simplify SPF verifier and update routing exports and tests 2026-02-11 07:55:28 +00:00
cf8fcb6efa v4.1.1
Some checks failed
CI / Type Check & Lint (push) Failing after 4s
CI / Build Test (Current Platform) (push) Failing after 4s
CI / Build All Platforms (push) Failing after 4s
Publish to npm / npm-publish (push) Failing after 5s
Release / build-and-release (push) Failing after 5s
2026-02-11 07:36:54 +00:00
2088c9f76e fix(readme): clarify architecture and IPC, document outbound flow and testing, and update module and crate descriptions in README 2026-02-11 07:36:54 +00:00
17 changed files with 763 additions and 2059 deletions

View File

@@ -1,5 +1,26 @@
# Changelog # Changelog
## 2026-02-11 - 5.0.0 - BREAKING CHANGE(mail)
remove DMARC and DKIM verifier implementations and MTA error classes; introduce DkimManager and EmailActionExecutor; simplify SPF verifier and update routing exports and tests
- Removed ts/mail/security/classes.dmarcverifier.ts and ts/mail/security/classes.dkimverifier.ts — DMARC and DKIM verifier implementations deleted
- Removed ts/errors/index.ts — MTA-specific error classes removed
- Added ts/mail/routing/classes.dkim.manager.ts — new DKIM key management and rotation logic
- Added ts/mail/routing/classes.email.action.executor.ts — centralized email action execution (forward/process/deliver/reject)
- Updated ts/mail/security/classes.spfverifier.ts to retain SPF parsing but removed verify/verifyAndApply logic delegating to Rust bridge
- Updated ts/mail/routing/index.ts to export new routing classes and adjusted import paths (e.g. delivery queue import updated)
- Tests trimmed: DMARC tests and rate limiter tests removed; SPF parsing test retained and simplified
- This set of changes alters public exports and removes previously available verifier APIs — major version bump recommended
## 2026-02-11 - 4.1.1 - fix(readme)
clarify architecture and IPC, document outbound flow and testing, and update module and crate descriptions in README
- Changed IPC description to JSON-over-stdin/stdout (clarifies communication format between Rust and TypeScript)
- Added Rust SMTP client entry and documented outbound mail data flow (TypeScript -> Rust signing/delivery -> result back)
- Expanded testing instructions with commands for building Rust binary and running unit/E2E tests
- Updated architecture diagram labels and Rust crate/module descriptions (mailer-smtp now includes client; test counts noted)
- Documentation-only changes; no source code behavior modified
## 2026-02-11 - 4.1.0 - feat(e2e-tests) ## 2026-02-11 - 4.1.0 - feat(e2e-tests)
add Node.js end-to-end tests covering server lifecycle, inbound SMTP handling, outbound delivery and routing actions add Node.js end-to-end tests covering server lifecycle, inbound SMTP handling, outbound delivery and routing actions

View File

@@ -1,6 +1,6 @@
{ {
"name": "@push.rocks/smartmta", "name": "@push.rocks/smartmta",
"version": "4.1.0", "version": "5.0.0",
"description": "A high-performance, enterprise-grade Mail Transfer Agent (MTA) built from scratch in TypeScript with Rust acceleration.", "description": "A high-performance, enterprise-grade Mail Transfer Agent (MTA) built from scratch in TypeScript with Rust acceleration.",
"keywords": [ "keywords": [
"mta", "mta",

322
readme.md
View File

@@ -18,14 +18,14 @@ After installation, run `pnpm build` to compile the Rust binary (`mailer-bin`).
## Overview ## Overview
`@push.rocks/smartmta` is a **complete mail server solution** — SMTP server, SMTP client, email security, content scanning, and delivery management — all built with a custom SMTP implementation. The SMTP server itself runs as a Rust binary for maximum performance, communicating with the TypeScript orchestration layer via IPC. `@push.rocks/smartmta` is a **complete mail server solution** — SMTP server, SMTP client, email security, content scanning, and delivery management — all built with a custom SMTP implementation. The SMTP engine runs as a Rust binary for maximum performance, communicating with the TypeScript orchestration layer via JSON-over-stdin/stdout IPC.
### ⚡ What's Inside ### ⚡ What's Inside
| Module | What It Does | | Module | What It Does |
|---|---| |---|---|
| **Rust SMTP Server** | High-performance SMTP engine written in Rust — TCP/TLS listener, STARTTLS, AUTH, pipelining, per-connection rate limiting | | **Rust SMTP Server** | High-performance SMTP engine in Rust — TCP/TLS listener, STARTTLS, AUTH, pipelining, per-connection rate limiting |
| **SMTP Client** | Outbound delivery with connection pooling, retry logic, TLS negotiation | | **Rust SMTP Client** | Outbound delivery with connection pooling, retry logic, TLS negotiation, DKIM signing — all in Rust |
| **DKIM** | Key generation, signing, and verification — per domain, with automatic rotation | | **DKIM** | Key generation, signing, and verification — per domain, with automatic rotation |
| **SPF** | Full SPF record validation via Rust | | **SPF** | Full SPF record validation via Rust |
| **DMARC** | Policy enforcement and verification | | **DMARC** | Policy enforcement and verification |
@@ -37,8 +37,7 @@ After installation, run `pnpm build` to compile the Rust binary (`mailer-bin`).
| **Delivery Queue** | Persistent queue with exponential backoff retry | | **Delivery Queue** | Persistent queue with exponential backoff retry |
| **Template Engine** | Email templates with variable substitution | | **Template Engine** | Email templates with variable substitution |
| **Domain Registry** | Multi-domain management with per-domain configuration | | **Domain Registry** | Multi-domain management with per-domain configuration |
| **DNS Manager** | Automatic DNS record management with Cloudflare API integration | | **DNS Manager** | Automatic DNS record management (MX, SPF, DKIM, DMARC) |
| **Rust Security Bridge** | All security ops (DKIM+SPF+DMARC+DNSBL+content scanning) run in Rust via IPC |
### 🏗️ Architecture ### 🏗️ Architecture
@@ -52,9 +51,9 @@ After installation, run `pnpm build` to compile the Rust binary (`mailer-bin`).
│ ┌──────┐ │ ┌───────┐ │ ┌──────────┐ │ ┌────────────────┐ │ │ ┌──────┐ │ ┌───────┐ │ ┌──────────┐ │ ┌────────────────┐ │
│ │Match │ │ │ DKIM │ │ │ Queue │ │ │ DomainRegistry │ │ │ │Match │ │ │ DKIM │ │ │ Queue │ │ │ DomainRegistry │ │
│ │Route │ │ │ SPF │ │ │ Rate Lim │ │ │ DnsManager │ │ │ │Route │ │ │ SPF │ │ │ Rate Lim │ │ │ DnsManager │ │
│ │ Act │ │ │ DMARC │ │ │ SMTP Cli │ │ │ DKIMCreator │ │ │ │ Act │ │ │ DMARC │ │ │ Retry │ │ │ DKIMCreator │ │
│ └──────┘ │ │ IPRep │ │ │ Retry │ │ │ Templates │ │ │ └──────┘ │ │ IPRep │ │ └──────────┘ │ │ Templates │ │
│ │ │ Scan │ │ └──────────┘ │ └────────────────┘ │ │ │ │ Scan │ │ │ └────────────────┘ │
│ │ └───────┘ │ │ │ │ │ └───────┘ │ │ │
├───────────┴───────────┴──────────────┴───────────────────────┤ ├───────────┴───────────┴──────────────┴───────────────────────┤
│ Rust Security Bridge (smartrust IPC) │ │ Rust Security Bridge (smartrust IPC) │
@@ -63,18 +62,25 @@ After installation, run `pnpm build` to compile the Rust binary (`mailer-bin`).
│ ┌──────────────┐ ┌───────────────┐ ┌──────────────────┐ │ │ ┌──────────────┐ ┌───────────────┐ ┌──────────────────┐ │
│ │ mailer-smtp │ │mailer-security│ │ mailer-core │ │ │ │ mailer-smtp │ │mailer-security│ │ mailer-core │ │
│ │ SMTP Server │ │DKIM/SPF/DMARC │ │ Types/Validation │ │ │ │ SMTP Server │ │DKIM/SPF/DMARC │ │ Types/Validation │ │
│ │ TLS/AUTH │ │IP Rep/Content │ │ MIME/Bounce │ │ │ │ SMTP Client │ │IP Rep/Content │ │ MIME/Bounce │ │
│ │ TLS/AUTH │ │ Scanning │ │ Detection │ │
│ └──────────────┘ └───────────────┘ └──────────────────┘ │ │ └──────────────┘ └───────────────┘ └──────────────────┘ │
└──────────────────────────────────────────────────────────────┘ └──────────────────────────────────────────────────────────────┘
``` ```
**Data flow for inbound mail:** **Data flow for inbound mail:**
1. Rust SMTP server accepts the connection and handles the full SMTP protocol 1. 📨 Rust SMTP server accepts the connection and handles the full SMTP protocol
2. On `DATA` completion, Rust runs the security pipeline **in-process** (DKIM/SPF/DMARC verification, content scanning, IP reputation check) — zero IPC round-trips 2. 🔒 On `DATA` completion, Rust runs the security pipeline **in-process** (DKIM/SPF/DMARC verification, content scanning, IP reputation check) — zero IPC round-trips
3. Rust emits an `emailReceived` event via IPC with pre-computed security results attached 3. 📤 Rust emits an `emailReceived` event via IPC with pre-computed security results attached
4. TypeScript processes the email (routing decisions using the pre-computed results, delivery) 4. 🔀 TypeScript processes the email (routing decisions using the pre-computed results, delivery)
5. Rust sends the final SMTP response to the client 5. Rust sends the final SMTP response to the client
**Data flow for outbound mail:**
1. 📝 TypeScript constructs the email and resolves DKIM keys for the sender domain
2. 🦀 Sends to Rust via IPC — Rust builds the RFC 2822 message, signs with DKIM, and delivers via its SMTP client with connection pooling
3. 📬 Result (accepted/rejected recipients, server response) returned to TypeScript
## Usage ## Usage
@@ -163,32 +169,19 @@ await emailServer.start();
> 🔒 **Note:** `start()` will throw if the Rust binary is not compiled. Run `pnpm build` first. > 🔒 **Note:** `start()` will throw if the Rust binary is not compiled. Run `pnpm build` first.
### 📧 Sending Emails with the SMTP Client ### 📧 Sending Outbound Emails
Create and send emails using the built-in SMTP client with connection pooling: All outbound email delivery goes through the Rust SMTP client, accessed via `UnifiedEmailServer.sendOutboundEmail()`. The Rust client handles connection pooling, TLS negotiation, and DKIM signing automatically:
```typescript ```typescript
import { Email, Delivery } from '@push.rocks/smartmta'; import { Email, UnifiedEmailServer } from '@push.rocks/smartmta';
// Create a client with connection pooling
const client = Delivery.smtpClientMod.createSmtpClient({
host: 'smtp.example.com',
port: 587,
secure: false, // will upgrade via STARTTLS
pool: true,
maxConnections: 5,
auth: {
user: 'sender@example.com',
pass: 'your-password',
},
});
// Build an email // Build an email
const email = new Email({ const email = new Email({
from: 'sender@example.com', from: 'sender@example.com',
to: ['recipient@example.com'], to: ['recipient@example.com'],
cc: ['cc@example.com'], cc: ['cc@example.com'],
subject: 'Hello from smartmta!', subject: 'Hello from smartmta! 🚀',
text: 'Plain text body', text: 'Plain text body',
html: '<h1>Hello!</h1><p>HTML body with <strong>formatting</strong></p>', html: '<h1>Hello!</h1><p>HTML body with <strong>formatting</strong></p>',
priority: 'high', priority: 'high',
@@ -201,32 +194,32 @@ const email = new Email({
], ],
}); });
// Send it // Send via the Rust SMTP client (connection pooling, TLS, DKIM signing)
const result = await client.sendMail(email); const result = await emailServer.sendOutboundEmail('smtp.example.com', 587, email, {
console.log(`Message sent: ${result.messageId}`); auth: { user: 'sender@example.com', pass: 'your-password' },
dkimDomain: 'example.com',
dkimSelector: 'default',
});
console.log(`Accepted: ${result.accepted.join(', ')}`);
console.log(`Response: ${result.response}`);
// -> Accepted: recipient@example.com
// -> Response: 2.0.0 Ok: queued
``` ```
Additional client factories are available: The `sendOutboundEmail` method:
- 🔑 Automatically resolves DKIM keys from the `DKIMCreator` for the specified domain
- 🔗 Uses connection pooling in Rust — reuses TCP/TLS connections across sends
- ⏱️ Configurable connection and socket timeouts via `outbound` options on the server
```typescript ### 🔑 DKIM Signing & Key Management
// Pooled client for high-throughput scenarios
const pooled = Delivery.smtpClientMod.createPooledSmtpClient({ /* ... */ });
// Optimized for bulk sending DKIM key management is handled by `DKIMCreator`, which generates, stores, and rotates keys per domain. Signing is performed automatically by the Rust SMTP client during outbound delivery:
const bulk = Delivery.smtpClientMod.createBulkSmtpClient({ /* ... */ });
// Optimized for transactional emails
const transactional = Delivery.smtpClientMod.createTransactionalSmtpClient({ /* ... */ });
```
### 🔑 DKIM Signing
DKIM key management is handled by `DKIMCreator`, which generates, stores, and rotates keys per domain. Signing is performed automatically by `UnifiedEmailServer` during outbound delivery:
```typescript ```typescript
import { DKIMCreator } from '@push.rocks/smartmta'; import { DKIMCreator } from '@push.rocks/smartmta';
const dkimCreator = new DKIMCreator('/path/to/keys'); const dkimCreator = new DKIMCreator('/path/to/keys', storageManager);
// Auto-generate keys if they don't exist // Auto-generate keys if they don't exist
await dkimCreator.handleDKIMKeysForDomain('example.com'); await dkimCreator.handleDKIMKeysForDomain('example.com');
@@ -244,30 +237,34 @@ if (needsRotation) {
} }
``` ```
When `UnifiedEmailServer.start()` is called, DKIM signing is applied to all outbound mail automatically using the Rust security bridge's `signDkim()` method for maximum performance. When `UnifiedEmailServer.start()` is called:
- DKIM keys are generated or loaded for every configured domain
- Signing is applied to all outbound mail via the Rust security bridge
- Key rotation is checked automatically based on your `rotationInterval` config
### 🛡️ Email Authentication (SPF, DKIM, DMARC) ### 🛡️ Email Authentication (SPF, DKIM, DMARC)
Verify incoming emails against all three authentication standards. All verification is powered by the Rust binary: All verification is powered by the Rust binary. For inbound mail, `UnifiedEmailServer` runs the full security pipeline **automatically** — DKIM, SPF, DMARC, content scanning, and IP reputation in a single Rust pass. Results are attached as headers (`Received-SPF`, `X-DKIM-Result`, `X-DMARC-Result`).
You can also use the individual verifiers directly:
```typescript ```typescript
import { DKIMVerifier, SpfVerifier, DmarcVerifier } from '@push.rocks/smartmta'; import { DKIMVerifier, SpfVerifier, DmarcVerifier } from '@push.rocks/smartmta';
// SPF verification — first arg is an Email object // SPF verification
const spfVerifier = new SpfVerifier(); const spfVerifier = new SpfVerifier();
const spfResult = await spfVerifier.verify(email, senderIP, heloDomain); const spfResult = await spfVerifier.verify(email, senderIP, heloDomain);
// -> { result: 'pass' | 'fail' | 'softfail' | 'neutral' | 'none' | 'temperror' | 'permerror', // -> { result: 'pass' | 'fail' | 'softfail' | 'neutral' | 'none', domain, ip }
// domain: string, ip: string }
// DKIM verification — takes raw email content // DKIM verification
const dkimVerifier = new DKIMVerifier(); const dkimVerifier = new DKIMVerifier();
const dkimResult = await dkimVerifier.verify(rawEmailContent); const dkimResult = await dkimVerifier.verify(rawEmailContent);
// -> [{ is_valid: true, domain: 'example.com', selector: 'default', status: 'pass' }]
// DMARC verification — first arg is an Email object // DMARC verification
const dmarcVerifier = new DmarcVerifier(); const dmarcVerifier = new DmarcVerifier();
const dmarcResult = await dmarcVerifier.verify(email, spfResult, dkimResult); const dmarcResult = await dmarcVerifier.verify(email, spfResult, dkimResult);
// -> { action: 'pass' | 'quarantine' | 'reject', hasDmarc: boolean, // -> { action: 'pass' | 'quarantine' | 'reject', policy, spfDomainAligned, dkimDomainAligned }
// spfDomainAligned: boolean, dkimDomainAligned: boolean, ... }
``` ```
### 🔀 Email Routing ### 🔀 Email Routing
@@ -294,7 +291,7 @@ const router = new EmailRouter([
priority: 50, priority: 50,
match: { match: {
recipients: '*@example.com', recipients: '*@example.com',
sizeRange: { max: 10 * 1024 * 1024 }, // under 10MB sizeRange: { max: 10 * 1024 * 1024 }, // under 10MB
}, },
action: { action: {
type: 'forward', type: 'forward',
@@ -326,7 +323,16 @@ const router = new EmailRouter([
const matchedRoute = await router.evaluateRoutes(emailContext); const matchedRoute = await router.evaluateRoutes(emailContext);
``` ```
**Match criteria available:** #### Route Action Types
| Action | Description |
|---|---|
| `forward` | Forward the email to another SMTP server via the Rust SMTP client |
| `deliver` | Queue for local MTA delivery |
| `process` | Queue for processing (with optional content scanning and DKIM signing) |
| `reject` | Reject with a configurable SMTP error code and message |
#### Match Criteria
| Criterion | Description | | Criterion | Description |
|---|---| |---|---|
@@ -339,52 +345,6 @@ const matchedRoute = await router.evaluateRoutes(emailContext);
| `subject` | Subject line pattern (string or RegExp) | | `subject` | Subject line pattern (string or RegExp) |
| `hasAttachments` | Filter by attachment presence | | `hasAttachments` | Filter by attachment presence |
### 🔍 Content Scanning
Built-in content scanner for detecting spam, phishing, malware, and other threats. Text pattern scanning runs in Rust for performance; binary attachment scanning (PE headers, VBA macros) runs in TypeScript:
```typescript
import { ContentScanner } from '@push.rocks/smartmta';
const scanner = new ContentScanner({
scanSubject: true,
scanBody: true,
scanAttachments: true,
blockExecutables: true,
blockMacros: true,
minThreatScore: 30,
highThreatScore: 70,
customRules: [
{
pattern: /bitcoin.*wallet/i,
type: 'scam',
score: 80,
description: 'Cryptocurrency scam pattern',
},
],
});
const result = await scanner.scanEmail(email);
// -> { isClean: false, threatScore: 85, threatType: 'phishing', scannedElements: [...] }
```
### 🌐 IP Reputation Checking
Check sender IP addresses against DNSBL blacklists and classify IP types. DNSBL lookups run in Rust:
```typescript
import { IPReputationChecker } from '@push.rocks/smartmta';
const ipChecker = IPReputationChecker.getInstance({
enableDNSBL: true,
dnsblServers: ['zen.spamhaus.org', 'bl.spamcop.net'],
cacheTTL: 24 * 60 * 60 * 1000, // 24 hours
});
const reputation = await ipChecker.checkReputation('192.168.1.1');
// -> { score: 85, isSpam: false, isProxy: false, isTor: false, blacklists: [] }
```
### ⏱️ Rate Limiting ### ⏱️ Rate Limiting
Hierarchical rate limiting to protect your server and maintain deliverability: Hierarchical rate limiting to protect your server and maintain deliverability:
@@ -445,7 +405,7 @@ const bounce = await bounceManager.processSmtpFailure(
// Check if an address is suppressed due to bounces // Check if an address is suppressed due to bounces
const suppressed = bounceManager.isEmailSuppressed('recipient@example.com'); const suppressed = bounceManager.isEmailSuppressed('recipient@example.com');
// Manually manage the suppression list // Manage the suppression list
bounceManager.addToSuppressionList('bad@example.com', 'repeated hard bounces'); bounceManager.addToSuppressionList('bad@example.com', 'repeated hard bounces');
bounceManager.removeFromSuppressionList('recovered@example.com'); bounceManager.removeFromSuppressionList('recovered@example.com');
``` ```
@@ -484,7 +444,7 @@ const email = await templates.createEmail('welcome', {
### 🌍 DNS Management ### 🌍 DNS Management
DNS record management for email authentication is handled automatically by `UnifiedEmailServer`. When the server starts, it ensures MX, SPF, DKIM, and DMARC records are in place for all configured domains via the Cloudflare API: When `UnifiedEmailServer.start()` is called, it automatically ensures MX, SPF, DKIM, and DMARC records are in place for all configured domains:
```typescript ```typescript
const emailServer = new UnifiedEmailServer(dcRouterRef, { const emailServer = new UnifiedEmailServer(dcRouterRef, {
@@ -492,7 +452,7 @@ const emailServer = new UnifiedEmailServer(dcRouterRef, {
domains: [ domains: [
{ {
domain: 'example.com', domain: 'example.com',
dnsMode: 'external-dns', // managed via Cloudflare API dnsMode: 'external-dns', // managed via Cloudflare API
}, },
], ],
// ... other config // ... other config
@@ -506,99 +466,43 @@ const emailServer = new UnifiedEmailServer(dcRouterRef, {
await emailServer.start(); await emailServer.start();
``` ```
### 🦀 RustSecurityBridge
The `RustSecurityBridge` is the singleton that manages the Rust binary process. It handles security verification, content scanning, bounce detection, and the SMTP server lifecycle — all via `@push.rocks/smartrust` IPC:
```typescript
import { RustSecurityBridge } from '@push.rocks/smartmta';
const bridge = RustSecurityBridge.getInstance();
await bridge.start();
// Compound verification: DKIM + SPF + DMARC in a single IPC call
const securityResult = await bridge.verifyEmail({
rawMessage: rawEmailString,
ip: '203.0.113.10',
heloDomain: 'sender.example.com',
mailFrom: 'user@example.com',
});
// -> { dkim: [...], spf: { result, explanation }, dmarc: { result, policy } }
// Individual security operations
const dkimResults = await bridge.verifyDkim(rawEmailString);
const spfResult = await bridge.checkSpf({
ip: '203.0.113.10',
heloDomain: 'sender.example.com',
mailFrom: 'user@example.com',
});
const reputationResult = await bridge.checkIpReputation('203.0.113.10');
// DKIM signing
const signed = await bridge.signDkim({
email: rawEmailString,
domain: 'example.com',
selector: 'default',
privateKeyPem: privateKey,
});
// Content scanning
const scanResult = await bridge.scanContent({
subject: 'Win a free iPhone!!!',
body: '<a href="http://phishing.example.com">Click here</a>',
from: 'scammer@evil.com',
});
// Bounce detection
const bounceResult = await bridge.detectBounce({
subject: 'Delivery Status Notification (Failure)',
body: '550 5.1.1 User unknown',
from: 'mailer-daemon@example.com',
});
await bridge.stop();
```
> ⚠️ **Important:** The Rust bridge is **mandatory**. There are no TypeScript fallbacks. If the Rust binary is unavailable, `UnifiedEmailServer.start()` will throw an error.
## 🦀 Rust Acceleration Layer ## 🦀 Rust Acceleration Layer
Performance-critical operations are implemented in Rust and communicate with the TypeScript runtime via `@push.rocks/smartrust` (JSON-over-stdin/stdout IPC). The Rust workspace lives at `rust/` with five crates: Performance-critical operations are implemented in Rust and communicate with the TypeScript runtime via `@push.rocks/smartrust` (JSON-over-stdin/stdout IPC). The Rust workspace lives at `rust/` with four crates:
| Crate | Status | Purpose | | Crate | Status | Purpose |
|---|---|---| |---|---|---|
| `mailer-core` | ✅ Complete (26 tests) | Email types, validation, MIME building, bounce detection | | `mailer-core` | ✅ Complete (26 tests) | Email types, validation, MIME building, bounce detection |
| `mailer-security` | ✅ Complete (22 tests) | DKIM sign/verify, SPF, DMARC, IP reputation/DNSBL, content scanning | | `mailer-security` | ✅ Complete (22 tests) | DKIM sign/verify, SPF, DMARC, IP reputation/DNSBL, content scanning |
| `mailer-smtp` | ✅ Complete (77 tests) | Full SMTP protocol engine — TCP/TLS server, STARTTLS, AUTH, pipelining, in-process security pipeline, rate limiting | | `mailer-smtp` | ✅ Complete (106 tests) | Full SMTP protocol engine — TCP/TLS server + client, STARTTLS, AUTH, pipelining, connection pooling, in-process security pipeline |
| `mailer-bin` | ✅ Complete | CLI + smartrust IPC bridge — security, content scanning, SMTP server lifecycle | | `mailer-bin` | ✅ Complete | CLI + smartrust IPC bridge — wires everything together |
| `mailer-napi` | 🔜 Planned | Native Node.js addon (N-API) |
### What Runs in Rust ### What Runs Where
| Operation | Runs In | Why | | Operation | Runs In | Why |
|---|---|---| |---|---|---|
| SMTP server (port listening, protocol, TLS) | Rust | Performance, memory safety, zero-copy parsing | | SMTP server (port listening, protocol, TLS) | 🦀 Rust | Performance, memory safety, zero-copy parsing |
| DKIM signing & verification | Rust | Crypto-heavy, benefits from native speed | | SMTP client (outbound delivery, connection pooling) | 🦀 Rust | Connection management, TLS negotiation |
| SPF validation | Rust | DNS lookups with async resolver | | DKIM signing & verification | 🦀 Rust | Crypto-heavy, benefits from native speed |
| DMARC policy checking | Rust | Integrates with SPF/DKIM results | | SPF validation | 🦀 Rust | DNS lookups with async resolver |
| IP reputation / DNSBL | Rust | Parallel DNS queries | | DMARC policy checking | 🦀 Rust | Integrates with SPF/DKIM results |
| Content scanning (text patterns) | Rust | Regex engine performance | | IP reputation / DNSBL | 🦀 Rust | Parallel DNS queries |
| Bounce detection (pattern matching) | Rust | Regex engine performance | | Content scanning (text patterns) | 🦀 Rust | Regex engine performance |
| Email validation & MIME building | Rust | Parsing performance | | Bounce detection (pattern matching) | 🦀 Rust | Regex engine performance |
| Binary attachment scanning | TypeScript | Buffer data too large for IPC | | Email validation & MIME building | 🦀 Rust | Parsing performance |
| Email routing & orchestration | TypeScript | Business logic, flexibility | | Email routing & orchestration | 🟦 TypeScript | Business logic, flexibility |
| Delivery queue & retry | TypeScript | State management, persistence | | Delivery queue & retry | 🟦 TypeScript | State management, persistence |
| Template rendering | TypeScript | String interpolation | | Template rendering | 🟦 TypeScript | String interpolation |
| Domain & DNS management | 🟦 TypeScript | API integrations |
## Project Structure ## 📁 Project Structure
``` ```
smartmta/ smartmta/
├── ts/ # TypeScript source ├── ts/ # TypeScript source
│ ├── mail/ │ ├── mail/
│ │ ├── core/ # Email, EmailValidator, BounceManager, TemplateManager │ │ ├── core/ # Email, EmailValidator, BounceManager, TemplateManager
│ │ ├── delivery/ # DeliverySystem, Queue, RateLimiter │ │ ├── delivery/ # DeliveryQueue, DeliverySystem, RateLimiter
│ │ │ └── smtpclient/ # SMTP client with connection pooling
│ │ ├── routing/ # UnifiedEmailServer, EmailRouter, DomainRegistry, DnsManager │ │ ├── routing/ # UnifiedEmailServer, EmailRouter, DomainRegistry, DnsManager
│ │ └── security/ # DKIMCreator, DKIMVerifier, SpfVerifier, DmarcVerifier │ │ └── security/ # DKIMCreator, DKIMVerifier, SpfVerifier, DmarcVerifier
│ └── security/ # ContentScanner, IPReputationChecker, RustSecurityBridge │ └── security/ # ContentScanner, IPReputationChecker, RustSecurityBridge
@@ -606,14 +510,56 @@ smartmta/
│ └── crates/ │ └── crates/
│ ├── mailer-core/ # Email types, validation, MIME, bounce detection │ ├── mailer-core/ # Email types, validation, MIME, bounce detection
│ ├── mailer-security/ # DKIM, SPF, DMARC, IP reputation, content scanning │ ├── mailer-security/ # DKIM, SPF, DMARC, IP reputation, content scanning
│ ├── mailer-smtp/ # Full SMTP server (TCP/TLS, state machine, rate limiting) │ ├── mailer-smtp/ # Full SMTP server + client (TCP/TLS, rate limiting, pooling)
── mailer-bin/ # CLI + smartrust IPC bridge ── mailer-bin/ # CLI + smartrust IPC bridge
│ └── mailer-napi/ # N-API addon (planned) ├── test/ # Test suite (116 TypeScript + 154 Rust tests)
├── test/ # Test suite
├── dist_ts/ # Compiled TypeScript output ├── dist_ts/ # Compiled TypeScript output
└── dist_rust/ # Compiled Rust binaries └── dist_rust/ # Compiled Rust binaries
``` ```
## 🧪 Testing
The project has comprehensive test coverage with both unit and end-to-end tests:
```bash
# Build Rust binary first
pnpm build
# Run all tests
pnpm test
# Run specific test files
tstest test/test.e2e.server-lifecycle.node.ts --verbose --timeout 60
tstest test/test.e2e.inbound-smtp.node.ts --verbose --timeout 60
tstest test/test.e2e.routing-actions.node.ts --verbose --timeout 60
tstest test/test.e2e.outbound-delivery.node.ts --verbose --timeout 60
```
**E2E tests** exercise the full pipeline — starting `UnifiedEmailServer`, connecting via raw TCP sockets, sending SMTP transactions, verifying routing actions, and testing outbound delivery through a mock SMTP receiver.
## API Reference
### Exported Classes (top-level)
| Class | Description |
|---|---|
| `UnifiedEmailServer` | 🎯 Main entry point — orchestrates SMTP server, routing, security, and delivery |
| `Email` | Email message class with validation, attachments, headers, and RFC 822 serialization |
| `EmailRouter` | Pattern-based route matching and evaluation engine |
| `DomainRegistry` | Multi-domain configuration manager |
| `DnsManager` | Automatic DNS record management |
| `DKIMCreator` | DKIM key generation, storage, rotation |
| `DKIMVerifier` | DKIM signature verification (delegates to Rust) |
| `SpfVerifier` | SPF record validation (delegates to Rust) |
| `DmarcVerifier` | DMARC policy enforcement (delegates to Rust) |
### Namespaced Exports
| Namespace | Classes |
|---|---|
| `Core` | `Email`, `EmailValidator`, `TemplateManager`, `BounceManager` |
| `Delivery` | `UnifiedDeliveryQueue`, `MultiModeDeliverySystem`, `DeliveryStatus`, `UnifiedRateLimiter` |
## License and Legal Information ## License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file. This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.

View File

@@ -1,10 +1,8 @@
import { tap, expect } from '@git.zone/tstest/tapbundle'; import { tap, expect } from '@git.zone/tstest/tapbundle';
import { SpfVerifier, SpfQualifier, SpfMechanismType } from '../ts/mail/security/classes.spfverifier.js'; import { SpfVerifier, SpfQualifier, SpfMechanismType } from '../ts/mail/security/classes.spfverifier.js';
import { DmarcVerifier, DmarcPolicy, DmarcAlignment } from '../ts/mail/security/classes.dmarcverifier.js';
import { Email } from '../ts/mail/core/classes.email.js';
/** /**
* Test email authentication systems: SPF and DMARC * Test email authentication systems: SPF parsing
*/ */
// SPF Verifier Tests // SPF Verifier Tests
@@ -41,153 +39,6 @@ tap.test('SPF Verifier - should parse SPF record', async () => {
expect(invalidParsed).toBeNull(); expect(invalidParsed).toBeNull();
}); });
// DMARC Verifier Tests
tap.test('DMARC Verifier - should parse DMARC record', async () => {
const dmarcVerifier = new DmarcVerifier();
// Test valid DMARC record parsing
const record = 'v=DMARC1; p=reject; sp=quarantine; pct=50; adkim=s; aspf=r; rua=mailto:dmarc@example.com';
const parsedRecord = dmarcVerifier.parseDmarcRecord(record);
expect(parsedRecord).toBeTruthy();
expect(parsedRecord.version).toEqual('DMARC1');
expect(parsedRecord.policy).toEqual(DmarcPolicy.REJECT);
expect(parsedRecord.subdomainPolicy).toEqual(DmarcPolicy.QUARANTINE);
expect(parsedRecord.pct).toEqual(50);
expect(parsedRecord.adkim).toEqual(DmarcAlignment.STRICT);
expect(parsedRecord.aspf).toEqual(DmarcAlignment.RELAXED);
expect(parsedRecord.reportUriAggregate).toContain('dmarc@example.com');
// Test invalid record
const invalidRecord = 'not-a-dmarc-record';
const invalidParsed = dmarcVerifier.parseDmarcRecord(invalidRecord);
expect(invalidParsed).toBeNull();
});
tap.test('DMARC Verifier - should verify DMARC alignment', async () => {
const dmarcVerifier = new DmarcVerifier();
// Test email domains with DMARC alignment
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.net',
subject: 'Test DMARC alignment',
text: 'This is a test email'
});
// Test when both SPF and DKIM pass with alignment
const dmarcResult = await dmarcVerifier.verify(
email,
{ domain: 'example.com', result: true }, // SPF - aligned and passed
{ domain: 'example.com', result: true } // DKIM - aligned and passed
);
expect(dmarcResult).toBeTruthy();
expect(dmarcResult.spfPassed).toEqual(true);
expect(dmarcResult.dkimPassed).toEqual(true);
expect(dmarcResult.spfDomainAligned).toEqual(true);
expect(dmarcResult.dkimDomainAligned).toEqual(true);
expect(dmarcResult.action).toEqual('pass');
// Test when neither SPF nor DKIM is aligned
const dmarcResult2 = await dmarcVerifier.verify(
email,
{ domain: 'differentdomain.com', result: true }, // SPF - passed but not aligned
{ domain: 'anotherdomain.com', result: true } // DKIM - passed but not aligned
);
// Without a DNS manager, no DMARC record will be found
expect(dmarcResult2).toBeTruthy();
expect(dmarcResult2.spfPassed).toEqual(true);
expect(dmarcResult2.dkimPassed).toEqual(true);
expect(dmarcResult2.spfDomainAligned).toEqual(false);
expect(dmarcResult2.dkimDomainAligned).toEqual(false);
// Without a DMARC record, the default action is 'pass'
expect(dmarcResult2.hasDmarc).toEqual(false);
expect(dmarcResult2.policyEvaluated).toEqual(DmarcPolicy.NONE);
expect(dmarcResult2.actualPolicy).toEqual(DmarcPolicy.NONE);
expect(dmarcResult2.action).toEqual('pass');
});
tap.test('DMARC Verifier - should apply policy correctly', async () => {
const dmarcVerifier = new DmarcVerifier();
// Create test email
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.net',
subject: 'Test DMARC policy application',
text: 'This is a test email'
});
// Test pass action
const passResult: any = {
hasDmarc: true,
spfDomainAligned: true,
dkimDomainAligned: true,
spfPassed: true,
dkimPassed: true,
policyEvaluated: DmarcPolicy.NONE,
actualPolicy: DmarcPolicy.NONE,
appliedPercentage: 100,
action: 'pass',
details: 'DMARC passed'
};
const passApplied = dmarcVerifier.applyPolicy(email, passResult);
expect(passApplied).toEqual(true);
expect(email.mightBeSpam).toEqual(false);
expect(email.headers['X-DMARC-Result']).toEqual('DMARC passed');
// Test quarantine action
const quarantineResult: any = {
hasDmarc: true,
spfDomainAligned: false,
dkimDomainAligned: false,
spfPassed: false,
dkimPassed: false,
policyEvaluated: DmarcPolicy.QUARANTINE,
actualPolicy: DmarcPolicy.QUARANTINE,
appliedPercentage: 100,
action: 'quarantine',
details: 'DMARC failed, policy=quarantine'
};
// Reset email spam flag
email.mightBeSpam = false;
email.headers = {};
const quarantineApplied = dmarcVerifier.applyPolicy(email, quarantineResult);
expect(quarantineApplied).toEqual(true);
expect(email.mightBeSpam).toEqual(true);
expect(email.headers['X-Spam-Flag']).toEqual('YES');
expect(email.headers['X-DMARC-Result']).toEqual('DMARC failed, policy=quarantine');
// Test reject action
const rejectResult: any = {
hasDmarc: true,
spfDomainAligned: false,
dkimDomainAligned: false,
spfPassed: false,
dkimPassed: false,
policyEvaluated: DmarcPolicy.REJECT,
actualPolicy: DmarcPolicy.REJECT,
appliedPercentage: 100,
action: 'reject',
details: 'DMARC failed, policy=reject'
};
// Reset email spam flag
email.mightBeSpam = false;
email.headers = {};
const rejectApplied = dmarcVerifier.applyPolicy(email, rejectResult);
expect(rejectApplied).toEqual(false);
expect(email.mightBeSpam).toEqual(true);
});
tap.test('stop', async () => { tap.test('stop', async () => {
await tap.stopForcefully(); await tap.stopForcefully();
}); });

View File

@@ -1,141 +0,0 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { RateLimiter } from '../ts/mail/delivery/classes.ratelimiter.js';
tap.test('RateLimiter - should be instantiable', async () => {
const limiter = new RateLimiter({
maxPerPeriod: 10,
periodMs: 1000,
perKey: true
});
expect(limiter).toBeTruthy();
});
tap.test('RateLimiter - should allow requests within rate limit', async () => {
const limiter = new RateLimiter({
maxPerPeriod: 5,
periodMs: 1000,
perKey: true
});
// Should allow 5 requests
for (let i = 0; i < 5; i++) {
expect(limiter.isAllowed('test')).toEqual(true);
}
// 6th request should be denied
expect(limiter.isAllowed('test')).toEqual(false);
});
tap.test('RateLimiter - should enforce per-key limits', async () => {
const limiter = new RateLimiter({
maxPerPeriod: 3,
periodMs: 1000,
perKey: true
});
// Should allow 3 requests for key1
for (let i = 0; i < 3; i++) {
expect(limiter.isAllowed('key1')).toEqual(true);
}
// 4th request for key1 should be denied
expect(limiter.isAllowed('key1')).toEqual(false);
// But key2 should still be allowed
expect(limiter.isAllowed('key2')).toEqual(true);
});
tap.test('RateLimiter - should refill tokens over time', async () => {
const limiter = new RateLimiter({
maxPerPeriod: 2,
periodMs: 100, // Short period for testing
perKey: true
});
// Use all tokens
expect(limiter.isAllowed('test')).toEqual(true);
expect(limiter.isAllowed('test')).toEqual(true);
expect(limiter.isAllowed('test')).toEqual(false);
// Wait for refill
await new Promise(resolve => setTimeout(resolve, 150));
// Should have tokens again
expect(limiter.isAllowed('test')).toEqual(true);
});
tap.test('RateLimiter - should support burst allowance', async () => {
const limiter = new RateLimiter({
maxPerPeriod: 2,
periodMs: 100,
perKey: true,
burstTokens: 2, // Allow 2 extra tokens for bursts
initialTokens: 4 // Start with max + burst tokens
});
// Should allow 4 requests (2 regular + 2 burst)
for (let i = 0; i < 4; i++) {
expect(limiter.isAllowed('test')).toEqual(true);
}
// 5th request should be denied
expect(limiter.isAllowed('test')).toEqual(false);
// Wait for refill
await new Promise(resolve => setTimeout(resolve, 150));
// Should have 2 tokens again (rate-limited to normal max, not burst)
expect(limiter.isAllowed('test')).toEqual(true);
expect(limiter.isAllowed('test')).toEqual(true);
// 3rd request after refill should fail (only normal max is refilled, not burst)
expect(limiter.isAllowed('test')).toEqual(false);
});
tap.test('RateLimiter - should return correct stats', async () => {
const limiter = new RateLimiter({
maxPerPeriod: 10,
periodMs: 1000,
perKey: true
});
// Make some requests
limiter.isAllowed('test');
limiter.isAllowed('test');
limiter.isAllowed('test');
// Get stats
const stats = limiter.getStats('test');
expect(stats.remaining).toEqual(7);
expect(stats.limit).toEqual(10);
expect(stats.allowed).toEqual(3);
expect(stats.denied).toEqual(0);
});
tap.test('RateLimiter - should reset limits', async () => {
const limiter = new RateLimiter({
maxPerPeriod: 3,
periodMs: 1000,
perKey: true
});
// Use all tokens
expect(limiter.isAllowed('test')).toEqual(true);
expect(limiter.isAllowed('test')).toEqual(true);
expect(limiter.isAllowed('test')).toEqual(true);
expect(limiter.isAllowed('test')).toEqual(false);
// Reset
limiter.reset('test');
// Should have tokens again
expect(limiter.isAllowed('test')).toEqual(true);
});
tap.test('stop', async () => {
await tap.stopForcefully();
});
export default tap.start();

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartmta', name: '@push.rocks/smartmta',
version: '4.1.0', version: '5.0.0',
description: 'A high-performance, enterprise-grade Mail Transfer Agent (MTA) built from scratch in TypeScript with Rust acceleration.' description: 'A high-performance, enterprise-grade Mail Transfer Agent (MTA) built from scratch in TypeScript with Rust acceleration.'
} }

View File

@@ -1,119 +0,0 @@
/**
* MTA error classes for SMTP client operations
*/
export class MtaConnectionError extends Error {
public code: string;
public details?: any;
constructor(message: string, detailsOrCode?: any) {
super(message);
this.name = 'MtaConnectionError';
if (typeof detailsOrCode === 'string') {
this.code = detailsOrCode;
} else {
this.code = 'CONNECTION_ERROR';
this.details = detailsOrCode;
}
}
static timeout(host: string, port: number, timeoutMs?: number): MtaConnectionError {
return new MtaConnectionError(`Connection to ${host}:${port} timed out${timeoutMs ? ` after ${timeoutMs}ms` : ''}`, 'TIMEOUT');
}
static refused(host: string, port: number): MtaConnectionError {
return new MtaConnectionError(`Connection to ${host}:${port} refused`, 'REFUSED');
}
static dnsError(host: string, err?: any): MtaConnectionError {
const errMsg = typeof err === 'string' ? err : err?.message || '';
return new MtaConnectionError(`DNS resolution failed for ${host}${errMsg ? `: ${errMsg}` : ''}`, 'DNS_ERROR');
}
}
export class MtaAuthenticationError extends Error {
public code: string;
public details?: any;
constructor(message: string, detailsOrCode?: any) {
super(message);
this.name = 'MtaAuthenticationError';
if (typeof detailsOrCode === 'string') {
this.code = detailsOrCode;
} else {
this.code = 'AUTH_ERROR';
this.details = detailsOrCode;
}
}
static invalidCredentials(host?: string, user?: string): MtaAuthenticationError {
const detail = host && user ? `${user}@${host}` : host || user || '';
return new MtaAuthenticationError(`Authentication failed${detail ? `: ${detail}` : ''}`, 'INVALID_CREDENTIALS');
}
}
export class MtaDeliveryError extends Error {
public code: string;
public responseCode?: number;
public details?: any;
constructor(message: string, detailsOrCode?: any, responseCode?: number) {
super(message);
this.name = 'MtaDeliveryError';
if (typeof detailsOrCode === 'string') {
this.code = detailsOrCode;
this.responseCode = responseCode;
} else {
this.code = 'DELIVERY_ERROR';
this.details = detailsOrCode;
}
}
static temporary(message: string, ...args: any[]): MtaDeliveryError {
return new MtaDeliveryError(message, 'TEMPORARY');
}
static permanent(message: string, ...args: any[]): MtaDeliveryError {
return new MtaDeliveryError(message, 'PERMANENT');
}
}
export class MtaConfigurationError extends Error {
public code: string;
public details?: any;
constructor(message: string, detailsOrCode?: any) {
super(message);
this.name = 'MtaConfigurationError';
if (typeof detailsOrCode === 'string') {
this.code = detailsOrCode;
} else {
this.code = 'CONFIG_ERROR';
this.details = detailsOrCode;
}
}
}
export class MtaTimeoutError extends Error {
public code: string;
public details?: any;
constructor(message: string, detailsOrCode?: any) {
super(message);
this.name = 'MtaTimeoutError';
if (typeof detailsOrCode === 'string') {
this.code = detailsOrCode;
} else {
this.code = 'TIMEOUT';
this.details = detailsOrCode;
}
}
static commandTimeout(command: string, hostOrTimeout?: any, timeoutMs?: number): MtaTimeoutError {
const timeout = typeof hostOrTimeout === 'number' ? hostOrTimeout : timeoutMs;
return new MtaTimeoutError(`Command '${command}' timed out${timeout ? ` after ${timeout}ms` : ''}`, 'COMMAND_TIMEOUT');
}
}
export class MtaProtocolError extends Error {
public code: string;
public details?: any;
constructor(message: string, detailsOrCode?: any) {
super(message);
this.name = 'MtaProtocolError';
if (typeof detailsOrCode === 'string') {
this.code = detailsOrCode;
} else {
this.code = 'PROTOCOL_ERROR';
this.details = detailsOrCode;
}
}
}

View File

@@ -3,7 +3,7 @@ import { EventEmitter } from 'node:events';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import * as path from 'node:path'; import * as path from 'node:path';
import { logger } from '../../logger.js'; import { logger } from '../../logger.js';
import { type EmailProcessingMode } from '../routing/classes.email.config.js'; import { type EmailProcessingMode } from './interfaces.js';
import type { IEmailRoute } from '../routing/interfaces.js'; import type { IEmailRoute } from '../routing/interfaces.js';
/** /**

View File

@@ -0,0 +1,153 @@
import { logger } from '../../logger.js';
import { DKIMCreator } from '../security/classes.dkimcreator.js';
import { DomainRegistry } from './classes.domain.registry.js';
import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js';
import { Email } from '../core/classes.email.js';
/** External DcRouter interface shape used by DkimManager */
interface DcRouter {
storageManager: any;
dnsServer?: any;
}
/**
* Manages DKIM key setup, rotation, and signing for all configured domains
*/
export class DkimManager {
private dkimKeys: Map<string, string> = new Map();
constructor(
private dkimCreator: DKIMCreator,
private domainRegistry: DomainRegistry,
private dcRouter: DcRouter,
private rustBridge: RustSecurityBridge,
) {}
async setupDkimForDomains(): Promise<void> {
const domainConfigs = this.domainRegistry.getAllConfigs();
if (domainConfigs.length === 0) {
logger.log('warn', 'No domains configured for DKIM');
return;
}
for (const domainConfig of domainConfigs) {
const domain = domainConfig.domain;
const selector = domainConfig.dkim?.selector || 'default';
try {
let keyPair: { privateKey: string; publicKey: string };
try {
keyPair = await this.dkimCreator.readDKIMKeys(domain);
logger.log('info', `Using existing DKIM keys for domain: ${domain}`);
} catch (error) {
keyPair = await this.dkimCreator.createDKIMKeys();
await this.dkimCreator.createAndStoreDKIMKeys(domain);
logger.log('info', `Generated new DKIM keys for domain: ${domain}`);
}
this.dkimKeys.set(domain, keyPair.privateKey);
logger.log('info', `DKIM keys loaded for domain: ${domain} with selector: ${selector}`);
} catch (error) {
logger.log('error', `Failed to set up DKIM for domain ${domain}: ${error.message}`);
}
}
}
async checkAndRotateDkimKeys(): Promise<void> {
const domainConfigs = this.domainRegistry.getAllConfigs();
for (const domainConfig of domainConfigs) {
const domain = domainConfig.domain;
const selector = domainConfig.dkim?.selector || 'default';
const rotateKeys = domainConfig.dkim?.rotateKeys || false;
const rotationInterval = domainConfig.dkim?.rotationInterval || 90;
const keySize = domainConfig.dkim?.keySize || 2048;
if (!rotateKeys) {
logger.log('debug', `DKIM key rotation disabled for ${domain}`);
continue;
}
try {
const needsRotation = await this.dkimCreator.needsRotation(domain, selector, rotationInterval);
if (needsRotation) {
logger.log('info', `DKIM keys need rotation for ${domain} (selector: ${selector})`);
const newSelector = await this.dkimCreator.rotateDkimKeys(domain, selector, keySize);
domainConfig.dkim = {
...domainConfig.dkim,
selector: newSelector
};
if (domainConfig.dnsMode === 'internal-dns' && this.dcRouter.dnsServer) {
const keyPair = await this.dkimCreator.readDKIMKeysForSelector(domain, newSelector);
const publicKeyBase64 = keyPair.publicKey
.replace(/-----BEGIN PUBLIC KEY-----/g, '')
.replace(/-----END PUBLIC KEY-----/g, '')
.replace(/\s/g, '');
const ttl = domainConfig.dns?.internal?.ttl || 3600;
this.dcRouter.dnsServer.registerHandler(
`${newSelector}._domainkey.${domain}`,
['TXT'],
() => ({
name: `${newSelector}._domainkey.${domain}`,
type: 'TXT',
class: 'IN',
ttl: ttl,
data: `v=DKIM1; k=rsa; p=${publicKeyBase64}`
})
);
logger.log('info', `DKIM DNS handler registered for new selector: ${newSelector}._domainkey.${domain}`);
await this.dcRouter.storageManager.set(
`/email/dkim/${domain}/public.key`,
keyPair.publicKey
);
}
this.dkimCreator.cleanupOldKeys(domain, 30).catch(error => {
logger.log('warn', `Failed to cleanup old DKIM keys for ${domain}: ${error.message}`);
});
} else {
logger.log('debug', `DKIM keys for ${domain} are up to date`);
}
} catch (error) {
logger.log('error', `Failed to check/rotate DKIM keys for ${domain}: ${error.message}`);
}
}
}
async handleDkimSigning(email: Email, domain: string, selector: string): Promise<void> {
try {
await this.dkimCreator.handleDKIMKeysForDomain(domain);
const { privateKey } = await this.dkimCreator.readDKIMKeys(domain);
const rawEmail = email.toRFC822String();
const signResult = await this.rustBridge.signDkim({
rawMessage: rawEmail,
domain,
selector,
privateKey,
});
if (signResult.header) {
email.addHeader('DKIM-Signature', signResult.header);
logger.log('info', `Successfully added DKIM signature for ${domain}`);
}
} catch (error) {
logger.log('error', `Failed to sign email with DKIM: ${error.message}`);
}
}
getDkimKey(domain: string): string | undefined {
return this.dkimKeys.get(domain);
}
}

View File

@@ -0,0 +1,174 @@
import { logger } from '../../logger.js';
import {
SecurityLogger,
SecurityLogLevel,
SecurityEventType
} from '../../security/index.js';
import type { IEmailAction, IEmailContext } from './interfaces.js';
import { Email } from '../core/classes.email.js';
import { BounceManager } from '../core/classes.bouncemanager.js';
import { UnifiedDeliveryQueue } from '../delivery/classes.delivery.queue.js';
import type { ISmtpSendResult } from '../../security/classes.rustsecuritybridge.js';
/**
* Dependencies injected from UnifiedEmailServer to avoid circular imports
*/
export interface IActionExecutorDeps {
sendOutboundEmail: (host: string, port: number, email: Email, options?: {
auth?: { user: string; pass: string };
dkimDomain?: string;
dkimSelector?: string;
}) => Promise<ISmtpSendResult>;
bounceManager: BounceManager;
deliveryQueue: UnifiedDeliveryQueue;
}
/**
* Executes email routing actions (forward, process, deliver, reject)
*/
export class EmailActionExecutor {
constructor(private deps: IActionExecutorDeps) {}
async executeAction(action: IEmailAction, email: Email, context: IEmailContext): Promise<void> {
switch (action.type) {
case 'forward':
await this.handleForwardAction(action, email, context);
break;
case 'process':
await this.handleProcessAction(action, email, context);
break;
case 'deliver':
await this.handleDeliverAction(action, email, context);
break;
case 'reject':
await this.handleRejectAction(action, email, context);
break;
default:
throw new Error(`Unknown action type: ${(action as any).type}`);
}
}
private async handleForwardAction(action: IEmailAction, email: Email, context: IEmailContext): Promise<void> {
if (!action.forward) {
throw new Error('Forward action requires forward configuration');
}
const { host, port = 25, auth, addHeaders } = action.forward;
logger.log('info', `Forwarding email to ${host}:${port}`);
// Add forwarding headers
if (addHeaders) {
for (const [key, value] of Object.entries(addHeaders)) {
email.headers[key] = value;
}
}
// Add standard forwarding headers
email.headers['X-Forwarded-For'] = context.session.remoteAddress || 'unknown';
email.headers['X-Forwarded-To'] = email.to.join(', ');
email.headers['X-Forwarded-Date'] = new Date().toISOString();
try {
// Send email via Rust SMTP client
await this.deps.sendOutboundEmail(host, port, email, {
auth: auth as { user: string; pass: string } | undefined,
});
logger.log('info', `Successfully forwarded email to ${host}:${port}`);
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.INFO,
type: SecurityEventType.EMAIL_FORWARDING,
message: 'Email forwarded successfully',
ipAddress: context.session.remoteAddress,
details: {
sessionId: context.session.id,
routeName: context.session.matchedRoute?.name,
targetHost: host,
targetPort: port,
recipients: email.to
},
success: true
});
} catch (error) {
logger.log('error', `Failed to forward email: ${error.message}`);
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.ERROR,
type: SecurityEventType.EMAIL_FORWARDING,
message: 'Email forwarding failed',
ipAddress: context.session.remoteAddress,
details: {
sessionId: context.session.id,
routeName: context.session.matchedRoute?.name,
targetHost: host,
targetPort: port,
error: error.message
},
success: false
});
// Handle as bounce
for (const recipient of email.getAllRecipients()) {
await this.deps.bounceManager.processSmtpFailure(recipient, error.message, {
sender: email.from,
originalEmailId: email.headers['Message-ID'] as string
});
}
throw error;
}
}
private async handleProcessAction(action: IEmailAction, email: Email, context: IEmailContext): Promise<void> {
logger.log('info', `Processing email with action options`);
// Apply scanning if requested
if (action.process?.scan) {
logger.log('info', 'Content scanning requested');
}
// Queue for delivery
const queue = action.process?.queue || 'normal';
await this.deps.deliveryQueue.enqueue(email, 'process', context.session.matchedRoute!);
logger.log('info', `Email queued for delivery in ${queue} queue`);
}
private async handleDeliverAction(_action: IEmailAction, email: Email, context: IEmailContext): Promise<void> {
logger.log('info', `Delivering email locally`);
// Queue for local delivery
await this.deps.deliveryQueue.enqueue(email, 'mta', context.session.matchedRoute!);
logger.log('info', 'Email queued for local delivery');
}
private async handleRejectAction(action: IEmailAction, _email: Email, context: IEmailContext): Promise<void> {
const code = action.reject?.code || 550;
const message = action.reject?.message || 'Message rejected';
logger.log('info', `Rejecting email with code ${code}: ${message}`);
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.WARN,
type: SecurityEventType.EMAIL_PROCESSING,
message: 'Email rejected by routing rule',
ipAddress: context.session.remoteAddress,
details: {
sessionId: context.session.id,
routeName: context.session.matchedRoute?.name,
rejectCode: code,
rejectMessage: message,
from: _email.from,
to: _email.to
},
success: false
});
// Throw error with SMTP code and message
const error = new Error(message);
(error as any).responseCode = code;
throw error;
}
}

View File

@@ -1,82 +0,0 @@
import type { EmailProcessingMode } from '../delivery/interfaces.js';
// Re-export EmailProcessingMode type
export type { EmailProcessingMode };
/**
* Domain rule interface for pattern-based routing
*/
export interface IDomainRule {
// Domain pattern (e.g., "*@example.com", "*@*.example.net")
pattern: string;
// Handling mode for this pattern
mode: EmailProcessingMode;
// Forward mode configuration
target?: {
server: string;
port?: number;
useTls?: boolean;
authentication?: {
user?: string;
pass?: string;
};
};
// MTA mode configuration
mtaOptions?: IMtaOptions;
// Process mode configuration
contentScanning?: boolean;
scanners?: IContentScanner[];
transformations?: ITransformation[];
// Rate limits for this domain
rateLimits?: {
maxMessagesPerMinute?: number;
maxRecipientsPerMessage?: number;
};
}
/**
* MTA options interface
*/
export interface IMtaOptions {
domain?: string;
allowLocalDelivery?: boolean;
localDeliveryPath?: string;
dkimSign?: boolean;
dkimOptions?: {
domainName: string;
keySelector: string;
privateKey?: string;
};
smtpBanner?: string;
maxConnections?: number;
connTimeout?: number;
spoolDir?: string;
}
/**
* Content scanner interface
*/
export interface IContentScanner {
type: 'spam' | 'virus' | 'attachment';
threshold?: number;
action: 'tag' | 'reject';
blockedExtensions?: string[];
}
/**
* Transformation interface
*/
export interface ITransformation {
type: string;
header?: string;
value?: string;
domains?: string[];
append?: boolean;
[key: string]: any;
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,3 +4,6 @@ export * from './classes.unified.email.server.js';
export * from './classes.dns.manager.js'; export * from './classes.dns.manager.js';
export * from './interfaces.js'; export * from './interfaces.js';
export * from './classes.domain.registry.js'; export * from './classes.domain.registry.js';
export * from './classes.email.action.executor.js';
export * from './classes.dkim.manager.js';

View File

@@ -1,86 +0,0 @@
import { logger } from '../../logger.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js';
import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js';
/**
* Result of a DKIM verification
*/
export interface IDkimVerificationResult {
isValid: boolean;
domain?: string;
selector?: string;
status?: string;
details?: any;
errorMessage?: string;
signatureFields?: Record<string, string>;
}
/**
* DKIM verifier — delegates to the Rust security bridge.
*/
export class DKIMVerifier {
constructor() {}
/**
* Verify DKIM signature for an email via Rust bridge
*/
public async verify(
emailData: string,
options: {
useCache?: boolean;
returnDetails?: boolean;
} = {}
): Promise<IDkimVerificationResult> {
try {
const bridge = RustSecurityBridge.getInstance();
const results = await bridge.verifyDkim(emailData);
const first = results[0];
const result: IDkimVerificationResult = {
isValid: first?.is_valid ?? false,
domain: first?.domain ?? undefined,
selector: first?.selector ?? undefined,
status: first?.status ?? 'none',
details: options.returnDetails ? results : undefined,
};
SecurityLogger.getInstance().logEvent({
level: result.isValid ? SecurityLogLevel.INFO : SecurityLogLevel.WARN,
type: SecurityEventType.DKIM,
message: `DKIM verification ${result.isValid ? 'passed' : 'failed'} for domain ${result.domain || 'unknown'}`,
details: { selector: result.selector, status: result.status },
domain: result.domain || 'unknown',
success: result.isValid
});
logger.log(result.isValid ? 'info' : 'warn',
`DKIM verification: ${result.status} for domain ${result.domain || 'unknown'}`);
return result;
} catch (error) {
logger.log('error', `DKIM verification failed: ${error.message}`);
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.ERROR,
type: SecurityEventType.DKIM,
message: `DKIM verification error`,
details: { error: error.message },
success: false
});
return {
isValid: false,
status: 'temperror',
errorMessage: `Verification error: ${error.message}`
};
}
}
/** No-op — Rust bridge handles its own caching */
public clearCache(): void {}
/** Always 0 — cache is managed by the Rust side */
public getCacheSize(): number {
return 0;
}
}

View File

@@ -1,475 +0,0 @@
import { logger } from '../../logger.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js';
import type { Email } from '../core/classes.email.js';
/**
* DMARC policy types
*/
export enum DmarcPolicy {
NONE = 'none',
QUARANTINE = 'quarantine',
REJECT = 'reject'
}
/**
* DMARC alignment modes
*/
export enum DmarcAlignment {
RELAXED = 'r',
STRICT = 's'
}
/**
* DMARC record fields
*/
export interface DmarcRecord {
// Required fields
version: string;
policy: DmarcPolicy;
// Optional fields
subdomainPolicy?: DmarcPolicy;
pct?: number;
adkim?: DmarcAlignment;
aspf?: DmarcAlignment;
reportInterval?: number;
failureOptions?: string;
reportUriAggregate?: string[];
reportUriForensic?: string[];
}
/**
* DMARC verification result
*/
export interface DmarcResult {
hasDmarc: boolean;
record?: DmarcRecord;
spfDomainAligned: boolean;
dkimDomainAligned: boolean;
spfPassed: boolean;
dkimPassed: boolean;
policyEvaluated: DmarcPolicy;
actualPolicy: DmarcPolicy;
appliedPercentage: number;
action: 'pass' | 'quarantine' | 'reject';
details: string;
error?: string;
}
/**
* Class for verifying and enforcing DMARC policies
*/
export class DmarcVerifier {
// DNS Manager reference for verifying records
private dnsManager?: any;
constructor(dnsManager?: any) {
this.dnsManager = dnsManager;
}
/**
* Parse a DMARC record from a TXT record string
* @param record DMARC TXT record string
* @returns Parsed DMARC record or null if invalid
*/
public parseDmarcRecord(record: string): DmarcRecord | null {
if (!record.startsWith('v=DMARC1')) {
return null;
}
try {
// Initialize record with default values
const dmarcRecord: DmarcRecord = {
version: 'DMARC1',
policy: DmarcPolicy.NONE,
pct: 100,
adkim: DmarcAlignment.RELAXED,
aspf: DmarcAlignment.RELAXED
};
// Split the record into tag/value pairs
const parts = record.split(';').map(part => part.trim());
for (const part of parts) {
if (!part || !part.includes('=')) continue;
const [tag, value] = part.split('=').map(p => p.trim());
// Process based on tag
switch (tag.toLowerCase()) {
case 'v':
dmarcRecord.version = value;
break;
case 'p':
dmarcRecord.policy = value as DmarcPolicy;
break;
case 'sp':
dmarcRecord.subdomainPolicy = value as DmarcPolicy;
break;
case 'pct':
const pctValue = parseInt(value, 10);
if (!isNaN(pctValue) && pctValue >= 0 && pctValue <= 100) {
dmarcRecord.pct = pctValue;
}
break;
case 'adkim':
dmarcRecord.adkim = value as DmarcAlignment;
break;
case 'aspf':
dmarcRecord.aspf = value as DmarcAlignment;
break;
case 'ri':
const interval = parseInt(value, 10);
if (!isNaN(interval) && interval > 0) {
dmarcRecord.reportInterval = interval;
}
break;
case 'fo':
dmarcRecord.failureOptions = value;
break;
case 'rua':
dmarcRecord.reportUriAggregate = value.split(',').map(uri => {
if (uri.startsWith('mailto:')) {
return uri.substring(7).trim();
}
return uri.trim();
});
break;
case 'ruf':
dmarcRecord.reportUriForensic = value.split(',').map(uri => {
if (uri.startsWith('mailto:')) {
return uri.substring(7).trim();
}
return uri.trim();
});
break;
}
}
// Ensure subdomain policy is set if not explicitly provided
if (!dmarcRecord.subdomainPolicy) {
dmarcRecord.subdomainPolicy = dmarcRecord.policy;
}
return dmarcRecord;
} catch (error) {
logger.log('error', `Error parsing DMARC record: ${error.message}`, {
record,
error: error.message
});
return null;
}
}
/**
* Check if domains are aligned according to DMARC policy
* @param headerDomain Domain from header (From)
* @param authDomain Domain from authentication (SPF, DKIM)
* @param alignment Alignment mode
* @returns Whether the domains are aligned
*/
private isDomainAligned(
headerDomain: string,
authDomain: string,
alignment: DmarcAlignment
): boolean {
if (!headerDomain || !authDomain) {
return false;
}
// For strict alignment, domains must match exactly
if (alignment === DmarcAlignment.STRICT) {
return headerDomain.toLowerCase() === authDomain.toLowerCase();
}
// For relaxed alignment, the authenticated domain must be a subdomain of the header domain
// or the same as the header domain
const headerParts = headerDomain.toLowerCase().split('.');
const authParts = authDomain.toLowerCase().split('.');
// Ensures we have at least two parts (domain and TLD)
if (headerParts.length < 2 || authParts.length < 2) {
return false;
}
// Get organizational domain (last two parts)
const headerOrgDomain = headerParts.slice(-2).join('.');
const authOrgDomain = authParts.slice(-2).join('.');
return headerOrgDomain === authOrgDomain;
}
/**
* Extract domain from an email address
* @param email Email address
* @returns Domain part of the email
*/
private getDomainFromEmail(email: string): string {
if (!email) return '';
// Handle name + email format: "John Doe <john@example.com>"
const matches = email.match(/<([^>]+)>/);
const address = matches ? matches[1] : email;
const parts = address.split('@');
return parts.length > 1 ? parts[1] : '';
}
/**
* Check if DMARC verification should be applied based on percentage
* @param record DMARC record
* @returns Whether DMARC verification should be applied
*/
private shouldApplyDmarc(record: DmarcRecord): boolean {
if (record.pct === undefined || record.pct === 100) {
return true;
}
// Apply DMARC randomly based on percentage
const random = Math.floor(Math.random() * 100) + 1;
return random <= record.pct;
}
/**
* Determine the action to take based on DMARC policy
* @param policy DMARC policy
* @returns Action to take
*/
private determineAction(policy: DmarcPolicy): 'pass' | 'quarantine' | 'reject' {
switch (policy) {
case DmarcPolicy.REJECT:
return 'reject';
case DmarcPolicy.QUARANTINE:
return 'quarantine';
case DmarcPolicy.NONE:
default:
return 'pass';
}
}
/**
* Verify DMARC for an incoming email
* @param email Email to verify
* @param spfResult SPF verification result
* @param dkimResult DKIM verification result
* @returns DMARC verification result
*/
public async verify(
email: Email,
spfResult: { domain: string; result: boolean },
dkimResult: { domain: string; result: boolean }
): Promise<DmarcResult> {
const securityLogger = SecurityLogger.getInstance();
// Initialize result
const result: DmarcResult = {
hasDmarc: false,
spfDomainAligned: false,
dkimDomainAligned: false,
spfPassed: spfResult.result,
dkimPassed: dkimResult.result,
policyEvaluated: DmarcPolicy.NONE,
actualPolicy: DmarcPolicy.NONE,
appliedPercentage: 100,
action: 'pass',
details: 'DMARC not configured'
};
try {
// Extract From domain
const fromHeader = email.getFromEmail();
const fromDomain = this.getDomainFromEmail(fromHeader);
if (!fromDomain) {
result.error = 'Invalid From domain';
return result;
}
// Check alignment
result.spfDomainAligned = this.isDomainAligned(
fromDomain,
spfResult.domain,
DmarcAlignment.RELAXED
);
result.dkimDomainAligned = this.isDomainAligned(
fromDomain,
dkimResult.domain,
DmarcAlignment.RELAXED
);
// Lookup DMARC record
const dmarcVerificationResult = this.dnsManager ?
await this.dnsManager.verifyDmarcRecord(fromDomain) :
{ found: false, valid: false, error: 'DNS Manager not available' };
// If DMARC record exists and is valid
if (dmarcVerificationResult.found && dmarcVerificationResult.valid) {
result.hasDmarc = true;
// Parse DMARC record
const parsedRecord = this.parseDmarcRecord(dmarcVerificationResult.value);
if (parsedRecord) {
result.record = parsedRecord;
result.actualPolicy = parsedRecord.policy;
result.appliedPercentage = parsedRecord.pct || 100;
// Override alignment modes if specified in record
if (parsedRecord.adkim) {
result.dkimDomainAligned = this.isDomainAligned(
fromDomain,
dkimResult.domain,
parsedRecord.adkim
);
}
if (parsedRecord.aspf) {
result.spfDomainAligned = this.isDomainAligned(
fromDomain,
spfResult.domain,
parsedRecord.aspf
);
}
// Determine DMARC compliance
const spfAligned = result.spfPassed && result.spfDomainAligned;
const dkimAligned = result.dkimPassed && result.dkimDomainAligned;
// Email passes DMARC if either SPF or DKIM passes with alignment
const dmarcPass = spfAligned || dkimAligned;
// Use record percentage to determine if policy should be applied
const applyPolicy = this.shouldApplyDmarc(parsedRecord);
if (!dmarcPass) {
// DMARC failed, apply policy
result.policyEvaluated = applyPolicy ? parsedRecord.policy : DmarcPolicy.NONE;
result.action = this.determineAction(result.policyEvaluated);
result.details = `DMARC failed: SPF aligned=${spfAligned}, DKIM aligned=${dkimAligned}, policy=${result.policyEvaluated}`;
} else {
result.policyEvaluated = DmarcPolicy.NONE;
result.action = 'pass';
result.details = `DMARC passed: SPF aligned=${spfAligned}, DKIM aligned=${dkimAligned}`;
}
} else {
result.error = 'Invalid DMARC record format';
result.details = 'DMARC record invalid';
}
} else {
// No DMARC record found or invalid
result.details = dmarcVerificationResult.error || 'No DMARC record found';
}
// Log the DMARC verification
securityLogger.logEvent({
level: result.action === 'pass' ? SecurityLogLevel.INFO : SecurityLogLevel.WARN,
type: SecurityEventType.DMARC,
message: result.details,
domain: fromDomain,
details: {
fromDomain,
spfDomain: spfResult.domain,
dkimDomain: dkimResult.domain,
spfPassed: result.spfPassed,
dkimPassed: result.dkimPassed,
spfAligned: result.spfDomainAligned,
dkimAligned: result.dkimDomainAligned,
dmarcPolicy: result.policyEvaluated,
action: result.action
},
success: result.action === 'pass'
});
return result;
} catch (error) {
logger.log('error', `Error verifying DMARC: ${error.message}`, {
error: error.message,
emailId: email.getMessageId()
});
result.error = `DMARC verification error: ${error.message}`;
// Log error
securityLogger.logEvent({
level: SecurityLogLevel.ERROR,
type: SecurityEventType.DMARC,
message: `DMARC verification failed with error`,
details: {
error: error.message,
emailId: email.getMessageId()
},
success: false
});
return result;
}
}
/**
* Apply DMARC policy to an email
* @param email Email to apply policy to
* @param dmarcResult DMARC verification result
* @returns Whether the email should be accepted
*/
public applyPolicy(email: Email, dmarcResult: DmarcResult): boolean {
// Apply action based on DMARC verification result
switch (dmarcResult.action) {
case 'reject':
// Reject the email
email.mightBeSpam = true;
logger.log('warn', `Email rejected due to DMARC policy: ${dmarcResult.details}`, {
emailId: email.getMessageId(),
from: email.getFromEmail(),
subject: email.subject
});
return false;
case 'quarantine':
// Quarantine the email (mark as spam)
email.mightBeSpam = true;
// Add spam header
if (!email.headers['X-Spam-Flag']) {
email.headers['X-Spam-Flag'] = 'YES';
}
// Add DMARC reason header
email.headers['X-DMARC-Result'] = dmarcResult.details;
logger.log('warn', `Email quarantined due to DMARC policy: ${dmarcResult.details}`, {
emailId: email.getMessageId(),
from: email.getFromEmail(),
subject: email.subject
});
return true;
case 'pass':
default:
// Accept the email
// Add DMARC result header for information
email.headers['X-DMARC-Result'] = dmarcResult.details;
return true;
}
}
/**
* End-to-end DMARC verification and policy application
* This method should be called after SPF and DKIM verification
* @param email Email to verify
* @param spfResult SPF verification result
* @param dkimResult DKIM verification result
* @returns Whether the email should be accepted
*/
public async verifyAndApply(
email: Email,
spfResult: { domain: string; result: boolean },
dkimResult: { domain: string; result: boolean }
): Promise<boolean> {
// Verify DMARC
const dmarcResult = await this.verify(email, spfResult, dkimResult);
// Apply DMARC policy
return this.applyPolicy(email, dmarcResult);
}
}

View File

@@ -1,8 +1,4 @@
import * as plugins from '../../plugins.js';
import { logger } from '../../logger.js'; import { logger } from '../../logger.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js';
import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js';
import type { Email } from '../core/classes.email.js';
/** /**
* SPF result qualifiers * SPF result qualifiers
@@ -127,107 +123,4 @@ export class SpfVerifier {
return null; return null;
} }
} }
/**
* Verify SPF for a given email — delegates to Rust bridge
*/
public async verify(
email: Email,
ip: string,
heloDomain: string
): Promise<SpfResult> {
const securityLogger = SecurityLogger.getInstance();
const mailFrom = email.from || '';
const domain = mailFrom.split('@')[1] || '';
try {
const bridge = RustSecurityBridge.getInstance();
const result = await bridge.checkSpf({
ip,
heloDomain,
hostname: plugins.os.hostname(),
mailFrom,
});
const spfResult: SpfResult = {
result: result.result as SpfResult['result'],
domain: result.domain,
ip: result.ip,
explanation: result.explanation ?? undefined,
};
securityLogger.logEvent({
level: spfResult.result === 'pass' ? SecurityLogLevel.INFO :
(spfResult.result === 'fail' ? SecurityLogLevel.WARN : SecurityLogLevel.INFO),
type: SecurityEventType.SPF,
message: `SPF ${spfResult.result} for ${spfResult.domain} from IP ${ip}`,
domain: spfResult.domain,
details: { ip, heloDomain, result: spfResult.result, explanation: spfResult.explanation },
success: spfResult.result === 'pass'
});
return spfResult;
} catch (error) {
logger.log('error', `SPF verification error: ${error.message}`, { domain, ip, error: error.message });
securityLogger.logEvent({
level: SecurityLogLevel.ERROR,
type: SecurityEventType.SPF,
message: `SPF verification error for ${domain}`,
domain,
details: { ip, error: error.message },
success: false
});
return {
result: 'temperror',
explanation: `Error verifying SPF: ${error.message}`,
domain,
ip,
error: error.message
};
}
}
/**
* Check if email passes SPF verification and apply headers
*/
public async verifyAndApply(
email: Email,
ip: string,
heloDomain: string
): Promise<boolean> {
const result = await this.verify(email, ip, heloDomain);
email.headers['Received-SPF'] = `${result.result} (${result.domain}: ${result.explanation || ''}) client-ip=${ip}; envelope-from=${email.getEnvelopeFrom()}; helo=${heloDomain};`;
switch (result.result) {
case 'fail':
email.mightBeSpam = true;
logger.log('warn', `SPF failed for ${result.domain} from ${ip}: ${result.explanation}`);
return false;
case 'softfail':
email.mightBeSpam = true;
logger.log('info', `SPF softfailed for ${result.domain} from ${ip}: ${result.explanation}`);
return true;
case 'neutral':
case 'none':
logger.log('info', `SPF ${result.result} for ${result.domain} from ${ip}: ${result.explanation}`);
return true;
case 'pass':
logger.log('info', `SPF passed for ${result.domain} from ${ip}: ${result.explanation}`);
return true;
case 'temperror':
case 'permerror':
logger.log('error', `SPF error for ${result.domain} from ${ip}: ${result.explanation}`);
return true;
default:
return true;
}
}
} }

View File

@@ -1,5 +1,3 @@
// Email security components // Email security components
export * from './classes.dkimcreator.js'; export * from './classes.dkimcreator.js';
export * from './classes.dkimverifier.js';
export * from './classes.dmarcverifier.js';
export * from './classes.spfverifier.js'; export * from './classes.spfverifier.js';