Files
mailgosend/PLAN.md
T
2026-05-21 20:27:58 +00:00

32 KiB

mailgosend — Full-Stack Go Email Server: Project Plan

Go 1.26.3 · Single binary · Dark TailwindCSS UI · Security-first


1. Goal

Self-hosted email server + webmail client in one executable. Combines and supersedes gomta (outbound MTA) and gowebmail (IMAP webclient).

What it runs

Service Default port Protocol
SMTP inbound (MTA) 25 SMTP plain + STARTTLS
SMTP submission 587 SMTP + mandatory STARTTLS
SMTPS submission 465 SMTP implicit TLS
IMAP 143 IMAP + STARTTLS
IMAPS 993 IMAP implicit TLS
Web client 8080 HTTP (or TLS)
Web admin 8081 HTTP (or TLS)
CalDAV / CardDAV 5232 HTTP (or TLS)

All ports, bind IPs, and TLS settings configurable in app_config.conf.


2. Design Constraints

  • Go stdlib only — except packages listed in §4.
  • Zero CGO — all deps must be pure Go.
  • Single executable — all web assets embedded with go:embed.
  • Config auto-genapp_config.conf created on first run with secure random secrets.
  • Encryption at rest — all email bodies, attachments, contacts, calendar events encrypted AES-256-GCM.
  • Input = malicious — validate/sanitize every boundary.
  • No global mutable state.
  • context.Context timeouts on all I/O.
  • No frameworksnet/http only for HTTP.
  • TailwindCSS via CDN, vanilla JS, dark theme.

3. Directory Structure

mailgosend/
├── cmd/
│   └── mailgosend/
│       └── main.go               # Entrypoint — starts all servers, graceful shutdown
├── internal/
│   ├── config/
│   │   └── config.go             # app_config.conf load/autogen/validate
│   ├── db/
│   │   ├── db.go                 # database/sql wrapper, multi-driver support
│   │   ├── migrate.go            # Schema migrations (sequential versioned)
│   │   └── queries/              # Named SQL queries per domain
│   ├── crypto/
│   │   └── crypto.go             # AES-256-GCM encrypt/decrypt, key derivation, bcrypt
│   ├── auth/
│   │   ├── session.go            # Session store (DB-backed, signed tokens)
│   │   ├── brute.go              # Rate limiting + IP ban
│   │   └── totp.go               # TOTP / MFA (stdlib crypto)
│   ├── tls/
│   │   ├── tls.go                # TLS config builder, cert reload (SIGHUP), SNI routing
│   │   ├── acme.go               # ACME client: HTTP-01 + DNS-01 (lego), cert renewal loop
│   │   └── providers/            # DNS provider configs (Cloudflare, Route53, etc.)
│   ├── smtp/
│   │   ├── inbound.go            # Port 25 — receive mail for local domains
│   │   ├── submission.go         # Port 587/465 — authenticated send
│   │   ├── session.go            # SMTP session FSM (shared)
│   │   └── queue.go              # Delivery queue + retry scheduler
│   ├── imap/
│   │   ├── server.go             # IMAP server (go-imap/v2 backend interface)
│   │   └── backend.go            # Backend: maps IMAP ops to DB/storage layer
│   ├── delivery/
│   │   └── delivery.go           # Outbound MTA: DNS MX lookup, TLS dial, queue retry
│   ├── dkim/
│   │   └── dkim.go               # DKIM sign (outbound) + verify (inbound) — stdlib crypto
│   ├── spf/
│   │   └── spf.go                # SPF DNS lookup + policy check (stdlib net)
│   ├── dmarc/
│   │   └── dmarc.go              # DMARC policy fetch + alignment check
│   ├── spam/
│   │   └── spam.go               # Spam scorer: DNSBL, header heuristics, score threshold
│   ├── storage/
│   │   └── storage.go            # Message storage: DB metadata + encrypted body (file or DB blob)
│   ├── caldav/
│   │   ├── server.go             # CalDAV HTTP handler (go-webdav backend)
│   │   └── backend.go            # iCalendar storage backend
│   ├── carddav/
│   │   ├── server.go             # CardDAV HTTP handler
│   │   └── backend.go            # vCard storage backend
│   ├── models/
│   │   └── models.go             # Shared structs: User, Domain, Message, Folder, etc.
│   ├── middleware/
│   │   └── middleware.go         # Auth check, CSRF, rate limit, host check, logging
│   ├── webadmin/
│   │   ├── handlers.go           # Admin panel route handlers
│   │   └── api.go                # Admin JSON API
│   └── webclient/
│       ├── handlers.go           # Webmail route handlers
│       ├── compose.go            # Compose + send
│       └── api.go                # Client JSON API (messages, folders, contacts, calendar)
├── web/
│   ├── admin/
│   │   ├── templates/            # Admin HTML templates
│   │   └── static/               # Admin CSS/JS
│   └── client/
│       ├── templates/            # Webmail HTML templates
│       └── static/               # Client CSS/JS
├── webfs.go                      # go:embed declarations
├── go.mod
├── go.sum
├── app_config.conf               # Auto-generated, never committed
├── .gitignore
└── PLAN.md                       # This file

