EmailEngine holds your users' mailbox credentials, so you are owed specifics rather than assurances. This page covers what is stored and where, what is encrypted and with what, everything that ever leaves your network, and how to report a problem. The source is available, so every claim here is one your security team can check.
EmailEngine runs on your infrastructure. Message content is fetched on demand from the original mailbox and never copied to anyone else's servers, ours included. The strongest security property is the one that needs no policy: your users' mail has nowhere else to go.
What EmailEngine keeps is a sync index: message IDs, flags and folder structure, in your own Redis. There is no message archive, no attachment store and no webhook delivery log to protect, because none of them exist.
There is no remote management channel and no backdoor. The only inbound signal EmailEngine accepts from us is the licence validation response, which can invalidate a licence and nothing else: it cannot push code, change configuration or read data. We cannot reach your instance even if you ask us to.
| Data | Where it lives | Protection |
|---|---|---|
| Mailbox passwords, OAuth2 access and refresh tokens | Your Redis | Encrypted with AES-256-GCM |
| OAuth2 client secrets, service keys, TOTP seeds | Your Redis | Encrypted with AES-256-GCM |
| API access tokens | Your Redis | Only the SHA-256 hash; the token itself is shown once and never stored |
| Message index: IDs, flags, labels, folder structure | Your Redis | Metadata only, no content |
| Queued sends and webhook deliveries | Your Redis | Transient; discarded once completed or permanently failed |
| Message bodies, attachments, full headers | Nowhere | Fetched on demand from the original mailbox, never stored |
| Sent-mail archive, webhook history | Nowhere | Not kept; EmailEngine is a gateway, not a datastore |
Mailbox exports, when you request them, are written as chunked AES-256-GCM streams with owner-only file permissions. The full inventory, including the optional features that change it, is in the compliance documentation.
Field-level encryption for everything that could log into a mailbox, with a key that never leaves your side.
Every stored credential, OAuth2 token and secret is encrypted with AES-256-GCM: a fresh random IV and salt per value, an authentication tag checked on every read, and a 256-bit key derived with scrypt.
The encryption key comes from the EENGINE_SECRET environment variable, or from a file path for Docker, Kubernetes and Vault-style secret managers. It is never written to Redis and never sent anywhere.
A CLI command re-encrypts all stored data under a new key, and accepts multiple previous keys in one pass, so rotating generations of secrets is an operation rather than a project.
Mailbox exports are gzip-compressed, then encrypted as chunked AES-256-GCM streams with a per-chunk IV and authentication tag, written with owner-only permissions.
Certificates are validated by default; accepting invalid ones is a deliberate, per-deployment opt-in that the schema itself labels as not for production. The TLS version and cipher floors default to maximum mailbox compatibility and can be raised by environment variable.
Admin sessions use an authenticated, encrypted cookie with SameSite set, and every session is invalidated the moment the password changes.
An API token is 32 random bytes, shown once at creation. EmailEngine stores only its SHA-256 hash, so no dump of Redis can recover a working token.
Tokens can be limited to one account, one scope, an IP or CIDR allowlist, specific HTTP referrers and a per-token rate limit, so the credential in your least-trusted integration carries the smallest possible blast radius.
Admin passwords are hashed with PBKDF2-SHA256 at 600,000 iterations, in line with current OWASP guidance, with per-IP and per-username rate limits on the login form.
TOTP two-factor authentication with a replay guard that burns each code after use, and WebAuthn passkeys with single-use, short-lived challenges. A password change revokes every passkey.
OpenID Connect against any provider using the authorization code flow with PKCE, plus an SSO-only mode that disables local logins entirely. Okta is supported as a dedicated integration.
SSO access can be allow-listed by email, domain or IdP group, and the lists are re-evaluated on every request rather than at login, so removing someone takes effect immediately instead of when their cookie expires.
The dashboard can additionally be restricted to an admin IP allowlist, and unverified IdP email addresses are refused even when they match an allow-list. Details in the security documentation.
Everything is structured JSON on standard output, ready for whatever log pipeline you already run.
Successful and failed logins, TOTP attempts and replays, passkey registrations, deletions and each distinct failure reason, logouts and rate-limit hits, all with the acting user and remote address.
Creating and deleting access tokens is logged with the token's hash and its restriction metadata, so you can reconstruct who could access what, and when that changed.
Authorization headers, cookies and raw network packets are redacted from logs, and settings values that decrypt are deliberately kept out of error messages.
Per-account IMAP protocol logging can be switched on while you investigate an incident, then downloaded as a transcript of exactly what the mail server said.
This is the complete list, from the source code, of what EmailEngine sends anywhere other than your own mail servers and webhook endpoints. It is short enough to read, which is the point.
At most once per 24 hours, subscription licences check in with the licence key, the EmailEngine version, the application name and an instance identifier. If the check cannot be made, a 28-day grace period keeps your instance running. Perpetual licences never make this call at all: they verify offline, so neither the validation request nor the digest below exists for them.
Alongside validation, an optional digest reports which features are enabled and coarse order-of-magnitude buckets rather than raw counts. It never contains addresses, URLs, credentials or message data, and EENGINE_BEACON_DISABLED turns it off.
A periodic request to the GitHub releases API drives the "update available" notice in the dashboard. It sends a user-agent string and nothing else.
Deliverability tests, IMAP settings autodiscovery and Let's Encrypt certificate issuance each contact their respective services, and each happens only on an explicit action or opt-in, never in the background.
Sentry error reporting is off until you enable it, and you can point it at your own self-hosted Sentry instead of ours.
No telemetry beyond the digest above, no analytics, no content ever. The source is available, so this claim is verifiable rather than contractual: grep it.
Every delivery carries an HMAC-SHA256 signature over the exact request body, so your endpoint can verify that the event came from your instance and was not altered in flight.
A stable event ID and attempt counter accompany every delivery, so retried webhooks are recognised rather than double-processed.
Webhook destinations are checked against an egress policy that blocks cloud metadata endpoints by default and can block all private address space. Redirects are not followed, so an allowed host cannot bounce a request to a blocked one.
Every install ships its SPDX software bill of materials and serves it at /sbom.json, so your instance can hand your auditor its own dependency inventory.
The npm package is published with a provenance attestation, giving a publicly verifiable link from the package on the registry back to the exact source and build that produced it.
The Docker image builds from a digest-pinned Node base, runs as a non-root user with a proper init process, and installs exact-pinned production dependencies from the lockfile.
Every dependency version is pinned exactly, and updates are batched, reviewed and tested against supported Node versions rather than merged automatically.
Every push runs a dependency licence check that fails the build on a disallowed licence, and the resulting inventory is served by your instance at /licenses.html.
EmailEngine supports Node.js 20 and later, prebuilt binaries bundle a current Node runtime, and Redis is the only other moving part.
No. EmailEngine is software you run, so it inherits the compliance posture of your own environment rather than carrying certifications of its own. If your environment is certified, EmailEngine adds no third party to assess, because your users' mail never reaches us. If your buyers require the email vendor itself to hold the certificate, a hosted API is the better fit.
Only once you set a secret. Field-level encryption of credentials and tokens activates when the EENGINE_SECRET encryption key is configured; without it, stored values are not encrypted. Setting it is the first item in the deployment security checklist, and a built-in CLI re-encrypts existing data when you add or rotate the key.
Your reverse proxy. EmailEngine binds to localhost by default and is designed to run behind Caddy, nginx or a similar proxy that terminates TLS and adds security headers; the documentation ships ready-made configurations. The application does not emit CSP or HSTS headers itself.
Webhook filter and transform functions run in an isolated execution context with a restricted set of globals and an enforced timeout, and only administrators can author them. This is an isolation mechanism, not a hardened security sandbox: treat script authoring as an admin-level privilege and never expose it to untrusted users. The documentation says the same thing, in the same words.
There is no published third-party audit report. The full source is available, so your security team can review the code directly rather than taking a summary on trust, and vulnerabilities are handled through a private disclosure process with published advisories.
The latest release only. Fixes are not backported; upgrading to the current release line is the supported way to receive security updates. EmailEngine ships a release every few days, so staying current is the normal operating mode.
Please report privately rather than in a public issue. Reports go to the person who wrote the code, and reporters who want credit are named in the advisory.
The preferred channel: open a private report on GitHub. The discussion stays private until a fix ships, and accepted issues are disclosed as published advisories.
Send details to andris@postalsys.com, encrypted with the published PGP key if the report is sensitive. A signed security.txt carries the same contacts.
The security policy lists what is in scope (API and admin authentication, OAuth2 handling, credential encryption, the proxy servers, webhook delivery, exports) and what is not, so you know before you start digging.
The strongest claim on this page is that none of it has to be taken on trust. Read the source, then run the trial on your own hardware.