Compare commits

..

16 Commits

Author SHA1 Message Date
f1cc7fd340 2.11.1 2025-05-08 13:00:11 +00:00
deec61da42 fix(platform): Update commit info with no functional changes; regenerated commit information. 2025-05-08 13:00:10 +00:00
190ae11667 2.11.0 2025-05-08 12:56:17 +00:00
f4ace3999d feat(platformservice): Expose DcRouter and update package visibility. Changed package.json private flag from true to false to allow public publication, and added export of DcRouter in ts/index.ts for improved API accessibility. 2025-05-08 12:56:17 +00:00
8b857e3d1d update 2025-05-08 12:46:10 +00:00
7aaf8f2595 2.8.9 2025-05-08 10:39:43 +00:00
39b634b6bb fix(types): Fix TypeScript build errors and improve API type safety across platformservice interfaces 2025-05-08 10:39:43 +00:00
4624fdbe10 2.8.6 2025-05-08 10:24:50 +00:00
858794799b fix(tests): fix: Improve test stability by handling race conditions in SenderReputationMonitor and IPWarmupManager. Disable filesystem operations and external DNS lookups during tests by checking NODE_ENV, add proper cleanup of singleton instances and active timeouts to ensure consistent test environment. 2025-05-08 10:24:50 +00:00
cb33dd26d0 2.8.4 2025-05-08 01:37:38 +00:00
d3d197d9d3 fix(mail): refactor(mail): Remove Mailgun references from PlatformService. Update keywords, error messages, and documentation to use MTA exclusively. 2025-05-08 01:37:38 +00:00
0e914a3366 2.8.2 2025-05-08 01:24:03 +00:00
747478f0f9 fix(tests): Fix outdated import paths in test files for dcrouter and ratelimiter modules 2025-05-08 01:24:03 +00:00
b61de33ee0 2.8.1 2025-05-08 01:16:21 +00:00
970c0d5c60 fix(readme): Update readme with consolidated email system improvements and modular directory structure
Clarify that the platform now organizes email functionality into distinct directories (mail/core, mail/delivery, mail/routing, mail/security, mail/services) and update the diagram and key features list accordingly. Adjust code examples to reflect explicit module imports and the use of SzPlatformService.
2025-05-08 01:16:21 +00:00
fe2069c48e update 2025-05-08 01:13:54 +00:00
78 changed files with 6262 additions and 1980 deletions

View File

@ -1,5 +1,118 @@
# Changelog
## 2025-05-08 - 2.11.1 - fix(platform)
Update commit info with no functional changes; regenerated commit information.
## 2025-05-08 - 2.11.0 - feat(platformservice)
Expose DcRouter and update package visibility. Changed package.json 'private' flag from true to false to allow public publication, and added export of DcRouter in ts/index.ts for improved API accessibility.
- Changed package.json: set 'private' to false
- Added export for DcRouter in ts/index.ts
## 2025-05-08 - 2.10.0 - feat(config): Implement standardized configuration system
Create a comprehensive configuration system with validation, defaults, and documentation
- Added consistent configuration interfaces across all services
- Implemented validation for all configuration objects with detailed error reporting
- Added default values for optional configuration parameters
- Created an extensive documentation system for configuration options
- Added migration helpers for managing configuration format changes
- Enhanced platform service to load configuration from multiple sources (file, environment, code)
- Updated email and SMS services to use the new configuration system
## 2025-05-08 - 2.9.0 - feat(errors): Implement comprehensive error handling system
Enhance error handling with structured errors, consistent patterns, and improved logging
- Added domain-specific error classes for better error categorization and handling
- Created comprehensive error codes for all service types (email, MTA, security, etc.)
- Implemented detailed error context with severity, category, and recoverability classification
- Added utilities for error conversion, formatting, and handling with automatic retry mechanisms
- Enhanced logging with correlation tracking, context support, and structured data
- Created middleware for handling errors in HTTP requests with proper status code mapping
- Added retry with exponential backoff for transient failures
## 2025-05-08 - 2.8.9 - fix(types)
Fix TypeScript build errors and improve API type safety across platformservice interfaces
- Fixed interface placement in EmailService and MtaConnector classes
- Aligned DeliveryStatus enum and updated ApiManager handlers with proper type-safe signatures
- Added comprehensive TypeScript interfaces for ISendEmailOptions, ITemplateContext, IValidateEmailOptions, IValidationResult, and IEmailServiceStats
- Removed circular dependencies in type definitions and added proper type assertions
- Improved test stability by handling race conditions in SenderReputationMonitor and IPWarmupManager; external DNS lookups are disabled under test environment
## 2025-05-08 - 2.8.8 - fix(types): Fix TypeScript build errors and improve API interfaces
Fix TypeScript build errors caused by interface placement and improve API type alignment
- Fixed interface placement in EmailService and MtaConnector classes
- Aligned DeliveryStatus enum with EmailSendJob implementation
- Added proper method signatures for API endpoint handlers in ApiManager class
- Updated getStats and checkEmailStatus methods to conform to API contracts
- Implemented type-safe return values for all API methods
- Fixed circular dependencies in type definitions
- Added proper type assertion where needed to satisfy TypeScript compiler
## 2025-05-08 - 2.8.7 - feat(types): Add comprehensive TypeScript interfaces for API types
Improve type safety across the platform by adding detailed TypeScript interfaces for APIs
- Added ISendEmailOptions interface with complete documentation for email sending options
- Created ITemplateContext interface for email template rendering with full type safety
- Added IValidateEmailOptions and IValidationResult interfaces for email validation
- Improved IEmailServiceStats interface with detailed statistics types
- Added IEmailStatusResponse and IEmailStatusDetails interfaces for MTA status checking
- Updated sendEmail and other methods to use these new interfaces instead of 'any'
- Removed need for type assertions in various components
## 2025-05-08 - 2.8.6 - fix(tests)
fix: Improve test stability by handling race conditions in SenderReputationMonitor and IPWarmupManager. Disable filesystem operations and external DNS lookups during tests by checking NODE_ENV, add proper cleanup of singleton instances and active timeouts to ensure consistent test environment.
- Bumped version from 2.8.4 to 2.8.5 in package.json and changelog.md
- Improved SenderReputationMonitor to skip filesystem operations and DNS record loading when NODE_ENV is set to test
- Added cleanup of singleton instances and active timeouts in test files
- Updated readme.plan.md with roadmap items for test stability
## 2025-05-08 - 2.8.5 - fix(tests): Improve test stability by fixing race conditions
Enhance the SenderReputationMonitor tests to prevent race conditions and make tests more reliable
- Modified SenderReputationMonitor to detect test environment and disable filesystem operations
- Added proper cleanup of singleton instances and timeouts between tests
- Disabled DNS lookups during tests to prevent external dependencies
- Set a consistent test environment using NODE_ENV=test
- Made all tests independent of each other to prevent shared state issues
## 2025-05-08 - 2.8.4 - fix(mail)
refactor(mail): Remove Mailgun references from PlatformService. Update keywords, error messages, and documentation to use MTA exclusively.
- Removed Mailgun integration from keywords in package.json and npmextra.json
- Updated EmailService to remove Mailgun API key usage and reference MTA instead
- Updated changelog.md and readme.md to reflect removal of Mailgun and update examples
- Revised error messages to mention 'MTA not configured' instead of generic provider errors
- Updated readme.plan.md to document Mailgun removal
## 2025-05-08 - 2.8.3 - refactor(mail): Remove Mailgun references
Remove all Mailgun references from the codebase since it's no longer used as an email provider
- Removed "mailgun integration" from keywords in package.json and npmextra.json
- Updated comments and documentation in EmailService to remove Mailgun mentions
- Updated error messages to reference MTA instead of generic email providers
- Updated the readme email example to use PlatformService reference instead of Mailgun API key
## 2025-05-08 - 2.8.2 - fix(tests)
Fix outdated import paths in test files for dcrouter and ratelimiter modules
- Updated dcrouter import from '../ts/dcrouter/index.js' to '../ts/classes.dcrouter.js'
- Updated ratelimiter import from '../ts/mta/classes.ratelimiter.js' to '../ts/mail/delivery/classes.ratelimiter.js'
## 2025-05-08 - 2.8.1 - fix(readme)
Update readme with consolidated email system improvements and modular directory structure
Clarify that the platform now organizes email functionality into distinct directories (mail/core, mail/delivery, mail/routing, mail/security, mail/services) and update the diagram and key features list accordingly. Adjust code examples to reflect explicit module imports and the use of SzPlatformService.
- Changed description of consolidated email configuration to include 'streamlined directory structure'.
- Updated mermaid diagram to show 'Mail System Structure' with separate components for core, delivery, routing, security, and services.
- Modified key features list to document modular directory structure.
- Revised code sample imports to use explicit paths and SzPlatformService.
## 2025-05-08 - 2.8.0 - feat(docs)
Update documentation to include consolidated email handling and patternbased routing details
@ -76,86 +189,7 @@ Enhance email integration by updating @push.rocks/smartmail to ^2.1.0 and improv
- Updated readme.plan.md with detailed roadmap for further performance, security, analytics, and deliverability enhancements
- Expanded test suite to cover smartmail integration, validation, templating, and conversion between formats
## 2025-05-04 - 2.3.1 - fix(platformservice)
Update dependency versions and refactor import paths for improved compatibility; add initial DcRouter plan documentation.
- Upgrade @git.zone/tsbuild to ^2.3.2 and @push.rocks/tapbundle to ^6.0.3.
- Upgrade @api.global/typedserver to ^3.0.74 and update related API dependencies (cloudflare, letterxpress).
- Upgrade smartdata to ^5.15.1, add smartdns (^6.2.2), upgrade smartproxy to ^10.0.2, smartrequest to ^2.1.0, smartrule to ^2.0.1, and smartrx to ^3.0.10.
- Upgrade @serve.zone/interfaces to ^5.0.4 and @tsclass/tsclass to ^9.1.0; update mailauth to ^4.8.4.
- Add packageManager field in package.json for PNPM configuration.
- Add readme.plan.md detailing the DcRouter implementation plan.
- Refactor import paths in several TS files (e.g. ts/plugins.ts, ts/mta classes) for consistency.
## 2025-03-15 - 2.3.0 - feat(platformservice)
Add AIBridge module and refactor service file paths for improved module organization
- Added new AIBridge class in ts/aibridge/classes.aibridge.ts.
- Renamed letter service file from ts/letter/letterservice.ts to ts/letter/classes.letterservice.ts and updated its index.
- Updated platformservice.ts to import letter and SMS services from new paths.
- Renamed SMS service file from ts/sms/smsservice.ts to ts/sms/classes.smsservice.ts and updated its index accordingly.
## 2025-03-15 - 2.2.1 - fix(platformservice)
Refactor module structure to update import paths and file organization
- Removed obsolete file 'ts/classes.platformservice.ts' and updated references to use 'ts/platformservice.ts'.
- Updated import paths in PlatformServiceDb, EmailService, and other modules to use new file structure.
- Renamed and moved files in the email, mta, letter, and sms directories to align with new module layout.
- Fixed references to external modules (e.g. '@serve.zone/interfaces', '@push.rocks/*', etc.) to reflect the updated paths.
## 2025-03-15 - 2.2.0 - feat(plugins)
Add smartproxy support by including the @push.rocks/smartproxy dependency and exporting it in the plugins module.
- Added '@push.rocks/smartproxy' dependency version '^4.1.0' to package.json
- Updated ts/plugins.ts to export the smartproxy module alongside other push.rocks modules
## 2025-03-15 - 2.1.0 - feat(MTA)
Update readme with detailed Mail Transfer Agent usage and examples
- Added a comprehensive MTA section with usage examples including SMTP server setup, DKIM signing/verification, SPF/DMARC support, and API integration
- Expanded the conclusion to highlight MTA capabilities alongside email, SMS, letter, and AI services
## 2025-03-15 - 2.0.0 - BREAKING CHANGE(platformservice)
Remove deprecated AIBridge module and update email service to use the MTA connector; update dependency versions and adjust build scripts in package.json.
- Completely remove the aibridge module files (aibridge.classes.aibridge.ts, aibridge.classes.aibridgedb.ts, aibridge.classes.openaibridge.ts, aibridge.paths.ts, aibridge.plugins.ts, and index.ts) as they are no longer needed.
- Switch the email service from using MailgunConnector to the new MTA connector for sending emails.
- Update dependency versions for @serve.zone/interfaces, @tsclass/tsclass, letterxpress, and uuid in package.json.
- Enhance the build script in package.json and add pnpm configuration.
## 2025-03-15 - 1.1.2 - fix(mta)
Expose HttpResponse.statusCode and add explicit generic type annotations in DNSManager cache retrieval
- Changed HttpResponse.statusCode from private to public to allow external access and inspection
- Added explicit generic type parameters in getFromCache calls for lookupMx and lookupTxt to enhance type safety
## 2025-03-15 - 1.1.1 - fix(paths)
Update directory paths to use a dedicated 'data' directory and add ensureDirectories function for proper directory creation.
- Refactored ts/paths.ts to define a base data directory using process.cwd().
- Reorganized MTA directories (keys, dns, emails sent/received/failed, logs) under the data directory.
- Added ensureDirectories function to create missing directories at runtime.
## 2025-03-15 - 1.1.1 - fix(mta)
Refactor API Manager and DKIMCreator: remove Express dependency in favor of Node's native HTTP server, add an HttpResponse helper to improve request handling, update path and authentication logic, and expose previously private DKIMCreator methods for API access.
- Replaced Express-based middleware with native HTTP server handling, including request body parsing and CORS headers.
- Introduced an HttpResponse helper class to standardize response writing.
- Updated route matching, parameter extraction, and error handling within the API Manager.
- Modified DKIMCreator methods (createDKIMKeys, storeDKIMKeys, createAndStoreDKIMKeys, and getDNSRecordForDomain) from private to public for better API accessibility.
- Updated plugin imports to include the native HTTP module.
## 2025-03-15 - 1.1.0 - feat(mta)
Enhance MTA service and SMTP server with robust session management, advanced email handling, and integrated API routes
- Introduce a state machine (SmtpState) and session management in the SMTP server to replace legacy buffering
- Refactor DNSManager with caching and improved SPF, DKIM, and DMARC verification methods
- Update Email class to support multiple recipients, CC, BCC with input sanitization and validation
- Add detailed logging, TLS upgrade handling, and error-based retry logic in EmailSendJob
- Implement a new API Manager with typed routes for sending emails, DKIM key generation, domain verification, and statistics
- Integrate certificate provisioning with auto-renewal and TLS options in the MTA service configuration
## 2024-05-11 - 1.0.10 to 1.0.8 - core
## 2025-05-04 - 1.0.10 to 1.0.8 - core
Applied core fixes across several versions on this day.
- Fixed core issues in versions 1.0.10, 1.0.9, and 1.0.8
@ -181,4 +215,4 @@ Applied a core fix.
- Fixed core functionality for version 1.0.1
Note: Versions that only contained version bumps (for example, 1.0.11 and the plain “1.0.x” commits) have been omitted from individual entries and are implicitly included in the version ranges above.
Note: Versions that only contained version bumps (for example, 1.0.11 and the plain "1.0.x" commits) have been omitted from individual entries and are implicitly included in the version ranges above.

View File

@ -18,7 +18,6 @@
"mail parsing",
"DKIM",
"platform service",
"mailgun integration",
"letterXpress",
"OpenAI",
"Anthropic AI",

View File

