Email that acts as a notification center as well for schedules too
npm install hazo_notifyA reusable component library for sending email notifications with support for multiple integration methods. Currently implements Zeptomail API integration with support for text and HTML emails, multiple attachments, and comprehensive security features.
- 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
- Test UI: Optional test UI at /hazo_notify/emailer_test for testing email functionality
- TypeScript: Fully typed with TypeScript
- Testing: Comprehensive test coverage with Jest
``bash`
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.
For structured logging with file rotation, install the optional hazo_logs peer dependency:
`bash`
npm install hazo_logs
Then create config/hazo_logs_config.ini:
`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.
1. Create .env.local file with your Zeptomail API key:
`env`
ZEPTOMAIL_API_KEY=your_zeptomail_api_key
2. Create config/hazo_notify_config.ini file (in a config/ directory):`
ini`
[emailer]
emailer_module=zeptoemail_api
zeptomail_api_endpoint=https://api.zeptomail.com.au/v1.1/email
from_email=noreply@example.com
from_name=Hazo Notify
[ui]
enable_ui=false
3. Use in your code:
`typescript`
import { send_email } from 'hazo_notify';
const result = await send_email({
to: 'recipient@example.com',
subject: 'Hello',
content: { text: 'This is a test email' }
});
if (result.success) {
console.log('Email sent!', result.message_id);
}
IMPORTANT: For security, store sensitive credentials in environment variables.
Create a .env.local file in your project root:
`env`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.
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:
`ini
[emailer]Emailer module: zeptoemail_api, smtp, pop3
emailer_module=zeptoemail_api
[ui]
$3
#### 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
$3
`typescript
import { send_email } from 'hazo_notify';
// or
import { send_email } from 'hazo_notify/emailer';
`$3
#### 1. Send a Text Email
Input:
`typescript
const result = await send_email({
to: 'recipient@example.com',
subject: 'Welcome to Our Service',
content: {
text: 'Thank you for signing up! We are excited to have you on board.'
}
});
`Expected Output (Success):
`typescript
{
success: true,
message_id: 'msg_abc123def456',
message: 'Email sent successfully',
raw_response: {
status: 200,
status_text: 'OK',
headers: { / response headers / },
body: {
data: {
message_id: 'msg_abc123def456'
}
}
}
}
`Expected Output (Error):
`typescript
{
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:
`typescript
const result = await send_email({
to: 'recipient@example.com',
subject: 'Welcome Email',
content: {
html: 'Welcome!
Thank you for joining us.
We are excited to have you on board.
'
}
});
`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:
`typescript
const result = await send_email({
to: 'recipient@example.com',
subject: 'Newsletter',
content: {
text: 'This is the plain text version of the email.',
html: 'Newsletter
This is the HTML version of the email.
'
}
});
`Expected Output:
Same structure as text email example above.
#### 4. Send Email with Single Attachment
Input:
`typescript
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: 'recipient@example.com',
subject: 'Document Attached',
content: {
text: 'Please find the attached document.',
html: '
Please find the attached document.
'
},
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:
`typescript
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: 'recipient@example.com',
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:
`typescript
const result = await send_email({
to: ['user1@example.com', 'user2@example.com', 'user3@example.com'],
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:
`typescript
const result = await send_email({
to: 'recipient@example.com',
cc: ['cc1@example.com', 'cc2@example.com'],
bcc: 'bcc@example.com',
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:
`typescript
const result = await send_email({
to: 'recipient@example.com',
subject: 'Custom Sender',
content: {
text: 'This email is from a custom sender.',
},
from: 'custom@example.com',
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:
`typescript
const result = await send_email({
to: 'recipient@example.com',
subject: 'Support Request',
content: {
text: 'Please reply to this email for support.',
},
reply_to: 'support@example.com'
});
`Expected Output:
Same structure as text email example above.
#### 10. Complete Example with All Options
Input:
`typescript
import fs from 'fs';const attachment_content = fs.readFileSync('report.pdf').toString('base64');
const result = await send_email({
to: ['primary@example.com', 'secondary@example.com'],
cc: 'manager@example.com',
bcc: 'archive@example.com',
subject: 'Monthly Report - December 2024',
content: {
text: 'Please find the monthly report attached. This is the plain text version.',
html:
Please find the monthly report attached.
This is the HTML version of the email.
Best regards,
Team
},
attachments: [
{
filename: 'monthly-report-december-2024.pdf',
content: attachment_content,
mime_type: 'application/pdf'
}
],
from: 'reports@example.com',
from_name: 'Reporting Team',
reply_to: 'support@example.com'
});
`Expected Output (Success):
`typescript
{
success: true,
message_id: 'msg_xyz789abc123',
message: 'Email sent successfully',
raw_response: {
status: 200,
status_text: 'OK',
headers: {
'content-type': 'application/json',
'content-length': '156',
// ... other headers
},
body: {
data: {
message_id: 'msg_xyz789abc123'
}
}
}
}
`Expected Output (Error - Validation):
`typescript
{
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):
`typescript
{
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
$3
####
SendEmailOptions Interface| Parameter | Type | Required | Description | Example |
|-----------|------|----------|-------------|---------|
|
to | string \| string[] | Yes | Recipient email address(es) | 'user@example.com' or ['user1@example.com', 'user2@example.com'] |
| subject | string | Yes | Email subject line | 'Welcome Email' |
| content | EmailContent | Yes | Email content (text and/or HTML) | { text: 'Hello', html: 'Hello
' } |
| content.text | string | No* | Plain text email content | 'This is plain text' |
| content.html | string | No* | HTML email content | 'Content
' |
| attachments | EmailAttachment[] | No | Array of file attachments | See attachment format below |
| from | string | No | Override default from email | 'custom@example.com' |
| from_name | string | No | Override default from name | 'Custom Sender' |
| reply_to | string | No | Reply-to email address | 'support@example.com' |
| cc | string \| string[] | No | CC recipient(s) | 'cc@example.com' or ['cc1@example.com', 'cc2@example.com'] |
| bcc | string \| string[] | No | BCC recipient(s) | 'bcc@example.com' or ['bcc1@example.com', 'bcc2@example.com'] |\* 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' |$3
####
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 | 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)
`typescript
{
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.$3
The library performs comprehensive validation on all inputs:
#### Email Address Validation
- Format validation using RFC 5322 compliant regex
- Maximum length: 254 characters
- Examples:
- ✅ Valid:
'user@example.com', 'user.name@example.co.uk'
- ❌ Invalid: 'invalid-email', 'user@', '@example.com'#### 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: 'Hello
' } 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)$3
All errors are returned in a consistent format:
`typescript
{
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_windowAPI Reference
$3
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#### Returns
-
Promise: Promise that resolves to email send response
- See Output Response section above for detailed field descriptions#### Example
`typescript
import { send_email } from 'hazo_notify';try {
const result = await send_email({
to: 'recipient@example.com',
subject: 'Test Email',
content: {
text: 'This is a test email',
html: '
This is a test email
'
}
}); 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);
}
`$3
Load emailer configuration from
config/hazo_notify_config.ini.#### Returns
-
EmailerConfig: Emailer configuration object#### Example
`typescript
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);
`Test UI
To enable the UI component and all routes, set
enable_ui=true in the [ui] section of config/hazo_notify_config.ini. The test UI will be available at /hazo_notify/emailer_test.$3
- Form Interface: Easy-to-use form for testing email functionality
- Text and HTML Support: Test both plain text and HTML emails
- Newline Conversion: HTML input automatically converts newlines to
tags
- Raw Request Display: View the exact request payload that will be sent
- Response Display: View the complete response from the email provider
- Error Handling: Clear error messages for validation and API errors$3
1. Set
enable_ui=true in config/hazo_notify_config.ini
2. Start your Next.js development server: npm run dev
3. Navigate to: http://localhost:3000/hazo_notify/emailer_testNote: When
enable_ui=false, both the UI and API routes are disabled for security.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:
`bash
npm test
`Run tests in watch mode:
`bash
npm run test:watch
`Run tests with coverage:
`bash
npm run test:coverage
`Development
$3
`
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
``MIT
Pubs Abayasiri
For issues and questions, please visit the GitHub repository.