Files
2026-05-21 20:27:58 +00:00

839 lines
32 KiB
Markdown

# 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-gen** — `app_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 frameworks** — `net/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
```ini
# --- 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
```sql
-- 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
```bash
go build -o mailgosend ./cmd/mailgosend/
```
### Embedded assets
`webfs.go`:
```go
//go:embed web/admin web/client
var webFS embed.FS
```
### Build tags for DB drivers
```bash
# 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)
```dockerfile
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
```
### Wildcard flow (recommended, Cloudflare example)
```
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*