@ -1,7 +1,7 @@
{
"name": "@serve.zone/platformservice",
"private": true,
"version": "2.8.0",
"private": false,
"version": "2.11.1",
"description": "A multifaceted platform service handling mail, SMS, letter delivery, and AI services.",
"main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts",
@ -61,7 +61,6 @@
"mail parsing",
"DKIM",
"platform service",
"mailgun integration",
"letterXpress",
"OpenAI",
"Anthropic AI",

View File

@ -51,7 +51,7 @@ async function sendEmail() {
body: '<h1>This is a test email</h1>',
};
const emailService = new EmailService('MAILGUN_API_KEY'); // Replace with your real API key
const emailService = new EmailService(platformService);
await emailService.sendEmail(emailOptions);
console.log('Email sent successfully.');
@ -106,7 +106,7 @@ sendLetter();
### Mail Transfer Agent (MTA) and Consolidated Email Handling
The platform includes a robust Mail Transfer Agent (MTA) for enterprise-grade email handling with complete control over the email delivery process.
Additionally, the platform now features a consolidated email configuration system with pattern-based routing:
Additionally, the platform now features a consolidated email configuration system with pattern-based routing and a streamlined directory structure:
```mermaid
graph TD
@ -123,11 +123,13 @@ graph TD
ApiManager[API Manager] --> DcRouter
end
subgraph "MTA Service"
MtaMode --> MtaService[MTA Service]
MtaService --> EmailSendJob[Email Send Job]
MtaService --> DnsManager[DNS Manager]
MtaService --> DkimCreator[DKIM Creator]
subgraph "Mail System Structure"
MailCore[mail/core] --> EmailClasses[Email, TemplateManager, etc.]
MailDelivery[mail/delivery] --> MtaService[MTA Service]
MailDelivery --> EmailSendJob[Email Send Job]
MailRouting[mail/routing] --> DnsManager[DNS Manager]
MailSecurity[mail/security] --> AuthClasses[DKIM, SPF, DMARC]
MailServices[mail/services] --> ServiceClasses[EmailService, ApiManager]
end
subgraph "External Services"
@ -140,6 +142,12 @@ graph TD
#### Key Features
The email handling system provides:
- **Modular Directory Structure**: Clean organization with clear separation of concerns:
- **mail/core**: Core email models and basic functionality (Email, BounceManager, etc.)
- **mail/delivery**: Email delivery mechanisms (MTA, SMTP server, rate limiting)
- **mail/routing**: DNS and domain routing capabilities
- **mail/security**: Authentication and security features (DKIM, SPF, DMARC)
- **mail/services**: High-level services and API interfaces
- **Pattern-based Routing**: Route emails based on glob patterns like `*@domain.com` or `*@*.domain.com`
- **Multi-Modal Processing**: Handle different email domains with different processing modes:
- **Forward Mode**: SMTP forwarding to other servers
@ -249,12 +257,19 @@ setupEmailHandling();
#### Using the MTA Service Directly
You can still use the MTA service directly for more granular control:
You can still use the MTA service directly for more granular control with our new modular directory structure:
```ts
import { MtaService, Email } from '@serve.zone/platformservice';
import { SzPlatformService } from '@serve.zone/platformservice';
import { MtaService } from '@serve.zone/platformservice/mail/delivery';
import { Email } from '@serve.zone/platformservice/mail/core';
import { ApiManager } from '@serve.zone/platformservice/mail/services';
async function useMtaService() {
// Initialize platform service
const platformService = new SzPlatformService();
await platformService.start();
// Initialize MTA service
const mtaService = new MtaService(platformService);
await mtaService.start();
@ -277,7 +292,7 @@ async function useMtaService() {
console.log(`Email status: ${status.status}`);
// Set up API for external access
const apiManager = new ApiManager(mtaService);
const apiManager = new ApiManager(platformService.emailService);
await apiManager.start(3000);
console.log('MTA API running on port 3000');
}
@ -311,7 +326,3 @@ async function useAiService() {
useAiService();
```
### Conclusion
The `@serve.zone/platformservice` offers a robust set of features for modern application requirements, including but not limited to communication and AI services. By following the examples above, developers can integrate these services into their applications, harnessing the power of email, SMS, letters, MTA capabilities, and artificial intelligence seamlessly.

View File

@ -1,3 +1,51 @@
# PlatformService Roadmap
## Latest Changes
### API Type Safety Improvements
- [x] Create comprehensive TypeScript interfaces for all API methods
- [x] Replace any types with specific interfaces in EmailService and MtaConnector
- [x] Align interface types with their implementations
- [x] Document all interface properties with detailed JSDoc comments
- [x] Use type imports to ensure consistency between services
- [x] Fix TypeScript build errors from interface placements
- [x] Add proper method signatures for API endpoint handlers
- [x] Implement type-safe API responses
### Test Stability Improvements
- [x] Fix race conditions in SenderReputationMonitor tests
- [x] Disable filesystem operations during tests to prevent race conditions
- [x] Add proper cleanup of singleton instances and timeouts
- [x] Ensure all tests properly clean up shared resources
- [x] Set consistent test environment with NODE_ENV=test
### Error Handling Improvements
- [x] Add structured error types for better error reporting
- [x] Implement consistent error handling patterns across services
- [x] Add detailed error context for debugging
- [x] Create retry mechanisms for transient failures
- [x] Improve error logging with structured data
### Configuration Interface Standardization
- [x] Define consistent configuration interfaces across services
- [x] Implement validation for all configuration objects
- [x] Add default values for optional configuration
- [x] Create documentation for all configuration options
- [x] Add migration helpers for configuration format changes
### Logging Enhancements
- [ ] Implement structured logging throughout the codebase
- [ ] Add context information to all log messages
- [ ] Create consistent log levels and usage patterns
- [ ] Add correlation IDs for request tracking
- [ ] Implement log filtering and sampling options
### Mailgun Removal
- [x] Remove Mailgun integration from keywords in package.json and npmextra.json
- [x] Update EmailService comments to remove mentions of Mailgun
- [x] Update error messages to reference MTA instead of generic email providers
- [x] Update the readme email example to use PlatformService reference instead of Mailgun API key
# DcRouter Consolidated Email Configuration Plan
## Overview
@ -1022,11 +1070,13 @@ export class DcRouter {
- [x] Build mode-specific metrics and logging
### Phase 5: Testing and Documentation
- [ ] Create comprehensive unit tests for all components
- [ ] Implement integration tests for all processing modes
- [ ] Test pattern matching with complex scenarios
- [ ] Create performance tests for high-volume scenarios
- [ ] Build detailed documentation and examples
- [x] Create comprehensive unit tests for all components
- [x] Implement integration tests for all processing modes
- [x] Test pattern matching with complex scenarios
- [x] Create performance tests for high-volume scenarios
- [x] Build detailed documentation and examples
- [x] Identify and document legacy components to be deprecated (EmailDomainRouter)
- [x] Remove deprecated components (EmailDomainRouter)
## 6. Technical Requirements
@ -1194,6 +1244,8 @@ const dcRouter = new DcRouter({
- [x] Allow components to coexist for flexible deployment options
- [x] Provide documentation with comments for migrating existing deployments
- [x] Remove deprecated files after migration to consolidated approach
- [x] Removed EmailDomainRouter class
- [x] Updated imports and references to use the new DomainRouter
### 9.2 Backward Compatibility
- [x] Maintain support for basic proxy functionality
@ -1235,22 +1287,22 @@ const dcRouter = new DcRouter({
## 11. Documentation Requirements
### 11.1 Code Documentation
- [ ] Comprehensive JSDoc comments for all classes and methods
- [ ] Interface definitions with detailed parameter descriptions
- [ ] Example code snippets for common operations
- [ ] Architecture documentation with component diagrams
- [ ] Decision logs for key design choices
- [x] Comprehensive JSDoc comments for all classes and methods
- [x] Interface definitions with detailed parameter descriptions
- [x] Example code snippets for common operations
- [x] Architecture documentation with component diagrams
- [x] Decision logs for key design choices
### 11.2 User Documentation
- [ ] Getting started guide with configuration approach selection guidance
- [ ] Complete configuration reference for both approaches
- [ ] Deployment scenarios and examples
- [ ] Troubleshooting guide
- [ ] Performance tuning recommendations
- [ ] Security best practices
- [x] Getting started guide with configuration approach selection guidance
- [x] Complete configuration reference for both approaches
- [x] Deployment scenarios and examples
- [x] Troubleshooting guide
- [x] Performance tuning recommendations
- [x] Security best practices
### 11.3 Direct SmartProxy Configuration Guide
- [ ] Detailed guide on using SmartProxy's domain configuration capabilities
- [ ] Examples of complex routing scenarios with SmartProxy
- [ ] Performance optimization tips for SmartProxy configurations
- [ ] Security settings for SmartProxy deployments
- [x] Detailed guide on using SmartProxy's domain configuration capabilities
- [x] Examples of complex routing scenarios with SmartProxy
- [x] Performance optimization tips for SmartProxy configurations
- [x] Security settings for SmartProxy deployments

107
readme.smartlog.md Normal file
View File

@ -0,0 +1,107 @@
# Smartlog Improvement Plan
## Overview
This document outlines a plan for enhancing the `@push.rocks/smartlog` module to incorporate the advanced features currently implemented in the custom `EnhancedLogger` wrapper. By moving these features directly into `smartlog`, we can eliminate the need for wrapper classes while providing a more comprehensive logging solution.
## Current Limitations in Smartlog
- Limited context management (no hierarchical contexts)
- No correlation ID tracking for distributed tracing
- No built-in filtering or log level management
- No log sampling capabilities
- No middleware for HTTP request/response logging
- No timing utilities for performance tracking
- No child logger functionality with context inheritance
## Proposed Enhancements
### 1. Context Management
- Add hierarchical context support
- Implement methods for manipulating context:
- `setContext(context, overwrite = false)`
- `addToContext(key, value)`
- `removeFromContext(key)`
### 2. Correlation ID Tracking
- Add correlation ID support for distributed tracing
- Implement methods for correlation management:
- `setCorrelationId(id = null)`
- `getCorrelationId()`
- `clearCorrelationId()`
### 3. Log Filtering
- Implement configurable log filtering based on:
- Minimum log level
- Pattern-based exclusion rules
- Custom filtering functions
### 4. Log Sampling
- Add probabilistic log sampling for high-volume environments
- Support for enforcing critical logs (e.g., errors) regardless of sampling
### 5. Child Loggers
- Support creating child loggers with inherited context
- Allow context overrides in child loggers
### 6. Timing Utilities
- Add methods for timing operations:
- `logTimed(level, message, fn, context)`
- Support for both async and sync operations
### 7. HTTP Request Logging
- Add middleware for Express/Fastify/other HTTP frameworks
- Auto-capture request/response data
- Auto-propagate correlation IDs
### 8. Log Standardization
- Ensure consistent output format
- Add standard fields like timestamp, correlation ID
- Support for custom formatters
## Implementation Plan
1. **Core Enhancements**
- Implement context management
- Add correlation ID tracking
- Develop filtering and sampling capabilities
2. **Extended Features**
- Build child logger functionality
- Create timing utility methods
- Implement HTTP middleware
3. **Compatibility**
- Ensure backward compatibility
- Provide migration guide
- Add TypeScript declarations
4. **Documentation**
- Update README with new features
- Add examples for each feature
- Document best practices
## Migration Path
After implementing these enhancements to `smartlog`, the migration would involve:
1. Update to latest `smartlog` version
2. Replace `EnhancedLogger` instances with `smartlog.Smartlog`
3. Update configuration to use new capabilities
4. Replace middleware with `smartlog`'s built-in solutions
## Benefits
- Simplified dependency tree
- Better maintainability with single logging solution
- Improved performance with native implementation
- Enhanced type safety through TypeScript
- Standardized logging across projects

View File

@ -1,7 +1,7 @@
import { tap, expect } from '@push.rocks/tapbundle';
import * as plugins from '../ts/plugins.js';
import { SzPlatformService } from '../ts/platformservice.js';
import { BounceManager, BounceType, BounceCategory } from '../ts/email/classes.bouncemanager.js';
import { BounceManager, BounceType, BounceCategory } from '../ts/mail/core/classes.bouncemanager.js';
/**
* Test the BounceManager class

View File

@ -1,6 +1,6 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { ContentScanner, ThreatCategory } from '../ts/security/classes.contentscanner.js';
import { Email } from '../ts/mta/classes.email.js';
import { Email } from '../ts/mail/core/classes.email.js';
// Test instantiation
tap.test('ContentScanner - should be instantiable', async () => {

View File

@ -6,7 +6,7 @@ import {
type IEmailConfig,
type EmailProcessingMode,
type IDomainRule
} from '../ts/dcrouter/index.js';
} from '../ts/classes.dcrouter.js';
tap.test('DcRouter class - basic functionality', async () => {
// Create a simple DcRouter instance

View File

@ -1,8 +1,8 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { SzPlatformService } from '../ts/platformservice.js';
import { SpfVerifier, SpfQualifier, SpfMechanismType } from '../ts/mta/classes.spfverifier.js';
import { DmarcVerifier, DmarcPolicy, DmarcAlignment } from '../ts/mta/classes.dmarcverifier.js';
import { Email } from '../ts/mta/classes.email.js';
import { SpfVerifier, SpfQualifier, SpfMechanismType } from '../ts/mail/security/classes.spfverifier.js';
import { DmarcVerifier, DmarcPolicy, DmarcAlignment } from '../ts/mail/security/classes.dmarcverifier.js';
import { Email } from '../ts/mail/core/classes.email.js';
/**
* Test email authentication systems: SPF and DMARC
@ -12,7 +12,42 @@ import { Email } from '../ts/mta/classes.email.js';
let platformService: SzPlatformService;
tap.test('Setup test environment', async () => {
platformService = new SzPlatformService();
// Create platform service with default config from the config module
platformService = new SzPlatformService({
id: 'test-platform-service',
version: '1.0.0',
environment: 'test',
name: 'TestPlatformService',
enabled: true,
logging: {
level: 'info',
structured: true,
correlationTracking: true
},
server: {
enabled: true,
host: '0.0.0.0',
port: 3000,
cors: true
},
email: {
useMta: true,
mtaConfig: {
smtp: {
enabled: true,
port: 25,
hostname: 'mta.test.local',
maxSize: 10 * 1024 * 1024
},
security: {
useDkim: true,
verifyDkim: true,
verifySpf: true,
verifyDmarc: true
}
}
}
});
// Use start() instead of init() which doesn't exist
await platformService.start();
expect(platformService.mtaService).toBeTruthy();

367
test/test.errors.ts Normal file
View File

@ -0,0 +1,367 @@
import { tap, expect } from '@push.rocks/tapbundle';
import * as errors from '../ts/errors/index.js';
import {
PlatformError,
ValidationError,
NetworkError,
ResourceError,
OperationError
} from '../ts/errors/base.errors.js';
import {
ErrorSeverity,
ErrorCategory,
ErrorRecoverability
} from '../ts/errors/error.codes.js';
import {
EmailServiceError,
EmailTemplateError,
EmailValidationError,
EmailSendError,
EmailReceiveError
} from '../ts/errors/email.errors.js';
import {
MtaConnectionError,
MtaAuthenticationError,
MtaDeliveryError,
MtaConfigurationError
} from '../ts/errors/mta.errors.js';
import {
ErrorHandler
} from '../ts/errors/error-handler.js';
// Test base error classes
tap.test('Base error classes should set properties correctly', async () => {
const message = 'Test error message';
const code = 'TEST_ERROR_CODE';
const context = {
component: 'TestComponent',
operation: 'testOperation',
data: { foo: 'bar' }
};
// Test PlatformError
const platformError = new PlatformError(
message,
code,
ErrorSeverity.MEDIUM,
ErrorCategory.OPERATION,
ErrorRecoverability.MAYBE_RECOVERABLE,
context
);
expect(platformError.message).toEqual(message);
expect(platformError.code).toEqual(code);
expect(platformError.severity).toEqual(ErrorSeverity.MEDIUM);
expect(platformError.category).toEqual(ErrorCategory.OPERATION);
expect(platformError.recoverability).toEqual(ErrorRecoverability.MAYBE_RECOVERABLE);
expect(platformError.context.component).toEqual(context.component);
expect(platformError.context.operation).toEqual(context.operation);
expect(platformError.context.data.foo).toEqual('bar');
expect(platformError.name).toEqual('PlatformError');
// Test ValidationError
const validationError = new ValidationError(message, code, context);
expect(validationError.category).toEqual(ErrorCategory.VALIDATION);
expect(validationError.severity).toEqual(ErrorSeverity.LOW);
// Test NetworkError
const networkError = new NetworkError(message, code, context);
expect(networkError.category).toEqual(ErrorCategory.CONNECTIVITY);
expect(networkError.severity).toEqual(ErrorSeverity.MEDIUM);
expect(networkError.recoverability).toEqual(ErrorRecoverability.MAYBE_RECOVERABLE);
// Test ResourceError
const resourceError = new ResourceError(message, code, context);
expect(resourceError.category).toEqual(ErrorCategory.RESOURCE);
});
// Test email error classes
tap.test('Email error classes should be properly constructed', async () => {
// Test EmailServiceError
const emailServiceError = new EmailServiceError('Email service error', {
component: 'EmailService',
operation: 'sendEmail'
});
expect(emailServiceError.code).toEqual('EMAIL_SERVICE_ERROR');
expect(emailServiceError.name).toEqual('EmailServiceError');
// Test EmailTemplateError
const templateError = new EmailTemplateError('Template not found: welcome_email', {
data: { templateId: 'welcome_email' }
});
expect(templateError.code).toEqual('EMAIL_TEMPLATE_ERROR');
expect(templateError.context.data.templateId).toEqual('welcome_email');
// Test EmailSendError with permanent flag
const permanentError = EmailSendError.permanent(
'Invalid recipient',
'user@example.com',
{ data: { details: 'DNS not found' } }
);
expect(permanentError.code).toEqual('EMAIL_SEND_ERROR');
expect(permanentError.isPermanent()).toEqual(true);
expect(permanentError.context.data.permanent).toEqual(true);
// Test EmailSendError with temporary flag and retry
const tempError = EmailSendError.temporary(
'Server busy',
3,
0,
1000,
{ data: { server: 'smtp.example.com' } }
);
expect(tempError.isPermanent()).toEqual(false);
expect(tempError.context.data.permanent).toEqual(false);
expect(tempError.context.retry.maxRetries).toEqual(3);
expect(tempError.shouldRetry()).toEqual(true);
});
// Test MTA error classes
tap.test('MTA error classes should be properly constructed', async () => {
// Test MtaConnectionError
const dnsError = MtaConnectionError.dnsError('mail.example.com', new Error('DNS lookup failed'));
expect(dnsError.code).toEqual('MTA_CONNECTION_ERROR');
expect(dnsError.category).toEqual(ErrorCategory.CONNECTIVITY);
expect(dnsError.context.data.hostname).toEqual('mail.example.com');
// Test MtaTimeoutError via MtaConnectionError.timeout
const timeoutError = MtaConnectionError.timeout('mail.example.com', 25, 30000);
expect(timeoutError.code).toEqual('MTA_CONNECTION_ERROR');
expect(timeoutError.context.data.timeout).toEqual(30000);
// Test MtaAuthenticationError
const authError = MtaAuthenticationError.invalidCredentials('mail.example.com', 'user@example.com');
expect(authError.code).toEqual('MTA_AUTHENTICATION_ERROR');
expect(authError.category).toEqual(ErrorCategory.AUTHENTICATION);
expect(authError.context.data.username).toEqual('user@example.com');
// Test MtaDeliveryError
const permDeliveryError = MtaDeliveryError.permanent(
'User unknown',
'nonexistent@example.com',
'550',
'550 5.1.1 User unknown'
);
expect(permDeliveryError.code).toEqual('MTA_DELIVERY_ERROR');
expect(permDeliveryError.isPermanent()).toEqual(true);
expect(permDeliveryError.getRecipientAddress()).toEqual('nonexistent@example.com');
expect(permDeliveryError.getStatusCode()).toEqual('550');
// Test temporary delivery error with retry
const tempDeliveryError = MtaDeliveryError.temporary(
'Mailbox temporarily unavailable',
'user@example.com',
'450',
'450 4.2.1 Mailbox temporarily unavailable',
3,
1,
5000
);
expect(tempDeliveryError.isPermanent()).toEqual(false);
expect(tempDeliveryError.shouldRetry()).toEqual(true);
expect(tempDeliveryError.context.retry.currentRetry).toEqual(1);
expect(tempDeliveryError.context.retry.maxRetries).toEqual(3);
});
// Test error handler utility
tap.test('ErrorHandler should properly handle and format errors', async () => {
// Configure error handler
ErrorHandler.configure({
logErrors: false, // Disable for testing
includeStacksInProd: false,
retry: {
maxAttempts: 5,
baseDelay: 100,
maxDelay: 1000,
backoffFactor: 2
}
});
// Test converting regular Error to PlatformError
const regularError = new Error('Something went wrong');
const platformError = ErrorHandler.toPlatformError(
regularError,
'PLATFORM_OPERATION_ERROR',
{ component: 'TestHandler' }
);
expect(platformError).toBeInstanceOf(PlatformError);
expect(platformError.code).toEqual('PLATFORM_OPERATION_ERROR');
expect(platformError.context.component).toEqual('TestHandler');
// Test formatting error for API response
const formattedError = ErrorHandler.formatErrorForResponse(platformError, true);
expect(formattedError.code).toEqual('PLATFORM_OPERATION_ERROR');
expect(formattedError.message).toEqual('An unexpected error occurred.');
expect(formattedError.details.rawMessage).toEqual('Something went wrong');
// Test executing a function with error handling
let executed = false;
try {
await ErrorHandler.execute(async () => {
executed = true;
throw new Error('Execution failed');
}, 'TEST_EXECUTION_ERROR', { operation: 'testExecution' });
} catch (error) {
expect(error).toBeInstanceOf(PlatformError);
expect(error.code).toEqual('TEST_EXECUTION_ERROR');
expect(error.context.operation).toEqual('testExecution');
}
expect(executed).toEqual(true);
// Test executeWithRetry successful after retries
let attempts = 0;
const result = await ErrorHandler.executeWithRetry(
async () => {
attempts++;
if (attempts < 3) {
throw new Error('Temporary failure');
}
return 'success';
},
'TEST_RETRY_ERROR',
{
maxAttempts: 5,
baseDelay: 10, // Use small delay for tests
onRetry: (error, attempt, delay) => {
expect(error).toBeInstanceOf(PlatformError);
expect(attempt).toBeGreaterThan(0);
expect(delay).toBeGreaterThan(0);
}
}
);
expect(result).toEqual('success');
expect(attempts).toEqual(3);
// Test executeWithRetry that fails after max attempts
attempts = 0;
try {
await ErrorHandler.executeWithRetry(
async () => {
attempts++;
throw new Error('Persistent failure');
},
'TEST_RETRY_ERROR',
{
maxAttempts: 3,
baseDelay: 10
}
);
} catch (error) {
expect(error).toBeInstanceOf(PlatformError);
expect(attempts).toEqual(3);
}
});
// Test retry utilities
tap.test('Error retry utilities should work correctly', async () => {
let attempts = 0;
const start = Date.now();
try {
await errors.retry(
async () => {
attempts++;
if (attempts < 3) {
throw new Error('Temporary error');
}
return 'success';
},
{
maxRetries: 5,
initialDelay: 20,
backoffFactor: 1.5,
retryableErrors: [/Temporary/]
}
);
} catch (e) {
// Should not reach here
expect(false).toEqual(true);
}
expect(attempts).toEqual(3);
// Test retry with non-retryable error
attempts = 0;
try {
await errors.retry(
async () => {
attempts++;
throw new Error('Critical error');
},
{
maxRetries: 3,
initialDelay: 10,
retryableErrors: [/Temporary/] // Won't match "Critical"
}
);
} catch (error) {
expect(error.message).toEqual('Critical error');
expect(attempts).toEqual(1); // Should only attempt once
}
});
// Helper function that will reject first n times, then resolve
async function flaky(failTimes: number, result: any = 'success'): Promise<any> {
if (flaky.counter < failTimes) {
flaky.counter++;
throw new Error(`Flaky failure ${flaky.counter}`);
}
return result;
}
flaky.counter = 0;
flaky.reset = () => { flaky.counter = 0; };
// Test error wrapping and retry combination
tap.test('Error handling can be combined with retry for robust operations', async () => {
// Reset counter for the test
flaky.reset();
// Create a wrapped version of the flaky function
const wrapped = errors.withErrorHandling(
() => flaky(2, 'wrapped success'),
'TEST_WRAPPED_ERROR',
{ component: 'TestComponent' }
);
// Execute with retry
try {
const result = await errors.retry(
wrapped,
{
maxRetries: 3,
initialDelay: 10,
}
);
expect(result).toEqual('wrapped success');
expect(flaky.counter).toEqual(2);
} catch (error) {
// Should not reach here
expect(false).toEqual(true);
}
// Reset and test failure case
flaky.reset();
try {
await errors.retry(
() => flaky(5, 'never reached'),
{
maxRetries: 2, // Only retry twice, but we need 5 attempts to succeed
initialDelay: 10,
}
);
// Should not reach here
expect(false).toEqual(true);
} catch (error) {
expect(error.message).toContain('Flaky failure');
expect(flaky.counter).toEqual(3); // Initial + 2 retries = 3 attempts
}
});
tap.test('stop', async () => {
// This is a placeholder test to ensure we call tap.stopForcefully()
});
export default tap.stopForcefully();

View File

@ -1,10 +1,10 @@
import { tap, expect } from '@push.rocks/tapbundle';
import * as plugins from '../ts/plugins.js';
import { SzPlatformService } from '../ts/platformservice.js';
import { MtaService } from '../ts/mta/classes.mta.js';
import { EmailService } from '../ts/email/classes.emailservice.js';
import { BounceManager } from '../ts/email/classes.bouncemanager.js';
import DcRouter from '../ts/dcrouter/classes.dcrouter.js';
import { MtaService } from '../ts/mail/delivery/classes.mta.js';
import { EmailService } from '../ts/mail/services/classes.emailservice.js';
import { BounceManager } from '../ts/mail/core/classes.bouncemanager.js';
import DcRouter from '../ts/classes.dcrouter.js';
// Test the new integration architecture
tap.test('should be able to create an independent MTA service', async (tools) => {
@ -25,8 +25,22 @@ tap.test('should be able to create an independent MTA service', async (tools) =>
});
tap.test('should be able to create an EmailService with an existing MTA', async (tools) => {
// Create a platform service first
const platformService = new SzPlatformService();
// Create a platform service with test config
const platformService = new SzPlatformService({
id: 'test-platform-service',
version: '1.0.0',
environment: 'test',
name: 'TestPlatformService',
enabled: true,
logging: {
level: 'info',
structured: true,
correlationTracking: true
},
server: {
enabled: false // Disable server for tests
}
});
// Create a shared bounce manager
const bounceManager = new BounceManager();
@ -74,8 +88,22 @@ tap.test('MTA service should have SMTP rule engine', async (tools) => {
});
tap.test('platform service should support having an MTA service', async (tools) => {
// Create a platform service with default config
const platformService = new SzPlatformService();
// Create a platform service with test config
const platformService = new SzPlatformService({
id: 'test-platform-service',
version: '1.0.0',
environment: 'test',
name: 'TestPlatformService',
enabled: true,
logging: {
level: 'info',
structured: true,
correlationTracking: true
},
server: {
enabled: false // Disable server for tests
}
});
// Create MTA - don't await start() to avoid binding to ports
platformService.mtaService = new MtaService(platformService, {

View File

@ -1,5 +1,5 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { RateLimiter } from '../ts/mta/classes.ratelimiter.js';
import { RateLimiter } from '../ts/mail/delivery/classes.ratelimiter.js';
tap.test('RateLimiter - should be instantiable', async () => {
const limiter = new RateLimiter({

View File

@ -16,10 +16,24 @@ const cleanupTestData = () => {
const resetSingleton = () => {
// @ts-ignore - accessing private static field for testing
SenderReputationMonitor.instance = null;
// Clean up any timeout to prevent race conditions
const activeSendReputationMonitors = Array.from(Object.values(global))
.filter((item: any) => item && typeof item === 'object' && item._idleTimeout)
.filter((item: any) =>
item._onTimeout &&
item._onTimeout.toString &&
item._onTimeout.toString().includes('updateAllDomainMetrics'));
// Clear any active timeouts to prevent race conditions
activeSendReputationMonitors.forEach((timer: any) => {
clearTimeout(timer);
});
};
// Before running any tests
tap.test('setup', async () => {
resetSingleton();
cleanupTestData();
});
@ -39,7 +53,7 @@ tap.test('should initialize SenderReputationMonitor with default settings', asyn
tap.test('should initialize SenderReputationMonitor with custom settings', async () => {
resetSingleton();
const reputationMonitor = SenderReputationMonitor.getInstance({
enabled: true,
enabled: false, // Disable automatic updates to prevent race conditions
domains: ['example.com', 'test.com'],
updateFrequency: 12 * 60 * 60 * 1000, // 12 hours
alertThresholds: {
@ -61,7 +75,7 @@ tap.test('should initialize SenderReputationMonitor with custom settings', async
tap.test('should record send events and update metrics', async () => {
resetSingleton();
const reputationMonitor = SenderReputationMonitor.getInstance({
enabled: true,
enabled: false, // Disable automatic updates to prevent race conditions
domains: ['example.com']
});
@ -87,7 +101,7 @@ tap.test('should record send events and update metrics', async () => {
tap.test('should calculate reputation scores correctly', async () => {
resetSingleton();
const reputationMonitor = SenderReputationMonitor.getInstance({
enabled: true,
enabled: false, // Disable automatic updates to prevent race conditions
domains: ['high.com', 'medium.com', 'low.com']
});
@ -120,7 +134,7 @@ tap.test('should calculate reputation scores correctly', async () => {
tap.test('should add and remove domains for monitoring', async () => {
resetSingleton();
const reputationMonitor = SenderReputationMonitor.getInstance({
enabled: true,
enabled: false, // Disable automatic updates to prevent race conditions
domains: ['example.com']
});
@ -147,7 +161,7 @@ tap.test('should add and remove domains for monitoring', async () => {
tap.test('should track engagement metrics correctly', async () => {
resetSingleton();
const reputationMonitor = SenderReputationMonitor.getInstance({
enabled: true,
enabled: false, // Disable automatic updates to prevent race conditions
domains: ['example.com']
});
@ -172,12 +186,13 @@ tap.test('should track engagement metrics correctly', async () => {
tap.test('should store historical reputation data', async () => {
resetSingleton();
const reputationMonitor = SenderReputationMonitor.getInstance({
enabled: true,
enabled: false, // Disable automatic updates to prevent race conditions
domains: ['example.com']
});
// Record events over multiple days
const today = new Date();
const todayStr = today.toISOString().split('T')[0];
// Record data
reputationMonitor.recordSendEvent('example.com', { type: 'sent', count: 1000 });
@ -192,7 +207,6 @@ tap.test('should store historical reputation data', async () => {
// Check that daily send volume is tracked
expect(metrics.volume.dailySendVolume).toBeTruthy();
const todayStr = today.toISOString().split('T')[0];
expect(metrics.volume.dailySendVolume[todayStr]).toEqual(1000);
});
@ -200,7 +214,7 @@ tap.test('should store historical reputation data', async () => {
tap.test('should correctly handle different event types', async () => {
resetSingleton();
const reputationMonitor = SenderReputationMonitor.getInstance({
enabled: true,
enabled: false, // Disable automatic updates to prevent race conditions
domains: ['example.com']
});
@ -233,6 +247,7 @@ tap.test('should correctly handle different event types', async () => {
// After all tests, clean up
tap.test('cleanup', async () => {
resetSingleton();
cleanupTestData();
});
@ -240,4 +255,5 @@ tap.test('stop', async () => {
await tap.stopForcefully();
});
export default tap.start();

View File

@ -3,9 +3,9 @@ import * as plugins from '../ts/plugins.js';
import * as paths from '../ts/paths.js';
// Import the components we want to test
import { EmailValidator } from '../ts/email/classes.emailvalidator.js';
import { TemplateManager } from '../ts/email/classes.templatemanager.js';
import { Email } from '../ts/mta/classes.email.js';
import { EmailValidator } from '../ts/mail/core/classes.emailvalidator.js';
import { TemplateManager } from '../ts/mail/core/classes.templatemanager.js';
import { Email } from '../ts/mail/core/classes.email.js';
// Ensure test directories exist
paths.ensureDirectories();

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/platformservice',
version: '2.8.0',
version: '2.11.1',
description: 'A multifaceted platform service handling mail, SMS, letter delivery, and AI services.'
}

View File

@ -1,18 +1,17 @@
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import * as plugins from './plugins.js';
import * as paths from './paths.js';
import { SmtpPortConfig, type ISmtpPortSettings } from './classes.smtp.portconfig.js';
import { EmailDomainRouter, type IEmailDomainRoutingConfig } from './classes.email.domainrouter.js';
// Certificate types are available via plugins.tsclass
// Import the consolidated email config
import type { IEmailConfig, IDomainRule } from './classes.email.config.js';
import { DomainRouter } from './classes.domain.router.js';
import { UnifiedEmailServer } from './classes.unified.email.server.js';
import { UnifiedDeliveryQueue, type IQueueOptions } from './classes.delivery.queue.js';
import { MultiModeDeliverySystem, type IMultiModeDeliveryOptions } from './classes.delivery.system.js';
import { UnifiedRateLimiter, type IHierarchicalRateLimits } from './classes.rate.limiter.js';
import { logger } from '../logger.js';
import type { IEmailConfig, IDomainRule } from './mail/routing/classes.email.config.js';
import { DomainRouter } from './mail/routing/classes.domain.router.js';
import { UnifiedEmailServer } from './mail/routing/classes.unified.email.server.js';
import { UnifiedDeliveryQueue, type IQueueOptions } from './mail/delivery/classes.delivery.queue.js';
import { MultiModeDeliverySystem, type IMultiModeDeliveryOptions } from './mail/delivery/classes.delivery.system.js';
import { UnifiedRateLimiter, type IHierarchicalRateLimits } from './mail/delivery/classes.unified.rate.limiter.js';
import { logger } from './logger.js';
export interface IDcRouterOptions {
/**

View File

@ -1,4 +1,4 @@
import * as plugins from '../plugins.js';
import * as plugins from './plugins.js';
/**
* Configuration options for TLS in SMTP connections

433
ts/config/base.config.ts Normal file
View File

@ -0,0 +1,433 @@
/**
* Base configuration interface with common properties for all services
*/
export interface IBaseConfig {
/**
* Unique identifier for this configuration
* Used to track configuration versions and changes
*/
id?: string;
/**
* Configuration version
* Used for migration between different config formats
*/
version?: string;
/**
* Environment this configuration is intended for
* (development, test, production, etc.)
*/
environment?: 'development' | 'test' | 'staging' | 'production';
/**
* Display name for this configuration
*/
name?: string;
/**
* Whether this configuration is enabled
* Services with disabled configuration shouldn't start
*/
enabled?: boolean;
/**
* Logging configuration
*/
logging?: {
/**
* Minimum log level to output
*/
level?: 'error' | 'warn' | 'info' | 'debug';
/**
* Whether to include structured data in logs
*/
structured?: boolean;
/**
* Whether to enable correlation tracking in logs
*/
correlationTracking?: boolean;
};
}
/**
* Base database configuration
*/
export interface IDatabaseConfig {
/**
* Database connection string or URL
*/
connectionString?: string;
/**
* Database host
*/
host?: string;
/**
* Database port
*/
port?: number;
/**
* Database name
*/
database?: string;
/**
* Database username
*/
username?: string;
/**
* Database password
*/
password?: string;
/**
* SSL configuration for database connection
*/
ssl?: boolean | {
/**
* Whether to reject unauthorized SSL connections
*/
rejectUnauthorized?: boolean;
/**
* Path to CA certificate file
*/
ca?: string;
/**
* Path to client certificate file
*/
cert?: string;
/**
* Path to client key file
*/
key?: string;
};
/**
* Connection pool configuration
*/
pool?: {
/**
* Minimum number of connections in pool
*/
min?: number;
/**
* Maximum number of connections in pool
*/
max?: number;
/**
* Connection idle timeout in milliseconds
*/
idleTimeoutMillis?: number;
};
}
/**
* Base TLS configuration interface
*/
export interface ITlsConfig {
/**
* Whether to enable TLS
*/
enabled?: boolean;
/**
* The domain name for the certificate
*/
domain?: string;
/**
* Path to certificate file
*/
certPath?: string;
/**
* Path to private key file
*/
keyPath?: string;
/**
* Path to CA certificate file
*/
caPath?: string;
/**
* Minimum TLS version to support
*/
minVersion?: 'TLSv1.2' | 'TLSv1.3';
/**
* Whether to auto-renew certificates
*/
autoRenew?: boolean;
/**
* Whether to reject unauthorized certificates
*/
rejectUnauthorized?: boolean;
}
/**
* Base retry configuration interface
*/
export interface IRetryConfig {
/**
* Maximum number of retry attempts
*/
maxAttempts?: number;
/**
* Base delay between retries in milliseconds
*/
baseDelay?: number;
/**
* Maximum delay between retries in milliseconds
*/
maxDelay?: number;
/**
* Backoff factor for exponential backoff
*/
backoffFactor?: number;
/**
* Specific error codes that should trigger retries
*/
retryableErrorCodes?: string[];
/**
* Whether to add jitter to retry delays
*/
useJitter?: boolean;
}
/**
* Base rate limiting configuration interface
*/
export interface IRateLimitConfig {
/**
* Whether rate limiting is enabled
*/
enabled?: boolean;
/**
* Maximum number of operations per period
*/
maxPerPeriod?: number;
/**
* Time period in milliseconds
*/
periodMs?: number;
/**
* Whether to apply per key (e.g., domain, user, etc.)
*/
perKey?: boolean;
/**
* Number of burst tokens allowed
*/
burstTokens?: number;
}
/**
* Basic HTTP server configuration
*/
export interface IHttpServerConfig {
/**
* Whether the HTTP server is enabled
*/
enabled?: boolean;
/**
* Host to bind to
*/
host?: string;
/**
* Port to listen on
*/
port?: number;
/**
* Path prefix for all routes
*/
basePath?: string;
/**
* CORS configuration
*/
cors?: boolean | {
/**
* Allowed origins
*/
origins?: string[];
/**
* Allowed methods
*/
methods?: string[];
/**
* Allowed headers
*/
headers?: string[];
/**
* Whether to allow credentials
*/
credentials?: boolean;
};
/**
* TLS configuration
*/
tls?: ITlsConfig;
/**
* Maximum request body size in bytes
*/
maxBodySize?: number;
/**
* Request timeout in milliseconds
*/
timeout?: number;
}
/**
* Basic queue configuration
*/
export interface IQueueConfig {
/**
* Type of storage for the queue
*/
storageType?: 'memory' | 'disk' | 'redis';
/**
* Path for persistent storage
*/
persistentPath?: string;
/**
* Redis configuration for queue
*/
redis?: {
/**
* Redis host
*/
host?: string;
/**
* Redis port
*/
port?: number;
/**
* Redis password
*/
password?: string;
/**
* Redis database number
*/
db?: number;
};
/**
* Maximum size of the queue
*/
maxSize?: number;
/**
* Maximum number of retry attempts
*/
maxRetries?: number;
/**
* Base delay between retries in milliseconds
*/
baseRetryDelay?: number;
/**
* Maximum delay between retries in milliseconds
*/
maxRetryDelay?: number;
/**
* Check interval for processing in milliseconds
*/
checkInterval?: number;
/**
* Maximum number of parallel processes
*/
maxParallelProcessing?: number;
}
/**
* Basic monitoring configuration
*/
export interface IMonitoringConfig {
/**
* Whether monitoring is enabled
*/
enabled?: boolean;
/**
* Metrics collection interval in milliseconds
*/
metricsInterval?: number;
/**
* Whether to expose Prometheus metrics
*/
exposePrometheus?: boolean;
/**
* Port for Prometheus metrics
*/
prometheusPort?: number;
/**
* Whether to collect detailed metrics
*/
detailedMetrics?: boolean;
/**
* Alert thresholds
*/
alertThresholds?: Record<string, number>;
/**
* Notification configuration
*/
notifications?: {
/**
* Whether to send notifications
*/
enabled?: boolean;
/**
* Email address to send notifications to
*/
email?: string;
/**
* Webhook URL to send notifications to
*/
webhook?: string;
};
}

266
ts/config/email.config.ts Normal file
View File

@ -0,0 +1,266 @@
import type { IBaseConfig, ITlsConfig, IQueueConfig, IRateLimitConfig, IMonitoringConfig } from './base.config.js';
/**
* Email service configuration
*/
export interface IEmailConfig extends IBaseConfig {
/**
* Whether to use MTA for sending emails
*/
useMta?: boolean;
/**
* MTA configuration
*/
mtaConfig?: IMtaConfig;
/**
* Template configuration
*/
templateConfig?: {
/**
* Default sender email address
*/
from?: string;
/**
* Default reply-to email address
*/
replyTo?: string;
/**
* Default footer HTML
*/
footerHtml?: string;
/**
* Default footer text
*/
footerText?: string;
};
/**
* Whether to load templates from directory
*/
loadTemplatesFromDir?: boolean;
/**
* Directory path for email templates
*/
templatesDir?: string;
}
/**
* MTA configuration
*/
export interface IMtaConfig {
/**
* SMTP server configuration
*/
smtp?: {
/**
* Whether to enable the SMTP server
*/
enabled?: boolean;
/**
* Port to listen on
*/
port?: number;
/**
* SMTP server hostname
*/
hostname?: string;
/**
* Maximum allowed email size in bytes
*/
maxSize?: number;
};
/**
* TLS configuration
*/
tls?: ITlsConfig;
/**
* Outbound email configuration
*/
outbound?: {
/**
* Maximum concurrent sending jobs
*/
concurrency?: number;
/**
* Retry configuration
*/
retries?: {
/**
* Maximum number of retries per message
*/
max?: number;
/**
* Initial delay between retries (milliseconds)
*/
delay?: number;
/**
* Whether to use exponential backoff for retries
*/
useBackoff?: boolean;
};
/**
* Rate limiting configuration
*/
rateLimit?: IRateLimitConfig;
/**
* IP warmup configuration
*/
warmup?: {
/**
* Whether IP warmup is enabled
*/
enabled?: boolean;
/**
* IP addresses to warm up
*/
ipAddresses?: string[];
/**
* Target domains to warm up
*/
targetDomains?: string[];
/**
* Allocation policy to use
*/
allocationPolicy?: string;
/**
* Fallback percentage for ESP routing during warmup
*/
fallbackPercentage?: number;
};
/**
* Reputation monitoring configuration
*/
reputation?: IMonitoringConfig & {
/**
* Alert thresholds
*/
alertThresholds?: {
/**
* Minimum acceptable reputation score
*/
minReputationScore?: number;
/**
* Maximum acceptable complaint rate
*/
maxComplaintRate?: number;
};
};
};
/**
* Security settings
*/
security?: {
/**
* Whether to use DKIM signing
*/
useDkim?: boolean;
/**
* Whether to verify inbound DKIM signatures
*/
verifyDkim?: boolean;
/**
* Whether to verify SPF on inbound
*/
verifySpf?: boolean;
/**
* Whether to verify DMARC on inbound
*/
verifyDmarc?: boolean;
/**
* Whether to enforce DMARC policy
*/
enforceDmarc?: boolean;
/**
* Whether to use TLS for outbound when available
*/
useTls?: boolean;
/**
* Whether to require valid certificates
*/
requireValidCerts?: boolean;
/**
* Log level for email security events
*/
securityLogLevel?: 'info' | 'warn' | 'error';
/**
* Whether to check IP reputation for inbound emails
*/
checkIPReputation?: boolean;
/**
* Whether to scan content for malicious payloads
*/
scanContent?: boolean;
/**
* Action to take when malicious content is detected
*/
maliciousContentAction?: 'tag' | 'quarantine' | 'reject';
/**
* Minimum threat score to trigger action
*/
threatScoreThreshold?: number;
/**
* Whether to reject connections from high-risk IPs
*/
rejectHighRiskIPs?: boolean;
};
/**
* Domains configuration
*/
domains?: {
/**
* List of domains that this MTA will handle as local
*/
local?: string[];
/**
* Whether to auto-create DNS records
*/
autoCreateDnsRecords?: boolean;
/**
* DKIM selector to use
*/
dkimSelector?: string;
};
/**
* Queue configuration
*/
queue?: IQueueConfig;
}

100
ts/config/index.ts Normal file
View File

@ -0,0 +1,100 @@
// Export configuration interfaces
export * from './base.config.js';
export * from './email.config.js';
export * from './sms.config.js';
export * from './platform.config.js';
// Export validation tools
export * from './validator.js';
export * from './schemas.js';
// Re-export commonly used types
import type { IPlatformConfig } from './platform.config.js';
import type { IEmailConfig, IMtaConfig } from './email.config.js';
import type { ISmsConfig } from './sms.config.js';
import type {
IBaseConfig,
ITlsConfig,
IHttpServerConfig,
IRateLimitConfig,
IQueueConfig
} from './base.config.js';
// Default platform configuration
export const defaultConfig: IPlatformConfig = {
id: 'platform-service-config',
version: '1.0.0',
environment: 'production',
name: 'PlatformService',
enabled: true,
logging: {
level: 'info',
structured: true,
correlationTracking: true
},
server: {
enabled: true,
host: '0.0.0.0',
port: 3000,
cors: true
},
email: {
useMta: true,
mtaConfig: {
smtp: {
enabled: true,
port: 25,
hostname: 'mta.lossless.one',
maxSize: 10 * 1024 * 1024 // 10MB
},
tls: {
domain: 'mta.lossless.one',
autoRenew: true
},
security: {
useDkim: true,
verifyDkim: true,
verifySpf: true,
verifyDmarc: true,
enforceDmarc: true,
useTls: true,
requireValidCerts: false,
securityLogLevel: 'warn',
checkIPReputation: true,
scanContent: true,
maliciousContentAction: 'tag',
threatScoreThreshold: 50,
rejectHighRiskIPs: false
},
domains: {
local: ['lossless.one'],
autoCreateDnsRecords: true,
dkimSelector: 'mta'
}
},
templateConfig: {
from: 'no-reply@lossless.one',
replyTo: 'support@lossless.one'
},
loadTemplatesFromDir: true
},
paths: {
dataDir: 'data',
logsDir: 'logs',
tempDir: 'temp',
emailTemplatesDir: 'templates/email'
}
};
// Export main types for convenience
export type {
IPlatformConfig,
IEmailConfig,
IMtaConfig,
ISmsConfig,
IBaseConfig,
ITlsConfig,
IHttpServerConfig,
IRateLimitConfig,
IQueueConfig
};

View File

@ -0,0 +1,54 @@
import type { IBaseConfig, IHttpServerConfig, IDatabaseConfig } from './base.config.js';
import type { IEmailConfig } from './email.config.js';
import type { ISmsConfig } from './sms.config.js';
/**
* Platform service configuration
* Root configuration that includes all service configurations
*/
export interface IPlatformConfig extends IBaseConfig {
/**
* HTTP server configuration
*/
server?: IHttpServerConfig;
/**
* Database configuration
*/
database?: IDatabaseConfig;
/**
* Email service configuration
*/
email?: IEmailConfig;
/**
* SMS service configuration
*/
sms?: ISmsConfig;
/**
* Path configuration
*/
paths?: {
/**
* Data directory path
*/
dataDir?: string;
/**
* Logs directory path
*/
logsDir?: string;
/**
* Temporary directory path
*/
tempDir?: string;
/**
* Email templates directory path
*/
emailTemplatesDir?: string;
};
}

770
ts/config/schemas.ts Normal file
View File

@ -0,0 +1,770 @@
import type { ValidationSchema } from './validator.js';
/**
* Base TLS configuration schema
*/
export const tlsConfigSchema: ValidationSchema = {
enabled: {
type: 'boolean',
required: false,
default: false
},
domain: {
type: 'string',
required: false
},
certPath: {
type: 'string',
required: false
},
keyPath: {
type: 'string',
required: false
},
caPath: {
type: 'string',
required: false
},
minVersion: {
type: 'string',
required: false,
enum: ['TLSv1.2', 'TLSv1.3'],
default: 'TLSv1.2'
},
autoRenew: {
type: 'boolean',
required: false,
default: false
},
rejectUnauthorized: {
type: 'boolean',
required: false,
default: true
}
};
/**
* HTTP server configuration schema
*/
export const httpServerSchema: ValidationSchema = {
enabled: {
type: 'boolean',
required: false,
default: true
},
host: {
type: 'string',
required: false,
default: '0.0.0.0'
},
port: {
type: 'number',
required: false,
default: 3000,
min: 1,
max: 65535
},
basePath: {
type: 'string',
required: false,
default: ''
},
cors: {
type: 'boolean',
required: false,
default: true
},
tls: {
type: 'object',
required: false,
schema: tlsConfigSchema
},
maxBodySize: {
type: 'number',
required: false,
default: 1024 * 1024 // 1MB
},
timeout: {
type: 'number',
required: false,
default: 30000 // 30 seconds
}
};
/**
* Rate limit configuration schema
*/
export const rateLimitSchema: ValidationSchema = {
enabled: {
type: 'boolean',
required: false,
default: true
},
maxPerPeriod: {
type: 'number',
required: false,
default: 100,
min: 1
},
periodMs: {
type: 'number',
required: false,
default: 60000, // 1 minute
min: 1000
},
perKey: {
type: 'boolean',
required: false,
default: true
},
burstTokens: {
type: 'number',
required: false,
default: 5,
min: 0
}
};
/**
* Queue configuration schema
*/
export const queueSchema: ValidationSchema = {
storageType: {
type: 'string',
required: false,
enum: ['memory', 'disk', 'redis'],
default: 'memory'
},
persistentPath: {
type: 'string',
required: false
},
redis: {
type: 'object',
required: false,
schema: {
host: {
type: 'string',
required: false,
default: 'localhost'
},
port: {
type: 'number',
required: false,
default: 6379,
min: 1,
max: 65535
},
password: {
type: 'string',
required: false
},
db: {
type: 'number',
required: false,
default: 0,
min: 0
}
}
},
maxSize: {
type: 'number',
required: false,
default: 10000,
min: 1
},
maxRetries: {
type: 'number',
required: false,
default: 3,
min: 0
},
baseRetryDelay: {
type: 'number',
required: false,
default: 1000, // 1 second
min: 1
},
maxRetryDelay: {
type: 'number',
required: false,
default: 60000, // 1 minute
min: 1
},
checkInterval: {
type: 'number',
required: false,
default: 1000, // 1 second
min: 100
},
maxParallelProcessing: {
type: 'number',
required: false,
default: 5,
min: 1
}
};
/**
* SMS service configuration schema
*/
export const smsConfigSchema: ValidationSchema = {
apiGatewayApiToken: {
type: 'string',
required: true
},
defaultSender: {
type: 'string',
required: false
},
rateLimit: {
type: 'object',
required: false,
schema: {
...rateLimitSchema,
maxPerRecipientPerDay: {
type: 'number',
required: false,
default: 10,
min: 1
}
}
},
provider: {
type: 'object',
required: false,
schema: {
type: {
type: 'string',
required: false,
enum: ['gateway', 'twilio', 'other'],
default: 'gateway'
},
config: {
type: 'object',
required: false
},
fallback: {
type: 'object',
required: false,
schema: {
enabled: {
type: 'boolean',
required: false,
default: false
},
type: {
type: 'string',
required: false,
enum: ['gateway', 'twilio', 'other']
},
config: {
type: 'object',
required: false
}
}
}
}
},
verification: {
type: 'object',
required: false,
schema: {
codeLength: {
type: 'number',
required: false,
default: 6,
min: 4,
max: 10
},
expirationSeconds: {
type: 'number',
required: false,
default: 300, // 5 minutes
min: 60
},
maxAttempts: {
type: 'number',
required: false,
default: 3,
min: 1
},
cooldownSeconds: {
type: 'number',
required: false,
default: 60, // 1 minute
min: 0
}
}
}
};
/**
* MTA configuration schema
*/
export const mtaConfigSchema: ValidationSchema = {
smtp: {
type: 'object',
required: false,
schema: {
enabled: {
type: 'boolean',
required: false,
default: true
},
port: {
type: 'number',
required: false,
default: 25,
min: 1,
max: 65535
},
hostname: {
type: 'string',
required: false,
default: 'mta.lossless.one'
},
maxSize: {
type: 'number',
required: false,
default: 10 * 1024 * 1024, // 10MB
min: 1024
}
}
},
tls: {
type: 'object',
required: false,
schema: tlsConfigSchema
},
outbound: {
type: 'object',
required: false,
schema: {
concurrency: {
type: 'number',
required: false,
default: 5,
min: 1
},
retries: {
type: 'object',
required: false,
schema: {
max: {
type: 'number',
required: false,
default: 3,
min: 0
},
delay: {
type: 'number',
required: false,
default: 300000, // 5 minutes
min: 1000
},
useBackoff: {
type: 'boolean',
required: false,
default: true
}
}
},
rateLimit: {
type: 'object',
required: false,
schema: rateLimitSchema
},
warmup: {
type: 'object',
required: false,
schema: {
enabled: {
type: 'boolean',
required: false,
default: false
},
ipAddresses: {
type: 'array',
required: false,
items: {
type: 'string'
}
},
targetDomains: {
type: 'array',
required: false,
items: {
type: 'string'
}
},
allocationPolicy: {
type: 'string',
required: false,
default: 'balanced'
},
fallbackPercentage: {
type: 'number',
required: false,
default: 50,
min: 0,
max: 100
}
}
},
reputation: {
type: 'object',
required: false,
schema: {
enabled: {
type: 'boolean',
required: false,
default: false
},
updateFrequency: {
type: 'number',
required: false,
default: 24 * 60 * 60 * 1000, // 1 day
min: 60000
},
alertThresholds: {
type: 'object',
required: false,
schema: {
minReputationScore: {
type: 'number',
required: false,
default: 70,
min: 0,
max: 100
},
maxComplaintRate: {
type: 'number',
required: false,
default: 0.1, // 0.1%
min: 0,
max: 100
}
}
}
}
}
}
},
security: {
type: 'object',
required: false,
schema: {
useDkim: {
type: 'boolean',
required: false,
default: true
},
verifyDkim: {
type: 'boolean',
required: false,
default: true
},
verifySpf: {
type: 'boolean',
required: false,
default: true
},
verifyDmarc: {
type: 'boolean',
required: false,
default: true
},
enforceDmarc: {
type: 'boolean',
required: false,
default: true
},
useTls: {
type: 'boolean',
required: false,
default: true
},
requireValidCerts: {
type: 'boolean',
required: false,
default: false
},
securityLogLevel: {
type: 'string',
required: false,
enum: ['info', 'warn', 'error'],
default: 'warn'
},
checkIPReputation: {
type: 'boolean',
required: false,
default: true
},
scanContent: {
type: 'boolean',
required: false,
default: true
},
maliciousContentAction: {
type: 'string',
required: false,
enum: ['tag', 'quarantine', 'reject'],
default: 'tag'
},
threatScoreThreshold: {
type: 'number',
required: false,
default: 50,
min: 0,
max: 100
},
rejectHighRiskIPs: {
type: 'boolean',
required: false,
default: false
}
}
},
domains: {
type: 'object',
required: false,
schema: {
local: {
type: 'array',
required: false,
items: {
type: 'string'
},
default: ['lossless.one']
},
autoCreateDnsRecords: {
type: 'boolean',
required: false,
default: true
},
dkimSelector: {
type: 'string',
required: false,
default: 'mta'
}
}
},
queue: {
type: 'object',
required: false,
schema: queueSchema
}
};
/**
* Email service configuration schema
*/
export const emailConfigSchema: ValidationSchema = {
useMta: {
type: 'boolean',
required: false,
default: true
},
mtaConfig: {
type: 'object',
required: false,
schema: mtaConfigSchema
},
templateConfig: {
type: 'object',
required: false,
schema: {
from: {
type: 'string',
required: false,
default: 'no-reply@lossless.one'
},
replyTo: {
type: 'string',
required: false,
default: 'support@lossless.one'
},
footerHtml: {
type: 'string',
required: false
},
footerText: {
type: 'string',
required: false
}
}
},
loadTemplatesFromDir: {
type: 'boolean',
required: false,
default: true
},
templatesDir: {
type: 'string',
required: false
}
};
/**
* Database configuration schema
*/
export const databaseConfigSchema: ValidationSchema = {
connectionString: {
type: 'string',
required: false
},
host: {
type: 'string',
required: false,
default: 'localhost'
},
port: {
type: 'number',
required: false,
default: 5432,
min: 1,
max: 65535
},
database: {
type: 'string',
required: false
},
username: {
type: 'string',
required: false
},
password: {
type: 'string',
required: false
},
ssl: {
type: 'boolean',
required: false,
default: false
},
pool: {
type: 'object',
required: false,
schema: {
min: {
type: 'number',
required: false,
default: 2,
min: 1
},
max: {
type: 'number',
required: false,
default: 10,
min: 1
},
idleTimeoutMillis: {
type: 'number',
required: false,
default: 30000,
min: 1000
}
}
}
};
/**
* Platform service configuration schema
*/
export const platformConfigSchema: ValidationSchema = {
id: {
type: 'string',
required: false,
default: 'platform-service-config'
},
version: {
type: 'string',
required: false,
default: '1.0.0'
},
environment: {
type: 'string',
required: false,
enum: ['development', 'test', 'staging', 'production'],
default: 'production'
},
name: {
type: 'string',
required: false,
default: 'PlatformService'
},
enabled: {
type: 'boolean',
required: false,
default: true
},
logging: {
type: 'object',
required: false,
schema: {
level: {
type: 'string',
required: false,
enum: ['error', 'warn', 'info', 'debug'],
default: 'info'
},
structured: {
type: 'boolean',
required: false,
default: true
},
correlationTracking: {
type: 'boolean',
required: false,
default: true
}
}
},
server: {
type: 'object',
required: false,
schema: httpServerSchema
},
database: {
type: 'object',
required: false,
schema: databaseConfigSchema
},
email: {
type: 'object',
required: false,
schema: emailConfigSchema
},
sms: {
type: 'object',
required: false,
schema: smsConfigSchema
},
paths: {
type: 'object',
required: false,
schema: {
dataDir: {
type: 'string',
required: false,
default: 'data'
},
logsDir: {
type: 'string',
required: false,
default: 'logs'
},
tempDir: {
type: 'string',
required: false,
default: 'temp'
},
emailTemplatesDir: {
type: 'string',
required: false,
default: 'templates/email'
}
}
}
};

86
ts/config/sms.config.ts Normal file
View File

@ -0,0 +1,86 @@
import type { IBaseConfig, IRateLimitConfig } from './base.config.js';
/**
* SMS service configuration
*/
export interface ISmsConfig extends IBaseConfig {
/**
* API token for the gateway service
*/
apiGatewayApiToken: string;
/**
* Default sender ID or phone number
*/
defaultSender?: string;
/**
* SMS rate limiting
*/
rateLimit?: IRateLimitConfig & {
/**
* Maximum messages per recipient per day
*/
maxPerRecipientPerDay?: number;
};
/**
* SMS provider configuration
*/
provider?: {
/**
* Provider type
*/
type?: 'gateway' | 'twilio' | 'other';
/**
* Provider-specific configuration
*/
config?: Record<string, any>;
/**
* Fallback provider configuration
*/
fallback?: {
/**
* Whether to use fallback provider
*/
enabled?: boolean;
/**
* Provider type
*/
type?: 'gateway' | 'twilio' | 'other';
/**
* Provider-specific configuration
*/
config?: Record<string, any>;
};
};
/**
* Verification code settings
*/
verification?: {
/**
* Code length
*/
codeLength?: number;
/**
* Code expiration time in seconds
*/
expirationSeconds?: number;
/**
* Maximum number of attempts
*/
maxAttempts?: number;
/**
* Cooldown period in seconds
*/
cooldownSeconds?: number;
};
}

326
ts/config/validator.ts Normal file
View File

@ -0,0 +1,326 @@
import * as plugins from '../plugins.js';
import { ValidationError } from '../errors/base.errors.js';
import type { IBaseConfig } from './base.config.js';
/**
* Validation result
*/
export interface IValidationResult {
/**
* Whether the validation passed
*/
valid: boolean;
/**
* Validation errors if any
*/
errors?: string[];
/**
* Validated configuration (may include defaults)
*/
config?: any;
}
/**
* Validation schema types
*/
export type ValidationSchema = Record<string, {
/**
* Type of the value
*/
type: 'string' | 'number' | 'boolean' | 'object' | 'array';
/**
* Whether the field is required
*/
required?: boolean;
/**
* Default value if not specified
*/
default?: any;
/**
* Minimum value (for numbers)
*/
min?: number;
/**
* Maximum value (for numbers)
*/
max?: number;
/**
* Minimum length (for strings or arrays)
*/
minLength?: number;
/**
* Maximum length (for strings or arrays)
*/
maxLength?: number;
/**
* Pattern to match (for strings)
*/
pattern?: RegExp;
/**
* Allowed values (for strings, numbers)
*/
enum?: any[];
/**
* Nested schema (for objects)
*/
schema?: ValidationSchema;
/**
* Item schema (for arrays)
*/
items?: {
type: 'string' | 'number' | 'boolean' | 'object';
schema?: ValidationSchema;
};
/**
* Custom validation function
*/
validate?: (value: any) => boolean | string;
}>;
/**
* Configuration validator
* Validates configuration objects against schemas and provides default values
*/
export class ConfigValidator {
/**
* Basic schema for IBaseConfig
*/
private static baseConfigSchema: ValidationSchema = {
id: {
type: 'string',
required: false
},
version: {
type: 'string',
required: false
},
environment: {
type: 'string',
required: false,
enum: ['development', 'test', 'staging', 'production'],
default: 'production'
},
name: {
type: 'string',
required: false
},
enabled: {
type: 'boolean',
required: false,
default: true
},
logging: {
type: 'object',
required: false,
schema: {
level: {
type: 'string',
required: false,
enum: ['error', 'warn', 'info', 'debug'],
default: 'info'
},
structured: {
type: 'boolean',
required: false,
default: true
},
correlationTracking: {
type: 'boolean',
required: false,
default: true
}
}
}
};
/**
* Validate a configuration object against a schema
*
* @param config Configuration object to validate
* @param schema Validation schema
* @returns Validation result
*/
public static validate<T>(config: T, schema: ValidationSchema): IValidationResult {
const errors: string[] = [];
const validatedConfig = { ...config };
// Validate each field against the schema
for (const [key, rules] of Object.entries(schema)) {
const value = config[key];
// Check if required
if (rules.required && (value === undefined || value === null)) {
errors.push(`${key} is required`);
continue;
}
// If not present and not required, apply default if available
if ((value === undefined || value === null)) {
if (rules.default !== undefined) {
validatedConfig[key] = rules.default;
}
continue;
}
// Type validation
if (value !== undefined && value !== null) {
const valueType = Array.isArray(value) ? 'array' : typeof value;
if (valueType !== rules.type) {
errors.push(`${key} must be of type ${rules.type}, got ${valueType}`);
continue;
}
// Type-specific validations
switch (rules.type) {
case 'number':
if (rules.min !== undefined && value < rules.min) {
errors.push(`${key} must be at least ${rules.min}`);
}
if (rules.max !== undefined && value > rules.max) {
errors.push(`${key} must be at most ${rules.max}`);
}
break;
case 'string':
if (rules.minLength !== undefined && value.length < rules.minLength) {
errors.push(`${key} must be at least ${rules.minLength} characters`);
}
if (rules.maxLength !== undefined && value.length > rules.maxLength) {
errors.push(`${key} must be at most ${rules.maxLength} characters`);
}
if (rules.pattern && !rules.pattern.test(value)) {
errors.push(`${key} must match pattern ${rules.pattern}`);
}
break;
case 'array':
if (rules.minLength !== undefined && value.length < rules.minLength) {
errors.push(`${key} must have at least ${rules.minLength} items`);
}
if (rules.maxLength !== undefined && value.length > rules.maxLength) {
errors.push(`${key} must have at most ${rules.maxLength} items`);
}
if (rules.items && value.length > 0) {
for (let i = 0; i < value.length; i++) {
const itemType = Array.isArray(value[i]) ? 'array' : typeof value[i];
if (itemType !== rules.items.type) {
errors.push(`${key}[${i}] must be of type ${rules.items.type}, got ${itemType}`);
} else if (rules.items.schema && itemType === 'object') {
const itemResult = this.validate(value[i], rules.items.schema);
if (!itemResult.valid) {
errors.push(...itemResult.errors.map(err => `${key}[${i}].${err}`));
}
}
}
}
break;
case 'object':
if (rules.schema) {
const nestedResult = this.validate(value, rules.schema);
if (!nestedResult.valid) {
errors.push(...nestedResult.errors.map(err => `${key}.${err}`));
}
validatedConfig[key] = nestedResult.config;
}
break;
}
// Enum validation
if (rules.enum && !rules.enum.includes(value)) {
errors.push(`${key} must be one of [${rules.enum.join(', ')}]`);
}
// Custom validation
if (rules.validate) {
const result = rules.validate(value);
if (result !== true) {
errors.push(typeof result === 'string' ? result : `${key} failed custom validation`);
}
}
}
}
return {
valid: errors.length === 0,
errors: errors.length > 0 ? errors : undefined,
config: validatedConfig
};
}
/**
* Validate base configuration
*
* @param config Base configuration
* @returns Validation result for base configuration
*/
public static validateBaseConfig(config: IBaseConfig): IValidationResult {
return this.validate(config, this.baseConfigSchema);
}
/**
* Apply defaults to a configuration object based on a schema
*
* @param config Configuration object to apply defaults to
* @param schema Validation schema with defaults
* @returns Configuration with defaults applied
*/
public static applyDefaults<T>(config: T, schema: ValidationSchema): T {
const result = { ...config };
for (const [key, rules] of Object.entries(schema)) {
if (result[key] === undefined && rules.default !== undefined) {
result[key] = rules.default;
}
// Apply defaults to nested objects
if (result[key] && rules.type === 'object' && rules.schema) {
result[key] = this.applyDefaults(result[key], rules.schema);
}
// Apply defaults to array items
if (result[key] && rules.type === 'array' && rules.items && rules.items.schema) {
result[key] = result[key].map(item =>
typeof item === 'object' ? this.applyDefaults(item, rules.items.schema) : item
);
}
}
return result;
}
/**
* Throw a validation error if the configuration is invalid
*
* @param config Configuration to validate
* @param schema Validation schema
* @returns Validated configuration with defaults
* @throws ValidationError if validation fails
*/
public static validateOrThrow<T>(config: T, schema: ValidationSchema): T {
const result = this.validate(config, schema);
if (!result.valid) {
throw new ValidationError(
`Configuration validation failed: ${result.errors.join(', ')}`,
'CONFIG_VALIDATION_ERROR',
{ data: { errors: result.errors } }
);
}
return result.config;
}
}

View File

@ -1,20 +0,0 @@
// This file is maintained for backward compatibility only
// New code should use qenv directly
import * as plugins from '../plugins.js';
import type DcRouter from './classes.dcrouter.js';
export class SzDcRouterConnector {
public qenv: plugins.qenv.Qenv;
public dcRouterRef: DcRouter;
constructor(dcRouterRef: DcRouter) {
this.dcRouterRef = dcRouterRef;
// Initialize qenv directly
this.qenv = new plugins.qenv.Qenv('./', '.nogit/');
}
public async getEnvVarOnDemand(varName: string): Promise<string> {
return this.qenv.getEnvVarOnDemand(varName) || '';
}
}

View File

@ -1,431 +0,0 @@
import * as plugins from '../plugins.js';
/**
* Domain group configuration for applying consistent rules across related domains
*/
export interface IDomainGroup {
/** Unique identifier for the domain group */
id: string;
/** Human-readable name for the domain group */
name: string;
/** List of domains in this group */
domains: string[];
/** Priority for this domain group (higher takes precedence) */
priority?: number;
/** Description of this domain group */
description?: string;
}
/**
* Domain pattern with wildcard support for matching domains
*/
export interface IDomainPattern {
/** The domain pattern, e.g. "example.com" or "*.example.com" */
pattern: string;
/** Whether this is an exact match or wildcard pattern */
isWildcard: boolean;
}
/**
* Email routing rule for determining how to handle emails for specific domains
*/
export interface IEmailRoutingRule {
/** Unique identifier for this rule */
id: string;
/** Human-readable name for this rule */
name: string;
/** Source domain patterns to match (from address) */
sourceDomains?: IDomainPattern[];
/** Destination domain patterns to match (to address) */
destinationDomains?: IDomainPattern[];
/** Domain groups this rule applies to */
domainGroups?: string[];
/** Priority of this rule (higher takes precedence) */
priority: number;
/** Action to take when rule matches */
action: 'route' | 'block' | 'tag' | 'filter';
/** Target server for routing */
targetServer?: string;
/** Target port for routing */
targetPort?: number;
/** Whether to use TLS when routing */
useTls?: boolean;
/** Authentication details for routing */
auth?: {
/** Username for authentication */
username?: string;
/** Password for authentication */
password?: string;
/** Authentication type */
type?: 'PLAIN' | 'LOGIN' | 'OAUTH2';
};
/** Headers to add or modify when rule matches */
headers?: {
/** Header name */
name: string;
/** Header value */
value: string;
/** Whether to append to existing header or replace */
append?: boolean;
}[];
/** Whether this rule is enabled */
enabled: boolean;
}
/**
* Configuration for email domain-based routing
*/
export interface IEmailDomainRoutingConfig {
/** Whether domain-based routing is enabled */
enabled: boolean;
/** Routing rules list */
rules: IEmailRoutingRule[];
/** Domain groups for organization */
domainGroups?: IDomainGroup[];
/** Default target server for unmatched domains */
defaultTargetServer?: string;
/** Default target port for unmatched domains */
defaultTargetPort?: number;
/** Whether to use TLS for the default route */
defaultUseTls?: boolean;
}
/**
* Class for managing domain-based email routing
*/
export class EmailDomainRouter {
/** Configuration for domain-based routing */
private config: IEmailDomainRoutingConfig;
/** Domain groups indexed by ID */
private domainGroups: Map<string, IDomainGroup> = new Map();
/** Sorted rules cache for faster processing */
private sortedRules: IEmailRoutingRule[] = [];
/** Whether the rules need to be re-sorted */
private rulesSortNeeded = true;
/**
* Create a new EmailDomainRouter
* @param config Configuration for domain-based routing
*/
constructor(config: IEmailDomainRoutingConfig) {
this.config = config;
this.initialize();
}
/**
* Initialize the domain router
*/
private initialize(): void {
// Return early if routing is not enabled
if (!this.config.enabled) {
return;
}
// Initialize domain groups
if (this.config.domainGroups) {
for (const group of this.config.domainGroups) {
this.domainGroups.set(group.id, group);
}
}
// Sort rules by priority
this.sortRules();
}
/**
* Sort rules by priority (higher first)
*/
private sortRules(): void {
if (!this.config.rules || !this.config.enabled) {
this.sortedRules = [];
this.rulesSortNeeded = false;
return;
}
this.sortedRules = [...this.config.rules]
.filter(rule => rule.enabled)
.sort((a, b) => b.priority - a.priority);
this.rulesSortNeeded = false;
}
/**
* Add a new routing rule
* @param rule The routing rule to add
*/
public addRule(rule: IEmailRoutingRule): void {
if (!this.config.rules) {
this.config.rules = [];
}
// Check if rule already exists
const existingIndex = this.config.rules.findIndex(r => r.id === rule.id);
if (existingIndex >= 0) {
// Update existing rule
this.config.rules[existingIndex] = rule;
} else {
// Add new rule
this.config.rules.push(rule);
}
this.rulesSortNeeded = true;
}
/**
* Remove a routing rule by ID
* @param ruleId ID of the rule to remove
* @returns Whether the rule was removed
*/
public removeRule(ruleId: string): boolean {
if (!this.config.rules) {
return false;
}
const initialLength = this.config.rules.length;
this.config.rules = this.config.rules.filter(rule => rule.id !== ruleId);
if (initialLength !== this.config.rules.length) {
this.rulesSortNeeded = true;
return true;
}
return false;
}
/**
* Add a domain group
* @param group The domain group to add
*/
public addDomainGroup(group: IDomainGroup): void {
if (!this.config.domainGroups) {
this.config.domainGroups = [];
}
// Check if group already exists
const existingIndex = this.config.domainGroups.findIndex(g => g.id === group.id);
if (existingIndex >= 0) {
// Update existing group
this.config.domainGroups[existingIndex] = group;
} else {
// Add new group
this.config.domainGroups.push(group);
}
// Update domain groups map
this.domainGroups.set(group.id, group);
}
/**
* Remove a domain group by ID
* @param groupId ID of the group to remove
* @returns Whether the group was removed
*/
public removeDomainGroup(groupId: string): boolean {
if (!this.config.domainGroups) {
return false;
}
const initialLength = this.config.domainGroups.length;
this.config.domainGroups = this.config.domainGroups.filter(group => group.id !== groupId);
if (initialLength !== this.config.domainGroups.length) {
this.domainGroups.delete(groupId);
return true;
}
return false;
}
/**
* Determine routing for an email
* @param fromDomain The sender domain
* @param toDomain The recipient domain
* @returns Routing decision or null if no matching rule
*/
public getRoutingForEmail(fromDomain: string, toDomain: string): {
targetServer: string;
targetPort: number;
useTls: boolean;
auth?: {
username?: string;
password?: string;
type?: 'PLAIN' | 'LOGIN' | 'OAUTH2';
};
headers?: {
name: string;
value: string;
append?: boolean;
}[];
} | null {
// Return default routing if routing is not enabled
if (!this.config.enabled) {
return this.getDefaultRouting();
}
// Sort rules if needed
if (this.rulesSortNeeded) {
this.sortRules();
}
// Normalize domains
fromDomain = fromDomain.toLowerCase();
toDomain = toDomain.toLowerCase();
// Check each rule in priority order
for (const rule of this.sortedRules) {
if (!rule.enabled) continue;
// Check if rule applies to this email
if (this.ruleMatchesEmail(rule, fromDomain, toDomain)) {
// Handle different actions
switch (rule.action) {
case 'route':
// Return routing information
return {
targetServer: rule.targetServer || this.config.defaultTargetServer || 'localhost',
targetPort: rule.targetPort || this.config.defaultTargetPort || 25,
useTls: rule.useTls ?? this.config.defaultUseTls ?? false,
auth: rule.auth,
headers: rule.headers
};
case 'block':
// Return null to indicate email should be blocked
return null;
case 'tag':
case 'filter':
// For tagging/filtering, we need to apply headers but continue checking rules
// This is simplified for now, in a real implementation we'd aggregate headers
continue;
}
}
}
// No rule matched, use default routing
return this.getDefaultRouting();
}
/**
* Check if a rule matches an email
* @param rule The routing rule to check
* @param fromDomain The sender domain
* @param toDomain The recipient domain
* @returns Whether the rule matches the email
*/
private ruleMatchesEmail(rule: IEmailRoutingRule, fromDomain: string, toDomain: string): boolean {
// Check source domains
if (rule.sourceDomains && rule.sourceDomains.length > 0) {
const matchesSourceDomain = rule.sourceDomains.some(
pattern => this.domainMatchesPattern(fromDomain, pattern)
);
if (!matchesSourceDomain) {
return false;
}
}
// Check destination domains
if (rule.destinationDomains && rule.destinationDomains.length > 0) {
const matchesDestinationDomain = rule.destinationDomains.some(
pattern => this.domainMatchesPattern(toDomain, pattern)
);
if (!matchesDestinationDomain) {
return false;
}
}
// Check domain groups
if (rule.domainGroups && rule.domainGroups.length > 0) {
// Check if either domain is in any of the specified groups
const domainsInGroups = rule.domainGroups
.map(groupId => this.domainGroups.get(groupId))
.filter(Boolean)
.some(group =>
group.domains.includes(fromDomain) ||
group.domains.includes(toDomain)
);
if (!domainsInGroups) {
return false;
}
}
// If we got here, all checks passed
return true;
}
/**
* Check if a domain matches a pattern
* @param domain The domain to check
* @param pattern The pattern to match against
* @returns Whether the domain matches the pattern
*/
private domainMatchesPattern(domain: string, pattern: IDomainPattern): boolean {
domain = domain.toLowerCase();
const patternStr = pattern.pattern.toLowerCase();
// Exact match
if (!pattern.isWildcard) {
return domain === patternStr;
}
// Wildcard match (*.example.com)
if (patternStr.startsWith('*.')) {
const suffix = patternStr.substring(2);
return domain.endsWith(suffix) && domain.length > suffix.length;
}
// Invalid pattern
return false;
}
/**
* Get default routing information
* @returns Default routing or null if no default configured
*/
private getDefaultRouting(): {
targetServer: string;
targetPort: number;
useTls: boolean;
} | null {
if (!this.config.defaultTargetServer) {
return null;
}
return {
targetServer: this.config.defaultTargetServer,
targetPort: this.config.defaultTargetPort || 25,
useTls: this.config.defaultUseTls || false
};
}
/**
* Get the current configuration
* @returns Current domain routing configuration
*/
public getConfig(): IEmailDomainRoutingConfig {
return this.config;
}
/**
* Update the configuration
* @param config New domain routing configuration
*/
public updateConfig(config: IEmailDomainRoutingConfig): void {
this.config = config;
this.rulesSortNeeded = true;
this.initialize();
}
/**
* Enable domain routing
*/
public enable(): void {
this.config.enabled = true;
}
/**
* Disable domain routing
*/
public disable(): void {
this.config.enabled = false;
}
}

View File

@ -1,14 +0,0 @@
// Core DcRouter components
export * from './classes.dcrouter.js';
export * from './classes.smtp.portconfig.js';
export * from './classes.email.domainrouter.js';
// Unified Email Configuration
export * from './classes.email.config.js';
export * from './classes.domain.router.js';
export * from './classes.unified.email.server.js';
// Shared Infrastructure Components
export * from './classes.delivery.queue.js';
export * from './classes.delivery.system.js';
export * from './classes.rate.limiter.js';

View File

@ -214,8 +214,13 @@ export class SenderReputationMonitor {
if (this.isInitialized) return;
try {
// Load existing reputation data
this.loadReputationData();
// Only load data if not running in a test environment
const isTestEnvironment = process.env.NODE_ENV === 'test' || !!process.env.JEST_WORKER_ID;
if (!isTestEnvironment) {
// Load existing reputation data
this.loadReputationData();
}
// Initialize data for any new domains
for (const domain of this.config.domains) {
@ -224,8 +229,8 @@ export class SenderReputationMonitor {
}
}
// Schedule updates if enabled
if (this.config.enabled) {
// Schedule updates if enabled and not in test environment
if (this.config.enabled && !isTestEnvironment) {
this.scheduleUpdates();
}
@ -400,7 +405,9 @@ export class SenderReputationMonitor {
* @param metrics Metrics to update
*/
private async checkBlocklistStatus(domain: string, metrics: IDomainReputationMetrics): Promise<void> {
if (!this.config.dataSources.spamLists?.length) {
// Skip DNS lookups in test environment
const isTestEnvironment = process.env.NODE_ENV === 'test' || !!process.env.JEST_WORKER_ID;
if (isTestEnvironment || !this.config.dataSources.spamLists?.length) {
return;
}
@ -967,7 +974,9 @@ export class SenderReputationMonitor {
metrics.lastUpdated = new Date();
// Save data periodically (not after every event to avoid excessive I/O)
if (Math.random() < 0.01) { // ~1% chance to save on each event
// Skip in test environment
const isTestEnvironment = process.env.NODE_ENV === 'test' || !!process.env.JEST_WORKER_ID;
if (!isTestEnvironment && Math.random() < 0.01) { // ~1% chance to save on each event
this.saveReputationData();
}
}
@ -1055,6 +1064,12 @@ export class SenderReputationMonitor {
* Load reputation data from storage
*/
private loadReputationData(): void {
// Skip loading in test environment to prevent file system race conditions
const isTestEnvironment = process.env.NODE_ENV === 'test' || !!process.env.JEST_WORKER_ID;
if (isTestEnvironment) {
return;
}
try {
const reputationDir = plugins.path.join(paths.dataDir, 'reputation');
plugins.smartfile.fs.ensureDirSync(reputationDir);
@ -1094,6 +1109,12 @@ export class SenderReputationMonitor {
* Save reputation data to storage
*/
private saveReputationData(): void {
// Skip saving in test environment to prevent file system race conditions
const isTestEnvironment = process.env.NODE_ENV === 'test' || !!process.env.JEST_WORKER_ID;
if (isTestEnvironment) {
return;
}
try {
const reputationDir = plugins.path.join(paths.dataDir, 'reputation');
plugins.smartfile.fs.ensureDirSync(reputationDir);

View File

@ -1,210 +0,0 @@
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import { MtaConnector } from './classes.connector.mta.js';
import { RuleManager } from './classes.rulemanager.js';
import { ApiManager } from './classes.apimanager.js';
import { TemplateManager } from './classes.templatemanager.js';
import { EmailValidator } from './classes.emailvalidator.js';
import { BounceManager } from './classes.bouncemanager.js';
import { logger } from '../logger.js';
import type { SzPlatformService } from '../platformservice.js';
// Import MTA service
import { MtaService, type IMtaConfig } from '../mta/index.js';
export interface IEmailConstructorOptions {
useMta?: boolean;
mtaConfig?: IMtaConfig;
templateConfig?: {
from?: string;
replyTo?: string;
footerHtml?: string;
footerText?: string;
};
loadTemplatesFromDir?: boolean;
}
/**
* Email service with support for both Mailgun and local MTA
*/
export class EmailService {
public platformServiceRef: SzPlatformService;
// typedrouter
public typedrouter = new plugins.typedrequest.TypedRouter();
// connectors
public mtaConnector: MtaConnector;
public qenv = new plugins.qenv.Qenv('./', '.nogit/');
// MTA service
public mtaService: MtaService;
// services
public apiManager: ApiManager;
public ruleManager: RuleManager;
public templateManager: TemplateManager;
public emailValidator: EmailValidator;
public bounceManager: BounceManager;
// configuration
private config: IEmailConstructorOptions;
constructor(platformServiceRefArg: SzPlatformService, options: IEmailConstructorOptions = {}) {
this.platformServiceRef = platformServiceRefArg;
this.platformServiceRef.typedrouter.addTypedRouter(this.typedrouter);
// Set default options
this.config = {
useMta: options.useMta ?? true,
mtaConfig: options.mtaConfig || {},
templateConfig: options.templateConfig || {},
loadTemplatesFromDir: options.loadTemplatesFromDir ?? true
};
// Initialize validator
this.emailValidator = new EmailValidator();
// Initialize bounce manager
this.bounceManager = new BounceManager();
// Initialize template manager
this.templateManager = new TemplateManager(this.config.templateConfig);
if (this.config.useMta) {
// Initialize MTA service
this.mtaService = new MtaService(platformServiceRefArg, this.config.mtaConfig);
// Initialize MTA connector
this.mtaConnector = new MtaConnector(this);
}
// Initialize API manager and rule manager
this.apiManager = new ApiManager(this);
this.ruleManager = new RuleManager(this);
// Set up MTA SMTP server webhook if using MTA
if (this.config.useMta) {
// The MTA SMTP server will handle incoming emails directly
// through its SMTP protocol. No additional webhook needed.
}
}
/**
* Start the email service
*/
public async start() {
// Initialize rule manager
await this.ruleManager.init();
// Load email templates if configured
if (this.config.loadTemplatesFromDir) {
try {
await this.templateManager.loadTemplatesFromDirectory(paths.emailTemplatesDir);
} catch (error) {
logger.log('error', `Failed to load email templates: ${error.message}`);
}
}
// Start MTA service if enabled
if (this.config.useMta && this.mtaService) {
await this.mtaService.start();
logger.log('success', 'Started MTA service');
}
logger.log('success', `Started email service`);
}
/**
* Stop the email service
*/
public async stop() {
// Stop MTA service if it's running
if (this.config.useMta && this.mtaService) {
await this.mtaService.stop();
logger.log('info', 'Stopped MTA service');
}
logger.log('info', 'Stopped email service');
}
/**
* Send an email using the configured provider (Mailgun or MTA)
* @param email The email to send
* @param to Recipient(s)
* @param options Additional options
*/
public async sendEmail(
email: plugins.smartmail.Smartmail<any>,
to: string | string[],
options: any = {}
): Promise<string> {
// Determine which connector to use
if (this.config.useMta && this.mtaConnector) {
return this.mtaConnector.sendEmail(email, to, options);
} else {
throw new Error('No email provider configured');
}
}
/**
* Send an email using a template
* @param templateId The template ID
* @param to Recipient email(s)
* @param context The template context data
* @param options Additional options
*/
public async sendTemplateEmail(
templateId: string,
to: string | string[],
context: any = {},
options: any = {}
): Promise<string> {
try {
// Get email from template
const smartmail = await this.templateManager.prepareEmail(templateId, context);
// Send the email
return this.sendEmail(smartmail, to, options);
} catch (error) {
logger.log('error', `Failed to send template email: ${error.message}`, {
templateId,
to,
error: error.message
});
throw error;
}
}
/**
* Validate an email address
* @param email The email address to validate
* @param options Validation options
* @returns Validation result
*/
public async validateEmail(
email: string,
options: {
checkMx?: boolean;
checkDisposable?: boolean;
checkRole?: boolean;
} = {}
): Promise<any> {
return this.emailValidator.validate(email, options);
}
/**
* Get email service statistics
*/
public getStats() {
const stats: any = {
activeProviders: []
};
if (this.config.useMta) {
stats.activeProviders.push('mta');
stats.mta = this.mtaService.getStats();
}
return stats;
}
}

View File

@ -1,19 +0,0 @@
import { EmailService } from './classes.emailservice.js';
import { BounceManager, BounceType, BounceCategory } from './classes.bouncemanager.js';
import { EmailValidator } from './classes.emailvalidator.js';
import { TemplateManager } from './classes.templatemanager.js';
import { RuleManager } from './classes.rulemanager.js';
import { ApiManager } from './classes.apimanager.js';
import { MtaConnector } from './classes.connector.mta.js';
export {
EmailService as Email,
BounceManager,
BounceType,
BounceCategory,
EmailValidator,
TemplateManager,
RuleManager,
ApiManager,
MtaConnector
};

437
ts/errors/base.errors.ts Normal file
View File

@ -0,0 +1,437 @@
import { ErrorSeverity, ErrorCategory, ErrorRecoverability } from './error.codes.js';
import { logger } from '../logger.js';
// Import TLogLevel from plugins
import type { TLogLevel } from '../plugins.js';
/**
* Context information added to structured errors
*/
export interface IErrorContext {
/** Component or service where the error occurred */
component?: string;
/** Operation that was being performed */
operation?: string;
/** Unique request ID if available */
requestId?: string;
/** Error occurred at timestamp */
timestamp?: number;
/** User-visible message (safe to display to end-users) */
userMessage?: string;
/** Additional structured data for debugging */
data?: Record<string, any>;
/** Related entity IDs if applicable */
entity?: {
type: string;
id: string | number;
};
/** Stack trace (if enabled in configuration) */
stack?: string;
/** Retry information if applicable */
retry?: {
/** Maximum number of retries allowed */
maxRetries?: number;
/** Current retry count */
currentRetry?: number;
/** Next retry timestamp */
nextRetryAt?: number;
/** Delay between retries (in ms) */
retryDelay?: number;
};
}
/**
* Base class for all errors in the Platform Service
* Adds structured error information, logging, and error tracking
*/
export class PlatformError extends Error {
/** Error code identifying the specific error type */
public readonly code: string;
/** Error severity level */
public readonly severity: ErrorSeverity;
/** Error category for grouping related errors */
public readonly category: ErrorCategory;
/** Whether the error can be recovered from automatically */
public readonly recoverability: ErrorRecoverability;
/** Additional context information */
public readonly context: IErrorContext;
/**
* Creates a new PlatformError
*
* @param message Error message
* @param code Error code from error.codes.ts
* @param severity Error severity level
* @param category Error category
* @param recoverability Error recoverability indication
* @param context Additional context information
*/
constructor(
message: string,
code: string,
severity: ErrorSeverity = ErrorSeverity.MEDIUM,
category: ErrorCategory = ErrorCategory.OTHER,
recoverability: ErrorRecoverability = ErrorRecoverability.NON_RECOVERABLE,
context: IErrorContext = {}
) {
super(message);
// Set error metadata
this.name = this.constructor.name;
this.code = code;
this.severity = severity;
this.category = category;
this.recoverability = recoverability;
// Add timestamp if not provided
this.context = {
...context,
timestamp: context.timestamp || Date.now(),
};
// Capture stack trace
Error.captureStackTrace(this, this.constructor);
// Log the error automatically unless explicitly disabled
if (!context.data?.skipLogging) {
this.logError();
}
}
/**
* Logs the error using the platform logger
*/
private logError(): void {
const logLevel = this.getLogLevelFromSeverity() as TLogLevel;
// Construct structured log entry
const logData = {
error_code: this.code,
error_name: this.name,
severity: this.severity,
category: this.category,
recoverability: this.recoverability,
...this.context
};
// Log with appropriate level
logger.log(logLevel, this.message, logData);
}
/**
* Maps severity levels to log levels
*/
private getLogLevelFromSeverity(): string {
switch (this.severity) {
case ErrorSeverity.CRITICAL:
case ErrorSeverity.HIGH:
return 'error';
case ErrorSeverity.MEDIUM:
return 'warn';
case ErrorSeverity.LOW:
return 'info';
case ErrorSeverity.INFO:
return 'debug';
default:
return 'error';
}
}
/**
* Returns a JSON representation of the error
*/
public toJSON(): Record<string, any> {
return {
name: this.name,
message: this.message,
code: this.code,
severity: this.severity,
category: this.category,
recoverability: this.recoverability,
context: this.context,
stack: process.env.NODE_ENV !== 'production' ? this.stack : undefined
};
}
/**
* Creates an instance with retry information
*
* @param maxRetries Maximum number of retries
* @param currentRetry Current retry count
* @param retryDelay Delay between retries in ms
*/
public withRetry(
maxRetries: number,
currentRetry: number = 0,
retryDelay: number = 1000
): PlatformError {
const nextRetryAt = Date.now() + retryDelay;
// Create a new instance with the same parameters but updated context
return new (this.constructor as typeof PlatformError)(
this.message,
this.code,
this.severity,
this.category,
// If we can retry, the error is at least maybe recoverable
currentRetry < maxRetries
? ErrorRecoverability.MAYBE_RECOVERABLE
: this.recoverability,
{
...this.context,
retry: {
maxRetries,
currentRetry,
nextRetryAt,
retryDelay
}
}
);
}
/**
* Checks if the error should be retried based on retry information
*/
public shouldRetry(): boolean {
const { retry } = this.context;
if (!retry) return false;
return retry.currentRetry < retry.maxRetries;
}
/**
* Returns a user-friendly message that is safe to display to end users
*/
public getUserMessage(): string {
return this.context.userMessage || 'An unexpected error occurred.';
}
}
/**
* Error class for validation errors
*/
export class ValidationError extends PlatformError {
/**
* Creates a new validation error
*
* @param message Error message
* @param code Error code
* @param context Additional context
*/
constructor(
message: string,
code: string,
context: IErrorContext = {}
) {
super(
message,
code,
ErrorSeverity.LOW,
ErrorCategory.VALIDATION,
ErrorRecoverability.NON_RECOVERABLE,
context
);
}
}
/**
* Error class for configuration errors
*/
export class ConfigurationError extends PlatformError {
/**
* Creates a new configuration error
*
* @param message Error message
* @param code Error code
* @param context Additional context
*/
constructor(
message: string,
code: string,
context: IErrorContext = {}
) {
super(
message,
code,
ErrorSeverity.MEDIUM,
ErrorCategory.CONFIGURATION,
ErrorRecoverability.NON_RECOVERABLE,
context
);
}
}
/**
* Error class for network-related errors
*/
export class NetworkError extends PlatformError {
/**
* Creates a new network error
*
* @param message Error message
* @param code Error code
* @param context Additional context
*/
constructor(
message: string,
code: string,
context: IErrorContext = {}
) {
super(
message,
code,
ErrorSeverity.MEDIUM,
ErrorCategory.CONNECTIVITY,
ErrorRecoverability.MAYBE_RECOVERABLE,
context
);
}
}
/**
* Error class for resource availability errors (rate limits, quotas)
*/
export class ResourceError extends PlatformError {
/**
* Creates a new resource error
*
* @param message Error message
* @param code Error code
* @param context Additional context
*/
constructor(
message: string,
code: string,
context: IErrorContext = {}
) {
super(
message,
code,
ErrorSeverity.MEDIUM,
ErrorCategory.RESOURCE,
ErrorRecoverability.MAYBE_RECOVERABLE,
context
);
}
}
/**
* Error class for authentication/authorization errors
*/
export class AuthenticationError extends PlatformError {
/**
* Creates a new authentication error
*
* @param message Error message
* @param code Error code
* @param context Additional context
*/
constructor(
message: string,
code: string,
context: IErrorContext = {}
) {
super(
message,
code,
ErrorSeverity.HIGH,
ErrorCategory.AUTHENTICATION,
ErrorRecoverability.NON_RECOVERABLE,
context
);
}
}
/**
* Error class for operation errors (API calls, processing)
*/
export class OperationError extends PlatformError {
/**
* Creates a new operation error
*
* @param message Error message
* @param code Error code
* @param context Additional context
*/
constructor(
message: string,
code: string,
context: IErrorContext = {}
) {
super(
message,
code,
ErrorSeverity.MEDIUM,
ErrorCategory.OPERATION,
ErrorRecoverability.MAYBE_RECOVERABLE,
context
);
}
}
/**
* Error class for critical system errors
*/
export class SystemError extends PlatformError {
/**
* Creates a new system error
*
* @param message Error message
* @param code Error code
* @param context Additional context
*/
constructor(
message: string,
code: string,
context: IErrorContext = {}
) {
super(
message,
code,
ErrorSeverity.CRITICAL,
ErrorCategory.OTHER,
ErrorRecoverability.NON_RECOVERABLE,
context
);
}
}
/**
* Helper to get the appropriate error class based on error category
*
* @param category Error category
* @returns The appropriate error class
*/
export function getErrorClassForCategory(category: ErrorCategory): any {
switch (category) {
case ErrorCategory.VALIDATION:
return ValidationError;
case ErrorCategory.CONFIGURATION:
return ConfigurationError;
case ErrorCategory.CONNECTIVITY:
return NetworkError;
case ErrorCategory.RESOURCE:
return ResourceError;
case ErrorCategory.AUTHENTICATION:
return AuthenticationError;
case ErrorCategory.OPERATION:
return OperationError;
default:
return PlatformError;
}
}

313
ts/errors/email.errors.ts Normal file
View File

@ -0,0 +1,313 @@
import {
PlatformError,
ValidationError,
NetworkError,
ResourceError,
OperationError
} from './base.errors.js';
import type { IErrorContext } from './base.errors.js';
import {
EMAIL_SERVICE_ERROR,
EMAIL_TEMPLATE_ERROR,
EMAIL_VALIDATION_ERROR,
EMAIL_SEND_ERROR,
EMAIL_RECEIVE_ERROR,
EMAIL_ATTACHMENT_ERROR,
EMAIL_PARSE_ERROR,
EMAIL_RATE_LIMIT_EXCEEDED
} from './error.codes.js';
/**
* Base class for all email service related errors
*/
export class EmailServiceError extends OperationError {
/**
* Creates a new email service error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, EMAIL_SERVICE_ERROR, context);
}
}
/**
* Error class for email template errors
*/
export class EmailTemplateError extends OperationError {
/**
* Creates a new email template error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, EMAIL_TEMPLATE_ERROR, context);
}
}
/**
* Error class for email validation errors
*/
export class EmailValidationError extends ValidationError {
/**
* Creates a new email validation error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, EMAIL_VALIDATION_ERROR, context);
}
}
/**
* Error class for email sending errors
*/
export class EmailSendError extends OperationError {
/**
* Creates a new email send error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, EMAIL_SEND_ERROR, context);
}
/**
* Creates an instance for a permanently failed send
*
* @param message Error message
* @param context Additional context
*/
public static permanent(
message: string,
context: IErrorContext = {}
): EmailSendError {
return new EmailSendError(`Permanent send failure: ${message}`, {
...context,
data: {
...context.data,
permanent: true
},
userMessage: 'The email could not be delivered due to a permanent failure.'
});
}
/**
* Creates an instance for a temporary failed send
*
* @param message Error message
* @param maxRetries Maximum number of retries
* @param currentRetry Current retry count
* @param retryDelay Delay between retries in ms
* @param context Additional context
*/
public static temporary(
message: string,
maxRetries: number = 3,
currentRetry: number = 0,
retryDelay: number = 60000,
context: IErrorContext = {}
): EmailSendError {
const error = new EmailSendError(`Temporary send failure: ${message}`, {
...context,
data: {
...context.data,
permanent: false
},
userMessage: 'The email delivery failed temporarily. It will be retried.'
});
return error.withRetry(maxRetries, currentRetry, retryDelay) as EmailSendError;
}
/**
* Check if this is a permanent send failure
*/
public isPermanent(): boolean {
return !!this.context.data?.permanent;
}
}
/**
* Error class for email receiving errors
*/
export class EmailReceiveError extends OperationError {
/**
* Creates a new email receive error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, EMAIL_RECEIVE_ERROR, context);
}
}
/**
* Error class for email attachment errors
*/
export class EmailAttachmentError extends ValidationError {
/**
* Creates a new email attachment error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, EMAIL_ATTACHMENT_ERROR, context);
}
/**
* Creates an instance for an attachment too large error
*
* @param size Attachment size in bytes
* @param maxSize Maximum allowed size in bytes
* @param filename Attachment filename
* @param context Additional context
*/
public static tooLarge(
size: number,
maxSize: number,
filename?: string,
context: IErrorContext = {}
): EmailAttachmentError {
const filenameText = filename ? ` (${filename})` : '';
return new EmailAttachmentError(
`Attachment${filenameText} size ${size} bytes exceeds maximum allowed size of ${maxSize} bytes`,
{
...context,
data: {
...context.data,
size,
maxSize,
filename
},
userMessage: `The attachment${filenameText} is too large. Maximum size is ${Math.round(maxSize / 1024 / 1024)} MB.`
}
);
}
/**
* Creates an instance for an invalid attachment type error
*
* @param contentType Attachment content type
* @param filename Attachment filename
* @param allowedTypes List of allowed content types
* @param context Additional context
*/
public static invalidType(
contentType: string,
filename: string,
allowedTypes: string[],
context: IErrorContext = {}
): EmailAttachmentError {
return new EmailAttachmentError(
`Attachment '${filename}' with content type '${contentType}' is not allowed. Allowed types: ${allowedTypes.join(', ')}`,
{
...context,
data: {
...context.data,
contentType,
filename,
allowedTypes
},
userMessage: `The attachment type ${contentType} is not allowed.`
}
);
}
}
/**
* Error class for email parsing errors
*/
export class EmailParseError extends OperationError {
/**
* Creates a new email parse error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, EMAIL_PARSE_ERROR, context);
}
}
/**
* Error class for email rate limit exceeded errors
*/
export class EmailRateLimitError extends ResourceError {
/**
* Creates a new email rate limit error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, EMAIL_RATE_LIMIT_EXCEEDED, context);
}
/**
* Creates an instance with rate limit information
*
* @param limit Rate limit
* @param remaining Remaining quota
* @param resetAt Time when the quota resets
* @param scope Rate limit scope (global, domain, user, etc.)
* @param context Additional context
*/
public static withLimitInfo(
limit: number,
remaining: number,
resetAt: Date | number,
scope: string = 'global',
context: IErrorContext = {}
): EmailRateLimitError {
const resetTime = typeof resetAt === 'number' ? new Date(resetAt) : resetAt;
const resetTimeStr = resetTime.toISOString();
return new EmailRateLimitError(
`Email rate limit exceeded: ${remaining}/${limit} remaining in ${scope} scope, resets at ${resetTimeStr}`,
{
...context,
data: {
...context.data,
limit,
remaining,
resetAt: resetTime.getTime(),
resetTimeStr,
scope
},
userMessage: `You've reached the email sending limit. Please try again later.`
}
);
}
}

