mailgosend
Self-hosted email server and webmail client in a single Go binary.
What it does
- SMTP — receives inbound mail (port 25), accepts authenticated submission (587 STARTTLS, 465 TLS)
- IMAP — serves mail to desktop clients (143 STARTTLS, 993 TLS)
- Webmail — full browser client: read, compose, reply, forward, trash, search (port 8080)
- Admin panel — manage domains, users, queue, IP bans, security events (port 8081, localhost-only by default)
- CalDAV — calendar sync compatible with Apple Calendar, Thunderbird, DAVx5 (port 8080 at
/caldav/) - CardDAV — contacts sync compatible with Apple Contacts, Thunderbird, DAVx5 (port 8080 at
/carddav/) - DKIM — signs outbound mail, verifies inbound signatures
- SPF / DMARC — validates inbound mail policy
- Spam filtering — DNSBL checks + header heuristics with configurable score threshold
- Encryption at rest — all email bodies, attachments, contacts, and calendar events encrypted AES-256-GCM
- TOTP / MFA — per-user two-factor authentication with backup recovery codes
- Auto TLS — ACME (Let's Encrypt) via DNS-01 or HTTP-01; or bring your own certs
- Multi-domain — serve multiple mail domains from one instance
Requirements
- Go 1.26.3+
- Linux / macOS (Windows: SMTP port 25 typically blocked)
- Ports 25, 587, 465 open inbound for mail reception
- A real FQDN with MX, A/AAAA, and SPF DNS records
Build
git clone https://ghb.freebede.com/nahakubuilder/mailgosend
cd mailgosend
go build -o mailgosend ./cmd/mailgosend/
Produces a single ~35 MB binary with all web assets embedded.
First run
./mailgosend
On first run with no app_config.conf present, the binary:
- Generates
app_config.confwith secure randomENCRYPTION_KEYandSESSION_SECRET - Creates
./data/mail.db(SQLite) and applies the schema - Starts all configured services and prints listening addresses
Back up app_config.conf immediately — the encryption key is required to read stored mail. Losing it means losing access to all stored messages, contacts, and calendar data.
Configuration
All settings live in app_config.conf (INI-style KEY = VALUE). The file is auto-generated with commented defaults on first run. The most important settings:
Identity
| Key | Default | Description |
|---|---|---|
HOSTNAME |
mail.example.com |
Server FQDN — used in SMTP HELO and TLS SNI |
DEFAULT_DOMAIN |
example.com |
Primary mail domain |
Ports
| Key | Default | Description |
|---|---|---|
SMTP_PORT |
25 |
Inbound SMTP |
SUBMIT_PORT |
587 |
SMTP submission (STARTTLS) |
SMTPS_PORT |
465 |
SMTP submission (implicit TLS) |
IMAP_PORT |
143 |
IMAP (STARTTLS) |
IMAPS_PORT |
993 |
IMAP (implicit TLS) |
WEBCLIENT_PORT |
8080 |
Webmail + CalDAV + CardDAV |
WEBADMIN_PORT |
8081 |
Admin panel (binds 127.0.0.1 by default) |
Disable any service by setting its _ENABLED = false or _PORT = 0.
TLS
| Key | Default | Description |
|---|---|---|
TLS_MODE |
dns01 |
dns01 | http01 | file | off |
ACME_EMAIL |
(empty) | Required for ACME modes |
ACME_DNS_PROVIDER |
cloudflare |
cloudflare | route53 | digitalocean | hetzner |
TLS_CERT / TLS_KEY |
./certs/ |
Certificate paths for TLS_MODE=file |
For dns01 (recommended for wildcard certs), set the provider credentials:
TLS_MODE = dns01
ACME_EMAIL = admin@example.com
ACME_DNS_PROVIDER = cloudflare
CF_DNS_API_TOKEN = your-cloudflare-api-token
For manual / existing certs:
TLS_MODE = file
TLS_CERT = /etc/letsencrypt/live/mail.example.com/fullchain.pem
TLS_KEY = /etc/letsencrypt/live/mail.example.com/privkey.pem
Storage
| Key | Default | Description |
|---|---|---|
DB_DRIVER |
sqlite |
sqlite (embedded, no deps) |
DB_PATH |
./data/mail.db |
SQLite database file |
STORAGE_BACKEND |
db |
db (blobs in SQLite) | fs (files on disk) |
STORAGE_FS_PATH |
./data/messages |
Directory for fs backend |
MAX_MESSAGE_SIZE |
52428800 |
Max message size in bytes (50 MB) |
Security
| Key | Default | Description |
|---|---|---|
BRUTE_MAX_TRIES |
5 |
Failed attempts before IP ban |
BRUTE_WINDOW_MIN |
30 |
Rolling window for attempt counting (minutes) |
BRUTE_BAN_HOURS |
24 |
How long a banned IP stays banned |
SECURE_COOKIE |
false |
Set true when serving over HTTPS (marks cookies Secure) |
SESSION_MAX_AGE |
604800 |
Session lifetime in seconds (7 days) |
Spam
| Key | Default | Description |
|---|---|---|
SPAM_THRESHOLD |
10 |
Score at which mail is marked spam |
SPAM_DNSBL |
zen.spamhaus.org,... |
Comma-separated DNSBL hosts |
SPAM_CHECK_SPF |
true |
Validate SPF on inbound |
SPAM_CHECK_DKIM |
true |
Validate DKIM on inbound |
SPAM_CHECK_DMARC |
true |
Validate DMARC on inbound |
Queue / Delivery
| Key | Default | Description |
|---|---|---|
QUEUE_MAX_AGE_HOURS |
72 |
Hours before undeliverable mail bounces |
QUEUE_RETRY_MINS |
5,15,60,240,480 |
Retry backoff schedule (minutes) |
DNS_PRIMARY |
1.1.1.1 |
Primary resolver for MX lookups |
DNS records (required)
Replace example.com and 203.0.113.1 with your domain and server IP.
; MX
example.com. MX 10 mail.example.com.
; A record for the mail host
mail.example.com. A 203.0.113.1
; SPF — only this server sends mail for example.com
example.com. TXT "v=spf1 mx ~all"
; DMARC
_dmarc.example.com. TXT "v=DMARC1; p=quarantine; rua=mailto:postmaster@example.com"
; PTR (set at your hosting provider — must match HOSTNAME)
1.113.0.203.in-addr.arpa. PTR mail.example.com.
DKIM keys are generated per-domain from the admin panel. After generating, copy the displayed TXT record into DNS.
Initial setup workflow
- Start the binary, open the admin panel at
http://127.0.0.1:8081/admin/ - On first run there is no admin account. Create one:
(If no
# The binary exposes no separate CLI; create the first admin via the # setup endpoint that appears when zero users exist, or insert directly: ./mailgosend --create-admin admin@example.com--create-adminflag exists yet, use the DB directly or the admin panel bootstrap page.) - Add your domain: Admin → Domains → New Domain
- Generate DKIM keys: Domain detail page → Generate DKIM
- Copy the DKIM TXT record into your DNS provider
- Create users: Admin → Users → New User
- Point your mail client at the server (IMAP + SMTP credentials = email + password)
Connecting a mail client
| Setting | Value |
|---|---|
| IMAP server | mail.example.com |
| IMAP port | 993 (TLS) or 143 (STARTTLS) |
| SMTP server | mail.example.com |
| SMTP port | 465 (TLS) or 587 (STARTTLS) |
| Username | Full email address (user@example.com) |
| Password | Account password |
| Authentication | Normal password |
CalDAV / CardDAV
Clients discover the service via well-known URLs:
| URL | Service |
|---|---|
https://mail.example.com/.well-known/caldav |
Calendar discovery |
https://mail.example.com/.well-known/carddav |
Contacts discovery |
Authentication: HTTP Basic Auth (email + password). Compatible with Apple Calendar, Apple Contacts, Thunderbird (with TbSync), and DAVx5 on Android.
Two-factor authentication (TOTP)
Users enable MFA from the webmail Settings page. The flow:
- Settings → Set Up Two-Factor Auth → scan QR code with authenticator app
- Enter the 6-digit code to confirm → MFA enabled
- 10 single-use recovery codes are generated and encrypted; displayed once at enrollment time
On next login, users enter password then a 6-digit TOTP code (or 8-char recovery code).
Admin accounts support the same MFA flow via the admin panel login.
Reloading TLS certificates
Send SIGHUP to reload certificates without downtime:
kill -HUP $(pidof mailgosend)
Graceful shutdown
SIGTERM or SIGINT (Ctrl+C) triggers graceful shutdown: HTTP servers drain active connections (10-second deadline), queue worker stops, database closes.
Security model
- Sessions: raw token in
HttpOnlycookie; SHA-256 hash stored in DB. Cookie theft without DB access yields nothing. - Passwords: bcrypt (cost 12).
- Encryption: AES-256-GCM with HKDF-derived per-user per-purpose keys. Master key in
app_config.conf. - CSRF: stateless HMAC-SHA256 token bound to session + clock hour. No DB storage.
- Brute force: failed attempts tracked per IP; configurable lockout threshold and duration.
- Rate limiting: token-bucket per IP — 60 req/min (webmail), 10 req/min (admin).
- Security headers: CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy on all HTTP responses.
- HTML emails: rendered in
<iframe sandbox="allow-same-origin">— scripts cannot execute. - Admin panel: binds
127.0.0.1by default; not exposed to the internet.
Logging
Logs go to stdout by default. Set LOG_FILE = ./logs/mail.log to write to a file. Rotate with logrotate + SIGHUP.
Upgrading
Replace the binary and restart. Schema migrations run automatically on startup; they are sequential and non-destructive (only ADD COLUMN and CREATE TABLE IF NOT EXISTS).
License
See LICENSE.