by Lin X.

Postal is a fully featured, self-hosted mail server for websites and web servers. A free open-source alternative to Sendgrid, Mailgun, and Postmark.
Postal is a full-featured, open-source mail server written in Ruby on Rails that handles inbound and outbound email for web applications. It provides a self-hosted alternative to services like Sendgrid or Mailgun, exposing HTTP APIs for sending mail and webhooks for delivery events. The typical buyer is a backend engineer embedding transactional email infrastructure into their own infrastructure stack.
.github/ - CI workflow definitions and issue templates for the upstream projectapp/ - Core Rails application: controllers, models, mailers, services, senders, scheduled tasks, and assetsapp/assets/ - Compiled frontend assets: stylesheets, fonts, and icon SVGs for the web UIapp/controllers/ - Rails controllers handling the web UI and HTTP API endpointsapp/helpers/ - Rails view helper modulesapp/lib/ - Internal application-level libraries and modulesapp/mailers/ - ActionMailer classes for system-level emails (alerts, invitations, etc.)app/models/ - ActiveRecord models: Server, Domain, Message, Credential, IPPool, etc.app/scheduled_tasks/ - Background/cron job definitions for queue processing and cleanupapp/senders/ - Sender strategy classes (SMTP, SendGrid relay, etc.)app/services/ - Service objects encapsulating complex business logic (message processing, DNS checks)app/util/ - Utility classes and helpers used across the applicationconfig/ - Rails configuration: routes, initializers, database, environmentsdb/ - Database schema and migrationsdoc/ - Internal documentation and architecture notesdocker/ - Dockerfile and entrypoint scripts for containerised deploymentlib/ - Rails lib directory: rake tasks, custom middleware, extensionspublic/ - Static files served directly by the web serverscript/ - Developer utility scriptsdocker-compose.yml - Local development stack definition (MySQL, Redis, Postal)CHANGELOG.md - Release historyCONTRIBUTING.md - Contribution guidelinesREADME.md - Project overview and quickstart linksSpin up an isolated sandbox and run it server-side — no local setup.
Tetrees AI Review of this version
This Ruby library / package 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 6c9526f3b2708c91…
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…
SECURITY.md - Security disclosure policyThis is a Ruby on Rails application, not a Node.js package. There are no npm dependencies to install.
# No npm install required - this is a Ruby/Rails application.
# Runtime dependencies are managed via Bundler (Gemfile).
Native / non-Node build steps required before any integration:
# 1. Ruby >= 3.1 and Bundler
gem install bundler
bundle install
# 2. MySQL (or MariaDB) and Redis must be running
# Use the provided docker-compose for local development:
docker compose up -d
# 3. MariaDB schema setup
bundle exec rails db:create db:schema:load db:seed
# 4. Build assets
bundle exec rails assets:precompile
Clone or drop the source/ directory onto a host that has Ruby, MySQL/MariaDB, and Redis available.
Copy and edit the configuration file:
cp config/postal.example.yml config/postal.yml
# Edit config/postal.yml: set database credentials, Redis URL, SMTP bind address, signing keys
Set required environment variables (or populate config/postal.yml):
export POSTAL_DB_HOST=127.0.0.1
export POSTAL_DB_PORT=3306
export POSTAL_DB_NAME=postal
export POSTAL_DB_USERNAME=postal
export POSTAL_DB_PASSWORD=secret
export POSTAL_REDIS_URL=redis://127.0.0.1:6379/1
export POSTAL_WEB_HOST=postal.yourdomain.com
export POSTAL_SIGNING_KEY_PATH=/opt/postal/config/signing.key
Generate a signing key (run once):
openssl genrsa -out /opt/postal/config/signing.key 2048
Start all Postal processes:
bundle exec postal web-server # HTTP API + UI on port 5000
bundle exec postal smtp-server # SMTP listener
bundle exec postal worker # Background message processing
bundle exec postal cron # Scheduled tasks
To call Postal's HTTP Send API from a Node/TypeScript service, point your HTTP client at the running Postal instance (no native Node module needed).
Because Postal is a Rails application with no exported TypeScript/JavaScript symbols, the public surface is its HTTP API. The following are the real HTTP endpoints documented by the project and visible in the controllers directory.
interface PostalSendMessagePayload {
to: string[];
cc?: string[];
bcc?: string[];
from: string;
sender?: string;
subject: string;
plain_body?: string;
html_body?: string;
attachments?: Array<{ name: string; content_type: string; data: string }>; // base64 data
headers?: Record<string, string>;
reply_to?: string;
tag?: string;
bounce?: boolean;
}
interface PostalSendMessageResponse {
status: "success" | "error";
time: number;
flags: Record<string, unknown>;
data: {
message_id: string;
messages: Record<string, { id: number; token: string }>;
};
}
Send a single transactional message. Requires an X-Server-API-Key header set to a credential created in the Postal UI.
interface PostalSendRawPayload {
mail_from: string;
rcpt_to: string[];
data: string; // Raw RFC 2822 message, base64-encoded
bounce?: boolean;
}
Send a pre-composed RFC 2822 raw message. Use this when you already have a fully-formed MIME message (e.g., produced by Nodemailer's buildMessage()).
interface PostalGetMessageParams {
id: number;
_expansions?: boolean | string[];
}
interface PostalMessageDetail {
id: number;
token: string;
status: string;
details: Record<string, unknown>;
inspection: Record<string, unknown>;
plain_body: string;
html_body: string;
attachments: unknown[];
headers: Record<string, string>;
raw_message: string;
}
Retrieve full details and delivery status of a message by its numeric ID. Use after sending to audit delivery or fetch bounce details.
A backend service sends a password-reset email through a running Postal instance using the HTTP API.
import axios from "axios";
const POSTAL_URL = process.env.POSTAL_URL ?? "https://postal.yourdomain.com";
const POSTAL_API_KEY = process.env.POSTAL_API_KEY ?? "";
async function sendPasswordReset(toEmail: string, resetLink: string) {
const response = await axios.post(
`${POSTAL_URL}/api/v1/send/message`,
{
to: [toEmail],
from: "user@example.com",
subject: "Reset your password",
html_body: `<p>Click <a href="${resetLink}">here</a> to reset your password.</p>`,
plain_body: `Reset your password: ${resetLink}`,
tag: "password-reset",
},
{
headers: {
"X-Server-API-Key": POSTAL_API_KEY,
"Content-Type": "application/json",
},
}
);
const data = response.data as { status: string; data: { message_id: string } };
if (data.status !== "success") throw new Error("Postal send failed");
return data.data.message_id;
}
When you need full MIME control (inline images, S/MIME signing), build the raw message and submit it via /api/v1/send/raw.
import nodemailer from "nodemailer";
import axios from "axios";
const POSTAL_URL = process.env.POSTAL_URL!;
const POSTAL_API_KEY = process.env.POSTAL_API_KEY!;
async function sendRawMime(to: string) {
const transporter = nodemailer.createTransport({ jsonTransport: true });
const info = await transporter.sendMail({
from: "user@example.com",
to,
subject: "Invoice #1234",
text: "Please find your invoice attached.",
});
// nodemailer jsonTransport gives us the raw source
const rawBase64 = Buffer.from(
(info as unknown as { message: string }).message
).toString("base64");
const res = await axios.post(
`${POSTAL_URL}/api/v1/send/raw`,
{
mail_from: "user@example.com",
rcpt_to: [to],
data: rawBase64,
},
{ headers: { "X-Server-API-Key": POSTAL_API_KEY } }
);
return res.data;
}
After sending, retrieve message status to confirm delivery or detect bounces.
import axios from "axios";
async function getMessageStatus(messageId: number): Promise<string> {
const POSTAL_URL = process.env.POSTAL_URL!;
const POSTAL_API_KEY = process.env.POSTAL_API_KEY!;
const res = await axios.get(`${POSTAL_URL}/api/v1/messages/message`, {
params: { id: messageId, _expansions: true },
headers: { "X-Server-API-Key": POSTAL_API_KEY },
});
const detail = res.data.data as { status: string };
return detail.status; // "Sent", "Bounced", "HeldAsSpam", etc.
}
.github/ - GitHub Actions CI pipeline (ci.yml) and issue template configuration.app/assets/ - Sprockets-managed fonts, icons (SVG), stylesheets, and the asset manifest.app/controllers/ - Rails controllers for both the browser UI and the versioned HTTP API (/api/v1/).app/helpers/ - View helpers used in ERB templates.app/lib/ - Application-scoped Ruby modules (not gem-level), such as message parsers.app/mailers/ - ActionMailer classes for outbound system notifications (alerts, user invites).app/models/ - ActiveRecord models covering every core domain concept: Server, Domain, Message, Credential, IPPool, Webhook, etc.app/scheduled_tasks/ - Cron-triggered tasks (queue expiry, bounce processing, log cleanup).app/senders/ - Strategy pattern implementations for delivering mail (direct SMTP, relay, SpamAssassin integration).app/services/ - Service objects for DNS validation, message queuing, suppression list handling.app/util/ - Standalone utility classes (IP range parsing, token generation).config/ - routes.rb, database.yml, environment configs, and initializers.db/ - schema.rb (authoritative DB structure) and numbered migrations.doc/ - Architecture diagrams and internal decision records.docker/ - Dockerfile and docker-entrypoint for the official container image.lib/ - Rake tasks, Rack middleware, and Ruby extensions loaded outside app/.public/ - Static HTML error pages and compiled asset output directory.script/ - One-off developer scripts (data migrations, diagnostic helpers).docker-compose.yml - Spins up MySQL, Redis, and Postal together for local development.POSTAL_SIGNING_KEY_PATH points to a non-existent file. Fix: run openssl genrsa -out <path> 2048 before first start.utf8mb4 charset and utf8mb4_unicode_ci collation or migrations fail. Fix: add character-set-server=utf8mb4 to my.cnf.401.postal.yml.GET /api/v1/public_key and verify the X-Postal-Signature header using RSA-SHA1 before trusting payload content.RAILS_ENV=production bundle exec rails assets:precompile before starting the web server.I have the source code for the Postal open-source mail server (a Ruby on Rails application)
located in the `source/` directory of my project. I also have `USAGE.md` which documents
the HTTP API surface and setup steps.
My project is a Node.js/TypeScript backend (Express). I need you to:
1. Read `USAGE.md` and `source/` to understand all available HTTP API endpoints
(especially /api/v1/send/message, /api/v1/send/raw, /api/v1/messages/message).
2. Create a typed TypeScript client module at `src/lib/postalClient.ts` that wraps
these endpoints with full request/response types.
3. Add environment variable handling for POSTAL_URL and POSTAL_API_KEY with
validation on startup.
4. Wire the client into my existing Express app so I can call `postalClient.sendMessage(...)`
from any route handler.
5. Add a webhook receiver route that verifies the X-Postal-Signature header using
the public key from /api/v1/public_key.
6. Show me how to run Postal locally using the docker-compose.yml in source/ alongside
my Express app.
Work step by step and confirm each step before proceeding to the next.
Postal is released under the MIT License (see source/LICENSE if present, or the upstream repository). The upstream project is maintained by the Postal Server team and community contributors.
Upstream repository: https://github.com/postalserver/postal Documentation: https://docs.postalserver.io
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