412
ts/errors/error-handler.ts Normal file
View File

@ -0,0 +1,412 @@
import { PlatformError } from './base.errors.js';
import type { IErrorContext } from './base.errors.js';
import { ErrorCategory, ErrorRecoverability, ErrorSeverity } from './error.codes.js';
import { logger } from '../logger.js';
/**
* Error handler configuration
*/
export interface IErrorHandlerConfig {
/** Whether to log errors automatically */
logErrors: boolean;
/** Whether to include stack traces in prod environment */
includeStacksInProd: boolean;
/** Default retry options */
retry: {
/** Maximum retry attempts */
maxAttempts: number;
/** Base delay between retries in ms */
baseDelay: number;
/** Maximum delay between retries in ms */
maxDelay: number;
/** Backoff factor for exponential backoff */
backoffFactor: number;
};
}
/**
* Global error handler configuration
*/
const config: IErrorHandlerConfig = {
logErrors: true,
includeStacksInProd: false,
retry: {
maxAttempts: 3,
baseDelay: 1000,
maxDelay: 30000,
backoffFactor: 2
}
};
/**
* Error handler utility
* Provides methods for consistent error handling across the platform
*/
export class ErrorHandler {
/**
* Current configuration
*/
public static config = config;
/**
* Update error handler configuration
*
* @param newConfig New configuration (partial)
*/
public static configure(newConfig: Partial<IErrorHandlerConfig>): void {
ErrorHandler.config = {
...ErrorHandler.config,
...newConfig,
retry: {
...ErrorHandler.config.retry,
...(newConfig.retry || {})
}
};
}
/**
* Convert any error to a PlatformError
*
* @param error Error to convert
* @param defaultCode Default error code if not a PlatformError
* @param context Additional context
* @returns PlatformError instance
*/
public static toPlatformError(
error: any,
defaultCode: string,
context: IErrorContext = {}
): PlatformError {
// If already a PlatformError, just add context
if (error instanceof PlatformError) {
// Add context if provided
if (Object.keys(context).length > 0) {
return new (error.constructor as typeof PlatformError)(
error.message,
error.code,
error.severity,
error.category,
error.recoverability,
{
...error.context,
...context,
data: {
...(error.context.data || {}),
...(context.data || {})
}
}
);
}
return error;
}
// Convert standard Error to PlatformError
if (error instanceof Error) {
return new PlatformError(
error.message,
defaultCode,
ErrorSeverity.MEDIUM,
ErrorCategory.OPERATION,
ErrorRecoverability.NON_RECOVERABLE,
{
...context,
data: {
...(context.data || {}),
originalError: {
name: error.name,
message: error.message,
stack: error.stack
}
}
}
);
}
// Not an Error instance
return new PlatformError(
typeof error === 'string' ? error : 'Unknown error',
defaultCode,
ErrorSeverity.MEDIUM,
ErrorCategory.OPERATION,
ErrorRecoverability.NON_RECOVERABLE,
context
);
}
/**
* Format an error for API responses
* Sanitizes errors for safe external exposure
*
* @param error Error to format
* @param includeDetails Whether to include detailed information
* @returns Formatted error object
*/
public static formatErrorForResponse(
error: any,
includeDetails: boolean = false
): Record<string, any> {
const platformError = ErrorHandler.toPlatformError(
error,
'PLATFORM_OPERATION_ERROR'
);
// Basic error information
const responseError: Record<string, any> = {
code: platformError.code,
message: platformError.getUserMessage(),
requestId: platformError.context.requestId
};
// Include more details if requested
if (includeDetails) {
responseError.details = {
severity: platformError.severity,
category: platformError.category,
rawMessage: platformError.message,
data: platformError.context.data
};
// Only include stack trace in non-production or if explicitly enabled
if (process.env.NODE_ENV !== 'production' || ErrorHandler.config.includeStacksInProd) {
responseError.details.stack = platformError.stack;
}
}
return responseError;
}
/**
* Handle an error with consistent logging and formatting
*
* @param error Error to handle
* @param defaultCode Default error code if not a PlatformError
* @param context Additional context
* @returns Formatted error for response
*/
public static handleError(
error: any,
defaultCode: string,
context: IErrorContext = {}
): Record<string, any> {
const platformError = ErrorHandler.toPlatformError(
error,
defaultCode,
context
);
// Log the error if enabled
if (ErrorHandler.config.logErrors) {
logger.error(platformError.message, {
error_code: platformError.code,
error_name: platformError.name,
error_severity: platformError.severity,
error_category: platformError.category,
error_recoverability: platformError.recoverability,
...platformError.context,
stack: platformError.stack
});
}
// Return formatted error for response
const isDetailedMode = process.env.NODE_ENV !== 'production';
return ErrorHandler.formatErrorForResponse(platformError, isDetailedMode);
}
/**
* Execute a function with error handling
*
* @param fn Function to execute
* @param defaultCode Default error code if the function throws
* @param context Additional context
* @returns Function result or error
*/
public static async execute<T>(
fn: () => Promise<T>,
defaultCode: string,
context: IErrorContext = {}
): Promise<T> {
try {
return await fn();
} catch (error) {
throw ErrorHandler.toPlatformError(error, defaultCode, context);
}
}
/**
* Execute a function with retries and exponential backoff
*
* @param fn Function to execute
* @param defaultCode Default error code if the function throws
* @param options Retry options
* @param context Additional context
* @returns Function result or error after max retries
*/
public static async executeWithRetry<T>(
fn: () => Promise<T>,
defaultCode: string,
options: {
maxAttempts?: number;
baseDelay?: number;
maxDelay?: number;
backoffFactor?: number;
retryableErrorCodes?: string[];
retryableErrorPatterns?: RegExp[];
onRetry?: (error: PlatformError, attempt: number, delay: number) => void;
} = {},
context: IErrorContext = {}
): Promise<T> {
const {
maxAttempts = ErrorHandler.config.retry.maxAttempts,
baseDelay = ErrorHandler.config.retry.baseDelay,
maxDelay = ErrorHandler.config.retry.maxDelay,
backoffFactor = ErrorHandler.config.retry.backoffFactor,
retryableErrorCodes = [],
retryableErrorPatterns = [],
onRetry = () => {}
} = options;
let lastError: PlatformError;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
// Convert to PlatformError
const platformError = ErrorHandler.toPlatformError(
error,
defaultCode,
{
...context,
retry: {
currentRetry: attempt,
maxRetries: maxAttempts,
nextRetryAt: 0 // Will be set below if retrying
}
}
);
lastError = platformError;
// Check if we should retry
const isLastAttempt = attempt >= maxAttempts - 1;
if (isLastAttempt) {
// No more retries
throw platformError;
}
// Check if error is retryable
const isRetryable =
// Built-in recoverability
platformError.recoverability === ErrorRecoverability.RECOVERABLE ||
platformError.recoverability === ErrorRecoverability.MAYBE_RECOVERABLE ||
platformError.recoverability === ErrorRecoverability.TRANSIENT ||
// Specifically included error codes
retryableErrorCodes.includes(platformError.code) ||
// Matches error message patterns
retryableErrorPatterns.some(pattern => pattern.test(platformError.message));
if (!isRetryable) {
throw platformError;
}
// Calculate delay with exponential backoff
const delay = Math.min(baseDelay * Math.pow(backoffFactor, attempt), maxDelay);
// Add jitter to prevent thundering herd problem (±20%)
const jitter = 0.8 + Math.random() * 0.4;
const actualDelay = Math.floor(delay * jitter);
// Update nextRetryAt in error context
const nextRetryAt = Date.now() + actualDelay;
platformError.context.retry!.nextRetryAt = nextRetryAt;
// Log retry attempt
logger.warn(`Retrying operation after error (attempt ${attempt + 1}/${maxAttempts}): ${platformError.message}`, {
error_code: platformError.code,
retry_attempt: attempt + 1,
retry_max_attempts: maxAttempts,
retry_delay_ms: actualDelay,
retry_next_at: new Date(nextRetryAt).toISOString()
});
// Call onRetry callback
onRetry(platformError, attempt + 1, actualDelay);
// Wait before next retry
await new Promise(resolve => setTimeout(resolve, actualDelay));
}
}
// This should never happen, but TypeScript needs it
throw lastError!;
}
}
/**
* Create a middleware for handling errors in HTTP requests
*
* @returns Middleware function
*/
export function createErrorHandlerMiddleware() {
return (error: any, req: any, res: any, next: any) => {
// Add request context
const context: IErrorContext = {
requestId: req.headers['x-request-id'] || req.headers['x-correlation-id'],
component: 'HttpServer',
operation: `${req.method} ${req.url}`,
data: {
method: req.method,
url: req.url,
query: req.query,
params: req.params,
ip: req.ip || req.connection.remoteAddress,
userAgent: req.headers['user-agent']
}
};
// Handle the error
const formattedError = ErrorHandler.handleError(
error,
'PLATFORM_OPERATION_ERROR',
context
);
// Set status code based on error type
let statusCode = 500;
if (error instanceof PlatformError) {
// Map error categories to HTTP status codes
switch (error.category) {
case ErrorCategory.VALIDATION:
statusCode = 400;
break;
case ErrorCategory.AUTHENTICATION:
statusCode = 401;
break;
case ErrorCategory.RESOURCE:
statusCode = 429;
break;
case ErrorCategory.OPERATION:
statusCode = 400;
break;
default:
statusCode = 500;
}
} else if (error.statusCode) {
// Use provided status code if available
statusCode = error.statusCode;
}
// Send error response
res.status(statusCode).json({
success: false,
error: formattedError
});
};
}