4. Third-Party Dependencies

Only where stdlib has no equivalent. All actively maintained.

Package Why Stars / Maintainer
modernc.org/sqlite Pure-Go SQLite, no CGO ~2k / cznic
golang.org/x/crypto bcrypt (passwords), low-level ACME protocol golang.org
golang.org/x/text Charset decoding (email MIME) golang.org
golang.org/x/oauth2 Google + Microsoft OAuth2 for external accounts golang.org
github.com/go-acme/lego/v4 ACME client: HTTP-01 and DNS-01 challenges, wildcard certs, 90+ DNS providers (Cloudflare, Route53, etc.) ~8k / go-acme
github.com/emersion/go-imap/v2 IMAP server + client protocol (RFC 3501 is 300+ pages) ~2k / Simon Ser
github.com/emersion/go-smtp SMTP protocol library (RFC 5321 state machine) ~800 / Simon Ser
github.com/emersion/go-message Full MIME email parsing (RFC 2045-2049) ~400 / Simon Ser
github.com/emersion/go-sasl SASL auth mechanisms (PLAIN, LOGIN, XOAUTH2) transitive
github.com/emersion/go-webdav CalDAV + CardDAV server framework (RFC 4791/6352) ~600 / Simon Ser

Optional DB drivers (user selects in config, only linked if needed via build tags):

Driver Package
PostgreSQL github.com/lib/pq
MySQL/MariaDB github.com/go-sql-driver/mysql
MSSQL github.com/microsoft/go-mssqldb

Note: mattn/go-sqlite3 (used in old projects) requires CGO — replaced by modernc.org/sqlite. github.com/gorilla/mux (used in gowebmail) dropped — net/http ServeMux (Go 1.22+) supports method+path routing.


5. Configuration (app_config.conf)

Auto-generated on first run if missing. INI-style KEY = value with comments. All secrets (encryption key, session secret) are random hex on first gen — never default insecure values.

Config sections

# --- Server Identity ---
HOSTNAME         = mail.example.com   # FQDN for SMTP HELO/EHLO, TLS SNI, URLs
DOMAIN_DEFAULT   = example.com        # Primary mail domain

# --- Network Ports & Interfaces ---
SMTP_IFACE       = 0.0.0.0
SMTP_PORT        = 25
SUBMISSION_IFACE = 0.0.0.0
SUBMISSION_PORT  = 587
SMTPS_PORT       = 465
IMAP_IFACE       = 0.0.0.0
IMAP_PORT        = 143
IMAPS_PORT       = 993
WEBCLIENT_IFACE  = 0.0.0.0
WEBCLIENT_PORT   = 8080
WEBADMIN_IFACE   = 127.0.0.1         # Admin on loopback by default
WEBADMIN_PORT    = 8081
CALDAV_IFACE     = 0.0.0.0
CALDAV_PORT      = 5232

# --- TLS ---
# TLS_MODE options:
#   dns01     = Let's Encrypt via DNS-01 challenge (no open ports, supports wildcards)
#               Requires ACME_DNS_PROVIDER + provider credentials below.
#               Works behind NAT/firewall. RECOMMENDED for self-hosted.
#   http01    = Let's Encrypt via HTTP-01 challenge (requires port 80 reachable from internet)
#               No DNS API needed. No wildcard support.
#   manual    = Provide TLS_CERT + TLS_KEY paths (certbot, acme.sh, self-signed, Vault, etc.)
#               SIGHUP reloads certs without restart.
#   off       = No TLS. Use ONLY behind a TLS-terminating reverse proxy.
TLS_MODE         = dns01

