hazo_notify
v4.0.2
Published
Email notification system with scope-aware template management
Maintainers
Readme
Hazo Notify
A reusable component library for sending email notifications with scope-aware template management. Implements Zeptomail API integration with support for text and HTML emails, Handlebars-based email templates with multi-tenant resolution, multiple attachments, and comprehensive security features.
Version: 4.0.2
⚠ Readers on v4: Many examples below still show the v3.x API surface (
send_email,hazo_notify/emailer,channels: { in_app, email, banner }, directtemplate_html/template_textaccess). v4.0.0 introduced a channel-pluggable architecture — outbound sends go throughdispatch()→ inbox row + per-channel delivery row →EmailChannel/TelegramChanneladapter, with a multi-channel worker fleet. For the current v4 API see:
TECHDOC.md— v4 architecture and API reference (channel registry, dispatch flow, worker fleet, adapters, schema)SETUP_CHECKLIST.md§"v4.0.0 Migration" — migrations 005/006/007, dispatch rewrite, boot changesCHANGE_LOG.md— full breaking-change manifest for v4.0.0A full v4-aware rewrite of this README is tracked for v4.0.2. The examples that follow remain accurate for v3.x consumers; v4 consumers should read the docs above.
Features
Emailer
- Multiple Integration Methods: Support for API (Zeptomail - implemented), SMTP (placeholder), and POP3 (placeholder)
- Text and HTML Emails: Send both plain text and HTML formatted emails
- Multiple Attachments: Support for sending multiple file attachments (up to 10, configurable)
- Security Features: HTML sanitization, email injection protection, rate limiting, input validation
- Configurable: Configuration via
config/hazo_notify_config.inifile usinghazo_configpackage
Template Manager
- Handlebars Templates: Email template rendering with variable substitution
- Database-Backed Storage: Template and category CRUD via
hazo_connect - System Variables: Auto-generated date/time variables (11 formats)
- Template Composition:
@templatereference syntax for including templates within templates - Default Templates: Built-in welcome, verification, password reset, and signature templates
- Caching: Template compilation caching for performance
General
- Test app (v4.0.2+): Self-contained Next.js demo at
test-app/mirroring the layout ofhazo_jobs/test-app/. Runs on port 3020 (npm run dev:test-app) and covers every shipping surface — emailer, telegram, dispatch, templates, inbox/bell/banner, worker, jobs admin. Excluded from the published tarball. - hazo_jobs integration (v4.0.2+): New
hazo_notify/jobssubpath exportingcreateInboxFlushHandlerandsubmitInboxFlushJobsso consumers can drive the inbox-flush worker from their ownhazo_jobsqueue.hazo_jobsis an optional peer dep. - TypeScript: Fully typed with TypeScript
- Testing: Test coverage with Jest
Installation
npm install hazo_notifyThe package automatically installs required dependencies including hazo_config for configuration management and isomorphic-dompurify for HTML sanitization.
Note: This package is an ESM module ("type": "module") and requires Node.js 18+ with ESM support.
Upgrading from v1.x to v2.0.0
v2.0.0 introduces scope-aware template management, which requires a database migration:
- Run database migration: Execute
migrations/002_scope_migration.sqlagainst your database to renameorg_idandroot_org_idcolumns toscope_id. - Update environment variables: Add
HAZO_NOTIFY_CORS_ORIGINSto.env.localor configurecors_allowed_originsinhazo_notify_config.ini. - Add auth permissions: Register
notify_templates_adminandnotify_templates_super_adminroles in your auth system. - Initialize template manager: Call
init_template_manager(...)in yourinstrumentation.tsor app bootstrap. - Update Tailwind config (if using admin UI): Add
@source "../node_modules/hazo_notify/dist";to your Tailwind CSS entry.
The send_email() API is fully backward-compatible through v3.x. v4.0.0 removed the top-level send_email() export — see SETUP_CHECKLIST.md §"v4.0.0 Migration" for the dispatch() / EmailChannel#send() replacement pattern.
Optional: Enhanced Logging with hazo_logs
For structured logging with file rotation, install the optional hazo_logs peer dependency:
npm install hazo_logsThen create config/hazo_logs_config.ini:
[hazo_logs]
log_directory = ./logs
log_level = debug
enable_console = true
enable_file = true
max_file_size = 20m
max_files = 14dIf hazo_logs is not installed, hazo_notify will use a built-in console logger.
Optional: Database-Backed Templates with hazo_connect
For the template manager module, install the optional hazo_connect peer dependency:
npm install hazo_connectThen create the required tables (hazo_notify_template_cat and hazo_notify_templates) by running the appropriate SQL below for your database. The same SQL is also packaged at node_modules/hazo_notify/migrations/001_create_template_tables.sql.
If hazo_connect is not installed, the emailer module still works independently.
Database Tables
The template manager requires two tables:
| Table | Purpose |
|-------|---------|
| hazo_notify_template_cat | Template categories (groups templates per scope) |
| hazo_notify_templates | Email templates with HTML, text, and variable definitions |
Both tables use scope_id (nullable UUID) for multi-tenant isolation: NULL = global/system templates; non-NULL = tenant-scoped templates. hazo_notify_templates.template_category_id references hazo_notify_template_cat.id.
PostgreSQL Schema (v2.0.0+)
CREATE TABLE IF NOT EXISTS hazo_notify_template_cat (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
scope_id UUID,
template_category_name VARCHAR(100) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
changed_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_notify_template_cat_scope
ON hazo_notify_template_cat (scope_id);
CREATE TABLE IF NOT EXISTS hazo_notify_templates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
scope_id UUID,
template_category_id UUID NOT NULL REFERENCES hazo_notify_template_cat(id),
template_variables JSONB DEFAULT '{}',
template_name VARCHAR(100) NOT NULL,
template_html TEXT NOT NULL DEFAULT '',
template_text TEXT NOT NULL DEFAULT '',
email_type VARCHAR(10) NOT NULL DEFAULT 'user',
created_at TIMESTAMPTZ DEFAULT NOW(),
changed_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_notify_templates_scope
ON hazo_notify_templates (scope_id);
CREATE INDEX IF NOT EXISTS idx_notify_templates_category
ON hazo_notify_templates (template_category_id);
CREATE INDEX IF NOT EXISTS idx_notify_templates_name
ON hazo_notify_templates (scope_id, template_name);Note: scope_id is nullable. NULL represents global/system templates; non-NULL represents tenant-scoped templates.
SQLite Schema (v2.0.0+)
CREATE TABLE IF NOT EXISTS hazo_notify_template_cat (
id TEXT PRIMARY KEY,
scope_id TEXT,
template_category_name TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now')),
changed_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_notify_template_cat_scope
ON hazo_notify_template_cat (scope_id);
CREATE TABLE IF NOT EXISTS hazo_notify_templates (
id TEXT PRIMARY KEY,
scope_id TEXT,
template_category_id TEXT NOT NULL REFERENCES hazo_notify_template_cat(id),
template_variables TEXT DEFAULT '{}',
template_name TEXT NOT NULL,
template_html TEXT NOT NULL DEFAULT '',
template_text TEXT NOT NULL DEFAULT '',
email_type TEXT NOT NULL DEFAULT 'user',
created_at TEXT DEFAULT (datetime('now')),
changed_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_notify_templates_scope
ON hazo_notify_templates (scope_id);
CREATE INDEX IF NOT EXISTS idx_notify_templates_category
ON hazo_notify_templates (template_category_id);
CREATE INDEX IF NOT EXISTS idx_notify_templates_name
ON hazo_notify_templates (scope_id, template_name);Note: scope_id is nullable. NULL represents global/system templates; non-NULL represents tenant-scoped templates.
Type Mapping (PostgreSQL ↔ SQLite)
| Concept | PostgreSQL | SQLite |
|---------|------------|--------|
| Primary / foreign keys | UUID (with gen_random_uuid() default) | TEXT (UUID string supplied by app) |
| Scope key (scope_id) | UUID (nullable) | TEXT (nullable, UUID string or NULL) |
| JSON column (template_variables) | JSONB | TEXT (serialized JSON) |
| Timestamps | TIMESTAMPTZ with NOW() default | TEXT with datetime('now') default |
Quick Start
Create
.env.localfile with your Zeptomail API key:ZEPTOMAIL_API_KEY=your_zeptomail_api_keyCreate
config/hazo_notify_config.inifile (in aconfig/directory):[emailer] emailer_module=zeptoemail_api zeptomail_api_endpoint=https://api.zeptomail.com.au/v1.1/email [email protected] from_name=Hazo Notify [ui] enable_ui=falseUse in your code:
import { send_email } from 'hazo_notify'; const result = await send_email({ to: '[email protected]', subject: 'Hello', content: { text: 'This is a test email' } }); if (result.success) { console.log('Email sent!', result.message_id); }
v4 multi-channel quick start
import { initInbox, startInboxWorker } from "hazo_notify/inbox";
import { registerChannel } from "hazo_notify/channels";
import { EmailChannel } from "hazo_notify/adapters/email";
import { TelegramChannel } from "hazo_notify/adapters/telegram";
import { dispatch } from "hazo_notify/dispatcher";
// 1. Initialize the inbox connection
await initInbox({ hazo_connect_factory: () => createHazoConnect(...) });
// 2. Register channels (explicit, no auto-discovery)
registerChannel(new EmailChannel());
registerChannel(new TelegramChannel());
// 3. Start the worker fleet
startInboxWorker({
resolveRecipient: async (user_id, scope_id, channel_id) => {
if (channel_id === "email") return (await users.get(user_id))?.email ?? null;
if (channel_id === "telegram") return process.env.TELEGRAM_CHAT_ID ?? null;
return null;
},
});
// 4. Dispatch
await dispatch({
event_type: "site.run_failed",
subject_id: site_id,
scope_id, recipient_user_ids: [user_id],
in_app_text: "Run failed",
deep_link: `/runs/${run_id}`,
surfaces: { in_app: true, banner: false },
channels: { email: true, telegram: true },
channel_payloads: {
email: { subject: "Run failed", body_text: "...", template_name: "run-failure", template_vars: {/* ... */} },
telegram: { text: "Run failed", parse_mode: "HTML", template_name: "run-failure", template_vars: {/* ... */} },
},
batch_window_ms: 0,
});See CHANGE_LOG.md for the v3 → v4 migration guide.
Live e2e tests (opt-in)
RUN_LIVE_TESTS=1 \
TELEGRAM_TEST_BOT_TOKEN=… \
TELEGRAM_TEST_CHAT_ID=… \
npm test -- __tests__/e2eSkipped by default in CI.
Configuration
Step 1: Create Environment Variables File
IMPORTANT: For security, store sensitive credentials in environment variables.
Create a .env.local file in your project root:
# Zeptomail API Configuration
# Only the API key is required (no token needed)
ZEPTOMAIL_API_KEY=your_zeptomail_api_keySecurity Note: The .env.local file is automatically excluded from git (via .gitignore). Never commit sensitive credentials to version control.
Step 2: Create Configuration File
Create a config/hazo_notify_config.ini file in your project's config/ directory. See config/hazo_notify_config.ini in the package for a complete template with all available options.
Minimum required configuration:
[emailer]
# Emailer module: zeptoemail_api, smtp, pop3
emailer_module=zeptoemail_api
# Zeptomail API Provider Configuration (required when emailer_module=zeptoemail_api)
zeptomail_api_endpoint=https://api.zeptomail.com.au/v1.1/email
# API key is read from .env.local file (ZEPTOMAIL_API_KEY)
# If not set in .env.local, you can uncomment and set it here (not recommended for production)
# zeptomail_api_key=your_zeptomail_api_key
# Required: Default sender email address (must be verified in your Zeptomail account)
[email protected]
# Required: Default sender name displayed in email clients
from_name=Hazo Notify
[ui]
# Enable UI component and all routes (e.g., /hazo_notify/emailer_test)
# Default: false
enable_ui=falseConfiguration Options
Required Configuration
emailer_module: Emailer module (zeptoemail_api,smtp, orpop3)from_email: Default sender email address (must be verified in your Zeptomail account)from_name: Default sender name displayed in email clientszeptomail_api_endpoint: Zeptomail API endpoint (required whenemailer_module=zeptoemail_api) - Default:https://api.zeptomail.com.au/v1.1/emailZEPTOMAIL_API_KEY: Zeptomail API key (required whenemailer_module=zeptoemail_api) - Store in.env.localfile
Optional Configuration
reply_to_email: Reply-to email addressbounce_email: Bounce handling emailreturn_path_email: Return path emailenable_ui: Enable UI component and all routes (default:false)rate_limit_requests: Maximum requests per minute (default:10)rate_limit_window: Time window for rate limiting in seconds (default:60)max_attachment_size: Maximum size per attachment in bytes (default:10485760= 10MB)max_attachments: Maximum number of attachments (default:10)request_timeout: Timeout for API requests in milliseconds (default:30000= 30 seconds)max_subject_length: Maximum length for email subject (default:255)max_body_length: Maximum size for email body content in bytes (default:1048576= 1MB)cors_allowed_origins: Comma-separated list of allowed origins for CORS (default: empty)
Usage
Import the Library
import { send_email } from 'hazo_notify';
// or
import { send_email } from 'hazo_notify/emailer';Basic Usage Examples
1. Send a Text Email
Input:
const result = await send_email({
to: '[email protected]',
subject: 'Welcome to Our Service',
content: {
text: 'Thank you for signing up! We are excited to have you on board.'
}
});Expected Output (Success):
{
success: true,
// message_id is the request_id Zeptomail assigns to the send. Use it for
// delivery lookup in Zeptomail logs.
message_id: '7a6803.5aa56c8edd39e440.m1.…',
message: 'Email sent successfully',
raw_response: {
status: 201,
status_text: '',
headers: { /* response headers */ },
body: {
data: [{ code: 'EM_104', additional_info: [], message: 'Email request received' }],
message: 'OK',
request_id: '7a6803.5aa56c8edd39e440.m1.…',
object: 'email'
}
}
}Expected Output (Error):
{
success: false,
error: 'Invalid recipient email address: invalid-email',
message: 'Invalid recipient email address: invalid-email',
raw_response: undefined // In production, raw_response is masked for security
}2. Send an HTML Email
Input:
const result = await send_email({
to: '[email protected]',
subject: 'Welcome Email',
content: {
html: '<h1>Welcome!</h1><p>Thank you for joining us.</p><p>We are excited to have you on board.</p>'
}
});Note: HTML content is automatically sanitized to prevent XSS attacks.
Expected Output: Same structure as text email example above.
3. Send Both Text and HTML Email
Input:
const result = await send_email({
to: '[email protected]',
subject: 'Newsletter',
content: {
text: 'This is the plain text version of the email.',
html: '<html><body><h1>Newsletter</h1><p>This is the HTML version of the email.</p></body></html>'
}
});Expected Output: Same structure as text email example above.
4. Send Email with Single Attachment
Input:
import fs from 'fs';
// Read file and convert to base64
const file_content = fs.readFileSync('document.pdf');
const base64_content = file_content.toString('base64');
const result = await send_email({
to: '[email protected]',
subject: 'Document Attached',
content: {
text: 'Please find the attached document.',
html: '<p>Please find the attached document.</p>'
},
attachments: [
{
filename: 'document.pdf',
content: base64_content, // Base64 encoded file content
mime_type: 'application/pdf'
}
]
});Attachment Format:
filename: String - The name of the file (e.g.,'document.pdf')content: String - Base64 encoded file content (e.g.,'JVBERi0xLjQKJeLjz9MK...')mime_type: String - MIME type of the file (e.g.,'application/pdf','image/jpeg','text/plain')
Common MIME Types:
- PDF:
'application/pdf' - JPEG:
'image/jpeg' - PNG:
'image/png' - Text:
'text/plain' - CSV:
'text/csv' - ZIP:
'application/zip'
Expected Output: Same structure as text email example above.
5. Send Email with Multiple Attachments
Input:
import fs from 'fs';
const pdf_content = fs.readFileSync('document.pdf').toString('base64');
const image_content = fs.readFileSync('image.jpg').toString('base64');
const result = await send_email({
to: '[email protected]',
subject: 'Multiple Attachments',
content: {
text: 'Please find the attached files.',
},
attachments: [
{
filename: 'document.pdf',
content: pdf_content,
mime_type: 'application/pdf'
},
{
filename: 'image.jpg',
content: image_content,
mime_type: 'image/jpeg'
}
]
});Expected Output: Same structure as text email example above.
Limits:
- Maximum attachments: 10 (configurable via
max_attachments) - Maximum size per attachment: 10MB (configurable via
max_attachment_size)
6. Send Email to Multiple Recipients
Input:
const result = await send_email({
to: ['[email protected]', '[email protected]', '[email protected]'],
subject: 'Group Announcement',
content: {
text: 'This email is sent to multiple recipients.',
}
});Expected Output: Same structure as text email example above.
7. Send Email with CC and BCC
Input:
const result = await send_email({
to: '[email protected]',
cc: ['[email protected]', '[email protected]'],
bcc: '[email protected]',
subject: 'Email with CC and BCC',
content: {
text: 'This email has CC and BCC recipients.',
}
});Expected Output: Same structure as text email example above.
8. Send Email with Custom From Address
Input:
const result = await send_email({
to: '[email protected]',
subject: 'Custom Sender',
content: {
text: 'This email is from a custom sender.',
},
from: '[email protected]',
from_name: 'Custom Sender Name'
});Expected Output: Same structure as text email example above.
Note: The from email must be verified in your Zeptomail account.
9. Send Email with Reply-To Address
Input:
const result = await send_email({
to: '[email protected]',
subject: 'Support Request',
content: {
text: 'Please reply to this email for support.',
},
reply_to: '[email protected]'
});Expected Output: Same structure as text email example above.
10. Complete Example with All Options
Input:
import fs from 'fs';
const attachment_content = fs.readFileSync('report.pdf').toString('base64');
const result = await send_email({
to: ['[email protected]', '[email protected]'],
cc: '[email protected]',
bcc: '[email protected]',
subject: 'Monthly Report - December 2024',
content: {
text: 'Please find the monthly report attached. This is the plain text version.',
html: `
<html>
<body>
<h1>Monthly Report</h1>
<p>Please find the monthly report attached.</p>
<p>This is the <strong>HTML version</strong> of the email.</p>
<p>Best regards,<br>Team</p>
</body>
</html>
`
},
attachments: [
{
filename: 'monthly-report-december-2024.pdf',
content: attachment_content,
mime_type: 'application/pdf'
}
],
from: '[email protected]',
from_name: 'Reporting Team',
reply_to: '[email protected]'
});Expected Output (Success):
{
success: true,
// message_id is Zeptomail's request_id (the unique identifier for the send)
message_id: '7a6803.5aa56c8edd39e440.m1.…',
message: 'Email sent successfully',
raw_response: {
status: 201,
status_text: '',
headers: { /* response headers */ },
body: {
data: [{ code: 'EM_104', additional_info: [], message: 'Email request received' }],
message: 'OK',
request_id: '7a6803.5aa56c8edd39e440.m1.…',
object: 'email'
}
}
}Expected Output (Error - Validation):
{
success: false,
error: 'Email subject exceeds maximum length of 255 characters',
message: 'Email subject exceeds maximum length of 255 characters',
raw_response: undefined
}Expected Output (Error - API Failure):
{
success: false,
error: 'HTTP 400: Bad Request',
message: 'HTTP 400: Bad Request',
raw_response: {
status: 400,
status_text: 'Bad Request',
headers: { /* response headers */ },
body: {
error: 'Invalid email address format'
}
}
}Input/Output Reference
Input Parameters
SendEmailOptions Interface
| Parameter | Type | Required | Description | Example |
|-----------|------|----------|-------------|---------|
| to | string \| string[] | Yes | Recipient email address(es) | '[email protected]' or ['[email protected]', '[email protected]'] |
| subject | string | Yes | Email subject line | 'Welcome Email' |
| content | EmailContent | Yes | Email content (text and/or HTML) | { text: 'Hello', html: '<p>Hello</p>' } |
| content.text | string | No* | Plain text email content | 'This is plain text' |
| content.html | string | No* | HTML email content | '<h1>Title</h1><p>Content</p>' |
| attachments | EmailAttachment[] | No | Array of file attachments | See attachment format below |
| from | string | No | Override default from email | '[email protected]' |
| from_name | string | No | Override default from name | 'Custom Sender' |
| reply_to | string | No | Reply-to email address | '[email protected]' |
| cc | string \| string[] | No | CC recipient(s) | '[email protected]' or ['[email protected]', '[email protected]'] |
| bcc | string \| string[] | No | BCC recipient(s) | '[email protected]' or ['[email protected]', '[email protected]'] |
* At least one of content.text or content.html must be provided.
EmailAttachment Interface
| Parameter | Type | Required | Description | Example |
|-----------|------|----------|-------------|---------|
| filename | string | Yes | Attachment filename | 'document.pdf' |
| content | string | Yes | Base64 encoded file content | 'JVBERi0xLjQKJeLjz9MK...' |
| mime_type | string | Yes | MIME type of the file | 'application/pdf' |
Output Response
EmailSendResponse Interface
| Field | Type | Description | Example (Success) | Example (Error) |
|-------|------|-------------|-------------------|-----------------|
| success | boolean | Whether the email was sent successfully | true | false |
| message_id | string? | Message ID from the email provider | 'msg_abc123def456' | undefined |
| message | string? | Success or error message | 'Email sent successfully' | 'Invalid email address' |
| raw_response | Record<string, unknown> \| string? | Raw response from the provider (masked in production) | See raw_response example below | See raw_response example below |
| error | string? | Error message if failed | undefined | 'Invalid email address' |
Raw Response Structure (Development)
{
status: 200, // HTTP status code
status_text: 'OK', // HTTP status text
headers: {
'content-type': 'application/json',
'content-length': '156',
// ... other response headers
},
body: {
data: {
message_id: 'msg_abc123def456'
}
}
}Note: In production (NODE_ENV=production), raw_response is masked for security and only contains status and status_text.
Input Validation
The library performs comprehensive validation on all inputs:
Email Address Validation
- Format validation using RFC 5322 compliant regex
- Maximum length: 254 characters
- Examples:
- ✅ Valid:
'[email protected]','[email protected]' - ❌ Invalid:
'invalid-email','user@','@example.com'
- ✅ Valid:
Subject Validation
- Required field
- Maximum length: 255 characters (RFC 5322 standard)
- Examples:
- ✅ Valid:
'Welcome Email'(14 characters) - ❌ Invalid:
''(empty),'A'.repeat(256)(exceeds limit)
- ✅ Valid:
Body Content Validation
- At least one of
textorhtmlmust be provided - Maximum size: 1MB (1,048,576 bytes) per content type
- HTML content is automatically sanitized to prevent XSS attacks
- Examples:
- ✅ Valid:
{ text: 'Hello' }or{ html: '<p>Hello</p>' }or both - ❌ Invalid:
{}(empty content)
- ✅ Valid:
Attachment Validation
- Maximum count: 10 attachments (configurable)
- Maximum size per attachment: 10MB (10,485,760 bytes, configurable)
- Content must be valid base64 encoded string
- Examples:
- ✅ Valid:
[{ filename: 'doc.pdf', content: 'JVBERi0x...', mime_type: 'application/pdf' }] - ❌ Invalid:
[]with 11 items (exceeds count), attachment > 10MB (exceeds size)
- ✅ Valid:
Error Handling
All errors are returned in a consistent format:
{
success: false,
error: 'Error message describing what went wrong',
message: 'Error message describing what went wrong',
raw_response: undefined // or detailed response in development
}Common Error Scenarios:
Validation Errors (400):
- Invalid email address format
- Missing required fields (to, subject, content)
- Subject exceeds maximum length
- Body content exceeds maximum size
- Attachment count/size exceeds limits
Configuration Errors (500):
- Missing API key
- Invalid configuration
- Missing required config values
API Errors (varies):
- HTTP 400: Bad Request (invalid data)
- HTTP 401: Unauthorized (invalid API key)
- HTTP 429: Rate Limited (too many requests)
- HTTP 500: Server Error (provider issue)
- Timeout errors (request took too long)
Rate Limiting (429):
- Too many requests per time window
- Configurable via
rate_limit_requestsandrate_limit_window
API Reference
send_email(options: SendEmailOptions, config?: EmailerConfig): Promise<EmailSendResponse>
Send an email using the configured provider.
Parameters
options(required):SendEmailOptions- Email send options- See Input Parameters section above for detailed field descriptions
config(optional):EmailerConfig- Emailer configuration- If not provided, configuration is loaded from
config/hazo_notify_config.ini - Useful for programmatic configuration or testing
- If not provided, configuration is loaded from
Returns
Promise<EmailSendResponse>: Promise that resolves to email send response- See Output Response section above for detailed field descriptions
Example
import { send_email } from 'hazo_notify';
try {
const result = await send_email({
to: '[email protected]',
subject: 'Test Email',
content: {
text: 'This is a test email',
html: '<p>This is a test email</p>'
}
});
if (result.success) {
console.log('Email sent successfully!');
console.log('Message ID:', result.message_id);
} else {
console.error('Failed to send email:', result.error);
}
} catch (error) {
console.error('Unexpected error:', error);
}load_emailer_config(): EmailerConfig
Load emailer configuration from config/hazo_notify_config.ini.
Returns
EmailerConfig: Emailer configuration object
Example
import { load_emailer_config } from 'hazo_notify';
const config = load_emailer_config();
console.log('Emailer module:', config.emailer_module);
console.log('From email:', config.from_email);Template Manager (v2.0.0+)
The scope-aware template manager allows you to define, store, and render email templates with multi-tenant isolation. Templates are stored in the database and support hierarchical resolution: tenant-scoped templates take precedence over global system templates.
Initialization
In your app's instrumentation.ts or bootstrap code, initialize the template manager once:
import { init_template_manager, register_template_type } from 'hazo_notify/template_manager';
import { createHazoConnect } from 'hazo_connect/server';
export async function register() {
const hazo_connect = await createHazoConnect();
// Register template types (optional, but recommended)
register_template_type({
type_id: 'welcome_email',
display_name: 'Welcome Email',
variables: {
user_name: { type: 'string', description: 'User full name' },
login_url: { type: 'string', description: 'Link to login page' }
}
});
// Initialize template manager with optional scope resolver
await init_template_manager({
hazo_connect,
scope_resolver: async (scope_id) => {
// Optional: resolve tenant hierarchy (e.g., org > parent_org > system)
// Return { scope_id, parent_scope_id? }
return { scope_id };
},
system_templates: [
// Optional: define system templates to auto-seed
{
template_name: 'welcome_email',
type_id: 'welcome_email',
template_html: '<h1>Welcome, {{user_name}}!</h1><p><a href="{{login_url}}">Log in here</a></p>',
template_text: 'Welcome, {{user_name}}! Visit {{login_url}} to log in.'
}
]
});
}Sending Template Emails
import { send_template_email } from 'hazo_notify/template_manager';
const result = await send_template_email({
template_name: 'welcome_email',
variables: {
user_name: 'Alice',
login_url: 'https://app.example.com/login'
},
to: '[email protected]',
subject: 'Welcome to Our App'
}, hazo_connect, scope_id);
if (result.success) {
console.log('Email sent:', result.message_id);
} else {
console.error('Failed:', result.error);
}Admin UI
Mount the admin UI in a protected Next.js page:
// app/admin/templates/page.tsx
import { TemplateManagerAdmin, TemplateGlobalsAdmin } from 'hazo_notify/template_manager_admin';
import { requireAdmin } from '@/lib/auth'; // Your auth check
export default async function TemplatesPage() {
await requireAdmin();
return (
<div className="space-y-8">
<section>
<h1>Tenant Templates</h1>
<TemplateManagerAdmin scope_id={current_tenant_id} />
</section>
<section>
<h1>System Templates (Global)</h1>
<TemplateGlobalsAdmin />
</section>
</div>
);
}Permission requirements:
notify_templates_admin— read/write templatesnotify_templates_super_admin— delete system templates
Tailwind v4 requirement: If using the admin UI, add this to your globals.css:
@import "tailwindcss";
@source "../node_modules/hazo_notify/dist";Template Manager API
send_template_email(options, hazo_connect, scope_id, config?): Promise<SendTemplateEmailResponse>
Render a named template and send it as an email.
import { send_template_email } from 'hazo_notify/template_manager';
const result = await send_template_email({
template_name: 'welcome_email',
variables: { user_name: 'John', login_url: 'https://example.com/login' },
to: '[email protected]',
subject: 'Welcome!'
}, hazo_connect, org_id);render_template(template_name, variables, hazo_connect, org_id, config?, logger?): Promise<TemplateRenderResult>
Render a template without sending (useful for previews).
seed_default_templates(hazo_connect, org_id, root_org_id): Promise<TemplateOperationResponse>
Seed the database with default templates (welcome, verification, password reset, signature).
Template CRUD
import {
list_categories, create_category, update_category, delete_category,
list_templates, create_template, update_template, delete_template,
get_template_by_name, seed_default_templates,
} from 'hazo_notify/template_manager';All CRUD functions take a hazo_connect instance as their first argument.
Test app (v4.0.2+)
The Next.js test UI that lived under src/app/ in v3.x and v4.0.x has moved to
a dedicated test-app/ sibling directory (mirroring hazo_jobs/test-app/).
The library itself no longer depends on Next.js at runtime.
# from the package root
npm run dev:test-app # build lib + boot Next dev on http://localhost:3020
npm run worker:test-app # in a second terminal — drains inbox-flush jobsThe test-app provides one route per shipping surface:
| Route | What it exercises |
|---|---|
| / | Overview dashboard with pending/unread/queued counts |
| /emailer | Direct EmailChannel.send via /api/notify/send/email |
| /telegram | Direct TelegramChannel.send via /api/notify/send/telegram |
| /dispatch | dispatch() + submitInboxFlushJobs() (auto-queues a worker job per channel) |
| /templates | Mounted <TemplateManagerAdmin> |
| /inbox | <NotificationBell> + <NotificationBanner> + raw inbox API view |
| /worker | Last-10 hazo_notify.inbox_flush job table from hazo_jobs |
| /admin | Mounted <JobsAdminPanel> from hazo_jobs/ui |
The test-app is excluded from the published npm tarball.
hazo_jobs integration (hazo_notify/jobs)
v4.0.2 ships a new subpath export for projects that want to drive the inbox
flush worker through their own hazo_jobs
queue rather than running the bundled startInboxWorker:
import {
INBOX_FLUSH_JOB_TYPE,
createInboxFlushHandler,
submitInboxFlushJobs,
} from "hazo_notify/jobs";
// In your hazo_jobs worker process:
const proc = createWorkerProcess({
adapter,
dialect: "sqlite",
types: [INBOX_FLUSH_JOB_TYPE],
handlers: {
[INBOX_FLUSH_JOB_TYPE]: createInboxFlushHandler({
resolveRecipient, // (user_id, scope_id, channel_id) => Promise<string | null>
batch_size: 50,
concurrency: 4,
}),
},
});
// Immediately after dispatch(), enqueue one flush per enabled channel:
await submitInboxFlushJobs(jobsClient, { channels: ["email", "telegram"] });hazo_jobs is an optional peer dependency — the package only declares the
structural types it needs and never imports hazo_jobs at runtime. See
test-app/worker.ts for a full worker example.
Security Features
The library includes comprehensive security features:
- HTML Sanitization: All HTML content is sanitized using DOMPurify to prevent XSS attacks
- Email Injection Protection: Email headers are sanitized to prevent header injection attacks
- Rate Limiting: Configurable rate limiting to prevent abuse (default: 10 requests/minute)
- Input Validation: Comprehensive validation of all inputs (email format, length, size)
- Attachment Limits: Size and count limits for attachments
- Request Timeouts: Configurable timeouts for external API calls (default: 30 seconds)
- Error Masking: Stack traces and sensitive data are masked in production
- CORS Support: Configurable CORS headers for API routes
Testing
Run tests with:
npm testRun tests in watch mode:
npm run test:watchRun tests with coverage:
npm run test:coverageDevelopment
Project Structure
hazo_notify/
├── src/
│ ├── lib/
│ │ ├── index.ts # Main library entry point
│ │ └── emailer/
│ │ ├── index.ts # Emailer entry point
│ │ ├── emailer.ts # Main emailer service
│ │ ├── types.ts # TypeScript type definitions
│ │ ├── providers/
│ │ │ ├── index.ts # Provider factory
│ │ │ ├── zeptomail_provider.ts # Zeptomail API implementation
│ │ │ ├── smtp_provider.ts # SMTP placeholder
│ │ │ └── pop3_provider.ts # POP3 placeholder
│ │ └── utils/
│ │ ├── constants.ts # Constants and defaults
│ │ ├── validation.ts # Input validation utilities
│ │ └── logger.ts # Centralized logging
│ └── app/
│ ├── api/
│ │ └── hazo_notify/
│ │ └── emailer/
│ │ └── send/
│ │ └── route.ts # Next.js API route
│ └── hazo_notify/
│ ├── page.tsx # Default page
│ ├── layout.tsx # Layout with sidebar
│ └── emailer_test/
│ ├── page.tsx # Test UI page
│ └── layout.tsx # Test UI layout
├── components/
│ └── ui/ # Shadcn UI components
├── config/
│ ├── hazo_notify_config.ini # Configuration file template
│ └── hazo_logs_config.ini # Optional logging configuration
├── .env.local.example # Environment variables example
└── package.jsonLicense
MIT
Author
Pubs Abayasiri
Support
For issues and questions, please visit the GitHub repository.