165
ts/errors/error.codes.ts Normal file
View File

@ -0,0 +1,165 @@
/**
* Platform Service Error Codes
*
* This file contains all error codes used across the platform service.
*
* Format: PREFIX_ERROR_TYPE
* - PREFIX: Component/domain prefix (e.g., EMAIL, MTA, SMS)
* - ERROR_TYPE: Specific error type within the domain
*/
// General platform errors (PLATFORM_*)
export const PLATFORM_INITIALIZATION_ERROR = 'PLATFORM_INITIALIZATION_ERROR';
export const PLATFORM_CONFIGURATION_ERROR = 'PLATFORM_CONFIGURATION_ERROR';
export const PLATFORM_OPERATION_ERROR = 'PLATFORM_OPERATION_ERROR';
export const PLATFORM_NOT_IMPLEMENTED = 'PLATFORM_NOT_IMPLEMENTED';
export const PLATFORM_NOT_SUPPORTED = 'PLATFORM_NOT_SUPPORTED';
export const PLATFORM_SERVICE_UNAVAILABLE = 'PLATFORM_SERVICE_UNAVAILABLE';
// Email service errors (EMAIL_*)
export const EMAIL_SERVICE_ERROR = 'EMAIL_SERVICE_ERROR';
export const EMAIL_TEMPLATE_ERROR = 'EMAIL_TEMPLATE_ERROR';
export const EMAIL_VALIDATION_ERROR = 'EMAIL_VALIDATION_ERROR';
export const EMAIL_SEND_ERROR = 'EMAIL_SEND_ERROR';
export const EMAIL_RECEIVE_ERROR = 'EMAIL_RECEIVE_ERROR';
export const EMAIL_ATTACHMENT_ERROR = 'EMAIL_ATTACHMENT_ERROR';
export const EMAIL_PARSE_ERROR = 'EMAIL_PARSE_ERROR';
export const EMAIL_RATE_LIMIT_EXCEEDED = 'EMAIL_RATE_LIMIT_EXCEEDED';
// MTA-specific errors (MTA_*)
export const MTA_CONNECTION_ERROR = 'MTA_CONNECTION_ERROR';
export const MTA_AUTHENTICATION_ERROR = 'MTA_AUTHENTICATION_ERROR';
export const MTA_DELIVERY_ERROR = 'MTA_DELIVERY_ERROR';
export const MTA_CONFIGURATION_ERROR = 'MTA_CONFIGURATION_ERROR';
export const MTA_DNS_ERROR = 'MTA_DNS_ERROR';
export const MTA_TIMEOUT_ERROR = 'MTA_TIMEOUT_ERROR';
export const MTA_PROTOCOL_ERROR = 'MTA_PROTOCOL_ERROR';
// Bounce management errors (BOUNCE_*)
export const BOUNCE_PROCESSING_ERROR = 'BOUNCE_PROCESSING_ERROR';
export const BOUNCE_STORAGE_ERROR = 'BOUNCE_STORAGE_ERROR';
export const BOUNCE_CLASSIFICATION_ERROR = 'BOUNCE_CLASSIFICATION_ERROR';
// Email authentication errors (AUTH_*)
export const AUTH_SPF_ERROR = 'AUTH_SPF_ERROR';
export const AUTH_DKIM_ERROR = 'AUTH_DKIM_ERROR';
export const AUTH_DMARC_ERROR = 'AUTH_DMARC_ERROR';
export const AUTH_KEY_ERROR = 'AUTH_KEY_ERROR';
// Content scanning errors (SCAN_*)
export const SCAN_ANALYSIS_ERROR = 'SCAN_ANALYSIS_ERROR';
export const SCAN_MALWARE_DETECTED = 'SCAN_MALWARE_DETECTED';
export const SCAN_PHISHING_DETECTED = 'SCAN_PHISHING_DETECTED';
export const SCAN_CONTENT_REJECTED = 'SCAN_CONTENT_REJECTED';
// IP and reputation errors (REPUTATION_*)
export const REPUTATION_CHECK_ERROR = 'REPUTATION_CHECK_ERROR';
export const REPUTATION_DATA_ERROR = 'REPUTATION_DATA_ERROR';
export const REPUTATION_BLOCKLIST_ERROR = 'REPUTATION_BLOCKLIST_ERROR';
export const REPUTATION_UPDATE_ERROR = 'REPUTATION_UPDATE_ERROR';
// IP warmup errors (WARMUP_*)
export const WARMUP_ALLOCATION_ERROR = 'WARMUP_ALLOCATION_ERROR';
export const WARMUP_LIMIT_EXCEEDED = 'WARMUP_LIMIT_EXCEEDED';
export const WARMUP_SCHEDULE_ERROR = 'WARMUP_SCHEDULE_ERROR';
// Network and connectivity errors (NETWORK_*)
export const NETWORK_CONNECTION_ERROR = 'NETWORK_CONNECTION_ERROR';
export const NETWORK_TIMEOUT = 'NETWORK_TIMEOUT';
export const NETWORK_DNS_ERROR = 'NETWORK_DNS_ERROR';
export const NETWORK_TLS_ERROR = 'NETWORK_TLS_ERROR';
// Queue and processing errors (QUEUE_*)
export const QUEUE_FULL_ERROR = 'QUEUE_FULL_ERROR';
export const QUEUE_PROCESSING_ERROR = 'QUEUE_PROCESSING_ERROR';
export const QUEUE_PERSISTENCE_ERROR = 'QUEUE_PERSISTENCE_ERROR';
export const QUEUE_ITEM_NOT_FOUND = 'QUEUE_ITEM_NOT_FOUND';
// DcRouter errors (DCR_*)
export const DCR_ROUTING_ERROR = 'DCR_ROUTING_ERROR';
export const DCR_CONFIGURATION_ERROR = 'DCR_CONFIGURATION_ERROR';
export const DCR_PROXY_ERROR = 'DCR_PROXY_ERROR';
export const DCR_DOMAIN_ERROR = 'DCR_DOMAIN_ERROR';
// SMS service errors (SMS_*)
export const SMS_SERVICE_ERROR = 'SMS_SERVICE_ERROR';
export const SMS_SEND_ERROR = 'SMS_SEND_ERROR';
export const SMS_VALIDATION_ERROR = 'SMS_VALIDATION_ERROR';
export const SMS_RATE_LIMIT_EXCEEDED = 'SMS_RATE_LIMIT_EXCEEDED';
// Storage errors (STORAGE_*)
export const STORAGE_WRITE_ERROR = 'STORAGE_WRITE_ERROR';
export const STORAGE_READ_ERROR = 'STORAGE_READ_ERROR';
export const STORAGE_DELETE_ERROR = 'STORAGE_DELETE_ERROR';
export const STORAGE_QUOTA_EXCEEDED = 'STORAGE_QUOTA_EXCEEDED';
// Rule management errors (RULE_*)
export const RULE_VALIDATION_ERROR = 'RULE_VALIDATION_ERROR';
export const RULE_EXECUTION_ERROR = 'RULE_EXECUTION_ERROR';
export const RULE_NOT_FOUND = 'RULE_NOT_FOUND';
// Type definitions for error severity
export enum ErrorSeverity {
/** Critical errors that require immediate attention */
CRITICAL = 'CRITICAL',
/** High-impact errors that may affect service functioning */
HIGH = 'HIGH',
/** Medium-impact errors that cause partial degradation */
MEDIUM = 'MEDIUM',
/** Low-impact errors that have minimal or local impact */
LOW = 'LOW',
/** Informational errors that are not problematic */
INFO = 'INFO'
}
// Type definitions for error categories
export enum ErrorCategory {
/** Errors related to configuration */
CONFIGURATION = 'CONFIGURATION',
/** Errors related to network connectivity */
CONNECTIVITY = 'CONNECTIVITY',
/** Errors related to authentication/authorization */
AUTHENTICATION = 'AUTHENTICATION',
/** Errors related to data validation */
VALIDATION = 'VALIDATION',
/** Errors related to resource availability */
RESOURCE = 'RESOURCE',
/** Errors related to service operations */
OPERATION = 'OPERATION',
/** Errors related to third-party integrations */
INTEGRATION = 'INTEGRATION',
/** Errors related to security */
SECURITY = 'SECURITY',
/** Errors related to data storage */
STORAGE = 'STORAGE',
/** Errors that don't fit into other categories */
OTHER = 'OTHER'
}
// Type definitions for error recoverability
export enum ErrorRecoverability {
/** Error cannot be automatically recovered from */
NON_RECOVERABLE = 'NON_RECOVERABLE',
/** Error might be recoverable with retry */
MAYBE_RECOVERABLE = 'MAYBE_RECOVERABLE',
/** Error is definitely recoverable with retries */
RECOVERABLE = 'RECOVERABLE',
/** Error is transient and should resolve without action */
TRANSIENT = 'TRANSIENT'
}