# ACME (Let's Encrypt) — used for dns01 and http01 modes
ACME_EMAIL       = admin@example.com  # Required for Let's Encrypt ToS
ACME_CACHE_DIR   = ./acme-cache       # Cache dir for certs, keys, account
ACME_STAGING     = false              # true = use Let's Encrypt staging (for testing)
ACME_DOMAINS     = example.com,*.example.com   # Domains to certify; blank = HOSTNAME only
                                               # Include wildcard for *.example.com coverage

# DNS-01 provider (required when TLS_MODE=dns01)
# Supported providers: cloudflare | route53 | digitalocean | dnsimple | godaddy |
#   namecheap | linode | hetzner | ovh | gandi | porkbun | desec | acmedns | + 80 more
#   Full list: https://go-acme.github.io/lego/dns/
ACME_DNS_PROVIDER = cloudflare

# Cloudflare credentials (ACME_DNS_PROVIDER=cloudflare)
# Option A — API Token (recommended, least privilege: Zone.DNS edit on target zones)
CF_DNS_API_TOKEN  =
# Option B — Global API Key
CF_API_KEY        =
CF_API_EMAIL      =

# AWS Route53 credentials (ACME_DNS_PROVIDER=route53)
AWS_REGION        =
AWS_ACCESS_KEY_ID =
AWS_SECRET_ACCESS_KEY =
AWS_HOSTED_ZONE_ID =      # Optional: skip auto-detection

# DigitalOcean (ACME_DNS_PROVIDER=digitalocean)
DO_AUTH_TOKEN     =

# Generic: any provider using environment variables — lego reads standard env vars
# for each provider. Set them here or export before starting mailgosend.
# See https://go-acme.github.io/lego/dns/<provider>/ for required vars.

# Manual cert paths (TLS_MODE=manual, or per-service override below)
TLS_CERT          = ./certs/cert.pem
TLS_KEY           = ./certs/key.pem

# Per-service cert override — leave blank to inherit global TLS_MODE
# Useful when using wildcard autocert globally but a specific cert for admin
SMTP_TLS_CERT     =
SMTP_TLS_KEY      =
IMAP_TLS_CERT     =
IMAP_TLS_KEY      =
WEB_TLS_CERT      =
WEB_TLS_KEY       =

# --- Secrets (auto-generated, BACK THESE UP) ---
ENCRYPTION_KEY   = <64 hex chars>     # AES-256 master key — email content at rest
SESSION_SECRET   = <64 hex chars>     # Session cookie signing
DKIM_SELECTOR    = mail               # Default DKIM selector

# --- Database ---
DB_DRIVER        = sqlite             # sqlite | postgres | mysql | mssql
DB_PATH          = ./data/mail.db     # SQLite path
DB_DSN           =                    # PostgreSQL/MySQL/MSSQL connection string

# --- Security ---
MAX_MESSAGE_SIZE = 52428800           # 50 MB
SESSION_MAX_AGE  = 604800             # 7 days
BRUTE_MAX_TRIES  = 5
BRUTE_WINDOW_MIN = 30
BRUTE_BAN_HOURS  = 24
TRUSTED_PROXIES  =                    # CIDR list for X-Forwarded-For
SECURE_COOKIE    = false              # true when behind TLS

# --- Spam ---
SPAM_THRESHOLD   = 10                 # Messages scoring >= this → Spam folder
SPAM_DNSBL       = zen.spamhaus.org,bl.spamcop.net
SPAM_CHECK_SPF   = true
SPAM_CHECK_DKIM  = true
SPAM_CHECK_DMARC = true

# --- Delivery ---
QUEUE_MAX_AGE_H  = 72                 # Bounce after 72h
QUEUE_RETRY_MIN  = 5,15,60,240,480   # Retry backoff in minutes
DNS_PRIMARY      = 1.1.1.1
DNS_SECONDARY    = 8.8.8.8

# --- Debug ---
DEBUG            = false
LOG_FILE         = ./logs/mail.log
LOG_LEVEL        = info               # debug | info | warn | error

6. Database Schema

Tables

-- System
config           (key, value, updated_at)
migrations       (version, applied_at)

