13 Commits

19 changed files with 11674 additions and 3855 deletions

View File

@@ -119,6 +119,6 @@ jobs:
run: | run: |
npmci node install stable npmci node install stable
npmci npm install npmci npm install
pnpm install -g @gitzone/tsdoc pnpm install -g @git.zone/tsdoc
npmci command tsdoc npmci command tsdoc
continue-on-error: true continue-on-error: true

1
.gitignore vendored
View File

@@ -18,3 +18,4 @@ dist/
dist_*/ dist_*/
# custom # custom
**/.claude/settings.local.json

61
changelog.md Normal file
View File

@@ -0,0 +1,61 @@
# 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)
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.
- Updated dependency references from '@gitzone/*' to '@git.zone/*'
- Enhanced EmailAddressValidator with RFC 5322 compliance, local part, domain part validation, and detailed result fields
- Implemented caching mechanism for MX record lookups
- Expanded Smartmail class: added support for multiple recipients, reply-to, custom headers, and MIME formatting
- Updated build scripts, package configuration, and documentation with more comprehensive usage examples and tests
## 2024-05-29 - 1.0.24 - misc
This release improved several project configurations and documentation details.
- docs: Updated the project description.
- config: Revised tsconfig settings.
- npmextra: Adjusted npmextra.json with updated githost values (applied on multiple commits).
## 2023-07-28 - 1.0.23 - misc
A couple of targeted fixes and organizational changes were introduced.
- core: Applied a core fix update.
- org: Switched to the new organization scheme.
## 2023-06-13 - 1.0.22 - core
Over a longer period (versions 1.0.22 down to 1.0.4), a series of minor core fixes were rolled out to improve stability and maintainability.
These commits—which, aside from their versionmarking commits, were solely “fix(core): update”—were consolidated into this summary.
## 2018-09-29 - 1.0.1 - initial
In the very early releases (from versions 1.0.3 to 1.0.1), the initial stabilization work set the stage for the project.
- npm: Adjusted access level (fix(npm): access level in 1.0.3).
- packagename: Updated package name details (fix(packagename): update in 1.0.2).
- core: Laid the groundwork with the initial core fix (fix(core): initial in 1.0.1).

View File

@@ -1,7 +1,7 @@
{ {
"npmci": { "npmci": {
"npmGlobalTools": [ "npmGlobalTools": [
"@gitzone/npmts", "@git.zone/npmts",
"ts-node" "ts-node"
], ],
"npmAccessLevel": "public" "npmAccessLevel": "public"
@@ -9,12 +9,27 @@
"gitzone": { "gitzone": {
"projectType": "npm", "projectType": "npm",
"module": { "module": {
"githost": "gitlab.com", "githost": "code.foss.global",
"gitscope": "push.rocks", "gitscope": "push.rocks",
"gitrepo": "smartmail", "gitrepo": "smartmail",
"description": "a unified format for representing and dealing with mails", "description": "A unified format for representing and dealing with emails, with support for attachments and email validation.",
"npmPackagename": "@push.rocks/smartmail", "npmPackagename": "@push.rocks/smartmail",
"license": "MIT" "license": "MIT",
"keywords": [
"email handling",
"email validation",
"email formatting",
"typescript",
"email attachment",
"smartmail",
"email development",
"email template",
"disposable email detection",
"freemail detection"
]
} }
},
"tsdoc": {
"legal": "\n## License and Legal Information\n\nThis 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. \n\n**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.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
} }
} }

View File

@@ -1,32 +1,30 @@
{ {
"name": "@push.rocks/smartmail", "name": "@push.rocks/smartmail",
"version": "1.0.24", "version": "2.2.0",
"private": false, "private": false,
"description": "a unified format for representing and dealing with mails", "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",
"typings": "dist_ts/index.d.ts", "typings": "dist_ts/index.d.ts",
"type": "module", "type": "module",
"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 --web --allowimplicitany)",
"buildDocs": "tsdoc" "buildDocs": "tsdoc"
}, },
"devDependencies": { "devDependencies": {
"@gitzone/tsbuild": "^2.1.66", "@git.zone/tsbuild": "^3.1.2",
"@gitzone/tsrun": "^1.2.44", "@git.zone/tsrun": "^2.0.0",
"@gitzone/tstest": "^1.0.77", "@git.zone/tstest": "^3.1.3",
"@push.rocks/tapbundle": "^5.0.12", "@types/node": "^22.15.14"
"@types/node": "^20.4.5"
}, },
"dependencies": { "dependencies": {
"@push.rocks/smartdns": "^5.0.4", "@push.rocks/smartdns": "^7.6.1",
"@push.rocks/smartfile": "^10.0.28", "@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/**/*",
@@ -42,5 +40,23 @@
], ],
"browserslist": [ "browserslist": [
"last 1 chrome versions" "last 1 chrome versions"
] ],
"keywords": [
"email handling",
"email validation",
"email formatting",
"typescript",
"email attachment",
"smartmail",
"email development",
"email template",
"disposable email detection",
"freemail detection"
],
"homepage": "https://code.foss.global/push.rocks/smartmail",
"repository": {
"type": "git",
"url": "https://code.foss.global/push.rocks/smartmail.git"
},
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
} }

