6 Commits
v2.0.0 ... main

14 changed files with 4091 additions and 2542 deletions

View File

@@ -1,5 +1,33 @@
# Changelog # 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.
- Introduced getMxRecords in EmailAddressValidator to extract MX records from DNS responses.
- Added isDisposableEmail to determine if an email is from a disposable domain.
- Added isRoleAccount to identify role-based email addresses.
- Implemented applyVariables in Smartmail to update subject, body, and htmlBody templates with provided data.
## 2025-05-07 - 2.0.1 - fix(readme)
Update documentation to include usage of creation object reference and update API details.
- Added a new section explaining how to use the creationObjectRef in Smartmail.
- Included detailed examples on retrieving and using the creation object reference.
- Updated the API reference to document the getCreationObject method.
## 2025-05-07 - 2.0.0 - BREAKING CHANGE(smartmail) ## 2025-05-07 - 2.0.0 - BREAKING CHANGE(smartmail)
Improve email validation and Smartmail features: add detailed validation for email parts, caching for MX lookups, multi-recipient support, custom headers, and update dependency imports and build scripts. Improve email validation and Smartmail features: add detailed validation for email parts, caching for MX lookups, multi-recipient support, custom headers, and update dependency imports and build scripts.

View File

@@ -1,6 +1,6 @@
{ {
"name": "@push.rocks/smartmail", "name": "@push.rocks/smartmail",
"version": "2.0.0", "version": "2.2.0",
"private": false, "private": false,
"description": "A unified format for representing and dealing with emails, with support for attachments and email validation.", "description": "A unified format for representing and dealing with emails, with support for attachments and email validation.",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",
@@ -9,24 +9,22 @@
"author": "Lossless GmbH", "author": "Lossless GmbH",
"license": "UNLICENSED", "license": "UNLICENSED",
"scripts": { "scripts": {
"test": "(tstest test/)", "test": "(tstest test/ --verbose)",
"format": "(gitzone format)",
"build": "(tsbuild tsfolders --allowimplicitany)", "build": "(tsbuild tsfolders --allowimplicitany)",
"buildDocs": "tsdoc" "buildDocs": "tsdoc"
}, },
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^2.3.2", "@git.zone/tsbuild": "^3.1.2",
"@git.zone/tsrun": "^1.3.3", "@git.zone/tsrun": "^2.0.0",
"@git.zone/tstest": "^1.0.96", "@git.zone/tstest": "^3.1.3",
"@push.rocks/tapbundle": "^6.0.3",
"@types/node": "^22.15.14" "@types/node": "^22.15.14"
}, },
"dependencies": { "dependencies": {
"@push.rocks/smartdns": "^6.2.2", "@push.rocks/smartdns": "^7.6.1",
"@push.rocks/smartfile": "^11.2.0", "@push.rocks/smartfile": "^13.0.1",
"@push.rocks/smartmustache": "^3.0.2", "@push.rocks/smartmustache": "^3.0.2",
"@push.rocks/smartpath": "^5.0.11", "@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartrequest": "^2.0.18" "@push.rocks/smartrequest": "^5.0.1"
}, },
"files": [ "files": [
"ts/**/*", "ts/**/*",

4941
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

495
readme.md
View File

@@ -1,48 +1,71 @@
# @push.rocks/smartmail # @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 ## 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 ```bash
pnpm add @push.rocks/smartmail pnpm add @push.rocks/smartmail
``` ```
This will add `@push.rocks/smartmail` to your project's dependencies.
## Features ## Features
- **Advanced Email Address Validation**: Validate email format, check for disposable/free domains, and verify MX records - 📧 **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, attachments, HTML content, and custom headers - 📝 **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 - 🎨 **Template Support** - Use mustache templates for dynamic content in subject, body, and HTML
- **MIME Formatting**: Convert emails to standard MIME format for sending - 📨 **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 - **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 ## 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 ### Importing the Module
First, ensure you're using ESM (ECMAScript Modules) syntax in your TypeScript project. Then, import the necessary classes:
```typescript ```typescript
import { import {
Smartmail, Smartmail,
EmailAddressValidator EmailAddressValidator,
WireTarget,
WireParser,
createMessageId,
createTimestamp
} from '@push.rocks/smartmail'; } 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 ### Basic Email Validation
```typescript ```typescript
// Create validator with default options
const emailValidator = new EmailAddressValidator(); const emailValidator = new EmailAddressValidator();
// Validate an email address
const result = await emailValidator.validate('user@example.com'); const result = await emailValidator.validate('user@example.com');
console.log(result); console.log(result);
/* /*
@@ -62,14 +85,13 @@ console.log(result);
### Advanced Validation Options ### Advanced Validation Options
```typescript ```typescript
// Create validator with custom options
const validator = new EmailAddressValidator({ const validator = new EmailAddressValidator({
skipOnlineDomainFetch: true, // Use only local domain list skipOnlineDomainFetch: true, // Use only local domain list
cacheDnsResults: true, // Cache DNS lookups cacheDnsResults: true, // Cache DNS lookups
cacheExpiryMs: 7200000 // Cache expires after 2 hours 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 isValidFormat = validator.isValidEmailFormat('user@example.com');
const isValidLocalPart = validator.isValidLocalPart('user'); const isValidLocalPart = validator.isValidLocalPart('user');
const isValidDomain = validator.isValidDomainPart('example.com'); 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 ### Basic Email Creation
```typescript ```typescript
// Create a simple email
const email = new Smartmail({ const email = new Smartmail({
from: 'sender@example.com', from: 'sender@example.com',
to: ['recipient@example.com'], to: ['recipient@example.com'],
@@ -95,20 +132,37 @@ const email = new Smartmail({
}); });
``` ```
### Adding Recipients ### Using Creation Object Reference
Associate any typed data with your email for tracking and context preservation:
```typescript ```typescript
const email = new Smartmail({ interface OrderNotification {
from: 'sender@example.com', orderId: string;
subject: 'Meeting Invitation', customerName: string;
body: 'Please join our meeting' items: string[];
total: number;
}
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}}!',
creationObjectRef: {
orderId: '12345',
customerName: 'John Smith',
items: ['Product A', 'Product B'],
total: 99.95
}
}); });
// Add recipients in various ways // Later, retrieve the original reference data
email.addRecipient('primary@example.com'); const orderData = orderEmail.getCreationObject();
email.addRecipients(['user1@example.com', 'user2@example.com']); console.log(`Processing email for order ${orderData.orderId}`);
email.addRecipient('manager@example.com', 'cc');
email.addRecipients(['observer1@example.com', 'observer2@example.com'], 'bcc'); // Use the reference data for templating
const subject = orderEmail.getSubject(orderData); // "Your Order #12345 Confirmation"
``` ```
### Template Variables in Subject and Body ### Template Variables in Subject and Body
@@ -117,20 +171,22 @@ email.addRecipients(['observer1@example.com', 'observer2@example.com'], 'bcc');
const template = new Smartmail({ const template = new Smartmail({
from: 'notifications@example.com', from: 'notifications@example.com',
subject: 'Welcome, {{name}}!', subject: 'Welcome, {{name}}!',
body: 'Hello {{name}},\n\nWelcome to our service. Your account ({{email}}) has been activated.', body: 'Hello {{name}},\n\nYour 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>' 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 subject = template.getSubject({ name: 'John Doe' });
const plainBody = template.getBody({ name: 'John Doe', email: 'john@example.com' }); 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 ### Adding Attachments
```typescript ```typescript
import { Smartfile } from '@push.rocks/smartfile'; import { SmartFile } from '@push.rocks/smartfile';
const email = new Smartmail({ const email = new Smartmail({
from: 'sender@example.com', from: 'sender@example.com',
@@ -139,32 +195,15 @@ const email = new Smartmail({
body: 'Please find the attached report.' body: 'Please find the attached report.'
}); });
// Add file attachments const report = await SmartFile.fromFilePath('/path/to/report.pdf');
const report = new Smartfile.fromLocalPath('/path/to/report.pdf'); const image = await SmartFile.fromFilePath('/path/to/image.png');
const image = new Smartfile.fromLocalPath('/path/to/image.png');
email.addAttachment(report); email
email.addAttachment(image); .addAttachment(report)
.addAttachment(image);
``` ```
### Setting Email Importance and Headers ### Converting to MIME Format
```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
```typescript ```typescript
const email = new Smartmail({ const email = new Smartmail({
@@ -173,25 +212,211 @@ const email = new Smartmail({
subject: 'Hello {{name}}', subject: 'Hello {{name}}',
body: 'Text version: Hello {{name}}', body: 'Text version: Hello {{name}}',
htmlBody: '<p>HTML version: Hello <strong>{{name}}</strong></p>', 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' }); 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 ## 📡 Wire Format Serialization
console.log(mimeObj);
/* 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', from: 'sender@example.com',
to: ['recipient@example.com'], to: ['recipient@example.com'],
subject: 'Hello John', subject: 'Wire Format Test',
text: 'Text version: Hello John', body: 'This email can be serialized!'
html: '<p>HTML version: Hello <strong>John</strong></p>', })
attachments: [...], .addAttachment(someFile)
headers: {...} .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 ## API Reference
@@ -199,41 +424,109 @@ console.log(mimeObj);
### EmailAddressValidator ### EmailAddressValidator
#### Constructor Options #### Constructor Options
- `skipOnlineDomainFetch`: Boolean (default: false) - Skip fetching domain list from online source | Option | Type | Default | Description |
- `cacheDnsResults`: Boolean (default: true) - Cache DNS lookup results |--------|------|---------|-------------|
- `cacheExpiryMs`: Number (default: 3600000) - Cache expiry in milliseconds | `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 #### Methods
- `validate(email: string)`: Validates email address completeness | Method | Returns | Description |
- `isValidEmailFormat(email: string)`: Checks if email format is valid |--------|---------|-------------|
- `isValidLocalPart(localPart: string)`: Validates local part of email | `validate(email)` | `Promise<IEmailValidationResult>` | Full validation with format, MX, and domain checks |
- `isValidDomainPart(domainPart: string)`: Validates domain part of email | `isValidEmailFormat(email)` | `boolean` | Check if email format is valid |
- `checkMxRecords(domain: string)`: Checks MX records for a domain | `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 ### Smartmail
#### Constructor Options #### Constructor Options
- `from`: Email address of sender | Option | Type | Default | Description |
- `to`, `cc`, `bcc`: Optional arrays of recipient email addresses |--------|------|---------|-------------|
- `subject`: Email subject line | `from` | string | *required* | Email address of sender |
- `body`: Plain text email body | `to` | string[] | `[]` | Array of primary recipient email addresses |
- `htmlBody`: Optional HTML version of email body | `cc` | string[] | `[]` | Array of CC recipient email addresses |
- `replyTo`: Optional reply-to email address | `bcc` | string[] | `[]` | Array of BCC recipient email addresses |
- `headers`: Optional key-value pairs of custom headers | `subject` | string | *required* | Email subject line (supports templates) |
- `priority`: 'high' | 'normal' | 'low' (default: 'normal') | `body` | string | *required* | Plain text email body (supports templates) |
- `validateEmails`: Boolean (default: false) - Validate all emails | `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 #### Methods
- `addRecipient(email, type?)`: Add a single recipient (to/cc/bcc) | Method | Returns | Description |
- `addRecipients(emails, type?)`: Add multiple recipients |--------|---------|-------------|
- `setReplyTo(email)`: Set reply-to address | `sendEmail(email)` | `Promise<IMailSendResponse>` | Send an email through this target |
- `setPriority(priority)`: Set email priority | `updateSettings(settings)` | `Promise<ISettingsUpdateResponse>` | Update settings on the target |
- `addHeader(name, value)`: Add custom header | `listMailbox(mailbox, options?)` | `Promise<IMailboxListResponse>` | List emails in a mailbox |
- `getSubject(data?)`: Get processed subject with template variables | `fetchEmail(mailbox, emailId)` | `Promise<Smartmail \| null>` | Fetch a specific email |
- `getBody(data?)`: Get processed plain text body | `getStatus(deliveryId)` | `Promise<IMailStatusResponse>` | Check delivery status |
- `getHtmlBody(data?)`: Get processed HTML body
- `validateAllEmails()`: Validate all email addresses ### WireParser
- `toMimeFormat(data?)`: Convert to MIME format object
#### 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 ## 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 smartmail from '../ts/index.js';
import * as plugins from '../ts/smartmail.plugins.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'); 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 = { export const commitinfo = {
name: '@push.rocks/smartmail', name: '@push.rocks/smartmail',
version: '2.0.0', version: '2.2.0',
description: 'A unified format for representing and dealing with emails, with support for attachments and email validation.' 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.smartmail.js';
export * from './smartmail.classes.emailaddressvalidator.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

@@ -110,6 +110,65 @@ export class EmailAddressValidator {
return result; return result;
} }
/**
* Gets the MX records for a domain
* @param domain The domain to get MX records for
* @returns Array of MX records as strings
*/
public async getMxRecords(domain: string): Promise<string[]> {
const mxRecords = await this.checkMxRecords(domain);
if (!mxRecords || !Array.isArray(mxRecords)) {
return [];
}
// Extract exchange values from MX records
return mxRecords.map((record: any) => {
if (record && record.exchange) {
return record.exchange;
}
return '';
}).filter(Boolean);
}
/**
* Checks if an email is from a disposable domain
* @param email The email address to check
* @returns True if the email is from a disposable domain
*/
public async isDisposableEmail(email: string): Promise<boolean> {
await this.fetchDomains();
if (!this.isValidEmailFormat(email)) {
return false;
}
const domainPart = email.split('@')[1];
return this.domainMap[domainPart] === 'disposable';
}
/**
* Checks if an email is a role account (e.g. info@, support@, etc.)
* @param email The email address to check
* @returns True if the email is a role account
*/
public isRoleAccount(email: string): boolean {
if (!this.isValidEmailFormat(email)) {
return false;
}
const localPart = email.split('@')[0].toLowerCase();
const roleAccounts = [
'admin', 'administrator', 'webmaster', 'hostmaster', 'postmaster',
'info', 'support', 'sales', 'marketing', 'contact', 'help',
'abuse', 'noc', 'security', 'billing', 'donations', 'donate',
'staff', 'office', 'hr', 'jobs', 'careers', 'team',
'enquiry', 'enquiries', 'feedback', 'no-reply', 'noreply'
];
return roleAccounts.includes(localPart);
}
/** /**
* Validates an email address * Validates an email address
* @param emailArg The email address to validate * @param emailArg The email address to validate
@@ -192,8 +251,9 @@ export class EmailAddressValidator {
*/ */
public async fetchDomains() { public async fetchDomains() {
if (!this.domainMap) { if (!this.domainMap) {
const localFileString = plugins.smartfile.fs.toStringSync( const localFileString = plugins.fs.readFileSync(
plugins.path.join(paths.assetDir, 'domains.json') plugins.path.join(paths.assetDir, 'domains.json'),
'utf8'
); );
const localFileObject = JSON.parse(localFileString); const localFileObject = JSON.parse(localFileString);
@@ -203,12 +263,10 @@ export class EmailAddressValidator {
} }
try { try {
const onlineFileObject = ( const response = await plugins.SmartRequest.create()
await plugins.smartrequest.getJson( .url('https://raw.githubusercontent.com/romainsimon/emailvalid/master/domains.json')
'https://raw.githubusercontent.com/romainsimon/emailvalid/master/domains.json' .get();
) this.domainMap = await response.json();
).body;
this.domainMap = onlineFileObject;
} catch (e) { } catch (e) {
this.domainMap = localFileObject; this.domainMap = localFileObject;
} }

View File

@@ -1,5 +1,7 @@
import * as plugins from './smartmail.plugins.js'; import * as plugins from './smartmail.plugins.js';
import { EmailAddressValidator } from './smartmail.classes.emailaddressvalidator.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 EmailAddress = string;
export type EmailAddressList = EmailAddress[]; export type EmailAddressList = EmailAddress[];
@@ -19,6 +21,33 @@ export interface ISmartmailOptions<T> {
validateEmails?: boolean; 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 { export interface IMimeAttachment {
filename: string; filename: string;
content: Buffer; content: Buffer;
@@ -51,9 +80,11 @@ export class Smartmail<T> {
/** /**
* Adds an attachment to the email * Adds an attachment to the email
* @param smartfileArg The file to attach * @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); this.attachments.push(smartfileArg);
return this;
} }
/** /**
@@ -74,6 +105,35 @@ export class Smartmail<T> {
return smartmustache.applyData(dataArg); return smartmustache.applyData(dataArg);
} }
/**
* 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>): this {
if (!variables || typeof variables !== 'object') {
return this;
}
// Process the subject, body, and HTML body with the provided variables
if (this.options.subject) {
const subjectMustache = new plugins.smartmustache.SmartMustache(this.options.subject);
this.options.subject = subjectMustache.applyData(variables);
}
if (this.options.body) {
const bodyMustache = new plugins.smartmustache.SmartMustache(this.options.body);
this.options.body = bodyMustache.applyData(variables);
}
if (this.options.htmlBody) {
const htmlBodyMustache = new plugins.smartmustache.SmartMustache(this.options.htmlBody);
this.options.htmlBody = htmlBodyMustache.applyData(variables);
}
return this;
}
/** /**
* Gets the processed plain text body with template variables applied * Gets the processed plain text body with template variables applied
* @param dataArg Data to apply to the template * @param dataArg Data to apply to the template
@@ -102,55 +162,65 @@ export class Smartmail<T> {
* Adds a recipient to the email * Adds a recipient to the email
* @param email Email address to add * @param email Email address to add
* @param type Type of recipient (to, cc, bcc) * @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]) { if (!this.options[type]) {
this.options[type] = []; this.options[type] = [];
} }
this.options[type]!.push(email); this.options[type]!.push(email);
return this;
} }
/** /**
* Adds multiple recipients to the email * Adds multiple recipients to the email
* @param emails Email addresses to add * @param emails Email addresses to add
* @param type Type of recipients (to, cc, bcc) * @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]) { if (!this.options[type]) {
this.options[type] = []; this.options[type] = [];
} }
this.options[type] = [...this.options[type]!, ...emails]; this.options[type] = [...this.options[type]!, ...emails];
return this;
} }
/** /**
* Sets the reply-to address * Sets the reply-to address
* @param email Email address for reply-to * @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; this.options.replyTo = email;
return this;
} }
/** /**
* Sets the priority of the email * Sets the priority of the email
* @param priority Priority level * @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; this.options.priority = priority;
return this;
} }
/** /**
* Adds a custom header to the email * Adds a custom header to the email
* @param name Header name * @param name Header name
* @param value Header value * @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) { if (!this.options.headers) {
this.options.headers = {}; this.options.headers = {};
} }
this.options.headers[name] = value; this.options.headers[name] = value;
return this;
} }
/** /**
@@ -177,6 +247,102 @@ export class Smartmail<T> {
return results; 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 * Converts the email to a MIME format object for sending
* @param dataArg Data to apply to templates * @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 // node native scope
import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
export { path }; export { fs, path };
// pushrocks scope // pushrocks scope
import * as smartdns from '@push.rocks/smartdns/client'; import * as smartdns from '@push.rocks/smartdns/client';
import * as smartfile from '@push.rocks/smartfile'; import * as smartfile from '@push.rocks/smartfile';
import * as smartmustache from '@push.rocks/smartmustache'; import * as smartmustache from '@push.rocks/smartmustache';
import * as smartpath from '@push.rocks/smartpath'; 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, "esModuleInterop": true,
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true,
"declaration": true, "declaration": true,
"sourceMap": true,
"outDir": "./dist_ts", "outDir": "./dist_ts",
"rootDir": "./ts", "rootDir": "./ts",
"strict": false, "strict": false,
"lib": ["ES2022", "DOM"], "skipLibCheck": true
"skipLibCheck": false
}, },
"include": [ "include": [
"ts/**/*" "ts/**/*"