Express.js Integration
This guide shows how to integrate genro-mail-proxy with an Express.js (Node.js) application.
The standard Node.js email library is Nodemailer. It works well for simple cases but has limitations:
Synchronous API (callbacks or promises, but blocks the event loop during SMTP)
No built-in queue or retry mechanism
No delivery tracking beyond the initial send result
Rate limiting requires manual implementation
For production use, you typically add Bull/BullMQ (Redis-based queue), which adds infrastructure complexity similar to Python’s Celery.
genro-mail-proxy provides queuing, retry, and delivery reports as a standalone service, accessible via simple HTTP calls from any Node.js application.
When to use the proxy with Express
Consider genro-mail-proxy when:
You don’t want to introduce Redis + Bull just for email
You need delivery reports with automatic callback
Multiple services (Node.js, Python, etc.) share email infrastructure
You need rate limiting shared across application instances
When Nodemailer is sufficient:
Low volume, non-critical emails
Immediate send with no retry requirements
Single instance with no rate limiting needs
Comparison with Nodemailer + Bull
Feature |
Nodemailer + Bull |
genro-mail-proxy |
|---|---|---|
Dependencies |
Redis + Bull worker |
Only the proxy |
Delivery reports |
Manual implementation |
Built-in HTTP callback |
Retry on failure |
Bull retry options |
Built-in exponential backoff |
Rate limiting |
Bull rate limiter |
Built-in per account |
Language agnostic |
No (Node.js only) |
Yes (HTTP API) |
Operational complexity |
Medium (Redis + worker) |
Medium (one service) |
Installation
npm install axios # or use native fetch in Node 18+
Configuration
// config.js
module.exports = {
mailProxy: {
url: process.env.MAIL_PROXY_URL || 'http://localhost:8000',
token: process.env.MAIL_PROXY_TOKEN || 'your-api-token',
accountId: process.env.MAIL_PROXY_ACCOUNT || 'default',
},
};
Client module
// mailProxy.js
const axios = require('axios');
const { v4: uuidv4 } = require('uuid');
const config = require('./config');
class MailProxyClient {
constructor() {
this.baseUrl = config.mailProxy.url;
this.token = config.mailProxy.token;
this.accountId = config.mailProxy.accountId;
}
/**
* Send an email through the mail proxy.
*
* @param {Object} options - Email options
* @param {string} options.subject - Email subject
* @param {string} options.body - Plain text body
* @param {string} options.from - Sender address
* @param {string|string[]} options.to - Recipient(s)
* @param {string|string[]} [options.cc] - CC recipient(s)
* @param {string|string[]} [options.bcc] - BCC recipient(s)
* @param {string} [options.html] - HTML body
* @param {Object[]} [options.attachments] - Attachments
* @param {number} [options.priority=2] - Priority (0-3)
* @param {string} [options.messageId] - Custom message ID
* @returns {Promise<Object>} Response with queued/rejected counts
*/
async sendMail({
subject,
body,
from,
to,
cc,
bcc,
html,
attachments,
priority = 2,
messageId,
}) {
const message = {
id: messageId || uuidv4(),
account_id: this.accountId,
from,
to: Array.isArray(to) ? to : [to],
subject,
body: html || body,
content_type: html ? 'html' : 'plain',
priority,
};
if (cc) {
message.cc = Array.isArray(cc) ? cc : [cc];
}
if (bcc) {
message.bcc = Array.isArray(bcc) ? bcc : [bcc];
}
if (attachments) {
message.attachments = attachments;
}
const response = await axios.post(
`${this.baseUrl}/commands/add-messages`,
{ messages: [message] },
{
headers: {
'X-API-Token': this.token,
'Content-Type': 'application/json',
},
timeout: 10000,
}
);
return response.data;
}
}
module.exports = new MailProxyClient();
Express application
// app.js
const express = require('express');
const mailProxy = require('./mailProxy');
const app = express();
app.use(express.json());
// Send welcome email
app.post('/send-welcome/:userId', async (req, res, next) => {
try {
const user = await getUser(req.params.userId);
const result = await mailProxy.sendMail({
subject: `Welcome ${user.name}!`,
body: `Hello ${user.name}, thanks for signing up.`,
from: 'noreply@example.com',
to: user.email,
priority: 1,
});
res.json(result);
} catch (err) {
next(err);
}
});
// Send invoice with attachment
app.post('/send-invoice/:invoiceId', async (req, res, next) => {
try {
const invoice = await getInvoice(req.params.invoiceId);
const result = await mailProxy.sendMail({
subject: `Invoice #${invoice.number}`,
body: `Please find attached invoice #${invoice.number}.`,
html: `<p>Please find attached invoice <strong>#${invoice.number}</strong>.</p>`,
from: 'billing@example.com',
to: invoice.customerEmail,
attachments: [
{
filename: `invoice_${invoice.number}.pdf`,
storage_path: `invoice_id=${invoice.id}`,
fetch_mode: 'endpoint',
},
],
});
res.json(result);
} catch (err) {
next(err);
}
});
app.listen(3000);
Delivery reports endpoint
// app.js (continued)
// Receive delivery reports from the mail proxy
app.post('/mail/delivery-report', (req, res) => {
const { delivery_report: reports = [] } = req.body;
let sent = 0;
let error = 0;
for (const report of reports) {
const { id: messageId, sent_ts, error_ts, error: errorMsg } = report;
if (sent_ts) {
sent++;
// Update database
// await markEmailSent(messageId, sent_ts);
} else if (error_ts) {
error++;
// Log error
// await markEmailFailed(messageId, errorMsg);
}
}
res.json({ ok: true, queued: 0 });
});
// Serve attachments to the mail proxy
app.post('/mail/attachments', async (req, res) => {
const { invoice_id: invoiceId } = req.body;
if (invoiceId) {
const invoice = await getInvoice(invoiceId);
const pdfBuffer = await generateInvoicePdf(invoice);
res.set('Content-Type', 'application/pdf');
res.send(pdfBuffer);
return;
}
res.status(404).send();
});
TypeScript version
// mailProxy.ts
import axios from 'axios';
import { v4 as uuidv4 } from 'uuid';
interface MailOptions {
subject: string;
body: string;
from: string;
to: string | string[];
cc?: string | string[];
bcc?: string | string[];
html?: string;
attachments?: Attachment[];
priority?: number;
messageId?: string;
}
interface Attachment {
filename: string;
storage_path: string;
fetch_mode?: string;
}
interface SendResult {
queued: number;
rejected: Array<{ id: string; reason: string }>;
}
class MailProxyClient {
private baseUrl: string;
private token: string;
private accountId: string;
constructor() {
this.baseUrl = process.env.MAIL_PROXY_URL || 'http://localhost:8000';
this.token = process.env.MAIL_PROXY_TOKEN || '';
this.accountId = process.env.MAIL_PROXY_ACCOUNT || 'default';
}
async sendMail(options: MailOptions): Promise<SendResult> {
const {
subject,
body,
from,
to,
cc,
bcc,
html,
attachments,
priority = 2,
messageId,
} = options;
const message: Record<string, unknown> = {
id: messageId || uuidv4(),
account_id: this.accountId,
from,
to: Array.isArray(to) ? to : [to],
subject,
body: html || body,
content_type: html ? 'html' : 'plain',
priority,
};
if (cc) message.cc = Array.isArray(cc) ? cc : [cc];
if (bcc) message.bcc = Array.isArray(bcc) ? bcc : [bcc];
if (attachments) message.attachments = attachments;
const response = await axios.post<SendResult>(
`${this.baseUrl}/commands/add-messages`,
{ messages: [message] },
{
headers: {
'X-API-Token': this.token,
'Content-Type': 'application/json',
},
timeout: 10000,
}
);
return response.data;
}
}
export default new MailProxyClient();
Nodemailer comparison
For reference, here’s how Nodemailer with Bull looks:
// With Nodemailer + Bull
const nodemailer = require('nodemailer');
const Queue = require('bull');
const transporter = nodemailer.createTransport({
host: 'smtp.example.com',
port: 587,
auth: { user: 'user', pass: 'password' },
});
const emailQueue = new Queue('email', 'redis://localhost:6379');
emailQueue.process(async (job) => {
await transporter.sendMail(job.data);
// No automatic delivery report to your app
});
// Usage
await emailQueue.add({
from: 'noreply@example.com',
to: user.email,
subject: 'Welcome!',
text: 'Hello',
});
This requires running a Redis server. The mail proxy consolidates queuing, retry, and delivery tracking into a single service.
Using native fetch (Node 18+)
If you prefer not to use axios:
// mailProxy.js (native fetch version)
const { v4: uuidv4 } = require('uuid');
class MailProxyClient {
constructor() {
this.baseUrl = process.env.MAIL_PROXY_URL || 'http://localhost:8000';
this.token = process.env.MAIL_PROXY_TOKEN || '';
this.accountId = process.env.MAIL_PROXY_ACCOUNT || 'default';
}
async sendMail({ subject, body, from, to, html, priority = 2, messageId }) {
const message = {
id: messageId || uuidv4(),
account_id: this.accountId,
from,
to: Array.isArray(to) ? to : [to],
subject,
body: html || body,
content_type: html ? 'html' : 'plain',
priority,
};
const response = await fetch(`${this.baseUrl}/commands/add-messages`, {
method: 'POST',
headers: {
'X-API-Token': this.token,
'Content-Type': 'application/json',
},
body: JSON.stringify({ messages: [message] }),
});
if (!response.ok) {
throw new Error(`Mail proxy error: ${response.status}`);
}
return response.json();
}
}
module.exports = new MailProxyClient();
Proxy tenant configuration
Configure the proxy tenant to point to your Express endpoints:
mail-proxy myserver tenants add myexpressapp \
--base-url "https://myexpressapp.example.com" \
--sync-path "/mail/delivery-report" \
--attachment-path "/mail/attachments" \
--auth-method bearer \
--auth-token "shared-secret"