From 162795802f2db4b33028377afcb7de1deabef9a8 Mon Sep 17 00:00:00 2001 From: Philipp Kunz Date: Wed, 21 May 2025 02:17:18 +0000 Subject: [PATCH] update --- package.json | 12 +- pnpm-lock.yaml | 120 +- readme.plan.md | 76 ++ test/test.mta.ts | 258 ++++ test/test.smtp.client.ts | 258 ++++ ts/classes.dcrouter.ts | 9 +- ts/mail/delivery/classes.smtp.client.ts | 1201 +++++++++++++++++ ts/mail/delivery/classes.smtpserver.ts | 57 +- ts/mail/delivery/index.ts | 5 +- ts/mail/delivery/interfaces.ts | 223 +++ ts/mail/routing/classes.email.config.ts | 8 +- .../routing/classes.unified.email.server.ts | 72 +- ts/mail/services/classes.emailservice.ts | 8 +- ts/sms/classes.smsservice.ts | 8 +- ts/types/platform.interfaces.ts | 19 - 15 files changed, 2144 insertions(+), 190 deletions(-) create mode 100644 test/test.mta.ts create mode 100644 test/test.smtp.client.ts create mode 100644 ts/mail/delivery/classes.smtp.client.ts create mode 100644 ts/mail/delivery/interfaces.ts delete mode 100644 ts/types/platform.interfaces.ts diff --git a/package.json b/package.json index ac19c7a..28fe754 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { - "name": "@serve.zone/platformservice", + "name": "@serve.zone/dcrouter", "private": false, "version": "2.12.0", - "description": "A multifaceted platform service handling mail, SMS, letter delivery, and AI services.", + "description": "A multifaceted routing service handling mail and SMS delivery functions.", "main": "dist_ts/index.js", "typings": "dist_ts/index.d.ts", "type": "module", @@ -16,11 +16,11 @@ "localPublish": "" }, "devDependencies": { - "@git.zone/tsbuild": "^2.5.1", + "@git.zone/tsbuild": "^2.5.2", "@git.zone/tsrun": "^1.2.8", "@git.zone/tstest": "^1.9.0", "@git.zone/tswatch": "^2.0.1", - "@types/node": "^22.15.20" + "@types/node": "^22.15.21" }, "dependencies": { "@api.global/typedrequest": "^3.0.19", @@ -33,7 +33,7 @@ "@push.rocks/smartdata": "^5.15.1", "@push.rocks/smartdns": "^6.2.2", "@push.rocks/smartfile": "^11.0.4", - "@push.rocks/smartlog": "^3.1.7", + "@push.rocks/smartlog": "^3.1.8", "@push.rocks/smartmail": "^2.1.0", "@push.rocks/smartpath": "^5.0.5", "@push.rocks/smartpromise": "^4.0.3", @@ -59,7 +59,7 @@ "SMTP server", "mail parsing", "DKIM", - "platform service", + "mail router", "letterXpress", "OpenAI", "Anthropic AI", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de202ec..751837d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,8 +39,8 @@ importers: specifier: ^11.0.4 version: 11.2.0 '@push.rocks/smartlog': - specifier: ^3.1.7 - version: 3.1.7 + specifier: ^3.1.8 + version: 3.1.8 '@push.rocks/smartmail': specifier: ^2.1.0 version: 2.1.0 @@ -91,8 +91,8 @@ importers: version: 11.1.0 devDependencies: '@git.zone/tsbuild': - specifier: ^2.5.1 - version: 2.5.1 + specifier: ^2.5.2 + version: 2.5.2 '@git.zone/tsrun': specifier: ^1.2.8 version: 1.3.3 @@ -103,8 +103,8 @@ importers: specifier: ^2.0.1 version: 2.1.0 '@types/node': - specifier: ^22.15.20 - version: 22.15.20 + specifier: ^22.15.21 + version: 22.15.21 packages: @@ -623,8 +623,8 @@ packages: cpu: [x64] os: [win32] - '@git.zone/tsbuild@2.5.1': - resolution: {integrity: sha512-b1TyaNnaPCD3dvdRZ2da0MkZbH9liCrhzg57pwFIB2Gx4g8UMv8ZLN2cA1NRaNE0o8NCybf3gV1L+V0FO0DrMQ==} + '@git.zone/tsbuild@2.5.2': + resolution: {integrity: sha512-GoZ2vNgMe6OeGcejwhx7Sem8YCbwybEuU4r2/wWnrNrozw+HuT5UTROVGW7rTAxcxr2Hi4jWHSsuoCz9/6ZzrA==} hasBin: true '@git.zone/tsbundle@2.2.5': @@ -869,8 +869,8 @@ packages: '@push.rocks/smartlog-interfaces@3.0.2': resolution: {integrity: sha512-8hGRTJehbsFSJxLhCQkA018mZtXVPxPTblbg9VaE/EqISRzUw+eosJ2EJV7M4Qu0eiTJZjnWnNLn8CkD77ziWw==} - '@push.rocks/smartlog@3.1.7': - resolution: {integrity: sha512-ltg4oALFBrAflWMTOhN7lcK6OJ50MvrDx46wyXVGZZ+zRzYUUjBqyBzSXLQrOohOt/r6vQH+KYPgt6D0tYJA9A==} + '@push.rocks/smartlog@3.1.8': + resolution: {integrity: sha512-j4H5x4/hEmiIO7q+/LKyX3N+AhRIOj1jDE4TvZDvujZkbT/9wEWfpO1bqeMe/EQbg1eOQMlAuyrcLXUcDICpQg==} '@push.rocks/smartmail@2.1.0': resolution: {integrity: sha512-PUzxHuDQRdUZGKb5CejjkFr4JyRAZjUmEyX0jL/kTr9BEX+Ba+/t0cAOFC4v/xFbIxvFAlF3VvFtULCbgAvwaA==} @@ -1494,11 +1494,11 @@ packages: '@types/node-forge@1.3.11': resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==} - '@types/node@18.19.102': - resolution: {integrity: sha512-8+SHxopyHeI9xdylfts948LTTr7ZOCSQWMEEDS1qmFIv1kdl03YoMcy3H2NhmxeozCxJiTw6al1h5PAp9h0a5w==} + '@types/node@18.19.103': + resolution: {integrity: sha512-hHTHp+sEz6SxFsp+SA+Tqrua3AbmlAw+Y//aEwdHrdZkYVRWdvWD3y5uPZ0flYOkgskaFWqZ/YGFm3FaFQ0pRw==} - '@types/node@22.15.20': - resolution: {integrity: sha512-A6BohGFRGHAscJsTslDCA9JG7qSJr/DWUvrvY8yi9IgnGtMxCyat7vvQ//MFa0DnLsyuS3wYTpLdw4Hf+Q5JXw==} + '@types/node@22.15.21': + resolution: {integrity: sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==} '@types/ping@0.4.4': resolution: {integrity: sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==} @@ -4178,7 +4178,7 @@ snapshots: '@push.rocks/smartfeed': 1.0.11 '@push.rocks/smartfile': 11.2.0 '@push.rocks/smartjson': 5.0.20 - '@push.rocks/smartlog': 3.1.7 + '@push.rocks/smartlog': 3.1.8 '@push.rocks/smartlog-destination-devtools': 1.0.12 '@push.rocks/smartlog-interfaces': 3.0.2 '@push.rocks/smartmanifest': 2.0.2 @@ -4232,7 +4232,7 @@ snapshots: '@apiclient.xyz/cloudflare@6.4.1': dependencies: '@push.rocks/smartdelay': 3.0.5 - '@push.rocks/smartlog': 3.1.7 + '@push.rocks/smartlog': 3.1.8 '@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartrequest': 2.1.0 '@push.rocks/smartstring': 4.0.15 @@ -5001,14 +5001,14 @@ snapshots: '@esbuild/win32-x64@0.25.4': optional: true - '@git.zone/tsbuild@2.5.1': + '@git.zone/tsbuild@2.5.2': dependencies: '@git.zone/tspublish': 1.9.1 '@push.rocks/early': 4.0.4 '@push.rocks/smartcli': 4.0.11 '@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartfile': 11.2.0 - '@push.rocks/smartlog': 3.1.7 + '@push.rocks/smartlog': 3.1.8 '@push.rocks/smartpath': 5.0.18 '@push.rocks/smartpromise': 4.2.3 typescript: 5.7.3 @@ -5021,7 +5021,7 @@ snapshots: '@push.rocks/smartcli': 4.0.11 '@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartfile': 11.2.0 - '@push.rocks/smartlog': 3.1.7 + '@push.rocks/smartlog': 3.1.8 '@push.rocks/smartlog-destination-local': 9.0.2 '@push.rocks/smartpath': 5.0.18 '@push.rocks/smartpromise': 4.2.3 @@ -5038,7 +5038,7 @@ snapshots: '@push.rocks/smartcli': 4.0.11 '@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartfile': 11.2.0 - '@push.rocks/smartlog': 3.1.7 + '@push.rocks/smartlog': 3.1.8 '@push.rocks/smartnpm': 2.0.4 '@push.rocks/smartpath': 5.0.18 '@push.rocks/smartrequest': 2.1.0 @@ -5066,7 +5066,7 @@ snapshots: '@push.rocks/smartexpect': 2.4.2 '@push.rocks/smartfile': 11.2.0 '@push.rocks/smartjson': 5.0.20 - '@push.rocks/smartlog': 3.1.7 + '@push.rocks/smartlog': 3.1.8 '@push.rocks/smartmongo': 2.0.12(@aws-sdk/credential-providers@3.812.0)(socks@2.8.4) '@push.rocks/smartpath': 5.0.18 '@push.rocks/smartpromise': 4.2.3 @@ -5106,7 +5106,7 @@ snapshots: '@push.rocks/smartcli': 4.0.11 '@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartfile': 11.2.0 - '@push.rocks/smartlog': 3.1.7 + '@push.rocks/smartlog': 3.1.8 '@push.rocks/smartlog-destination-local': 9.0.2 '@push.rocks/smartshell': 3.2.3 '@push.rocks/taskbuffer': 3.1.7 @@ -5384,7 +5384,7 @@ snapshots: '@api.global/typedrequest': 3.1.10 '@configvault.io/interfaces': 1.0.17 '@push.rocks/smartfile': 11.2.0 - '@push.rocks/smartlog': 3.1.7 + '@push.rocks/smartlog': 3.1.8 '@push.rocks/smartpath': 5.0.18 '@push.rocks/smartacme@8.0.0(@aws-sdk/credential-providers@3.812.0)(socks@2.8.4)': @@ -5396,7 +5396,7 @@ snapshots: '@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdns': 6.2.2 '@push.rocks/smartfile': 11.2.0 - '@push.rocks/smartlog': 3.1.7 + '@push.rocks/smartlog': 3.1.8 '@push.rocks/smartnetwork': 4.0.2 '@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartrequest': 2.1.0 @@ -5410,7 +5410,6 @@ snapshots: - '@mongodb-js/zstd' - '@nuxt/kit' - aws-crt - - bufferutil - encoding - gcp-metadata - kerberos @@ -5419,7 +5418,6 @@ snapshots: - snappy - socks - supports-color - - utf-8-validate - vue '@push.rocks/smartarchive@3.0.8': @@ -5486,7 +5484,7 @@ snapshots: '@push.rocks/smartcli@4.0.11': dependencies: '@push.rocks/lik': 6.2.2 - '@push.rocks/smartlog': 3.1.7 + '@push.rocks/smartlog': 3.1.8 '@push.rocks/smartobject': 1.0.12 '@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartrx': 3.0.10 @@ -5511,7 +5509,7 @@ snapshots: dependencies: '@push.rocks/lik': 6.2.2 '@push.rocks/smartdelay': 3.0.5 - '@push.rocks/smartlog': 3.1.7 + '@push.rocks/smartlog': 3.1.8 '@push.rocks/smartmongo': 2.0.12(@aws-sdk/credential-providers@3.812.0)(socks@2.8.4) '@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartrx': 3.0.10 @@ -5649,7 +5647,7 @@ snapshots: '@api.global/typedrequest-interfaces': 2.0.2 '@tsclass/tsclass': 4.4.4 - '@push.rocks/smartlog@3.1.7': + '@push.rocks/smartlog@3.1.8': dependencies: '@api.global/typedrequest-interfaces': 3.0.19 '@push.rocks/consolecolor': 2.0.2 @@ -5820,7 +5818,7 @@ snapshots: '@push.rocks/smartcrypto': 2.0.4 '@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartfile': 11.2.0 - '@push.rocks/smartlog': 3.1.7 + '@push.rocks/smartlog': 3.1.8 '@push.rocks/smartnetwork': 4.0.2 '@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartrequest': 2.1.0 @@ -5922,7 +5920,7 @@ snapshots: '@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartenv': 5.0.12 '@push.rocks/smartjson': 5.0.20 - '@push.rocks/smartlog': 3.1.7 + '@push.rocks/smartlog': 3.1.8 '@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartrx': 3.0.10 '@push.rocks/smarttime': 4.1.1 @@ -6020,7 +6018,7 @@ snapshots: dependencies: '@push.rocks/lik': 6.2.2 '@push.rocks/smartdelay': 3.0.5 - '@push.rocks/smartlog': 3.1.7 + '@push.rocks/smartlog': 3.1.8 '@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartrx': 3.0.10 '@push.rocks/smarttime': 4.1.1 @@ -6616,27 +6614,27 @@ snapshots: '@types/bn.js@5.1.6': dependencies: - '@types/node': 22.15.20 + '@types/node': 22.15.21 '@types/body-parser@1.19.5': dependencies: '@types/connect': 3.4.38 - '@types/node': 22.15.20 + '@types/node': 22.15.21 '@types/buffer-json@2.0.3': {} '@types/clean-css@4.2.11': dependencies: - '@types/node': 22.15.20 + '@types/node': 22.15.21 source-map: 0.6.1 '@types/connect@3.4.38': dependencies: - '@types/node': 22.15.20 + '@types/node': 22.15.21 '@types/cors@2.8.18': dependencies: - '@types/node': 22.15.20 + '@types/node': 22.15.21 '@types/debug@4.1.12': dependencies: @@ -6648,7 +6646,7 @@ snapshots: '@types/dns-packet@5.6.5': dependencies: - '@types/node': 22.15.20 + '@types/node': 22.15.21 '@types/elliptic@6.4.18': dependencies: @@ -6656,7 +6654,7 @@ snapshots: '@types/express-serve-static-core@5.0.6': dependencies: - '@types/node': 22.15.20 + '@types/node': 22.15.21 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -6673,30 +6671,30 @@ snapshots: '@types/from2@2.3.5': dependencies: - '@types/node': 22.15.20 + '@types/node': 22.15.21 '@types/fs-extra@11.0.4': dependencies: '@types/jsonfile': 6.1.4 - '@types/node': 22.15.20 + '@types/node': 22.15.21 '@types/fs-extra@9.0.13': dependencies: - '@types/node': 22.15.20 + '@types/node': 22.15.21 '@types/glob@7.2.0': dependencies: '@types/minimatch': 5.1.2 - '@types/node': 22.15.20 + '@types/node': 22.15.21 '@types/glob@8.1.0': dependencies: '@types/minimatch': 5.1.2 - '@types/node': 22.15.20 + '@types/node': 22.15.21 '@types/gunzip-maybe@1.4.2': dependencies: - '@types/node': 22.15.20 + '@types/node': 22.15.21 '@types/hast@3.0.4': dependencies: @@ -6718,11 +6716,11 @@ snapshots: '@types/jsonfile@6.1.4': dependencies: - '@types/node': 22.15.20 + '@types/node': 22.15.21 '@types/mailparser@3.4.6': dependencies: - '@types/node': 22.15.20 + '@types/node': 22.15.21 iconv-lite: 0.6.3 '@types/mdast@4.0.4': @@ -6741,18 +6739,18 @@ snapshots: '@types/node-fetch@2.6.12': dependencies: - '@types/node': 22.15.20 + '@types/node': 22.15.21 form-data: 4.0.2 '@types/node-forge@1.3.11': dependencies: - '@types/node': 22.15.20 + '@types/node': 22.15.21 - '@types/node@18.19.102': + '@types/node@18.19.103': dependencies: undici-types: 5.26.5 - '@types/node@22.15.20': + '@types/node@22.15.21': dependencies: undici-types: 6.21.0 @@ -6768,30 +6766,30 @@ snapshots: '@types/s3rver@3.7.4': dependencies: - '@types/node': 22.15.20 + '@types/node': 22.15.21 '@types/semver@7.7.0': {} '@types/send@0.17.4': dependencies: '@types/mime': 1.3.5 - '@types/node': 22.15.20 + '@types/node': 22.15.21 '@types/serve-static@1.15.7': dependencies: '@types/http-errors': 2.0.4 - '@types/node': 22.15.20 + '@types/node': 22.15.21 '@types/send': 0.17.4 '@types/symbol-tree@3.2.5': {} '@types/tar-stream@2.2.3': dependencies: - '@types/node': 22.15.20 + '@types/node': 22.15.21 '@types/through2@2.0.41': dependencies: - '@types/node': 22.15.20 + '@types/node': 22.15.21 '@types/triple-beam@1.3.5': {} @@ -6815,18 +6813,18 @@ snapshots: '@types/whatwg-url@8.2.2': dependencies: - '@types/node': 22.15.20 + '@types/node': 22.15.21 '@types/webidl-conversions': 7.0.3 '@types/which@3.0.4': {} '@types/ws@8.18.1': dependencies: - '@types/node': 22.15.20 + '@types/node': 22.15.21 '@types/yauzl@2.10.3': dependencies: - '@types/node': 22.15.20 + '@types/node': 22.15.21 optional: true '@ungap/structured-clone@1.3.0': {} @@ -7106,7 +7104,7 @@ snapshots: cloudflare@4.2.0: dependencies: - '@types/node': 18.19.102 + '@types/node': 18.19.103 '@types/node-fetch': 2.6.12 abort-controller: 3.0.0 agentkeepalive: 4.6.0 @@ -7377,7 +7375,7 @@ snapshots: engine.io@6.6.4: dependencies: '@types/cors': 2.8.18 - '@types/node': 22.15.20 + '@types/node': 22.15.21 accepts: 1.3.8 base64id: 2.0.0 cookie: 0.7.2 diff --git a/readme.plan.md b/readme.plan.md index e69de29..bd6454f 100644 --- a/readme.plan.md +++ b/readme.plan.md @@ -0,0 +1,76 @@ +# DCRouter Project Improvement Plan + +## Type Safety Enhancement Plan + +Our goal is to improve type safety across the DCRouter codebase to reduce runtime errors, improve developer experience, and make the code more maintainable. This document outlines the specific changes we'll implement. + +### 1. Platform Service Interface Improvements + +- [ ] Create a comprehensive `IPlatformService` interface to replace `any` type usage +- [ ] Define specific methods and properties that platform services should implement +- [ ] Update all references to use the new interface + +### 2. SMTP Session Type Safety + +- [ ] Define a proper `ISmtpSession` interface with all required properties +- [ ] Ensure consistent usage across SMTPServer implementation +- [ ] Add proper validation for session properties + +### 3. Configuration Type Enhancements + +- [ ] Replace loose config objects with strictly typed interfaces +- [ ] Add validation functions for all configuration objects +- [ ] Create union types for configuration options with specific values + +### 4. Function Return Types + +- [ ] Audit all async functions to ensure they have explicit return types +- [ ] Replace `Promise` with specific return type interfaces +- [ ] Add proper error types for rejected promises + +### 5. String Literal Types and Enums + +- [ ] Replace string constants with proper TypeScript enums or string literal unions +- [ ] Create specific types for email status values, priorities, etc. +- [ ] Ensure consistent usage throughout the codebase + +### 6. Event System Types + +- [ ] Create typed event emitters for all event-based components +- [ ] Define specific event payload interfaces for each event type +- [ ] Ensure type safety for event handlers + +### 7. Authentication Data Types + +- [ ] Create proper interfaces for authentication data +- [ ] Replace generic Record types with specific property interfaces +- [ ] Add validation for auth data objects + +### 8. Email Attachment Type Safety + +- [ ] Define comprehensive interfaces for email attachments +- [ ] Ensure consistent usage between different email handling components +- [ ] Add validation for attachment properties + +### 9. Processing Mode Type Safety + +- [ ] Create discriminated unions for different email processing modes +- [ ] Add type guards to ensure safe handling of mode-specific properties +- [ ] Ensure proper validation of processing mode values + +### 10. Third-Party Integration Types + +- [ ] Review and update type definitions for third-party dependencies +- [ ] Create additional type definitions where missing +- [ ] Ensure consistent usage of external libraries + +## Implementation Order + +We'll implement these improvements in the following order: + +1. First, focus on the core interfaces (Platform Service, SMTP Session) +2. Next, improve configuration types as they affect multiple components +3. Then, address function return types and string literals +4. Finally, handle the remaining specialized types (auth, attachments, etc.) + +This approach allows us to tackle the most widely-used types first, providing the greatest immediate benefit while establishing patterns for the rest of the implementation. \ No newline at end of file diff --git a/test/test.mta.ts b/test/test.mta.ts new file mode 100644 index 0000000..fe828f9 --- /dev/null +++ b/test/test.mta.ts @@ -0,0 +1,258 @@ +import { tap, expect } from '@git.zone/tstest/tapbundle'; +import * as plugins from '../ts/plugins.js'; +import * as paths from '../ts/paths.js'; +import { SmtpClient } from '../ts/mail/delivery/classes.smtp.client.js'; +import type { ISmtpClientOptions } from '../ts/mail/delivery/classes.smtp.client.js'; +import { Email } from '../ts/mail/core/classes.email.js'; + +/** + * Tests for the SMTP client class + */ +tap.test('verify SMTP client initialization', async () => { + // Create test configuration + const options: ISmtpClientOptions = { + host: 'smtp.example.com', + port: 587, + secure: false, + connectionTimeout: 10000, + domain: 'test.example.com' + }; + + // Create MTA instance + const mta = new MailTransferAgent(options); + + // Verify instance was created correctly + expect(mta).toBeTruthy(); + expect(mta.isConnected()).toBeFalsy(); // Should start disconnected +}); + +tap.test('test MTA configuration update', async () => { + // Create test configuration + const options: IMtaConnectionOptions = { + host: 'smtp.example.com', + port: 587, + secure: false + }; + + // Create MTA instance + const mta = new MailTransferAgent(options); + + // Update configuration + mta.updateOptions({ + host: 'new-smtp.example.com', + port: 465, + secure: true + }); + + // Can't directly test private fields, but we can verify it doesn't throw + expect(() => mta.updateOptions({ + tls: { + rejectUnauthorized: false + } + })).not.toThrow(); +}); + +// Mocked SMTP server for testing +class MockSmtpServer { + private responses: Map; + + constructor() { + this.responses = new Map(); + + // Default responses + this.responses.set('connect', '220 smtp.example.com ESMTP ready'); + this.responses.set('EHLO', '250-smtp.example.com\r\n250-PIPELINING\r\n250-SIZE 10240000\r\n250-STARTTLS\r\n250-AUTH PLAIN LOGIN\r\n250 HELP'); + this.responses.set('MAIL FROM', '250 OK'); + this.responses.set('RCPT TO', '250 OK'); + this.responses.set('DATA', '354 Start mail input; end with .'); + this.responses.set('data content', '250 OK: message accepted'); + this.responses.set('QUIT', '221 Bye'); + } + + public setResponse(command: string, response: string): void { + this.responses.set(command, response); + } + + public getResponse(command: string): string { + if (command.startsWith('MAIL FROM')) { + return this.responses.get('MAIL FROM') || '250 OK'; + } else if (command.startsWith('RCPT TO')) { + return this.responses.get('RCPT TO') || '250 OK'; + } else if (command.startsWith('EHLO') || command.startsWith('HELO')) { + return this.responses.get('EHLO') || '250 OK'; + } else if (command === 'DATA') { + return this.responses.get('DATA') || '354 Start mail input; end with .'; + } else if (command.includes('Content-Type')) { + return this.responses.get('data content') || '250 OK: message accepted'; + } else if (command === 'QUIT') { + return this.responses.get('QUIT') || '221 Bye'; + } + + return this.responses.get(command) || '250 OK'; + } +} + +/** + * This test validates the MTA capabilities without connecting to a real server + * It uses a simple mock that only checks method signatures and properties + */ +tap.test('verify MTA email delivery functionality with mock', async () => { + // Create a mock SMTP server + const mockServer = new MockSmtpServer(); + + // Create a test email + const testEmail = new Email({ + from: 'sender@example.com', + to: ['recipient@example.com'], + subject: 'Test Email', + text: 'This is a test email' + }); + + // Create MTA options + const options: IMtaConnectionOptions = { + host: 'smtp.example.com', + port: 587, + secure: false, + domain: 'test.example.com', + auth: { + user: 'testuser', + pass: 'testpass' + } + }; + + // Create MTA instance + const mta = new MailTransferAgent(options); + + // Mock the connect method + mta['connect'] = async function() { + // @ts-ignore: setting private property for testing + this.connected = true; + // @ts-ignore: setting private property for testing + this.socket = { + write: (data: string, callback: () => void) => { + callback(); + }, + on: () => {}, + once: () => {}, + removeListener: () => {}, + destroy: () => {}, + setTimeout: () => {} + }; + // @ts-ignore: setting private property for testing + this.supportedExtensions = new Set(['PIPELINING', 'SIZE', 'STARTTLS', 'AUTH']); + + return Promise.resolve(); + }; + + // Mock the sendCommand method + mta['sendCommand'] = async function(command: string) { + return Promise.resolve(mockServer.getResponse(command)); + }; + + // Mock the readResponse method + mta['readResponse'] = async function() { + return Promise.resolve(mockServer.getResponse('connect')); + }; + + // Test sending an email + try { + const result = await mta.sendMail(testEmail); + + // Verify the result + expect(result).toBeTruthy(); + expect(result.success).toEqual(true); + expect(result.acceptedRecipients).toEqual(['recipient@example.com']); + expect(result.rejectedRecipients).toEqual([]); + + } catch (error) { + // This should not happen + expect(error).toBeUndefined(); + } + + // Test closing the connection + await mta.close(); + expect(mta.isConnected()).toBeFalsy(); +}); + +tap.test('test MTA error handling with mock', async () => { + // Create a mock SMTP server + const mockServer = new MockSmtpServer(); + + // Set error response for RCPT TO + mockServer.setResponse('RCPT TO', '550 No such user here'); + + // Create a test email + const testEmail = new Email({ + from: 'sender@example.com', + to: ['unknown@example.com'], + subject: 'Test Email', + text: 'This is a test email' + }); + + // Create MTA instance + const mta = new MailTransferAgent({ + host: 'smtp.example.com', + port: 587, + secure: false + }); + + // Mock the connect method + mta['connect'] = async function() { + // @ts-ignore: setting private property for testing + this.connected = true; + // @ts-ignore: setting private property for testing + this.socket = { + write: (data: string, callback: () => void) => { + callback(); + }, + on: () => {}, + once: () => {}, + removeListener: () => {}, + destroy: () => {}, + setTimeout: () => {} + }; + // @ts-ignore: setting private property for testing + this.supportedExtensions = new Set(['PIPELINING', 'SIZE', 'STARTTLS', 'AUTH']); + + return Promise.resolve(); + }; + + // Mock the sendCommand method + mta['sendCommand'] = async function(command: string) { + const response = mockServer.getResponse(command); + + // Simulate an error response for RCPT TO + if (command.startsWith('RCPT TO') && response.startsWith('550')) { + const error = new Error(response); + error['context'] = { + data: { + statusCode: '550' + } + }; + throw error; + } + + return Promise.resolve(response); + }; + + // Test sending an email that will fail + const result = await mta.sendMail(testEmail); + + // Verify the result shows failure + expect(result).toBeTruthy(); + expect(result.success).toEqual(false); + expect(result.acceptedRecipients).toEqual([]); + expect(result.rejectedRecipients).toEqual(['unknown@example.com']); + expect(result.error).toBeTruthy(); +}); + +// Final clean-up test +tap.test('clean up after tests', async () => { + // No-op - just to make sure everything is cleaned up properly +}); + +tap.test('stop', async () => { + await tap.stopForcefully(); +}); + +export default tap.start(); \ No newline at end of file diff --git a/test/test.smtp.client.ts b/test/test.smtp.client.ts new file mode 100644 index 0000000..1587308 --- /dev/null +++ b/test/test.smtp.client.ts @@ -0,0 +1,258 @@ +import { tap, expect } from '@git.zone/tstest/tapbundle'; +import * as plugins from '../ts/plugins.js'; +import * as paths from '../ts/paths.js'; +import { SmtpClient } from '../ts/mail/delivery/classes.smtp.client.js'; +import type { ISmtpClientOptions } from '../ts/mail/delivery/classes.smtp.client.js'; +import { Email } from '../ts/mail/core/classes.email.js'; + +/** + * Tests for the SMTP client class + */ +tap.test('verify SMTP client initialization', async () => { + // Create test configuration + const options: ISmtpClientOptions = { + host: 'smtp.example.com', + port: 587, + secure: false, + connectionTimeout: 10000, + domain: 'test.example.com' + }; + + // Create SMTP client instance + const smtpClient = new SmtpClient(options); + + // Verify instance was created correctly + expect(smtpClient).toBeTruthy(); + expect(smtpClient.isConnected()).toBeFalsy(); // Should start disconnected +}); + +tap.test('test SMTP client configuration update', async () => { + // Create test configuration + const options: ISmtpClientOptions = { + host: 'smtp.example.com', + port: 587, + secure: false + }; + + // Create SMTP client instance + const smtpClient = new SmtpClient(options); + + // Update configuration + smtpClient.updateOptions({ + host: 'new-smtp.example.com', + port: 465, + secure: true + }); + + // Can't directly test private fields, but we can verify it doesn't throw + expect(() => smtpClient.updateOptions({ + tls: { + rejectUnauthorized: false + } + })).not.toThrow(); +}); + +// Mocked SMTP server for testing +class MockSmtpServer { + private responses: Map; + + constructor() { + this.responses = new Map(); + + // Default responses + this.responses.set('connect', '220 smtp.example.com ESMTP ready'); + this.responses.set('EHLO', '250-smtp.example.com\r\n250-PIPELINING\r\n250-SIZE 10240000\r\n250-STARTTLS\r\n250-AUTH PLAIN LOGIN\r\n250 HELP'); + this.responses.set('MAIL FROM', '250 OK'); + this.responses.set('RCPT TO', '250 OK'); + this.responses.set('DATA', '354 Start mail input; end with .'); + this.responses.set('data content', '250 OK: message accepted'); + this.responses.set('QUIT', '221 Bye'); + } + + public setResponse(command: string, response: string): void { + this.responses.set(command, response); + } + + public getResponse(command: string): string { + if (command.startsWith('MAIL FROM')) { + return this.responses.get('MAIL FROM') || '250 OK'; + } else if (command.startsWith('RCPT TO')) { + return this.responses.get('RCPT TO') || '250 OK'; + } else if (command.startsWith('EHLO') || command.startsWith('HELO')) { + return this.responses.get('EHLO') || '250 OK'; + } else if (command === 'DATA') { + return this.responses.get('DATA') || '354 Start mail input; end with .'; + } else if (command.includes('Content-Type')) { + return this.responses.get('data content') || '250 OK: message accepted'; + } else if (command === 'QUIT') { + return this.responses.get('QUIT') || '221 Bye'; + } + + return this.responses.get(command) || '250 OK'; + } +} + +/** + * This test validates the SMTP client capabilities without connecting to a real server + * It uses a simple mock that only checks method signatures and properties + */ +tap.test('verify SMTP client email delivery functionality with mock', async () => { + // Create a mock SMTP server + const mockServer = new MockSmtpServer(); + + // Create a test email + const testEmail = new Email({ + from: 'sender@example.com', + to: ['recipient@example.com'], + subject: 'Test Email', + text: 'This is a test email' + }); + + // Create SMTP client options + const options: ISmtpClientOptions = { + host: 'smtp.example.com', + port: 587, + secure: false, + domain: 'test.example.com', + auth: { + user: 'testuser', + pass: 'testpass' + } + }; + + // Create SMTP client instance + const smtpClient = new SmtpClient(options); + + // Mock the connect method + smtpClient['connect'] = async function() { + // @ts-ignore: setting private property for testing + this.connected = true; + // @ts-ignore: setting private property for testing + this.socket = { + write: (data: string, callback: () => void) => { + callback(); + }, + on: () => {}, + once: () => {}, + removeListener: () => {}, + destroy: () => {}, + setTimeout: () => {} + }; + // @ts-ignore: setting private property for testing + this.supportedExtensions = new Set(['PIPELINING', 'SIZE', 'STARTTLS', 'AUTH']); + + return Promise.resolve(); + }; + + // Mock the sendCommand method + smtpClient['sendCommand'] = async function(command: string) { + return Promise.resolve(mockServer.getResponse(command)); + }; + + // Mock the readResponse method + smtpClient['readResponse'] = async function() { + return Promise.resolve(mockServer.getResponse('connect')); + }; + + // Test sending an email + try { + const result = await smtpClient.sendMail(testEmail); + + // Verify the result + expect(result).toBeTruthy(); + expect(result.success).toEqual(true); + expect(result.acceptedRecipients).toEqual(['recipient@example.com']); + expect(result.rejectedRecipients).toEqual([]); + + } catch (error) { + // This should not happen + expect(error).toBeUndefined(); + } + + // Test closing the connection + await smtpClient.close(); + expect(smtpClient.isConnected()).toBeFalsy(); +}); + +tap.test('test SMTP client error handling with mock', async () => { + // Create a mock SMTP server + const mockServer = new MockSmtpServer(); + + // Set error response for RCPT TO + mockServer.setResponse('RCPT TO', '550 No such user here'); + + // Create a test email + const testEmail = new Email({ + from: 'sender@example.com', + to: ['unknown@example.com'], + subject: 'Test Email', + text: 'This is a test email' + }); + + // Create SMTP client instance + const smtpClient = new SmtpClient({ + host: 'smtp.example.com', + port: 587, + secure: false + }); + + // Mock the connect method + smtpClient['connect'] = async function() { + // @ts-ignore: setting private property for testing + this.connected = true; + // @ts-ignore: setting private property for testing + this.socket = { + write: (data: string, callback: () => void) => { + callback(); + }, + on: () => {}, + once: () => {}, + removeListener: () => {}, + destroy: () => {}, + setTimeout: () => {} + }; + // @ts-ignore: setting private property for testing + this.supportedExtensions = new Set(['PIPELINING', 'SIZE', 'STARTTLS', 'AUTH']); + + return Promise.resolve(); + }; + + // Mock the sendCommand method + smtpClient['sendCommand'] = async function(command: string) { + const response = mockServer.getResponse(command); + + // Simulate an error response for RCPT TO + if (command.startsWith('RCPT TO') && response.startsWith('550')) { + const error = new Error(response); + error['context'] = { + data: { + statusCode: '550' + } + }; + throw error; + } + + return Promise.resolve(response); + }; + + // Test sending an email that will fail + const result = await smtpClient.sendMail(testEmail); + + // Verify the result shows failure + expect(result).toBeTruthy(); + expect(result.success).toEqual(false); + expect(result.acceptedRecipients).toEqual([]); + expect(result.rejectedRecipients).toEqual(['unknown@example.com']); + expect(result.error).toBeTruthy(); +}); + +// Final clean-up test +tap.test('clean up after tests', async () => { + // No-op - just to make sure everything is cleaned up properly +}); + +tap.test('stop', async () => { + await tap.stopForcefully(); +}); + +export default tap.start(); \ No newline at end of file diff --git a/ts/classes.dcrouter.ts b/ts/classes.dcrouter.ts index f4245be..bcfed7e 100644 --- a/ts/classes.dcrouter.ts +++ b/ts/classes.dcrouter.ts @@ -94,20 +94,16 @@ export class DcRouter { public deliverySystem?: MultiModeDeliverySystem; public rateLimiter?: UnifiedRateLimiter; - // Reference to the platform service for accessing MTA and other services - public platformServiceRef?: any; // Environment access private qenv = new plugins.qenv.Qenv('./', '.nogit/'); - constructor(optionsArg: IDcRouterOptions, platformServiceRef?: any) { + constructor(optionsArg: IDcRouterOptions) { // Set defaults in options this.options = { ...optionsArg }; - // Store reference to platform service if provided - this.platformServiceRef = platformServiceRef; } public async start() { @@ -681,9 +677,6 @@ export class DcRouter { }): Promise { logger.log('info', 'Configuring MTA service with custom settings'); - if (!this.platformServiceRef) { - throw new Error('Platform service reference is required for MTA configuration'); - } // Update email port configuration if (!this.options.emailPortConfig) { diff --git a/ts/mail/delivery/classes.smtp.client.ts b/ts/mail/delivery/classes.smtp.client.ts new file mode 100644 index 0000000..244f057 --- /dev/null +++ b/ts/mail/delivery/classes.smtp.client.ts @@ -0,0 +1,1201 @@ +import * as plugins from '../../plugins.js'; +import { logger } from '../../logger.js'; +import { + SecurityLogger, + SecurityLogLevel, + SecurityEventType +} from '../../security/index.js'; + +import { + MtaConnectionError, + MtaAuthenticationError, + MtaDeliveryError, + MtaConfigurationError, + MtaTimeoutError, + MtaProtocolError +} from '../../errors/index.js'; + +import { Email } from '../core/classes.email.js'; +import type { EmailProcessingMode } from './interfaces.js'; + +// Custom error type extension +interface NodeNetworkError extends Error { + code?: string; +} + +/** + * SMTP client connection options + */ +export type ISmtpClientOptions = { + /** + * Hostname of the SMTP server + */ + host: string; + + /** + * Port to connect to + */ + port: number; + + /** + * Whether to use TLS for the connection + */ + secure?: boolean; + + /** + * Connection timeout in milliseconds + */ + connectionTimeout?: number; + + /** + * Socket timeout in milliseconds + */ + socketTimeout?: number; + + /** + * Command timeout in milliseconds + */ + commandTimeout?: number; + + /** + * TLS options + */ + tls?: { + /** + * Whether to verify certificates + */ + rejectUnauthorized?: boolean; + + /** + * Minimum TLS version + */ + minVersion?: string; + + /** + * CA certificate path + */ + ca?: string; + }; + + /** + * Authentication options + */ + auth?: { + /** + * Authentication user + */ + user: string; + + /** + * Authentication password + */ + pass: string; + + /** + * Authentication method + */ + method?: 'PLAIN' | 'LOGIN' | 'OAUTH2'; + }; + + /** + * Domain name for EHLO + */ + domain?: string; + + /** + * DKIM options for signing outgoing emails + */ + dkim?: { + /** + * Whether to sign emails with DKIM + */ + enabled: boolean; + + /** + * Domain name for DKIM + */ + domain: string; + + /** + * Selector for DKIM + */ + selector: string; + + /** + * Private key for DKIM signing + */ + privateKey: string; + + /** + * Headers to sign + */ + headers?: string[]; + }; +}; + +/** + * SMTP delivery result + */ +export type ISmtpDeliveryResult = { + /** + * Whether the delivery was successful + */ + success: boolean; + + /** + * Message ID if successful + */ + messageId?: string; + + /** + * Error message if failed + */ + error?: string; + + /** + * SMTP response code + */ + responseCode?: string; + + /** + * Recipients successfully delivered to + */ + acceptedRecipients: string[]; + + /** + * Recipients rejected during delivery + */ + rejectedRecipients: string[]; + + /** + * Server response + */ + response?: string; + + /** + * Timestamp of the delivery attempt + */ + timestamp: number; + + /** + * Whether DKIM signing was applied + */ + dkimSigned?: boolean; + + /** + * Whether this was a TLS secured delivery + */ + secure?: boolean; + + /** + * Whether authentication was used + */ + authenticated?: boolean; +}; + +/** + * SMTP client for sending emails to remote mail servers + */ +export class SmtpClient { + private options: ISmtpClientOptions; + private connected: boolean = false; + private socket?: plugins.net.Socket | plugins.tls.TLSSocket; + private supportedExtensions: Set = new Set(); + + /** + * Create a new SMTP client instance + * @param options SMTP client connection options + */ + constructor(options: ISmtpClientOptions) { + // Set default options + this.options = { + ...options, + connectionTimeout: options.connectionTimeout || 30000, // 30 seconds + socketTimeout: options.socketTimeout || 60000, // 60 seconds + commandTimeout: options.commandTimeout || 30000, // 30 seconds + secure: options.secure || false, + domain: options.domain || 'localhost', + tls: { + rejectUnauthorized: options.tls?.rejectUnauthorized !== false, // Default to true + minVersion: options.tls?.minVersion || 'TLSv1.2' + } + }; + } + + /** + * Connect to the SMTP server + */ + public async connect(): Promise { + if (this.connected && this.socket) { + return; + } + + try { + logger.log('info', `Connecting to SMTP server ${this.options.host}:${this.options.port}`); + + // Create socket + const socket = new plugins.net.Socket(); + + // Set timeouts + socket.setTimeout(this.options.socketTimeout); + + // Connect to the server + await new Promise((resolve, reject) => { + // Handle connection events + socket.once('connect', () => { + logger.log('debug', `Connected to ${this.options.host}:${this.options.port}`); + resolve(); + }); + + socket.once('timeout', () => { + reject(MtaConnectionError.timeout( + this.options.host, + this.options.port, + this.options.connectionTimeout + )); + }); + + socket.once('error', (err: NodeNetworkError) => { + if (err.code === 'ECONNREFUSED') { + reject(MtaConnectionError.refused( + this.options.host, + this.options.port + )); + } else if (err.code === 'ENOTFOUND') { + reject(MtaConnectionError.dnsError( + this.options.host, + err + )); + } else { + reject(new MtaConnectionError( + `Connection error to ${this.options.host}:${this.options.port}: ${err.message}`, + { + data: { + host: this.options.host, + port: this.options.port, + error: err.message, + code: err.code + } + } + )); + } + }); + + // Connect to the server + const connectOptions = { + host: this.options.host, + port: this.options.port + }; + + // For direct TLS connections + if (this.options.secure) { + const tlsSocket = plugins.tls.connect({ + ...connectOptions, + rejectUnauthorized: this.options.tls.rejectUnauthorized, + minVersion: this.options.tls.minVersion as any, + ca: this.options.tls.ca ? [this.options.tls.ca] : undefined + } as plugins.tls.ConnectionOptions); + + tlsSocket.once('secureConnect', () => { + logger.log('debug', `Secure connection established to ${this.options.host}:${this.options.port}`); + this.socket = tlsSocket; + resolve(); + }); + + tlsSocket.once('error', (err: NodeNetworkError) => { + reject(new MtaConnectionError( + `TLS connection error to ${this.options.host}:${this.options.port}: ${err.message}`, + { + data: { + host: this.options.host, + port: this.options.port, + error: err.message, + code: err.code + } + } + )); + }); + + tlsSocket.setTimeout(this.options.socketTimeout); + + tlsSocket.once('timeout', () => { + reject(MtaConnectionError.timeout( + this.options.host, + this.options.port, + this.options.connectionTimeout + )); + }); + } else { + socket.connect(connectOptions); + this.socket = socket; + } + }); + + // Wait for server greeting + const greeting = await this.readResponse(); + + if (!greeting.startsWith('220')) { + throw new MtaConnectionError( + `Unexpected greeting from server: ${greeting}`, + { + data: { + host: this.options.host, + port: this.options.port, + greeting + } + } + ); + } + + // Send EHLO + await this.sendEhlo(); + + // Start TLS if not secure and supported + if (!this.options.secure && this.supportedExtensions.has('STARTTLS')) { + await this.startTls(); + + // Send EHLO again after STARTTLS + await this.sendEhlo(); + } + + // Authenticate if credentials provided + if (this.options.auth) { + await this.authenticate(); + } + + this.connected = true; + logger.log('info', `Successfully connected to SMTP server ${this.options.host}:${this.options.port}`); + + // Set up error handling for the socket + this.socket.on('error', (err) => { + logger.log('error', `Socket error: ${err.message}`); + this.connected = false; + this.socket = undefined; + }); + + this.socket.on('close', () => { + logger.log('debug', 'Socket closed'); + this.connected = false; + this.socket = undefined; + }); + + this.socket.on('timeout', () => { + logger.log('error', 'Socket timeout'); + this.connected = false; + if (this.socket) { + this.socket.destroy(); + this.socket = undefined; + } + }); + + } catch (error) { + // Clean up socket if connection failed + if (this.socket) { + this.socket.destroy(); + this.socket = undefined; + } + + logger.log('error', `Failed to connect to SMTP server: ${error.message}`); + throw error; + } + } + + /** + * Send EHLO command to the server + */ + private async sendEhlo(): Promise { + // Clear previous extensions + this.supportedExtensions.clear(); + + // Send EHLO + const response = await this.sendCommand(`EHLO ${this.options.domain}`); + + // Parse supported extensions + const lines = response.split('\r\n'); + for (let i = 1; i < lines.length; i++) { + const line = lines[i]; + if (line.startsWith('250-') || line.startsWith('250 ')) { + const extension = line.substring(4).split(' ')[0]; + this.supportedExtensions.add(extension); + } + } + + logger.log('debug', `Server supports extensions: ${Array.from(this.supportedExtensions).join(', ')}`); + } + + /** + * Start TLS negotiation + */ + private async startTls(): Promise { + logger.log('debug', 'Starting TLS negotiation'); + + // Send STARTTLS command + const response = await this.sendCommand('STARTTLS'); + + if (!response.startsWith('220')) { + throw new MtaConnectionError( + `Failed to start TLS: ${response}`, + { + data: { + host: this.options.host, + port: this.options.port, + response + } + } + ); + } + + if (!this.socket) { + throw new MtaConnectionError( + 'No socket available for TLS upgrade', + { + data: { + host: this.options.host, + port: this.options.port + } + } + ); + } + + // Upgrade socket to TLS + const currentSocket = this.socket; + this.socket = await this.upgradeTls(currentSocket); + } + + /** + * Upgrade socket to TLS + * @param socket Original socket + */ + private async upgradeTls(socket: plugins.net.Socket): Promise { + return new Promise((resolve, reject) => { + const tlsOptions: plugins.tls.ConnectionOptions = { + socket, + servername: this.options.host, + rejectUnauthorized: this.options.tls.rejectUnauthorized, + minVersion: this.options.tls.minVersion as any, + ca: this.options.tls.ca ? [this.options.tls.ca] : undefined + }; + + const tlsSocket = plugins.tls.connect(tlsOptions); + + tlsSocket.once('secureConnect', () => { + logger.log('debug', 'TLS negotiation successful'); + resolve(tlsSocket); + }); + + tlsSocket.once('error', (err: NodeNetworkError) => { + reject(new MtaConnectionError( + `TLS error: ${err.message}`, + { + data: { + host: this.options.host, + port: this.options.port, + error: err.message, + code: err.code + } + } + )); + }); + + tlsSocket.setTimeout(this.options.socketTimeout); + + tlsSocket.once('timeout', () => { + reject(MtaTimeoutError.commandTimeout( + 'STARTTLS', + this.options.host, + this.options.socketTimeout + )); + }); + }); + } + + /** + * Authenticate with the server + */ + private async authenticate(): Promise { + if (!this.options.auth) { + return; + } + + const { user, pass, method = 'LOGIN' } = this.options.auth; + + logger.log('debug', `Authenticating as ${user} using ${method}`); + + try { + switch (method) { + case 'PLAIN': + await this.authPlain(user, pass); + break; + + case 'LOGIN': + await this.authLogin(user, pass); + break; + + case 'OAUTH2': + await this.authOAuth2(user, pass); + break; + + default: + throw new MtaAuthenticationError( + `Authentication method ${method} not supported by client`, + { + data: { + method + } + } + ); + } + + logger.log('info', `Successfully authenticated as ${user}`); + } catch (error) { + logger.log('error', `Authentication failed: ${error.message}`); + throw error; + } + } + + /** + * Authenticate using PLAIN method + * @param user Username + * @param pass Password + */ + private async authPlain(user: string, pass: string): Promise { + // PLAIN authentication format: \0username\0password + const authString = Buffer.from(`\0${user}\0${pass}`).toString('base64'); + const response = await this.sendCommand(`AUTH PLAIN ${authString}`); + + if (!response.startsWith('235')) { + throw MtaAuthenticationError.invalidCredentials( + this.options.host, + user + ); + } + } + + /** + * Authenticate using LOGIN method + * @param user Username + * @param pass Password + */ + private async authLogin(user: string, pass: string): Promise { + // Start LOGIN authentication + const response = await this.sendCommand('AUTH LOGIN'); + + if (!response.startsWith('334')) { + throw new MtaAuthenticationError( + `Server did not accept AUTH LOGIN: ${response}`, + { + data: { + host: this.options.host, + response + } + } + ); + } + + // Send username (base64) + const userResponse = await this.sendCommand(Buffer.from(user).toString('base64')); + + if (!userResponse.startsWith('334')) { + throw MtaAuthenticationError.invalidCredentials( + this.options.host, + user + ); + } + + // Send password (base64) + const passResponse = await this.sendCommand(Buffer.from(pass).toString('base64')); + + if (!passResponse.startsWith('235')) { + throw MtaAuthenticationError.invalidCredentials( + this.options.host, + user + ); + } + } + + /** + * Authenticate using OAuth2 method + * @param user Username + * @param token OAuth2 token + */ + private async authOAuth2(user: string, token: string): Promise { + // XOAUTH2 format + const authString = `user=${user}\x01auth=Bearer ${token}\x01\x01`; + const response = await this.sendCommand(`AUTH XOAUTH2 ${Buffer.from(authString).toString('base64')}`); + + if (!response.startsWith('235')) { + throw MtaAuthenticationError.invalidCredentials( + this.options.host, + user + ); + } + } + + /** + * Send an email through the SMTP client + * @param email Email to send + * @param processingMode Optional processing mode + */ + public async sendMail(email: Email, processingMode?: EmailProcessingMode): Promise { + // Ensure we're connected + if (!this.connected || !this.socket) { + await this.connect(); + } + + const startTime = Date.now(); + const result: ISmtpDeliveryResult = { + success: false, + acceptedRecipients: [], + rejectedRecipients: [], + timestamp: startTime, + secure: this.options.secure || this.socket instanceof plugins.tls.TLSSocket, + authenticated: !!this.options.auth + }; + + try { + logger.log('info', `Sending email to ${email.getAllRecipients().join(', ')}`); + + // Apply DKIM signing if configured + if (this.options.dkim?.enabled) { + await this.applyDkimSignature(email); + result.dkimSigned = true; + } + + // Send MAIL FROM + const envelope_from = email.getEnvelopeFrom() || email.from; + await this.sendCommand(`MAIL FROM:<${envelope_from}> SIZE=${this.getEmailSize(email)}`); + + // Send RCPT TO for each recipient + const recipients = email.getAllRecipients(); + + for (const recipient of recipients) { + try { + await this.sendCommand(`RCPT TO:<${recipient}>`); + result.acceptedRecipients.push(recipient); + } catch (error) { + logger.log('warn', `Recipient ${recipient} rejected: ${error.message}`); + result.rejectedRecipients.push(recipient); + } + } + + // Check if at least one recipient was accepted + if (result.acceptedRecipients.length === 0) { + throw new MtaDeliveryError( + 'All recipients were rejected', + { + data: { + recipients, + rejectedRecipients: result.rejectedRecipients + } + } + ); + } + + // Send DATA + const dataResponse = await this.sendCommand('DATA'); + + if (!dataResponse.startsWith('354')) { + throw new MtaProtocolError( + `Failed to start DATA phase: ${dataResponse}`, + { + data: { + response: dataResponse + } + } + ); + } + + // Format email content (simplified for now) + const emailContent = await this.getFormattedEmail(email); + + // Send email content + const finalResponse = await this.sendCommand(emailContent + '\r\n.'); + + // Extract message ID if available + const messageIdMatch = finalResponse.match(/\[(.*?)\]/); + if (messageIdMatch) { + result.messageId = messageIdMatch[1]; + } + + result.success = true; + result.response = finalResponse; + + logger.log('info', `Email sent successfully to ${result.acceptedRecipients.join(', ')}`); + + // Log security event + SecurityLogger.getInstance().logEvent({ + level: SecurityLogLevel.INFO, + type: SecurityEventType.EMAIL_DELIVERY, + message: 'Email sent successfully', + details: { + recipients: result.acceptedRecipients, + rejectedRecipients: result.rejectedRecipients, + messageId: result.messageId, + secure: result.secure, + authenticated: result.authenticated, + server: `${this.options.host}:${this.options.port}`, + dkimSigned: result.dkimSigned + }, + success: true + }); + + return result; + } catch (error) { + logger.log('error', `Failed to send email: ${error.message}`); + + // Format error for result + result.error = error.message; + + // Extract SMTP code if available + if (error.context?.data?.statusCode) { + result.responseCode = error.context.data.statusCode; + } + + // Log security event + SecurityLogger.getInstance().logEvent({ + level: SecurityLogLevel.ERROR, + type: SecurityEventType.EMAIL_DELIVERY, + message: 'Email delivery failed', + details: { + error: error.message, + server: `${this.options.host}:${this.options.port}`, + recipients: email.getAllRecipients(), + acceptedRecipients: result.acceptedRecipients, + rejectedRecipients: result.rejectedRecipients, + secure: result.secure, + authenticated: result.authenticated + }, + success: false + }); + + return result; + } + } + + /** + * Apply DKIM signature to email + * @param email Email to sign + */ + private async applyDkimSignature(email: Email): Promise { + if (!this.options.dkim?.enabled || !this.options.dkim?.privateKey) { + return; + } + + try { + logger.log('debug', `Signing email with DKIM for domain ${this.options.dkim.domain}`); + + // Format email for DKIM signing + const { dkimSign } = plugins; + const emailContent = await this.getFormattedEmail(email); + + // Sign email + const signOptions = { + domainName: this.options.dkim.domain, + keySelector: this.options.dkim.selector, + privateKey: this.options.dkim.privateKey, + headerFieldNames: this.options.dkim.headers || [ + 'from', 'to', 'subject', 'date', 'message-id' + ] + }; + + const signedEmail = await dkimSign(emailContent, signOptions); + + // Replace headers in original email + const dkimHeader = signedEmail.substring(0, signedEmail.indexOf('\r\n\r\n')).split('\r\n') + .find(line => line.startsWith('DKIM-Signature: ')); + + if (dkimHeader) { + email.addHeader('DKIM-Signature', dkimHeader.substring('DKIM-Signature: '.length)); + } + + logger.log('debug', 'DKIM signature applied successfully'); + } catch (error) { + logger.log('error', `Failed to apply DKIM signature: ${error.message}`); + throw error; + } + } + + /** + * Format email for SMTP transmission + * @param email Email to format + */ + private async getFormattedEmail(email: Email): Promise { + // This is a simplified implementation + // In a full implementation, this would use proper MIME formatting + + let content = ''; + + // Add headers + content += `From: ${email.from}\r\n`; + content += `To: ${email.to.join(', ')}\r\n`; + content += `Subject: ${email.subject}\r\n`; + content += `Date: ${new Date().toUTCString()}\r\n`; + content += `Message-ID: <${plugins.uuid.v4()}@${this.options.domain}>\r\n`; + + // Add additional headers + for (const [name, value] of Object.entries(email.headers || {})) { + content += `${name}: ${value}\r\n`; + } + + // Add content type for multipart + if (email.attachments && email.attachments.length > 0) { + const boundary = `----_=_NextPart_${Math.random().toString(36).substr(2)}`; + content += `MIME-Version: 1.0\r\n`; + content += `Content-Type: multipart/mixed; boundary="${boundary}"\r\n`; + content += `\r\n`; + + // Add text part + content += `--${boundary}\r\n`; + content += `Content-Type: text/plain; charset="UTF-8"\r\n`; + content += `\r\n`; + content += `${email.text}\r\n`; + + // Add HTML part if present + if (email.html) { + content += `--${boundary}\r\n`; + content += `Content-Type: text/html; charset="UTF-8"\r\n`; + content += `\r\n`; + content += `${email.html}\r\n`; + } + + // Add attachments + for (const attachment of email.attachments) { + content += `--${boundary}\r\n`; + content += `Content-Type: ${attachment.contentType || 'application/octet-stream'}; name="${attachment.filename}"\r\n`; + content += `Content-Disposition: attachment; filename="${attachment.filename}"\r\n`; + content += `Content-Transfer-Encoding: base64\r\n`; + content += `\r\n`; + + // Add base64 encoded content + const base64Content = attachment.content.toString('base64'); + + // Split into lines of 76 characters + for (let i = 0; i < base64Content.length; i += 76) { + content += base64Content.substring(i, i + 76) + '\r\n'; + } + } + + // End boundary + content += `--${boundary}--\r\n`; + } else { + // Simple email with just text + content += `Content-Type: text/plain; charset="UTF-8"\r\n`; + content += `\r\n`; + content += `${email.text}\r\n`; + } + + return content; + } + + /** + * Get size of email in bytes + * @param email Email to measure + */ + private getEmailSize(email: Email): number { + // Simplified size estimation + let size = 0; + + // Headers + size += `From: ${email.from}\r\n`.length; + size += `To: ${email.to.join(', ')}\r\n`.length; + size += `Subject: ${email.subject}\r\n`.length; + + // Body + size += (email.text?.length || 0) + 2; // +2 for CRLF + + // HTML part if present + if (email.html) { + size += email.html.length + 2; + } + + // Attachments + for (const attachment of email.attachments || []) { + size += attachment.content.length; + } + + // Add overhead for MIME boundaries and headers + const overhead = email.attachments?.length ? 1000 + (email.attachments.length * 200) : 200; + + return size + overhead; + } + + /** + * Send SMTP command and wait for response + * @param command SMTP command to send + */ + private async sendCommand(command: string): Promise { + if (!this.socket) { + throw new MtaConnectionError( + 'Not connected to server', + { + data: { + host: this.options.host, + port: this.options.port + } + } + ); + } + + // Log command if not sensitive + if (!command.startsWith('AUTH')) { + logger.log('debug', `> ${command}`); + } else { + logger.log('debug', '> AUTH ***'); + } + + return new Promise((resolve, reject) => { + // Set up timeout for command + const timeout = setTimeout(() => { + reject(MtaTimeoutError.commandTimeout( + command.split(' ')[0], + this.options.host, + this.options.commandTimeout + )); + }, this.options.commandTimeout); + + // Send command + this.socket.write(command + '\r\n', (err) => { + if (err) { + clearTimeout(timeout); + reject(new MtaConnectionError( + `Failed to send command: ${err.message}`, + { + data: { + command: command.split(' ')[0], + error: err.message + } + } + )); + } + }); + + // Read response + this.readResponse() + .then((response) => { + clearTimeout(timeout); + resolve(response); + }) + .catch((err) => { + clearTimeout(timeout); + reject(err); + }); + }); + } + + /** + * Read response from the server + */ + private async readResponse(): Promise { + if (!this.socket) { + throw new MtaConnectionError( + 'Not connected to server', + { + data: { + host: this.options.host, + port: this.options.port + } + } + ); + } + + return new Promise((resolve, reject) => { + let responseData = ''; + + const onData = (data: Buffer) => { + responseData += data.toString(); + + // Check if this is a complete response + if (this.isCompleteResponse(responseData)) { + // Clean up listeners + this.socket.removeListener('data', onData); + this.socket.removeListener('error', onError); + this.socket.removeListener('close', onClose); + this.socket.removeListener('end', onEnd); + + logger.log('debug', `< ${responseData.trim()}`); + + // Check if this is an error response + if (this.isErrorResponse(responseData)) { + const code = responseData.substring(0, 3); + reject(this.createErrorFromResponse(responseData, code)); + } else { + resolve(responseData.trim()); + } + } + }; + + const onError = (err: Error) => { + // Clean up listeners + this.socket.removeListener('data', onData); + this.socket.removeListener('error', onError); + this.socket.removeListener('close', onClose); + this.socket.removeListener('end', onEnd); + + reject(new MtaConnectionError( + `Socket error while waiting for response: ${err.message}`, + { + data: { + error: err.message + } + } + )); + }; + + const onClose = () => { + // Clean up listeners + this.socket.removeListener('data', onData); + this.socket.removeListener('error', onError); + this.socket.removeListener('close', onClose); + this.socket.removeListener('end', onEnd); + + reject(new MtaConnectionError( + 'Connection closed while waiting for response', + { + data: { + partialResponse: responseData + } + } + )); + }; + + const onEnd = () => { + // Clean up listeners + this.socket.removeListener('data', onData); + this.socket.removeListener('error', onError); + this.socket.removeListener('close', onClose); + this.socket.removeListener('end', onEnd); + + reject(new MtaConnectionError( + 'Connection ended while waiting for response', + { + data: { + partialResponse: responseData + } + } + )); + }; + + // Set up listeners + this.socket.on('data', onData); + this.socket.once('error', onError); + this.socket.once('close', onClose); + this.socket.once('end', onEnd); + }); + } + + /** + * Check if the response is complete + * @param response Response to check + */ + private isCompleteResponse(response: string): boolean { + // Check if it's a multi-line response + const lines = response.split('\r\n'); + const lastLine = lines[lines.length - 2]; // Second to last because of the trailing CRLF + + // Check if the last line starts with a code followed by a space + // If it does, this is a complete response + if (lastLine && /^\d{3} /.test(lastLine)) { + return true; + } + + // For single line responses + if (lines.length === 2 && lines[0].length >= 3 && /^\d{3} /.test(lines[0])) { + return true; + } + + return false; + } + + /** + * Check if the response is an error + * @param response Response to check + */ + private isErrorResponse(response: string): boolean { + // Get the status code (first 3 characters) + const code = response.substring(0, 3); + + // 4xx and 5xx are error codes + return code.startsWith('4') || code.startsWith('5'); + } + + /** + * Create appropriate error from response + * @param response Error response + * @param code SMTP status code + */ + private createErrorFromResponse(response: string, code: string): Error { + // Extract message part + const message = response.substring(4).trim(); + + switch (code.charAt(0)) { + case '4': // Temporary errors + return MtaDeliveryError.temporary( + message, + 'recipient', + code, + response + ); + + case '5': // Permanent errors + return MtaDeliveryError.permanent( + message, + 'recipient', + code, + response + ); + + default: + return new MtaDeliveryError( + `Unexpected error response: ${response}`, + { + data: { + response, + code + } + } + ); + } + } + + /** + * Close the connection to the server + */ + public async close(): Promise { + if (!this.connected || !this.socket) { + return; + } + + try { + // Send QUIT + await this.sendCommand('QUIT'); + } catch (error) { + logger.log('warn', `Error sending QUIT command: ${error.message}`); + } finally { + // Close socket + this.socket.destroy(); + this.socket = undefined; + this.connected = false; + logger.log('info', 'SMTP connection closed'); + } + } + + /** + * Checks if the connection is active + */ + public isConnected(): boolean { + return this.connected && !!this.socket; + } + + /** + * Update SMTP client options + * @param options New options + */ + public updateOptions(options: Partial): void { + this.options = { + ...this.options, + ...options + }; + + logger.log('info', 'SMTP client options updated'); + } +} \ No newline at end of file diff --git a/ts/mail/delivery/classes.smtpserver.ts b/ts/mail/delivery/classes.smtpserver.ts index 1fe04a8..637d4d6 100644 --- a/ts/mail/delivery/classes.smtpserver.ts +++ b/ts/mail/delivery/classes.smtpserver.ts @@ -11,55 +11,18 @@ import { ReputationThreshold } from '../../security/index.js'; -export interface ISmtpServerOptions { - port: number; - key: string; - cert: string; - hostname?: string; -} - -// SMTP Session States -enum SmtpState { - GREETING, - AFTER_EHLO, - MAIL_FROM, - RCPT_TO, - DATA, - DATA_RECEIVING, - FINISHED -} - -// Structure to store session information -interface SmtpSession { - id: string; - state: SmtpState; - clientHostname: string; - mailFrom: string; - rcptTo: string[]; - emailData: string; - useTLS: boolean; - connectionEnded: boolean; - remoteAddress: string; - secure: boolean; - authenticated: boolean; - envelope: { - mailFrom: { - address: string; - args: any; - }; - rcptTo: Array<{ - address: string; - args: any; - }>; - }; - processingMode?: 'forward' | 'mta' | 'process'; -} +import type { + ISmtpServerOptions, + ISmtpSession, + EmailProcessingMode +} from './interfaces.js'; +import { SmtpState } from './interfaces.js'; export class SMTPServer { public emailServerRef: UnifiedEmailServer; private smtpServerOptions: ISmtpServerOptions; private server: plugins.net.Server; - private sessions: Map; + private sessions: Map; private hostname: string; constructor(emailServerRefArg: UnifiedEmailServer, optionsArg: ISmtpServerOptions) { @@ -722,6 +685,12 @@ export class SMTPServer { try { await this.emailServerRef.processEmailByMode(email, { id: session.id, + state: session.state, + mailFrom: session.mailFrom, + rcptTo: session.rcptTo, + emailData: session.emailData, + useTLS: session.useTLS, + connectionEnded: session.connectionEnded, remoteAddress: session.remoteAddress, clientHostname: session.clientHostname, secure: session.useTLS, diff --git a/ts/mail/delivery/index.ts b/ts/mail/delivery/index.ts index f21c5e3..bf585c2 100644 --- a/ts/mail/delivery/index.ts +++ b/ts/mail/delivery/index.ts @@ -15,5 +15,6 @@ export type { IRateLimitConfig } from './classes.ratelimiter.js'; // Unified rate limiter export * from './classes.unified.rate.limiter.js'; -// MTA configuration helpers -export * from './classes.mta.config.js'; \ No newline at end of file +// SMTP client and configuration +export * from './classes.mta.config.js'; +export * from './classes.smtp.client.js'; \ No newline at end of file diff --git a/ts/mail/delivery/interfaces.ts b/ts/mail/delivery/interfaces.ts new file mode 100644 index 0000000..4876b99 --- /dev/null +++ b/ts/mail/delivery/interfaces.ts @@ -0,0 +1,223 @@ +/** + * SMTP and email delivery interface definitions + */ + +import type { Email } from '../core/classes.email.js'; + +/** + * SMTP session state enumeration + */ +export enum SmtpState { + GREETING = 'GREETING', + AFTER_EHLO = 'AFTER_EHLO', + MAIL_FROM = 'MAIL_FROM', + RCPT_TO = 'RCPT_TO', + DATA = 'DATA', + DATA_RECEIVING = 'DATA_RECEIVING', + FINISHED = 'FINISHED' +} + +/** + * Email processing mode type + */ +export type EmailProcessingMode = 'forward' | 'mta' | 'process'; + +/** + * Envelope recipient information + */ +export interface IEnvelopeRecipient { + /** + * Email address of the recipient + */ + address: string; + + /** + * Additional SMTP command arguments + */ + args: Record; +} + +/** + * SMTP session envelope information + */ +export interface ISmtpEnvelope { + /** + * Envelope sender (MAIL FROM) information + */ + mailFrom: { + /** + * Email address of the sender + */ + address: string; + + /** + * Additional SMTP command arguments + */ + args: Record; + }; + + /** + * Envelope recipients (RCPT TO) information + */ + rcptTo: IEnvelopeRecipient[]; +} + +/** + * SMTP Session interface - represents an active SMTP connection + */ +export interface ISmtpSession { + /** + * Unique session identifier + */ + id: string; + + /** + * Current session state in the SMTP conversation + */ + state: SmtpState; + + /** + * Hostname provided by the client in EHLO/HELO command + */ + clientHostname: string; + + /** + * MAIL FROM email address (legacy format) + */ + mailFrom: string; + + /** + * RCPT TO email addresses (legacy format) + */ + rcptTo: string[]; + + /** + * Raw email data being received + */ + emailData: string; + + /** + * Whether the connection is using TLS + */ + useTLS: boolean; + + /** + * Whether the connection has ended + */ + connectionEnded: boolean; + + /** + * Remote IP address of the client + */ + remoteAddress: string; + + /** + * Whether the connection is secure (TLS) + */ + secure: boolean; + + /** + * Whether the client has been authenticated + */ + authenticated: boolean; + + /** + * SMTP envelope information (structured format) + */ + envelope: ISmtpEnvelope; + + /** + * Email processing mode to use for this session + */ + processingMode?: EmailProcessingMode; +} + +/** + * SMTP authentication data + */ +export interface ISmtpAuth { + /** + * Authentication method used + */ + method: 'PLAIN' | 'LOGIN' | 'OAUTH2' | string; + + /** + * Username for authentication + */ + username: string; + + /** + * Password or token for authentication + */ + password: string; +} + +/** + * SMTP server options + */ +export interface ISmtpServerOptions { + /** + * Port to listen on + */ + port: number; + + /** + * TLS private key (PEM format) + */ + key: string; + + /** + * TLS certificate (PEM format) + */ + cert: string; + + /** + * Server hostname for SMTP banner + */ + hostname?: string; + + /** + * Maximum size of messages in bytes + */ + maxSize?: number; + + /** + * Authentication options + */ + auth?: { + /** + * Whether authentication is required + */ + required: boolean; + + /** + * Allowed authentication methods + */ + methods: ('PLAIN' | 'LOGIN' | 'OAUTH2')[]; + }; +} + +/** + * Result of SMTP transaction + */ +export interface ISmtpTransactionResult { + /** + * Whether the transaction was successful + */ + success: boolean; + + /** + * Error message if failed + */ + error?: string; + + /** + * Message ID if successful + */ + messageId?: string; + + /** + * Resulting email if successful + */ + email?: Email; +} \ No newline at end of file diff --git a/ts/mail/routing/classes.email.config.ts b/ts/mail/routing/classes.email.config.ts index b05ae41..d9b997e 100644 --- a/ts/mail/routing/classes.email.config.ts +++ b/ts/mail/routing/classes.email.config.ts @@ -1,9 +1,9 @@ import * as plugins from '../../plugins.js'; -/** - * Email processing modes - */ -export type EmailProcessingMode = 'forward' | 'mta' | 'process'; +import type { EmailProcessingMode } from '../delivery/interfaces.js'; + +// Re-export EmailProcessingMode type +export type { EmailProcessingMode }; /** * Consolidated email configuration interface diff --git a/ts/mail/routing/classes.unified.email.server.ts b/ts/mail/routing/classes.unified.email.server.ts index 536aeef..f3a5f17 100644 --- a/ts/mail/routing/classes.unified.email.server.ts +++ b/ts/mail/routing/classes.unified.email.server.ts @@ -18,7 +18,6 @@ import { import { DomainRouter } from './classes.domain.router.js'; import type { IEmailConfig, - EmailProcessingMode, IDomainRule } from './classes.email.config.js'; import { Email } from '../core/classes.email.js'; @@ -29,6 +28,18 @@ import * as stream from 'node:stream'; import { SMTPServer as MtaSmtpServer } from '../delivery/classes.smtpserver.js'; import { MultiModeDeliverySystem, type IMultiModeDeliveryOptions } from '../delivery/classes.delivery.system.js'; import { UnifiedDeliveryQueue, type IQueueOptions } from '../delivery/classes.delivery.queue.js'; +import { SmtpState } from '../delivery/interfaces.js'; +import type { EmailProcessingMode, ISmtpSession as IBaseSmtpSession } from '../delivery/interfaces.js'; + +/** + * Extended SMTP session interface with domain rule information + */ +export interface IExtendedSmtpSession extends ISmtpSession { + /** + * Matched domain rule for this session + */ + matchedRule?: IDomainRule; +} /** * Options for the unified email server @@ -78,41 +89,30 @@ export interface IUnifiedEmailServerOptions { reputationMonitorConfig?: IReputationMonitorConfig; } + /** - * Interface describing SMTP session data + * Extended SMTP session interface for UnifiedEmailServer */ -export interface ISmtpSession { - id: string; - remoteAddress: string; - clientHostname: string; - secure: boolean; - authenticated: boolean; +export interface ISmtpSession extends IBaseSmtpSession { + /** + * User information if authenticated + */ user?: { username: string; [key: string]: any; }; - envelope: { - mailFrom: { - address: string; - args: any; - }; - rcptTo: Array<{ - address: string; - args: any; - }>; - }; - processingMode?: EmailProcessingMode; + + /** + * Matched domain rule for this session + */ matchedRule?: IDomainRule; } /** * Authentication data for SMTP */ -export interface IAuthData { - method: string; - username: string; - password: string; -} +import type { ISmtpAuth } from '../delivery/interfaces.js'; +export type IAuthData = ISmtpAuth; /** * Server statistics @@ -330,6 +330,12 @@ export class UnifiedEmailServer extends EventEmitter { // Process based on the mode await this.processEmailByMode(email, { id: 'session-' + Math.random().toString(36).substring(2), + state: SmtpState.FINISHED, + mailFrom: email.from, + rcptTo: email.to, + emailData: email.toRFC822String(), // Use the proper method to get the full email content + useTLS: false, + connectionEnded: true, remoteAddress: '127.0.0.1', clientHostname: '', secure: false, @@ -431,7 +437,7 @@ export class UnifiedEmailServer extends EventEmitter { /** * Handle new SMTP connection with IP reputation checking */ - private async onConnect(session: ISmtpSession, callback: (err?: Error) => void): Promise { + private async onConnect(session: IExtendedSmtpSession, callback: (err?: Error) => void): Promise { logger.log('info', `New connection from ${session.remoteAddress}`); // Update connection statistics @@ -498,7 +504,7 @@ export class UnifiedEmailServer extends EventEmitter { /** * Handle authentication (stub implementation) */ - private onAuth(auth: IAuthData, session: ISmtpSession, callback: (err?: Error, user?: any) => void): void { + private onAuth(auth: IAuthData, session: IExtendedSmtpSession, callback: (err?: Error, user?: any) => void): void { if (!this.options.auth || !this.options.auth.users || this.options.auth.users.length === 0) { // No authentication configured, reject const error = new Error('Authentication not supported'); @@ -564,7 +570,7 @@ export class UnifiedEmailServer extends EventEmitter { /** * Handle MAIL FROM command (stub implementation) */ - private onMailFrom(address: {address: string}, session: ISmtpSession, callback: (err?: Error) => void): void { + private onMailFrom(address: {address: string}, session: IExtendedSmtpSession, callback: (err?: Error) => void): void { logger.log('info', `MAIL FROM: ${address.address}`); // Validate the email address @@ -614,7 +620,7 @@ export class UnifiedEmailServer extends EventEmitter { /** * Handle RCPT TO command (stub implementation) */ - private onRcptTo(address: {address: string}, session: ISmtpSession, callback: (err?: Error) => void): void { + private onRcptTo(address: {address: string}, session: IExtendedSmtpSession, callback: (err?: Error) => void): void { logger.log('info', `RCPT TO: ${address.address}`); // Validate the email address @@ -658,7 +664,7 @@ export class UnifiedEmailServer extends EventEmitter { /** * Handle incoming email data (stub implementation) */ - private onData(stream: stream.Readable, session: ISmtpSession, callback: (err?: Error) => void): void { + private onData(stream: stream.Readable, session: IExtendedSmtpSession, callback: (err?: Error) => void): void { logger.log('info', `Processing email data for session ${session.id}`); const startTime = Date.now(); @@ -762,7 +768,7 @@ export class UnifiedEmailServer extends EventEmitter { /** * Process email based on the determined mode */ - public async processEmailByMode(emailData: Email | Buffer, session: ISmtpSession, mode: EmailProcessingMode): Promise { + public async processEmailByMode(emailData: Email | Buffer, session: IExtendedSmtpSession, mode: EmailProcessingMode): Promise { // Convert Buffer to Email if needed let email: Email; if (Buffer.isBuffer(emailData)) { @@ -833,7 +839,7 @@ export class UnifiedEmailServer extends EventEmitter { /** * Handle email in forward mode (SMTP proxy) */ - private async handleForwardMode(email: Email, session: ISmtpSession): Promise { + private async handleForwardMode(email: Email, session: IExtendedSmtpSession): Promise { logger.log('info', `Handling email in forward mode for session ${session.id}`); // Get target server information @@ -927,7 +933,7 @@ export class UnifiedEmailServer extends EventEmitter { /** * Handle email in MTA mode (programmatic processing) */ - private async handleMtaMode(email: Email, session: ISmtpSession): Promise { + private async handleMtaMode(email: Email, session: IExtendedSmtpSession): Promise { logger.log('info', `Handling email in MTA mode for session ${session.id}`); try { @@ -1022,7 +1028,7 @@ export class UnifiedEmailServer extends EventEmitter { /** * Handle email in process mode (store-and-forward with scanning) */ - private async handleProcessMode(email: Email, session: ISmtpSession): Promise { + private async handleProcessMode(email: Email, session: IExtendedSmtpSession): Promise { logger.log('info', `Handling email in process mode for session ${session.id}`); try { diff --git a/ts/mail/services/classes.emailservice.ts b/ts/mail/services/classes.emailservice.ts index 6fd63c9..da3fc18 100644 --- a/ts/mail/services/classes.emailservice.ts +++ b/ts/mail/services/classes.emailservice.ts @@ -6,8 +6,7 @@ import { TemplateManager } from '../core/classes.templatemanager.js'; import { EmailValidator } from '../core/classes.emailvalidator.js'; import { BounceManager } from '../core/classes.bouncemanager.js'; import { logger } from '../../logger.js'; -// Import types from platform interfaces -import type { default as platformInterfaces } from '../../types/platform.interfaces.js'; +// Import types from router interfaces import { UnifiedEmailServer } from '../routing/classes.unified.email.server.js'; import { DomainRouter } from '../routing/classes.domain.router.js'; import { Email } from '../core/classes.email.js'; @@ -144,7 +143,6 @@ export interface IEmailServiceStats { * Email service with MTA support */ export class EmailService { - public platformServiceRef: any; // Reference to platform service // typedrouter public typedrouter = new plugins.typedrequest.TypedRouter(); @@ -166,9 +164,7 @@ export class EmailService { // configuration private config: IEmailConfig; - constructor(platformServiceRefArg: any, options: IEmailConfig = {}) { - this.platformServiceRef = platformServiceRefArg; - this.platformServiceRef.typedrouter.addTypedRouter(this.typedrouter); + constructor(options: IEmailConfig = {}) { // Validate and apply defaults to configuration const validationResult = ConfigValidator.validate(options, emailConfigSchema); diff --git a/ts/sms/classes.smsservice.ts b/ts/sms/classes.smsservice.ts index db06451..38509fc 100644 --- a/ts/sms/classes.smsservice.ts +++ b/ts/sms/classes.smsservice.ts @@ -1,21 +1,16 @@ import * as plugins from '../plugins.js'; import * as paths from '../paths.js'; import { logger } from '../logger.js'; -// Import types from platform interfaces -import type { default as platformInterfaces } from '../types/platform.interfaces.js'; import type { ISmsConfig } from '../config/sms.config.js'; import { ConfigValidator, smsConfigSchema } from '../config/index.js'; export class SmsService { - public platformServiceRef: any; // Platform service reference, using any to avoid dependency public projectinfo: plugins.projectinfo.ProjectInfo; public typedrouter = new plugins.typedrequest.TypedRouter(); public config: ISmsConfig; - constructor(platformServiceRefArg: any, options: ISmsConfig) { - this.platformServiceRef = platformServiceRefArg; - + constructor(options: ISmsConfig) { // Validate and apply defaults to configuration const validationResult = ConfigValidator.validate(options, smsConfigSchema); @@ -27,7 +22,6 @@ export class SmsService { this.config = validationResult.config; // Add router to platform service - this.platformServiceRef.typedrouter.addTypedRouter(this.typedrouter); } /** diff --git a/ts/types/platform.interfaces.ts b/ts/types/platform.interfaces.ts deleted file mode 100644 index f56f7eb..0000000 --- a/ts/types/platform.interfaces.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Minimal platform interfaces to support transition - */ - -/** - * Dummy placeholder for SzPlatformService interface - */ -export interface SzPlatformService { - // Empty interface for now - typedrouter?: any; -} - -// Create a default export with an object that has the SzPlatformService property -const interfaces = { - // Add a dummy constructor function that returns an empty object - SzPlatformService: function() { return {}; } -}; - -export default interfaces; \ No newline at end of file