feat(wire): Add wire protocol, WireTarget & WireParser, Smartmail JSON serialization; refactor plugins and update dependencies
This commit is contained in:
13
changelog.md
13
changelog.md
@@ -1,5 +1,18 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-11-29 - 2.2.0 - feat(wire)
|
||||
Add wire protocol, WireTarget & WireParser, Smartmail JSON serialization; refactor plugins and update dependencies
|
||||
|
||||
- Introduce a wire protocol (ts/smartmail.wire.ts) with typed message interfaces and helper utils (createMessageId, createTimestamp) for microservice communication.
|
||||
- Add WireTarget (ts/smartmail.classes.wiretarget.ts) to send wire messages to an endpoint (sendEmail, updateSettings, listMailbox, fetchEmail, getStatus).
|
||||
- Add WireParser (ts/smartmail.classes.wireparser.ts) to parse and handle incoming wire messages on the SMTP/service side with handler callbacks.
|
||||
- Add Smartmail JSON serialization/deserialization and transmission helpers (toObject, toJson, fromObject, fromJson, sendTo) and attachment base64 handling in Smartmail (ts/smartmail.classes.smartmail.ts).
|
||||
- Refactor plugin exports (ts/smartmail.plugins.ts) to export node fs/path and to use SmartRequest default export (SmartRequest.create()).
|
||||
- Update EmailAddressValidator.fetchDomains to use plugins.fs.readFileSync with encoding and the new SmartRequest create()/get().json() flow, falling back to local domains file on error.
|
||||
- Bump devDependencies and dependencies in package.json to newer versions and change test script to be verbose.
|
||||
- Adjust tsconfig.json: disable sourceMap and enable skipLibCheck (skipLibCheck: true).
|
||||
- Export newly added wire modules from package index (ts/index.ts).
|
||||
|
||||
## 2025-05-07 - 2.1.0 - feat(smartmail)
|
||||
Add new email validation helper methods (getMxRecords, isDisposableEmail, isRoleAccount) and an applyVariables method to Smartmail for dynamic templating.
|
||||
|
||||
|
||||
18
package.json
18
package.json
@@ -9,24 +9,22 @@
|
||||
"author": "Lossless GmbH",
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"test": "(tstest test/)",
|
||||
"format": "(gitzone format)",
|
||||
"test": "(tstest test/ --verbose)",
|
||||
"build": "(tsbuild tsfolders --allowimplicitany)",
|
||||
"buildDocs": "tsdoc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^2.3.2",
|
||||
"@git.zone/tsrun": "^1.3.3",
|
||||
"@git.zone/tstest": "^1.0.96",
|
||||
"@push.rocks/tapbundle": "^6.0.3",
|
||||
"@git.zone/tsbuild": "^3.1.2",
|
||||
"@git.zone/tsrun": "^2.0.0",
|
||||
"@git.zone/tstest": "^3.1.3",
|
||||
"@types/node": "^22.15.14"
|
||||
},
|
||||
"dependencies": {
|
||||
"@push.rocks/smartdns": "^6.2.2",
|
||||
"@push.rocks/smartfile": "^11.2.0",
|
||||
"@push.rocks/smartdns": "^7.6.1",
|
||||
"@push.rocks/smartfile": "^13.0.1",
|
||||
"@push.rocks/smartmustache": "^3.0.2",
|
||||
"@push.rocks/smartpath": "^5.0.11",
|
||||
"@push.rocks/smartrequest": "^2.0.18"
|
||||
"@push.rocks/smartpath": "^6.0.0",
|
||||
"@push.rocks/smartrequest": "^5.0.1"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
|
||||
4941
pnpm-lock.yaml
generated
4941
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
488
readme.md
488
readme.md
@@ -1,48 +1,71 @@
|
||||
# @push.rocks/smartmail
|
||||
A unified format for representing and dealing with emails
|
||||
|
||||
A unified format for representing and dealing with emails, with support for attachments, email validation, dynamic templating, and wire format serialization for microservice communication.
|
||||
|
||||
## Issue Reporting and Security
|
||||
|
||||
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
||||
|
||||
## Install
|
||||
|
||||
To install `@push.rocks/smartmail`, you'll need Node.js installed on your system. With Node.js installed, run the following command in your terminal:
|
||||
|
||||
```bash
|
||||
pnpm add @push.rocks/smartmail
|
||||
```
|
||||
|
||||
This will add `@push.rocks/smartmail` to your project's dependencies.
|
||||
|
||||
## Features
|
||||
|
||||
- **Advanced Email Address Validation**: Validate email format, check for disposable/free domains, and verify MX records
|
||||
- **Rich Email Representation**: Create emails with multiple recipients, attachments, HTML content, and custom headers
|
||||
- **Template Support**: Use mustache templates for dynamic content in subject, body, and HTML
|
||||
- **MIME Formatting**: Convert emails to standard MIME format for sending
|
||||
- **Caching & Performance**: Smart caching of DNS lookups for better performance
|
||||
- 📧 **Advanced Email Address Validation** - Validate email format, check for disposable/free domains, verify MX records, and detect role accounts
|
||||
- 📝 **Rich Email Representation** - Create emails with multiple recipients (to/cc/bcc), attachments, HTML content, and custom headers
|
||||
- 🎨 **Template Support** - Use mustache templates for dynamic content in subject, body, and HTML
|
||||
- 📨 **MIME Formatting** - Convert emails to standard MIME format compatible with nodemailer and other sending libraries
|
||||
- ⚡ **Caching & Performance** - Smart caching of DNS lookups for better performance
|
||||
- 🔗 **Creation Object Reference** - Associate arbitrary typed data with emails for tracking and context preservation
|
||||
- 🔄 **Fluent Chainable API** - All methods return `this` for elegant method chaining
|
||||
- 📡 **Wire Format Serialization** - JSON serialization with base64 attachments for microservice communication
|
||||
- 🌐 **Wire Protocol Classes** - `WireTarget` and `WireParser` for client-server email service architecture
|
||||
|
||||
## Usage
|
||||
|
||||
`@push.rocks/smartmail` provides a unified format for representing and dealing with emails in a Node.js environment. Below, you will find several examples showcasing how to use its main features.
|
||||
|
||||
### Importing the Module
|
||||
|
||||
First, ensure you're using ESM (ECMAScript Modules) syntax in your TypeScript project. Then, import the necessary classes:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
Smartmail,
|
||||
EmailAddressValidator
|
||||
EmailAddressValidator,
|
||||
WireTarget,
|
||||
WireParser,
|
||||
createMessageId,
|
||||
createTimestamp
|
||||
} from '@push.rocks/smartmail';
|
||||
```
|
||||
|
||||
## Email Address Validation
|
||||
## 🔗 Fluent Chainable API
|
||||
|
||||
All mutation methods return `this`, enabling elegant method chaining:
|
||||
|
||||
```typescript
|
||||
const response = await new Smartmail({
|
||||
from: 'sender@example.com',
|
||||
subject: 'Hello {{name}}',
|
||||
body: 'Welcome to our service!'
|
||||
})
|
||||
.addRecipient('user@example.com')
|
||||
.addRecipient('manager@example.com', 'cc')
|
||||
.addRecipients(['team1@example.com', 'team2@example.com'], 'bcc')
|
||||
.setReplyTo('support@example.com')
|
||||
.setPriority('high')
|
||||
.addHeader('X-Campaign-ID', '12345')
|
||||
.applyVariables({ name: 'John' })
|
||||
.sendTo(wireTarget); // Send directly to a WireTarget
|
||||
```
|
||||
|
||||
## 📧 Email Address Validation
|
||||
|
||||
### Basic Email Validation
|
||||
|
||||
```typescript
|
||||
// Create validator with default options
|
||||
const emailValidator = new EmailAddressValidator();
|
||||
|
||||
// Validate an email address
|
||||
const result = await emailValidator.validate('user@example.com');
|
||||
console.log(result);
|
||||
/*
|
||||
@@ -62,14 +85,13 @@ console.log(result);
|
||||
### Advanced Validation Options
|
||||
|
||||
```typescript
|
||||
// Create validator with custom options
|
||||
const validator = new EmailAddressValidator({
|
||||
skipOnlineDomainFetch: true, // Use only local domain list
|
||||
cacheDnsResults: true, // Cache DNS lookups
|
||||
cacheExpiryMs: 7200000 // Cache expires after 2 hours
|
||||
});
|
||||
|
||||
// Validate each part of an email separately
|
||||
// Validate each part separately
|
||||
const isValidFormat = validator.isValidEmailFormat('user@example.com');
|
||||
const isValidLocalPart = validator.isValidLocalPart('user');
|
||||
const isValidDomain = validator.isValidDomainPart('example.com');
|
||||
@@ -81,12 +103,27 @@ if (result.freemail) {
|
||||
}
|
||||
```
|
||||
|
||||
## Creating and Using Smartmail Objects
|
||||
### Checking for Disposable Emails and Role Accounts
|
||||
|
||||
```typescript
|
||||
const validator = new EmailAddressValidator();
|
||||
|
||||
// Check if an email is from a disposable domain
|
||||
const isDisposable = await validator.isDisposableEmail('user@tempmail.com');
|
||||
|
||||
// Check if an email is a role account (info@, support@, admin@, etc.)
|
||||
const isRole = validator.isRoleAccount('support@example.com');
|
||||
|
||||
// Get MX records for a domain
|
||||
const mxRecords = await validator.getMxRecords('example.com');
|
||||
// ['mx1.example.com', 'mx2.example.com']
|
||||
```
|
||||
|
||||
## 📝 Creating and Using Smartmail Objects
|
||||
|
||||
### Basic Email Creation
|
||||
|
||||
```typescript
|
||||
// Create a simple email
|
||||
const email = new Smartmail({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
@@ -97,10 +134,9 @@ const email = new Smartmail({
|
||||
|
||||
### Using Creation Object Reference
|
||||
|
||||
The Smartmail constructor accepts a generic type parameter that lets you associate any additional data with your email, accessible later via the `getCreationObject()` method. This is useful for tracking, referencing original data sources, or maintaining context:
|
||||
Associate any typed data with your email for tracking and context preservation:
|
||||
|
||||
```typescript
|
||||
// Define your custom reference type
|
||||
interface OrderNotification {
|
||||
orderId: string;
|
||||
customerName: string;
|
||||
@@ -108,13 +144,11 @@ interface OrderNotification {
|
||||
total: number;
|
||||
}
|
||||
|
||||
// Create email with typed creation object reference
|
||||
const orderEmail = new Smartmail<OrderNotification>({
|
||||
from: 'orders@example.com',
|
||||
to: ['customer@example.com'],
|
||||
subject: 'Your Order #{{orderId}} Confirmation',
|
||||
body: 'Thank you for your order, {{customerName}}!',
|
||||
// Store the full order data as reference
|
||||
creationObjectRef: {
|
||||
orderId: '12345',
|
||||
customerName: 'John Smith',
|
||||
@@ -126,34 +160,9 @@ const orderEmail = new Smartmail<OrderNotification>({
|
||||
// Later, retrieve the original reference data
|
||||
const orderData = orderEmail.getCreationObject();
|
||||
console.log(`Processing email for order ${orderData.orderId}`);
|
||||
console.log(`Order total: $${orderData.total}`);
|
||||
|
||||
// Use the reference data for templating
|
||||
const subject = orderEmail.getSubject(orderData); // "Your Order #12345 Confirmation"
|
||||
const body = orderEmail.getBody(orderData); // "Thank you for your order, John Smith!"
|
||||
```
|
||||
|
||||
This powerful feature allows you to:
|
||||
- Maintain a link to original data sources
|
||||
- Pass the email object between systems while preserving context
|
||||
- Avoid duplicating data between email content and your application
|
||||
- Use the reference data to fill template variables
|
||||
- Access metadata about the email that doesn't get included in the actual message
|
||||
|
||||
### Adding Recipients
|
||||
|
||||
```typescript
|
||||
const email = new Smartmail({
|
||||
from: 'sender@example.com',
|
||||
subject: 'Meeting Invitation',
|
||||
body: 'Please join our meeting'
|
||||
});
|
||||
|
||||
// Add recipients in various ways
|
||||
email.addRecipient('primary@example.com');
|
||||
email.addRecipients(['user1@example.com', 'user2@example.com']);
|
||||
email.addRecipient('manager@example.com', 'cc');
|
||||
email.addRecipients(['observer1@example.com', 'observer2@example.com'], 'bcc');
|
||||
```
|
||||
|
||||
### Template Variables in Subject and Body
|
||||
@@ -162,20 +171,22 @@ email.addRecipients(['observer1@example.com', 'observer2@example.com'], 'bcc');
|
||||
const template = new Smartmail({
|
||||
from: 'notifications@example.com',
|
||||
subject: 'Welcome, {{name}}!',
|
||||
body: 'Hello {{name}},\n\nWelcome to our service. Your account ({{email}}) has been activated.',
|
||||
htmlBody: '<h1>Welcome, {{name}}!</h1><p>Hello {{name}},<br><br>Welcome to our service. Your account (<strong>{{email}}</strong>) has been activated.</p>'
|
||||
body: 'Hello {{name}},\n\nYour account ({{email}}) has been activated.',
|
||||
htmlBody: '<h1>Welcome, {{name}}!</h1><p>Your account (<strong>{{email}}</strong>) has been activated.</p>'
|
||||
});
|
||||
|
||||
// Apply template variables
|
||||
// Apply template variables when retrieving content
|
||||
const subject = template.getSubject({ name: 'John Doe' });
|
||||
const plainBody = template.getBody({ name: 'John Doe', email: 'john@example.com' });
|
||||
const htmlBody = template.getHtmlBody({ name: 'John Doe', email: 'john@example.com' });
|
||||
|
||||
// Or apply variables directly (modifies in place, chainable)
|
||||
template.applyVariables({ name: 'John Doe', email: 'john@example.com' });
|
||||
```
|
||||
|
||||
### Adding Attachments
|
||||
|
||||
```typescript
|
||||
import { Smartfile } from '@push.rocks/smartfile';
|
||||
import { SmartFile } from '@push.rocks/smartfile';
|
||||
|
||||
const email = new Smartmail({
|
||||
from: 'sender@example.com',
|
||||
@@ -184,32 +195,15 @@ const email = new Smartmail({
|
||||
body: 'Please find the attached report.'
|
||||
});
|
||||
|
||||
// Add file attachments
|
||||
const report = new Smartfile.fromLocalPath('/path/to/report.pdf');
|
||||
const image = new Smartfile.fromLocalPath('/path/to/image.png');
|
||||
email.addAttachment(report);
|
||||
email.addAttachment(image);
|
||||
const report = await SmartFile.fromFilePath('/path/to/report.pdf');
|
||||
const image = await SmartFile.fromFilePath('/path/to/image.png');
|
||||
|
||||
email
|
||||
.addAttachment(report)
|
||||
.addAttachment(image);
|
||||
```
|
||||
|
||||
### Setting Email Importance and Headers
|
||||
|
||||
```typescript
|
||||
const email = new Smartmail({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Urgent: System Alert',
|
||||
body: 'Critical system alert requires immediate attention.'
|
||||
});
|
||||
|
||||
// Set high priority
|
||||
email.setPriority('high');
|
||||
|
||||
// Add custom headers
|
||||
email.addHeader('X-Custom-ID', '12345');
|
||||
email.addHeader('X-System-Alert', 'Critical');
|
||||
```
|
||||
|
||||
### Converting to MIME Format for Sending
|
||||
### Converting to MIME Format
|
||||
|
||||
```typescript
|
||||
const email = new Smartmail({
|
||||
@@ -218,25 +212,211 @@ const email = new Smartmail({
|
||||
subject: 'Hello {{name}}',
|
||||
body: 'Text version: Hello {{name}}',
|
||||
htmlBody: '<p>HTML version: Hello <strong>{{name}}</strong></p>',
|
||||
validateEmails: true // Will validate all emails before converting
|
||||
validateEmails: true
|
||||
});
|
||||
|
||||
// Convert to MIME format with template data
|
||||
const mimeObj = await email.toMimeFormat({ name: 'John' });
|
||||
// Use with nodemailer or other email sending libraries
|
||||
```
|
||||
|
||||
// Result can be used with nodemailer or other email sending libraries
|
||||
console.log(mimeObj);
|
||||
/*
|
||||
{
|
||||
## 📡 Wire Format Serialization
|
||||
|
||||
Smartmail provides JSON serialization for transmitting emails between microservices. Attachments are automatically encoded as base64.
|
||||
|
||||
### Serializing to JSON
|
||||
|
||||
```typescript
|
||||
const email = new Smartmail({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Hello John',
|
||||
text: 'Text version: Hello John',
|
||||
html: '<p>HTML version: Hello <strong>John</strong></p>',
|
||||
attachments: [...],
|
||||
headers: {...}
|
||||
subject: 'Wire Format Test',
|
||||
body: 'This email can be serialized!'
|
||||
})
|
||||
.addAttachment(someFile)
|
||||
.setPriority('high');
|
||||
|
||||
// Serialize to JSON object
|
||||
const jsonObject = email.toObject();
|
||||
|
||||
// Serialize to JSON string
|
||||
const jsonString = email.toJson();
|
||||
```
|
||||
|
||||
### Deserializing from JSON
|
||||
|
||||
```typescript
|
||||
// From JSON object
|
||||
const email1 = Smartmail.fromObject(jsonObject);
|
||||
|
||||
// From JSON string
|
||||
const email2 = Smartmail.fromJson(jsonString);
|
||||
|
||||
// Attachments are automatically reconstructed from base64
|
||||
console.log(email2.attachments[0].contentBuffer);
|
||||
```
|
||||
|
||||
## 🌐 Wire Protocol Classes
|
||||
|
||||
For microservice architectures, smartmail provides `WireTarget` and `WireParser` classes to handle client-server communication.
|
||||
|
||||
### WireTarget (Client/SaaS Side)
|
||||
|
||||
The `WireTarget` is used by your SaaS application to communicate with an SMTP service:
|
||||
|
||||
```typescript
|
||||
import { WireTarget, Smartmail } from '@push.rocks/smartmail';
|
||||
|
||||
// Configure the target endpoint
|
||||
const smtpTarget = new WireTarget({
|
||||
endpoint: 'https://smtp-service.example.com/api/wire',
|
||||
authToken: 'your-secret-token'
|
||||
});
|
||||
|
||||
// Submit SMTP settings (extensible - add any custom settings)
|
||||
await smtpTarget.updateSettings({
|
||||
smtp: {
|
||||
host: 'smtp.example.com',
|
||||
port: 587,
|
||||
secure: true,
|
||||
username: 'user',
|
||||
password: 'pass'
|
||||
},
|
||||
defaultFrom: 'noreply@example.com',
|
||||
customSetting: 'custom-value' // Extensible!
|
||||
});
|
||||
|
||||
// Send an email using fluent API
|
||||
const response = await new Smartmail({
|
||||
from: 'sender@example.com',
|
||||
subject: 'Hello {{name}}',
|
||||
body: 'Welcome!'
|
||||
})
|
||||
.addRecipient('user@example.com')
|
||||
.setPriority('high')
|
||||
.applyVariables({ name: 'John' })
|
||||
.sendTo(smtpTarget);
|
||||
|
||||
console.log(`Sent! Delivery ID: ${response.deliveryId}`);
|
||||
|
||||
// Check delivery status
|
||||
const status = await smtpTarget.getStatus(response.deliveryId);
|
||||
console.log(`Status: ${status.status}`); // 'queued' | 'sending' | 'sent' | 'failed'
|
||||
|
||||
// List emails in a mailbox
|
||||
const inbox = await smtpTarget.listMailbox('INBOX', { limit: 10, offset: 0 });
|
||||
console.log(`Found ${inbox.total} emails`);
|
||||
|
||||
// Fetch a specific email
|
||||
const fetchedEmail = await smtpTarget.fetchEmail('INBOX', 'email-id-123');
|
||||
```
|
||||
|
||||
### WireParser (Server/SMTP Side)
|
||||
|
||||
The `WireParser` is used by your SMTP service to handle incoming wire messages:
|
||||
|
||||
```typescript
|
||||
import { WireParser, createMessageId, createTimestamp } from '@push.rocks/smartmail';
|
||||
|
||||
const parser = new WireParser({
|
||||
// Handle email send requests
|
||||
async onMailSend(email, options) {
|
||||
const mimeFormat = await email.toMimeFormat();
|
||||
await transporter.sendMail(mimeFormat);
|
||||
|
||||
return {
|
||||
type: 'mail.send.response',
|
||||
messageId: createMessageId(),
|
||||
timestamp: createTimestamp(),
|
||||
success: true,
|
||||
deliveryId: generateDeliveryId()
|
||||
};
|
||||
},
|
||||
|
||||
// Handle settings updates
|
||||
async onSettingsUpdate(settings) {
|
||||
if (settings.smtp) {
|
||||
configureSmtpTransport(settings.smtp);
|
||||
}
|
||||
*/
|
||||
// Handle custom settings
|
||||
if (settings.customSetting) {
|
||||
handleCustomSetting(settings.customSetting);
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'settings.update.response',
|
||||
messageId: createMessageId(),
|
||||
timestamp: createTimestamp(),
|
||||
success: true
|
||||
};
|
||||
},
|
||||
|
||||
// Handle mailbox list requests
|
||||
async onMailboxList(mailbox, options) {
|
||||
const emails = await getEmailsFromMailbox(mailbox, options);
|
||||
return {
|
||||
type: 'mailbox.list.response',
|
||||
messageId: createMessageId(),
|
||||
timestamp: createTimestamp(),
|
||||
mailbox,
|
||||
emails: emails.map(e => e.toObject()),
|
||||
total: emails.length
|
||||
};
|
||||
},
|
||||
|
||||
// Handle email fetch requests
|
||||
async onMailFetch(mailbox, emailId) {
|
||||
const email = await getEmailById(mailbox, emailId);
|
||||
return {
|
||||
type: 'mail.fetch.response',
|
||||
messageId: createMessageId(),
|
||||
timestamp: createTimestamp(),
|
||||
email: email ? email.toObject() : null
|
||||
};
|
||||
},
|
||||
|
||||
// Handle status check requests
|
||||
async onMailStatus(deliveryId) {
|
||||
const status = await getDeliveryStatus(deliveryId);
|
||||
return {
|
||||
type: 'mail.status.response',
|
||||
messageId: createMessageId(),
|
||||
timestamp: createTimestamp(),
|
||||
deliveryId,
|
||||
status: status.state
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Express route handler example
|
||||
app.post('/api/wire', async (req, res) => {
|
||||
const responseJson = await parser.parseAndHandle(JSON.stringify(req.body));
|
||||
res.json(JSON.parse(responseJson));
|
||||
});
|
||||
```
|
||||
|
||||
### Wire Protocol Message Types
|
||||
|
||||
```typescript
|
||||
// All available message type interfaces
|
||||
import type {
|
||||
IWireMessage,
|
||||
IMailSendRequest,
|
||||
IMailSendResponse,
|
||||
IMailboxListRequest,
|
||||
IMailboxListResponse,
|
||||
IMailFetchRequest,
|
||||
IMailFetchResponse,
|
||||
IMailStatusRequest,
|
||||
IMailStatusResponse,
|
||||
ISettingsUpdateRequest,
|
||||
ISettingsUpdateResponse,
|
||||
IWireSettings,
|
||||
ISmtpSettings,
|
||||
TWireMessage // Union type for type discrimination
|
||||
} from '@push.rocks/smartmail';
|
||||
|
||||
// Helper functions
|
||||
import { createMessageId, createTimestamp } from '@push.rocks/smartmail';
|
||||
```
|
||||
|
||||
## API Reference
|
||||
@@ -244,43 +424,109 @@ console.log(mimeObj);
|
||||
### EmailAddressValidator
|
||||
|
||||
#### Constructor Options
|
||||
- `skipOnlineDomainFetch`: Boolean (default: false) - Skip fetching domain list from online source
|
||||
- `cacheDnsResults`: Boolean (default: true) - Cache DNS lookup results
|
||||
- `cacheExpiryMs`: Number (default: 3600000) - Cache expiry in milliseconds
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `skipOnlineDomainFetch` | boolean | `false` | Skip fetching domain list from online source |
|
||||
| `cacheDnsResults` | boolean | `true` | Cache DNS lookup results |
|
||||
| `cacheExpiryMs` | number | `3600000` | Cache expiry in milliseconds (1 hour) |
|
||||
|
||||
#### Methods
|
||||
- `validate(email: string)`: Validates email address completeness
|
||||
- `isValidEmailFormat(email: string)`: Checks if email format is valid
|
||||
- `isValidLocalPart(localPart: string)`: Validates local part of email
|
||||
- `isValidDomainPart(domainPart: string)`: Validates domain part of email
|
||||
- `checkMxRecords(domain: string)`: Checks MX records for a domain
|
||||
| Method | Returns | Description |
|
||||
|--------|---------|-------------|
|
||||
| `validate(email)` | `Promise<IEmailValidationResult>` | Full validation with format, MX, and domain checks |
|
||||
| `isValidEmailFormat(email)` | `boolean` | Check if email format is valid |
|
||||
| `isValidLocalPart(localPart)` | `boolean` | Validate local part (before @) |
|
||||
| `isValidDomainPart(domainPart)` | `boolean` | Validate domain part (after @) |
|
||||
| `checkMxRecords(domain)` | `Promise<any>` | Check MX records for a domain |
|
||||
| `getMxRecords(domain)` | `Promise<string[]>` | Get MX record hostnames for a domain |
|
||||
| `isDisposableEmail(email)` | `Promise<boolean>` | Check if email is from a disposable domain |
|
||||
| `isRoleAccount(email)` | `boolean` | Check if email is a role account |
|
||||
|
||||
### Smartmail
|
||||
|
||||
#### Constructor Options
|
||||
- `from`: Email address of sender
|
||||
- `to`, `cc`, `bcc`: Optional arrays of recipient email addresses
|
||||
- `subject`: Email subject line
|
||||
- `body`: Plain text email body
|
||||
- `htmlBody`: Optional HTML version of email body
|
||||
- `replyTo`: Optional reply-to email address
|
||||
- `headers`: Optional key-value pairs of custom headers
|
||||
- `priority`: 'high' | 'normal' | 'low' (default: 'normal')
|
||||
- `validateEmails`: Boolean (default: false) - Validate all emails
|
||||
- `creationObjectRef`: Optional reference data of any type (generic T) - Store arbitrary data with the email
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `from` | string | *required* | Email address of sender |
|
||||
| `to` | string[] | `[]` | Array of primary recipient email addresses |
|
||||
| `cc` | string[] | `[]` | Array of CC recipient email addresses |
|
||||
| `bcc` | string[] | `[]` | Array of BCC recipient email addresses |
|
||||
| `subject` | string | *required* | Email subject line (supports templates) |
|
||||
| `body` | string | *required* | Plain text email body (supports templates) |
|
||||
| `htmlBody` | string | `undefined` | HTML version of email body (supports templates) |
|
||||
| `replyTo` | string | `undefined` | Reply-to email address |
|
||||
| `headers` | Record<string, string> | `{}` | Custom email headers |
|
||||
| `priority` | `'high' \| 'normal' \| 'low'` | `'normal'` | Email priority level |
|
||||
| `validateEmails` | boolean | `false` | Validate all emails before MIME conversion |
|
||||
| `creationObjectRef` | T | `undefined` | Reference data of any type (generic) |
|
||||
|
||||
#### Methods (All chainable methods return `this`)
|
||||
| Method | Returns | Description |
|
||||
|--------|---------|-------------|
|
||||
| `addRecipient(email, type?)` | `this` | Add a single recipient (to/cc/bcc) |
|
||||
| `addRecipients(emails, type?)` | `this` | Add multiple recipients |
|
||||
| `setReplyTo(email)` | `this` | Set reply-to address |
|
||||
| `setPriority(priority)` | `this` | Set email priority |
|
||||
| `addHeader(name, value)` | `this` | Add custom header |
|
||||
| `addAttachment(smartfile)` | `this` | Add a file attachment |
|
||||
| `applyVariables(variables)` | `this` | Apply template variables in place |
|
||||
| `getSubject(data?)` | `string` | Get processed subject with template variables |
|
||||
| `getBody(data?)` | `string` | Get processed plain text body |
|
||||
| `getHtmlBody(data?)` | `string \| null` | Get processed HTML body |
|
||||
| `getCreationObject()` | `T` | Get the stored reference data |
|
||||
| `validateAllEmails()` | `Promise<Record<string, boolean>>` | Validate all email addresses |
|
||||
| `toMimeFormat(data?)` | `Promise<object>` | Convert to MIME format object |
|
||||
| `toObject()` | `ISmartmailJson<T>` | Serialize to JSON object |
|
||||
| `toJson()` | `string` | Serialize to JSON string |
|
||||
| `sendTo(target)` | `Promise<IMailSendResponse>` | Send to a WireTarget |
|
||||
|
||||
#### Static Methods
|
||||
| Method | Returns | Description |
|
||||
|--------|---------|-------------|
|
||||
| `Smartmail.fromObject(obj)` | `Smartmail<T>` | Create from JSON object |
|
||||
| `Smartmail.fromJson(json)` | `Smartmail<T>` | Create from JSON string |
|
||||
|
||||
### WireTarget
|
||||
|
||||
#### Constructor Options
|
||||
| Option | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `endpoint` | string | URL of the SMTP service endpoint |
|
||||
| `authToken` | string | Optional authentication token |
|
||||
|
||||
#### Methods
|
||||
- `addRecipient(email, type?)`: Add a single recipient (to/cc/bcc)
|
||||
- `addRecipients(emails, type?)`: Add multiple recipients
|
||||
- `setReplyTo(email)`: Set reply-to address
|
||||
- `setPriority(priority)`: Set email priority
|
||||
- `addHeader(name, value)`: Add custom header
|
||||
- `getSubject(data?)`: Get processed subject with template variables
|
||||
- `getBody(data?)`: Get processed plain text body
|
||||
- `getHtmlBody(data?)`: Get processed HTML body
|
||||
- `validateAllEmails()`: Validate all email addresses
|
||||
- `toMimeFormat(data?)`: Convert to MIME format object
|
||||
- `getCreationObject()`: Get the stored reference data of type T
|
||||
| Method | Returns | Description |
|
||||
|--------|---------|-------------|
|
||||
| `sendEmail(email)` | `Promise<IMailSendResponse>` | Send an email through this target |
|
||||
| `updateSettings(settings)` | `Promise<ISettingsUpdateResponse>` | Update settings on the target |
|
||||
| `listMailbox(mailbox, options?)` | `Promise<IMailboxListResponse>` | List emails in a mailbox |
|
||||
| `fetchEmail(mailbox, emailId)` | `Promise<Smartmail \| null>` | Fetch a specific email |
|
||||
| `getStatus(deliveryId)` | `Promise<IMailStatusResponse>` | Check delivery status |
|
||||
|
||||
### WireParser
|
||||
|
||||
#### Constructor
|
||||
```typescript
|
||||
new WireParser(handlers: IWireHandlers)
|
||||
```
|
||||
|
||||
#### Handler Interface
|
||||
```typescript
|
||||
interface IWireHandlers {
|
||||
onMailSend?: (email: Smartmail, options?) => Promise<IMailSendResponse>;
|
||||
onMailboxList?: (mailbox: string, options?) => Promise<IMailboxListResponse>;
|
||||
onMailFetch?: (mailbox: string, emailId: string) => Promise<IMailFetchResponse>;
|
||||
onMailStatus?: (deliveryId: string) => Promise<IMailStatusResponse>;
|
||||
onSettingsUpdate?: (settings: IWireSettings) => Promise<ISettingsUpdateResponse>;
|
||||
}
|
||||
```
|
||||
|
||||
#### Methods
|
||||
| Method | Returns | Description |
|
||||
|--------|---------|-------------|
|
||||
| `parse(json)` | `TWireMessage` | Parse a wire message from JSON string |
|
||||
| `handle(message)` | `Promise<IWireMessage>` | Handle a wire message and return response |
|
||||
| `parseAndHandle(json)` | `Promise<string>` | Parse and handle in one step |
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
|
||||
275
test/test.ts
275
test/test.ts
@@ -1,4 +1,4 @@
|
||||
import { expect, tap } from '@push.rocks/tapbundle';
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as smartmail from '../ts/index.js';
|
||||
import * as plugins from '../ts/smartmail.plugins.js';
|
||||
|
||||
@@ -144,4 +144,275 @@ tap.test('should add email headers', async () => {
|
||||
expect(mimeObj.headers['X-Tracking-ID']).toEqual('12345');
|
||||
});
|
||||
|
||||
tap.start();
|
||||
// Fluent Chaining Tests
|
||||
tap.test('should support fluent chaining for all mutation methods', async () => {
|
||||
const email = new smartmail.Smartmail({
|
||||
from: 'sender@example.com',
|
||||
subject: 'Fluent {{name}}',
|
||||
body: 'Hello {{name}}'
|
||||
});
|
||||
|
||||
// Chain all methods together
|
||||
const result = email
|
||||
.addRecipient('user1@example.com')
|
||||
.addRecipient('cc@example.com', 'cc')
|
||||
.addRecipients(['user2@example.com', 'user3@example.com'])
|
||||
.setReplyTo('reply@example.com')
|
||||
.setPriority('high')
|
||||
.addHeader('X-Custom', 'value')
|
||||
.applyVariables({ name: 'John' });
|
||||
|
||||
// Result should be the same instance
|
||||
expect(result).toEqual(email);
|
||||
|
||||
// All values should be set
|
||||
expect(email.options.to!.length).toEqual(3);
|
||||
expect(email.options.cc!.length).toEqual(1);
|
||||
expect(email.options.replyTo).toEqual('reply@example.com');
|
||||
expect(email.options.priority).toEqual('high');
|
||||
expect(email.options.headers!['X-Custom']).toEqual('value');
|
||||
expect(email.options.subject).toEqual('Fluent John');
|
||||
expect(email.options.body).toEqual('Hello John');
|
||||
});
|
||||
|
||||
// Wire Format Serialization Tests
|
||||
tap.test('should serialize to JSON and back with toObject/fromObject', async () => {
|
||||
const original = new smartmail.Smartmail({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
cc: ['cc@example.com'],
|
||||
subject: 'Test Subject',
|
||||
body: 'Test Body',
|
||||
htmlBody: '<p>Test HTML</p>',
|
||||
priority: 'high',
|
||||
headers: { 'X-Custom': 'value' },
|
||||
creationObjectRef: { orderId: '12345' }
|
||||
});
|
||||
|
||||
const obj = original.toObject();
|
||||
|
||||
expect(obj.from).toEqual('sender@example.com');
|
||||
expect(obj.to).toInclude('recipient@example.com');
|
||||
expect(obj.cc).toInclude('cc@example.com');
|
||||
expect(obj.subject).toEqual('Test Subject');
|
||||
expect(obj.body).toEqual('Test Body');
|
||||
expect(obj.htmlBody).toEqual('<p>Test HTML</p>');
|
||||
expect(obj.priority).toEqual('high');
|
||||
expect(obj.headers!['X-Custom']).toEqual('value');
|
||||
expect(obj.creationObjectRef).toEqual({ orderId: '12345' });
|
||||
expect(obj.attachments).toBeDefined();
|
||||
|
||||
// Reconstruct from object
|
||||
const reconstructed = smartmail.Smartmail.fromObject(obj);
|
||||
|
||||
expect(reconstructed.options.from).toEqual(original.options.from);
|
||||
expect(reconstructed.options.subject).toEqual(original.options.subject);
|
||||
expect(reconstructed.options.body).toEqual(original.options.body);
|
||||
expect(reconstructed.options.htmlBody).toEqual(original.options.htmlBody);
|
||||
expect(reconstructed.options.priority).toEqual(original.options.priority);
|
||||
expect(reconstructed.getCreationObject()).toEqual({ orderId: '12345' });
|
||||
});
|
||||
|
||||
tap.test('should serialize to JSON string and back with toJson/fromJson', async () => {
|
||||
const original = new smartmail.Smartmail({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'JSON Test',
|
||||
body: 'JSON Body'
|
||||
});
|
||||
|
||||
const jsonString = original.toJson();
|
||||
|
||||
// Should be valid JSON
|
||||
const parsed = JSON.parse(jsonString);
|
||||
expect(parsed.from).toEqual('sender@example.com');
|
||||
expect(parsed.subject).toEqual('JSON Test');
|
||||
|
||||
// Reconstruct from JSON string
|
||||
const reconstructed = smartmail.Smartmail.fromJson(jsonString);
|
||||
|
||||
expect(reconstructed.options.from).toEqual(original.options.from);
|
||||
expect(reconstructed.options.subject).toEqual(original.options.subject);
|
||||
expect(reconstructed.options.body).toEqual(original.options.body);
|
||||
});
|
||||
|
||||
tap.test('should serialize attachments to base64 and back', async () => {
|
||||
const testContent = 'Hello, this is test file content!';
|
||||
const testBuffer = Buffer.from(testContent);
|
||||
|
||||
const original = new smartmail.Smartmail({
|
||||
from: 'sender@example.com',
|
||||
subject: 'Attachment Test',
|
||||
body: 'Test Body'
|
||||
});
|
||||
|
||||
// Create a SmartFile and add it as attachment
|
||||
const smartfile = new plugins.smartfile.SmartFile({
|
||||
path: 'test-file.txt',
|
||||
contentBuffer: testBuffer,
|
||||
base: './'
|
||||
});
|
||||
original.addAttachment(smartfile);
|
||||
|
||||
// Serialize to object
|
||||
const obj = original.toObject();
|
||||
|
||||
expect(obj.attachments.length).toEqual(1);
|
||||
expect(obj.attachments[0].filename).toEqual('test-file.txt');
|
||||
expect(obj.attachments[0].contentBase64).toEqual(testBuffer.toString('base64'));
|
||||
|
||||
// Reconstruct
|
||||
const reconstructed = smartmail.Smartmail.fromObject(obj);
|
||||
|
||||
expect(reconstructed.attachments.length).toEqual(1);
|
||||
expect(reconstructed.attachments[0].contentBuffer.toString()).toEqual(testContent);
|
||||
});
|
||||
|
||||
// Wire Protocol Message Types Tests
|
||||
tap.test('should have correct wire message type interfaces', async () => {
|
||||
// Test createMessageId and createTimestamp helpers
|
||||
const messageId = smartmail.createMessageId();
|
||||
const timestamp = smartmail.createTimestamp();
|
||||
|
||||
expect(typeof messageId).toEqual('string');
|
||||
expect(messageId.length).toBeGreaterThan(0);
|
||||
expect(typeof timestamp).toEqual('string');
|
||||
expect(timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
||||
});
|
||||
|
||||
// WireParser Tests
|
||||
tap.test('should parse and handle mail.send requests with WireParser', async () => {
|
||||
let receivedEmail: smartmail.Smartmail<any> | null = null;
|
||||
|
||||
const parser = new smartmail.WireParser({
|
||||
onMailSend: async (email, options) => {
|
||||
receivedEmail = email;
|
||||
return {
|
||||
type: 'mail.send.response',
|
||||
messageId: smartmail.createMessageId(),
|
||||
timestamp: smartmail.createTimestamp(),
|
||||
success: true,
|
||||
deliveryId: 'test-delivery-id'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const testEmail = new smartmail.Smartmail({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Wire Test',
|
||||
body: 'Wire Body'
|
||||
});
|
||||
|
||||
const request: smartmail.IMailSendRequest = {
|
||||
type: 'mail.send',
|
||||
messageId: smartmail.createMessageId(),
|
||||
timestamp: smartmail.createTimestamp(),
|
||||
email: testEmail.toObject()
|
||||
};
|
||||
|
||||
const response = await parser.handle(request) as smartmail.IMailSendResponse;
|
||||
|
||||
expect(response.type).toEqual('mail.send.response');
|
||||
expect(response.success).toBeTrue();
|
||||
expect(response.deliveryId).toEqual('test-delivery-id');
|
||||
expect(receivedEmail).not.toBeNull();
|
||||
expect(receivedEmail!.options.subject).toEqual('Wire Test');
|
||||
});
|
||||
|
||||
tap.test('should parse and handle settings.update requests with WireParser', async () => {
|
||||
let receivedSettings: smartmail.IWireSettings | null = null;
|
||||
|
||||
const parser = new smartmail.WireParser({
|
||||
onSettingsUpdate: async (settings) => {
|
||||
receivedSettings = settings;
|
||||
return {
|
||||
type: 'settings.update.response',
|
||||
messageId: smartmail.createMessageId(),
|
||||
timestamp: smartmail.createTimestamp(),
|
||||
success: true
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const request: smartmail.ISettingsUpdateRequest = {
|
||||
type: 'settings.update',
|
||||
messageId: smartmail.createMessageId(),
|
||||
timestamp: smartmail.createTimestamp(),
|
||||
settings: {
|
||||
smtp: {
|
||||
host: 'smtp.example.com',
|
||||
port: 587,
|
||||
secure: true,
|
||||
username: 'user',
|
||||
password: 'pass'
|
||||
},
|
||||
defaultFrom: 'noreply@example.com',
|
||||
customSetting: 'custom-value'
|
||||
}
|
||||
};
|
||||
|
||||
const response = await parser.handle(request) as smartmail.ISettingsUpdateResponse;
|
||||
|
||||
expect(response.type).toEqual('settings.update.response');
|
||||
expect(response.success).toBeTrue();
|
||||
expect(receivedSettings).not.toBeNull();
|
||||
expect(receivedSettings!.smtp!.host).toEqual('smtp.example.com');
|
||||
expect(receivedSettings!.defaultFrom).toEqual('noreply@example.com');
|
||||
expect(receivedSettings!.customSetting).toEqual('custom-value');
|
||||
});
|
||||
|
||||
tap.test('should handle parseAndHandle convenience method', async () => {
|
||||
const parser = new smartmail.WireParser({
|
||||
onMailSend: async (email) => ({
|
||||
type: 'mail.send.response',
|
||||
messageId: smartmail.createMessageId(),
|
||||
timestamp: smartmail.createTimestamp(),
|
||||
success: true,
|
||||
deliveryId: 'convenience-test-id'
|
||||
})
|
||||
});
|
||||
|
||||
const testEmail = new smartmail.Smartmail({
|
||||
from: 'sender@example.com',
|
||||
subject: 'Convenience Test',
|
||||
body: 'Test Body'
|
||||
});
|
||||
|
||||
const requestJson = JSON.stringify({
|
||||
type: 'mail.send',
|
||||
messageId: smartmail.createMessageId(),
|
||||
timestamp: smartmail.createTimestamp(),
|
||||
email: testEmail.toObject()
|
||||
});
|
||||
|
||||
const responseJson = await parser.parseAndHandle(requestJson);
|
||||
const response = JSON.parse(responseJson);
|
||||
|
||||
expect(response.type).toEqual('mail.send.response');
|
||||
expect(response.success).toBeTrue();
|
||||
expect(response.deliveryId).toEqual('convenience-test-id');
|
||||
});
|
||||
|
||||
tap.test('should return error response for unsupported handlers', async () => {
|
||||
const parser = new smartmail.WireParser({}); // No handlers
|
||||
|
||||
const request: smartmail.IMailSendRequest = {
|
||||
type: 'mail.send',
|
||||
messageId: 'test-msg-id',
|
||||
timestamp: smartmail.createTimestamp(),
|
||||
email: new smartmail.Smartmail({
|
||||
from: 'sender@example.com',
|
||||
subject: 'Test',
|
||||
body: 'Test'
|
||||
}).toObject()
|
||||
};
|
||||
|
||||
const response = await parser.handle(request) as smartmail.IMailSendResponse;
|
||||
|
||||
expect(response.type).toEqual('mail.send.response');
|
||||
expect(response.success).toBeFalse();
|
||||
expect(response.error).toEqual('Mail send not supported');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartmail',
|
||||
version: '2.1.0',
|
||||
version: '2.2.0',
|
||||
description: 'A unified format for representing and dealing with emails, with support for attachments and email validation.'
|
||||
}
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
export * from './smartmail.classes.smartmail.js';
|
||||
export * from './smartmail.classes.emailaddressvalidator.js';
|
||||
export * from './smartmail.wire.js';
|
||||
export * from './smartmail.classes.wiretarget.js';
|
||||
export * from './smartmail.classes.wireparser.js';
|
||||
|
||||
@@ -251,8 +251,9 @@ export class EmailAddressValidator {
|
||||
*/
|
||||
public async fetchDomains() {
|
||||
if (!this.domainMap) {
|
||||
const localFileString = plugins.smartfile.fs.toStringSync(
|
||||
plugins.path.join(paths.assetDir, 'domains.json')
|
||||
const localFileString = plugins.fs.readFileSync(
|
||||
plugins.path.join(paths.assetDir, 'domains.json'),
|
||||
'utf8'
|
||||
);
|
||||
const localFileObject = JSON.parse(localFileString);
|
||||
|
||||
@@ -262,12 +263,10 @@ export class EmailAddressValidator {
|
||||
}
|
||||
|
||||
try {
|
||||
const onlineFileObject = (
|
||||
await plugins.smartrequest.getJson(
|
||||
'https://raw.githubusercontent.com/romainsimon/emailvalid/master/domains.json'
|
||||
)
|
||||
).body;
|
||||
this.domainMap = onlineFileObject;
|
||||
const response = await plugins.SmartRequest.create()
|
||||
.url('https://raw.githubusercontent.com/romainsimon/emailvalid/master/domains.json')
|
||||
.get();
|
||||
this.domainMap = await response.json();
|
||||
} catch (e) {
|
||||
this.domainMap = localFileObject;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import * as plugins from './smartmail.plugins.js';
|
||||
import { EmailAddressValidator } from './smartmail.classes.emailaddressvalidator.js';
|
||||
import type { IMailSendResponse } from './smartmail.wire.js';
|
||||
import type { WireTarget } from './smartmail.classes.wiretarget.js';
|
||||
|
||||
export type EmailAddress = string;
|
||||
export type EmailAddressList = EmailAddress[];
|
||||
@@ -19,6 +21,33 @@ export interface ISmartmailOptions<T> {
|
||||
validateEmails?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON representation of an attachment for wire transmission
|
||||
*/
|
||||
export interface IAttachmentJson {
|
||||
filename: string;
|
||||
contentBase64: string;
|
||||
contentType: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON representation of a Smartmail for wire transmission
|
||||
*/
|
||||
export interface ISmartmailJson<T = unknown> {
|
||||
from: string;
|
||||
to?: string[];
|
||||
cc?: string[];
|
||||
bcc?: string[];
|
||||
replyTo?: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
htmlBody?: string;
|
||||
headers?: Record<string, string>;
|
||||
priority?: 'high' | 'normal' | 'low';
|
||||
creationObjectRef?: T;
|
||||
attachments: IAttachmentJson[];
|
||||
}
|
||||
|
||||
export interface IMimeAttachment {
|
||||
filename: string;
|
||||
content: Buffer;
|
||||
@@ -51,9 +80,11 @@ export class Smartmail<T> {
|
||||
/**
|
||||
* Adds an attachment to the email
|
||||
* @param smartfileArg The file to attach
|
||||
* @returns this for chaining
|
||||
*/
|
||||
public addAttachment(smartfileArg: plugins.smartfile.SmartFile) {
|
||||
public addAttachment(smartfileArg: plugins.smartfile.SmartFile): this {
|
||||
this.attachments.push(smartfileArg);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,10 +108,11 @@ export class Smartmail<T> {
|
||||
/**
|
||||
* Applies variables to all template strings in the email
|
||||
* @param variables Variables to apply to templates
|
||||
* @returns this for chaining
|
||||
*/
|
||||
public applyVariables(variables: Record<string, any>): void {
|
||||
public applyVariables(variables: Record<string, any>): this {
|
||||
if (!variables || typeof variables !== 'object') {
|
||||
return;
|
||||
return this;
|
||||
}
|
||||
|
||||
// Process the subject, body, and HTML body with the provided variables
|
||||
@@ -98,6 +130,8 @@ export class Smartmail<T> {
|
||||
const htmlBodyMustache = new plugins.smartmustache.SmartMustache(this.options.htmlBody);
|
||||
this.options.htmlBody = htmlBodyMustache.applyData(variables);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,55 +162,65 @@ export class Smartmail<T> {
|
||||
* Adds a recipient to the email
|
||||
* @param email Email address to add
|
||||
* @param type Type of recipient (to, cc, bcc)
|
||||
* @returns this for chaining
|
||||
*/
|
||||
public addRecipient(email: EmailAddress, type: 'to' | 'cc' | 'bcc' = 'to'): void {
|
||||
public addRecipient(email: EmailAddress, type: 'to' | 'cc' | 'bcc' = 'to'): this {
|
||||
if (!this.options[type]) {
|
||||
this.options[type] = [];
|
||||
}
|
||||
|
||||
this.options[type]!.push(email);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds multiple recipients to the email
|
||||
* @param emails Email addresses to add
|
||||
* @param type Type of recipients (to, cc, bcc)
|
||||
* @returns this for chaining
|
||||
*/
|
||||
public addRecipients(emails: EmailAddressList, type: 'to' | 'cc' | 'bcc' = 'to'): void {
|
||||
public addRecipients(emails: EmailAddressList, type: 'to' | 'cc' | 'bcc' = 'to'): this {
|
||||
if (!this.options[type]) {
|
||||
this.options[type] = [];
|
||||
}
|
||||
|
||||
this.options[type] = [...this.options[type]!, ...emails];
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the reply-to address
|
||||
* @param email Email address for reply-to
|
||||
* @returns this for chaining
|
||||
*/
|
||||
public setReplyTo(email: EmailAddress): void {
|
||||
public setReplyTo(email: EmailAddress): this {
|
||||
this.options.replyTo = email;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the priority of the email
|
||||
* @param priority Priority level
|
||||
* @returns this for chaining
|
||||
*/
|
||||
public setPriority(priority: 'high' | 'normal' | 'low'): void {
|
||||
public setPriority(priority: 'high' | 'normal' | 'low'): this {
|
||||
this.options.priority = priority;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a custom header to the email
|
||||
* @param name Header name
|
||||
* @param value Header value
|
||||
* @returns this for chaining
|
||||
*/
|
||||
public addHeader(name: string, value: string): void {
|
||||
public addHeader(name: string, value: string): this {
|
||||
if (!this.options.headers) {
|
||||
this.options.headers = {};
|
||||
}
|
||||
|
||||
this.options.headers[name] = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -203,6 +247,102 @@ export class Smartmail<T> {
|
||||
return results;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Wire Format Serialization Methods
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Converts the email to a JSON-serializable object for wire transmission
|
||||
* @returns JSON-serializable object
|
||||
*/
|
||||
public toObject(): ISmartmailJson<T> {
|
||||
const attachmentsJson: IAttachmentJson[] = this.attachments.map((file) => ({
|
||||
filename: file.path.split('/').pop() || 'attachment',
|
||||
contentBase64: file.contentBuffer.toString('base64'),
|
||||
contentType: 'application/octet-stream',
|
||||
}));
|
||||
|
||||
return {
|
||||
from: this.options.from,
|
||||
to: this.options.to,
|
||||
cc: this.options.cc,
|
||||
bcc: this.options.bcc,
|
||||
replyTo: this.options.replyTo,
|
||||
subject: this.options.subject,
|
||||
body: this.options.body,
|
||||
htmlBody: this.options.htmlBody,
|
||||
headers: this.options.headers,
|
||||
priority: this.options.priority,
|
||||
creationObjectRef: this.options.creationObjectRef,
|
||||
attachments: attachmentsJson,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the email to a JSON string for wire transmission
|
||||
* @returns JSON string
|
||||
*/
|
||||
public toJson(): string {
|
||||
return JSON.stringify(this.toObject());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Smartmail instance from a JSON-serializable object
|
||||
* @param obj JSON object representing the email
|
||||
* @returns Smartmail instance
|
||||
*/
|
||||
public static fromObject<T = unknown>(obj: ISmartmailJson<T>): Smartmail<T> {
|
||||
const email = new Smartmail<T>({
|
||||
from: obj.from,
|
||||
to: obj.to,
|
||||
cc: obj.cc,
|
||||
bcc: obj.bcc,
|
||||
replyTo: obj.replyTo,
|
||||
subject: obj.subject,
|
||||
body: obj.body,
|
||||
htmlBody: obj.htmlBody,
|
||||
headers: obj.headers,
|
||||
priority: obj.priority,
|
||||
creationObjectRef: obj.creationObjectRef,
|
||||
});
|
||||
|
||||
// Reconstruct attachments from base64
|
||||
for (const att of obj.attachments || []) {
|
||||
const buffer = Buffer.from(att.contentBase64, 'base64');
|
||||
const smartfile = new plugins.smartfile.SmartFile({
|
||||
path: att.filename,
|
||||
contentBuffer: buffer,
|
||||
base: './',
|
||||
});
|
||||
email.attachments.push(smartfile);
|
||||
}
|
||||
|
||||
return email;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes a Smartmail instance from a JSON string
|
||||
* @param json JSON string representing the email
|
||||
* @returns Smartmail instance
|
||||
*/
|
||||
public static fromJson<T = unknown>(json: string): Smartmail<T> {
|
||||
const obj = JSON.parse(json) as ISmartmailJson<T>;
|
||||
return Smartmail.fromObject<T>(obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send this email to a WireTarget
|
||||
* @param target The WireTarget to send the email to
|
||||
* @returns Promise resolving to the send response
|
||||
*/
|
||||
public async sendTo(target: WireTarget): Promise<IMailSendResponse> {
|
||||
return target.sendEmail(this);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// MIME Format Methods
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Converts the email to a MIME format object for sending
|
||||
* @param dataArg Data to apply to templates
|
||||
|
||||
252
ts/smartmail.classes.wireparser.ts
Normal file
252
ts/smartmail.classes.wireparser.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
import { Smartmail } from './smartmail.classes.smartmail.js';
|
||||
import {
|
||||
type IWireMessage,
|
||||
type IMailSendRequest,
|
||||
type IMailSendResponse,
|
||||
type IMailboxListRequest,
|
||||
type IMailboxListResponse,
|
||||
type IMailFetchRequest,
|
||||
type IMailFetchResponse,
|
||||
type IMailStatusRequest,
|
||||
type IMailStatusResponse,
|
||||
type ISettingsUpdateRequest,
|
||||
type ISettingsUpdateResponse,
|
||||
type IWireSettings,
|
||||
type TWireMessage,
|
||||
createMessageId,
|
||||
createTimestamp,
|
||||
} from './smartmail.wire.js';
|
||||
|
||||
/**
|
||||
* Handler functions for different wire message types
|
||||
*/
|
||||
export interface IWireHandlers {
|
||||
/** Handler for mail send requests */
|
||||
onMailSend?: (
|
||||
email: Smartmail<any>,
|
||||
options?: IMailSendRequest['options']
|
||||
) => Promise<IMailSendResponse>;
|
||||
|
||||
/** Handler for mailbox list requests */
|
||||
onMailboxList?: (
|
||||
mailbox: string,
|
||||
options?: { limit?: number; offset?: number }
|
||||
) => Promise<IMailboxListResponse>;
|
||||
|
||||
/** Handler for mail fetch requests */
|
||||
onMailFetch?: (mailbox: string, emailId: string) => Promise<IMailFetchResponse>;
|
||||
|
||||
/** Handler for mail status requests */
|
||||
onMailStatus?: (deliveryId: string) => Promise<IMailStatusResponse>;
|
||||
|
||||
/** Handler for settings update requests */
|
||||
onSettingsUpdate?: (settings: IWireSettings) => Promise<ISettingsUpdateResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
* WireParser is used by the SMTP service to parse and handle incoming wire messages.
|
||||
* It provides a handler-based approach for processing different message types.
|
||||
*/
|
||||
export class WireParser {
|
||||
private handlers: IWireHandlers;
|
||||
|
||||
constructor(handlers: IWireHandlers = {}) {
|
||||
this.handlers = handlers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a wire message from JSON string
|
||||
* @param json The JSON string to parse
|
||||
* @returns Parsed wire message
|
||||
*/
|
||||
public parse(json: string): TWireMessage {
|
||||
return JSON.parse(json) as TWireMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming wire message and return the response
|
||||
* @param message The wire message to handle
|
||||
* @returns Promise resolving to the response message
|
||||
*/
|
||||
public async handle(message: TWireMessage): Promise<IWireMessage> {
|
||||
switch (message.type) {
|
||||
case 'mail.send':
|
||||
return this.handleMailSend(message);
|
||||
case 'mailbox.list':
|
||||
return this.handleMailboxList(message);
|
||||
case 'mail.fetch':
|
||||
return this.handleMailFetch(message);
|
||||
case 'mail.status':
|
||||
return this.handleMailStatus(message);
|
||||
case 'settings.update':
|
||||
return this.handleSettingsUpdate(message);
|
||||
default:
|
||||
return this.createErrorResponse(message, 'Unknown message type');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and handle in one step (convenience method)
|
||||
* @param json The JSON string to parse and handle
|
||||
* @returns Promise resolving to JSON response string
|
||||
*/
|
||||
public async parseAndHandle(json: string): Promise<string> {
|
||||
const message = this.parse(json);
|
||||
const response = await this.handle(message);
|
||||
return JSON.stringify(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle mail send request
|
||||
*/
|
||||
private async handleMailSend(message: IMailSendRequest): Promise<IMailSendResponse> {
|
||||
if (!this.handlers.onMailSend) {
|
||||
return {
|
||||
type: 'mail.send.response',
|
||||
messageId: message.messageId,
|
||||
timestamp: createTimestamp(),
|
||||
success: false,
|
||||
error: 'Mail send not supported',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const email = Smartmail.fromObject(message.email);
|
||||
return await this.handlers.onMailSend(email, message.options);
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'mail.send.response',
|
||||
messageId: message.messageId,
|
||||
timestamp: createTimestamp(),
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle mailbox list request
|
||||
*/
|
||||
private async handleMailboxList(message: IMailboxListRequest): Promise<IMailboxListResponse> {
|
||||
if (!this.handlers.onMailboxList) {
|
||||
return {
|
||||
type: 'mailbox.list.response',
|
||||
messageId: message.messageId,
|
||||
timestamp: createTimestamp(),
|
||||
mailbox: message.mailbox,
|
||||
emails: [],
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.handlers.onMailboxList(message.mailbox, {
|
||||
limit: message.limit,
|
||||
offset: message.offset,
|
||||
});
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'mailbox.list.response',
|
||||
messageId: message.messageId,
|
||||
timestamp: createTimestamp(),
|
||||
mailbox: message.mailbox,
|
||||
emails: [],
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle mail fetch request
|
||||
*/
|
||||
private async handleMailFetch(message: IMailFetchRequest): Promise<IMailFetchResponse> {
|
||||
if (!this.handlers.onMailFetch) {
|
||||
return {
|
||||
type: 'mail.fetch.response',
|
||||
messageId: message.messageId,
|
||||
timestamp: createTimestamp(),
|
||||
email: null,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.handlers.onMailFetch(message.mailbox, message.emailId);
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'mail.fetch.response',
|
||||
messageId: message.messageId,
|
||||
timestamp: createTimestamp(),
|
||||
email: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle mail status request
|
||||
*/
|
||||
private async handleMailStatus(message: IMailStatusRequest): Promise<IMailStatusResponse> {
|
||||
if (!this.handlers.onMailStatus) {
|
||||
return {
|
||||
type: 'mail.status.response',
|
||||
messageId: message.messageId,
|
||||
timestamp: createTimestamp(),
|
||||
deliveryId: message.deliveryId,
|
||||
status: 'failed',
|
||||
error: 'Mail status not supported',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.handlers.onMailStatus(message.deliveryId);
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'mail.status.response',
|
||||
messageId: message.messageId,
|
||||
timestamp: createTimestamp(),
|
||||
deliveryId: message.deliveryId,
|
||||
status: 'failed',
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle settings update request
|
||||
*/
|
||||
private async handleSettingsUpdate(
|
||||
message: ISettingsUpdateRequest
|
||||
): Promise<ISettingsUpdateResponse> {
|
||||
if (!this.handlers.onSettingsUpdate) {
|
||||
return {
|
||||
type: 'settings.update.response',
|
||||
messageId: message.messageId,
|
||||
timestamp: createTimestamp(),
|
||||
success: false,
|
||||
error: 'Settings update not supported',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.handlers.onSettingsUpdate(message.settings);
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'settings.update.response',
|
||||
messageId: message.messageId,
|
||||
timestamp: createTimestamp(),
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an error response for unknown message types
|
||||
*/
|
||||
private createErrorResponse(message: IWireMessage, error: string): IWireMessage {
|
||||
return {
|
||||
type: `${message.type}.response`,
|
||||
messageId: message.messageId,
|
||||
timestamp: createTimestamp(),
|
||||
};
|
||||
}
|
||||
}
|
||||
150
ts/smartmail.classes.wiretarget.ts
Normal file
150
ts/smartmail.classes.wiretarget.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import * as plugins from './smartmail.plugins.js';
|
||||
import { Smartmail } from './smartmail.classes.smartmail.js';
|
||||
import {
|
||||
type IWireMessage,
|
||||
type IMailSendRequest,
|
||||
type IMailSendResponse,
|
||||
type IMailboxListRequest,
|
||||
type IMailboxListResponse,
|
||||
type IMailFetchRequest,
|
||||
type IMailFetchResponse,
|
||||
type IMailStatusRequest,
|
||||
type IMailStatusResponse,
|
||||
type ISettingsUpdateRequest,
|
||||
type ISettingsUpdateResponse,
|
||||
type IWireSettings,
|
||||
createMessageId,
|
||||
createTimestamp,
|
||||
} from './smartmail.wire.js';
|
||||
|
||||
/**
|
||||
* Options for configuring a WireTarget
|
||||
*/
|
||||
export interface IWireTargetOptions {
|
||||
/** URL of the SMTP service endpoint */
|
||||
endpoint: string;
|
||||
/** Optional authentication token */
|
||||
authToken?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* WireTarget is used by the SaaS service to communicate with the SMTP service.
|
||||
* It provides methods for sending emails, updating settings, and managing mailboxes.
|
||||
*/
|
||||
export class WireTarget {
|
||||
private endpoint: string;
|
||||
private authToken?: string;
|
||||
|
||||
constructor(options: IWireTargetOptions) {
|
||||
this.endpoint = options.endpoint;
|
||||
this.authToken = options.authToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an email through this target
|
||||
* @param email The Smartmail instance to send
|
||||
* @returns Promise resolving to the send response
|
||||
*/
|
||||
public async sendEmail(email: Smartmail<any>): Promise<IMailSendResponse> {
|
||||
const request: IMailSendRequest = {
|
||||
type: 'mail.send',
|
||||
messageId: createMessageId(),
|
||||
timestamp: createTimestamp(),
|
||||
email: email.toObject(),
|
||||
};
|
||||
return this.send<IMailSendResponse>(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update settings on the target (SMTP config, etc.)
|
||||
* Settings are extensible - any key-value pairs can be sent
|
||||
* @param settings The settings to update
|
||||
* @returns Promise resolving to the update response
|
||||
*/
|
||||
public async updateSettings(settings: IWireSettings): Promise<ISettingsUpdateResponse> {
|
||||
const request: ISettingsUpdateRequest = {
|
||||
type: 'settings.update',
|
||||
messageId: createMessageId(),
|
||||
timestamp: createTimestamp(),
|
||||
settings,
|
||||
};
|
||||
return this.send<ISettingsUpdateResponse>(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* List emails in a mailbox
|
||||
* @param mailbox The mailbox to list (e.g., 'INBOX', 'Sent')
|
||||
* @param options Optional limit and offset for pagination
|
||||
* @returns Promise resolving to the mailbox list response
|
||||
*/
|
||||
public async listMailbox(
|
||||
mailbox: string,
|
||||
options?: { limit?: number; offset?: number }
|
||||
): Promise<IMailboxListResponse> {
|
||||
const request: IMailboxListRequest = {
|
||||
type: 'mailbox.list',
|
||||
messageId: createMessageId(),
|
||||
timestamp: createTimestamp(),
|
||||
mailbox,
|
||||
limit: options?.limit,
|
||||
offset: options?.offset,
|
||||
};
|
||||
return this.send<IMailboxListResponse>(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a specific email from a mailbox
|
||||
* @param mailbox The mailbox containing the email
|
||||
* @param emailId The ID of the email to fetch
|
||||
* @returns Promise resolving to the Smartmail or null if not found
|
||||
*/
|
||||
public async fetchEmail(mailbox: string, emailId: string): Promise<Smartmail<any> | null> {
|
||||
const request: IMailFetchRequest = {
|
||||
type: 'mail.fetch',
|
||||
messageId: createMessageId(),
|
||||
timestamp: createTimestamp(),
|
||||
mailbox,
|
||||
emailId,
|
||||
};
|
||||
const response = await this.send<IMailFetchResponse>(request);
|
||||
if (response.email) {
|
||||
return Smartmail.fromObject(response.email);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check delivery status of a sent email
|
||||
* @param deliveryId The delivery ID returned from sendEmail
|
||||
* @returns Promise resolving to the status response
|
||||
*/
|
||||
public async getStatus(deliveryId: string): Promise<IMailStatusResponse> {
|
||||
const request: IMailStatusRequest = {
|
||||
type: 'mail.status',
|
||||
messageId: createMessageId(),
|
||||
timestamp: createTimestamp(),
|
||||
deliveryId,
|
||||
};
|
||||
return this.send<IMailStatusResponse>(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a wire message to the endpoint
|
||||
* @param message The message to send
|
||||
* @returns Promise resolving to the response
|
||||
*/
|
||||
private async send<T extends IWireMessage>(message: IWireMessage): Promise<T> {
|
||||
let request = plugins.SmartRequest.create()
|
||||
.url(this.endpoint)
|
||||
.header('Content-Type', 'application/json');
|
||||
|
||||
if (this.authToken) {
|
||||
request = request.header('Authorization', `Bearer ${this.authToken}`);
|
||||
}
|
||||
|
||||
const response = await request.json(message).post();
|
||||
const responseData = await response.json();
|
||||
|
||||
return responseData as T;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
// node native scope
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export { path };
|
||||
export { fs, path };
|
||||
|
||||
// pushrocks scope
|
||||
import * as smartdns from '@push.rocks/smartdns/client';
|
||||
import * as smartfile from '@push.rocks/smartfile';
|
||||
import * as smartmustache from '@push.rocks/smartmustache';
|
||||
import * as smartpath from '@push.rocks/smartpath';
|
||||
import * as smartrequest from '@push.rocks/smartrequest';
|
||||
import SmartRequest from '@push.rocks/smartrequest';
|
||||
|
||||
export { smartdns, smartfile, smartmustache, smartpath, smartrequest };
|
||||
export { smartdns, smartfile, smartmustache, smartpath, SmartRequest };
|
||||
|
||||
188
ts/smartmail.wire.ts
Normal file
188
ts/smartmail.wire.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import type { ISmartmailJson } from './smartmail.classes.smartmail.js';
|
||||
|
||||
// ==========================================
|
||||
// Base Message Structure
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Base interface for all wire messages
|
||||
*/
|
||||
export interface IWireMessage {
|
||||
type: string;
|
||||
messageId: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Mail Send Operations
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Request to send an email
|
||||
*/
|
||||
export interface IMailSendRequest extends IWireMessage {
|
||||
type: 'mail.send';
|
||||
email: ISmartmailJson;
|
||||
options?: {
|
||||
validateBeforeSend?: boolean;
|
||||
templateVariables?: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Response after sending an email
|
||||
*/
|
||||
export interface IMailSendResponse extends IWireMessage {
|
||||
type: 'mail.send.response';
|
||||
success: boolean;
|
||||
error?: string;
|
||||
deliveryId?: string;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Mailbox Operations
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Request to list emails in a mailbox
|
||||
*/
|
||||
export interface IMailboxListRequest extends IWireMessage {
|
||||
type: 'mailbox.list';
|
||||
mailbox: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response with mailbox email list
|
||||
*/
|
||||
export interface IMailboxListResponse extends IWireMessage {
|
||||
type: 'mailbox.list.response';
|
||||
mailbox: string;
|
||||
emails: ISmartmailJson[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Mail Fetch Operations
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Request to fetch a specific email
|
||||
*/
|
||||
export interface IMailFetchRequest extends IWireMessage {
|
||||
type: 'mail.fetch';
|
||||
mailbox: string;
|
||||
emailId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response with fetched email
|
||||
*/
|
||||
export interface IMailFetchResponse extends IWireMessage {
|
||||
type: 'mail.fetch.response';
|
||||
email: ISmartmailJson | null;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Mail Status Operations
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Request to check delivery status
|
||||
*/
|
||||
export interface IMailStatusRequest extends IWireMessage {
|
||||
type: 'mail.status';
|
||||
deliveryId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response with delivery status
|
||||
*/
|
||||
export interface IMailStatusResponse extends IWireMessage {
|
||||
type: 'mail.status.response';
|
||||
deliveryId: string;
|
||||
status: 'queued' | 'sending' | 'sent' | 'failed';
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Settings Operations (Extensible)
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* SMTP server configuration
|
||||
*/
|
||||
export interface ISmtpSettings {
|
||||
host: string;
|
||||
port: number;
|
||||
secure: boolean;
|
||||
username?: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wire settings - extensible with arbitrary key-value pairs
|
||||
*/
|
||||
export interface IWireSettings {
|
||||
smtp?: ISmtpSettings;
|
||||
defaultFrom?: string;
|
||||
defaultReplyTo?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to update settings
|
||||
*/
|
||||
export interface ISettingsUpdateRequest extends IWireMessage {
|
||||
type: 'settings.update';
|
||||
settings: IWireSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response after updating settings
|
||||
*/
|
||||
export interface ISettingsUpdateResponse extends IWireMessage {
|
||||
type: 'settings.update.response';
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Union Type for Type Discrimination
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Union of all wire message types for type discrimination
|
||||
*/
|
||||
export type TWireMessage =
|
||||
| IMailSendRequest
|
||||
| IMailSendResponse
|
||||
| IMailboxListRequest
|
||||
| IMailboxListResponse
|
||||
| IMailFetchRequest
|
||||
| IMailFetchResponse
|
||||
| IMailStatusRequest
|
||||
| IMailStatusResponse
|
||||
| ISettingsUpdateRequest
|
||||
| ISettingsUpdateResponse;
|
||||
|
||||
// ==========================================
|
||||
// Helper Functions
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Creates a unique message ID
|
||||
* @returns UUID string
|
||||
*/
|
||||
export function createMessageId(): string {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an ISO timestamp
|
||||
* @returns ISO 8601 timestamp string
|
||||
*/
|
||||
export function createTimestamp(): string {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
@@ -8,12 +8,10 @@
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist_ts",
|
||||
"rootDir": "./ts",
|
||||
"strict": false,
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"skipLibCheck": false
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"ts/**/*"
|
||||
|
||||
Reference in New Issue
Block a user