Dev compose gains rspamd + redis-rspamd, matching production. The dev API and worker run on the host rather than in the compose network, so the ports are published on loopback and RSPAMD_URL points at 127.0.0.1 — documented in .env.example. Contacts now cap a verdict at "banner": mail from someone in the user's address book is never moved to Junk. Capped rather than exempted, because the way this fails in practice is a contact's account being compromised and spamming its own address book — the warning still has value even when the move would be wrong. An explicit deny rule still wins; that is the user overruling us deliberately. The lookup reads the contacts plugin's table directly and forgives its absence: no contacts plugin means no contacts to check, not an error. Addresses are stored as the user typed them, so both sides are lower-cased rather than relying on the jsonb containment index, and secondary addresses in the emails array count as well as the primary. It runs only when the verdict would otherwise be a move, so the common path pays nothing. Also fixes a test that asserted stats degrade "without an engine" by relying on no engine being present — true until this commit. It now simulates the absence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018SqJYt7jD7oUEkoY68sBZo
133 lines
4.1 KiB
YAML
133 lines
4.1 KiB
YAML
name: webmail
|
|
services:
|
|
postgres:
|
|
image: postgres:16
|
|
environment:
|
|
POSTGRES_USER: webmail
|
|
POSTGRES_PASSWORD: webmail_dev
|
|
POSTGRES_DB: webmail_dev
|
|
ports: ["127.0.0.1:5433:5432"]
|
|
volumes: [pgdata:/var/lib/postgresql/data]
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U webmail -d webmail_dev"]
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 10
|
|
|
|
redis:
|
|
image: redis:7
|
|
ports: ["127.0.0.1:6380:6379"]
|
|
healthcheck:
|
|
test: ["CMD", "redis-cli", "ping"]
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 10
|
|
|
|
meilisearch:
|
|
image: getmeili/meilisearch:v1.8
|
|
environment:
|
|
MEILI_MASTER_KEY: dev_master_key_change_me
|
|
MEILI_ENV: development
|
|
ports: ["127.0.0.1:7700:7700"]
|
|
volumes: [meilidata:/meili_data]
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:7700/health | grep -q available"]
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 10
|
|
|
|
# Sync-engine v2 workers (refactor §1): two roles sharing one image. Scale the
|
|
# sync role with `--scale worker-sync=N` (lease protocol prevents double-owning).
|
|
worker-sync:
|
|
build: { context: ../backend }
|
|
command: ["python", "-m", "workers.run"]
|
|
environment: &worker-env
|
|
WORKER_ROLE: sync
|
|
APP_DOMAIN: dev.webmail.local
|
|
DATABASE_URL: postgresql+asyncpg://webmail:webmail_dev@postgres:5432/webmail_dev
|
|
REDIS_URL: redis://redis:6379/0
|
|
MASTER_KEY: CHANGE_ME_base64_32_bytes
|
|
BLOB_ROOT: /var/blobs
|
|
volumes: [blobs:/var/blobs]
|
|
ulimits: { nofile: 65536 }
|
|
depends_on:
|
|
postgres: { condition: service_healthy }
|
|
redis: { condition: service_healthy }
|
|
|
|
worker-jobs:
|
|
build: { context: ../backend }
|
|
command: ["python", "-m", "workers.run"]
|
|
environment:
|
|
<<: *worker-env
|
|
WORKER_ROLE: jobs
|
|
volumes: [blobs:/var/blobs]
|
|
ulimits: { nofile: 65536 }
|
|
depends_on:
|
|
postgres: { condition: service_healthy }
|
|
redis: { condition: service_healthy }
|
|
|
|
# Anti-spam engine, mirroring the production stack (see ../docker-compose.yml).
|
|
# Dev publishes the ports on loopback so the plugin can be driven from the host
|
|
# (RSPAMD_URL=http://127.0.0.1:11333) as well as from a container.
|
|
rspamd:
|
|
image: rspamd/rspamd:3.9
|
|
# Root only long enough to write the controller password; rspamd then drops
|
|
# to _rspamd itself. /etc/rspamd is root-owned in the image.
|
|
user: root
|
|
entrypoint:
|
|
- /bin/sh
|
|
- -c
|
|
- |
|
|
mkdir -p /etc/rspamd/override.d
|
|
printf 'password = "%s";\nenable_password = "%s";\n' \
|
|
"$$RSPAMD_CONTROLLER_PASSWORD" "$$RSPAMD_CONTROLLER_PASSWORD" \
|
|
> /etc/rspamd/override.d/worker-controller.inc
|
|
exec /usr/bin/rspamd -f -u _rspamd -g _rspamd
|
|
environment:
|
|
RSPAMD_CONTROLLER_PASSWORD: ${RSPAMD_CONTROLLER_PASSWORD:-devpass}
|
|
volumes:
|
|
- ./rspamd/local.d:/etc/rspamd/local.d:ro
|
|
- rspamddata:/var/lib/rspamd
|
|
ports:
|
|
- "127.0.0.1:11333:11333" # scan worker (/checkv2)
|
|
- "127.0.0.1:11334:11334" # controller (/learnspam, /stat)
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:11334/ping | grep -qi pong"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 12
|
|
start_period: 15s
|
|
depends_on:
|
|
redis-rspamd: { condition: service_healthy }
|
|
|
|
# Bayes/fuzzy storage, separate from the app redis on purpose (SPEC §C.2).
|
|
redis-rspamd:
|
|
image: redis:7-alpine
|
|
command: [redis-server, --save, "300", "1", --maxmemory, 128mb, --maxmemory-policy, allkeys-lru]
|
|
volumes:
|
|
- rspamdredis:/data
|
|
healthcheck:
|
|
test: ["CMD", "redis-cli", "ping"]
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 10
|
|
|
|
# Test mail servers (integration tests + manual dev against a throwaway mailbox)
|
|
greenmail:
|
|
image: greenmail/standalone:2.0.1
|
|
environment:
|
|
GREENMAIL_OPTS: >-
|
|
-Dgreenmail.setup.test.all -Dgreenmail.hostname=0.0.0.0
|
|
-Dgreenmail.users=dev:devpass@dev.local -Dgreenmail.verbose
|
|
ports:
|
|
- "127.0.0.1:3025:3025" # SMTP
|
|
- "127.0.0.1:3143:3143" # IMAP
|
|
- "127.0.0.1:3993:3993" # IMAPS
|
|
|
|
volumes:
|
|
pgdata:
|
|
meilidata:
|
|
blobs:
|
|
rspamddata:
|
|
rspamdredis:
|