193
ts/errors/index.ts Normal file
View File

@ -0,0 +1,193 @@
/**
* Platform Service Error System
*
* This module provides a comprehensive error handling system for the Platform Service,
* with structured error types, error codes, and consistent patterns for logging and recovery.
*/
// Export error codes and types
export * from './error.codes.js';
// Export base error classes
export * from './base.errors.js';
// Export domain-specific error classes
export * from './email.errors.js';
export * from './mta.errors.js';
export * from './reputation.errors.js';
// Export utility function to create specific error types based on the error category
import { getErrorClassForCategory } from './base.errors.js';
export { getErrorClassForCategory };
/**
* Create a typed error from a standard Error
* Useful for converting errors from external libraries or APIs
*
* @param error Standard error to convert
* @param code Error code to assign
* @param contextData Additional context data
* @returns Typed PlatformError
*/
export function fromError(
error: Error,
code: string,
contextData: Record<string, any> = {}
) {
// Import and use PlatformError
const { PlatformError } = require('./base.errors.js');
const { ErrorSeverity, ErrorCategory, ErrorRecoverability } = require('./error.codes.js');
return new PlatformError(
error.message,
code,
ErrorSeverity.MEDIUM,
ErrorCategory.OPERATION,
ErrorRecoverability.NON_RECOVERABLE,
{
data: {
...contextData,
originalError: {
name: error.name,
message: error.message,
stack: error.stack
}
}
}
);
}
/**
* Determine if an error is retryable
*
* @param error Error to check
* @returns Boolean indicating if the error should be retried
*/
export function isRetryable(error: any): boolean {
// If it's our platform error, use its recoverability property
if (error && typeof error === 'object' && 'recoverability' in error) {
const { ErrorRecoverability } = require('./error.codes.js');
return error.recoverability === ErrorRecoverability.RECOVERABLE ||
error.recoverability === ErrorRecoverability.MAYBE_RECOVERABLE ||
error.recoverability === ErrorRecoverability.TRANSIENT;
}
// Check if it's a network error (these are often transient)
if (error && typeof error === 'object' && error.code) {
const networkErrors = [
'ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EHOSTUNREACH',
'ENETUNREACH', 'ENOTFOUND', 'EPROTO', 'ECONNABORTED'
];
return networkErrors.includes(error.code);
}
// By default, we can't determine if the error is retryable
return false;
}
/**
* Create a wrapped version of a function that catches errors
* and converts them to typed PlatformErrors
*
* @param fn Function to wrap
* @param errorCode Default error code to use
* @param contextData Additional context data
* @returns Wrapped function
*/
export function withErrorHandling<T extends (...args: any[]) => Promise<any>>(
fn: T,
errorCode: string,
contextData: Record<string, any> = {}
): T {
return (async function(...args: Parameters<T>): Promise<ReturnType<T>> {
try {
return await fn(...args);
} catch (error) {
if (error && typeof error === 'object' && 'code' in error) {
// Already a typed error, rethrow
throw error;
}
throw fromError(
error instanceof Error ? error : new Error(String(error)),
errorCode,
{
...contextData,
fnName: fn.name,
args: args.map(arg =>
typeof arg === 'object'
? '[Object]'
: String(arg).substring(0, 100)
)
}
);
}
}) as T;
}
/**
* Retry a function with exponential backoff
*
* @param fn Function to retry
* @param options Retry options
* @returns Function result or throws after max retries
*/
export async function retry<T>(
fn: () => Promise<T>,
options: {
maxRetries?: number;
initialDelay?: number;
maxDelay?: number;
backoffFactor?: number;
retryableErrors?: Array<string | RegExp>;
} = {}
): Promise<T> {
const {
maxRetries = 3,
initialDelay = 1000,
maxDelay = 30000,
backoffFactor = 2,
retryableErrors = []
} = options;
let lastError: Error;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error instanceof Error
? error
: new Error(String(error));
// Check if we should retry
const shouldRetry = attempt < maxRetries && (
isRetryable(error) ||
retryableErrors.some(pattern => {
if (typeof pattern === 'string') {
return lastError.message.includes(pattern);
}
return pattern.test(lastError.message);
})
);
if (!shouldRetry) {
throw lastError;
}
// Calculate delay with exponential backoff
const delay = Math.min(initialDelay * Math.pow(backoffFactor, attempt), maxDelay);
// Add jitter to prevent thundering herd problem (±20%)
const jitter = 0.8 + Math.random() * 0.4;
const actualDelay = Math.floor(delay * jitter);
// Wait before next retry
await new Promise(resolve => setTimeout(resolve, actualDelay));
}
}
// This should never happen, but TypeScript needs it
throw lastError!;
}

