RepoPilotOpen in app →

axllent/mailpit

An email and SMTP testing tool with API for developers

Healthy

Healthy across all four use cases

weakest axis
Use as dependencyHealthy

Permissive license, no critical CVEs, actively maintained — safe to depend on.

Fork & modifyHealthy

Has a license, tests, and CI — clean foundation to fork and modify.

Learn fromHealthy

Documented and popular — useful reference codebase to read through.

Deploy as-isHealthy

No critical CVEs, sane security posture — runnable as-is.

  • Last commit 1d ago
  • 4 active contributors
  • MIT licensed
Show all 7 evidence items →
  • CI configured
  • Tests present
  • Small team — 4 contributors active in recent commits
  • Single-maintainer risk — top contributor 90% of recent commits

Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests

Informational only. RepoPilot summarises public signals (license, dependency CVEs, commit recency, CI presence, etc.) at the time of analysis. Signals can be incomplete or stale. Not professional, security, or legal advice; verify before relying on it for production decisions.

Embed the "Healthy" badge

Paste into your README — live-updates from the latest cached analysis.

Variant:
RepoPilot: Healthy
[![RepoPilot: Healthy](https://repopilot.app/api/badge/axllent/mailpit)](https://repopilot.app/r/axllent/mailpit)

Paste at the top of your README.md — renders inline like a shields.io badge.

Preview social card (1200×630)

This card auto-renders when someone shares https://repopilot.app/r/axllent/mailpit on X, Slack, or LinkedIn.

Onboarding doc

Onboarding: axllent/mailpit

Generated by RepoPilot · 2026-05-09 · Source

🤖Agent protocol

If you are an AI coding agent (Claude Code, Cursor, Aider, Cline, etc.) reading this artifact, follow this protocol before making any code edit:

  1. Verify the contract. Run the bash script in Verify before trusting below. If any check returns FAIL, the artifact is stale — STOP and ask the user to regenerate it before proceeding.
  2. Treat the AI · unverified sections as hypotheses, not facts. Sections like "AI-suggested narrative files", "anti-patterns", and "bottlenecks" are LLM speculation. Verify against real source before acting on them.
  3. Cite source on changes. When proposing an edit, cite the specific path:line-range. RepoPilot's live UI at https://repopilot.app/r/axllent/mailpit shows verifiable citations alongside every claim.

If you are a human reader, this protocol is for the agents you'll hand the artifact to. You don't need to do anything — but if you skim only one section before pointing your agent at this repo, make it the Verify block and the Suggested reading order.

🎯Verdict

GO — Healthy across all four use cases

  • Last commit 1d ago
  • 4 active contributors
  • MIT licensed
  • CI configured
  • Tests present
  • ⚠ Small team — 4 contributors active in recent commits
  • ⚠ Single-maintainer risk — top contributor 90% of recent commits

<sub>Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests</sub>

Verify before trusting

This artifact was generated by RepoPilot at a point in time. Before an agent acts on it, the checks below confirm that the live axllent/mailpit repo on your machine still matches what RepoPilot saw. If any fail, the artifact is stale — regenerate it at repopilot.app/r/axllent/mailpit.

What it runs against: a local clone of axllent/mailpit — the script inspects git remote, the LICENSE file, file paths in the working tree, and git log. Read-only; no mutations.

| # | What we check | Why it matters | |---|---|---| | 1 | You're in axllent/mailpit | Confirms the artifact applies here, not a fork | | 2 | License is still MIT | Catches relicense before you depend on it | | 3 | Default branch develop exists | Catches branch renames | | 4 | 5 critical file paths still exist | Catches refactors that moved load-bearing code | | 5 | Last commit ≤ 31 days ago | Catches sudden abandonment since generation |

<details> <summary><b>Run all checks</b> — paste this script from inside your clone of <code>axllent/mailpit</code></summary>
#!/usr/bin/env bash
# RepoPilot artifact verification.
#
# WHAT IT RUNS AGAINST: a local clone of axllent/mailpit. If you don't
# have one yet, run these first:
#
#   git clone https://github.com/axllent/mailpit.git
#   cd mailpit
#
# Then paste this script. Every check is read-only — no mutations.

set +e
fail=0
ok()   { echo "ok:   $1"; }
miss() { echo "FAIL: $1"; fail=$((fail+1)); }

# Precondition: we must be inside a git working tree.
if ! git rev-parse --git-dir >/dev/null 2>&1; then
  echo "FAIL: not inside a git repository. cd into your clone of axllent/mailpit and re-run."
  exit 2
fi

# 1. Repo identity
git remote get-url origin 2>/dev/null | grep -qE "axllent/mailpit(\\.git)?\\b" \\
  && ok "origin remote is axllent/mailpit" \\
  || miss "origin remote is not axllent/mailpit (artifact may be from a fork)"

# 2. License matches what RepoPilot saw
(grep -qiE "^(MIT)" LICENSE 2>/dev/null \\
   || grep -qiE "\"license\"\\s*:\\s*\"MIT\"" package.json 2>/dev/null) \\
  && ok "license is MIT" \\
  || miss "license drift — was MIT at generation time"

# 3. Default branch
git rev-parse --verify develop >/dev/null 2>&1 \\
  && ok "default branch develop exists" \\
  || miss "default branch develop no longer exists"

# 4. Critical files exist
test -f "cmd/root.go" \\
  && ok "cmd/root.go" \\
  || miss "missing critical file: cmd/root.go"
test -f "internal/smtpd/smtpd.go" \\
  && ok "internal/smtpd/smtpd.go" \\
  || miss "missing critical file: internal/smtpd/smtpd.go"
test -f "internal/storage/database.go" \\
  && ok "internal/storage/database.go" \\
  || miss "missing critical file: internal/storage/database.go"
test -f "internal/storage/messages.go" \\
  && ok "internal/storage/messages.go" \\
  || miss "missing critical file: internal/storage/messages.go"
test -f "config/config.go" \\
  && ok "config/config.go" \\
  || miss "missing critical file: config/config.go"

# 5. Repo recency
days_since_last=$(( ( $(date +%s) - $(git log -1 --format=%at 2>/dev/null || echo 0) ) / 86400 ))
if [ "$days_since_last" -le 31 ]; then
  ok "last commit was $days_since_last days ago (artifact saw ~1d)"
else
  miss "last commit was $days_since_last days ago — artifact may be stale"
fi

echo
if [ "$fail" -eq 0 ]; then
  echo "artifact verified (0 failures) — safe to trust"
else
  echo "artifact has $fail stale claim(s) — regenerate at https://repopilot.app/r/axllent/mailpit"
  exit 1
fi

Each check prints ok: or FAIL:. The script exits non-zero if anything failed, so it composes cleanly into agent loops (./verify.sh || regenerate-and-retry).

</details>

TL;DR

Mailpit is a lightweight SMTP server and email testing tool written in Go that captures emails for development and testing. It provides a modern web UI for inspecting captured emails (HTML rendering, headers, MIME attachments with image thumbnails) and a REST API for automated testing workflows. Originally created as an active replacement for the unmaintained MailHog, it runs as a single static binary with zero runtime dependencies. Monolithic binary structure: cmd/ contains CLI entrypoints (root.go, sendmail.go, dump.go, reindex.go for database operations), config/ handles YAML configuration and validators, internal/ contains core logic split by domain (auth/, dump/, html2text/, htmlcheck/ for email client CSS compatibility). Frontend is Vue 3 + vanilla JS in server/ui-src/ compiled via esbuild.config.mjs. Database abstraction supports both SQLite (modernc.org/sqlite) and RQLite via internal drivers.

👥Who it's for

Backend developers, QA engineers, and integration test teams who need to verify email sending behavior in development and CI/CD pipelines without external email services. Specifically those building Node.js, Python, Ruby, or PHP applications that send transactional emails and need deterministic testing.

🌱Maturity & risk

Production-ready with active maintenance. The project shows strong engineering discipline: comprehensive CI/CD pipelines (GitHub Actions for tests, Docker builds, CodeQL scanning), modern Go 1.25.0 with pinned dependencies, and a clear Docker distribution strategy. Single-maintainer (@axllent) but well-organized with security policy (.github/SECURITY.md) and structured releases via changelog automation.

Low risk. Dependency surface is lean (~20 direct Go dependencies, mostly database/SMTP/crypto libraries) with no bloated frameworks. Last commit age not visible in provided data but CHANGELOG and automated release workflows suggest active upkeep. Single maintainer is typical for this class of tool; mitigated by MIT licensing and straightforward codebase. No known breaking changes pattern visible.

Active areas of work

Active release pipeline: build-release.yml workflow suggests frequent tagged releases with cliff.toml automating changelog generation. CI matrix includes tests.yml (main tests) and tests-rqlite.yml (distributed DB variant). Recent dependency updates visible (gorqlite, golang.org packages pinned to specific versions). Stale issue automation suggests active triage.

🚀Get running

git clone https://github.com/axllent/mailpit.git
cd mailpit
go mod download
go run ./cmd root.go

Default SMTP listens on 0.0.0.0:1025, web UI on http://localhost:8025. No external DB required (SQLite embedded by default).

Daily commands:

go build -o mailpit ./cmd && ./mailpit

Web UI available at http://localhost:8025 (configurable via --ui-bind and --ui-port flags). SMTP server defaults to localhost:1025. For frontend development: requires Node.js for esbuild compilation (esbuild.config.mjs handles Vue → JS transpilation).

🗺️Map of the codebase

  • cmd/root.go — Application entry point and CLI command configuration—all new features must be wired through Cobra commands here
  • internal/smtpd/smtpd.go — Core SMTP server implementation that receives and processes all incoming emails; understanding this is essential for mail flow
  • internal/storage/database.go — Database abstraction layer managing SQLite/RQLite connections; all persistent data operations depend on this
  • internal/storage/messages.go — Message storage and retrieval logic—critical for email persistence and indexing workflows
  • config/config.go — Configuration schema and validation; defines all environment variables, flags, and settings used throughout the app
  • internal/logger/logger.go — Centralized logging system used across all internal packages for observability and debugging
  • internal/pop3/server.go — POP3 server implementation; required for email client integration alongside SMTP

🛠️How to make changes

Add a new CLI command

  1. Create a new command file in cmd/ directory (e.g., cmd/mycommand.go) with a NewCommand() function (cmd/mycommand.go)
  2. Register the command in the root command's init() function by adding rootCmd.AddCommand(NewCommand()) (cmd/root.go)
  3. Use config.Get() to access application configuration and log.Log for structured logging (config/config.go)

Add email processing logic (spam/link/HTML checks)

  1. Create a new package under internal/ (e.g., internal/mycheck/) with your analysis logic (internal/htmlcheck/caniemail.go)
  2. Hook into the message ingestion pipeline in internal/storage/messages.go where emails are stored (internal/storage/messages.go)
  3. Store results in the database via new columns or tables defined in internal/storage/schemas/ (internal/storage/schemas.go)
  4. Expose results via REST API endpoints (typically in server code, not shown in file list)

Add SMTP/POP3 protocol feature or constraint

  1. Modify the relevant server in internal/smtpd/smtpd.go for SMTP or internal/pop3/server.go for POP3 (internal/smtpd/smtpd.go)
  2. Update configuration validation in config/validators.go if new config flags are needed (config/validators.go)
  3. Add tests in internal/smtpd/smtpd_test.go or internal/pop3/pop3_test.go (internal/smtpd/smtpd_test.go)

Add a database query or message property

  1. Define the schema change in a new migration file under internal/storage/schemas/ (e.g., 1.25.0.sql) (internal/storage/schemas.go)
  2. Add getter/setter functions in internal/storage/messages.go using leporo/sqlf query builder (internal/storage/messages.go)
  3. Update the Message struct in internal/storage/structs.go to include the new field (internal/storage/structs.go)

🔧Why these technologies

  • Go 1.25 — Provides fast, single-binary deployment with built-in concurrency primitives ideal for a multi-protocol server
  • SQLite / RQLite — SQLite for local testing, RQLite for distributed replication without external dependencies; single-file or replicated schema
  • Cobra CLI framework — Standard Go CLI library enabling sub-commands, flags, and POSIX compliance for sendmail compatibility
  • jhillyerd/enmime — RFC-compliant email parsing library handling complex MIME structures, attachments, and encoding
  • Gorilla WebSocket — Persistent bi-directional communication for real-time UI updates (new messages, status changes)

⚖️Trade-offs already made

  • Single-binary, embedded database vs. microservices + external DB

    • Why: Simpler deployment and zero external dependencies for developers; testing tool scope doesn't require HA
    • Consequence: Limited horizontal scalability; database is co-located with SMTP/POP3 servers; suitable for dev/staging environments only
  • Optional POP3 server alongside SMTP

    • Why: Allows testing with standard mail clients (Thunderbird, Outlook) without custom tooling
    • Consequence: Increases code complexity and maintenance surface; some email client workflows may not be fully testable
  • Pluggable spam/link analysis (SpamAssassin, custom checks)

    • Why: Reuse mature spam detection; extensible for custom rules
    • Consequence: External process dependency (spamd) for SpamAssassin; adds latency (~500ms) and operational overhead
  • Full-text search via database (no Elasticsearch/Solr)

    • Why: Single-binary, no external services required
    • Consequence: Search performance degrades on very large mailboxes; indexes stored in same DB as messages

🚫Non-goals (don't propose these)

  • Does not provide end-to-end encryption (TLS in transit only)
  • Does not handle calendar/contact sync or CalDAV/CardDAV protocols
  • Does not persist state across container restarts without explicit volume mount (ephemeral by default)
  • Does not implement OAuth/SAML—only basic auth (htpasswd) and API tokens

🪤Traps & gotchas

Environment variables: None required for basic operation; all config via CLI flags or config.yaml. Database: SQLite default requires no setup; RQLite mode requires external RQLite cluster (tests.yml has separate tests-rqlite.yml pipeline). Frontend build: Must run esbuild before serving; no auto-rebuild in development visible in repo—requires manual recompilation (see esbuild.config.mjs for bundle entry points: main.js, landing.js). SMTP: Default port 1025 may conflict on some systems; set via --smtp-bind. UI authentication: htpasswd file format strictly enforced via go-htpasswd validator; malformed files will silently fail auth checks. Memory: No explicit memory limits in codebase; embedded SQLite can consume significant RAM on very large email stores (consider RQLite for scale).

🏗️Architecture

💡Concepts to learn

  • SMTP Server Implementation — Understanding SMTP protocol handling is essential for modifying email acceptance logic, authentication mechanisms, or TLS/STARTTLS configuration in Mailpit's server stack
  • MIME Email Structure — Mailpit parses and renders MIME multipart messages (attachments, inline images, text/HTML alternatives); knowledge of MIME structure is critical for debugging email rendering or adding attachment features
  • CSS Email Client Compatibility (Can I Email?) — The htmlcheck/ package validates email HTML against caniemail-data.json; understanding client-specific CSS support is core to Mailpit's email preview feature
  • WebSocket Real-time Updates — Gorilla WebSocket integration enables live email list refresh in the web UI without polling; understanding bidirectional messaging is key for modifying UI synchronization
  • SQLite vs RQLite Trade-off — Mailpit supports embedded SQLite (default, single-instance) and RQLite (distributed raft-based consensus); choosing the right backend affects clustering capabilities and persistence strategy
  • HTPasswd Authentication — HTTP and SMTP auth can use htpasswd files (via tg123/go-htpasswd); understanding bcrypt/plaintext password storage in this format is essential for implementing custom auth backends
  • Vue 3 Single File Components (SFCs) — Frontend code uses .vue files with template/script/style blocks compiled by esbuild; understanding SFC structure is necessary for any UI modifications or feature additions
  • mailhog/MailHog — Direct predecessor that Mailpit was inspired by and actively replaces; same use case but unmaintained since ~2021
  • inbucket/inbucket — Alternative Go-based SMTP testing server with web UI; similar feature set but different codebase architecture and maintenance status
  • haraka/Haraka — Node.js SMTP server framework used for email testing; more extensible plugin model but heavier footprint than Mailpit
  • axllent/semver — Semantic versioning library by same author; used internally in Mailpit for version comparison and upgrade checks
  • jhillyerd/enmime — Go MIME email parser library used by Mailpit for message parsing; direct upstream dependency for email processing

🪄PR ideas

To work on one of these in Claude Code or Cursor, paste: Implement the "<title>" PR idea from CLAUDE.md, working through the checklist as the task list.

Add integration tests for POP3 server functionality

The repo has internal/pop3/pop3_test.go but only tests the POP3 parser. There are no integration tests verifying the full POP3 server lifecycle (connection, authentication, message retrieval). Given that internal/pop3/server.go implements a complete POP3 server, adding end-to-end tests would catch regressions and validate the server works correctly with real clients.

  • [ ] Create internal/pop3/server_test.go with integration tests for POP3 server startup and client connections
  • [ ] Test POP3 authentication flow using internal/auth/auth.go credentials
  • [ ] Test message listing and retrieval commands against the running server
  • [ ] Verify proper error handling for malformed commands and invalid credentials

Add link checker validation tests and benchmarks

internal/linkcheck/main.go implements link validation logic but internal/linkcheck/linkcheck_test.go appears incomplete (only has status tests). The link checker is a user-facing feature that needs comprehensive coverage for different URL types, edge cases, and performance characteristics to prevent regressions.

  • [ ] Expand internal/linkcheck/linkcheck_test.go with test cases for various URL formats (https, mailto, relative URLs, fragments)
  • [ ] Add tests for timeout handling and concurrent link checking
  • [ ] Add benchmark tests (BenchmarkCheckLink) to ensure link checking performance remains acceptable
  • [ ] Test integration with the email message parsing to validate real email link extraction

Add HTML compatibility checker tests with CanIEmail data validation

internal/htmlcheck/caniemail.go loads caniemail-data.json but internal/htmlcheck/inline_test.go only has minimal test coverage. The HTML compatibility checker is critical for the email testing use case, and the CanIEmail data should be validated to ensure CSS support detection accuracy.

  • [ ] Add tests in internal/htmlcheck/caniemail_test.go validating that caniemail-data.json loads correctly and has expected structure
  • [ ] Test CSS property support detection (e.g., verify known-good and known-bad CSS properties return correct compatibility)
  • [ ] Add tests for different email platform data in internal/htmlcheck/platforms.go
  • [ ] Expand internal/htmlcheck/inline_test.go with realistic HTML email samples and their expected compatibility results

🌿Good first issues

  • Add unit tests for config/validators.go: File exists but no _test.go companion visible in file list. Could validate SMTP address parsing, port range checks, and htpasswd path existence before startup.
  • Implement search result export feature: API exists (REST endpoints visible in go.mod and file structure hints config options), but no obvious JSON/CSV export endpoint. Could add POST /api/v1/search/export to return matching emails in structured format.
  • Add DKIM/SPF header validation in htmlcheck: caniemail-data.json has client compatibility data, but no DKIM signature validation visible. Could extend internal/htmlcheck to parse and highlight common email authentication headers.

Top contributors

Click to expand

📝Recent commits

Click to expand
  • 8bc966e — Chore: Refactor Prometheus metrics implementation and remove unused dependencies (axllent)
  • ec2a085 — Build: Update CI actions to use npm ci (axllent)
  • 4bdbeeb — Chore: Bump axios version to v1.16.0 (axllent)
  • 10430f7 — Chore: Improve iframe height adjustment with optional chaining (axllent)
  • 878c68b — Chore: Replace lithammer/shortuuid with custom shortuuid implementation and update tests (axllent)
  • 86b0cf8 — Chore: Remove go-telnet dependency and implement TCP/Unix socket handling for SMTP (axllent)
  • 123ec9a — Chore: Remove logrus dependency and implement slog-based logging (axllent)
  • 3b2423b — Chore: Remove gorilla/mux dependency and replace with stdlib routing (axllent)
  • f0777c7 — Release v1.29.7 (axllent)
  • 91a4b81 — Chore: Update node dependencies (axllent)

🔒Security observations

  • High · Insecure Deserialization in YAML Processing — config/config.go, go.mod (goccy/go-yaml v1.19.2). The codebase uses 'github.com/goccy/go-yaml' for YAML parsing. If user-controlled YAML data is parsed without proper validation, it could lead to arbitrary code execution or object injection attacks, particularly if the YAML parser supports unsafe deserialization patterns. Fix: Implement strict YAML schema validation. Use SafeLoad or equivalent safe parsing modes. Validate and sanitize all user-supplied YAML input before parsing. Consider using a schema validator library.
  • High · SQL Injection Risk in Dynamic Query Building — internal/storage/database.go, internal/storage/functions_test.go. The codebase uses 'github.com/leporo/sqlf' for SQL query construction. While this library provides parameterized queries, the presence of direct SQL query files and dynamic query building in storage/database.go suggests potential for SQL injection if sqlf is not used consistently throughout all database operations. Fix: Audit all database operations to ensure sqlf parameterized queries are used consistently. Never concatenate user input directly into SQL queries. Use prepared statements for all database operations. Implement input validation and sanitization.
  • High · Potential HTML/XSS Injection in Email Display — internal/html2text/html2text.go, internal/htmlcheck/html.go, internal/dump/dump.go. The application processes and displays email content including HTML. The presence of html2text, htmlcheck, and markdown processing modules suggests HTML content is being parsed and potentially displayed to users. Without proper sanitization, malicious HTML or JavaScript in emails could execute in user browsers. Fix: Implement strict Content Security Policy (CSP) headers. Sanitize all HTML content before displaying using libraries like 'bluemonday' or 'sanitize'. Use HTML templating with auto-escaping. Avoid using dangerouslySetInnerHTML or equivalent unsafe rendering methods in the frontend.
  • Medium · Weak Cryptography - Snakeoil Certificate Generation — internal/snakeoil/snakeoil.go. The codebase includes 'internal/snakeoil' module for generating self-signed certificates. While acceptable for testing, if these certificates are used in production or for any security-critical operations, it represents a significant weakness. Fix: Ensure snakeoil certificates are only used in development/testing environments. For production, enforce proper TLS certificates from trusted CAs. Add environment-based checks to prevent snakeoil usage in production.
  • Medium · Exposed Debug/Test Endpoints Without Authentication — Dockerfile (EXPOSE 1025/tcp 1110/tcp 8025/tcp), internal/smtpd/main.go, internal/pop3/server.go. The application exposes multiple testing and debugging endpoints (SMTP on 1025, POP3 on 1110, HTTP on 8025). These are meant for testing but could be exploited if deployed without proper network segmentation. No evidence of authentication on the HTTP endpoints visible. Fix: Restrict network access to these ports using firewalls or network policies. Run behind a reverse proxy with authentication. Implement API authentication on all HTTP endpoints. Document that this tool is for testing only and should never be exposed publicly without proper security controls.
  • Medium · Missing Security Headers Configuration — Web server configuration (likely in cmd/root.go or internal storage). The codebase serves a web UI but there's no visible evidence of security header configuration (X-Frame-Options, X-Content-Type-Options, Strict-Transport-Security, etc.) in the provided file structure. Fix: Implement comprehensive security headers: X-Frame-Options: DENY, X-Content-Type-Options: nosniff, X-XSS-Protection, Strict-Transport-Security, Content-Security-Policy. Use middleware to enforce these on all responses.
  • Medium · Authentication Implementation Using htpasswd — internal/auth/auth.go, go.mod (tg123/go-htpasswd v1.2.4). The codebase uses 'github.com/tg123/go-htpasswd' for authentication. While functional, htpasswd-based auth stores passwords in files which could be vulnerable if file permissions are misconfigured or files are exposed. Fix: undefined

LLM-derived; treat as a starting point, not a security audit.


Generated by RepoPilot. Verdict based on maintenance signals — see the live page for receipts. Re-run on a new commit to refresh.

Healthy signals · axllent/mailpit — RepoPilot