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

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

View File

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