2 Commits
v2.1.0 ... main

14 changed files with 3970 additions and 2568 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

@@ -1,6 +1,6 @@
{
"name": "@push.rocks/smartmail",
"version": "2.1.0",
"version": "2.2.0",
"private": false,
"description": "A unified format for representing and dealing with emails, with support for attachments and email validation.",
"main": "dist_ts/index.js",
@@ -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

494
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,47 +424,113 @@ 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
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
@@ -294,7 +540,7 @@ This project is owned and maintained by Task Venture Capital GmbH. The names and
### Company Information
Task Venture Capital GmbH
Task Venture Capital GmbH
Registered at District court Bremen HRB 35230 HB, Germany
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.

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';
@@ -134,14 +134,285 @@ tap.test('should add email headers', async () => {
body: 'Test body',
from: 'noreply@example.com'
});
headerMailer.addHeader('X-Test-Header', 'TestValue');
headerMailer.addHeader('X-Tracking-ID', '12345');
const mimeObj = await headerMailer.toMimeFormat();
expect(mimeObj.headers['X-Test-Header']).toEqual('TestValue');
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,27 +108,30 @@ 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
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;
}
/**
@@ -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/**/*"