add docker setup
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
.git
|
||||||
|
.claude
|
||||||
|
graphify-out
|
||||||
|
tests
|
||||||
|
server_data
|
||||||
|
ssl_certs
|
||||||
|
*.db
|
||||||
|
*.crt
|
||||||
|
*.key
|
||||||
@@ -1,4 +1,89 @@
|
|||||||
# Deploying mailgoserver
|
# mailgoserver
|
||||||
|
|
||||||
|
A self-hosted email server in one static Go binary: SMTP (send + receive, direct-to-MX
|
||||||
|
delivery, DKIM signing), IMAP mailbox storage, an admin web dashboard, and a self-service
|
||||||
|
webmail portal. It's a Go port of PyMTA-server — same feature set, one process instead of
|
||||||
|
a Python venv + separate services.
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
- **SMTP MTA** — accepts mail for domains you configure, relays outbound mail directly
|
||||||
|
to the recipient's MX (no smarthost needed), signs outgoing mail with DKIM, and
|
||||||
|
enforces per-domain sender authentication or IP whitelisting so it can't be used as an
|
||||||
|
open relay.
|
||||||
|
- **IMAP mailbox storage** — real, retrievable mailboxes (Thunderbird, Outlook, any IMAP
|
||||||
|
client) with AES-encrypted-at-rest message storage, per-mailbox quotas, send-as
|
||||||
|
aliases, and filter rules (move to folder, forward, discard, based on from/subject/
|
||||||
|
body conditions with AND/OR logic).
|
||||||
|
- **Spam filtering** — a built-in heuristic score always runs; optionally point it at an
|
||||||
|
[rspamd](https://rspamd.com) instance for a lot more signal (see `docker-deploy/` for
|
||||||
|
a container that bundles rspamd for you).
|
||||||
|
- **Webmail portal** (`/webmail`) — inbox/folders, HTML compose with attachments,
|
||||||
|
drafts, search, keyboard shortcuts, conversation grouping, recipient autocomplete,
|
||||||
|
filter-rule management, and PGP (OpenPGP encrypt/decrypt/sign/verify) and S/MIME
|
||||||
|
(sign/verify) support per mailbox.
|
||||||
|
- **Admin dashboard** (`/pymta-manager`) — manage domains, senders, mailboxes, DKIM
|
||||||
|
keys, IP whitelisting, TLS (self-signed or Let's Encrypt via DNS-01: Cloudflare,
|
||||||
|
Route53, DigitalOcean, Google Cloud DNS), and review email + auth logs.
|
||||||
|
- **Security hardening built in** — CSRF protection, security headers (CSP, X-Frame-
|
||||||
|
Options, etc.), Cloudflare-aware trusted-proxy IP resolution, login rate limiting and
|
||||||
|
account lockout, TOTP + WebAuthn/passkey MFA (admin and mailbox owners), and automatic
|
||||||
|
temporary IP blacklisting for SMTP/IMAP brute-force or relay-abuse attempts with an
|
||||||
|
admin-visible Blacklist page and dashboard attack-count tiles.
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
### Docker (fastest)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd docker-deploy
|
||||||
|
docker compose --profile standalone up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
See [`docker-deploy/README.md`](docker-deploy/README.md) for the rspamd-bundled variant
|
||||||
|
and full details (ports, volumes, first-boot config).
|
||||||
|
|
||||||
|
### From source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -o mailgoserver .
|
||||||
|
./mailgoserver
|
||||||
|
```
|
||||||
|
|
||||||
|
One static binary — no venv, no `pip install`, no gunicorn. Requires Go 1.26+ to build;
|
||||||
|
the binary itself has no runtime dependencies (pure-Go SQLite driver, no cgo).
|
||||||
|
|
||||||
|
## What to expect on first run
|
||||||
|
|
||||||
|
There's no config to write up front. The first time it starts in a given working
|
||||||
|
directory, it generates:
|
||||||
|
|
||||||
|
- `settings.ini` — every setting with an inline comment explaining it (SMTP/IMAP ports,
|
||||||
|
hostname, TLS, DKIM key size, mailbox quotas, MFA enforcement, rate limits, and so
|
||||||
|
on). Regenerated only if missing — it's never overwritten or merged into on later
|
||||||
|
runs, so edits stick.
|
||||||
|
- A self-signed TLS certificate (used until you either supply your own or enable Let's
|
||||||
|
Encrypt from the admin dashboard).
|
||||||
|
- An empty SQLite database, with one seeded admin account: username `admin`, password
|
||||||
|
`Password123!`. Logging in with it **immediately forces** a username + password
|
||||||
|
change before anything else in the dashboard is reachable — the default credentials
|
||||||
|
can never be left in place.
|
||||||
|
- A mailstore master key and a CSRF-signing app secret, both generated once and reused
|
||||||
|
on every subsequent start — back these up like any other secret (losing the mailstore
|
||||||
|
master key makes all stored mail unrecoverable, even for admins).
|
||||||
|
|
||||||
|
From there: log into `/pymta-manager`, add a domain (and complete its DNS ownership
|
||||||
|
verification), add a mailbox or sender, and you're sending/receiving. The bare `/` root
|
||||||
|
redirects to the webmail login (`/webmail/login`) by default, since most visitors are
|
||||||
|
mailbox owners, not admins — there's a "Login as Admin" link from there to
|
||||||
|
`/pymta-manager`.
|
||||||
|
|
||||||
|
Default ports (all configurable in `settings.ini`): SMTP `25`, direct-TLS SMTP `465`,
|
||||||
|
IMAP `143`, direct-TLS IMAP `993` — the real standard mail ports, so binding them
|
||||||
|
directly needs root or `setcap` (see below), which the Docker deployment already
|
||||||
|
handles for you. Admin/webmail HTTP `5000` / HTTPS `5001` stay deliberately
|
||||||
|
non-privileged; put a reverse proxy or your own `80`/`443` mapping in front of those if
|
||||||
|
you want the dashboard on standard web ports too.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
@@ -7,15 +92,20 @@ cd mailgoserver
|
|||||||
go build -o mailgoserver .
|
go build -o mailgoserver .
|
||||||
```
|
```
|
||||||
|
|
||||||
One static binary — no venv, no `pip install`, no gunicorn.
|
## Bind ports 25/143/465/993 without root
|
||||||
|
|
||||||
## Bind ports 25/587 without root
|
The default SMTP/IMAP ports are the real standard ones now, so running the binary
|
||||||
|
directly (not via Docker) needs one of:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo setcap 'cap_net_bind_service=+ep' ./mailgoserver
|
sudo setcap 'cap_net_bind_service=+ep' ./mailgoserver
|
||||||
```
|
```
|
||||||
|
|
||||||
Same purpose as `script_setup_py_environment.sh`'s `setcap` step on the Python venv, applied to the compiled binary instead.
|
or run it as root, or via the systemd unit below (which grants the capability instead
|
||||||
|
of running as root). Same purpose as `script_setup_py_environment.sh`'s `setcap` step
|
||||||
|
on the Python venv, applied to the compiled binary instead. **Not needed for the Docker
|
||||||
|
deployment** — those containers run as root, so binding 25/143/465/993 directly just
|
||||||
|
works with no extra setup.
|
||||||
|
|
||||||
## systemd (unified process)
|
## systemd (unified process)
|
||||||
|
|
||||||
@@ -56,6 +146,12 @@ and terminates its own TLS for the web UI — mailgoserver listens on the same h
|
|||||||
by default, so the existing nginx config works unmodified. The SMTP TLS listener still
|
by default, so the existing nginx config works unmodified. The SMTP TLS listener still
|
||||||
consumes `ssl_certs/server.crt`/`server.key`, same as before.
|
consumes `ssl_certs/server.crt`/`server.key`, same as before.
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
See [`docker-deploy/`](docker-deploy/) — a standalone image and one that bundles the
|
||||||
|
latest rspamd in the same container, both via a single `docker-compose.yml` using
|
||||||
|
Compose profiles.
|
||||||
|
|
||||||
## Admin dashboard login
|
## Admin dashboard login
|
||||||
|
|
||||||
First run seeds one account: username `admin`, password `Password123!`. Logging in
|
First run seeds one account: username `admin`, password `Password123!`. Logging in
|
||||||
@@ -70,7 +166,17 @@ Optional second factors, enabled per-account from **Account** in the sidebar:
|
|||||||
`rp_origin = https://mail.example.com`). The defaults (`localhost` /
|
`rp_origin = https://mail.example.com`). The defaults (`localhost` /
|
||||||
`http://localhost:5000`) only work for local testing — WebAuthn requires either
|
`http://localhost:5000`) only work for local testing — WebAuthn requires either
|
||||||
HTTPS or the literal host `localhost`, so passkeys need the nginx+TLS setup above
|
HTTPS or the literal host `localhost`, so passkeys need the nginx+TLS setup above
|
||||||
to work behind a real domain.
|
(or the Docker deployment) to work behind a real domain.
|
||||||
|
|
||||||
|
## Abuse protection
|
||||||
|
|
||||||
|
Failed SMTP/IMAP auth attempts (bad passwords, denied relay attempts) are counted per
|
||||||
|
source IP; once a configurable threshold is hit within a window, that IP is temporarily
|
||||||
|
blocked at the listener level — before the SMTP/IMAP banner is even sent — with the
|
||||||
|
block duration doubling on repeat offenses up to a cap. Manage active blocks and exempt
|
||||||
|
trusted IPs from this check entirely from **Blacklist** in the admin sidebar (a global-
|
||||||
|
admin-only page) — this is separate from the IP whitelist used to authorize
|
||||||
|
unauthenticated relay for a domain. Tunable in `settings.ini`'s `[Security]` section.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Copy to .env (in this docker-deploy/ folder) and adjust if the defaults below clash
|
||||||
|
# with something else on the host, or if you want to run both the "standalone" and
|
||||||
|
# "with-rspamd" profiles side by side (give each its own set of host ports).
|
||||||
|
SMTP_PORT=25
|
||||||
|
SMTP_TLS_PORT=465
|
||||||
|
IMAP_PORT=143
|
||||||
|
IMAP_TLS_PORT=993
|
||||||
|
WEB_HTTP_PORT=5000
|
||||||
|
WEB_HTTPS_PORT=5001
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
#
|
||||||
|
# Standalone mailgoserver image — SMTP + IMAP + admin dashboard + webmail, no rspamd.
|
||||||
|
# The built-in heuristic spam score (internal/mailstore/spam.go) always runs regardless;
|
||||||
|
# this is for anyone who doesn't want rspamd's extra dependency. See Dockerfile.rspamd
|
||||||
|
# for the bundled variant.
|
||||||
|
#
|
||||||
|
# Build from the repo root:
|
||||||
|
# docker build -f docker-deploy/Dockerfile -t mailgoserver .
|
||||||
|
|
||||||
|
FROM golang:1.26-bookworm AS build
|
||||||
|
WORKDIR /src
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY . .
|
||||||
|
# modernc.org/sqlite and every other dependency here are pure Go (no cgo), so a fully
|
||||||
|
# static binary is just CGO_ENABLED=0 — no libc/gcc needed in the runtime image.
|
||||||
|
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/mailgoserver .
|
||||||
|
|
||||||
|
FROM debian:bookworm-slim
|
||||||
|
# ca-certificates: outbound direct-to-MX delivery verifies remote STARTTLS certs.
|
||||||
|
# tzdata: [Server] time_zone (e.g. "Europe/London") needs the real IANA zone
|
||||||
|
# database — time.LoadLocation silently falls back to UTC without it.
|
||||||
|
# curl: used by the HEALTHCHECK below.
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates tzdata curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY --from=build /out/mailgoserver /usr/local/bin/mailgoserver
|
||||||
|
|
||||||
|
# Everything persistent — the auto-generated settings.ini, the SQLite DB, encrypted
|
||||||
|
# mailbox storage, DKIM/mailstore master keys, TLS certs, the app secret — lives under
|
||||||
|
# whatever directory the process is started from (see config.Load / main.go's
|
||||||
|
# os.Getwd()). One volume here covers all of it; no need to enumerate subpaths.
|
||||||
|
WORKDIR /app/data
|
||||||
|
VOLUME ["/app/data"]
|
||||||
|
|
||||||
|
# Defaults from internal/config/config.go's generated settings.ini: SMTP 25, direct-TLS
|
||||||
|
# SMTP 465, IMAP 143, direct-TLS IMAP 993 (the actual standard ports — binding them
|
||||||
|
# needs no setcap/capability here since this container runs as root), admin/webmail
|
||||||
|
# HTTP 5000, HTTPS 5001 (deliberately non-privileged; put a reverse proxy or the host's
|
||||||
|
# own 80/443 in front if you want those too).
|
||||||
|
EXPOSE 25 465 143 993 5000 5001
|
||||||
|
|
||||||
|
# --host 0.0.0.0 is required: the binary's own default is 127.0.0.1, which would only
|
||||||
|
# be reachable from inside this container, never through a published port.
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD curl -fs http://127.0.0.1:5000/health || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["mailgoserver", "--host", "0.0.0.0"]
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
#
|
||||||
|
# mailgoserver + the latest rspamd, bundled in one container. Both processes share this
|
||||||
|
# container's network namespace, so rspamd's default "normal" worker
|
||||||
|
# (127.0.0.1:11333, the /checkv2 scanning API — see internal/mailstore/rspamd.go) is
|
||||||
|
# already exactly what [Rspamd] url defaults to in settings.ini. Just set
|
||||||
|
# [Rspamd] enabled = true after first boot (see docker-deploy/README.md) and restart.
|
||||||
|
#
|
||||||
|
# Build from the repo root:
|
||||||
|
# docker build -f docker-deploy/Dockerfile.rspamd -t mailgoserver-rspamd .
|
||||||
|
|
||||||
|
FROM golang:1.26-bookworm AS build
|
||||||
|
WORKDIR /src
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY . .
|
||||||
|
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/mailgoserver .
|
||||||
|
|
||||||
|
FROM debian:bookworm-slim
|
||||||
|
# rspamd is installed from its own APT repo (rspamd.com), not Debian's bundled
|
||||||
|
# package, which tends to lag several releases behind — this is genuinely the latest
|
||||||
|
# stable release, matching what was asked for.
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates tzdata curl gnupg lsb-release \
|
||||||
|
&& mkdir -p /usr/share/keyrings \
|
||||||
|
&& curl -fsSL https://rspamd.com/apt-stable/gpg.key | gpg --dearmor -o /usr/share/keyrings/rspamd.gpg \
|
||||||
|
&& echo "deb [signed-by=/usr/share/keyrings/rspamd.gpg] https://rspamd.com/apt-stable/ $(lsb_release -cs) main" \
|
||||||
|
> /etc/apt/sources.list.d/rspamd.list \
|
||||||
|
&& apt-get update && apt-get install -y --no-install-recommends rspamd \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY --from=build /out/mailgoserver /usr/local/bin/mailgoserver
|
||||||
|
COPY docker-deploy/entrypoint-rspamd.sh /usr/local/bin/entrypoint-rspamd.sh
|
||||||
|
RUN chmod +x /usr/local/bin/entrypoint-rspamd.sh
|
||||||
|
|
||||||
|
# /app/data: same as the standalone image (settings.ini, DB, mailstore, keys, certs).
|
||||||
|
# /var/lib/rspamd: rspamd's own Bayes/fuzzy-hash storage, kept persistent separately so
|
||||||
|
# rebuilding the image doesn't reset spam-learning state.
|
||||||
|
WORKDIR /app/data
|
||||||
|
VOLUME ["/app/data", "/var/lib/rspamd"]
|
||||||
|
|
||||||
|
EXPOSE 25 465 143 993 5000 5001
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
||||||
|
CMD curl -fs http://127.0.0.1:5000/health || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/local/bin/entrypoint-rspamd.sh"]
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# Docker deployment
|
||||||
|
|
||||||
|
Two independent images, both built from the same source tree:
|
||||||
|
|
||||||
|
- **`Dockerfile`** — mailgoserver only.
|
||||||
|
- **`Dockerfile.rspamd`** — mailgoserver + the latest [rspamd](https://rspamd.com) in the
|
||||||
|
*same* container, already wired together (see below). Use this one if you want
|
||||||
|
stronger spam filtering than the built-in heuristic score alone.
|
||||||
|
|
||||||
|
`docker-compose.yml` defines both as Compose **profiles** so a plain `docker compose up`
|
||||||
|
can't accidentally start both at once:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd docker-deploy
|
||||||
|
|
||||||
|
# mailserver only
|
||||||
|
docker compose --profile standalone up -d --build
|
||||||
|
|
||||||
|
# mailserver + rspamd, bundled
|
||||||
|
docker compose --profile with-rspamd up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Either way, the app itself now binds the real standard mail ports by default — 25
|
||||||
|
(SMTP), 465 (direct-TLS SMTP), 143 (IMAP), 993 (direct-TLS IMAP) — and the admin/webmail
|
||||||
|
UI on its usual non-privileged 5000/5001 (HTTP/HTTPS); put your own reverse proxy or a
|
||||||
|
`80:5000`/`443:5001` port mapping in front if you want those on 80/443 too. Binding the
|
||||||
|
low mail ports needs no extra capability here since the container runs as root. Copy
|
||||||
|
`.env.example` to `.env` in this folder to change any host-side port — useful if
|
||||||
|
something else on the host already owns 25/143/etc., or if you want to run both
|
||||||
|
profiles side by side.
|
||||||
|
|
||||||
|
## What happens on first boot
|
||||||
|
|
||||||
|
There's no baked-in config. On first start, the binary generates a fresh
|
||||||
|
`settings.ini` with defaults (mirroring `internal/config/config.go`), a self-signed TLS
|
||||||
|
certificate, DKIM/mailstore master keys, and an empty SQLite database — all inside the
|
||||||
|
`/app/data` volume, so it survives container restarts/rebuilds. The web UI seeds one
|
||||||
|
admin account: username `admin`, password `Password123!`, and forces an immediate
|
||||||
|
username + password change on first login — see the main [README](../README.md) for
|
||||||
|
the full first-login walkthrough.
|
||||||
|
|
||||||
|
**Before using this for real mail**, exec into the container (or edit the volume from
|
||||||
|
the host) and update `settings.ini`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec -it mailgoserver sh -c 'vi /app/data/settings.ini'
|
||||||
|
docker restart mailgoserver
|
||||||
|
```
|
||||||
|
|
||||||
|
At minimum, set `[Server] HOSTNAME` / `helo_hostname` to your real mail domain, and if
|
||||||
|
you'll use passkeys, `[Auth] rp_id` / `rp_origin` to match the exact domain the admin
|
||||||
|
dashboard is reached at (`rp_id` can't be `localhost` once you're on a real domain —
|
||||||
|
see the main README's WebAuthn note). There's no environment-variable override
|
||||||
|
mechanism — `settings.ini` in the volume is the one source of config truth.
|
||||||
|
|
||||||
|
## Enabling rspamd (the `with-rspamd` profile)
|
||||||
|
|
||||||
|
The bundled rspamd's default config already listens on `127.0.0.1:11333` for scanning
|
||||||
|
requests — exactly what `[Rspamd] url` defaults to in `settings.ini`, and since both
|
||||||
|
processes share the container's network namespace, no networking setup is needed. All
|
||||||
|
that's left is turning it on:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Rspamd]
|
||||||
|
enabled = true
|
||||||
|
url = http://127.0.0.1:11333
|
||||||
|
reject_score = 15
|
||||||
|
```
|
||||||
|
|
||||||
|
...then restart the container. The built-in heuristic spam score
|
||||||
|
(`internal/mailstore/spam.go`) always runs regardless of this setting — rspamd is
|
||||||
|
additive, not a replacement, and if it's ever unreachable, mail still flows on the
|
||||||
|
heuristic score alone (rspamd errors are logged, never fatal to delivery).
|
||||||
|
|
||||||
|
This bundle intentionally skips Redis — rspamd runs fine without it for SPF/DKIM/RBL/
|
||||||
|
regexp-based scoring, but Bayes learning and greylisting need it. Add a `redis` service
|
||||||
|
to `docker-compose.yml` and point rspamd's `redis.conf` at it if you need those.
|
||||||
|
|
||||||
|
## Persistence
|
||||||
|
|
||||||
|
| Volume | What's in it |
|
||||||
|
|---|---|
|
||||||
|
| `mailserver-data` / `mailserver-rspamd-data` | `settings.ini`, the SQLite DB, encrypted mailbox storage, DKIM/mailstore master keys, TLS certs, the CSRF app secret — everything mailgoserver itself owns. |
|
||||||
|
| `rspamd-data` (rspamd profile only) | rspamd's own Bayes/fuzzy-hash storage, so spam-learning state survives image rebuilds. |
|
||||||
|
|
||||||
|
Back up the `*-data` volume like you would the equivalent bare-metal `server_data/`
|
||||||
|
directory — losing the mailstore master key makes all stored mail unrecoverable, same
|
||||||
|
as a non-Docker install.
|
||||||
|
|
||||||
|
## Logs / health
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose --profile standalone logs -f # or --profile with-rspamd
|
||||||
|
docker inspect --format '{{.State.Health.Status}}' mailgoserver
|
||||||
|
```
|
||||||
|
|
||||||
|
Both images expose `GET /health` (used by the container `HEALTHCHECK`), matching the
|
||||||
|
JSON the admin dashboard's own health check reads.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Two independent setups, selected with --profile so they don't both start by accident:
|
||||||
|
#
|
||||||
|
# docker compose --profile standalone up -d --build # mailserver only
|
||||||
|
# docker compose --profile with-rspamd up -d --build # mailserver + rspamd, same container
|
||||||
|
#
|
||||||
|
# Both default to the standard mail ports on the host (25/465/143/993) — the app itself
|
||||||
|
# now binds those directly, no port remapping needed — plus the app's own non-privileged
|
||||||
|
# web ports (5000/5001; put a reverse proxy or your own 80/443 mapping in front of those
|
||||||
|
# if you want the dashboard on standard web ports too). Only run one profile at a time
|
||||||
|
# unless you've overridden the host ports for one of them (see .env.example) — they'd
|
||||||
|
# otherwise both try to bind the same host ports.
|
||||||
|
services:
|
||||||
|
mailserver:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: docker-deploy/Dockerfile
|
||||||
|
container_name: mailgoserver
|
||||||
|
restart: unless-stopped
|
||||||
|
profiles: ["standalone"]
|
||||||
|
ports:
|
||||||
|
- "${SMTP_PORT:-25}:25"
|
||||||
|
- "${SMTP_TLS_PORT:-465}:465"
|
||||||
|
- "${IMAP_PORT:-143}:143"
|
||||||
|
- "${IMAP_TLS_PORT:-993}:993"
|
||||||
|
- "${WEB_HTTP_PORT:-5000}:5000"
|
||||||
|
- "${WEB_HTTPS_PORT:-5001}:5001"
|
||||||
|
volumes:
|
||||||
|
- mailserver-data:/app/data
|
||||||
|
|
||||||
|
mailserver-rspamd:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: docker-deploy/Dockerfile.rspamd
|
||||||
|
container_name: mailgoserver-rspamd
|
||||||
|
restart: unless-stopped
|
||||||
|
profiles: ["with-rspamd"]
|
||||||
|
ports:
|
||||||
|
- "${SMTP_PORT:-25}:25"
|
||||||
|
- "${SMTP_TLS_PORT:-465}:465"
|
||||||
|
- "${IMAP_PORT:-143}:143"
|
||||||
|
- "${IMAP_TLS_PORT:-993}:993"
|
||||||
|
- "${WEB_HTTP_PORT:-5000}:5000"
|
||||||
|
- "${WEB_HTTPS_PORT:-5001}:5001"
|
||||||
|
volumes:
|
||||||
|
- mailserver-rspamd-data:/app/data
|
||||||
|
- rspamd-data:/var/lib/rspamd
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mailserver-data:
|
||||||
|
mailserver-rspamd-data:
|
||||||
|
rspamd-data:
|
||||||
Executable
+19
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Starts rspamd in the background, then hands off to mailgoserver as the container's
|
||||||
|
# main (foreground) process.
|
||||||
|
#
|
||||||
|
# ponytail: no supervisor / restart-on-crash for rspamd here — if it dies mid-run, mail
|
||||||
|
# keeps flowing without the extra scoring rather than the container crashing (the
|
||||||
|
# built-in heuristic score in internal/mailstore/spam.go always runs regardless, and
|
||||||
|
# CheckRspamd swallows connection errors rather than rejecting mail over them — see
|
||||||
|
# internal/mailstore/rspamd.go). Add s6-overlay or supervisord if unattended rspamd
|
||||||
|
# uptime matters more than that.
|
||||||
|
rspamd -f &
|
||||||
|
|
||||||
|
# Give rspamd's normal worker a moment to bind :11333 before the first message can
|
||||||
|
# possibly arrive — purely cosmetic, mailgoserver degrades gracefully either way.
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
exec mailgoserver --host 0.0.0.0 "$@"
|
||||||
@@ -25,9 +25,11 @@ var defaults = []struct {
|
|||||||
{"Server", []defaultKV{
|
{"Server", []defaultKV{
|
||||||
{"", "", "Server configuration for SMTP ports and hostname"},
|
{"", "", "Server configuration for SMTP ports and hostname"},
|
||||||
{"", "", "Plain SMTP port for internal/whitelisted IPs"},
|
{"", "", "Plain SMTP port for internal/whitelisted IPs"},
|
||||||
{"SMTP_PORT", "4025", ""},
|
{"", "", "(standard port 25 — the process needs CAP_NET_BIND_SERVICE or root to bind"},
|
||||||
{"", "", "TLS SMTP port for authenticated users"},
|
{"", "", "it directly; not an issue in the Docker deployment, which runs as root)"},
|
||||||
{"SMTP_TLS_PORT", "40465", ""},
|
{"SMTP_PORT", "25", ""},
|
||||||
|
{"", "", "Direct/implicit-TLS SMTP port for authenticated users (like SMTPS, not STARTTLS)"},
|
||||||
|
{"SMTP_TLS_PORT", "465", ""},
|
||||||
{"", "", "Server hostname for HELO/EHLO identification"},
|
{"", "", "Server hostname for HELO/EHLO identification"},
|
||||||
{"HOSTNAME", "mail.example.com", ""},
|
{"HOSTNAME", "mail.example.com", ""},
|
||||||
{"", "", "Override HELO hostname"},
|
{"", "", "Override HELO hostname"},
|
||||||
@@ -112,9 +114,9 @@ var defaults = []struct {
|
|||||||
{"IMAP", []defaultKV{
|
{"IMAP", []defaultKV{
|
||||||
{"", "", "IMAP server configuration for mailbox retrieval (Thunderbird, etc.)"},
|
{"", "", "IMAP server configuration for mailbox retrieval (Thunderbird, etc.)"},
|
||||||
{"", "", "Plain IMAP port (STARTTLS not offered, matching the SMTP plain-port design)"},
|
{"", "", "Plain IMAP port (STARTTLS not offered, matching the SMTP plain-port design)"},
|
||||||
{"IMAP_PORT", "1143", ""},
|
{"IMAP_PORT", "143", ""},
|
||||||
{"", "", "Implicit-TLS IMAP port (IMAPS)"},
|
{"", "", "Implicit-TLS IMAP port (IMAPS)"},
|
||||||
{"IMAP_TLS_PORT", "1993", ""},
|
{"IMAP_TLS_PORT", "993", ""},
|
||||||
}},
|
}},
|
||||||
{"Mailstore", []defaultKV{
|
{"Mailstore", []defaultKV{
|
||||||
{"", "", "Local mailbox storage configuration"},
|
{"", "", "Local mailbox storage configuration"},
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ func TestGenerateAndLoadRoundTrip(t *testing.T) {
|
|||||||
if _, err := os.Stat(path); err != nil {
|
if _, err := os.Stat(path); err != nil {
|
||||||
t.Fatalf("settings.ini was not written: %v", err)
|
t.Fatalf("settings.ini was not written: %v", err)
|
||||||
}
|
}
|
||||||
if got := cfg.Section("Server").Key("SMTP_PORT").String(); got != "4025" {
|
if got := cfg.Section("Server").Key("SMTP_PORT").String(); got != "25" {
|
||||||
t.Errorf("SMTP_PORT = %q, want 4025", got)
|
t.Errorf("SMTP_PORT = %q, want 25", got)
|
||||||
}
|
}
|
||||||
if got := cfg.Section("Attachments").Key("attachments_path").String(); got == "" {
|
if got := cfg.Section("Attachments").Key("attachments_path").String(); got == "" {
|
||||||
t.Error("Attachments.attachments_path default is missing (the approved bug fix)")
|
t.Error("Attachments.attachments_path default is missing (the approved bug fix)")
|
||||||
@@ -29,8 +29,8 @@ func TestGenerateAndLoadRoundTrip(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Load (existing): %v", err)
|
t.Fatalf("Load (existing): %v", err)
|
||||||
}
|
}
|
||||||
if got := cfg2.Section("Server").Key("SMTP_PORT").String(); got != "4025" {
|
if got := cfg2.Section("Server").Key("SMTP_PORT").String(); got != "25" {
|
||||||
t.Errorf("second Load: SMTP_PORT = %q, want 4025", got)
|
t.Errorf("second Load: SMTP_PORT = %q, want 25", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -138,8 +138,8 @@ func main() {
|
|||||||
var smtpRunning atomic.Bool
|
var smtpRunning atomic.Bool
|
||||||
|
|
||||||
runSMTP := func() {
|
runSMTP := func() {
|
||||||
smtpPort := cfg.Section("Server").Key("SMTP_PORT").MustInt(4025)
|
smtpPort := cfg.Section("Server").Key("SMTP_PORT").MustInt(25)
|
||||||
smtpTLSPort := cfg.Section("Server").Key("SMTP_TLS_PORT").MustInt(40465)
|
smtpTLSPort := cfg.Section("Server").Key("SMTP_TLS_PORT").MustInt(465)
|
||||||
banner := smtpserver.ResolveBanner(cfg, heloHostname)
|
banner := smtpserver.ResolveBanner(cfg, heloHostname)
|
||||||
|
|
||||||
// Bind IPv4-only ("0.0.0.0"), matching both listeners in server_runner.py
|
// Bind IPv4-only ("0.0.0.0"), matching both listeners in server_runner.py
|
||||||
@@ -175,8 +175,8 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
runIMAP := func() {
|
runIMAP := func() {
|
||||||
imapPort := cfg.Section("IMAP").Key("IMAP_PORT").MustInt(1143)
|
imapPort := cfg.Section("IMAP").Key("IMAP_PORT").MustInt(143)
|
||||||
imapTLSPort := cfg.Section("IMAP").Key("IMAP_TLS_PORT").MustInt(1993)
|
imapTLSPort := cfg.Section("IMAP").Key("IMAP_TLS_PORT").MustInt(993)
|
||||||
|
|
||||||
plainServer := imapserver.NewPlainServer(imapBackend)
|
plainServer := imapserver.NewPlainServer(imapBackend)
|
||||||
tlsServer := imapserver.NewTLSServer(imapBackend, tlsConfig)
|
tlsServer := imapserver.NewTLSServer(imapBackend, tlsConfig)
|
||||||
|
|||||||
Reference in New Issue
Block a user