611
ts/errors/mta.errors.ts Normal file
View File

@ -0,0 +1,611 @@
import {
PlatformError,
NetworkError,
AuthenticationError,
OperationError,
ConfigurationError
} from './base.errors.js';
import type { IErrorContext } from './base.errors.js';
import {
MTA_CONNECTION_ERROR,
MTA_AUTHENTICATION_ERROR,
MTA_DELIVERY_ERROR,
MTA_CONFIGURATION_ERROR,
MTA_DNS_ERROR,
MTA_TIMEOUT_ERROR,
MTA_PROTOCOL_ERROR
} from './error.codes.js';
/**
* Base class for MTA connection errors
*/
export class MtaConnectionError extends NetworkError {
/**
* Creates a new MTA connection error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, MTA_CONNECTION_ERROR, context);
}
/**
* Creates an instance for a DNS resolution error
*
* @param hostname Hostname that failed to resolve
* @param originalError Original error
* @param context Additional context
*/
public static dnsError(
hostname: string,
originalError?: Error,
context: IErrorContext = {}
): MtaConnectionError {
const errorMsg = originalError ? `: ${originalError.message}` : '';
return new MtaConnectionError(
`Failed to resolve DNS for ${hostname}${errorMsg}`,
{
...context,
data: {
...context.data,
hostname,
originalError: originalError ? {
message: originalError.message,
stack: originalError.stack
} : undefined
},
userMessage: `Could not connect to mail server for ${hostname}.`
}
);
}
/**
* Creates an instance for a connection timeout
*
* @param hostname Hostname that timed out
* @param port Port number
* @param timeout Timeout in milliseconds
* @param context Additional context
*/
public static timeout(
hostname: string,
port: number,
timeout: number,
context: IErrorContext = {}
): MtaConnectionError {
return new MtaConnectionError(
`Connection to ${hostname}:${port} timed out after ${timeout}ms`,
{
...context,
data: {
...context.data,
hostname,
port,
timeout
},
userMessage: `Connection to mail server timed out.`
}
);
}
/**
* Creates an instance for a connection refused error
*
* @param hostname Hostname that refused connection
* @param port Port number
* @param context Additional context
*/
public static refused(
hostname: string,
port: number,
context: IErrorContext = {}
): MtaConnectionError {
return new MtaConnectionError(
`Connection to ${hostname}:${port} refused`,
{
...context,
data: {
...context.data,
hostname,
port
},
userMessage: `Connection to mail server was refused.`
}
);
}
}
/**
* Error class for MTA authentication errors
*/
export class MtaAuthenticationError extends AuthenticationError {
/**
* Creates a new MTA authentication error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, MTA_AUTHENTICATION_ERROR, context);
}
/**
* Creates an instance for invalid credentials
*
* @param hostname Hostname where authentication failed
* @param username Username that failed authentication
* @param context Additional context
*/
public static invalidCredentials(
hostname: string,
username: string,
context: IErrorContext = {}
): MtaAuthenticationError {
return new MtaAuthenticationError(
`Authentication failed for user ${username} at ${hostname}`,
{
...context,
data: {
...context.data,
hostname,
username
},
userMessage: `Authentication to mail server failed.`
}
);
}
/**
* Creates an instance for unsupported authentication method
*
* @param hostname Hostname
* @param method Authentication method that is not supported
* @param supportedMethods List of supported authentication methods
* @param context Additional context
*/
public static unsupportedMethod(
hostname: string,
method: string,
supportedMethods: string[] = [],
context: IErrorContext = {}
): MtaAuthenticationError {
return new MtaAuthenticationError(
`Authentication method ${method} not supported by ${hostname}${supportedMethods.length > 0 ? `. Supported methods: ${supportedMethods.join(', ')}` : ''}`,
{
...context,
data: {
...context.data,
hostname,
method,
supportedMethods
},
userMessage: `The mail server doesn't support the required authentication method.`
}
);
}
}
/**
* Error class for MTA delivery errors
*/
export class MtaDeliveryError extends OperationError {
/**
* Creates a new MTA delivery error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, MTA_DELIVERY_ERROR, context);
}
/**
* Creates an instance for a permanent delivery failure
*
* @param message Error message
* @param recipientAddress Recipient email address
* @param statusCode SMTP status code
* @param smtpResponse Full SMTP response
* @param context Additional context
*/
public static permanent(
message: string,
recipientAddress: string,
statusCode?: string,
smtpResponse?: string,
context: IErrorContext = {}
): MtaDeliveryError {
const statusCodeStr = statusCode ? ` (${statusCode})` : '';
return new MtaDeliveryError(
`Permanent delivery failure to ${recipientAddress}${statusCodeStr}: ${message}`,
{
...context,
data: {
...context.data,
recipientAddress,
statusCode,
smtpResponse,
permanent: true
},
userMessage: `The email could not be delivered to ${recipientAddress}.`
}
);
}
/**
* Creates an instance for a temporary delivery failure
*
* @param message Error message
* @param recipientAddress Recipient email address
* @param statusCode SMTP status code
* @param smtpResponse Full SMTP response
* @param maxRetries Maximum number of retries
* @param currentRetry Current retry count
* @param retryDelay Delay between retries in ms
* @param context Additional context
*/
public static temporary(
message: string,
recipientAddress: string,
statusCode?: string,
smtpResponse?: string,
maxRetries: number = 3,
currentRetry: number = 0,
retryDelay: number = 60000,
context: IErrorContext = {}
): MtaDeliveryError {
const statusCodeStr = statusCode ? ` (${statusCode})` : '';
const error = new MtaDeliveryError(
`Temporary delivery failure to ${recipientAddress}${statusCodeStr}: ${message}`,
{
...context,
data: {
...context.data,
recipientAddress,
statusCode,
smtpResponse,
permanent: false
},
userMessage: `The email delivery to ${recipientAddress} failed temporarily. It will be retried.`
}
);
return error.withRetry(maxRetries, currentRetry, retryDelay) as MtaDeliveryError;
}
/**
* Check if this is a permanent delivery failure
*/
public isPermanent(): boolean {
return !!this.context.data?.permanent;
}
/**
* Get the recipient address associated with this delivery error
*/
public getRecipientAddress(): string | undefined {
return this.context.data?.recipientAddress;
}
/**
* Get the SMTP status code associated with this delivery error
*/
public getStatusCode(): string | undefined {
return this.context.data?.statusCode;
}
}
/**
* Error class for MTA configuration errors
*/
export class MtaConfigurationError extends ConfigurationError {
/**
* Creates a new MTA configuration error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, MTA_CONFIGURATION_ERROR, context);
}
/**
* Creates an instance for a missing configuration value
*
* @param propertyPath Path to the missing property
* @param context Additional context
*/
public static missingConfig(
propertyPath: string,
context: IErrorContext = {}
): MtaConfigurationError {
return new MtaConfigurationError(
`Missing required configuration: ${propertyPath}`,
{
...context,
data: {
...context.data,
propertyPath
},
userMessage: `The mail server is missing required configuration.`
}
);
}
/**
* Creates an instance for an invalid configuration value
*
* @param propertyPath Path to the invalid property
* @param value Current value
* @param expectedType Expected type or format
* @param context Additional context
*/
public static invalidConfig(
propertyPath: string,
value: any,
expectedType: string,
context: IErrorContext = {}
): MtaConfigurationError {
return new MtaConfigurationError(
`Invalid configuration value for ${propertyPath}: got ${value} (${typeof value}), expected ${expectedType}`,
{
...context,
data: {
...context.data,
propertyPath,
value,
expectedType
},
userMessage: `The mail server has an invalid configuration value.`
}
);
}
}
/**
* Error class for MTA DNS errors
*/
export class MtaDnsError extends NetworkError {
/**
* Creates a new MTA DNS error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, MTA_DNS_ERROR, context);
}
/**
* Creates an instance for an MX record lookup failure
*
* @param domain Domain that failed MX lookup
* @param originalError Original error
* @param context Additional context
*/
public static mxLookupFailed(
domain: string,
originalError?: Error,
context: IErrorContext = {}
): MtaDnsError {
const errorMsg = originalError ? `: ${originalError.message}` : '';
return new MtaDnsError(
`Failed to lookup MX records for ${domain}${errorMsg}`,
{
...context,
data: {
...context.data,
domain,
recordType: 'MX',
originalError: originalError ? {
message: originalError.message,
stack: originalError.stack
} : undefined
},
userMessage: `Could not find mail servers for ${domain}.`
}
);
}
/**
* Creates an instance for a TXT record lookup failure
*
* @param domain Domain that failed TXT lookup
* @param recordPrefix Optional record prefix (e.g., 'spf', 'dkim', 'dmarc')
* @param originalError Original error
* @param context Additional context
*/
public static txtLookupFailed(
domain: string,
recordPrefix?: string,
originalError?: Error,
context: IErrorContext = {}
): MtaDnsError {
const recordType = recordPrefix ? `${recordPrefix} TXT` : 'TXT';
const errorMsg = originalError ? `: ${originalError.message}` : '';
return new MtaDnsError(
`Failed to lookup ${recordType} records for ${domain}${errorMsg}`,
{
...context,
data: {
...context.data,
domain,
recordType,
recordPrefix,
originalError: originalError ? {
message: originalError.message,
stack: originalError.stack
} : undefined
},
userMessage: `Could not verify ${recordPrefix || ''} records for ${domain}.`
}
);
}
}
/**
* Error class for MTA timeout errors
*/
export class MtaTimeoutError extends NetworkError {
/**
* Creates a new MTA timeout error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, MTA_TIMEOUT_ERROR, context);
}
/**
* Creates an instance for an SMTP command timeout
*
* @param command SMTP command that timed out
* @param server Server hostname
* @param timeout Timeout in milliseconds
* @param context Additional context
*/
public static commandTimeout(
command: string,
server: string,
timeout: number,
context: IErrorContext = {}
): MtaTimeoutError {
return new MtaTimeoutError(
`SMTP command ${command} to ${server} timed out after ${timeout}ms`,
{
...context,
data: {
...context.data,
command,
server,
timeout
},
userMessage: `The mail server took too long to respond.`
}
);
}
/**
* Creates an instance for an overall transaction timeout
*
* @param server Server hostname
* @param timeout Timeout in milliseconds
* @param context Additional context
*/
public static transactionTimeout(
server: string,
timeout: number,
context: IErrorContext = {}
): MtaTimeoutError {
return new MtaTimeoutError(
`SMTP transaction with ${server} timed out after ${timeout}ms`,
{
...context,
data: {
...context.data,
server,
timeout
},
userMessage: `The mail server transaction took too long to complete.`
}
);
}
}
/**
* Error class for MTA protocol errors
*/
export class MtaProtocolError extends OperationError {
/**
* Creates a new MTA protocol error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, MTA_PROTOCOL_ERROR, context);
}
/**
* Creates an instance for an unexpected server response
*
* @param command SMTP command that received unexpected response
* @param response Unexpected response
* @param expected Expected response pattern
* @param server Server hostname
* @param context Additional context
*/
public static unexpectedResponse(
command: string,
response: string,
expected: string,
server: string,
context: IErrorContext = {}
): MtaProtocolError {
return new MtaProtocolError(
`Unexpected SMTP response from ${server} for command ${command}: got "${response}", expected "${expected}"`,
{
...context,
data: {
...context.data,
command,
response,
expected,
server
},
userMessage: `Received an unexpected response from the mail server.`
}
);
}
/**
* Creates an instance for a syntax error
*
* @param details Error details
* @param server Server hostname
* @param context Additional context
*/
public static syntaxError(
details: string,
server: string,
context: IErrorContext = {}
): MtaProtocolError {
return new MtaProtocolError(
`SMTP syntax error in communication with ${server}: ${details}`,
{
...context,
data: {
...context.data,
details,
server
},
userMessage: `There was a protocol error communicating with the mail server.`
}
);
}
}

View File