-- Domains & Users
domains          (id, name, enabled, dkim_private_enc, dkim_public, dkim_selector,
                  spf_policy, dmarc_policy, created_at)
users            (id, domain_id, username, email, password_hash, display_name,
                  quota_bytes, enabled, admin, mfa_secret_enc, created_at, last_login)
user_sessions    (id, user_id, token_hash, ip, user_agent, created_at, expires_at)
user_aliases     (id, user_id, alias_email)

-- Mail storage
mailboxes        (id, user_id, name, type, parent_id, uid_validity, uid_next,
                  subscribed, created_at)
-- type: inbox|sent|drafts|trash|spam|archive|custom

messages         (id, mailbox_id, uid, remote_uid, message_id, subject,
                  from_email, from_name, to_list, cc_list, bcc_list, reply_to,
                  date, body_text_enc, body_html_enc, raw_enc,
                  size_bytes, has_attachment, is_read, is_starred, is_draft,
                  flags, spam_score, received_at, deleted_at)
-- All *_enc columns are AES-256-GCM encrypted blobs

attachments      (id, message_id, filename, content_type, size_bytes, data_enc,
                  content_id, inline)

-- Delivery queue
queue            (id, domain_id, from_addr, to_addr, raw_enc, message_id,
                  status, attempts, last_attempt, next_attempt, error_log,
                  created_at, expires_at)
-- status: pending|sending|failed|bounced|delivered

delivery_log     (id, queue_id, from_addr, to_addr, status, smtp_code,
                  smtp_message, mx_host, client_ip, created_at)

-- Security
ip_bans          (id, ip, reason, banned_at, expires_at, released_by)
login_attempts   (id, ip, user_email, success, created_at)
security_events  (id, type, ip, user_id, detail, created_at)

-- Contacts (CardDAV)
addressbooks     (id, user_id, name, description, color, sync_token)
contacts         (id, addressbook_id, uid, vcard_enc, etag, created_at, updated_at)

-- Calendar (CalDAV)
calendars        (id, user_id, name, description, color, timezone, sync_token)
calendar_events  (id, calendar_id, uid, ical_enc, etag, dtstart, dtend,
                  summary, recurring, created_at, updated_at)

7. Core Modules — Detail

