Everything needed to run the app on an Intel NAS that is also doing other jobs: one published port, no host networking, memory/CPU caps and log rotation on every container, and all state in bind mounts under DATA_ROOT so mail survives image updates, project deletion in the NAS UI, and full reinstalls — and so Hyper Backup/HBS3 can actually see it. - docker/Dockerfile.backend: multi-stage, non-root (uid 10001), tini for the <10s graceful lease release. Installs the four plugins as entry-point distributions. One image, several roles via docker/entrypoint.sh (api | worker-sync | worker-jobs | migrate | reindex | plugins). - docker/Dockerfile.frontend + nginx.conf: SPA + /api reverse proxy with the WebSocket upgrade for /api/ws, and X-Forwarded-Proto preserved so the API still emits __Host-/Secure cookies when TLS is terminated by the NAS reverse proxy (identity/cookies.py keys off request scheme). - docker/docker-compose.prod.yml: project name webmail-prod, NOT webmail — docker-compose.yml (dev services) owns that name, and two compose files sharing one project name fight over each other's containers. Meilisearch is an opt-in profile; SEARCH_BACKEND=pg is the default so a small NAS pays nothing for search. - migrate runs as a one-shot (core Alembic head + every plugin branch) that everything else waits on, so a bad migration can never leave new code running against an old schema. - docker/backup.sh: nightly pg_dump verified with `pg_restore --list` before it counts as a backup, with retention and a baseline on first start. - deploy/webmail.sh: init/doctor/up/down/status/logs/update/rollback/ backup/restore/reindex/psql/shell. `update` is backup -> images -> migrate -> roll workers, api, web -> health-gate, with automatic rollback to the previous version if the gate fails. `doctor` refuses to be quiet when DATA_ROOT is on NFS/SMB (silent Postgres corruption). - deploy/build-push.sh: build images off-NAS (the frontend build wants ~2GB RAM) and push to Gitea, or --save a tar to carry over. - scripts/reindex_search.py: rebuild the search index from Postgres alone. Every SearchDoc field is already stored, so the index is derived data that never needs backing up — this closes the only real objection to running Meilisearch on a box whose volumes may be wiped. - pyproject: package-data for accounts/presets.yaml and the shared plugin Alembic env. Without it a non-editable install (i.e. any image) silently ships a package that fails at runtime. - GET /api/version, used by the deploy health gate. Verified end to end on a real stack: fresh install, all services healthy, core + 4 plugin migration branches applied, login/SPA/plugin bundles through nginx, WS upgrade returning 101, __Host- cookies under a simulated TLS proxy, backup -> restore round-trip preserving user and plugin schema, failed-update auto-rollback (both a bad migration and a boot-crashing API), and Meilisearch profile serving a live query. ruff + mypy --strict clean, 363/363 backend tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nux3LCu4zP9nih4BRhPyHw
86 lines
2.4 KiB
TOML
86 lines
2.4 KiB
TOML
[build-system]
|
|
requires = ["setuptools>=68", "wheel"]
|
|
build-backend = "setuptools.build_meta"
|
|
|
|
[project]
|
|
name = "webmail-backend"
|
|
version = "0.1.0"
|
|
description = "Webmail client backend (sync-first, see webmail-client-spec.md)"
|
|
requires-python = "==3.11.*"
|
|
# Pinned per SPEC §3. Do not swap libraries or add deps without a `# DEPENDENCY:` comment.
|
|
dependencies = [
|
|
"fastapi~=0.115",
|
|
"uvicorn[standard]~=0.30",
|
|
"sqlalchemy[asyncio]~=2.0",
|
|
"asyncpg~=0.29",
|
|
"alembic~=1.13",
|
|
"pydantic~=2.8",
|
|
"pydantic-settings~=2.4",
|
|
"arq~=0.26",
|
|
"redis~=5.0",
|
|
"aioimaplib~=1.1",
|
|
"imapclient~=3.0",
|
|
"aiosmtplib~=3.0",
|
|
"argon2-cffi~=23.1",
|
|
"pyotp~=2.9",
|
|
"dnspython~=2.6",
|
|
"httpx~=0.27",
|
|
"nh3~=0.2",
|
|
"cryptography~=43.0",
|
|
"mail-parser~=4.0",
|
|
"meilisearch-python-sdk==2.7.0", # last release that fits the pinned Meilisearch v1.8
|
|
"python-multipart~=0.0.9",
|
|
# DEPENDENCY: SPEC §6.2 mandates accounts/presets.yaml (config, not code); the
|
|
# stdlib has no YAML parser, so PyYAML is required to load it.
|
|
"pyyaml~=6.0",
|
|
# DEPENDENCY: autoconfig/ISPDB return XML from untrusted servers; defusedxml
|
|
# blocks XXE/billion-laughs that stdlib ElementTree is vulnerable to (§10 hostile input).
|
|
"defusedxml~=0.7",
|
|
]
|
|
|
|
[project.optional-dependencies]
|
|
dev = [
|
|
"ruff~=0.6",
|
|
"mypy~=1.11",
|
|
"pytest~=8.3",
|
|
"pytest-asyncio~=0.24",
|
|
"respx~=0.21",
|
|
"aiosqlite~=0.20",
|
|
]
|
|
|
|
[tool.setuptools.packages.find]
|
|
where = ["."]
|
|
include = ["app*", "workers*"]
|
|
|
|
# Non-Python files that a *non-editable* install (the Docker image) must carry:
|
|
# the account presets config (SPEC §6.2) and the shared Alembic env used by the
|
|
# per-plugin migration runner (app/plugins/migrate.py). Without these, a
|
|
# `pip install ./backend` produces a package that fails at runtime.
|
|
[tool.setuptools.package-data]
|
|
app = ["accounts/presets.yaml", "plugins/alembic/*"]
|
|
|
|
[tool.ruff]
|
|
line-length = 100
|
|
target-version = "py311"
|
|
|
|
[tool.ruff.lint]
|
|
select = ["E", "F", "I", "UP", "B", "ASYNC", "C4", "SIM"]
|
|
ignore = ["B008"] # FastAPI Depends() in defaults is idiomatic
|
|
|
|
[tool.mypy]
|
|
python_version = "3.11"
|
|
strict = true
|
|
plugins = ["pydantic.mypy"]
|
|
mypy_path = "."
|
|
explicit_package_bases = true
|
|
namespace_packages = true
|
|
|
|
[[tool.mypy.overrides]]
|
|
module = ["aioimaplib.*", "mailparser.*", "imapclient.*", "arq.*", "yaml", "defusedxml.*"]
|
|
ignore_missing_imports = true
|
|
|
|
[tool.pytest.ini_options]
|
|
asyncio_mode = "auto"
|
|
testpaths = ["tests"]
|
|
pythonpath = ["."]
|