Files
webmail-client/docker/docker-compose.yml
T
lukasandClaude Opus 5.5 b88855931e M6 hosted mailboxes (off by default); dark message mode; fixes
Hosted mailboxes (SPEC App. D), behind HOSTED_MAILBOXES (off: unchanged):
- Stalwart mail server, provisioned by `python -m app.hosted.provision`;
  directory read from a Postgres view, app passwords only for mail apps
- native account per user, the worker signs in as master for the user
- raw messages of hosted mailboxes only cached, fetched back on demand
- migrations 0025 (accounts.kind, directory view), 0026 (IMAP ids bigint)

Mail:
- dark message mode switch in the reading pane (Thunderbird's algorithm)
- moves and folder creation quote the folder name ("Deleted Items"):
  swipe-to-delete / Trash no longer bounce back
- thread unread counters: lock before recount, sorted lock order,
  UIDVALIDITY re-import recounts, nightly repair_threads
- sidebar accounts sorted alphabetically, more space above headings

Settings: Security headings renamed; antispam accounts are explicit
(none ticked = none filtered), input widths, Activity hidden.
Plugin bundles served with Cache-Control: no-cache.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:25:10 +02:00

179 lines
6.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
# Must match the rspamd service below, or the controller 401s every learn.
RSPAMD_CONTROLLER_PASSWORD: ${RSPAMD_CONTROLLER_PASSWORD:-devpass}
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:
# rspamc, not wget/curl: the rspamd image ships neither, so the obvious
# HTTP probe fails 127 forever and the container sits "unhealthy" while
# the engine is answering perfectly.
test: ["CMD-SHELL", "rspamc stat > /dev/null 2>&1"]
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
# Hosted mailboxes (SPEC Appendix D). Everything but where its database is
# (config.json) lives in that database and is set up by
# `python -m app.hosted.provision`, which also creates the database and its
# role — so the first start waits (restarting) until that has run once.
# Dev publishes the ports on loopback, off the standard numbers.
stalwart:
image: stalwartlabs/stalwart:v0.16.19
environment:
STALWART_DB_PASSWORD: ${STALWART_DB_PASSWORD:-stalwart_dev}
STALWART_RECOVERY_ADMIN: admin:${STALWART_ADMIN_SECRET:-stalwart_admin_dev}
volumes:
- ./stalwart/config.json:/etc/stalwart/config.json:ro
- stalwartdata:/var/lib/stalwart
ports:
- "127.0.0.1:8180:8080" # management API (JMAP)
- "127.0.0.1:1025:25" # SMTP — mail from "outside"
- "127.0.0.1:1465:465" # submissions
- "127.0.0.1:1587:587" # submission (STARTTLS)
- "127.0.0.1:1993:993" # IMAPS
depends_on:
postgres: { condition: service_healthy }
restart: unless-stopped
# Test mail servers (integration tests + manual dev against a throwaway mailbox)
# Spelling and grammar checking for the compose window (POST /api/spellcheck
# forwards to it; the browser never talks to it directly). No n-gram data:
# it would add several GB per language for a modest gain in grammar checks.
languagetool:
image: erikvl87/languagetool:6.8
environment:
Java_Xms: 256m
Java_Xmx: 1g
ports:
- "127.0.0.1:8010:8010"
healthcheck:
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:8010/v2/languages || exit 1"]
interval: 10s
timeout: 5s
retries: 12
restart: unless-stopped
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:
stalwartdata: