by orion

Haraka is a scalable, plugin-driven SMTP server built on Node.js for filtering, relaying, and outbound mail delivery. Ideal for teams needing a highly customizable MTA with spam protection and async plugin architecture.
Haraka is a high-performance, plugin-driven SMTP server built on Node.js. It handles inbound mail filtering, outbound delivery queuing, TLS negotiation, and connection lifecycle management via a hook-based plugin architecture. The typical buyer is a backend engineer embedding a programmable MTA into an existing Node.js service or building a custom email gateway.
server.js - Cluster-aware SMTP server bootstrap; manages listeners, workers, and graceful shutdownconnection.js - Per-connection SMTP state machine; handles EHLO/MAIL/RCPT/DATA and fires plugin hookstransaction.js - Encapsulates a single SMTP message transaction (envelope + headers + body)plugins.js - Plugin loader, hook registration, and dispatch enginelogger.js - Structured leveled logger with per-plugin log-method injectiontls_socket.js - Pluggable TLS stream wrapper; manages cert loading, SNI, and OCSPoutbound/index.js - Outbound delivery queue entry point; exposes queue stats and flush utilitiesoutbound/ - Full outbound subsystem: HMailItem, TODOItem, queue file I/O, DNS MX resolutionsmtp_client.js - SMTP client used by outbound to relay messageshost_pool.js - Pool of target SMTP hosts for load-balanced deliveryrfc1869.js - ESMTP extension parameter parserendpoint.js - Network endpoint abstraction (TCP/UNIX socket)line_socket.js - Line-buffered socket wrapperharaka.js - CLI entry point (haraka -i / -c)config/ - INI configuration files for SMTP, TLS, outbound, logging, etc.plugins/ - Bundled plugin implementations (auth, queue, DKIM, etc.)docs/ - Markdown documentation for plugins, transactions, and configurationnpm install address-rfc2821 address-rfc2822 haraka-config haraka-constants \
haraka-dsn haraka-email-message haraka-message-stream haraka-net-utils \
haraka-notes haraka-plugin-redis haraka-results haraka-tld haraka-utils \
ipaddr.js nopt semver redis
Native module note: node-gyp is listed as a dependency. Ensure a C++ build toolchain is present (build-essential on Debian/Ubuntu, Xcode CLI tools on macOS). Run after installation if native bindings fail to load.
Spin up an isolated sandbox and run it server-side — no local setup.
Tetrees AI Review of this version
This Express backend / api completed archive review with strong static results. Structure, dependency manifests, documentation, functional source, and common risk patterns were checked by the Tetrees verification pipeline; runtime phases are stated separately.
Deterministic AVCP artifact review
Pipeline avcp-2026-08-04.1 · SHA-256 02ea27fc371d1545…
This version-scoped review deterministically inspects the submitted archive for structure, dependencies, documentation, functional source, and common malicious or high-risk signals. Build and test phases are reported as passed only after an isolated sandbox audition. It is not a guarantee of perfect security.
Reviewed Aug 4, 2026
Push this product straight into your AI IDE, web builder or cloud IDE.
Connect Tetrees to a compatible AI IDE, list products you own, and request the verified ZIP without exposing seller upload controls.
No reviews yet.
Sign in to join the discussion
Loading discussion…
npm rebuildsource/ directory into your project root, e.g. ./haraka-src/.HARAKA environment variable to your runtime config directory so Haraka resolves plugins and config correctly:
export HARAKA=/path/to/your/haraka_config
mkdir -p /path/to/your/haraka_config/config
mkdir -p /path/to/your/haraka_config/plugins
cp haraka-src/config/*.ini /path/to/your/haraka_config/config/
/path/to/your/haraka_config/config/smtp.ini to set ports and node count:
[main]
nodes=2
port=25
listen=0.0.0.0
config/plugins to list which plugins load at startup (one per line).process.env.HARAKA = '/path/to/your/haraka_config'
const Server = require('./haraka-src/server')
Server.log_ini = Server.load_smtp_ini()
Server.setup(Server.start_smtp)
paths alias in tsconfig.json:
{
"compilerOptions": {
"paths": {
"haraka/*": ["./haraka-src/*"]
}
}
}
// server.js exports an object (not a class)
const Server: {
config: HarakaConfig,
plugins: Plugins,
logger: Logger,
notes: Record<string, unknown>,
listeners: any[],
load_smtp_ini(): void,
load_http_ini(): void,
setup(callback: () => void): void,
start_smtp(): void,
}
Server is the singleton SMTP server. Call Server.load_smtp_ini() then Server.setup(Server.start_smtp) to initialize the cluster and begin accepting connections. Access Server.notes for process-wide shared state across plugins.
class Connection {
constructor(client: net.Socket, server: any, smtp_cfg: object)
local: { ip: string|null, port: number|null, host: string, info: string }
remote: { ip: string|null, port: number|null, host: string|null }
transaction: Transaction | null
results: ResultStore
notes: Notes
}
Connection represents one live TCP session. Plugins receive a connection instance in every hook call. Read connection.remote.ip for the client address, connection.transaction for the active message envelope, and connection.results to store per-connection plugin results.
// outbound/index.js named exports
const outbound: {
temp_fail_queue: Queue,
delivery_queue: Queue,
name: 'outbound',
get_stats(): Stats,
list_queue(): Promise<QueueItem[]>,
stat_queue(): Promise<object>,
flush_queue(domain?: string): Promise<void>,
ensure_queue_dir(): Promise<void>,
init_queue(): Promise<void>,
send_email(...args): void,
}
Use outbound.get_stats() for real-time queue depth metrics, outbound.flush_queue() to retry all deferred messages immediately, and outbound.init_queue() during server startup to prepare the spool directory.
Embed Haraka's server inside an existing Node.js application, listening on a non-privileged port for testing.
// src/mail-server.ts
import path from 'node:path'
process.env.HARAKA = path.resolve(__dirname, '../haraka_config')
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Server = require('../haraka-src/server')
export function startMailServer(): void {
Server.load_smtp_ini()
// Override port programmatically before setup
Server.cfg.main.port = 2525
Server.cfg.main.nodes = 1
Server.setup(() => {
Server.start_smtp()
Server.logger.loginfo('SMTP server started on port 2525')
})
}
Register a hook that rejects mail from a blocked sender domain.
// haraka_config/plugins/block_sender.js
'use strict'
exports.register = function () {
this.register_hook('mail', 'check_sender')
}
exports.check_sender = function (next, connection, params) {
const txn = connection.transaction
const mail_from = params[0] // Address object
const blocked = ['spam.example.com']
if (blocked.includes(mail_from.host)) {
txn.results.add(this, { fail: 'blocked_sender' })
return next(DENY, 'Sender domain is not accepted')
}
txn.results.add(this, { pass: 'sender_ok' })
return next()
}
Add block_sender to haraka_config/config/plugins to activate it.
Expose queue metrics via an Express endpoint alongside the running Haraka server.
// src/queue-api.ts
import express from 'express'
import path from 'node:path'
process.env.HARAKA = path.resolve(__dirname, '../haraka_config')
const outbound = require('../haraka-src/outbound')
const app = express()
app.get('/queue/stats', async (_req, res) => {
try {
await outbound.init_queue()
const stats = outbound.get_stats()
const items = await outbound.list_queue()
res.json({ stats, queued: items.length })
} catch (err) {
res.status(500).json({ error: (err as Error).message })
}
})
app.post('/queue/flush', async (_req, res) => {
await outbound.flush_queue()
res.json({ flushed: true })
})
app.listen(3001)
server.js - Initializes cluster workers, loads TLS config, binds TCP endpoints, and dispatches to connection.js for each accepted socket.connection.js - Full SMTP command parser and state machine; emits plugin hooks at each protocol stage (connect, helo, mail, rcpt, data, quit).transaction.js - Stores the envelope (MAIL FROM / RCPT TO), message headers, and body stream for a single delivery attempt.plugins.js - Scans plugin paths, require()s each plugin file, registers hook callbacks, and calls them in order with a next() continuation.logger.js - Provides logdebug/loginfo/logwarn/logerr at the module level; add_log_methods injects these onto any object (Server, Plugin instances).tls_socket.js - Wraps net.Socket in a pluggableStream that can upgrade to TLS mid-connection; handles SNI certificate lookup via certsByHost.outbound/index.js - Entry point for the delivery subsystem; re-exports queue functions from queue.js and HMailItem delivery logic.outbound/ - Contains hmail.js (per-message delivery state), todo.js (queue item schema), qfile.js (spool file format), config.js, and fsync_writestream.js.smtp_client.js - Async SMTP client used by outbound to connect to destination MX hosts and relay messages.host_pool.js - Manages a round-robin or weighted pool of relay hosts for forwarding scenarios.rfc1869.js - Parses ESMTP extension parameters from EHLO responses.endpoint.js - Abstracts a single listen address (host:port or UNIX path) and its associated server instance.line_socket.js - Buffers raw socket data into CRLF-delimited lines for SMTP command parsing.haraka.js - CLI interface; handles -i (init), -c (run), -h (help) flags.config/ - Default INI files consumed by haraka-config; copy to your config dir and override.plugins/ - Bundled plugins: auth backends, queue handlers, SMTP bridge, forward, proxy.docs/ - Reference documentation for the plugin API, transaction object, and configuration keys.HARAKA env var not set: Haraka resolves config and plugin paths relative to process.env.HARAKA; if unset it falls back to cwd, causing "config not found" errors. Fix: always export HARAKA=/your/config/dir before requiring server.js.sudo, use authbind, or set port=2525 in smtp.ini for development.node-gyp build failures: Some dependencies compile native addons. Fix: install build-essential (Linux) or Xcode CLI tools (macOS) and ensure python3 is on PATH.require() throughout. Fix: import via createRequire or set "type": "commonjs" in your wrapper package.json.'mail' not 'mail_from'). Fix: cross-reference docs/Plugins.md for the canonical hook list.outbound writes spool files to queue/ inside the config dir; the process user must own that directory. Fix: mkdir -p $HARAKA/queue && chown $USER $HARAKA/queue.I have dropped the Haraka Node.js mail server source into `./haraka-src/`
and placed USAGE.md at the project root. The upstream package is `user@example.com`.
Please help me integrate Haraka into my existing Node.js/Express project step by step:
1. Read USAGE.md and the file excerpts for server.js, connection.js, plugins.js,
and outbound/index.js to understand the real exported API.
2. Set up the HARAKA environment variable and config directory structure as described
in USAGE.md § "Project setup".
3. Add a `startMailServer()` function that programmatically starts the SMTP server
on port 2525 without forking cluster workers (nodes=1).
4. Create a custom plugin in haraka_config/plugins/ that logs every accepted sender
address using the connection.results API.
5. Add an Express route GET /mail/queue that calls outbound.get_stats() and returns
the result as JSON.
6. Wire everything so the Express HTTP server and the Haraka SMTP server start
together from a single `npm start` command.
7. Point out any pitfalls from USAGE.md § "Common pitfalls and fixes" that apply
to my setup and suggest fixes.
Only use symbols and imports that are visible in the file excerpts in USAGE.md.
Do not invent plugin hooks or API methods.
Haraka is released under the MIT License - see source/LICENSE for the full text. The project was started by Matt Sergeant and is maintained by the Haraka community. Upstream repository and package: Haraka on npm / github.com/haraka/Haraka.
This product’s unique contribution is clean room implementation of valuable code block into AVCP compatible standard.
The full install guide and integration prompts unlock after purchase.
CMS, Storefront & Platform Add-ons
Free