HappyDeliver - Email Deliverability Tester
An open-source email deliverability testing platform that analyzes test emails and provides detailed deliverability reports with scoring.
Features
- Complete Email Analysis: Analyzes SPF, DKIM, DMARC, BIMI, ARC, SpamAssassin and rspamd scores, DNS records, blacklist status, content quality, and more
- REST API: Full-featured API for creating tests and retrieving reports
- LMTP Server: Built-in LMTP server for seamless MTA integration
- Scoring System: Gives A to F grades and scoring with weighted factors across dns, authentication, spam, blacklists, content, and headers
- Database Storage: SQLite or PostgreSQL support
- Configurable: via environment or config file for all settings
Quick Start
With Docker (Recommended)
The easiest way to run HappyDeliver is using the all-in-one Docker container that includes Postfix, authentication_milter, SpamAssassin, and the HappyDeliver application.
What's included in the Docker container:
- Postfix MTA: Receives emails on port 25
- authentication_milter: Entreprise grade email authentication
- SpamAssassin: Spam scoring and analysis
- rspamd: Second spam filter for cross-validated scoring
- HappyDeliver API: REST API server on port 8080
- SQLite Database: Persistent storage for tests and reports
1. Using docker-compose
# Clone the repository
git clone https://git.nemunai.re/happyDomain/happyDeliver.git
cd happydeliver
Edit docker-compose.yml to set your domain
Change HAPPYDELIVER_DOMAIN environment variable and hostname
Build and start
docker-compose up -d
View logs
docker-compose logs -f
Stop
docker-compose down
The API will be available at http://localhost:8080 and SMTP at localhost:25.
2. Using docker build directly
# Build the image
docker build -t happydeliver:latest .
Run the container
docker run -d \
--name happydeliver \
-p 25:25 \
-p 8080:8080 \
-e HAPPYDELIVER_DOMAIN=yourdomain.com \
--hostname mail.yourdomain.com \
-v $(pwd)/data:/var/lib/happydeliver \
-v $(pwd)/logs:/var/log/happydeliver \
happydeliver:latest
3. Configure TLS Certificates (Optional but Recommended)
To enable TLS encryption for incoming SMTP connections, you can configure Postfix to use your SSL/TLS certificates. This is highly recommended for production deployments.
##### Using docker-compose
Add the certificate paths to your docker-compose.yml:
environment:
- POSTFIX_CERT_FILE=/etc/ssl/certs/mail.yourdomain.com.crt
- POSTFIX_KEY_FILE=/etc/ssl/private/mail.yourdomain.com.key
volumes:
- /path/to/your/certificate.crt:/etc/ssl/certs/mail.yourdomain.com.crt:ro
- /path/to/your/private.key:/etc/ssl/private/mail.yourdomain.com.key:ro
##### Using docker run
docker run -d \
--name happydeliver \
-p 25:25 \
-p 8080:8080 \
-e HAPPYDELIVER_DOMAIN=yourdomain.com \
-e POSTFIX_CERT_FILE=/etc/ssl/certs/mail.yourdomain.com.crt \
-e POSTFIX_KEY_FILE=/etc/ssl/private/mail.yourdomain.com.key \
--hostname mail.yourdomain.com \
-v /path/to/your/certificate.crt:/etc/ssl/certs/mail.yourdomain.com.crt:ro \
-v /path/to/your/private.key:/etc/ssl/private/mail.yourdomain.com.key:ro \
-v $(pwd)/data:/var/lib/happydeliver \
-v $(pwd)/logs:/var/log/happydeliver \
happydeliver:latest
Notes:
- The certificate file should contain the full certificate chain (certificate + intermediate CAs)
- The private key file must be readable by the postfix user inside the container
- TLS is configured with
smtpd_tls_security_level = may, which means it's opportunistic (STARTTLS supported but not required) - If both environment variables are not set, Postfix will run without TLS support
4. Configure Network and DNS
##### Open SMTP Port
Port 25 (SMTP) must be accessible from the internet to receive test emails:
# Check if port 25 is listening
netstat -ln | grep :25
Allow port 25 through firewall (example with ufw)
sudo ufw allow 25/tcp
For iptables
sudo iptables -A INPUT -p tcp --dport 25 -j ACCEPT
Note: Many ISPs and cloud providers block port 25 by default to prevent spam. You may need to request port 25 to be unblocked through your provider's support.
##### Configure DNS Records
Point your domain to the server's IP address.
yourdomain.com. IN A 203.0.113.10
yourdomain.com. IN AAAA 2001:db8::10
Replace yourdomain.com with the value you set for HAPPYDELIVER_DOMAIN and IPs accordingly.
There is no need for an MX record here since the same host will serve both HTTP and SMTP.
Manual Build
1. Build
go generate
go build -o happyDeliver ./cmd/happyDeliver
2. Run the API Server
./happyDeliver server
The server will start on http://localhost:8080 by default.
3. Integrate with your existing e-mail setup
It is expected your setup annotate the email with eg. opendkim, spamassassin, rspamd, ... HappyDeliver will not perform thoses checks, it relies instead on standard software to have real world annotations.
Receiver Hostname
HappyDeliver filters Authentication-Results headers by hostname to only trust headers added by your MTA (and not headers that may have been injected by the sender). By default, it uses the system hostname (os.Hostname()).
If your MTA's authserv-id (the hostname at the beginning of Authentication-Results headers) differs from the machine running HappyDeliver, you must set it explicitly:
./happyDeliver server -receiver-hostname mail.example.com
Or via environment variable:
HAPPYDELIVER_RECEIVER_HOSTNAME=mail.example.com ./happyDeliver server
How to find the correct value: look at the Authentication-Results headers in a received email. They start with the authserv-id, e.g. Authentication-Results: mail.example.com; spf=pass ... — in this case, use mail.example.com.
If the value is misconfigured, HappyDeliver will log a warning when the last Received hop doesn't match the expected hostname.
This setting only applies to messages delivered to HappyDeliver. For an uploaded .eml file the authserv-id cannot be known in advance, so it is detected from the file itself: the topmost Authentication-Results header wins, as it was written by the last server that handled the message. Both the source and the authserv-id that was trusted are recorded in the report (source, authserv_id, authserv_ids_found).
Attachment Analysis
HappyDeliver reads the files a message carries alongside its body: file type mismatches (a .pdf that is really an executable, .pdf.exe double extensions), Office macros, PDF active content, scripts, nested and password-protected archives. Two optional external scanners improve detection:
- ClamAV: point HappyDeliver at a running
clamddaemon:
./happyDeliver server -clamav-address tcp://127.0.0.1:3310
# or a unix socket:
./happyDeliver server -clamav-address unix:///run/clamav/clamd.sock
With docker compose, start the bundled ClamAV service (requires ~1.5 GB RAM): docker compose --profile clamav up -d, then set HAPPYDELIVER_CLAMAV_ADDRESS=tcp://clamav:3310.
- VirusTotal: attachment SHA-256 hashes are looked up against the VirusTotal database (file content is not shared):
./happyDeliver server -virustotal-api-key YOUR_API_KEY
Add -virustotal-upload to also submit files unknown to VirusTotal for analysis. Warning: uploaded files are shared with the VirusTotal community; do not enable it if test emails may contain confidential documents. Note the free API tier is limited to 4 requests/minute.
Related options: -scan-timeout (default 30s) bounds the reading of one attachment, and -max-attachment-size (default 25 MiB) caps the size of the attachments whose content is analyzed. A file above that size is still named, sized and hashed in the report; only its content is left unread. When no scanner is configured, the checks HappyDeliver runs itself still apply and scanner verdicts are reported as disabled without affecting the score.
Domain Blacklist Sources
The domain check (/domain/) queries free blocklists (DNSBLs, Quad9, OISD, OpenPhish, Disconnect, PhishTank, …) out of the box. The following sources only run once the operator supplies a key:
| Flag | Sources |
|---|---|
| -blacklist-abusech-auth-key | abuse.ch URLhaus, ThreatFox and MalwareBazaar (free account at auth.abuse.ch) |
| -blacklist-otx-api-key | AlienVault OTX (free account) |
| -blacklist-pulsedive-api-key | Pulsedive (free account) |
| -blacklist-criminalip-api-key | Criminal IP (free tier: 100 requests/day) |
| -virustotal-api-key | VirusTotal (free tier: 4 requests/minute; shared with attachment scanning, see above) |
| -blacklist-safebrowsing-api-key | Google Safe Browsing |
As any other option, each can be given through the environment, e.g. HAPPYDELIVER_BLACKLIST_ABUSECH_AUTH_KEY.
Postfix LMTP Transport
You'll obtain the best results with a custom transport rule using LMTP.
- Start the HappyDeliver server with LMTP enabled (default listens on
127.0.0.1:2525):
./happyDeliver server
You can customize the LMTP address with the -lmtp-addr flag or in the config file.
- Create the file
/etc/postfix/transport_happydeliverwith the following content:
# Transport map - route test emails to happyDeliver LMTP server
# Pattern: test-<base32-uuid>@yourdomain.com -> LMTP on localhost:2525
/^test-[a-zA-Z2-7-]{26,30}@yourdomain\.com$/ lmtp:inet:127.0.0.1:2525
- Append the created file to
transport_mapsin yourmain.cf:
-transport_maps = texthash:/etc/postfix/transport
+transport_maps = texthash:/etc/postfix/transport, pcre:/etc/postfix/transport_happydeliver
If your transport_maps option is not set, just append this line:
transport_maps = pcre:/etc/postfix/transport_happydeliver
Note: to use the pcre: type, you need to have postfix-pcre installed.
- Reload Postfix configuration:
postfix reload
4. Create a Test
curl -X POST http://localhost:8080/api/test
Response:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "test-kfauqaao-ukj2if3n-fgrfkiafaa@localhost",
"status": "pending",
"message": "Send your test email to the address above"
}
5. Send Test Email
Send a test email to the address provided (you'll need to configure your MTA to route emails to the analyzer - see MTA Integration below).
Alternatively, upload a message you already received elsewhere (no test address needed!), the report is produced immediately:
curl -F "[email protected]" http://localhost:8080/api/test/upload
Only what the file already carries can be reported: SPF, DKIM, DMARC, ARC, BIMI, reverse DNS, transport encryption and the spam filters' verdicts are all produced by the server that receives a message, so an uploaded file only has them if that server wrote them. The report records "source": "uploaded" so a missing verdict is not mistaken for a misconfiguration of your instance.
The content, though, is in the file. With -rspamd-scan-url set to an rspamd normal worker (http://127.0.0.1:11333 in the bundled image), an uploaded message is submitted to it and what it observes about the content (hidden text, a linked image standing in for the whole message, an archive inside an archive, a link to a domain listed for phishing) is reported as advice in the content section, just as it is for a message received over SMTP. The bundled image sets it to its own rspamd, so this works out of the box; it is off by default for a binary run on its own.
The scan is deliberately envelope-less: no client IP, HELO or MAIL FROM is claimed, because an uploaded file has no SMTP connection behind it and reconstructing one from Received: headers would describe whichever relay wrote them rather than the sender under test. Everything rspamd derives from a connection is therefore meaningless in such a scan, so only the content observations are read out of it: the spam score of the report is not affected, and an uploaded message keeps whatever verdict its own headers carried. Note also that checking link reputation means DNS queries to public URI blocklists for every upload, from your instance's resolver.
Three settings control this endpoint:
| Flag | Environment variable | Default | Description |
|------|----------------------|---------|-------------|
| -disable-eml-upload | HAPPYDELIVER_DISABLE_EML_UPLOAD | enabled | Turn the upload endpoint off (it also hides the widget in the web UI) |
| -max-message-size | HAPPYDELIVER_MAX_MESSAGE_SIZE | 52428800 (50 MiB) | Maximum size in bytes of a message, matching the largest message Gmail accepts. It caps uploaded files, and is also announced as the SMTP SIZE limit by the LMTP receiver |
| -rspamd-scan-url | HAPPYDELIVER_RSPAMD_SCAN_URL | unset standalone; http://127.0.0.1:11333 in the image | rspamd normal worker to submit an uploaded message to, for what it can say about the content. Not the controller -rspamd-api-url names: the controller answers /checkv2 too, but behind its password. Unset leaves uploads unscanned |
Note that an uploaded message is stored and listed like any other report; disable the upload endpoint, or the public listing with -disable-test-list, if that does not suit your instance.
6. Get Report
curl http://localhost:8080/api/report/550e8400-e29b-41d4-a716-446655440000
API Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| /api/test | POST | Create a new deliverability test |
| /api/test/upload | POST | Analyze an uploaded .eml file (multipart, field file) |
| /api/test/{id} | GET | Get test metadata and status |
| /api/report/{id} | GET | Get detailed analysis report |
| /api/report/{id}/raw | GET | Get raw annotated email |
| /api/status | GET | Service health and status |
Email Analyzer (CLI Mode)
For manual testing or debugging, you can analyze emails from the command line:
cat email.eml | ./happyDeliver analyze
Or specify recipient explicitly:
cat email.eml | ./happyDeliver analyze -recipient [email protected]
For a message received by another server (an archive, a mail from your inbox), add -eml so the authserv-id is taken from the file instead of -receiver-hostname:
cat inbox-message.eml | ./happyDeliver analyze -eml -json
Note: In production, emails are delivered via LMTP (see integration instructions above).
Go Libraries
Beyond the server and CLI, HappyDeliver publishes some of its email checks as
standalone Go libraries under pkg/, so you can embed the same validation
logic in your own tools without running the whole platform. They have no
dependency on the server, its API or a database, and reach the network only
through interfaces you provide (so they are easy to test and to point at your
own resolver or HTTP client).
pkg/bimi: BIMI validation
Validate BIMI records and the assets they reference (the SVG Tiny Portable/Secure logo and the Verified Mark Certificate, or VMC), and get back, for each record, the detailed evidence explaining why it is valid or not (rather than a bare yes/no).
import "git.happydns.org/happyDeliver/pkg/bimi"
v := bimi.NewValidator()
rec, err := v.Analyze(ctx, "example.com", "default")
// rec.Valid, rec.Error, rec.Checks (per-asset evidence) and rec.VMC
You can also call the individual checks directly when you already hold the
inputs: ParseRecord (a record string), CheckLogoXML / CheckLogoSVGTinyPS
(an SVG document), or AnalyzeVMC (a PEM certificate chain).
More checks (SPF, DKIM, DMARC, ...) will be made available as libraries here over time.
Use with HappyDomain
HappyDeliver can be driven by HappyDomain through
the checker-happydeliver
plugin, so the deliverability of a domain you manage is monitored alongside
its DNS and inbound SMTP posture.
How it works:
- Attach the Outbound deliverability checker to the mail service of a zone
happydeliver_url;
operators can configure a default instance globally.
- On each run, the checker calls
POST /api/testto allocate a fresh
GET /api/test/{id} until the report is
ready.
- The structured report from
GET /api/report/{id}is translated into
min_score/warn_score).
- Runs repeat on a configurable interval so a regression in deliverability (a
See the checker-happydeliver repository
for build instructions and the full list of run options.
Scoring System
The deliverability score is calculated from A to F based on:
- DNS: Step-by-step analysis of PTR, Forward-Confirmed Reverse DNS, MX, SPF, DKIM, DMARC and BIMI records
- Authentication: IPRev, SPF, DKIM, DMARC, BIMI and ARC validation
- Blacklist: RBL/DNSBL checks
- Headers: Required headers, MIME structure, Domain alignment
- Spam: SpamAssassin and rspamd scores (combined 50/50)
- Content: HTML quality, links, images, unsubscribe
Funding
This project is funded through NGI Zero Core, a fund established by NLnet with financial support from the European Commission's Next Generation Internet program. Learn more at the NLnet project page.
License
GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later)
The datasets an email is read against, under pkg/emaildata/, are third-party
material used verbatim under their own licenses: the URL shortener domain list
under CC-BY-SA-4.0, the Can I email client support data under MIT. See
THIRD-PARTY-NOTICES.md.
