npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

hazo_notify

v4.0.2

Published

Email notification system with scope-aware template management

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 }, direct template_html / template_text access). v4.0.0 introduced a channel-pluggable architecture — outbound sends go through dispatch() → inbox row + per-channel delivery row → EmailChannel/TelegramChannel adapter, 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 changes
  • CHANGE_LOG.md — full breaking-change manifest for v4.0.0

A 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.ini file using hazo_config package

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: @template reference 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 of hazo_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/jobs subpath exporting createInboxFlushHandler and submitInboxFlushJobs so consumers can drive the inbox-flush worker from their own hazo_jobs queue. hazo_jobs is an optional peer dep.
  • TypeScript: Fully typed with TypeScript
  • Testing: Test coverage with Jest

Installation

npm install hazo_notify

The 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:

  1. Run database migration: Execute migrations/002_scope_migration.sql against your database to rename org_id and root_org_id columns to scope_id.
  2. Update environment variables: Add HAZO_NOTIFY_CORS_ORIGINS to .env.local or configure cors_allowed_origins in hazo_notify_config.ini.
  3. Add auth permissions: Register notify_templates_admin and notify_templates_super_admin roles in your auth system.
  4. Initialize template manager: Call init_template_manager(...) in your instrumentation.ts or app bootstrap.
  5. 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_logs

Then 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 = 14d

If 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_connect

Then 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

  1. Create .env.local file with your Zeptomail API key:

    ZEPTOMAIL_API_KEY=your_zeptomail_api_key
  2. Create config/hazo_notify_config.ini file (in a config/ 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=false
  3. Use 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__/e2e

Skipped 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_key

Security 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=false

Configuration Options

Required Configuration

  • emailer_module: Emailer module (zeptoemail_api, smtp, or pop3)
  • from_email: Default sender email address (must be verified in your Zeptomail account)
  • from_name: Default sender name displayed in email clients
  • zeptomail_api_endpoint: Zeptomail API endpoint (required when emailer_module=zeptoemail_api) - Default: https://api.zeptomail.com.au/v1.1/email
  • ZEPTOMAIL_API_KEY: Zeptomail API key (required when emailer_module=zeptoemail_api) - Store in .env.local file

Optional Configuration

  • reply_to_email: Reply-to email address
  • bounce_email: Bounce handling email
  • return_path_email: Return path email
  • enable_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:

Subject Validation

  • Required field
  • Maximum length: 255 characters (RFC 5322 standard)
  • Examples:
    • ✅ Valid: 'Welcome Email' (14 characters)
    • ❌ Invalid: '' (empty), 'A'.repeat(256) (exceeds limit)

Body Content Validation

  • At least one of text or html must 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)

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)

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:

  1. 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
  2. Configuration Errors (500):

    • Missing API key
    • Invalid configuration
    • Missing required config values
  3. 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)
  4. Rate Limiting (429):

    • Too many requests per time window
    • Configurable via rate_limit_requests and rate_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

  • config (optional): EmailerConfig - Emailer configuration

    • If not provided, configuration is loaded from config/hazo_notify_config.ini
    • Useful for programmatic configuration or testing

Returns

  • Promise<EmailSendResponse>: Promise that resolves to email send response

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 templates
  • notify_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 jobs

The 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:

  1. HTML Sanitization: All HTML content is sanitized using DOMPurify to prevent XSS attacks
  2. Email Injection Protection: Email headers are sanitized to prevent header injection attacks
  3. Rate Limiting: Configurable rate limiting to prevent abuse (default: 10 requests/minute)
  4. Input Validation: Comprehensive validation of all inputs (email format, length, size)
  5. Attachment Limits: Size and count limits for attachments
  6. Request Timeouts: Configurable timeouts for external API calls (default: 30 seconds)
  7. Error Masking: Stack traces and sensitive data are masked in production
  8. CORS Support: Configurable CORS headers for API routes

Testing

Run tests with:

npm test

Run tests in watch mode:

npm run test:watch

Run tests with coverage:

npm run test:coverage

Development

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.json

License

MIT

Author

Pubs Abayasiri

Support

For issues and questions, please visit the GitHub repository.