Second-pass spam filtering per SPEC Appendix C. The engine is Rspamd in its own container; this plugin is glue and policy, never a spam engine of its own. Classification (§C.4) hooks message.new, acts only on inbox arrivals of enabled accounts, and runs deny list -> allow list -> engine -> thresholds. Every failure path ends in "deliver normally, unclassified": an unreachable engine, a missing blob, an account with no Junk folder, a failed verdict write. Nothing here may delay or lose mail, so no service declares depends_on: rspamd. Two containers, started with everything else rather than behind a profile: rspamd, and a redis of its own. The separate redis is not incidental — the app's redis holds sync leases and arq queues under noeviction, and an ever-growing Bayes corpus must never be able to fail a job enqueue. This one takes RDB snapshots and evicts, because its data is derived and retrainable. Network checks (RBL/SURBL/fuzzy) are off: they leak message-derived hashes and URLs to third parties, which a self-hosted server's users did not ask for. The engine makes no outbound connections in the default configuration. The controller password is generated into .env like MASTER_KEY. It is internal; humans will reach Rspamd through the admin panel, authenticated as the ordinary administrator, rather than typing it. local.d/groups.conf zeroes six SMTP-time symbols — DATE_IN_PAST, PREVIOUSLY_DELIVERED, HFILTER_HOSTNAME_UNKNOWN, RCVD_NO_TLS_LAST, ARC_NA, DMARC_NA — and greylisting is disabled. Measured against 398 real delivered messages, those checks were firing on 44-71% of ordinary mail and pushing 11.3% of it over the banner threshold; scoring mail at rest for facts true of every delivered message is simply the wrong question. After: median 3.09 -> 0.70, p95 7.00 -> 3.40, banner rate 11.3% -> 1.0%. The spec's 12.0/6.0 thresholds were never the problem and are unchanged. Symbols are zeroed rather than removed so verdicts stay explainable, and they matter again at the M6 edge (§C.7). Still to come in C1: the §C.6 router and UI slots. C2 adds the learning loop — until Bayes is trained, genuine scams and legitimate bulk mail overlap in the 5-8 band and no threshold separates them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018SqJYt7jD7oUEkoY68sBZo
133 lines
5.2 KiB
Bash
133 lines
5.2 KiB
Bash
# ============================================================================
|
|
# Webmail production configuration.
|
|
#
|
|
# Copy to docker/.env.prod, fill in the three REQUIRED values, keep it safe:
|
|
# cp docker/.env.prod.example docker/.env.prod && chmod 600 docker/.env.prod
|
|
#
|
|
# THIS FILE IS A BACKUP ARTEFACT. MASTER_KEY is not stored anywhere else: lose
|
|
# it and every saved mail-account password becomes permanently undecryptable,
|
|
# even from a perfect database backup. Keep a copy in a password manager.
|
|
# ============================================================================
|
|
|
|
# --- REQUIRED ---------------------------------------------------------------
|
|
|
|
# Where all persistent data lives on the host. Use a real shared folder your NAS
|
|
# backup tool can see — not a Docker named volume.
|
|
# Synology: /volume1/docker/webmail/data
|
|
# QNAP: /share/Container/webmail/data
|
|
DATA_ROOT=/volume1/docker/webmail/data
|
|
|
|
# 32 random bytes, base64. Generate ONCE, then never change it:
|
|
# openssl rand -base64 32
|
|
MASTER_KEY=
|
|
|
|
# Database password. Generate once:
|
|
# openssl rand -base64 24 | tr -d '/+=' | cut -c1-24
|
|
POSTGRES_PASSWORD=
|
|
|
|
# --- Identity ---------------------------------------------------------------
|
|
# The domain your user handles are minted under (user@APP_DOMAIN) and the issuer
|
|
# shown in 2FA authenticator apps. It does NOT have to be publicly resolvable.
|
|
APP_DOMAIN=mail.home.lan
|
|
|
|
# --- Network ----------------------------------------------------------------
|
|
# Two published ports, same container. Avoid 5000/5001 (Synology DSM) and
|
|
# 8080 (QNAP QTS).
|
|
#
|
|
# WEB_PORT is the mail app. The admin API is refused on it, so this is the one
|
|
# you may expose beyond the LAN.
|
|
WEB_PORT=8088
|
|
# ADMIN_PORT is the same app with the admin API reachable. Restrict it to your
|
|
# LAN in the NAS firewall — see docs/nas-install.md §5.1. Publishing it is not
|
|
# what protects it.
|
|
ADMIN_PORT=8089
|
|
# Bind to one NAS interface instead of every network, e.g. WEB_BIND=192.168.1.185
|
|
WEB_BIND=0.0.0.0
|
|
ADMIN_BIND=0.0.0.0
|
|
|
|
TZ=Europe/Warsaw
|
|
|
|
# --- Anti-spam (SPEC Appendix C) ---------------------------------------------
|
|
# Generated by `webmail.sh init`. Internal only: it authenticates the app to the
|
|
# Rspamd controller. Humans never type it — the admin panel gates Rspamd behind
|
|
# the ordinary administrator account.
|
|
RSPAMD_CONTROLLER_PASSWORD=
|
|
# Network checks (RBL/SURBL/fuzzy) are OFF in docker/rspamd/local.d by default:
|
|
# they leak message-derived hashes and URLs to third parties. Enable the modules
|
|
# there first if you want them.
|
|
RSPAMD_MEM_LIMIT=256m
|
|
RSPAMD_REDIS_MEM_LIMIT=160m
|
|
RSPAMD_REDIS_MAXMEMORY=128mb
|
|
|
|
# --- Search -----------------------------------------------------------------
|
|
# pg = Postgres full-text. No extra container, no extra RAM. Default.
|
|
# meili = Meilisearch: typo tolerance, better ranking, faster on large mailboxes.
|
|
# Costs a container and ~0.7-1 GB RAM. Start it with:
|
|
# ./deploy/webmail.sh up --meili
|
|
# and after switching an existing install, rebuild the index:
|
|
# ./deploy/webmail.sh reindex
|
|
SEARCH_BACKEND=pg
|
|
# Required if SEARCH_BACKEND=meili — generate with: openssl rand -base64 32
|
|
MEILI_MASTER_KEY=
|
|
|
|
# --- Images -----------------------------------------------------------------
|
|
# `local` = built on this host from the repo. To deploy prebuilt images from
|
|
# your Gitea registry instead, set both of these (see deploy/build-push.sh):
|
|
# REGISTRY_PREFIX=192.168.1.185:8418/claude/webmail
|
|
# IMAGE_TAG=v1.0.0
|
|
REGISTRY_PREFIX=webmail
|
|
IMAGE_TAG=local
|
|
|
|
# --- Application ------------------------------------------------------------
|
|
LOG_LEVEL=INFO
|
|
UNDO_SECONDS=10
|
|
SESSION_TTL_DAYS=30
|
|
MAX_ATTACHMENT_MB=25
|
|
# Auto-purge Trash/Spam older than N days (0 disables).
|
|
TRASH_RETENTION_DAYS=30
|
|
# Server-side sender avatars (Gravatar + favicon), fetched by the server so
|
|
# senders never see your IP. Set false to make zero outbound web requests.
|
|
SENDER_AVATARS_ENABLED=true
|
|
|
|
GOOGLE_OAUTH_ENABLED=false
|
|
OAUTH_GOOGLE_CLIENT_ID=
|
|
OAUTH_GOOGLE_CLIENT_SECRET=
|
|
OAUTH_MS_CLIENT_ID=
|
|
OAUTH_MS_CLIENT_SECRET=
|
|
|
|
# Plugins whose schema is migrated on every start. Remove one to stop shipping
|
|
# its tables; `hello` is the SDK demo and is intentionally not listed.
|
|
ENABLED_PLUGINS="calendar contacts labels filters"
|
|
|
|
# --- Backups ----------------------------------------------------------------
|
|
BACKUP_AT=03:30
|
|
BACKUP_KEEP_DAYS=14
|
|
|
|
# --- Sizing (defaults suit a personal install on a shared NAS) --------------
|
|
# Peak worker RAM is roughly PARSE_CONCURRENCY x STREAM_PARSE_MB plus overhead.
|
|
INTERACTIVE_CONCURRENCY=8
|
|
BULK_CONCURRENCY=1
|
|
PARSE_CONCURRENCY=2
|
|
STREAM_PARSE_MB=8
|
|
PER_HOST_CONN_CAP=10
|
|
HOT_BUDGET_PER_WORKER=50
|
|
|
|
# Container caps. Memory only: CPU limits (`cpus:`) are deliberately absent
|
|
# because Synology's Docker daemon rejects them (NanoCPUs).
|
|
# Container caps. Total without Meilisearch: ~2.2 GB of *limits*; real steady
|
|
# usage for one or two mail accounts is around 700 MB-1 GB.
|
|
PG_MEM_LIMIT=768m
|
|
PG_SHARED_BUFFERS=256MB
|
|
PG_MAX_CONNECTIONS=100
|
|
PG_WORK_MEM=8MB
|
|
PG_MAINTENANCE_WORK_MEM=128MB
|
|
PG_EFFECTIVE_CACHE_SIZE=768MB
|
|
REDIS_MEM_LIMIT=160m
|
|
REDIS_MAXMEMORY=96mb
|
|
API_MEM_LIMIT=512m
|
|
WORKER_SYNC_MEM_LIMIT=512m
|
|
WORKER_JOBS_MEM_LIMIT=640m
|
|
WEB_MEM_LIMIT=96m
|
|
BACKUP_MEM_LIMIT=256m
|
|
MEILI_MEM_LIMIT=1g
|