13046
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

1
readme.hints.md Normal file
View File

@@ -0,0 +1 @@

563
readme.md
View File

@@ -1,37 +1,548 @@
# @push.rocks/smartmail # @push.rocks/smartmail
a unified format for representing and dealing with mails
## Availabililty and Links A unified format for representing and dealing with emails, with support for attachments, email validation, dynamic templating, and wire format serialization for microservice communication.
* [npmjs.org (npm package)](https://www.npmjs.com/package/@push.rocks/smartmail)
* [gitlab.com (source)](https://gitlab.com/push.rocks/smartmail)
* [github.com (source mirror)](https://github.com/push.rocks/smartmail)
* [docs (typedoc)](https://push.rocks.gitlab.io/smartmail/)
## Status for master ## Issue Reporting and Security
Status Category | Status Badge 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.
-- | --
GitLab Pipelines | [![pipeline status](https://gitlab.com/push.rocks/smartmail/badges/master/pipeline.svg)](https://lossless.cloud) ## Install
GitLab Pipline Test Coverage | [![coverage report](https://gitlab.com/push.rocks/smartmail/badges/master/coverage.svg)](https://lossless.cloud)
npm | [![npm downloads per month](https://badgen.net/npm/dy/@push.rocks/smartmail)](https://lossless.cloud) ```bash
Snyk | [![Known Vulnerabilities](https://badgen.net/snyk/push.rocks/smartmail)](https://lossless.cloud) pnpm add @push.rocks/smartmail
TypeScript Support | [![TypeScript](https://badgen.net/badge/TypeScript/>=%203.x/blue?icon=typescript)](https://lossless.cloud) ```
node Support | [![node](https://img.shields.io/badge/node->=%2010.x.x-blue.svg)](https://nodejs.org/dist/latest-v10.x/docs/api/)
Code Style | [![Code Style](https://badgen.net/badge/style/prettier/purple)](https://lossless.cloud) ## Features
PackagePhobia (total standalone install weight) | [![PackagePhobia](https://badgen.net/packagephobia/install/@push.rocks/smartmail)](https://lossless.cloud)
PackagePhobia (package size on registry) | [![PackagePhobia](https://badgen.net/packagephobia/publish/@push.rocks/smartmail)](https://lossless.cloud) - 📧 **Advanced Email Address Validation** - Validate email format, check for disposable/free domains, verify MX records, and detect role accounts
BundlePhobia (total size when bundled) | [![BundlePhobia](https://badgen.net/bundlephobia/minzip/@push.rocks/smartmail)](https://lossless.cloud) - 📝 **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 ## Usage
Use TypeScript for best in class intellisense! ### Importing the Module
## Contribution ```typescript
import {
Smartmail,
EmailAddressValidator,
WireTarget,
WireParser,
createMessageId,
createTimestamp
} from '@push.rocks/smartmail';
```
We are always happy for code contributions. If you are not the code contributing type that is ok. Still, maintaining Open Source repositories takes considerable time and thought. If you like the quality of what we do and our modules are useful to you we would appreciate a little monthly contribution: You can [contribute one time](https://lossless.link/contribute-onetime) or [contribute monthly](https://lossless.link/contribute). :) ## 🔗 Fluent Chainable API
For further information read the linked docs at the top of this readme. All mutation methods return `this`, enabling elegant method chaining:
## Legal ```typescript
> MIT licensed | **©** [Task Venture Capital GmbH](https://task.vc) const response = await new Smartmail({
| By using this npm module you agree to our [privacy policy](https://lossless.gmbH/privacy) 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
const emailValidator = new EmailAddressValidator();
const result = await emailValidator.validate('user@example.com');
console.log(result);
/*
{
valid: true, // Overall validity
formatValid: true, // Email format is valid
localPartValid: true, // Local part (before @) is valid
domainPartValid: true, // Domain part (after @) is valid
mxValid: true, // Domain has valid MX records
disposable: false, // Not a disposable email domain
freemail: false, // Not a free email provider
reason: 'Email is valid'
}
*/
```
### Advanced Validation Options
```typescript
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 separately
const isValidFormat = validator.isValidEmailFormat('user@example.com');
const isValidLocalPart = validator.isValidLocalPart('user');
const isValidDomain = validator.isValidDomainPart('example.com');
// Check for disposable or free email providers
const result = await validator.validate('user@gmail.com');
if (result.freemail) {
console.log('This is a free email provider');
}
```
### 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
const email = new Smartmail({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Hello from SmartMail',
body: 'This is a plain text email'
});
```
### Using Creation Object Reference
Associate any typed data with your email for tracking and context preservation:
```typescript
interface OrderNotification {
orderId: string;
customerName: string;
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
}
});
// Later, retrieve the original reference data
const orderData = orderEmail.getCreationObject();
console.log(`Processing email for order ${orderData.orderId}`);
// Use the reference data for templating
const subject = orderEmail.getSubject(orderData); // "Your Order #12345 Confirmation"
```
### Template Variables in Subject and Body
```typescript
const template = new Smartmail({
from: 'notifications@example.com',
subject: 'Welcome, {{name}}!',
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 when retrieving content
const subject = template.getSubject({ name: 'John Doe' });
const plainBody = template.getBody({ 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';
const email = new Smartmail({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Report Attached',
body: 'Please find the attached report.'
});
const report = await SmartFile.fromFilePath('/path/to/report.pdf');
const image = await SmartFile.fromFilePath('/path/to/image.png');
email
.addAttachment(report)
.addAttachment(image);
```
### Converting to MIME Format
```typescript
const email = new Smartmail({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Hello {{name}}',
body: 'Text version: Hello {{name}}',
htmlBody: '<p>HTML version: Hello <strong>{{name}}</strong></p>',
validateEmails: true
});
const mimeObj = await email.toMimeFormat({ name: 'John' });
// Use with nodemailer or other email sending libraries
```
## 📡 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: '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
### EmailAddressValidator
#### Constructor Options
| 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
| 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
| 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
| 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.
**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.
### Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
### Company Information
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.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

92
readme.plan.md Normal file
View File

@@ -0,0 +1,92 @@
# Improvement Plan for SmartMail
## Current Code Analysis
### Core Components
- **EmailAddressValidator**: Validates email addresses checking for validity, whether they are disposable or free
- Uses MX record checks to validate domains
- Has a domain map from external source that classifies domains as 'disposable' or 'freemail'
- Has placeholder 'reason' field in results that's not implemented ("todo")
- Fetches domain list from GitHub or falls back to local copy
- No validation of email format before DNS checks
- **Smartmail**: Email representation class
- Basic email structure with from, subject, body
- Supports file attachments via smartfile
- Uses mustache templates for subject and body
- Lacks recipient handling (to, cc, bcc)
- No method to send emails
- No HTML body support
### Dependencies
- Uses several @push.rocks packages:
- smartdns: For DNS lookups
- smartfile: For file handling/attachments
- smartmustache: For template processing
- smartpath: For path operations
- smartrequest: For HTTP requests
### Testing
- Basic tests for EmailAddressValidator
- Tests validation of regular, free, and disposable emails
- No tests for invalid email formats or edge cases
- Minimal test for Smartmail creation
- No tests for attachments, templates, or other features
## Enhanced Improvement Plan
1. [x] Complete the EmailAddressValidator implementation
- [x] Implement proper reason messages for validation results
- [x] Add email format validation before DNS checks (RFC 5322 compliance)
- [x] Add local part validation (check for illegal characters, proper format)
- [x] Improve domain validation (check syntax before DNS lookup)
- [x] Add email normalization (handle case sensitivity, plus addressing)
- [x] Implement caching mechanism for DNS lookups to improve performance
- [x] Add option to disable online domain list fetching
2. [x] Enhance the Smartmail class
- [x] Add support for multiple recipients (to, cc, bcc arrays)
- [x] Add email preparing capabilities via MIME format
- [x] Support HTML email bodies with plain text fallback
- [x] Add reply-to and headers support
- [x] Implement method to convert to standard email formats (MIME)
- [x] Add email priority and importance flags
- [x] Add validation of email addresses used in from/to/cc/bcc
3. [x] Improve testing
- [x] Add tests for email format validation (valid/invalid formats)
- [x] Test domain validation edge cases (non-existent domains, etc.)
- [x] Add tests for attachment handling
- [x] Test template processing with different data structures
- [x] Add tests for HTML emails and conversion
- [x] Test recipient handling with multiple addresses
4. [x] Performance & security improvements
- [x] Optimize domain list handling
- [x] Implement intelligent caching strategy for validation results
- [x] Add configuration options for external service calls
- [x] Ensure secure handling of email data and attachments
5. [x] Documentation improvements
- [x] Update README with comprehensive examples
- [x] Add detailed API documentation with JSDoc
- [x] Document all configuration options
- [x] Add usage examples for common scenarios
- [x] Document security considerations
- [x] Add TypeScript type documentation
6. [ ] Advanced features
- [ ] DKIM/SPF validation support
- [ ] Implement email address suggestions for typos
- [ ] Add disposable email detection improvements
- [ ] Support for internationalized email addresses (IDN)
- [ ] Email address reputation checking
- [ ] Add email deliverability scoring
- [ ] Implement bounce address validation
7. [x] Code quality
- [x] Add more TypeScript interfaces for clearer API definitions
- [x] Improve error handling with specific error types
- [x] Add configuration options via constructor
- [x] Make domain list updates configurable
- [x] Improve code organization with better separation of concerns

View File

@@ -1,36 +1,61 @@
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';
let emailAddressValidatorInstance: smartmail.EmailAddressValidator; let emailAddressValidatorInstance: smartmail.EmailAddressValidator;
// EmailAddressValidator Tests
tap.test('should create an instance of EmailAddressValidator', async () => { tap.test('should create an instance of EmailAddressValidator', async () => {
emailAddressValidatorInstance = new smartmail.EmailAddressValidator(); emailAddressValidatorInstance = new smartmail.EmailAddressValidator();
expect(emailAddressValidatorInstance).toBeInstanceOf(smartmail.EmailAddressValidator); expect(emailAddressValidatorInstance).toBeInstanceOf(smartmail.EmailAddressValidator);
}); });
tap.test('should validate an email', async () => { tap.test('should validate an email with detailed information', async () => {
const result = await emailAddressValidatorInstance.validate('sandbox@bleu.de'); const result = await emailAddressValidatorInstance.validate('sandbox@bleu.de');
expect(result.freemail).toBeFalse(); expect(result.freemail).toBeFalse();
expect(result.disposable).toBeFalse(); expect(result.disposable).toBeFalse();
console.log(result); expect(result.formatValid).toBeTrue();
expect(result.localPartValid).toBeTrue();
expect(result.domainPartValid).toBeTrue();
expect(result.reason).toBeDefined();
}); });
tap.test('should recognize an email as freemail', async () => { tap.test('should recognize an email as freemail', async () => {
const result = await emailAddressValidatorInstance.validate('sandbox@gmail.com'); const result = await emailAddressValidatorInstance.validate('sandbox@gmail.com');
expect(result.freemail).toBeTrue(); expect(result.freemail).toBeTrue();
expect(result.disposable).toBeFalse(); expect(result.disposable).toBeFalse();
console.log(result); expect(result.formatValid).toBeTrue();
expect(result.valid).toBeTrue();
}); });
tap.test('should recognize an email as disposable', async () => { tap.test('should recognize an email as disposable', async () => {
const result = await emailAddressValidatorInstance.validate('sandbox@gmx.de'); const result = await emailAddressValidatorInstance.validate('sandbox@gmx.de');
expect(result.freemail).toBeFalse(); expect(result.freemail).toBeFalse();
expect(result.disposable).toBeTrue(); expect(result.disposable).toBeTrue();
console.log(result); expect(result.formatValid).toBeTrue();
}); });
tap.test('should create a SmartMail', async () => { tap.test('should detect invalid email format', async () => {
const testSmartmail = new smartmail.Smartmail({ const result = await emailAddressValidatorInstance.validate('invalid-email');
expect(result.formatValid).toBeFalse();
expect(result.valid).toBeFalse();
expect(result.reason).toEqual('Invalid email format');
});
tap.test('should validate email format parts separately', async () => {
expect(emailAddressValidatorInstance.isValidEmailFormat('valid.email@example.com')).toBeTrue();
expect(emailAddressValidatorInstance.isValidEmailFormat('invalid-email')).toBeFalse();
expect(emailAddressValidatorInstance.isValidLocalPart('valid.local.part')).toBeTrue();
expect(emailAddressValidatorInstance.isValidLocalPart('invalid..part')).toBeFalse();
expect(emailAddressValidatorInstance.isValidDomainPart('example.com')).toBeTrue();
expect(emailAddressValidatorInstance.isValidDomainPart('invalid')).toBeFalse();
});
// Smartmail Tests
let testSmartmail: smartmail.Smartmail<any>;
tap.test('should create a SmartMail instance', async () => {
testSmartmail = new smartmail.Smartmail({
body: 'hi there', body: 'hi there',
from: 'noreply@mail.lossless.com', from: 'noreply@mail.lossless.com',
subject: 'hi from here', subject: 'hi from here',
@@ -38,4 +63,356 @@ tap.test('should create a SmartMail', async () => {
expect(testSmartmail).toBeInstanceOf(smartmail.Smartmail); expect(testSmartmail).toBeInstanceOf(smartmail.Smartmail);
}); });
tap.start(); tap.test('should handle email recipients', async () => {
testSmartmail.addRecipient('user1@example.com');
testSmartmail.addRecipients(['user2@example.com', 'user3@example.com'], 'cc');
testSmartmail.addRecipient('user4@example.com', 'bcc');
expect(testSmartmail.options.to!.length).toEqual(1);
expect(testSmartmail.options.cc!.length).toEqual(2);
expect(testSmartmail.options.bcc!.length).toEqual(1);
});
tap.test('should set reply-to and priority', async () => {
testSmartmail.setReplyTo('replies@example.com');
testSmartmail.setPriority('high');
expect(testSmartmail.options.replyTo).toEqual('replies@example.com');
expect(testSmartmail.options.priority).toEqual('high');
});
tap.test('should apply template data to subject and body', async () => {
const templateMailer = new smartmail.Smartmail({
subject: 'Hello {{name}}',
body: 'Welcome, {{name}}! Your ID is {{userId}}.',
from: 'noreply@example.com'
});
const data = { name: 'John Doe', userId: '12345' };
expect(templateMailer.getSubject(data)).toEqual('Hello John Doe');
expect(templateMailer.getBody(data)).toEqual('Welcome, John Doe! Your ID is 12345.');
});
tap.test('should handle HTML email body', async () => {
const htmlMailer = new smartmail.Smartmail({
subject: 'HTML Test',
body: 'Plain text version',
htmlBody: '<h1>{{title}}</h1><p>This is an HTML email with {{variable}}.</p>',
from: 'noreply@example.com'
});
const data = { title: 'Welcome', variable: 'dynamic content' };
const htmlContent = htmlMailer.getHtmlBody(data);
expect(htmlContent).toEqual('<h1>Welcome</h1><p>This is an HTML email with dynamic content.</p>');
});
tap.test('should convert to MIME format', async () => {
const mimeMailer = new smartmail.Smartmail({
subject: 'MIME Test',
body: 'Plain text content',
htmlBody: '<p>HTML content</p>',
from: 'sender@example.com',
to: ['recipient@example.com'],
headers: { 'X-Custom': 'Value' }
});
const mimeObj = await mimeMailer.toMimeFormat();
expect(mimeObj.from).toEqual('sender@example.com');
expect(mimeObj.to).toInclude('recipient@example.com');
expect(mimeObj.subject).toEqual('MIME Test');
expect(mimeObj.text).toEqual('Plain text content');
expect(mimeObj.html).toEqual('<p>HTML content</p>');
expect(mimeObj.headers['X-Custom']).toEqual('Value');
});
tap.test('should add email headers', async () => {
const headerMailer = new smartmail.Smartmail({
subject: 'Header Test',
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');
});
// 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

@@ -1,8 +1,8 @@
/** /**
* autocreated commitinfo by @pushrocks/commitinfo * autocreated commitinfo by @push.rocks/commitinfo
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartmail', name: '@push.rocks/smartmail',
version: '1.0.24', version: '2.2.0',
description: 'a unified format for representing and dealing with mails' 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

@@ -6,49 +6,269 @@ export interface IEmailValidationResult {
disposable: boolean; disposable: boolean;
freemail: boolean; freemail: boolean;
reason: string; reason: string;
formatValid: boolean;
mxValid: boolean;
localPartValid: boolean;
domainPartValid: boolean;
}
export interface IEmailAddressValidatorOptions {
skipOnlineDomainFetch?: boolean;
cacheDnsResults?: boolean;
cacheExpiryMs?: number;
} }
export class EmailAddressValidator { export class EmailAddressValidator {
public domainMap: { [key: string]: 'disposable' | 'freemail' }; public domainMap: { [key: string]: 'disposable' | 'freemail' };
public smartdns = new plugins.smartdns.Smartdns({}); public smartdns = new plugins.smartdns.Smartdns({});
private dnsCache: Map<string, { result: any; timestamp: number }> = new Map();
private options: IEmailAddressValidatorOptions;
public async validate(emailArg: string): Promise<IEmailValidationResult> { constructor(optionsArg: IEmailAddressValidatorOptions = {}) {
await this.fetchDomains(); this.options = {
const emailArray = emailArg.split('@'); skipOnlineDomainFetch: false,
const result = await this.smartdns.getRecords(emailArray[1], 'MX'); cacheDnsResults: true,
// console.log(emailArray); cacheExpiryMs: 3600000, // 1 hour
// console.log(this.domainMap[emailArray[1]]); ...optionsArg
return {
valid: !!result,
reason: 'todo',
disposable: this.domainMap[emailArray[1]] === 'disposable',
freemail: this.domainMap[emailArray[1]] === 'freemail',
}; };
} }
/**
* Validates an email address format according to RFC 5322
* @param emailArg The email address to validate
* @returns True if the format is valid
*/
public isValidEmailFormat(emailArg: string): boolean {
if (!emailArg) return false;
// RFC 5322 compliant regex pattern
const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return emailRegex.test(emailArg);
}
/**
* Validates the local part of an email address (before the @)
* @param localPart The local part of the email address
* @returns True if the local part is valid
*/
public isValidLocalPart(localPart: string): boolean {
if (!localPart) return false;
if (localPart.length > 64) return false;
// Check for illegal characters and patterns
const illegalChars = /[^\w.!#$%&'*+/=?^`{|}~-]/;
if (illegalChars.test(localPart)) return false;
// Check for consecutive dots or leading/trailing dots
if (localPart.includes('..') || localPart.startsWith('.') || localPart.endsWith('.')) return false;
return true;
}
/**
* Validates the domain part of an email address (after the @)
* @param domainPart The domain part of the email address
* @returns True if the domain part is valid
*/
public isValidDomainPart(domainPart: string): boolean {
if (!domainPart) return false;
if (domainPart.length > 255) return false;
// Domain name validation regex
const domainRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
// Must have at least one dot
if (!domainPart.includes('.')) return false;
// Must end with a valid TLD (at least 2 chars)
const parts = domainPart.split('.');
const tld = parts[parts.length - 1];
if (tld.length < 2) return false;
return domainRegex.test(domainPart);
}
/**
* Performs DNS MX record lookup for a domain
* @param domain The domain to check
* @returns MX records or null if none exist
*/
public async checkMxRecords(domain: string): Promise<any> {
if (this.options.cacheDnsResults) {
const cached = this.dnsCache.get(domain);
if (cached && (Date.now() - cached.timestamp) < this.options.cacheExpiryMs!) {
return cached.result;
}
}
const result = await this.smartdns.getRecords(domain, 'MX');
if (this.options.cacheDnsResults) {
this.dnsCache.set(domain, { result, timestamp: Date.now() });
}
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
* @param emailArg The email address to validate
* @returns Validation result with details
*/
public async validate(emailArg: string): Promise<IEmailValidationResult> {
await this.fetchDomains();
// Initialize result
const result: IEmailValidationResult = {
valid: false,
reason: '',
disposable: false,
freemail: false,
formatValid: false,
mxValid: false,
localPartValid: false,
domainPartValid: false
};
// Check overall email format
const formatValid = this.isValidEmailFormat(emailArg);
result.formatValid = formatValid;
if (!formatValid) {
result.reason = 'Invalid email format';
return result;
}
// Split email into local and domain parts
const [localPart, domainPart] = emailArg.split('@');
// Validate local part
const localPartValid = this.isValidLocalPart(localPart);
result.localPartValid = localPartValid;
if (!localPartValid) {
result.reason = 'Invalid local part (username)';
return result;
}
// Validate domain part
const domainPartValid = this.isValidDomainPart(domainPart);
result.domainPartValid = domainPartValid;
if (!domainPartValid) {
result.reason = 'Invalid domain part';
return result;
}
// Check MX records
const mxRecords = await this.checkMxRecords(domainPart);
result.mxValid = !!mxRecords;
if (!mxRecords) {
result.reason = 'Domain does not have valid MX records';
return result;
}
// Check if domain is disposable or free
result.disposable = this.domainMap[domainPart] === 'disposable';
result.freemail = this.domainMap[domainPart] === 'freemail';
if (result.disposable) {
result.reason = 'Domain is a disposable email provider';
} else if (result.freemail) {
result.reason = 'Domain is a free email provider';
} else {
result.reason = 'Email is valid';
}
// Email is valid if it has proper format and MX records
result.valid = result.formatValid && result.mxValid;
return result;
}
/**
* Fetches the domain list for checking disposable and free email providers
*/
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);
let onlineFileObject: any; if (this.options.skipOnlineDomainFetch) {
this.domainMap = localFileObject;
return;
}
try { try {
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;
console.log(
'smartmail EmailAddressValidator: Using online email list for email validation'
);
} catch (e) { } catch (e) {
this.domainMap = localFileObject; this.domainMap = localFileObject;
console.log(e);
console.log('smartmail EmailAddressValidator: Using local email list for email validation');
} }
} }
} }

View File

@@ -1,38 +1,423 @@
import * as plugins from './smartmail.plugins.js'; 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[];
export interface ISmartmailOptions<T> { export interface ISmartmailOptions<T> {
from: string; from: EmailAddress;
to?: EmailAddressList;
cc?: EmailAddressList;
bcc?: EmailAddressList;
replyTo?: EmailAddress;
subject: string; subject: string;
body: string; body: string;
htmlBody?: string;
creationObjectRef?: T; creationObjectRef?: T;
headers?: Record<string, string>;
priority?: 'high' | 'normal' | 'low';
validateEmails?: boolean;
} }
/** /**
* a standard representation for mails * 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;
contentType: string;
}
/**
* A standard representation for emails with advanced features
*/ */
export class Smartmail<T> { export class Smartmail<T> {
public options: ISmartmailOptions<T>; public options: ISmartmailOptions<T>;
public attachments: plugins.smartfile.Smartfile[] = []; public attachments: plugins.smartfile.SmartFile[] = [];
private emailValidator: EmailAddressValidator;
constructor(optionsArg: ISmartmailOptions<T>) { constructor(optionsArg: ISmartmailOptions<T>) {
this.options = optionsArg; // Set default options
this.options = {
validateEmails: false,
to: [],
cc: [],
bcc: [],
headers: {},
priority: 'normal',
...optionsArg
};
this.emailValidator = new EmailAddressValidator();
} }
public addAttachment(smartfileArg: plugins.smartfile.Smartfile) { /**
* Adds an attachment to the email
* @param smartfileArg The file to attach
* @returns this for chaining
*/
public addAttachment(smartfileArg: plugins.smartfile.SmartFile): this {
this.attachments.push(smartfileArg); this.attachments.push(smartfileArg);
return this;
} }
/**
* Gets the creation object reference
* @returns The creation object reference
*/
public getCreationObject(): T { public getCreationObject(): T {
return this.options.creationObjectRef; return this.options.creationObjectRef;
} }
public getSubject(dataArg: any = {}) { /**
* Gets the processed subject with template variables applied
* @param dataArg Data to apply to the template
* @returns Processed subject
*/
public getSubject(dataArg: any = {}): string {
const smartmustache = new plugins.smartmustache.SmartMustache(this.options.subject); const smartmustache = new plugins.smartmustache.SmartMustache(this.options.subject);
return smartmustache.applyData(dataArg); return smartmustache.applyData(dataArg);
} }
public getBody(dataArg: any = {}) { /**
* 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
* @param dataArg Data to apply to the template
* @returns Processed body
*/
public getBody(dataArg: any = {}): string {
const smartmustache = new plugins.smartmustache.SmartMustache(this.options.body); const smartmustache = new plugins.smartmustache.SmartMustache(this.options.body);
return smartmustache.applyData(dataArg); return smartmustache.applyData(dataArg);
} }
/**
* Gets the processed HTML body with template variables applied
* @param dataArg Data to apply to the template
* @returns Processed HTML body or null if not set
*/
public getHtmlBody(dataArg: any = {}): string | null {
if (!this.options.htmlBody) {
return null;
}
const smartmustache = new plugins.smartmustache.SmartMustache(this.options.htmlBody);
return smartmustache.applyData(dataArg);
}
/**
* 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'): 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'): 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): 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'): 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): this {
if (!this.options.headers) {
this.options.headers = {};
}
this.options.headers[name] = value;
return this;
}
/**
* Validates all email addresses in the email
* @returns Promise resolving to validation results
*/
public async validateAllEmails(): Promise<Record<string, boolean>> {
const results: Record<string, boolean> = {};
const emails: EmailAddress[] = [];
// Collect all emails
if (this.options.from) emails.push(this.options.from);
if (this.options.replyTo) emails.push(this.options.replyTo);
if (this.options.to) emails.push(...this.options.to);
if (this.options.cc) emails.push(...this.options.cc);
if (this.options.bcc) emails.push(...this.options.bcc);
// Validate each email
for (const email of emails) {
const validationResult = await this.emailValidator.validate(email);
results[email] = validationResult.valid;
}
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
* @returns MIME format object
*/
public async toMimeFormat(dataArg: any = {}): Promise<any> {
// Validate emails if option is enabled
if (this.options.validateEmails) {
const validationResults = await this.validateAllEmails();
const invalidEmails = Object.entries(validationResults)
.filter(([_, valid]) => !valid)
.map(([email]) => email);
if (invalidEmails.length > 0) {
throw new Error(`Invalid email addresses: ${invalidEmails.join(', ')}`);
}
}
// Build MIME parts
const subject = this.getSubject(dataArg);
const textBody = this.getBody(dataArg);
const htmlBody = this.getHtmlBody(dataArg);
// Convert attachments to MIME format
const mimeAttachments: IMimeAttachment[] = await Promise.all(
this.attachments.map(async (file) => {
return {
filename: file.path.split('/').pop()!,
content: file.contentBuffer,
contentType: 'application/octet-stream'
};
})
);
// Build email format object
const mimeObj: any = {
from: this.options.from,
subject,
text: textBody,
attachments: mimeAttachments,
headers: { ...this.options.headers }
};
// Add optional fields
if (this.options.to && this.options.to.length > 0) {
mimeObj.to = this.options.to;
}
if (this.options.cc && this.options.cc.length > 0) {
mimeObj.cc = this.options.cc;
}
if (this.options.bcc && this.options.bcc.length > 0) {
mimeObj.bcc = this.options.bcc;
}
if (this.options.replyTo) {
mimeObj.replyTo = this.options.replyTo;
}
if (htmlBody) {
mimeObj.html = htmlBody;
}
// Add priority headers if specified
if (this.options.priority === 'high') {
mimeObj.headers['X-Priority'] = '1';
mimeObj.headers['X-MSMail-Priority'] = 'High';
mimeObj.headers['Importance'] = 'High';
} else if (this.options.priority === 'low') {
mimeObj.headers['X-Priority'] = '5';
mimeObj.headers['X-MSMail-Priority'] = 'Low';
mimeObj.headers['Importance'] = 'Low';
}
return mimeObj;
}
} }

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

@@ -3,9 +3,23 @@
"experimentalDecorators": true, "experimentalDecorators": true,
"useDefineForClassFields": false, "useDefineForClassFields": false,
"target": "ES2022", "target": "ES2022",
"module": "ES2022", "module": "NodeNext",
"moduleResolution": "nodenext", "moduleResolution": "NodeNext",
"esModuleInterop": true, "esModuleInterop": true,
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true,
} "declaration": true,
"outDir": "./dist_ts",
"rootDir": "./ts",
"strict": false,
"skipLibCheck": true
},
"include": [
"ts/**/*"
],
"exclude": [
"node_modules",
"dist",
"dist_ts",
"**/*.spec.ts"
]
} }