feat(wire): Add wire protocol, WireTarget & WireParser, Smartmail JSON serialization; refactor plugins and update dependencies

This commit is contained in:
2025-11-29 15:36:36 +00:00
parent 4277ace8cd
commit ddf442274e
14 changed files with 3969 additions and 2567 deletions

View File

@@ -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.

View File

@@ -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

File diff suppressed because it is too large Load Diff

490
readme.md
View File

@@ -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

View File

@@ -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();

View File

@@ -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.'
}

View File

@@ -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';

View File

@@ -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;
}

View File

@@ -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

View 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(),
};
}
}

View 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;
}
}

View File

@@ -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
View 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();
}

View File

@@ -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/**/*"