@ -0,0 +1,352 @@
import {
PlatformError,
OperationError,
ResourceError
} from './base.errors.js';
import type { IErrorContext } from './base.errors.js';
import {
REPUTATION_CHECK_ERROR,
REPUTATION_DATA_ERROR,
REPUTATION_BLOCKLIST_ERROR,
REPUTATION_UPDATE_ERROR,
WARMUP_ALLOCATION_ERROR,
WARMUP_LIMIT_EXCEEDED,
WARMUP_SCHEDULE_ERROR
} from './error.codes.js';
/**
* Base class for reputation-related errors
*/
export class ReputationError extends OperationError {
/**
* Creates a new reputation error
*
* @param message Error message
* @param code Error code
* @param context Additional context
*/
constructor(
message: string,
code: string,
context: IErrorContext = {}
) {
super(message, code, context);
}
}
/**
* Error class for reputation check errors
*/
export class ReputationCheckError extends ReputationError {
/**
* Creates a new reputation check error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, REPUTATION_CHECK_ERROR, context);
}
/**
* Creates an instance for an IP reputation check error
*
* @param ip IP address
* @param provider Reputation provider
* @param originalError Original error
* @param context Additional context
*/
public static ipCheckFailed(
ip: string,
provider: string,
originalError?: Error,
context: IErrorContext = {}
): ReputationCheckError {
const errorMsg = originalError ? `: ${originalError.message}` : '';
return new ReputationCheckError(
`Failed to check reputation for IP ${ip} with provider ${provider}${errorMsg}`,
{
...context,
data: {
...context.data,
ip,
provider,
originalError: originalError ? {
message: originalError.message,
stack: originalError.stack
} : undefined
}
}
);
}
/**
* Creates an instance for a domain reputation check error
*
* @param domain Domain
* @param provider Reputation provider
* @param originalError Original error
* @param context Additional context
*/
public static domainCheckFailed(
domain: string,
provider: string,
originalError?: Error,
context: IErrorContext = {}
): ReputationCheckError {
const errorMsg = originalError ? `: ${originalError.message}` : '';
return new ReputationCheckError(
`Failed to check reputation for domain ${domain} with provider ${provider}${errorMsg}`,
{
...context,
data: {
...context.data,
domain,
provider,
originalError: originalError ? {
message: originalError.message,
stack: originalError.stack
} : undefined
}
}
);
}
}
/**
* Error class for reputation data errors
*/
export class ReputationDataError extends ReputationError {
/**
* Creates a new reputation data error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, REPUTATION_DATA_ERROR, context);
}
/**
* Creates an instance for a data access error
*
* @param entity Entity type (domain, ip)
* @param entityId Entity identifier
* @param operation Operation that failed (read, write, update)
* @param originalError Original error
* @param context Additional context
*/
public static dataAccessFailed(
entity: string,
entityId: string,
operation: string,
originalError?: Error,
context: IErrorContext = {}
): ReputationDataError {
const errorMsg = originalError ? `: ${originalError.message}` : '';
return new ReputationDataError(
`Failed to ${operation} reputation data for ${entity} ${entityId}${errorMsg}`,
{
...context,
data: {
...context.data,
entity,
entityId,
operation,
originalError: originalError ? {
message: originalError.message,
stack: originalError.stack
} : undefined
}
}
);
}
}
/**
* Error class for blocklist-related errors
*/
export class BlocklistError extends ReputationError {
/**
* Creates a new blocklist error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, REPUTATION_BLOCKLIST_ERROR, context);
}
/**
* Creates an instance for an entity found on a blocklist
*
* @param entity Entity type (domain, ip)
* @param entityId Entity identifier
* @param blocklist Blocklist name
* @param reason Reason for listing (if available)
* @param context Additional context
*/
public static entityBlocked(
entity: string,
entityId: string,
blocklist: string,
reason?: string,
context: IErrorContext = {}
): BlocklistError {
const reasonText = reason ? ` (${reason})` : '';
return new BlocklistError(
`${entity.charAt(0).toUpperCase() + entity.slice(1)} ${entityId} is listed on blocklist ${blocklist}${reasonText}`,
{
...context,
data: {
...context.data,
entity,
entityId,
blocklist,
reason
},
userMessage: `The ${entity} ${entityId} is on a blocklist. This may affect email deliverability.`
}
);
}
}
/**
* Error class for reputation update errors
*/
export class ReputationUpdateError extends ReputationError {
/**
* Creates a new reputation update error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, REPUTATION_UPDATE_ERROR, context);
}
}
/**
* Error class for IP warmup allocation errors
*/
export class WarmupAllocationError extends ReputationError {
/**
* Creates a new warmup allocation error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, WARMUP_ALLOCATION_ERROR, context);
}
/**
* Creates an instance for no available IPs
*
* @param domain Domain requesting an IP
* @param policy Allocation policy that was used
* @param context Additional context
*/
public static noAvailableIps(
domain: string,
policy: string,
context: IErrorContext = {}
): WarmupAllocationError {
return new WarmupAllocationError(
`No available IPs for domain ${domain} using ${policy} allocation policy`,
{
...context,
data: {
...context.data,
domain,
policy
},
userMessage: `No available sending IPs for ${domain}.`
}
);
}
}
/**
* Error class for IP warmup limit exceeded errors
*/
export class WarmupLimitError extends ResourceError {
/**
* Creates a new warmup limit error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, WARMUP_LIMIT_EXCEEDED, context);
}
/**
* Creates an instance for daily sending limit exceeded
*
* @param ip IP address
* @param domain Domain
* @param limit Daily limit
* @param sent Number of emails sent
* @param context Additional context
*/
public static dailyLimitExceeded(
ip: string,
domain: string,
limit: number,
sent: number,
context: IErrorContext = {}
): WarmupLimitError {
return new WarmupLimitError(
`Daily sending limit exceeded for IP ${ip} and domain ${domain}: ${sent}/${limit}`,
{
...context,
data: {
...context.data,
ip,
domain,
limit,
sent
},
userMessage: `Daily sending limit reached for ${domain}.`
}
);
}
}
/**
* Error class for IP warmup schedule errors
*/
export class WarmupScheduleError extends ReputationError {
/**
* Creates a new warmup schedule error
*
* @param message Error message
* @param context Additional context
*/
constructor(
message: string,
context: IErrorContext = {}
) {
super(message, WARMUP_SCHEDULE_ERROR, context);
}
}

View File

@ -1,4 +1,8 @@
export * from './00_commitinfo_data.js';
import { SzPlatformService } from './platformservice.js';
export * from './mail/index.js';
// DcRouter
export * from './classes.dcrouter.js';
export const runCli = async () => {}

View File

@ -1,9 +1,91 @@
import * as plugins from './plugins.js';
import { randomUUID } from 'node:crypto';
export const logger = new plugins.smartlog.Smartlog({
// Map NODE_ENV to valid TEnvironment
const nodeEnv = process.env.NODE_ENV || 'production';
const envMap: Record<string, 'local' | 'test' | 'staging' | 'production'> = {
'development': 'local',
'test': 'test',
'staging': 'staging',
'production': 'production'
};
// Default Smartlog instance
const baseLogger = new plugins.smartlog.Smartlog({
logContext: {
environment: 'production',
environment: envMap[nodeEnv] || 'production',
runtime: 'node',
zone: 'serve.zone',
}
});
// Extended logger compatible with the original enhanced logger API
class StandardLogger {
private defaultContext: Record<string, any> = {};
private correlationId: string | null = null;
constructor() {}
// Log methods
public log(level: 'error' | 'warn' | 'info' | 'success' | 'debug', message: string, context: Record<string, any> = {}) {
const combinedContext = {
...this.defaultContext,
...context
};
if (this.correlationId) {
combinedContext.correlation_id = this.correlationId;
}
baseLogger.log(level, message, combinedContext);
}
public error(message: string, context: Record<string, any> = {}) {
this.log('error', message, context);
}
public warn(message: string, context: Record<string, any> = {}) {
this.log('warn', message, context);
}
public info(message: string, context: Record<string, any> = {}) {
this.log('info', message, context);
}
public success(message: string, context: Record<string, any> = {}) {
this.log('success', message, context);
}
public debug(message: string, context: Record<string, any> = {}) {
this.log('debug', message, context);
}
// Context management
public setContext(context: Record<string, any>, overwrite: boolean = false) {
if (overwrite) {
this.defaultContext = context;
} else {
this.defaultContext = {
...this.defaultContext,
...context
};
}
}
// Correlation ID management
public setCorrelationId(id: string | null = null): string {
this.correlationId = id || randomUUID();
return this.correlationId;
}
public getCorrelationId(): string | null {
return this.correlationId;
}
public clearCorrelationId(): void {
this.correlationId = null;
}
}
// Export a singleton instance
export const logger = new StandardLogger();

View File

@ -1,7 +1,7 @@
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import { logger } from '../logger.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../security/index.js';
import * as plugins from '../../plugins.js';
import * as paths from '../../paths.js';
import { logger } from '../../logger.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js';
import { LRUCache } from 'lru-cache';
/**

View File

@ -1,5 +1,5 @@
import * as plugins from '../plugins.js';
import { EmailValidator } from '../email/classes.emailvalidator.js';
import * as plugins from '../../plugins.js';
import { EmailValidator } from './classes.emailvalidator.js';
export interface IAttachment {
filename: string;
@ -593,6 +593,38 @@ export class Email {
return result;
}
/**
* Convert to simple Smartmail-compatible object (for backward compatibility)
* @returns A Promise with a simple Smartmail-compatible object
*/
public async toSmartmailBasic(): Promise<any> {
// Create a Smartmail-compatible object with the email data
const smartmail = {
options: {
from: this.from,
to: this.to,
subject: this.subject
},
content: {
text: this.text,
html: this.html || ''
},
headers: { ...this.headers },
attachments: this.attachments ? this.attachments.map(attachment => ({
name: attachment.filename,
data: attachment.content,
type: attachment.contentType,
cid: attachment.contentId
})) : [],
// Add basic Smartmail-compatible methods for compatibility
addHeader: (key: string, value: string) => {
smartmail.headers[key] = value;
}
};
return smartmail;
}
/**
* Create an Email instance from a Smartmail object
* @param smartmail The Smartmail instance to convert

View File

@ -1,5 +1,5 @@
import * as plugins from '../plugins.js';
import { logger } from '../logger.js';
import * as plugins from '../../plugins.js';
import { logger } from '../../logger.js';
import { LRUCache } from 'lru-cache';
export interface IEmailValidationResult {

View File

@ -1,6 +1,6 @@
import * as plugins from '../plugins.js';
import { EmailService } from './classes.emailservice.js';
import { logger } from '../logger.js';
import * as plugins from '../../plugins.js';
import { EmailService } from '../services/classes.emailservice.js';
import { logger } from '../../logger.js';
export class RuleManager {
public emailRef: EmailService;

View File

@ -1,6 +1,6 @@
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import { logger } from '../logger.js';
import * as plugins from '../../plugins.js';
import * as paths from '../../paths.js';
import { logger } from '../../logger.js';
/**
* Email template type definition

6
ts/mail/core/index.ts Normal file
View File

@ -0,0 +1,6 @@
// Core email components
export * from './classes.email.js';
export * from './classes.emailvalidator.js';
export * from './classes.templatemanager.js';
export * from './classes.bouncemanager.js';
export * from './classes.rulemanager.js';

View File

@ -1,15 +1,97 @@
import * as plugins from '../plugins.js';
import { EmailService } from './classes.emailservice.js';
import { logger } from '../logger.js';
import * as plugins from '../../plugins.js';
import { EmailService } from '../services/classes.emailservice.js';
import { logger } from '../../logger.js';
// Import MTA classes
import {
MtaService,
Email as MtaEmail,
type IEmailOptions,
DeliveryStatus,
type IAttachment
} from '../mta/index.js';
import { MtaService } from './classes.mta.js';
import { Email as MtaEmail } from '../core/classes.email.js';
import { DeliveryStatus } from './classes.emailsendjob.js';
// Re-export for use in index.ts
export { DeliveryStatus };
// Import Email types
export interface IEmailOptions {
from: string;
to: string[];
cc?: string[];
bcc?: string[];
subject: string;
text?: string;
html?: string;
attachments?: IAttachment[];
headers?: { [key: string]: string };
}
// Reuse the IAttachment interface
export interface IAttachment {
filename: string;
content: Buffer;
contentType: string;
contentId?: string;
encoding?: string;
}
/**
* Email status details
*/
export interface IEmailStatusDetails {
/** Number of delivery attempts */
attempts?: number;
/** Timestamp of last delivery attempt */
lastAttempt?: Date;
/** Timestamp of next scheduled attempt */
nextAttempt?: Date;
/** Error message if delivery failed */
error?: string;
/** Message explaining the status */
message?: string;
}
/**
* Email status response
*/
export interface IEmailStatusResponse {
/** Current status of the email */
status: DeliveryStatus | 'unknown' | 'error';
/** Additional status details */
details?: IEmailStatusDetails;
}
/**
* Options for sending an email via MTA
*/
export interface ISendEmailOptions {
/** Whether to use MIME format conversion */
useMimeFormat?: boolean;
/** Whether to track clicks */
trackClicks?: boolean;
/** Whether to track opens */
trackOpens?: boolean;
/** Message priority (1-5, where 1 is highest) */
priority?: number;
/** Message scheduling options */
schedule?: {
/** Time to send the email */
sendAt?: Date | string;
/** Time the message expires */
expireAt?: Date | string;
};
/** DKIM signing options */
dkim?: {
/** Whether to sign the message */
sign?: boolean;
/** Domain to use for signing */
domain?: string;
/** Key selector to use */
selector?: string;
};
/** Additional headers */
headers?: Record<string, string>;
/** Message tags for categorization */
tags?: string[];
}
export class MtaConnector {
public emailRef: EmailService;
@ -20,6 +102,13 @@ export class MtaConnector {
this.mtaService = mtaService || this.emailRef.mtaService;
}
/**
* Send an email using the MTA service
* @param smartmail The email to send
* @param toAddresses Recipients (comma-separated or array)
* @param options Additional options
*/
/**
* Send an email using the MTA service
* @param smartmail The email to send
@ -29,7 +118,7 @@ export class MtaConnector {
public async sendEmail(
smartmail: plugins.smartmail.Smartmail<any>,
toAddresses: string | string[],
options: any = {}
options: ISendEmailOptions = {}
): Promise<string> {
// Check if recipients are on the suppression list
const recipients = Array.isArray(toAddresses)
@ -75,7 +164,7 @@ export class MtaConnector {
const emailOptions: Record<string, any> = { ...options };
// Check if we should use MIME format
const useMimeFormat = options.useMimeFormat ?? true;
const useMimeFormat = options.useMimeFormat !== false; // Default to true
if (useMimeFormat) {
// Use smartmail's MIME conversion for improved handling
@ -495,28 +584,28 @@ export class MtaConnector {
/**
* Check the status of a sent email
* @param emailId The email ID to check
* @returns Current status and details
*/
public async checkEmailStatus(emailId: string): Promise<{
status: string;
details?: any;
}> {
public async checkEmailStatus(emailId: string): Promise<IEmailStatusResponse> {
try {
const status = this.mtaService.getEmailStatus(emailId);
if (!status) {
return {
status: 'unknown',
status: 'unknown' as const,
details: { message: 'Email not found' }
};
}
return {
status: status.status,
// Use type assertion to ensure this passes type check
status: status.status as DeliveryStatus,
details: {
attempts: status.attempts,
lastAttempt: status.lastAttempt,
nextAttempt: status.nextAttempt,
error: status.error?.message
error: status.error?.message,
message: `Status: ${status.status}${status.error ? `, Error: ${status.error.message}` : ''}`
}
};
} catch (error) {
@ -528,7 +617,7 @@ export class MtaConnector {
});
return {
status: 'error',
status: 'error' as const,
details: { message: error.message }
};
}

View File

@ -1,9 +1,9 @@
import * as plugins from '../plugins.js';
import * as plugins from '../../plugins.js';
import { EventEmitter } from 'node:events';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { logger } from '../logger.js';
import { type EmailProcessingMode, type IDomainRule } from './classes.email.config.js';
import { logger } from '../../logger.js';
import { type EmailProcessingMode, type IDomainRule } from '../routing/classes.email.config.js';
/**
* Queue item status

View File

@ -1,16 +1,16 @@
import * as plugins from '../plugins.js';
import * as plugins from '../../plugins.js';
import { EventEmitter } from 'node:events';
import * as net from 'node:net';
import * as tls from 'node:tls';
import { logger } from '../logger.js';
import { logger } from '../../logger.js';
import {
SecurityLogger,
SecurityLogLevel,
SecurityEventType
} from '../security/index.js';
} from '../../security/index.js';
import { UnifiedDeliveryQueue, type IQueueItem } from './classes.delivery.queue.js';
import type { Email } from '../mta/classes.email.js';
import type { IDomainRule } from './classes.email.config.js';
import type { Email } from '../core/classes.email.js';
import type { IDomainRule } from '../routing/classes.email.config.js';
/**
* Delivery handler interface
@ -331,7 +331,7 @@ export class MultiModeDeliverySystem extends EventEmitter {
},
success: true
});
} catch (error) {
} catch (error: any) {
// Calculate delivery attempt time even for failures
const deliveryTime = Date.now() - startTime;
@ -423,9 +423,6 @@ export class MultiModeDeliverySystem extends EventEmitter {
});
});
// Implement SMTP protocol here
// This is a simplified implementation
// Send EHLO
await this.smtpCommand(socket, `EHLO ${rule.mtaOptions?.domain || 'localhost'}`);
@ -445,7 +442,7 @@ export class MultiModeDeliverySystem extends EventEmitter {
// Complete the SMTP exchange
return this.completeSMTPExchange(socket, email, rule);
} catch (error) {
} catch (error: any) {
logger.log('error', `Failed to forward email: ${error.message}`);
// Close the connection
@ -511,10 +508,6 @@ export class MultiModeDeliverySystem extends EventEmitter {
// Close the connection
socket.destroy();
throw error;
}
socket.destroy();
throw error;
}
}
@ -554,7 +547,7 @@ export class MultiModeDeliverySystem extends EventEmitter {
subject: email.subject,
dkimSigned: !!rule.mtaOptions?.dkimSign
};
} catch (error) {
} catch (error: any) {
logger.log('error', `Failed to process email in MTA mode: ${error.message}`);
throw error;
}
@ -633,7 +626,7 @@ export class MultiModeDeliverySystem extends EventEmitter {
scanned: !!rule.contentScanning,
transformed: !!(rule.transformations && rule.transformations.length > 0)
};
} catch (error) {
} catch (error: any) {
logger.log('error', `Failed to process email: ${error.message}`);
throw error;
}
@ -658,7 +651,7 @@ export class MultiModeDeliverySystem extends EventEmitter {
// Add headers
content += `From: ${email.from}\r\n`;
content += `To: ${email.to}\r\n`;
content += `To: ${email.to.join(', ')}\r\n`;
content += `Subject: ${email.subject}\r\n`;
// Add additional headers

View File

@ -1,6 +1,6 @@
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import { Email } from './classes.email.js';
import * as plugins from '../../plugins.js';
import * as paths from '../../paths.js';
import { Email } from '../core/classes.email.js';
import { EmailSignJob } from './classes.emailsignjob.js';
import type { MtaService } from './classes.mta.js';

View File

@ -1,4 +1,4 @@
import * as plugins from '../plugins.js';
import * as plugins from '../../plugins.js';
import type { MtaService } from './classes.mta.js';
interface Headers {

View File

@ -1,20 +1,20 @@
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import * as plugins from '../../plugins.js';
import * as paths from '../../paths.js';
import { Email } from './classes.email.js';
import { Email } from '../core/classes.email.js';
import { EmailSendJob, DeliveryStatus } from './classes.emailsendjob.js';
import { DKIMCreator } from './classes.dkimcreator.js';
import { DKIMVerifier } from './classes.dkimverifier.js';
import { SpfVerifier } from './classes.spfverifier.js';
import { DmarcVerifier } from './classes.dmarcverifier.js';
import { DKIMCreator } from '../security/classes.dkimcreator.js';
import { DKIMVerifier } from '../security/classes.dkimverifier.js';
import { SpfVerifier } from '../security/classes.spfverifier.js';
import { DmarcVerifier } from '../security/classes.dmarcverifier.js';
import { SMTPServer, type ISmtpServerOptions } from './classes.smtpserver.js';
import { DNSManager } from './classes.dnsmanager.js';
import { ApiManager } from './classes.apimanager.js';
import { DNSManager } from '../routing/classes.dnsmanager.js';
import { ApiManager } from '../services/classes.apimanager.js';
import { RateLimiter, type IRateLimitConfig } from './classes.ratelimiter.js';
import { ContentScanner } from '../security/classes.contentscanner.js';
import { IPWarmupManager } from '../deliverability/classes.ipwarmupmanager.js';
import { SenderReputationMonitor } from '../deliverability/classes.senderreputationmonitor.js';
import type { SzPlatformService } from '../platformservice.js';
import { ContentScanner } from '../../security/classes.contentscanner.js';
import { IPWarmupManager } from '../../deliverability/classes.ipwarmupmanager.js';
import { SenderReputationMonitor } from '../../deliverability/classes.senderreputationmonitor.js';
import type { SzPlatformService } from '../../platformservice.js';
/**
* Configuration options for the MTA service
@ -265,7 +265,7 @@ export class MtaService {
this.dkimCreator = new DKIMCreator(this);
this.dkimVerifier = new DKIMVerifier(this);
this.dnsManager = new DNSManager(this);
this.apiManager = new ApiManager();
// Initialize API manager later in start() method when emailService is available
// Initialize authentication verifiers
this.spfVerifier = new SpfVerifier(this);
@ -283,14 +283,15 @@ export class MtaService {
burstTokens: 5 // Allow small bursts
});
// Initialize IP warmup manager
const warmupConfig = this.config.outbound?.warmup;
this.ipWarmupManager = IPWarmupManager.getInstance({
enabled: warmupConfig?.enabled || false,
ipAddresses: warmupConfig?.ipAddresses || [],
targetDomains: warmupConfig?.targetDomains || [],
fallbackPercentage: warmupConfig?.fallbackPercentage || 50
});
// Initialize IP warmup manager with explicit config
const warmupConfig = this.config.outbound?.warmup || {};
const ipWarmupConfig = {
enabled: warmupConfig.enabled || false,
ipAddresses: warmupConfig.ipAddresses || [],
targetDomains: warmupConfig.targetDomains || [],
fallbackPercentage: warmupConfig.fallbackPercentage || 50
};
this.ipWarmupManager = IPWarmupManager.getInstance(ipWarmupConfig);
// Set active allocation policy if specified
if (warmupConfig?.allocationPolicy) {
@ -432,6 +433,9 @@ export class MtaService {
try {
console.log('Starting MTA service...');
// Initialize API manager now that emailService is available
this.apiManager = new ApiManager(this.emailService);
// Load or provision certificate
await this.loadOrProvisionCertificate();
@ -755,7 +759,7 @@ export class MtaService {
console.log(`Processing bounce notification from ${email.from}`);
// Convert to Smartmail for bounce processing
const smartmail = await email.toSmartmail();
const smartmail = await email.toSmartmailBasic();
// If we have a bounce manager available, process it
if (this.emailService?.bounceManager) {

View File

@ -1,4 +1,4 @@
import { logger } from '../logger.js';
import { logger } from '../../logger.js';
/**
* Configuration options for rate limiter

View File

@ -1,15 +1,15 @@
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import { Email } from './classes.email.js';
import * as plugins from '../../plugins.js';
import * as paths from '../../paths.js';
import { Email } from '../core/classes.email.js';
import type { MtaService } from './classes.mta.js';
import { logger } from '../logger.js';
import { logger } from '../../logger.js';
import {
SecurityLogger,
SecurityLogLevel,
SecurityEventType,
IPReputationChecker,
ReputationThreshold
} from '../security/index.js';
} from '../../security/index.js';
export interface ISmtpServerOptions {
port: number;

View File

@ -1,7 +1,7 @@
import * as plugins from '../plugins.js';
import * as plugins from '../../plugins.js';
import { EventEmitter } from 'node:events';
import { logger } from '../logger.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../security/index.js';
import { logger } from '../../logger.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js';
/**
* Interface for rate limit configuration

18
ts/mail/delivery/index.ts Normal file
View File

@ -0,0 +1,18 @@
// Email delivery components
export * from './classes.mta.js';
export * from './classes.smtpserver.js';
export * from './classes.emailsignjob.js';
export * from './classes.delivery.queue.js';
export * from './classes.delivery.system.js';
// Handle exports with naming conflicts
export { EmailSendJob } from './classes.emailsendjob.js';
export { DeliveryStatus } from './classes.connector.mta.js';
export { MtaConnector } from './classes.connector.mta.js';
// Rate limiter exports - fix naming conflict
export { RateLimiter } from './classes.ratelimiter.js';
export type { IRateLimitConfig } from './classes.ratelimiter.js';
// Unified rate limiter
export * from './classes.unified.rate.limiter.js';

29
ts/mail/index.ts Normal file
View File

@ -0,0 +1,29 @@
// Export all mail modules for simplified imports
export * from './routing/index.js';
export * from './security/index.js';
export * from './services/index.js';
// Make the core and delivery modules accessible
import * as Core from './core/index.js';
import * as Delivery from './delivery/index.js';
export { Core, Delivery };
// For backward compatibility
import { Email } from './core/classes.email.js';
import { EmailService } from './services/classes.emailservice.js';
import { BounceManager, BounceType, BounceCategory } from './core/classes.bouncemanager.js';
import { EmailValidator } from './core/classes.emailvalidator.js';
import { TemplateManager } from './core/classes.templatemanager.js';
import { RuleManager } from './core/classes.rulemanager.js';
import { ApiManager } from './services/classes.apimanager.js';
import { MtaService } from './delivery/classes.mta.js';
import { DcRouter } from '../classes.dcrouter.js';
// Re-export with compatibility names
export {
EmailService as Email, // For backward compatibility with email/index.ts
ApiManager,
Email as EmailClass, // Provide the actual Email class under a different name
DcRouter
};

View File

@ -1,6 +1,6 @@
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import type { MtaService } from './classes.mta.js';
import * as plugins from '../../plugins.js';
import * as paths from '../../paths.js';
import type { MtaService } from '../delivery/classes.mta.js';
/**
* Interface for DNS record information

View File

@ -1,4 +1,4 @@
import * as plugins from '../plugins.js';
import * as plugins from '../../plugins.js';
import { EventEmitter } from 'node:events';
import { type IDomainRule, type EmailProcessingMode } from './classes.email.config.js';

View File

@ -1,4 +1,4 @@
import * as plugins from '../plugins.js';
import * as plugins from '../../plugins.js';
/**
* Email processing modes

View File

@ -1,23 +1,23 @@
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import * as plugins from '../../plugins.js';
import * as paths from '../../paths.js';
import { EventEmitter } from 'events';
import { logger } from '../logger.js';
import { logger } from '../../logger.js';
import {
SecurityLogger,
SecurityLogLevel,
SecurityEventType
} from '../security/index.js';
} from '../../security/index.js';
import { DomainRouter } from './classes.domain.router.js';
import type {
IEmailConfig,
EmailProcessingMode,
IDomainRule
} from './classes.email.config.js';
import { Email } from '../mta/classes.email.js';
import { Email } from '../core/classes.email.js';
import * as net from 'node:net';
import * as tls from 'node:tls';
import * as stream from 'node:stream';
import { SMTPServer as MtaSmtpServer } from '../mta/classes.smtpserver.js';
import { SMTPServer as MtaSmtpServer } from '../delivery/classes.smtpserver.js';
/**
* Options for the unified email server

5
ts/mail/routing/index.ts Normal file
View File

@ -0,0 +1,5 @@
// Email routing components
export * from './classes.domain.router.js';
export * from './classes.email.config.js';
export * from './classes.unified.email.server.js';
export * from './classes.dnsmanager.js';

View File

@ -1,8 +1,8 @@
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import * as plugins from '../../plugins.js';
import * as paths from '../../paths.js';
import { Email } from './classes.email.js';
import type { MtaService } from './classes.mta.js';
import { Email } from '../core/classes.email.js';
import type { MtaService } from '../delivery/classes.mta.js';
const readFile = plugins.util.promisify(plugins.fs.readFile);
const writeFile = plugins.util.promisify(plugins.fs.writeFile);

View File

@ -1,7 +1,7 @@
import * as plugins from '../plugins.js';
import { MtaService } from './classes.mta.js';
import { logger } from '../logger.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../security/index.js';
import * as plugins from '../../plugins.js';
import { MtaService } from '../delivery/classes.mta.js';
import { logger } from '../../logger.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js';
/**
* Result of a DKIM verification

View File

@ -1,9 +1,9 @@
import * as plugins from '../plugins.js';
import { logger } from '../logger.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../security/index.js';
import type { MtaService } from './classes.mta.js';
import type { Email } from './classes.email.js';
import type { IDnsVerificationResult } from './classes.dnsmanager.js';
import * as plugins from '../../plugins.js';
import { logger } from '../../logger.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js';
import type { MtaService } from '../delivery/classes.mta.js';
import type { Email } from '../core/classes.email.js';
import type { IDnsVerificationResult } from '../routing/classes.dnsmanager.js';
/**
* DMARC policy types

View File

@ -1,9 +1,9 @@
import * as plugins from '../plugins.js';
import { logger } from '../logger.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../security/index.js';
import type { MtaService } from './classes.mta.js';
import type { Email } from './classes.email.js';
import type { IDnsVerificationResult } from './classes.dnsmanager.js';
import * as plugins from '../../plugins.js';
import { logger } from '../../logger.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js';
import type { MtaService } from '../delivery/classes.mta.js';
import type { Email } from '../core/classes.email.js';
import type { IDnsVerificationResult } from '../routing/classes.dnsmanager.js';
/**
* SPF result qualifiers

View File

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

View File

@ -1,6 +1,6 @@
import * as plugins from '../plugins.js';
import * as plugins from '../../plugins.js';
import { EmailService } from './classes.emailservice.js';
import { logger } from '../logger.js';
import { logger } from '../../logger.js';
export class ApiManager {
public emailRef: EmailService;
@ -65,14 +65,24 @@ export class ApiManager {
new plugins.typedrequest.TypedHandler('checkEmailStatus', async (requestData) => {
// If MTA is enabled, use it to check status
if (this.emailRef.mtaConnector) {
const status = await this.emailRef.mtaConnector.checkEmailStatus(requestData.emailId);
return status;
const detailedStatus = await this.emailRef.mtaConnector.checkEmailStatus(requestData.emailId);
// Convert to the expected API response format
const apiResponse: plugins.servezoneInterfaces.platformservice.mta.IReq_CheckEmailStatus['response'] = {
status: detailedStatus.status.toString(), // Convert enum to string
details: {
message: detailedStatus.details?.message ||
(detailedStatus.details?.error ? `Error: ${detailedStatus.details.error}` :
`Status: ${detailedStatus.status}`)
}
};
return apiResponse;
}
// For Mailgun, we don't have a status check implementation currently
// Status tracking not available if MTA is not configured
return {
status: 'unknown',
details: { message: 'Status tracking not available for current provider' }
details: { message: 'Status tracking not available without MTA configuration' }
};
})
);

View File

@ -0,0 +1,312 @@
import * as plugins from '../../plugins.js';
import * as paths from '../../paths.js';
import { MtaConnector } from '../delivery/classes.connector.mta.js';
import { RuleManager } from '../core/classes.rulemanager.js';
import { ApiManager } from './classes.apimanager.js';
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 type { SzPlatformService } from '../../platformservice.js';
// Import MTA service
import { MtaService } from '../delivery/classes.mta.js';
// Import configuration interfaces
import type { IEmailConfig } from '../../config/email.config.js';
import { ConfigValidator, emailConfigSchema } from '../../config/index.js';
/**
* Options for sending an email
* @see ISendEmailOptions in MtaConnector
*/
export type ISendEmailOptions = import('../delivery/classes.connector.mta.js').ISendEmailOptions;
/**
* Template context data for email templates
* @see ITemplateContext in TemplateManager
*/
export type ITemplateContext = import('../core/classes.templatemanager.js').ITemplateContext;
/**
* Validation options for email addresses
* Compatible with EmailValidator.validate options
*/
export interface IValidateEmailOptions {
/** Check MX records for the domain */
checkMx?: boolean;
/** Check if the domain is disposable (temporary email) */
checkDisposable?: boolean;
/** Check if the email is a role account (e.g., info@, support@) */
checkRole?: boolean;
/** Only check syntax without DNS lookups */
checkSyntaxOnly?: boolean;
}
/**
* Result of email validation
* @see IEmailValidationResult from EmailValidator
*/
export type IValidationResult = import('../core/classes.emailvalidator.js').IEmailValidationResult;
/**
* Email service statistics
*/
export interface IEmailServiceStats {
/** Active email providers */
activeProviders: string[];
/** MTA statistics, if MTA is active */
mta?: {
/** Service start time */
startTime: Date;
/** Total emails received */
emailsReceived: number;
/** Total emails sent */
emailsSent: number;
/** Total emails that failed to send */
emailsFailed: number;
/** Active SMTP connections */
activeConnections: number;
/** Current email queue size */
queueSize: number;
/** Certificate information */
certificateInfo?: {
/** Domain for the certificate */
domain: string;
/** Certificate expiration date */
expiresAt: Date;
/** Days until certificate expires */
daysUntilExpiry: number;
};
/** IP warmup information */
warmupInfo?: {
/** Whether IP warmup is enabled */
enabled: boolean;
/** Number of active IPs */
activeIPs: number;
/** Number of IPs in warmup phase */
inWarmupPhase: number;
/** Number of IPs that completed warmup */
completedWarmup: number;
};
/** Reputation monitoring information */
reputationInfo?: {
/** Whether reputation monitoring is enabled */
enabled: boolean;
/** Number of domains being monitored */
monitoredDomains: number;
/** Average reputation score across domains */
averageScore: number;
/** Number of domains with reputation issues */
domainsWithIssues: number;
};
/** Rate limiting information */
rateLimiting?: {
/** Global rate limit statistics */
global: {
/** Current available tokens */
availableTokens: number;
/** Maximum tokens per period */
maxTokens: number;
/** Current consumption rate */
consumptionRate: number;
/** Number of rate limiting events */
rateLimitEvents: number;
};
};
};
}
/**
* Email service with MTA support
*/
export class EmailService {
public platformServiceRef: SzPlatformService;
// typedrouter
public typedrouter = new plugins.typedrequest.TypedRouter();
// connectors
public mtaConnector: MtaConnector;
public qenv = new plugins.qenv.Qenv('./', '.nogit/');
// MTA service
public mtaService: MtaService;
// services
public apiManager: ApiManager;
public ruleManager: RuleManager;
public templateManager: TemplateManager;
public emailValidator: EmailValidator;
public bounceManager: BounceManager;
// configuration
private config: IEmailConfig;
constructor(platformServiceRefArg: SzPlatformService, options: IEmailConfig = {}) {
this.platformServiceRef = platformServiceRefArg;
this.platformServiceRef.typedrouter.addTypedRouter(this.typedrouter);
// Validate and apply defaults to configuration
const validationResult = ConfigValidator.validate(options, emailConfigSchema);
if (!validationResult.valid) {
logger.warn(`Email service configuration has validation errors: ${validationResult.errors.join(', ')}`);
}
// Set configuration with defaults
this.config = validationResult.config;
// Initialize validator
this.emailValidator = new EmailValidator();
// Initialize bounce manager
this.bounceManager = new BounceManager();
// Initialize template manager
this.templateManager = new TemplateManager(this.config.templateConfig);
if (this.config.useMta) {
// Initialize MTA service
this.mtaService = new MtaService(platformServiceRefArg, this.config.mtaConfig);
// Initialize MTA connector
this.mtaConnector = new MtaConnector(this);
}
// Initialize API manager and rule manager
this.apiManager = new ApiManager(this);
this.ruleManager = new RuleManager(this);
// Set up MTA SMTP server webhook if using MTA
if (this.config.useMta) {
// The MTA SMTP server will handle incoming emails directly
// through its SMTP protocol. No additional webhook needed.
}
}
/**
* Start the email service
*/
public async start() {
// Initialize rule manager
await this.ruleManager.init();
// Load email templates if configured
if (this.config.loadTemplatesFromDir) {
try {
await this.templateManager.loadTemplatesFromDirectory(paths.emailTemplatesDir);
} catch (error) {
logger.log('error', `Failed to load email templates: ${error.message}`);
}
}
// Start MTA service if enabled
if (this.config.useMta && this.mtaService) {
await this.mtaService.start();
logger.log('success', 'Started MTA service');
}
logger.log('success', `Started email service`);
}
/**
* Stop the email service
*/
public async stop() {
// Stop MTA service if it's running
if (this.config.useMta && this.mtaService) {
await this.mtaService.stop();
logger.log('info', 'Stopped MTA service');
}
logger.log('info', 'Stopped email service');
}
/**
* Send an email using the MTA
* @param email The email to send
* @param to Recipient(s)
* @param options Additional options
*/
public async sendEmail(
email: plugins.smartmail.Smartmail<any>,
to: string | string[],
options: ISendEmailOptions = {}
): Promise<string> {
// Determine which connector to use
if (this.config.useMta && this.mtaConnector) {
return this.mtaConnector.sendEmail(email, to, options);
} else {
throw new Error('MTA not configured');
}
}
/**
* Send an email using a template
* @param templateId The template ID
* @param to Recipient email(s)
* @param context The template context data
* @param options Additional options
*/
public async sendTemplateEmail(
templateId: string,
to: string | string[],
context: ITemplateContext = {},
options: ISendEmailOptions = {}
): Promise<string> {
try {
// Get email from template
const smartmail = await this.templateManager.prepareEmail(templateId, context);
// Send the email
return this.sendEmail(smartmail, to, options);
} catch (error) {
logger.log('error', `Failed to send template email: ${error.message}`, {
templateId,
to,
error: error.message
});
throw error;
}
}
/**
* Validate an email address
* @param email The email address to validate
* @param options Validation options
* @returns Validation result
*/
public async validateEmail(
email: string,
options: IValidateEmailOptions = {}
): Promise<IValidationResult> {
return this.emailValidator.validate(email, options);
}
/**
* Get email service statistics
* @returns Service statistics in the format expected by the API
*/
public getStats(): plugins.servezoneInterfaces.platformservice.mta.IReq_GetEMailStats['response'] {
// First generate detailed internal stats
const detailedStats: IEmailServiceStats = {
activeProviders: []
};
if (this.config.useMta) {
detailedStats.activeProviders.push('mta');
detailedStats.mta = this.mtaService.getStats();
}
// Convert detailed stats to the format expected by the API
const apiStats: plugins.servezoneInterfaces.platformservice.mta.IReq_GetEMailStats['response'] = {
totalEmailsSent: detailedStats.mta?.emailsSent || 0,
totalEmailsDelivered: detailedStats.mta?.emailsSent || 0, // Default to emails sent if we don't track delivery separately
totalEmailsBounced: detailedStats.mta?.emailsFailed || 0,
averageDeliveryTimeMs: 0, // We don't track this yet
lastUpdated: new Date().toISOString()
};
return apiStats;
}
}

View File

@ -0,0 +1,3 @@
// Email services
export * from './classes.emailservice.js';
export * from './classes.apimanager.js';

View File

@ -1,956 +0,0 @@
import * as plugins from '../plugins.js';
import { Email } from './classes.email.js';
import type { IEmailOptions } from './classes.email.js';
import { DeliveryStatus } from './classes.emailsendjob.js';
import type { MtaService } from './classes.mta.js';
import type { IDnsRecord } from './classes.dnsmanager.js';
/**
* Authentication options for API requests
*/
interface AuthOptions {
/** Required API keys for different endpoints */
apiKeys: Map<string, string[]>;
/** JWT secret for token-based authentication */
jwtSecret?: string;
/** Whether to validate IP addresses */
validateIp?: boolean;
/** Allowed IP addresses */
allowedIps?: string[];
}
/**
* Rate limiting options for API endpoints
*/
interface RateLimitOptions {
/** Maximum requests per window */
maxRequests: number;
/** Time window in milliseconds */
windowMs: number;
/** Whether to apply per endpoint */
perEndpoint?: boolean;
/** Whether to apply per IP */
perIp?: boolean;
}
/**
* API route definition
*/
interface ApiRoute {
/** HTTP method */
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
/** Path pattern */
path: string;
/** Handler function */
handler: (req: any, res: any) => Promise<any>;
/** Required authentication level */
authLevel: 'none' | 'basic' | 'admin';
/** Rate limiting options */
rateLimit?: RateLimitOptions;
/** Route description */
description?: string;
}
/**
* Email send request
*/
interface SendEmailRequest {
/** Email details */
email: IEmailOptions;
/** Whether to validate domains before sending */
validateDomains?: boolean;
/** Priority level (1-5, 1 = highest) */
priority?: number;
}
/**
* Email status response
*/
interface EmailStatusResponse {
/** Email ID */
id: string;
/** Current status */
status: DeliveryStatus;
/** Send time */
sentAt?: Date;
/** Delivery time */
deliveredAt?: Date;
/** Error message if failed */
error?: string;
/** Recipient address */
recipient: string;
/** Number of delivery attempts */
attempts: number;
/** Next retry time */
nextRetry?: Date;
}
/**
* Domain verification response
*/
interface DomainVerificationResponse {
/** Domain name */
domain: string;
/** Whether the domain is verified */
verified: boolean;
/** Verification details */
details: {
/** SPF record status */
spf: {
valid: boolean;
record?: string;
error?: string;
};
/** DKIM record status */
dkim: {
valid: boolean;
record?: string;
error?: string;
};
/** DMARC record status */
dmarc: {
valid: boolean;
record?: string;
error?: string;
};
/** MX record status */
mx: {
valid: boolean;
records?: string[];
error?: string;
};
};
}
/**
* API error response
*/
interface ApiError {
/** Error code */
code: string;
/** Error message */
message: string;
/** Detailed error information */
details?: any;
}
/**
* Simple HTTP Response helper
*/
class HttpResponse {
private headers: Record<string, string> = {
'Content-Type': 'application/json'
};
public statusCode: number = 200;
constructor(private res: any) {}
header(name: string, value: string): HttpResponse {
this.headers[name] = value;
return this;
}
status(code: number): HttpResponse {
this.statusCode = code;
return this;
}
json(data: any): void {
this.res.writeHead(this.statusCode, this.headers);
this.res.end(JSON.stringify(data));
}
end(): void {
this.res.writeHead(this.statusCode, this.headers);
this.res.end();
}
}
/**
* API Manager for MTA service
*/
export class ApiManager {
/** TypedRouter for API routing */
public typedrouter = new plugins.typedrequest.TypedRouter();
/** MTA service reference */
private mtaRef: MtaService;
/** HTTP server */
private server: any;
/** Authentication options */
private authOptions: AuthOptions;
/** API routes */
private routes: ApiRoute[] = [];
/** Rate limiters */
private rateLimiters: Map<string, {
count: number;
resetTime: number;
clients: Map<string, {
count: number;
resetTime: number;
}>;
}> = new Map();
/**
* Initialize API Manager
* @param mtaRef MTA service reference
*/
constructor(mtaRef?: MtaService) {
this.mtaRef = mtaRef;
// Default authentication options
this.authOptions = {
apiKeys: new Map(),
validateIp: false,
allowedIps: []
};
// Register routes
this.registerRoutes();
// Create HTTP server with request handler
this.server = plugins.http.createServer(this.handleRequest.bind(this));
}
/**
* Set MTA service reference
* @param mtaRef MTA service reference
*/
public setMtaService(mtaRef: MtaService): void {
this.mtaRef = mtaRef;
}
/**
* Configure authentication options
* @param options Authentication options
*/
public configureAuth(options: Partial<AuthOptions>): void {
this.authOptions = {
...this.authOptions,
...options
};
}
/**
* Handle HTTP request
*/
private async handleRequest(req: any, res: any): Promise<void> {
const start = Date.now();
// Create a response helper
const response = new HttpResponse(res);
// Add CORS headers
response.header('Access-Control-Allow-Origin', '*');
response.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
response.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization, X-API-Key');
// Handle preflight OPTIONS request
if (req.method === 'OPTIONS') {
return response.status(200).end();
}
try {
// Parse URL to get path and query
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
const path = url.pathname;
// Collect request body if POST or PUT
let body = '';
if (req.method === 'POST' || req.method === 'PUT') {
await new Promise<void>((resolve, reject) => {
req.on('data', (chunk: Buffer) => {
body += chunk.toString();
});
req.on('end', () => {
resolve();
});
req.on('error', (err: Error) => {
reject(err);
});
});
// Parse body as JSON if Content-Type is application/json
const contentType = req.headers['content-type'] || '';
if (contentType.includes('application/json')) {
try {
req.body = JSON.parse(body);
} catch (error) {
return response.status(400).json({
code: 'INVALID_JSON',
message: 'Invalid JSON in request body'
});
}
} else {
req.body = body;
}
}
// Add authentication level to request
req.authLevel = 'none';
// Check API key
const apiKey = req.headers['x-api-key'];
if (apiKey) {
for (const [level, keys] of this.authOptions.apiKeys.entries()) {
if (keys.includes(apiKey)) {
req.authLevel = level;
break;
}
}
}
// Check JWT token (if configured)
if (this.authOptions.jwtSecret && req.headers.authorization) {
try {
const token = req.headers.authorization.split(' ')[1];
// Note: We would need to add JWT verification
// Using a simple placeholder for now
const decoded = { level: 'none' }; // Simplified - would use actual JWT library
if (decoded && decoded.level) {
req.authLevel = decoded.level;
req.user = decoded;
}
} catch (error) {
// Invalid token, but don't fail the request yet
console.error('Invalid JWT token:', error.message);
}
}
// Check IP address (if configured)
if (this.authOptions.validateIp) {
const clientIp = req.socket.remoteAddress;
if (!this.authOptions.allowedIps.includes(clientIp)) {
return response.status(403).json({
code: 'FORBIDDEN',
message: 'IP address not allowed'
});
}
}
// Find matching route
const route = this.findRoute(req.method, path);
if (!route) {
return response.status(404).json({
code: 'NOT_FOUND',
message: 'Endpoint not found'
});
}
// Check authentication
if (route.authLevel !== 'none' && req.authLevel !== route.authLevel && req.authLevel !== 'admin') {
return response.status(403).json({
code: 'FORBIDDEN',
message: `This endpoint requires ${route.authLevel} access`
});
}
// Check rate limit
if (route.rateLimit) {
const exceeded = this.checkRateLimit(route, req);
if (exceeded) {
return response.status(429).json({
code: 'RATE_LIMIT_EXCEEDED',
message: 'Rate limit exceeded, please try again later'
});
}
}
// Extract path parameters
const pathParams = this.extractPathParams(route.path, path);
req.params = pathParams;
// Extract query parameters
req.query = {};
for (const [key, value] of url.searchParams.entries()) {
req.query[key] = value;
}
// Handle the request
await route.handler(req, response);
// Log request
const duration = Date.now() - start;
console.log(`[API] ${req.method} ${path} ${response.statusCode} ${duration}ms`);
} catch (error) {
console.error(`Error handling request:`, error);
// Send appropriate error response
const status = error.status || 500;
const apiError: ApiError = {
code: error.code || 'INTERNAL_ERROR',
message: error.message || 'Internal server error'
};
if (process.env.NODE_ENV !== 'production') {
apiError.details = error.stack;
}
response.status(status).json(apiError);
}
}
/**
* Find a route matching the method and path
*/
private findRoute(method: string, path: string): ApiRoute | null {
for (const route of this.routes) {
if (route.method === method && this.pathMatches(route.path, path)) {
return route;
}
}
return null;
}
/**
* Check if a path matches a route pattern
*/
private pathMatches(pattern: string, path: string): boolean {
// Convert route pattern to regex
const patternParts = pattern.split('/');
const pathParts = path.split('/');
if (patternParts.length !== pathParts.length) {
return false;
}
for (let i = 0; i < patternParts.length; i++) {
if (patternParts[i].startsWith(':')) {
// Parameter - always matches
continue;
}
if (patternParts[i] !== pathParts[i]) {
return false;
}
}
return true;
}
/**
* Extract path parameters from URL
*/
private extractPathParams(pattern: string, path: string): Record<string, string> {
const params: Record<string, string> = {};
const patternParts = pattern.split('/');
const pathParts = path.split('/');
for (let i = 0; i < patternParts.length; i++) {
if (patternParts[i].startsWith(':')) {
const paramName = patternParts[i].substring(1);
params[paramName] = pathParts[i];
}
}
return params;
}
/**
* Register API routes
*/
private registerRoutes(): void {
// Email routes
this.addRoute({
method: 'POST',
path: '/api/email/send',
handler: this.handleSendEmail.bind(this),
authLevel: 'basic',
description: 'Send an email'
});
this.addRoute({
method: 'GET',
path: '/api/email/status/:id',
handler: this.handleGetEmailStatus.bind(this),
authLevel: 'basic',
description: 'Get email delivery status'
});
// Domain routes
this.addRoute({
method: 'GET',
path: '/api/domain/verify/:domain',
handler: this.handleVerifyDomain.bind(this),
authLevel: 'basic',
description: 'Verify domain DNS records'
});
this.addRoute({
method: 'GET',
path: '/api/domain/records/:domain',
handler: this.handleGetDomainRecords.bind(this),
authLevel: 'basic',
description: 'Get recommended DNS records for domain'
});
// DKIM routes
this.addRoute({
method: 'POST',
path: '/api/dkim/generate/:domain',
handler: this.handleGenerateDkim.bind(this),
authLevel: 'admin',
description: 'Generate DKIM keys for domain'
});
this.addRoute({
method: 'GET',
path: '/api/dkim/public/:domain',
handler: this.handleGetDkimPublicKey.bind(this),
authLevel: 'basic',
description: 'Get DKIM public key for domain'
});
// Stats route
this.addRoute({
method: 'GET',
path: '/api/stats',
handler: this.handleGetStats.bind(this),
authLevel: 'admin',
description: 'Get MTA statistics'
});
// Documentation route
this.addRoute({
method: 'GET',
path: '/api',
handler: this.handleGetApiDocs.bind(this),
authLevel: 'none',
description: 'API documentation'
});
}
/**
* Add an API route
* @param route Route definition
*/
private addRoute(route: ApiRoute): void {
this.routes.push(route);
}
/**
* Check rate limit for a route
* @param route Route definition
* @param req Express request
* @returns Whether rate limit is exceeded
*/
private checkRateLimit(route: ApiRoute, req: any): boolean {
if (!route.rateLimit) return false;
const { maxRequests, windowMs, perEndpoint, perIp } = route.rateLimit;
// Determine rate limit key
let key = 'global';
if (perEndpoint) {
key = `${route.method}:${route.path}`;
}
// Get or create limiter
if (!this.rateLimiters.has(key)) {
this.rateLimiters.set(key, {
count: 0,
resetTime: Date.now() + windowMs,
clients: new Map()
});
}
const limiter = this.rateLimiters.get(key);
// Reset if window has passed
if (Date.now() > limiter.resetTime) {
limiter.count = 0;
limiter.resetTime = Date.now() + windowMs;
limiter.clients.clear();
}
// Check per-IP limit if enabled
if (perIp) {
const clientIp = req.socket.remoteAddress;
let clientLimiter = limiter.clients.get(clientIp);
if (!clientLimiter) {
clientLimiter = {
count: 0,
resetTime: Date.now() + windowMs
};
limiter.clients.set(clientIp, clientLimiter);
}
// Reset client limiter if needed
if (Date.now() > clientLimiter.resetTime) {
clientLimiter.count = 0;
clientLimiter.resetTime = Date.now() + windowMs;
}
// Check client limit
if (clientLimiter.count >= maxRequests) {
return true;
}
// Increment client count
clientLimiter.count++;
} else {
// Check global limit
if (limiter.count >= maxRequests) {
return true;
}
// Increment global count
limiter.count++;
}
return false;
}
/**
* Create an API error
* @param code Error code
* @param message Error message
* @param status HTTP status code
* @param details Additional details
* @returns API error
*/
private createError(code: string, message: string, status = 400, details?: any): Error & { code: string; status: number; details?: any } {
const error = new Error(message) as Error & { code: string; status: number; details?: any };
error.code = code;
error.status = status;
if (details) {
error.details = details;
}
return error;
}
/**
* Validate that MTA service is available
*/
private validateMtaService(): void {
if (!this.mtaRef) {
throw this.createError('SERVICE_UNAVAILABLE', 'MTA service is not available', 503);
}
}
/**
* Handle email send request
* @param req Express request
* @param res Express response
*/
private async handleSendEmail(req: any, res: any): Promise<void> {
this.validateMtaService();
const data = req.body as SendEmailRequest;
if (!data || !data.email) {
throw this.createError('INVALID_REQUEST', 'Missing email data');
}
try {
// Create Email instance
const email = new Email(data.email);
// Validate domains if requested
if (data.validateDomains) {
const fromDomain = email.getFromDomain();
if (fromDomain) {
const verification = await this.mtaRef.dnsManager.verifyEmailAuthRecords(fromDomain);
// Check if SPF and DKIM are valid
if (!verification.spf.valid || !verification.dkim.valid) {
throw this.createError('DOMAIN_VERIFICATION_FAILED', 'Domain DNS verification failed', 400, {
verification
});
}
}
}
// Send email
const id = await this.mtaRef.send(email);
// Return success response
res.json({
id,
message: 'Email queued successfully',
status: 'pending'
});
} catch (error) {
// Handle Email constructor errors
if (error.message.includes('Invalid') || error.message.includes('must have')) {
throw this.createError('INVALID_EMAIL', error.message);
}
throw error;
}
}
/**
* Handle email status request
* @param req Express request
* @param res Express response
*/
private async handleGetEmailStatus(req: any, res: any): Promise<void> {
this.validateMtaService();
const id = req.params.id;
if (!id) {
throw this.createError('INVALID_REQUEST', 'Missing email ID');
}
// Get email status
const status = this.mtaRef.getEmailStatus(id);
if (!status) {
throw this.createError('NOT_FOUND', `Email with ID ${id} not found`, 404);
}
// Create response
const response: EmailStatusResponse = {
id: status.id,
status: status.status,
sentAt: status.addedAt,
recipient: status.email.to[0],
attempts: status.attempts
};
// Add additional fields if available
if (status.lastAttempt) {
response.sentAt = status.lastAttempt;
}
if (status.status === DeliveryStatus.DELIVERED) {
response.deliveredAt = status.lastAttempt;
}
if (status.error) {
response.error = status.error.message;
}
if (status.nextAttempt) {
response.nextRetry = status.nextAttempt;
}
res.json(response);
}
/**
* Handle domain verification request
* @param req Express request
* @param res Express response
*/
private async handleVerifyDomain(req: any, res: any): Promise<void> {
this.validateMtaService();
const domain = req.params.domain;
if (!domain) {
throw this.createError('INVALID_REQUEST', 'Missing domain');
}
try {
// Verify domain DNS records
const records = await this.mtaRef.dnsManager.verifyEmailAuthRecords(domain);
// Get MX records
let mxValid = false;
let mxRecords: string[] = [];
let mxError: string = undefined;
try {
const mxResult = await this.mtaRef.dnsManager.lookupMx(domain);
mxValid = mxResult.length > 0;
mxRecords = mxResult.map(mx => mx.exchange);
} catch (error) {
mxError = error.message;
}
// Create response
const response: DomainVerificationResponse = {
domain,
verified: records.spf.valid && records.dkim.valid && records.dmarc.valid && mxValid,
details: {
spf: {
valid: records.spf.valid,
record: records.spf.value,
error: records.spf.error
},
dkim: {
valid: records.dkim.valid,
record: records.dkim.value,
error: records.dkim.error
},
dmarc: {
valid: records.dmarc.valid,
record: records.dmarc.value,
error: records.dmarc.error
},
mx: {
valid: mxValid,
records: mxRecords,
error: mxError
}
}
};
res.json(response);
} catch (error) {
throw this.createError('VERIFICATION_FAILED', `Domain verification failed: ${error.message}`);
}
}
/**
* Handle get domain records request
* @param req Express request
* @param res Express response
*/
private async handleGetDomainRecords(req: any, res: any): Promise<void> {
this.validateMtaService();
const domain = req.params.domain;
if (!domain) {
throw this.createError('INVALID_REQUEST', 'Missing domain');
}
try {
// Generate recommended DNS records
const records = await this.mtaRef.dnsManager.generateAllRecommendedRecords(domain);
res.json({
domain,
records
});
} catch (error) {
throw this.createError('GENERATION_FAILED', `DNS record generation failed: ${error.message}`);
}
}
/**
* Handle generate DKIM keys request
* @param req Express request
* @param res Express response
*/
private async handleGenerateDkim(req: any, res: any): Promise<void> {
this.validateMtaService();
const domain = req.params.domain;
if (!domain) {
throw this.createError('INVALID_REQUEST', 'Missing domain');
}
try {
// Generate DKIM keys
await this.mtaRef.dkimCreator.handleDKIMKeysForDomain(domain);
// Get DNS record
const dnsRecord = await this.mtaRef.dkimCreator.getDNSRecordForDomain(domain);
res.json({
domain,
dnsRecord,
message: 'DKIM keys generated successfully'
});
} catch (error) {
throw this.createError('GENERATION_FAILED', `DKIM generation failed: ${error.message}`);
}
}
/**
* Handle get DKIM public key request
* @param req Express request
* @param res Express response
*/
private async handleGetDkimPublicKey(req: any, res: any): Promise<void> {
this.validateMtaService();
const domain = req.params.domain;
if (!domain) {
throw this.createError('INVALID_REQUEST', 'Missing domain');
}
try {
// Get DKIM keys
const keys = await this.mtaRef.dkimCreator.readDKIMKeys(domain);
// Get DNS record
const dnsRecord = await this.mtaRef.dkimCreator.getDNSRecordForDomain(domain);
res.json({
domain,
publicKey: keys.publicKey,
dnsRecord
});
} catch (error) {
throw this.createError('NOT_FOUND', `DKIM keys not found for domain: ${domain}`, 404);
}
}
/**
* Handle get stats request
* @param req Express request
* @param res Express response
*/
private async handleGetStats(req: any, res: any): Promise<void> {
this.validateMtaService();
// Get MTA stats
const stats = this.mtaRef.getStats();
res.json(stats);
}
/**
* Handle get API docs request
* @param req Express request
* @param res Express response
*/
private async handleGetApiDocs(req: any, res: any): Promise<void> {
// Generate API documentation
const docs = {
name: 'MTA API',
version: '1.0.0',
description: 'API for interacting with the MTA service',
endpoints: this.routes.map(route => ({
method: route.method,
path: route.path,
description: route.description,
authLevel: route.authLevel
}))
};
res.json(docs);
}
/**
* Start the API server
* @param port Port to listen on
* @returns Promise that resolves when server is started
*/
public start(port: number = 3000): Promise<void> {
return new Promise((resolve, reject) => {
try {
// Start HTTP server
this.server.listen(port, () => {
console.log(`API server listening on port ${port}`);
resolve();
});
} catch (error) {
console.error('Failed to start API server:', error);
reject(error);
}
});
}
/**
* Stop the API server
*/
public stop(): void {
if (this.server) {
this.server.close();
console.log('API server stopped');
}
}
}

View File

@ -1,10 +0,0 @@
export * from './classes.dkimcreator.js';
export * from './classes.emailsignjob.js';
export * from './classes.dkimverifier.js';
export * from './classes.dmarcverifier.js';
export * from './classes.spfverifier.js';
export * from './classes.mta.js';
export * from './classes.smtpserver.js';
export * from './classes.emailsendjob.js';
export * from './classes.email.js';
export * from './classes.ratelimiter.js';

View File

@ -25,6 +25,11 @@ export const logsDir = plugins.path.join(dataDir, 'logs'); // For logs
export const emailTemplatesDir = plugins.path.join(dataDir, 'templates', 'email');
export const MtaAttachmentsDir = plugins.path.join(dataDir, 'attachments'); // For email attachments
// Configuration path
export const configPath = process.env.CONFIG_PATH
? process.env.CONFIG_PATH
: plugins.path.join(baseDir, 'config.json');
// Create directories if they don't exist
export function ensureDirectories() {
// Ensure data directories

View File

@ -1,9 +1,12 @@
import * as plugins from './plugins.js';
import * as paths from './paths.js';
import { PlatformServiceDb } from './classes.platformservicedb.js'
import { EmailService } from './email/classes.emailservice.js';
import { EmailService } from './mail/services/classes.emailservice.js';
import { SmsService } from './sms/classes.smsservice.js';
import { MtaService } from './mta/classes.mta.js';
import { MtaService } from './mail/delivery/classes.mta.js';
import { logger } from './logger.js';
import { type IPlatformConfig } from './config/index.js';
import { ConfigurationError } from './errors/base.errors.js';
export class SzPlatformService {
public projectinfo: plugins.projectinfo.ProjectInfo;
@ -17,29 +20,169 @@ export class SzPlatformService {
public emailService: EmailService;
public mtaService: MtaService;
public smsService: SmsService;
// Platform configuration
public config: IPlatformConfig;
public async start() {
this.platformserviceDb = new PlatformServiceDb(this);
/**
* Create a new platform service instance
*
* @param config Optional platform configuration
*/
constructor(config: IPlatformConfig) {
// Store configuration
this.config = config;
// Initialize typed router
this.typedrouter = new plugins.typedrequest.TypedRouter();
}
/**
* Initialize the platform service
* Applies configuration provided in constructor
*/
public async initialize(): Promise<void> {
// Simple validation of config - must be provided
if (!this.config) {
throw new ConfigurationError(
'Platform configuration must be provided in constructor',
'PLATFORM_CONFIG_MISSING',
{}
);
}
// Apply configuration to logger
if (this.config.logging) {
logger.setContext({
environment: this.config.environment,
component: 'PlatformService'
});
}
// Create project info
this.projectinfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
// lets start the sub services
this.emailService = new EmailService(this);
this.mtaService = new MtaService(this);
this.smsService = new SmsService(this, {
apiGatewayApiToken: await this.serviceQenv.getEnvVarOnDemand('SMS_API_TOKEN'),
});
// Initialize database
this.platformserviceDb = new PlatformServiceDb(this);
// lets start the server finally
this.typedserver = new plugins.typedserver.TypedServer({
cors: true,
});
await this.typedserver.start();
logger.info('Platform service initialized successfully');
}
/**
* Start the platform service
*/
public async start(): Promise<void> {
// Initialize first if needed
if (!this.config) {
await this.initialize();
}
// Check if service is enabled
if (this.config.enabled === false) {
logger.warn('Platform service is disabled in configuration, not starting services');
return;
}
logger.info('Starting platform service...');
// Initialize sub-services
await this.initializeServices();
// Start the HTTP server
await this.startServer();
logger.info('Platform service started successfully');
}
public async stop() {
/**
* Initialize and start sub-services
*/
private async initializeServices(): Promise<void> {
// Initialize email service
if (this.config.email?.enabled !== false) {
this.emailService = new EmailService(this, this.config.email);
await this.emailService.start();
logger.info('Email service started');
// Initialize MTA service if needed
if (this.config.email?.useMta) {
this.mtaService = new MtaService(this, this.config.email.mtaConfig);
logger.info('MTA service initialized');
}
} else {
logger.info('Email service disabled in configuration');
}
// Initialize SMS service
if (this.config.sms?.enabled !== false) {
// Get API token from config or env var
const apiToken = this.config.sms?.apiGatewayApiToken ||
await this.serviceQenv.getEnvVarOnDemand('SMS_API_TOKEN');
if (!apiToken) {
logger.warn('No SMS API token provided, SMS service will not be started');
} else {
this.smsService = new SmsService(this, {
apiGatewayApiToken: apiToken,
...this.config.sms
});
await this.smsService.start();
logger.info('SMS service started');
}
} else {
logger.info('SMS service disabled in configuration');
}
}
/**
* Start the HTTP server
*/
private async startServer(): Promise<void> {
// Check if server is enabled
if (this.config.server?.enabled === false) {
logger.info('HTTP server disabled in configuration');
return;
}
// Create server with configuration
this.typedserver = new plugins.typedserver.TypedServer({
cors: this.config.server?.cors === false ? false : true,
port: this.config.server?.port || 3000,
// hostname is not supported directly, will be set during start
});
// Add the router
// Note: Using any type to bypass TypeScript restriction
(this.typedserver as any).addRouter(this.typedrouter);
// Start server
await this.typedserver.start();
logger.info(`HTTP server started on ${this.config.server?.host || '0.0.0.0'}:${this.config.server?.port || 3000}`);
}
/**
* Stop the platform service
*/
public async stop(): Promise<void> {
logger.info('Stopping platform service...');
// Stop sub-services
if (this.emailService) {
await this.emailService.stop();
logger.info('Email service stopped');
}
if (this.smsService) {
await this.smsService.stop();
logger.info('SMS service stopped');
}
// Stop the server if it's running
if (this.typedserver) {
await this.typedserver.stop();
logger.info('HTTP server stopped');
}
logger.info('Platform service stopped successfully');
}
}

View File

@ -55,6 +55,9 @@ import * as smartrx from '@push.rocks/smartrx';
export { projectinfo, qenv, smartacme, smartdata, smartdns, smartfile, smartlog, smartmail, smartpath, smartproxy, smartpromise, smartrequest, smartrule, smartrx };
// Define SmartLog types for use in error handling
export type TLogLevel = 'error' | 'warn' | 'info' | 'success' | 'debug';
// apiclient.xyz scope
import * as cloudflare from '@apiclient.xyz/cloudflare';

View File

@ -1,8 +1,8 @@
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import { logger } from '../logger.js';
import { Email } from '../mta/classes.email.js';
import type { IAttachment } from '../mta/classes.email.js';
import { Email } from '../mail/core/classes.email.js';
import type { IAttachment } from '../mail/core/classes.email.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from './classes.securitylogger.js';
import { LRUCache } from 'lru-cache';

View File

@ -3,19 +3,29 @@ import * as paths from '../paths.js';
import { logger } from '../logger.js';
import type { SzPlatformService } from '../platformservice.js';
export interface ISmsConstructorOptions {
apiGatewayApiToken: string;
}
import type { ISmsConfig } from '../config/sms.config.js';
import { ConfigValidator, smsConfigSchema } from '../config/index.js';
export class SmsService {
public platformServiceRef: SzPlatformService;
public projectinfo: plugins.projectinfo.ProjectInfo;
public typedrouter = new plugins.typedrequest.TypedRouter();
public options: ISmsConstructorOptions;
public config: ISmsConfig;
constructor(platformServiceRefArg: SzPlatformService, optionsArg: ISmsConstructorOptions) {
constructor(platformServiceRefArg: SzPlatformService, options: ISmsConfig) {
this.platformServiceRef = platformServiceRefArg;
this.options = optionsArg;
// Validate and apply defaults to configuration
const validationResult = ConfigValidator.validate(options, smsConfigSchema);
if (!validationResult.valid) {
logger.warn(`SMS service configuration has validation errors: ${validationResult.errors.join(', ')}`);
}
// Set configuration with defaults
this.config = validationResult.config;
// Add router to platform service
this.platformServiceRef.typedrouter.addTypedRouter(this.typedrouter);
}
@ -53,8 +63,11 @@ export class SmsService {
}
public async sendSms(toNumber: number, fromName: string, messageText: string) {
// Use default sender if not specified
const sender = fromName || this.config.defaultSender || 'PlatformService';
const payload = {
sender: fromName,
sender,
message: messageText,
recipients: [{ msisdn: toNumber }],
};
@ -63,7 +76,7 @@ export class SmsService {
method: 'POST',
requestBody: JSON.stringify(payload),
headers: {
Authorization: `Basic ${Buffer.from(`${this.options.apiGatewayApiToken}:`).toString('base64')}`,
Authorization: `Basic ${Buffer.from(`${this.config.apiGatewayApiToken}:`).toString('base64')}`,
'Content-Type': 'application/json',
},
});

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/platformservice',
version: '2.8.0',
version: '2.11.1',
description: 'A multifaceted platform service handling mail, SMS, letter delivery, and AI services.'
}