7.1 Config (internal/config/)

  • Read app_config.conf (INI format, # comments)
  • Missing file → generate with random secrets + safe defaults
  • Missing keys → append to existing file (non-destructive update)
  • Env var MAILGO_<KEY> overrides file value
  • Startup validation: required fields, port ranges, key lengths

7.2 Database (internal/db/)

  • database/sql interface — driver loaded by config
  • Build tags select driver: sqlite, postgres, mysql, mssql
  • Sequential migrations (integer version in migrations table)
  • Prepared statements everywhere — no fmt.Sprintf into SQL
  • Connection pool config (max open, idle, lifetime)
  • context.Context on all queries with configurable timeout

7.3 Crypto (internal/crypto/)

  • Master key: 32-byte AES-256 from config (hex)
  • Per-user key: HKDF-SHA256(master_key, user_id+salt)
  • Encrypt: AES-256-GCM, random nonce prepended to ciphertext
  • Decrypt: split nonce, decrypt, authenticate
  • Password hash: bcrypt cost 12 minimum
  • Session tokens: crypto/rand 32 bytes, stored as SHA-256 hash in DB
  • DKIM keys: RSA-2048 or Ed25519 (configurable)

7.4 SMTP Inbound (internal/smtp/inbound.go)

Port 25 — receives mail from the internet for local domains.

Flow:

  1. Accept TCP connection
  2. Greet: 220 hostname ESMTP mailgosend
  3. EHLO → advertise: STARTTLS, SIZE <max>, 8BITMIME, SMTPUTF8
  4. STARTTLS (optional but offered)
  5. MAIL FROM — validate format, check SPF
  6. RCPT TO — validate recipient exists in local domain
  7. DATA — accept message
  8. Post-receive pipeline:
    • Parse headers + MIME
    • Verify DKIM signature
    • Check DMARC alignment
    • Score spam (DNSBL + heuristics)
    • Encrypt body + attachments
    • Store in recipient's mailbox
    • Emit IMAP push notification (IDLE clients)
  9. 250 OK or appropriate error code

Rejected on arrival: open relay (no local recipient), size exceeded, malformed envelope.

7.5 SMTP Submission (internal/smtp/submission.go)

Ports 587 (STARTTLS mandatory) and 465 (implicit TLS).

Differences from inbound:

  • STARTTLS mandatory before AUTH on 587 (reject AUTH without TLS)
  • AUTH PLAIN + AUTH LOGIN + AUTH XOAUTH2
  • Authenticated user must match MAIL FROM domain
  • No relay to arbitrary domains unless relay rules configured
  • DKIM-sign outbound messages
  • Add Received: header
  • Queue for async delivery

7.6 SMTP Session FSM (internal/smtp/session.go)

Shared state machine (reused by both inbound and submission):

States: Greeting → Helo → (Auth) → Mail → Rcpt → Data → Done
  • Command timeout: 5 min per command (configurable)
  • Max recipients: 100 (configurable)
  • Pipeline support (ESMTP PIPELINING extension)
  • Idle timeout: 10 min

7.7 Delivery Queue (internal/smtp/queue.go + internal/delivery/)

Queue flow:

  1. Message inserted into queue table (status=pending, raw body encrypted)
  2. Background goroutine polls queue every 30s
  3. For each pending/retry-eligible row:
    • DNS MX lookup for recipient domain
    • Attempt SMTP delivery with TLS (STARTTLS preferred, fallback plain)
    • On success: status=delivered, log entry
    • On soft failure (4xx): increment attempts, set next_attempt (backoff)
    • On hard failure (5xx): status=failed, send bounce to sender
    • On timeout (>queue_max_age): bounce
  4. Bounce messages: generated RFC 3464 DSN, stored in sender's mailbox

7.8 IMAP Server (internal/imap/)

Uses github.com/emersion/go-imap/v2 server package. Implement the backend.Backend interface backed by our DB/storage layer.

Supported capabilities:

  • IMAP4rev1 (RFC 3501)
  • UIDPLUS
  • IDLE (push notification when new mail arrives)
  • NAMESPACE
  • QUOTA
  • MOVE
  • SPECIAL-USE
  • AUTH=PLAIN + AUTH=LOGIN
  • STARTTLS (port 143), implicit TLS (port 993)

Backend operations map to:

  • Login → verify user credentials against DB
  • ListMailboxes → query mailboxes table
  • SelectMailbox → load mailbox metadata, update uid_validity
  • FetchMessages → query + decrypt messages
  • StoreFlags → update is_read, is_starred, flags
  • AppendMessage → encrypt + insert into messages
  • CopyMessages / MoveMessages → DB operations
  • ExpungeMessages → soft-delete (set deleted_at)

7.9 DKIM (internal/dkim/)

Ported + improved from gomta:

  • Sign outbound: rsa-sha256, relaxed/relaxed canonicalization
  • Verify inbound: fetch public key via DNS TXT, validate signature
  • Per-domain keys stored encrypted in domains.dkim_private_enc
  • Key generation: RSA-2048 minimum, Ed25519 optional
  • DNS record helper: prints ready-to-paste TXT record

7.10 SPF (internal/spf/)

From scratch using net stdlib:

  • Parse SPF1 TXT records from sender domain DNS
  • Walk mechanisms: include, a, mx, ip4, ip6, all
  • Return pass, fail, softfail, neutral, permerror, temperror
  • DNS lookup limit: 10 (per RFC 7208)
  • Result fed into spam scorer

7.11 DMARC (internal/dmarc/)

From scratch:

  • Fetch _dmarc.<domain> TXT record
  • Parse policy (p=none|quarantine|reject)
  • Check DKIM + SPF alignment (strict/relaxed)
  • Apply policy to inbound messages
  • Log aggregate data (future: reporting)

7.12 Spam Scorer (internal/spam/)

Rule-based, no ML dependencies:

Check Max points
DNSBL hit (zen.spamhaus.org etc.) +5 per list
SPF fail +4
SPF softfail +2
DKIM fail / missing +3
DMARC reject policy violated +5
Missing Date header +1
Missing Message-ID +1
Suspicious From (forged display name) +2
HTML only (no text/plain) +1
Excessive recipients +2
All-caps subject +1

Score >= SPAM_THRESHOLD (default 10) → deliver to Spam folder + set flag.

7.13 Storage (internal/storage/)

  • Message raw RFC822 + parsed parts stored encrypted in DB (BLOB)
  • Attachments stored as separate encrypted blobs (in DB or filesystem, configurable)
  • Encryption: AES-256-GCM with per-user derived key
  • Quota enforcement: check users.quota_bytes before storing

7.14 CalDAV (internal/caldav/)

Uses github.com/emersion/go-webdav caldav package. Implement caldav.Backend interface:

  • GetCalendar → query calendars table
  • ListCalendars → all user calendars
  • GetCalendarObject → decrypt + return iCal
  • PutCalendarObject → encrypt + upsert event
  • DeleteCalendarObject → delete event
  • Sync token: monotonic counter per calendar (for calendar-sync report)

Exposed at /dav/calendars/<user>/ on CalDAV port. Supports iOS/Android/Thunderbird/Evolution native calendar sync.

7.15 CardDAV (internal/carddav/)

Uses github.com/emersion/go-webdav carddav package.

  • GetAddressBook / ListAddressBooks
  • GetAddressObject → decrypt + return vCard
  • PutAddressObject → encrypt + upsert contact
  • Supports iOS/Android contacts sync

Exposed at /dav/contacts/<user>/ on same CalDAV port.

7.16 Web Admin (internal/webadmin/)

Accessible on WEBADMIN_PORT (default 8081, loopback only by default).

Pages / features:

  • Dashboard: server stats, queue depth, delivery rate, active connections
  • Domains: add/remove/enable, view DKIM DNS records (copy-paste ready), SPF guidance
  • Users: create/edit/delete/quota, reset password, enable MFA
  • Queue: view pending/failed messages, retry manually, cancel
  • Delivery logs: searchable, filterable by status/domain/date
  • Security: IP ban list, login attempt log, security events
  • Settings: edit config keys (except secrets), reload config without restart
  • TLS: cert upload or trigger ACME renewal
  • System: log viewer (tail), version info

7.17 Web Client (internal/webclient/)

Accessible on WEBCLIENT_PORT (default 8080).

Pages / features:

  • Login (+ MFA if enabled)
  • Multi-account unified inbox (all local accounts a user has access to, plus external IMAP)
  • Folder tree sidebar (all mailboxes, unread counts)
  • Message list (virtual scroll, infinite load, search)
  • Message view (HTML with sandbox iframe, plain text fallback, inline images, attachment download)
  • Compose (rich editor, attachments, To/CC/BCC autocomplete from contacts)
  • Contacts view (CardDAV-backed)
  • Calendar view (month/week/day, CalDAV-backed)
  • Account settings: password change, MFA setup, aliases, display name
  • External IMAP accounts: add Gmail/Outlook/custom IMAP servers (like gowebmail)
  • Dark theme, responsive, TailwindCSS CDN

8. Security Architecture

Authentication

  • Passwords: bcrypt cost 12, never stored plain
  • Sessions: random 32-byte token → SHA-256 stored in DB
  • Session cookie: HttpOnly, SameSite=Strict, Secure (when TLS)
  • MFA: TOTP (RFC 6238), QR code at setup, recovery codes
  • SMTP auth: PLAIN + LOGIN (both over TLS only)
  • Admin panel: separate session store, optional IP allowlist

Transport Security

  • SMTP port 25: STARTTLS offered, not mandatory (internet compatibility)
  • SMTP port 587: STARTTLS mandatory before AUTH
  • SMTP port 465: implicit TLS
  • IMAP port 143: STARTTLS mandatory before AUTH
  • IMAP port 993: implicit TLS
  • Web ports: TLS via cert file or Let's Encrypt autocert
  • Min TLS: 1.2, prefer 1.3

Input Validation

  • Every HTTP handler validates: Content-Type, body size, all fields
  • Email addresses: RFC 5321 parse (net/mail)
  • SQL: prepared statements only
  • HTML output: html/template auto-escaping
  • File uploads: MIME type + magic bytes check, size limit, path traversal prevention
  • CSRF: double-submit cookie pattern on all state-changing endpoints
  • Host header check: reject unexpected Host: values

Rate Limiting

  • SMTP: per-IP connection limit, per-session command rate
  • IMAP: per-IP login rate
  • HTTP login: brute force counter (configurable window/threshold/ban)
  • HTTP API: per-user request rate (sliding window, in-memory + DB fallback)
  • Geo blocking: optional deny/allow country lists

Data Security

  • Email bodies: AES-256-GCM, per-user derived key (HKDF)
  • DKIM private keys: encrypted in DB
  • OAuth tokens (external accounts): encrypted in DB
  • MFA secrets: encrypted in DB
  • Logs: never log passwords, tokens, or email content
  • Bounce messages: no sensitive headers in DSN body

9. Build System

Single binary

go build -o mailgosend ./cmd/mailgosend/

Embedded assets

webfs.go:

//go:embed web/admin web/client
var webFS embed.FS

Build tags for DB drivers

# SQLite only (default)
go build -tags sqlite ./cmd/mailgosend/

# With PostgreSQL support
go build -tags sqlite,postgres ./cmd/mailgosend/

# All drivers
go build -tags sqlite,postgres,mysql,mssql ./cmd/mailgosend/

Dockerfile (optional)

FROM golang:1.26-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -tags sqlite -o mailgosend ./cmd/mailgosend/

FROM alpine:latest
RUN apk add --no-cache ca-certificates tzdata
COPY --from=builder /app/mailgosend /usr/local/bin/
EXPOSE 25 465 587 143 993 8080 8081 5232
CMD ["mailgosend"]

10. Implementation Phases

Phase 1 — Foundation (Week 1)

  • go.mod with all deps
  • internal/config/ — config file load/autogen/validate
  • internal/db/ — DB wrapper, SQLite driver, migrations
  • internal/crypto/ — AES-256-GCM, bcrypt, HKDF
  • internal/models/ — all shared structs
  • internal/auth/session.go — session store
  • internal/tls/ — TLS config builder
  • cmd/mailgosend/main.go — startup scaffold, graceful shutdown

Phase 2 — SMTP + Delivery (Week 2)

  • internal/smtp/session.go — FSM + command parser
  • internal/smtp/inbound.go — port 25 server
  • internal/smtp/submission.go — ports 587/465
  • internal/smtp/queue.go — queue table + worker
  • internal/delivery/delivery.go — MX lookup + outbound SMTP
  • internal/dkim/dkim.go — sign + verify
  • internal/spf/spf.go — SPF check
  • internal/dmarc/dmarc.go — DMARC check
  • internal/spam/spam.go — scoring engine

Phase 3 — IMAP Server (Week 3)

  • internal/imap/backend.go — go-imap/v2 backend
  • internal/imap/server.go — IMAP + IMAPS listeners
  • internal/storage/storage.go — encrypted message storage
  • IMAP IDLE support (push)
  • Quota enforcement

Phase 4 — Web Admin (Week 4)

  • internal/webadmin/handlers.go
  • internal/auth/brute.go — brute force protection
  • internal/middleware/middleware.go — auth, CSRF, logging
  • All admin templates + TailwindCSS dark theme
  • web/admin/static/js/admin.js

Phase 5 — Web Client (Week 5)

  • internal/webclient/handlers.go — message list, view, compose
  • internal/webclient/api.go — JSON endpoints
  • Multi-account support (local + external IMAP)
  • All client templates + TailwindCSS dark theme
  • web/client/static/js/client.js — virtual scroll, compose editor

Phase 6 — CalDAV / CardDAV (Week 6)

  • internal/caldav/ — calendar CRUD + sync
  • internal/carddav/ — contacts CRUD + sync
  • Calendar UI in webclient (month view)
  • Contacts UI in webclient

Phase 7 — Polish & Hardening (Week 7)

  • Let's Encrypt autocert integration
  • MFA (TOTP) for web login
  • Delivery log + admin queue management
  • Bounce message generation (RFC 3464 DSN)
  • Import: migrate from gowebmail external accounts
  • End-to-end testing (SMTP → IMAP → webmail round-trip)
  • Security audit pass

11. Key Differences from Existing Projects

Feature gomta gowebmail mailgosend
SMTP inbound No No Yes
SMTP outbound Yes Relay only Yes (full MTA)
IMAP server No No Yes
IMAP client No Yes Yes (external accounts)
Web client No Yes Yes (improved)
Admin panel Basic Basic Full
CalDAV No Basic Full RFC
CardDAV No Basic Full RFC
DKIM verify No No Yes
SPF check No No Yes
DMARC No No Yes
Spam scoring No No Yes
Encryption at rest No Yes (tokens only) Yes (all content)
Pure Go (no CGO) No (go-sqlite3) No (go-sqlite3) Yes (modernc)
Multi-DB support No No Yes
Single binary No No Yes
Let's Encrypt No No Yes
MFA No No Yes

12. Design Decisions (Resolved)

# Question Decision
1 External IMAP accounts Yes — local accounts + external Gmail/Outlook/custom IMAP (OAuth2 + credentials). Reuse gowebmail approach.
2 Storage backend Configurable — DB blob default, filesystem optional. Config switch STORAGE_BACKEND = db | fs.
3 Admin model Global admin + per-domain admin — domain admin can manage users/settings for their domain only.
4 Spam filter Rules + Bayesian — static scoring (SPF/DKIM/DMARC/DNSBL) + per-user Bayesian token training from "mark as spam/not spam".
5 TLS DNS-01 default (lego) — wildcard certs, no ports exposed, Cloudflare/Route53/90+ providers. http01 for simple setups. manual for self-managed certs. Per-service cert override. SIGHUP hot-reload.
6 DB drivers Build tags — default binary SQLite only. -tags postgres/mysql/mssql to add drivers.
7 DKIM keys RSA-2048 default + Ed25519 option — configurable per domain (DKIM_ALGO = rsa2048 | ed25519).
8 Queue DB-backed — survives crash. Rows in queue table with status/retry/backoff tracking.

Additional dependency: OAuth2

External account support requires:

  • golang.org/x/oauth2 — Google + Microsoft OAuth2 flows (same as gowebmail)
  • Google: Gmail scope https://mail.google.com/
  • Microsoft: IMAP.AccessAsUser.All, SMTP.Send, offline_access

13. TLS Architecture Detail

Challenge modes

TLS_MODE = dns01   →  github.com/go-acme/lego/v4
                        DNS-01 ACME challenge via DNS provider API
                        ✓ No port 80/443 needed — works behind NAT/firewall
                        ✓ Wildcard certificates (*.example.com)
                        ✓ Server can be completely non-public (LAN-only)
                        ✓ 90+ DNS providers (Cloudflare, Route53, DO, etc.)
                        Renewal: automatic background goroutine, 30 days before expiry

TLS_MODE = http01  →  github.com/go-acme/lego/v4
                        HTTP-01 challenge: lego spins up ephemeral listener on port 80
                        ✓ No DNS API credentials needed
                        ✗ Port 80 must be reachable from internet
                        ✗ No wildcard support
                        Renewal: same automatic background goroutine

TLS_MODE = manual  →  Load TLS_CERT + TLS_KEY at startup
                        SIGHUP signal triggers cert reload without restart
                        Compatible with: certbot, acme.sh, Vault PKI, self-signed
                        Use when: internal CA, corporate cert, or existing automation
startup → lego checks ACME_CACHE_DIR for cached cert
        → if missing or expiring: start DNS-01 challenge
            → lego calls Cloudflare API: add TXT _acme-challenge.example.com
            → Let's Encrypt verifies TXT record
            → lego calls Cloudflare API: remove TXT record
            → cert issued for: example.com + *.example.com
            → cached in ACME_CACHE_DIR/certificates/
        → build *tls.Config from cert
        → all listeners (SMTP/IMAP/web) share same *tls.Config (or per-service)
renewal → background goroutine checks cert expiry every 12h
        → renews 30 days before expiry, same DNS-01 flow
        → atomic swap of *tls.Config — zero downtime, no restart

Per-service cert override

Priority (highest to lowest):
  SMTP_TLS_CERT/KEY  →  used for SMTP + SMTPS
  IMAP_TLS_CERT/KEY  →  used for IMAP + IMAPS
  WEB_TLS_CERT/KEY   →  used for webclient + admin + CalDAV ports
  TLS_CERT/KEY       →  global fallback (TLS_MODE=manual)
  ACME cert          →  global fallback (TLS_MODE=dns01 or http01)
  off                →  no TLS on that service

Minimum security

  • TLS 1.2 minimum on all listeners
  • TLS 1.3 preferred
  • Cipher suites: ECDHE + AES-256-GCM, CHACHA20-POLY1305 only
  • HSTS header on all HTTP responses (when TLS active)
  • SMTP: 220 banner does not reveal software version

Plan created: 2026-05-21 Author: nahakubuilder Last updated: 2026-05-21 — all design decisions resolved