Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cf8fcb6efa | |||
| 2088c9f76e | |||
| 7853ef67b6 | |||
| f7af8c4534 | |||
| a7ea1d86cb | |||
| 27bab5f345 | |||
| fc4877e06b | |||
| 36006191fc | |||
| d43fc15d8e | |||
| 248bfcfe78 | |||
| 1e7c9f6822 | |||
| f3a74a7660 |
51
changelog.md
51
changelog.md
@@ -1,5 +1,56 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
add Node.js end-to-end tests covering server lifecycle, inbound SMTP handling, outbound delivery and routing actions
|
||||||
|
|
||||||
|
- Adds four end-to-end test files: test.e2e.server-lifecycle.node.ts, test.e2e.inbound-smtp.node.ts, test.e2e.outbound-delivery.node.ts, test.e2e.routing-actions.node.ts
|
||||||
|
- Tests exercise UnifiedEmailServer start/stop, SMTP handshake and transactions, outbound delivery via a mock SMTP server, routing actions (process, deliver, reject, forward), concurrency, and RSET handling mid-session
|
||||||
|
- Introduces a minimal mock SMTP server to avoid IPC deadlock with the Rust SMTP client during outbound delivery tests
|
||||||
|
- Tests will skip when the Rust bridge or server cannot start (binary build required)
|
||||||
|
|
||||||
|
## 2026-02-11 - 4.0.0 - BREAKING CHANGE(smtp-client)
|
||||||
|
Replace the legacy TypeScript SMTP client with a new Rust-based SMTP client and IPC bridge for outbound delivery
|
||||||
|
|
||||||
|
- Introduce a Rust SMTP client crate with connection handling, TLS, protocol engine, and connection pooling (new modules: connection, pool, protocol, error, config).
|
||||||
|
- Add IPC handlers and management commands in the Rust binary: sendEmail, sendRawEmail, verifySmtpConnection, closeSmtpPool, getSmtpPoolStatus and integrate a SmtpClientManager into the runtime.
|
||||||
|
- Update TypeScript bridge (RustSecurityBridge) with new types and methods (ISmtpSendOptions, ISmtpSendResult, verifySmtpConnection, sendOutboundEmail, sendRawEmail, getSmtpPoolStatus, closeSmtpPool) and rework UnifiedEmailServer to use the Rust bridge for outbound delivery and DKIM signing.
|
||||||
|
- Remove the previous TypeScript SMTP client implementation and associated tests/utilities (many ts/mail/delivery/smtpclient modules and tests deleted) in favor of the Rust implementation.
|
||||||
|
- Bump dependencies and cargo config: @push.rocks/smartrust to ^1.2.0 in package.json and add/require crates (uuid, base64, webpki-roots) in Rust Cargo files.
|
||||||
|
|
||||||
|
## 2026-02-10 - 3.0.0 - BREAKING CHANGE(security)
|
||||||
|
implement resilience and lifecycle management for RustSecurityBridge (auto-restart, health checks, state machine and eventing); remove legacy TS SMTP test helper and DNSManager; remove deliverability IP-warmup/sender-reputation integrations and related types; drop unused dependencies
|
||||||
|
|
||||||
|
- RustSecurityBridge now extends EventEmitter and includes a BridgeState state machine, IBridgeResilienceConfig with DEFAULT_RESILIENCE_CONFIG, auto-restart with exponential backoff, periodic health checks, restart/restore logic, and descriptive ensureRunning() guards on command methods.
|
||||||
|
- Added static methods: resetInstance() (test-friendly) and configure(...) to tweak resilience settings at runtime.
|
||||||
|
- Added stateChange events and logging for lifecycle transitions; new tests added for resilience: test/test.rustsecuritybridge.resilience.node.ts.
|
||||||
|
- Removed the TypeScript SMTP test helper (test/helpers/server.loader.ts), the DNSManager (ts/mail/routing/classes.dnsmanager.ts), and many deliverability-related interfaces/implementations (IP warmup manager and sender reputation monitor) from unified email server.
|
||||||
|
- Removed public types ISmtpServerOptions and ISmtpTransactionResult from ts/mail/delivery/interfaces.ts, which is a breaking API change for consumers relying on those types.
|
||||||
|
- Removed unused dependencies from package.json: ip and mailauth.
|
||||||
|
|
||||||
|
## 2026-02-10 - 2.4.0 - feat(docs)
|
||||||
|
document Rust-side in-process security pipeline and update README to reflect SMTP server behavior and crate/test counts
|
||||||
|
|
||||||
|
- Clarifies that the Rust SMTP server accepts the full SMTP protocol and runs the security pipeline in-process (DKIM/SPF/DMARC verification, content scanning, IP reputation/DNSBL) to avoid IPC round-trips
|
||||||
|
- Notes that Rust now emits an emailReceived IPC event with pre-computed security results attached for TypeScript to use in routing/delivery decisions
|
||||||
|
- Updates mailer-smtp crate description to include the in-process security pipeline and increments its test count from 72 to 77
|
||||||
|
- Adjusts TypeScript directory comments to reflect removal/relocation of the legacy TS SMTP server and the smtpclient path
|
||||||
|
|
||||||
|
## 2026-02-10 - 2.3.2 - fix(tests)
|
||||||
|
remove large SMTP client test suites and update SmartFile API usage
|
||||||
|
|
||||||
|
- Deleted ~80 test files under test/suite/ (multiple smtpclient command, connection, edge-cases, email-composition, error-handling and performance test suites)
|
||||||
|
- Updated SmartFile usage in test/test.smartmail.ts: replaced plugins.smartfile.SmartFile.fromString(...) with plugins.smartfile.SmartFileFactory.nodeFs().fromString(...)
|
||||||
|
- Removes a large set of tests to reduce test surface / simplify test runtime
|
||||||
|
|
||||||
## 2026-02-10 - 2.3.1 - fix(npmextra)
|
## 2026-02-10 - 2.3.1 - fix(npmextra)
|
||||||
update .gitignore and npmextra.json to add ignore patterns, registries, and module metadata
|
update .gitignore and npmextra.json to add ignore patterns, registries, and module metadata
|
||||||
|
|
||||||
|
|||||||
26
package.json
26
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartmta",
|
"name": "@push.rocks/smartmta",
|
||||||
"version": "2.3.1",
|
"version": "4.1.1",
|
||||||
"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",
|
||||||
@@ -44,36 +44,14 @@
|
|||||||
"tsx": "^4.21.0"
|
"tsx": "^4.21.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@api.global/typedrequest": "^3.2.5",
|
|
||||||
"@api.global/typedserver": "^8.3.0",
|
|
||||||
"@api.global/typedsocket": "^4.1.0",
|
|
||||||
"@apiclient.xyz/cloudflare": "^7.1.0",
|
|
||||||
"@push.rocks/projectinfo": "^5.0.1",
|
|
||||||
"@push.rocks/qenv": "^6.1.0",
|
|
||||||
"@push.rocks/smartacme": "^8.0.0",
|
|
||||||
"@push.rocks/smartdata": "^7.0.15",
|
|
||||||
"@push.rocks/smartdns": "^7.5.0",
|
|
||||||
"@push.rocks/smartfile": "^13.1.2",
|
"@push.rocks/smartfile": "^13.1.2",
|
||||||
"@push.rocks/smartfs": "^1.3.1",
|
"@push.rocks/smartfs": "^1.3.1",
|
||||||
"@push.rocks/smartguard": "^3.1.0",
|
|
||||||
"@push.rocks/smartjwt": "^2.2.1",
|
|
||||||
"@push.rocks/smartlog": "^3.1.8",
|
"@push.rocks/smartlog": "^3.1.8",
|
||||||
"@push.rocks/smartmail": "^2.2.0",
|
"@push.rocks/smartmail": "^2.2.0",
|
||||||
"@push.rocks/smartmetrics": "^2.0.10",
|
|
||||||
"@push.rocks/smartnetwork": "^4.0.2",
|
|
||||||
"@push.rocks/smartpath": "^6.0.0",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"@push.rocks/smartpromise": "^4.0.3",
|
"@push.rocks/smartrust": "^1.2.0",
|
||||||
"@push.rocks/smartproxy": "^23.1.0",
|
|
||||||
"@push.rocks/smartrequest": "^5.0.1",
|
|
||||||
"@push.rocks/smartrule": "^2.0.1",
|
|
||||||
"@push.rocks/smartrust": "^1.1.1",
|
|
||||||
"@push.rocks/smartrx": "^3.0.10",
|
|
||||||
"@push.rocks/smartunique": "^3.0.9",
|
|
||||||
"@serve.zone/interfaces": "^5.0.4",
|
|
||||||
"@tsclass/tsclass": "^9.2.0",
|
"@tsclass/tsclass": "^9.2.0",
|
||||||
"ip": "^2.0.1",
|
|
||||||
"lru-cache": "^11.2.5",
|
"lru-cache": "^11.2.5",
|
||||||
"mailauth": "^4.13.0",
|
|
||||||
"mailparser": "^3.9.3",
|
"mailparser": "^3.9.3",
|
||||||
"uuid": "^13.0.0"
|
"uuid": "^13.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
1887
pnpm-lock.yaml
generated
1887
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
323
readme.md
323
readme.md
@@ -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 SMTP protocol
|
1. 📨 Rust SMTP server accepts the connection and handles the full SMTP protocol
|
||||||
2. On `DATA` completion, Rust emits an `emailReceived` event via IPC
|
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. TypeScript processes the email (routing, scanning, delivery decisions)
|
3. 📤 Rust emits an `emailReceived` event via IPC with pre-computed security results attached
|
||||||
4. TypeScript sends the processing result back to Rust via IPC
|
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,100 +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 (72 tests) | Full SMTP protocol engine — TCP/TLS server, STARTTLS, AUTH, pipelining, 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
|
|
||||||
│ │ │ └── smtpserver/ # Legacy TS SMTP server (socket-handler fallback)
|
|
||||||
│ │ ├── 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
|
||||||
@@ -607,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.
|
||||||
|
|||||||
143
rust/Cargo.lock
generated
143
rust/Cargo.lock
generated
@@ -274,15 +274,6 @@ version = "0.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
|
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "convert_case"
|
|
||||||
version = "0.6.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
|
|
||||||
dependencies = [
|
|
||||||
"unicode-segmentation",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
@@ -356,16 +347,6 @@ dependencies = [
|
|||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ctor"
|
|
||||||
version = "0.2.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
|
|
||||||
dependencies = [
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dashmap"
|
name = "dashmap"
|
||||||
version = "6.1.0"
|
version = "6.1.0"
|
||||||
@@ -913,16 +894,6 @@ version = "0.2.181"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5"
|
checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libloading"
|
|
||||||
version = "0.8.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"windows-link",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@@ -1004,6 +975,7 @@ dependencies = [
|
|||||||
name = "mailer-bin"
|
name = "mailer-bin"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"base64",
|
||||||
"clap",
|
"clap",
|
||||||
"dashmap",
|
"dashmap",
|
||||||
"hickory-resolver 0.25.2",
|
"hickory-resolver 0.25.2",
|
||||||
@@ -1014,6 +986,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1021,48 +994,28 @@ name = "mailer-core"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bytes",
|
|
||||||
"mailparse",
|
"mailparse",
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tracing",
|
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mailer-napi"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"mailer-core",
|
|
||||||
"mailer-security",
|
|
||||||
"mailer-smtp",
|
|
||||||
"napi",
|
|
||||||
"napi-build",
|
|
||||||
"napi-derive",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"tokio",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mailer-security"
|
name = "mailer-security"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hickory-resolver 0.25.2",
|
"hickory-resolver 0.25.2",
|
||||||
"ipnet",
|
|
||||||
"mail-auth",
|
"mail-auth",
|
||||||
"mailer-core",
|
"mailer-core",
|
||||||
"psl",
|
"psl",
|
||||||
"regex",
|
"regex",
|
||||||
"ring",
|
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1070,7 +1023,6 @@ name = "mailer-smtp"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bytes",
|
|
||||||
"dashmap",
|
"dashmap",
|
||||||
"hickory-resolver 0.25.2",
|
"hickory-resolver 0.25.2",
|
||||||
"mailer-core",
|
"mailer-core",
|
||||||
@@ -1087,6 +1039,7 @@ dependencies = [
|
|||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
"tracing",
|
"tracing",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
"webpki-roots 0.26.11",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1144,66 +1097,6 @@ dependencies = [
|
|||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "napi"
|
|
||||||
version = "2.16.17"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "55740c4ae1d8696773c78fdafd5d0e5fe9bc9f1b071c7ba493ba5c413a9184f3"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"ctor",
|
|
||||||
"napi-derive",
|
|
||||||
"napi-sys",
|
|
||||||
"once_cell",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"tokio",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "napi-build"
|
|
||||||
version = "2.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d376940fd5b723c6893cd1ee3f33abbfd86acb1cd1ec079f3ab04a2a3bc4d3b1"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "napi-derive"
|
|
||||||
version = "2.16.13"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"convert_case",
|
|
||||||
"napi-derive-backend",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "napi-derive-backend"
|
|
||||||
version = "1.0.75"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf"
|
|
||||||
dependencies = [
|
|
||||||
"convert_case",
|
|
||||||
"once_cell",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"regex",
|
|
||||||
"semver",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "napi-sys"
|
|
||||||
version = "2.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3"
|
|
||||||
dependencies = [
|
|
||||||
"libloading",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -1543,12 +1436,6 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "semver"
|
|
||||||
version = "1.0.27"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.228"
|
version = "1.0.228"
|
||||||
@@ -1859,12 +1746,6 @@ version = "1.0.23"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e"
|
checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-segmentation"
|
|
||||||
version = "1.12.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -1972,6 +1853,24 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "0.26.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
|
||||||
|
dependencies = [
|
||||||
|
"webpki-roots 1.0.6",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "1.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed"
|
||||||
|
dependencies = [
|
||||||
|
"rustls-pki-types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "widestring"
|
name = "widestring"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ members = [
|
|||||||
"crates/mailer-core",
|
"crates/mailer-core",
|
||||||
"crates/mailer-smtp",
|
"crates/mailer-smtp",
|
||||||
"crates/mailer-security",
|
"crates/mailer-security",
|
||||||
"crates/mailer-napi",
|
|
||||||
"crates/mailer-bin",
|
"crates/mailer-bin",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -19,19 +18,14 @@ tokio-rustls = "0.26"
|
|||||||
hickory-resolver = "0.25"
|
hickory-resolver = "0.25"
|
||||||
mail-auth = "0.7"
|
mail-auth = "0.7"
|
||||||
mailparse = "0.16"
|
mailparse = "0.16"
|
||||||
napi = { version = "2", features = ["napi9", "async", "serde-json"] }
|
|
||||||
napi-derive = "2"
|
|
||||||
ring = "0.17"
|
|
||||||
dashmap = "6"
|
dashmap = "6"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
bytes = "1"
|
|
||||||
regex = "1"
|
regex = "1"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
uuid = { version = "1", features = ["v4"] }
|
uuid = { version = "1", features = ["v4"] }
|
||||||
ipnet = "2"
|
|
||||||
rustls-pki-types = "1"
|
rustls-pki-types = "1"
|
||||||
psl = "2"
|
psl = "2"
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
|||||||
@@ -19,3 +19,5 @@ serde_json.workspace = true
|
|||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
hickory-resolver.workspace = true
|
hickory-resolver.workspace = true
|
||||||
dashmap.workspace = true
|
dashmap.workspace = true
|
||||||
|
base64.workspace = true
|
||||||
|
uuid.workspace = true
|
||||||
|
|||||||
@@ -327,6 +327,7 @@ struct ManagementState {
|
|||||||
callbacks: Arc<PendingCallbacks>,
|
callbacks: Arc<PendingCallbacks>,
|
||||||
smtp_handle: Option<mailer_smtp::server::SmtpServerHandle>,
|
smtp_handle: Option<mailer_smtp::server::SmtpServerHandle>,
|
||||||
smtp_event_rx: Option<tokio::sync::mpsc::Receiver<ConnectionEvent>>,
|
smtp_event_rx: Option<tokio::sync::mpsc::Receiver<ConnectionEvent>>,
|
||||||
|
smtp_client_manager: Arc<mailer_smtp::client::SmtpClientManager>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run in management/IPC mode for smartrust bridge.
|
/// Run in management/IPC mode for smartrust bridge.
|
||||||
@@ -349,10 +350,12 @@ fn run_management_mode() {
|
|||||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
|
|
||||||
let callbacks = Arc::new(PendingCallbacks::new());
|
let callbacks = Arc::new(PendingCallbacks::new());
|
||||||
|
let smtp_client_manager = Arc::new(mailer_smtp::client::SmtpClientManager::new());
|
||||||
let mut state = ManagementState {
|
let mut state = ManagementState {
|
||||||
callbacks: callbacks.clone(),
|
callbacks: callbacks.clone(),
|
||||||
smtp_handle: None,
|
smtp_handle: None,
|
||||||
smtp_event_rx: None,
|
smtp_event_rx: None,
|
||||||
|
smtp_client_manager: smtp_client_manager.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// We need to read stdin in a separate thread (blocking I/O)
|
// We need to read stdin in a separate thread (blocking I/O)
|
||||||
@@ -833,6 +836,28 @@ async fn handle_ipc_request(req: &IpcRequest, state: &mut ManagementState) -> Ip
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- SMTP Client commands ---
|
||||||
|
|
||||||
|
"sendEmail" => {
|
||||||
|
handle_send_email(req, state).await
|
||||||
|
}
|
||||||
|
|
||||||
|
"sendRawEmail" => {
|
||||||
|
handle_send_raw_email(req, state).await
|
||||||
|
}
|
||||||
|
|
||||||
|
"verifySmtpConnection" => {
|
||||||
|
handle_verify_smtp_connection(req, state).await
|
||||||
|
}
|
||||||
|
|
||||||
|
"closeSmtpPool" => {
|
||||||
|
handle_close_smtp_pool(req, state).await
|
||||||
|
}
|
||||||
|
|
||||||
|
"getSmtpPoolStatus" => {
|
||||||
|
handle_get_smtp_pool_status(req, state)
|
||||||
|
}
|
||||||
|
|
||||||
_ => IpcResponse {
|
_ => IpcResponse {
|
||||||
id: req.id.clone(),
|
id: req.id.clone(),
|
||||||
success: false,
|
success: false,
|
||||||
@@ -1052,3 +1077,297 @@ fn parse_smtp_config(
|
|||||||
|
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// SMTP Client IPC handlers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Structured email to build a MIME message from.
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct OutboundEmail {
|
||||||
|
from: String,
|
||||||
|
to: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
cc: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
bcc: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
subject: String,
|
||||||
|
#[serde(default)]
|
||||||
|
text: String,
|
||||||
|
#[serde(default)]
|
||||||
|
html: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
headers: std::collections::HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutboundEmail {
|
||||||
|
/// Convert to `mailer_core::Email` for proper RFC 5322 MIME building.
|
||||||
|
fn to_core_email(&self) -> mailer_core::Email {
|
||||||
|
let mut email = mailer_core::Email::new(&self.from, &self.subject, &self.text);
|
||||||
|
for addr in &self.to {
|
||||||
|
email.add_to(addr);
|
||||||
|
}
|
||||||
|
for addr in &self.cc {
|
||||||
|
email.add_cc(addr);
|
||||||
|
}
|
||||||
|
for addr in &self.bcc {
|
||||||
|
email.add_bcc(addr);
|
||||||
|
}
|
||||||
|
if let Some(html) = &self.html {
|
||||||
|
email.set_html(html);
|
||||||
|
}
|
||||||
|
for (key, value) in &self.headers {
|
||||||
|
email.add_header(key, value);
|
||||||
|
}
|
||||||
|
email
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build an RFC 5322 compliant message using `mailer_core::build_rfc822`.
|
||||||
|
fn to_rfc822(&self) -> Vec<u8> {
|
||||||
|
let email = self.to_core_email();
|
||||||
|
match mailer_core::build_rfc822(&email) {
|
||||||
|
Ok(msg) => msg.into_bytes(),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to build RFC 822 message: {e}");
|
||||||
|
// Fallback: minimal message
|
||||||
|
format!(
|
||||||
|
"From: {}\r\nTo: {}\r\nSubject: {}\r\n\r\n{}",
|
||||||
|
self.from,
|
||||||
|
self.to.join(", "),
|
||||||
|
self.subject,
|
||||||
|
self.text
|
||||||
|
)
|
||||||
|
.into_bytes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collect all recipients (to + cc + bcc).
|
||||||
|
fn all_recipients(&self) -> Vec<String> {
|
||||||
|
let mut all = self.to.clone();
|
||||||
|
all.extend(self.cc.clone());
|
||||||
|
all.extend(self.bcc.clone());
|
||||||
|
all
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle sendEmail IPC command — build MIME, optional DKIM sign, send via pool.
|
||||||
|
async fn handle_send_email(req: &IpcRequest, state: &ManagementState) -> IpcResponse {
|
||||||
|
// Parse client config from params
|
||||||
|
let config: mailer_smtp::client::SmtpClientConfig = match serde_json::from_value(req.params.clone()) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
return IpcResponse {
|
||||||
|
id: req.id.clone(),
|
||||||
|
success: false,
|
||||||
|
result: None,
|
||||||
|
error: Some(format!("Invalid config: {}", e)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse the email
|
||||||
|
let email: OutboundEmail = match req.params.get("email").and_then(|v| serde_json::from_value(v.clone()).ok()) {
|
||||||
|
Some(e) => e,
|
||||||
|
None => {
|
||||||
|
return IpcResponse {
|
||||||
|
id: req.id.clone(),
|
||||||
|
success: false,
|
||||||
|
result: None,
|
||||||
|
error: Some("Missing or invalid 'email' field".into()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build raw message
|
||||||
|
let mut raw_message = email.to_rfc822();
|
||||||
|
|
||||||
|
// Optional DKIM signing
|
||||||
|
if let Some(dkim_val) = req.params.get("dkim") {
|
||||||
|
if let Ok(dkim_config) = serde_json::from_value::<mailer_smtp::client::DkimSignConfig>(dkim_val.clone()) {
|
||||||
|
match mailer_security::sign_dkim(
|
||||||
|
&raw_message,
|
||||||
|
&dkim_config.domain,
|
||||||
|
&dkim_config.selector,
|
||||||
|
&dkim_config.private_key,
|
||||||
|
) {
|
||||||
|
Ok(header) => {
|
||||||
|
// Prepend DKIM header to the message
|
||||||
|
let mut signed = header.into_bytes();
|
||||||
|
signed.extend_from_slice(&raw_message);
|
||||||
|
raw_message = signed;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// Log but don't fail — send unsigned
|
||||||
|
eprintln!("DKIM signing failed: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let all_recipients = email.all_recipients();
|
||||||
|
let sender = &email.from;
|
||||||
|
|
||||||
|
match state
|
||||||
|
.smtp_client_manager
|
||||||
|
.send_message(&config, sender, &all_recipients, &raw_message)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(result) => IpcResponse {
|
||||||
|
id: req.id.clone(),
|
||||||
|
success: true,
|
||||||
|
result: Some(serde_json::to_value(&result).unwrap()),
|
||||||
|
error: None,
|
||||||
|
},
|
||||||
|
Err(e) => IpcResponse {
|
||||||
|
id: req.id.clone(),
|
||||||
|
success: false,
|
||||||
|
result: None,
|
||||||
|
error: Some(serde_json::to_string(&serde_json::json!({
|
||||||
|
"message": e.to_string(),
|
||||||
|
"errorType": e.error_type(),
|
||||||
|
"retryable": e.is_retryable(),
|
||||||
|
"smtpCode": e.smtp_code(),
|
||||||
|
}))
|
||||||
|
.unwrap()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle sendRawEmail IPC command — send a pre-formatted message.
|
||||||
|
async fn handle_send_raw_email(req: &IpcRequest, state: &ManagementState) -> IpcResponse {
|
||||||
|
// Parse client config from params
|
||||||
|
let config: mailer_smtp::client::SmtpClientConfig = match serde_json::from_value(req.params.clone()) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
return IpcResponse {
|
||||||
|
id: req.id.clone(),
|
||||||
|
success: false,
|
||||||
|
result: None,
|
||||||
|
error: Some(format!("Invalid config: {}", e)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let envelope_from = req
|
||||||
|
.params
|
||||||
|
.get("envelopeFrom")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("");
|
||||||
|
let envelope_to: Vec<String> = req
|
||||||
|
.params
|
||||||
|
.get("envelopeTo")
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.map(|a| {
|
||||||
|
a.iter()
|
||||||
|
.filter_map(|v| v.as_str().map(String::from))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
let raw_b64 = req
|
||||||
|
.params
|
||||||
|
.get("rawMessageBase64")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("");
|
||||||
|
|
||||||
|
// Decode base64 message
|
||||||
|
use base64::Engine;
|
||||||
|
let raw_message = match base64::engine::general_purpose::STANDARD.decode(raw_b64) {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(e) => {
|
||||||
|
return IpcResponse {
|
||||||
|
id: req.id.clone(),
|
||||||
|
success: false,
|
||||||
|
result: None,
|
||||||
|
error: Some(format!("Invalid base64 message: {}", e)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match state
|
||||||
|
.smtp_client_manager
|
||||||
|
.send_message(&config, envelope_from, &envelope_to, &raw_message)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(result) => IpcResponse {
|
||||||
|
id: req.id.clone(),
|
||||||
|
success: true,
|
||||||
|
result: Some(serde_json::to_value(&result).unwrap()),
|
||||||
|
error: None,
|
||||||
|
},
|
||||||
|
Err(e) => IpcResponse {
|
||||||
|
id: req.id.clone(),
|
||||||
|
success: false,
|
||||||
|
result: None,
|
||||||
|
error: Some(serde_json::to_string(&serde_json::json!({
|
||||||
|
"message": e.to_string(),
|
||||||
|
"errorType": e.error_type(),
|
||||||
|
"retryable": e.is_retryable(),
|
||||||
|
"smtpCode": e.smtp_code(),
|
||||||
|
}))
|
||||||
|
.unwrap()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle verifySmtpConnection IPC command.
|
||||||
|
async fn handle_verify_smtp_connection(
|
||||||
|
req: &IpcRequest,
|
||||||
|
state: &ManagementState,
|
||||||
|
) -> IpcResponse {
|
||||||
|
let config: mailer_smtp::client::SmtpClientConfig = match serde_json::from_value(req.params.clone()) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
return IpcResponse {
|
||||||
|
id: req.id.clone(),
|
||||||
|
success: false,
|
||||||
|
result: None,
|
||||||
|
error: Some(format!("Invalid config: {}", e)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match state.smtp_client_manager.verify_connection(&config).await {
|
||||||
|
Ok(result) => IpcResponse {
|
||||||
|
id: req.id.clone(),
|
||||||
|
success: true,
|
||||||
|
result: Some(serde_json::to_value(&result).unwrap()),
|
||||||
|
error: None,
|
||||||
|
},
|
||||||
|
Err(e) => IpcResponse {
|
||||||
|
id: req.id.clone(),
|
||||||
|
success: false,
|
||||||
|
result: None,
|
||||||
|
error: Some(e.to_string()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle closeSmtpPool IPC command.
|
||||||
|
async fn handle_close_smtp_pool(req: &IpcRequest, state: &ManagementState) -> IpcResponse {
|
||||||
|
if let Some(pool_key) = req.params.get("poolKey").and_then(|v| v.as_str()) {
|
||||||
|
state.smtp_client_manager.close_pool(pool_key).await;
|
||||||
|
} else {
|
||||||
|
state.smtp_client_manager.close_all_pools().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcResponse {
|
||||||
|
id: req.id.clone(),
|
||||||
|
success: true,
|
||||||
|
result: Some(serde_json::json!({"closed": true})),
|
||||||
|
error: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle getSmtpPoolStatus IPC command.
|
||||||
|
fn handle_get_smtp_pool_status(req: &IpcRequest, state: &ManagementState) -> IpcResponse {
|
||||||
|
let pools = state.smtp_client_manager.pool_status();
|
||||||
|
IpcResponse {
|
||||||
|
id: req.id.clone(),
|
||||||
|
success: true,
|
||||||
|
result: Some(serde_json::json!({"pools": pools})),
|
||||||
|
error: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ license.workspace = true
|
|||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
tracing.workspace = true
|
|
||||||
bytes.workspace = true
|
|
||||||
mailparse.workspace = true
|
mailparse.workspace = true
|
||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "mailer-napi"
|
|
||||||
version.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
crate-type = ["cdylib"]
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
mailer-core = { path = "../mailer-core" }
|
|
||||||
mailer-smtp = { path = "../mailer-smtp" }
|
|
||||||
mailer-security = { path = "../mailer-security" }
|
|
||||||
napi.workspace = true
|
|
||||||
napi-derive.workspace = true
|
|
||||||
tokio.workspace = true
|
|
||||||
serde.workspace = true
|
|
||||||
serde_json.workspace = true
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
napi-build = "2"
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
extern crate napi_build;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
napi_build::setup();
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
//! mailer-napi: N-API bindings exposing Rust mailer to Node.js/TypeScript.
|
|
||||||
|
|
||||||
use napi_derive::napi;
|
|
||||||
|
|
||||||
/// Returns the version of the native mailer module.
|
|
||||||
#[napi]
|
|
||||||
pub fn get_version() -> String {
|
|
||||||
format!(
|
|
||||||
"mailer-napi v{} (core: {}, smtp: {}, security: {})",
|
|
||||||
env!("CARGO_PKG_VERSION"),
|
|
||||||
mailer_core::version(),
|
|
||||||
mailer_smtp::version(),
|
|
||||||
mailer_security::version(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -7,14 +7,11 @@ license.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
mailer-core = { path = "../mailer-core" }
|
mailer-core = { path = "../mailer-core" }
|
||||||
mail-auth.workspace = true
|
mail-auth.workspace = true
|
||||||
ring.workspace = true
|
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
tracing.workspace = true
|
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
hickory-resolver.workspace = true
|
hickory-resolver.workspace = true
|
||||||
ipnet.workspace = true
|
|
||||||
rustls-pki-types.workspace = true
|
rustls-pki-types.workspace = true
|
||||||
psl.workspace = true
|
psl.workspace = true
|
||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
|
|||||||
@@ -111,16 +111,18 @@ static MACRO_DOCUMENT_EXTENSIONS: LazyLock<Vec<&'static str>> = LazyLock::new(||
|
|||||||
// HTML helpers
|
// HTML helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Regexes for HTML text extraction (compiled once via LazyLock).
|
||||||
|
static HTML_STYLE_RE: LazyLock<Regex> =
|
||||||
|
LazyLock::new(|| Regex::new(r"(?is)<style[^>]*>.*?</style>").unwrap());
|
||||||
|
static HTML_SCRIPT_RE: LazyLock<Regex> =
|
||||||
|
LazyLock::new(|| Regex::new(r"(?is)<script[^>]*>.*?</script>").unwrap());
|
||||||
|
static HTML_TAG_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"<[^>]+>").unwrap());
|
||||||
|
|
||||||
/// Strip HTML tags and decode common entities to produce plain text.
|
/// Strip HTML tags and decode common entities to produce plain text.
|
||||||
fn extract_text_from_html(html: &str) -> String {
|
fn extract_text_from_html(html: &str) -> String {
|
||||||
// Remove style and script blocks first
|
let text = HTML_STYLE_RE.replace_all(html, " ");
|
||||||
let no_style = Regex::new(r"(?is)<style[^>]*>.*?</style>").unwrap();
|
let text = HTML_SCRIPT_RE.replace_all(&text, " ");
|
||||||
let no_script = Regex::new(r"(?is)<script[^>]*>.*?</script>").unwrap();
|
let text = HTML_TAG_RE.replace_all(&text, " ");
|
||||||
let no_tags = Regex::new(r"<[^>]+>").unwrap();
|
|
||||||
|
|
||||||
let text = no_style.replace_all(html, " ");
|
|
||||||
let text = no_script.replace_all(&text, " ");
|
|
||||||
let text = no_tags.replace_all(&text, " ");
|
|
||||||
|
|
||||||
text.replace(" ", " ")
|
text.replace(" ", " ")
|
||||||
.replace("<", "<")
|
.replace("<", "<")
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ hickory-resolver.workspace = true
|
|||||||
dashmap.workspace = true
|
dashmap.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
bytes.workspace = true
|
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json = "1"
|
serde_json.workspace = true
|
||||||
regex = "1"
|
regex.workspace = true
|
||||||
uuid = { version = "1", features = ["v4"] }
|
uuid.workspace = true
|
||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
rustls-pki-types.workspace = true
|
rustls-pki-types.workspace = true
|
||||||
rustls = { version = "0.23", default-features = false, features = ["ring", "logging", "std", "tls12"] }
|
rustls = { version = "0.23", default-features = false, features = ["ring", "logging", "std", "tls12"] }
|
||||||
rustls-pemfile = "2"
|
rustls-pemfile = "2"
|
||||||
mailparse.workspace = true
|
mailparse.workspace = true
|
||||||
|
webpki-roots = "0.26"
|
||||||
|
|||||||
157
rust/crates/mailer-smtp/src/client/config.rs
Normal file
157
rust/crates/mailer-smtp/src/client/config.rs
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
//! SMTP client configuration types.
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
/// Configuration for connecting to an SMTP server.
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SmtpClientConfig {
|
||||||
|
/// Target SMTP server hostname.
|
||||||
|
pub host: String,
|
||||||
|
|
||||||
|
/// Target port (25 = SMTP, 465 = implicit TLS, 587 = submission).
|
||||||
|
pub port: u16,
|
||||||
|
|
||||||
|
/// Use implicit TLS (port 465). If false, STARTTLS is attempted.
|
||||||
|
#[serde(default)]
|
||||||
|
pub secure: bool,
|
||||||
|
|
||||||
|
/// Domain to use in EHLO command. Defaults to "localhost".
|
||||||
|
#[serde(default = "default_domain")]
|
||||||
|
pub domain: String,
|
||||||
|
|
||||||
|
/// Authentication credentials (optional).
|
||||||
|
pub auth: Option<SmtpAuthConfig>,
|
||||||
|
|
||||||
|
/// Connection timeout in seconds. Default: 30.
|
||||||
|
#[serde(default = "default_connection_timeout")]
|
||||||
|
pub connection_timeout_secs: u64,
|
||||||
|
|
||||||
|
/// Socket read/write timeout in seconds. Default: 120.
|
||||||
|
#[serde(default = "default_socket_timeout")]
|
||||||
|
pub socket_timeout_secs: u64,
|
||||||
|
|
||||||
|
/// Pool key override. Defaults to "host:port".
|
||||||
|
pub pool_key: Option<String>,
|
||||||
|
|
||||||
|
/// Maximum connections per pool. Default: 10.
|
||||||
|
#[serde(default = "default_max_pool_connections")]
|
||||||
|
pub max_pool_connections: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Authentication configuration.
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SmtpAuthConfig {
|
||||||
|
/// Username.
|
||||||
|
pub user: String,
|
||||||
|
/// Password.
|
||||||
|
pub pass: String,
|
||||||
|
/// Method: "PLAIN" or "LOGIN". Default: "PLAIN".
|
||||||
|
#[serde(default = "default_auth_method")]
|
||||||
|
pub method: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DKIM signing configuration (applied before sending).
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct DkimSignConfig {
|
||||||
|
/// Signing domain (e.g. "example.com").
|
||||||
|
pub domain: String,
|
||||||
|
/// DKIM selector (e.g. "default" or "mta").
|
||||||
|
pub selector: String,
|
||||||
|
/// PEM-encoded RSA private key.
|
||||||
|
pub private_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SmtpClientConfig {
|
||||||
|
/// Get the effective pool key for this config.
|
||||||
|
pub fn effective_pool_key(&self) -> String {
|
||||||
|
self.pool_key
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| format!("{}:{}", self.host, self.port))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_domain() -> String {
|
||||||
|
"localhost".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_connection_timeout() -> u64 {
|
||||||
|
30
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_socket_timeout() -> u64 {
|
||||||
|
120
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_max_pool_connections() -> usize {
|
||||||
|
10
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_auth_method() -> String {
|
||||||
|
"PLAIN".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_minimal_config() {
|
||||||
|
let json = r#"{"host":"mail.example.com","port":25}"#;
|
||||||
|
let config: SmtpClientConfig = serde_json::from_str(json).unwrap();
|
||||||
|
assert_eq!(config.host, "mail.example.com");
|
||||||
|
assert_eq!(config.port, 25);
|
||||||
|
assert!(!config.secure);
|
||||||
|
assert_eq!(config.domain, "localhost");
|
||||||
|
assert!(config.auth.is_none());
|
||||||
|
assert_eq!(config.connection_timeout_secs, 30);
|
||||||
|
assert_eq!(config.socket_timeout_secs, 120);
|
||||||
|
assert_eq!(config.max_pool_connections, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_full_config() {
|
||||||
|
let json = r#"{
|
||||||
|
"host": "smtp.gmail.com",
|
||||||
|
"port": 465,
|
||||||
|
"secure": true,
|
||||||
|
"domain": "myserver.com",
|
||||||
|
"auth": { "user": "u", "pass": "p", "method": "LOGIN" },
|
||||||
|
"connectionTimeoutSecs": 60,
|
||||||
|
"socketTimeoutSecs": 300,
|
||||||
|
"poolKey": "gmail",
|
||||||
|
"maxPoolConnections": 5
|
||||||
|
}"#;
|
||||||
|
let config: SmtpClientConfig = serde_json::from_str(json).unwrap();
|
||||||
|
assert_eq!(config.host, "smtp.gmail.com");
|
||||||
|
assert_eq!(config.port, 465);
|
||||||
|
assert!(config.secure);
|
||||||
|
assert_eq!(config.domain, "myserver.com");
|
||||||
|
assert_eq!(config.connection_timeout_secs, 60);
|
||||||
|
assert_eq!(config.socket_timeout_secs, 300);
|
||||||
|
assert_eq!(config.effective_pool_key(), "gmail");
|
||||||
|
assert_eq!(config.max_pool_connections, 5);
|
||||||
|
let auth = config.auth.unwrap();
|
||||||
|
assert_eq!(auth.user, "u");
|
||||||
|
assert_eq!(auth.pass, "p");
|
||||||
|
assert_eq!(auth.method, "LOGIN");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_effective_pool_key_default() {
|
||||||
|
let json = r#"{"host":"mx.example.com","port":587}"#;
|
||||||
|
let config: SmtpClientConfig = serde_json::from_str(json).unwrap();
|
||||||
|
assert_eq!(config.effective_pool_key(), "mx.example.com:587");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dkim_config_deserialize() {
|
||||||
|
let json = r#"{"domain":"example.com","selector":"mta","privateKey":"-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----"}"#;
|
||||||
|
let dkim: DkimSignConfig = serde_json::from_str(json).unwrap();
|
||||||
|
assert_eq!(dkim.domain, "example.com");
|
||||||
|
assert_eq!(dkim.selector, "mta");
|
||||||
|
assert!(dkim.private_key.contains("RSA PRIVATE KEY"));
|
||||||
|
}
|
||||||
|
}
|
||||||
206
rust/crates/mailer-smtp/src/client/connection.rs
Normal file
206
rust/crates/mailer-smtp/src/client/connection.rs
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
//! TCP/TLS connection management for the SMTP client.
|
||||||
|
|
||||||
|
use super::error::SmtpClientError;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||||
|
use tokio::net::TcpStream;
|
||||||
|
use tokio::time::{timeout, Duration};
|
||||||
|
use tokio_rustls::client::TlsStream;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
/// A client-side SMTP stream that may be plain or TLS.
|
||||||
|
pub enum ClientSmtpStream {
|
||||||
|
Plain(BufReader<TcpStream>),
|
||||||
|
Tls(BufReader<TlsStream<TcpStream>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for ClientSmtpStream {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
ClientSmtpStream::Plain(_) => write!(f, "ClientSmtpStream::Plain"),
|
||||||
|
ClientSmtpStream::Tls(_) => write!(f, "ClientSmtpStream::Tls"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientSmtpStream {
|
||||||
|
/// Read a line from the stream (CRLF-terminated).
|
||||||
|
pub async fn read_line(&mut self, buf: &mut String) -> Result<usize, SmtpClientError> {
|
||||||
|
match self {
|
||||||
|
ClientSmtpStream::Plain(reader) => reader.read_line(buf).await.map_err(|e| {
|
||||||
|
SmtpClientError::ConnectionError {
|
||||||
|
message: format!("Read error: {e}"),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ClientSmtpStream::Tls(reader) => reader.read_line(buf).await.map_err(|e| {
|
||||||
|
SmtpClientError::ConnectionError {
|
||||||
|
message: format!("TLS read error: {e}"),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write bytes to the stream.
|
||||||
|
pub async fn write_all(&mut self, data: &[u8]) -> Result<(), SmtpClientError> {
|
||||||
|
match self {
|
||||||
|
ClientSmtpStream::Plain(reader) => {
|
||||||
|
reader.get_mut().write_all(data).await.map_err(|e| {
|
||||||
|
SmtpClientError::ConnectionError {
|
||||||
|
message: format!("Write error: {e}"),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ClientSmtpStream::Tls(reader) => {
|
||||||
|
reader.get_mut().write_all(data).await.map_err(|e| {
|
||||||
|
SmtpClientError::ConnectionError {
|
||||||
|
message: format!("TLS write error: {e}"),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flush the stream.
|
||||||
|
pub async fn flush(&mut self) -> Result<(), SmtpClientError> {
|
||||||
|
match self {
|
||||||
|
ClientSmtpStream::Plain(reader) => {
|
||||||
|
reader.get_mut().flush().await.map_err(|e| {
|
||||||
|
SmtpClientError::ConnectionError {
|
||||||
|
message: format!("Flush error: {e}"),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ClientSmtpStream::Tls(reader) => {
|
||||||
|
reader.get_mut().flush().await.map_err(|e| {
|
||||||
|
SmtpClientError::ConnectionError {
|
||||||
|
message: format!("TLS flush error: {e}"),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consume this stream and return the inner TcpStream (for STARTTLS upgrade).
|
||||||
|
/// Only works on Plain streams; returns an error on TLS streams.
|
||||||
|
pub fn into_tcp_stream(self) -> Result<TcpStream, SmtpClientError> {
|
||||||
|
match self {
|
||||||
|
ClientSmtpStream::Plain(reader) => Ok(reader.into_inner()),
|
||||||
|
ClientSmtpStream::Tls(_) => Err(SmtpClientError::TlsError {
|
||||||
|
message: "Cannot extract TcpStream from an already-TLS stream".into(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connect to an SMTP server via plain TCP.
|
||||||
|
pub async fn connect_plain(
|
||||||
|
host: &str,
|
||||||
|
port: u16,
|
||||||
|
timeout_secs: u64,
|
||||||
|
) -> Result<ClientSmtpStream, SmtpClientError> {
|
||||||
|
debug!("Connecting to {}:{} (plain)", host, port);
|
||||||
|
let addr = format!("{host}:{port}");
|
||||||
|
let stream = timeout(Duration::from_secs(timeout_secs), TcpStream::connect(&addr))
|
||||||
|
.await
|
||||||
|
.map_err(|_| SmtpClientError::TimeoutError {
|
||||||
|
message: format!("Connection to {addr} timed out after {timeout_secs}s"),
|
||||||
|
})?
|
||||||
|
.map_err(|e| SmtpClientError::ConnectionError {
|
||||||
|
message: format!("Failed to connect to {addr}: {e}"),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(ClientSmtpStream::Plain(BufReader::new(stream)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connect to an SMTP server via implicit TLS (port 465).
|
||||||
|
pub async fn connect_tls(
|
||||||
|
host: &str,
|
||||||
|
port: u16,
|
||||||
|
timeout_secs: u64,
|
||||||
|
) -> Result<ClientSmtpStream, SmtpClientError> {
|
||||||
|
debug!("Connecting to {}:{} (implicit TLS)", host, port);
|
||||||
|
let addr = format!("{host}:{port}");
|
||||||
|
|
||||||
|
let tcp_stream = timeout(Duration::from_secs(timeout_secs), TcpStream::connect(&addr))
|
||||||
|
.await
|
||||||
|
.map_err(|_| SmtpClientError::TimeoutError {
|
||||||
|
message: format!("Connection to {addr} timed out after {timeout_secs}s"),
|
||||||
|
})?
|
||||||
|
.map_err(|e| SmtpClientError::ConnectionError {
|
||||||
|
message: format!("Failed to connect to {addr}: {e}"),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let tls_stream = perform_tls_handshake(tcp_stream, host).await?;
|
||||||
|
Ok(ClientSmtpStream::Tls(BufReader::new(tls_stream)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upgrade a plain TCP connection to TLS (STARTTLS).
|
||||||
|
pub async fn upgrade_to_tls(
|
||||||
|
stream: ClientSmtpStream,
|
||||||
|
hostname: &str,
|
||||||
|
) -> Result<ClientSmtpStream, SmtpClientError> {
|
||||||
|
debug!("Upgrading connection to TLS (STARTTLS) for {}", hostname);
|
||||||
|
let tcp_stream = stream.into_tcp_stream()?;
|
||||||
|
let tls_stream = perform_tls_handshake(tcp_stream, hostname).await?;
|
||||||
|
Ok(ClientSmtpStream::Tls(BufReader::new(tls_stream)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Perform the TLS handshake on a TCP stream using webpki-roots.
|
||||||
|
async fn perform_tls_handshake(
|
||||||
|
tcp_stream: TcpStream,
|
||||||
|
hostname: &str,
|
||||||
|
) -> Result<TlsStream<TcpStream>, SmtpClientError> {
|
||||||
|
let mut root_store = rustls::RootCertStore::empty();
|
||||||
|
root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||||
|
|
||||||
|
let tls_config = rustls::ClientConfig::builder()
|
||||||
|
.with_root_certificates(root_store)
|
||||||
|
.with_no_client_auth();
|
||||||
|
|
||||||
|
let connector = tokio_rustls::TlsConnector::from(Arc::new(tls_config));
|
||||||
|
let server_name = rustls_pki_types::ServerName::try_from(hostname.to_string()).map_err(|e| {
|
||||||
|
SmtpClientError::TlsError {
|
||||||
|
message: format!("Invalid server name '{hostname}': {e}"),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let tls_stream = connector
|
||||||
|
.connect(server_name, tcp_stream)
|
||||||
|
.await
|
||||||
|
.map_err(|e| SmtpClientError::TlsError {
|
||||||
|
message: format!("TLS handshake with {hostname} failed: {e}"),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(tls_stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_connect_plain_refused() {
|
||||||
|
// Connecting to a port that's not listening should fail
|
||||||
|
let result = connect_plain("127.0.0.1", 19999, 2).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
assert!(matches!(err, SmtpClientError::ConnectionError { .. }));
|
||||||
|
assert!(err.is_retryable());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_connect_tls_refused() {
|
||||||
|
let result = connect_tls("127.0.0.1", 19998, 2).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_connect_timeout() {
|
||||||
|
// 192.0.2.1 is TEST-NET, should time out
|
||||||
|
let result = connect_plain("192.0.2.1", 25, 1).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
// May be timeout or connection error depending on network
|
||||||
|
assert!(err.is_retryable());
|
||||||
|
}
|
||||||
|
}
|
||||||
160
rust/crates/mailer-smtp/src/client/error.rs
Normal file
160
rust/crates/mailer-smtp/src/client/error.rs
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
//! SMTP client error types.
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
/// Errors that can occur during SMTP client operations.
|
||||||
|
#[derive(Debug, thiserror::Error, Serialize)]
|
||||||
|
pub enum SmtpClientError {
|
||||||
|
#[error("Connection error: {message}")]
|
||||||
|
ConnectionError { message: String },
|
||||||
|
|
||||||
|
#[error("Timeout: {message}")]
|
||||||
|
TimeoutError { message: String },
|
||||||
|
|
||||||
|
#[error("TLS error: {message}")]
|
||||||
|
TlsError { message: String },
|
||||||
|
|
||||||
|
#[error("Authentication failed: {message}")]
|
||||||
|
AuthenticationError { message: String },
|
||||||
|
|
||||||
|
#[error("Protocol error ({code}): {message}")]
|
||||||
|
ProtocolError { code: u16, message: String },
|
||||||
|
|
||||||
|
#[error("Pool exhausted: {message}")]
|
||||||
|
PoolExhausted { message: String },
|
||||||
|
|
||||||
|
#[error("Invalid configuration: {message}")]
|
||||||
|
ConfigError { message: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SmtpClientError {
|
||||||
|
/// Whether this error is retryable (temporary failure).
|
||||||
|
/// Permanent failures (5xx, auth failures) are not retryable.
|
||||||
|
pub fn is_retryable(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
SmtpClientError::ConnectionError { .. } => true,
|
||||||
|
SmtpClientError::TimeoutError { .. } => true,
|
||||||
|
SmtpClientError::TlsError { .. } => false,
|
||||||
|
SmtpClientError::AuthenticationError { .. } => false,
|
||||||
|
SmtpClientError::ProtocolError { code, .. } => *code >= 400 && *code < 500,
|
||||||
|
SmtpClientError::PoolExhausted { .. } => true,
|
||||||
|
SmtpClientError::ConfigError { .. } => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The error type as a string for IPC serialization.
|
||||||
|
pub fn error_type(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
SmtpClientError::ConnectionError { .. } => "connection",
|
||||||
|
SmtpClientError::TimeoutError { .. } => "timeout",
|
||||||
|
SmtpClientError::TlsError { .. } => "tls",
|
||||||
|
SmtpClientError::AuthenticationError { .. } => "authentication",
|
||||||
|
SmtpClientError::ProtocolError { .. } => "protocol",
|
||||||
|
SmtpClientError::PoolExhausted { .. } => "pool_exhausted",
|
||||||
|
SmtpClientError::ConfigError { .. } => "config",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The SMTP code if this is a protocol error.
|
||||||
|
pub fn smtp_code(&self) -> Option<u16> {
|
||||||
|
match self {
|
||||||
|
SmtpClientError::ProtocolError { code, .. } => Some(*code),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_retryable_errors() {
|
||||||
|
assert!(SmtpClientError::ConnectionError {
|
||||||
|
message: "refused".into()
|
||||||
|
}
|
||||||
|
.is_retryable());
|
||||||
|
assert!(SmtpClientError::TimeoutError {
|
||||||
|
message: "timed out".into()
|
||||||
|
}
|
||||||
|
.is_retryable());
|
||||||
|
assert!(SmtpClientError::PoolExhausted {
|
||||||
|
message: "full".into()
|
||||||
|
}
|
||||||
|
.is_retryable());
|
||||||
|
assert!(SmtpClientError::ProtocolError {
|
||||||
|
code: 421,
|
||||||
|
message: "try later".into()
|
||||||
|
}
|
||||||
|
.is_retryable());
|
||||||
|
assert!(SmtpClientError::ProtocolError {
|
||||||
|
code: 450,
|
||||||
|
message: "mailbox busy".into()
|
||||||
|
}
|
||||||
|
.is_retryable());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_non_retryable_errors() {
|
||||||
|
assert!(!SmtpClientError::AuthenticationError {
|
||||||
|
message: "bad creds".into()
|
||||||
|
}
|
||||||
|
.is_retryable());
|
||||||
|
assert!(!SmtpClientError::TlsError {
|
||||||
|
message: "cert invalid".into()
|
||||||
|
}
|
||||||
|
.is_retryable());
|
||||||
|
assert!(!SmtpClientError::ProtocolError {
|
||||||
|
code: 550,
|
||||||
|
message: "no such user".into()
|
||||||
|
}
|
||||||
|
.is_retryable());
|
||||||
|
assert!(!SmtpClientError::ProtocolError {
|
||||||
|
code: 554,
|
||||||
|
message: "rejected".into()
|
||||||
|
}
|
||||||
|
.is_retryable());
|
||||||
|
assert!(!SmtpClientError::ConfigError {
|
||||||
|
message: "bad config".into()
|
||||||
|
}
|
||||||
|
.is_retryable());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_error_type_strings() {
|
||||||
|
assert_eq!(
|
||||||
|
SmtpClientError::ConnectionError {
|
||||||
|
message: "x".into()
|
||||||
|
}
|
||||||
|
.error_type(),
|
||||||
|
"connection"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
SmtpClientError::ProtocolError {
|
||||||
|
code: 550,
|
||||||
|
message: "x".into()
|
||||||
|
}
|
||||||
|
.error_type(),
|
||||||
|
"protocol"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_smtp_code() {
|
||||||
|
assert_eq!(
|
||||||
|
SmtpClientError::ProtocolError {
|
||||||
|
code: 550,
|
||||||
|
message: "x".into()
|
||||||
|
}
|
||||||
|
.smtp_code(),
|
||||||
|
Some(550)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
SmtpClientError::ConnectionError {
|
||||||
|
message: "x".into()
|
||||||
|
}
|
||||||
|
.smtp_code(),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
rust/crates/mailer-smtp/src/client/mod.rs
Normal file
16
rust/crates/mailer-smtp/src/client/mod.rs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
//! SMTP client module for outbound email delivery.
|
||||||
|
//!
|
||||||
|
//! Provides connection pooling, SMTP protocol, TLS, and authentication
|
||||||
|
//! for sending outbound emails through remote SMTP servers.
|
||||||
|
|
||||||
|
pub mod config;
|
||||||
|
pub mod connection;
|
||||||
|
pub mod error;
|
||||||
|
pub mod pool;
|
||||||
|
pub mod protocol;
|
||||||
|
|
||||||
|
// Re-export key types for convenience.
|
||||||
|
pub use config::{DkimSignConfig, SmtpAuthConfig, SmtpClientConfig};
|
||||||
|
pub use error::SmtpClientError;
|
||||||
|
pub use pool::{SmtpClientManager, SmtpSendResult, SmtpVerifyResult};
|
||||||
|
pub use protocol::{dot_stuff, EhloCapabilities, SmtpClientResponse};
|
||||||
503
rust/crates/mailer-smtp/src/client/pool.rs
Normal file
503
rust/crates/mailer-smtp/src/client/pool.rs
Normal file
@@ -0,0 +1,503 @@
|
|||||||
|
//! Connection pooling for the SMTP client.
|
||||||
|
//!
|
||||||
|
//! Manages reusable connections per destination `host:port`.
|
||||||
|
|
||||||
|
use super::config::SmtpClientConfig;
|
||||||
|
use super::connection::{connect_plain, connect_tls, ClientSmtpStream};
|
||||||
|
use super::error::SmtpClientError;
|
||||||
|
use super::protocol::{self, EhloCapabilities};
|
||||||
|
use dashmap::DashMap;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Instant;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use tracing::{debug, info};
|
||||||
|
|
||||||
|
/// Maximum age of a pooled connection (5 minutes).
|
||||||
|
const MAX_CONNECTION_AGE_SECS: u64 = 300;
|
||||||
|
|
||||||
|
/// Maximum idle time before a connection is reaped (30 seconds).
|
||||||
|
const MAX_IDLE_SECS: u64 = 30;
|
||||||
|
|
||||||
|
/// Maximum messages per pooled connection before it's recycled.
|
||||||
|
const MAX_MESSAGES_PER_CONNECTION: u32 = 100;
|
||||||
|
|
||||||
|
/// A pooled SMTP connection.
|
||||||
|
pub struct PooledConnection {
|
||||||
|
pub stream: ClientSmtpStream,
|
||||||
|
pub capabilities: EhloCapabilities,
|
||||||
|
pub created_at: Instant,
|
||||||
|
pub last_used: Instant,
|
||||||
|
pub message_count: u32,
|
||||||
|
pub idle: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a pooled connection is stale (too old, too many messages, or idle too long).
|
||||||
|
fn is_connection_stale(conn: &PooledConnection) -> bool {
|
||||||
|
conn.created_at.elapsed().as_secs() > MAX_CONNECTION_AGE_SECS
|
||||||
|
|| conn.message_count >= MAX_MESSAGES_PER_CONNECTION
|
||||||
|
|| (conn.idle && conn.last_used.elapsed().as_secs() > MAX_IDLE_SECS)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Per-destination connection pool.
|
||||||
|
pub struct ConnectionPool {
|
||||||
|
connections: Vec<PooledConnection>,
|
||||||
|
max_connections: usize,
|
||||||
|
config: SmtpClientConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConnectionPool {
|
||||||
|
fn new(config: SmtpClientConfig) -> Self {
|
||||||
|
let max_connections = config.max_pool_connections;
|
||||||
|
Self {
|
||||||
|
connections: Vec::new(),
|
||||||
|
max_connections,
|
||||||
|
config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get an idle connection or create a new one.
|
||||||
|
async fn acquire(&mut self) -> Result<PooledConnection, SmtpClientError> {
|
||||||
|
// Remove stale connections first
|
||||||
|
self.cleanup_stale();
|
||||||
|
|
||||||
|
// Find an idle connection
|
||||||
|
if let Some(idx) = self
|
||||||
|
.connections
|
||||||
|
.iter()
|
||||||
|
.position(|c| c.idle && !is_connection_stale(c))
|
||||||
|
{
|
||||||
|
let mut conn = self.connections.remove(idx);
|
||||||
|
conn.idle = false;
|
||||||
|
conn.last_used = Instant::now();
|
||||||
|
debug!(
|
||||||
|
"Reusing pooled connection (age={}s, msgs={})",
|
||||||
|
conn.created_at.elapsed().as_secs(),
|
||||||
|
conn.message_count
|
||||||
|
);
|
||||||
|
return Ok(conn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we can create a new connection
|
||||||
|
if self.connections.len() >= self.max_connections {
|
||||||
|
return Err(SmtpClientError::PoolExhausted {
|
||||||
|
message: format!(
|
||||||
|
"Pool for {} is at max capacity ({})",
|
||||||
|
self.config.effective_pool_key(),
|
||||||
|
self.max_connections
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new connection
|
||||||
|
self.create_connection().await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a connection to the pool (or close it if it's expired).
|
||||||
|
fn release(&mut self, mut conn: PooledConnection) {
|
||||||
|
conn.message_count += 1;
|
||||||
|
conn.last_used = Instant::now();
|
||||||
|
conn.idle = true;
|
||||||
|
|
||||||
|
// Don't return if it's stale
|
||||||
|
if is_connection_stale(&conn) || self.connections.len() >= self.max_connections {
|
||||||
|
debug!("Discarding stale/excess pooled connection");
|
||||||
|
// Drop the connection (stream will be closed)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.connections.push(conn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a fresh SMTP connection and complete the handshake.
|
||||||
|
async fn create_connection(&self) -> Result<PooledConnection, SmtpClientError> {
|
||||||
|
let mut stream = if self.config.secure {
|
||||||
|
connect_tls(
|
||||||
|
&self.config.host,
|
||||||
|
self.config.port,
|
||||||
|
self.config.connection_timeout_secs,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
} else {
|
||||||
|
connect_plain(
|
||||||
|
&self.config.host,
|
||||||
|
self.config.port,
|
||||||
|
self.config.connection_timeout_secs,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read greeting
|
||||||
|
protocol::read_greeting(&mut stream, self.config.socket_timeout_secs).await?;
|
||||||
|
|
||||||
|
// Send EHLO
|
||||||
|
let mut capabilities =
|
||||||
|
protocol::send_ehlo(&mut stream, &self.config.domain, self.config.socket_timeout_secs)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// STARTTLS if available and not already secure
|
||||||
|
if !self.config.secure && capabilities.starttls {
|
||||||
|
protocol::send_starttls(&mut stream, self.config.socket_timeout_secs).await?;
|
||||||
|
stream =
|
||||||
|
super::connection::upgrade_to_tls(stream, &self.config.host).await?;
|
||||||
|
|
||||||
|
// Re-EHLO after STARTTLS — use updated capabilities for auth
|
||||||
|
capabilities = protocol::send_ehlo(
|
||||||
|
&mut stream,
|
||||||
|
&self.config.domain,
|
||||||
|
self.config.socket_timeout_secs,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate if credentials provided
|
||||||
|
if let Some(auth) = &self.config.auth {
|
||||||
|
protocol::authenticate(
|
||||||
|
&mut stream,
|
||||||
|
auth,
|
||||||
|
&capabilities,
|
||||||
|
self.config.socket_timeout_secs,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"New SMTP connection to {} established",
|
||||||
|
self.config.effective_pool_key()
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(PooledConnection {
|
||||||
|
stream,
|
||||||
|
capabilities,
|
||||||
|
created_at: Instant::now(),
|
||||||
|
last_used: Instant::now(),
|
||||||
|
message_count: 0,
|
||||||
|
idle: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanup_stale(&mut self) {
|
||||||
|
self.connections.retain(|c| !is_connection_stale(c));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of connections in the pool.
|
||||||
|
fn total(&self) -> usize {
|
||||||
|
self.connections.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of idle connections.
|
||||||
|
fn idle_count(&self) -> usize {
|
||||||
|
self.connections.iter().filter(|c| c.idle).count()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close all connections.
|
||||||
|
fn close_all(&mut self) {
|
||||||
|
self.connections.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Status report for a single pool.
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct PoolStatus {
|
||||||
|
pub total: usize,
|
||||||
|
pub active: usize,
|
||||||
|
pub idle: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Manages connection pools for multiple SMTP destinations.
|
||||||
|
pub struct SmtpClientManager {
|
||||||
|
pools: DashMap<String, Arc<Mutex<ConnectionPool>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SmtpClientManager {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
pools: DashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get or create a pool for the given config.
|
||||||
|
fn get_pool(&self, config: &SmtpClientConfig) -> Arc<Mutex<ConnectionPool>> {
|
||||||
|
let key = config.effective_pool_key();
|
||||||
|
self.pools
|
||||||
|
.entry(key)
|
||||||
|
.or_insert_with(|| Arc::new(Mutex::new(ConnectionPool::new(config.clone()))))
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Acquire a connection from the pool, send a message, and release it.
|
||||||
|
pub async fn send_message(
|
||||||
|
&self,
|
||||||
|
config: &SmtpClientConfig,
|
||||||
|
sender: &str,
|
||||||
|
recipients: &[String],
|
||||||
|
message: &[u8],
|
||||||
|
) -> Result<SmtpSendResult, SmtpClientError> {
|
||||||
|
let pool_arc = self.get_pool(config);
|
||||||
|
let mut pool = pool_arc.lock().await;
|
||||||
|
|
||||||
|
let mut conn = pool.acquire().await?;
|
||||||
|
drop(pool); // Release the pool lock while we do network I/O
|
||||||
|
|
||||||
|
// Reset server state if reusing a connection that has already sent messages
|
||||||
|
if conn.message_count > 0 {
|
||||||
|
protocol::send_rset(&mut conn.stream, config.socket_timeout_secs).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform the SMTP transaction
|
||||||
|
let result =
|
||||||
|
Self::perform_send(&mut conn.stream, sender, recipients, message, config).await;
|
||||||
|
|
||||||
|
// Re-acquire the pool lock and release the connection
|
||||||
|
let mut pool = pool_arc.lock().await;
|
||||||
|
match &result {
|
||||||
|
Ok(_) => pool.release(conn),
|
||||||
|
Err(_) => {
|
||||||
|
// Don't return failed connections to the pool
|
||||||
|
debug!("Discarding connection after send failure");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Perform the SMTP send transaction on a connected stream.
|
||||||
|
async fn perform_send(
|
||||||
|
stream: &mut ClientSmtpStream,
|
||||||
|
sender: &str,
|
||||||
|
recipients: &[String],
|
||||||
|
message: &[u8],
|
||||||
|
config: &SmtpClientConfig,
|
||||||
|
) -> Result<SmtpSendResult, SmtpClientError> {
|
||||||
|
let timeout_secs = config.socket_timeout_secs;
|
||||||
|
|
||||||
|
// MAIL FROM
|
||||||
|
protocol::send_mail_from(stream, sender, timeout_secs).await?;
|
||||||
|
|
||||||
|
// RCPT TO for each recipient
|
||||||
|
let mut accepted = Vec::new();
|
||||||
|
let mut rejected = Vec::new();
|
||||||
|
|
||||||
|
for rcpt in recipients {
|
||||||
|
match protocol::send_rcpt_to(stream, rcpt, timeout_secs).await {
|
||||||
|
Ok(resp) => {
|
||||||
|
if resp.is_success() {
|
||||||
|
accepted.push(rcpt.clone());
|
||||||
|
} else {
|
||||||
|
rejected.push(rcpt.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
rejected.push(rcpt.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no recipients were accepted, fail
|
||||||
|
if accepted.is_empty() {
|
||||||
|
return Err(SmtpClientError::ProtocolError {
|
||||||
|
code: 550,
|
||||||
|
message: "All recipients were rejected".into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// DATA
|
||||||
|
let data_resp = protocol::send_data(stream, message, timeout_secs).await?;
|
||||||
|
|
||||||
|
// Extract message ID from the response if present
|
||||||
|
let message_id = data_resp
|
||||||
|
.lines
|
||||||
|
.iter()
|
||||||
|
.find_map(|line| {
|
||||||
|
// Look for a pattern like "queued as XXXX" or message-id
|
||||||
|
if line.contains("queued") || line.contains("id=") {
|
||||||
|
Some(line.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(SmtpSendResult {
|
||||||
|
accepted,
|
||||||
|
rejected,
|
||||||
|
message_id,
|
||||||
|
response: data_resp.full_message(),
|
||||||
|
envelope: SmtpEnvelope {
|
||||||
|
from: sender.to_string(),
|
||||||
|
to: recipients.to_vec(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify connectivity to an SMTP server (connect, EHLO, QUIT).
|
||||||
|
pub async fn verify_connection(
|
||||||
|
&self,
|
||||||
|
config: &SmtpClientConfig,
|
||||||
|
) -> Result<SmtpVerifyResult, SmtpClientError> {
|
||||||
|
let mut stream = if config.secure {
|
||||||
|
connect_tls(
|
||||||
|
&config.host,
|
||||||
|
config.port,
|
||||||
|
config.connection_timeout_secs,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
} else {
|
||||||
|
connect_plain(
|
||||||
|
&config.host,
|
||||||
|
config.port,
|
||||||
|
config.connection_timeout_secs,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
};
|
||||||
|
|
||||||
|
let greeting = protocol::read_greeting(&mut stream, config.socket_timeout_secs).await?;
|
||||||
|
let caps =
|
||||||
|
protocol::send_ehlo(&mut stream, &config.domain, config.socket_timeout_secs).await?;
|
||||||
|
let _ = protocol::send_quit(&mut stream, config.socket_timeout_secs).await;
|
||||||
|
|
||||||
|
Ok(SmtpVerifyResult {
|
||||||
|
reachable: true,
|
||||||
|
greeting: Some(greeting.full_message()),
|
||||||
|
capabilities: Some(caps.extensions),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get status of all pools.
|
||||||
|
pub fn pool_status(&self) -> std::collections::HashMap<String, PoolStatus> {
|
||||||
|
let mut result = std::collections::HashMap::new();
|
||||||
|
|
||||||
|
for entry in self.pools.iter() {
|
||||||
|
let key = entry.key().clone();
|
||||||
|
// Try to get the lock without blocking — if locked, report as active
|
||||||
|
match entry.value().try_lock() {
|
||||||
|
Ok(pool) => {
|
||||||
|
let total = pool.total();
|
||||||
|
let idle = pool.idle_count();
|
||||||
|
result.insert(
|
||||||
|
key,
|
||||||
|
PoolStatus {
|
||||||
|
total,
|
||||||
|
active: total - idle,
|
||||||
|
idle,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// Pool is in use; report as busy
|
||||||
|
result.insert(
|
||||||
|
key,
|
||||||
|
PoolStatus {
|
||||||
|
total: 0,
|
||||||
|
active: 1,
|
||||||
|
idle: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close a specific pool.
|
||||||
|
pub async fn close_pool(&self, key: &str) {
|
||||||
|
if let Some(pool_ref) = self.pools.get(key) {
|
||||||
|
let mut pool = pool_ref.lock().await;
|
||||||
|
pool.close_all();
|
||||||
|
}
|
||||||
|
self.pools.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close all pools.
|
||||||
|
pub async fn close_all_pools(&self) {
|
||||||
|
let keys: Vec<String> = self.pools.iter().map(|e| e.key().clone()).collect();
|
||||||
|
for key in keys {
|
||||||
|
self.close_pool(&key).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of sending an email via SMTP.
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct SmtpSendResult {
|
||||||
|
pub accepted: Vec<String>,
|
||||||
|
pub rejected: Vec<String>,
|
||||||
|
#[serde(rename = "messageId")]
|
||||||
|
pub message_id: Option<String>,
|
||||||
|
pub response: String,
|
||||||
|
pub envelope: SmtpEnvelope,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SMTP envelope (sender + recipients).
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct SmtpEnvelope {
|
||||||
|
pub from: String,
|
||||||
|
pub to: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of verifying an SMTP connection.
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct SmtpVerifyResult {
|
||||||
|
pub reachable: bool,
|
||||||
|
pub greeting: Option<String>,
|
||||||
|
pub capabilities: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_pool_status_serialization() {
|
||||||
|
let status = PoolStatus {
|
||||||
|
total: 5,
|
||||||
|
active: 2,
|
||||||
|
idle: 3,
|
||||||
|
};
|
||||||
|
let json = serde_json::to_string(&status).unwrap();
|
||||||
|
assert!(json.contains("\"total\":5"));
|
||||||
|
assert!(json.contains("\"active\":2"));
|
||||||
|
assert!(json.contains("\"idle\":3"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_send_result_serialization() {
|
||||||
|
let result = SmtpSendResult {
|
||||||
|
accepted: vec!["a@b.com".into()],
|
||||||
|
rejected: vec![],
|
||||||
|
message_id: Some("abc123".into()),
|
||||||
|
response: "250 OK".into(),
|
||||||
|
envelope: SmtpEnvelope {
|
||||||
|
from: "from@test.com".into(),
|
||||||
|
to: vec!["a@b.com".into()],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let json = serde_json::to_string(&result).unwrap();
|
||||||
|
assert!(json.contains("\"messageId\":\"abc123\""));
|
||||||
|
assert!(json.contains("\"accepted\":[\"a@b.com\"]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_verify_result_serialization() {
|
||||||
|
let result = SmtpVerifyResult {
|
||||||
|
reachable: true,
|
||||||
|
greeting: Some("220 mail.example.com".into()),
|
||||||
|
capabilities: Some(vec!["SIZE 10485760".into(), "STARTTLS".into()]),
|
||||||
|
};
|
||||||
|
let json = serde_json::to_string(&result).unwrap();
|
||||||
|
assert!(json.contains("\"reachable\":true"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_smtp_client_manager_new() {
|
||||||
|
let mgr = SmtpClientManager::new();
|
||||||
|
assert!(mgr.pool_status().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_close_all_empty() {
|
||||||
|
let mgr = SmtpClientManager::new();
|
||||||
|
mgr.close_all_pools().await;
|
||||||
|
assert!(mgr.pool_status().is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
520
rust/crates/mailer-smtp/src/client/protocol.rs
Normal file
520
rust/crates/mailer-smtp/src/client/protocol.rs
Normal file
@@ -0,0 +1,520 @@
|
|||||||
|
//! SMTP client protocol engine.
|
||||||
|
//!
|
||||||
|
//! Implements the SMTP command/response flow for sending outbound email.
|
||||||
|
|
||||||
|
use super::config::SmtpAuthConfig;
|
||||||
|
use super::connection::ClientSmtpStream;
|
||||||
|
use super::error::SmtpClientError;
|
||||||
|
use base64::engine::general_purpose::STANDARD as BASE64;
|
||||||
|
use base64::Engine;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::time::{timeout, Duration};
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
/// Parsed SMTP response (from the remote server).
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SmtpClientResponse {
|
||||||
|
pub code: u16,
|
||||||
|
pub lines: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SmtpClientResponse {
|
||||||
|
pub fn is_success(&self) -> bool {
|
||||||
|
self.code >= 200 && self.code < 300
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_positive_intermediate(&self) -> bool {
|
||||||
|
self.code >= 300 && self.code < 400
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_temp_error(&self) -> bool {
|
||||||
|
self.code >= 400 && self.code < 500
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_perm_error(&self) -> bool {
|
||||||
|
self.code >= 500
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full response text (all lines joined).
|
||||||
|
pub fn full_message(&self) -> String {
|
||||||
|
self.lines.join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert to a protocol error if this is an error response.
|
||||||
|
pub fn to_error(&self) -> SmtpClientError {
|
||||||
|
SmtpClientError::ProtocolError {
|
||||||
|
code: self.code,
|
||||||
|
message: self.full_message(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Server capabilities parsed from EHLO response.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct EhloCapabilities {
|
||||||
|
pub extensions: Vec<String>,
|
||||||
|
pub max_size: Option<u64>,
|
||||||
|
pub starttls: bool,
|
||||||
|
pub auth_methods: Vec<String>,
|
||||||
|
pub pipelining: bool,
|
||||||
|
pub eight_bit_mime: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a multi-line SMTP response from the server.
|
||||||
|
pub async fn read_response(
|
||||||
|
stream: &mut ClientSmtpStream,
|
||||||
|
timeout_secs: u64,
|
||||||
|
) -> Result<SmtpClientResponse, SmtpClientError> {
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
let mut code: u16;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut line = String::new();
|
||||||
|
let n = timeout(
|
||||||
|
Duration::from_secs(timeout_secs),
|
||||||
|
stream.read_line(&mut line),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| SmtpClientError::TimeoutError {
|
||||||
|
message: format!("Timeout reading SMTP response after {timeout_secs}s"),
|
||||||
|
})??;
|
||||||
|
|
||||||
|
if n == 0 {
|
||||||
|
return Err(SmtpClientError::ConnectionError {
|
||||||
|
message: "Connection closed while reading response".into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guard against unbounded lines from malicious servers (RFC 5321 §4.5.3.1.4 says 512 max)
|
||||||
|
if line.len() > 4096 {
|
||||||
|
return Err(SmtpClientError::ProtocolError {
|
||||||
|
code: 0,
|
||||||
|
message: format!("Response line too long ({} bytes, max 4096)", line.len()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let line = line.trim_end_matches('\n').trim_end_matches('\r');
|
||||||
|
|
||||||
|
if line.len() < 3 {
|
||||||
|
return Err(SmtpClientError::ProtocolError {
|
||||||
|
code: 0,
|
||||||
|
message: format!("Invalid response line: {line}"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the 3-digit code
|
||||||
|
let parsed_code: u16 = line[..3].parse().map_err(|_| SmtpClientError::ProtocolError {
|
||||||
|
code: 0,
|
||||||
|
message: format!("Invalid response code in: {line}"),
|
||||||
|
})?;
|
||||||
|
code = parsed_code;
|
||||||
|
|
||||||
|
// Text after the code (skip the separator character)
|
||||||
|
let text = if line.len() > 4 { &line[4..] } else { "" };
|
||||||
|
lines.push(text.to_string());
|
||||||
|
|
||||||
|
// Check for continuation: "250-" means more lines, "250 " means last line
|
||||||
|
if line.len() >= 4 && line.as_bytes()[3] == b'-' {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("SMTP response: {} {}", code, lines.join(" | "));
|
||||||
|
Ok(SmtpClientResponse { code, lines })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the server greeting (first response after connect).
|
||||||
|
pub async fn read_greeting(
|
||||||
|
stream: &mut ClientSmtpStream,
|
||||||
|
timeout_secs: u64,
|
||||||
|
) -> Result<SmtpClientResponse, SmtpClientError> {
|
||||||
|
let resp = read_response(stream, timeout_secs).await?;
|
||||||
|
if resp.code == 220 {
|
||||||
|
Ok(resp)
|
||||||
|
} else {
|
||||||
|
Err(SmtpClientError::ProtocolError {
|
||||||
|
code: resp.code,
|
||||||
|
message: format!("Unexpected greeting: {}", resp.full_message()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a raw command and read the response.
|
||||||
|
async fn send_command(
|
||||||
|
stream: &mut ClientSmtpStream,
|
||||||
|
command: &str,
|
||||||
|
timeout_secs: u64,
|
||||||
|
) -> Result<SmtpClientResponse, SmtpClientError> {
|
||||||
|
debug!("SMTP C: {}", command);
|
||||||
|
stream
|
||||||
|
.write_all(format!("{command}\r\n").as_bytes())
|
||||||
|
.await?;
|
||||||
|
stream.flush().await?;
|
||||||
|
read_response(stream, timeout_secs).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send EHLO and parse capabilities.
|
||||||
|
pub async fn send_ehlo(
|
||||||
|
stream: &mut ClientSmtpStream,
|
||||||
|
domain: &str,
|
||||||
|
timeout_secs: u64,
|
||||||
|
) -> Result<EhloCapabilities, SmtpClientError> {
|
||||||
|
let resp = send_command(stream, &format!("EHLO {domain}"), timeout_secs).await?;
|
||||||
|
|
||||||
|
if !resp.is_success() {
|
||||||
|
// Fall back to HELO
|
||||||
|
let helo_resp = send_command(stream, &format!("HELO {domain}"), timeout_secs).await?;
|
||||||
|
if !helo_resp.is_success() {
|
||||||
|
return Err(helo_resp.to_error());
|
||||||
|
}
|
||||||
|
return Ok(EhloCapabilities::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut caps = EhloCapabilities::default();
|
||||||
|
|
||||||
|
// First line is the greeting, remaining lines are capabilities
|
||||||
|
for line in resp.lines.iter().skip(1) {
|
||||||
|
let upper = line.to_uppercase();
|
||||||
|
if upper.starts_with("SIZE ") {
|
||||||
|
caps.max_size = upper[5..].trim().parse().ok();
|
||||||
|
} else if upper == "STARTTLS" {
|
||||||
|
caps.starttls = true;
|
||||||
|
} else if upper.starts_with("AUTH ") {
|
||||||
|
caps.auth_methods = upper[5..]
|
||||||
|
.split_whitespace()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect();
|
||||||
|
} else if upper == "PIPELINING" {
|
||||||
|
caps.pipelining = true;
|
||||||
|
} else if upper == "8BITMIME" {
|
||||||
|
caps.eight_bit_mime = true;
|
||||||
|
}
|
||||||
|
caps.extensions.push(line.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(caps)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send STARTTLS command (does not perform the TLS handshake itself).
|
||||||
|
pub async fn send_starttls(
|
||||||
|
stream: &mut ClientSmtpStream,
|
||||||
|
timeout_secs: u64,
|
||||||
|
) -> Result<(), SmtpClientError> {
|
||||||
|
let resp = send_command(stream, "STARTTLS", timeout_secs).await?;
|
||||||
|
if resp.code != 220 {
|
||||||
|
return Err(SmtpClientError::ProtocolError {
|
||||||
|
code: resp.code,
|
||||||
|
message: format!("STARTTLS rejected: {}", resp.full_message()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Authenticate using AUTH PLAIN.
|
||||||
|
pub async fn send_auth_plain(
|
||||||
|
stream: &mut ClientSmtpStream,
|
||||||
|
user: &str,
|
||||||
|
pass: &str,
|
||||||
|
timeout_secs: u64,
|
||||||
|
) -> Result<(), SmtpClientError> {
|
||||||
|
// AUTH PLAIN sends \0user\0pass in base64
|
||||||
|
let credentials = format!("\x00{user}\x00{pass}");
|
||||||
|
let encoded = BASE64.encode(credentials.as_bytes());
|
||||||
|
let resp = send_command(stream, &format!("AUTH PLAIN {encoded}"), timeout_secs).await?;
|
||||||
|
|
||||||
|
if resp.code != 235 {
|
||||||
|
return Err(SmtpClientError::AuthenticationError {
|
||||||
|
message: format!("AUTH PLAIN failed ({}): {}", resp.code, resp.full_message()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Authenticate using AUTH LOGIN.
|
||||||
|
pub async fn send_auth_login(
|
||||||
|
stream: &mut ClientSmtpStream,
|
||||||
|
user: &str,
|
||||||
|
pass: &str,
|
||||||
|
timeout_secs: u64,
|
||||||
|
) -> Result<(), SmtpClientError> {
|
||||||
|
// Step 1: Send AUTH LOGIN
|
||||||
|
let resp = send_command(stream, "AUTH LOGIN", timeout_secs).await?;
|
||||||
|
if resp.code != 334 {
|
||||||
|
return Err(SmtpClientError::AuthenticationError {
|
||||||
|
message: format!(
|
||||||
|
"AUTH LOGIN challenge failed ({}): {}",
|
||||||
|
resp.code,
|
||||||
|
resp.full_message()
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Send base64 username
|
||||||
|
let user_b64 = BASE64.encode(user.as_bytes());
|
||||||
|
let resp = send_command(stream, &user_b64, timeout_secs).await?;
|
||||||
|
if resp.code != 334 {
|
||||||
|
return Err(SmtpClientError::AuthenticationError {
|
||||||
|
message: format!(
|
||||||
|
"AUTH LOGIN username rejected ({}): {}",
|
||||||
|
resp.code,
|
||||||
|
resp.full_message()
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Send base64 password
|
||||||
|
let pass_b64 = BASE64.encode(pass.as_bytes());
|
||||||
|
let resp = send_command(stream, &pass_b64, timeout_secs).await?;
|
||||||
|
if resp.code != 235 {
|
||||||
|
return Err(SmtpClientError::AuthenticationError {
|
||||||
|
message: format!(
|
||||||
|
"AUTH LOGIN password rejected ({}): {}",
|
||||||
|
resp.code,
|
||||||
|
resp.full_message()
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Authenticate using the configured method.
|
||||||
|
pub async fn authenticate(
|
||||||
|
stream: &mut ClientSmtpStream,
|
||||||
|
auth: &SmtpAuthConfig,
|
||||||
|
_caps: &EhloCapabilities,
|
||||||
|
timeout_secs: u64,
|
||||||
|
) -> Result<(), SmtpClientError> {
|
||||||
|
match auth.method.to_uppercase().as_str() {
|
||||||
|
"LOGIN" => send_auth_login(stream, &auth.user, &auth.pass, timeout_secs).await,
|
||||||
|
_ => send_auth_plain(stream, &auth.user, &auth.pass, timeout_secs).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send MAIL FROM.
|
||||||
|
pub async fn send_mail_from(
|
||||||
|
stream: &mut ClientSmtpStream,
|
||||||
|
sender: &str,
|
||||||
|
timeout_secs: u64,
|
||||||
|
) -> Result<SmtpClientResponse, SmtpClientError> {
|
||||||
|
let resp = send_command(stream, &format!("MAIL FROM:<{sender}>"), timeout_secs).await?;
|
||||||
|
if !resp.is_success() {
|
||||||
|
return Err(resp.to_error());
|
||||||
|
}
|
||||||
|
Ok(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send RCPT TO. Returns per-recipient success/failure.
|
||||||
|
pub async fn send_rcpt_to(
|
||||||
|
stream: &mut ClientSmtpStream,
|
||||||
|
recipient: &str,
|
||||||
|
timeout_secs: u64,
|
||||||
|
) -> Result<SmtpClientResponse, SmtpClientError> {
|
||||||
|
let resp = send_command(stream, &format!("RCPT TO:<{recipient}>"), timeout_secs).await?;
|
||||||
|
// We don't fail the entire send on per-recipient errors;
|
||||||
|
// the caller decides based on the response code.
|
||||||
|
Ok(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send DATA command, followed by the message body with dot-stuffing.
|
||||||
|
pub async fn send_data(
|
||||||
|
stream: &mut ClientSmtpStream,
|
||||||
|
message: &[u8],
|
||||||
|
timeout_secs: u64,
|
||||||
|
) -> Result<SmtpClientResponse, SmtpClientError> {
|
||||||
|
// Send DATA command
|
||||||
|
let resp = send_command(stream, "DATA", timeout_secs).await?;
|
||||||
|
if !resp.is_positive_intermediate() {
|
||||||
|
return Err(resp.to_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the message body with dot-stuffing
|
||||||
|
let stuffed = dot_stuff(message);
|
||||||
|
stream.write_all(&stuffed).await?;
|
||||||
|
|
||||||
|
// Send terminator: CRLF.CRLF
|
||||||
|
// If the message doesn't end with CRLF, add one
|
||||||
|
if !stuffed.ends_with(b"\r\n") {
|
||||||
|
stream.write_all(b"\r\n").await?;
|
||||||
|
}
|
||||||
|
stream.write_all(b".\r\n").await?;
|
||||||
|
stream.flush().await?;
|
||||||
|
|
||||||
|
// Read final response
|
||||||
|
let final_resp = read_response(stream, timeout_secs).await?;
|
||||||
|
if !final_resp.is_success() {
|
||||||
|
return Err(final_resp.to_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(final_resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send RSET command to reset the server state between messages on a reused connection.
|
||||||
|
pub async fn send_rset(
|
||||||
|
stream: &mut ClientSmtpStream,
|
||||||
|
timeout_secs: u64,
|
||||||
|
) -> Result<(), SmtpClientError> {
|
||||||
|
let resp = send_command(stream, "RSET", timeout_secs).await?;
|
||||||
|
if !resp.is_success() {
|
||||||
|
return Err(resp.to_error());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send QUIT command.
|
||||||
|
pub async fn send_quit(
|
||||||
|
stream: &mut ClientSmtpStream,
|
||||||
|
timeout_secs: u64,
|
||||||
|
) -> Result<(), SmtpClientError> {
|
||||||
|
// Best-effort QUIT — ignore errors since we're closing anyway
|
||||||
|
let _ = send_command(stream, "QUIT", timeout_secs).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply SMTP dot-stuffing to a message body.
|
||||||
|
///
|
||||||
|
/// Any line starting with a period gets an extra period prepended.
|
||||||
|
/// Also normalizes bare LF to CRLF.
|
||||||
|
pub fn dot_stuff(data: &[u8]) -> Vec<u8> {
|
||||||
|
let mut result = Vec::with_capacity(data.len() + data.len() / 40);
|
||||||
|
let mut at_line_start = true;
|
||||||
|
|
||||||
|
for i in 0..data.len() {
|
||||||
|
let byte = data[i];
|
||||||
|
|
||||||
|
// Normalize bare LF to CRLF
|
||||||
|
if byte == b'\n' && (i == 0 || data[i - 1] != b'\r') {
|
||||||
|
result.push(b'\r');
|
||||||
|
result.push(b'\n');
|
||||||
|
at_line_start = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dot-stuff: add extra dot at start of line
|
||||||
|
if at_line_start && byte == b'.' {
|
||||||
|
result.push(b'.');
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(byte);
|
||||||
|
at_line_start = byte == b'\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dot_stuffing_basic() {
|
||||||
|
assert_eq!(
|
||||||
|
dot_stuff(b"Hello\r\n.World\r\n"),
|
||||||
|
b"Hello\r\n..World\r\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dot_stuffing_leading_dot() {
|
||||||
|
assert_eq!(dot_stuff(b".starts with dot\r\n"), b"..starts with dot\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dot_stuffing_multiple_dots() {
|
||||||
|
assert_eq!(
|
||||||
|
dot_stuff(b"ok\r\n.line1\r\n..line2\r\n"),
|
||||||
|
b"ok\r\n..line1\r\n...line2\r\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dot_stuffing_bare_lf() {
|
||||||
|
assert_eq!(
|
||||||
|
dot_stuff(b"line1\nline2\n"),
|
||||||
|
b"line1\r\nline2\r\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dot_stuffing_bare_lf_with_dot() {
|
||||||
|
assert_eq!(
|
||||||
|
dot_stuff(b"ok\n.dotline\n"),
|
||||||
|
b"ok\r\n..dotline\r\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dot_stuffing_no_change() {
|
||||||
|
assert_eq!(
|
||||||
|
dot_stuff(b"Hello World\r\nNo dots here\r\n"),
|
||||||
|
b"Hello World\r\nNo dots here\r\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dot_stuffing_empty() {
|
||||||
|
assert_eq!(dot_stuff(b""), b"");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_response_is_success() {
|
||||||
|
let resp = SmtpClientResponse {
|
||||||
|
code: 250,
|
||||||
|
lines: vec!["OK".into()],
|
||||||
|
};
|
||||||
|
assert!(resp.is_success());
|
||||||
|
assert!(!resp.is_temp_error());
|
||||||
|
assert!(!resp.is_perm_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_response_temp_error() {
|
||||||
|
let resp = SmtpClientResponse {
|
||||||
|
code: 450,
|
||||||
|
lines: vec!["Mailbox busy".into()],
|
||||||
|
};
|
||||||
|
assert!(!resp.is_success());
|
||||||
|
assert!(resp.is_temp_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_response_perm_error() {
|
||||||
|
let resp = SmtpClientResponse {
|
||||||
|
code: 550,
|
||||||
|
lines: vec!["No such user".into()],
|
||||||
|
};
|
||||||
|
assert!(!resp.is_success());
|
||||||
|
assert!(resp.is_perm_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_response_positive_intermediate() {
|
||||||
|
let resp = SmtpClientResponse {
|
||||||
|
code: 354,
|
||||||
|
lines: vec!["Start mail input".into()],
|
||||||
|
};
|
||||||
|
assert!(resp.is_positive_intermediate());
|
||||||
|
assert!(!resp.is_success());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_response_full_message() {
|
||||||
|
let resp = SmtpClientResponse {
|
||||||
|
code: 250,
|
||||||
|
lines: vec!["OK".into(), "SIZE 10485760".into()],
|
||||||
|
};
|
||||||
|
assert_eq!(resp.full_message(), "OK SIZE 10485760");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ehlo_capabilities_default() {
|
||||||
|
let caps = EhloCapabilities::default();
|
||||||
|
assert!(!caps.starttls);
|
||||||
|
assert!(!caps.pipelining);
|
||||||
|
assert!(!caps.eight_bit_mime);
|
||||||
|
assert!(caps.auth_methods.is_empty());
|
||||||
|
assert!(caps.max_size.is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
//! - TCP/TLS server (`server`)
|
//! - TCP/TLS server (`server`)
|
||||||
//! - Connection handling (`connection`)
|
//! - Connection handling (`connection`)
|
||||||
|
|
||||||
|
pub mod client;
|
||||||
pub mod command;
|
pub mod command;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod connection;
|
pub mod connection;
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Get version from deno.json
|
|
||||||
VERSION=$(cat deno.json | grep -o '"version": *"[^"]*"' | cut -d'"' -f4)
|
|
||||||
BINARY_DIR="dist/binaries"
|
|
||||||
|
|
||||||
echo "================================================"
|
|
||||||
echo " MAILER Compilation Script"
|
|
||||||
echo " Version: ${VERSION}"
|
|
||||||
echo "================================================"
|
|
||||||
echo ""
|
|
||||||
echo "Compiling for all supported platforms..."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Clean up old binaries and create fresh directory
|
|
||||||
rm -rf "$BINARY_DIR"
|
|
||||||
mkdir -p "$BINARY_DIR"
|
|
||||||
echo "→ Cleaned old binaries from $BINARY_DIR"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Linux x86_64
|
|
||||||
echo "→ Compiling for Linux x86_64..."
|
|
||||||
deno compile --allow-all --no-check --output "$BINARY_DIR/mailer-linux-x64" \
|
|
||||||
--target x86_64-unknown-linux-gnu mod.ts
|
|
||||||
echo " ✓ Linux x86_64 complete"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Linux ARM64
|
|
||||||
echo "→ Compiling for Linux ARM64..."
|
|
||||||
deno compile --allow-all --no-check --output "$BINARY_DIR/mailer-linux-arm64" \
|
|
||||||
--target aarch64-unknown-linux-gnu mod.ts
|
|
||||||
echo " ✓ Linux ARM64 complete"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# macOS x86_64
|
|
||||||
echo "→ Compiling for macOS x86_64..."
|
|
||||||
deno compile --allow-all --no-check --output "$BINARY_DIR/mailer-macos-x64" \
|
|
||||||
--target x86_64-apple-darwin mod.ts
|
|
||||||
echo " ✓ macOS x86_64 complete"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# macOS ARM64
|
|
||||||
echo "→ Compiling for macOS ARM64..."
|
|
||||||
deno compile --allow-all --no-check --output "$BINARY_DIR/mailer-macos-arm64" \
|
|
||||||
--target aarch64-apple-darwin mod.ts
|
|
||||||
echo " ✓ macOS ARM64 complete"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Windows x86_64
|
|
||||||
echo "→ Compiling for Windows x86_64..."
|
|
||||||
deno compile --allow-all --no-check --output "$BINARY_DIR/mailer-windows-x64.exe" \
|
|
||||||
--target x86_64-pc-windows-msvc mod.ts
|
|
||||||
echo " ✓ Windows x86_64 complete"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "================================================"
|
|
||||||
echo " Compilation Summary"
|
|
||||||
echo "================================================"
|
|
||||||
echo ""
|
|
||||||
ls -lh "$BINARY_DIR/" | tail -n +2
|
|
||||||
echo ""
|
|
||||||
echo "✓ All binaries compiled successfully!"
|
|
||||||
echo ""
|
|
||||||
echo "Binary location: $BINARY_DIR/"
|
|
||||||
echo ""
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
import * as plugins from '../../ts/plugins.js';
|
|
||||||
|
|
||||||
export interface ITestServerConfig {
|
|
||||||
port: number;
|
|
||||||
hostname?: string;
|
|
||||||
tlsEnabled?: boolean;
|
|
||||||
authRequired?: boolean;
|
|
||||||
timeout?: number;
|
|
||||||
testCertPath?: string;
|
|
||||||
testKeyPath?: string;
|
|
||||||
maxConnections?: number;
|
|
||||||
size?: number;
|
|
||||||
maxRecipients?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ITestServer {
|
|
||||||
server: any;
|
|
||||||
smtpServer: any;
|
|
||||||
port: number;
|
|
||||||
hostname: string;
|
|
||||||
config: ITestServerConfig;
|
|
||||||
startTime: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts a test SMTP server with the given configuration.
|
|
||||||
*
|
|
||||||
* NOTE: The TS SMTP server implementation was removed in Phase 7B
|
|
||||||
* (replaced by the Rust SMTP server). This stub preserves the interface
|
|
||||||
* for smtpclient tests that import it, but those tests require `node-forge`
|
|
||||||
* which is not installed (pre-existing issue).
|
|
||||||
*/
|
|
||||||
export async function startTestServer(_config: ITestServerConfig): Promise<ITestServer> {
|
|
||||||
throw new Error(
|
|
||||||
'startTestServer is no longer available — the TS SMTP server was removed in Phase 7B. ' +
|
|
||||||
'Use the Rust SMTP server (via UnifiedEmailServer) for integration testing.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops a test SMTP server
|
|
||||||
*/
|
|
||||||
export async function stopTestServer(testServer: ITestServer): Promise<void> {
|
|
||||||
if (!testServer || !testServer.smtpServer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (testServer.smtpServer.close && typeof testServer.smtpServer.close === 'function') {
|
|
||||||
await testServer.smtpServer.close();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error stopping test server:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an available port for testing
|
|
||||||
*/
|
|
||||||
export async function getAvailablePort(startPort: number = 25000): Promise<number> {
|
|
||||||
for (let port = startPort; port < startPort + 1000; port++) {
|
|
||||||
if (await isPortFree(port)) {
|
|
||||||
return port;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new Error(`No available ports found starting from ${startPort}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a port is free
|
|
||||||
*/
|
|
||||||
async function isPortFree(port: number): Promise<boolean> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const server = plugins.net.createServer();
|
|
||||||
|
|
||||||
server.listen(port, () => {
|
|
||||||
server.close(() => resolve(true));
|
|
||||||
});
|
|
||||||
|
|
||||||
server.on('error', () => resolve(false));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create test email data
|
|
||||||
*/
|
|
||||||
export function createTestEmail(options: {
|
|
||||||
from?: string;
|
|
||||||
to?: string | string[];
|
|
||||||
subject?: string;
|
|
||||||
text?: string;
|
|
||||||
html?: string;
|
|
||||||
attachments?: any[];
|
|
||||||
} = {}): any {
|
|
||||||
return {
|
|
||||||
from: options.from || 'test@example.com',
|
|
||||||
to: options.to || 'recipient@example.com',
|
|
||||||
subject: options.subject || 'Test Email',
|
|
||||||
text: options.text || 'This is a test email',
|
|
||||||
html: options.html || '<p>This is a test email</p>',
|
|
||||||
attachments: options.attachments || [],
|
|
||||||
date: new Date(),
|
|
||||||
messageId: `<${Date.now()}@test.example.com>`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple test server for custom protocol testing
|
|
||||||
*/
|
|
||||||
export interface ISimpleTestServer {
|
|
||||||
server: any;
|
|
||||||
hostname: string;
|
|
||||||
port: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createTestServer(options: {
|
|
||||||
onConnection?: (socket: any) => void | Promise<void>;
|
|
||||||
port?: number;
|
|
||||||
hostname?: string;
|
|
||||||
}): Promise<ISimpleTestServer> {
|
|
||||||
const hostname = options.hostname || 'localhost';
|
|
||||||
const port = options.port || await getAvailablePort();
|
|
||||||
|
|
||||||
const server = plugins.net.createServer((socket) => {
|
|
||||||
if (options.onConnection) {
|
|
||||||
const result = options.onConnection(socket);
|
|
||||||
if (result && typeof result.then === 'function') {
|
|
||||||
result.catch(error => {
|
|
||||||
console.error('Error in onConnection handler:', error);
|
|
||||||
socket.destroy();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
server.listen(port, hostname, () => {
|
|
||||||
resolve({
|
|
||||||
server,
|
|
||||||
hostname,
|
|
||||||
port
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
server.on('error', reject);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
import { smtpClientMod } from '../../ts/mail/delivery/index.js';
|
|
||||||
import type { ISmtpClientOptions, SmtpClient } from '../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import { Email } from '../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a test SMTP client
|
|
||||||
*/
|
|
||||||
export function createTestSmtpClient(options: Partial<ISmtpClientOptions> = {}): SmtpClient {
|
|
||||||
const defaultOptions: ISmtpClientOptions = {
|
|
||||||
host: options.host || 'localhost',
|
|
||||||
port: options.port || 2525,
|
|
||||||
secure: options.secure || false,
|
|
||||||
auth: options.auth,
|
|
||||||
connectionTimeout: options.connectionTimeout || 5000,
|
|
||||||
socketTimeout: options.socketTimeout || 5000,
|
|
||||||
maxConnections: options.maxConnections || 5,
|
|
||||||
maxMessages: options.maxMessages || 100,
|
|
||||||
debug: options.debug || false,
|
|
||||||
tls: options.tls || {
|
|
||||||
rejectUnauthorized: false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return smtpClientMod.createSmtpClient(defaultOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send test email using SMTP client
|
|
||||||
*/
|
|
||||||
export async function sendTestEmail(
|
|
||||||
client: SmtpClient,
|
|
||||||
options: {
|
|
||||||
from?: string;
|
|
||||||
to?: string | string[];
|
|
||||||
subject?: string;
|
|
||||||
text?: string;
|
|
||||||
html?: string;
|
|
||||||
} = {}
|
|
||||||
): Promise<any> {
|
|
||||||
const mailOptions = {
|
|
||||||
from: options.from || 'test@example.com',
|
|
||||||
to: options.to || 'recipient@example.com',
|
|
||||||
subject: options.subject || 'Test Email',
|
|
||||||
text: options.text || 'This is a test email',
|
|
||||||
html: options.html
|
|
||||||
};
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: mailOptions.from,
|
|
||||||
to: mailOptions.to,
|
|
||||||
subject: mailOptions.subject,
|
|
||||||
text: mailOptions.text,
|
|
||||||
html: mailOptions.html
|
|
||||||
});
|
|
||||||
return client.sendMail(email);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test SMTP client connection
|
|
||||||
*/
|
|
||||||
export async function testClientConnection(
|
|
||||||
host: string,
|
|
||||||
port: number,
|
|
||||||
timeout: number = 5000
|
|
||||||
): Promise<boolean> {
|
|
||||||
const client = createTestSmtpClient({
|
|
||||||
host,
|
|
||||||
port,
|
|
||||||
connectionTimeout: timeout
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await client.verify();
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
if (client.close) {
|
|
||||||
await client.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create authenticated SMTP client
|
|
||||||
*/
|
|
||||||
export function createAuthenticatedClient(
|
|
||||||
host: string,
|
|
||||||
port: number,
|
|
||||||
username: string,
|
|
||||||
password: string,
|
|
||||||
authMethod: 'PLAIN' | 'LOGIN' = 'PLAIN'
|
|
||||||
): SmtpClient {
|
|
||||||
return createTestSmtpClient({
|
|
||||||
host,
|
|
||||||
port,
|
|
||||||
auth: {
|
|
||||||
user: username,
|
|
||||||
pass: password,
|
|
||||||
method: authMethod
|
|
||||||
},
|
|
||||||
secure: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create TLS-enabled SMTP client
|
|
||||||
*/
|
|
||||||
export function createTlsClient(
|
|
||||||
host: string,
|
|
||||||
port: number,
|
|
||||||
options: {
|
|
||||||
secure?: boolean;
|
|
||||||
rejectUnauthorized?: boolean;
|
|
||||||
} = {}
|
|
||||||
): SmtpClient {
|
|
||||||
return createTestSmtpClient({
|
|
||||||
host,
|
|
||||||
port,
|
|
||||||
secure: options.secure || false,
|
|
||||||
tls: {
|
|
||||||
rejectUnauthorized: options.rejectUnauthorized || false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test client pool status
|
|
||||||
*/
|
|
||||||
export async function testClientPoolStatus(client: SmtpClient): Promise<any> {
|
|
||||||
if (typeof client.getPoolStatus === 'function') {
|
|
||||||
return client.getPoolStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback for clients without pool status
|
|
||||||
return {
|
|
||||||
size: 1,
|
|
||||||
available: 1,
|
|
||||||
pending: 0,
|
|
||||||
connecting: 0,
|
|
||||||
active: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send multiple emails concurrently
|
|
||||||
*/
|
|
||||||
export async function sendConcurrentEmails(
|
|
||||||
client: SmtpClient,
|
|
||||||
count: number,
|
|
||||||
emailOptions: {
|
|
||||||
from?: string;
|
|
||||||
to?: string;
|
|
||||||
subject?: string;
|
|
||||||
text?: string;
|
|
||||||
} = {}
|
|
||||||
): Promise<any[]> {
|
|
||||||
const promises = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
promises.push(
|
|
||||||
sendTestEmail(client, {
|
|
||||||
...emailOptions,
|
|
||||||
subject: `${emailOptions.subject || 'Test Email'} ${i + 1}`
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.all(promises);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Measure client throughput
|
|
||||||
*/
|
|
||||||
export async function measureClientThroughput(
|
|
||||||
client: SmtpClient,
|
|
||||||
duration: number = 10000,
|
|
||||||
emailOptions: {
|
|
||||||
from?: string;
|
|
||||||
to?: string;
|
|
||||||
subject?: string;
|
|
||||||
text?: string;
|
|
||||||
} = {}
|
|
||||||
): Promise<{ totalSent: number; successCount: number; errorCount: number; throughput: number }> {
|
|
||||||
const startTime = Date.now();
|
|
||||||
let totalSent = 0;
|
|
||||||
let successCount = 0;
|
|
||||||
let errorCount = 0;
|
|
||||||
|
|
||||||
while (Date.now() - startTime < duration) {
|
|
||||||
try {
|
|
||||||
await sendTestEmail(client, emailOptions);
|
|
||||||
successCount++;
|
|
||||||
} catch (error) {
|
|
||||||
errorCount++;
|
|
||||||
}
|
|
||||||
totalSent++;
|
|
||||||
}
|
|
||||||
|
|
||||||
const actualDuration = (Date.now() - startTime) / 1000; // in seconds
|
|
||||||
const throughput = totalSent / actualDuration;
|
|
||||||
|
|
||||||
return {
|
|
||||||
totalSent,
|
|
||||||
successCount,
|
|
||||||
errorCount,
|
|
||||||
throughput
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
let smtpClient: SmtpClient;
|
|
||||||
|
|
||||||
tap.test('setup - start SMTP server for command tests', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2540,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(testServer.port).toEqual(2540);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-01: EHLO/HELO - should send EHLO with custom domain', async () => {
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Create SMTP client with custom domain
|
|
||||||
smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
domain: 'mail.example.com', // Custom EHLO domain
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Verify connection (which sends EHLO)
|
|
||||||
const isConnected = await smtpClient.verify();
|
|
||||||
expect(isConnected).toBeTrue();
|
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
console.log(`✅ EHLO command sent with custom domain in ${duration}ms`);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
console.error(`❌ EHLO command failed after ${duration}ms:`, error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-01: EHLO/HELO - should use default domain when not specified', async () => {
|
|
||||||
const defaultClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
// No domain specified - should use default
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const isConnected = await defaultClient.verify();
|
|
||||||
expect(isConnected).toBeTrue();
|
|
||||||
|
|
||||||
await defaultClient.close();
|
|
||||||
console.log('✅ EHLO sent with default domain');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-01: EHLO/HELO - should handle international domains', async () => {
|
|
||||||
const intlClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
domain: 'mail.例え.jp', // International domain
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const isConnected = await intlClient.verify();
|
|
||||||
expect(isConnected).toBeTrue();
|
|
||||||
|
|
||||||
await intlClient.close();
|
|
||||||
console.log('✅ EHLO sent with international domain');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-01: EHLO/HELO - should fall back to HELO if needed', async () => {
|
|
||||||
// Most modern servers support EHLO, but client should handle HELO fallback
|
|
||||||
const heloClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
domain: 'legacy.example.com',
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// The client should handle EHLO/HELO automatically
|
|
||||||
const isConnected = await heloClient.verify();
|
|
||||||
expect(isConnected).toBeTrue();
|
|
||||||
|
|
||||||
await heloClient.close();
|
|
||||||
console.log('✅ EHLO/HELO fallback mechanism working');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-01: EHLO/HELO - should parse server capabilities', async () => {
|
|
||||||
const capClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
pool: true, // Enable pooling to maintain connections
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// verify() creates a temporary connection and closes it
|
|
||||||
const verifyResult = await capClient.verify();
|
|
||||||
expect(verifyResult).toBeTrue();
|
|
||||||
|
|
||||||
// After verify(), the pool might be empty since verify() closes its connection
|
|
||||||
// Instead, let's send an actual email to test capabilities
|
|
||||||
const poolStatus = capClient.getPoolStatus();
|
|
||||||
|
|
||||||
// Pool starts empty
|
|
||||||
expect(poolStatus.total).toEqual(0);
|
|
||||||
|
|
||||||
await capClient.close();
|
|
||||||
console.log('✅ Server capabilities parsed from EHLO response');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-01: EHLO/HELO - should handle very long domain names', async () => {
|
|
||||||
const longDomain = 'very-long-subdomain.with-many-parts.and-labels.example.com';
|
|
||||||
|
|
||||||
const longDomainClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
domain: longDomain,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const isConnected = await longDomainClient.verify();
|
|
||||||
expect(isConnected).toBeTrue();
|
|
||||||
|
|
||||||
await longDomainClient.close();
|
|
||||||
console.log('✅ Long domain name handled correctly');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-01: EHLO/HELO - should reconnect with EHLO after disconnect', async () => {
|
|
||||||
// First connection - verify() creates and closes its own connection
|
|
||||||
const firstVerify = await smtpClient.verify();
|
|
||||||
expect(firstVerify).toBeTrue();
|
|
||||||
|
|
||||||
// After verify(), no connections should be in the pool
|
|
||||||
expect(smtpClient.isConnected()).toBeFalse();
|
|
||||||
|
|
||||||
// Second verify - should send EHLO again
|
|
||||||
const secondVerify = await smtpClient.verify();
|
|
||||||
expect(secondVerify).toBeTrue();
|
|
||||||
|
|
||||||
console.log('✅ EHLO sent correctly on reconnection');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - close SMTP client', async () => {
|
|
||||||
if (smtpClient && smtpClient.isConnected()) {
|
|
||||||
await smtpClient.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - stop SMTP server', async () => {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
let smtpClient: SmtpClient;
|
|
||||||
|
|
||||||
tap.test('setup - start SMTP server for MAIL FROM tests', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2541,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false,
|
|
||||||
size: 10 * 1024 * 1024 // 10MB size limit
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(testServer.port).toEqual(2541);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('setup - create SMTP client', async () => {
|
|
||||||
smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const isConnected = await smtpClient.verify();
|
|
||||||
expect(isConnected).toBeTrue();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-02: MAIL FROM - should send basic MAIL FROM command', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Basic MAIL FROM Test',
|
|
||||||
text: 'Testing basic MAIL FROM command'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
expect(result.envelope?.from).toEqual('sender@example.com');
|
|
||||||
|
|
||||||
console.log('✅ Basic MAIL FROM command sent successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-02: MAIL FROM - should handle display names correctly', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'John Doe <john.doe@example.com>',
|
|
||||||
to: 'Jane Smith <jane.smith@example.com>',
|
|
||||||
subject: 'Display Name Test',
|
|
||||||
text: 'Testing MAIL FROM with display names'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
// Envelope should contain only email address, not display name
|
|
||||||
expect(result.envelope?.from).toEqual('john.doe@example.com');
|
|
||||||
|
|
||||||
console.log('✅ Display names handled correctly in MAIL FROM');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-02: MAIL FROM - should handle SIZE parameter if server supports it', async () => {
|
|
||||||
// Send a larger email to test SIZE parameter
|
|
||||||
const largeContent = 'x'.repeat(1000000); // 1MB of content
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'SIZE Parameter Test',
|
|
||||||
text: largeContent
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ SIZE parameter handled for large email');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-02: MAIL FROM - should handle international email addresses', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'user@例え.jp',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'International Domain Test',
|
|
||||||
text: 'Testing international domains in MAIL FROM'
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
console.log('✅ International domain accepted');
|
|
||||||
expect(result.envelope?.from).toContain('@');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Some servers may not support international domains
|
|
||||||
console.log('ℹ️ Server does not support international domains');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-02: MAIL FROM - should handle empty return path (bounce address)', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: '<>', // Empty return path for bounces
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Bounce Message Test',
|
|
||||||
text: 'This is a bounce message with empty return path'
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
console.log('✅ Empty return path accepted for bounce');
|
|
||||||
expect(result.envelope?.from).toEqual('');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('ℹ️ Server rejected empty return path');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-02: MAIL FROM - should handle special characters in local part', async () => {
|
|
||||||
const specialEmails = [
|
|
||||||
'user+tag@example.com',
|
|
||||||
'first.last@example.com',
|
|
||||||
'user_name@example.com',
|
|
||||||
'user-name@example.com'
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const fromEmail of specialEmails) {
|
|
||||||
const email = new Email({
|
|
||||||
from: fromEmail,
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Special Character Test',
|
|
||||||
text: `Testing special characters in: ${fromEmail}`
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
expect(result.envelope?.from).toEqual(fromEmail);
|
|
||||||
|
|
||||||
console.log(`✅ Special character email accepted: ${fromEmail}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-02: MAIL FROM - should reject invalid sender addresses', async () => {
|
|
||||||
const invalidSenders = [
|
|
||||||
'no-at-sign',
|
|
||||||
'@example.com',
|
|
||||||
'user@',
|
|
||||||
'user@@example.com',
|
|
||||||
'user@.com',
|
|
||||||
'user@example.',
|
|
||||||
'user with spaces@example.com'
|
|
||||||
];
|
|
||||||
|
|
||||||
let rejectedCount = 0;
|
|
||||||
|
|
||||||
for (const invalidSender of invalidSenders) {
|
|
||||||
try {
|
|
||||||
const email = new Email({
|
|
||||||
from: invalidSender,
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Invalid Sender Test',
|
|
||||||
text: 'This should fail'
|
|
||||||
});
|
|
||||||
|
|
||||||
await smtpClient.sendMail(email);
|
|
||||||
} catch (error) {
|
|
||||||
rejectedCount++;
|
|
||||||
console.log(`✅ Invalid sender rejected: ${invalidSender}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(rejectedCount).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-02: MAIL FROM - should handle 8BITMIME parameter', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'UTF-8 Test – with special characters',
|
|
||||||
text: 'This email contains UTF-8 characters: 你好世界 🌍',
|
|
||||||
html: '<p>UTF-8 content: <strong>你好世界</strong> 🌍</p>'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ 8BITMIME content handled correctly');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-02: MAIL FROM - should handle AUTH parameter if authenticated', async () => {
|
|
||||||
// Create authenticated client - auth requires TLS per RFC 8314
|
|
||||||
const authServer = await startTestServer({
|
|
||||||
port: 2542,
|
|
||||||
tlsEnabled: true,
|
|
||||||
authRequired: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const authClient = createSmtpClient({
|
|
||||||
host: authServer.hostname,
|
|
||||||
port: authServer.port,
|
|
||||||
secure: false, // Use STARTTLS instead of direct TLS
|
|
||||||
requireTLS: true, // Require TLS upgrade
|
|
||||||
tls: {
|
|
||||||
rejectUnauthorized: false // Accept self-signed cert for testing
|
|
||||||
},
|
|
||||||
auth: {
|
|
||||||
user: 'testuser',
|
|
||||||
pass: 'testpass'
|
|
||||||
},
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'authenticated@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'AUTH Parameter Test',
|
|
||||||
text: 'Sent with authentication'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await authClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ AUTH parameter handled in MAIL FROM');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('AUTH test error:', error);
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
await authClient.close();
|
|
||||||
await stopTestServer(authServer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-02: MAIL FROM - should handle very long email addresses', async () => {
|
|
||||||
// RFC allows up to 320 characters total (64 + @ + 255)
|
|
||||||
const longLocal = 'a'.repeat(64);
|
|
||||||
const longDomain = 'subdomain.' + 'a'.repeat(60) + '.example.com';
|
|
||||||
const longEmail = `${longLocal}@${longDomain}`;
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: longEmail,
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Long Email Address Test',
|
|
||||||
text: 'Testing maximum length email addresses'
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
console.log('✅ Long email address accepted');
|
|
||||||
expect(result.envelope?.from).toEqual(longEmail);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('ℹ️ Server enforces email length limits');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - close SMTP client', async () => {
|
|
||||||
if (smtpClient && smtpClient.isConnected()) {
|
|
||||||
await smtpClient.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - stop SMTP server', async () => {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,283 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
let smtpClient: SmtpClient;
|
|
||||||
|
|
||||||
tap.test('setup - start SMTP server for RCPT TO tests', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2543,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false,
|
|
||||||
maxRecipients: 10 // Set recipient limit
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(testServer.port).toEqual(2543);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('setup - create SMTP client', async () => {
|
|
||||||
smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const isConnected = await smtpClient.verify();
|
|
||||||
expect(isConnected).toBeTrue();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-03: RCPT TO - should send to single recipient', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'single@example.com',
|
|
||||||
subject: 'Single Recipient Test',
|
|
||||||
text: 'Testing single RCPT TO command'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
expect(result.acceptedRecipients).toContain('single@example.com');
|
|
||||||
expect(result.acceptedRecipients.length).toEqual(1);
|
|
||||||
expect(result.envelope?.to).toContain('single@example.com');
|
|
||||||
|
|
||||||
console.log('✅ Single RCPT TO command successful');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-03: RCPT TO - should send to multiple TO recipients', async () => {
|
|
||||||
const recipients = [
|
|
||||||
'recipient1@example.com',
|
|
||||||
'recipient2@example.com',
|
|
||||||
'recipient3@example.com'
|
|
||||||
];
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: recipients,
|
|
||||||
subject: 'Multiple Recipients Test',
|
|
||||||
text: 'Testing multiple RCPT TO commands'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
expect(result.acceptedRecipients.length).toEqual(3);
|
|
||||||
recipients.forEach(recipient => {
|
|
||||||
expect(result.acceptedRecipients).toContain(recipient);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`✅ Sent to ${result.acceptedRecipients.length} recipients`);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-03: RCPT TO - should handle CC recipients', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'primary@example.com',
|
|
||||||
cc: ['cc1@example.com', 'cc2@example.com'],
|
|
||||||
subject: 'CC Recipients Test',
|
|
||||||
text: 'Testing RCPT TO with CC recipients'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
expect(result.acceptedRecipients.length).toEqual(3);
|
|
||||||
expect(result.acceptedRecipients).toContain('primary@example.com');
|
|
||||||
expect(result.acceptedRecipients).toContain('cc1@example.com');
|
|
||||||
expect(result.acceptedRecipients).toContain('cc2@example.com');
|
|
||||||
|
|
||||||
console.log('✅ CC recipients handled correctly');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-03: RCPT TO - should handle BCC recipients', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'visible@example.com',
|
|
||||||
bcc: ['hidden1@example.com', 'hidden2@example.com'],
|
|
||||||
subject: 'BCC Recipients Test',
|
|
||||||
text: 'Testing RCPT TO with BCC recipients'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
expect(result.acceptedRecipients.length).toEqual(3);
|
|
||||||
expect(result.acceptedRecipients).toContain('visible@example.com');
|
|
||||||
expect(result.acceptedRecipients).toContain('hidden1@example.com');
|
|
||||||
expect(result.acceptedRecipients).toContain('hidden2@example.com');
|
|
||||||
|
|
||||||
// BCC recipients should be in envelope but not in headers
|
|
||||||
expect(result.envelope?.to.length).toEqual(3);
|
|
||||||
|
|
||||||
console.log('✅ BCC recipients handled correctly');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-03: RCPT TO - should handle mixed TO, CC, and BCC', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['to1@example.com', 'to2@example.com'],
|
|
||||||
cc: ['cc1@example.com', 'cc2@example.com'],
|
|
||||||
bcc: ['bcc1@example.com', 'bcc2@example.com'],
|
|
||||||
subject: 'Mixed Recipients Test',
|
|
||||||
text: 'Testing all recipient types together'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
expect(result.acceptedRecipients.length).toEqual(6);
|
|
||||||
|
|
||||||
console.log('✅ Mixed recipient types handled correctly');
|
|
||||||
console.log(` TO: 2, CC: 2, BCC: 2 = Total: ${result.acceptedRecipients.length}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-03: RCPT TO - should handle recipient limit', async () => {
|
|
||||||
// Create more recipients than server allows
|
|
||||||
const manyRecipients = [];
|
|
||||||
for (let i = 0; i < 15; i++) {
|
|
||||||
manyRecipients.push(`recipient${i}@example.com`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: manyRecipients,
|
|
||||||
subject: 'Recipient Limit Test',
|
|
||||||
text: 'Testing server recipient limits'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
// Server should accept up to its limit
|
|
||||||
if (result.rejectedRecipients.length > 0) {
|
|
||||||
console.log(`✅ Server enforced recipient limit:`);
|
|
||||||
console.log(` Accepted: ${result.acceptedRecipients.length}`);
|
|
||||||
console.log(` Rejected: ${result.rejectedRecipients.length}`);
|
|
||||||
|
|
||||||
expect(result.acceptedRecipients.length).toBeLessThanOrEqual(10);
|
|
||||||
} else {
|
|
||||||
// Server accepted all
|
|
||||||
expect(result.acceptedRecipients.length).toEqual(15);
|
|
||||||
console.log('ℹ️ Server accepted all recipients');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-03: RCPT TO - should handle invalid recipients gracefully', async () => {
|
|
||||||
const mixedRecipients = [
|
|
||||||
'valid1@example.com',
|
|
||||||
'invalid@address@with@multiple@ats.com',
|
|
||||||
'valid2@example.com',
|
|
||||||
'no-domain@',
|
|
||||||
'valid3@example.com'
|
|
||||||
];
|
|
||||||
|
|
||||||
// Filter out invalid recipients before creating the email
|
|
||||||
const validRecipients = mixedRecipients.filter(r => {
|
|
||||||
// Basic validation: must have @ and non-empty parts before and after @
|
|
||||||
const parts = r.split('@');
|
|
||||||
return parts.length === 2 && parts[0].length > 0 && parts[1].length > 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: validRecipients,
|
|
||||||
subject: 'Mixed Valid/Invalid Recipients',
|
|
||||||
text: 'Testing partial recipient acceptance'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
expect(result.acceptedRecipients).toContain('valid1@example.com');
|
|
||||||
expect(result.acceptedRecipients).toContain('valid2@example.com');
|
|
||||||
expect(result.acceptedRecipients).toContain('valid3@example.com');
|
|
||||||
|
|
||||||
console.log('✅ Valid recipients accepted, invalid filtered');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-03: RCPT TO - should handle duplicate recipients', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['user@example.com', 'user@example.com'],
|
|
||||||
cc: ['user@example.com'],
|
|
||||||
bcc: ['user@example.com'],
|
|
||||||
subject: 'Duplicate Recipients Test',
|
|
||||||
text: 'Testing duplicate recipient handling'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
|
|
||||||
// Check if duplicates were removed
|
|
||||||
const uniqueAccepted = [...new Set(result.acceptedRecipients)];
|
|
||||||
console.log(`✅ Duplicate handling: ${result.acceptedRecipients.length} total, ${uniqueAccepted.length} unique`);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-03: RCPT TO - should handle special characters in recipient addresses', async () => {
|
|
||||||
const specialRecipients = [
|
|
||||||
'user+tag@example.com',
|
|
||||||
'first.last@example.com',
|
|
||||||
'user_name@example.com',
|
|
||||||
'user-name@example.com',
|
|
||||||
'"quoted.user"@example.com'
|
|
||||||
];
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: specialRecipients.filter(r => !r.includes('"')), // Skip quoted for Email class
|
|
||||||
subject: 'Special Characters Test',
|
|
||||||
text: 'Testing special characters in recipient addresses'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
expect(result.acceptedRecipients.length).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
console.log(`✅ Special character recipients accepted: ${result.acceptedRecipients.length}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-03: RCPT TO - should maintain recipient order', async () => {
|
|
||||||
const orderedRecipients = [
|
|
||||||
'first@example.com',
|
|
||||||
'second@example.com',
|
|
||||||
'third@example.com',
|
|
||||||
'fourth@example.com'
|
|
||||||
];
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: orderedRecipients,
|
|
||||||
subject: 'Recipient Order Test',
|
|
||||||
text: 'Testing if recipient order is maintained'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
expect(result.envelope?.to.length).toEqual(orderedRecipients.length);
|
|
||||||
|
|
||||||
// Check order preservation
|
|
||||||
orderedRecipients.forEach((recipient, index) => {
|
|
||||||
expect(result.envelope?.to[index]).toEqual(recipient);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('✅ Recipient order maintained in envelope');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - close SMTP client', async () => {
|
|
||||||
if (smtpClient && smtpClient.isConnected()) {
|
|
||||||
await smtpClient.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - stop SMTP server', async () => {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,274 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
let smtpClient: SmtpClient;
|
|
||||||
|
|
||||||
tap.test('setup - start SMTP server for DATA command tests', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2544,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false,
|
|
||||||
size: 10 * 1024 * 1024 // 10MB message size limit
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(testServer.port).toEqual(2544);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('setup - create SMTP client', async () => {
|
|
||||||
smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
socketTimeout: 30000, // Longer timeout for data transmission
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const isConnected = await smtpClient.verify();
|
|
||||||
expect(isConnected).toBeTrue();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-04: DATA - should transmit simple text email', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Simple DATA Test',
|
|
||||||
text: 'This is a simple text email transmitted via DATA command.'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
expect(result.response).toBeTypeofString();
|
|
||||||
|
|
||||||
console.log('✅ Simple text email transmitted successfully');
|
|
||||||
console.log('📧 Server response:', result.response);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-04: DATA - should handle dot stuffing', async () => {
|
|
||||||
// Lines starting with dots should be escaped
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Dot Stuffing Test',
|
|
||||||
text: 'This email tests dot stuffing:\n.This line starts with a dot\n..So does this one\n...And this one'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Dot stuffing handled correctly');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-04: DATA - should transmit HTML email', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'HTML Email Test',
|
|
||||||
text: 'This is the plain text version',
|
|
||||||
html: `
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>HTML Email Test</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>HTML Email</h1>
|
|
||||||
<p>This is an <strong>HTML</strong> email with:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Lists</li>
|
|
||||||
<li>Formatting</li>
|
|
||||||
<li>Links: <a href="https://example.com">Example</a></li>
|
|
||||||
</ul>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ HTML email transmitted successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-04: DATA - should handle large message body', async () => {
|
|
||||||
// Create a large message (1MB)
|
|
||||||
const largeText = 'This is a test line that will be repeated many times.\n'.repeat(20000);
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Large Message Test',
|
|
||||||
text: largeText
|
|
||||||
});
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log(`✅ Large message (${Math.round(largeText.length / 1024)}KB) transmitted in ${duration}ms`);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-04: DATA - should handle binary attachments', async () => {
|
|
||||||
// Create a binary attachment
|
|
||||||
const binaryData = Buffer.alloc(1024);
|
|
||||||
for (let i = 0; i < binaryData.length; i++) {
|
|
||||||
binaryData[i] = i % 256;
|
|
||||||
}
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Binary Attachment Test',
|
|
||||||
text: 'This email contains a binary attachment',
|
|
||||||
attachments: [{
|
|
||||||
filename: 'test.bin',
|
|
||||||
content: binaryData,
|
|
||||||
contentType: 'application/octet-stream'
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Binary attachment transmitted successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-04: DATA - should handle special characters and encoding', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Special Characters Test – "Quotes" & More',
|
|
||||||
text: 'Special characters: © ® ™ € £ ¥ • … « » " " \' \'',
|
|
||||||
html: '<p>Unicode: 你好世界 🌍 🚀 ✉️</p>'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Special characters and Unicode handled correctly');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-04: DATA - should handle line length limits', async () => {
|
|
||||||
// RFC 5321 specifies 1000 character line limit (including CRLF)
|
|
||||||
const longLine = 'a'.repeat(990); // Leave room for CRLF and safety
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Long Line Test',
|
|
||||||
text: `Short line\n${longLine}\nAnother short line`
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Long lines handled within RFC limits');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-04: DATA - should handle empty message body', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Empty Body Test',
|
|
||||||
text: '' // Empty body
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Empty message body handled correctly');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-04: DATA - should handle CRLF line endings', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'CRLF Test',
|
|
||||||
text: 'Line 1\r\nLine 2\r\nLine 3\nLine 4 (LF only)\r\nLine 5'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Mixed line endings normalized to CRLF');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-04: DATA - should handle message headers correctly', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
cc: 'cc@example.com',
|
|
||||||
subject: 'Header Test',
|
|
||||||
text: 'Testing header transmission',
|
|
||||||
priority: 'high',
|
|
||||||
headers: {
|
|
||||||
'X-Custom-Header': 'custom-value',
|
|
||||||
'X-Mailer': 'SMTP Client Test Suite',
|
|
||||||
'Reply-To': 'replies@example.com'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ All headers transmitted in DATA command');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-04: DATA - should handle timeout for slow transmission', async () => {
|
|
||||||
// Create a very large message to test timeout handling
|
|
||||||
const hugeText = 'x'.repeat(5 * 1024 * 1024); // 5MB
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Timeout Test',
|
|
||||||
text: hugeText
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should complete within socket timeout
|
|
||||||
const startTime = Date.now();
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
expect(duration).toBeLessThan(30000); // Should complete within socket timeout
|
|
||||||
|
|
||||||
console.log(`✅ Large data transmission completed in ${duration}ms`);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-04: DATA - should handle server rejection after DATA', async () => {
|
|
||||||
// Some servers might reject after seeing content
|
|
||||||
const email = new Email({
|
|
||||||
from: 'spam@spammer.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Potential Spam Test',
|
|
||||||
text: 'BUY NOW! SPECIAL OFFER! CLICK HERE!',
|
|
||||||
mightBeSpam: true // Flag as potential spam
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
// Test server might accept or reject
|
|
||||||
if (result.success) {
|
|
||||||
console.log('ℹ️ Test server accepted potential spam (normal for test)');
|
|
||||||
} else {
|
|
||||||
console.log('✅ Server can reject messages after DATA inspection');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - close SMTP client', async () => {
|
|
||||||
if (smtpClient && smtpClient.isConnected()) {
|
|
||||||
await smtpClient.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - stop SMTP server', async () => {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,306 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let authServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup - start SMTP server with authentication', async () => {
|
|
||||||
authServer = await startTestServer({
|
|
||||||
port: 2580,
|
|
||||||
tlsEnabled: true, // Enable STARTTLS capability
|
|
||||||
authRequired: true
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(authServer.port).toEqual(2580);
|
|
||||||
expect(authServer.config.authRequired).toBeTrue();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-05: AUTH - should fail without credentials', async () => {
|
|
||||||
const noAuthClient = createSmtpClient({
|
|
||||||
host: authServer.hostname,
|
|
||||||
port: authServer.port,
|
|
||||||
secure: false, // Start plain, upgrade with STARTTLS
|
|
||||||
tls: {
|
|
||||||
rejectUnauthorized: false // Accept self-signed certs for testing
|
|
||||||
},
|
|
||||||
connectionTimeout: 5000
|
|
||||||
// No auth provided
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'No Auth Test',
|
|
||||||
text: 'Should fail without authentication'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await noAuthClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeFalse();
|
|
||||||
expect(result.error).toBeInstanceOf(Error);
|
|
||||||
expect(result.error?.message).toContain('Authentication required');
|
|
||||||
console.log('✅ Authentication required error:', result.error?.message);
|
|
||||||
|
|
||||||
await noAuthClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-05: AUTH - should authenticate with PLAIN mechanism', async () => {
|
|
||||||
const plainAuthClient = createSmtpClient({
|
|
||||||
host: authServer.hostname,
|
|
||||||
port: authServer.port,
|
|
||||||
secure: false, // Start plain, upgrade with STARTTLS
|
|
||||||
tls: {
|
|
||||||
rejectUnauthorized: false // Accept self-signed certs for testing
|
|
||||||
},
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
auth: {
|
|
||||||
user: 'testuser',
|
|
||||||
pass: 'testpass',
|
|
||||||
method: 'PLAIN'
|
|
||||||
},
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const isConnected = await plainAuthClient.verify();
|
|
||||||
expect(isConnected).toBeTrue();
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'PLAIN Auth Test',
|
|
||||||
text: 'Sent with PLAIN authentication'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await plainAuthClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
|
|
||||||
await plainAuthClient.close();
|
|
||||||
console.log('✅ PLAIN authentication successful');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-05: AUTH - should authenticate with LOGIN mechanism', async () => {
|
|
||||||
const loginAuthClient = createSmtpClient({
|
|
||||||
host: authServer.hostname,
|
|
||||||
port: authServer.port,
|
|
||||||
secure: false, // Start plain, upgrade with STARTTLS
|
|
||||||
tls: {
|
|
||||||
rejectUnauthorized: false // Accept self-signed certs for testing
|
|
||||||
},
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
auth: {
|
|
||||||
user: 'testuser',
|
|
||||||
pass: 'testpass',
|
|
||||||
method: 'LOGIN'
|
|
||||||
},
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const isConnected = await loginAuthClient.verify();
|
|
||||||
expect(isConnected).toBeTrue();
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'LOGIN Auth Test',
|
|
||||||
text: 'Sent with LOGIN authentication'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await loginAuthClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
|
|
||||||
await loginAuthClient.close();
|
|
||||||
console.log('✅ LOGIN authentication successful');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-05: AUTH - should auto-select authentication method', async () => {
|
|
||||||
const autoAuthClient = createSmtpClient({
|
|
||||||
host: authServer.hostname,
|
|
||||||
port: authServer.port,
|
|
||||||
secure: false, // Start plain, upgrade with STARTTLS
|
|
||||||
tls: {
|
|
||||||
rejectUnauthorized: false // Accept self-signed certs for testing
|
|
||||||
},
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
auth: {
|
|
||||||
user: 'testuser',
|
|
||||||
pass: 'testpass'
|
|
||||||
// No method specified - should auto-select
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const isConnected = await autoAuthClient.verify();
|
|
||||||
expect(isConnected).toBeTrue();
|
|
||||||
|
|
||||||
await autoAuthClient.close();
|
|
||||||
console.log('✅ Auto-selected authentication method');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-05: AUTH - should handle invalid credentials', async () => {
|
|
||||||
const badAuthClient = createSmtpClient({
|
|
||||||
host: authServer.hostname,
|
|
||||||
port: authServer.port,
|
|
||||||
secure: false, // Start plain, upgrade with STARTTLS
|
|
||||||
tls: {
|
|
||||||
rejectUnauthorized: false // Accept self-signed certs for testing
|
|
||||||
},
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
auth: {
|
|
||||||
user: 'wronguser',
|
|
||||||
pass: 'wrongpass'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const isConnected = await badAuthClient.verify();
|
|
||||||
expect(isConnected).toBeFalse();
|
|
||||||
console.log('✅ Invalid credentials rejected');
|
|
||||||
|
|
||||||
await badAuthClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-05: AUTH - should handle special characters in credentials', async () => {
|
|
||||||
const specialAuthClient = createSmtpClient({
|
|
||||||
host: authServer.hostname,
|
|
||||||
port: authServer.port,
|
|
||||||
secure: false, // Start plain, upgrade with STARTTLS
|
|
||||||
tls: {
|
|
||||||
rejectUnauthorized: false // Accept self-signed certs for testing
|
|
||||||
},
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
auth: {
|
|
||||||
user: 'user@domain.com',
|
|
||||||
pass: 'p@ssw0rd!#$%'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Server might accept or reject based on implementation
|
|
||||||
try {
|
|
||||||
await specialAuthClient.verify();
|
|
||||||
await specialAuthClient.close();
|
|
||||||
console.log('✅ Special characters in credentials handled');
|
|
||||||
} catch (error) {
|
|
||||||
console.log('ℹ️ Test server rejected special character credentials');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-05: AUTH - should prefer secure auth over TLS', async () => {
|
|
||||||
// Start TLS-enabled server
|
|
||||||
const tlsAuthServer = await startTestServer({
|
|
||||||
port: 2581,
|
|
||||||
tlsEnabled: true,
|
|
||||||
authRequired: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const tlsAuthClient = createSmtpClient({
|
|
||||||
host: tlsAuthServer.hostname,
|
|
||||||
port: tlsAuthServer.port,
|
|
||||||
secure: false, // Use STARTTLS
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
auth: {
|
|
||||||
user: 'testuser',
|
|
||||||
pass: 'testpass'
|
|
||||||
},
|
|
||||||
tls: {
|
|
||||||
rejectUnauthorized: false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const isConnected = await tlsAuthClient.verify();
|
|
||||||
expect(isConnected).toBeTrue();
|
|
||||||
|
|
||||||
await tlsAuthClient.close();
|
|
||||||
await stopTestServer(tlsAuthServer);
|
|
||||||
console.log('✅ Secure authentication over TLS');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-05: AUTH - should maintain auth state across multiple sends', async () => {
|
|
||||||
const persistentAuthClient = createSmtpClient({
|
|
||||||
host: authServer.hostname,
|
|
||||||
port: authServer.port,
|
|
||||||
secure: false, // Start plain, upgrade with STARTTLS
|
|
||||||
tls: {
|
|
||||||
rejectUnauthorized: false // Accept self-signed certs for testing
|
|
||||||
},
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
auth: {
|
|
||||||
user: 'testuser',
|
|
||||||
pass: 'testpass'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await persistentAuthClient.verify();
|
|
||||||
|
|
||||||
// Send multiple emails without re-authenticating
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: `Persistent Auth Test ${i + 1}`,
|
|
||||||
text: `Email ${i + 1} using same auth session`
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await persistentAuthClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
await persistentAuthClient.close();
|
|
||||||
console.log('✅ Authentication state maintained across sends');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-05: AUTH - should handle auth with connection pooling', async () => {
|
|
||||||
const pooledAuthClient = createSmtpClient({
|
|
||||||
host: authServer.hostname,
|
|
||||||
port: authServer.port,
|
|
||||||
secure: false, // Start plain, upgrade with STARTTLS
|
|
||||||
tls: {
|
|
||||||
rejectUnauthorized: false // Accept self-signed certs for testing
|
|
||||||
},
|
|
||||||
pool: true,
|
|
||||||
maxConnections: 3,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
auth: {
|
|
||||||
user: 'testuser',
|
|
||||||
pass: 'testpass'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send concurrent emails with pooled authenticated connections
|
|
||||||
const promises = [];
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: `recipient${i}@example.com`,
|
|
||||||
subject: `Pooled Auth Test ${i}`,
|
|
||||||
text: 'Testing auth with connection pooling'
|
|
||||||
});
|
|
||||||
promises.push(pooledAuthClient.sendMail(email));
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = await Promise.all(promises);
|
|
||||||
|
|
||||||
// Debug output to understand failures
|
|
||||||
results.forEach((result, index) => {
|
|
||||||
if (!result.success) {
|
|
||||||
console.log(`❌ Email ${index} failed:`, result.error?.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const successCount = results.filter(r => r.success).length;
|
|
||||||
console.log(`📧 Sent ${successCount} of ${results.length} emails successfully`);
|
|
||||||
|
|
||||||
const poolStatus = pooledAuthClient.getPoolStatus();
|
|
||||||
console.log('📊 Auth pool status:', poolStatus);
|
|
||||||
|
|
||||||
// Check that at least one email was sent (connection pooling might limit concurrent sends)
|
|
||||||
expect(successCount).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
await pooledAuthClient.close();
|
|
||||||
console.log('✅ Authentication works with connection pooling');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - stop auth server', async () => {
|
|
||||||
await stopTestServer(authServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup test SMTP server', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2546,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
expect(testServer).toBeTruthy();
|
|
||||||
expect(testServer.port).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-06: Check PIPELINING capability', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// The SmtpClient handles pipelining internally
|
|
||||||
// We can verify the server supports it by checking a successful send
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Pipelining Test',
|
|
||||||
text: 'Testing pipelining support'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
|
|
||||||
// Server logs show PIPELINING is advertised
|
|
||||||
console.log('✅ Server supports PIPELINING (advertised in EHLO response)');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-06: Basic command pipelining', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send email with multiple recipients to test pipelining
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient1@example.com', 'recipient2@example.com'],
|
|
||||||
subject: 'Multi-recipient Test',
|
|
||||||
text: 'Testing pipelining with multiple recipients'
|
|
||||||
});
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
const elapsed = Date.now() - startTime;
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
expect(result.acceptedRecipients.length).toEqual(2);
|
|
||||||
|
|
||||||
console.log(`✅ Sent to ${result.acceptedRecipients.length} recipients in ${elapsed}ms`);
|
|
||||||
console.log('Pipelining improves performance by sending multiple commands without waiting');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-06: Pipelining with DATA command', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send a normal email - pipelining is handled internally
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'DATA Command Test',
|
|
||||||
text: 'Testing pipelining up to DATA command'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
|
|
||||||
console.log('✅ Commands pipelined up to DATA successfully');
|
|
||||||
console.log('DATA command requires synchronous handling as per RFC');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-06: Pipelining error handling', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send email with mix of valid and potentially problematic recipients
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: [
|
|
||||||
'valid1@example.com',
|
|
||||||
'valid2@example.com',
|
|
||||||
'valid3@example.com'
|
|
||||||
],
|
|
||||||
subject: 'Error Handling Test',
|
|
||||||
text: 'Testing pipelining error handling'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
|
|
||||||
console.log(`✅ Handled ${result.acceptedRecipients.length} recipients`);
|
|
||||||
console.log('Pipelining handles errors gracefully');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-06: Pipelining performance comparison', async () => {
|
|
||||||
// Create two clients - both use pipelining by default when available
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test with multiple recipients
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: [
|
|
||||||
'recipient1@example.com',
|
|
||||||
'recipient2@example.com',
|
|
||||||
'recipient3@example.com',
|
|
||||||
'recipient4@example.com',
|
|
||||||
'recipient5@example.com'
|
|
||||||
],
|
|
||||||
subject: 'Performance Test',
|
|
||||||
text: 'Testing performance with multiple recipients'
|
|
||||||
});
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
const elapsed = Date.now() - startTime;
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
expect(result.acceptedRecipients.length).toEqual(5);
|
|
||||||
|
|
||||||
console.log(`✅ Sent to ${result.acceptedRecipients.length} recipients in ${elapsed}ms`);
|
|
||||||
console.log('Pipelining provides significant performance improvements');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-06: Pipelining with multiple recipients', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send to many recipients
|
|
||||||
const recipients = Array.from({ length: 10 }, (_, i) => `recipient${i + 1}@example.com`);
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: recipients,
|
|
||||||
subject: 'Many Recipients Test',
|
|
||||||
text: 'Testing pipelining with many recipients'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
expect(result.acceptedRecipients.length).toEqual(recipients.length);
|
|
||||||
|
|
||||||
console.log(`✅ Successfully sent to ${result.acceptedRecipients.length} recipients`);
|
|
||||||
console.log('Pipelining efficiently handles multiple RCPT TO commands');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-06: Pipelining limits and buffering', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test with a reasonable number of recipients
|
|
||||||
const recipients = Array.from({ length: 50 }, (_, i) => `user${i + 1}@example.com`);
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: recipients.slice(0, 20), // Use first 20 for TO
|
|
||||||
cc: recipients.slice(20, 35), // Next 15 for CC
|
|
||||||
bcc: recipients.slice(35), // Rest for BCC
|
|
||||||
subject: 'Buffering Test',
|
|
||||||
text: 'Testing pipelining limits and buffering'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
|
|
||||||
const totalRecipients = email.to.length + email.cc.length + email.bcc.length;
|
|
||||||
console.log(`✅ Handled ${totalRecipients} total recipients`);
|
|
||||||
console.log('Pipelining respects server limits and buffers appropriately');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup test SMTP server', async () => {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
expect(testServer).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,243 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup test SMTP server', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2547,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
expect(testServer).toBeTruthy();
|
|
||||||
expect(testServer.port).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-07: Parse successful send responses', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Response Test',
|
|
||||||
text: 'Testing response parsing'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
// Verify successful response parsing
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
expect(result.response).toBeTruthy();
|
|
||||||
expect(result.messageId).toBeTruthy();
|
|
||||||
|
|
||||||
// The response should contain queue ID
|
|
||||||
expect(result.response).toInclude('queued');
|
|
||||||
console.log(`✅ Parsed success response: ${result.response}`);
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-07: Parse multiple recipient responses', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send to multiple recipients
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient1@example.com', 'recipient2@example.com', 'recipient3@example.com'],
|
|
||||||
subject: 'Multi-recipient Test',
|
|
||||||
text: 'Testing multiple recipient response parsing'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
// Verify parsing of multiple recipient responses
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
expect(result.acceptedRecipients.length).toEqual(3);
|
|
||||||
expect(result.rejectedRecipients.length).toEqual(0);
|
|
||||||
|
|
||||||
console.log(`✅ Accepted ${result.acceptedRecipients.length} recipients`);
|
|
||||||
console.log('Multiple RCPT TO responses parsed correctly');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-07: Parse error response codes', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test with invalid email to trigger error
|
|
||||||
try {
|
|
||||||
const email = new Email({
|
|
||||||
from: '', // Empty from should trigger error
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Error Test',
|
|
||||||
text: 'Testing error response'
|
|
||||||
});
|
|
||||||
|
|
||||||
await smtpClient.sendMail(email);
|
|
||||||
expect(false).toBeTrue(); // Should not reach here
|
|
||||||
} catch (error: any) {
|
|
||||||
expect(error).toBeInstanceOf(Error);
|
|
||||||
expect(error.message).toBeTruthy();
|
|
||||||
console.log(`✅ Error response parsed: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-07: Parse enhanced status codes', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Normal send - server advertises ENHANCEDSTATUSCODES
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Enhanced Status Test',
|
|
||||||
text: 'Testing enhanced status code parsing'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
// Server logs show it advertises ENHANCEDSTATUSCODES in EHLO
|
|
||||||
console.log('✅ Server advertises ENHANCEDSTATUSCODES capability');
|
|
||||||
console.log('Enhanced status codes are parsed automatically');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-07: Parse response timing and delays', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Measure response time
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Timing Test',
|
|
||||||
text: 'Testing response timing'
|
|
||||||
});
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
const elapsed = Date.now() - startTime;
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
expect(elapsed).toBeGreaterThan(0);
|
|
||||||
expect(elapsed).toBeLessThan(5000); // Should complete within 5 seconds
|
|
||||||
|
|
||||||
console.log(`✅ Response received and parsed in ${elapsed}ms`);
|
|
||||||
console.log('Client handles response timing appropriately');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-07: Parse envelope information', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const from = 'sender@example.com';
|
|
||||||
const to = ['recipient1@example.com', 'recipient2@example.com'];
|
|
||||||
const cc = ['cc@example.com'];
|
|
||||||
const bcc = ['bcc@example.com'];
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
cc,
|
|
||||||
bcc,
|
|
||||||
subject: 'Envelope Test',
|
|
||||||
text: 'Testing envelope parsing'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
expect(result.envelope).toBeTruthy();
|
|
||||||
expect(result.envelope.from).toEqual(from);
|
|
||||||
expect(result.envelope.to).toBeArray();
|
|
||||||
|
|
||||||
// Envelope should include all recipients (to, cc, bcc)
|
|
||||||
const totalRecipients = to.length + cc.length + bcc.length;
|
|
||||||
expect(result.envelope.to.length).toEqual(totalRecipients);
|
|
||||||
|
|
||||||
console.log(`✅ Envelope parsed with ${result.envelope.to.length} recipients`);
|
|
||||||
console.log('Envelope information correctly extracted from responses');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-07: Parse connection state responses', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test verify() which checks connection state
|
|
||||||
const isConnected = await smtpClient.verify();
|
|
||||||
expect(isConnected).toBeTrue();
|
|
||||||
|
|
||||||
console.log('✅ Connection verified through greeting and EHLO responses');
|
|
||||||
|
|
||||||
// Send email to test active connection
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'State Test',
|
|
||||||
text: 'Testing connection state'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
|
|
||||||
console.log('✅ Connection state maintained throughout session');
|
|
||||||
console.log('Response parsing handles connection state correctly');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup test SMTP server', async () => {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
expect(testServer).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,333 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup test SMTP server', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2548,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
expect(testServer).toBeTruthy();
|
|
||||||
expect(testServer.port).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-08: Client handles transaction reset internally', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send first email
|
|
||||||
const email1 = new Email({
|
|
||||||
from: 'sender1@example.com',
|
|
||||||
to: 'recipient1@example.com',
|
|
||||||
subject: 'First Email',
|
|
||||||
text: 'This is the first email'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result1 = await smtpClient.sendMail(email1);
|
|
||||||
expect(result1.success).toBeTrue();
|
|
||||||
|
|
||||||
// Send second email - client handles RSET internally if needed
|
|
||||||
const email2 = new Email({
|
|
||||||
from: 'sender2@example.com',
|
|
||||||
to: 'recipient2@example.com',
|
|
||||||
subject: 'Second Email',
|
|
||||||
text: 'This is the second email'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result2 = await smtpClient.sendMail(email2);
|
|
||||||
expect(result2.success).toBeTrue();
|
|
||||||
|
|
||||||
console.log('✅ Client handles transaction reset between emails');
|
|
||||||
console.log('RSET is used internally to ensure clean state');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-08: Clean state after failed recipient', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send email with multiple recipients - if one fails, RSET ensures clean state
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: [
|
|
||||||
'valid1@example.com',
|
|
||||||
'valid2@example.com',
|
|
||||||
'valid3@example.com'
|
|
||||||
],
|
|
||||||
subject: 'Multi-recipient Email',
|
|
||||||
text: 'Testing state management'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
|
|
||||||
// All recipients should be accepted
|
|
||||||
expect(result.acceptedRecipients.length).toEqual(3);
|
|
||||||
|
|
||||||
console.log('✅ State remains clean with multiple recipients');
|
|
||||||
console.log('Internal RSET ensures proper transaction handling');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-08: Multiple emails in sequence', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send multiple emails in sequence
|
|
||||||
const emails = [
|
|
||||||
{
|
|
||||||
from: 'sender1@example.com',
|
|
||||||
to: 'recipient1@example.com',
|
|
||||||
subject: 'Email 1',
|
|
||||||
text: 'First email'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
from: 'sender2@example.com',
|
|
||||||
to: 'recipient2@example.com',
|
|
||||||
subject: 'Email 2',
|
|
||||||
text: 'Second email'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
from: 'sender3@example.com',
|
|
||||||
to: 'recipient3@example.com',
|
|
||||||
subject: 'Email 3',
|
|
||||||
text: 'Third email'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const emailData of emails) {
|
|
||||||
const email = new Email(emailData);
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✅ Successfully sent multiple emails in sequence');
|
|
||||||
console.log('RSET ensures clean state between each transaction');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-08: Connection pooling with clean state', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
pool: true,
|
|
||||||
maxConnections: 2,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send emails concurrently
|
|
||||||
const promises = Array.from({ length: 5 }, (_, i) => {
|
|
||||||
const email = new Email({
|
|
||||||
from: `sender${i}@example.com`,
|
|
||||||
to: `recipient${i}@example.com`,
|
|
||||||
subject: `Pooled Email ${i}`,
|
|
||||||
text: `This is pooled email ${i}`
|
|
||||||
});
|
|
||||||
return smtpClient.sendMail(email);
|
|
||||||
});
|
|
||||||
|
|
||||||
const results = await Promise.all(promises);
|
|
||||||
|
|
||||||
// Check results and log any failures
|
|
||||||
results.forEach((result, index) => {
|
|
||||||
console.log(`Email ${index}: ${result.success ? '✅' : '❌'} ${!result.success ? result.error?.message : ''}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// With connection pooling, at least some emails should succeed
|
|
||||||
const successCount = results.filter(r => r.success).length;
|
|
||||||
console.log(`Successfully sent ${successCount} of ${results.length} emails`);
|
|
||||||
expect(successCount).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
console.log('✅ Connection pool maintains clean state');
|
|
||||||
console.log('RSET ensures each pooled connection starts fresh');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-08: Error recovery with state reset', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// First, try with invalid sender (should fail early)
|
|
||||||
try {
|
|
||||||
const badEmail = new Email({
|
|
||||||
from: '', // Invalid
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Bad Email',
|
|
||||||
text: 'This should fail'
|
|
||||||
});
|
|
||||||
await smtpClient.sendMail(badEmail);
|
|
||||||
} catch (error) {
|
|
||||||
// Expected to fail
|
|
||||||
console.log('✅ Invalid email rejected as expected');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now send a valid email - should work fine
|
|
||||||
const goodEmail = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Good Email',
|
|
||||||
text: 'This should succeed'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(goodEmail);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
|
|
||||||
console.log('✅ State recovered after error');
|
|
||||||
console.log('RSET ensures clean state after failures');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-08: Verify command maintains session', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// verify() creates temporary connection
|
|
||||||
const verified1 = await smtpClient.verify();
|
|
||||||
expect(verified1).toBeTrue();
|
|
||||||
|
|
||||||
// Send email after verify
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'After Verify',
|
|
||||||
text: 'Email after verification'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
|
|
||||||
// verify() again
|
|
||||||
const verified2 = await smtpClient.verify();
|
|
||||||
expect(verified2).toBeTrue();
|
|
||||||
|
|
||||||
console.log('✅ Verify operations maintain clean session state');
|
|
||||||
console.log('Each operation ensures proper state management');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-08: Rapid sequential sends', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send emails rapidly
|
|
||||||
const count = 10;
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: `Rapid Email ${i}`,
|
|
||||||
text: `Rapid test email ${i}`
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
const elapsed = Date.now() - startTime;
|
|
||||||
const avgTime = elapsed / count;
|
|
||||||
|
|
||||||
console.log(`✅ Sent ${count} emails in ${elapsed}ms`);
|
|
||||||
console.log(`Average time per email: ${avgTime.toFixed(2)}ms`);
|
|
||||||
console.log('RSET maintains efficiency in rapid sends');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-08: State isolation between clients', async () => {
|
|
||||||
// Create two separate clients
|
|
||||||
const client1 = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const client2 = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send from both clients
|
|
||||||
const email1 = new Email({
|
|
||||||
from: 'client1@example.com',
|
|
||||||
to: 'recipient1@example.com',
|
|
||||||
subject: 'From Client 1',
|
|
||||||
text: 'Email from client 1'
|
|
||||||
});
|
|
||||||
|
|
||||||
const email2 = new Email({
|
|
||||||
from: 'client2@example.com',
|
|
||||||
to: 'recipient2@example.com',
|
|
||||||
subject: 'From Client 2',
|
|
||||||
text: 'Email from client 2'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send concurrently
|
|
||||||
const [result1, result2] = await Promise.all([
|
|
||||||
client1.sendMail(email1),
|
|
||||||
client2.sendMail(email2)
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(result1.success).toBeTrue();
|
|
||||||
expect(result2.success).toBeTrue();
|
|
||||||
|
|
||||||
console.log('✅ Each client maintains isolated state');
|
|
||||||
console.log('RSET ensures no cross-contamination');
|
|
||||||
|
|
||||||
await client1.close();
|
|
||||||
await client2.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup test SMTP server', async () => {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
expect(testServer).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,339 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
let smtpClient: SmtpClient;
|
|
||||||
|
|
||||||
tap.test('setup test SMTP server', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2549,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
expect(testServer).toBeTruthy();
|
|
||||||
expect(testServer.port).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-09: Connection keepalive test', async () => {
|
|
||||||
// NOOP is used internally for keepalive - test that connections remain active
|
|
||||||
smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 10000,
|
|
||||||
greetingTimeout: 5000,
|
|
||||||
socketTimeout: 10000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send an initial email to establish connection
|
|
||||||
const email1 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Initial connection test',
|
|
||||||
text: 'Testing connection establishment'
|
|
||||||
});
|
|
||||||
|
|
||||||
await smtpClient.sendMail(email1);
|
|
||||||
console.log('First email sent successfully');
|
|
||||||
|
|
||||||
// Wait 5 seconds (connection should stay alive with internal NOOP)
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
||||||
|
|
||||||
// Send another email on the same connection
|
|
||||||
const email2 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Keepalive test',
|
|
||||||
text: 'Testing connection after delay'
|
|
||||||
});
|
|
||||||
|
|
||||||
await smtpClient.sendMail(email2);
|
|
||||||
console.log('Second email sent successfully after 5 second delay');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-09: Multiple emails in sequence', async () => {
|
|
||||||
// Test that client can handle multiple emails without issues
|
|
||||||
// Internal NOOP commands may be used between transactions
|
|
||||||
|
|
||||||
const emails = [];
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
emails.push(new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: [`recipient${i}@example.com`],
|
|
||||||
subject: `Sequential email ${i + 1}`,
|
|
||||||
text: `This is email number ${i + 1}`
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Sending 5 emails in sequence...');
|
|
||||||
|
|
||||||
for (let i = 0; i < emails.length; i++) {
|
|
||||||
await smtpClient.sendMail(emails[i]);
|
|
||||||
console.log(`Email ${i + 1} sent successfully`);
|
|
||||||
|
|
||||||
// Small delay between emails
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('All emails sent successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-09: Rapid email sending', async () => {
|
|
||||||
// Test rapid email sending without delays
|
|
||||||
// Internal connection management should handle this properly
|
|
||||||
|
|
||||||
const emailCount = 10;
|
|
||||||
const emails = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < emailCount; i++) {
|
|
||||||
emails.push(new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: `Rapid email ${i + 1}`,
|
|
||||||
text: `Rapid fire email number ${i + 1}`
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Sending ${emailCount} emails rapidly...`);
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
// Send all emails as fast as possible
|
|
||||||
for (const email of emails) {
|
|
||||||
await smtpClient.sendMail(email);
|
|
||||||
}
|
|
||||||
|
|
||||||
const elapsed = Date.now() - startTime;
|
|
||||||
console.log(`All ${emailCount} emails sent in ${elapsed}ms`);
|
|
||||||
console.log(`Average: ${(elapsed / emailCount).toFixed(2)}ms per email`);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-09: Long-lived connection test', async () => {
|
|
||||||
// Test that connection stays alive over extended period
|
|
||||||
// SmtpClient should use internal keepalive mechanisms
|
|
||||||
|
|
||||||
console.log('Testing connection over 10 seconds with periodic emails...');
|
|
||||||
|
|
||||||
const testDuration = 10000;
|
|
||||||
const emailInterval = 2500;
|
|
||||||
const iterations = Math.floor(testDuration / emailInterval);
|
|
||||||
|
|
||||||
for (let i = 0; i < iterations; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: `Keepalive test ${i + 1}`,
|
|
||||||
text: `Testing connection keepalive - email ${i + 1}`
|
|
||||||
});
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
await smtpClient.sendMail(email);
|
|
||||||
const elapsed = Date.now() - startTime;
|
|
||||||
|
|
||||||
console.log(`Email ${i + 1} sent in ${elapsed}ms`);
|
|
||||||
|
|
||||||
if (i < iterations - 1) {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, emailInterval));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Connection remained stable over 10 seconds');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-09: Connection pooling behavior', async () => {
|
|
||||||
// Test connection pooling with different email patterns
|
|
||||||
// Internal NOOP may be used to maintain pool connections
|
|
||||||
|
|
||||||
const testPatterns = [
|
|
||||||
{ count: 3, delay: 0, desc: 'Burst of 3 emails' },
|
|
||||||
{ count: 2, delay: 1000, desc: '2 emails with 1s delay' },
|
|
||||||
{ count: 1, delay: 3000, desc: '1 email after 3s delay' }
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const pattern of testPatterns) {
|
|
||||||
console.log(`\nTesting: ${pattern.desc}`);
|
|
||||||
|
|
||||||
if (pattern.delay > 0) {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, pattern.delay));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < pattern.count; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: `${pattern.desc} - Email ${i + 1}`,
|
|
||||||
text: 'Testing connection pooling behavior'
|
|
||||||
});
|
|
||||||
|
|
||||||
await smtpClient.sendMail(email);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Completed: ${pattern.desc}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-09: Email sending performance', async () => {
|
|
||||||
// Measure email sending performance
|
|
||||||
// Connection management (including internal NOOP) affects timing
|
|
||||||
|
|
||||||
const measurements = 20;
|
|
||||||
const times: number[] = [];
|
|
||||||
|
|
||||||
console.log(`Measuring performance over ${measurements} emails...`);
|
|
||||||
|
|
||||||
for (let i = 0; i < measurements; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: `Performance test ${i + 1}`,
|
|
||||||
text: 'Measuring email sending performance'
|
|
||||||
});
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
await smtpClient.sendMail(email);
|
|
||||||
const elapsed = Date.now() - startTime;
|
|
||||||
times.push(elapsed);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate statistics
|
|
||||||
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
|
|
||||||
const minTime = Math.min(...times);
|
|
||||||
const maxTime = Math.max(...times);
|
|
||||||
|
|
||||||
// Calculate standard deviation
|
|
||||||
const variance = times.reduce((sum, time) => sum + Math.pow(time - avgTime, 2), 0) / times.length;
|
|
||||||
const stdDev = Math.sqrt(variance);
|
|
||||||
|
|
||||||
console.log(`\nPerformance analysis (${measurements} emails):`);
|
|
||||||
console.log(` Average: ${avgTime.toFixed(2)}ms`);
|
|
||||||
console.log(` Min: ${minTime}ms`);
|
|
||||||
console.log(` Max: ${maxTime}ms`);
|
|
||||||
console.log(` Std Dev: ${stdDev.toFixed(2)}ms`);
|
|
||||||
|
|
||||||
// First email might be slower due to connection establishment
|
|
||||||
const avgWithoutFirst = times.slice(1).reduce((a, b) => a + b, 0) / (times.length - 1);
|
|
||||||
console.log(` Average (excl. first): ${avgWithoutFirst.toFixed(2)}ms`);
|
|
||||||
|
|
||||||
// Performance should be reasonable
|
|
||||||
expect(avgTime).toBeLessThan(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-09: Email with NOOP in content', async () => {
|
|
||||||
// Test that NOOP as email content doesn't affect delivery
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Email containing NOOP',
|
|
||||||
text: `This email contains SMTP commands as content:
|
|
||||||
|
|
||||||
NOOP
|
|
||||||
HELO test
|
|
||||||
MAIL FROM:<test@example.com>
|
|
||||||
|
|
||||||
These should be treated as plain text, not commands.
|
|
||||||
The word NOOP appears multiple times in this email.
|
|
||||||
|
|
||||||
NOOP is used internally by SMTP for keepalive.`
|
|
||||||
});
|
|
||||||
|
|
||||||
await smtpClient.sendMail(email);
|
|
||||||
console.log('Email with NOOP content sent successfully');
|
|
||||||
|
|
||||||
// Send another email to verify connection still works
|
|
||||||
const email2 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Follow-up email',
|
|
||||||
text: 'Verifying connection still works after NOOP content'
|
|
||||||
});
|
|
||||||
|
|
||||||
await smtpClient.sendMail(email2);
|
|
||||||
console.log('Follow-up email sent successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-09: Concurrent email sending', async () => {
|
|
||||||
// Test concurrent email sending
|
|
||||||
// Connection pooling and internal management should handle this
|
|
||||||
|
|
||||||
const concurrentCount = 5;
|
|
||||||
const emails = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < concurrentCount; i++) {
|
|
||||||
emails.push(new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: [`recipient${i}@example.com`],
|
|
||||||
subject: `Concurrent email ${i + 1}`,
|
|
||||||
text: `Testing concurrent email sending - message ${i + 1}`
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Sending ${concurrentCount} emails concurrently...`);
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
// Send all emails concurrently
|
|
||||||
try {
|
|
||||||
await Promise.all(emails.map(email => smtpClient.sendMail(email)));
|
|
||||||
const elapsed = Date.now() - startTime;
|
|
||||||
console.log(`All ${concurrentCount} emails sent concurrently in ${elapsed}ms`);
|
|
||||||
} catch (error) {
|
|
||||||
// Concurrent sending might not be supported - that's OK
|
|
||||||
console.log('Concurrent sending not supported, falling back to sequential');
|
|
||||||
for (const email of emails) {
|
|
||||||
await smtpClient.sendMail(email);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-09: Connection recovery test', async () => {
|
|
||||||
// Test connection recovery and error handling
|
|
||||||
// SmtpClient should handle connection issues gracefully
|
|
||||||
|
|
||||||
// Create a new client with shorter timeouts for testing
|
|
||||||
const testClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 3000,
|
|
||||||
socketTimeout: 3000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send initial email
|
|
||||||
const email1 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Connection test 1',
|
|
||||||
text: 'Testing initial connection'
|
|
||||||
});
|
|
||||||
|
|
||||||
await testClient.sendMail(email1);
|
|
||||||
console.log('Initial email sent');
|
|
||||||
|
|
||||||
// Simulate long delay that might timeout connection
|
|
||||||
console.log('Waiting 5 seconds to test connection recovery...');
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
||||||
|
|
||||||
// Try to send another email - client should recover if needed
|
|
||||||
const email2 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Connection test 2',
|
|
||||||
text: 'Testing connection recovery'
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await testClient.sendMail(email2);
|
|
||||||
console.log('Email sent successfully after delay - connection recovered');
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Connection recovery failed (this might be expected):', error.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup test SMTP server', async () => {
|
|
||||||
if (testServer) {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,457 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
import { EmailValidator } from '../../../ts/mail/core/classes.emailvalidator.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
let smtpClient: SmtpClient;
|
|
||||||
|
|
||||||
tap.test('setup test SMTP server', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2550,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
expect(testServer).toBeTruthy();
|
|
||||||
expect(testServer.port).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-10: Email address validation', async () => {
|
|
||||||
// Test email address validation which is what VRFY conceptually does
|
|
||||||
const validator = new EmailValidator();
|
|
||||||
|
|
||||||
const testAddresses = [
|
|
||||||
{ address: 'user@example.com', expected: true },
|
|
||||||
{ address: 'postmaster@example.com', expected: true },
|
|
||||||
{ address: 'admin@example.com', expected: true },
|
|
||||||
{ address: 'user.name+tag@example.com', expected: true },
|
|
||||||
{ address: 'test@sub.domain.example.com', expected: true },
|
|
||||||
{ address: 'invalid@', expected: false },
|
|
||||||
{ address: '@example.com', expected: false },
|
|
||||||
{ address: 'not-an-email', expected: false },
|
|
||||||
{ address: '', expected: false },
|
|
||||||
{ address: 'user@', expected: false }
|
|
||||||
];
|
|
||||||
|
|
||||||
console.log('Testing email address validation (VRFY equivalent):\n');
|
|
||||||
|
|
||||||
for (const test of testAddresses) {
|
|
||||||
const isValid = validator.isValidFormat(test.address);
|
|
||||||
expect(isValid).toEqual(test.expected);
|
|
||||||
console.log(`Address: "${test.address}" - Valid: ${isValid} (expected: ${test.expected})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test sending to valid addresses
|
|
||||||
const validEmail = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['user@example.com'],
|
|
||||||
subject: 'Address validation test',
|
|
||||||
text: 'Testing address validation'
|
|
||||||
});
|
|
||||||
|
|
||||||
await smtpClient.sendMail(validEmail);
|
|
||||||
console.log('\nEmail sent successfully to validated address');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-10: Multiple recipient handling (EXPN equivalent)', async () => {
|
|
||||||
// Test multiple recipients which is conceptually similar to mailing list expansion
|
|
||||||
|
|
||||||
console.log('Testing multiple recipient handling (EXPN equivalent):\n');
|
|
||||||
|
|
||||||
// Create email with multiple recipients (like a mailing list)
|
|
||||||
const multiRecipientEmail = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: [
|
|
||||||
'user1@example.com',
|
|
||||||
'user2@example.com',
|
|
||||||
'user3@example.com'
|
|
||||||
],
|
|
||||||
cc: [
|
|
||||||
'cc1@example.com',
|
|
||||||
'cc2@example.com'
|
|
||||||
],
|
|
||||||
bcc: [
|
|
||||||
'bcc1@example.com'
|
|
||||||
],
|
|
||||||
subject: 'Multi-recipient test (mailing list)',
|
|
||||||
text: 'Testing email distribution to multiple recipients'
|
|
||||||
});
|
|
||||||
|
|
||||||
const toAddresses = multiRecipientEmail.getToAddresses();
|
|
||||||
const ccAddresses = multiRecipientEmail.getCcAddresses();
|
|
||||||
const bccAddresses = multiRecipientEmail.getBccAddresses();
|
|
||||||
|
|
||||||
console.log(`To recipients: ${toAddresses.length}`);
|
|
||||||
toAddresses.forEach(addr => console.log(` - ${addr}`));
|
|
||||||
|
|
||||||
console.log(`\nCC recipients: ${ccAddresses.length}`);
|
|
||||||
ccAddresses.forEach(addr => console.log(` - ${addr}`));
|
|
||||||
|
|
||||||
console.log(`\nBCC recipients: ${bccAddresses.length}`);
|
|
||||||
bccAddresses.forEach(addr => console.log(` - ${addr}`));
|
|
||||||
|
|
||||||
console.log(`\nTotal recipients: ${toAddresses.length + ccAddresses.length + bccAddresses.length}`);
|
|
||||||
|
|
||||||
// Send the email
|
|
||||||
await smtpClient.sendMail(multiRecipientEmail);
|
|
||||||
console.log('\nEmail sent successfully to all recipients');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-10: Email addresses with display names', async () => {
|
|
||||||
// Test email addresses with display names (full names)
|
|
||||||
|
|
||||||
console.log('Testing email addresses with display names:\n');
|
|
||||||
|
|
||||||
const fullNameTests = [
|
|
||||||
{ from: '"John Doe" <john@example.com>', expectedAddress: 'john@example.com' },
|
|
||||||
{ from: '"Smith, John" <john.smith@example.com>', expectedAddress: 'john.smith@example.com' },
|
|
||||||
{ from: 'Mary Johnson <mary@example.com>', expectedAddress: 'mary@example.com' },
|
|
||||||
{ from: '<bob@example.com>', expectedAddress: 'bob@example.com' }
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const test of fullNameTests) {
|
|
||||||
const email = new Email({
|
|
||||||
from: test.from,
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Display name test',
|
|
||||||
text: `Testing from: ${test.from}`
|
|
||||||
});
|
|
||||||
|
|
||||||
const fromAddress = email.getFromAddress();
|
|
||||||
console.log(`Full: "${test.from}"`);
|
|
||||||
console.log(`Extracted: "${fromAddress}"`);
|
|
||||||
expect(fromAddress).toEqual(test.expectedAddress);
|
|
||||||
|
|
||||||
await smtpClient.sendMail(email);
|
|
||||||
console.log('Email sent successfully\n');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-10: Email validation security', async () => {
|
|
||||||
// Test security aspects of email validation
|
|
||||||
|
|
||||||
console.log('Testing email validation security considerations:\n');
|
|
||||||
|
|
||||||
// Test common system/role addresses that should be handled carefully
|
|
||||||
const systemAddresses = [
|
|
||||||
'root@example.com',
|
|
||||||
'admin@example.com',
|
|
||||||
'administrator@example.com',
|
|
||||||
'webmaster@example.com',
|
|
||||||
'hostmaster@example.com',
|
|
||||||
'abuse@example.com',
|
|
||||||
'postmaster@example.com',
|
|
||||||
'noreply@example.com'
|
|
||||||
];
|
|
||||||
|
|
||||||
const validator = new EmailValidator();
|
|
||||||
|
|
||||||
console.log('Checking if addresses are role accounts:');
|
|
||||||
for (const addr of systemAddresses) {
|
|
||||||
const validationResult = await validator.validate(addr, { checkRole: true, checkMx: false });
|
|
||||||
console.log(` ${addr}: ${validationResult.details?.role ? 'Role account' : 'Not a role account'} (format valid: ${validationResult.details?.formatValid})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that we don't expose information about which addresses exist
|
|
||||||
console.log('\nTesting information disclosure prevention:');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Try sending to a non-existent address
|
|
||||||
const testEmail = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['definitely-does-not-exist-12345@example.com'],
|
|
||||||
subject: 'Test',
|
|
||||||
text: 'Test'
|
|
||||||
});
|
|
||||||
|
|
||||||
await smtpClient.sendMail(testEmail);
|
|
||||||
console.log('Server accepted email (does not disclose non-existence)');
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Server rejected email:', error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\nSecurity best practice: Servers should not disclose address existence');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-10: Validation during email sending', async () => {
|
|
||||||
// Test that validation doesn't interfere with email sending
|
|
||||||
|
|
||||||
console.log('Testing validation during email transaction:\n');
|
|
||||||
|
|
||||||
const validator = new EmailValidator();
|
|
||||||
|
|
||||||
// Create a series of emails with validation between them
|
|
||||||
const emails = [
|
|
||||||
{
|
|
||||||
from: 'sender1@example.com',
|
|
||||||
to: ['recipient1@example.com'],
|
|
||||||
subject: 'First email',
|
|
||||||
text: 'Testing validation during transaction'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
from: 'sender2@example.com',
|
|
||||||
to: ['recipient2@example.com', 'recipient3@example.com'],
|
|
||||||
subject: 'Second email',
|
|
||||||
text: 'Multiple recipients'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
from: '"Test User" <sender3@example.com>',
|
|
||||||
to: ['recipient4@example.com'],
|
|
||||||
subject: 'Third email',
|
|
||||||
text: 'Display name test'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
for (let i = 0; i < emails.length; i++) {
|
|
||||||
const emailData = emails[i];
|
|
||||||
|
|
||||||
// Validate addresses before sending
|
|
||||||
console.log(`Email ${i + 1}:`);
|
|
||||||
const fromAddr = emailData.from.includes('<') ? emailData.from.match(/<([^>]+)>/)?.[1] || emailData.from : emailData.from;
|
|
||||||
console.log(` From: ${emailData.from} - Valid: ${validator.isValidFormat(fromAddr)}`);
|
|
||||||
|
|
||||||
for (const to of emailData.to) {
|
|
||||||
console.log(` To: ${to} - Valid: ${validator.isValidFormat(to)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create and send email
|
|
||||||
const email = new Email(emailData);
|
|
||||||
await smtpClient.sendMail(email);
|
|
||||||
console.log(` Sent successfully\n`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('All emails sent successfully with validation');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-10: Special characters in email addresses', async () => {
|
|
||||||
// Test email addresses with special characters
|
|
||||||
|
|
||||||
console.log('Testing email addresses with special characters:\n');
|
|
||||||
|
|
||||||
const validator = new EmailValidator();
|
|
||||||
|
|
||||||
const specialAddresses = [
|
|
||||||
{ address: 'user+tag@example.com', shouldBeValid: true, description: 'Plus addressing' },
|
|
||||||
{ address: 'first.last@example.com', shouldBeValid: true, description: 'Dots in local part' },
|
|
||||||
{ address: 'user_name@example.com', shouldBeValid: true, description: 'Underscore' },
|
|
||||||
{ address: 'user-name@example.com', shouldBeValid: true, description: 'Hyphen' },
|
|
||||||
{ address: '"quoted string"@example.com', shouldBeValid: true, description: 'Quoted string' },
|
|
||||||
{ address: 'user@sub.domain.example.com', shouldBeValid: true, description: 'Subdomain' },
|
|
||||||
{ address: 'user@example.co.uk', shouldBeValid: true, description: 'Multi-part TLD' },
|
|
||||||
{ address: 'user..name@example.com', shouldBeValid: false, description: 'Double dots' },
|
|
||||||
{ address: '.user@example.com', shouldBeValid: false, description: 'Leading dot' },
|
|
||||||
{ address: 'user.@example.com', shouldBeValid: false, description: 'Trailing dot' }
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const test of specialAddresses) {
|
|
||||||
const isValid = validator.isValidFormat(test.address);
|
|
||||||
console.log(`${test.description}:`);
|
|
||||||
console.log(` Address: "${test.address}"`);
|
|
||||||
console.log(` Valid: ${isValid} (expected: ${test.shouldBeValid})`);
|
|
||||||
|
|
||||||
if (test.shouldBeValid && isValid) {
|
|
||||||
// Try sending an email with this address
|
|
||||||
try {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: [test.address],
|
|
||||||
subject: 'Special character test',
|
|
||||||
text: `Testing special characters in: ${test.address}`
|
|
||||||
});
|
|
||||||
|
|
||||||
await smtpClient.sendMail(email);
|
|
||||||
console.log(` Email sent successfully`);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` Failed to send: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log('');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-10: Large recipient lists', async () => {
|
|
||||||
// Test handling of large recipient lists (similar to EXPN multi-line)
|
|
||||||
|
|
||||||
console.log('Testing large recipient lists:\n');
|
|
||||||
|
|
||||||
// Create email with many recipients
|
|
||||||
const recipientCount = 20;
|
|
||||||
const toRecipients = [];
|
|
||||||
const ccRecipients = [];
|
|
||||||
|
|
||||||
for (let i = 1; i <= recipientCount; i++) {
|
|
||||||
if (i <= 10) {
|
|
||||||
toRecipients.push(`user${i}@example.com`);
|
|
||||||
} else {
|
|
||||||
ccRecipients.push(`user${i}@example.com`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Creating email with ${recipientCount} total recipients:`);
|
|
||||||
console.log(` To: ${toRecipients.length} recipients`);
|
|
||||||
console.log(` CC: ${ccRecipients.length} recipients`);
|
|
||||||
|
|
||||||
const largeListEmail = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: toRecipients,
|
|
||||||
cc: ccRecipients,
|
|
||||||
subject: 'Large distribution list test',
|
|
||||||
text: `This email is being sent to ${recipientCount} recipients total`
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show extracted addresses
|
|
||||||
const allTo = largeListEmail.getToAddresses();
|
|
||||||
const allCc = largeListEmail.getCcAddresses();
|
|
||||||
|
|
||||||
console.log('\nExtracted addresses:');
|
|
||||||
console.log(`To (first 3): ${allTo.slice(0, 3).join(', ')}...`);
|
|
||||||
console.log(`CC (first 3): ${allCc.slice(0, 3).join(', ')}...`);
|
|
||||||
|
|
||||||
// Send the email
|
|
||||||
const startTime = Date.now();
|
|
||||||
await smtpClient.sendMail(largeListEmail);
|
|
||||||
const elapsed = Date.now() - startTime;
|
|
||||||
|
|
||||||
console.log(`\nEmail sent to all ${recipientCount} recipients in ${elapsed}ms`);
|
|
||||||
console.log(`Average: ${(elapsed / recipientCount).toFixed(2)}ms per recipient`);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-10: Email validation performance', async () => {
|
|
||||||
// Test validation performance
|
|
||||||
|
|
||||||
console.log('Testing email validation performance:\n');
|
|
||||||
|
|
||||||
const validator = new EmailValidator();
|
|
||||||
const testCount = 1000;
|
|
||||||
|
|
||||||
// Generate test addresses
|
|
||||||
const testAddresses = [];
|
|
||||||
for (let i = 0; i < testCount; i++) {
|
|
||||||
testAddresses.push(`user${i}@example${i % 10}.com`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Time validation
|
|
||||||
const startTime = Date.now();
|
|
||||||
let validCount = 0;
|
|
||||||
|
|
||||||
for (const address of testAddresses) {
|
|
||||||
if (validator.isValidFormat(address)) {
|
|
||||||
validCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const elapsed = Date.now() - startTime;
|
|
||||||
const rate = (testCount / elapsed) * 1000;
|
|
||||||
|
|
||||||
console.log(`Validated ${testCount} addresses in ${elapsed}ms`);
|
|
||||||
console.log(`Rate: ${rate.toFixed(0)} validations/second`);
|
|
||||||
console.log(`Valid addresses: ${validCount}/${testCount}`);
|
|
||||||
|
|
||||||
// Test rapid email sending to see if there's rate limiting
|
|
||||||
console.log('\nTesting rapid email sending:');
|
|
||||||
|
|
||||||
const emailCount = 10;
|
|
||||||
const sendStartTime = Date.now();
|
|
||||||
let sentCount = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < emailCount; i++) {
|
|
||||||
try {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: [`recipient${i}@example.com`],
|
|
||||||
subject: `Rate test ${i + 1}`,
|
|
||||||
text: 'Testing rate limits'
|
|
||||||
});
|
|
||||||
|
|
||||||
await smtpClient.sendMail(email);
|
|
||||||
sentCount++;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`Rate limit hit at email ${i + 1}: ${error.message}`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const sendElapsed = Date.now() - sendStartTime;
|
|
||||||
const sendRate = (sentCount / sendElapsed) * 1000;
|
|
||||||
|
|
||||||
console.log(`Sent ${sentCount}/${emailCount} emails in ${sendElapsed}ms`);
|
|
||||||
console.log(`Rate: ${sendRate.toFixed(2)} emails/second`);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-10: Email validation error handling', async () => {
|
|
||||||
// Test error handling for invalid email addresses
|
|
||||||
|
|
||||||
console.log('Testing email validation error handling:\n');
|
|
||||||
|
|
||||||
const validator = new EmailValidator();
|
|
||||||
|
|
||||||
const errorTests = [
|
|
||||||
{ address: null, description: 'Null address' },
|
|
||||||
{ address: undefined, description: 'Undefined address' },
|
|
||||||
{ address: '', description: 'Empty string' },
|
|
||||||
{ address: ' ', description: 'Whitespace only' },
|
|
||||||
{ address: '@', description: 'Just @ symbol' },
|
|
||||||
{ address: 'user@', description: 'Missing domain' },
|
|
||||||
{ address: '@domain.com', description: 'Missing local part' },
|
|
||||||
{ address: 'user@@domain.com', description: 'Double @ symbol' },
|
|
||||||
{ address: 'user@domain@com', description: 'Multiple @ symbols' },
|
|
||||||
{ address: 'user space@domain.com', description: 'Space in local part' },
|
|
||||||
{ address: 'user@domain .com', description: 'Space in domain' },
|
|
||||||
{ address: 'x'.repeat(256) + '@domain.com', description: 'Very long local part' },
|
|
||||||
{ address: 'user@' + 'x'.repeat(256) + '.com', description: 'Very long domain' }
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const test of errorTests) {
|
|
||||||
console.log(`${test.description}:`);
|
|
||||||
console.log(` Input: "${test.address}"`);
|
|
||||||
|
|
||||||
// Test validation
|
|
||||||
let isValid = false;
|
|
||||||
try {
|
|
||||||
isValid = validator.isValidFormat(test.address as any);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` Validation threw: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isValid) {
|
|
||||||
console.log(` Correctly rejected as invalid`);
|
|
||||||
} else {
|
|
||||||
console.log(` WARNING: Accepted as valid!`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to send email with invalid address
|
|
||||||
if (test.address) {
|
|
||||||
try {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: [test.address],
|
|
||||||
subject: 'Error test',
|
|
||||||
text: 'Testing invalid address'
|
|
||||||
});
|
|
||||||
|
|
||||||
await smtpClient.sendMail(email);
|
|
||||||
console.log(` WARNING: Email sent with invalid address!`);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` Email correctly rejected: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log('');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup test SMTP server', async () => {
|
|
||||||
if (testServer) {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,409 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
let smtpClient: SmtpClient;
|
|
||||||
|
|
||||||
tap.test('setup test SMTP server', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2551,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
expect(testServer).toBeTruthy();
|
|
||||||
expect(testServer.port).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-11: Server capabilities discovery', async () => {
|
|
||||||
// Test server capabilities which is what HELP provides info about
|
|
||||||
smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Testing server capabilities discovery (HELP equivalent):\n');
|
|
||||||
|
|
||||||
// Send a test email to see server capabilities in action
|
|
||||||
const testEmail = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Capability test',
|
|
||||||
text: 'Testing server capabilities'
|
|
||||||
});
|
|
||||||
|
|
||||||
await smtpClient.sendMail(testEmail);
|
|
||||||
console.log('Email sent successfully - server supports basic SMTP commands');
|
|
||||||
|
|
||||||
// Test different configurations to understand server behavior
|
|
||||||
const capabilities = {
|
|
||||||
basicSMTP: true,
|
|
||||||
multiplRecipients: false,
|
|
||||||
largeMessages: false,
|
|
||||||
internationalDomains: false
|
|
||||||
};
|
|
||||||
|
|
||||||
// Test multiple recipients
|
|
||||||
try {
|
|
||||||
const multiEmail = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient1@example.com', 'recipient2@example.com', 'recipient3@example.com'],
|
|
||||||
subject: 'Multi-recipient test',
|
|
||||||
text: 'Testing multiple recipients'
|
|
||||||
});
|
|
||||||
await smtpClient.sendMail(multiEmail);
|
|
||||||
capabilities.multiplRecipients = true;
|
|
||||||
console.log('✓ Server supports multiple recipients');
|
|
||||||
} catch (error) {
|
|
||||||
console.log('✗ Multiple recipients not supported');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\nDetected capabilities:', capabilities);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-11: Error message diagnostics', async () => {
|
|
||||||
// Test error messages which HELP would explain
|
|
||||||
console.log('Testing error message diagnostics:\n');
|
|
||||||
|
|
||||||
const errorTests = [
|
|
||||||
{
|
|
||||||
description: 'Invalid sender address',
|
|
||||||
email: {
|
|
||||||
from: 'invalid-sender',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Test',
|
|
||||||
text: 'Test'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'Empty recipient list',
|
|
||||||
email: {
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: [],
|
|
||||||
subject: 'Test',
|
|
||||||
text: 'Test'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'Null subject',
|
|
||||||
email: {
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: null as any,
|
|
||||||
text: 'Test'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const test of errorTests) {
|
|
||||||
console.log(`Testing: ${test.description}`);
|
|
||||||
try {
|
|
||||||
const email = new Email(test.email);
|
|
||||||
await smtpClient.sendMail(email);
|
|
||||||
console.log(' Unexpectedly succeeded');
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` Error: ${error.message}`);
|
|
||||||
console.log(` This would be explained in HELP documentation`);
|
|
||||||
}
|
|
||||||
console.log('');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-11: Connection configuration help', async () => {
|
|
||||||
// Test different connection configurations
|
|
||||||
console.log('Testing connection configurations:\n');
|
|
||||||
|
|
||||||
const configs = [
|
|
||||||
{
|
|
||||||
name: 'Standard connection',
|
|
||||||
config: {
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
},
|
|
||||||
shouldWork: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'With greeting timeout',
|
|
||||||
config: {
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
greetingTimeout: 3000
|
|
||||||
},
|
|
||||||
shouldWork: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'With socket timeout',
|
|
||||||
config: {
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
socketTimeout: 10000
|
|
||||||
},
|
|
||||||
shouldWork: true
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const testConfig of configs) {
|
|
||||||
console.log(`Testing: ${testConfig.name}`);
|
|
||||||
try {
|
|
||||||
const client = createSmtpClient(testConfig.config);
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Config test',
|
|
||||||
text: `Testing ${testConfig.name}`
|
|
||||||
});
|
|
||||||
|
|
||||||
await client.sendMail(email);
|
|
||||||
console.log(` ✓ Configuration works`);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ✗ Error: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-11: Protocol flow documentation', async () => {
|
|
||||||
// Document the protocol flow (what HELP would explain)
|
|
||||||
console.log('SMTP Protocol Flow (as HELP would document):\n');
|
|
||||||
|
|
||||||
const protocolSteps = [
|
|
||||||
'1. Connection established',
|
|
||||||
'2. Server sends greeting (220)',
|
|
||||||
'3. Client sends EHLO',
|
|
||||||
'4. Server responds with capabilities',
|
|
||||||
'5. Client sends MAIL FROM',
|
|
||||||
'6. Server accepts sender (250)',
|
|
||||||
'7. Client sends RCPT TO',
|
|
||||||
'8. Server accepts recipient (250)',
|
|
||||||
'9. Client sends DATA',
|
|
||||||
'10. Server ready for data (354)',
|
|
||||||
'11. Client sends message content',
|
|
||||||
'12. Client sends . to end',
|
|
||||||
'13. Server accepts message (250)',
|
|
||||||
'14. Client can send more or QUIT'
|
|
||||||
];
|
|
||||||
|
|
||||||
console.log('Standard SMTP transaction flow:');
|
|
||||||
protocolSteps.forEach(step => console.log(` ${step}`));
|
|
||||||
|
|
||||||
// Demonstrate the flow
|
|
||||||
console.log('\nDemonstrating flow with actual email:');
|
|
||||||
const email = new Email({
|
|
||||||
from: 'demo@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Protocol flow demo',
|
|
||||||
text: 'Demonstrating SMTP protocol flow'
|
|
||||||
});
|
|
||||||
|
|
||||||
await smtpClient.sendMail(email);
|
|
||||||
console.log('✓ Protocol flow completed successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-11: Command availability matrix', async () => {
|
|
||||||
// Test what commands are available (HELP info)
|
|
||||||
console.log('Testing command availability:\n');
|
|
||||||
|
|
||||||
// Test various email features to determine support
|
|
||||||
const features = {
|
|
||||||
plainText: { supported: false, description: 'Plain text emails' },
|
|
||||||
htmlContent: { supported: false, description: 'HTML emails' },
|
|
||||||
attachments: { supported: false, description: 'File attachments' },
|
|
||||||
multipleRecipients: { supported: false, description: 'Multiple recipients' },
|
|
||||||
ccRecipients: { supported: false, description: 'CC recipients' },
|
|
||||||
bccRecipients: { supported: false, description: 'BCC recipients' },
|
|
||||||
customHeaders: { supported: false, description: 'Custom headers' },
|
|
||||||
priorities: { supported: false, description: 'Email priorities' }
|
|
||||||
};
|
|
||||||
|
|
||||||
// Test plain text
|
|
||||||
try {
|
|
||||||
await smtpClient.sendMail(new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Plain text test',
|
|
||||||
text: 'Plain text content'
|
|
||||||
}));
|
|
||||||
features.plainText.supported = true;
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
// Test HTML
|
|
||||||
try {
|
|
||||||
await smtpClient.sendMail(new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'HTML test',
|
|
||||||
html: '<p>HTML content</p>'
|
|
||||||
}));
|
|
||||||
features.htmlContent.supported = true;
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
// Test multiple recipients
|
|
||||||
try {
|
|
||||||
await smtpClient.sendMail(new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient1@example.com', 'recipient2@example.com'],
|
|
||||||
subject: 'Multiple recipients test',
|
|
||||||
text: 'Test'
|
|
||||||
}));
|
|
||||||
features.multipleRecipients.supported = true;
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
// Test CC
|
|
||||||
try {
|
|
||||||
await smtpClient.sendMail(new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
cc: ['cc@example.com'],
|
|
||||||
subject: 'CC test',
|
|
||||||
text: 'Test'
|
|
||||||
}));
|
|
||||||
features.ccRecipients.supported = true;
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
// Test BCC
|
|
||||||
try {
|
|
||||||
await smtpClient.sendMail(new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
bcc: ['bcc@example.com'],
|
|
||||||
subject: 'BCC test',
|
|
||||||
text: 'Test'
|
|
||||||
}));
|
|
||||||
features.bccRecipients.supported = true;
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
console.log('Feature support matrix:');
|
|
||||||
Object.entries(features).forEach(([key, value]) => {
|
|
||||||
console.log(` ${value.description}: ${value.supported ? '✓ Supported' : '✗ Not supported'}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-11: Error code reference', async () => {
|
|
||||||
// Document error codes (HELP would explain these)
|
|
||||||
console.log('SMTP Error Code Reference (as HELP would provide):\n');
|
|
||||||
|
|
||||||
const errorCodes = [
|
|
||||||
{ code: '220', meaning: 'Service ready', type: 'Success' },
|
|
||||||
{ code: '221', meaning: 'Service closing transmission channel', type: 'Success' },
|
|
||||||
{ code: '250', meaning: 'Requested action completed', type: 'Success' },
|
|
||||||
{ code: '251', meaning: 'User not local; will forward', type: 'Success' },
|
|
||||||
{ code: '354', meaning: 'Start mail input', type: 'Intermediate' },
|
|
||||||
{ code: '421', meaning: 'Service not available', type: 'Temporary failure' },
|
|
||||||
{ code: '450', meaning: 'Mailbox unavailable', type: 'Temporary failure' },
|
|
||||||
{ code: '451', meaning: 'Local error in processing', type: 'Temporary failure' },
|
|
||||||
{ code: '452', meaning: 'Insufficient storage', type: 'Temporary failure' },
|
|
||||||
{ code: '500', meaning: 'Syntax error', type: 'Permanent failure' },
|
|
||||||
{ code: '501', meaning: 'Syntax error in parameters', type: 'Permanent failure' },
|
|
||||||
{ code: '502', meaning: 'Command not implemented', type: 'Permanent failure' },
|
|
||||||
{ code: '503', meaning: 'Bad sequence of commands', type: 'Permanent failure' },
|
|
||||||
{ code: '550', meaning: 'Mailbox not found', type: 'Permanent failure' },
|
|
||||||
{ code: '551', meaning: 'User not local', type: 'Permanent failure' },
|
|
||||||
{ code: '552', meaning: 'Storage allocation exceeded', type: 'Permanent failure' },
|
|
||||||
{ code: '553', meaning: 'Mailbox name not allowed', type: 'Permanent failure' },
|
|
||||||
{ code: '554', meaning: 'Transaction failed', type: 'Permanent failure' }
|
|
||||||
];
|
|
||||||
|
|
||||||
console.log('Common SMTP response codes:');
|
|
||||||
errorCodes.forEach(({ code, meaning, type }) => {
|
|
||||||
console.log(` ${code} - ${meaning} (${type})`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test triggering some errors
|
|
||||||
console.log('\nDemonstrating error handling:');
|
|
||||||
|
|
||||||
// Invalid email format
|
|
||||||
try {
|
|
||||||
await smtpClient.sendMail(new Email({
|
|
||||||
from: 'invalid-email-format',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Test',
|
|
||||||
text: 'Test'
|
|
||||||
}));
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`Invalid format error: ${error.message}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-11: Debugging assistance', async () => {
|
|
||||||
// Test debugging features (HELP assists with debugging)
|
|
||||||
console.log('Debugging assistance features:\n');
|
|
||||||
|
|
||||||
// Create client with debug enabled
|
|
||||||
const debugClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Sending email with debug mode enabled:');
|
|
||||||
console.log('(Debug output would show full SMTP conversation)\n');
|
|
||||||
|
|
||||||
const debugEmail = new Email({
|
|
||||||
from: 'debug@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Debug test',
|
|
||||||
text: 'Testing with debug mode'
|
|
||||||
});
|
|
||||||
|
|
||||||
// The debug output will be visible in the console
|
|
||||||
await debugClient.sendMail(debugEmail);
|
|
||||||
|
|
||||||
console.log('\nDebug mode helps troubleshoot:');
|
|
||||||
console.log('- Connection issues');
|
|
||||||
console.log('- Authentication problems');
|
|
||||||
console.log('- Message formatting errors');
|
|
||||||
console.log('- Server response codes');
|
|
||||||
console.log('- Protocol violations');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCMD-11: Performance benchmarks', async () => {
|
|
||||||
// Performance info (HELP might mention performance tips)
|
|
||||||
console.log('Performance benchmarks:\n');
|
|
||||||
|
|
||||||
const messageCount = 10;
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
for (let i = 0; i < messageCount; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'perf@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: `Performance test ${i + 1}`,
|
|
||||||
text: 'Testing performance'
|
|
||||||
});
|
|
||||||
|
|
||||||
await smtpClient.sendMail(email);
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalTime = Date.now() - startTime;
|
|
||||||
const avgTime = totalTime / messageCount;
|
|
||||||
|
|
||||||
console.log(`Sent ${messageCount} emails in ${totalTime}ms`);
|
|
||||||
console.log(`Average time per email: ${avgTime.toFixed(2)}ms`);
|
|
||||||
console.log(`Throughput: ${(1000 / avgTime).toFixed(2)} emails/second`);
|
|
||||||
|
|
||||||
console.log('\nPerformance tips:');
|
|
||||||
console.log('- Use connection pooling for multiple emails');
|
|
||||||
console.log('- Enable pipelining when supported');
|
|
||||||
console.log('- Batch recipients when possible');
|
|
||||||
console.log('- Use appropriate timeouts');
|
|
||||||
console.log('- Monitor connection limits');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup test SMTP server', async () => {
|
|
||||||
if (testServer) {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
let smtpClient: SmtpClient;
|
|
||||||
|
|
||||||
tap.test('setup - start SMTP server for basic connection test', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2525,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(testServer.port).toEqual(2525);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-01: Basic TCP Connection - should connect to SMTP server', async () => {
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Create SMTP client
|
|
||||||
smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Verify connection
|
|
||||||
const isConnected = await smtpClient.verify();
|
|
||||||
expect(isConnected).toBeTrue();
|
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
console.log(`✅ Basic TCP connection established in ${duration}ms`);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
console.error(`❌ Basic TCP connection failed after ${duration}ms:`, error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-01: Basic TCP Connection - should report connection status', async () => {
|
|
||||||
// After verify(), connection is closed, so isConnected should be false
|
|
||||||
expect(smtpClient.isConnected()).toBeFalse();
|
|
||||||
|
|
||||||
const poolStatus = smtpClient.getPoolStatus();
|
|
||||||
console.log('📊 Connection pool status:', poolStatus);
|
|
||||||
|
|
||||||
// After verify(), pool should be empty
|
|
||||||
expect(poolStatus.total).toEqual(0);
|
|
||||||
expect(poolStatus.active).toEqual(0);
|
|
||||||
|
|
||||||
// Test that connection status is correct during actual email send
|
|
||||||
const email = new (await import('../../../ts/mail/core/classes.email.js')).Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Connection status test',
|
|
||||||
text: 'Testing connection status'
|
|
||||||
});
|
|
||||||
|
|
||||||
// During sendMail, connection should be established
|
|
||||||
const sendPromise = smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
// Check status while sending (might be too fast to catch)
|
|
||||||
const duringStatus = smtpClient.getPoolStatus();
|
|
||||||
console.log('📊 Pool status during send:', duringStatus);
|
|
||||||
|
|
||||||
await sendPromise;
|
|
||||||
|
|
||||||
// After send, connection might be pooled or closed
|
|
||||||
const afterStatus = smtpClient.getPoolStatus();
|
|
||||||
console.log('📊 Pool status after send:', afterStatus);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-01: Basic TCP Connection - should handle multiple connect/disconnect cycles', async () => {
|
|
||||||
// Close existing connection
|
|
||||||
await smtpClient.close();
|
|
||||||
expect(smtpClient.isConnected()).toBeFalse();
|
|
||||||
|
|
||||||
// Create new client and test reconnection
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const cycleClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const isConnected = await cycleClient.verify();
|
|
||||||
expect(isConnected).toBeTrue();
|
|
||||||
|
|
||||||
await cycleClient.close();
|
|
||||||
expect(cycleClient.isConnected()).toBeFalse();
|
|
||||||
|
|
||||||
console.log(`✅ Connection cycle ${i + 1} completed`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-01: Basic TCP Connection - should fail with invalid host', async () => {
|
|
||||||
const invalidClient = createSmtpClient({
|
|
||||||
host: 'invalid.host.that.does.not.exist',
|
|
||||||
port: 2525,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 3000
|
|
||||||
});
|
|
||||||
|
|
||||||
// verify() returns false on connection failure, doesn't throw
|
|
||||||
const result = await invalidClient.verify();
|
|
||||||
expect(result).toBeFalse();
|
|
||||||
console.log('✅ Correctly failed to connect to invalid host');
|
|
||||||
|
|
||||||
await invalidClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-01: Basic TCP Connection - should timeout on unresponsive port', async () => {
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
const timeoutClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: 9999, // Port that's not listening
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 2000
|
|
||||||
});
|
|
||||||
|
|
||||||
// verify() returns false on connection failure, doesn't throw
|
|
||||||
const result = await timeoutClient.verify();
|
|
||||||
expect(result).toBeFalse();
|
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
expect(duration).toBeLessThan(3000); // Should timeout within 3 seconds
|
|
||||||
console.log(`✅ Connection timeout working correctly (${duration}ms)`);
|
|
||||||
|
|
||||||
await timeoutClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - close SMTP client', async () => {
|
|
||||||
if (smtpClient && smtpClient.isConnected()) {
|
|
||||||
await smtpClient.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - stop SMTP server', async () => {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
let smtpClient: SmtpClient;
|
|
||||||
|
|
||||||
tap.test('setup - start SMTP server with TLS', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2526,
|
|
||||||
tlsEnabled: true,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(testServer.port).toEqual(2526);
|
|
||||||
expect(testServer.config.tlsEnabled).toBeTrue();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-02: TLS Connection - should establish secure connection via STARTTLS', async () => {
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Create SMTP client with STARTTLS (not direct TLS)
|
|
||||||
smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false, // Start with plain connection
|
|
||||||
connectionTimeout: 10000,
|
|
||||||
tls: {
|
|
||||||
rejectUnauthorized: false // For self-signed test certificates
|
|
||||||
},
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Verify connection (will upgrade to TLS via STARTTLS)
|
|
||||||
const isConnected = await smtpClient.verify();
|
|
||||||
expect(isConnected).toBeTrue();
|
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
console.log(`✅ STARTTLS connection established in ${duration}ms`);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
console.error(`❌ STARTTLS connection failed after ${duration}ms:`, error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-02: TLS Connection - should send email over secure connection', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'test@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'TLS Connection Test',
|
|
||||||
text: 'This email was sent over a secure TLS connection',
|
|
||||||
html: '<p>This email was sent over a <strong>secure TLS connection</strong></p>'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result).toBeTruthy();
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
expect(result.messageId).toBeTruthy();
|
|
||||||
|
|
||||||
console.log(`✅ Email sent over TLS with message ID: ${result.messageId}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-02: TLS Connection - should reject invalid certificates when required', async () => {
|
|
||||||
// Create new client with strict certificate validation
|
|
||||||
const strictClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
tls: {
|
|
||||||
rejectUnauthorized: true // Strict validation
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should fail with self-signed certificate
|
|
||||||
const result = await strictClient.verify();
|
|
||||||
expect(result).toBeFalse();
|
|
||||||
|
|
||||||
console.log('✅ Correctly rejected self-signed certificate with strict validation');
|
|
||||||
|
|
||||||
await strictClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-02: TLS Connection - should work with direct TLS if supported', async () => {
|
|
||||||
// Try direct TLS connection (might fail if server doesn't support it)
|
|
||||||
const directTlsClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: true, // Direct TLS from start
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
tls: {
|
|
||||||
rejectUnauthorized: false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await directTlsClient.verify();
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
console.log('✅ Direct TLS connection supported and working');
|
|
||||||
} else {
|
|
||||||
console.log('ℹ️ Direct TLS not supported, STARTTLS is the way');
|
|
||||||
}
|
|
||||||
|
|
||||||
await directTlsClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-02: TLS Connection - should verify TLS cipher suite', async () => {
|
|
||||||
// Send email and check connection details
|
|
||||||
const email = new Email({
|
|
||||||
from: 'cipher-test@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'TLS Cipher Test',
|
|
||||||
text: 'Testing TLS cipher suite'
|
|
||||||
});
|
|
||||||
|
|
||||||
// The actual cipher info would be in debug logs
|
|
||||||
console.log('ℹ️ TLS cipher information available in debug logs');
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
|
|
||||||
console.log('✅ Email sent successfully over encrypted connection');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - close SMTP client', async () => {
|
|
||||||
if (smtpClient) {
|
|
||||||
await smtpClient.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - stop SMTP server', async () => {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
let smtpClient: SmtpClient;
|
|
||||||
|
|
||||||
tap.test('setup - start SMTP server with STARTTLS support', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2528,
|
|
||||||
tlsEnabled: true, // Enables STARTTLS capability
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(testServer.port).toEqual(2528);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-03: STARTTLS Upgrade - should upgrade plain connection to TLS', async () => {
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Create SMTP client starting with plain connection
|
|
||||||
smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false, // Start with plain connection
|
|
||||||
connectionTimeout: 10000,
|
|
||||||
tls: {
|
|
||||||
rejectUnauthorized: false // For self-signed test certificates
|
|
||||||
},
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// The client should automatically upgrade to TLS via STARTTLS
|
|
||||||
const isConnected = await smtpClient.verify();
|
|
||||||
expect(isConnected).toBeTrue();
|
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
console.log(`✅ STARTTLS upgrade completed in ${duration}ms`);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
console.error(`❌ STARTTLS upgrade failed after ${duration}ms:`, error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-03: STARTTLS Upgrade - should send email after upgrade', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'test@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'STARTTLS Upgrade Test',
|
|
||||||
text: 'This email was sent after STARTTLS upgrade',
|
|
||||||
html: '<p>This email was sent after <strong>STARTTLS upgrade</strong></p>'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
expect(result.acceptedRecipients).toContain('recipient@example.com');
|
|
||||||
expect(result.rejectedRecipients.length).toEqual(0);
|
|
||||||
|
|
||||||
console.log('✅ Email sent successfully after STARTTLS upgrade');
|
|
||||||
console.log('📧 Message ID:', result.messageId);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-03: STARTTLS Upgrade - should handle servers without STARTTLS', async () => {
|
|
||||||
// Start a server without TLS support
|
|
||||||
const plainServer = await startTestServer({
|
|
||||||
port: 2529,
|
|
||||||
tlsEnabled: false // No STARTTLS support
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const plainClient = createSmtpClient({
|
|
||||||
host: plainServer.hostname,
|
|
||||||
port: plainServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should still connect but without TLS
|
|
||||||
const isConnected = await plainClient.verify();
|
|
||||||
expect(isConnected).toBeTrue();
|
|
||||||
|
|
||||||
// Send test email over plain connection
|
|
||||||
const email = new Email({
|
|
||||||
from: 'test@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Plain Connection Test',
|
|
||||||
text: 'This email was sent over plain connection'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await plainClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
|
|
||||||
await plainClient.close();
|
|
||||||
console.log('✅ Successfully handled server without STARTTLS');
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
await stopTestServer(plainServer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-03: STARTTLS Upgrade - should respect TLS options during upgrade', async () => {
|
|
||||||
const customTlsClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false, // Start plain
|
|
||||||
connectionTimeout: 10000,
|
|
||||||
tls: {
|
|
||||||
rejectUnauthorized: false
|
|
||||||
// Removed specific TLS version and cipher requirements that might not be supported
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const isConnected = await customTlsClient.verify();
|
|
||||||
expect(isConnected).toBeTrue();
|
|
||||||
|
|
||||||
// Test that we can send email with custom TLS client
|
|
||||||
const email = new Email({
|
|
||||||
from: 'tls-test@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Custom TLS Options Test',
|
|
||||||
text: 'Testing with custom TLS configuration'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await customTlsClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
|
|
||||||
await customTlsClient.close();
|
|
||||||
console.log('✅ Custom TLS options applied during STARTTLS upgrade');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-03: STARTTLS Upgrade - should handle upgrade failures gracefully', async () => {
|
|
||||||
// Create a scenario where STARTTLS might fail
|
|
||||||
// verify() returns false on failure, doesn't throw
|
|
||||||
|
|
||||||
const strictTlsClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
tls: {
|
|
||||||
rejectUnauthorized: true, // Strict validation with self-signed cert
|
|
||||||
servername: 'wrong.hostname.com' // Wrong hostname
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should return false due to certificate validation failure
|
|
||||||
const result = await strictTlsClient.verify();
|
|
||||||
expect(result).toBeFalse();
|
|
||||||
|
|
||||||
await strictTlsClient.close();
|
|
||||||
console.log('✅ STARTTLS upgrade failure handled gracefully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-03: STARTTLS Upgrade - should maintain connection state after upgrade', async () => {
|
|
||||||
const stateClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 10000,
|
|
||||||
tls: {
|
|
||||||
rejectUnauthorized: false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// verify() closes the connection after testing, so isConnected will be false
|
|
||||||
const verified = await stateClient.verify();
|
|
||||||
expect(verified).toBeTrue();
|
|
||||||
expect(stateClient.isConnected()).toBeFalse(); // Connection closed after verify
|
|
||||||
|
|
||||||
// Send multiple emails to verify connection pooling works correctly
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'test@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: `STARTTLS State Test ${i + 1}`,
|
|
||||||
text: `Message ${i + 1} after STARTTLS upgrade`
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await stateClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check pool status to understand connection management
|
|
||||||
const poolStatus = stateClient.getPoolStatus();
|
|
||||||
console.log('Connection pool status:', poolStatus);
|
|
||||||
|
|
||||||
await stateClient.close();
|
|
||||||
console.log('✅ Connection state maintained after STARTTLS upgrade');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - close SMTP client', async () => {
|
|
||||||
if (smtpClient && smtpClient.isConnected()) {
|
|
||||||
await smtpClient.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - stop SMTP server', async () => {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createPooledSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
let pooledClient: SmtpClient;
|
|
||||||
|
|
||||||
tap.test('setup - start SMTP server for pooling test', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2530,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false,
|
|
||||||
maxConnections: 10
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(testServer.port).toEqual(2530);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-04: Connection Pooling - should create pooled client', async () => {
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Create pooled SMTP client
|
|
||||||
pooledClient = createPooledSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
maxConnections: 5,
|
|
||||||
maxMessages: 100,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Verify connection pool is working
|
|
||||||
const isConnected = await pooledClient.verify();
|
|
||||||
expect(isConnected).toBeTrue();
|
|
||||||
|
|
||||||
const poolStatus = pooledClient.getPoolStatus();
|
|
||||||
console.log('📊 Initial pool status:', poolStatus);
|
|
||||||
expect(poolStatus.total).toBeGreaterThanOrEqual(0);
|
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
console.log(`✅ Connection pool created in ${duration}ms`);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
console.error(`❌ Connection pool creation failed after ${duration}ms:`, error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-04: Connection Pooling - should handle concurrent connections', async () => {
|
|
||||||
// Send multiple emails concurrently
|
|
||||||
const emailPromises = [];
|
|
||||||
const concurrentCount = 5;
|
|
||||||
|
|
||||||
for (let i = 0; i < concurrentCount; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'test@example.com',
|
|
||||||
to: `recipient${i}@example.com`,
|
|
||||||
subject: `Concurrent Email ${i}`,
|
|
||||||
text: `This is concurrent email number ${i}`
|
|
||||||
});
|
|
||||||
|
|
||||||
emailPromises.push(
|
|
||||||
pooledClient.sendMail(email).catch(error => {
|
|
||||||
console.error(`❌ Failed to send email ${i}:`, error);
|
|
||||||
return { success: false, error: error.message, acceptedRecipients: [] };
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for all emails to be sent
|
|
||||||
const results = await Promise.all(emailPromises);
|
|
||||||
|
|
||||||
// Check results and count successes
|
|
||||||
let successCount = 0;
|
|
||||||
results.forEach((result, index) => {
|
|
||||||
if (result.success) {
|
|
||||||
successCount++;
|
|
||||||
expect(result.acceptedRecipients).toContain(`recipient${index}@example.com`);
|
|
||||||
} else {
|
|
||||||
console.log(`Email ${index} failed:`, result.error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// At least some emails should succeed with pooling
|
|
||||||
expect(successCount).toBeGreaterThan(0);
|
|
||||||
console.log(`✅ Sent ${successCount}/${concurrentCount} emails successfully`);
|
|
||||||
|
|
||||||
// Check pool status after concurrent sends
|
|
||||||
const poolStatus = pooledClient.getPoolStatus();
|
|
||||||
console.log('📊 Pool status after concurrent sends:', poolStatus);
|
|
||||||
expect(poolStatus.total).toBeGreaterThanOrEqual(1);
|
|
||||||
expect(poolStatus.total).toBeLessThanOrEqual(5); // Should not exceed max
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-04: Connection Pooling - should reuse connections', async () => {
|
|
||||||
// Get initial pool status
|
|
||||||
const initialStatus = pooledClient.getPoolStatus();
|
|
||||||
console.log('📊 Initial status:', initialStatus);
|
|
||||||
|
|
||||||
// Send emails sequentially to test connection reuse
|
|
||||||
const emailCount = 10;
|
|
||||||
const connectionCounts = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < emailCount; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'test@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: `Sequential Email ${i}`,
|
|
||||||
text: `Testing connection reuse - email ${i}`
|
|
||||||
});
|
|
||||||
|
|
||||||
await pooledClient.sendMail(email);
|
|
||||||
|
|
||||||
const status = pooledClient.getPoolStatus();
|
|
||||||
connectionCounts.push(status.total);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that connections were reused (total shouldn't grow linearly)
|
|
||||||
const maxConnections = Math.max(...connectionCounts);
|
|
||||||
expect(maxConnections).toBeLessThan(emailCount); // Should reuse connections
|
|
||||||
|
|
||||||
console.log(`✅ Sent ${emailCount} emails using max ${maxConnections} connections`);
|
|
||||||
console.log('📊 Connection counts:', connectionCounts);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-04: Connection Pooling - should respect max connections limit', async () => {
|
|
||||||
// Create a client with small pool
|
|
||||||
const limitedClient = createPooledSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
maxConnections: 2, // Very small pool
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send many concurrent emails
|
|
||||||
const emailPromises = [];
|
|
||||||
for (let i = 0; i < 10; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'test@example.com',
|
|
||||||
to: `test${i}@example.com`,
|
|
||||||
subject: `Pool Limit Test ${i}`,
|
|
||||||
text: 'Testing pool limits'
|
|
||||||
});
|
|
||||||
emailPromises.push(limitedClient.sendMail(email));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Monitor pool during sending
|
|
||||||
const checkInterval = setInterval(() => {
|
|
||||||
const status = limitedClient.getPoolStatus();
|
|
||||||
console.log('📊 Pool status during load:', status);
|
|
||||||
expect(status.total).toBeLessThanOrEqual(2); // Should never exceed max
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
await Promise.all(emailPromises);
|
|
||||||
clearInterval(checkInterval);
|
|
||||||
|
|
||||||
await limitedClient.close();
|
|
||||||
console.log('✅ Connection pool respected max connections limit');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-04: Connection Pooling - should handle connection failures in pool', async () => {
|
|
||||||
// Create a new pooled client
|
|
||||||
const resilientClient = createPooledSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
maxConnections: 3,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send some emails successfully
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'test@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: `Pre-failure Email ${i}`,
|
|
||||||
text: 'Before simulated failure'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await resilientClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pool should recover and continue working
|
|
||||||
const poolStatus = resilientClient.getPoolStatus();
|
|
||||||
console.log('📊 Pool status after recovery test:', poolStatus);
|
|
||||||
expect(poolStatus.total).toBeGreaterThanOrEqual(1);
|
|
||||||
|
|
||||||
await resilientClient.close();
|
|
||||||
console.log('✅ Connection pool handled failures gracefully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-04: Connection Pooling - should clean up idle connections', async () => {
|
|
||||||
// Create client with specific idle settings
|
|
||||||
const idleClient = createPooledSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
maxConnections: 5,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send burst of emails
|
|
||||||
const promises = [];
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'test@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: `Idle Test ${i}`,
|
|
||||||
text: 'Testing idle cleanup'
|
|
||||||
});
|
|
||||||
promises.push(idleClient.sendMail(email));
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(promises);
|
|
||||||
|
|
||||||
const activeStatus = idleClient.getPoolStatus();
|
|
||||||
console.log('📊 Pool status after burst:', activeStatus);
|
|
||||||
|
|
||||||
// Wait for connections to become idle
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
||||||
|
|
||||||
const idleStatus = idleClient.getPoolStatus();
|
|
||||||
console.log('📊 Pool status after idle period:', idleStatus);
|
|
||||||
|
|
||||||
await idleClient.close();
|
|
||||||
console.log('✅ Idle connection management working');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - close pooled client', async () => {
|
|
||||||
if (pooledClient && pooledClient.isConnected()) {
|
|
||||||
await pooledClient.close();
|
|
||||||
|
|
||||||
// Verify pool is cleaned up
|
|
||||||
const finalStatus = pooledClient.getPoolStatus();
|
|
||||||
console.log('📊 Final pool status:', finalStatus);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - stop SMTP server', async () => {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,288 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
let smtpClient: SmtpClient;
|
|
||||||
|
|
||||||
tap.test('setup - start SMTP server for connection reuse test', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2531,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(testServer.port).toEqual(2531);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-05: Connection Reuse - should reuse single connection for multiple emails', async () => {
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Verify initial connection
|
|
||||||
const verified = await smtpClient.verify();
|
|
||||||
expect(verified).toBeTrue();
|
|
||||||
// Note: verify() closes the connection, so isConnected() will be false
|
|
||||||
|
|
||||||
// Send multiple emails on same connection
|
|
||||||
const emailCount = 5;
|
|
||||||
const results = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < emailCount; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: `Connection Reuse Test ${i + 1}`,
|
|
||||||
text: `This is email ${i + 1} using the same connection`
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
results.push(result);
|
|
||||||
|
|
||||||
// Note: Connection state may vary depending on implementation
|
|
||||||
console.log(`Connection status after email ${i + 1}: ${smtpClient.isConnected() ? 'connected' : 'disconnected'}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// All emails should succeed
|
|
||||||
results.forEach((result, index) => {
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log(`✅ Email ${index + 1} sent successfully`);
|
|
||||||
});
|
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
console.log(`✅ Sent ${emailCount} emails on single connection in ${duration}ms`);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-05: Connection Reuse - should track message count per connection', async () => {
|
|
||||||
// Create a new client with message limit
|
|
||||||
const limitedClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
maxMessages: 3, // Limit messages per connection
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send emails up to and beyond the limit
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: `Message Limit Test ${i + 1}`,
|
|
||||||
text: `Testing message limits`
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await limitedClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
|
|
||||||
// After 3 messages, connection should be refreshed
|
|
||||||
if (i === 2) {
|
|
||||||
console.log('✅ Connection should refresh after message limit');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await limitedClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-05: Connection Reuse - should handle connection state changes', async () => {
|
|
||||||
// Test connection state management
|
|
||||||
const stateClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// First email
|
|
||||||
const email1 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'First Email',
|
|
||||||
text: 'Testing connection state'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result1 = await stateClient.sendMail(email1);
|
|
||||||
expect(result1.success).toBeTrue();
|
|
||||||
|
|
||||||
// Second email
|
|
||||||
const email2 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Second Email',
|
|
||||||
text: 'Testing connection reuse'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result2 = await stateClient.sendMail(email2);
|
|
||||||
expect(result2.success).toBeTrue();
|
|
||||||
|
|
||||||
await stateClient.close();
|
|
||||||
console.log('✅ Connection state handled correctly');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-05: Connection Reuse - should handle idle connection timeout', async () => {
|
|
||||||
const idleClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
socketTimeout: 3000 // Short timeout for testing
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send first email
|
|
||||||
const email1 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Pre-idle Email',
|
|
||||||
text: 'Before idle period'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result1 = await idleClient.sendMail(email1);
|
|
||||||
expect(result1.success).toBeTrue();
|
|
||||||
|
|
||||||
// Wait for potential idle timeout
|
|
||||||
console.log('⏳ Testing idle connection behavior...');
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
|
||||||
|
|
||||||
// Send another email
|
|
||||||
const email2 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Post-idle Email',
|
|
||||||
text: 'After idle period'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should handle reconnection if needed
|
|
||||||
const result = await idleClient.sendMail(email2);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
|
|
||||||
await idleClient.close();
|
|
||||||
console.log('✅ Idle connection handling working correctly');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-05: Connection Reuse - should optimize performance with reuse', async () => {
|
|
||||||
// Compare performance with and without connection reuse
|
|
||||||
|
|
||||||
// Test 1: Multiple connections (no reuse)
|
|
||||||
const noReuseStart = Date.now();
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const tempClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: `No Reuse ${i}`,
|
|
||||||
text: 'Testing without reuse'
|
|
||||||
});
|
|
||||||
|
|
||||||
await tempClient.sendMail(email);
|
|
||||||
await tempClient.close();
|
|
||||||
}
|
|
||||||
const noReuseDuration = Date.now() - noReuseStart;
|
|
||||||
|
|
||||||
// Test 2: Single connection (with reuse)
|
|
||||||
const reuseStart = Date.now();
|
|
||||||
const reuseClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: `With Reuse ${i}`,
|
|
||||||
text: 'Testing with reuse'
|
|
||||||
});
|
|
||||||
|
|
||||||
await reuseClient.sendMail(email);
|
|
||||||
}
|
|
||||||
|
|
||||||
await reuseClient.close();
|
|
||||||
const reuseDuration = Date.now() - reuseStart;
|
|
||||||
|
|
||||||
console.log(`📊 Performance comparison:`);
|
|
||||||
console.log(` Without reuse: ${noReuseDuration}ms`);
|
|
||||||
console.log(` With reuse: ${reuseDuration}ms`);
|
|
||||||
console.log(` Improvement: ${Math.round((1 - reuseDuration/noReuseDuration) * 100)}%`);
|
|
||||||
|
|
||||||
// Both approaches should work, performance may vary based on implementation
|
|
||||||
// Connection reuse doesn't always guarantee better performance for local connections
|
|
||||||
expect(noReuseDuration).toBeGreaterThan(0);
|
|
||||||
expect(reuseDuration).toBeGreaterThan(0);
|
|
||||||
console.log('✅ Both connection strategies completed successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-05: Connection Reuse - should handle errors without breaking reuse', async () => {
|
|
||||||
const resilientClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send valid email
|
|
||||||
const validEmail = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Valid Email',
|
|
||||||
text: 'This should work'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result1 = await resilientClient.sendMail(validEmail);
|
|
||||||
expect(result1.success).toBeTrue();
|
|
||||||
|
|
||||||
// Try to send invalid email
|
|
||||||
try {
|
|
||||||
const invalidEmail = new Email({
|
|
||||||
from: 'invalid sender format',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Invalid Email',
|
|
||||||
text: 'This should fail'
|
|
||||||
});
|
|
||||||
await resilientClient.sendMail(invalidEmail);
|
|
||||||
} catch (error) {
|
|
||||||
console.log('✅ Invalid email rejected as expected');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connection should still be usable
|
|
||||||
const validEmail2 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Valid Email After Error',
|
|
||||||
text: 'Connection should still work'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result2 = await resilientClient.sendMail(validEmail2);
|
|
||||||
expect(result2.success).toBeTrue();
|
|
||||||
|
|
||||||
await resilientClient.close();
|
|
||||||
console.log('✅ Connection reuse survived error condition');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - close SMTP client', async () => {
|
|
||||||
if (smtpClient && smtpClient.isConnected()) {
|
|
||||||
await smtpClient.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - stop SMTP server', async () => {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.start();
|
|
||||||
@@ -1,267 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
import * as net from 'net';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup - start SMTP server for timeout tests', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2532,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(testServer.port).toEqual(2532);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-06: Connection Timeout - should timeout on unresponsive server', async () => {
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
const timeoutClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: 9999, // Non-existent port
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 2000, // 2 second timeout
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// verify() returns false on connection failure, doesn't throw
|
|
||||||
const verified = await timeoutClient.verify();
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
|
|
||||||
expect(verified).toBeFalse();
|
|
||||||
expect(duration).toBeLessThan(3000); // Should timeout within 3s
|
|
||||||
|
|
||||||
console.log(`✅ Connection timeout after ${duration}ms`);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-06: Connection Timeout - should handle slow server response', async () => {
|
|
||||||
// Create a mock slow server
|
|
||||||
const slowServer = net.createServer((socket) => {
|
|
||||||
// Accept connection but delay response
|
|
||||||
setTimeout(() => {
|
|
||||||
socket.write('220 Slow server ready\r\n');
|
|
||||||
}, 3000); // 3 second delay
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
slowServer.listen(2533, () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
const slowClient = createSmtpClient({
|
|
||||||
host: 'localhost',
|
|
||||||
port: 2533,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 1000, // 1 second timeout
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// verify() should return false when server is too slow
|
|
||||||
const verified = await slowClient.verify();
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
|
|
||||||
expect(verified).toBeFalse();
|
|
||||||
// Note: actual timeout might be longer due to system defaults
|
|
||||||
console.log(`✅ Slow server timeout after ${duration}ms`);
|
|
||||||
|
|
||||||
slowServer.close();
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-06: Connection Timeout - should respect socket timeout during data transfer', async () => {
|
|
||||||
const socketTimeoutClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
socketTimeout: 10000, // 10 second socket timeout
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
await socketTimeoutClient.verify();
|
|
||||||
|
|
||||||
// Send a normal email
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Socket Timeout Test',
|
|
||||||
text: 'Testing socket timeout configuration'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await socketTimeoutClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
|
|
||||||
await socketTimeoutClient.close();
|
|
||||||
console.log('✅ Socket timeout configuration applied');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-06: Connection Timeout - should handle timeout during TLS handshake', async () => {
|
|
||||||
// Create a server that accepts connections but doesn't complete TLS
|
|
||||||
const badTlsServer = net.createServer((socket) => {
|
|
||||||
// Accept connection but don't respond to TLS
|
|
||||||
socket.on('data', () => {
|
|
||||||
// Do nothing - simulate hung TLS handshake
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
badTlsServer.listen(2534, () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
const tlsTimeoutClient = createSmtpClient({
|
|
||||||
host: 'localhost',
|
|
||||||
port: 2534,
|
|
||||||
secure: true, // Try TLS
|
|
||||||
connectionTimeout: 2000,
|
|
||||||
tls: {
|
|
||||||
rejectUnauthorized: false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// verify() should return false when TLS handshake times out
|
|
||||||
const verified = await tlsTimeoutClient.verify();
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
|
|
||||||
expect(verified).toBeFalse();
|
|
||||||
// Note: actual timeout might be longer due to system defaults
|
|
||||||
console.log(`✅ TLS handshake timeout after ${duration}ms`);
|
|
||||||
|
|
||||||
badTlsServer.close();
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-06: Connection Timeout - should not timeout on successful quick connection', async () => {
|
|
||||||
const quickClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 30000, // Very long timeout
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
const isConnected = await quickClient.verify();
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
|
|
||||||
expect(isConnected).toBeTrue();
|
|
||||||
expect(duration).toBeLessThan(5000); // Should connect quickly
|
|
||||||
|
|
||||||
await quickClient.close();
|
|
||||||
console.log(`✅ Quick connection established in ${duration}ms`);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-06: Connection Timeout - should handle timeout during authentication', async () => {
|
|
||||||
// Start auth server
|
|
||||||
const authServer = await startTestServer({
|
|
||||||
port: 2535,
|
|
||||||
authRequired: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create mock auth that delays
|
|
||||||
const authTimeoutClient = createSmtpClient({
|
|
||||||
host: authServer.hostname,
|
|
||||||
port: authServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
socketTimeout: 1000, // Very short socket timeout
|
|
||||||
auth: {
|
|
||||||
user: 'testuser',
|
|
||||||
pass: 'testpass'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await authTimeoutClient.verify();
|
|
||||||
// If this succeeds, auth was fast enough
|
|
||||||
await authTimeoutClient.close();
|
|
||||||
console.log('✅ Authentication completed within timeout');
|
|
||||||
} catch (error) {
|
|
||||||
console.log('✅ Authentication timeout handled');
|
|
||||||
}
|
|
||||||
|
|
||||||
await stopTestServer(authServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-06: Connection Timeout - should apply different timeouts for different operations', async () => {
|
|
||||||
const multiTimeoutClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000, // Connection establishment
|
|
||||||
socketTimeout: 30000, // Data operations
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Connection should be quick
|
|
||||||
const connectStart = Date.now();
|
|
||||||
await multiTimeoutClient.verify();
|
|
||||||
const connectDuration = Date.now() - connectStart;
|
|
||||||
|
|
||||||
expect(connectDuration).toBeLessThan(5000);
|
|
||||||
|
|
||||||
// Send email with potentially longer operation
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Multi-timeout Test',
|
|
||||||
text: 'Testing different timeout values',
|
|
||||||
attachments: [{
|
|
||||||
filename: 'test.txt',
|
|
||||||
content: Buffer.from('Test content'),
|
|
||||||
contentType: 'text/plain'
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
const sendStart = Date.now();
|
|
||||||
const result = await multiTimeoutClient.sendMail(email);
|
|
||||||
const sendDuration = Date.now() - sendStart;
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log(`✅ Different timeouts applied: connect=${connectDuration}ms, send=${sendDuration}ms`);
|
|
||||||
|
|
||||||
await multiTimeoutClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-06: Connection Timeout - should retry after timeout with pooled connections', async () => {
|
|
||||||
const retryClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
pool: true,
|
|
||||||
maxConnections: 2,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// First connection should succeed
|
|
||||||
const email1 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Pre-timeout Email',
|
|
||||||
text: 'Before any timeout'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result1 = await retryClient.sendMail(email1);
|
|
||||||
expect(result1.success).toBeTrue();
|
|
||||||
|
|
||||||
// Pool should handle connection management
|
|
||||||
const poolStatus = retryClient.getPoolStatus();
|
|
||||||
console.log('📊 Pool status:', poolStatus);
|
|
||||||
|
|
||||||
await retryClient.close();
|
|
||||||
console.log('✅ Connection pool handles timeouts gracefully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - stop SMTP server', async () => {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,324 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient, createPooledSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
import * as net from 'net';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup - start SMTP server for reconnection tests', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2533,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(testServer.port).toEqual(2533);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-07: Automatic Reconnection - should reconnect after connection loss', async () => {
|
|
||||||
const client = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// First connection and email
|
|
||||||
const email1 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Before Disconnect',
|
|
||||||
text: 'First email before connection loss'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result1 = await client.sendMail(email1);
|
|
||||||
expect(result1.success).toBeTrue();
|
|
||||||
// Note: Connection state may vary after sending
|
|
||||||
|
|
||||||
// Force disconnect
|
|
||||||
await client.close();
|
|
||||||
expect(client.isConnected()).toBeFalse();
|
|
||||||
|
|
||||||
// Try to send another email - should auto-reconnect
|
|
||||||
const email2 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'After Reconnect',
|
|
||||||
text: 'Email after automatic reconnection'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result2 = await client.sendMail(email2);
|
|
||||||
expect(result2.success).toBeTrue();
|
|
||||||
// Connection successfully handled reconnection
|
|
||||||
|
|
||||||
await client.close();
|
|
||||||
console.log('✅ Automatic reconnection successful');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-07: Automatic Reconnection - pooled client should reconnect failed connections', async () => {
|
|
||||||
const pooledClient = createPooledSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
maxConnections: 3,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send emails to establish pool connections
|
|
||||||
const promises = [];
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: `recipient${i}@example.com`,
|
|
||||||
subject: `Pool Test ${i}`,
|
|
||||||
text: 'Testing connection pool'
|
|
||||||
});
|
|
||||||
promises.push(
|
|
||||||
pooledClient.sendMail(email).catch(error => {
|
|
||||||
console.error(`Failed to send initial email ${i}:`, error.message);
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(promises);
|
|
||||||
|
|
||||||
const poolStatus1 = pooledClient.getPoolStatus();
|
|
||||||
console.log('📊 Pool status before disruption:', poolStatus1);
|
|
||||||
|
|
||||||
// Send more emails - pool should handle any connection issues
|
|
||||||
const promises2 = [];
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: `recipient${i}@example.com`,
|
|
||||||
subject: `Pool Recovery ${i}`,
|
|
||||||
text: 'Testing pool recovery'
|
|
||||||
});
|
|
||||||
promises2.push(
|
|
||||||
pooledClient.sendMail(email).catch(error => {
|
|
||||||
console.error(`Failed to send email ${i}:`, error.message);
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = await Promise.all(promises2);
|
|
||||||
let successCount = 0;
|
|
||||||
results.forEach(result => {
|
|
||||||
if (result.success) {
|
|
||||||
successCount++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// At least some emails should succeed
|
|
||||||
expect(successCount).toBeGreaterThan(0);
|
|
||||||
console.log(`✅ Pool recovery: ${successCount}/${results.length} emails succeeded`);
|
|
||||||
|
|
||||||
const poolStatus2 = pooledClient.getPoolStatus();
|
|
||||||
console.log('📊 Pool status after recovery:', poolStatus2);
|
|
||||||
|
|
||||||
await pooledClient.close();
|
|
||||||
console.log('✅ Connection pool handles reconnection automatically');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-07: Automatic Reconnection - should handle server restart', async () => {
|
|
||||||
// Create client
|
|
||||||
const client = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send first email
|
|
||||||
const email1 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Before Server Restart',
|
|
||||||
text: 'Email before server restart'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result1 = await client.sendMail(email1);
|
|
||||||
expect(result1.success).toBeTrue();
|
|
||||||
|
|
||||||
// Simulate server restart
|
|
||||||
console.log('🔄 Simulating server restart...');
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
|
|
||||||
// Restart server on same port
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2533,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Try to send another email
|
|
||||||
const email2 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'After Server Restart',
|
|
||||||
text: 'Email after server restart'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result2 = await client.sendMail(email2);
|
|
||||||
expect(result2.success).toBeTrue();
|
|
||||||
|
|
||||||
await client.close();
|
|
||||||
console.log('✅ Client recovered from server restart');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-07: Automatic Reconnection - should handle network interruption', async () => {
|
|
||||||
const client = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
socketTimeout: 10000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Establish connection
|
|
||||||
await client.verify();
|
|
||||||
|
|
||||||
// Send emails with simulated network issues
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: `Network Test ${i}`,
|
|
||||||
text: `Testing network resilience ${i}`
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await client.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log(`✅ Email ${i + 1} sent successfully`);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`⚠️ Email ${i + 1} failed, will retry`);
|
|
||||||
// Client should recover on next attempt
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add small delay between sends
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
}
|
|
||||||
|
|
||||||
await client.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-07: Automatic Reconnection - should limit reconnection attempts', async () => {
|
|
||||||
// Connect to a port that will be closed
|
|
||||||
const tempServer = net.createServer();
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
tempServer.listen(2534, () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const client = createSmtpClient({
|
|
||||||
host: 'localhost',
|
|
||||||
port: 2534,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 2000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close the server to simulate failure
|
|
||||||
tempServer.close();
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
|
|
||||||
let failureCount = 0;
|
|
||||||
const maxAttempts = 3;
|
|
||||||
|
|
||||||
// Try multiple times
|
|
||||||
for (let i = 0; i < maxAttempts; i++) {
|
|
||||||
const verified = await client.verify();
|
|
||||||
if (!verified) {
|
|
||||||
failureCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(failureCount).toEqual(maxAttempts);
|
|
||||||
console.log('✅ Reconnection attempts are limited to prevent infinite loops');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-07: Automatic Reconnection - should maintain state after reconnect', async () => {
|
|
||||||
const client = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send email with specific settings
|
|
||||||
const email1 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'State Test 1',
|
|
||||||
text: 'Testing state persistence',
|
|
||||||
priority: 'high',
|
|
||||||
headers: {
|
|
||||||
'X-Test-ID': 'test-123'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result1 = await client.sendMail(email1);
|
|
||||||
expect(result1.success).toBeTrue();
|
|
||||||
|
|
||||||
// Force reconnection
|
|
||||||
await client.close();
|
|
||||||
|
|
||||||
// Send another email - client state should be maintained
|
|
||||||
const email2 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'State Test 2',
|
|
||||||
text: 'After reconnection',
|
|
||||||
priority: 'high',
|
|
||||||
headers: {
|
|
||||||
'X-Test-ID': 'test-456'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result2 = await client.sendMail(email2);
|
|
||||||
expect(result2.success).toBeTrue();
|
|
||||||
|
|
||||||
await client.close();
|
|
||||||
console.log('✅ Client state maintained after reconnection');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-07: Automatic Reconnection - should handle rapid reconnections', async () => {
|
|
||||||
const client = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Rapid connect/disconnect cycles
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: `Rapid Test ${i}`,
|
|
||||||
text: 'Testing rapid reconnections'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await client.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
|
|
||||||
// Force disconnect
|
|
||||||
await client.close();
|
|
||||||
|
|
||||||
// No delay - immediate next attempt
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✅ Rapid reconnections handled successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - stop SMTP server', async () => {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import * as dns from 'dns';
|
|
||||||
import { promisify } from 'util';
|
|
||||||
|
|
||||||
const resolveMx = promisify(dns.resolveMx);
|
|
||||||
const resolve4 = promisify(dns.resolve4);
|
|
||||||
const resolve6 = promisify(dns.resolve6);
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup test SMTP server', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2534,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
expect(testServer).toBeTruthy();
|
|
||||||
expect(testServer.port).toEqual(2534);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-08: DNS resolution and MX record lookup', async () => {
|
|
||||||
// Test basic DNS resolution
|
|
||||||
try {
|
|
||||||
const ipv4Addresses = await resolve4('example.com');
|
|
||||||
expect(ipv4Addresses).toBeArray();
|
|
||||||
expect(ipv4Addresses.length).toBeGreaterThan(0);
|
|
||||||
console.log('IPv4 addresses for example.com:', ipv4Addresses);
|
|
||||||
} catch (error) {
|
|
||||||
console.log('IPv4 resolution failed (may be expected in test environment):', error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test IPv6 resolution
|
|
||||||
try {
|
|
||||||
const ipv6Addresses = await resolve6('example.com');
|
|
||||||
expect(ipv6Addresses).toBeArray();
|
|
||||||
console.log('IPv6 addresses for example.com:', ipv6Addresses);
|
|
||||||
} catch (error) {
|
|
||||||
console.log('IPv6 resolution failed (common for many domains):', error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test MX record lookup
|
|
||||||
try {
|
|
||||||
const mxRecords = await resolveMx('example.com');
|
|
||||||
expect(mxRecords).toBeArray();
|
|
||||||
if (mxRecords.length > 0) {
|
|
||||||
expect(mxRecords[0]).toHaveProperty('priority');
|
|
||||||
expect(mxRecords[0]).toHaveProperty('exchange');
|
|
||||||
console.log('MX records for example.com:', mxRecords);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('MX record lookup failed (may be expected in test environment):', error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test local resolution (should work in test environment)
|
|
||||||
try {
|
|
||||||
const localhostIpv4 = await resolve4('localhost');
|
|
||||||
expect(localhostIpv4).toContain('127.0.0.1');
|
|
||||||
} catch (error) {
|
|
||||||
// Fallback for environments where localhost doesn't resolve via DNS
|
|
||||||
console.log('Localhost DNS resolution not available, using direct IP');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test invalid domain handling
|
|
||||||
try {
|
|
||||||
await resolve4('this-domain-definitely-does-not-exist-12345.com');
|
|
||||||
expect(true).toBeFalsy(); // Should not reach here
|
|
||||||
} catch (error) {
|
|
||||||
expect(error.code).toMatch(/ENOTFOUND|ENODATA/);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test MX record priority sorting
|
|
||||||
const mockMxRecords = [
|
|
||||||
{ priority: 20, exchange: 'mx2.example.com' },
|
|
||||||
{ priority: 10, exchange: 'mx1.example.com' },
|
|
||||||
{ priority: 30, exchange: 'mx3.example.com' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const sortedRecords = mockMxRecords.sort((a, b) => a.priority - b.priority);
|
|
||||||
expect(sortedRecords[0].exchange).toEqual('mx1.example.com');
|
|
||||||
expect(sortedRecords[1].exchange).toEqual('mx2.example.com');
|
|
||||||
expect(sortedRecords[2].exchange).toEqual('mx3.example.com');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-08: DNS caching behavior', async () => {
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
// First resolution (cold cache)
|
|
||||||
try {
|
|
||||||
await resolve4('example.com');
|
|
||||||
} catch (error) {
|
|
||||||
// Ignore errors, we're testing timing
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstResolutionTime = Date.now() - startTime;
|
|
||||||
|
|
||||||
// Second resolution (potentially cached)
|
|
||||||
const secondStartTime = Date.now();
|
|
||||||
try {
|
|
||||||
await resolve4('example.com');
|
|
||||||
} catch (error) {
|
|
||||||
// Ignore errors, we're testing timing
|
|
||||||
}
|
|
||||||
|
|
||||||
const secondResolutionTime = Date.now() - secondStartTime;
|
|
||||||
|
|
||||||
console.log(`First resolution: ${firstResolutionTime}ms, Second resolution: ${secondResolutionTime}ms`);
|
|
||||||
|
|
||||||
// Note: We can't guarantee caching behavior in all environments
|
|
||||||
// so we just log the times for manual inspection
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-08: Multiple A record handling', async () => {
|
|
||||||
// Test handling of domains with multiple A records
|
|
||||||
try {
|
|
||||||
const googleIps = await resolve4('google.com');
|
|
||||||
if (googleIps.length > 1) {
|
|
||||||
expect(googleIps).toBeArray();
|
|
||||||
expect(googleIps.length).toBeGreaterThan(1);
|
|
||||||
console.log('Multiple A records found for google.com:', googleIps);
|
|
||||||
|
|
||||||
// Verify all are valid IPv4 addresses
|
|
||||||
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
|
|
||||||
for (const ip of googleIps) {
|
|
||||||
expect(ip).toMatch(ipv4Regex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Could not resolve google.com:', error.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup test SMTP server', async () => {
|
|
||||||
if (testServer) {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import * as net from 'net';
|
|
||||||
import * as os from 'os';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup test SMTP server', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2535,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
expect(testServer).toBeTruthy();
|
|
||||||
expect(testServer.port).toEqual(2535);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-09: Check system IPv6 support', async () => {
|
|
||||||
const networkInterfaces = os.networkInterfaces();
|
|
||||||
let hasIPv6 = false;
|
|
||||||
|
|
||||||
for (const interfaceName in networkInterfaces) {
|
|
||||||
const interfaces = networkInterfaces[interfaceName];
|
|
||||||
if (interfaces) {
|
|
||||||
for (const iface of interfaces) {
|
|
||||||
if (iface.family === 'IPv6' && !iface.internal) {
|
|
||||||
hasIPv6 = true;
|
|
||||||
console.log(`Found IPv6 address: ${iface.address} on ${interfaceName}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`System has IPv6 support: ${hasIPv6}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-09: IPv4 connection test', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1', // Explicit IPv4
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test connection using verify
|
|
||||||
const verified = await smtpClient.verify();
|
|
||||||
expect(verified).toBeTrue();
|
|
||||||
|
|
||||||
console.log('Successfully connected via IPv4');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-09: IPv6 connection test (if supported)', async () => {
|
|
||||||
// Check if IPv6 is available
|
|
||||||
const hasIPv6 = await new Promise<boolean>((resolve) => {
|
|
||||||
const testSocket = net.createConnection({
|
|
||||||
host: '::1',
|
|
||||||
port: 1, // Any port, will fail but tells us if IPv6 works
|
|
||||||
timeout: 100
|
|
||||||
});
|
|
||||||
|
|
||||||
testSocket.on('error', (err: any) => {
|
|
||||||
// ECONNREFUSED means IPv6 works but port is closed (expected)
|
|
||||||
// ENETUNREACH or EAFNOSUPPORT means IPv6 not available
|
|
||||||
resolve(err.code === 'ECONNREFUSED');
|
|
||||||
});
|
|
||||||
|
|
||||||
testSocket.on('connect', () => {
|
|
||||||
testSocket.end();
|
|
||||||
resolve(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!hasIPv6) {
|
|
||||||
console.log('IPv6 not available on this system, skipping IPv6 tests');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try IPv6 connection
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '::1', // IPv6 loopback
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const verified = await smtpClient.verify();
|
|
||||||
if (verified) {
|
|
||||||
console.log('Successfully connected via IPv6');
|
|
||||||
await smtpClient.close();
|
|
||||||
} else {
|
|
||||||
console.log('IPv6 connection failed (server may not support IPv6)');
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
console.log('IPv6 connection failed (server may not support IPv6):', error.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-09: Hostname resolution preference', async () => {
|
|
||||||
// Test that client can handle hostnames that resolve to both IPv4 and IPv6
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: 'localhost', // Should resolve to both 127.0.0.1 and ::1
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const verified = await smtpClient.verify();
|
|
||||||
expect(verified).toBeTrue();
|
|
||||||
|
|
||||||
console.log('Successfully connected to localhost');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-09: Happy Eyeballs algorithm simulation', async () => {
|
|
||||||
// Test connecting to multiple addresses with preference
|
|
||||||
const addresses = ['127.0.0.1', '::1', 'localhost'];
|
|
||||||
const results: Array<{ address: string; time: number; success: boolean }> = [];
|
|
||||||
|
|
||||||
for (const address of addresses) {
|
|
||||||
const startTime = Date.now();
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: address,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 1000,
|
|
||||||
debug: false
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const verified = await smtpClient.verify();
|
|
||||||
const elapsed = Date.now() - startTime;
|
|
||||||
results.push({ address, time: elapsed, success: verified });
|
|
||||||
|
|
||||||
if (verified) {
|
|
||||||
await smtpClient.close();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
const elapsed = Date.now() - startTime;
|
|
||||||
results.push({ address, time: elapsed, success: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Connection race results:');
|
|
||||||
results.forEach(r => {
|
|
||||||
console.log(` ${r.address}: ${r.success ? 'SUCCESS' : 'FAILED'} in ${r.time}ms`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// At least one should succeed
|
|
||||||
const successfulConnections = results.filter(r => r.success);
|
|
||||||
expect(successfulConnections.length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup test SMTP server', async () => {
|
|
||||||
if (testServer) {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,305 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import * as net from 'net';
|
|
||||||
import * as http from 'http';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
let proxyServer: http.Server;
|
|
||||||
let socksProxyServer: net.Server;
|
|
||||||
|
|
||||||
tap.test('setup test SMTP server', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2536,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
expect(testServer).toBeTruthy();
|
|
||||||
expect(testServer.port).toEqual(2536);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-10: Setup HTTP CONNECT proxy', async () => {
|
|
||||||
// Create a simple HTTP CONNECT proxy
|
|
||||||
proxyServer = http.createServer();
|
|
||||||
|
|
||||||
proxyServer.on('connect', (req, clientSocket, head) => {
|
|
||||||
console.log(`Proxy CONNECT request to ${req.url}`);
|
|
||||||
|
|
||||||
const [host, port] = req.url!.split(':');
|
|
||||||
const serverSocket = net.connect(parseInt(port), host, () => {
|
|
||||||
clientSocket.write('HTTP/1.1 200 Connection Established\r\n' +
|
|
||||||
'Proxy-agent: Test-Proxy\r\n' +
|
|
||||||
'\r\n');
|
|
||||||
|
|
||||||
// Pipe data between client and server
|
|
||||||
serverSocket.pipe(clientSocket);
|
|
||||||
clientSocket.pipe(serverSocket);
|
|
||||||
});
|
|
||||||
|
|
||||||
serverSocket.on('error', (err) => {
|
|
||||||
console.error('Proxy server socket error:', err);
|
|
||||||
clientSocket.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
clientSocket.on('error', (err) => {
|
|
||||||
console.error('Proxy client socket error:', err);
|
|
||||||
serverSocket.end();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
proxyServer.listen(0, '127.0.0.1', () => {
|
|
||||||
const address = proxyServer.address() as net.AddressInfo;
|
|
||||||
console.log(`HTTP proxy listening on port ${address.port}`);
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-10: Test connection through HTTP proxy', async () => {
|
|
||||||
const proxyAddress = proxyServer.address() as net.AddressInfo;
|
|
||||||
|
|
||||||
// Note: Real SMTP clients would need proxy configuration
|
|
||||||
// This simulates what a proxy-aware SMTP client would do
|
|
||||||
const proxyOptions = {
|
|
||||||
host: proxyAddress.address,
|
|
||||||
port: proxyAddress.port,
|
|
||||||
method: 'CONNECT',
|
|
||||||
path: `127.0.0.1:${testServer.port}`,
|
|
||||||
headers: {
|
|
||||||
'Proxy-Authorization': 'Basic dGVzdDp0ZXN0' // test:test in base64
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const connected = await new Promise<boolean>((resolve) => {
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
console.log('Proxy test timed out');
|
|
||||||
resolve(false);
|
|
||||||
}, 10000); // 10 second timeout
|
|
||||||
|
|
||||||
const req = http.request(proxyOptions);
|
|
||||||
|
|
||||||
req.on('connect', (res, socket, head) => {
|
|
||||||
console.log('Connected through proxy, status:', res.statusCode);
|
|
||||||
expect(res.statusCode).toEqual(200);
|
|
||||||
|
|
||||||
// Now we have a raw socket to the SMTP server through the proxy
|
|
||||||
clearTimeout(timeout);
|
|
||||||
|
|
||||||
// For the purpose of this test, just verify we can connect through the proxy
|
|
||||||
// Real SMTP operations through proxy would require more complex handling
|
|
||||||
socket.end();
|
|
||||||
resolve(true);
|
|
||||||
|
|
||||||
socket.on('error', (err) => {
|
|
||||||
console.error('Socket error:', err);
|
|
||||||
resolve(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
req.on('error', (err) => {
|
|
||||||
console.error('Proxy request error:', err);
|
|
||||||
resolve(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
req.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(connected).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-10: Test SOCKS5 proxy simulation', async () => {
|
|
||||||
// Create a minimal SOCKS5 proxy for testing
|
|
||||||
socksProxyServer = net.createServer((clientSocket) => {
|
|
||||||
let authenticated = false;
|
|
||||||
let targetHost: string;
|
|
||||||
let targetPort: number;
|
|
||||||
|
|
||||||
clientSocket.on('data', (data) => {
|
|
||||||
if (!authenticated) {
|
|
||||||
// SOCKS5 handshake
|
|
||||||
if (data[0] === 0x05) { // SOCKS version 5
|
|
||||||
// Send back: no authentication required
|
|
||||||
clientSocket.write(Buffer.from([0x05, 0x00]));
|
|
||||||
authenticated = true;
|
|
||||||
}
|
|
||||||
} else if (!targetHost) {
|
|
||||||
// Connection request
|
|
||||||
if (data[0] === 0x05 && data[1] === 0x01) { // CONNECT command
|
|
||||||
const addressType = data[3];
|
|
||||||
|
|
||||||
if (addressType === 0x01) { // IPv4
|
|
||||||
targetHost = `${data[4]}.${data[5]}.${data[6]}.${data[7]}`;
|
|
||||||
targetPort = (data[8] << 8) + data[9];
|
|
||||||
|
|
||||||
// Connect to target
|
|
||||||
const serverSocket = net.connect(targetPort, targetHost, () => {
|
|
||||||
// Send success response
|
|
||||||
const response = Buffer.alloc(10);
|
|
||||||
response[0] = 0x05; // SOCKS version
|
|
||||||
response[1] = 0x00; // Success
|
|
||||||
response[2] = 0x00; // Reserved
|
|
||||||
response[3] = 0x01; // IPv4
|
|
||||||
response[4] = data[4]; // Copy address
|
|
||||||
response[5] = data[5];
|
|
||||||
response[6] = data[6];
|
|
||||||
response[7] = data[7];
|
|
||||||
response[8] = data[8]; // Copy port
|
|
||||||
response[9] = data[9];
|
|
||||||
|
|
||||||
clientSocket.write(response);
|
|
||||||
|
|
||||||
// Start proxying
|
|
||||||
serverSocket.pipe(clientSocket);
|
|
||||||
clientSocket.pipe(serverSocket);
|
|
||||||
});
|
|
||||||
|
|
||||||
serverSocket.on('error', (err) => {
|
|
||||||
console.error('SOCKS target connection error:', err);
|
|
||||||
clientSocket.end();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
clientSocket.on('error', (err) => {
|
|
||||||
console.error('SOCKS client error:', err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
socksProxyServer.listen(0, '127.0.0.1', () => {
|
|
||||||
const address = socksProxyServer.address() as net.AddressInfo;
|
|
||||||
console.log(`SOCKS5 proxy listening on port ${address.port}`);
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test connection through SOCKS proxy
|
|
||||||
const socksAddress = socksProxyServer.address() as net.AddressInfo;
|
|
||||||
const socksClient = net.connect(socksAddress.port, socksAddress.address);
|
|
||||||
|
|
||||||
const connected = await new Promise<boolean>((resolve) => {
|
|
||||||
let phase = 'handshake';
|
|
||||||
|
|
||||||
socksClient.on('connect', () => {
|
|
||||||
// Send SOCKS5 handshake
|
|
||||||
socksClient.write(Buffer.from([0x05, 0x01, 0x00])); // Version 5, 1 method, no auth
|
|
||||||
});
|
|
||||||
|
|
||||||
socksClient.on('data', (data) => {
|
|
||||||
if (phase === 'handshake' && data[0] === 0x05 && data[1] === 0x00) {
|
|
||||||
phase = 'connect';
|
|
||||||
// Send connection request
|
|
||||||
const connectReq = Buffer.alloc(10);
|
|
||||||
connectReq[0] = 0x05; // SOCKS version
|
|
||||||
connectReq[1] = 0x01; // CONNECT
|
|
||||||
connectReq[2] = 0x00; // Reserved
|
|
||||||
connectReq[3] = 0x01; // IPv4
|
|
||||||
connectReq[4] = 127; // 127.0.0.1
|
|
||||||
connectReq[5] = 0;
|
|
||||||
connectReq[6] = 0;
|
|
||||||
connectReq[7] = 1;
|
|
||||||
connectReq[8] = (testServer.port >> 8) & 0xFF; // Port high byte
|
|
||||||
connectReq[9] = testServer.port & 0xFF; // Port low byte
|
|
||||||
|
|
||||||
socksClient.write(connectReq);
|
|
||||||
} else if (phase === 'connect' && data[0] === 0x05 && data[1] === 0x00) {
|
|
||||||
phase = 'connected';
|
|
||||||
console.log('Connected through SOCKS5 proxy');
|
|
||||||
// Now we're connected to the SMTP server
|
|
||||||
} else if (phase === 'connected') {
|
|
||||||
const response = data.toString();
|
|
||||||
console.log('SMTP response through SOCKS:', response.trim());
|
|
||||||
if (response.includes('220')) {
|
|
||||||
socksClient.write('QUIT\r\n');
|
|
||||||
socksClient.end();
|
|
||||||
resolve(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socksClient.on('error', (err) => {
|
|
||||||
console.error('SOCKS client error:', err);
|
|
||||||
resolve(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => resolve(false), 5000); // Timeout after 5 seconds
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(connected).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-10: Test proxy authentication failure', async () => {
|
|
||||||
// Create a proxy that requires authentication
|
|
||||||
const authProxyServer = http.createServer();
|
|
||||||
|
|
||||||
authProxyServer.on('connect', (req, clientSocket, head) => {
|
|
||||||
const authHeader = req.headers['proxy-authorization'];
|
|
||||||
|
|
||||||
if (!authHeader || authHeader !== 'Basic dGVzdDp0ZXN0') {
|
|
||||||
clientSocket.write('HTTP/1.1 407 Proxy Authentication Required\r\n' +
|
|
||||||
'Proxy-Authenticate: Basic realm="Test Proxy"\r\n' +
|
|
||||||
'\r\n');
|
|
||||||
clientSocket.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authentication successful, proceed with connection
|
|
||||||
const [host, port] = req.url!.split(':');
|
|
||||||
const serverSocket = net.connect(parseInt(port), host, () => {
|
|
||||||
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
|
||||||
serverSocket.pipe(clientSocket);
|
|
||||||
clientSocket.pipe(serverSocket);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
authProxyServer.listen(0, '127.0.0.1', () => {
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const authProxyAddress = authProxyServer.address() as net.AddressInfo;
|
|
||||||
|
|
||||||
// Test without authentication
|
|
||||||
const failedAuth = await new Promise<boolean>((resolve) => {
|
|
||||||
const req = http.request({
|
|
||||||
host: authProxyAddress.address,
|
|
||||||
port: authProxyAddress.port,
|
|
||||||
method: 'CONNECT',
|
|
||||||
path: `127.0.0.1:${testServer.port}`
|
|
||||||
});
|
|
||||||
|
|
||||||
req.on('connect', () => resolve(false));
|
|
||||||
req.on('response', (res) => {
|
|
||||||
expect(res.statusCode).toEqual(407);
|
|
||||||
resolve(true);
|
|
||||||
});
|
|
||||||
req.on('error', () => resolve(false));
|
|
||||||
|
|
||||||
req.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Skip strict assertion as proxy behavior can vary
|
|
||||||
console.log('Proxy authentication test completed');
|
|
||||||
|
|
||||||
authProxyServer.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup test servers', async () => {
|
|
||||||
if (proxyServer) {
|
|
||||||
await new Promise<void>((resolve) => proxyServer.close(() => resolve()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (socksProxyServer) {
|
|
||||||
await new Promise<void>((resolve) => socksProxyServer.close(() => resolve()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (testServer) {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,299 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup test SMTP server', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2537,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false,
|
|
||||||
socketTimeout: 30000 // 30 second timeout for keep-alive tests
|
|
||||||
});
|
|
||||||
expect(testServer).toBeTruthy();
|
|
||||||
expect(testServer.port).toEqual(2537);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-11: Basic keep-alive functionality', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
keepAlive: true,
|
|
||||||
keepAliveInterval: 5000, // 5 seconds
|
|
||||||
connectionTimeout: 10000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Verify connection works
|
|
||||||
const verified = await smtpClient.verify();
|
|
||||||
expect(verified).toBeTrue();
|
|
||||||
|
|
||||||
// Send an email to establish connection
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Keep-alive test',
|
|
||||||
text: 'Testing connection keep-alive'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
|
|
||||||
// Wait to simulate idle time
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
||||||
|
|
||||||
// Send another email to verify connection is still working
|
|
||||||
const result2 = await smtpClient.sendMail(email);
|
|
||||||
expect(result2.success).toBeTrue();
|
|
||||||
|
|
||||||
console.log('✅ Keep-alive functionality verified');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-11: Connection reuse with keep-alive', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
keepAlive: true,
|
|
||||||
keepAliveInterval: 3000,
|
|
||||||
connectionTimeout: 10000,
|
|
||||||
poolSize: 1, // Use single connection to test keep-alive
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send multiple emails with delays to test keep-alive
|
|
||||||
const emails = [];
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: `Keep-alive test ${i + 1}`,
|
|
||||||
text: `Testing connection keep-alive - email ${i + 1}`
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
emails.push(result);
|
|
||||||
|
|
||||||
// Wait between emails (less than keep-alive interval)
|
|
||||||
if (i < 2) {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// All emails should have been sent successfully
|
|
||||||
expect(emails.length).toEqual(3);
|
|
||||||
expect(emails.every(r => r.success)).toBeTrue();
|
|
||||||
|
|
||||||
console.log('✅ Connection reused successfully with keep-alive');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-11: Connection without keep-alive', async () => {
|
|
||||||
// Create a client without keep-alive
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
keepAlive: false, // Disabled
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
socketTimeout: 5000, // 5 second socket timeout
|
|
||||||
poolSize: 1,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send first email
|
|
||||||
const email1 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'No keep-alive test 1',
|
|
||||||
text: 'Testing without keep-alive'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result1 = await smtpClient.sendMail(email1);
|
|
||||||
expect(result1.success).toBeTrue();
|
|
||||||
|
|
||||||
// Wait longer than socket timeout
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 7000));
|
|
||||||
|
|
||||||
// Send second email - connection might need to be re-established
|
|
||||||
const email2 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'No keep-alive test 2',
|
|
||||||
text: 'Testing without keep-alive after timeout'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result2 = await smtpClient.sendMail(email2);
|
|
||||||
expect(result2.success).toBeTrue();
|
|
||||||
|
|
||||||
console.log('✅ Client handles reconnection without keep-alive');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-11: Keep-alive with long operations', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
keepAlive: true,
|
|
||||||
keepAliveInterval: 2000,
|
|
||||||
connectionTimeout: 10000,
|
|
||||||
poolSize: 2, // Use small pool
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send multiple emails with varying delays
|
|
||||||
const operations = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
operations.push((async () => {
|
|
||||||
// Simulate random processing delay
|
|
||||||
await new Promise(resolve => setTimeout(resolve, Math.random() * 3000));
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: `Long operation test ${i + 1}`,
|
|
||||||
text: `Testing keep-alive during long operations - email ${i + 1}`
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
return { index: i, result };
|
|
||||||
})());
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = await Promise.all(operations);
|
|
||||||
|
|
||||||
// All operations should succeed
|
|
||||||
const successCount = results.filter(r => r.result.success).length;
|
|
||||||
expect(successCount).toEqual(5);
|
|
||||||
|
|
||||||
console.log('✅ Keep-alive maintained during long operations');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-11: Keep-alive interval effect on connection pool', async () => {
|
|
||||||
const intervals = [1000, 3000, 5000]; // Different intervals to test
|
|
||||||
|
|
||||||
for (const interval of intervals) {
|
|
||||||
console.log(`\nTesting keep-alive with ${interval}ms interval`);
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
keepAlive: true,
|
|
||||||
keepAliveInterval: interval,
|
|
||||||
connectionTimeout: 10000,
|
|
||||||
poolSize: 2,
|
|
||||||
debug: false // Less verbose for this test
|
|
||||||
});
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
// Send multiple emails over time period longer than interval
|
|
||||||
const emails = [];
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: `Interval test ${i + 1}`,
|
|
||||||
text: `Testing with ${interval}ms keep-alive interval`
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
emails.push(result);
|
|
||||||
|
|
||||||
// Wait approximately one interval
|
|
||||||
if (i < 2) {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, interval));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalTime = Date.now() - startTime;
|
|
||||||
console.log(`Sent ${emails.length} emails in ${totalTime}ms with ${interval}ms keep-alive`);
|
|
||||||
|
|
||||||
// Check pool status
|
|
||||||
const poolStatus = smtpClient.getPoolStatus();
|
|
||||||
console.log(`Pool status: ${JSON.stringify(poolStatus)}`);
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CCM-11: Event monitoring during keep-alive', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
keepAlive: true,
|
|
||||||
keepAliveInterval: 2000,
|
|
||||||
connectionTimeout: 10000,
|
|
||||||
poolSize: 1,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
let connectionEvents = 0;
|
|
||||||
let disconnectEvents = 0;
|
|
||||||
let errorEvents = 0;
|
|
||||||
|
|
||||||
// Monitor events
|
|
||||||
smtpClient.on('connection', () => {
|
|
||||||
connectionEvents++;
|
|
||||||
console.log('📡 Connection event');
|
|
||||||
});
|
|
||||||
|
|
||||||
smtpClient.on('disconnect', () => {
|
|
||||||
disconnectEvents++;
|
|
||||||
console.log('🔌 Disconnect event');
|
|
||||||
});
|
|
||||||
|
|
||||||
smtpClient.on('error', (error) => {
|
|
||||||
errorEvents++;
|
|
||||||
console.log('❌ Error event:', error.message);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send emails with delays
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: `Event test ${i + 1}`,
|
|
||||||
text: 'Testing events during keep-alive'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
|
|
||||||
if (i < 2) {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should have at least one connection event
|
|
||||||
expect(connectionEvents).toBeGreaterThan(0);
|
|
||||||
console.log(`✅ Captured ${connectionEvents} connection events`);
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
|
|
||||||
// Wait a bit for close event
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup test SMTP server', async () => {
|
|
||||||
if (testServer) {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,529 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
import * as net from 'net';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup test SMTP server', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2570,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
expect(testServer).toBeTruthy();
|
|
||||||
expect(testServer.port).toEqual(2570);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-01: Multi-line greeting', async () => {
|
|
||||||
// Create custom server with multi-line greeting
|
|
||||||
const customServer = net.createServer((socket) => {
|
|
||||||
// Send multi-line greeting
|
|
||||||
socket.write('220-mail.example.com ESMTP Server\r\n');
|
|
||||||
socket.write('220-Welcome to our mail server!\r\n');
|
|
||||||
socket.write('220-Please be patient during busy times.\r\n');
|
|
||||||
socket.write('220 Ready to serve\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
console.log('Received:', command);
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO') || command.startsWith('HELO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
} else {
|
|
||||||
socket.write('500 Command not recognized\r\n');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
customServer.listen(0, '127.0.0.1', () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const customPort = (customServer.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: customPort,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Testing multi-line greeting handling...');
|
|
||||||
|
|
||||||
const connected = await smtpClient.verify();
|
|
||||||
expect(connected).toBeTrue();
|
|
||||||
|
|
||||||
console.log('Successfully handled multi-line greeting');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
customServer.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-01: Slow server responses', async () => {
|
|
||||||
// Create server with delayed responses
|
|
||||||
const slowServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 Slow Server Ready\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
console.log('Slow server received:', command);
|
|
||||||
|
|
||||||
// Add artificial delays
|
|
||||||
const delay = 1000 + Math.random() * 2000; // 1-3 seconds
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250-slow.example.com\r\n');
|
|
||||||
setTimeout(() => socket.write('250 OK\r\n'), 500);
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye... slowly\r\n');
|
|
||||||
setTimeout(() => socket.end(), 1000);
|
|
||||||
} else {
|
|
||||||
socket.write('250 OK... eventually\r\n');
|
|
||||||
}
|
|
||||||
}, delay);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
slowServer.listen(0, '127.0.0.1', () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const slowPort = (slowServer.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: slowPort,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 10000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('\nTesting slow server response handling...');
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
const connected = await smtpClient.verify();
|
|
||||||
const connectTime = Date.now() - startTime;
|
|
||||||
|
|
||||||
expect(connected).toBeTrue();
|
|
||||||
console.log(`Connected after ${connectTime}ms (slow server)`);
|
|
||||||
expect(connectTime).toBeGreaterThan(1000);
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
slowServer.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-01: Unusual status codes', async () => {
|
|
||||||
// Create server that returns unusual status codes
|
|
||||||
const unusualServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 Unusual Server\r\n');
|
|
||||||
|
|
||||||
let commandCount = 0;
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
commandCount++;
|
|
||||||
|
|
||||||
// Return unusual but valid responses
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250-unusual.example.com\r\n');
|
|
||||||
socket.write('250-PIPELINING\r\n');
|
|
||||||
socket.write('250 OK\r\n'); // Use 250 OK as final response
|
|
||||||
} else if (command.startsWith('MAIL FROM')) {
|
|
||||||
socket.write('250 Sender OK (#2.0.0)\r\n'); // Valid with enhanced code
|
|
||||||
} else if (command.startsWith('RCPT TO')) {
|
|
||||||
socket.write('250 Recipient OK\r\n'); // Keep it simple
|
|
||||||
} else if (command === 'DATA') {
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
} else if (command === '.') {
|
|
||||||
socket.write('250 Message accepted for delivery (#2.0.0)\r\n'); // With enhanced code
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye (#2.0.0 closing connection)\r\n');
|
|
||||||
socket.end();
|
|
||||||
} else {
|
|
||||||
socket.write('250 OK\r\n'); // Default response
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
unusualServer.listen(0, '127.0.0.1', () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const unusualPort = (unusualServer.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: unusualPort,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('\nTesting unusual status code handling...');
|
|
||||||
|
|
||||||
const connected = await smtpClient.verify();
|
|
||||||
expect(connected).toBeTrue();
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Unusual Status Test',
|
|
||||||
text: 'Testing unusual server responses'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should handle unusual codes gracefully
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
console.log('Email sent despite unusual status codes');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
unusualServer.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-01: Mixed line endings', async () => {
|
|
||||||
// Create server with inconsistent line endings
|
|
||||||
const mixedServer = net.createServer((socket) => {
|
|
||||||
// Mix CRLF, LF, and CR
|
|
||||||
socket.write('220 Mixed line endings server\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
// Mix different line endings
|
|
||||||
socket.write('250-mixed.example.com\n'); // LF only
|
|
||||||
socket.write('250-PIPELINING\r'); // CR only
|
|
||||||
socket.write('250-SIZE 10240000\r\n'); // Proper CRLF
|
|
||||||
socket.write('250 8BITMIME\n'); // LF only
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
} else {
|
|
||||||
socket.write('250 OK\n'); // LF only
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
mixedServer.listen(0, '127.0.0.1', () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const mixedPort = (mixedServer.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: mixedPort,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('\nTesting mixed line ending handling...');
|
|
||||||
|
|
||||||
const connected = await smtpClient.verify();
|
|
||||||
expect(connected).toBeTrue();
|
|
||||||
|
|
||||||
console.log('Successfully handled mixed line endings');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
mixedServer.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-01: Empty responses', async () => {
|
|
||||||
// Create server that sends minimal but valid responses
|
|
||||||
const emptyServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 Server with minimal responses\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
// Send minimal but valid EHLO response
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
} else {
|
|
||||||
// Default minimal response
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
emptyServer.listen(0, '127.0.0.1', () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const emptyPort = (emptyServer.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: emptyPort,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('\nTesting empty response handling...');
|
|
||||||
|
|
||||||
const connected = await smtpClient.verify();
|
|
||||||
expect(connected).toBeTrue();
|
|
||||||
|
|
||||||
console.log('Connected successfully with minimal server responses');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
emptyServer.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-01: Responses with special characters', async () => {
|
|
||||||
// Create server with special characters in responses
|
|
||||||
const specialServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 ✉️ Unicode SMTP Server 🚀\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250-Hello 你好 مرحبا שלום\r\n');
|
|
||||||
socket.write('250-Special chars: <>&"\'`\r\n');
|
|
||||||
socket.write('250-Tabs\tand\tspaces here\r\n');
|
|
||||||
socket.write('250 OK ✓\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 👋 Goodbye!\r\n');
|
|
||||||
socket.end();
|
|
||||||
} else {
|
|
||||||
socket.write('250 OK 👍\r\n');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
specialServer.listen(0, '127.0.0.1', () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const specialPort = (specialServer.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: specialPort,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('\nTesting special character handling...');
|
|
||||||
|
|
||||||
const connected = await smtpClient.verify();
|
|
||||||
expect(connected).toBeTrue();
|
|
||||||
|
|
||||||
console.log('Successfully handled special characters in responses');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
specialServer.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-01: Pipelined responses', async () => {
|
|
||||||
// Create server that batches pipelined responses
|
|
||||||
const pipelineServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 Pipeline Test Server\r\n');
|
|
||||||
|
|
||||||
let inDataMode = false;
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const commands = data.toString().split('\r\n').filter(cmd => cmd.length > 0);
|
|
||||||
|
|
||||||
commands.forEach(command => {
|
|
||||||
console.log('Pipeline server received:', command);
|
|
||||||
|
|
||||||
if (inDataMode) {
|
|
||||||
if (command === '.') {
|
|
||||||
// End of DATA
|
|
||||||
socket.write('250 Message accepted\r\n');
|
|
||||||
inDataMode = false;
|
|
||||||
}
|
|
||||||
// Otherwise, we're receiving email data - don't respond
|
|
||||||
} else if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250-pipeline.example.com\r\n250-PIPELINING\r\n250 OK\r\n');
|
|
||||||
} else if (command.startsWith('MAIL FROM')) {
|
|
||||||
socket.write('250 Sender OK\r\n');
|
|
||||||
} else if (command.startsWith('RCPT TO')) {
|
|
||||||
socket.write('250 Recipient OK\r\n');
|
|
||||||
} else if (command === 'DATA') {
|
|
||||||
socket.write('354 Send data\r\n');
|
|
||||||
inDataMode = true;
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
pipelineServer.listen(0, '127.0.0.1', () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const pipelinePort = (pipelineServer.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: pipelinePort,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('\nTesting pipelined responses...');
|
|
||||||
|
|
||||||
const connected = await smtpClient.verify();
|
|
||||||
expect(connected).toBeTrue();
|
|
||||||
|
|
||||||
// Test sending email with pipelined server
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Pipeline Test',
|
|
||||||
text: 'Testing pipelined responses'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('Successfully handled pipelined responses');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
pipelineServer.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-01: Extremely long response lines', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const connected = await smtpClient.verify();
|
|
||||||
expect(connected).toBeTrue();
|
|
||||||
|
|
||||||
// Create very long message
|
|
||||||
const longString = 'x'.repeat(1000);
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Long line test',
|
|
||||||
text: 'Testing long lines',
|
|
||||||
headers: {
|
|
||||||
'X-Long-Header': longString,
|
|
||||||
'X-Another-Long': `Start ${longString} End`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('\nTesting extremely long response line handling...');
|
|
||||||
|
|
||||||
// Note: sendCommand is not a public API method
|
|
||||||
// We'll monitor line length through the actual email sending
|
|
||||||
let maxLineLength = 1000; // Estimate based on header content
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
console.log(`Maximum line length sent: ${maxLineLength} characters`);
|
|
||||||
console.log(`RFC 5321 limit: 998 characters (excluding CRLF)`);
|
|
||||||
|
|
||||||
if (maxLineLength > 998) {
|
|
||||||
console.log('WARNING: Line length exceeds RFC limit');
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(result).toBeTruthy();
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-01: Server closes connection unexpectedly', async () => {
|
|
||||||
// Create server that closes connection at various points
|
|
||||||
let closeAfterCommands = 3;
|
|
||||||
let commandCount = 0;
|
|
||||||
|
|
||||||
const abruptServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 Abrupt Server\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
commandCount++;
|
|
||||||
|
|
||||||
console.log(`Abrupt server: command ${commandCount} - ${command}`);
|
|
||||||
|
|
||||||
if (commandCount >= closeAfterCommands) {
|
|
||||||
console.log('Abrupt server: Closing connection unexpectedly!');
|
|
||||||
socket.destroy(); // Abrupt close
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normal responses until close
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('MAIL FROM')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('RCPT TO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
abruptServer.listen(0, '127.0.0.1', () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const abruptPort = (abruptServer.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: abruptPort,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('\nTesting abrupt connection close handling...');
|
|
||||||
|
|
||||||
// The verify should fail or succeed depending on when the server closes
|
|
||||||
const connected = await smtpClient.verify();
|
|
||||||
|
|
||||||
if (connected) {
|
|
||||||
// If verify succeeded, try sending email which should fail
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Abrupt close test',
|
|
||||||
text: 'Testing abrupt connection close'
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await smtpClient.sendMail(email);
|
|
||||||
console.log('Email sent before abrupt close');
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Expected error due to abrupt close:', error.message);
|
|
||||||
expect(error.message).toMatch(/closed|reset|abort|end|timeout/i);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Verify failed due to abrupt close
|
|
||||||
console.log('Connection failed as expected due to abrupt server close');
|
|
||||||
}
|
|
||||||
|
|
||||||
abruptServer.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup test SMTP server', async () => {
|
|
||||||
if (testServer) {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,438 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
import * as net from 'net';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup test SMTP server', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2571,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
expect(testServer).toBeTruthy();
|
|
||||||
expect(testServer.port).toEqual(2571);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-02: Commands with extra spaces', async () => {
|
|
||||||
// Create server that accepts commands with extra spaces
|
|
||||||
const spaceyServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 mail.example.com ESMTP\r\n');
|
|
||||||
let inData = false;
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (!line && lines[lines.length - 1] === '') return; // Skip empty trailing line
|
|
||||||
|
|
||||||
console.log(`Server received: "${line}"`);
|
|
||||||
|
|
||||||
if (inData) {
|
|
||||||
if (line === '.') {
|
|
||||||
socket.write('250 Message accepted\r\n');
|
|
||||||
inData = false;
|
|
||||||
}
|
|
||||||
// Otherwise it's email data, ignore
|
|
||||||
} else if (line.match(/^EHLO\s+/i)) {
|
|
||||||
socket.write('250-mail.example.com\r\n');
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.match(/^MAIL\s+FROM:/i)) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.match(/^RCPT\s+TO:/i)) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line === 'DATA') {
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
inData = true;
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
} else if (line) {
|
|
||||||
socket.write('500 Command not recognized\r\n');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
spaceyServer.listen(0, '127.0.0.1', () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const spaceyPort = (spaceyServer.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: spaceyPort,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const verified = await smtpClient.verify();
|
|
||||||
expect(verified).toBeTrue();
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Test with extra spaces',
|
|
||||||
text: 'Testing command formatting'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Server handled commands with extra spaces');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
spaceyServer.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-02: Mixed case commands', async () => {
|
|
||||||
// Create server that accepts mixed case commands
|
|
||||||
const mixedCaseServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 mail.example.com ESMTP\r\n');
|
|
||||||
let inData = false;
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (!line && lines[lines.length - 1] === '') return;
|
|
||||||
|
|
||||||
const upperLine = line.toUpperCase();
|
|
||||||
console.log(`Server received: "${line}"`);
|
|
||||||
|
|
||||||
if (inData) {
|
|
||||||
if (line === '.') {
|
|
||||||
socket.write('250 Message accepted\r\n');
|
|
||||||
inData = false;
|
|
||||||
}
|
|
||||||
} else if (upperLine.startsWith('EHLO')) {
|
|
||||||
socket.write('250-mail.example.com\r\n');
|
|
||||||
socket.write('250 8BITMIME\r\n');
|
|
||||||
} else if (upperLine.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (upperLine.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (upperLine === 'DATA') {
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
inData = true;
|
|
||||||
} else if (upperLine === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
mixedCaseServer.listen(0, '127.0.0.1', () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const mixedPort = (mixedCaseServer.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: mixedPort,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const verified = await smtpClient.verify();
|
|
||||||
expect(verified).toBeTrue();
|
|
||||||
console.log('✅ Server accepts mixed case commands');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
mixedCaseServer.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-02: Commands with missing parameters', async () => {
|
|
||||||
// Create server that handles incomplete commands
|
|
||||||
const incompleteServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 mail.example.com ESMTP\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (!line && lines[lines.length - 1] === '') return;
|
|
||||||
|
|
||||||
console.log(`Server received: "${line}"`);
|
|
||||||
|
|
||||||
if (line.startsWith('EHLO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line === 'MAIL FROM:' || line === 'MAIL FROM') {
|
|
||||||
// Missing email address
|
|
||||||
socket.write('501 Syntax error in parameters\r\n');
|
|
||||||
} else if (line === 'RCPT TO:' || line === 'RCPT TO') {
|
|
||||||
// Missing recipient
|
|
||||||
socket.write('501 Syntax error in parameters\r\n');
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
} else if (line) {
|
|
||||||
socket.write('500 Command not recognized\r\n');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
incompleteServer.listen(0, '127.0.0.1', () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const incompletePort = (incompleteServer.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: incompletePort,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// This should succeed as the client sends proper commands
|
|
||||||
const verified = await smtpClient.verify();
|
|
||||||
expect(verified).toBeTrue();
|
|
||||||
console.log('✅ Client sends properly formatted commands');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
incompleteServer.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-02: Commands with extra parameters', async () => {
|
|
||||||
// Create server that handles commands with extra parameters
|
|
||||||
const extraParamsServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 mail.example.com ESMTP\r\n');
|
|
||||||
let inData = false;
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (!line && lines[lines.length - 1] === '') return;
|
|
||||||
|
|
||||||
console.log(`Server received: "${line}"`);
|
|
||||||
|
|
||||||
if (inData) {
|
|
||||||
if (line === '.') {
|
|
||||||
socket.write('250 Message accepted\r\n');
|
|
||||||
inData = false;
|
|
||||||
}
|
|
||||||
} else if (line.startsWith('EHLO')) {
|
|
||||||
// Accept EHLO with any parameter
|
|
||||||
socket.write('250-mail.example.com\r\n');
|
|
||||||
socket.write('250-SIZE 10240000\r\n');
|
|
||||||
socket.write('250 8BITMIME\r\n');
|
|
||||||
} else if (line.match(/^MAIL FROM:.*SIZE=/i)) {
|
|
||||||
// Accept SIZE parameter
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line === 'DATA') {
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
inData = true;
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
extraParamsServer.listen(0, '127.0.0.1', () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const extraPort = (extraParamsServer.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: extraPort,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Test with parameters',
|
|
||||||
text: 'Testing extra parameters'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Server handled commands with extra parameters');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
extraParamsServer.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-02: Invalid command sequences', async () => {
|
|
||||||
// Create server that enforces command sequence
|
|
||||||
const sequenceServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 mail.example.com ESMTP\r\n');
|
|
||||||
let state = 'GREETING';
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (!line && lines[lines.length - 1] === '') return;
|
|
||||||
|
|
||||||
console.log(`Server received: "${line}" in state ${state}`);
|
|
||||||
|
|
||||||
if (state === 'DATA' && line !== '.') {
|
|
||||||
// In DATA state, ignore everything except the terminating period
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
|
||||||
state = 'READY';
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('MAIL FROM:')) {
|
|
||||||
if (state !== 'READY') {
|
|
||||||
socket.write('503 Bad sequence of commands\r\n');
|
|
||||||
} else {
|
|
||||||
state = 'MAIL';
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}
|
|
||||||
} else if (line.startsWith('RCPT TO:')) {
|
|
||||||
if (state !== 'MAIL' && state !== 'RCPT') {
|
|
||||||
socket.write('503 Bad sequence of commands\r\n');
|
|
||||||
} else {
|
|
||||||
state = 'RCPT';
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}
|
|
||||||
} else if (line === 'DATA') {
|
|
||||||
if (state !== 'RCPT') {
|
|
||||||
socket.write('503 Bad sequence of commands\r\n');
|
|
||||||
} else {
|
|
||||||
state = 'DATA';
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
}
|
|
||||||
} else if (line === '.' && state === 'DATA') {
|
|
||||||
state = 'READY';
|
|
||||||
socket.write('250 Message accepted\r\n');
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
} else if (line === 'RSET') {
|
|
||||||
state = 'READY';
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
sequenceServer.listen(0, '127.0.0.1', () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const sequencePort = (sequenceServer.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: sequencePort,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Client should handle proper command sequencing
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Test sequence',
|
|
||||||
text: 'Testing command sequence'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Client maintains proper command sequence');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
sequenceServer.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-02: Malformed email addresses', async () => {
|
|
||||||
// Test how client handles various email formats
|
|
||||||
const emailServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 mail.example.com ESMTP\r\n');
|
|
||||||
let inData = false;
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (!line && lines[lines.length - 1] === '') return;
|
|
||||||
|
|
||||||
console.log(`Server received: "${line}"`);
|
|
||||||
|
|
||||||
if (inData) {
|
|
||||||
if (line === '.') {
|
|
||||||
socket.write('250 Message accepted\r\n');
|
|
||||||
inData = false;
|
|
||||||
}
|
|
||||||
} else if (line.startsWith('EHLO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('MAIL FROM:')) {
|
|
||||||
// Accept any sender format
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('RCPT TO:')) {
|
|
||||||
// Accept any recipient format
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line === 'DATA') {
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
inData = true;
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
emailServer.listen(0, '127.0.0.1', () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const emailPort = (emailServer.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: emailPort,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test with properly formatted email
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Test email formats',
|
|
||||||
text: 'Testing email address handling'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Client properly formats email addresses');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
emailServer.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup test SMTP server', async () => {
|
|
||||||
if (testServer) {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,446 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
import * as net from 'net';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup test SMTP server', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2572,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
expect(testServer).toBeTruthy();
|
|
||||||
expect(testServer.port).toEqual(2572);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-03: Server closes connection during MAIL FROM', async () => {
|
|
||||||
// Create server that abruptly closes during MAIL FROM
|
|
||||||
const abruptServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 mail.example.com ESMTP\r\n');
|
|
||||||
let commandCount = 0;
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (!line && lines[lines.length - 1] === '') return;
|
|
||||||
|
|
||||||
commandCount++;
|
|
||||||
console.log(`Server received command ${commandCount}: "${line}"`);
|
|
||||||
|
|
||||||
if (line.startsWith('EHLO')) {
|
|
||||||
socket.write('250-mail.example.com\r\n');
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('MAIL FROM:')) {
|
|
||||||
// Abruptly close connection
|
|
||||||
console.log('Server closing connection unexpectedly');
|
|
||||||
socket.destroy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
abruptServer.listen(0, '127.0.0.1', () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const abruptPort = (abruptServer.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: abruptPort,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Connection closure test',
|
|
||||||
text: 'Testing unexpected disconnection'
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
// Should not succeed due to connection closure
|
|
||||||
expect(result.success).toBeFalse();
|
|
||||||
console.log('✅ Client handled abrupt connection closure gracefully');
|
|
||||||
} catch (error) {
|
|
||||||
// Expected to fail due to connection closure
|
|
||||||
console.log('✅ Client threw expected error for connection closure:', error.message);
|
|
||||||
expect(error.message).toMatch(/closed|reset|abort|end|timeout/i);
|
|
||||||
}
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
abruptServer.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-03: Server sends invalid response codes', async () => {
|
|
||||||
// Create server that sends non-standard response codes
|
|
||||||
const invalidServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 mail.example.com ESMTP\r\n');
|
|
||||||
let inData = false;
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (!line && lines[lines.length - 1] === '') return;
|
|
||||||
|
|
||||||
console.log(`Server received: "${line}"`);
|
|
||||||
|
|
||||||
if (inData) {
|
|
||||||
if (line === '.') {
|
|
||||||
socket.write('999 Invalid response code\r\n'); // Invalid 9xx code
|
|
||||||
inData = false;
|
|
||||||
}
|
|
||||||
} else if (line.startsWith('EHLO')) {
|
|
||||||
socket.write('150 Intermediate response\r\n'); // Invalid for EHLO
|
|
||||||
} else if (line.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line === 'DATA') {
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
inData = true;
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
invalidServer.listen(0, '127.0.0.1', () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const invalidPort = (invalidServer.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: invalidPort,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
// This will likely fail due to invalid EHLO response
|
|
||||||
const verified = await smtpClient.verify();
|
|
||||||
expect(verified).toBeFalse();
|
|
||||||
console.log('✅ Client rejected invalid response codes');
|
|
||||||
} catch (error) {
|
|
||||||
console.log('✅ Client properly handled invalid response codes:', error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
invalidServer.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-03: Server sends malformed multi-line responses', async () => {
|
|
||||||
// Create server with malformed multi-line responses
|
|
||||||
const malformedServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 mail.example.com ESMTP\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (!line && lines[lines.length - 1] === '') return;
|
|
||||||
|
|
||||||
console.log(`Server received: "${line}"`);
|
|
||||||
|
|
||||||
if (line.startsWith('EHLO')) {
|
|
||||||
// Malformed multi-line response (missing final line)
|
|
||||||
socket.write('250-mail.example.com\r\n');
|
|
||||||
socket.write('250-PIPELINING\r\n');
|
|
||||||
// Missing final 250 line - this violates RFC
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
malformedServer.listen(0, '127.0.0.1', () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const malformedPort = (malformedServer.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: malformedPort,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 3000, // Shorter timeout for faster test
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Should timeout due to incomplete EHLO response
|
|
||||||
const verified = await smtpClient.verify();
|
|
||||||
|
|
||||||
// If we get here, the client accepted the malformed response
|
|
||||||
// This is acceptable if the client can work around it
|
|
||||||
if (verified === false) {
|
|
||||||
console.log('✅ Client rejected malformed multi-line response');
|
|
||||||
} else {
|
|
||||||
console.log('⚠️ Client accepted malformed multi-line response');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('✅ Client handled malformed response with error:', error.message);
|
|
||||||
// Should timeout or error on malformed response
|
|
||||||
expect(error.message).toMatch(/timeout|Command timeout|Greeting timeout|response|parse/i);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force close since the connection might still be waiting
|
|
||||||
try {
|
|
||||||
await smtpClient.close();
|
|
||||||
} catch (closeError) {
|
|
||||||
// Ignore close errors
|
|
||||||
}
|
|
||||||
|
|
||||||
malformedServer.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-03: Server violates command sequence rules', async () => {
|
|
||||||
// Create server that accepts commands out of sequence
|
|
||||||
const sequenceServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 mail.example.com ESMTP\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (!line && lines[lines.length - 1] === '') return;
|
|
||||||
|
|
||||||
console.log(`Server received: "${line}"`);
|
|
||||||
|
|
||||||
// Accept any command in any order (protocol violation)
|
|
||||||
if (line.startsWith('EHLO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line === 'DATA') {
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
} else if (line === '.') {
|
|
||||||
socket.write('250 Message accepted\r\n');
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
sequenceServer.listen(0, '127.0.0.1', () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const sequencePort = (sequenceServer.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: sequencePort,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Client should still work correctly despite server violations
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Sequence violation test',
|
|
||||||
text: 'Testing command sequence violations'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Client maintains proper sequence despite server violations');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
sequenceServer.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-03: Server sends responses without CRLF', async () => {
|
|
||||||
// Create server that sends responses with incorrect line endings
|
|
||||||
const crlfServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 mail.example.com ESMTP\n'); // LF only, not CRLF
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (!line && lines[lines.length - 1] === '') return;
|
|
||||||
|
|
||||||
console.log(`Server received: "${line}"`);
|
|
||||||
|
|
||||||
if (line.startsWith('EHLO')) {
|
|
||||||
socket.write('250 OK\n'); // LF only
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\n'); // LF only
|
|
||||||
socket.end();
|
|
||||||
} else {
|
|
||||||
socket.write('250 OK\n'); // LF only
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
crlfServer.listen(0, '127.0.0.1', () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const crlfPort = (crlfServer.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: crlfPort,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const verified = await smtpClient.verify();
|
|
||||||
if (verified) {
|
|
||||||
console.log('✅ Client handled non-CRLF responses gracefully');
|
|
||||||
} else {
|
|
||||||
console.log('✅ Client rejected non-CRLF responses');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('✅ Client handled CRLF violation with error:', error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
crlfServer.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-03: Server sends oversized responses', async () => {
|
|
||||||
// Create server that sends very long response lines
|
|
||||||
const oversizeServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 mail.example.com ESMTP\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (!line && lines[lines.length - 1] === '') return;
|
|
||||||
|
|
||||||
console.log(`Server received: "${line}"`);
|
|
||||||
|
|
||||||
if (line.startsWith('EHLO')) {
|
|
||||||
// Send an extremely long response line (over RFC limit)
|
|
||||||
const longResponse = '250 ' + 'x'.repeat(2000) + '\r\n';
|
|
||||||
socket.write(longResponse);
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
} else {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
oversizeServer.listen(0, '127.0.0.1', () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const oversizePort = (oversizeServer.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: oversizePort,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const verified = await smtpClient.verify();
|
|
||||||
console.log(`Verification with oversized response: ${verified}`);
|
|
||||||
console.log('✅ Client handled oversized response');
|
|
||||||
} catch (error) {
|
|
||||||
console.log('✅ Client handled oversized response with error:', error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
oversizeServer.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-03: Server violates RFC timing requirements', async () => {
|
|
||||||
// Create server that has excessive delays
|
|
||||||
const slowServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 mail.example.com ESMTP\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (!line && lines[lines.length - 1] === '') return;
|
|
||||||
|
|
||||||
console.log(`Server received: "${line}"`);
|
|
||||||
|
|
||||||
if (line.startsWith('EHLO')) {
|
|
||||||
// Extreme delay (violates RFC timing recommendations)
|
|
||||||
setTimeout(() => {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}, 2000); // 2 second delay
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
} else {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
slowServer.listen(0, '127.0.0.1', () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const slowPort = (slowServer.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: slowPort,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 10000, // Allow time for slow response
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
try {
|
|
||||||
const verified = await smtpClient.verify();
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
|
|
||||||
console.log(`Verification completed in ${duration}ms`);
|
|
||||||
if (verified) {
|
|
||||||
console.log('✅ Client handled slow server responses');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('✅ Client handled timing violation with error:', error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
slowServer.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup test SMTP server', async () => {
|
|
||||||
if (testServer) {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,530 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
import * as net from 'net';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup test SMTP server', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2573,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
expect(testServer).toBeTruthy();
|
|
||||||
expect(testServer.port).toEqual(2573);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-04: Server with connection limits', async () => {
|
|
||||||
// Create server that only accepts 2 connections
|
|
||||||
let connectionCount = 0;
|
|
||||||
const maxConnections = 2;
|
|
||||||
|
|
||||||
const limitedServer = net.createServer((socket) => {
|
|
||||||
connectionCount++;
|
|
||||||
console.log(`Connection ${connectionCount} established`);
|
|
||||||
|
|
||||||
if (connectionCount > maxConnections) {
|
|
||||||
console.log('Rejecting connection due to limit');
|
|
||||||
socket.write('421 Too many connections\r\n');
|
|
||||||
socket.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.write('220 mail.example.com ESMTP\r\n');
|
|
||||||
let inData = false;
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (!line && lines[lines.length - 1] === '') return;
|
|
||||||
|
|
||||||
console.log(`Server received: "${line}"`);
|
|
||||||
|
|
||||||
if (inData) {
|
|
||||||
if (line === '.') {
|
|
||||||
socket.write('250 Message accepted\r\n');
|
|
||||||
inData = false;
|
|
||||||
}
|
|
||||||
} else if (line.startsWith('EHLO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line === 'DATA') {
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
inData = true;
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('close', () => {
|
|
||||||
connectionCount--;
|
|
||||||
console.log(`Connection closed, ${connectionCount} remaining`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
limitedServer.listen(0, '127.0.0.1', () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const limitedPort = (limitedServer.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
// Create multiple clients to test connection limits
|
|
||||||
const clients: SmtpClient[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < 4; i++) {
|
|
||||||
const client = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: limitedPort,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
clients.push(client);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to verify all clients concurrently to test connection limits
|
|
||||||
const promises = clients.map(async (client) => {
|
|
||||||
try {
|
|
||||||
const verified = await client.verify();
|
|
||||||
return verified;
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Connection failed:', error.message);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const results = await Promise.all(promises);
|
|
||||||
|
|
||||||
// Since verify() closes connections immediately, we can't really test concurrent limits
|
|
||||||
// Instead, test that all clients can connect sequentially
|
|
||||||
const successCount = results.filter(r => r).length;
|
|
||||||
console.log(`${successCount} out of ${clients.length} connections succeeded`);
|
|
||||||
expect(successCount).toBeGreaterThan(0);
|
|
||||||
console.log('✅ Clients handled connection attempts gracefully');
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
for (const client of clients) {
|
|
||||||
await client.close();
|
|
||||||
}
|
|
||||||
limitedServer.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-04: Large email message handling', async () => {
|
|
||||||
// Test with very large email content
|
|
||||||
const largeServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 mail.example.com ESMTP\r\n');
|
|
||||||
let inData = false;
|
|
||||||
let dataSize = 0;
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (!line && lines[lines.length - 1] === '') return;
|
|
||||||
|
|
||||||
if (inData) {
|
|
||||||
dataSize += line.length;
|
|
||||||
if (line === '.') {
|
|
||||||
console.log(`Received email data: ${dataSize} bytes`);
|
|
||||||
if (dataSize > 50000) {
|
|
||||||
socket.write('552 Message size exceeds limit\r\n');
|
|
||||||
} else {
|
|
||||||
socket.write('250 Message accepted\r\n');
|
|
||||||
}
|
|
||||||
inData = false;
|
|
||||||
dataSize = 0;
|
|
||||||
}
|
|
||||||
} else if (line.startsWith('EHLO')) {
|
|
||||||
socket.write('250-mail.example.com\r\n');
|
|
||||||
socket.write('250-SIZE 50000\r\n'); // 50KB limit
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line === 'DATA') {
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
inData = true;
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
largeServer.listen(0, '127.0.0.1', () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const largePort = (largeServer.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: largePort,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test with large content
|
|
||||||
const largeContent = 'X'.repeat(60000); // 60KB content
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Large email test',
|
|
||||||
text: largeContent
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
// Should fail due to size limit
|
|
||||||
expect(result.success).toBeFalse();
|
|
||||||
console.log('✅ Server properly rejected oversized email');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
largeServer.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-04: Memory pressure simulation', async () => {
|
|
||||||
// Create server that simulates memory pressure
|
|
||||||
const memoryServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 mail.example.com ESMTP\r\n');
|
|
||||||
let inData = false;
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (!line && lines[lines.length - 1] === '') return;
|
|
||||||
|
|
||||||
if (inData) {
|
|
||||||
if (line === '.') {
|
|
||||||
// Simulate memory pressure by delaying response
|
|
||||||
setTimeout(() => {
|
|
||||||
socket.write('451 Temporary failure due to system load\r\n');
|
|
||||||
}, 1000);
|
|
||||||
inData = false;
|
|
||||||
}
|
|
||||||
} else if (line.startsWith('EHLO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line === 'DATA') {
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
inData = true;
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
memoryServer.listen(0, '127.0.0.1', () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const memoryPort = (memoryServer.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: memoryPort,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Memory pressure test',
|
|
||||||
text: 'Testing memory constraints'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
// Should handle temporary failure gracefully
|
|
||||||
expect(result.success).toBeFalse();
|
|
||||||
console.log('✅ Client handled temporary failure gracefully');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
memoryServer.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-04: High concurrent connections', async () => {
|
|
||||||
// Test multiple concurrent connections
|
|
||||||
const concurrentServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 mail.example.com ESMTP\r\n');
|
|
||||||
let inData = false;
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (!line && lines[lines.length - 1] === '') return;
|
|
||||||
|
|
||||||
if (inData) {
|
|
||||||
if (line === '.') {
|
|
||||||
socket.write('250 Message accepted\r\n');
|
|
||||||
inData = false;
|
|
||||||
}
|
|
||||||
} else if (line.startsWith('EHLO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line === 'DATA') {
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
inData = true;
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
concurrentServer.listen(0, '127.0.0.1', () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const concurrentPort = (concurrentServer.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
// Create multiple clients concurrently
|
|
||||||
const clientPromises: Promise<boolean>[] = [];
|
|
||||||
const numClients = 10;
|
|
||||||
|
|
||||||
for (let i = 0; i < numClients; i++) {
|
|
||||||
const clientPromise = (async () => {
|
|
||||||
const client = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: concurrentPort,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
pool: true,
|
|
||||||
maxConnections: 2,
|
|
||||||
debug: false // Reduce noise
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const email = new Email({
|
|
||||||
from: `sender${i}@example.com`,
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: `Concurrent test ${i}`,
|
|
||||||
text: `Message from client ${i}`
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await client.sendMail(email);
|
|
||||||
await client.close();
|
|
||||||
return result.success;
|
|
||||||
} catch (error) {
|
|
||||||
await client.close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
clientPromises.push(clientPromise);
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = await Promise.all(clientPromises);
|
|
||||||
const successCount = results.filter(r => r).length;
|
|
||||||
|
|
||||||
console.log(`${successCount} out of ${numClients} concurrent operations succeeded`);
|
|
||||||
expect(successCount).toBeGreaterThan(5); // At least half should succeed
|
|
||||||
console.log('✅ Handled concurrent connections successfully');
|
|
||||||
|
|
||||||
concurrentServer.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-04: Bandwidth limitations', async () => {
|
|
||||||
// Simulate bandwidth constraints
|
|
||||||
const slowBandwidthServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 mail.example.com ESMTP\r\n');
|
|
||||||
let inData = false;
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (!line && lines[lines.length - 1] === '') return;
|
|
||||||
|
|
||||||
if (inData) {
|
|
||||||
if (line === '.') {
|
|
||||||
// Slow response to simulate bandwidth constraint
|
|
||||||
setTimeout(() => {
|
|
||||||
socket.write('250 Message accepted\r\n');
|
|
||||||
}, 500);
|
|
||||||
inData = false;
|
|
||||||
}
|
|
||||||
} else if (line.startsWith('EHLO')) {
|
|
||||||
// Slow EHLO response
|
|
||||||
setTimeout(() => {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}, 300);
|
|
||||||
} else if (line.startsWith('MAIL FROM:')) {
|
|
||||||
setTimeout(() => {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}, 200);
|
|
||||||
} else if (line.startsWith('RCPT TO:')) {
|
|
||||||
setTimeout(() => {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}, 200);
|
|
||||||
} else if (line === 'DATA') {
|
|
||||||
setTimeout(() => {
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
inData = true;
|
|
||||||
}, 200);
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
slowBandwidthServer.listen(0, '127.0.0.1', () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const slowPort = (slowBandwidthServer.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: slowPort,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 10000, // Higher timeout for slow server
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Bandwidth test',
|
|
||||||
text: 'Testing bandwidth constraints'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
expect(duration).toBeGreaterThan(1000); // Should take time due to delays
|
|
||||||
console.log(`✅ Handled bandwidth constraints (${duration}ms)`);
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
slowBandwidthServer.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-04: Resource exhaustion recovery', async () => {
|
|
||||||
// Test recovery from resource exhaustion
|
|
||||||
let isExhausted = true;
|
|
||||||
|
|
||||||
const exhaustionServer = net.createServer((socket) => {
|
|
||||||
if (isExhausted) {
|
|
||||||
socket.write('421 Service temporarily unavailable\r\n');
|
|
||||||
socket.end();
|
|
||||||
// Simulate recovery after first connection
|
|
||||||
setTimeout(() => {
|
|
||||||
isExhausted = false;
|
|
||||||
}, 1000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.write('220 mail.example.com ESMTP\r\n');
|
|
||||||
let inData = false;
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (!line && lines[lines.length - 1] === '') return;
|
|
||||||
|
|
||||||
if (inData) {
|
|
||||||
if (line === '.') {
|
|
||||||
socket.write('250 Message accepted\r\n');
|
|
||||||
inData = false;
|
|
||||||
}
|
|
||||||
} else if (line.startsWith('EHLO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line === 'DATA') {
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
inData = true;
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
exhaustionServer.listen(0, '127.0.0.1', () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const exhaustionPort = (exhaustionServer.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
// First attempt should fail
|
|
||||||
const client1 = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: exhaustionPort,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const verified1 = await client1.verify();
|
|
||||||
expect(verified1).toBeFalse();
|
|
||||||
console.log('✅ First connection failed due to exhaustion');
|
|
||||||
await client1.close();
|
|
||||||
|
|
||||||
// Wait for recovery
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
||||||
|
|
||||||
// Second attempt should succeed
|
|
||||||
const client2 = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: exhaustionPort,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Recovery test',
|
|
||||||
text: 'Testing recovery from exhaustion'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await client2.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Successfully recovered from resource exhaustion');
|
|
||||||
|
|
||||||
await client2.close();
|
|
||||||
exhaustionServer.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup test SMTP server', async () => {
|
|
||||||
if (testServer) {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
import * as net from 'net';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup test SMTP server', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2570,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
expect(testServer).toBeTruthy();
|
|
||||||
expect(testServer.port).toEqual(2570);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-05: Mixed character encodings in email content', async () => {
|
|
||||||
console.log('Testing mixed character encodings');
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Email with mixed encodings
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Test with émojis 🎉 and spéçiål characters',
|
|
||||||
text: 'Plain text with Unicode: café, naïve, 你好, مرحبا',
|
|
||||||
html: '<p>HTML with entities: café, naïve, and emoji 🌟</p>',
|
|
||||||
attachments: [{
|
|
||||||
filename: 'tëst-filé.txt',
|
|
||||||
content: 'Attachment content with special chars: ñ, ü, ß'
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
console.log(`Result: ${result.messageId ? 'Success' : 'Failed'}`);
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.messageId).toBeDefined();
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-05: Base64 encoding edge cases', async () => {
|
|
||||||
console.log('Testing Base64 encoding edge cases');
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create various sizes of binary content
|
|
||||||
const sizes = [0, 1, 2, 3, 57, 58, 59, 76, 77]; // Edge cases for base64 line wrapping
|
|
||||||
|
|
||||||
for (const size of sizes) {
|
|
||||||
const binaryContent = Buffer.alloc(size);
|
|
||||||
for (let i = 0; i < size; i++) {
|
|
||||||
binaryContent[i] = i % 256;
|
|
||||||
}
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: `Base64 test with ${size} bytes`,
|
|
||||||
text: 'Testing base64 encoding',
|
|
||||||
attachments: [{
|
|
||||||
filename: `test-${size}.bin`,
|
|
||||||
content: binaryContent
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(` Testing with ${size} byte attachment...`);
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.messageId).toBeDefined();
|
|
||||||
}
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-05: Header encoding (RFC 2047)', async () => {
|
|
||||||
console.log('Testing header encoding (RFC 2047)');
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test various header encodings
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
subject: 'Simple ASCII subject',
|
|
||||||
from: 'john@example.com'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
subject: 'Subject with émojis 🎉 and spéçiål çhåracters',
|
|
||||||
from: 'john@example.com'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
subject: 'Japanese: こんにちは, Chinese: 你好, Arabic: مرحبا',
|
|
||||||
from: 'yamada@example.com'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const testCase of testCases) {
|
|
||||||
console.log(` Testing: "${testCase.subject.substring(0, 50)}..."`);
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: testCase.from,
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: testCase.subject,
|
|
||||||
text: 'Testing header encoding',
|
|
||||||
headers: {
|
|
||||||
'X-Custom': `Custom header with special chars: ${testCase.subject.substring(0, 20)}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.messageId).toBeDefined();
|
|
||||||
}
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup test SMTP server', async () => {
|
|
||||||
if (testServer) {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup test SMTP server', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2575,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
expect(testServer).toBeTruthy();
|
|
||||||
expect(testServer.port).toEqual(2575);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-06: Very long subject lines', async () => {
|
|
||||||
console.log('Testing very long subject lines');
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test various subject line lengths
|
|
||||||
const testSubjects = [
|
|
||||||
'Normal Subject Line',
|
|
||||||
'A'.repeat(100), // 100 chars
|
|
||||||
'B'.repeat(500), // 500 chars
|
|
||||||
'C'.repeat(1000), // 1000 chars
|
|
||||||
'D'.repeat(2000), // 2000 chars - very long
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const subject of testSubjects) {
|
|
||||||
console.log(` Testing subject length: ${subject.length} chars`);
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: subject,
|
|
||||||
text: 'Testing large subject headers'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.messageId).toBeDefined();
|
|
||||||
}
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-06: Multiple large headers', async () => {
|
|
||||||
console.log('Testing multiple large headers');
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create email with multiple large headers
|
|
||||||
const largeValue = 'X'.repeat(500);
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Multiple large headers test',
|
|
||||||
text: 'Testing multiple large headers',
|
|
||||||
headers: {
|
|
||||||
'X-Large-Header-1': largeValue,
|
|
||||||
'X-Large-Header-2': largeValue,
|
|
||||||
'X-Large-Header-3': largeValue,
|
|
||||||
'X-Large-Header-4': largeValue,
|
|
||||||
'X-Large-Header-5': largeValue,
|
|
||||||
'X-Very-Long-Header-Name-That-Exceeds-Normal-Limits': 'Value for long header name',
|
|
||||||
'X-Mixed-Content': `Start-${largeValue}-Middle-${largeValue}-End`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
console.log(`Result: ${result.messageId ? 'Success' : 'Failed'}`);
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.messageId).toBeDefined();
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-06: Header folding and wrapping', async () => {
|
|
||||||
console.log('Testing header folding and wrapping');
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create headers that should be folded
|
|
||||||
const longHeaderValue = 'This is a very long header value that should exceed the recommended 78 character line limit and force the header to be folded across multiple lines according to RFC 5322 specifications';
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Header folding test with a very long subject line that should also be folded properly',
|
|
||||||
text: 'Testing header folding',
|
|
||||||
headers: {
|
|
||||||
'X-Long-Header': longHeaderValue,
|
|
||||||
'X-Multi-Line': `Line 1 ${longHeaderValue}\nLine 2 ${longHeaderValue}\nLine 3 ${longHeaderValue}`,
|
|
||||||
'X-Special-Chars': `Header with special chars: \t\r\n\x20 and unicode: 🎉 émojis`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
console.log(`Result: ${result.messageId ? 'Success' : 'Failed'}`);
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.messageId).toBeDefined();
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-06: Maximum header size limits', async () => {
|
|
||||||
console.log('Testing maximum header size limits');
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test near RFC limits (recommended 998 chars per line)
|
|
||||||
const nearMaxValue = 'Y'.repeat(900); // Near but under limit
|
|
||||||
const overMaxValue = 'Z'.repeat(1500); // Over recommended limit
|
|
||||||
|
|
||||||
const testCases = [
|
|
||||||
{ name: 'Near limit', value: nearMaxValue },
|
|
||||||
{ name: 'Over limit', value: overMaxValue }
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const testCase of testCases) {
|
|
||||||
console.log(` Testing ${testCase.name}: ${testCase.value.length} chars`);
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: `Header size test: ${testCase.name}`,
|
|
||||||
text: 'Testing header size limits',
|
|
||||||
headers: {
|
|
||||||
'X-Size-Test': testCase.value
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
console.log(` ${testCase.name}: Success`);
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ${testCase.name}: Failed (${error.message})`);
|
|
||||||
// Some failures might be expected for oversized headers
|
|
||||||
expect(error).toBeDefined();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup test SMTP server', async () => {
|
|
||||||
if (testServer) {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup test SMTP server', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2576,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false,
|
|
||||||
maxConnections: 20 // Allow more connections for concurrent testing
|
|
||||||
});
|
|
||||||
expect(testServer).toBeTruthy();
|
|
||||||
expect(testServer.port).toEqual(2576);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-07: Multiple simultaneous connections', async () => {
|
|
||||||
console.log('Testing multiple simultaneous connections');
|
|
||||||
|
|
||||||
const connectionCount = 5;
|
|
||||||
const clients = [];
|
|
||||||
|
|
||||||
// Create multiple clients
|
|
||||||
for (let i = 0; i < connectionCount; i++) {
|
|
||||||
const client = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: false, // Reduce noise
|
|
||||||
maxConnections: 2
|
|
||||||
});
|
|
||||||
clients.push(client);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test concurrent verification
|
|
||||||
console.log(` Testing ${connectionCount} concurrent verifications...`);
|
|
||||||
const verifyPromises = clients.map(async (client, index) => {
|
|
||||||
try {
|
|
||||||
const result = await client.verify();
|
|
||||||
console.log(` Client ${index + 1}: ${result ? 'Success' : 'Failed'}`);
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` Client ${index + 1}: Error - ${error.message}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const verifyResults = await Promise.all(verifyPromises);
|
|
||||||
const successCount = verifyResults.filter(r => r).length;
|
|
||||||
console.log(` Verify results: ${successCount}/${connectionCount} successful`);
|
|
||||||
|
|
||||||
// We expect at least some connections to succeed
|
|
||||||
expect(successCount).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
// Clean up clients
|
|
||||||
await Promise.all(clients.map(client => client.close().catch(() => {})));
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-07: Concurrent email sending', async () => {
|
|
||||||
console.log('Testing concurrent email sending');
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: false,
|
|
||||||
maxConnections: 5
|
|
||||||
});
|
|
||||||
|
|
||||||
const emailCount = 10;
|
|
||||||
console.log(` Sending ${emailCount} emails concurrently...`);
|
|
||||||
|
|
||||||
const sendPromises = [];
|
|
||||||
for (let i = 0; i < emailCount; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: [`recipient${i}@example.com`],
|
|
||||||
subject: `Concurrent test email ${i + 1}`,
|
|
||||||
text: `This is concurrent test email number ${i + 1}`
|
|
||||||
});
|
|
||||||
|
|
||||||
sendPromises.push(
|
|
||||||
smtpClient.sendMail(email).then(
|
|
||||||
result => {
|
|
||||||
console.log(` Email ${i + 1}: Success`);
|
|
||||||
return { success: true, result };
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
console.log(` Email ${i + 1}: Failed - ${error.message}`);
|
|
||||||
return { success: false, error };
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = await Promise.all(sendPromises);
|
|
||||||
const successCount = results.filter(r => r.success).length;
|
|
||||||
console.log(` Send results: ${successCount}/${emailCount} successful`);
|
|
||||||
|
|
||||||
// We expect a high success rate
|
|
||||||
expect(successCount).toBeGreaterThan(emailCount * 0.7); // At least 70% success
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-07: Rapid connection cycling', async () => {
|
|
||||||
console.log('Testing rapid connection cycling');
|
|
||||||
|
|
||||||
const cycleCount = 8;
|
|
||||||
console.log(` Performing ${cycleCount} rapid connect/disconnect cycles...`);
|
|
||||||
|
|
||||||
const cyclePromises = [];
|
|
||||||
for (let i = 0; i < cycleCount; i++) {
|
|
||||||
cyclePromises.push(
|
|
||||||
(async () => {
|
|
||||||
const client = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 3000,
|
|
||||||
debug: false
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const verified = await client.verify();
|
|
||||||
console.log(` Cycle ${i + 1}: ${verified ? 'Success' : 'Failed'}`);
|
|
||||||
await client.close();
|
|
||||||
return verified;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` Cycle ${i + 1}: Error - ${error.message}`);
|
|
||||||
await client.close().catch(() => {});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const cycleResults = await Promise.all(cyclePromises);
|
|
||||||
const successCount = cycleResults.filter(r => r).length;
|
|
||||||
console.log(` Cycle results: ${successCount}/${cycleCount} successful`);
|
|
||||||
|
|
||||||
// We expect most cycles to succeed
|
|
||||||
expect(successCount).toBeGreaterThan(cycleCount * 0.6); // At least 60% success
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEDGE-07: Connection pool stress test', async () => {
|
|
||||||
console.log('Testing connection pool under stress');
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: false,
|
|
||||||
maxConnections: 3,
|
|
||||||
maxMessages: 50
|
|
||||||
});
|
|
||||||
|
|
||||||
const stressCount = 15;
|
|
||||||
console.log(` Sending ${stressCount} emails to stress connection pool...`);
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
const stressPromises = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < stressCount; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'stress@example.com',
|
|
||||||
to: [`stress${i}@example.com`],
|
|
||||||
subject: `Stress test ${i + 1}`,
|
|
||||||
text: `Connection pool stress test email ${i + 1}`
|
|
||||||
});
|
|
||||||
|
|
||||||
stressPromises.push(
|
|
||||||
smtpClient.sendMail(email).then(
|
|
||||||
result => ({ success: true, index: i }),
|
|
||||||
error => ({ success: false, index: i, error: error.message })
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const stressResults = await Promise.all(stressPromises);
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
const successCount = stressResults.filter(r => r.success).length;
|
|
||||||
|
|
||||||
console.log(` Stress results: ${successCount}/${stressCount} successful in ${duration}ms`);
|
|
||||||
console.log(` Average: ${Math.round(duration / stressCount)}ms per email`);
|
|
||||||
|
|
||||||
// Under stress, we still expect reasonable success rate
|
|
||||||
expect(successCount).toBeGreaterThan(stressCount * 0.5); // At least 50% success under stress
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup test SMTP server', async () => {
|
|
||||||
if (testServer) {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
let smtpClient: SmtpClient;
|
|
||||||
|
|
||||||
tap.test('setup - start SMTP server for email composition tests', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2570,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(testServer.port).toEqual(2570);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('setup - create SMTP client', async () => {
|
|
||||||
smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const isConnected = await smtpClient.verify();
|
|
||||||
expect(isConnected).toBeTrue();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-01: Basic Headers - should send email with required headers', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Test Email with Basic Headers',
|
|
||||||
text: 'This is the plain text body'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
expect(result.acceptedRecipients).toContain('recipient@example.com');
|
|
||||||
expect(result.messageId).toBeTypeofString();
|
|
||||||
|
|
||||||
console.log('✅ Basic email headers sent successfully');
|
|
||||||
console.log('📧 Message ID:', result.messageId);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-01: Basic Headers - should handle multiple recipients', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient1@example.com', 'recipient2@example.com', 'recipient3@example.com'],
|
|
||||||
subject: 'Email to Multiple Recipients',
|
|
||||||
text: 'This email has multiple recipients'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
expect(result.acceptedRecipients).toContain('recipient1@example.com');
|
|
||||||
expect(result.acceptedRecipients).toContain('recipient2@example.com');
|
|
||||||
expect(result.acceptedRecipients).toContain('recipient3@example.com');
|
|
||||||
|
|
||||||
console.log(`✅ Sent to ${result.acceptedRecipients.length} recipients`);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-01: Basic Headers - should support CC and BCC recipients', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'primary@example.com',
|
|
||||||
cc: ['cc1@example.com', 'cc2@example.com'],
|
|
||||||
bcc: ['bcc1@example.com', 'bcc2@example.com'],
|
|
||||||
subject: 'Email with CC and BCC',
|
|
||||||
text: 'Testing CC and BCC functionality'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
// All recipients should be accepted
|
|
||||||
expect(result.acceptedRecipients.length).toEqual(5);
|
|
||||||
|
|
||||||
console.log('✅ CC and BCC recipients handled correctly');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-01: Basic Headers - should add custom headers', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Email with Custom Headers',
|
|
||||||
text: 'This email contains custom headers',
|
|
||||||
headers: {
|
|
||||||
'X-Custom-Header': 'custom-value',
|
|
||||||
'X-Priority': '1',
|
|
||||||
'X-Mailer': 'DCRouter Test Suite',
|
|
||||||
'Reply-To': 'replies@example.com'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Custom headers added to email');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-01: Basic Headers - should set email priority', async () => {
|
|
||||||
// Test high priority
|
|
||||||
const highPriorityEmail = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'High Priority Email',
|
|
||||||
text: 'This is a high priority message',
|
|
||||||
priority: 'high'
|
|
||||||
});
|
|
||||||
|
|
||||||
const highResult = await smtpClient.sendMail(highPriorityEmail);
|
|
||||||
expect(highResult.success).toBeTrue();
|
|
||||||
|
|
||||||
// Test normal priority
|
|
||||||
const normalPriorityEmail = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Normal Priority Email',
|
|
||||||
text: 'This is a normal priority message',
|
|
||||||
priority: 'normal'
|
|
||||||
});
|
|
||||||
|
|
||||||
const normalResult = await smtpClient.sendMail(normalPriorityEmail);
|
|
||||||
expect(normalResult.success).toBeTrue();
|
|
||||||
|
|
||||||
// Test low priority
|
|
||||||
const lowPriorityEmail = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Low Priority Email',
|
|
||||||
text: 'This is a low priority message',
|
|
||||||
priority: 'low'
|
|
||||||
});
|
|
||||||
|
|
||||||
const lowResult = await smtpClient.sendMail(lowPriorityEmail);
|
|
||||||
expect(lowResult.success).toBeTrue();
|
|
||||||
|
|
||||||
console.log('✅ All priority levels handled correctly');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-01: Basic Headers - should handle sender with display name', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'John Doe <john.doe@example.com>',
|
|
||||||
to: 'Jane Smith <jane.smith@example.com>',
|
|
||||||
subject: 'Email with Display Names',
|
|
||||||
text: 'Testing display names in email addresses'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
expect(result.envelope?.from).toContain('john.doe@example.com');
|
|
||||||
|
|
||||||
console.log('✅ Display names in addresses handled correctly');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-01: Basic Headers - should generate proper Message-ID', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Message-ID Test',
|
|
||||||
text: 'Testing Message-ID generation'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
expect(result.messageId).toBeTypeofString();
|
|
||||||
|
|
||||||
// Message-ID should contain id@domain format (without angle brackets)
|
|
||||||
expect(result.messageId).toMatch(/^.+@.+$/);
|
|
||||||
|
|
||||||
console.log('✅ Valid Message-ID generated:', result.messageId);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-01: Basic Headers - should handle long subject lines', async () => {
|
|
||||||
const longSubject = 'This is a very long subject line that exceeds the typical length and might need to be wrapped according to RFC specifications for email headers';
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: longSubject,
|
|
||||||
text: 'Email with long subject line'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Long subject line handled correctly');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-01: Basic Headers - should sanitize header values', async () => {
|
|
||||||
// Test with potentially problematic characters
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Subject with\nnewline and\rcarriage return',
|
|
||||||
text: 'Testing header sanitization',
|
|
||||||
headers: {
|
|
||||||
'X-Test-Header': 'Value with\nnewline'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Header values sanitized correctly');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-01: Basic Headers - should include Date header', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Date Header Test',
|
|
||||||
text: 'Testing automatic Date header'
|
|
||||||
});
|
|
||||||
|
|
||||||
const beforeSend = new Date();
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
const afterSend = new Date();
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
|
|
||||||
// The email should have been sent between beforeSend and afterSend
|
|
||||||
console.log('✅ Date header automatically included');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - close SMTP client', async () => {
|
|
||||||
if (smtpClient && smtpClient.isConnected()) {
|
|
||||||
await smtpClient.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - stop SMTP server', async () => {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,321 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
let smtpClient: SmtpClient;
|
|
||||||
|
|
||||||
tap.test('setup - start SMTP server for MIME tests', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2571,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false,
|
|
||||||
size: 25 * 1024 * 1024 // 25MB for attachment tests
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(testServer.port).toEqual(2571);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('setup - create SMTP client', async () => {
|
|
||||||
smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
socketTimeout: 60000, // Longer timeout for large attachments
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const isConnected = await smtpClient.verify();
|
|
||||||
expect(isConnected).toBeTrue();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-02: MIME Multipart - should send multipart/alternative (text + HTML)', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Multipart Alternative Test',
|
|
||||||
text: 'This is the plain text version of the email.',
|
|
||||||
html: '<html><body><h1>HTML Version</h1><p>This is the <strong>HTML version</strong> of the email.</p></body></html>'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Multipart/alternative email sent successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-02: MIME Multipart - should send multipart/mixed with attachments', async () => {
|
|
||||||
const textAttachment = Buffer.from('This is a text file attachment content.');
|
|
||||||
const csvData = 'Name,Email,Score\nJohn Doe,john@example.com,95\nJane Smith,jane@example.com,87';
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Multipart Mixed with Attachments',
|
|
||||||
text: 'This email contains attachments.',
|
|
||||||
html: '<p>This email contains <strong>attachments</strong>.</p>',
|
|
||||||
attachments: [
|
|
||||||
{
|
|
||||||
filename: 'document.txt',
|
|
||||||
content: textAttachment,
|
|
||||||
contentType: 'text/plain'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
filename: 'data.csv',
|
|
||||||
content: Buffer.from(csvData),
|
|
||||||
contentType: 'text/csv'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Multipart/mixed with attachments sent successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-02: MIME Multipart - should handle inline images', async () => {
|
|
||||||
// Create a small test image (1x1 red pixel PNG)
|
|
||||||
const redPixelPng = Buffer.from(
|
|
||||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==',
|
|
||||||
'base64'
|
|
||||||
);
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Inline Image Test',
|
|
||||||
text: 'This email contains an inline image.',
|
|
||||||
html: '<p>Here is an inline image: <img src="cid:red-pixel" alt="Red Pixel"></p>',
|
|
||||||
attachments: [
|
|
||||||
{
|
|
||||||
filename: 'red-pixel.png',
|
|
||||||
content: redPixelPng,
|
|
||||||
contentType: 'image/png',
|
|
||||||
contentId: 'red-pixel' // Content-ID for inline reference
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Email with inline image sent successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-02: MIME Multipart - should handle multiple attachment types', async () => {
|
|
||||||
const attachments = [
|
|
||||||
{
|
|
||||||
filename: 'text.txt',
|
|
||||||
content: Buffer.from('Plain text file'),
|
|
||||||
contentType: 'text/plain'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
filename: 'data.json',
|
|
||||||
content: Buffer.from(JSON.stringify({ test: 'data', value: 123 })),
|
|
||||||
contentType: 'application/json'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
filename: 'binary.bin',
|
|
||||||
content: Buffer.from([0x00, 0x01, 0x02, 0x03, 0xFF, 0xFE, 0xFD]),
|
|
||||||
contentType: 'application/octet-stream'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
filename: 'document.pdf',
|
|
||||||
content: Buffer.from('%PDF-1.4\n%fake pdf content for testing'),
|
|
||||||
contentType: 'application/pdf'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Multiple Attachment Types',
|
|
||||||
text: 'Testing various attachment types',
|
|
||||||
attachments
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Multiple attachment types handled correctly');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-02: MIME Multipart - should encode binary attachments with base64', async () => {
|
|
||||||
// Create binary data with all byte values
|
|
||||||
const binaryData = Buffer.alloc(256);
|
|
||||||
for (let i = 0; i < 256; i++) {
|
|
||||||
binaryData[i] = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Binary Attachment Encoding Test',
|
|
||||||
text: 'This email contains binary data that must be base64 encoded',
|
|
||||||
attachments: [
|
|
||||||
{
|
|
||||||
filename: 'binary-data.bin',
|
|
||||||
content: binaryData,
|
|
||||||
contentType: 'application/octet-stream',
|
|
||||||
encoding: 'base64' // Explicitly specify encoding
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Binary attachment base64 encoded correctly');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-02: MIME Multipart - should handle large attachments', async () => {
|
|
||||||
// Create a 5MB attachment
|
|
||||||
const largeData = Buffer.alloc(5 * 1024 * 1024);
|
|
||||||
for (let i = 0; i < largeData.length; i++) {
|
|
||||||
largeData[i] = i % 256;
|
|
||||||
}
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Large Attachment Test',
|
|
||||||
text: 'This email contains a large attachment',
|
|
||||||
attachments: [
|
|
||||||
{
|
|
||||||
filename: 'large-file.dat',
|
|
||||||
content: largeData,
|
|
||||||
contentType: 'application/octet-stream'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log(`✅ Large attachment (5MB) sent in ${duration}ms`);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-02: MIME Multipart - should handle nested multipart structures', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Complex Multipart Structure',
|
|
||||||
text: 'Plain text version',
|
|
||||||
html: '<p>HTML version with <img src="cid:logo" alt="Logo"></p>',
|
|
||||||
attachments: [
|
|
||||||
{
|
|
||||||
filename: 'logo.png',
|
|
||||||
content: Buffer.from('fake png data'),
|
|
||||||
contentType: 'image/png',
|
|
||||||
contentId: 'logo' // Inline image
|
|
||||||
},
|
|
||||||
{
|
|
||||||
filename: 'attachment.txt',
|
|
||||||
content: Buffer.from('Regular attachment'),
|
|
||||||
contentType: 'text/plain'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Nested multipart structure (mixed + related + alternative) handled');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-02: MIME Multipart - should handle attachment filenames with special characters', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Special Filename Test',
|
|
||||||
text: 'Testing attachments with special filenames',
|
|
||||||
attachments: [
|
|
||||||
{
|
|
||||||
filename: 'file with spaces.txt',
|
|
||||||
content: Buffer.from('Content 1'),
|
|
||||||
contentType: 'text/plain'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
filename: 'файл.txt', // Cyrillic
|
|
||||||
content: Buffer.from('Content 2'),
|
|
||||||
contentType: 'text/plain'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
filename: '文件.txt', // Chinese
|
|
||||||
content: Buffer.from('Content 3'),
|
|
||||||
contentType: 'text/plain'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Special characters in filenames handled correctly');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-02: MIME Multipart - should handle empty attachments', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Empty Attachment Test',
|
|
||||||
text: 'This email has an empty attachment',
|
|
||||||
attachments: [
|
|
||||||
{
|
|
||||||
filename: 'empty.txt',
|
|
||||||
content: Buffer.from(''), // Empty content
|
|
||||||
contentType: 'text/plain'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Empty attachment handled correctly');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-02: MIME Multipart - should respect content-type parameters', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Content-Type Parameters Test',
|
|
||||||
text: 'Testing content-type with charset',
|
|
||||||
html: '<p>HTML with specific charset</p>',
|
|
||||||
attachments: [
|
|
||||||
{
|
|
||||||
filename: 'utf8-text.txt',
|
|
||||||
content: Buffer.from('UTF-8 text: 你好世界'),
|
|
||||||
contentType: 'text/plain; charset=utf-8'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
filename: 'data.xml',
|
|
||||||
content: Buffer.from('<?xml version="1.0" encoding="UTF-8"?><root>Test</root>'),
|
|
||||||
contentType: 'application/xml; charset=utf-8'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Content-type parameters preserved correctly');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - close SMTP client', async () => {
|
|
||||||
if (smtpClient && smtpClient.isConnected()) {
|
|
||||||
await smtpClient.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - stop SMTP server', async () => {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,334 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
import * as crypto from 'crypto';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
let smtpClient: SmtpClient;
|
|
||||||
|
|
||||||
tap.test('setup - start SMTP server for attachment encoding tests', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2572,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false,
|
|
||||||
size: 50 * 1024 * 1024 // 50MB for large attachment tests
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(testServer.port).toEqual(2572);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('setup - create SMTP client', async () => {
|
|
||||||
smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
socketTimeout: 120000, // 2 minutes for large attachments
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const isConnected = await smtpClient.verify();
|
|
||||||
expect(isConnected).toBeTrue();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-03: Attachment Encoding - should encode text attachment with base64', async () => {
|
|
||||||
const textContent = 'This is a test text file.\nIt contains multiple lines.\nAnd some special characters: © ® ™';
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Text Attachment Base64 Test',
|
|
||||||
text: 'Email with text attachment',
|
|
||||||
attachments: [{
|
|
||||||
filename: 'test.txt',
|
|
||||||
content: Buffer.from(textContent),
|
|
||||||
contentType: 'text/plain',
|
|
||||||
encoding: 'base64'
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Text attachment encoded with base64');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-03: Attachment Encoding - should encode binary data correctly', async () => {
|
|
||||||
// Create binary data with all possible byte values
|
|
||||||
const binaryData = Buffer.alloc(256);
|
|
||||||
for (let i = 0; i < 256; i++) {
|
|
||||||
binaryData[i] = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Binary Attachment Test',
|
|
||||||
text: 'Email with binary attachment',
|
|
||||||
attachments: [{
|
|
||||||
filename: 'binary.dat',
|
|
||||||
content: binaryData,
|
|
||||||
contentType: 'application/octet-stream'
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Binary data encoded correctly');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-03: Attachment Encoding - should handle various file types', async () => {
|
|
||||||
const attachments = [
|
|
||||||
{
|
|
||||||
filename: 'image.jpg',
|
|
||||||
content: Buffer.from('/9j/4AAQSkZJRgABAQEASABIAAD/2wBD', 'base64'), // Partial JPEG header
|
|
||||||
contentType: 'image/jpeg'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
filename: 'document.pdf',
|
|
||||||
content: Buffer.from('%PDF-1.4\n%âÃÏÓ\n', 'utf8'),
|
|
||||||
contentType: 'application/pdf'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
filename: 'archive.zip',
|
|
||||||
content: Buffer.from('PK\x03\x04'), // ZIP magic number
|
|
||||||
contentType: 'application/zip'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
filename: 'audio.mp3',
|
|
||||||
content: Buffer.from('ID3'), // MP3 ID3 tag
|
|
||||||
contentType: 'audio/mpeg'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Multiple File Types Test',
|
|
||||||
text: 'Testing various attachment types',
|
|
||||||
attachments
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Various file types encoded correctly');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-03: Attachment Encoding - should handle quoted-printable encoding', async () => {
|
|
||||||
const textWithSpecialChars = 'This line has special chars: café, naïve, résumé\r\nThis line is very long and might need soft line breaks when encoded with quoted-printable encoding method\r\n=This line starts with equals sign';
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Quoted-Printable Test',
|
|
||||||
text: 'Email with quoted-printable attachment',
|
|
||||||
attachments: [{
|
|
||||||
filename: 'special-chars.txt',
|
|
||||||
content: Buffer.from(textWithSpecialChars, 'utf8'),
|
|
||||||
contentType: 'text/plain; charset=utf-8',
|
|
||||||
encoding: 'quoted-printable'
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Quoted-printable encoding handled correctly');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-03: Attachment Encoding - should handle content-disposition', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Content-Disposition Test',
|
|
||||||
text: 'Testing attachment vs inline disposition',
|
|
||||||
html: '<p>Image below: <img src="cid:inline-image"></p>',
|
|
||||||
attachments: [
|
|
||||||
{
|
|
||||||
filename: 'attachment.txt',
|
|
||||||
content: Buffer.from('This is an attachment'),
|
|
||||||
contentType: 'text/plain'
|
|
||||||
// Default disposition is 'attachment'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
filename: 'inline-image.png',
|
|
||||||
content: Buffer.from('fake png data'),
|
|
||||||
contentType: 'image/png',
|
|
||||||
contentId: 'inline-image' // Makes it inline
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Content-disposition handled correctly');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-03: Attachment Encoding - should handle large attachments efficiently', async () => {
|
|
||||||
// Create a 10MB attachment
|
|
||||||
const largeSize = 10 * 1024 * 1024;
|
|
||||||
const largeData = crypto.randomBytes(largeSize);
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Large Attachment Test',
|
|
||||||
text: 'Email with large attachment',
|
|
||||||
attachments: [{
|
|
||||||
filename: 'large-file.bin',
|
|
||||||
content: largeData,
|
|
||||||
contentType: 'application/octet-stream'
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log(`✅ Large attachment (${largeSize / 1024 / 1024}MB) sent in ${duration}ms`);
|
|
||||||
console.log(` Throughput: ${(largeSize / 1024 / 1024 / (duration / 1000)).toFixed(2)} MB/s`);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-03: Attachment Encoding - should handle Unicode filenames', async () => {
|
|
||||||
const unicodeAttachments = [
|
|
||||||
{
|
|
||||||
filename: '文档.txt', // Chinese
|
|
||||||
content: Buffer.from('Chinese filename test'),
|
|
||||||
contentType: 'text/plain'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
filename: 'файл.txt', // Russian
|
|
||||||
content: Buffer.from('Russian filename test'),
|
|
||||||
contentType: 'text/plain'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
filename: 'ファイル.txt', // Japanese
|
|
||||||
content: Buffer.from('Japanese filename test'),
|
|
||||||
contentType: 'text/plain'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
filename: '🎉emoji🎊.txt', // Emoji
|
|
||||||
content: Buffer.from('Emoji filename test'),
|
|
||||||
contentType: 'text/plain'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Unicode Filenames Test',
|
|
||||||
text: 'Testing Unicode characters in filenames',
|
|
||||||
attachments: unicodeAttachments
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Unicode filenames encoded correctly');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-03: Attachment Encoding - should handle special MIME headers', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'MIME Headers Test',
|
|
||||||
text: 'Testing special MIME headers',
|
|
||||||
attachments: [{
|
|
||||||
filename: 'report.xml',
|
|
||||||
content: Buffer.from('<?xml version="1.0"?><root>test</root>'),
|
|
||||||
contentType: 'application/xml; charset=utf-8',
|
|
||||||
encoding: 'base64',
|
|
||||||
headers: {
|
|
||||||
'Content-Description': 'Monthly Report',
|
|
||||||
'Content-Transfer-Encoding': 'base64',
|
|
||||||
'Content-ID': '<report-2024-01@example.com>'
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Special MIME headers handled correctly');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-03: Attachment Encoding - should handle attachment size limits', async () => {
|
|
||||||
// Test with attachment near server limit
|
|
||||||
const nearLimitSize = 45 * 1024 * 1024; // 45MB (near 50MB limit)
|
|
||||||
const nearLimitData = Buffer.alloc(nearLimitSize);
|
|
||||||
|
|
||||||
// Fill with some pattern to avoid compression benefits
|
|
||||||
for (let i = 0; i < nearLimitSize; i++) {
|
|
||||||
nearLimitData[i] = i % 256;
|
|
||||||
}
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Near Size Limit Test',
|
|
||||||
text: 'Testing attachment near size limit',
|
|
||||||
attachments: [{
|
|
||||||
filename: 'near-limit.bin',
|
|
||||||
content: nearLimitData,
|
|
||||||
contentType: 'application/octet-stream'
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log(`✅ Attachment near size limit (${nearLimitSize / 1024 / 1024}MB) accepted`);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-03: Attachment Encoding - should handle mixed encoding types', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Mixed Encoding Test',
|
|
||||||
text: 'Plain text body',
|
|
||||||
html: '<p>HTML body with special chars: café</p>',
|
|
||||||
attachments: [
|
|
||||||
{
|
|
||||||
filename: 'base64.bin',
|
|
||||||
content: crypto.randomBytes(1024),
|
|
||||||
contentType: 'application/octet-stream',
|
|
||||||
encoding: 'base64'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
filename: 'quoted.txt',
|
|
||||||
content: Buffer.from('Text with special chars: naïve café résumé'),
|
|
||||||
contentType: 'text/plain; charset=utf-8',
|
|
||||||
encoding: 'quoted-printable'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
filename: '7bit.txt',
|
|
||||||
content: Buffer.from('Simple ASCII text only'),
|
|
||||||
contentType: 'text/plain',
|
|
||||||
encoding: '7bit'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Mixed encoding types handled correctly');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - close SMTP client', async () => {
|
|
||||||
if (smtpClient && smtpClient.isConnected()) {
|
|
||||||
await smtpClient.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - stop SMTP server', async () => {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup test SMTP server', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2577,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
expect(testServer).toBeTruthy();
|
|
||||||
expect(testServer.port).toEqual(2577);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-04: Basic BCC handling', async () => {
|
|
||||||
console.log('Testing basic BCC handling');
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create email with BCC recipients
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['visible@example.com'],
|
|
||||||
bcc: ['hidden1@example.com', 'hidden2@example.com'],
|
|
||||||
subject: 'BCC Test Email',
|
|
||||||
text: 'This email tests BCC functionality'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.messageId).toBeDefined();
|
|
||||||
|
|
||||||
console.log('Successfully sent email with BCC recipients');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-04: Multiple BCC recipients', async () => {
|
|
||||||
console.log('Testing multiple BCC recipients');
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create email with many BCC recipients
|
|
||||||
const bccRecipients = Array.from({ length: 10 },
|
|
||||||
(_, i) => `bcc${i + 1}@example.com`
|
|
||||||
);
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['primary@example.com'],
|
|
||||||
bcc: bccRecipients,
|
|
||||||
subject: 'Multiple BCC Test',
|
|
||||||
text: 'Testing with multiple BCC recipients'
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Sending email with ${bccRecipients.length} BCC recipients...`);
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
const elapsed = Date.now() - startTime;
|
|
||||||
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.messageId).toBeDefined();
|
|
||||||
|
|
||||||
console.log(`Processed ${bccRecipients.length} BCC recipients in ${elapsed}ms`);
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-04: BCC-only email', async () => {
|
|
||||||
console.log('Testing BCC-only email');
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create email with only BCC recipients (no TO or CC)
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
bcc: ['hidden1@example.com', 'hidden2@example.com', 'hidden3@example.com'],
|
|
||||||
subject: 'BCC-Only Email',
|
|
||||||
text: 'This email has only BCC recipients'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.messageId).toBeDefined();
|
|
||||||
|
|
||||||
console.log('Successfully sent BCC-only email');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-04: Mixed recipient types', async () => {
|
|
||||||
console.log('Testing mixed recipient types');
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create email with all recipient types
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['to1@example.com', 'to2@example.com'],
|
|
||||||
cc: ['cc1@example.com', 'cc2@example.com'],
|
|
||||||
bcc: ['bcc1@example.com', 'bcc2@example.com'],
|
|
||||||
subject: 'Mixed Recipients Test',
|
|
||||||
text: 'Testing all recipient types together'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.messageId).toBeDefined();
|
|
||||||
|
|
||||||
console.log('Recipient breakdown:');
|
|
||||||
console.log(` TO: ${email.to?.length || 0} recipients`);
|
|
||||||
console.log(` CC: ${email.cc?.length || 0} recipients`);
|
|
||||||
console.log(` BCC: ${email.bcc?.length || 0} recipients`);
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-04: BCC with special characters in addresses', async () => {
|
|
||||||
console.log('Testing BCC with special characters');
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// BCC addresses with special characters
|
|
||||||
const specialBccAddresses = [
|
|
||||||
'user+tag@example.com',
|
|
||||||
'first.last@example.com',
|
|
||||||
'user_name@example.com'
|
|
||||||
];
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['visible@example.com'],
|
|
||||||
bcc: specialBccAddresses,
|
|
||||||
subject: 'BCC Special Characters Test',
|
|
||||||
text: 'Testing BCC with special character addresses'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.messageId).toBeDefined();
|
|
||||||
|
|
||||||
console.log('Successfully processed BCC addresses with special characters');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup test SMTP server', async () => {
|
|
||||||
if (testServer) {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup test SMTP server', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2578,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
expect(testServer).toBeTruthy();
|
|
||||||
expect(testServer.port).toEqual(2578);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-05: Basic Reply-To header', async () => {
|
|
||||||
console.log('Testing basic Reply-To header');
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create email with Reply-To header
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
replyTo: 'replies@example.com',
|
|
||||||
subject: 'Reply-To Test',
|
|
||||||
text: 'This email tests Reply-To header functionality'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.messageId).toBeDefined();
|
|
||||||
|
|
||||||
console.log('Successfully sent email with Reply-To header');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-05: Multiple Reply-To addresses', async () => {
|
|
||||||
console.log('Testing multiple Reply-To addresses');
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create email with multiple Reply-To addresses
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
replyTo: ['reply1@example.com', 'reply2@example.com'],
|
|
||||||
subject: 'Multiple Reply-To Test',
|
|
||||||
text: 'This email tests multiple Reply-To addresses'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.messageId).toBeDefined();
|
|
||||||
|
|
||||||
console.log('Successfully sent email with multiple Reply-To addresses');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-05: Reply-To with display names', async () => {
|
|
||||||
console.log('Testing Reply-To with display names');
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create email with Reply-To containing display names
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
replyTo: 'Support Team <support@example.com>',
|
|
||||||
subject: 'Reply-To Display Name Test',
|
|
||||||
text: 'This email tests Reply-To with display names'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.messageId).toBeDefined();
|
|
||||||
|
|
||||||
console.log('Successfully sent email with Reply-To display name');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-05: Return-Path header', async () => {
|
|
||||||
console.log('Testing Return-Path header');
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create email with custom Return-Path
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Return-Path Test',
|
|
||||||
text: 'This email tests Return-Path functionality',
|
|
||||||
headers: {
|
|
||||||
'Return-Path': '<bounces@example.com>'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.messageId).toBeDefined();
|
|
||||||
|
|
||||||
console.log('Successfully sent email with Return-Path header');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-05: Different From and Return-Path', async () => {
|
|
||||||
console.log('Testing different From and Return-Path addresses');
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create email with different From and Return-Path
|
|
||||||
const email = new Email({
|
|
||||||
from: 'noreply@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Different Return-Path Test',
|
|
||||||
text: 'This email has different From and Return-Path addresses',
|
|
||||||
headers: {
|
|
||||||
'Return-Path': '<bounces+tracking@example.com>'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.messageId).toBeDefined();
|
|
||||||
|
|
||||||
console.log('Successfully sent email with different From and Return-Path');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-05: Reply-To and Return-Path together', async () => {
|
|
||||||
console.log('Testing Reply-To and Return-Path together');
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create email with both Reply-To and Return-Path
|
|
||||||
const email = new Email({
|
|
||||||
from: 'notifications@example.com',
|
|
||||||
to: ['user@example.com'],
|
|
||||||
replyTo: 'support@example.com',
|
|
||||||
subject: 'Reply-To and Return-Path Test',
|
|
||||||
text: 'This email tests both Reply-To and Return-Path headers',
|
|
||||||
headers: {
|
|
||||||
'Return-Path': '<bounces@example.com>'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.messageId).toBeDefined();
|
|
||||||
|
|
||||||
console.log('Successfully sent email with both Reply-To and Return-Path');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-05: International characters in Reply-To', async () => {
|
|
||||||
console.log('Testing international characters in Reply-To');
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create email with international characters in Reply-To
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
replyTo: 'Suppört Téam <support@example.com>',
|
|
||||||
subject: 'International Reply-To Test',
|
|
||||||
text: 'This email tests international characters in Reply-To'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.messageId).toBeDefined();
|
|
||||||
|
|
||||||
console.log('Successfully sent email with international Reply-To');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-05: Empty and invalid Reply-To handling', async () => {
|
|
||||||
console.log('Testing empty and invalid Reply-To handling');
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test with empty Reply-To (should work)
|
|
||||||
const email1 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'No Reply-To Test',
|
|
||||||
text: 'This email has no Reply-To header'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result1 = await smtpClient.sendMail(email1);
|
|
||||||
expect(result1).toBeDefined();
|
|
||||||
expect(result1.messageId).toBeDefined();
|
|
||||||
|
|
||||||
console.log('Successfully sent email without Reply-To');
|
|
||||||
|
|
||||||
// Test with empty string Reply-To
|
|
||||||
const email2 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
replyTo: '',
|
|
||||||
subject: 'Empty Reply-To Test',
|
|
||||||
text: 'This email has empty Reply-To'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result2 = await smtpClient.sendMail(email2);
|
|
||||||
expect(result2).toBeDefined();
|
|
||||||
expect(result2.messageId).toBeDefined();
|
|
||||||
|
|
||||||
console.log('Successfully sent email with empty Reply-To');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup test SMTP server', async () => {
|
|
||||||
if (testServer) {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup test SMTP server', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2579,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
expect(testServer).toBeTruthy();
|
|
||||||
expect(testServer.port).toEqual(2579);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-06: Basic UTF-8 characters', async () => {
|
|
||||||
console.log('Testing basic UTF-8 characters');
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Email with basic UTF-8 characters
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'UTF-8 Test: café, naïve, résumé',
|
|
||||||
text: 'This email contains UTF-8 characters: café, naïve, résumé, piñata',
|
|
||||||
html: '<p>HTML with UTF-8: <strong>café</strong>, <em>naïve</em>, résumé, piñata</p>'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.messageId).toBeDefined();
|
|
||||||
|
|
||||||
console.log('Successfully sent email with basic UTF-8 characters');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-06: European characters', async () => {
|
|
||||||
console.log('Testing European characters');
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Email with European characters
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'European: ñ, ü, ø, å, ß, æ',
|
|
||||||
text: [
|
|
||||||
'German: Müller, Größe, Weiß',
|
|
||||||
'Spanish: niño, señor, España',
|
|
||||||
'French: français, crème, être',
|
|
||||||
'Nordic: København, Göteborg, Ålesund',
|
|
||||||
'Polish: Kraków, Gdańsk, Wrocław'
|
|
||||||
].join('\n'),
|
|
||||||
html: `
|
|
||||||
<h1>European Characters Test</h1>
|
|
||||||
<ul>
|
|
||||||
<li>German: Müller, Größe, Weiß</li>
|
|
||||||
<li>Spanish: niño, señor, España</li>
|
|
||||||
<li>French: français, crème, être</li>
|
|
||||||
<li>Nordic: København, Göteborg, Ålesund</li>
|
|
||||||
<li>Polish: Kraków, Gdańsk, Wrocław</li>
|
|
||||||
</ul>
|
|
||||||
`
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.messageId).toBeDefined();
|
|
||||||
|
|
||||||
console.log('Successfully sent email with European characters');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-06: Asian characters', async () => {
|
|
||||||
console.log('Testing Asian characters');
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Email with Asian characters
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Asian: 你好, こんにちは, 안녕하세요',
|
|
||||||
text: [
|
|
||||||
'Chinese (Simplified): 你好世界',
|
|
||||||
'Chinese (Traditional): 你好世界',
|
|
||||||
'Japanese: こんにちは世界',
|
|
||||||
'Korean: 안녕하세요 세계',
|
|
||||||
'Thai: สวัสดีโลก',
|
|
||||||
'Hindi: नमस्ते संसार'
|
|
||||||
].join('\n'),
|
|
||||||
html: `
|
|
||||||
<h1>Asian Characters Test</h1>
|
|
||||||
<table>
|
|
||||||
<tr><td>Chinese (Simplified):</td><td>你好世界</td></tr>
|
|
||||||
<tr><td>Chinese (Traditional):</td><td>你好世界</td></tr>
|
|
||||||
<tr><td>Japanese:</td><td>こんにちは世界</td></tr>
|
|
||||||
<tr><td>Korean:</td><td>안녕하세요 세계</td></tr>
|
|
||||||
<tr><td>Thai:</td><td>สวัสดีโลก</td></tr>
|
|
||||||
<tr><td>Hindi:</td><td>नमस्ते संसार</td></tr>
|
|
||||||
</table>
|
|
||||||
`
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.messageId).toBeDefined();
|
|
||||||
|
|
||||||
console.log('Successfully sent email with Asian characters');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-06: Emojis and symbols', async () => {
|
|
||||||
console.log('Testing emojis and symbols');
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Email with emojis and symbols
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Emojis: 🎉 🚀 ✨ 🌈',
|
|
||||||
text: [
|
|
||||||
'Faces: 😀 😃 😄 😁 😆 😅 😂',
|
|
||||||
'Objects: 🎉 🚀 ✨ 🌈 ⭐ 🔥 💎',
|
|
||||||
'Animals: 🐶 🐱 🐭 🐹 🐰 🦊 🐻',
|
|
||||||
'Food: 🍎 🍌 🍇 🍓 🥝 🍅 🥑',
|
|
||||||
'Symbols: ✓ ✗ ⚠ ♠ ♣ ♥ ♦',
|
|
||||||
'Math: ∑ ∏ ∫ ∞ ± × ÷ ≠ ≤ ≥'
|
|
||||||
].join('\n'),
|
|
||||||
html: `
|
|
||||||
<h1>Emojis and Symbols Test 🎉</h1>
|
|
||||||
<p>Faces: 😀 😃 😄 😁 😆 😅 😂</p>
|
|
||||||
<p>Objects: 🎉 🚀 ✨ 🌈 ⭐ 🔥 💎</p>
|
|
||||||
<p>Animals: 🐶 🐱 🐭 🐹 🐰 🦊 🐻</p>
|
|
||||||
<p>Food: 🍎 🍌 🍇 🍓 🥝 🍅 🥑</p>
|
|
||||||
<p>Symbols: ✓ ✗ ⚠ ♠ ♣ ♥ ♦</p>
|
|
||||||
<p>Math: ∑ ∏ ∫ ∞ ± × ÷ ≠ ≤ ≥</p>
|
|
||||||
`
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.messageId).toBeDefined();
|
|
||||||
|
|
||||||
console.log('Successfully sent email with emojis and symbols');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-06: Mixed international content', async () => {
|
|
||||||
console.log('Testing mixed international content');
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Email with mixed international content
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Mixed: Hello 你好 مرحبا こんにちは 🌍',
|
|
||||||
text: [
|
|
||||||
'English: Hello World!',
|
|
||||||
'Chinese: 你好世界!',
|
|
||||||
'Arabic: مرحبا بالعالم!',
|
|
||||||
'Japanese: こんにちは世界!',
|
|
||||||
'Russian: Привет мир!',
|
|
||||||
'Greek: Γεια σας κόσμε!',
|
|
||||||
'Mixed: Hello 世界 🌍 مرحبا こんにちは!'
|
|
||||||
].join('\n'),
|
|
||||||
html: `
|
|
||||||
<h1>International Mix 🌍</h1>
|
|
||||||
<div style="font-family: Arial, sans-serif;">
|
|
||||||
<p><strong>English:</strong> Hello World!</p>
|
|
||||||
<p><strong>Chinese:</strong> 你好世界!</p>
|
|
||||||
<p><strong>Arabic:</strong> مرحبا بالعالم!</p>
|
|
||||||
<p><strong>Japanese:</strong> こんにちは世界!</p>
|
|
||||||
<p><strong>Russian:</strong> Привет мир!</p>
|
|
||||||
<p><strong>Greek:</strong> Γεια σας κόσμε!</p>
|
|
||||||
<p><strong>Mixed:</strong> Hello 世界 🌍 مرحبا こんにちは!</p>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.messageId).toBeDefined();
|
|
||||||
|
|
||||||
console.log('Successfully sent email with mixed international content');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup test SMTP server', async () => {
|
|
||||||
if (testServer) {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,489 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup test SMTP server', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2567,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
expect(testServer).toBeTruthy();
|
|
||||||
expect(testServer.port).toEqual(2567);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-07: Basic HTML email', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create HTML email
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'HTML Email Test',
|
|
||||||
html: `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<style>
|
|
||||||
body { font-family: Arial, sans-serif; }
|
|
||||||
.header { color: #333; background: #f0f0f0; padding: 20px; }
|
|
||||||
.content { padding: 20px; }
|
|
||||||
.footer { color: #666; font-size: 12px; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="header">
|
|
||||||
<h1>Welcome!</h1>
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<p>This is an <strong>HTML email</strong> with <em>formatting</em>.</p>
|
|
||||||
<ul>
|
|
||||||
<li>Feature 1</li>
|
|
||||||
<li>Feature 2</li>
|
|
||||||
<li>Feature 3</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="footer">
|
|
||||||
<p>© 2024 Example Corp</p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`,
|
|
||||||
text: 'Welcome! This is an HTML email with formatting. Features: 1, 2, 3. © 2024 Example Corp'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
console.log('Basic HTML email sent successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-07: HTML email with inline images', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 10000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create a simple 1x1 red pixel PNG
|
|
||||||
const redPixelBase64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==';
|
|
||||||
|
|
||||||
// Create HTML email with inline image
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Email with Inline Images',
|
|
||||||
html: `
|
|
||||||
<html>
|
|
||||||
<body>
|
|
||||||
<h1>Email with Inline Images</h1>
|
|
||||||
<p>Here's an inline image:</p>
|
|
||||||
<img src="cid:image001" alt="Red pixel" width="100" height="100">
|
|
||||||
<p>And here's another one:</p>
|
|
||||||
<img src="cid:logo" alt="Company logo">
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`,
|
|
||||||
attachments: [
|
|
||||||
{
|
|
||||||
filename: 'red-pixel.png',
|
|
||||||
content: Buffer.from(redPixelBase64, 'base64'),
|
|
||||||
contentType: 'image/png',
|
|
||||||
cid: 'image001' // Content-ID for inline reference
|
|
||||||
},
|
|
||||||
{
|
|
||||||
filename: 'logo.png',
|
|
||||||
content: Buffer.from(redPixelBase64, 'base64'), // Reuse for demo
|
|
||||||
contentType: 'image/png',
|
|
||||||
cid: 'logo'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
console.log('HTML email with inline images sent successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-07: Complex HTML with multiple inline resources', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 10000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create email with multiple inline resources
|
|
||||||
const email = new Email({
|
|
||||||
from: 'newsletter@example.com',
|
|
||||||
to: 'subscriber@example.com',
|
|
||||||
subject: 'Newsletter with Rich Content',
|
|
||||||
html: `
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<style>
|
|
||||||
body { font-family: Arial, sans-serif; margin: 0; padding: 0; }
|
|
||||||
.header { background: url('cid:header-bg') center/cover; height: 200px; }
|
|
||||||
.logo { width: 150px; }
|
|
||||||
.product { display: inline-block; margin: 10px; }
|
|
||||||
.product img { width: 100px; height: 100px; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="header">
|
|
||||||
<img src="cid:logo" alt="Company Logo" class="logo">
|
|
||||||
</div>
|
|
||||||
<h1>Monthly Newsletter</h1>
|
|
||||||
<div class="products">
|
|
||||||
<div class="product">
|
|
||||||
<img src="cid:product1" alt="Product 1">
|
|
||||||
<p>Product 1</p>
|
|
||||||
</div>
|
|
||||||
<div class="product">
|
|
||||||
<img src="cid:product2" alt="Product 2">
|
|
||||||
<p>Product 2</p>
|
|
||||||
</div>
|
|
||||||
<div class="product">
|
|
||||||
<img src="cid:product3" alt="Product 3">
|
|
||||||
<p>Product 3</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<img src="cid:footer-divider" alt="" style="width: 100%; height: 2px;">
|
|
||||||
<p>© 2024 Example Corp</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`,
|
|
||||||
text: 'Monthly Newsletter - View in HTML for best experience',
|
|
||||||
attachments: [
|
|
||||||
{
|
|
||||||
filename: 'header-bg.jpg',
|
|
||||||
content: Buffer.from('fake-image-data'),
|
|
||||||
contentType: 'image/jpeg',
|
|
||||||
cid: 'header-bg'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
filename: 'logo.png',
|
|
||||||
content: Buffer.from('fake-logo-data'),
|
|
||||||
contentType: 'image/png',
|
|
||||||
cid: 'logo'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
filename: 'product1.jpg',
|
|
||||||
content: Buffer.from('fake-product1-data'),
|
|
||||||
contentType: 'image/jpeg',
|
|
||||||
cid: 'product1'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
filename: 'product2.jpg',
|
|
||||||
content: Buffer.from('fake-product2-data'),
|
|
||||||
contentType: 'image/jpeg',
|
|
||||||
cid: 'product2'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
filename: 'product3.jpg',
|
|
||||||
content: Buffer.from('fake-product3-data'),
|
|
||||||
contentType: 'image/jpeg',
|
|
||||||
cid: 'product3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
filename: 'divider.gif',
|
|
||||||
content: Buffer.from('fake-divider-data'),
|
|
||||||
contentType: 'image/gif',
|
|
||||||
cid: 'footer-divider'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
console.log('Complex HTML with multiple inline resources sent successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-07: HTML with external and inline images mixed', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mix of inline and external images
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Mixed Image Sources',
|
|
||||||
html: `
|
|
||||||
<html>
|
|
||||||
<body>
|
|
||||||
<h1>Mixed Image Sources</h1>
|
|
||||||
<h2>Inline Image:</h2>
|
|
||||||
<img src="cid:inline-logo" alt="Inline Logo" width="100">
|
|
||||||
<h2>External Images:</h2>
|
|
||||||
<img src="https://via.placeholder.com/150" alt="External Image 1">
|
|
||||||
<img src="http://example.com/image.jpg" alt="External Image 2">
|
|
||||||
<h2>Data URI Image:</h2>
|
|
||||||
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==" alt="Data URI">
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`,
|
|
||||||
attachments: [
|
|
||||||
{
|
|
||||||
filename: 'logo.png',
|
|
||||||
content: Buffer.from('logo-data'),
|
|
||||||
contentType: 'image/png',
|
|
||||||
cid: 'inline-logo'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
console.log('Successfully sent email with mixed image sources');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-07: HTML email responsive design', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Responsive HTML email
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Responsive HTML Email',
|
|
||||||
html: `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<style>
|
|
||||||
@media screen and (max-width: 600px) {
|
|
||||||
.container { width: 100% !important; }
|
|
||||||
.column { width: 100% !important; display: block !important; }
|
|
||||||
.mobile-hide { display: none !important; }
|
|
||||||
}
|
|
||||||
.container { width: 600px; margin: 0 auto; }
|
|
||||||
.column { width: 48%; display: inline-block; vertical-align: top; }
|
|
||||||
img { max-width: 100%; height: auto; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<h1>Responsive Design Test</h1>
|
|
||||||
<div class="column">
|
|
||||||
<img src="cid:left-image" alt="Left Column">
|
|
||||||
<p>Left column content</p>
|
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
<img src="cid:right-image" alt="Right Column">
|
|
||||||
<p>Right column content</p>
|
|
||||||
</div>
|
|
||||||
<p class="mobile-hide">This text is hidden on mobile devices</p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`,
|
|
||||||
text: 'Responsive Design Test - View in HTML',
|
|
||||||
attachments: [
|
|
||||||
{
|
|
||||||
filename: 'left.jpg',
|
|
||||||
content: Buffer.from('left-image-data'),
|
|
||||||
contentType: 'image/jpeg',
|
|
||||||
cid: 'left-image'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
filename: 'right.jpg',
|
|
||||||
content: Buffer.from('right-image-data'),
|
|
||||||
contentType: 'image/jpeg',
|
|
||||||
cid: 'right-image'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
console.log('Successfully sent responsive HTML email');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-07: HTML sanitization and security', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Email with potentially dangerous HTML
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'HTML Security Test',
|
|
||||||
html: `
|
|
||||||
<html>
|
|
||||||
<body>
|
|
||||||
<h1>Security Test</h1>
|
|
||||||
<!-- Scripts should be handled safely -->
|
|
||||||
<script>alert('This should not execute');</script>
|
|
||||||
<img src="x" onerror="alert('XSS')">
|
|
||||||
<a href="javascript:alert('Click')">Dangerous Link</a>
|
|
||||||
<iframe src="https://evil.com"></iframe>
|
|
||||||
<form action="https://evil.com/steal">
|
|
||||||
<input type="text" name="data">
|
|
||||||
</form>
|
|
||||||
<!-- Safe content -->
|
|
||||||
<p>This is safe text content.</p>
|
|
||||||
<img src="cid:safe-image" alt="Safe Image">
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`,
|
|
||||||
text: 'Security Test - Plain text version',
|
|
||||||
attachments: [
|
|
||||||
{
|
|
||||||
filename: 'safe.png',
|
|
||||||
content: Buffer.from('safe-image-data'),
|
|
||||||
contentType: 'image/png',
|
|
||||||
cid: 'safe-image'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
console.log('HTML security test sent successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-07: Large HTML email with many inline images', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 30000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create email with many inline images
|
|
||||||
const imageCount = 10; // Reduced for testing
|
|
||||||
const attachments: any[] = [];
|
|
||||||
let htmlContent = '<html><body><h1>Performance Test</h1>';
|
|
||||||
|
|
||||||
for (let i = 0; i < imageCount; i++) {
|
|
||||||
const cid = `image${i}`;
|
|
||||||
htmlContent += `<img src="cid:${cid}" alt="Image ${i}" width="50" height="50">`;
|
|
||||||
|
|
||||||
attachments.push({
|
|
||||||
filename: `image${i}.png`,
|
|
||||||
content: Buffer.from(`fake-image-data-${i}`),
|
|
||||||
contentType: 'image/png',
|
|
||||||
cid: cid
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
htmlContent += '</body></html>';
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: `Email with ${imageCount} inline images`,
|
|
||||||
html: htmlContent,
|
|
||||||
attachments: attachments
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
console.log(`Performance test with ${imageCount} inline images sent successfully`);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-07: Alternative content for non-HTML clients', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Email with rich HTML and good plain text alternative
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Newsletter - March 2024',
|
|
||||||
html: `
|
|
||||||
<html>
|
|
||||||
<body style="font-family: Arial, sans-serif;">
|
|
||||||
<div style="background: #f0f0f0; padding: 20px;">
|
|
||||||
<img src="cid:header" alt="Company Newsletter" style="width: 100%; max-width: 600px;">
|
|
||||||
</div>
|
|
||||||
<div style="padding: 20px;">
|
|
||||||
<h1 style="color: #333;">March Newsletter</h1>
|
|
||||||
<h2 style="color: #666;">Featured Articles</h2>
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://example.com/article1">10 Tips for Spring Cleaning</a></li>
|
|
||||||
<li><a href="https://example.com/article2">New Product Launch</a></li>
|
|
||||||
<li><a href="https://example.com/article3">Customer Success Story</a></li>
|
|
||||||
</ul>
|
|
||||||
<div style="background: #e0e0e0; padding: 15px; margin: 20px 0;">
|
|
||||||
<h3>Special Offer!</h3>
|
|
||||||
<p>Get 20% off with code: <strong>SPRING20</strong></p>
|
|
||||||
<img src="cid:offer" alt="Special Offer" style="width: 100%; max-width: 400px;">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="background: #333; color: #fff; padding: 20px; text-align: center;">
|
|
||||||
<p>© 2024 Example Corp | <a href="https://example.com/unsubscribe" style="color: #fff;">Unsubscribe</a></p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`,
|
|
||||||
text: `COMPANY NEWSLETTER
|
|
||||||
March 2024
|
|
||||||
|
|
||||||
FEATURED ARTICLES
|
|
||||||
* 10 Tips for Spring Cleaning
|
|
||||||
https://example.com/article1
|
|
||||||
* New Product Launch
|
|
||||||
https://example.com/article2
|
|
||||||
* Customer Success Story
|
|
||||||
https://example.com/article3
|
|
||||||
|
|
||||||
SPECIAL OFFER!
|
|
||||||
Get 20% off with code: SPRING20
|
|
||||||
|
|
||||||
---
|
|
||||||
© 2024 Example Corp
|
|
||||||
Unsubscribe: https://example.com/unsubscribe`,
|
|
||||||
attachments: [
|
|
||||||
{
|
|
||||||
filename: 'header.jpg',
|
|
||||||
content: Buffer.from('header-image'),
|
|
||||||
contentType: 'image/jpeg',
|
|
||||||
cid: 'header'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
filename: 'offer.jpg',
|
|
||||||
content: Buffer.from('offer-image'),
|
|
||||||
contentType: 'image/jpeg',
|
|
||||||
cid: 'offer'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
console.log('Newsletter with alternative content sent successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup test SMTP server', async () => {
|
|
||||||
if (testServer) {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,293 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup test SMTP server', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2568,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
expect(testServer).toBeTruthy();
|
|
||||||
expect(testServer.port).toEqual(2568);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-08: Basic custom headers', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create email with custom headers
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Custom Headers Test',
|
|
||||||
text: 'Testing custom headers',
|
|
||||||
headers: {
|
|
||||||
'X-Custom-Header': 'Custom Value',
|
|
||||||
'X-Campaign-ID': 'CAMP-2024-03',
|
|
||||||
'X-Priority': 'High',
|
|
||||||
'X-Mailer': 'Custom SMTP Client v1.0'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
console.log('Basic custom headers test sent successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-08: Standard headers override protection', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Try to override standard headers via custom headers
|
|
||||||
const email = new Email({
|
|
||||||
from: 'real-sender@example.com',
|
|
||||||
to: 'real-recipient@example.com',
|
|
||||||
subject: 'Real Subject',
|
|
||||||
text: 'Testing header override protection',
|
|
||||||
headers: {
|
|
||||||
'From': 'fake-sender@example.com', // Should not override
|
|
||||||
'To': 'fake-recipient@example.com', // Should not override
|
|
||||||
'Subject': 'Fake Subject', // Should not override
|
|
||||||
'Date': 'Mon, 1 Jan 2000 00:00:00 +0000', // Might be allowed
|
|
||||||
'Message-ID': '<fake@example.com>', // Might be allowed
|
|
||||||
'X-Original-From': 'tracking@example.com' // Custom header, should work
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
console.log('Header override protection test sent successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-08: Tracking and analytics headers', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Common tracking headers
|
|
||||||
const email = new Email({
|
|
||||||
from: 'marketing@example.com',
|
|
||||||
to: 'customer@example.com',
|
|
||||||
subject: 'Special Offer Inside!',
|
|
||||||
text: 'Check out our special offers',
|
|
||||||
headers: {
|
|
||||||
'X-Campaign-ID': 'SPRING-2024-SALE',
|
|
||||||
'X-Customer-ID': 'CUST-12345',
|
|
||||||
'X-Segment': 'high-value-customers',
|
|
||||||
'X-AB-Test': 'variant-b',
|
|
||||||
'X-Send-Time': new Date().toISOString(),
|
|
||||||
'X-Template-Version': '2.1.0',
|
|
||||||
'List-Unsubscribe': '<https://example.com/unsubscribe?id=12345>',
|
|
||||||
'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click',
|
|
||||||
'Precedence': 'bulk'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
console.log('Tracking and analytics headers test sent successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-08: MIME extension headers', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// MIME-related custom headers
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'MIME Extensions Test',
|
|
||||||
html: '<p>HTML content</p>',
|
|
||||||
text: 'Plain text content',
|
|
||||||
headers: {
|
|
||||||
'MIME-Version': '1.0', // Usually auto-added
|
|
||||||
'X-Accept-Language': 'en-US, en;q=0.9, fr;q=0.8',
|
|
||||||
'X-Auto-Response-Suppress': 'DR, RN, NRN, OOF',
|
|
||||||
'Importance': 'high',
|
|
||||||
'X-Priority': '1',
|
|
||||||
'X-MSMail-Priority': 'High',
|
|
||||||
'Sensitivity': 'Company-Confidential'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
console.log('MIME extension headers test sent successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-08: Email threading headers', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Simulate email thread
|
|
||||||
const messageId = `<${Date.now()}.${Math.random()}@example.com>`;
|
|
||||||
const inReplyTo = '<original-message@example.com>';
|
|
||||||
const references = '<thread-start@example.com> <second-message@example.com>';
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Re: Email Threading Test',
|
|
||||||
text: 'This is a reply in the thread',
|
|
||||||
headers: {
|
|
||||||
'Message-ID': messageId,
|
|
||||||
'In-Reply-To': inReplyTo,
|
|
||||||
'References': references,
|
|
||||||
'Thread-Topic': 'Email Threading Test',
|
|
||||||
'Thread-Index': Buffer.from('thread-data').toString('base64')
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
console.log('Email threading headers test sent successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-08: Security and authentication headers', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Security-related headers
|
|
||||||
const email = new Email({
|
|
||||||
from: 'secure@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Security Headers Test',
|
|
||||||
text: 'Testing security headers',
|
|
||||||
headers: {
|
|
||||||
'X-Originating-IP': '[192.168.1.100]',
|
|
||||||
'X-Auth-Result': 'PASS',
|
|
||||||
'X-Spam-Score': '0.1',
|
|
||||||
'X-Spam-Status': 'No, score=0.1',
|
|
||||||
'X-Virus-Scanned': 'ClamAV using ClamSMTP',
|
|
||||||
'Authentication-Results': 'example.com; spf=pass smtp.mailfrom=sender@example.com',
|
|
||||||
'ARC-Seal': 'i=1; cv=none; d=example.com; s=arc-20240315; t=1710500000;',
|
|
||||||
'ARC-Message-Signature': 'i=1; a=rsa-sha256; c=relaxed/relaxed;',
|
|
||||||
'ARC-Authentication-Results': 'i=1; example.com; spf=pass'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
console.log('Security and authentication headers test sent successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-08: Header folding for long values', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create headers with long values that need folding
|
|
||||||
const longValue = 'This is a very long header value that exceeds the recommended 78 character limit per line and should be folded according to RFC 5322 specifications for proper email transmission';
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Header Folding Test with a very long subject line that should be properly folded',
|
|
||||||
text: 'Testing header folding',
|
|
||||||
headers: {
|
|
||||||
'X-Long-Header': longValue,
|
|
||||||
'X-Multiple-Values': 'value1@example.com, value2@example.com, value3@example.com, value4@example.com, value5@example.com, value6@example.com',
|
|
||||||
'References': '<msg1@example.com> <msg2@example.com> <msg3@example.com> <msg4@example.com> <msg5@example.com> <msg6@example.com> <msg7@example.com>'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
console.log('Header folding test sent successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-08: Custom headers with special characters', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Headers with special characters
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Special Characters in Headers',
|
|
||||||
text: 'Testing special characters',
|
|
||||||
headers: {
|
|
||||||
'X-Special-Chars': 'Value with special: !@#$%^&*()',
|
|
||||||
'X-Quoted-String': '"This is a quoted string"',
|
|
||||||
'X-Unicode': 'Unicode: café, naïve, 你好',
|
|
||||||
'X-Control-Chars': 'No\ttabs\nor\rnewlines', // Should be sanitized
|
|
||||||
'X-Empty': '',
|
|
||||||
'X-Spaces': ' trimmed ',
|
|
||||||
'X-Semicolon': 'part1; part2; part3'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
console.log('Special characters test sent successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-08: Duplicate header handling', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Some headers can appear multiple times
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Duplicate Headers Test',
|
|
||||||
text: 'Testing duplicate headers',
|
|
||||||
headers: {
|
|
||||||
'Received': 'from server1.example.com',
|
|
||||||
'X-Received': 'from server2.example.com', // Workaround for multiple
|
|
||||||
'Comments': 'First comment',
|
|
||||||
'X-Comments': 'Second comment', // Workaround for multiple
|
|
||||||
'X-Tag': 'tag1, tag2, tag3' // String instead of array
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
console.log('Duplicate header handling test sent successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup test SMTP server', async () => {
|
|
||||||
if (testServer) {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,314 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup test SMTP server', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2569,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
expect(testServer).toBeTruthy();
|
|
||||||
expect(testServer.port).toEqual(2569);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-09: Basic priority headers', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test different priority levels
|
|
||||||
const priorityLevels = [
|
|
||||||
{ priority: 'high', headers: { 'X-Priority': '1', 'Importance': 'high' } },
|
|
||||||
{ priority: 'normal', headers: { 'X-Priority': '3', 'Importance': 'normal' } },
|
|
||||||
{ priority: 'low', headers: { 'X-Priority': '5', 'Importance': 'low' } }
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const level of priorityLevels) {
|
|
||||||
console.log(`Testing ${level.priority} priority email...`);
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: `${level.priority.toUpperCase()} Priority Test`,
|
|
||||||
text: `This is a ${level.priority} priority message`,
|
|
||||||
priority: level.priority as 'high' | 'normal' | 'low'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Basic priority headers test completed successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-09: Multiple priority header formats', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test various priority header combinations
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Multiple Priority Headers Test',
|
|
||||||
text: 'Testing various priority header formats',
|
|
||||||
headers: {
|
|
||||||
'X-Priority': '1 (Highest)',
|
|
||||||
'X-MSMail-Priority': 'High',
|
|
||||||
'Importance': 'high',
|
|
||||||
'Priority': 'urgent',
|
|
||||||
'X-Message-Flag': 'Follow up'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
console.log('Multiple priority header formats test sent successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-09: Client-specific priority mappings', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send test email with comprehensive priority headers
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Cross-client Priority Test',
|
|
||||||
text: 'This should appear as high priority in all clients',
|
|
||||||
priority: 'high'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
console.log('Client-specific priority mappings test sent successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-09: Sensitivity and confidentiality headers', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test sensitivity levels
|
|
||||||
const sensitivityLevels = [
|
|
||||||
{ level: 'Personal', description: 'Personal information' },
|
|
||||||
{ level: 'Private', description: 'Private communication' },
|
|
||||||
{ level: 'Company-Confidential', description: 'Internal use only' },
|
|
||||||
{ level: 'Normal', description: 'No special handling' }
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const sensitivity of sensitivityLevels) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: `${sensitivity.level} Message`,
|
|
||||||
text: sensitivity.description,
|
|
||||||
headers: {
|
|
||||||
'Sensitivity': sensitivity.level,
|
|
||||||
'X-Sensitivity': sensitivity.level
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Sensitivity and confidentiality headers test completed successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-09: Auto-response suppression headers', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Headers to suppress auto-responses (vacation messages, etc.)
|
|
||||||
const email = new Email({
|
|
||||||
from: 'noreply@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Automated Notification',
|
|
||||||
text: 'This is an automated message. Please do not reply.',
|
|
||||||
headers: {
|
|
||||||
'X-Auto-Response-Suppress': 'All', // Microsoft
|
|
||||||
'Auto-Submitted': 'auto-generated', // RFC 3834
|
|
||||||
'Precedence': 'bulk', // Traditional
|
|
||||||
'X-Autoreply': 'no',
|
|
||||||
'X-Autorespond': 'no',
|
|
||||||
'List-Id': '<notifications.example.com>', // Mailing list header
|
|
||||||
'List-Unsubscribe': '<mailto:unsubscribe@example.com>'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
console.log('Auto-response suppression headers test sent successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-09: Expiration and retention headers', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set expiration date for the email
|
|
||||||
const expirationDate = new Date();
|
|
||||||
expirationDate.setDate(expirationDate.getDate() + 7); // Expires in 7 days
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Time-sensitive Information',
|
|
||||||
text: 'This information expires in 7 days',
|
|
||||||
headers: {
|
|
||||||
'Expiry-Date': expirationDate.toUTCString(),
|
|
||||||
'X-Message-TTL': '604800', // 7 days in seconds
|
|
||||||
'X-Auto-Delete-After': expirationDate.toISOString(),
|
|
||||||
'X-Retention-Date': expirationDate.toISOString()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
console.log('Expiration and retention headers test sent successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-09: Message flags and categories', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test various message flags and categories
|
|
||||||
const flaggedEmails = [
|
|
||||||
{
|
|
||||||
flag: 'Follow up',
|
|
||||||
category: 'Action Required',
|
|
||||||
color: 'red'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
flag: 'For Your Information',
|
|
||||||
category: 'Informational',
|
|
||||||
color: 'blue'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
flag: 'Review',
|
|
||||||
category: 'Pending Review',
|
|
||||||
color: 'yellow'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const flaggedEmail of flaggedEmails) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: `${flaggedEmail.flag}: Important Document`,
|
|
||||||
text: `This email is flagged as: ${flaggedEmail.flag}`,
|
|
||||||
headers: {
|
|
||||||
'X-Message-Flag': flaggedEmail.flag,
|
|
||||||
'X-Category': flaggedEmail.category,
|
|
||||||
'X-Color-Label': flaggedEmail.color,
|
|
||||||
'Keywords': flaggedEmail.flag.replace(' ', '-')
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Message flags and categories test completed successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-09: Priority with delivery timing', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test deferred delivery with priority
|
|
||||||
const futureDate = new Date();
|
|
||||||
futureDate.setHours(futureDate.getHours() + 2); // Deliver in 2 hours
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Scheduled High Priority Message',
|
|
||||||
text: 'This high priority message should be delivered at a specific time',
|
|
||||||
priority: 'high',
|
|
||||||
headers: {
|
|
||||||
'Deferred-Delivery': futureDate.toUTCString(),
|
|
||||||
'X-Delay-Until': futureDate.toISOString(),
|
|
||||||
'X-Priority': '1',
|
|
||||||
'Importance': 'High'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
console.log('Priority with delivery timing test sent successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-09: Priority impact on routing', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test batch of emails with different priorities
|
|
||||||
const emails = [
|
|
||||||
{ priority: 'high', subject: 'URGENT: Server Down' },
|
|
||||||
{ priority: 'high', subject: 'Critical Security Update' },
|
|
||||||
{ priority: 'normal', subject: 'Weekly Report' },
|
|
||||||
{ priority: 'low', subject: 'Newsletter' },
|
|
||||||
{ priority: 'low', subject: 'Promotional Offer' }
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const emailData of emails) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: emailData.subject,
|
|
||||||
text: `Priority: ${emailData.priority}`,
|
|
||||||
priority: emailData.priority as 'high' | 'normal' | 'low'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Priority impact on routing test completed successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup test SMTP server', async () => {
|
|
||||||
if (testServer) {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,411 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup test SMTP server', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2570,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
expect(testServer).toBeTruthy();
|
|
||||||
expect(testServer.port).toEqual(2570);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-10: Read receipt headers', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create email requesting read receipt
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Important: Please confirm receipt',
|
|
||||||
text: 'Please confirm you have read this message',
|
|
||||||
headers: {
|
|
||||||
'Disposition-Notification-To': 'sender@example.com',
|
|
||||||
'Return-Receipt-To': 'sender@example.com',
|
|
||||||
'X-Confirm-Reading-To': 'sender@example.com',
|
|
||||||
'X-MS-Receipt-Request': 'sender@example.com'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
console.log('Read receipt headers test sent successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-10: DSN (Delivery Status Notification) requests', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create email with DSN options
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'DSN Test Email',
|
|
||||||
text: 'Testing delivery status notifications',
|
|
||||||
headers: {
|
|
||||||
'X-DSN-Options': 'notify=SUCCESS,FAILURE,DELAY;return=HEADERS',
|
|
||||||
'X-Envelope-ID': `msg-${Date.now()}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
console.log('DSN requests test sent successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-10: DSN notify options', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test different DSN notify combinations
|
|
||||||
const notifyOptions = [
|
|
||||||
{ notify: ['SUCCESS'], description: 'Notify on successful delivery only' },
|
|
||||||
{ notify: ['FAILURE'], description: 'Notify on failure only' },
|
|
||||||
{ notify: ['DELAY'], description: 'Notify on delays only' },
|
|
||||||
{ notify: ['SUCCESS', 'FAILURE'], description: 'Notify on success and failure' },
|
|
||||||
{ notify: ['NEVER'], description: 'Never send notifications' }
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const option of notifyOptions) {
|
|
||||||
console.log(`Testing DSN: ${option.description}`);
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: `DSN Test: ${option.description}`,
|
|
||||||
text: 'Testing DSN notify options',
|
|
||||||
headers: {
|
|
||||||
'X-DSN-Notify': option.notify.join(','),
|
|
||||||
'X-DSN-Return': 'HEADERS'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('DSN notify options test completed successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-10: DSN return types', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test different return types
|
|
||||||
const returnTypes = [
|
|
||||||
{ type: 'FULL', description: 'Return full message on failure' },
|
|
||||||
{ type: 'HEADERS', description: 'Return headers only' }
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const returnType of returnTypes) {
|
|
||||||
console.log(`Testing DSN return type: ${returnType.description}`);
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: `DSN Return Type: ${returnType.type}`,
|
|
||||||
text: 'Testing DSN return types',
|
|
||||||
headers: {
|
|
||||||
'X-DSN-Notify': 'FAILURE',
|
|
||||||
'X-DSN-Return': returnType.type
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('DSN return types test completed successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-10: MDN (Message Disposition Notification)', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create MDN request email
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Please confirm reading',
|
|
||||||
text: 'This message requests a read receipt',
|
|
||||||
headers: {
|
|
||||||
'Disposition-Notification-To': 'sender@example.com',
|
|
||||||
'Disposition-Notification-Options': 'signed-receipt-protocol=optional,pkcs7-signature',
|
|
||||||
'Original-Message-ID': `<${Date.now()}@example.com>`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
|
|
||||||
// Simulate MDN response
|
|
||||||
const mdnResponse = new Email({
|
|
||||||
from: 'recipient@example.com',
|
|
||||||
to: 'sender@example.com',
|
|
||||||
subject: 'Read: Please confirm reading',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/report; report-type=disposition-notification',
|
|
||||||
'In-Reply-To': `<${Date.now()}@example.com>`,
|
|
||||||
'References': `<${Date.now()}@example.com>`,
|
|
||||||
'Auto-Submitted': 'auto-replied'
|
|
||||||
},
|
|
||||||
text: 'The message was displayed to the recipient',
|
|
||||||
attachments: [{
|
|
||||||
filename: 'disposition-notification.txt',
|
|
||||||
content: Buffer.from(`Reporting-UA: mail.example.com; MailClient/1.0
|
|
||||||
Original-Recipient: rfc822;recipient@example.com
|
|
||||||
Final-Recipient: rfc822;recipient@example.com
|
|
||||||
Original-Message-ID: <${Date.now()}@example.com>
|
|
||||||
Disposition: automatic-action/MDN-sent-automatically; displayed`),
|
|
||||||
contentType: 'message/disposition-notification'
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
const mdnResult = await smtpClient.sendMail(mdnResponse);
|
|
||||||
expect(mdnResult.success).toBeTruthy();
|
|
||||||
console.log('MDN test completed successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-10: Multiple recipients with different DSN', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Email with multiple recipients
|
|
||||||
const emails = [
|
|
||||||
{
|
|
||||||
to: 'important@example.com',
|
|
||||||
dsn: 'SUCCESS,FAILURE,DELAY'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
to: 'normal@example.com',
|
|
||||||
dsn: 'FAILURE'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
to: 'optional@example.com',
|
|
||||||
dsn: 'NEVER'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const emailData of emails) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: emailData.to,
|
|
||||||
subject: 'Multi-recipient DSN Test',
|
|
||||||
text: 'Testing per-recipient DSN options',
|
|
||||||
headers: {
|
|
||||||
'X-DSN-Notify': emailData.dsn,
|
|
||||||
'X-DSN-Return': 'HEADERS'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Multiple recipients DSN test completed successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-10: DSN with ORCPT', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test ORCPT (Original Recipient) parameter
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'forwarded@example.com',
|
|
||||||
subject: 'DSN with ORCPT Test',
|
|
||||||
text: 'Testing original recipient tracking',
|
|
||||||
headers: {
|
|
||||||
'X-DSN-Notify': 'SUCCESS,FAILURE',
|
|
||||||
'X-DSN-Return': 'HEADERS',
|
|
||||||
'X-Original-Recipient': 'rfc822;original@example.com'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
console.log('DSN with ORCPT test sent successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-10: Receipt request formats', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test various receipt request formats
|
|
||||||
const receiptFormats = [
|
|
||||||
{
|
|
||||||
name: 'Simple email',
|
|
||||||
value: 'receipts@example.com'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'With display name',
|
|
||||||
value: '"Receipt Handler" <receipts@example.com>'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Multiple addresses',
|
|
||||||
value: 'receipts@example.com, backup@example.com'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'With comment',
|
|
||||||
value: 'receipts@example.com (Automated System)'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const format of receiptFormats) {
|
|
||||||
console.log(`Testing receipt format: ${format.name}`);
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: `Receipt Format: ${format.name}`,
|
|
||||||
text: 'Testing receipt address formats',
|
|
||||||
headers: {
|
|
||||||
'Disposition-Notification-To': format.value
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Receipt request formats test completed successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-10: Non-delivery reports', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Simulate bounce/NDR structure
|
|
||||||
const ndrEmail = new Email({
|
|
||||||
from: 'MAILER-DAEMON@example.com',
|
|
||||||
to: 'original-sender@example.com',
|
|
||||||
subject: 'Undelivered Mail Returned to Sender',
|
|
||||||
headers: {
|
|
||||||
'Auto-Submitted': 'auto-replied',
|
|
||||||
'Content-Type': 'multipart/report; report-type=delivery-status',
|
|
||||||
'X-Failed-Recipients': 'nonexistent@example.com'
|
|
||||||
},
|
|
||||||
text: 'This is the mail delivery agent at example.com.\n\n' +
|
|
||||||
'I was unable to deliver your message to the following addresses:\n\n' +
|
|
||||||
'<nonexistent@example.com>: User unknown',
|
|
||||||
attachments: [
|
|
||||||
{
|
|
||||||
filename: 'delivery-status.txt',
|
|
||||||
content: Buffer.from(`Reporting-MTA: dns; mail.example.com
|
|
||||||
X-Queue-ID: 123456789
|
|
||||||
Arrival-Date: ${new Date().toUTCString()}
|
|
||||||
|
|
||||||
Final-Recipient: rfc822;nonexistent@example.com
|
|
||||||
Original-Recipient: rfc822;nonexistent@example.com
|
|
||||||
Action: failed
|
|
||||||
Status: 5.1.1
|
|
||||||
Diagnostic-Code: smtp; 550 5.1.1 User unknown`),
|
|
||||||
contentType: 'message/delivery-status'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
filename: 'original-message.eml',
|
|
||||||
content: Buffer.from('From: original-sender@example.com\r\n' +
|
|
||||||
'To: nonexistent@example.com\r\n' +
|
|
||||||
'Subject: Original Subject\r\n\r\n' +
|
|
||||||
'Original message content'),
|
|
||||||
contentType: 'message/rfc822'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(ndrEmail);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
console.log('Non-delivery report test sent successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CEP-10: Delivery delay notifications', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Simulate delayed delivery notification
|
|
||||||
const delayNotification = new Email({
|
|
||||||
from: 'postmaster@example.com',
|
|
||||||
to: 'sender@example.com',
|
|
||||||
subject: 'Delivery Status: Delayed',
|
|
||||||
headers: {
|
|
||||||
'Auto-Submitted': 'auto-replied',
|
|
||||||
'Content-Type': 'multipart/report; report-type=delivery-status',
|
|
||||||
'X-Delay-Reason': 'Remote server temporarily unavailable'
|
|
||||||
},
|
|
||||||
text: 'This is an automatically generated Delivery Delay Notification.\n\n' +
|
|
||||||
'Your message has not been delivered to the following recipients yet:\n\n' +
|
|
||||||
' recipient@remote-server.com\n\n' +
|
|
||||||
'The server will continue trying to deliver your message for 48 hours.',
|
|
||||||
attachments: [{
|
|
||||||
filename: 'delay-status.txt',
|
|
||||||
content: Buffer.from(`Reporting-MTA: dns; mail.example.com
|
|
||||||
Arrival-Date: ${new Date(Date.now() - 3600000).toUTCString()}
|
|
||||||
Last-Attempt-Date: ${new Date().toUTCString()}
|
|
||||||
|
|
||||||
Final-Recipient: rfc822;recipient@remote-server.com
|
|
||||||
Action: delayed
|
|
||||||
Status: 4.4.1
|
|
||||||
Will-Retry-Until: ${new Date(Date.now() + 172800000).toUTCString()}
|
|
||||||
Diagnostic-Code: smtp; 421 4.4.1 Remote server temporarily unavailable`),
|
|
||||||
contentType: 'message/delivery-status'
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(delayNotification);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
console.log('Delivery delay notification test sent successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup test SMTP server', async () => {
|
|
||||||
if (testServer) {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
let smtpClient: SmtpClient;
|
|
||||||
|
|
||||||
tap.test('setup - start SMTP server for error handling tests', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2550,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false,
|
|
||||||
maxRecipients: 5 // Low limit to trigger errors
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(testServer.port).toEqual(2550);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-01: 4xx Errors - should handle invalid recipient (450)', async () => {
|
|
||||||
smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create email with syntactically valid but nonexistent recipient
|
|
||||||
const email = new Email({
|
|
||||||
from: 'test@example.com',
|
|
||||||
to: 'nonexistent-user@nonexistent-domain-12345.invalid',
|
|
||||||
subject: 'Testing 4xx Error',
|
|
||||||
text: 'This should trigger a 4xx error'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
// Test server may accept or reject - both are valid test outcomes
|
|
||||||
if (!result.success) {
|
|
||||||
console.log('✅ Invalid recipient handled:', result.error?.message);
|
|
||||||
} else {
|
|
||||||
console.log('ℹ️ Test server accepted recipient (common in test environments)');
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(result).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-01: 4xx Errors - should handle mailbox unavailable (450)', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'test@example.com',
|
|
||||||
to: 'mailbox-full@example.com', // Valid format but might be unavailable
|
|
||||||
subject: 'Mailbox Unavailable Test',
|
|
||||||
text: 'Testing mailbox unavailable error'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
// Depending on server configuration, this might be accepted or rejected
|
|
||||||
if (!result.success) {
|
|
||||||
console.log('✅ Mailbox unavailable handled:', result.error?.message);
|
|
||||||
} else {
|
|
||||||
// Some test servers accept all recipients
|
|
||||||
console.log('ℹ️ Test server accepted recipient (common in test environments)');
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(result).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-01: 4xx Errors - should handle quota exceeded (452)', async () => {
|
|
||||||
// Send multiple emails to trigger quota/limit errors
|
|
||||||
const emails = [];
|
|
||||||
for (let i = 0; i < 10; i++) {
|
|
||||||
emails.push(new Email({
|
|
||||||
from: 'test@example.com',
|
|
||||||
to: `recipient${i}@example.com`,
|
|
||||||
subject: `Quota Test ${i}`,
|
|
||||||
text: 'Testing quota limits'
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
let quotaErrorCount = 0;
|
|
||||||
const results = await Promise.allSettled(
|
|
||||||
emails.map(email => smtpClient.sendMail(email))
|
|
||||||
);
|
|
||||||
|
|
||||||
results.forEach((result, index) => {
|
|
||||||
if (result.status === 'rejected') {
|
|
||||||
quotaErrorCount++;
|
|
||||||
console.log(`Email ${index} rejected:`, result.reason);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`✅ Handled ${quotaErrorCount} quota-related errors`);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-01: 4xx Errors - should handle too many recipients (452)', async () => {
|
|
||||||
// Create email with many recipients to exceed limit
|
|
||||||
const recipients = [];
|
|
||||||
for (let i = 0; i < 10; i++) {
|
|
||||||
recipients.push(`recipient${i}@example.com`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'test@example.com',
|
|
||||||
to: recipients, // Many recipients
|
|
||||||
subject: 'Too Many Recipients Test',
|
|
||||||
text: 'Testing recipient limit'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
// Check if some recipients were rejected due to limits
|
|
||||||
if (result.rejectedRecipients.length > 0) {
|
|
||||||
console.log(`✅ Rejected ${result.rejectedRecipients.length} recipients due to limits`);
|
|
||||||
expect(result.rejectedRecipients).toBeArray();
|
|
||||||
} else {
|
|
||||||
// Server might accept all
|
|
||||||
expect(result.acceptedRecipients.length).toEqual(recipients.length);
|
|
||||||
console.log('ℹ️ Server accepted all recipients');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-01: 4xx Errors - should handle authentication required (450)', async () => {
|
|
||||||
// Create new server requiring auth
|
|
||||||
const authServer = await startTestServer({
|
|
||||||
port: 2551,
|
|
||||||
authRequired: true // This will reject unauthenticated commands
|
|
||||||
});
|
|
||||||
|
|
||||||
const unauthClient = await createSmtpClient({
|
|
||||||
host: authServer.hostname,
|
|
||||||
port: authServer.port,
|
|
||||||
secure: false,
|
|
||||||
// No auth credentials provided
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'test@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Auth Required Test',
|
|
||||||
text: 'Should fail without auth'
|
|
||||||
});
|
|
||||||
|
|
||||||
let authError = false;
|
|
||||||
try {
|
|
||||||
const result = await unauthClient.sendMail(email);
|
|
||||||
if (!result.success) {
|
|
||||||
authError = true;
|
|
||||||
console.log('✅ Authentication required error handled:', result.error?.message);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
authError = true;
|
|
||||||
console.log('✅ Authentication required error caught:', error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(authError).toBeTrue();
|
|
||||||
|
|
||||||
await stopTestServer(authServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-01: 4xx Errors - should parse enhanced status codes', async () => {
|
|
||||||
// 4xx errors often include enhanced status codes (e.g., 4.7.1)
|
|
||||||
const email = new Email({
|
|
||||||
from: 'test@blocked-domain.com', // Might trigger policy rejection
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Enhanced Status Code Test',
|
|
||||||
text: 'Testing enhanced status codes'
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
if (!result.success && result.error) {
|
|
||||||
console.log('✅ Error details:', {
|
|
||||||
message: result.error.message,
|
|
||||||
response: result.response
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
// Check if error includes status information
|
|
||||||
expect(error.message).toBeTypeofString();
|
|
||||||
console.log('✅ Error with potential enhanced status:', error.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-01: 4xx Errors - should not retry permanent 4xx errors', async () => {
|
|
||||||
// Track retry attempts
|
|
||||||
let attemptCount = 0;
|
|
||||||
|
|
||||||
const trackingClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'blocked-sender@blacklisted-domain.invalid', // Might trigger policy rejection
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Permanent Error Test',
|
|
||||||
text: 'Should not retry'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await trackingClient.sendMail(email);
|
|
||||||
|
|
||||||
// Test completed - whether success or failure, no retries should occur
|
|
||||||
if (!result.success) {
|
|
||||||
console.log('✅ Permanent error handled without retry:', result.error?.message);
|
|
||||||
} else {
|
|
||||||
console.log('ℹ️ Email accepted (no policy rejection in test server)');
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(result).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - close SMTP client', async () => {
|
|
||||||
if (smtpClient) {
|
|
||||||
try {
|
|
||||||
await smtpClient.close();
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Client already closed or error during close');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - stop SMTP server', async () => {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,309 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
let smtpClient: SmtpClient;
|
|
||||||
|
|
||||||
tap.test('setup - start SMTP server for 5xx error tests', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2552,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false,
|
|
||||||
maxRecipients: 3 // Low limit to help trigger errors
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(testServer.port).toEqual(2552);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-02: 5xx Errors - should handle command not recognized (500)', async () => {
|
|
||||||
smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// The client should handle standard commands properly
|
|
||||||
// This tests that the client doesn't send invalid commands
|
|
||||||
const result = await smtpClient.verify();
|
|
||||||
expect(result).toBeTruthy();
|
|
||||||
|
|
||||||
console.log('✅ Client sends only valid SMTP commands');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-02: 5xx Errors - should handle syntax error (501)', async () => {
|
|
||||||
// Test with malformed email that might cause syntax error
|
|
||||||
let syntaxError = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// The Email class should catch this before sending
|
|
||||||
const email = new Email({
|
|
||||||
from: '<invalid>from>@example.com', // Malformed
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Syntax Error Test',
|
|
||||||
text: 'This should fail'
|
|
||||||
});
|
|
||||||
|
|
||||||
await smtpClient.sendMail(email);
|
|
||||||
} catch (error: any) {
|
|
||||||
syntaxError = true;
|
|
||||||
expect(error).toBeInstanceOf(Error);
|
|
||||||
console.log('✅ Syntax error caught:', error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(syntaxError).toBeTrue();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-02: 5xx Errors - should handle command not implemented (502)', async () => {
|
|
||||||
// Most servers implement all required commands
|
|
||||||
// This test verifies client doesn't use optional/deprecated commands
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Standard Commands Test',
|
|
||||||
text: 'Using only standard SMTP commands'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
|
|
||||||
console.log('✅ Client uses only widely-implemented commands');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-02: 5xx Errors - should handle bad sequence (503)', async () => {
|
|
||||||
// The client should maintain proper command sequence
|
|
||||||
// This tests internal state management
|
|
||||||
|
|
||||||
// Send multiple emails to ensure sequence is maintained
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: `Sequence Test ${i}`,
|
|
||||||
text: 'Testing command sequence'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✅ Client maintains proper command sequence');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-02: 5xx Errors - should handle authentication failed (535)', async () => {
|
|
||||||
// Create server requiring authentication
|
|
||||||
const authServer = await startTestServer({
|
|
||||||
port: 2553,
|
|
||||||
authRequired: true
|
|
||||||
});
|
|
||||||
|
|
||||||
let authFailed = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const badAuthClient = await createSmtpClient({
|
|
||||||
host: authServer.hostname,
|
|
||||||
port: authServer.port,
|
|
||||||
secure: false,
|
|
||||||
auth: {
|
|
||||||
user: 'wronguser',
|
|
||||||
pass: 'wrongpass'
|
|
||||||
},
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await badAuthClient.verify();
|
|
||||||
if (!result.success) {
|
|
||||||
authFailed = true;
|
|
||||||
console.log('✅ Authentication failure (535) handled:', result.error?.message);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
authFailed = true;
|
|
||||||
console.log('✅ Authentication failure (535) handled:', error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(authFailed).toBeTrue();
|
|
||||||
|
|
||||||
await stopTestServer(authServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-02: 5xx Errors - should handle transaction failed (554)', async () => {
|
|
||||||
// Try to send email that might be rejected
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'postmaster@[127.0.0.1]', // IP literal might be rejected
|
|
||||||
subject: 'Transaction Test',
|
|
||||||
text: 'Testing transaction failure'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
// Depending on server configuration
|
|
||||||
if (!result.success) {
|
|
||||||
console.log('✅ Transaction failure handled gracefully');
|
|
||||||
expect(result.error).toBeInstanceOf(Error);
|
|
||||||
} else {
|
|
||||||
console.log('ℹ️ Test server accepted IP literal recipient');
|
|
||||||
expect(result.acceptedRecipients.length).toBeGreaterThan(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-02: 5xx Errors - should not retry permanent 5xx errors', async () => {
|
|
||||||
// Create a client for testing
|
|
||||||
const trackingClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Try to send with potentially problematic data
|
|
||||||
const email = new Email({
|
|
||||||
from: 'blocked-user@blacklisted-domain.invalid',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Permanent Error Test',
|
|
||||||
text: 'Should not retry'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await trackingClient.sendMail(email);
|
|
||||||
|
|
||||||
// Whether success or failure, permanent errors should not be retried
|
|
||||||
if (!result.success) {
|
|
||||||
console.log('✅ Permanent error not retried:', result.error?.message);
|
|
||||||
} else {
|
|
||||||
console.log('ℹ️ Email accepted (no permanent rejection in test server)');
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(result).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-02: 5xx Errors - should handle server unavailable (550)', async () => {
|
|
||||||
// Test with recipient that might be rejected
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'no-such-user@nonexistent-server.invalid',
|
|
||||||
subject: 'User Unknown Test',
|
|
||||||
text: 'Testing unknown user rejection'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
if (!result.success || result.rejectedRecipients.length > 0) {
|
|
||||||
console.log('✅ Unknown user (550) rejection handled');
|
|
||||||
} else {
|
|
||||||
// Test server might accept all
|
|
||||||
console.log('ℹ️ Test server accepted unknown user');
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(result).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-02: 5xx Errors - should close connection after fatal error', async () => {
|
|
||||||
// Test that client properly closes connection after fatal errors
|
|
||||||
const fatalClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Verify connection works
|
|
||||||
const verifyResult = await fatalClient.verify();
|
|
||||||
expect(verifyResult).toBeTruthy();
|
|
||||||
|
|
||||||
// Simulate a scenario that might cause fatal error
|
|
||||||
// For this test, we'll just verify the client can handle closure
|
|
||||||
try {
|
|
||||||
// The client should handle connection closure gracefully
|
|
||||||
console.log('✅ Connection properly closed after errors');
|
|
||||||
expect(true).toBeTrue(); // Test passed
|
|
||||||
} catch (error) {
|
|
||||||
console.log('✅ Fatal error handled properly');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-02: 5xx Errors - should provide detailed error information', async () => {
|
|
||||||
// Test error detail extraction
|
|
||||||
let errorDetails: any = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'a'.repeat(100) + '@example.com', // Very long local part
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Error Details Test',
|
|
||||||
text: 'Testing error details'
|
|
||||||
});
|
|
||||||
|
|
||||||
await smtpClient.sendMail(email);
|
|
||||||
} catch (error: any) {
|
|
||||||
errorDetails = error;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errorDetails) {
|
|
||||||
expect(errorDetails).toBeInstanceOf(Error);
|
|
||||||
expect(errorDetails.message).toBeTypeofString();
|
|
||||||
console.log('✅ Detailed error information provided:', errorDetails.message);
|
|
||||||
} else {
|
|
||||||
console.log('ℹ️ Long email address accepted by validator');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-02: 5xx Errors - should handle multiple 5xx errors gracefully', async () => {
|
|
||||||
// Send several emails that might trigger different 5xx errors
|
|
||||||
const testEmails = [
|
|
||||||
{
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@invalid-tld', // Invalid TLD
|
|
||||||
subject: 'Invalid TLD Test',
|
|
||||||
text: 'Test 1'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@.com', // Missing domain part
|
|
||||||
subject: 'Missing Domain Test',
|
|
||||||
text: 'Test 2'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Valid Email After Errors',
|
|
||||||
text: 'This should work'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
let successCount = 0;
|
|
||||||
let errorCount = 0;
|
|
||||||
|
|
||||||
for (const emailData of testEmails) {
|
|
||||||
try {
|
|
||||||
const email = new Email(emailData);
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
if (result.success) successCount++;
|
|
||||||
} catch (error) {
|
|
||||||
errorCount++;
|
|
||||||
console.log(` Error for ${emailData.to}: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`✅ Handled multiple errors: ${errorCount} errors, ${successCount} successes`);
|
|
||||||
expect(successCount).toBeGreaterThan(0); // At least the valid email should work
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - close SMTP client', async () => {
|
|
||||||
if (smtpClient) {
|
|
||||||
try {
|
|
||||||
await smtpClient.close();
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Client already closed or error during close');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - stop SMTP server', async () => {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,299 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
import * as net from 'net';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup - start SMTP server for network failure tests', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2554,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(testServer.port).toEqual(2554);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-03: Network Failures - should handle connection refused', async () => {
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
// Try to connect to a port that's not listening
|
|
||||||
const client = createSmtpClient({
|
|
||||||
host: 'localhost',
|
|
||||||
port: 9876, // Non-listening port
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 3000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await client.verify();
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
|
|
||||||
expect(result).toBeFalse();
|
|
||||||
console.log(`✅ Connection refused handled in ${duration}ms`);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-03: Network Failures - should handle DNS resolution failure', async () => {
|
|
||||||
const client = createSmtpClient({
|
|
||||||
host: 'non.existent.domain.that.should.not.resolve.example',
|
|
||||||
port: 25,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await client.verify();
|
|
||||||
|
|
||||||
expect(result).toBeFalse();
|
|
||||||
console.log('✅ DNS resolution failure handled');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-03: Network Failures - should handle connection drop during handshake', async () => {
|
|
||||||
// Create a server that drops connections immediately
|
|
||||||
const dropServer = net.createServer((socket) => {
|
|
||||||
// Drop connection after accepting
|
|
||||||
socket.destroy();
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
dropServer.listen(2555, () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const client = createSmtpClient({
|
|
||||||
host: 'localhost',
|
|
||||||
port: 2555,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 1000 // Faster timeout
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await client.verify();
|
|
||||||
|
|
||||||
expect(result).toBeFalse();
|
|
||||||
console.log('✅ Connection drop during handshake handled');
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
dropServer.close(() => resolve());
|
|
||||||
});
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-03: Network Failures - should handle connection drop during data transfer', async () => {
|
|
||||||
const client = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
socketTimeout: 10000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Establish connection first
|
|
||||||
await client.verify();
|
|
||||||
|
|
||||||
// For this test, we simulate network issues by attempting
|
|
||||||
// to send after server issues might occur
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Network Failure Test',
|
|
||||||
text: 'Testing network failure recovery'
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await client.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Email sent successfully (no network failure simulated)');
|
|
||||||
} catch (error) {
|
|
||||||
console.log('✅ Network failure handled during data transfer');
|
|
||||||
}
|
|
||||||
|
|
||||||
await client.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-03: Network Failures - should retry on transient network errors', async () => {
|
|
||||||
// Simplified test - just ensure client handles transient failures gracefully
|
|
||||||
const client = createSmtpClient({
|
|
||||||
host: 'localhost',
|
|
||||||
port: 9998, // Another non-listening port
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 1000
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await client.verify();
|
|
||||||
|
|
||||||
expect(result).toBeFalse();
|
|
||||||
console.log('✅ Network error handled gracefully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-03: Network Failures - should handle slow network (timeout)', async () => {
|
|
||||||
// Simplified test - just test with unreachable host instead of slow server
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
const client = createSmtpClient({
|
|
||||||
host: '192.0.2.99', // Another TEST-NET IP that should timeout
|
|
||||||
port: 25,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 3000
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await client.verify();
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
|
|
||||||
expect(result).toBeFalse();
|
|
||||||
console.log(`✅ Slow network timeout after ${duration}ms`);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-03: Network Failures - should recover from temporary network issues', async () => {
|
|
||||||
const client = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
pool: true,
|
|
||||||
maxConnections: 2,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send first email successfully
|
|
||||||
const email1 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Before Network Issue',
|
|
||||||
text: 'First email'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result1 = await client.sendMail(email1);
|
|
||||||
expect(result1.success).toBeTrue();
|
|
||||||
|
|
||||||
// Simulate network recovery by sending another email
|
|
||||||
const email2 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'After Network Recovery',
|
|
||||||
text: 'Second email after recovery'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result2 = await client.sendMail(email2);
|
|
||||||
expect(result2.success).toBeTrue();
|
|
||||||
|
|
||||||
console.log('✅ Recovered from simulated network issues');
|
|
||||||
|
|
||||||
await client.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-03: Network Failures - should handle EHOSTUNREACH', async () => {
|
|
||||||
// Use an IP that should be unreachable
|
|
||||||
const client = createSmtpClient({
|
|
||||||
host: '192.0.2.1', // TEST-NET-1, should be unreachable
|
|
||||||
port: 25,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 3000
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await client.verify();
|
|
||||||
|
|
||||||
expect(result).toBeFalse();
|
|
||||||
console.log('✅ Host unreachable error handled');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-03: Network Failures - should handle packet loss simulation', async () => {
|
|
||||||
// Create a server that randomly drops data
|
|
||||||
let packetCount = 0;
|
|
||||||
const lossyServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 Lossy server ready\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
packetCount++;
|
|
||||||
|
|
||||||
// Simulate 30% packet loss
|
|
||||||
if (Math.random() > 0.3) {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Otherwise, don't respond (simulate packet loss)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
lossyServer.listen(2558, () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const client = createSmtpClient({
|
|
||||||
host: 'localhost',
|
|
||||||
port: 2558,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 1000,
|
|
||||||
socketTimeout: 1000 // Short timeout to detect loss
|
|
||||||
});
|
|
||||||
|
|
||||||
let verifyResult = false;
|
|
||||||
let errorOccurred = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
verifyResult = await client.verify();
|
|
||||||
if (verifyResult) {
|
|
||||||
console.log('✅ Connected despite simulated packet loss');
|
|
||||||
} else {
|
|
||||||
console.log('✅ Connection failed due to packet loss');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
errorOccurred = true;
|
|
||||||
console.log(`✅ Packet loss detected after ${packetCount} packets: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Either verification failed or an error occurred - both are expected with packet loss
|
|
||||||
expect(!verifyResult || errorOccurred).toBeTrue();
|
|
||||||
|
|
||||||
// Clean up client first
|
|
||||||
try {
|
|
||||||
await client.close();
|
|
||||||
} catch (closeError) {
|
|
||||||
// Ignore close errors in this test
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then close server
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
lossyServer.close(() => resolve());
|
|
||||||
});
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-03: Network Failures - should provide meaningful error messages', async () => {
|
|
||||||
const errorScenarios = [
|
|
||||||
{
|
|
||||||
host: 'localhost',
|
|
||||||
port: 9999,
|
|
||||||
expectedError: 'ECONNREFUSED'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
host: 'invalid.domain.test',
|
|
||||||
port: 25,
|
|
||||||
expectedError: 'ENOTFOUND'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const scenario of errorScenarios) {
|
|
||||||
const client = createSmtpClient({
|
|
||||||
host: scenario.host,
|
|
||||||
port: scenario.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 3000
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await client.verify();
|
|
||||||
|
|
||||||
expect(result).toBeFalse();
|
|
||||||
console.log(`✅ Clear error for ${scenario.host}:${scenario.port} - connection failed as expected`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - stop SMTP server', async () => {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,255 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
import * as net from 'net';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup - start SMTP server for greylisting tests', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2559,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(testServer.port).toEqual(2559);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-04: Basic greylisting response handling', async () => {
|
|
||||||
// Create server that simulates greylisting
|
|
||||||
const greylistServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 Greylist Test Server\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO') || command.startsWith('HELO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('MAIL FROM')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('RCPT TO')) {
|
|
||||||
// Simulate greylisting response
|
|
||||||
socket.write('451 4.7.1 Greylisting in effect, please retry later\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
} else {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
greylistServer.listen(2560, () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: 2560,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Greylisting Test',
|
|
||||||
text: 'Testing greylisting response handling'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
// Should get a failed result due to greylisting
|
|
||||||
expect(result.success).toBeFalse();
|
|
||||||
console.log('Actual error:', result.error?.message);
|
|
||||||
expect(result.error?.message).toMatch(/451|greylist|rejected/i);
|
|
||||||
console.log('✅ Greylisting response handled correctly');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
greylistServer.close(() => resolve());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-04: Different greylisting response codes', async () => {
|
|
||||||
// Test recognition of various greylisting response patterns
|
|
||||||
const greylistResponses = [
|
|
||||||
{ code: '451 4.7.1', message: 'Greylisting in effect, please retry', isGreylist: true },
|
|
||||||
{ code: '450 4.7.1', message: 'Try again later', isGreylist: true },
|
|
||||||
{ code: '451 4.7.0', message: 'Temporary rejection', isGreylist: true },
|
|
||||||
{ code: '421 4.7.0', message: 'Too many connections, try later', isGreylist: false },
|
|
||||||
{ code: '452 4.2.2', message: 'Mailbox full', isGreylist: false },
|
|
||||||
{ code: '451', message: 'Requested action aborted', isGreylist: false }
|
|
||||||
];
|
|
||||||
|
|
||||||
console.log('Testing greylisting response recognition:');
|
|
||||||
|
|
||||||
for (const response of greylistResponses) {
|
|
||||||
console.log(`Response: ${response.code} ${response.message}`);
|
|
||||||
|
|
||||||
// Check if response matches greylisting patterns
|
|
||||||
const isGreylistPattern =
|
|
||||||
(response.code.startsWith('450') || response.code.startsWith('451')) &&
|
|
||||||
(response.message.toLowerCase().includes('grey') ||
|
|
||||||
response.message.toLowerCase().includes('try') ||
|
|
||||||
response.message.toLowerCase().includes('later') ||
|
|
||||||
response.message.toLowerCase().includes('temporary') ||
|
|
||||||
response.code.includes('4.7.'));
|
|
||||||
|
|
||||||
console.log(` Detected as greylisting: ${isGreylistPattern}`);
|
|
||||||
console.log(` Expected: ${response.isGreylist}`);
|
|
||||||
|
|
||||||
expect(isGreylistPattern).toEqual(response.isGreylist);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-04: Greylisting with temporary failure', async () => {
|
|
||||||
// Create server that sends 450 response (temporary failure)
|
|
||||||
const tempFailServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 Temp Fail Server\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('MAIL FROM')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('RCPT TO')) {
|
|
||||||
socket.write('450 4.7.1 Mailbox temporarily unavailable\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
tempFailServer.listen(2561, () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: 2561,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: '450 Test',
|
|
||||||
text: 'Testing 450 temporary failure response'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeFalse();
|
|
||||||
console.log('Actual error:', result.error?.message);
|
|
||||||
expect(result.error?.message).toMatch(/450|temporary|rejected/i);
|
|
||||||
console.log('✅ 450 temporary failure handled');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
tempFailServer.close(() => resolve());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-04: Greylisting with multiple recipients', async () => {
|
|
||||||
// Test successful email send to multiple recipients on working server
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['user1@normal.com', 'user2@example.com'],
|
|
||||||
subject: 'Multi-recipient Test',
|
|
||||||
text: 'Testing multiple recipients'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Multiple recipients handled correctly');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-04: Basic connection verification', async () => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.verify();
|
|
||||||
|
|
||||||
expect(result).toBeTrue();
|
|
||||||
console.log('✅ Connection verification successful');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-04: Server with RCPT rejection', async () => {
|
|
||||||
// Test server rejecting at RCPT TO stage
|
|
||||||
const rejectServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 Reject Server\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('MAIL FROM')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('RCPT TO')) {
|
|
||||||
socket.write('451 4.2.1 Recipient rejected temporarily\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
rejectServer.listen(2562, () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: 2562,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'RCPT Rejection Test',
|
|
||||||
text: 'Testing RCPT TO rejection'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeFalse();
|
|
||||||
console.log('Actual error:', result.error?.message);
|
|
||||||
expect(result.error?.message).toMatch(/451|reject|recipient/i);
|
|
||||||
console.log('✅ RCPT rejection handled correctly');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
rejectServer.close(() => resolve());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - stop SMTP server', async () => {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,273 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
import * as net from 'net';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup - start SMTP server for quota tests', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2563,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(testServer.port).toEqual(2563);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-05: Mailbox quota exceeded - 452 temporary', async () => {
|
|
||||||
// Create server that simulates temporary quota full
|
|
||||||
const quotaServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 Quota Test Server\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('MAIL FROM')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('RCPT TO')) {
|
|
||||||
socket.write('452 4.2.2 Mailbox full, try again later\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
quotaServer.listen(2564, () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: 2564,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'user@example.com',
|
|
||||||
subject: 'Quota Test',
|
|
||||||
text: 'Testing quota errors'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeFalse();
|
|
||||||
console.log('Actual error:', result.error?.message);
|
|
||||||
expect(result.error?.message).toMatch(/452|mailbox|full|recipient/i);
|
|
||||||
console.log('✅ 452 temporary quota error handled');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
quotaServer.close(() => resolve());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-05: Mailbox quota exceeded - 552 permanent', async () => {
|
|
||||||
// Create server that simulates permanent quota exceeded
|
|
||||||
const quotaServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 Quota Test Server\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('MAIL FROM')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('RCPT TO')) {
|
|
||||||
socket.write('552 5.2.2 Mailbox quota exceeded\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
quotaServer.listen(2565, () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: 2565,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'user@example.com',
|
|
||||||
subject: 'Quota Test',
|
|
||||||
text: 'Testing quota errors'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeFalse();
|
|
||||||
console.log('Actual error:', result.error?.message);
|
|
||||||
expect(result.error?.message).toMatch(/552|quota|recipient/i);
|
|
||||||
console.log('✅ 552 permanent quota error handled');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
quotaServer.close(() => resolve());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-05: System storage error - 452', async () => {
|
|
||||||
// Create server that simulates system storage issue
|
|
||||||
const storageServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 Storage Test Server\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('MAIL FROM')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('RCPT TO')) {
|
|
||||||
socket.write('452 4.3.1 Insufficient system storage\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
storageServer.listen(2566, () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: 2566,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'user@example.com',
|
|
||||||
subject: 'Storage Test',
|
|
||||||
text: 'Testing storage errors'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeFalse();
|
|
||||||
console.log('Actual error:', result.error?.message);
|
|
||||||
expect(result.error?.message).toMatch(/452|storage|recipient/i);
|
|
||||||
console.log('✅ 452 system storage error handled');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
storageServer.close(() => resolve());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-05: Message too large - 552', async () => {
|
|
||||||
// Create server that simulates message size limit
|
|
||||||
const sizeServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 Size Test Server\r\n');
|
|
||||||
let inData = false;
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (!line && lines[lines.length - 1] === '') return;
|
|
||||||
|
|
||||||
if (inData) {
|
|
||||||
// We're in DATA mode - look for the terminating dot
|
|
||||||
if (line === '.') {
|
|
||||||
socket.write('552 5.3.4 Message too big for system\r\n');
|
|
||||||
inData = false;
|
|
||||||
}
|
|
||||||
// Otherwise, just consume the data
|
|
||||||
} else {
|
|
||||||
// We're in command mode
|
|
||||||
if (line.startsWith('EHLO')) {
|
|
||||||
socket.write('250-SIZE 1000\r\n250 OK\r\n');
|
|
||||||
} else if (line.startsWith('MAIL FROM')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('RCPT TO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line === 'DATA') {
|
|
||||||
socket.write('354 Send data\r\n');
|
|
||||||
inData = true;
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
sizeServer.listen(2567, () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: 2567,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'user@example.com',
|
|
||||||
subject: 'Large Message Test',
|
|
||||||
text: 'This is supposed to be a large message that exceeds the size limit'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeFalse();
|
|
||||||
console.log('Actual error:', result.error?.message);
|
|
||||||
expect(result.error?.message).toMatch(/552|big|size|data/i);
|
|
||||||
console.log('✅ 552 message size error handled');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
sizeServer.close(() => resolve());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-05: Successful email with normal server', async () => {
|
|
||||||
// Test successful email send with working server
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'user@example.com',
|
|
||||||
subject: 'Normal Test',
|
|
||||||
text: 'Testing normal operation'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Normal email sent successfully');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - stop SMTP server', async () => {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,320 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
import * as net from 'net';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup - start SMTP server for invalid recipient tests', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2568,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(testServer.port).toEqual(2568);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-06: Invalid email address formats', async () => {
|
|
||||||
// Test various invalid email formats that should be caught by Email validation
|
|
||||||
const invalidEmails = [
|
|
||||||
'notanemail',
|
|
||||||
'@example.com',
|
|
||||||
'user@',
|
|
||||||
'user@@example.com',
|
|
||||||
'user@domain..com'
|
|
||||||
];
|
|
||||||
|
|
||||||
console.log('Testing invalid email formats:');
|
|
||||||
|
|
||||||
for (const invalidEmail of invalidEmails) {
|
|
||||||
console.log(`Testing: ${invalidEmail}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: invalidEmail,
|
|
||||||
subject: 'Invalid Recipient Test',
|
|
||||||
text: 'Testing invalid email format'
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('✗ Should have thrown validation error');
|
|
||||||
} catch (error: any) {
|
|
||||||
console.log(`✅ Validation error caught: ${error.message}`);
|
|
||||||
expect(error).toBeInstanceOf(Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-06: SMTP 550 Invalid recipient', async () => {
|
|
||||||
// Create server that rejects certain recipients
|
|
||||||
const rejectServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 Reject Server\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('MAIL FROM')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('RCPT TO')) {
|
|
||||||
if (command.includes('invalid@')) {
|
|
||||||
socket.write('550 5.1.1 Invalid recipient\r\n');
|
|
||||||
} else if (command.includes('unknown@')) {
|
|
||||||
socket.write('550 5.1.1 User unknown\r\n');
|
|
||||||
} else {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
rejectServer.listen(2569, () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: 2569,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'invalid@example.com',
|
|
||||||
subject: 'Invalid Recipient Test',
|
|
||||||
text: 'Testing invalid recipient'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeFalse();
|
|
||||||
console.log('Actual error:', result.error?.message);
|
|
||||||
expect(result.error?.message).toMatch(/550|invalid|recipient/i);
|
|
||||||
console.log('✅ 550 invalid recipient error handled');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
rejectServer.close(() => resolve());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-06: SMTP 550 User unknown', async () => {
|
|
||||||
// Create server that responds with user unknown
|
|
||||||
const unknownServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 Unknown Server\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('MAIL FROM')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('RCPT TO')) {
|
|
||||||
socket.write('550 5.1.1 User unknown\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
unknownServer.listen(2570, () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: 2570,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'unknown@example.com',
|
|
||||||
subject: 'Unknown User Test',
|
|
||||||
text: 'Testing unknown user'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeFalse();
|
|
||||||
console.log('Actual error:', result.error?.message);
|
|
||||||
expect(result.error?.message).toMatch(/550|unknown|recipient/i);
|
|
||||||
console.log('✅ 550 user unknown error handled');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
unknownServer.close(() => resolve());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-06: Mixed valid and invalid recipients', async () => {
|
|
||||||
// Create server that accepts some recipients and rejects others
|
|
||||||
const mixedServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 Mixed Server\r\n');
|
|
||||||
let inData = false;
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (!line && lines[lines.length - 1] === '') return;
|
|
||||||
|
|
||||||
if (inData) {
|
|
||||||
// We're in DATA mode - look for the terminating dot
|
|
||||||
if (line === '.') {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
inData = false;
|
|
||||||
}
|
|
||||||
// Otherwise, just consume the data
|
|
||||||
} else {
|
|
||||||
// We're in command mode
|
|
||||||
if (line.startsWith('EHLO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('MAIL FROM')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('RCPT TO')) {
|
|
||||||
if (line.includes('valid@')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else {
|
|
||||||
socket.write('550 5.1.1 Recipient rejected\r\n');
|
|
||||||
}
|
|
||||||
} else if (line === 'DATA') {
|
|
||||||
socket.write('354 Send data\r\n');
|
|
||||||
inData = true;
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
mixedServer.listen(2571, () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: 2571,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['valid@example.com', 'invalid@example.com'],
|
|
||||||
subject: 'Mixed Recipients Test',
|
|
||||||
text: 'Testing mixed valid and invalid recipients'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
// When there are mixed valid/invalid recipients, the email might succeed for valid ones
|
|
||||||
// or fail entirely depending on the implementation. In this implementation, it appears
|
|
||||||
// the client sends to valid recipients and silently ignores the rejected ones.
|
|
||||||
if (result.success) {
|
|
||||||
console.log('✅ Email sent to valid recipients, invalid ones were rejected by server');
|
|
||||||
} else {
|
|
||||||
console.log('Actual error:', result.error?.message);
|
|
||||||
expect(result.error?.message).toMatch(/550|reject|recipient|partial/i);
|
|
||||||
console.log('✅ Mixed recipients error handled - all recipients rejected');
|
|
||||||
}
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
mixedServer.close(() => resolve());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-06: Domain not found - 550', async () => {
|
|
||||||
// Create server that rejects due to domain issues
|
|
||||||
const domainServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 Domain Server\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('MAIL FROM')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('RCPT TO')) {
|
|
||||||
socket.write('550 5.1.2 Domain not found\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
domainServer.listen(2572, () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: 2572,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'user@nonexistent.domain',
|
|
||||||
subject: 'Domain Not Found Test',
|
|
||||||
text: 'Testing domain not found'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeFalse();
|
|
||||||
console.log('Actual error:', result.error?.message);
|
|
||||||
expect(result.error?.message).toMatch(/550|domain|recipient/i);
|
|
||||||
console.log('✅ 550 domain not found error handled');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
domainServer.close(() => resolve());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-06: Valid recipient succeeds', async () => {
|
|
||||||
// Test successful email send with working server
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'valid@example.com',
|
|
||||||
subject: 'Valid Recipient Test',
|
|
||||||
text: 'Testing valid recipient'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Valid recipient email sent successfully');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - stop SMTP server', async () => {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,320 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
import * as net from 'net';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup - start SMTP server for size limit tests', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2573,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(testServer.port).toEqual(2573);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-07: Server with SIZE extension', async () => {
|
|
||||||
// Create server that advertises SIZE extension
|
|
||||||
const sizeServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 Size Test Server\r\n');
|
|
||||||
|
|
||||||
let buffer = '';
|
|
||||||
let inData = false;
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
buffer += data.toString();
|
|
||||||
|
|
||||||
let lines = buffer.split('\r\n');
|
|
||||||
buffer = lines.pop() || '';
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
const command = line.trim();
|
|
||||||
if (!command) continue;
|
|
||||||
|
|
||||||
if (inData) {
|
|
||||||
if (command === '.') {
|
|
||||||
inData = false;
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250-SIZE 1048576\r\n'); // 1MB limit
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('MAIL FROM')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('RCPT TO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'DATA') {
|
|
||||||
socket.write('354 Send data\r\n');
|
|
||||||
inData = true;
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
sizeServer.listen(2574, () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: 2574,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Size Test',
|
|
||||||
text: 'Testing SIZE extension'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Email sent with SIZE extension support');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
sizeServer.close(() => resolve());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-07: Message too large at MAIL FROM', async () => {
|
|
||||||
// Create server that rejects based on SIZE parameter
|
|
||||||
const strictSizeServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 Strict Size Server\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250-SIZE 1000\r\n'); // Very small limit
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('MAIL FROM')) {
|
|
||||||
// Always reject with size error
|
|
||||||
socket.write('552 5.3.4 Message size exceeds fixed maximum message size\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
strictSizeServer.listen(2575, () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: 2575,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Large Message',
|
|
||||||
text: 'This message will be rejected due to size'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeFalse();
|
|
||||||
console.log('Actual error:', result.error?.message);
|
|
||||||
expect(result.error?.message).toMatch(/552|size|exceeds|maximum/i);
|
|
||||||
console.log('✅ Message size rejection at MAIL FROM handled');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
strictSizeServer.close(() => resolve());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-07: Message too large at DATA', async () => {
|
|
||||||
// Create server that rejects after receiving data
|
|
||||||
const dataRejectServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 Data Reject Server\r\n');
|
|
||||||
|
|
||||||
let buffer = '';
|
|
||||||
let inData = false;
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
buffer += data.toString();
|
|
||||||
|
|
||||||
let lines = buffer.split('\r\n');
|
|
||||||
buffer = lines.pop() || '';
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
const command = line.trim();
|
|
||||||
if (!command) continue;
|
|
||||||
|
|
||||||
if (inData) {
|
|
||||||
if (command === '.') {
|
|
||||||
inData = false;
|
|
||||||
socket.write('552 5.3.4 Message too big for system\r\n');
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('MAIL FROM')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('RCPT TO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'DATA') {
|
|
||||||
socket.write('354 Send data\r\n');
|
|
||||||
inData = true;
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
dataRejectServer.listen(2576, () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: 2576,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Large Message Test',
|
|
||||||
text: 'x'.repeat(10000) // Simulate large content
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeFalse();
|
|
||||||
console.log('Actual error:', result.error?.message);
|
|
||||||
expect(result.error?.message).toMatch(/552|big|size|data/i);
|
|
||||||
console.log('✅ Message size rejection at DATA handled');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
dataRejectServer.close(() => resolve());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-07: Temporary size error - 452', async () => {
|
|
||||||
// Create server that returns temporary size error
|
|
||||||
const tempSizeServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 Temp Size Server\r\n');
|
|
||||||
|
|
||||||
let buffer = '';
|
|
||||||
let inData = false;
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
buffer += data.toString();
|
|
||||||
|
|
||||||
let lines = buffer.split('\r\n');
|
|
||||||
buffer = lines.pop() || '';
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
const command = line.trim();
|
|
||||||
if (!command) continue;
|
|
||||||
|
|
||||||
if (inData) {
|
|
||||||
if (command === '.') {
|
|
||||||
inData = false;
|
|
||||||
socket.write('452 4.3.1 Insufficient system storage\r\n');
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('MAIL FROM')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('RCPT TO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'DATA') {
|
|
||||||
socket.write('354 Send data\r\n');
|
|
||||||
inData = true;
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
tempSizeServer.listen(2577, () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: 2577,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Temporary Size Error Test',
|
|
||||||
text: 'Testing temporary size error'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeFalse();
|
|
||||||
console.log('Actual error:', result.error?.message);
|
|
||||||
expect(result.error?.message).toMatch(/452|storage|data/i);
|
|
||||||
console.log('✅ Temporary size error handled');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
tempSizeServer.close(() => resolve());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-07: Normal email within size limits', async () => {
|
|
||||||
// Test successful email send with working server
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Normal Size Test',
|
|
||||||
text: 'Testing normal size email that should succeed'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Normal size email sent successfully');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - stop SMTP server', async () => {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,261 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
import * as net from 'net';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup - start SMTP server for rate limiting tests', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2578,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(testServer.port).toEqual(2578);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-08: Server rate limiting - 421 too many connections', async () => {
|
|
||||||
// Create server that immediately rejects with rate limit
|
|
||||||
const rateLimitServer = net.createServer((socket) => {
|
|
||||||
socket.write('421 4.7.0 Too many connections, please try again later\r\n');
|
|
||||||
socket.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
rateLimitServer.listen(2579, () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: 2579,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.verify();
|
|
||||||
|
|
||||||
expect(result).toBeFalse();
|
|
||||||
console.log('✅ 421 rate limit response handled');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
rateLimitServer.close(() => resolve());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-08: Message rate limiting - 452', async () => {
|
|
||||||
// Create server that rate limits at MAIL FROM
|
|
||||||
const messageRateServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 Message Rate Server\r\n');
|
|
||||||
|
|
||||||
let buffer = '';
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
buffer += data.toString();
|
|
||||||
|
|
||||||
let lines = buffer.split('\r\n');
|
|
||||||
buffer = lines.pop() || '';
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
const command = line.trim();
|
|
||||||
if (!command) continue;
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('MAIL FROM')) {
|
|
||||||
socket.write('452 4.3.2 Too many messages sent, please try later\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
messageRateServer.listen(2580, () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: 2580,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Rate Limit Test',
|
|
||||||
text: 'Testing rate limiting'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeFalse();
|
|
||||||
console.log('Actual error:', result.error?.message);
|
|
||||||
expect(result.error?.message).toMatch(/452|many|messages|rate/i);
|
|
||||||
console.log('✅ 452 message rate limit handled');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
messageRateServer.close(() => resolve());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-08: User rate limiting - 550', async () => {
|
|
||||||
// Create server that permanently blocks user
|
|
||||||
const userRateServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 User Rate Server\r\n');
|
|
||||||
|
|
||||||
let buffer = '';
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
buffer += data.toString();
|
|
||||||
|
|
||||||
let lines = buffer.split('\r\n');
|
|
||||||
buffer = lines.pop() || '';
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
const command = line.trim();
|
|
||||||
if (!command) continue;
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('MAIL FROM')) {
|
|
||||||
if (command.includes('blocked@')) {
|
|
||||||
socket.write('550 5.7.1 User sending rate exceeded\r\n');
|
|
||||||
} else {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}
|
|
||||||
} else if (command.startsWith('RCPT TO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
userRateServer.listen(2581, () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: 2581,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'blocked@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'User Rate Test',
|
|
||||||
text: 'Testing user rate limiting'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeFalse();
|
|
||||||
console.log('Actual error:', result.error?.message);
|
|
||||||
expect(result.error?.message).toMatch(/550|rate|exceeded/i);
|
|
||||||
console.log('✅ 550 user rate limit handled');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
userRateServer.close(() => resolve());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-08: Connection throttling - delayed response', async () => {
|
|
||||||
// Create server that delays responses to simulate throttling
|
|
||||||
const throttleServer = net.createServer((socket) => {
|
|
||||||
// Delay initial greeting
|
|
||||||
setTimeout(() => {
|
|
||||||
socket.write('220 Throttle Server\r\n');
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
let buffer = '';
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
buffer += data.toString();
|
|
||||||
|
|
||||||
let lines = buffer.split('\r\n');
|
|
||||||
buffer = lines.pop() || '';
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
const command = line.trim();
|
|
||||||
if (!command) continue;
|
|
||||||
|
|
||||||
// Add delay to all responses
|
|
||||||
setTimeout(() => {
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
} else {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}
|
|
||||||
}, 50);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
throttleServer.listen(2582, () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: 2582,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
const result = await smtpClient.verify();
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
|
|
||||||
expect(result).toBeTrue();
|
|
||||||
console.log(`✅ Throttled connection succeeded in ${duration}ms`);
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
throttleServer.close(() => resolve());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-08: Normal email without rate limiting', async () => {
|
|
||||||
// Test successful email send with working server
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Normal Test',
|
|
||||||
text: 'Testing normal operation without rate limits'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Normal email sent successfully');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - stop SMTP server', async () => {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,299 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
import * as net from 'net';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup - start SMTP server for connection pool tests', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2583,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(testServer.port).toEqual(2583);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-09: Connection pool with concurrent sends', async () => {
|
|
||||||
// Test basic connection pooling functionality
|
|
||||||
const pooledClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
pool: true,
|
|
||||||
maxConnections: 2,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Testing connection pool with concurrent sends...');
|
|
||||||
|
|
||||||
// Send multiple messages concurrently
|
|
||||||
const emails = [
|
|
||||||
new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient1@example.com',
|
|
||||||
subject: 'Pool test 1',
|
|
||||||
text: 'Testing connection pool'
|
|
||||||
}),
|
|
||||||
new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient2@example.com',
|
|
||||||
subject: 'Pool test 2',
|
|
||||||
text: 'Testing connection pool'
|
|
||||||
}),
|
|
||||||
new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient3@example.com',
|
|
||||||
subject: 'Pool test 3',
|
|
||||||
text: 'Testing connection pool'
|
|
||||||
})
|
|
||||||
];
|
|
||||||
|
|
||||||
const results = await Promise.all(
|
|
||||||
emails.map(email => pooledClient.sendMail(email))
|
|
||||||
);
|
|
||||||
|
|
||||||
const successful = results.filter(r => r.success).length;
|
|
||||||
|
|
||||||
console.log(`✅ Sent ${successful} messages using connection pool`);
|
|
||||||
expect(successful).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
await pooledClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-09: Connection pool with server limit', async () => {
|
|
||||||
// Create server that limits concurrent connections
|
|
||||||
let activeConnections = 0;
|
|
||||||
const maxServerConnections = 1;
|
|
||||||
|
|
||||||
const limitedServer = net.createServer((socket) => {
|
|
||||||
activeConnections++;
|
|
||||||
|
|
||||||
if (activeConnections > maxServerConnections) {
|
|
||||||
socket.write('421 4.7.0 Too many connections\r\n');
|
|
||||||
socket.end();
|
|
||||||
activeConnections--;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.write('220 Limited Server\r\n');
|
|
||||||
|
|
||||||
let buffer = '';
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
buffer += data.toString();
|
|
||||||
|
|
||||||
let lines = buffer.split('\r\n');
|
|
||||||
buffer = lines.pop() || '';
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
const command = line.trim();
|
|
||||||
if (!command) continue;
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
} else {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('close', () => {
|
|
||||||
activeConnections--;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
limitedServer.listen(2584, () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const pooledClient = await createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: 2584,
|
|
||||||
secure: false,
|
|
||||||
pool: true,
|
|
||||||
maxConnections: 3, // Client wants 3 but server only allows 1
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Try concurrent connections
|
|
||||||
const results = await Promise.all([
|
|
||||||
pooledClient.verify(),
|
|
||||||
pooledClient.verify(),
|
|
||||||
pooledClient.verify()
|
|
||||||
]);
|
|
||||||
|
|
||||||
const successful = results.filter(r => r === true).length;
|
|
||||||
|
|
||||||
console.log(`✅ ${successful} connections succeeded with server limit`);
|
|
||||||
expect(successful).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
await pooledClient.close();
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
limitedServer.close(() => resolve());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-09: Connection pool recovery after error', async () => {
|
|
||||||
// Create server that fails sometimes
|
|
||||||
let requestCount = 0;
|
|
||||||
|
|
||||||
const flakyServer = net.createServer((socket) => {
|
|
||||||
requestCount++;
|
|
||||||
|
|
||||||
// Fail every 3rd connection
|
|
||||||
if (requestCount % 3 === 0) {
|
|
||||||
socket.destroy();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.write('220 Flaky Server\r\n');
|
|
||||||
|
|
||||||
let buffer = '';
|
|
||||||
let inData = false;
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
buffer += data.toString();
|
|
||||||
|
|
||||||
let lines = buffer.split('\r\n');
|
|
||||||
buffer = lines.pop() || '';
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
const command = line.trim();
|
|
||||||
if (!command) continue;
|
|
||||||
|
|
||||||
if (inData) {
|
|
||||||
if (command === '.') {
|
|
||||||
inData = false;
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('MAIL FROM')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('RCPT TO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'DATA') {
|
|
||||||
socket.write('354 Send data\r\n');
|
|
||||||
inData = true;
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
flakyServer.listen(2585, () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const pooledClient = await createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: 2585,
|
|
||||||
secure: false,
|
|
||||||
pool: true,
|
|
||||||
maxConnections: 2,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send multiple messages to test recovery
|
|
||||||
const results = [];
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: `Recovery test ${i}`,
|
|
||||||
text: 'Testing pool recovery'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await pooledClient.sendMail(email);
|
|
||||||
results.push(result.success);
|
|
||||||
console.log(`Message ${i}: ${result.success ? 'Success' : 'Failed'}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const successful = results.filter(r => r === true).length;
|
|
||||||
|
|
||||||
console.log(`✅ Pool recovered from errors: ${successful}/5 succeeded`);
|
|
||||||
expect(successful).toBeGreaterThan(2);
|
|
||||||
|
|
||||||
await pooledClient.close();
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
flakyServer.close(() => resolve());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-09: Connection pool timeout handling', async () => {
|
|
||||||
// Create very slow server
|
|
||||||
const slowServer = net.createServer((socket) => {
|
|
||||||
// Wait 2 seconds before sending greeting
|
|
||||||
setTimeout(() => {
|
|
||||||
socket.write('220 Very Slow Server\r\n');
|
|
||||||
}, 2000);
|
|
||||||
|
|
||||||
socket.on('data', () => {
|
|
||||||
// Don't respond to any commands
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
slowServer.listen(2586, () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const pooledClient = await createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: 2586,
|
|
||||||
secure: false,
|
|
||||||
pool: true,
|
|
||||||
connectionTimeout: 1000 // 1 second timeout
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await pooledClient.verify();
|
|
||||||
|
|
||||||
expect(result).toBeFalse();
|
|
||||||
console.log('✅ Connection pool handled timeout correctly');
|
|
||||||
|
|
||||||
await pooledClient.close();
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
slowServer.close(() => resolve());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-09: Normal pooled operation', async () => {
|
|
||||||
// Test successful pooled operation
|
|
||||||
const pooledClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
pool: true,
|
|
||||||
maxConnections: 2
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Pool Test',
|
|
||||||
text: 'Testing normal pooled operation'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await pooledClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Normal pooled email sent successfully');
|
|
||||||
|
|
||||||
await pooledClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - stop SMTP server', async () => {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,373 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
import * as net from 'net';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup test SMTP server', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 0,
|
|
||||||
enableStarttls: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
expect(testServer).toBeTruthy();
|
|
||||||
expect(testServer.port).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-10: Partial recipient failure', async (t) => {
|
|
||||||
// Create server that accepts some recipients and rejects others
|
|
||||||
const partialFailureServer = net.createServer((socket) => {
|
|
||||||
let inData = false;
|
|
||||||
socket.write('220 Partial Failure Test Server\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n').filter(line => line.length > 0);
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
const command = line.trim();
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('MAIL FROM')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('RCPT TO')) {
|
|
||||||
const recipient = command.match(/<([^>]+)>/)?.[1] || '';
|
|
||||||
|
|
||||||
// Accept/reject based on recipient
|
|
||||||
if (recipient.includes('valid')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (recipient.includes('invalid')) {
|
|
||||||
socket.write('550 5.1.1 User unknown\r\n');
|
|
||||||
} else if (recipient.includes('full')) {
|
|
||||||
socket.write('452 4.2.2 Mailbox full\r\n');
|
|
||||||
} else if (recipient.includes('greylisted')) {
|
|
||||||
socket.write('451 4.7.1 Greylisted, try again later\r\n');
|
|
||||||
} else {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}
|
|
||||||
} else if (command === 'DATA') {
|
|
||||||
inData = true;
|
|
||||||
socket.write('354 Send data\r\n');
|
|
||||||
} else if (inData && command === '.') {
|
|
||||||
inData = false;
|
|
||||||
socket.write('250 OK - delivered to accepted recipients only\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
partialFailureServer.listen(0, '127.0.0.1', () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const partialPort = (partialFailureServer.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: partialPort,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Testing partial recipient failure...');
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: [
|
|
||||||
'valid1@example.com',
|
|
||||||
'invalid@example.com',
|
|
||||||
'valid2@example.com',
|
|
||||||
'full@example.com',
|
|
||||||
'valid3@example.com',
|
|
||||||
'greylisted@example.com'
|
|
||||||
],
|
|
||||||
subject: 'Partial failure test',
|
|
||||||
text: 'Testing partial recipient failures'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
// The current implementation might not have detailed partial failure tracking
|
|
||||||
// So we just check if the email was sent (even with some recipients failing)
|
|
||||||
if (result && result.success) {
|
|
||||||
console.log('Email sent with partial success');
|
|
||||||
} else {
|
|
||||||
console.log('Email sending reported failure');
|
|
||||||
}
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
partialFailureServer.close(() => resolve());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-10: Partial data transmission failure', async (t) => {
|
|
||||||
// Server that fails during DATA phase
|
|
||||||
const dataFailureServer = net.createServer((socket) => {
|
|
||||||
let dataSize = 0;
|
|
||||||
let inData = false;
|
|
||||||
|
|
||||||
socket.write('220 Data Failure Test Server\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n').filter(line => line.length > 0);
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
const command = line.trim();
|
|
||||||
|
|
||||||
if (!inData) {
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('MAIL FROM')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('RCPT TO')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'DATA') {
|
|
||||||
inData = true;
|
|
||||||
dataSize = 0;
|
|
||||||
socket.write('354 Send data\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dataSize += data.length;
|
|
||||||
|
|
||||||
// Fail after receiving 1KB of data
|
|
||||||
if (dataSize > 1024) {
|
|
||||||
socket.write('451 4.3.0 Message transmission failed\r\n');
|
|
||||||
socket.destroy();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command === '.') {
|
|
||||||
inData = false;
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
dataFailureServer.listen(0, '127.0.0.1', () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const dataFailurePort = (dataFailureServer.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
console.log('Testing partial data transmission failure...');
|
|
||||||
|
|
||||||
// Try to send large message that will fail during transmission
|
|
||||||
const largeEmail = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Large message test',
|
|
||||||
text: 'x'.repeat(2048) // 2KB - will fail after 1KB
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: dataFailurePort,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(largeEmail);
|
|
||||||
|
|
||||||
if (!result || !result.success) {
|
|
||||||
console.log('Data transmission failed as expected');
|
|
||||||
} else {
|
|
||||||
console.log('Unexpected success');
|
|
||||||
}
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
|
|
||||||
// Try smaller message that should succeed
|
|
||||||
const smallEmail = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Small message test',
|
|
||||||
text: 'This is a small message'
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient2 = await createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: dataFailurePort,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const result2 = await smtpClient2.sendMail(smallEmail);
|
|
||||||
|
|
||||||
if (result2 && result2.success) {
|
|
||||||
console.log('Small message sent successfully');
|
|
||||||
} else {
|
|
||||||
console.log('Small message also failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
await smtpClient2.close();
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
dataFailureServer.close(() => resolve());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-10: Partial authentication failure', async (t) => {
|
|
||||||
// Server with selective authentication
|
|
||||||
const authFailureServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 Auth Failure Test Server\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n').filter(line => line.length > 0);
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
const command = line.trim();
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250-authfailure.example.com\r\n');
|
|
||||||
socket.write('250-AUTH PLAIN LOGIN\r\n');
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('AUTH')) {
|
|
||||||
// Randomly fail authentication
|
|
||||||
if (Math.random() > 0.5) {
|
|
||||||
socket.write('235 2.7.0 Authentication successful\r\n');
|
|
||||||
} else {
|
|
||||||
socket.write('535 5.7.8 Authentication credentials invalid\r\n');
|
|
||||||
}
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
} else {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
authFailureServer.listen(0, '127.0.0.1', () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const authPort = (authFailureServer.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
console.log('Testing partial authentication failure with fallback...');
|
|
||||||
|
|
||||||
// Try multiple authentication attempts
|
|
||||||
let authenticated = false;
|
|
||||||
let attempts = 0;
|
|
||||||
const maxAttempts = 3;
|
|
||||||
|
|
||||||
while (!authenticated && attempts < maxAttempts) {
|
|
||||||
attempts++;
|
|
||||||
console.log(`Attempt ${attempts}: PLAIN authentication`);
|
|
||||||
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: authPort,
|
|
||||||
secure: false,
|
|
||||||
auth: {
|
|
||||||
user: 'testuser',
|
|
||||||
pass: 'testpass'
|
|
||||||
},
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// The verify method will handle authentication
|
|
||||||
const isConnected = await smtpClient.verify();
|
|
||||||
|
|
||||||
if (isConnected) {
|
|
||||||
authenticated = true;
|
|
||||||
console.log('Authentication successful');
|
|
||||||
|
|
||||||
// Send test message
|
|
||||||
const result = await smtpClient.sendMail(new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Auth test',
|
|
||||||
text: 'Successfully authenticated'
|
|
||||||
}));
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
console.log('Authentication failed');
|
|
||||||
await smtpClient.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Authentication ${authenticated ? 'succeeded' : 'failed'} after ${attempts} attempts`);
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
authFailureServer.close(() => resolve());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CERR-10: Partial failure reporting', async (t) => {
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Testing partial failure reporting...');
|
|
||||||
|
|
||||||
// Send email to multiple recipients
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['user1@example.com', 'user2@example.com', 'user3@example.com'],
|
|
||||||
subject: 'Partial failure test',
|
|
||||||
text: 'Testing partial failures'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
if (result && result.success) {
|
|
||||||
console.log('Email sent successfully');
|
|
||||||
if (result.messageId) {
|
|
||||||
console.log(`Message ID: ${result.messageId}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('Email sending failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a mock partial failure report
|
|
||||||
const partialResult = {
|
|
||||||
messageId: '<123456@example.com>',
|
|
||||||
timestamp: new Date(),
|
|
||||||
from: 'sender@example.com',
|
|
||||||
accepted: ['user1@example.com', 'user2@example.com'],
|
|
||||||
rejected: [
|
|
||||||
{ recipient: 'invalid@example.com', code: '550', reason: 'User unknown' }
|
|
||||||
],
|
|
||||||
pending: [
|
|
||||||
{ recipient: 'grey@example.com', code: '451', reason: 'Greylisted' }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
const total = partialResult.accepted.length + partialResult.rejected.length + partialResult.pending.length;
|
|
||||||
const successRate = ((partialResult.accepted.length / total) * 100).toFixed(1);
|
|
||||||
|
|
||||||
console.log(`Partial Failure Summary:`);
|
|
||||||
console.log(` Total: ${total}`);
|
|
||||||
console.log(` Delivered: ${partialResult.accepted.length}`);
|
|
||||||
console.log(` Failed: ${partialResult.rejected.length}`);
|
|
||||||
console.log(` Deferred: ${partialResult.pending.length}`);
|
|
||||||
console.log(` Success rate: ${successRate}%`);
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup test SMTP server', async () => {
|
|
||||||
if (testServer) {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,332 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createBulkSmtpClient, createPooledSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
let bulkClient: SmtpClient;
|
|
||||||
|
|
||||||
tap.test('setup - start SMTP server for bulk sending tests', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 0,
|
|
||||||
enableStarttls: false,
|
|
||||||
authRequired: false,
|
|
||||||
testTimeout: 120000 // Increase timeout for performance tests
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(testServer.port).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CPERF-01: Bulk Sending - should send multiple emails efficiently', async (tools) => {
|
|
||||||
tools.timeout(60000); // 60 second timeout for bulk test
|
|
||||||
|
|
||||||
bulkClient = createBulkSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
debug: false // Disable debug for performance
|
|
||||||
});
|
|
||||||
|
|
||||||
const emailCount = 20; // Significantly reduced
|
|
||||||
const startTime = Date.now();
|
|
||||||
let successCount = 0;
|
|
||||||
|
|
||||||
// Send emails sequentially with small delay to avoid overwhelming
|
|
||||||
for (let i = 0; i < emailCount; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'bulk-sender@example.com',
|
|
||||||
to: [`recipient-${i}@example.com`],
|
|
||||||
subject: `Bulk Email ${i + 1}`,
|
|
||||||
text: `This is bulk email number ${i + 1} of ${emailCount}`
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await bulkClient.sendMail(email);
|
|
||||||
if (result.success) {
|
|
||||||
successCount++;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`Failed to send email ${i}: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Small delay between emails
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 50));
|
|
||||||
}
|
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
|
|
||||||
expect(successCount).toBeGreaterThan(emailCount * 0.5); // Allow 50% success rate
|
|
||||||
|
|
||||||
const rate = (successCount / (duration / 1000)).toFixed(2);
|
|
||||||
console.log(`✅ Sent ${successCount}/${emailCount} emails in ${duration}ms (${rate} emails/sec)`);
|
|
||||||
|
|
||||||
// Performance expectations - very relaxed
|
|
||||||
expect(duration).toBeLessThan(120000); // Should complete within 2 minutes
|
|
||||||
expect(parseFloat(rate)).toBeGreaterThan(0.1); // At least 0.1 emails/sec
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CPERF-01: Bulk Sending - should handle concurrent bulk sends', async (tools) => {
|
|
||||||
tools.timeout(60000);
|
|
||||||
|
|
||||||
const concurrentBatches = 2; // Very reduced
|
|
||||||
const emailsPerBatch = 5; // Very reduced
|
|
||||||
const startTime = Date.now();
|
|
||||||
let totalSuccess = 0;
|
|
||||||
|
|
||||||
// Send batches sequentially instead of concurrently
|
|
||||||
for (let batch = 0; batch < concurrentBatches; batch++) {
|
|
||||||
const batchPromises = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < emailsPerBatch; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'batch-sender@example.com',
|
|
||||||
to: [`batch${batch}-recipient${i}@example.com`],
|
|
||||||
subject: `Batch ${batch} Email ${i}`,
|
|
||||||
text: `Concurrent batch ${batch}, email ${i}`
|
|
||||||
});
|
|
||||||
batchPromises.push(bulkClient.sendMail(email));
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = await Promise.all(batchPromises);
|
|
||||||
totalSuccess += results.filter(r => r.success).length;
|
|
||||||
|
|
||||||
// Delay between batches
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
}
|
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
const totalEmails = concurrentBatches * emailsPerBatch;
|
|
||||||
|
|
||||||
expect(totalSuccess).toBeGreaterThan(0); // At least some emails sent
|
|
||||||
|
|
||||||
const rate = (totalSuccess / (duration / 1000)).toFixed(2);
|
|
||||||
console.log(`✅ Sent ${totalSuccess}/${totalEmails} emails in ${concurrentBatches} batches`);
|
|
||||||
console.log(` Duration: ${duration}ms (${rate} emails/sec)`);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CPERF-01: Bulk Sending - should optimize with connection pooling', async (tools) => {
|
|
||||||
tools.timeout(60000);
|
|
||||||
|
|
||||||
const testEmails = 10; // Very reduced
|
|
||||||
|
|
||||||
// Test with pooling
|
|
||||||
const pooledClient = createPooledSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
maxConnections: 3, // Reduced connections
|
|
||||||
debug: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const pooledStart = Date.now();
|
|
||||||
let pooledSuccessCount = 0;
|
|
||||||
|
|
||||||
// Send emails sequentially
|
|
||||||
for (let i = 0; i < testEmails; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'pooled@example.com',
|
|
||||||
to: [`recipient${i}@example.com`],
|
|
||||||
subject: `Pooled Email ${i}`,
|
|
||||||
text: 'Testing pooled performance'
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await pooledClient.sendMail(email);
|
|
||||||
if (result.success) {
|
|
||||||
pooledSuccessCount++;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`Pooled email ${i} failed: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
}
|
|
||||||
|
|
||||||
const pooledDuration = Date.now() - pooledStart;
|
|
||||||
const pooledRate = (pooledSuccessCount / (pooledDuration / 1000)).toFixed(2);
|
|
||||||
|
|
||||||
await pooledClient.close();
|
|
||||||
|
|
||||||
console.log(`✅ Pooled client: ${pooledSuccessCount}/${testEmails} emails in ${pooledDuration}ms (${pooledRate} emails/sec)`);
|
|
||||||
|
|
||||||
// Just expect some emails to be sent
|
|
||||||
expect(pooledSuccessCount).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CPERF-01: Bulk Sending - should handle emails with attachments', async (tools) => {
|
|
||||||
tools.timeout(60000);
|
|
||||||
|
|
||||||
// Create emails with small attachments
|
|
||||||
const largeEmailCount = 5; // Very reduced
|
|
||||||
const attachmentSize = 10 * 1024; // 10KB attachment (very reduced)
|
|
||||||
const attachmentData = Buffer.alloc(attachmentSize, 'x'); // Fill with 'x'
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
let successCount = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < largeEmailCount; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'bulk-sender@example.com',
|
|
||||||
to: [`recipient${i}@example.com`],
|
|
||||||
subject: `Large Bulk Email ${i}`,
|
|
||||||
text: 'This email contains an attachment',
|
|
||||||
attachments: [{
|
|
||||||
filename: `attachment-${i}.txt`,
|
|
||||||
content: attachmentData.toString('base64'),
|
|
||||||
encoding: 'base64',
|
|
||||||
contentType: 'text/plain'
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await bulkClient.sendMail(email);
|
|
||||||
if (result.success) {
|
|
||||||
successCount++;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`Large email ${i} failed: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 200));
|
|
||||||
}
|
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
|
|
||||||
expect(successCount).toBeGreaterThan(0); // At least one email sent
|
|
||||||
|
|
||||||
const totalSize = successCount * attachmentSize;
|
|
||||||
const throughput = totalSize > 0 ? (totalSize / 1024 / 1024 / (duration / 1000)).toFixed(2) : '0';
|
|
||||||
|
|
||||||
console.log(`✅ Sent ${successCount}/${largeEmailCount} emails with attachments in ${duration}ms`);
|
|
||||||
console.log(` Total data: ${(totalSize / 1024 / 1024).toFixed(2)}MB`);
|
|
||||||
console.log(` Throughput: ${throughput} MB/s`);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CPERF-01: Bulk Sending - should maintain performance under sustained load', async (tools) => {
|
|
||||||
tools.timeout(60000);
|
|
||||||
|
|
||||||
const sustainedDuration = 10000; // 10 seconds (very reduced)
|
|
||||||
const startTime = Date.now();
|
|
||||||
let emailsSent = 0;
|
|
||||||
let errors = 0;
|
|
||||||
|
|
||||||
console.log('📊 Starting sustained load test...');
|
|
||||||
|
|
||||||
// Send emails continuously for duration
|
|
||||||
while (Date.now() - startTime < sustainedDuration) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sustained@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: `Sustained Load Email ${emailsSent + 1}`,
|
|
||||||
text: `Email sent at ${new Date().toISOString()}`
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await bulkClient.sendMail(email);
|
|
||||||
if (result.success) {
|
|
||||||
emailsSent++;
|
|
||||||
} else {
|
|
||||||
errors++;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
errors++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Longer delay to avoid overwhelming server
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
|
||||||
|
|
||||||
// Log progress every 5 emails
|
|
||||||
if (emailsSent % 5 === 0 && emailsSent > 0) {
|
|
||||||
const elapsed = Date.now() - startTime;
|
|
||||||
const rate = (emailsSent / (elapsed / 1000)).toFixed(2);
|
|
||||||
console.log(` Progress: ${emailsSent} emails, ${rate} emails/sec`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalDuration = Date.now() - startTime;
|
|
||||||
const avgRate = (emailsSent / (totalDuration / 1000)).toFixed(2);
|
|
||||||
|
|
||||||
console.log(`✅ Sustained load test completed:`);
|
|
||||||
console.log(` Duration: ${totalDuration}ms`);
|
|
||||||
console.log(` Emails sent: ${emailsSent}`);
|
|
||||||
console.log(` Errors: ${errors}`);
|
|
||||||
console.log(` Average rate: ${avgRate} emails/sec`);
|
|
||||||
|
|
||||||
expect(emailsSent).toBeGreaterThan(5); // Should send at least 5 emails
|
|
||||||
expect(errors).toBeLessThan(emailsSent); // Fewer errors than successes
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CPERF-01: Bulk Sending - should track performance metrics', async () => {
|
|
||||||
const metricsClient = createBulkSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
debug: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const metrics = {
|
|
||||||
sent: 0,
|
|
||||||
failed: 0,
|
|
||||||
totalTime: 0,
|
|
||||||
minTime: Infinity,
|
|
||||||
maxTime: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
// Send emails and collect metrics
|
|
||||||
for (let i = 0; i < 5; i++) { // Very reduced
|
|
||||||
const email = new Email({
|
|
||||||
from: 'metrics@example.com',
|
|
||||||
to: [`recipient${i}@example.com`],
|
|
||||||
subject: `Metrics Test ${i}`,
|
|
||||||
text: 'Collecting performance metrics'
|
|
||||||
});
|
|
||||||
|
|
||||||
const sendStart = Date.now();
|
|
||||||
try {
|
|
||||||
const result = await metricsClient.sendMail(email);
|
|
||||||
const sendTime = Date.now() - sendStart;
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
metrics.sent++;
|
|
||||||
metrics.totalTime += sendTime;
|
|
||||||
metrics.minTime = Math.min(metrics.minTime, sendTime);
|
|
||||||
metrics.maxTime = Math.max(metrics.maxTime, sendTime);
|
|
||||||
} else {
|
|
||||||
metrics.failed++;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
metrics.failed++;
|
|
||||||
}
|
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 200));
|
|
||||||
}
|
|
||||||
|
|
||||||
const avgTime = metrics.sent > 0 ? metrics.totalTime / metrics.sent : 0;
|
|
||||||
|
|
||||||
console.log('📊 Performance metrics:');
|
|
||||||
console.log(` Sent: ${metrics.sent}`);
|
|
||||||
console.log(` Failed: ${metrics.failed}`);
|
|
||||||
console.log(` Avg time: ${avgTime.toFixed(2)}ms`);
|
|
||||||
console.log(` Min time: ${metrics.minTime === Infinity ? 'N/A' : metrics.minTime + 'ms'}`);
|
|
||||||
console.log(` Max time: ${metrics.maxTime}ms`);
|
|
||||||
|
|
||||||
await metricsClient.close();
|
|
||||||
|
|
||||||
expect(metrics.sent).toBeGreaterThan(0);
|
|
||||||
if (metrics.sent > 0) {
|
|
||||||
expect(avgTime).toBeLessThan(30000); // Average should be under 30 seconds
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - close bulk client', async () => {
|
|
||||||
if (bulkClient) {
|
|
||||||
await bulkClient.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - stop SMTP server', async () => {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,304 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient, createPooledSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
import * as net from 'net';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup - start SMTP server for throughput tests', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 0,
|
|
||||||
enableStarttls: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(testServer.port).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CPERF-02: Sequential message throughput', async (tools) => {
|
|
||||||
tools.timeout(60000);
|
|
||||||
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
debug: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const messageCount = 10;
|
|
||||||
const messages = Array(messageCount).fill(null).map((_, i) =>
|
|
||||||
new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: [`recipient${i + 1}@example.com`],
|
|
||||||
subject: `Sequential throughput test ${i + 1}`,
|
|
||||||
text: `Testing sequential message sending - message ${i + 1}`
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`Sending ${messageCount} messages sequentially...`);
|
|
||||||
const sequentialStart = Date.now();
|
|
||||||
let successCount = 0;
|
|
||||||
|
|
||||||
for (const message of messages) {
|
|
||||||
try {
|
|
||||||
const result = await smtpClient.sendMail(message);
|
|
||||||
if (result.success) successCount++;
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Failed to send:', error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const sequentialTime = Date.now() - sequentialStart;
|
|
||||||
const sequentialRate = (successCount / sequentialTime) * 1000;
|
|
||||||
|
|
||||||
console.log(`Sequential throughput: ${sequentialRate.toFixed(2)} messages/second`);
|
|
||||||
console.log(`Successfully sent: ${successCount}/${messageCount} messages`);
|
|
||||||
console.log(`Total time: ${sequentialTime}ms`);
|
|
||||||
|
|
||||||
expect(successCount).toBeGreaterThan(0);
|
|
||||||
expect(sequentialRate).toBeGreaterThan(0.1); // At least 0.1 message per second
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CPERF-02: Concurrent message throughput', async (tools) => {
|
|
||||||
tools.timeout(60000);
|
|
||||||
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
debug: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const messageCount = 10;
|
|
||||||
const messages = Array(messageCount).fill(null).map((_, i) =>
|
|
||||||
new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: [`recipient${i + 1}@example.com`],
|
|
||||||
subject: `Concurrent throughput test ${i + 1}`,
|
|
||||||
text: `Testing concurrent message sending - message ${i + 1}`
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`Sending ${messageCount} messages concurrently...`);
|
|
||||||
const concurrentStart = Date.now();
|
|
||||||
|
|
||||||
// Send in small batches to avoid overwhelming
|
|
||||||
const batchSize = 3;
|
|
||||||
const results = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < messages.length; i += batchSize) {
|
|
||||||
const batch = messages.slice(i, i + batchSize);
|
|
||||||
const batchResults = await Promise.all(
|
|
||||||
batch.map(message => smtpClient.sendMail(message).catch(err => ({ success: false, error: err })))
|
|
||||||
);
|
|
||||||
results.push(...batchResults);
|
|
||||||
|
|
||||||
// Small delay between batches
|
|
||||||
if (i + batchSize < messages.length) {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const successCount = results.filter(r => r.success).length;
|
|
||||||
const concurrentTime = Date.now() - concurrentStart;
|
|
||||||
const concurrentRate = (successCount / concurrentTime) * 1000;
|
|
||||||
|
|
||||||
console.log(`Concurrent throughput: ${concurrentRate.toFixed(2)} messages/second`);
|
|
||||||
console.log(`Successfully sent: ${successCount}/${messageCount} messages`);
|
|
||||||
console.log(`Total time: ${concurrentTime}ms`);
|
|
||||||
|
|
||||||
expect(successCount).toBeGreaterThan(0);
|
|
||||||
expect(concurrentRate).toBeGreaterThan(0.1);
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CPERF-02: Connection pooling throughput', async (tools) => {
|
|
||||||
tools.timeout(60000);
|
|
||||||
|
|
||||||
const pooledClient = await createPooledSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
maxConnections: 3,
|
|
||||||
debug: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const messageCount = 15;
|
|
||||||
const messages = Array(messageCount).fill(null).map((_, i) =>
|
|
||||||
new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: [`recipient${i + 1}@example.com`],
|
|
||||||
subject: `Pooled throughput test ${i + 1}`,
|
|
||||||
text: `Testing connection pooling - message ${i + 1}`
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`Sending ${messageCount} messages with connection pooling...`);
|
|
||||||
const poolStart = Date.now();
|
|
||||||
|
|
||||||
// Send in small batches
|
|
||||||
const batchSize = 5;
|
|
||||||
const results = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < messages.length; i += batchSize) {
|
|
||||||
const batch = messages.slice(i, i + batchSize);
|
|
||||||
const batchResults = await Promise.all(
|
|
||||||
batch.map(message => pooledClient.sendMail(message).catch(err => ({ success: false, error: err })))
|
|
||||||
);
|
|
||||||
results.push(...batchResults);
|
|
||||||
|
|
||||||
// Small delay between batches
|
|
||||||
if (i + batchSize < messages.length) {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 200));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const successCount = results.filter(r => r.success).length;
|
|
||||||
const poolTime = Date.now() - poolStart;
|
|
||||||
const poolRate = (successCount / poolTime) * 1000;
|
|
||||||
|
|
||||||
console.log(`Pooled throughput: ${poolRate.toFixed(2)} messages/second`);
|
|
||||||
console.log(`Successfully sent: ${successCount}/${messageCount} messages`);
|
|
||||||
console.log(`Total time: ${poolTime}ms`);
|
|
||||||
|
|
||||||
expect(successCount).toBeGreaterThan(0);
|
|
||||||
expect(poolRate).toBeGreaterThan(0.1);
|
|
||||||
|
|
||||||
await pooledClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CPERF-02: Variable message size throughput', async (tools) => {
|
|
||||||
tools.timeout(60000);
|
|
||||||
|
|
||||||
const smtpClient = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
debug: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create messages of varying sizes
|
|
||||||
const messageSizes = [
|
|
||||||
{ size: 'small', content: 'Short message' },
|
|
||||||
{ size: 'medium', content: 'Medium message: ' + 'x'.repeat(500) },
|
|
||||||
{ size: 'large', content: 'Large message: ' + 'x'.repeat(5000) }
|
|
||||||
];
|
|
||||||
|
|
||||||
const messages = [];
|
|
||||||
for (let i = 0; i < 9; i++) {
|
|
||||||
const sizeType = messageSizes[i % messageSizes.length];
|
|
||||||
messages.push(new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: [`recipient${i + 1}@example.com`],
|
|
||||||
subject: `Variable size test ${i + 1} (${sizeType.size})`,
|
|
||||||
text: sizeType.content
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Sending ${messages.length} messages of varying sizes...`);
|
|
||||||
const variableStart = Date.now();
|
|
||||||
let successCount = 0;
|
|
||||||
let totalBytes = 0;
|
|
||||||
|
|
||||||
for (const message of messages) {
|
|
||||||
try {
|
|
||||||
const result = await smtpClient.sendMail(message);
|
|
||||||
if (result.success) {
|
|
||||||
successCount++;
|
|
||||||
// Estimate message size
|
|
||||||
totalBytes += message.text ? message.text.length : 0;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Failed to send:', error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Small delay between messages
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
}
|
|
||||||
|
|
||||||
const variableTime = Date.now() - variableStart;
|
|
||||||
const variableRate = (successCount / variableTime) * 1000;
|
|
||||||
const bytesPerSecond = (totalBytes / variableTime) * 1000;
|
|
||||||
|
|
||||||
console.log(`Variable size throughput: ${variableRate.toFixed(2)} messages/second`);
|
|
||||||
console.log(`Data throughput: ${(bytesPerSecond / 1024).toFixed(2)} KB/second`);
|
|
||||||
console.log(`Successfully sent: ${successCount}/${messages.length} messages`);
|
|
||||||
|
|
||||||
expect(successCount).toBeGreaterThan(0);
|
|
||||||
expect(variableRate).toBeGreaterThan(0.1);
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CPERF-02: Sustained throughput over time', async (tools) => {
|
|
||||||
tools.timeout(60000);
|
|
||||||
|
|
||||||
const smtpClient = await createPooledSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
maxConnections: 2,
|
|
||||||
debug: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const totalMessages = 12;
|
|
||||||
const batchSize = 3;
|
|
||||||
const batchDelay = 1000; // 1 second between batches
|
|
||||||
|
|
||||||
console.log(`Sending ${totalMessages} messages in batches of ${batchSize}...`);
|
|
||||||
const sustainedStart = Date.now();
|
|
||||||
let totalSuccess = 0;
|
|
||||||
const timestamps: number[] = [];
|
|
||||||
|
|
||||||
for (let batch = 0; batch < totalMessages / batchSize; batch++) {
|
|
||||||
const batchMessages = Array(batchSize).fill(null).map((_, i) => {
|
|
||||||
const msgIndex = batch * batchSize + i + 1;
|
|
||||||
return new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: [`recipient${msgIndex}@example.com`],
|
|
||||||
subject: `Sustained test batch ${batch + 1} message ${i + 1}`,
|
|
||||||
text: `Testing sustained throughput - message ${msgIndex}`
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send batch
|
|
||||||
const batchStart = Date.now();
|
|
||||||
const results = await Promise.all(
|
|
||||||
batchMessages.map(message => smtpClient.sendMail(message).catch(err => ({ success: false })))
|
|
||||||
);
|
|
||||||
|
|
||||||
const batchSuccess = results.filter(r => r.success).length;
|
|
||||||
totalSuccess += batchSuccess;
|
|
||||||
timestamps.push(Date.now());
|
|
||||||
|
|
||||||
console.log(` Batch ${batch + 1} completed: ${batchSuccess}/${batchSize} successful`);
|
|
||||||
|
|
||||||
// Delay between batches (except last)
|
|
||||||
if (batch < (totalMessages / batchSize) - 1) {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, batchDelay));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const sustainedTime = Date.now() - sustainedStart;
|
|
||||||
const sustainedRate = (totalSuccess / sustainedTime) * 1000;
|
|
||||||
|
|
||||||
console.log(`Sustained throughput: ${sustainedRate.toFixed(2)} messages/second`);
|
|
||||||
console.log(`Successfully sent: ${totalSuccess}/${totalMessages} messages`);
|
|
||||||
console.log(`Total time: ${sustainedTime}ms`);
|
|
||||||
|
|
||||||
expect(totalSuccess).toBeGreaterThan(0);
|
|
||||||
expect(sustainedRate).toBeGreaterThan(0.05); // Very relaxed for sustained test
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - stop SMTP server', async () => {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,332 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient, createPooledSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
// Helper function to get memory usage
|
|
||||||
const getMemoryUsage = () => {
|
|
||||||
if (process.memoryUsage) {
|
|
||||||
const usage = process.memoryUsage();
|
|
||||||
return {
|
|
||||||
heapUsed: usage.heapUsed,
|
|
||||||
heapTotal: usage.heapTotal,
|
|
||||||
external: usage.external,
|
|
||||||
rss: usage.rss
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper function to format bytes
|
|
||||||
const formatBytes = (bytes: number) => {
|
|
||||||
if (bytes < 1024) return `${bytes} B`;
|
|
||||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
||||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
||||||
};
|
|
||||||
|
|
||||||
tap.test('setup - start SMTP server for memory tests', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 0,
|
|
||||||
enableStarttls: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(testServer.port).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CPERF-03: Memory usage during connection lifecycle', async (tools) => {
|
|
||||||
tools.timeout(30000);
|
|
||||||
|
|
||||||
const memoryBefore = getMemoryUsage();
|
|
||||||
console.log('Initial memory usage:', {
|
|
||||||
heapUsed: formatBytes(memoryBefore.heapUsed),
|
|
||||||
heapTotal: formatBytes(memoryBefore.heapTotal),
|
|
||||||
rss: formatBytes(memoryBefore.rss)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create and close multiple connections
|
|
||||||
const connectionCount = 10;
|
|
||||||
|
|
||||||
for (let i = 0; i < connectionCount; i++) {
|
|
||||||
const client = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
debug: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send a test email
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: `Memory test ${i + 1}`,
|
|
||||||
text: 'Testing memory usage'
|
|
||||||
});
|
|
||||||
|
|
||||||
await client.sendMail(email);
|
|
||||||
await client.close();
|
|
||||||
|
|
||||||
// Small delay between connections
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force garbage collection if available
|
|
||||||
if (global.gc) {
|
|
||||||
global.gc();
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
}
|
|
||||||
|
|
||||||
const memoryAfter = getMemoryUsage();
|
|
||||||
const memoryIncrease = memoryAfter.heapUsed - memoryBefore.heapUsed;
|
|
||||||
|
|
||||||
console.log(`Memory after ${connectionCount} connections:`, {
|
|
||||||
heapUsed: formatBytes(memoryAfter.heapUsed),
|
|
||||||
heapTotal: formatBytes(memoryAfter.heapTotal),
|
|
||||||
rss: formatBytes(memoryAfter.rss)
|
|
||||||
});
|
|
||||||
console.log(`Memory increase: ${formatBytes(memoryIncrease)}`);
|
|
||||||
console.log(`Average per connection: ${formatBytes(memoryIncrease / connectionCount)}`);
|
|
||||||
|
|
||||||
// Memory increase should be reasonable
|
|
||||||
expect(memoryIncrease / connectionCount).toBeLessThan(1024 * 1024); // Less than 1MB per connection
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CPERF-03: Memory usage with large messages', async (tools) => {
|
|
||||||
tools.timeout(30000);
|
|
||||||
|
|
||||||
const client = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
debug: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const memoryBefore = getMemoryUsage();
|
|
||||||
console.log('Memory before large messages:', {
|
|
||||||
heapUsed: formatBytes(memoryBefore.heapUsed)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send messages of increasing size
|
|
||||||
const sizes = [1024, 10240, 102400]; // 1KB, 10KB, 100KB
|
|
||||||
|
|
||||||
for (const size of sizes) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: `Large message test (${formatBytes(size)})`,
|
|
||||||
text: 'x'.repeat(size)
|
|
||||||
});
|
|
||||||
|
|
||||||
await client.sendMail(email);
|
|
||||||
|
|
||||||
const memoryAfter = getMemoryUsage();
|
|
||||||
console.log(`Memory after ${formatBytes(size)} message:`, {
|
|
||||||
heapUsed: formatBytes(memoryAfter.heapUsed),
|
|
||||||
increase: formatBytes(memoryAfter.heapUsed - memoryBefore.heapUsed)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Small delay
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 200));
|
|
||||||
}
|
|
||||||
|
|
||||||
await client.close();
|
|
||||||
|
|
||||||
const memoryFinal = getMemoryUsage();
|
|
||||||
const totalIncrease = memoryFinal.heapUsed - memoryBefore.heapUsed;
|
|
||||||
|
|
||||||
console.log(`Total memory increase: ${formatBytes(totalIncrease)}`);
|
|
||||||
|
|
||||||
// Memory should not grow excessively
|
|
||||||
expect(totalIncrease).toBeLessThan(10 * 1024 * 1024); // Less than 10MB total
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CPERF-03: Memory usage with connection pooling', async (tools) => {
|
|
||||||
tools.timeout(30000);
|
|
||||||
|
|
||||||
const memoryBefore = getMemoryUsage();
|
|
||||||
console.log('Memory before pooling test:', {
|
|
||||||
heapUsed: formatBytes(memoryBefore.heapUsed)
|
|
||||||
});
|
|
||||||
|
|
||||||
const pooledClient = await createPooledSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
maxConnections: 3,
|
|
||||||
debug: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send multiple emails through the pool
|
|
||||||
const emailCount = 15;
|
|
||||||
const emails = Array(emailCount).fill(null).map((_, i) =>
|
|
||||||
new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: [`recipient${i}@example.com`],
|
|
||||||
subject: `Pooled memory test ${i + 1}`,
|
|
||||||
text: 'Testing memory with connection pooling'
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Send in batches
|
|
||||||
for (let i = 0; i < emails.length; i += 3) {
|
|
||||||
const batch = emails.slice(i, i + 3);
|
|
||||||
await Promise.all(batch.map(email =>
|
|
||||||
pooledClient.sendMail(email).catch(err => console.log('Send error:', err.message))
|
|
||||||
));
|
|
||||||
|
|
||||||
// Check memory after each batch
|
|
||||||
const memoryNow = getMemoryUsage();
|
|
||||||
console.log(`Memory after batch ${Math.floor(i/3) + 1}:`, {
|
|
||||||
heapUsed: formatBytes(memoryNow.heapUsed),
|
|
||||||
increase: formatBytes(memoryNow.heapUsed - memoryBefore.heapUsed)
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
}
|
|
||||||
|
|
||||||
await pooledClient.close();
|
|
||||||
|
|
||||||
const memoryFinal = getMemoryUsage();
|
|
||||||
const totalIncrease = memoryFinal.heapUsed - memoryBefore.heapUsed;
|
|
||||||
|
|
||||||
console.log(`Total memory increase with pooling: ${formatBytes(totalIncrease)}`);
|
|
||||||
console.log(`Average per email: ${formatBytes(totalIncrease / emailCount)}`);
|
|
||||||
|
|
||||||
// Pooling should be memory efficient
|
|
||||||
expect(totalIncrease / emailCount).toBeLessThan(500 * 1024); // Less than 500KB per email
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CPERF-03: Memory cleanup after errors', async (tools) => {
|
|
||||||
tools.timeout(30000);
|
|
||||||
|
|
||||||
const memoryBefore = getMemoryUsage();
|
|
||||||
console.log('Memory before error test:', {
|
|
||||||
heapUsed: formatBytes(memoryBefore.heapUsed)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Try to send emails that might fail
|
|
||||||
const errorCount = 5;
|
|
||||||
|
|
||||||
for (let i = 0; i < errorCount; i++) {
|
|
||||||
try {
|
|
||||||
const client = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 1000, // Short timeout
|
|
||||||
debug: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create a large email that might cause issues
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: `Error test ${i + 1}`,
|
|
||||||
text: 'x'.repeat(100000), // 100KB
|
|
||||||
attachments: [{
|
|
||||||
filename: 'test.txt',
|
|
||||||
content: Buffer.alloc(50000).toString('base64'), // 50KB attachment
|
|
||||||
encoding: 'base64'
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
await client.sendMail(email);
|
|
||||||
await client.close();
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`Error ${i + 1} handled: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force garbage collection if available
|
|
||||||
if (global.gc) {
|
|
||||||
global.gc();
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
}
|
|
||||||
|
|
||||||
const memoryAfter = getMemoryUsage();
|
|
||||||
const memoryIncrease = memoryAfter.heapUsed - memoryBefore.heapUsed;
|
|
||||||
|
|
||||||
console.log(`Memory after ${errorCount} error scenarios:`, {
|
|
||||||
heapUsed: formatBytes(memoryAfter.heapUsed),
|
|
||||||
increase: formatBytes(memoryIncrease)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Memory should be properly cleaned up after errors
|
|
||||||
expect(memoryIncrease).toBeLessThan(5 * 1024 * 1024); // Less than 5MB increase
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CPERF-03: Long-running memory stability', async (tools) => {
|
|
||||||
tools.timeout(60000);
|
|
||||||
|
|
||||||
const client = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
debug: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const memorySnapshots = [];
|
|
||||||
const duration = 10000; // 10 seconds
|
|
||||||
const interval = 2000; // Check every 2 seconds
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
console.log('Testing memory stability over time...');
|
|
||||||
|
|
||||||
let emailsSent = 0;
|
|
||||||
|
|
||||||
while (Date.now() - startTime < duration) {
|
|
||||||
// Send an email
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: `Stability test ${++emailsSent}`,
|
|
||||||
text: `Testing memory stability at ${new Date().toISOString()}`
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await client.sendMail(email);
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Send error:', error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Take memory snapshot
|
|
||||||
const memory = getMemoryUsage();
|
|
||||||
const elapsed = Date.now() - startTime;
|
|
||||||
memorySnapshots.push({
|
|
||||||
time: elapsed,
|
|
||||||
heapUsed: memory.heapUsed
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`[${elapsed}ms] Heap: ${formatBytes(memory.heapUsed)}, Emails sent: ${emailsSent}`);
|
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, interval));
|
|
||||||
}
|
|
||||||
|
|
||||||
await client.close();
|
|
||||||
|
|
||||||
// Analyze memory growth
|
|
||||||
const firstSnapshot = memorySnapshots[0];
|
|
||||||
const lastSnapshot = memorySnapshots[memorySnapshots.length - 1];
|
|
||||||
const memoryGrowth = lastSnapshot.heapUsed - firstSnapshot.heapUsed;
|
|
||||||
const growthRate = memoryGrowth / (lastSnapshot.time / 1000); // bytes per second
|
|
||||||
|
|
||||||
console.log(`\nMemory stability results:`);
|
|
||||||
console.log(` Duration: ${lastSnapshot.time}ms`);
|
|
||||||
console.log(` Emails sent: ${emailsSent}`);
|
|
||||||
console.log(` Memory growth: ${formatBytes(memoryGrowth)}`);
|
|
||||||
console.log(` Growth rate: ${formatBytes(growthRate)}/second`);
|
|
||||||
|
|
||||||
// Memory growth should be minimal over time
|
|
||||||
expect(growthRate).toBeLessThan(150 * 1024); // Less than 150KB/second growth
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - stop SMTP server', async () => {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,373 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient, createPooledSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
// Helper function to measure CPU usage
|
|
||||||
const measureCpuUsage = async (duration: number) => {
|
|
||||||
const start = process.cpuUsage();
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, duration));
|
|
||||||
|
|
||||||
const end = process.cpuUsage(start);
|
|
||||||
const elapsed = Date.now() - startTime;
|
|
||||||
|
|
||||||
// Ensure minimum elapsed time to avoid division issues
|
|
||||||
const actualElapsed = Math.max(elapsed, 1);
|
|
||||||
|
|
||||||
return {
|
|
||||||
user: end.user / 1000, // Convert to milliseconds
|
|
||||||
system: end.system / 1000,
|
|
||||||
total: (end.user + end.system) / 1000,
|
|
||||||
elapsed: actualElapsed,
|
|
||||||
userPercent: (end.user / 1000) / actualElapsed * 100,
|
|
||||||
systemPercent: (end.system / 1000) / actualElapsed * 100,
|
|
||||||
totalPercent: Math.min(((end.user + end.system) / 1000) / actualElapsed * 100, 100)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
tap.test('setup - start SMTP server for CPU tests', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 0,
|
|
||||||
enableStarttls: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(testServer.port).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CPERF-04: CPU usage during connection establishment', async (tools) => {
|
|
||||||
tools.timeout(30000);
|
|
||||||
|
|
||||||
console.log('Testing CPU usage during connection establishment...');
|
|
||||||
|
|
||||||
// Measure baseline CPU
|
|
||||||
const baseline = await measureCpuUsage(1000);
|
|
||||||
console.log(`Baseline CPU: ${baseline.totalPercent.toFixed(2)}%`);
|
|
||||||
|
|
||||||
// Ensure we have a meaningful duration for measurement
|
|
||||||
const connectionCount = 5;
|
|
||||||
const startTime = Date.now();
|
|
||||||
const cpuStart = process.cpuUsage();
|
|
||||||
|
|
||||||
for (let i = 0; i < connectionCount; i++) {
|
|
||||||
const client = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
debug: false
|
|
||||||
});
|
|
||||||
|
|
||||||
await client.close();
|
|
||||||
|
|
||||||
// Small delay to ensure measurable duration
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
}
|
|
||||||
|
|
||||||
const elapsed = Date.now() - startTime;
|
|
||||||
const cpuEnd = process.cpuUsage(cpuStart);
|
|
||||||
|
|
||||||
// Ensure minimum elapsed time
|
|
||||||
const actualElapsed = Math.max(elapsed, 100);
|
|
||||||
const cpuPercent = Math.min(((cpuEnd.user + cpuEnd.system) / 1000) / actualElapsed * 100, 100);
|
|
||||||
|
|
||||||
console.log(`CPU usage for ${connectionCount} connections:`);
|
|
||||||
console.log(` Total time: ${actualElapsed}ms`);
|
|
||||||
console.log(` CPU time: ${(cpuEnd.user + cpuEnd.system) / 1000}ms`);
|
|
||||||
console.log(` CPU usage: ${cpuPercent.toFixed(2)}%`);
|
|
||||||
console.log(` Average per connection: ${(cpuPercent / connectionCount).toFixed(2)}%`);
|
|
||||||
|
|
||||||
// CPU usage should be reasonable (relaxed for test environment)
|
|
||||||
expect(cpuPercent).toBeLessThan(100); // Must be less than 100%
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CPERF-04: CPU usage during message sending', async (tools) => {
|
|
||||||
tools.timeout(30000);
|
|
||||||
|
|
||||||
console.log('\nTesting CPU usage during message sending...');
|
|
||||||
|
|
||||||
const client = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
debug: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const messageCount = 10; // Reduced for more stable measurement
|
|
||||||
|
|
||||||
// Measure CPU during message sending
|
|
||||||
const cpuStart = process.cpuUsage();
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
for (let i = 0; i < messageCount; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: [`recipient${i}@example.com`],
|
|
||||||
subject: `CPU test message ${i + 1}`,
|
|
||||||
text: `Testing CPU usage during message ${i + 1}`
|
|
||||||
});
|
|
||||||
|
|
||||||
await client.sendMail(email);
|
|
||||||
|
|
||||||
// Small delay between messages
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 50));
|
|
||||||
}
|
|
||||||
|
|
||||||
const elapsed = Date.now() - startTime;
|
|
||||||
const cpuEnd = process.cpuUsage(cpuStart);
|
|
||||||
const actualElapsed = Math.max(elapsed, 100);
|
|
||||||
const cpuPercent = Math.min(((cpuEnd.user + cpuEnd.system) / 1000) / actualElapsed * 100, 100);
|
|
||||||
|
|
||||||
await client.close();
|
|
||||||
|
|
||||||
console.log(`CPU usage for ${messageCount} messages:`);
|
|
||||||
console.log(` Total time: ${actualElapsed}ms`);
|
|
||||||
console.log(` CPU time: ${(cpuEnd.user + cpuEnd.system) / 1000}ms`);
|
|
||||||
console.log(` CPU usage: ${cpuPercent.toFixed(2)}%`);
|
|
||||||
console.log(` Messages per second: ${(messageCount / (actualElapsed / 1000)).toFixed(2)}`);
|
|
||||||
console.log(` CPU per message: ${(cpuPercent / messageCount).toFixed(2)}%`);
|
|
||||||
|
|
||||||
// CPU usage should be efficient (relaxed for test environment)
|
|
||||||
expect(cpuPercent).toBeLessThan(100);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CPERF-04: CPU usage with parallel operations', async (tools) => {
|
|
||||||
tools.timeout(30000);
|
|
||||||
|
|
||||||
console.log('\nTesting CPU usage with parallel operations...');
|
|
||||||
|
|
||||||
// Create multiple clients for parallel operations
|
|
||||||
const clientCount = 2; // Reduced
|
|
||||||
const messagesPerClient = 3; // Reduced
|
|
||||||
|
|
||||||
const clients = [];
|
|
||||||
for (let i = 0; i < clientCount; i++) {
|
|
||||||
clients.push(await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
debug: false
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Measure CPU during parallel operations
|
|
||||||
const cpuStart = process.cpuUsage();
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
const promises = [];
|
|
||||||
for (let clientIndex = 0; clientIndex < clientCount; clientIndex++) {
|
|
||||||
for (let msgIndex = 0; msgIndex < messagesPerClient; msgIndex++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: [`recipient${clientIndex}-${msgIndex}@example.com`],
|
|
||||||
subject: `Parallel CPU test ${clientIndex}-${msgIndex}`,
|
|
||||||
text: 'Testing CPU with parallel operations'
|
|
||||||
});
|
|
||||||
|
|
||||||
promises.push(clients[clientIndex].sendMail(email));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(promises);
|
|
||||||
|
|
||||||
const elapsed = Date.now() - startTime;
|
|
||||||
const cpuEnd = process.cpuUsage(cpuStart);
|
|
||||||
const actualElapsed = Math.max(elapsed, 100);
|
|
||||||
const cpuPercent = Math.min(((cpuEnd.user + cpuEnd.system) / 1000) / actualElapsed * 100, 100);
|
|
||||||
|
|
||||||
// Close all clients
|
|
||||||
await Promise.all(clients.map(client => client.close()));
|
|
||||||
|
|
||||||
const totalMessages = clientCount * messagesPerClient;
|
|
||||||
console.log(`CPU usage for ${totalMessages} messages across ${clientCount} clients:`);
|
|
||||||
console.log(` Total time: ${actualElapsed}ms`);
|
|
||||||
console.log(` CPU time: ${(cpuEnd.user + cpuEnd.system) / 1000}ms`);
|
|
||||||
console.log(` CPU usage: ${cpuPercent.toFixed(2)}%`);
|
|
||||||
|
|
||||||
// Parallel operations should complete successfully
|
|
||||||
expect(cpuPercent).toBeLessThan(100);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CPERF-04: CPU usage with large messages', async (tools) => {
|
|
||||||
tools.timeout(30000);
|
|
||||||
|
|
||||||
console.log('\nTesting CPU usage with large messages...');
|
|
||||||
|
|
||||||
const client = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
debug: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const messageSizes = [
|
|
||||||
{ name: 'small', size: 1024 }, // 1KB
|
|
||||||
{ name: 'medium', size: 10240 }, // 10KB
|
|
||||||
{ name: 'large', size: 51200 } // 50KB (reduced from 100KB)
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const { name, size } of messageSizes) {
|
|
||||||
const cpuStart = process.cpuUsage();
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: `Large message test (${name})`,
|
|
||||||
text: 'x'.repeat(size)
|
|
||||||
});
|
|
||||||
|
|
||||||
await client.sendMail(email);
|
|
||||||
|
|
||||||
const elapsed = Date.now() - startTime;
|
|
||||||
const cpuEnd = process.cpuUsage(cpuStart);
|
|
||||||
const actualElapsed = Math.max(elapsed, 1);
|
|
||||||
const cpuPercent = Math.min(((cpuEnd.user + cpuEnd.system) / 1000) / actualElapsed * 100, 100);
|
|
||||||
|
|
||||||
console.log(`CPU usage for ${name} message (${size} bytes):`);
|
|
||||||
console.log(` Time: ${actualElapsed}ms`);
|
|
||||||
console.log(` CPU: ${cpuPercent.toFixed(2)}%`);
|
|
||||||
console.log(` Throughput: ${(size / 1024 / (actualElapsed / 1000)).toFixed(2)} KB/s`);
|
|
||||||
|
|
||||||
// Small delay between messages
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
}
|
|
||||||
|
|
||||||
await client.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CPERF-04: CPU usage with connection pooling', async (tools) => {
|
|
||||||
tools.timeout(30000);
|
|
||||||
|
|
||||||
console.log('\nTesting CPU usage with connection pooling...');
|
|
||||||
|
|
||||||
const pooledClient = await createPooledSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
maxConnections: 2, // Reduced
|
|
||||||
debug: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const messageCount = 8; // Reduced
|
|
||||||
|
|
||||||
// Measure CPU with pooling
|
|
||||||
const cpuStart = process.cpuUsage();
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
const promises = [];
|
|
||||||
for (let i = 0; i < messageCount; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: [`recipient${i}@example.com`],
|
|
||||||
subject: `Pooled CPU test ${i + 1}`,
|
|
||||||
text: 'Testing CPU usage with connection pooling'
|
|
||||||
});
|
|
||||||
|
|
||||||
promises.push(pooledClient.sendMail(email));
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(promises);
|
|
||||||
|
|
||||||
const elapsed = Date.now() - startTime;
|
|
||||||
const cpuEnd = process.cpuUsage(cpuStart);
|
|
||||||
const actualElapsed = Math.max(elapsed, 100);
|
|
||||||
const cpuPercent = Math.min(((cpuEnd.user + cpuEnd.system) / 1000) / actualElapsed * 100, 100);
|
|
||||||
|
|
||||||
await pooledClient.close();
|
|
||||||
|
|
||||||
console.log(`CPU usage for ${messageCount} messages with pooling:`);
|
|
||||||
console.log(` Total time: ${actualElapsed}ms`);
|
|
||||||
console.log(` CPU time: ${(cpuEnd.user + cpuEnd.system) / 1000}ms`);
|
|
||||||
console.log(` CPU usage: ${cpuPercent.toFixed(2)}%`);
|
|
||||||
|
|
||||||
// Pooling should complete successfully
|
|
||||||
expect(cpuPercent).toBeLessThan(100);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CPERF-04: CPU profile over time', async (tools) => {
|
|
||||||
tools.timeout(30000);
|
|
||||||
|
|
||||||
console.log('\nTesting CPU profile over time...');
|
|
||||||
|
|
||||||
const client = await createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
debug: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const duration = 8000; // 8 seconds (reduced)
|
|
||||||
const interval = 2000; // Sample every 2 seconds
|
|
||||||
const samples = [];
|
|
||||||
|
|
||||||
const endTime = Date.now() + duration;
|
|
||||||
let emailsSent = 0;
|
|
||||||
|
|
||||||
while (Date.now() < endTime) {
|
|
||||||
const sampleStart = Date.now();
|
|
||||||
const cpuStart = process.cpuUsage();
|
|
||||||
|
|
||||||
// Send some emails
|
|
||||||
for (let i = 0; i < 2; i++) { // Reduced from 3
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: `CPU profile test ${++emailsSent}`,
|
|
||||||
text: `Testing CPU profile at ${new Date().toISOString()}`
|
|
||||||
});
|
|
||||||
|
|
||||||
await client.sendMail(email);
|
|
||||||
|
|
||||||
// Small delay between emails
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
}
|
|
||||||
|
|
||||||
const sampleElapsed = Date.now() - sampleStart;
|
|
||||||
const cpuEnd = process.cpuUsage(cpuStart);
|
|
||||||
const actualElapsed = Math.max(sampleElapsed, 100);
|
|
||||||
const cpuPercent = Math.min(((cpuEnd.user + cpuEnd.system) / 1000) / actualElapsed * 100, 100);
|
|
||||||
|
|
||||||
samples.push({
|
|
||||||
time: Date.now() - (endTime - duration),
|
|
||||||
cpu: cpuPercent,
|
|
||||||
emails: 2
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`[${samples[samples.length - 1].time}ms] CPU: ${cpuPercent.toFixed(2)}%, Emails sent: ${emailsSent}`);
|
|
||||||
|
|
||||||
// Wait for next interval
|
|
||||||
const waitTime = interval - sampleElapsed;
|
|
||||||
if (waitTime > 0 && Date.now() + waitTime < endTime) {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await client.close();
|
|
||||||
|
|
||||||
// Calculate average CPU
|
|
||||||
const avgCpu = samples.reduce((sum, s) => sum + s.cpu, 0) / samples.length;
|
|
||||||
const maxCpu = Math.max(...samples.map(s => s.cpu));
|
|
||||||
const minCpu = Math.min(...samples.map(s => s.cpu));
|
|
||||||
|
|
||||||
console.log(`\nCPU profile summary:`);
|
|
||||||
console.log(` Samples: ${samples.length}`);
|
|
||||||
console.log(` Average CPU: ${avgCpu.toFixed(2)}%`);
|
|
||||||
console.log(` Min CPU: ${minCpu.toFixed(2)}%`);
|
|
||||||
console.log(` Max CPU: ${maxCpu.toFixed(2)}%`);
|
|
||||||
console.log(` Total emails: ${emailsSent}`);
|
|
||||||
|
|
||||||
// CPU should be bounded
|
|
||||||
expect(avgCpu).toBeLessThan(100); // Average CPU less than 100%
|
|
||||||
expect(maxCpu).toBeLessThan(100); // Max CPU less than 100%
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - stop SMTP server', async () => {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
tap.test('setup - start SMTP server for network efficiency tests', async () => {
|
|
||||||
// Just a placeholder to ensure server starts properly
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CPERF-05: network efficiency - connection reuse', async () => {
|
|
||||||
const testServer = await startTestServer({
|
|
||||||
port: 2525,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Testing connection reuse efficiency...');
|
|
||||||
|
|
||||||
// Test 1: Individual connections (2 messages)
|
|
||||||
console.log('Sending 2 messages with individual connections...');
|
|
||||||
const individualStart = Date.now();
|
|
||||||
|
|
||||||
for (let i = 0; i < 2; i++) {
|
|
||||||
const client = createSmtpClient({
|
|
||||||
host: 'localhost',
|
|
||||||
port: 2525,
|
|
||||||
secure: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: [`recipient${i}@example.com`],
|
|
||||||
subject: `Test ${i}`,
|
|
||||||
text: `Message ${i}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await client.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
await client.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
const individualTime = Date.now() - individualStart;
|
|
||||||
console.log(`Individual connections: 2 connections, ${individualTime}ms`);
|
|
||||||
|
|
||||||
// Test 2: Connection reuse (2 messages)
|
|
||||||
console.log('Sending 2 messages with connection reuse...');
|
|
||||||
const reuseStart = Date.now();
|
|
||||||
|
|
||||||
const reuseClient = createSmtpClient({
|
|
||||||
host: 'localhost',
|
|
||||||
port: 2525,
|
|
||||||
secure: false
|
|
||||||
});
|
|
||||||
|
|
||||||
for (let i = 0; i < 2; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: [`reuse${i}@example.com`],
|
|
||||||
subject: `Reuse ${i}`,
|
|
||||||
text: `Message ${i}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await reuseClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
await reuseClient.close();
|
|
||||||
|
|
||||||
const reuseTime = Date.now() - reuseStart;
|
|
||||||
console.log(`Connection reuse: 1 connection, ${reuseTime}ms`);
|
|
||||||
|
|
||||||
// Connection reuse should complete reasonably quickly
|
|
||||||
expect(reuseTime).toBeLessThan(5000); // Less than 5 seconds
|
|
||||||
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CPERF-05: network efficiency - message throughput', async () => {
|
|
||||||
const testServer = await startTestServer({
|
|
||||||
port: 2525,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Testing message throughput...');
|
|
||||||
|
|
||||||
const client = createSmtpClient({
|
|
||||||
host: 'localhost',
|
|
||||||
port: 2525,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 10000,
|
|
||||||
socketTimeout: 10000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test with smaller message sizes to avoid timeout
|
|
||||||
const sizes = [512, 1024]; // 512B, 1KB
|
|
||||||
let totalBytes = 0;
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
for (const size of sizes) {
|
|
||||||
const content = 'x'.repeat(size);
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: `Test ${size} bytes`,
|
|
||||||
text: content,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await client.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
totalBytes += size;
|
|
||||||
}
|
|
||||||
|
|
||||||
const elapsed = Date.now() - startTime;
|
|
||||||
const throughput = (totalBytes / elapsed) * 1000; // bytes per second
|
|
||||||
|
|
||||||
console.log(`Total bytes sent: ${totalBytes}`);
|
|
||||||
console.log(`Time elapsed: ${elapsed}ms`);
|
|
||||||
console.log(`Throughput: ${(throughput / 1024).toFixed(1)} KB/s`);
|
|
||||||
|
|
||||||
// Should achieve reasonable throughput (lowered expectation)
|
|
||||||
expect(throughput).toBeGreaterThan(100); // At least 100 bytes/s
|
|
||||||
|
|
||||||
await client.close();
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CPERF-05: network efficiency - batch sending', async () => {
|
|
||||||
const testServer = await startTestServer({
|
|
||||||
port: 2525,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Testing batch email sending...');
|
|
||||||
|
|
||||||
const client = createSmtpClient({
|
|
||||||
host: 'localhost',
|
|
||||||
port: 2525,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 10000,
|
|
||||||
socketTimeout: 10000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send 3 emails in batch
|
|
||||||
const emails = Array(3).fill(null).map((_, i) =>
|
|
||||||
new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: [`batch${i}@example.com`],
|
|
||||||
subject: `Batch ${i}`,
|
|
||||||
text: `Testing batch sending - message ${i}`,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('Sending 3 emails in batch...');
|
|
||||||
const batchStart = Date.now();
|
|
||||||
|
|
||||||
// Send emails sequentially
|
|
||||||
for (let i = 0; i < emails.length; i++) {
|
|
||||||
const result = await client.sendMail(emails[i]);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log(`Email ${i + 1} sent`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const batchTime = Date.now() - batchStart;
|
|
||||||
|
|
||||||
console.log(`\nBatch complete: 3 emails in ${batchTime}ms`);
|
|
||||||
console.log(`Average time per email: ${(batchTime / 3).toFixed(1)}ms`);
|
|
||||||
|
|
||||||
// Batch should complete reasonably quickly
|
|
||||||
expect(batchTime).toBeLessThan(5000); // Less than 5 seconds total
|
|
||||||
|
|
||||||
await client.close();
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - stop SMTP server', async () => {
|
|
||||||
// Cleanup is handled in individual tests
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.start();
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
tap.test('setup - start SMTP server for caching tests', async () => {
|
|
||||||
// Just a placeholder to ensure server starts properly
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CPERF-06: caching strategies - connection caching', async () => {
|
|
||||||
const testServer = await startTestServer({
|
|
||||||
port: 2525,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Testing connection caching strategies...');
|
|
||||||
|
|
||||||
// Create client for testing connection reuse
|
|
||||||
const client = createSmtpClient({
|
|
||||||
host: 'localhost',
|
|
||||||
port: 2525,
|
|
||||||
secure: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// First batch - establish connections
|
|
||||||
console.log('Sending first batch to establish connections...');
|
|
||||||
const firstBatchStart = Date.now();
|
|
||||||
|
|
||||||
const firstBatch = Array(3).fill(null).map((_, i) =>
|
|
||||||
new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: [`cached${i}@example.com`],
|
|
||||||
subject: `Cache test ${i}`,
|
|
||||||
text: `Testing connection caching - message ${i}`,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Send emails sequentially
|
|
||||||
for (const email of firstBatch) {
|
|
||||||
const result = await client.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstBatchTime = Date.now() - firstBatchStart;
|
|
||||||
|
|
||||||
// Second batch - should reuse connection
|
|
||||||
console.log('Sending second batch using same connection...');
|
|
||||||
const secondBatchStart = Date.now();
|
|
||||||
|
|
||||||
const secondBatch = Array(3).fill(null).map((_, i) =>
|
|
||||||
new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: [`cached2-${i}@example.com`],
|
|
||||||
subject: `Cache test 2-${i}`,
|
|
||||||
text: `Testing cached connections - message ${i}`,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Send emails sequentially
|
|
||||||
for (const email of secondBatch) {
|
|
||||||
const result = await client.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
const secondBatchTime = Date.now() - secondBatchStart;
|
|
||||||
|
|
||||||
console.log(`First batch: ${firstBatchTime}ms`);
|
|
||||||
console.log(`Second batch: ${secondBatchTime}ms`);
|
|
||||||
|
|
||||||
// Both batches should complete successfully
|
|
||||||
expect(firstBatchTime).toBeGreaterThan(0);
|
|
||||||
expect(secondBatchTime).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
await client.close();
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CPERF-06: caching strategies - server capability caching', async () => {
|
|
||||||
const testServer = await startTestServer({
|
|
||||||
port: 2526,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Testing server capability caching...');
|
|
||||||
|
|
||||||
const client = createSmtpClient({
|
|
||||||
host: 'localhost',
|
|
||||||
port: 2526,
|
|
||||||
secure: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// First email - discovers capabilities
|
|
||||||
console.log('First email - discovering server capabilities...');
|
|
||||||
const firstStart = Date.now();
|
|
||||||
|
|
||||||
const email1 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient1@example.com'],
|
|
||||||
subject: 'Capability test 1',
|
|
||||||
text: 'Testing capability discovery',
|
|
||||||
});
|
|
||||||
|
|
||||||
const result1 = await client.sendMail(email1);
|
|
||||||
expect(result1.success).toBeTrue();
|
|
||||||
const firstTime = Date.now() - firstStart;
|
|
||||||
|
|
||||||
// Second email - uses cached capabilities
|
|
||||||
console.log('Second email - using cached capabilities...');
|
|
||||||
const secondStart = Date.now();
|
|
||||||
|
|
||||||
const email2 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient2@example.com'],
|
|
||||||
subject: 'Capability test 2',
|
|
||||||
text: 'Testing cached capabilities',
|
|
||||||
});
|
|
||||||
|
|
||||||
const result2 = await client.sendMail(email2);
|
|
||||||
expect(result2.success).toBeTrue();
|
|
||||||
const secondTime = Date.now() - secondStart;
|
|
||||||
|
|
||||||
console.log(`First email (capability discovery): ${firstTime}ms`);
|
|
||||||
console.log(`Second email (cached capabilities): ${secondTime}ms`);
|
|
||||||
|
|
||||||
// Both should complete quickly
|
|
||||||
expect(firstTime).toBeLessThan(1000);
|
|
||||||
expect(secondTime).toBeLessThan(1000);
|
|
||||||
|
|
||||||
await client.close();
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CPERF-06: caching strategies - message batching', async () => {
|
|
||||||
const testServer = await startTestServer({
|
|
||||||
port: 2527,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Testing message batching for cache efficiency...');
|
|
||||||
|
|
||||||
const client = createSmtpClient({
|
|
||||||
host: 'localhost',
|
|
||||||
port: 2527,
|
|
||||||
secure: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test sending messages in batches
|
|
||||||
const batchSizes = [2, 3, 4];
|
|
||||||
|
|
||||||
for (const batchSize of batchSizes) {
|
|
||||||
console.log(`\nTesting batch size: ${batchSize}`);
|
|
||||||
const batchStart = Date.now();
|
|
||||||
|
|
||||||
const emails = Array(batchSize).fill(null).map((_, i) =>
|
|
||||||
new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: [`batch${batchSize}-${i}@example.com`],
|
|
||||||
subject: `Batch ${batchSize} message ${i}`,
|
|
||||||
text: `Testing batching strategies - batch size ${batchSize}`,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Send emails sequentially
|
|
||||||
for (const email of emails) {
|
|
||||||
const result = await client.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
const batchTime = Date.now() - batchStart;
|
|
||||||
const avgTime = batchTime / batchSize;
|
|
||||||
|
|
||||||
console.log(` Batch completed in ${batchTime}ms`);
|
|
||||||
console.log(` Average time per message: ${avgTime.toFixed(1)}ms`);
|
|
||||||
|
|
||||||
// All batches should complete efficiently
|
|
||||||
expect(avgTime).toBeLessThan(1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
await client.close();
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - stop SMTP server', async () => {
|
|
||||||
// Cleanup is handled in individual tests
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.start();
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
tap.test('setup - start SMTP server for queue management tests', async () => {
|
|
||||||
// Just a placeholder to ensure server starts properly
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CPERF-07: queue management - basic queue processing', async () => {
|
|
||||||
const testServer = await startTestServer({
|
|
||||||
port: 2525,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Testing basic queue processing...');
|
|
||||||
|
|
||||||
const client = createSmtpClient({
|
|
||||||
host: 'localhost',
|
|
||||||
port: 2525,
|
|
||||||
secure: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Queue up 5 emails (reduced from 10)
|
|
||||||
const emailCount = 5;
|
|
||||||
const emails = Array(emailCount).fill(null).map((_, i) =>
|
|
||||||
new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: [`queue${i}@example.com`],
|
|
||||||
subject: `Queue test ${i}`,
|
|
||||||
text: `Testing queue management - message ${i}`,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`Sending ${emailCount} emails...`);
|
|
||||||
const queueStart = Date.now();
|
|
||||||
|
|
||||||
// Send all emails sequentially
|
|
||||||
const results = [];
|
|
||||||
for (let i = 0; i < emails.length; i++) {
|
|
||||||
const result = await client.sendMail(emails[i]);
|
|
||||||
console.log(` Email ${i} sent`);
|
|
||||||
results.push(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
const queueTime = Date.now() - queueStart;
|
|
||||||
|
|
||||||
// Verify all succeeded
|
|
||||||
results.forEach((result, index) => {
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`All ${emailCount} emails processed in ${queueTime}ms`);
|
|
||||||
console.log(`Average time per email: ${(queueTime / emailCount).toFixed(1)}ms`);
|
|
||||||
|
|
||||||
// Should complete within reasonable time
|
|
||||||
expect(queueTime).toBeLessThan(10000); // Less than 10 seconds for 5 emails
|
|
||||||
|
|
||||||
await client.close();
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CPERF-07: queue management - queue with rate limiting', async () => {
|
|
||||||
const testServer = await startTestServer({
|
|
||||||
port: 2526,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Testing queue with rate limiting...');
|
|
||||||
|
|
||||||
const client = createSmtpClient({
|
|
||||||
host: 'localhost',
|
|
||||||
port: 2526,
|
|
||||||
secure: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send 5 emails sequentially (simulating rate limiting)
|
|
||||||
const emailCount = 5;
|
|
||||||
const rateLimitDelay = 200; // 200ms between emails
|
|
||||||
|
|
||||||
console.log(`Sending ${emailCount} emails with ${rateLimitDelay}ms rate limit...`);
|
|
||||||
const rateStart = Date.now();
|
|
||||||
|
|
||||||
for (let i = 0; i < emailCount; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: [`ratelimit${i}@example.com`],
|
|
||||||
subject: `Rate limit test ${i}`,
|
|
||||||
text: `Testing rate limited queue - message ${i}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await client.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
|
|
||||||
console.log(` Email ${i} sent`);
|
|
||||||
|
|
||||||
// Simulate rate limiting delay
|
|
||||||
if (i < emailCount - 1) {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, rateLimitDelay));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const rateTime = Date.now() - rateStart;
|
|
||||||
const expectedMinTime = (emailCount - 1) * rateLimitDelay;
|
|
||||||
|
|
||||||
console.log(`Rate limited emails sent in ${rateTime}ms`);
|
|
||||||
console.log(`Expected minimum time: ${expectedMinTime}ms`);
|
|
||||||
|
|
||||||
// Should respect rate limiting
|
|
||||||
expect(rateTime).toBeGreaterThanOrEqual(expectedMinTime);
|
|
||||||
|
|
||||||
await client.close();
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CPERF-07: queue management - sequential processing', async () => {
|
|
||||||
const testServer = await startTestServer({
|
|
||||||
port: 2527,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Testing sequential email processing...');
|
|
||||||
|
|
||||||
const client = createSmtpClient({
|
|
||||||
host: 'localhost',
|
|
||||||
port: 2527,
|
|
||||||
secure: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send multiple emails sequentially
|
|
||||||
const emails = Array(3).fill(null).map((_, i) =>
|
|
||||||
new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: [`sequential${i}@example.com`],
|
|
||||||
subject: `Sequential test ${i}`,
|
|
||||||
text: `Testing sequential processing - message ${i}`,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('Sending 3 emails sequentially...');
|
|
||||||
const sequentialStart = Date.now();
|
|
||||||
|
|
||||||
const results = [];
|
|
||||||
for (const email of emails) {
|
|
||||||
const result = await client.sendMail(email);
|
|
||||||
results.push(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sequentialTime = Date.now() - sequentialStart;
|
|
||||||
|
|
||||||
// All should succeed
|
|
||||||
results.forEach((result, index) => {
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log(` Email ${index} processed`);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Sequential processing completed in ${sequentialTime}ms`);
|
|
||||||
console.log(`Average time per email: ${(sequentialTime / 3).toFixed(1)}ms`);
|
|
||||||
|
|
||||||
await client.close();
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - stop SMTP server', async () => {
|
|
||||||
// Cleanup is handled in individual tests
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.start();
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { createTestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createTestSmtpClient } from '../../helpers/smtp.client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
tap.test('CPERF-08: DNS Caching Tests', async () => {
|
|
||||||
console.log('\n🌐 Testing SMTP Client DNS Caching');
|
|
||||||
console.log('=' .repeat(60));
|
|
||||||
|
|
||||||
const testServer = await createTestServer({});
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log('\nTest: DNS caching with multiple connections');
|
|
||||||
|
|
||||||
// Create multiple clients to test DNS caching
|
|
||||||
const clients = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port
|
|
||||||
});
|
|
||||||
clients.push(smtpClient);
|
|
||||||
console.log(` ✓ Client ${i + 1} created (DNS should be cached)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send email with first client
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'DNS Caching Test',
|
|
||||||
text: 'Testing DNS caching efficiency'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await clients[0].sendMail(email);
|
|
||||||
console.log(' ✓ Email sent successfully');
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
|
|
||||||
// Clean up all clients
|
|
||||||
clients.forEach(client => client.close());
|
|
||||||
console.log(' ✓ All clients closed');
|
|
||||||
|
|
||||||
console.log('\n✅ CPERF-08: DNS caching tests completed');
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
testServer.server.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.start();
|
|
||||||
@@ -1,305 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup test SMTP server', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2600,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
expect(testServer).toBeTruthy();
|
|
||||||
expect(testServer.port).toEqual(2600);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CREL-01: Basic reconnection after close', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// First verify connection works
|
|
||||||
const result1 = await smtpClient.verify();
|
|
||||||
expect(result1).toBeTrue();
|
|
||||||
console.log('Initial connection verified');
|
|
||||||
|
|
||||||
// Close connection
|
|
||||||
await smtpClient.close();
|
|
||||||
console.log('Connection closed');
|
|
||||||
|
|
||||||
// Verify again - should reconnect automatically
|
|
||||||
const result2 = await smtpClient.verify();
|
|
||||||
expect(result2).toBeTrue();
|
|
||||||
console.log('Reconnection successful');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CREL-01: Multiple sequential connections', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send multiple emails with closes in between
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: `Sequential Test ${i + 1}`,
|
|
||||||
text: 'Testing sequential connections'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log(`Email ${i + 1} sent successfully`);
|
|
||||||
|
|
||||||
// Close connection after each send
|
|
||||||
await smtpClient.close();
|
|
||||||
console.log(`Connection closed after email ${i + 1}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CREL-01: Recovery from server restart', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send first email
|
|
||||||
const email1 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Before Server Restart',
|
|
||||||
text: 'Testing server restart recovery'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result1 = await smtpClient.sendMail(email1);
|
|
||||||
expect(result1.success).toBeTrue();
|
|
||||||
console.log('First email sent successfully');
|
|
||||||
|
|
||||||
// Simulate server restart by creating a brief interruption
|
|
||||||
console.log('Simulating server restart...');
|
|
||||||
|
|
||||||
// The SMTP client should handle the disconnection gracefully
|
|
||||||
// and reconnect for the next operation
|
|
||||||
|
|
||||||
// Wait a moment
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
|
|
||||||
// Try to send another email
|
|
||||||
const email2 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'After Server Restart',
|
|
||||||
text: 'Testing recovery after restart'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result2 = await smtpClient.sendMail(email2);
|
|
||||||
expect(result2.success).toBeTrue();
|
|
||||||
console.log('Second email sent successfully after simulated restart');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CREL-01: Connection pool reliability', async () => {
|
|
||||||
const pooledClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
pool: true,
|
|
||||||
maxConnections: 3,
|
|
||||||
maxMessages: 10,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send multiple emails concurrently
|
|
||||||
const emails = Array.from({ length: 10 }, (_, i) => new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: [`recipient${i}@example.com`],
|
|
||||||
subject: `Pool Test ${i}`,
|
|
||||||
text: 'Testing connection pool'
|
|
||||||
}));
|
|
||||||
|
|
||||||
console.log('Sending 10 emails through connection pool...');
|
|
||||||
|
|
||||||
const results = await Promise.allSettled(
|
|
||||||
emails.map(email => pooledClient.sendMail(email))
|
|
||||||
);
|
|
||||||
|
|
||||||
const successful = results.filter(r => r.status === 'fulfilled').length;
|
|
||||||
const failed = results.filter(r => r.status === 'rejected').length;
|
|
||||||
|
|
||||||
console.log(`Pool results: ${successful} successful, ${failed} failed`);
|
|
||||||
expect(successful).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
// Most should succeed
|
|
||||||
expect(successful).toBeGreaterThanOrEqual(8);
|
|
||||||
|
|
||||||
await pooledClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CREL-01: Rapid connection cycling', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Rapidly open and close connections
|
|
||||||
console.log('Testing rapid connection cycling...');
|
|
||||||
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
const result = await smtpClient.verify();
|
|
||||||
expect(result).toBeTrue();
|
|
||||||
await smtpClient.close();
|
|
||||||
console.log(`Cycle ${i + 1} completed`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Rapid cycling completed successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CREL-01: Error recovery', async () => {
|
|
||||||
// Test with invalid server first
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: 'invalid.host.local',
|
|
||||||
port: 9999,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 1000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// First attempt should fail
|
|
||||||
const result1 = await smtpClient.verify();
|
|
||||||
expect(result1).toBeFalse();
|
|
||||||
console.log('Connection to invalid host failed as expected');
|
|
||||||
|
|
||||||
// Now update to valid server (simulating failover)
|
|
||||||
// Since we can't update options, create a new client
|
|
||||||
const recoveredClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should connect successfully
|
|
||||||
const result2 = await recoveredClient.verify();
|
|
||||||
expect(result2).toBeTrue();
|
|
||||||
console.log('Connection to valid host succeeded');
|
|
||||||
|
|
||||||
// Send email to verify full functionality
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Recovery Test',
|
|
||||||
text: 'Testing error recovery'
|
|
||||||
});
|
|
||||||
|
|
||||||
const sendResult = await recoveredClient.sendMail(email);
|
|
||||||
expect(sendResult.success).toBeTrue();
|
|
||||||
console.log('Email sent successfully after recovery');
|
|
||||||
|
|
||||||
await recoveredClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CREL-01: Long-lived connection', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 30000, // 30 second timeout
|
|
||||||
socketTimeout: 30000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Testing long-lived connection...');
|
|
||||||
|
|
||||||
// Send emails over time
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: `Long-lived Test ${i + 1}`,
|
|
||||||
text: `Email ${i + 1} over long-lived connection`
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log(`Email ${i + 1} sent at ${new Date().toISOString()}`);
|
|
||||||
|
|
||||||
// Wait between sends
|
|
||||||
if (i < 2) {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Long-lived connection test completed');
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CREL-01: Concurrent operations', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
pool: true,
|
|
||||||
maxConnections: 5,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Testing concurrent operations...');
|
|
||||||
|
|
||||||
// Mix verify and send operations
|
|
||||||
const operations = [
|
|
||||||
smtpClient.verify(),
|
|
||||||
smtpClient.sendMail(new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient1@example.com'],
|
|
||||||
subject: 'Concurrent 1',
|
|
||||||
text: 'First concurrent email'
|
|
||||||
})),
|
|
||||||
smtpClient.verify(),
|
|
||||||
smtpClient.sendMail(new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient2@example.com'],
|
|
||||||
subject: 'Concurrent 2',
|
|
||||||
text: 'Second concurrent email'
|
|
||||||
})),
|
|
||||||
smtpClient.verify()
|
|
||||||
];
|
|
||||||
|
|
||||||
const results = await Promise.allSettled(operations);
|
|
||||||
|
|
||||||
const successful = results.filter(r => r.status === 'fulfilled').length;
|
|
||||||
console.log(`Concurrent operations: ${successful}/${results.length} successful`);
|
|
||||||
|
|
||||||
expect(successful).toEqual(results.length);
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup test SMTP server', async () => {
|
|
||||||
if (testServer) {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
import * as net from 'net';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup test SMTP server', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2601,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
expect(testServer).toBeTruthy();
|
|
||||||
expect(testServer.port).toEqual(2601);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CREL-02: Handle network interruption during verification', async () => {
|
|
||||||
// Create a server that drops connections mid-session
|
|
||||||
const interruptServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 Interrupt Test Server\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
console.log(`Server received: ${command}`);
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
// Start sending multi-line response then drop
|
|
||||||
socket.write('250-test.server\r\n');
|
|
||||||
socket.write('250-PIPELINING\r\n');
|
|
||||||
|
|
||||||
// Simulate network interruption
|
|
||||||
setTimeout(() => {
|
|
||||||
console.log('Simulating network interruption...');
|
|
||||||
socket.destroy();
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
interruptServer.listen(2602, () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: 2602,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 2000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should handle the interruption gracefully
|
|
||||||
const result = await smtpClient.verify();
|
|
||||||
expect(result).toBeFalse();
|
|
||||||
console.log('✅ Handled network interruption during verification');
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
interruptServer.close(() => resolve());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CREL-02: Recovery after brief network glitch', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send email successfully
|
|
||||||
const email1 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Before Glitch',
|
|
||||||
text: 'First email before network glitch'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result1 = await smtpClient.sendMail(email1);
|
|
||||||
expect(result1.success).toBeTrue();
|
|
||||||
console.log('First email sent successfully');
|
|
||||||
|
|
||||||
// Close to simulate brief network issue
|
|
||||||
await smtpClient.close();
|
|
||||||
console.log('Simulating brief network glitch...');
|
|
||||||
|
|
||||||
// Wait a moment
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
|
||||||
|
|
||||||
// Try to send another email - should reconnect automatically
|
|
||||||
const email2 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'After Glitch',
|
|
||||||
text: 'Second email after network recovery'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result2 = await smtpClient.sendMail(email2);
|
|
||||||
expect(result2.success).toBeTrue();
|
|
||||||
console.log('✅ Recovered from network glitch successfully');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CREL-02: Handle server becoming unresponsive', async () => {
|
|
||||||
// Create a server that stops responding
|
|
||||||
const unresponsiveServer = net.createServer((socket) => {
|
|
||||||
socket.write('220 Unresponsive Server\r\n');
|
|
||||||
let commandCount = 0;
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
commandCount++;
|
|
||||||
console.log(`Command ${commandCount}: ${command}`);
|
|
||||||
|
|
||||||
// Stop responding after first command
|
|
||||||
if (commandCount === 1 && command.startsWith('EHLO')) {
|
|
||||||
console.log('Server becoming unresponsive...');
|
|
||||||
// Don't send any response - simulate hung server
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Don't close the socket, just stop responding
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
unresponsiveServer.listen(2604, () => resolve());
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: 2604,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 2000, // Short timeout to detect unresponsiveness
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should timeout when server doesn't respond
|
|
||||||
const result = await smtpClient.verify();
|
|
||||||
expect(result).toBeFalse();
|
|
||||||
console.log('✅ Detected unresponsive server');
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
unresponsiveServer.close(() => resolve());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CREL-02: Handle large email successfully', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 10000,
|
|
||||||
socketTimeout: 10000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create a large email
|
|
||||||
const largeText = 'x'.repeat(10000); // 10KB of text
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Large Email Test',
|
|
||||||
text: largeText
|
|
||||||
});
|
|
||||||
|
|
||||||
// Should complete successfully despite size
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ Large email sent successfully');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CREL-02: Rapid reconnection after interruption', async () => {
|
|
||||||
const smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Rapid cycle of verify, close, verify
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const result = await smtpClient.verify();
|
|
||||||
expect(result).toBeTrue();
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
console.log(`Rapid cycle ${i + 1} completed`);
|
|
||||||
|
|
||||||
// Very short delay
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 50));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✅ Rapid reconnection handled successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup test SMTP server', async () => {
|
|
||||||
if (testServer) {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,469 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import * as net from 'net';
|
|
||||||
import { createTestSmtpClient } from '../../helpers/smtp.client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let messageCount = 0;
|
|
||||||
let processedMessages: string[] = [];
|
|
||||||
|
|
||||||
tap.test('CREL-03: Basic Email Persistence Through Client Lifecycle', async () => {
|
|
||||||
console.log('\n💾 Testing SMTP Client Queue Persistence Reliability');
|
|
||||||
console.log('=' .repeat(60));
|
|
||||||
console.log('\n🔄 Testing email handling through client lifecycle...');
|
|
||||||
|
|
||||||
messageCount = 0;
|
|
||||||
processedMessages = [];
|
|
||||||
|
|
||||||
// Create test server
|
|
||||||
const server = net.createServer(socket => {
|
|
||||||
socket.write('220 localhost SMTP Test Server\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
|
||||||
socket.write('250-localhost\r\n');
|
|
||||||
socket.write('250-SIZE 10485760\r\n');
|
|
||||||
socket.write('250 AUTH PLAIN LOGIN\r\n');
|
|
||||||
} else if (line.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line === 'DATA') {
|
|
||||||
socket.write('354 Send data\r\n');
|
|
||||||
} else if (line === '.') {
|
|
||||||
messageCount++;
|
|
||||||
socket.write(`250 OK Message ${messageCount} accepted\r\n`);
|
|
||||||
console.log(` [Server] Processed message ${messageCount}`);
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
server.listen(0, '127.0.0.1', () => {
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const port = (server.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log(' Phase 1: Creating first client instance...');
|
|
||||||
const smtpClient1 = createTestSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: port,
|
|
||||||
secure: false,
|
|
||||||
maxConnections: 2,
|
|
||||||
maxMessages: 10
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(' Creating emails for persistence test...');
|
|
||||||
const emails = [];
|
|
||||||
for (let i = 0; i < 6; i++) {
|
|
||||||
emails.push(new Email({
|
|
||||||
from: 'sender@persistence.test',
|
|
||||||
to: [`recipient${i}@persistence.test`],
|
|
||||||
subject: `Persistence Test Email ${i + 1}`,
|
|
||||||
text: `Testing queue persistence, email ${i + 1}`
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(' Sending emails to test persistence...');
|
|
||||||
const sendPromises = emails.map((email, index) => {
|
|
||||||
return smtpClient1.sendMail(email).then(result => {
|
|
||||||
console.log(` 📤 Email ${index + 1} sent successfully`);
|
|
||||||
processedMessages.push(`email-${index + 1}`);
|
|
||||||
return { success: true, result, index };
|
|
||||||
}).catch(error => {
|
|
||||||
console.log(` ❌ Email ${index + 1} failed: ${error.message}`);
|
|
||||||
return { success: false, error, index };
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for emails to be processed
|
|
||||||
const results = await Promise.allSettled(sendPromises);
|
|
||||||
|
|
||||||
// Wait a bit for all messages to be processed by the server
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
|
||||||
|
|
||||||
console.log(' Phase 2: Verifying results...');
|
|
||||||
const successful = results.filter(r => r.status === 'fulfilled' && r.value.success).length;
|
|
||||||
console.log(` Total messages processed by server: ${messageCount}`);
|
|
||||||
console.log(` Successful sends: ${successful}/${emails.length}`);
|
|
||||||
|
|
||||||
// With connection pooling, not all messages may be immediately processed
|
|
||||||
expect(messageCount).toBeGreaterThanOrEqual(1);
|
|
||||||
expect(successful).toEqual(emails.length);
|
|
||||||
|
|
||||||
smtpClient1.close();
|
|
||||||
|
|
||||||
// Wait for connections to close
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 200));
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
server.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CREL-03: Email Recovery After Connection Failure', async () => {
|
|
||||||
console.log('\n🛠️ Testing email recovery after connection failure...');
|
|
||||||
|
|
||||||
let connectionCount = 0;
|
|
||||||
let shouldReject = false;
|
|
||||||
|
|
||||||
// Create test server that can simulate failures
|
|
||||||
const server = net.createServer(socket => {
|
|
||||||
connectionCount++;
|
|
||||||
|
|
||||||
if (shouldReject) {
|
|
||||||
socket.destroy();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.write('220 localhost SMTP Test Server\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
|
||||||
socket.write('250-localhost\r\n');
|
|
||||||
socket.write('250 SIZE 10485760\r\n');
|
|
||||||
} else if (line.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line === 'DATA') {
|
|
||||||
socket.write('354 Send data\r\n');
|
|
||||||
} else if (line === '.') {
|
|
||||||
socket.write('250 OK Message accepted\r\n');
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
server.listen(0, '127.0.0.1', () => {
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const port = (server.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log(' Testing client behavior with connection failures...');
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 2000,
|
|
||||||
maxConnections: 1
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@recovery.test',
|
|
||||||
to: ['recipient@recovery.test'],
|
|
||||||
subject: 'Recovery Test',
|
|
||||||
text: 'Testing recovery from connection failure'
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(' Sending email with potential connection issues...');
|
|
||||||
|
|
||||||
// First attempt should succeed
|
|
||||||
try {
|
|
||||||
await smtpClient.sendMail(email);
|
|
||||||
console.log(' ✓ First email sent successfully');
|
|
||||||
} catch (error) {
|
|
||||||
console.log(' ✗ First email failed unexpectedly');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simulate connection issues
|
|
||||||
shouldReject = true;
|
|
||||||
console.log(' Simulating connection failure...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await smtpClient.sendMail(email);
|
|
||||||
console.log(' ✗ Email sent when it should have failed');
|
|
||||||
} catch (error) {
|
|
||||||
console.log(' ✓ Email failed as expected during connection issue');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore connection
|
|
||||||
shouldReject = false;
|
|
||||||
console.log(' Connection restored, attempting recovery...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await smtpClient.sendMail(email);
|
|
||||||
console.log(' ✓ Email sent successfully after recovery');
|
|
||||||
} catch (error) {
|
|
||||||
console.log(' ✗ Email failed after recovery');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(` Total connection attempts: ${connectionCount}`);
|
|
||||||
expect(connectionCount).toBeGreaterThanOrEqual(2);
|
|
||||||
|
|
||||||
smtpClient.close();
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
server.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CREL-03: Concurrent Email Handling', async () => {
|
|
||||||
console.log('\n🔒 Testing concurrent email handling...');
|
|
||||||
|
|
||||||
let processedEmails = 0;
|
|
||||||
|
|
||||||
// Create test server
|
|
||||||
const server = net.createServer(socket => {
|
|
||||||
socket.write('220 localhost SMTP Test Server\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
|
||||||
socket.write('250-localhost\r\n');
|
|
||||||
socket.write('250 SIZE 10485760\r\n');
|
|
||||||
} else if (line.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line === 'DATA') {
|
|
||||||
socket.write('354 Send data\r\n');
|
|
||||||
} else if (line === '.') {
|
|
||||||
processedEmails++;
|
|
||||||
socket.write('250 OK Message accepted\r\n');
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
server.listen(0, '127.0.0.1', () => {
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const port = (server.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log(' Creating multiple clients for concurrent access...');
|
|
||||||
|
|
||||||
const clients = [];
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
clients.push(createTestSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: port,
|
|
||||||
secure: false,
|
|
||||||
maxConnections: 2
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(' Creating emails for concurrent test...');
|
|
||||||
const allEmails = [];
|
|
||||||
for (let clientIndex = 0; clientIndex < clients.length; clientIndex++) {
|
|
||||||
for (let emailIndex = 0; emailIndex < 4; emailIndex++) {
|
|
||||||
allEmails.push({
|
|
||||||
client: clients[clientIndex],
|
|
||||||
email: new Email({
|
|
||||||
from: `sender${clientIndex}@concurrent.test`,
|
|
||||||
to: [`recipient${clientIndex}-${emailIndex}@concurrent.test`],
|
|
||||||
subject: `Concurrent Test Client ${clientIndex + 1} Email ${emailIndex + 1}`,
|
|
||||||
text: `Testing concurrent access from client ${clientIndex + 1}`
|
|
||||||
}),
|
|
||||||
clientId: clientIndex,
|
|
||||||
emailId: emailIndex
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(' Sending emails concurrently from multiple clients...');
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
const promises = allEmails.map(({ client, email, clientId, emailId }) => {
|
|
||||||
return client.sendMail(email).then(result => {
|
|
||||||
console.log(` ✓ Client ${clientId + 1} Email ${emailId + 1} sent`);
|
|
||||||
return { success: true, clientId, emailId, result };
|
|
||||||
}).catch(error => {
|
|
||||||
console.log(` ✗ Client ${clientId + 1} Email ${emailId + 1} failed: ${error.message}`);
|
|
||||||
return { success: false, clientId, emailId, error };
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const results = await Promise.all(promises);
|
|
||||||
const endTime = Date.now();
|
|
||||||
|
|
||||||
const successful = results.filter(r => r.success).length;
|
|
||||||
const failed = results.filter(r => !r.success).length;
|
|
||||||
|
|
||||||
console.log(` Concurrent operations completed in ${endTime - startTime}ms`);
|
|
||||||
console.log(` Total emails: ${allEmails.length}`);
|
|
||||||
console.log(` Successful: ${successful}, Failed: ${failed}`);
|
|
||||||
console.log(` Emails processed by server: ${processedEmails}`);
|
|
||||||
console.log(` Success rate: ${((successful / allEmails.length) * 100).toFixed(1)}%`);
|
|
||||||
|
|
||||||
expect(successful).toBeGreaterThanOrEqual(allEmails.length - 2);
|
|
||||||
|
|
||||||
// Close all clients
|
|
||||||
for (const client of clients) {
|
|
||||||
client.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
server.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CREL-03: Email Integrity During High Load', async () => {
|
|
||||||
console.log('\n🔍 Testing email integrity during high load...');
|
|
||||||
|
|
||||||
const receivedSubjects = new Set<string>();
|
|
||||||
|
|
||||||
// Create test server
|
|
||||||
const server = net.createServer(socket => {
|
|
||||||
socket.write('220 localhost SMTP Test Server\r\n');
|
|
||||||
let inData = false;
|
|
||||||
let currentData = '';
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (inData) {
|
|
||||||
if (line === '.') {
|
|
||||||
// Extract subject from email data
|
|
||||||
const subjectMatch = currentData.match(/Subject: (.+)/);
|
|
||||||
if (subjectMatch) {
|
|
||||||
receivedSubjects.add(subjectMatch[1]);
|
|
||||||
}
|
|
||||||
socket.write('250 OK Message accepted\r\n');
|
|
||||||
inData = false;
|
|
||||||
currentData = '';
|
|
||||||
} else {
|
|
||||||
if (line.trim() !== '') {
|
|
||||||
currentData += line + '\r\n';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
|
||||||
socket.write('250-localhost\r\n');
|
|
||||||
socket.write('250 SIZE 10485760\r\n');
|
|
||||||
} else if (line.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line === 'DATA') {
|
|
||||||
socket.write('354 Send data\r\n');
|
|
||||||
inData = true;
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
server.listen(0, '127.0.0.1', () => {
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const port = (server.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log(' Creating client for high load test...');
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: port,
|
|
||||||
secure: false,
|
|
||||||
maxConnections: 5,
|
|
||||||
maxMessages: 100
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(' Creating test emails with various content types...');
|
|
||||||
const emails = [
|
|
||||||
new Email({
|
|
||||||
from: 'sender@integrity.test',
|
|
||||||
to: ['recipient1@integrity.test'],
|
|
||||||
subject: 'Integrity Test - Plain Text',
|
|
||||||
text: 'Plain text email for integrity testing'
|
|
||||||
}),
|
|
||||||
new Email({
|
|
||||||
from: 'sender@integrity.test',
|
|
||||||
to: ['recipient2@integrity.test'],
|
|
||||||
subject: 'Integrity Test - HTML',
|
|
||||||
html: '<h1>HTML Email</h1><p>Testing integrity with HTML content</p>',
|
|
||||||
text: 'Testing integrity with HTML content'
|
|
||||||
}),
|
|
||||||
new Email({
|
|
||||||
from: 'sender@integrity.test',
|
|
||||||
to: ['recipient3@integrity.test'],
|
|
||||||
subject: 'Integrity Test - Special Characters',
|
|
||||||
text: 'Testing with special characters: ñáéíóú, 中文, العربية, русский'
|
|
||||||
})
|
|
||||||
];
|
|
||||||
|
|
||||||
console.log(' Sending emails rapidly to test integrity...');
|
|
||||||
const sendPromises = [];
|
|
||||||
|
|
||||||
// Send each email multiple times
|
|
||||||
for (let round = 0; round < 3; round++) {
|
|
||||||
for (let i = 0; i < emails.length; i++) {
|
|
||||||
sendPromises.push(
|
|
||||||
smtpClient.sendMail(emails[i]).then(() => {
|
|
||||||
console.log(` ✓ Round ${round + 1} Email ${i + 1} sent`);
|
|
||||||
return { success: true, round, emailIndex: i };
|
|
||||||
}).catch(error => {
|
|
||||||
console.log(` ✗ Round ${round + 1} Email ${i + 1} failed: ${error.message}`);
|
|
||||||
return { success: false, round, emailIndex: i, error };
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = await Promise.all(sendPromises);
|
|
||||||
const successful = results.filter(r => r.success).length;
|
|
||||||
|
|
||||||
// Wait for all messages to be processed
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
|
||||||
|
|
||||||
console.log(` Total emails sent: ${sendPromises.length}`);
|
|
||||||
console.log(` Successful: ${successful}`);
|
|
||||||
console.log(` Unique subjects received: ${receivedSubjects.size}`);
|
|
||||||
console.log(` Expected unique subjects: 3`);
|
|
||||||
console.log(` Received subjects: ${Array.from(receivedSubjects).join(', ')}`);
|
|
||||||
|
|
||||||
// With connection pooling and timing, we may not receive all unique subjects
|
|
||||||
expect(receivedSubjects.size).toBeGreaterThanOrEqual(1);
|
|
||||||
expect(successful).toBeGreaterThanOrEqual(sendPromises.length - 2);
|
|
||||||
|
|
||||||
smtpClient.close();
|
|
||||||
|
|
||||||
// Wait for connections to close
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 200));
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
server.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CREL-03: Test Summary', async () => {
|
|
||||||
console.log('\n✅ CREL-03: Queue Persistence Reliability Tests completed');
|
|
||||||
console.log('💾 All queue persistence scenarios tested successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.start();
|
|
||||||
@@ -1,520 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import * as net from 'net';
|
|
||||||
import { createTestSmtpClient } from '../../helpers/smtp.client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
tap.test('CREL-04: Basic Connection Recovery from Server Issues', async () => {
|
|
||||||
console.log('\n💥 Testing SMTP Client Connection Recovery');
|
|
||||||
console.log('=' .repeat(60));
|
|
||||||
console.log('\n🔌 Testing recovery from connection drops...');
|
|
||||||
|
|
||||||
let connectionCount = 0;
|
|
||||||
let dropConnections = false;
|
|
||||||
|
|
||||||
// Create test server that can simulate connection drops
|
|
||||||
const server = net.createServer(socket => {
|
|
||||||
connectionCount++;
|
|
||||||
console.log(` [Server] Connection ${connectionCount} established`);
|
|
||||||
|
|
||||||
if (dropConnections && connectionCount > 2) {
|
|
||||||
console.log(` [Server] Simulating connection drop for connection ${connectionCount}`);
|
|
||||||
setTimeout(() => {
|
|
||||||
socket.destroy();
|
|
||||||
}, 100);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.write('220 localhost SMTP Test Server\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
|
||||||
socket.write('250-localhost\r\n');
|
|
||||||
socket.write('250 SIZE 10485760\r\n');
|
|
||||||
} else if (line.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line === 'DATA') {
|
|
||||||
socket.write('354 Send data\r\n');
|
|
||||||
} else if (line === '.') {
|
|
||||||
socket.write('250 OK Message accepted\r\n');
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
server.listen(0, '127.0.0.1', () => {
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const port = (server.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log(' Creating SMTP client with connection recovery settings...');
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: port,
|
|
||||||
secure: false,
|
|
||||||
maxConnections: 2,
|
|
||||||
maxMessages: 50,
|
|
||||||
connectionTimeout: 2000
|
|
||||||
});
|
|
||||||
|
|
||||||
const emails = [];
|
|
||||||
for (let i = 0; i < 8; i++) {
|
|
||||||
emails.push(new Email({
|
|
||||||
from: 'sender@crashtest.example',
|
|
||||||
to: [`recipient${i}@crashtest.example`],
|
|
||||||
subject: `Connection Recovery Test ${i + 1}`,
|
|
||||||
text: `Testing connection recovery, email ${i + 1}`
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(' Phase 1: Sending initial emails (connections should succeed)...');
|
|
||||||
const results1 = [];
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
try {
|
|
||||||
await smtpClient.sendMail(emails[i]);
|
|
||||||
results1.push({ success: true, index: i });
|
|
||||||
console.log(` ✓ Email ${i + 1} sent successfully`);
|
|
||||||
} catch (error) {
|
|
||||||
results1.push({ success: false, index: i, error });
|
|
||||||
console.log(` ✗ Email ${i + 1} failed: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(' Phase 2: Enabling connection drops...');
|
|
||||||
dropConnections = true;
|
|
||||||
|
|
||||||
console.log(' Sending emails during connection instability...');
|
|
||||||
const results2 = [];
|
|
||||||
const promises = emails.slice(3).map((email, index) => {
|
|
||||||
const actualIndex = index + 3;
|
|
||||||
return smtpClient.sendMail(email).then(result => {
|
|
||||||
console.log(` ✓ Email ${actualIndex + 1} recovered and sent`);
|
|
||||||
return { success: true, index: actualIndex, result };
|
|
||||||
}).catch(error => {
|
|
||||||
console.log(` ✗ Email ${actualIndex + 1} failed permanently: ${error.message}`);
|
|
||||||
return { success: false, index: actualIndex, error };
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const results2Resolved = await Promise.all(promises);
|
|
||||||
results2.push(...results2Resolved);
|
|
||||||
|
|
||||||
const totalSuccessful = [...results1, ...results2].filter(r => r.success).length;
|
|
||||||
const totalFailed = [...results1, ...results2].filter(r => !r.success).length;
|
|
||||||
|
|
||||||
console.log(` Connection attempts: ${connectionCount}`);
|
|
||||||
console.log(` Emails sent successfully: ${totalSuccessful}/${emails.length}`);
|
|
||||||
console.log(` Failed emails: ${totalFailed}`);
|
|
||||||
console.log(` Recovery effectiveness: ${((totalSuccessful / emails.length) * 100).toFixed(1)}%`);
|
|
||||||
|
|
||||||
expect(totalSuccessful).toBeGreaterThanOrEqual(3); // At least initial emails should succeed
|
|
||||||
expect(connectionCount).toBeGreaterThanOrEqual(2); // Should have made multiple connection attempts
|
|
||||||
|
|
||||||
smtpClient.close();
|
|
||||||
} finally {
|
|
||||||
server.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CREL-04: Recovery from Server Restart', async () => {
|
|
||||||
console.log('\n💀 Testing recovery from server restart...');
|
|
||||||
|
|
||||||
// Start first server instance
|
|
||||||
let server1 = net.createServer(socket => {
|
|
||||||
console.log(' [Server1] Connection established');
|
|
||||||
socket.write('220 localhost SMTP Test Server\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
|
||||||
socket.write('250-localhost\r\n');
|
|
||||||
socket.write('250 SIZE 10485760\r\n');
|
|
||||||
} else if (line.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line === 'DATA') {
|
|
||||||
socket.write('354 Send data\r\n');
|
|
||||||
} else if (line === '.') {
|
|
||||||
socket.write('250 OK Message accepted\r\n');
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
server1.listen(0, '127.0.0.1', () => {
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const port = (server1.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log(' Creating client...');
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: port,
|
|
||||||
secure: false,
|
|
||||||
maxConnections: 1,
|
|
||||||
connectionTimeout: 3000
|
|
||||||
});
|
|
||||||
|
|
||||||
const emails = [];
|
|
||||||
for (let i = 0; i < 6; i++) {
|
|
||||||
emails.push(new Email({
|
|
||||||
from: 'sender@serverrestart.test',
|
|
||||||
to: [`recipient${i}@serverrestart.test`],
|
|
||||||
subject: `Server Restart Recovery ${i + 1}`,
|
|
||||||
text: `Testing server restart recovery, email ${i + 1}`
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(' Sending first batch of emails...');
|
|
||||||
await smtpClient.sendMail(emails[0]);
|
|
||||||
console.log(' ✓ Email 1 sent successfully');
|
|
||||||
|
|
||||||
await smtpClient.sendMail(emails[1]);
|
|
||||||
console.log(' ✓ Email 2 sent successfully');
|
|
||||||
|
|
||||||
console.log(' Simulating server restart by closing server...');
|
|
||||||
server1.close();
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
|
||||||
|
|
||||||
console.log(' Starting new server instance on same port...');
|
|
||||||
const server2 = net.createServer(socket => {
|
|
||||||
console.log(' [Server2] Connection established after restart');
|
|
||||||
socket.write('220 localhost SMTP Test Server Restarted\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
|
||||||
socket.write('250-localhost\r\n');
|
|
||||||
socket.write('250 SIZE 10485760\r\n');
|
|
||||||
} else if (line.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line === 'DATA') {
|
|
||||||
socket.write('354 Send data\r\n');
|
|
||||||
} else if (line === '.') {
|
|
||||||
socket.write('250 OK Message accepted\r\n');
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
server2.listen(port, '127.0.0.1', () => {
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(' Sending emails after server restart...');
|
|
||||||
const recoveryResults = [];
|
|
||||||
|
|
||||||
for (let i = 2; i < emails.length; i++) {
|
|
||||||
try {
|
|
||||||
await smtpClient.sendMail(emails[i]);
|
|
||||||
recoveryResults.push({ success: true, index: i });
|
|
||||||
console.log(` ✓ Email ${i + 1} sent after server recovery`);
|
|
||||||
} catch (error) {
|
|
||||||
recoveryResults.push({ success: false, index: i, error });
|
|
||||||
console.log(` ✗ Email ${i + 1} failed: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const successfulRecovery = recoveryResults.filter(r => r.success).length;
|
|
||||||
const totalSuccessful = 2 + successfulRecovery; // 2 from before restart + recovery
|
|
||||||
|
|
||||||
console.log(` Pre-restart emails: 2/2 successful`);
|
|
||||||
console.log(` Post-restart emails: ${successfulRecovery}/${recoveryResults.length} successful`);
|
|
||||||
console.log(` Overall success rate: ${((totalSuccessful / emails.length) * 100).toFixed(1)}%`);
|
|
||||||
console.log(` Server restart recovery: ${successfulRecovery > 0 ? 'Successful' : 'Failed'}`);
|
|
||||||
|
|
||||||
expect(successfulRecovery).toBeGreaterThanOrEqual(1); // At least some emails should work after restart
|
|
||||||
|
|
||||||
smtpClient.close();
|
|
||||||
server2.close();
|
|
||||||
} finally {
|
|
||||||
// Ensure cleanup
|
|
||||||
try {
|
|
||||||
server1.close();
|
|
||||||
} catch (e) { /* Already closed */ }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CREL-04: Error Recovery and State Management', async () => {
|
|
||||||
console.log('\n⚠️ Testing error recovery and state management...');
|
|
||||||
|
|
||||||
let errorInjectionEnabled = false;
|
|
||||||
const server = net.createServer(socket => {
|
|
||||||
socket.write('220 localhost SMTP Test Server\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (errorInjectionEnabled && line.startsWith('MAIL FROM')) {
|
|
||||||
console.log(' [Server] Injecting error response');
|
|
||||||
socket.write('550 Simulated server error\r\n');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
|
||||||
socket.write('250-localhost\r\n');
|
|
||||||
socket.write('250 SIZE 10485760\r\n');
|
|
||||||
} else if (line.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line === 'DATA') {
|
|
||||||
socket.write('354 Send data\r\n');
|
|
||||||
} else if (line === '.') {
|
|
||||||
socket.write('250 OK Message accepted\r\n');
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
} else if (line === 'RSET') {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
server.listen(0, '127.0.0.1', () => {
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const port = (server.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log(' Creating client with error handling...');
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: port,
|
|
||||||
secure: false,
|
|
||||||
maxConnections: 1,
|
|
||||||
connectionTimeout: 3000
|
|
||||||
});
|
|
||||||
|
|
||||||
const emails = [];
|
|
||||||
for (let i = 0; i < 6; i++) {
|
|
||||||
emails.push(new Email({
|
|
||||||
from: 'sender@exception.test',
|
|
||||||
to: [`recipient${i}@exception.test`],
|
|
||||||
subject: `Error Recovery Test ${i + 1}`,
|
|
||||||
text: `Testing error recovery, email ${i + 1}`
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(' Phase 1: Sending emails normally...');
|
|
||||||
await smtpClient.sendMail(emails[0]);
|
|
||||||
console.log(' ✓ Email 1 sent successfully');
|
|
||||||
|
|
||||||
await smtpClient.sendMail(emails[1]);
|
|
||||||
console.log(' ✓ Email 2 sent successfully');
|
|
||||||
|
|
||||||
console.log(' Phase 2: Enabling error injection...');
|
|
||||||
errorInjectionEnabled = true;
|
|
||||||
|
|
||||||
console.log(' Sending emails with error injection...');
|
|
||||||
const recoveryResults = [];
|
|
||||||
|
|
||||||
for (let i = 2; i < 4; i++) {
|
|
||||||
try {
|
|
||||||
await smtpClient.sendMail(emails[i]);
|
|
||||||
recoveryResults.push({ success: true, index: i });
|
|
||||||
console.log(` ✓ Email ${i + 1} sent despite errors`);
|
|
||||||
} catch (error) {
|
|
||||||
recoveryResults.push({ success: false, index: i, error });
|
|
||||||
console.log(` ✗ Email ${i + 1} failed: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(' Phase 3: Disabling error injection...');
|
|
||||||
errorInjectionEnabled = false;
|
|
||||||
|
|
||||||
console.log(' Sending final emails (recovery validation)...');
|
|
||||||
for (let i = 4; i < emails.length; i++) {
|
|
||||||
try {
|
|
||||||
await smtpClient.sendMail(emails[i]);
|
|
||||||
recoveryResults.push({ success: true, index: i });
|
|
||||||
console.log(` ✓ Email ${i + 1} sent after recovery`);
|
|
||||||
} catch (error) {
|
|
||||||
recoveryResults.push({ success: false, index: i, error });
|
|
||||||
console.log(` ✗ Email ${i + 1} failed: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const successful = recoveryResults.filter(r => r.success).length;
|
|
||||||
const totalSuccessful = 2 + successful; // 2 initial + recovery phase
|
|
||||||
|
|
||||||
console.log(` Pre-error emails: 2/2 successful`);
|
|
||||||
console.log(` Error/recovery phase emails: ${successful}/${recoveryResults.length} successful`);
|
|
||||||
console.log(` Total success rate: ${((totalSuccessful / emails.length) * 100).toFixed(1)}%`);
|
|
||||||
console.log(` Error recovery: ${successful >= recoveryResults.length - 2 ? 'Effective' : 'Partial'}`);
|
|
||||||
|
|
||||||
expect(totalSuccessful).toBeGreaterThanOrEqual(4); // At least initial + some recovery
|
|
||||||
|
|
||||||
smtpClient.close();
|
|
||||||
} finally {
|
|
||||||
server.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CREL-04: Resource Management During Issues', async () => {
|
|
||||||
console.log('\n🧠 Testing resource management during connection issues...');
|
|
||||||
|
|
||||||
let memoryBefore = process.memoryUsage();
|
|
||||||
|
|
||||||
const server = net.createServer(socket => {
|
|
||||||
socket.write('220 localhost SMTP Test Server\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
|
||||||
socket.write('250-localhost\r\n');
|
|
||||||
socket.write('250 SIZE 10485760\r\n');
|
|
||||||
} else if (line.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line === 'DATA') {
|
|
||||||
socket.write('354 Send data\r\n');
|
|
||||||
} else if (line === '.') {
|
|
||||||
socket.write('250 OK Message accepted\r\n');
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
server.listen(0, '127.0.0.1', () => {
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const port = (server.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log(' Creating client for resource management test...');
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: port,
|
|
||||||
secure: false,
|
|
||||||
maxConnections: 5,
|
|
||||||
maxMessages: 100
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(' Creating emails with various content types...');
|
|
||||||
const emails = [
|
|
||||||
new Email({
|
|
||||||
from: 'sender@resource.test',
|
|
||||||
to: ['recipient1@resource.test'],
|
|
||||||
subject: 'Resource Test - Normal',
|
|
||||||
text: 'Normal email content'
|
|
||||||
}),
|
|
||||||
new Email({
|
|
||||||
from: 'sender@resource.test',
|
|
||||||
to: ['recipient2@resource.test'],
|
|
||||||
subject: 'Resource Test - Large Content',
|
|
||||||
text: 'X'.repeat(50000) // Large content
|
|
||||||
}),
|
|
||||||
new Email({
|
|
||||||
from: 'sender@resource.test',
|
|
||||||
to: ['recipient3@resource.test'],
|
|
||||||
subject: 'Resource Test - Unicode',
|
|
||||||
text: '🎭🎪🎨🎯🎲🎸🎺🎻🎼🎵🎶🎷'.repeat(100)
|
|
||||||
})
|
|
||||||
];
|
|
||||||
|
|
||||||
console.log(' Sending emails and monitoring resource usage...');
|
|
||||||
const results = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < emails.length; i++) {
|
|
||||||
console.log(` Testing email ${i + 1} (${emails[i].subject.split(' - ')[1]})...`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Monitor memory usage before sending
|
|
||||||
const memBefore = process.memoryUsage();
|
|
||||||
console.log(` Memory before: ${Math.round(memBefore.heapUsed / 1024 / 1024)}MB`);
|
|
||||||
|
|
||||||
await smtpClient.sendMail(emails[i]);
|
|
||||||
|
|
||||||
const memAfter = process.memoryUsage();
|
|
||||||
console.log(` Memory after: ${Math.round(memAfter.heapUsed / 1024 / 1024)}MB`);
|
|
||||||
|
|
||||||
const memIncrease = memAfter.heapUsed - memBefore.heapUsed;
|
|
||||||
console.log(` Memory increase: ${Math.round(memIncrease / 1024)}KB`);
|
|
||||||
|
|
||||||
results.push({
|
|
||||||
success: true,
|
|
||||||
index: i,
|
|
||||||
memoryIncrease: memIncrease
|
|
||||||
});
|
|
||||||
console.log(` ✓ Email ${i + 1} sent successfully`);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
results.push({ success: false, index: i, error });
|
|
||||||
console.log(` ✗ Email ${i + 1} failed: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force garbage collection if available
|
|
||||||
if (global.gc) {
|
|
||||||
global.gc();
|
|
||||||
}
|
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
}
|
|
||||||
|
|
||||||
const successful = results.filter(r => r.success).length;
|
|
||||||
const totalMemoryIncrease = results.reduce((sum, r) => sum + (r.memoryIncrease || 0), 0);
|
|
||||||
|
|
||||||
console.log(` Resource management: ${successful}/${emails.length} emails processed`);
|
|
||||||
console.log(` Total memory increase: ${Math.round(totalMemoryIncrease / 1024)}KB`);
|
|
||||||
console.log(` Resource efficiency: ${((successful / emails.length) * 100).toFixed(1)}%`);
|
|
||||||
|
|
||||||
expect(successful).toBeGreaterThanOrEqual(2); // Most emails should succeed
|
|
||||||
expect(totalMemoryIncrease).toBeLessThan(100 * 1024 * 1024); // Less than 100MB increase
|
|
||||||
|
|
||||||
smtpClient.close();
|
|
||||||
} finally {
|
|
||||||
server.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CREL-04: Test Summary', async () => {
|
|
||||||
console.log('\n✅ CREL-04: Crash Recovery Reliability Tests completed');
|
|
||||||
console.log('💥 All connection recovery scenarios tested successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.start();
|
|
||||||
@@ -1,503 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import * as net from 'net';
|
|
||||||
import { createTestSmtpClient } from '../../helpers/smtp.client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
// Helper function to get memory usage
|
|
||||||
const getMemoryUsage = () => {
|
|
||||||
const usage = process.memoryUsage();
|
|
||||||
return {
|
|
||||||
heapUsed: Math.round(usage.heapUsed / 1024 / 1024 * 100) / 100, // MB
|
|
||||||
heapTotal: Math.round(usage.heapTotal / 1024 / 1024 * 100) / 100, // MB
|
|
||||||
external: Math.round(usage.external / 1024 / 1024 * 100) / 100, // MB
|
|
||||||
rss: Math.round(usage.rss / 1024 / 1024 * 100) / 100 // MB
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Force garbage collection if available
|
|
||||||
const forceGC = () => {
|
|
||||||
if (global.gc) {
|
|
||||||
global.gc();
|
|
||||||
global.gc(); // Run twice for thoroughness
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
tap.test('CREL-05: Connection Pool Memory Management', async () => {
|
|
||||||
console.log('\n🧠 Testing SMTP Client Memory Leak Prevention');
|
|
||||||
console.log('=' .repeat(60));
|
|
||||||
console.log('\n🏊 Testing connection pool memory management...');
|
|
||||||
|
|
||||||
// Create test server
|
|
||||||
const server = net.createServer(socket => {
|
|
||||||
socket.write('220 localhost SMTP Test Server\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
|
||||||
socket.write('250-localhost\r\n');
|
|
||||||
socket.write('250 SIZE 10485760\r\n');
|
|
||||||
} else if (line.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line === 'DATA') {
|
|
||||||
socket.write('354 Send data\r\n');
|
|
||||||
} else if (line === '.') {
|
|
||||||
socket.write('250 OK Message accepted\r\n');
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
server.listen(0, '127.0.0.1', () => {
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const port = (server.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const initialMemory = getMemoryUsage();
|
|
||||||
console.log(` Initial memory: ${initialMemory.heapUsed}MB heap, ${initialMemory.rss}MB RSS`);
|
|
||||||
|
|
||||||
console.log(' Phase 1: Creating and using multiple connection pools...');
|
|
||||||
const memorySnapshots = [];
|
|
||||||
|
|
||||||
for (let poolIndex = 0; poolIndex < 5; poolIndex++) {
|
|
||||||
console.log(` Creating connection pool ${poolIndex + 1}...`);
|
|
||||||
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: port,
|
|
||||||
secure: false,
|
|
||||||
maxConnections: 3,
|
|
||||||
maxMessages: 20,
|
|
||||||
connectionTimeout: 1000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send emails through this pool
|
|
||||||
const emails = [];
|
|
||||||
for (let i = 0; i < 6; i++) {
|
|
||||||
emails.push(new Email({
|
|
||||||
from: `sender${poolIndex}@memoryleak.test`,
|
|
||||||
to: [`recipient${i}@memoryleak.test`],
|
|
||||||
subject: `Memory Pool Test ${poolIndex + 1}-${i + 1}`,
|
|
||||||
text: `Testing memory management in pool ${poolIndex + 1}, email ${i + 1}`
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send emails concurrently
|
|
||||||
const promises = emails.map((email, index) => {
|
|
||||||
return smtpClient.sendMail(email).then(result => {
|
|
||||||
return { success: true, result };
|
|
||||||
}).catch(error => {
|
|
||||||
return { success: false, error };
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const results = await Promise.all(promises);
|
|
||||||
const successful = results.filter(r => r.success).length;
|
|
||||||
console.log(` Pool ${poolIndex + 1}: ${successful}/${emails.length} emails sent`);
|
|
||||||
|
|
||||||
// Close the pool
|
|
||||||
smtpClient.close();
|
|
||||||
console.log(` Pool ${poolIndex + 1} closed`);
|
|
||||||
|
|
||||||
// Force garbage collection and measure memory
|
|
||||||
forceGC();
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
|
|
||||||
const currentMemory = getMemoryUsage();
|
|
||||||
memorySnapshots.push({
|
|
||||||
pool: poolIndex + 1,
|
|
||||||
heap: currentMemory.heapUsed,
|
|
||||||
rss: currentMemory.rss,
|
|
||||||
external: currentMemory.external
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(` Memory after pool ${poolIndex + 1}: ${currentMemory.heapUsed}MB heap`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n Memory analysis:');
|
|
||||||
memorySnapshots.forEach((snapshot, index) => {
|
|
||||||
const memoryIncrease = snapshot.heap - initialMemory.heapUsed;
|
|
||||||
console.log(` Pool ${snapshot.pool}: +${memoryIncrease.toFixed(2)}MB heap increase`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check for memory leaks (memory should not continuously increase)
|
|
||||||
const firstIncrease = memorySnapshots[0].heap - initialMemory.heapUsed;
|
|
||||||
const lastIncrease = memorySnapshots[memorySnapshots.length - 1].heap - initialMemory.heapUsed;
|
|
||||||
const leakGrowth = lastIncrease - firstIncrease;
|
|
||||||
|
|
||||||
console.log(` Memory leak assessment:`);
|
|
||||||
console.log(` First pool increase: +${firstIncrease.toFixed(2)}MB`);
|
|
||||||
console.log(` Final memory increase: +${lastIncrease.toFixed(2)}MB`);
|
|
||||||
console.log(` Memory growth across pools: +${leakGrowth.toFixed(2)}MB`);
|
|
||||||
console.log(` Memory management: ${leakGrowth < 3.0 ? 'Good (< 3MB growth)' : 'Potential leak detected'}`);
|
|
||||||
|
|
||||||
expect(leakGrowth).toBeLessThan(5.0); // Allow some memory growth but detect major leaks
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
server.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CREL-05: Email Object Memory Lifecycle', async () => {
|
|
||||||
console.log('\n📧 Testing email object memory lifecycle...');
|
|
||||||
|
|
||||||
// Create test server
|
|
||||||
const server = net.createServer(socket => {
|
|
||||||
socket.write('220 localhost SMTP Test Server\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
|
||||||
socket.write('250-localhost\r\n');
|
|
||||||
socket.write('250 SIZE 10485760\r\n');
|
|
||||||
} else if (line.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line === 'DATA') {
|
|
||||||
socket.write('354 Send data\r\n');
|
|
||||||
} else if (line === '.') {
|
|
||||||
socket.write('250 OK Message accepted\r\n');
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
server.listen(0, '127.0.0.1', () => {
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const port = (server.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: port,
|
|
||||||
secure: false,
|
|
||||||
maxConnections: 2
|
|
||||||
});
|
|
||||||
|
|
||||||
const initialMemory = getMemoryUsage();
|
|
||||||
console.log(` Initial memory: ${initialMemory.heapUsed}MB heap`);
|
|
||||||
|
|
||||||
console.log(' Phase 1: Creating large batches of email objects...');
|
|
||||||
const batchSizes = [50, 100, 150, 100, 50]; // Varying batch sizes
|
|
||||||
const memorySnapshots = [];
|
|
||||||
|
|
||||||
for (let batchIndex = 0; batchIndex < batchSizes.length; batchIndex++) {
|
|
||||||
const batchSize = batchSizes[batchIndex];
|
|
||||||
console.log(` Creating batch ${batchIndex + 1} with ${batchSize} emails...`);
|
|
||||||
|
|
||||||
const emails = [];
|
|
||||||
for (let i = 0; i < batchSize; i++) {
|
|
||||||
emails.push(new Email({
|
|
||||||
from: 'sender@emailmemory.test',
|
|
||||||
to: [`recipient${i}@emailmemory.test`],
|
|
||||||
subject: `Memory Lifecycle Test Batch ${batchIndex + 1} Email ${i + 1}`,
|
|
||||||
text: `Testing email object memory lifecycle. This is a moderately long email body to test memory usage patterns. Email ${i + 1} in batch ${batchIndex + 1} of ${batchSize} emails.`,
|
|
||||||
html: `<h1>Email ${i + 1}</h1><p>Testing memory patterns with HTML content. Batch ${batchIndex + 1}.</p>`
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(` Sending batch ${batchIndex + 1}...`);
|
|
||||||
const promises = emails.map((email, index) => {
|
|
||||||
return smtpClient.sendMail(email).then(result => {
|
|
||||||
return { success: true };
|
|
||||||
}).catch(error => {
|
|
||||||
return { success: false, error };
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const results = await Promise.all(promises);
|
|
||||||
const successful = results.filter(r => r.success).length;
|
|
||||||
console.log(` Batch ${batchIndex + 1}: ${successful}/${batchSize} emails sent`);
|
|
||||||
|
|
||||||
// Clear email references
|
|
||||||
emails.length = 0;
|
|
||||||
|
|
||||||
// Force garbage collection
|
|
||||||
forceGC();
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
|
|
||||||
const currentMemory = getMemoryUsage();
|
|
||||||
memorySnapshots.push({
|
|
||||||
batch: batchIndex + 1,
|
|
||||||
size: batchSize,
|
|
||||||
heap: currentMemory.heapUsed,
|
|
||||||
external: currentMemory.external
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(` Memory after batch ${batchIndex + 1}: ${currentMemory.heapUsed}MB heap`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n Email object memory analysis:');
|
|
||||||
memorySnapshots.forEach((snapshot, index) => {
|
|
||||||
const memoryIncrease = snapshot.heap - initialMemory.heapUsed;
|
|
||||||
console.log(` Batch ${snapshot.batch} (${snapshot.size} emails): +${memoryIncrease.toFixed(2)}MB`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check if memory scales reasonably with email batch size
|
|
||||||
const maxMemoryIncrease = Math.max(...memorySnapshots.map(s => s.heap - initialMemory.heapUsed));
|
|
||||||
const avgBatchSize = batchSizes.reduce((a, b) => a + b, 0) / batchSizes.length;
|
|
||||||
|
|
||||||
console.log(` Maximum memory increase: +${maxMemoryIncrease.toFixed(2)}MB`);
|
|
||||||
console.log(` Average batch size: ${avgBatchSize} emails`);
|
|
||||||
console.log(` Memory per email: ~${(maxMemoryIncrease / avgBatchSize * 1024).toFixed(1)}KB`);
|
|
||||||
console.log(` Email object lifecycle: ${maxMemoryIncrease < 10 ? 'Efficient' : 'Needs optimization'}`);
|
|
||||||
|
|
||||||
expect(maxMemoryIncrease).toBeLessThan(15); // Allow reasonable memory usage
|
|
||||||
|
|
||||||
smtpClient.close();
|
|
||||||
} finally {
|
|
||||||
server.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CREL-05: Long-Running Client Memory Stability', async () => {
|
|
||||||
console.log('\n⏱️ Testing long-running client memory stability...');
|
|
||||||
|
|
||||||
// Create test server
|
|
||||||
const server = net.createServer(socket => {
|
|
||||||
socket.write('220 localhost SMTP Test Server\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
|
||||||
socket.write('250-localhost\r\n');
|
|
||||||
socket.write('250 SIZE 10485760\r\n');
|
|
||||||
} else if (line.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line === 'DATA') {
|
|
||||||
socket.write('354 Send data\r\n');
|
|
||||||
} else if (line === '.') {
|
|
||||||
socket.write('250 OK Message accepted\r\n');
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
server.listen(0, '127.0.0.1', () => {
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const port = (server.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: port,
|
|
||||||
secure: false,
|
|
||||||
maxConnections: 2,
|
|
||||||
maxMessages: 1000
|
|
||||||
});
|
|
||||||
|
|
||||||
const initialMemory = getMemoryUsage();
|
|
||||||
console.log(` Initial memory: ${initialMemory.heapUsed}MB heap`);
|
|
||||||
|
|
||||||
console.log(' Starting sustained email sending operation...');
|
|
||||||
const memoryMeasurements = [];
|
|
||||||
const totalEmails = 100; // Reduced for test efficiency
|
|
||||||
const measurementInterval = 20; // Measure every 20 emails
|
|
||||||
|
|
||||||
let emailsSent = 0;
|
|
||||||
let emailsFailed = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < totalEmails; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@longrunning.test',
|
|
||||||
to: [`recipient${i}@longrunning.test`],
|
|
||||||
subject: `Long Running Test ${i + 1}`,
|
|
||||||
text: `Sustained operation test email ${i + 1}`
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await smtpClient.sendMail(email);
|
|
||||||
emailsSent++;
|
|
||||||
} catch (error) {
|
|
||||||
emailsFailed++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Measure memory at intervals
|
|
||||||
if ((i + 1) % measurementInterval === 0) {
|
|
||||||
forceGC();
|
|
||||||
const currentMemory = getMemoryUsage();
|
|
||||||
memoryMeasurements.push({
|
|
||||||
emailCount: i + 1,
|
|
||||||
heap: currentMemory.heapUsed,
|
|
||||||
rss: currentMemory.rss,
|
|
||||||
timestamp: Date.now()
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(` ${i + 1}/${totalEmails} emails: ${currentMemory.heapUsed}MB heap`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n Long-running memory analysis:');
|
|
||||||
console.log(` Emails sent: ${emailsSent}, Failed: ${emailsFailed}`);
|
|
||||||
|
|
||||||
memoryMeasurements.forEach((measurement, index) => {
|
|
||||||
const memoryIncrease = measurement.heap - initialMemory.heapUsed;
|
|
||||||
console.log(` After ${measurement.emailCount} emails: +${memoryIncrease.toFixed(2)}MB heap`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Analyze memory growth trend
|
|
||||||
if (memoryMeasurements.length >= 2) {
|
|
||||||
const firstMeasurement = memoryMeasurements[0];
|
|
||||||
const lastMeasurement = memoryMeasurements[memoryMeasurements.length - 1];
|
|
||||||
|
|
||||||
const memoryGrowth = lastMeasurement.heap - firstMeasurement.heap;
|
|
||||||
const emailsProcessed = lastMeasurement.emailCount - firstMeasurement.emailCount;
|
|
||||||
const growthRate = (memoryGrowth / emailsProcessed) * 1000; // KB per email
|
|
||||||
|
|
||||||
console.log(` Memory growth over operation: +${memoryGrowth.toFixed(2)}MB`);
|
|
||||||
console.log(` Growth rate: ~${growthRate.toFixed(2)}KB per email`);
|
|
||||||
console.log(` Memory stability: ${growthRate < 10 ? 'Excellent' : growthRate < 25 ? 'Good' : 'Concerning'}`);
|
|
||||||
|
|
||||||
expect(growthRate).toBeLessThan(50); // Allow reasonable growth but detect major leaks
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(emailsSent).toBeGreaterThanOrEqual(totalEmails - 5); // Most emails should succeed
|
|
||||||
|
|
||||||
smtpClient.close();
|
|
||||||
} finally {
|
|
||||||
server.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CREL-05: Large Content Memory Management', async () => {
|
|
||||||
console.log('\n🌊 Testing large content memory management...');
|
|
||||||
|
|
||||||
// Create test server
|
|
||||||
const server = net.createServer(socket => {
|
|
||||||
socket.write('220 localhost SMTP Test Server\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
|
||||||
socket.write('250-localhost\r\n');
|
|
||||||
socket.write('250 SIZE 10485760\r\n');
|
|
||||||
} else if (line.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line === 'DATA') {
|
|
||||||
socket.write('354 Send data\r\n');
|
|
||||||
} else if (line === '.') {
|
|
||||||
socket.write('250 OK Message accepted\r\n');
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
server.listen(0, '127.0.0.1', () => {
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const port = (server.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: port,
|
|
||||||
secure: false,
|
|
||||||
maxConnections: 1
|
|
||||||
});
|
|
||||||
|
|
||||||
const initialMemory = getMemoryUsage();
|
|
||||||
console.log(` Initial memory: ${initialMemory.heapUsed}MB heap`);
|
|
||||||
|
|
||||||
console.log(' Testing with various content sizes...');
|
|
||||||
const contentSizes = [
|
|
||||||
{ size: 1024, name: '1KB' },
|
|
||||||
{ size: 10240, name: '10KB' },
|
|
||||||
{ size: 102400, name: '100KB' },
|
|
||||||
{ size: 256000, name: '250KB' }
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const contentTest of contentSizes) {
|
|
||||||
console.log(` Testing ${contentTest.name} content size...`);
|
|
||||||
|
|
||||||
const beforeMemory = getMemoryUsage();
|
|
||||||
|
|
||||||
// Create large text content
|
|
||||||
const largeText = 'X'.repeat(contentTest.size);
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@largemem.test',
|
|
||||||
to: ['recipient@largemem.test'],
|
|
||||||
subject: `Large Content Test - ${contentTest.name}`,
|
|
||||||
text: largeText
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await smtpClient.sendMail(email);
|
|
||||||
console.log(` ✓ ${contentTest.name} email sent successfully`);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ✗ ${contentTest.name} email failed: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force cleanup
|
|
||||||
forceGC();
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
|
|
||||||
const afterMemory = getMemoryUsage();
|
|
||||||
const memoryDiff = afterMemory.heapUsed - beforeMemory.heapUsed;
|
|
||||||
|
|
||||||
console.log(` Memory impact: ${memoryDiff > 0 ? '+' : ''}${memoryDiff.toFixed(2)}MB`);
|
|
||||||
console.log(` Efficiency: ${Math.abs(memoryDiff) < (contentTest.size / 1024 / 1024) * 2 ? 'Good' : 'High memory usage'}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalMemory = getMemoryUsage();
|
|
||||||
const totalMemoryIncrease = finalMemory.heapUsed - initialMemory.heapUsed;
|
|
||||||
|
|
||||||
console.log(`\n Large content memory summary:`);
|
|
||||||
console.log(` Total memory increase: +${totalMemoryIncrease.toFixed(2)}MB`);
|
|
||||||
console.log(` Memory management efficiency: ${totalMemoryIncrease < 5 ? 'Excellent' : 'Needs optimization'}`);
|
|
||||||
|
|
||||||
expect(totalMemoryIncrease).toBeLessThan(20); // Allow reasonable memory usage for large content
|
|
||||||
|
|
||||||
smtpClient.close();
|
|
||||||
} finally {
|
|
||||||
server.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CREL-05: Test Summary', async () => {
|
|
||||||
console.log('\n✅ CREL-05: Memory Leak Prevention Reliability Tests completed');
|
|
||||||
console.log('🧠 All memory management scenarios tested successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.start();
|
|
||||||
@@ -1,558 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import * as net from 'net';
|
|
||||||
import { createTestSmtpClient } from '../../helpers/smtp.client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
tap.test('CREL-06: Simultaneous Connection Management', async () => {
|
|
||||||
console.log('\n⚡ Testing SMTP Client Concurrent Operation Safety');
|
|
||||||
console.log('=' .repeat(60));
|
|
||||||
console.log('\n🔗 Testing simultaneous connection management safety...');
|
|
||||||
|
|
||||||
let connectionCount = 0;
|
|
||||||
let activeConnections = 0;
|
|
||||||
const connectionLog: string[] = [];
|
|
||||||
|
|
||||||
// Create test server that tracks connections
|
|
||||||
const server = net.createServer(socket => {
|
|
||||||
connectionCount++;
|
|
||||||
activeConnections++;
|
|
||||||
const connId = `CONN-${connectionCount}`;
|
|
||||||
connectionLog.push(`${new Date().toISOString()}: ${connId} OPENED (active: ${activeConnections})`);
|
|
||||||
console.log(` [Server] ${connId} opened (total: ${connectionCount}, active: ${activeConnections})`);
|
|
||||||
|
|
||||||
socket.on('close', () => {
|
|
||||||
activeConnections--;
|
|
||||||
connectionLog.push(`${new Date().toISOString()}: ${connId} CLOSED (active: ${activeConnections})`);
|
|
||||||
console.log(` [Server] ${connId} closed (active: ${activeConnections})`);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.write('220 localhost SMTP Test Server\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
|
||||||
socket.write('250-localhost\r\n');
|
|
||||||
socket.write('250 SIZE 10485760\r\n');
|
|
||||||
} else if (line.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line === 'DATA') {
|
|
||||||
socket.write('354 Send data\r\n');
|
|
||||||
} else if (line === '.') {
|
|
||||||
socket.write('250 OK Message accepted\r\n');
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
server.listen(0, '127.0.0.1', () => {
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const port = (server.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log(' Creating multiple SMTP clients with shared connection pool settings...');
|
|
||||||
const clients = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
clients.push(createTestSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: port,
|
|
||||||
secure: false,
|
|
||||||
maxConnections: 3, // Allow up to 3 connections
|
|
||||||
maxMessages: 10,
|
|
||||||
connectionTimeout: 2000
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(' Launching concurrent email sending operations...');
|
|
||||||
const emailBatches = clients.map((client, clientIndex) => {
|
|
||||||
return Array.from({ length: 8 }, (_, emailIndex) => {
|
|
||||||
return new Email({
|
|
||||||
from: `sender${clientIndex}@concurrent.test`,
|
|
||||||
to: [`recipient${clientIndex}-${emailIndex}@concurrent.test`],
|
|
||||||
subject: `Concurrent Safety Test Client ${clientIndex + 1} Email ${emailIndex + 1}`,
|
|
||||||
text: `Testing concurrent operation safety from client ${clientIndex + 1}, email ${emailIndex + 1}`
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
const allPromises: Promise<any>[] = [];
|
|
||||||
|
|
||||||
// Launch all email operations simultaneously
|
|
||||||
emailBatches.forEach((emails, clientIndex) => {
|
|
||||||
emails.forEach((email, emailIndex) => {
|
|
||||||
const promise = clients[clientIndex].sendMail(email).then(result => {
|
|
||||||
console.log(` ✓ Client ${clientIndex + 1} Email ${emailIndex + 1} sent`);
|
|
||||||
return { success: true, clientIndex, emailIndex, result };
|
|
||||||
}).catch(error => {
|
|
||||||
console.log(` ✗ Client ${clientIndex + 1} Email ${emailIndex + 1} failed: ${error.message}`);
|
|
||||||
return { success: false, clientIndex, emailIndex, error };
|
|
||||||
});
|
|
||||||
allPromises.push(promise);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const results = await Promise.all(allPromises);
|
|
||||||
const endTime = Date.now();
|
|
||||||
|
|
||||||
// Close all clients
|
|
||||||
clients.forEach(client => client.close());
|
|
||||||
|
|
||||||
// Wait for connections to close
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
|
||||||
|
|
||||||
const successful = results.filter(r => r.success).length;
|
|
||||||
const failed = results.filter(r => !r.success).length;
|
|
||||||
const totalEmails = emailBatches.flat().length;
|
|
||||||
|
|
||||||
console.log(`\n Concurrent operation results:`);
|
|
||||||
console.log(` Total operations: ${totalEmails}`);
|
|
||||||
console.log(` Successful: ${successful}, Failed: ${failed}`);
|
|
||||||
console.log(` Success rate: ${((successful / totalEmails) * 100).toFixed(1)}%`);
|
|
||||||
console.log(` Execution time: ${endTime - startTime}ms`);
|
|
||||||
console.log(` Peak connections: ${Math.max(...connectionLog.map(log => {
|
|
||||||
const match = log.match(/active: (\d+)/);
|
|
||||||
return match ? parseInt(match[1]) : 0;
|
|
||||||
}))}`);
|
|
||||||
console.log(` Connection management: ${activeConnections === 0 ? 'Clean' : 'Connections remaining'}`);
|
|
||||||
|
|
||||||
expect(successful).toBeGreaterThanOrEqual(totalEmails - 5); // Allow some failures
|
|
||||||
expect(activeConnections).toEqual(0); // All connections should be closed
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
server.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CREL-06: Concurrent Queue Operations', async () => {
|
|
||||||
console.log('\n🔒 Testing concurrent queue operations...');
|
|
||||||
|
|
||||||
let messageProcessingOrder: string[] = [];
|
|
||||||
|
|
||||||
// Create test server that tracks message processing order
|
|
||||||
const server = net.createServer(socket => {
|
|
||||||
socket.write('220 localhost SMTP Test Server\r\n');
|
|
||||||
let inData = false;
|
|
||||||
let currentData = '';
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (inData) {
|
|
||||||
if (line === '.') {
|
|
||||||
// Extract Message-ID from email data
|
|
||||||
const messageIdMatch = currentData.match(/Message-ID:\s*<([^>]+)>/);
|
|
||||||
if (messageIdMatch) {
|
|
||||||
messageProcessingOrder.push(messageIdMatch[1]);
|
|
||||||
console.log(` [Server] Processing: ${messageIdMatch[1]}`);
|
|
||||||
}
|
|
||||||
socket.write('250 OK Message accepted\r\n');
|
|
||||||
inData = false;
|
|
||||||
currentData = '';
|
|
||||||
} else {
|
|
||||||
currentData += line + '\r\n';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
|
||||||
socket.write('250-localhost\r\n');
|
|
||||||
socket.write('250 SIZE 10485760\r\n');
|
|
||||||
} else if (line.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line === 'DATA') {
|
|
||||||
socket.write('354 Send data\r\n');
|
|
||||||
inData = true;
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
server.listen(0, '127.0.0.1', () => {
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const port = (server.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log(' Creating SMTP client for concurrent queue operations...');
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: port,
|
|
||||||
secure: false,
|
|
||||||
maxConnections: 2,
|
|
||||||
maxMessages: 50
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(' Launching concurrent queue operations...');
|
|
||||||
const operations: Promise<any>[] = [];
|
|
||||||
const emailGroups = ['A', 'B', 'C', 'D'];
|
|
||||||
|
|
||||||
// Create concurrent operations that use the queue
|
|
||||||
emailGroups.forEach((group, groupIndex) => {
|
|
||||||
// Add multiple emails per group concurrently
|
|
||||||
for (let i = 0; i < 6; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: `sender${group}@queuetest.example`,
|
|
||||||
to: [`recipient${group}${i}@queuetest.example`],
|
|
||||||
subject: `Queue Safety Test Group ${group} Email ${i + 1}`,
|
|
||||||
text: `Testing queue safety for group ${group}, email ${i + 1}`
|
|
||||||
});
|
|
||||||
|
|
||||||
const operation = smtpClient.sendMail(email).then(result => {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
group,
|
|
||||||
index: i,
|
|
||||||
messageId: result.messageId,
|
|
||||||
timestamp: Date.now()
|
|
||||||
};
|
|
||||||
}).catch(error => {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
group,
|
|
||||||
index: i,
|
|
||||||
error: error.message
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
operations.push(operation);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
const results = await Promise.all(operations);
|
|
||||||
const endTime = Date.now();
|
|
||||||
|
|
||||||
// Wait for all processing to complete
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 300));
|
|
||||||
|
|
||||||
const successful = results.filter(r => r.success).length;
|
|
||||||
const failed = results.filter(r => !r.success).length;
|
|
||||||
|
|
||||||
console.log(`\n Queue safety results:`);
|
|
||||||
console.log(` Total queue operations: ${operations.length}`);
|
|
||||||
console.log(` Successful: ${successful}, Failed: ${failed}`);
|
|
||||||
console.log(` Success rate: ${((successful / operations.length) * 100).toFixed(1)}%`);
|
|
||||||
console.log(` Processing time: ${endTime - startTime}ms`);
|
|
||||||
|
|
||||||
// Analyze processing order
|
|
||||||
const groupCounts = emailGroups.reduce((acc, group) => {
|
|
||||||
acc[group] = messageProcessingOrder.filter(id => id && id.includes(`${group}`)).length;
|
|
||||||
return acc;
|
|
||||||
}, {} as Record<string, number>);
|
|
||||||
|
|
||||||
console.log(` Processing distribution:`);
|
|
||||||
Object.entries(groupCounts).forEach(([group, count]) => {
|
|
||||||
console.log(` Group ${group}: ${count} emails processed`);
|
|
||||||
});
|
|
||||||
|
|
||||||
const totalProcessed = Object.values(groupCounts).reduce((a, b) => a + b, 0);
|
|
||||||
console.log(` Queue integrity: ${totalProcessed === successful ? 'Maintained' : 'Some messages lost'}`);
|
|
||||||
|
|
||||||
expect(successful).toBeGreaterThanOrEqual(operations.length - 2); // Allow minimal failures
|
|
||||||
|
|
||||||
smtpClient.close();
|
|
||||||
} finally {
|
|
||||||
server.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CREL-06: Concurrent Error Handling', async () => {
|
|
||||||
console.log('\n❌ Testing concurrent error handling safety...');
|
|
||||||
|
|
||||||
let errorInjectionPhase = false;
|
|
||||||
let connectionAttempts = 0;
|
|
||||||
|
|
||||||
// Create test server that can inject errors
|
|
||||||
const server = net.createServer(socket => {
|
|
||||||
connectionAttempts++;
|
|
||||||
console.log(` [Server] Connection attempt ${connectionAttempts}`);
|
|
||||||
|
|
||||||
if (errorInjectionPhase && Math.random() < 0.4) {
|
|
||||||
console.log(` [Server] Injecting connection error ${connectionAttempts}`);
|
|
||||||
socket.destroy();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.write('220 localhost SMTP Test Server\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (errorInjectionPhase && line.startsWith('MAIL FROM') && Math.random() < 0.3) {
|
|
||||||
console.log(' [Server] Injecting SMTP error');
|
|
||||||
socket.write('450 Temporary failure, please retry\r\n');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
|
||||||
socket.write('250-localhost\r\n');
|
|
||||||
socket.write('250 SIZE 10485760\r\n');
|
|
||||||
} else if (line.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line === 'DATA') {
|
|
||||||
socket.write('354 Send data\r\n');
|
|
||||||
} else if (line === '.') {
|
|
||||||
socket.write('250 OK Message accepted\r\n');
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
server.listen(0, '127.0.0.1', () => {
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const port = (server.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log(' Creating multiple clients for concurrent error testing...');
|
|
||||||
const clients = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < 4; i++) {
|
|
||||||
clients.push(createTestSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: port,
|
|
||||||
secure: false,
|
|
||||||
maxConnections: 2,
|
|
||||||
connectionTimeout: 3000
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
const emails = [];
|
|
||||||
for (let clientIndex = 0; clientIndex < clients.length; clientIndex++) {
|
|
||||||
for (let emailIndex = 0; emailIndex < 5; emailIndex++) {
|
|
||||||
emails.push({
|
|
||||||
client: clients[clientIndex],
|
|
||||||
email: new Email({
|
|
||||||
from: `sender${clientIndex}@errortest.example`,
|
|
||||||
to: [`recipient${clientIndex}-${emailIndex}@errortest.example`],
|
|
||||||
subject: `Concurrent Error Test Client ${clientIndex + 1} Email ${emailIndex + 1}`,
|
|
||||||
text: `Testing concurrent error handling ${clientIndex + 1}-${emailIndex + 1}`
|
|
||||||
}),
|
|
||||||
clientIndex,
|
|
||||||
emailIndex
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(' Phase 1: Normal operation...');
|
|
||||||
const phase1Results = [];
|
|
||||||
const phase1Emails = emails.slice(0, 8); // First 8 emails
|
|
||||||
|
|
||||||
const phase1Promises = phase1Emails.map(({ client, email, clientIndex, emailIndex }) => {
|
|
||||||
return client.sendMail(email).then(result => {
|
|
||||||
console.log(` ✓ Phase 1: Client ${clientIndex + 1} Email ${emailIndex + 1} sent`);
|
|
||||||
return { success: true, phase: 1, clientIndex, emailIndex };
|
|
||||||
}).catch(error => {
|
|
||||||
console.log(` ✗ Phase 1: Client ${clientIndex + 1} Email ${emailIndex + 1} failed`);
|
|
||||||
return { success: false, phase: 1, clientIndex, emailIndex, error: error.message };
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const phase1Resolved = await Promise.all(phase1Promises);
|
|
||||||
phase1Results.push(...phase1Resolved);
|
|
||||||
|
|
||||||
console.log(' Phase 2: Error injection enabled...');
|
|
||||||
errorInjectionPhase = true;
|
|
||||||
|
|
||||||
const phase2Results = [];
|
|
||||||
const phase2Emails = emails.slice(8); // Remaining emails
|
|
||||||
|
|
||||||
const phase2Promises = phase2Emails.map(({ client, email, clientIndex, emailIndex }) => {
|
|
||||||
return client.sendMail(email).then(result => {
|
|
||||||
console.log(` ✓ Phase 2: Client ${clientIndex + 1} Email ${emailIndex + 1} recovered`);
|
|
||||||
return { success: true, phase: 2, clientIndex, emailIndex };
|
|
||||||
}).catch(error => {
|
|
||||||
console.log(` ✗ Phase 2: Client ${clientIndex + 1} Email ${emailIndex + 1} failed permanently`);
|
|
||||||
return { success: false, phase: 2, clientIndex, emailIndex, error: error.message };
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const phase2Resolved = await Promise.all(phase2Promises);
|
|
||||||
phase2Results.push(...phase2Resolved);
|
|
||||||
|
|
||||||
// Close all clients
|
|
||||||
clients.forEach(client => client.close());
|
|
||||||
|
|
||||||
const phase1Success = phase1Results.filter(r => r.success).length;
|
|
||||||
const phase2Success = phase2Results.filter(r => r.success).length;
|
|
||||||
const totalSuccess = phase1Success + phase2Success;
|
|
||||||
const totalEmails = emails.length;
|
|
||||||
|
|
||||||
console.log(`\n Concurrent error handling results:`);
|
|
||||||
console.log(` Phase 1 (normal): ${phase1Success}/${phase1Results.length} successful`);
|
|
||||||
console.log(` Phase 2 (errors): ${phase2Success}/${phase2Results.length} successful`);
|
|
||||||
console.log(` Overall success: ${totalSuccess}/${totalEmails} (${((totalSuccess / totalEmails) * 100).toFixed(1)}%)`);
|
|
||||||
console.log(` Error resilience: ${phase2Success > 0 ? 'Good' : 'Poor'}`);
|
|
||||||
console.log(` Concurrent error safety: ${phase1Success === phase1Results.length ? 'Maintained' : 'Some failures'}`);
|
|
||||||
|
|
||||||
expect(phase1Success).toBeGreaterThanOrEqual(phase1Results.length - 1); // Most should succeed
|
|
||||||
expect(phase2Success).toBeGreaterThanOrEqual(1); // Some should succeed despite errors
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
server.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CREL-06: Resource Contention Management', async () => {
|
|
||||||
console.log('\n🏁 Testing resource contention management...');
|
|
||||||
|
|
||||||
// Create test server with limited capacity
|
|
||||||
const server = net.createServer(socket => {
|
|
||||||
console.log(' [Server] New connection established');
|
|
||||||
|
|
||||||
socket.write('220 localhost SMTP Test Server\r\n');
|
|
||||||
|
|
||||||
// Add some delay to simulate slow server
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
const lines = data.toString().split('\r\n');
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
|
||||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
|
||||||
socket.write('250-localhost\r\n');
|
|
||||||
socket.write('250 SIZE 10485760\r\n');
|
|
||||||
} else if (line.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (line === 'DATA') {
|
|
||||||
socket.write('354 Send data\r\n');
|
|
||||||
} else if (line === '.') {
|
|
||||||
socket.write('250 OK Message accepted\r\n');
|
|
||||||
} else if (line === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, 20); // Add 20ms delay to responses
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
server.maxConnections = 3; // Limit server connections
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
server.listen(0, '127.0.0.1', () => {
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const port = (server.address() as net.AddressInfo).port;
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log(' Creating high-contention scenario with limited resources...');
|
|
||||||
const clients = [];
|
|
||||||
|
|
||||||
// Create more clients than server can handle simultaneously
|
|
||||||
for (let i = 0; i < 8; i++) {
|
|
||||||
clients.push(createTestSmtpClient({
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: port,
|
|
||||||
secure: false,
|
|
||||||
maxConnections: 1, // Force contention
|
|
||||||
maxMessages: 10,
|
|
||||||
connectionTimeout: 3000
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
const emails = [];
|
|
||||||
clients.forEach((client, clientIndex) => {
|
|
||||||
for (let emailIndex = 0; emailIndex < 4; emailIndex++) {
|
|
||||||
emails.push({
|
|
||||||
client,
|
|
||||||
email: new Email({
|
|
||||||
from: `sender${clientIndex}@contention.test`,
|
|
||||||
to: [`recipient${clientIndex}-${emailIndex}@contention.test`],
|
|
||||||
subject: `Resource Contention Test ${clientIndex + 1}-${emailIndex + 1}`,
|
|
||||||
text: `Testing resource contention management ${clientIndex + 1}-${emailIndex + 1}`
|
|
||||||
}),
|
|
||||||
clientIndex,
|
|
||||||
emailIndex
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(' Launching high-contention operations...');
|
|
||||||
const startTime = Date.now();
|
|
||||||
const promises = emails.map(({ client, email, clientIndex, emailIndex }) => {
|
|
||||||
return client.sendMail(email).then(result => {
|
|
||||||
console.log(` ✓ Client ${clientIndex + 1} Email ${emailIndex + 1} sent`);
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
clientIndex,
|
|
||||||
emailIndex,
|
|
||||||
completionTime: Date.now() - startTime
|
|
||||||
};
|
|
||||||
}).catch(error => {
|
|
||||||
console.log(` ✗ Client ${clientIndex + 1} Email ${emailIndex + 1} failed: ${error.message}`);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
clientIndex,
|
|
||||||
emailIndex,
|
|
||||||
error: error.message,
|
|
||||||
completionTime: Date.now() - startTime
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const results = await Promise.all(promises);
|
|
||||||
const endTime = Date.now();
|
|
||||||
|
|
||||||
// Close all clients
|
|
||||||
clients.forEach(client => client.close());
|
|
||||||
|
|
||||||
const successful = results.filter(r => r.success).length;
|
|
||||||
const failed = results.filter(r => !r.success).length;
|
|
||||||
const avgCompletionTime = results
|
|
||||||
.filter(r => r.success)
|
|
||||||
.reduce((sum, r) => sum + r.completionTime, 0) / successful || 0;
|
|
||||||
|
|
||||||
console.log(`\n Resource contention results:`);
|
|
||||||
console.log(` Total operations: ${emails.length}`);
|
|
||||||
console.log(` Successful: ${successful}, Failed: ${failed}`);
|
|
||||||
console.log(` Success rate: ${((successful / emails.length) * 100).toFixed(1)}%`);
|
|
||||||
console.log(` Total execution time: ${endTime - startTime}ms`);
|
|
||||||
console.log(` Average completion time: ${avgCompletionTime.toFixed(0)}ms`);
|
|
||||||
console.log(` Resource management: ${successful > emails.length * 0.8 ? 'Effective' : 'Needs improvement'}`);
|
|
||||||
|
|
||||||
expect(successful).toBeGreaterThanOrEqual(emails.length * 0.7); // At least 70% should succeed
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
server.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CREL-06: Test Summary', async () => {
|
|
||||||
console.log('\n✅ CREL-06: Concurrent Operation Safety Reliability Tests completed');
|
|
||||||
console.log('⚡ All concurrency safety scenarios tested successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.start();
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { createTestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createTestSmtpClient } from '../../helpers/smtp.client.js';
|
|
||||||
|
|
||||||
tap.test('CREL-07: Resource Cleanup Tests', async () => {
|
|
||||||
console.log('\n🧹 Testing SMTP Client Resource Cleanup');
|
|
||||||
console.log('=' .repeat(60));
|
|
||||||
|
|
||||||
const testServer = await createTestServer({});
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log('\nTest 1: Basic client creation and cleanup');
|
|
||||||
|
|
||||||
// Create a client
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port
|
|
||||||
});
|
|
||||||
console.log(' ✓ Client created');
|
|
||||||
|
|
||||||
// Verify connection
|
|
||||||
try {
|
|
||||||
const verifyResult = await smtpClient.verify();
|
|
||||||
console.log(' ✓ Connection verified:', verifyResult);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(' ⚠️ Verify failed:', error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the client
|
|
||||||
smtpClient.close();
|
|
||||||
console.log(' ✓ Client closed');
|
|
||||||
|
|
||||||
console.log('\nTest 2: Multiple close calls');
|
|
||||||
const testClient = createTestSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close multiple times - should not throw
|
|
||||||
testClient.close();
|
|
||||||
testClient.close();
|
|
||||||
testClient.close();
|
|
||||||
console.log(' ✓ Multiple close calls handled safely');
|
|
||||||
|
|
||||||
console.log('\n✅ CREL-07: Resource cleanup tests completed');
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
testServer.server.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.start();
|
|
||||||
@@ -1,283 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
|
||||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
let smtpClient: SmtpClient;
|
|
||||||
|
|
||||||
tap.test('setup - start SMTP server for RFC 5321 compliance tests', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2590,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(testServer.port).toEqual(2590);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CRFC-01: RFC 5321 §3.1 - Client MUST send EHLO/HELO first', async () => {
|
|
||||||
smtpClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
domain: 'client.example.com',
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// verify() establishes connection and sends EHLO
|
|
||||||
const isConnected = await smtpClient.verify();
|
|
||||||
expect(isConnected).toBeTrue();
|
|
||||||
|
|
||||||
console.log('✅ RFC 5321 §3.1: Client sends EHLO as first command');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CRFC-01: RFC 5321 §3.2 - Client MUST use CRLF line endings', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'CRLF Test',
|
|
||||||
text: 'Line 1\nLine 2\nLine 3' // LF only in input
|
|
||||||
});
|
|
||||||
|
|
||||||
// Client should convert to CRLF for transmission
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ RFC 5321 §3.2: Client converts line endings to CRLF');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CRFC-01: RFC 5321 §4.1.1.1 - EHLO parameter MUST be valid domain', async () => {
|
|
||||||
const domainClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
domain: 'valid-domain.example.com', // Valid domain format
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const isConnected = await domainClient.verify();
|
|
||||||
expect(isConnected).toBeTrue();
|
|
||||||
|
|
||||||
await domainClient.close();
|
|
||||||
console.log('✅ RFC 5321 §4.1.1.1: EHLO uses valid domain name');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CRFC-01: RFC 5321 §4.1.1.2 - Client MUST handle HELO fallback', async () => {
|
|
||||||
// Modern servers support EHLO, but client must be able to fall back
|
|
||||||
const heloClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const isConnected = await heloClient.verify();
|
|
||||||
expect(isConnected).toBeTrue();
|
|
||||||
|
|
||||||
await heloClient.close();
|
|
||||||
console.log('✅ RFC 5321 §4.1.1.2: Client supports HELO fallback capability');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CRFC-01: RFC 5321 §4.1.1.4 - MAIL FROM MUST use angle brackets', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'MAIL FROM Format Test',
|
|
||||||
text: 'Testing MAIL FROM command format'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Client should format as MAIL FROM:<sender@example.com>
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
expect(result.envelope?.from).toEqual('sender@example.com');
|
|
||||||
|
|
||||||
console.log('✅ RFC 5321 §4.1.1.4: MAIL FROM uses angle bracket format');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CRFC-01: RFC 5321 §4.1.1.5 - RCPT TO MUST use angle brackets', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient1@example.com', 'recipient2@example.com'],
|
|
||||||
subject: 'RCPT TO Format Test',
|
|
||||||
text: 'Testing RCPT TO command format'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Client should format as RCPT TO:<recipient@example.com>
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
expect(result.acceptedRecipients.length).toEqual(2);
|
|
||||||
|
|
||||||
console.log('✅ RFC 5321 §4.1.1.5: RCPT TO uses angle bracket format');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CRFC-01: RFC 5321 §4.1.1.9 - DATA termination sequence', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'DATA Termination Test',
|
|
||||||
text: 'This tests the <CRLF>.<CRLF> termination sequence'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Client MUST terminate DATA with <CRLF>.<CRLF>
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ RFC 5321 §4.1.1.9: DATA terminated with <CRLF>.<CRLF>');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CRFC-01: RFC 5321 §4.1.1.10 - QUIT command usage', async () => {
|
|
||||||
// Create new client for clean test
|
|
||||||
const quitClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
await quitClient.verify();
|
|
||||||
|
|
||||||
// Client SHOULD send QUIT before closing
|
|
||||||
await quitClient.close();
|
|
||||||
|
|
||||||
console.log('✅ RFC 5321 §4.1.1.10: Client sends QUIT before closing');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CRFC-01: RFC 5321 §4.5.3.1.1 - Line length limit (998 chars)', async () => {
|
|
||||||
// Create a line with 995 characters (leaving room for CRLF)
|
|
||||||
const longLine = 'a'.repeat(995);
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Long Line Test',
|
|
||||||
text: `Short line\n${longLine}\nAnother short line`
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ RFC 5321 §4.5.3.1.1: Lines limited to 998 characters');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CRFC-01: RFC 5321 §4.5.3.1.2 - Dot stuffing implementation', async () => {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Dot Stuffing Test',
|
|
||||||
text: '.This line starts with a dot\n..This has two dots\n...This has three'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Client MUST add extra dot to lines starting with dot
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
console.log('✅ RFC 5321 §4.5.3.1.2: Dot stuffing implemented correctly');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CRFC-01: RFC 5321 §5.1 - Reply code handling', async () => {
|
|
||||||
// Test various reply code scenarios
|
|
||||||
const scenarios = [
|
|
||||||
{
|
|
||||||
email: new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Success Test',
|
|
||||||
text: 'Should succeed'
|
|
||||||
}),
|
|
||||||
expectSuccess: true
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const scenario of scenarios) {
|
|
||||||
const result = await smtpClient.sendMail(scenario.email);
|
|
||||||
expect(result.success).toEqual(scenario.expectSuccess);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✅ RFC 5321 §5.1: Client handles reply codes correctly');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CRFC-01: RFC 5321 §4.1.4 - Order of commands', async () => {
|
|
||||||
// Commands must be in order: EHLO, MAIL, RCPT, DATA
|
|
||||||
const orderClient = createSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Command Order Test',
|
|
||||||
text: 'Testing proper command sequence'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await orderClient.sendMail(email);
|
|
||||||
|
|
||||||
expect(result.success).toBeTrue();
|
|
||||||
|
|
||||||
await orderClient.close();
|
|
||||||
console.log('✅ RFC 5321 §4.1.4: Commands sent in correct order');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CRFC-01: RFC 5321 §4.2.1 - Reply code categories', async () => {
|
|
||||||
// Client must understand reply code categories:
|
|
||||||
// 2xx = Success
|
|
||||||
// 3xx = Intermediate
|
|
||||||
// 4xx = Temporary failure
|
|
||||||
// 5xx = Permanent failure
|
|
||||||
|
|
||||||
console.log('✅ RFC 5321 §4.2.1: Client understands reply code categories');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CRFC-01: RFC 5321 §4.1.1.4 - Null reverse-path handling', async () => {
|
|
||||||
// Test bounce message with null sender
|
|
||||||
try {
|
|
||||||
const bounceEmail = new Email({
|
|
||||||
from: '<>', // Null reverse-path
|
|
||||||
to: 'postmaster@example.com',
|
|
||||||
subject: 'Bounce Message',
|
|
||||||
text: 'This is a bounce notification'
|
|
||||||
});
|
|
||||||
|
|
||||||
await smtpClient.sendMail(bounceEmail);
|
|
||||||
console.log('✅ RFC 5321 §4.1.1.4: Null reverse-path handled');
|
|
||||||
} catch (error) {
|
|
||||||
// Email class might reject empty from
|
|
||||||
console.log('ℹ️ Email class enforces non-empty sender');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CRFC-01: RFC 5321 §2.3.5 - Domain literals', async () => {
|
|
||||||
// Test IP address literal
|
|
||||||
try {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@[127.0.0.1]',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'Domain Literal Test',
|
|
||||||
text: 'Testing IP literal in email address'
|
|
||||||
});
|
|
||||||
|
|
||||||
await smtpClient.sendMail(email);
|
|
||||||
console.log('✅ RFC 5321 §2.3.5: Domain literals supported');
|
|
||||||
} catch (error) {
|
|
||||||
console.log('ℹ️ Domain literals not supported by Email class');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - close SMTP client', async () => {
|
|
||||||
if (smtpClient && smtpClient.isConnected()) {
|
|
||||||
await smtpClient.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup - stop SMTP server', async () => {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { createTestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createTestSmtpClient } from '../../helpers/smtp.client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
tap.test('CRFC-02: Basic ESMTP Compliance', async () => {
|
|
||||||
console.log('\n📧 Testing SMTP Client ESMTP Compliance');
|
|
||||||
console.log('=' .repeat(60));
|
|
||||||
|
|
||||||
const testServer = await createTestServer({});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('\nTest 1: Basic EHLO negotiation');
|
|
||||||
const email1 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'ESMTP test',
|
|
||||||
text: 'Testing ESMTP'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result1 = await smtpClient.sendMail(email1);
|
|
||||||
console.log(' ✓ EHLO negotiation successful');
|
|
||||||
expect(result1).toBeDefined();
|
|
||||||
|
|
||||||
console.log('\nTest 2: Multiple recipients');
|
|
||||||
const email2 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient1@example.com', 'recipient2@example.com'],
|
|
||||||
cc: ['cc@example.com'],
|
|
||||||
bcc: ['bcc@example.com'],
|
|
||||||
subject: 'Multiple recipients',
|
|
||||||
text: 'Testing multiple recipients'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result2 = await smtpClient.sendMail(email2);
|
|
||||||
console.log(' ✓ Multiple recipients handled');
|
|
||||||
expect(result2).toBeDefined();
|
|
||||||
|
|
||||||
console.log('\nTest 3: UTF-8 content');
|
|
||||||
const email3 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'UTF-8: café ☕ 测试',
|
|
||||||
text: 'International text: émojis 🎉, 日本語',
|
|
||||||
html: '<p>HTML: <strong>Zürich</strong></p>'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result3 = await smtpClient.sendMail(email3);
|
|
||||||
console.log(' ✓ UTF-8 content accepted');
|
|
||||||
expect(result3).toBeDefined();
|
|
||||||
|
|
||||||
console.log('\nTest 4: Long headers');
|
|
||||||
const longSubject = 'This is a very long subject line that exceeds 78 characters and should be properly folded according to RFC 2822';
|
|
||||||
const email4 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: longSubject,
|
|
||||||
text: 'Testing header folding'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result4 = await smtpClient.sendMail(email4);
|
|
||||||
console.log(' ✓ Long headers handled');
|
|
||||||
expect(result4).toBeDefined();
|
|
||||||
|
|
||||||
console.log('\n✅ CRFC-02: ESMTP compliance tests completed');
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
testServer.server.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.start();
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { createTestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createTestSmtpClient } from '../../helpers/smtp.client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
tap.test('CRFC-03: SMTP Command Syntax Compliance', async () => {
|
|
||||||
console.log('\n📧 Testing SMTP Client Command Syntax Compliance');
|
|
||||||
console.log('=' .repeat(60));
|
|
||||||
|
|
||||||
const testServer = await createTestServer({});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('\nTest 1: Valid email addresses');
|
|
||||||
const email1 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Valid email test',
|
|
||||||
text: 'Testing valid email addresses'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result1 = await smtpClient.sendMail(email1);
|
|
||||||
console.log(' ✓ Valid email addresses accepted');
|
|
||||||
expect(result1).toBeDefined();
|
|
||||||
|
|
||||||
console.log('\nTest 2: Email with display names');
|
|
||||||
const email2 = new Email({
|
|
||||||
from: 'Test Sender <sender@example.com>',
|
|
||||||
to: ['Test Recipient <recipient@example.com>'],
|
|
||||||
subject: 'Display name test',
|
|
||||||
text: 'Testing email addresses with display names'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result2 = await smtpClient.sendMail(email2);
|
|
||||||
console.log(' ✓ Display names handled correctly');
|
|
||||||
expect(result2).toBeDefined();
|
|
||||||
|
|
||||||
console.log('\nTest 3: Multiple recipients');
|
|
||||||
const email3 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['user1@example.com', 'user2@example.com'],
|
|
||||||
cc: ['cc@example.com'],
|
|
||||||
subject: 'Multiple recipients test',
|
|
||||||
text: 'Testing RCPT TO command with multiple recipients'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result3 = await smtpClient.sendMail(email3);
|
|
||||||
console.log(' ✓ Multiple RCPT TO commands sent correctly');
|
|
||||||
expect(result3).toBeDefined();
|
|
||||||
|
|
||||||
console.log('\nTest 4: Connection test (HELO/EHLO)');
|
|
||||||
const verified = await smtpClient.verify();
|
|
||||||
console.log(' ✓ HELO/EHLO command syntax correct');
|
|
||||||
expect(verified).toBeDefined();
|
|
||||||
|
|
||||||
console.log('\n✅ CRFC-03: Command syntax compliance tests completed');
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
testServer.server.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.start();
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { createTestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createTestSmtpClient } from '../../helpers/smtp.client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
tap.test('CRFC-04: SMTP Response Code Handling', async () => {
|
|
||||||
console.log('\n📧 Testing SMTP Client Response Code Handling');
|
|
||||||
console.log('=' .repeat(60));
|
|
||||||
|
|
||||||
const testServer = await createTestServer({});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('\nTest 1: Successful email (2xx responses)');
|
|
||||||
const email1 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Success test',
|
|
||||||
text: 'Testing successful response codes'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result1 = await smtpClient.sendMail(email1);
|
|
||||||
console.log(' ✓ 2xx response codes handled correctly');
|
|
||||||
expect(result1).toBeDefined();
|
|
||||||
|
|
||||||
console.log('\nTest 2: Verify connection');
|
|
||||||
const verified = await smtpClient.verify();
|
|
||||||
console.log(' ✓ Connection verification successful');
|
|
||||||
expect(verified).toBeDefined();
|
|
||||||
|
|
||||||
console.log('\nTest 3: Multiple recipients (multiple 250 responses)');
|
|
||||||
const email2 = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['user1@example.com', 'user2@example.com', 'user3@example.com'],
|
|
||||||
subject: 'Multiple recipients',
|
|
||||||
text: 'Testing multiple positive responses'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result2 = await smtpClient.sendMail(email2);
|
|
||||||
console.log(' ✓ Multiple positive responses handled');
|
|
||||||
expect(result2).toBeDefined();
|
|
||||||
|
|
||||||
console.log('\n✅ CRFC-04: Response code handling tests completed');
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
testServer.server.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.start();
|
|
||||||
@@ -1,703 +0,0 @@
|
|||||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { createTestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createTestSmtpClient } from '../../helpers/smtp.client.js';
|
|
||||||
import { Email } from '../../../ts/index.js';
|
|
||||||
|
|
||||||
tap.test('CRFC-05: should comply with SMTP state machine (RFC 5321)', async (tools) => {
|
|
||||||
const testId = 'CRFC-05-state-machine';
|
|
||||||
console.log(`\n${testId}: Testing SMTP state machine compliance...`);
|
|
||||||
|
|
||||||
let scenarioCount = 0;
|
|
||||||
|
|
||||||
// Scenario 1: Initial state and greeting
|
|
||||||
await (async () => {
|
|
||||||
scenarioCount++;
|
|
||||||
console.log(`\nScenario ${scenarioCount}: Testing initial state and greeting`);
|
|
||||||
|
|
||||||
const testServer = await createTestServer({
|
|
||||||
onConnection: async (socket) => {
|
|
||||||
console.log(' [Server] Client connected - Initial state');
|
|
||||||
|
|
||||||
let state = 'initial';
|
|
||||||
|
|
||||||
// Send greeting immediately upon connection
|
|
||||||
socket.write('220 statemachine.example.com ESMTP Service ready\r\n');
|
|
||||||
state = 'greeting-sent';
|
|
||||||
console.log(' [Server] State: initial -> greeting-sent');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
console.log(` [Server] State: ${state}, Received: ${command}`);
|
|
||||||
|
|
||||||
if (state === 'greeting-sent') {
|
|
||||||
if (command.startsWith('EHLO') || command.startsWith('HELO')) {
|
|
||||||
socket.write('250 statemachine.example.com\r\n');
|
|
||||||
state = 'ready';
|
|
||||||
console.log(' [Server] State: greeting-sent -> ready');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
} else {
|
|
||||||
socket.write('503 5.5.1 Bad sequence of commands\r\n');
|
|
||||||
}
|
|
||||||
} else if (state === 'ready') {
|
|
||||||
if (command.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
state = 'mail';
|
|
||||||
console.log(' [Server] State: ready -> mail');
|
|
||||||
} else if (command.startsWith('EHLO') || command.startsWith('HELO')) {
|
|
||||||
socket.write('250 statemachine.example.com\r\n');
|
|
||||||
// Stay in ready state
|
|
||||||
} else if (command === 'RSET' || command === 'NOOP') {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
// Stay in ready state
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
} else {
|
|
||||||
socket.write('503 5.5.1 Bad sequence of commands\r\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Just establish connection and send EHLO
|
|
||||||
try {
|
|
||||||
await smtpClient.verify();
|
|
||||||
console.log(' Initial state transition (connect -> EHLO) successful');
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` Connection/EHLO failed: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await testServer.server.close();
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Scenario 2: Transaction state machine
|
|
||||||
await (async () => {
|
|
||||||
scenarioCount++;
|
|
||||||
console.log(`\nScenario ${scenarioCount}: Testing transaction state machine`);
|
|
||||||
|
|
||||||
const testServer = await createTestServer({
|
|
||||||
onConnection: async (socket) => {
|
|
||||||
console.log(' [Server] Client connected');
|
|
||||||
socket.write('220 statemachine.example.com ESMTP\r\n');
|
|
||||||
|
|
||||||
let state = 'ready';
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
console.log(` [Server] State: ${state}, Command: ${command}`);
|
|
||||||
|
|
||||||
switch (state) {
|
|
||||||
case 'ready':
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250 statemachine.example.com\r\n');
|
|
||||||
// Stay in ready
|
|
||||||
} else if (command.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
state = 'mail';
|
|
||||||
console.log(' [Server] State: ready -> mail');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
} else {
|
|
||||||
socket.write('503 5.5.1 Bad sequence of commands\r\n');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'mail':
|
|
||||||
if (command.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
state = 'rcpt';
|
|
||||||
console.log(' [Server] State: mail -> rcpt');
|
|
||||||
} else if (command === 'RSET') {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
state = 'ready';
|
|
||||||
console.log(' [Server] State: mail -> ready (RSET)');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
} else {
|
|
||||||
socket.write('503 5.5.1 Bad sequence of commands\r\n');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'rcpt':
|
|
||||||
if (command.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
// Stay in rcpt (can have multiple recipients)
|
|
||||||
} else if (command === 'DATA') {
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
state = 'data';
|
|
||||||
console.log(' [Server] State: rcpt -> data');
|
|
||||||
} else if (command === 'RSET') {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
state = 'ready';
|
|
||||||
console.log(' [Server] State: rcpt -> ready (RSET)');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
} else {
|
|
||||||
socket.write('503 5.5.1 Bad sequence of commands\r\n');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'data':
|
|
||||||
if (command === '.') {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
state = 'ready';
|
|
||||||
console.log(' [Server] State: data -> ready (message complete)');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
// QUIT is not allowed during DATA
|
|
||||||
socket.write('503 5.5.1 Bad sequence of commands\r\n');
|
|
||||||
}
|
|
||||||
// All other input during DATA is message content
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient1@example.com', 'recipient2@example.com'],
|
|
||||||
subject: 'State machine test',
|
|
||||||
text: 'Testing SMTP transaction state machine'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
console.log(' Complete transaction state sequence successful');
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.messageId).toBeDefined();
|
|
||||||
|
|
||||||
await testServer.server.close();
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Scenario 3: Invalid state transitions
|
|
||||||
await (async () => {
|
|
||||||
scenarioCount++;
|
|
||||||
console.log(`\nScenario ${scenarioCount}: Testing invalid state transitions`);
|
|
||||||
|
|
||||||
const testServer = await createTestServer({
|
|
||||||
onConnection: async (socket) => {
|
|
||||||
console.log(' [Server] Client connected');
|
|
||||||
socket.write('220 statemachine.example.com ESMTP\r\n');
|
|
||||||
|
|
||||||
let state = 'ready';
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
console.log(` [Server] State: ${state}, Command: ${command}`);
|
|
||||||
|
|
||||||
// Strictly enforce state machine
|
|
||||||
switch (state) {
|
|
||||||
case 'ready':
|
|
||||||
if (command.startsWith('EHLO') || command.startsWith('HELO')) {
|
|
||||||
socket.write('250 statemachine.example.com\r\n');
|
|
||||||
} else if (command.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
state = 'mail';
|
|
||||||
} else if (command === 'RSET' || command === 'NOOP') {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
} else if (command.startsWith('RCPT TO:')) {
|
|
||||||
console.log(' [Server] RCPT TO without MAIL FROM');
|
|
||||||
socket.write('503 5.5.1 Need MAIL command first\r\n');
|
|
||||||
} else if (command === 'DATA') {
|
|
||||||
console.log(' [Server] DATA without MAIL FROM and RCPT TO');
|
|
||||||
socket.write('503 5.5.1 Need MAIL and RCPT commands first\r\n');
|
|
||||||
} else {
|
|
||||||
socket.write('503 5.5.1 Bad sequence of commands\r\n');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'mail':
|
|
||||||
if (command.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
state = 'rcpt';
|
|
||||||
} else if (command.startsWith('MAIL FROM:')) {
|
|
||||||
console.log(' [Server] Second MAIL FROM without RSET');
|
|
||||||
socket.write('503 5.5.1 Sender already specified\r\n');
|
|
||||||
} else if (command === 'DATA') {
|
|
||||||
console.log(' [Server] DATA without RCPT TO');
|
|
||||||
socket.write('503 5.5.1 Need RCPT command first\r\n');
|
|
||||||
} else if (command === 'RSET') {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
state = 'ready';
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
} else {
|
|
||||||
socket.write('503 5.5.1 Bad sequence of commands\r\n');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'rcpt':
|
|
||||||
if (command.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'DATA') {
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
state = 'data';
|
|
||||||
} else if (command.startsWith('MAIL FROM:')) {
|
|
||||||
console.log(' [Server] MAIL FROM after RCPT TO without RSET');
|
|
||||||
socket.write('503 5.5.1 Sender already specified\r\n');
|
|
||||||
} else if (command === 'RSET') {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
state = 'ready';
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
} else {
|
|
||||||
socket.write('503 5.5.1 Bad sequence of commands\r\n');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'data':
|
|
||||||
if (command === '.') {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
state = 'ready';
|
|
||||||
} else if (command.startsWith('MAIL FROM:') ||
|
|
||||||
command.startsWith('RCPT TO:') ||
|
|
||||||
command === 'RSET') {
|
|
||||||
console.log(' [Server] SMTP command during DATA mode');
|
|
||||||
socket.write('503 5.5.1 Commands not allowed during data transfer\r\n');
|
|
||||||
}
|
|
||||||
// During DATA, most input is treated as message content
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// We'll create a custom client to send invalid command sequences
|
|
||||||
const testCases = [
|
|
||||||
{
|
|
||||||
name: 'RCPT without MAIL',
|
|
||||||
commands: ['EHLO client.example.com', 'RCPT TO:<test@example.com>'],
|
|
||||||
expectError: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'DATA without RCPT',
|
|
||||||
commands: ['EHLO client.example.com', 'MAIL FROM:<sender@example.com>', 'DATA'],
|
|
||||||
expectError: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Double MAIL FROM',
|
|
||||||
commands: ['EHLO client.example.com', 'MAIL FROM:<sender1@example.com>', 'MAIL FROM:<sender2@example.com>'],
|
|
||||||
expectError: true
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const testCase of testCases) {
|
|
||||||
console.log(` Testing: ${testCase.name}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Create simple socket connection for manual command testing
|
|
||||||
const net = await import('net');
|
|
||||||
const client = net.createConnection(testServer.port, testServer.hostname);
|
|
||||||
|
|
||||||
let responseCount = 0;
|
|
||||||
let errorReceived = false;
|
|
||||||
|
|
||||||
client.on('data', (data) => {
|
|
||||||
const response = data.toString();
|
|
||||||
console.log(` Response: ${response.trim()}`);
|
|
||||||
|
|
||||||
if (response.startsWith('5')) {
|
|
||||||
errorReceived = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
responseCount++;
|
|
||||||
|
|
||||||
if (responseCount <= testCase.commands.length) {
|
|
||||||
const command = testCase.commands[responseCount - 1];
|
|
||||||
if (command) {
|
|
||||||
setTimeout(() => {
|
|
||||||
console.log(` Sending: ${command}`);
|
|
||||||
client.write(command + '\r\n');
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
client.write('QUIT\r\n');
|
|
||||||
client.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
client.on('end', () => {
|
|
||||||
if (testCase.expectError && errorReceived) {
|
|
||||||
console.log(` ✓ Expected error received`);
|
|
||||||
} else if (!testCase.expectError && !errorReceived) {
|
|
||||||
console.log(` ✓ No error as expected`);
|
|
||||||
} else {
|
|
||||||
console.log(` ✗ Unexpected result`);
|
|
||||||
}
|
|
||||||
resolve(void 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on('error', reject);
|
|
||||||
|
|
||||||
// Start with greeting response
|
|
||||||
setTimeout(() => {
|
|
||||||
if (testCase.commands.length > 0) {
|
|
||||||
console.log(` Sending: ${testCase.commands[0]}`);
|
|
||||||
client.write(testCase.commands[0] + '\r\n');
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` Error testing ${testCase.name}: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await testServer.server.close();
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Scenario 4: RSET command state transitions
|
|
||||||
await (async () => {
|
|
||||||
scenarioCount++;
|
|
||||||
console.log(`\nScenario ${scenarioCount}: Testing RSET command state transitions`);
|
|
||||||
|
|
||||||
const testServer = await createTestServer({
|
|
||||||
onConnection: async (socket) => {
|
|
||||||
console.log(' [Server] Client connected');
|
|
||||||
socket.write('220 statemachine.example.com ESMTP\r\n');
|
|
||||||
|
|
||||||
let state = 'ready';
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
console.log(` [Server] State: ${state}, Command: ${command}`);
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250 statemachine.example.com\r\n');
|
|
||||||
state = 'ready';
|
|
||||||
} else if (command.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
state = 'mail';
|
|
||||||
} else if (command.startsWith('RCPT TO:')) {
|
|
||||||
if (state === 'mail' || state === 'rcpt') {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
state = 'rcpt';
|
|
||||||
} else {
|
|
||||||
socket.write('503 5.5.1 Bad sequence of commands\r\n');
|
|
||||||
}
|
|
||||||
} else if (command === 'RSET') {
|
|
||||||
console.log(` [Server] RSET from state: ${state} -> ready`);
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
state = 'ready';
|
|
||||||
} else if (command === 'DATA') {
|
|
||||||
if (state === 'rcpt') {
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
state = 'data';
|
|
||||||
} else {
|
|
||||||
socket.write('503 5.5.1 Bad sequence of commands\r\n');
|
|
||||||
}
|
|
||||||
} else if (command === '.') {
|
|
||||||
if (state === 'data') {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
state = 'ready';
|
|
||||||
}
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
} else if (command === 'NOOP') {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test RSET at various points in transaction
|
|
||||||
console.log(' Testing RSET from different states...');
|
|
||||||
|
|
||||||
// We'll manually test RSET behavior
|
|
||||||
const net = await import('net');
|
|
||||||
const client = net.createConnection(testServer.port, testServer.hostname);
|
|
||||||
|
|
||||||
const commands = [
|
|
||||||
'EHLO client.example.com', // -> ready
|
|
||||||
'MAIL FROM:<sender@example.com>', // -> mail
|
|
||||||
'RSET', // -> ready (reset from mail state)
|
|
||||||
'MAIL FROM:<sender2@example.com>', // -> mail
|
|
||||||
'RCPT TO:<rcpt1@example.com>', // -> rcpt
|
|
||||||
'RCPT TO:<rcpt2@example.com>', // -> rcpt (multiple recipients)
|
|
||||||
'RSET', // -> ready (reset from rcpt state)
|
|
||||||
'MAIL FROM:<sender3@example.com>', // -> mail (fresh transaction)
|
|
||||||
'RCPT TO:<rcpt3@example.com>', // -> rcpt
|
|
||||||
'DATA', // -> data
|
|
||||||
'.', // -> ready (complete transaction)
|
|
||||||
'QUIT'
|
|
||||||
];
|
|
||||||
|
|
||||||
let commandIndex = 0;
|
|
||||||
|
|
||||||
client.on('data', (data) => {
|
|
||||||
const response = data.toString().trim();
|
|
||||||
console.log(` Response: ${response}`);
|
|
||||||
|
|
||||||
if (commandIndex < commands.length) {
|
|
||||||
setTimeout(() => {
|
|
||||||
const command = commands[commandIndex];
|
|
||||||
console.log(` Sending: ${command}`);
|
|
||||||
if (command === 'DATA') {
|
|
||||||
client.write(command + '\r\n');
|
|
||||||
// Send message content immediately after DATA
|
|
||||||
setTimeout(() => {
|
|
||||||
client.write('Subject: RSET test\r\n\r\nTesting RSET state transitions.\r\n.\r\n');
|
|
||||||
}, 100);
|
|
||||||
} else {
|
|
||||||
client.write(command + '\r\n');
|
|
||||||
}
|
|
||||||
commandIndex++;
|
|
||||||
}, 100);
|
|
||||||
} else {
|
|
||||||
client.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
client.on('end', () => {
|
|
||||||
console.log(' RSET state transitions completed successfully');
|
|
||||||
resolve(void 0);
|
|
||||||
});
|
|
||||||
client.on('error', reject);
|
|
||||||
});
|
|
||||||
|
|
||||||
await testServer.server.close();
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Scenario 5: Connection state persistence
|
|
||||||
await (async () => {
|
|
||||||
scenarioCount++;
|
|
||||||
console.log(`\nScenario ${scenarioCount}: Testing connection state persistence`);
|
|
||||||
|
|
||||||
const testServer = await createTestServer({
|
|
||||||
onConnection: async (socket) => {
|
|
||||||
console.log(' [Server] Client connected');
|
|
||||||
socket.write('220 statemachine.example.com ESMTP\r\n');
|
|
||||||
|
|
||||||
let state = 'ready';
|
|
||||||
let messageCount = 0;
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250-statemachine.example.com\r\n');
|
|
||||||
socket.write('250 PIPELINING\r\n');
|
|
||||||
state = 'ready';
|
|
||||||
} else if (command.startsWith('MAIL FROM:')) {
|
|
||||||
if (state === 'ready') {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
state = 'mail';
|
|
||||||
} else {
|
|
||||||
socket.write('503 5.5.1 Bad sequence\r\n');
|
|
||||||
}
|
|
||||||
} else if (command.startsWith('RCPT TO:')) {
|
|
||||||
if (state === 'mail' || state === 'rcpt') {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
state = 'rcpt';
|
|
||||||
} else {
|
|
||||||
socket.write('503 5.5.1 Bad sequence\r\n');
|
|
||||||
}
|
|
||||||
} else if (command === 'DATA') {
|
|
||||||
if (state === 'rcpt') {
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
state = 'data';
|
|
||||||
} else {
|
|
||||||
socket.write('503 5.5.1 Bad sequence\r\n');
|
|
||||||
}
|
|
||||||
} else if (command === '.') {
|
|
||||||
if (state === 'data') {
|
|
||||||
messageCount++;
|
|
||||||
console.log(` [Server] Message ${messageCount} completed`);
|
|
||||||
socket.write(`250 OK: Message ${messageCount} accepted\r\n`);
|
|
||||||
state = 'ready';
|
|
||||||
}
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
console.log(` [Server] Session ended after ${messageCount} messages`);
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
pool: true,
|
|
||||||
maxConnections: 1
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send multiple emails through same connection
|
|
||||||
for (let i = 1; i <= 3; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: [`recipient${i}@example.com`],
|
|
||||||
subject: `Persistence test ${i}`,
|
|
||||||
text: `Testing connection state persistence - message ${i}`
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
console.log(` Message ${i} sent successfully`);
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.response).toContain(`Message ${i}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the pooled connection
|
|
||||||
await smtpClient.close();
|
|
||||||
await testServer.server.close();
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Scenario 6: Error state recovery
|
|
||||||
await (async () => {
|
|
||||||
scenarioCount++;
|
|
||||||
console.log(`\nScenario ${scenarioCount}: Testing error state recovery`);
|
|
||||||
|
|
||||||
const testServer = await createTestServer({
|
|
||||||
onConnection: async (socket) => {
|
|
||||||
console.log(' [Server] Client connected');
|
|
||||||
socket.write('220 statemachine.example.com ESMTP\r\n');
|
|
||||||
|
|
||||||
let state = 'ready';
|
|
||||||
let errorCount = 0;
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
console.log(` [Server] State: ${state}, Command: ${command}`);
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250 statemachine.example.com\r\n');
|
|
||||||
state = 'ready';
|
|
||||||
errorCount = 0; // Reset error count on new session
|
|
||||||
} else if (command.startsWith('MAIL FROM:')) {
|
|
||||||
const address = command.match(/<(.+)>/)?.[1] || '';
|
|
||||||
if (address.includes('error')) {
|
|
||||||
errorCount++;
|
|
||||||
console.log(` [Server] Error ${errorCount} - invalid sender`);
|
|
||||||
socket.write('550 5.1.8 Invalid sender address\r\n');
|
|
||||||
// State remains ready after error
|
|
||||||
} else {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
state = 'mail';
|
|
||||||
}
|
|
||||||
} else if (command.startsWith('RCPT TO:')) {
|
|
||||||
if (state === 'mail' || state === 'rcpt') {
|
|
||||||
const address = command.match(/<(.+)>/)?.[1] || '';
|
|
||||||
if (address.includes('error')) {
|
|
||||||
errorCount++;
|
|
||||||
console.log(` [Server] Error ${errorCount} - invalid recipient`);
|
|
||||||
socket.write('550 5.1.1 User unknown\r\n');
|
|
||||||
// State remains the same after recipient error
|
|
||||||
} else {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
state = 'rcpt';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
socket.write('503 5.5.1 Bad sequence\r\n');
|
|
||||||
}
|
|
||||||
} else if (command === 'DATA') {
|
|
||||||
if (state === 'rcpt') {
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
state = 'data';
|
|
||||||
} else {
|
|
||||||
socket.write('503 5.5.1 Bad sequence\r\n');
|
|
||||||
}
|
|
||||||
} else if (command === '.') {
|
|
||||||
if (state === 'data') {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
state = 'ready';
|
|
||||||
}
|
|
||||||
} else if (command === 'RSET') {
|
|
||||||
console.log(` [Server] RSET - recovering from errors (${errorCount} errors so far)`);
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
state = 'ready';
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
console.log(` [Server] Session ended with ${errorCount} total errors`);
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
} else {
|
|
||||||
socket.write('500 5.5.1 Command not recognized\r\n');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test recovery from various errors
|
|
||||||
const testEmails = [
|
|
||||||
{
|
|
||||||
from: 'error@example.com', // Will cause sender error
|
|
||||||
to: ['valid@example.com'],
|
|
||||||
desc: 'invalid sender'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
from: 'valid@example.com',
|
|
||||||
to: ['error@example.com', 'valid@example.com'], // Mixed valid/invalid recipients
|
|
||||||
desc: 'mixed recipients'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
from: 'valid@example.com',
|
|
||||||
to: ['valid@example.com'],
|
|
||||||
desc: 'valid email after errors'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const testEmail of testEmails) {
|
|
||||||
console.log(` Testing ${testEmail.desc}...`);
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: testEmail.from,
|
|
||||||
to: testEmail.to,
|
|
||||||
subject: `Error recovery test: ${testEmail.desc}`,
|
|
||||||
text: `Testing error state recovery with ${testEmail.desc}`
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
console.log(` ${testEmail.desc}: Success`);
|
|
||||||
if (result.rejected && result.rejected.length > 0) {
|
|
||||||
console.log(` Rejected: ${result.rejected.length} recipients`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ${testEmail.desc}: Failed as expected - ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await testServer.server.close();
|
|
||||||
})();
|
|
||||||
|
|
||||||
console.log(`\n${testId}: All ${scenarioCount} state machine scenarios tested ✓`);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.start();
|
|
||||||
@@ -1,688 +0,0 @@
|
|||||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { createTestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createTestSmtpClient } from '../../helpers/smtp.client.js';
|
|
||||||
import { Email } from '../../../ts/index.js';
|
|
||||||
|
|
||||||
tap.test('CRFC-06: should handle protocol negotiation correctly (RFC 5321)', async (tools) => {
|
|
||||||
const testId = 'CRFC-06-protocol-negotiation';
|
|
||||||
console.log(`\n${testId}: Testing SMTP protocol negotiation compliance...`);
|
|
||||||
|
|
||||||
let scenarioCount = 0;
|
|
||||||
|
|
||||||
// Scenario 1: EHLO capability announcement and selection
|
|
||||||
await (async () => {
|
|
||||||
scenarioCount++;
|
|
||||||
console.log(`\nScenario ${scenarioCount}: Testing EHLO capability announcement`);
|
|
||||||
|
|
||||||
const testServer = await createTestServer({
|
|
||||||
onConnection: async (socket) => {
|
|
||||||
console.log(' [Server] Client connected');
|
|
||||||
socket.write('220 negotiation.example.com ESMTP Service Ready\r\n');
|
|
||||||
|
|
||||||
let negotiatedCapabilities: string[] = [];
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
console.log(` [Server] Received: ${command}`);
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
// Announce available capabilities
|
|
||||||
socket.write('250-negotiation.example.com\r\n');
|
|
||||||
socket.write('250-SIZE 52428800\r\n');
|
|
||||||
socket.write('250-8BITMIME\r\n');
|
|
||||||
socket.write('250-STARTTLS\r\n');
|
|
||||||
socket.write('250-ENHANCEDSTATUSCODES\r\n');
|
|
||||||
socket.write('250-PIPELINING\r\n');
|
|
||||||
socket.write('250-CHUNKING\r\n');
|
|
||||||
socket.write('250-SMTPUTF8\r\n');
|
|
||||||
socket.write('250-DSN\r\n');
|
|
||||||
socket.write('250-AUTH PLAIN LOGIN CRAM-MD5\r\n');
|
|
||||||
socket.write('250 HELP\r\n');
|
|
||||||
|
|
||||||
negotiatedCapabilities = [
|
|
||||||
'SIZE', '8BITMIME', 'STARTTLS', 'ENHANCEDSTATUSCODES',
|
|
||||||
'PIPELINING', 'CHUNKING', 'SMTPUTF8', 'DSN', 'AUTH', 'HELP'
|
|
||||||
];
|
|
||||||
console.log(` [Server] Announced capabilities: ${negotiatedCapabilities.join(', ')}`);
|
|
||||||
} else if (command.startsWith('HELO')) {
|
|
||||||
// Basic SMTP mode - no capabilities
|
|
||||||
socket.write('250 negotiation.example.com\r\n');
|
|
||||||
negotiatedCapabilities = [];
|
|
||||||
console.log(' [Server] Basic SMTP mode (no capabilities)');
|
|
||||||
} else if (command.startsWith('MAIL FROM:')) {
|
|
||||||
// Check for SIZE parameter
|
|
||||||
const sizeMatch = command.match(/SIZE=(\d+)/i);
|
|
||||||
if (sizeMatch && negotiatedCapabilities.includes('SIZE')) {
|
|
||||||
const size = parseInt(sizeMatch[1]);
|
|
||||||
console.log(` [Server] SIZE parameter used: ${size} bytes`);
|
|
||||||
if (size > 52428800) {
|
|
||||||
socket.write('552 5.3.4 Message size exceeds maximum\r\n');
|
|
||||||
} else {
|
|
||||||
socket.write('250 2.1.0 Sender OK\r\n');
|
|
||||||
}
|
|
||||||
} else if (sizeMatch && !negotiatedCapabilities.includes('SIZE')) {
|
|
||||||
console.log(' [Server] SIZE parameter used without capability');
|
|
||||||
socket.write('501 5.5.4 SIZE not supported\r\n');
|
|
||||||
} else {
|
|
||||||
socket.write('250 2.1.0 Sender OK\r\n');
|
|
||||||
}
|
|
||||||
} else if (command.startsWith('RCPT TO:')) {
|
|
||||||
// Check for DSN parameters
|
|
||||||
if (command.includes('NOTIFY=') && negotiatedCapabilities.includes('DSN')) {
|
|
||||||
console.log(' [Server] DSN NOTIFY parameter used');
|
|
||||||
} else if (command.includes('NOTIFY=') && !negotiatedCapabilities.includes('DSN')) {
|
|
||||||
console.log(' [Server] DSN parameter used without capability');
|
|
||||||
socket.write('501 5.5.4 DSN not supported\r\n');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
socket.write('250 2.1.5 Recipient OK\r\n');
|
|
||||||
} else if (command === 'DATA') {
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
} else if (command === '.') {
|
|
||||||
socket.write('250 2.0.0 Message accepted\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 2.0.0 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test EHLO negotiation
|
|
||||||
const esmtpClient = createTestSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Capability negotiation test',
|
|
||||||
text: 'Testing EHLO capability announcement and usage'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await esmtpClient.sendMail(email);
|
|
||||||
console.log(' EHLO capability negotiation successful');
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
|
|
||||||
await testServer.server.close();
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Scenario 2: Capability-based feature usage
|
|
||||||
await (async () => {
|
|
||||||
scenarioCount++;
|
|
||||||
console.log(`\nScenario ${scenarioCount}: Testing capability-based feature usage`);
|
|
||||||
|
|
||||||
const testServer = await createTestServer({
|
|
||||||
onConnection: async (socket) => {
|
|
||||||
console.log(' [Server] Client connected');
|
|
||||||
socket.write('220 features.example.com ESMTP\r\n');
|
|
||||||
|
|
||||||
let supportsUTF8 = false;
|
|
||||||
let supportsPipelining = false;
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
console.log(` [Server] Received: ${command}`);
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250-features.example.com\r\n');
|
|
||||||
socket.write('250-SMTPUTF8\r\n');
|
|
||||||
socket.write('250-PIPELINING\r\n');
|
|
||||||
socket.write('250-8BITMIME\r\n');
|
|
||||||
socket.write('250 SIZE 10485760\r\n');
|
|
||||||
|
|
||||||
supportsUTF8 = true;
|
|
||||||
supportsPipelining = true;
|
|
||||||
console.log(' [Server] UTF8 and PIPELINING capabilities announced');
|
|
||||||
} else if (command.startsWith('MAIL FROM:')) {
|
|
||||||
// Check for SMTPUTF8 parameter
|
|
||||||
if (command.includes('SMTPUTF8') && supportsUTF8) {
|
|
||||||
console.log(' [Server] SMTPUTF8 parameter accepted');
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.includes('SMTPUTF8') && !supportsUTF8) {
|
|
||||||
console.log(' [Server] SMTPUTF8 used without capability');
|
|
||||||
socket.write('555 5.6.7 SMTPUTF8 not supported\r\n');
|
|
||||||
} else {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}
|
|
||||||
} else if (command.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'DATA') {
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
} else if (command === '.') {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test with UTF-8 content
|
|
||||||
const utf8Email = new Email({
|
|
||||||
from: 'sénder@example.com', // Non-ASCII sender
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'UTF-8 test: café, naïve, 你好',
|
|
||||||
text: 'Testing SMTPUTF8 capability with international characters: émojis 🎉'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(utf8Email);
|
|
||||||
console.log(' UTF-8 email sent using SMTPUTF8 capability');
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
|
|
||||||
await testServer.server.close();
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Scenario 3: Extension parameter validation
|
|
||||||
await (async () => {
|
|
||||||
scenarioCount++;
|
|
||||||
console.log(`\nScenario ${scenarioCount}: Testing extension parameter validation`);
|
|
||||||
|
|
||||||
const testServer = await createTestServer({
|
|
||||||
onConnection: async (socket) => {
|
|
||||||
console.log(' [Server] Client connected');
|
|
||||||
socket.write('220 validation.example.com ESMTP\r\n');
|
|
||||||
|
|
||||||
const supportedExtensions = new Set(['SIZE', 'BODY', 'DSN', '8BITMIME']);
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
console.log(` [Server] Received: ${command}`);
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250-validation.example.com\r\n');
|
|
||||||
socket.write('250-SIZE 5242880\r\n');
|
|
||||||
socket.write('250-8BITMIME\r\n');
|
|
||||||
socket.write('250-DSN\r\n');
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('MAIL FROM:')) {
|
|
||||||
// Validate all ESMTP parameters
|
|
||||||
const params = command.substring(command.indexOf('>') + 1).trim();
|
|
||||||
if (params) {
|
|
||||||
console.log(` [Server] Validating parameters: ${params}`);
|
|
||||||
|
|
||||||
const paramPairs = params.split(/\s+/).filter(p => p.length > 0);
|
|
||||||
let allValid = true;
|
|
||||||
|
|
||||||
for (const param of paramPairs) {
|
|
||||||
const [key, value] = param.split('=');
|
|
||||||
|
|
||||||
if (key === 'SIZE') {
|
|
||||||
const size = parseInt(value || '0');
|
|
||||||
if (isNaN(size) || size < 0) {
|
|
||||||
socket.write('501 5.5.4 Invalid SIZE value\r\n');
|
|
||||||
allValid = false;
|
|
||||||
break;
|
|
||||||
} else if (size > 5242880) {
|
|
||||||
socket.write('552 5.3.4 Message size exceeds limit\r\n');
|
|
||||||
allValid = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
console.log(` [Server] SIZE=${size} validated`);
|
|
||||||
} else if (key === 'BODY') {
|
|
||||||
if (value !== '7BIT' && value !== '8BITMIME') {
|
|
||||||
socket.write('501 5.5.4 Invalid BODY value\r\n');
|
|
||||||
allValid = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
console.log(` [Server] BODY=${value} validated`);
|
|
||||||
} else if (key === 'RET') {
|
|
||||||
if (value !== 'FULL' && value !== 'HDRS') {
|
|
||||||
socket.write('501 5.5.4 Invalid RET value\r\n');
|
|
||||||
allValid = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
console.log(` [Server] RET=${value} validated`);
|
|
||||||
} else if (key === 'ENVID') {
|
|
||||||
// ENVID can be any string, just check format
|
|
||||||
if (!value) {
|
|
||||||
socket.write('501 5.5.4 ENVID requires value\r\n');
|
|
||||||
allValid = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
console.log(` [Server] ENVID=${value} validated`);
|
|
||||||
} else {
|
|
||||||
console.log(` [Server] Unknown parameter: ${key}`);
|
|
||||||
socket.write(`555 5.5.4 Unsupported parameter: ${key}\r\n`);
|
|
||||||
allValid = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allValid) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}
|
|
||||||
} else if (command.startsWith('RCPT TO:')) {
|
|
||||||
// Validate DSN parameters
|
|
||||||
const params = command.substring(command.indexOf('>') + 1).trim();
|
|
||||||
if (params) {
|
|
||||||
const paramPairs = params.split(/\s+/).filter(p => p.length > 0);
|
|
||||||
let allValid = true;
|
|
||||||
|
|
||||||
for (const param of paramPairs) {
|
|
||||||
const [key, value] = param.split('=');
|
|
||||||
|
|
||||||
if (key === 'NOTIFY') {
|
|
||||||
const notifyValues = value.split(',');
|
|
||||||
const validNotify = ['NEVER', 'SUCCESS', 'FAILURE', 'DELAY'];
|
|
||||||
|
|
||||||
for (const nv of notifyValues) {
|
|
||||||
if (!validNotify.includes(nv)) {
|
|
||||||
socket.write('501 5.5.4 Invalid NOTIFY value\r\n');
|
|
||||||
allValid = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allValid) {
|
|
||||||
console.log(` [Server] NOTIFY=${value} validated`);
|
|
||||||
}
|
|
||||||
} else if (key === 'ORCPT') {
|
|
||||||
// ORCPT format: addr-type;addr-value
|
|
||||||
if (!value.includes(';')) {
|
|
||||||
socket.write('501 5.5.4 Invalid ORCPT format\r\n');
|
|
||||||
allValid = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
console.log(` [Server] ORCPT=${value} validated`);
|
|
||||||
} else {
|
|
||||||
socket.write(`555 5.5.4 Unsupported RCPT parameter: ${key}\r\n`);
|
|
||||||
allValid = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allValid) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}
|
|
||||||
} else if (command === 'DATA') {
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
} else if (command === '.') {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test with various valid parameters
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Parameter validation test',
|
|
||||||
text: 'Testing ESMTP parameter validation',
|
|
||||||
dsn: {
|
|
||||||
notify: ['SUCCESS', 'FAILURE'],
|
|
||||||
envid: 'test-envelope-id-123',
|
|
||||||
ret: 'FULL'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
console.log(' ESMTP parameter validation successful');
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
|
|
||||||
await testServer.server.close();
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Scenario 4: Service extension discovery
|
|
||||||
await (async () => {
|
|
||||||
scenarioCount++;
|
|
||||||
console.log(`\nScenario ${scenarioCount}: Testing service extension discovery`);
|
|
||||||
|
|
||||||
const testServer = await createTestServer({
|
|
||||||
onConnection: async (socket) => {
|
|
||||||
console.log(' [Server] Client connected');
|
|
||||||
socket.write('220 discovery.example.com ESMTP Ready\r\n');
|
|
||||||
|
|
||||||
let clientName = '';
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
console.log(` [Server] Received: ${command}`);
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO ')) {
|
|
||||||
clientName = command.substring(5);
|
|
||||||
console.log(` [Server] Client identified as: ${clientName}`);
|
|
||||||
|
|
||||||
// Announce extensions in order of preference
|
|
||||||
socket.write('250-discovery.example.com\r\n');
|
|
||||||
|
|
||||||
// Security extensions first
|
|
||||||
socket.write('250-STARTTLS\r\n');
|
|
||||||
socket.write('250-AUTH PLAIN LOGIN CRAM-MD5 DIGEST-MD5\r\n');
|
|
||||||
|
|
||||||
// Core functionality extensions
|
|
||||||
socket.write('250-SIZE 104857600\r\n');
|
|
||||||
socket.write('250-8BITMIME\r\n');
|
|
||||||
socket.write('250-SMTPUTF8\r\n');
|
|
||||||
|
|
||||||
// Delivery extensions
|
|
||||||
socket.write('250-DSN\r\n');
|
|
||||||
socket.write('250-DELIVERBY 86400\r\n');
|
|
||||||
|
|
||||||
// Performance extensions
|
|
||||||
socket.write('250-PIPELINING\r\n');
|
|
||||||
socket.write('250-CHUNKING\r\n');
|
|
||||||
socket.write('250-BINARYMIME\r\n');
|
|
||||||
|
|
||||||
// Enhanced status and debugging
|
|
||||||
socket.write('250-ENHANCEDSTATUSCODES\r\n');
|
|
||||||
socket.write('250-NO-SOLICITING\r\n');
|
|
||||||
socket.write('250-MTRK\r\n');
|
|
||||||
|
|
||||||
// End with help
|
|
||||||
socket.write('250 HELP\r\n');
|
|
||||||
} else if (command.startsWith('HELO ')) {
|
|
||||||
clientName = command.substring(5);
|
|
||||||
console.log(` [Server] Basic SMTP client: ${clientName}`);
|
|
||||||
socket.write('250 discovery.example.com\r\n');
|
|
||||||
} else if (command.startsWith('MAIL FROM:')) {
|
|
||||||
// Client should use discovered capabilities appropriately
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'DATA') {
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
} else if (command === '.') {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'HELP') {
|
|
||||||
// Detailed help for discovered extensions
|
|
||||||
socket.write('214-This server supports the following features:\r\n');
|
|
||||||
socket.write('214-STARTTLS - Start TLS negotiation\r\n');
|
|
||||||
socket.write('214-AUTH - SMTP Authentication\r\n');
|
|
||||||
socket.write('214-SIZE - Message size declaration\r\n');
|
|
||||||
socket.write('214-8BITMIME - 8-bit MIME transport\r\n');
|
|
||||||
socket.write('214-SMTPUTF8 - UTF-8 support\r\n');
|
|
||||||
socket.write('214-DSN - Delivery Status Notifications\r\n');
|
|
||||||
socket.write('214-PIPELINING - Command pipelining\r\n');
|
|
||||||
socket.write('214-CHUNKING - BDAT chunking\r\n');
|
|
||||||
socket.write('214 For more information, visit our website\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Thank you for using our service\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
name: 'test-client.example.com'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test service discovery
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Service discovery test',
|
|
||||||
text: 'Testing SMTP service extension discovery'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
console.log(' Service extension discovery completed');
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
|
|
||||||
await testServer.server.close();
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Scenario 5: Backward compatibility negotiation
|
|
||||||
await (async () => {
|
|
||||||
scenarioCount++;
|
|
||||||
console.log(`\nScenario ${scenarioCount}: Testing backward compatibility negotiation`);
|
|
||||||
|
|
||||||
const testServer = await createTestServer({
|
|
||||||
onConnection: async (socket) => {
|
|
||||||
console.log(' [Server] Client connected');
|
|
||||||
socket.write('220 compat.example.com ESMTP\r\n');
|
|
||||||
|
|
||||||
let isESMTP = false;
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
console.log(` [Server] Received: ${command}`);
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
isESMTP = true;
|
|
||||||
console.log(' [Server] ESMTP mode enabled');
|
|
||||||
socket.write('250-compat.example.com\r\n');
|
|
||||||
socket.write('250-SIZE 10485760\r\n');
|
|
||||||
socket.write('250-8BITMIME\r\n');
|
|
||||||
socket.write('250 ENHANCEDSTATUSCODES\r\n');
|
|
||||||
} else if (command.startsWith('HELO')) {
|
|
||||||
isESMTP = false;
|
|
||||||
console.log(' [Server] Basic SMTP mode (RFC 821 compatibility)');
|
|
||||||
socket.write('250 compat.example.com\r\n');
|
|
||||||
} else if (command.startsWith('MAIL FROM:')) {
|
|
||||||
if (isESMTP) {
|
|
||||||
// Accept ESMTP parameters
|
|
||||||
if (command.includes('SIZE=') || command.includes('BODY=')) {
|
|
||||||
console.log(' [Server] ESMTP parameters accepted');
|
|
||||||
}
|
|
||||||
socket.write('250 2.1.0 Sender OK\r\n');
|
|
||||||
} else {
|
|
||||||
// Basic SMTP - reject ESMTP parameters
|
|
||||||
if (command.includes('SIZE=') || command.includes('BODY=')) {
|
|
||||||
console.log(' [Server] ESMTP parameters rejected in basic mode');
|
|
||||||
socket.write('501 5.5.4 Syntax error in parameters\r\n');
|
|
||||||
} else {
|
|
||||||
socket.write('250 Sender OK\r\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (command.startsWith('RCPT TO:')) {
|
|
||||||
if (isESMTP) {
|
|
||||||
socket.write('250 2.1.5 Recipient OK\r\n');
|
|
||||||
} else {
|
|
||||||
socket.write('250 Recipient OK\r\n');
|
|
||||||
}
|
|
||||||
} else if (command === 'DATA') {
|
|
||||||
if (isESMTP) {
|
|
||||||
socket.write('354 2.0.0 Start mail input\r\n');
|
|
||||||
} else {
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
}
|
|
||||||
} else if (command === '.') {
|
|
||||||
if (isESMTP) {
|
|
||||||
socket.write('250 2.0.0 Message accepted\r\n');
|
|
||||||
} else {
|
|
||||||
socket.write('250 Message accepted\r\n');
|
|
||||||
}
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
if (isESMTP) {
|
|
||||||
socket.write('221 2.0.0 Service closing\r\n');
|
|
||||||
} else {
|
|
||||||
socket.write('221 Service closing\r\n');
|
|
||||||
}
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test ESMTP mode
|
|
||||||
const esmtpClient = createTestSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const esmtpEmail = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'ESMTP compatibility test',
|
|
||||||
text: 'Testing ESMTP mode with extensions'
|
|
||||||
});
|
|
||||||
|
|
||||||
const esmtpResult = await esmtpClient.sendMail(esmtpEmail);
|
|
||||||
console.log(' ESMTP mode negotiation successful');
|
|
||||||
expect(esmtpResult.response).toContain('2.0.0');
|
|
||||||
|
|
||||||
// Test basic SMTP mode (fallback)
|
|
||||||
const basicClient = createTestSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
disableESMTP: true // Force HELO instead of EHLO
|
|
||||||
});
|
|
||||||
|
|
||||||
const basicEmail = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Basic SMTP compatibility test',
|
|
||||||
text: 'Testing basic SMTP mode without extensions'
|
|
||||||
});
|
|
||||||
|
|
||||||
const basicResult = await basicClient.sendMail(basicEmail);
|
|
||||||
console.log(' Basic SMTP mode fallback successful');
|
|
||||||
expect(basicResult.response).not.toContain('2.0.0'); // No enhanced status codes
|
|
||||||
|
|
||||||
await testServer.server.close();
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Scenario 6: Extension interdependencies
|
|
||||||
await (async () => {
|
|
||||||
scenarioCount++;
|
|
||||||
console.log(`\nScenario ${scenarioCount}: Testing extension interdependencies`);
|
|
||||||
|
|
||||||
const testServer = await createTestServer({
|
|
||||||
onConnection: async (socket) => {
|
|
||||||
console.log(' [Server] Client connected');
|
|
||||||
socket.write('220 interdep.example.com ESMTP\r\n');
|
|
||||||
|
|
||||||
let tlsEnabled = false;
|
|
||||||
let authenticated = false;
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
console.log(` [Server] Received: ${command} (TLS: ${tlsEnabled}, Auth: ${authenticated})`);
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250-interdep.example.com\r\n');
|
|
||||||
|
|
||||||
if (!tlsEnabled) {
|
|
||||||
// Before TLS
|
|
||||||
socket.write('250-STARTTLS\r\n');
|
|
||||||
socket.write('250-SIZE 1048576\r\n'); // Limited size before TLS
|
|
||||||
} else {
|
|
||||||
// After TLS
|
|
||||||
socket.write('250-SIZE 52428800\r\n'); // Larger size after TLS
|
|
||||||
socket.write('250-8BITMIME\r\n');
|
|
||||||
socket.write('250-SMTPUTF8\r\n');
|
|
||||||
socket.write('250-AUTH PLAIN LOGIN CRAM-MD5\r\n');
|
|
||||||
|
|
||||||
if (authenticated) {
|
|
||||||
// Additional capabilities after authentication
|
|
||||||
socket.write('250-DSN\r\n');
|
|
||||||
socket.write('250-DELIVERBY 86400\r\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.write('250 ENHANCEDSTATUSCODES\r\n');
|
|
||||||
} else if (command === 'STARTTLS') {
|
|
||||||
if (!tlsEnabled) {
|
|
||||||
socket.write('220 2.0.0 Ready to start TLS\r\n');
|
|
||||||
tlsEnabled = true;
|
|
||||||
console.log(' [Server] TLS enabled (simulated)');
|
|
||||||
// In real implementation, would upgrade to TLS here
|
|
||||||
} else {
|
|
||||||
socket.write('503 5.5.1 TLS already active\r\n');
|
|
||||||
}
|
|
||||||
} else if (command.startsWith('AUTH')) {
|
|
||||||
if (tlsEnabled) {
|
|
||||||
authenticated = true;
|
|
||||||
console.log(' [Server] Authentication successful (simulated)');
|
|
||||||
socket.write('235 2.7.0 Authentication successful\r\n');
|
|
||||||
} else {
|
|
||||||
console.log(' [Server] AUTH rejected - TLS required');
|
|
||||||
socket.write('538 5.7.11 Encryption required for authentication\r\n');
|
|
||||||
}
|
|
||||||
} else if (command.startsWith('MAIL FROM:')) {
|
|
||||||
if (command.includes('SMTPUTF8') && !tlsEnabled) {
|
|
||||||
console.log(' [Server] SMTPUTF8 requires TLS');
|
|
||||||
socket.write('530 5.7.0 Must issue STARTTLS first\r\n');
|
|
||||||
} else {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}
|
|
||||||
} else if (command.startsWith('RCPT TO:')) {
|
|
||||||
if (command.includes('NOTIFY=') && !authenticated) {
|
|
||||||
console.log(' [Server] DSN requires authentication');
|
|
||||||
socket.write('530 5.7.0 Authentication required for DSN\r\n');
|
|
||||||
} else {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}
|
|
||||||
} else if (command === 'DATA') {
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
} else if (command === '.') {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test extension dependencies
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
requireTLS: true, // This will trigger STARTTLS
|
|
||||||
auth: {
|
|
||||||
user: 'testuser',
|
|
||||||
pass: 'testpass'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Extension interdependency test',
|
|
||||||
text: 'Testing SMTP extension interdependencies',
|
|
||||||
dsn: {
|
|
||||||
notify: ['SUCCESS'],
|
|
||||||
envid: 'interdep-test-123'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
console.log(' Extension interdependency handling successful');
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` Extension dependency error (expected in test): ${error.message}`);
|
|
||||||
// In test environment, STARTTLS won't actually work
|
|
||||||
}
|
|
||||||
|
|
||||||
await testServer.server.close();
|
|
||||||
})();
|
|
||||||
|
|
||||||
console.log(`\n${testId}: All ${scenarioCount} protocol negotiation scenarios tested ✓`);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.start();
|
|
||||||
@@ -1,728 +0,0 @@
|
|||||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { createTestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createTestSmtpClient } from '../../helpers/smtp.client.js';
|
|
||||||
import { Email } from '../../../ts/index.js';
|
|
||||||
|
|
||||||
tap.test('CRFC-07: should ensure SMTP interoperability (RFC 5321)', async (tools) => {
|
|
||||||
const testId = 'CRFC-07-interoperability';
|
|
||||||
console.log(`\n${testId}: Testing SMTP interoperability compliance...`);
|
|
||||||
|
|
||||||
let scenarioCount = 0;
|
|
||||||
|
|
||||||
// Scenario 1: Different server implementations compatibility
|
|
||||||
await (async () => {
|
|
||||||
scenarioCount++;
|
|
||||||
console.log(`\nScenario ${scenarioCount}: Testing different server implementations`);
|
|
||||||
|
|
||||||
const serverImplementations = [
|
|
||||||
{
|
|
||||||
name: 'Sendmail-style',
|
|
||||||
greeting: '220 mail.example.com ESMTP Sendmail 8.15.2/8.15.2; Date Time',
|
|
||||||
ehloResponse: [
|
|
||||||
'250-mail.example.com Hello client.example.com [192.168.1.100]',
|
|
||||||
'250-ENHANCEDSTATUSCODES',
|
|
||||||
'250-PIPELINING',
|
|
||||||
'250-8BITMIME',
|
|
||||||
'250-SIZE 36700160',
|
|
||||||
'250-DSN',
|
|
||||||
'250-ETRN',
|
|
||||||
'250-DELIVERBY',
|
|
||||||
'250 HELP'
|
|
||||||
],
|
|
||||||
quirks: { verboseResponses: true, includesTimestamp: true }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Postfix-style',
|
|
||||||
greeting: '220 mail.example.com ESMTP Postfix',
|
|
||||||
ehloResponse: [
|
|
||||||
'250-mail.example.com',
|
|
||||||
'250-PIPELINING',
|
|
||||||
'250-SIZE 10240000',
|
|
||||||
'250-VRFY',
|
|
||||||
'250-ETRN',
|
|
||||||
'250-STARTTLS',
|
|
||||||
'250-ENHANCEDSTATUSCODES',
|
|
||||||
'250-8BITMIME',
|
|
||||||
'250-DSN',
|
|
||||||
'250 SMTPUTF8'
|
|
||||||
],
|
|
||||||
quirks: { shortResponses: true, strictSyntax: true }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Exchange-style',
|
|
||||||
greeting: '220 mail.example.com Microsoft ESMTP MAIL Service ready',
|
|
||||||
ehloResponse: [
|
|
||||||
'250-mail.example.com Hello [192.168.1.100]',
|
|
||||||
'250-SIZE 37748736',
|
|
||||||
'250-PIPELINING',
|
|
||||||
'250-DSN',
|
|
||||||
'250-ENHANCEDSTATUSCODES',
|
|
||||||
'250-STARTTLS',
|
|
||||||
'250-8BITMIME',
|
|
||||||
'250-BINARYMIME',
|
|
||||||
'250-CHUNKING',
|
|
||||||
'250 OK'
|
|
||||||
],
|
|
||||||
quirks: { windowsLineEndings: true, detailedErrors: true }
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const impl of serverImplementations) {
|
|
||||||
console.log(`\n Testing with ${impl.name} server...`);
|
|
||||||
|
|
||||||
const testServer = await createTestServer({
|
|
||||||
onConnection: async (socket) => {
|
|
||||||
console.log(` [${impl.name}] Client connected`);
|
|
||||||
socket.write(impl.greeting + '\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
console.log(` [${impl.name}] Received: ${command}`);
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
impl.ehloResponse.forEach(line => {
|
|
||||||
socket.write(line + '\r\n');
|
|
||||||
});
|
|
||||||
} else if (command.startsWith('MAIL FROM:')) {
|
|
||||||
if (impl.quirks.strictSyntax && !command.includes('<')) {
|
|
||||||
socket.write('501 5.5.4 Syntax error in MAIL command\r\n');
|
|
||||||
} else {
|
|
||||||
const response = impl.quirks.verboseResponses ?
|
|
||||||
'250 2.1.0 Sender OK' : '250 OK';
|
|
||||||
socket.write(response + '\r\n');
|
|
||||||
}
|
|
||||||
} else if (command.startsWith('RCPT TO:')) {
|
|
||||||
const response = impl.quirks.verboseResponses ?
|
|
||||||
'250 2.1.5 Recipient OK' : '250 OK';
|
|
||||||
socket.write(response + '\r\n');
|
|
||||||
} else if (command === 'DATA') {
|
|
||||||
const response = impl.quirks.detailedErrors ?
|
|
||||||
'354 Start mail input; end with <CRLF>.<CRLF>' :
|
|
||||||
'354 Enter message, ending with "." on a line by itself';
|
|
||||||
socket.write(response + '\r\n');
|
|
||||||
} else if (command === '.') {
|
|
||||||
const timestamp = impl.quirks.includesTimestamp ?
|
|
||||||
` at ${new Date().toISOString()}` : '';
|
|
||||||
socket.write(`250 2.0.0 Message accepted for delivery${timestamp}\r\n`);
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
const response = impl.quirks.verboseResponses ?
|
|
||||||
'221 2.0.0 Service closing transmission channel' :
|
|
||||||
'221 Bye';
|
|
||||||
socket.write(response + '\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: `Interoperability test with ${impl.name}`,
|
|
||||||
text: `Testing compatibility with ${impl.name} server implementation`
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
console.log(` ${impl.name} compatibility: Success`);
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.messageId).toBeDefined();
|
|
||||||
|
|
||||||
await testServer.server.close();
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Scenario 2: Character encoding and internationalization
|
|
||||||
await (async () => {
|
|
||||||
scenarioCount++;
|
|
||||||
console.log(`\nScenario ${scenarioCount}: Testing character encoding interoperability`);
|
|
||||||
|
|
||||||
const testServer = await createTestServer({
|
|
||||||
onConnection: async (socket) => {
|
|
||||||
console.log(' [Server] Client connected');
|
|
||||||
socket.write('220 international.example.com ESMTP\r\n');
|
|
||||||
|
|
||||||
let supportsUTF8 = false;
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString();
|
|
||||||
console.log(` [Server] Received (${data.length} bytes): ${command.trim()}`);
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250-international.example.com\r\n');
|
|
||||||
socket.write('250-8BITMIME\r\n');
|
|
||||||
socket.write('250-SMTPUTF8\r\n');
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
supportsUTF8 = true;
|
|
||||||
} else if (command.startsWith('MAIL FROM:')) {
|
|
||||||
// Check for non-ASCII characters
|
|
||||||
const hasNonASCII = /[^\x00-\x7F]/.test(command);
|
|
||||||
const hasUTF8Param = command.includes('SMTPUTF8');
|
|
||||||
|
|
||||||
console.log(` [Server] Non-ASCII: ${hasNonASCII}, UTF8 param: ${hasUTF8Param}`);
|
|
||||||
|
|
||||||
if (hasNonASCII && !hasUTF8Param) {
|
|
||||||
socket.write('553 5.6.7 Non-ASCII addresses require SMTPUTF8\r\n');
|
|
||||||
} else {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}
|
|
||||||
} else if (command.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.trim() === 'DATA') {
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
} else if (command.trim() === '.') {
|
|
||||||
socket.write('250 OK: International message accepted\r\n');
|
|
||||||
} else if (command.trim() === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test various international character sets
|
|
||||||
const internationalTests = [
|
|
||||||
{
|
|
||||||
desc: 'Latin characters with accents',
|
|
||||||
from: 'sénder@éxample.com',
|
|
||||||
to: 'récipient@éxample.com',
|
|
||||||
subject: 'Tëst with açcénts',
|
|
||||||
text: 'Café, naïve, résumé, piñata'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: 'Cyrillic characters',
|
|
||||||
from: 'отправитель@пример.com',
|
|
||||||
to: 'получатель@пример.com',
|
|
||||||
subject: 'Тест с кириллицей',
|
|
||||||
text: 'Привет мир! Это тест с русскими буквами.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: 'Chinese characters',
|
|
||||||
from: 'sender@example.com', // ASCII for compatibility
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: '测试中文字符',
|
|
||||||
text: '你好世界!这是一个中文测试。'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: 'Arabic characters',
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: 'اختبار النص العربي',
|
|
||||||
text: 'مرحبا بالعالم! هذا اختبار باللغة العربية.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: 'Emoji and symbols',
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: 'recipient@example.com',
|
|
||||||
subject: '🎉 Test with emojis 🌟',
|
|
||||||
text: 'Hello 👋 World 🌍! Testing emojis: 🚀 📧 ✨'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const test of internationalTests) {
|
|
||||||
console.log(` Testing: ${test.desc}`);
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: test.from,
|
|
||||||
to: [test.to],
|
|
||||||
subject: test.subject,
|
|
||||||
text: test.text
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
console.log(` ${test.desc}: Success`);
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ${test.desc}: Failed - ${error.message}`);
|
|
||||||
// Some may fail if server doesn't support international addresses
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await testServer.server.close();
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Scenario 3: Message format compatibility
|
|
||||||
await (async () => {
|
|
||||||
scenarioCount++;
|
|
||||||
console.log(`\nScenario ${scenarioCount}: Testing message format compatibility`);
|
|
||||||
|
|
||||||
const testServer = await createTestServer({
|
|
||||||
onConnection: async (socket) => {
|
|
||||||
console.log(' [Server] Client connected');
|
|
||||||
socket.write('220 formats.example.com ESMTP\r\n');
|
|
||||||
|
|
||||||
let inData = false;
|
|
||||||
let messageContent = '';
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
if (inData) {
|
|
||||||
messageContent += data.toString();
|
|
||||||
if (messageContent.includes('\r\n.\r\n')) {
|
|
||||||
inData = false;
|
|
||||||
|
|
||||||
// Analyze message format
|
|
||||||
const headers = messageContent.substring(0, messageContent.indexOf('\r\n\r\n'));
|
|
||||||
const body = messageContent.substring(messageContent.indexOf('\r\n\r\n') + 4);
|
|
||||||
|
|
||||||
console.log(' [Server] Message analysis:');
|
|
||||||
console.log(` Header count: ${(headers.match(/\r\n/g) || []).length + 1}`);
|
|
||||||
console.log(` Body size: ${body.length} bytes`);
|
|
||||||
|
|
||||||
// Check for proper header folding
|
|
||||||
const longHeaders = headers.split('\r\n').filter(h => h.length > 78);
|
|
||||||
if (longHeaders.length > 0) {
|
|
||||||
console.log(` Long headers detected: ${longHeaders.length}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for MIME structure
|
|
||||||
if (headers.includes('Content-Type:')) {
|
|
||||||
console.log(' MIME message detected');
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.write('250 OK: Message format validated\r\n');
|
|
||||||
messageContent = '';
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const command = data.toString().trim();
|
|
||||||
console.log(` [Server] Received: ${command}`);
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250-formats.example.com\r\n');
|
|
||||||
socket.write('250-8BITMIME\r\n');
|
|
||||||
socket.write('250-BINARYMIME\r\n');
|
|
||||||
socket.write('250 SIZE 52428800\r\n');
|
|
||||||
} else if (command.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'DATA') {
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
inData = true;
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test different message formats
|
|
||||||
const formatTests = [
|
|
||||||
{
|
|
||||||
desc: 'Plain text message',
|
|
||||||
email: new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Plain text test',
|
|
||||||
text: 'This is a simple plain text message.'
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: 'HTML message',
|
|
||||||
email: new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'HTML test',
|
|
||||||
html: '<h1>HTML Message</h1><p>This is an <strong>HTML</strong> message.</p>'
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: 'Multipart alternative',
|
|
||||||
email: new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Multipart test',
|
|
||||||
text: 'Plain text version',
|
|
||||||
html: '<p>HTML version</p>'
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: 'Message with attachment',
|
|
||||||
email: new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Attachment test',
|
|
||||||
text: 'Message with attachment',
|
|
||||||
attachments: [{
|
|
||||||
filename: 'test.txt',
|
|
||||||
content: 'This is a test attachment'
|
|
||||||
}]
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: 'Message with custom headers',
|
|
||||||
email: new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Custom headers test',
|
|
||||||
text: 'Message with custom headers',
|
|
||||||
headers: {
|
|
||||||
'X-Custom-Header': 'Custom value',
|
|
||||||
'X-Mailer': 'Test Mailer 1.0',
|
|
||||||
'Message-ID': '<test123@example.com>',
|
|
||||||
'References': '<ref1@example.com> <ref2@example.com>'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const test of formatTests) {
|
|
||||||
console.log(` Testing: ${test.desc}`);
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(test.email);
|
|
||||||
console.log(` ${test.desc}: Success`);
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.messageId).toBeDefined();
|
|
||||||
}
|
|
||||||
|
|
||||||
await testServer.server.close();
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Scenario 4: Error handling interoperability
|
|
||||||
await (async () => {
|
|
||||||
scenarioCount++;
|
|
||||||
console.log(`\nScenario ${scenarioCount}: Testing error handling interoperability`);
|
|
||||||
|
|
||||||
const testServer = await createTestServer({
|
|
||||||
onConnection: async (socket) => {
|
|
||||||
console.log(' [Server] Client connected');
|
|
||||||
socket.write('220 errors.example.com ESMTP\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
console.log(` [Server] Received: ${command}`);
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250-errors.example.com\r\n');
|
|
||||||
socket.write('250-ENHANCEDSTATUSCODES\r\n');
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('MAIL FROM:')) {
|
|
||||||
const address = command.match(/<(.+)>/)?.[1] || '';
|
|
||||||
|
|
||||||
if (address.includes('temp-fail')) {
|
|
||||||
// Temporary failure - client should retry
|
|
||||||
socket.write('451 4.7.1 Temporary system problem, try again later\r\n');
|
|
||||||
} else if (address.includes('perm-fail')) {
|
|
||||||
// Permanent failure - client should not retry
|
|
||||||
socket.write('550 5.1.8 Invalid sender address format\r\n');
|
|
||||||
} else if (address.includes('syntax-error')) {
|
|
||||||
// Syntax error
|
|
||||||
socket.write('501 5.5.4 Syntax error in MAIL command\r\n');
|
|
||||||
} else {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}
|
|
||||||
} else if (command.startsWith('RCPT TO:')) {
|
|
||||||
const address = command.match(/<(.+)>/)?.[1] || '';
|
|
||||||
|
|
||||||
if (address.includes('unknown')) {
|
|
||||||
socket.write('550 5.1.1 User unknown in local recipient table\r\n');
|
|
||||||
} else if (address.includes('temp-reject')) {
|
|
||||||
socket.write('450 4.2.1 Mailbox temporarily unavailable\r\n');
|
|
||||||
} else if (address.includes('quota-exceeded')) {
|
|
||||||
socket.write('552 5.2.2 Mailbox over quota\r\n');
|
|
||||||
} else {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}
|
|
||||||
} else if (command === 'DATA') {
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
} else if (command === '.') {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
} else {
|
|
||||||
// Unknown command
|
|
||||||
socket.write('500 5.5.1 Command unrecognized\r\n');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test various error scenarios
|
|
||||||
const errorTests = [
|
|
||||||
{
|
|
||||||
desc: 'Temporary sender failure',
|
|
||||||
from: 'temp-fail@example.com',
|
|
||||||
to: 'valid@example.com',
|
|
||||||
expectError: true,
|
|
||||||
errorType: '4xx'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: 'Permanent sender failure',
|
|
||||||
from: 'perm-fail@example.com',
|
|
||||||
to: 'valid@example.com',
|
|
||||||
expectError: true,
|
|
||||||
errorType: '5xx'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: 'Unknown recipient',
|
|
||||||
from: 'valid@example.com',
|
|
||||||
to: 'unknown@example.com',
|
|
||||||
expectError: true,
|
|
||||||
errorType: '5xx'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: 'Mixed valid/invalid recipients',
|
|
||||||
from: 'valid@example.com',
|
|
||||||
to: ['valid@example.com', 'unknown@example.com', 'temp-reject@example.com'],
|
|
||||||
expectError: false, // Partial success
|
|
||||||
errorType: 'mixed'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const test of errorTests) {
|
|
||||||
console.log(` Testing: ${test.desc}`);
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: test.from,
|
|
||||||
to: Array.isArray(test.to) ? test.to : [test.to],
|
|
||||||
subject: `Error test: ${test.desc}`,
|
|
||||||
text: `Testing error handling for ${test.desc}`
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
|
|
||||||
if (test.expectError && test.errorType !== 'mixed') {
|
|
||||||
console.log(` Unexpected success for ${test.desc}`);
|
|
||||||
} else {
|
|
||||||
console.log(` ${test.desc}: Handled correctly`);
|
|
||||||
if (result.rejected && result.rejected.length > 0) {
|
|
||||||
console.log(` Rejected: ${result.rejected.length} recipients`);
|
|
||||||
}
|
|
||||||
if (result.accepted && result.accepted.length > 0) {
|
|
||||||
console.log(` Accepted: ${result.accepted.length} recipients`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (test.expectError) {
|
|
||||||
console.log(` ${test.desc}: Failed as expected (${error.responseCode})`);
|
|
||||||
if (test.errorType === '4xx') {
|
|
||||||
expect(error.responseCode).toBeGreaterThanOrEqual(400);
|
|
||||||
expect(error.responseCode).toBeLessThan(500);
|
|
||||||
} else if (test.errorType === '5xx') {
|
|
||||||
expect(error.responseCode).toBeGreaterThanOrEqual(500);
|
|
||||||
expect(error.responseCode).toBeLessThan(600);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(` Unexpected error for ${test.desc}: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await testServer.server.close();
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Scenario 5: Connection management interoperability
|
|
||||||
await (async () => {
|
|
||||||
scenarioCount++;
|
|
||||||
console.log(`\nScenario ${scenarioCount}: Testing connection management interoperability`);
|
|
||||||
|
|
||||||
const testServer = await createTestServer({
|
|
||||||
onConnection: async (socket) => {
|
|
||||||
console.log(' [Server] Client connected');
|
|
||||||
|
|
||||||
let commandCount = 0;
|
|
||||||
let idleTime = Date.now();
|
|
||||||
const maxIdleTime = 5000; // 5 seconds for testing
|
|
||||||
const maxCommands = 10;
|
|
||||||
|
|
||||||
socket.write('220 connection.example.com ESMTP\r\n');
|
|
||||||
|
|
||||||
// Set up idle timeout
|
|
||||||
const idleCheck = setInterval(() => {
|
|
||||||
if (Date.now() - idleTime > maxIdleTime) {
|
|
||||||
console.log(' [Server] Idle timeout - closing connection');
|
|
||||||
socket.write('421 4.4.2 Idle timeout, closing connection\r\n');
|
|
||||||
socket.end();
|
|
||||||
clearInterval(idleCheck);
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
commandCount++;
|
|
||||||
idleTime = Date.now();
|
|
||||||
|
|
||||||
console.log(` [Server] Command ${commandCount}: ${command}`);
|
|
||||||
|
|
||||||
if (commandCount > maxCommands) {
|
|
||||||
console.log(' [Server] Too many commands - closing connection');
|
|
||||||
socket.write('421 4.7.0 Too many commands, closing connection\r\n');
|
|
||||||
socket.end();
|
|
||||||
clearInterval(idleCheck);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250-connection.example.com\r\n');
|
|
||||||
socket.write('250-PIPELINING\r\n');
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'DATA') {
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
} else if (command === '.') {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'RSET') {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'NOOP') {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
clearInterval(idleCheck);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('close', () => {
|
|
||||||
clearInterval(idleCheck);
|
|
||||||
console.log(` [Server] Connection closed after ${commandCount} commands`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
pool: true,
|
|
||||||
maxConnections: 1
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test connection reuse
|
|
||||||
console.log(' Testing connection reuse...');
|
|
||||||
|
|
||||||
for (let i = 1; i <= 3; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: [`recipient${i}@example.com`],
|
|
||||||
subject: `Connection test ${i}`,
|
|
||||||
text: `Testing connection management - email ${i}`
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
console.log(` Email ${i} sent successfully`);
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
|
|
||||||
// Small delay to test connection persistence
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test NOOP for keeping connection alive
|
|
||||||
console.log(' Testing connection keep-alive...');
|
|
||||||
|
|
||||||
await smtpClient.verify(); // This might send NOOP
|
|
||||||
console.log(' Connection verified (keep-alive)');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
await testServer.server.close();
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Scenario 6: Legacy SMTP compatibility
|
|
||||||
await (async () => {
|
|
||||||
scenarioCount++;
|
|
||||||
console.log(`\nScenario ${scenarioCount}: Testing legacy SMTP compatibility`);
|
|
||||||
|
|
||||||
const testServer = await createTestServer({
|
|
||||||
onConnection: async (socket) => {
|
|
||||||
console.log(' [Server] Legacy SMTP server');
|
|
||||||
|
|
||||||
// Old-style greeting without ESMTP
|
|
||||||
socket.write('220 legacy.example.com Simple Mail Transfer Service Ready\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
console.log(` [Server] Received: ${command}`);
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
// Legacy server doesn't understand EHLO
|
|
||||||
socket.write('500 Command unrecognized\r\n');
|
|
||||||
} else if (command.startsWith('HELO')) {
|
|
||||||
socket.write('250 legacy.example.com\r\n');
|
|
||||||
} else if (command.startsWith('MAIL FROM:')) {
|
|
||||||
// Very strict syntax checking
|
|
||||||
if (!command.match(/^MAIL FROM:\s*<[^>]+>\s*$/)) {
|
|
||||||
socket.write('501 Syntax error\r\n');
|
|
||||||
} else {
|
|
||||||
socket.write('250 Sender OK\r\n');
|
|
||||||
}
|
|
||||||
} else if (command.startsWith('RCPT TO:')) {
|
|
||||||
if (!command.match(/^RCPT TO:\s*<[^>]+>\s*$/)) {
|
|
||||||
socket.write('501 Syntax error\r\n');
|
|
||||||
} else {
|
|
||||||
socket.write('250 Recipient OK\r\n');
|
|
||||||
}
|
|
||||||
} else if (command === 'DATA') {
|
|
||||||
socket.write('354 Enter mail, end with "." on a line by itself\r\n');
|
|
||||||
} else if (command === '.') {
|
|
||||||
socket.write('250 Message accepted for delivery\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Service closing transmission channel\r\n');
|
|
||||||
socket.end();
|
|
||||||
} else if (command === 'HELP') {
|
|
||||||
socket.write('214-Commands supported:\r\n');
|
|
||||||
socket.write('214-HELO MAIL RCPT DATA QUIT HELP\r\n');
|
|
||||||
socket.write('214 End of HELP info\r\n');
|
|
||||||
} else {
|
|
||||||
socket.write('500 Command unrecognized\r\n');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test with client that can fall back to basic SMTP
|
|
||||||
const legacyClient = createTestSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
disableESMTP: true // Force HELO mode
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Legacy compatibility test',
|
|
||||||
text: 'Testing compatibility with legacy SMTP servers'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await legacyClient.sendMail(email);
|
|
||||||
console.log(' Legacy SMTP compatibility: Success');
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.messageId).toBeDefined();
|
|
||||||
|
|
||||||
await testServer.server.close();
|
|
||||||
})();
|
|
||||||
|
|
||||||
console.log(`\n${testId}: All ${scenarioCount} interoperability scenarios tested ✓`);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.start();
|
|
||||||
@@ -1,656 +0,0 @@
|
|||||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { createTestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createTestSmtpClient } from '../../helpers/smtp.client.js';
|
|
||||||
import { Email } from '../../../ts/index.js';
|
|
||||||
|
|
||||||
tap.test('CRFC-08: should handle SMTP extensions correctly (Various RFCs)', async (tools) => {
|
|
||||||
const testId = 'CRFC-08-smtp-extensions';
|
|
||||||
console.log(`\n${testId}: Testing SMTP extensions compliance...`);
|
|
||||||
|
|
||||||
let scenarioCount = 0;
|
|
||||||
|
|
||||||
// Scenario 1: CHUNKING extension (RFC 3030)
|
|
||||||
await (async () => {
|
|
||||||
scenarioCount++;
|
|
||||||
console.log(`\nScenario ${scenarioCount}: Testing CHUNKING extension (RFC 3030)`);
|
|
||||||
|
|
||||||
const testServer = await createTestServer({
|
|
||||||
onConnection: async (socket) => {
|
|
||||||
console.log(' [Server] Client connected');
|
|
||||||
socket.write('220 chunking.example.com ESMTP\r\n');
|
|
||||||
|
|
||||||
let chunkingMode = false;
|
|
||||||
let totalChunks = 0;
|
|
||||||
let totalBytes = 0;
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const text = data.toString();
|
|
||||||
|
|
||||||
if (chunkingMode) {
|
|
||||||
// In chunking mode, all data is message content
|
|
||||||
totalBytes += data.length;
|
|
||||||
console.log(` [Server] Received chunk: ${data.length} bytes`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const command = text.trim();
|
|
||||||
console.log(` [Server] Received: ${command}`);
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250-chunking.example.com\r\n');
|
|
||||||
socket.write('250-CHUNKING\r\n');
|
|
||||||
socket.write('250-8BITMIME\r\n');
|
|
||||||
socket.write('250-BINARYMIME\r\n');
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('MAIL FROM:')) {
|
|
||||||
if (command.includes('BODY=BINARYMIME')) {
|
|
||||||
console.log(' [Server] Binary MIME body declared');
|
|
||||||
}
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('BDAT ')) {
|
|
||||||
// BDAT command format: BDAT <size> [LAST]
|
|
||||||
const parts = command.split(' ');
|
|
||||||
const chunkSize = parseInt(parts[1]);
|
|
||||||
const isLast = parts.includes('LAST');
|
|
||||||
|
|
||||||
totalChunks++;
|
|
||||||
console.log(` [Server] BDAT chunk ${totalChunks}: ${chunkSize} bytes${isLast ? ' (LAST)' : ''}`);
|
|
||||||
|
|
||||||
if (isLast) {
|
|
||||||
socket.write(`250 OK: Message accepted, ${totalChunks} chunks, ${totalBytes} total bytes\r\n`);
|
|
||||||
chunkingMode = false;
|
|
||||||
totalChunks = 0;
|
|
||||||
totalBytes = 0;
|
|
||||||
} else {
|
|
||||||
socket.write('250 OK: Chunk accepted\r\n');
|
|
||||||
chunkingMode = true;
|
|
||||||
}
|
|
||||||
} else if (command === 'DATA') {
|
|
||||||
// DATA not allowed when CHUNKING is available
|
|
||||||
socket.write('503 5.5.1 Use BDAT instead of DATA\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test with binary content that would benefit from chunking
|
|
||||||
const binaryContent = Buffer.alloc(1024);
|
|
||||||
for (let i = 0; i < binaryContent.length; i++) {
|
|
||||||
binaryContent[i] = i % 256;
|
|
||||||
}
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'CHUNKING test',
|
|
||||||
text: 'Testing CHUNKING extension with binary data',
|
|
||||||
attachments: [{
|
|
||||||
filename: 'binary-data.bin',
|
|
||||||
content: binaryContent
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
console.log(' CHUNKING extension handled (if supported by client)');
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.messageId).toBeDefined();
|
|
||||||
|
|
||||||
await testServer.server.close();
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Scenario 2: DELIVERBY extension (RFC 2852)
|
|
||||||
await (async () => {
|
|
||||||
scenarioCount++;
|
|
||||||
console.log(`\nScenario ${scenarioCount}: Testing DELIVERBY extension (RFC 2852)`);
|
|
||||||
|
|
||||||
const testServer = await createTestServer({
|
|
||||||
onConnection: async (socket) => {
|
|
||||||
console.log(' [Server] Client connected');
|
|
||||||
socket.write('220 deliverby.example.com ESMTP\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
console.log(` [Server] Received: ${command}`);
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250-deliverby.example.com\r\n');
|
|
||||||
socket.write('250-DELIVERBY 86400\r\n'); // 24 hours max
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('MAIL FROM:')) {
|
|
||||||
// Check for DELIVERBY parameter
|
|
||||||
const deliverByMatch = command.match(/DELIVERBY=(\d+)([RN]?)/i);
|
|
||||||
if (deliverByMatch) {
|
|
||||||
const seconds = parseInt(deliverByMatch[1]);
|
|
||||||
const mode = deliverByMatch[2] || 'R'; // R=return, N=notify
|
|
||||||
|
|
||||||
console.log(` [Server] DELIVERBY: ${seconds} seconds, mode: ${mode}`);
|
|
||||||
|
|
||||||
if (seconds > 86400) {
|
|
||||||
socket.write('501 5.5.4 DELIVERBY time exceeds maximum\r\n');
|
|
||||||
} else if (seconds < 0) {
|
|
||||||
socket.write('501 5.5.4 Invalid DELIVERBY time\r\n');
|
|
||||||
} else {
|
|
||||||
socket.write('250 OK: Delivery deadline accepted\r\n');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
}
|
|
||||||
} else if (command.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'DATA') {
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
} else if (command === '.') {
|
|
||||||
socket.write('250 OK: Message queued with delivery deadline\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test with delivery deadline
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['urgent@example.com'],
|
|
||||||
subject: 'Urgent delivery test',
|
|
||||||
text: 'This message has a delivery deadline',
|
|
||||||
// Note: Most SMTP clients don't expose DELIVERBY directly
|
|
||||||
// but we can test server handling
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
console.log(' DELIVERBY extension supported by server');
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
|
|
||||||
await testServer.server.close();
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Scenario 3: ETRN extension (RFC 1985)
|
|
||||||
await (async () => {
|
|
||||||
scenarioCount++;
|
|
||||||
console.log(`\nScenario ${scenarioCount}: Testing ETRN extension (RFC 1985)`);
|
|
||||||
|
|
||||||
const testServer = await createTestServer({
|
|
||||||
onConnection: async (socket) => {
|
|
||||||
console.log(' [Server] Client connected');
|
|
||||||
socket.write('220 etrn.example.com ESMTP\r\n');
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
console.log(` [Server] Received: ${command}`);
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250-etrn.example.com\r\n');
|
|
||||||
socket.write('250-ETRN\r\n');
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('ETRN ')) {
|
|
||||||
const domain = command.substring(5);
|
|
||||||
console.log(` [Server] ETRN request for domain: ${domain}`);
|
|
||||||
|
|
||||||
if (domain === '@example.com') {
|
|
||||||
socket.write('250 OK: Queue processing started for example.com\r\n');
|
|
||||||
} else if (domain === '#urgent') {
|
|
||||||
socket.write('250 OK: Urgent queue processing started\r\n');
|
|
||||||
} else if (domain.includes('unknown')) {
|
|
||||||
socket.write('458 Unable to queue messages for node\r\n');
|
|
||||||
} else {
|
|
||||||
socket.write('250 OK: Queue processing started\r\n');
|
|
||||||
}
|
|
||||||
} else if (command.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'DATA') {
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
} else if (command === '.') {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ETRN is typically used by mail servers, not clients
|
|
||||||
// We'll test the server's ETRN capability manually
|
|
||||||
const net = await import('net');
|
|
||||||
const client = net.createConnection(testServer.port, testServer.hostname);
|
|
||||||
|
|
||||||
const commands = [
|
|
||||||
'EHLO client.example.com',
|
|
||||||
'ETRN @example.com', // Request queue processing for domain
|
|
||||||
'ETRN #urgent', // Request urgent queue processing
|
|
||||||
'ETRN unknown.domain.com', // Test error handling
|
|
||||||
'QUIT'
|
|
||||||
];
|
|
||||||
|
|
||||||
let commandIndex = 0;
|
|
||||||
|
|
||||||
client.on('data', (data) => {
|
|
||||||
const response = data.toString().trim();
|
|
||||||
console.log(` [Client] Response: ${response}`);
|
|
||||||
|
|
||||||
if (commandIndex < commands.length) {
|
|
||||||
setTimeout(() => {
|
|
||||||
const command = commands[commandIndex];
|
|
||||||
console.log(` [Client] Sending: ${command}`);
|
|
||||||
client.write(command + '\r\n');
|
|
||||||
commandIndex++;
|
|
||||||
}, 100);
|
|
||||||
} else {
|
|
||||||
client.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
client.on('end', () => {
|
|
||||||
console.log(' ETRN extension testing completed');
|
|
||||||
resolve(void 0);
|
|
||||||
});
|
|
||||||
client.on('error', reject);
|
|
||||||
});
|
|
||||||
|
|
||||||
await testServer.server.close();
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Scenario 4: VRFY and EXPN extensions (RFC 5321)
|
|
||||||
await (async () => {
|
|
||||||
scenarioCount++;
|
|
||||||
console.log(`\nScenario ${scenarioCount}: Testing VRFY and EXPN extensions`);
|
|
||||||
|
|
||||||
const testServer = await createTestServer({
|
|
||||||
onConnection: async (socket) => {
|
|
||||||
console.log(' [Server] Client connected');
|
|
||||||
socket.write('220 verify.example.com ESMTP\r\n');
|
|
||||||
|
|
||||||
// Simulated user database
|
|
||||||
const users = new Map([
|
|
||||||
['admin', { email: 'admin@example.com', fullName: 'Administrator' }],
|
|
||||||
['john', { email: 'john.doe@example.com', fullName: 'John Doe' }],
|
|
||||||
['support', { email: 'support@example.com', fullName: 'Support Team' }]
|
|
||||||
]);
|
|
||||||
|
|
||||||
const mailingLists = new Map([
|
|
||||||
['staff', ['admin@example.com', 'john.doe@example.com']],
|
|
||||||
['support-team', ['support@example.com', 'admin@example.com']]
|
|
||||||
]);
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
console.log(` [Server] Received: ${command}`);
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250-verify.example.com\r\n');
|
|
||||||
socket.write('250-VRFY\r\n');
|
|
||||||
socket.write('250-EXPN\r\n');
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('VRFY ')) {
|
|
||||||
const query = command.substring(5);
|
|
||||||
console.log(` [Server] VRFY query: ${query}`);
|
|
||||||
|
|
||||||
// Look up user
|
|
||||||
const user = users.get(query.toLowerCase());
|
|
||||||
if (user) {
|
|
||||||
socket.write(`250 ${user.fullName} <${user.email}>\r\n`);
|
|
||||||
} else {
|
|
||||||
// Check if it's an email address
|
|
||||||
const emailMatch = Array.from(users.values()).find(u =>
|
|
||||||
u.email.toLowerCase() === query.toLowerCase()
|
|
||||||
);
|
|
||||||
if (emailMatch) {
|
|
||||||
socket.write(`250 ${emailMatch.fullName} <${emailMatch.email}>\r\n`);
|
|
||||||
} else {
|
|
||||||
socket.write('550 5.1.1 User unknown\r\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (command.startsWith('EXPN ')) {
|
|
||||||
const listName = command.substring(5);
|
|
||||||
console.log(` [Server] EXPN query: ${listName}`);
|
|
||||||
|
|
||||||
const list = mailingLists.get(listName.toLowerCase());
|
|
||||||
if (list) {
|
|
||||||
socket.write(`250-Mailing list ${listName}:\r\n`);
|
|
||||||
list.forEach((email, index) => {
|
|
||||||
const prefix = index < list.length - 1 ? '250-' : '250 ';
|
|
||||||
socket.write(`${prefix}${email}\r\n`);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
socket.write('550 5.1.1 Mailing list not found\r\n');
|
|
||||||
}
|
|
||||||
} else if (command.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'DATA') {
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
} else if (command === '.') {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test VRFY and EXPN commands
|
|
||||||
const net = await import('net');
|
|
||||||
const client = net.createConnection(testServer.port, testServer.hostname);
|
|
||||||
|
|
||||||
const commands = [
|
|
||||||
'EHLO client.example.com',
|
|
||||||
'VRFY admin', // Verify user by username
|
|
||||||
'VRFY john.doe@example.com', // Verify user by email
|
|
||||||
'VRFY nonexistent', // Test unknown user
|
|
||||||
'EXPN staff', // Expand mailing list
|
|
||||||
'EXPN nonexistent-list', // Test unknown list
|
|
||||||
'QUIT'
|
|
||||||
];
|
|
||||||
|
|
||||||
let commandIndex = 0;
|
|
||||||
|
|
||||||
client.on('data', (data) => {
|
|
||||||
const response = data.toString().trim();
|
|
||||||
console.log(` [Client] Response: ${response}`);
|
|
||||||
|
|
||||||
if (commandIndex < commands.length) {
|
|
||||||
setTimeout(() => {
|
|
||||||
const command = commands[commandIndex];
|
|
||||||
console.log(` [Client] Sending: ${command}`);
|
|
||||||
client.write(command + '\r\n');
|
|
||||||
commandIndex++;
|
|
||||||
}, 200);
|
|
||||||
} else {
|
|
||||||
client.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
client.on('end', () => {
|
|
||||||
console.log(' VRFY and EXPN testing completed');
|
|
||||||
resolve(void 0);
|
|
||||||
});
|
|
||||||
client.on('error', reject);
|
|
||||||
});
|
|
||||||
|
|
||||||
await testServer.server.close();
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Scenario 5: HELP extension
|
|
||||||
await (async () => {
|
|
||||||
scenarioCount++;
|
|
||||||
console.log(`\nScenario ${scenarioCount}: Testing HELP extension`);
|
|
||||||
|
|
||||||
const testServer = await createTestServer({
|
|
||||||
onConnection: async (socket) => {
|
|
||||||
console.log(' [Server] Client connected');
|
|
||||||
socket.write('220 help.example.com ESMTP\r\n');
|
|
||||||
|
|
||||||
const helpTopics = new Map([
|
|
||||||
['commands', [
|
|
||||||
'Available commands:',
|
|
||||||
'EHLO <domain> - Extended HELLO',
|
|
||||||
'MAIL FROM:<addr> - Specify sender',
|
|
||||||
'RCPT TO:<addr> - Specify recipient',
|
|
||||||
'DATA - Start message text',
|
|
||||||
'QUIT - Close connection'
|
|
||||||
]],
|
|
||||||
['extensions', [
|
|
||||||
'Supported extensions:',
|
|
||||||
'SIZE - Message size declaration',
|
|
||||||
'8BITMIME - 8-bit MIME transport',
|
|
||||||
'STARTTLS - Start TLS negotiation',
|
|
||||||
'AUTH - SMTP Authentication',
|
|
||||||
'DSN - Delivery Status Notifications'
|
|
||||||
]],
|
|
||||||
['syntax', [
|
|
||||||
'Command syntax:',
|
|
||||||
'Commands are case-insensitive',
|
|
||||||
'Lines end with CRLF',
|
|
||||||
'Email addresses must be in <> brackets',
|
|
||||||
'Parameters are space-separated'
|
|
||||||
]]
|
|
||||||
]);
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
console.log(` [Server] Received: ${command}`);
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250-help.example.com\r\n');
|
|
||||||
socket.write('250-HELP\r\n');
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'HELP' || command === 'HELP HELP') {
|
|
||||||
socket.write('214-This server provides HELP for the following topics:\r\n');
|
|
||||||
socket.write('214-COMMANDS - List of available commands\r\n');
|
|
||||||
socket.write('214-EXTENSIONS - List of supported extensions\r\n');
|
|
||||||
socket.write('214-SYNTAX - Command syntax rules\r\n');
|
|
||||||
socket.write('214 Use HELP <topic> for specific information\r\n');
|
|
||||||
} else if (command.startsWith('HELP ')) {
|
|
||||||
const topic = command.substring(5).toLowerCase();
|
|
||||||
const helpText = helpTopics.get(topic);
|
|
||||||
|
|
||||||
if (helpText) {
|
|
||||||
helpText.forEach((line, index) => {
|
|
||||||
const prefix = index < helpText.length - 1 ? '214-' : '214 ';
|
|
||||||
socket.write(`${prefix}${line}\r\n`);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
socket.write('504 5.3.0 HELP topic not available\r\n');
|
|
||||||
}
|
|
||||||
} else if (command.startsWith('MAIL FROM:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command.startsWith('RCPT TO:')) {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'DATA') {
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
} else if (command === '.') {
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test HELP command
|
|
||||||
const net = await import('net');
|
|
||||||
const client = net.createConnection(testServer.port, testServer.hostname);
|
|
||||||
|
|
||||||
const commands = [
|
|
||||||
'EHLO client.example.com',
|
|
||||||
'HELP', // General help
|
|
||||||
'HELP COMMANDS', // Specific topic
|
|
||||||
'HELP EXTENSIONS', // Another topic
|
|
||||||
'HELP NONEXISTENT', // Unknown topic
|
|
||||||
'QUIT'
|
|
||||||
];
|
|
||||||
|
|
||||||
let commandIndex = 0;
|
|
||||||
|
|
||||||
client.on('data', (data) => {
|
|
||||||
const response = data.toString().trim();
|
|
||||||
console.log(` [Client] Response: ${response}`);
|
|
||||||
|
|
||||||
if (commandIndex < commands.length) {
|
|
||||||
setTimeout(() => {
|
|
||||||
const command = commands[commandIndex];
|
|
||||||
console.log(` [Client] Sending: ${command}`);
|
|
||||||
client.write(command + '\r\n');
|
|
||||||
commandIndex++;
|
|
||||||
}, 200);
|
|
||||||
} else {
|
|
||||||
client.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
client.on('end', () => {
|
|
||||||
console.log(' HELP extension testing completed');
|
|
||||||
resolve(void 0);
|
|
||||||
});
|
|
||||||
client.on('error', reject);
|
|
||||||
});
|
|
||||||
|
|
||||||
await testServer.server.close();
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Scenario 6: Extension combination and interaction
|
|
||||||
await (async () => {
|
|
||||||
scenarioCount++;
|
|
||||||
console.log(`\nScenario ${scenarioCount}: Testing extension combinations`);
|
|
||||||
|
|
||||||
const testServer = await createTestServer({
|
|
||||||
onConnection: async (socket) => {
|
|
||||||
console.log(' [Server] Client connected');
|
|
||||||
socket.write('220 combined.example.com ESMTP\r\n');
|
|
||||||
|
|
||||||
let activeExtensions: string[] = [];
|
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
|
||||||
const command = data.toString().trim();
|
|
||||||
console.log(` [Server] Received: ${command}`);
|
|
||||||
|
|
||||||
if (command.startsWith('EHLO')) {
|
|
||||||
socket.write('250-combined.example.com\r\n');
|
|
||||||
|
|
||||||
// Announce multiple extensions
|
|
||||||
const extensions = [
|
|
||||||
'SIZE 52428800',
|
|
||||||
'8BITMIME',
|
|
||||||
'SMTPUTF8',
|
|
||||||
'ENHANCEDSTATUSCODES',
|
|
||||||
'PIPELINING',
|
|
||||||
'DSN',
|
|
||||||
'DELIVERBY 86400',
|
|
||||||
'CHUNKING',
|
|
||||||
'BINARYMIME',
|
|
||||||
'HELP'
|
|
||||||
];
|
|
||||||
|
|
||||||
extensions.forEach(ext => {
|
|
||||||
socket.write(`250-${ext}\r\n`);
|
|
||||||
activeExtensions.push(ext.split(' ')[0]);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.write('250 OK\r\n');
|
|
||||||
console.log(` [Server] Active extensions: ${activeExtensions.join(', ')}`);
|
|
||||||
} else if (command.startsWith('MAIL FROM:')) {
|
|
||||||
// Check for multiple extension parameters
|
|
||||||
const params = [];
|
|
||||||
|
|
||||||
if (command.includes('SIZE=')) {
|
|
||||||
const sizeMatch = command.match(/SIZE=(\d+)/);
|
|
||||||
if (sizeMatch) params.push(`SIZE=${sizeMatch[1]}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.includes('BODY=')) {
|
|
||||||
const bodyMatch = command.match(/BODY=(\w+)/);
|
|
||||||
if (bodyMatch) params.push(`BODY=${bodyMatch[1]}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.includes('SMTPUTF8')) {
|
|
||||||
params.push('SMTPUTF8');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.includes('DELIVERBY=')) {
|
|
||||||
const deliverByMatch = command.match(/DELIVERBY=(\d+)/);
|
|
||||||
if (deliverByMatch) params.push(`DELIVERBY=${deliverByMatch[1]}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.length > 0) {
|
|
||||||
console.log(` [Server] Extension parameters: ${params.join(', ')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.write('250 2.1.0 Sender OK\r\n');
|
|
||||||
} else if (command.startsWith('RCPT TO:')) {
|
|
||||||
// Check for DSN parameters
|
|
||||||
if (command.includes('NOTIFY=')) {
|
|
||||||
const notifyMatch = command.match(/NOTIFY=([^,\s]+)/);
|
|
||||||
if (notifyMatch) {
|
|
||||||
console.log(` [Server] DSN NOTIFY: ${notifyMatch[1]}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.write('250 2.1.5 Recipient OK\r\n');
|
|
||||||
} else if (command === 'DATA') {
|
|
||||||
if (activeExtensions.includes('CHUNKING')) {
|
|
||||||
socket.write('503 5.5.1 Use BDAT when CHUNKING is available\r\n');
|
|
||||||
} else {
|
|
||||||
socket.write('354 Start mail input\r\n');
|
|
||||||
}
|
|
||||||
} else if (command.startsWith('BDAT ')) {
|
|
||||||
if (activeExtensions.includes('CHUNKING')) {
|
|
||||||
const parts = command.split(' ');
|
|
||||||
const size = parts[1];
|
|
||||||
const isLast = parts.includes('LAST');
|
|
||||||
console.log(` [Server] BDAT chunk: ${size} bytes${isLast ? ' (LAST)' : ''}`);
|
|
||||||
|
|
||||||
if (isLast) {
|
|
||||||
socket.write('250 2.0.0 Message accepted\r\n');
|
|
||||||
} else {
|
|
||||||
socket.write('250 2.0.0 Chunk accepted\r\n');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
socket.write('500 5.5.1 CHUNKING not available\r\n');
|
|
||||||
}
|
|
||||||
} else if (command === '.') {
|
|
||||||
socket.write('250 2.0.0 Message accepted\r\n');
|
|
||||||
} else if (command === 'QUIT') {
|
|
||||||
socket.write('221 2.0.0 Bye\r\n');
|
|
||||||
socket.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test email that could use multiple extensions
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Extension combination test with UTF-8: 测试',
|
|
||||||
text: 'Testing multiple SMTP extensions together',
|
|
||||||
dsn: {
|
|
||||||
notify: ['SUCCESS', 'FAILURE'],
|
|
||||||
envid: 'multi-ext-test-123'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
console.log(' Multiple extension combination handled');
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.messageId).toBeDefined();
|
|
||||||
|
|
||||||
await testServer.server.close();
|
|
||||||
})();
|
|
||||||
|
|
||||||
console.log(`\n${testId}: All ${scenarioCount} SMTP extension scenarios tested ✓`);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.start();
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { createTestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createTestSmtpClient } from '../../helpers/smtp.client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
tap.test('CSEC-01: TLS Security Tests', async () => {
|
|
||||||
console.log('\n🔒 Testing SMTP Client TLS Security');
|
|
||||||
console.log('=' .repeat(60));
|
|
||||||
|
|
||||||
// Test 1: Basic secure connection
|
|
||||||
console.log('\nTest 1: Basic secure connection');
|
|
||||||
const testServer1 = await createTestServer({});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: testServer1.hostname,
|
|
||||||
port: testServer1.port,
|
|
||||||
secure: false // Using STARTTLS instead of direct TLS
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'TLS Test',
|
|
||||||
text: 'Testing secure connection'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
console.log(' ✓ Email sent over secure connection');
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
testServer1.server.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 2: Connection with security options
|
|
||||||
console.log('\nTest 2: Connection with TLS options');
|
|
||||||
const testServer2 = await createTestServer({});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: testServer2.hostname,
|
|
||||||
port: testServer2.port,
|
|
||||||
secure: false,
|
|
||||||
tls: {
|
|
||||||
rejectUnauthorized: false // Accept self-signed for testing
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const verified = await smtpClient.verify();
|
|
||||||
console.log(' ✓ TLS connection established with custom options');
|
|
||||||
expect(verified).toBeDefined();
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
testServer2.server.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 3: Multiple secure emails
|
|
||||||
console.log('\nTest 3: Multiple secure emails');
|
|
||||||
const testServer3 = await createTestServer({});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: testServer3.hostname,
|
|
||||||
port: testServer3.port
|
|
||||||
});
|
|
||||||
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@secure.com',
|
|
||||||
to: [`recipient${i}@secure.com`],
|
|
||||||
subject: `Secure Email ${i + 1}`,
|
|
||||||
text: 'Testing TLS security'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
console.log(` ✓ Secure email ${i + 1} sent`);
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
}
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
testServer3.server.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n✅ CSEC-01: TLS security tests completed');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.start();
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createTestSmtpClient } from '../../helpers/smtp.client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup test SMTP server', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2562,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: true
|
|
||||||
});
|
|
||||||
expect(testServer).toBeTruthy();
|
|
||||||
expect(testServer.port).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CSEC-02: OAuth2 authentication configuration', async () => {
|
|
||||||
// Test client with OAuth2 configuration
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
auth: {
|
|
||||||
oauth2: {
|
|
||||||
user: 'oauth.user@example.com',
|
|
||||||
clientId: 'client-id-12345',
|
|
||||||
clientSecret: 'client-secret-67890',
|
|
||||||
accessToken: 'access-token-abcdef',
|
|
||||||
refreshToken: 'refresh-token-ghijkl'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test that OAuth2 config doesn't break the client
|
|
||||||
try {
|
|
||||||
const verified = await smtpClient.verify();
|
|
||||||
console.log('Client with OAuth2 config created successfully');
|
|
||||||
console.log('Note: Server does not support OAuth2, so auth will fail');
|
|
||||||
expect(verified).toBeFalsy(); // Expected to fail without OAuth2 support
|
|
||||||
} catch (error) {
|
|
||||||
console.log('OAuth2 authentication attempt:', error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CSEC-02: OAuth2 vs regular auth', async () => {
|
|
||||||
// Test regular auth (should work)
|
|
||||||
const regularClient = createTestSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
auth: {
|
|
||||||
user: 'testuser',
|
|
||||||
pass: 'testpass'
|
|
||||||
},
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: false
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const verified = await regularClient.verify();
|
|
||||||
console.log('Regular auth verification:', verified);
|
|
||||||
|
|
||||||
if (verified) {
|
|
||||||
// Send test email
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'Test with regular auth',
|
|
||||||
text: 'This uses regular PLAIN/LOGIN auth'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await regularClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
console.log('Email sent with regular auth');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Regular auth error:', error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
await regularClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CSEC-02: OAuth2 error handling', async () => {
|
|
||||||
// Test OAuth2 with invalid token
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
auth: {
|
|
||||||
method: 'OAUTH2',
|
|
||||||
oauth2: {
|
|
||||||
user: 'user@example.com',
|
|
||||||
clientId: 'test-client',
|
|
||||||
clientSecret: 'test-secret',
|
|
||||||
refreshToken: 'refresh-token',
|
|
||||||
accessToken: 'invalid-token'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: false
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'OAuth2 test',
|
|
||||||
text: 'Testing OAuth2 authentication'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
console.log('OAuth2 send result:', result.success);
|
|
||||||
} catch (error) {
|
|
||||||
console.log('OAuth2 error (expected):', error.message);
|
|
||||||
expect(error.message).toInclude('auth');
|
|
||||||
}
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup test SMTP server', async () => {
|
|
||||||
if (testServer) {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.start();
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
||||||
import { createTestSmtpClient } from '../../helpers/smtp.client.js';
|
|
||||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
||||||
import * as crypto from 'crypto';
|
|
||||||
|
|
||||||
let testServer: ITestServer;
|
|
||||||
|
|
||||||
tap.test('setup test SMTP server', async () => {
|
|
||||||
testServer = await startTestServer({
|
|
||||||
port: 2563,
|
|
||||||
tlsEnabled: false,
|
|
||||||
authRequired: false
|
|
||||||
});
|
|
||||||
expect(testServer).toBeTruthy();
|
|
||||||
expect(testServer.port).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CSEC-03: Basic DKIM signature structure', async () => {
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create email with DKIM configuration
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'DKIM Signed Email',
|
|
||||||
text: 'This email should be DKIM signed'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Note: DKIM signing would be handled by the Email class or SMTP client
|
|
||||||
// This test verifies the structure when it's implemented
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
|
|
||||||
console.log('Email sent successfully');
|
|
||||||
console.log('Note: DKIM signing functionality would be applied here');
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CSEC-03: DKIM with RSA key generation', async () => {
|
|
||||||
// Generate a test RSA key pair
|
|
||||||
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
|
|
||||||
modulusLength: 2048,
|
|
||||||
publicKeyEncoding: {
|
|
||||||
type: 'spki',
|
|
||||||
format: 'pem'
|
|
||||||
},
|
|
||||||
privateKeyEncoding: {
|
|
||||||
type: 'pkcs8',
|
|
||||||
format: 'pem'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Generated RSA key pair for DKIM:');
|
|
||||||
console.log('Public key (first line):', publicKey.split('\n')[1].substring(0, 50) + '...');
|
|
||||||
|
|
||||||
// Create DNS TXT record format
|
|
||||||
const publicKeyBase64 = publicKey
|
|
||||||
.replace(/-----BEGIN PUBLIC KEY-----/, '')
|
|
||||||
.replace(/-----END PUBLIC KEY-----/, '')
|
|
||||||
.replace(/\s/g, '');
|
|
||||||
|
|
||||||
console.log('\nDNS TXT record for default._domainkey.example.com:');
|
|
||||||
console.log(`v=DKIM1; k=rsa; p=${publicKeyBase64.substring(0, 50)}...`);
|
|
||||||
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: 'DKIM with Real RSA Key',
|
|
||||||
text: 'This email is signed with a real RSA key'
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('CSEC-03: DKIM body hash calculation', async () => {
|
|
||||||
const smtpClient = createTestSmtpClient({
|
|
||||||
host: testServer.hostname,
|
|
||||||
port: testServer.port,
|
|
||||||
secure: false,
|
|
||||||
connectionTimeout: 5000,
|
|
||||||
debug: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test body hash with different content
|
|
||||||
const testBodies = [
|
|
||||||
{ name: 'Simple text', body: 'Hello World' },
|
|
||||||
{ name: 'Multi-line text', body: 'Line 1\r\nLine 2\r\nLine 3' },
|
|
||||||
{ name: 'Empty body', body: '' }
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const test of testBodies) {
|
|
||||||
console.log(`\nTesting body hash for: ${test.name}`);
|
|
||||||
|
|
||||||
// Calculate expected body hash
|
|
||||||
const canonicalBody = test.body.replace(/\r\n/g, '\n').trimEnd() + '\n';
|
|
||||||
const bodyHash = crypto.createHash('sha256').update(canonicalBody).digest('base64');
|
|
||||||
console.log(` Expected hash: ${bodyHash.substring(0, 20)}...`);
|
|
||||||
|
|
||||||
const email = new Email({
|
|
||||||
from: 'sender@example.com',
|
|
||||||
to: ['recipient@example.com'],
|
|
||||||
subject: `Body Hash Test: ${test.name}`,
|
|
||||||
text: test.body
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await smtpClient.sendMail(email);
|
|
||||||
expect(result.success).toBeTruthy();
|
|
||||||
}
|
|
||||||
|
|
||||||
await smtpClient.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('cleanup test SMTP server', async () => {
|
|
||||||
if (testServer) {
|
|
||||||
await stopTestServer(testServer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.start();
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user