From 5d4e7147ceb4851be71973457349974e144fb06d Mon Sep 17 00:00:00 2001 From: Lucas Rayan Date: Fri, 30 Oct 2026 10:07:21 -0300 Subject: [PATCH 1/4] ci: cache what is rebuilt on every run npm and Go module caches, the embedded Postgres binaries, Trivy's database, and the daemon's own layer cache for image builds. The webmail Next build is no longer run a second time outside the Dockerfile. Co-Authored-By: Claude Sonnet 5.5 --- .gitea/workflows/ci.yml | 60 ++++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 053d5b3..2e27a75 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -70,6 +70,17 @@ jobs: docker rm "$cid" >/dev/null exit "$status" - run: go vet ./... + # Module and build caches live under the unprivileged user's home, which + # setup-go's cache does not cover, and the embedded Postgres binaries are + # otherwise downloaded again by every test that starts one. + - uses: actions/cache@v4 + with: + path: | + /home/tester/go/pkg/mod + /home/tester/.cache/go-build + /home/tester/.embedded-postgres-go + key: go-tester-${{ runner.arch }}-${{ hashFiles('services/protocols/go.sum') }} + restore-keys: go-tester-${{ runner.arch }}- # The end-to-end suites start an embedded Postgres, and initdb refuses to # run as root, which is what a job container is. Run the tests as a # throwaway unprivileged user (GitHub's hosted runners already are one). @@ -78,7 +89,7 @@ jobs: TEST_DATABASE_URL: postgres://lambdamail_test:lambdamail_test@localhost:5432/lambdamail_test?sslmode=disable run: | id tester >/dev/null 2>&1 || useradd -m -u 1001 tester - chown -R tester:tester "$GITHUB_WORKSPACE" + chown -R tester:tester "$GITHUB_WORKSPACE" /home/tester runuser -u tester -- env "HOME=/home/tester" "PATH=$PATH" \ "TEST_DATABASE_URL=$TEST_DATABASE_URL" go test ./... -v @@ -105,7 +116,10 @@ jobs: steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 - with: { node-version: "20" } + with: + node-version: "20" + cache: npm + cache-dependency-path: services/auth/package-lock.json # The auth suite asserts against real rows: password verification, # lockout, TOTP replay and recovery-code consumption are all decided in # the database, so a mock would prove nothing. @@ -139,13 +153,16 @@ jobs: steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 - with: { node-version: "20" } + with: + node-version: "20" + cache: npm + cache-dependency-path: services/storage/package-lock.json - run: npm install - run: npm run lint - run: npm test webmail-build: - name: Build (webmail) + name: Testes (webmail) runs-on: ubuntu-latest defaults: run: @@ -153,11 +170,15 @@ jobs: steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 - with: { node-version: "20" } + with: + node-version: "20" + cache: npm + cache-dependency-path: services/webmail/package-lock.json - run: npm install - run: npm run lint - run: npm test - - run: npm run build + # No `npm run build` here: the webmail image job runs the same Next build + # inside its Dockerfile, so building it twice only doubles the wait. security: name: Trivy (dependências, segredos e Dockerfiles) @@ -174,7 +195,7 @@ jobs: # which on Gitea is not GitHub. - name: Repositório run: | - cid=$(docker create --network host -w /src aquasec/trivy:0.70.0 \ + cid=$(docker create --network host -v trivy-cache:/root/.cache/ -w /src aquasec/trivy:0.70.0 \ fs --scanners vuln,secret,misconfig --severity HIGH,CRITICAL --exit-code 1 \ --skip-dirs services/auth/node_modules,services/storage/node_modules,services/webmail/node_modules,services/webmail/.next .) docker cp . "$cid:/src" @@ -195,7 +216,7 @@ jobs: # scanner something people learn to skip. - name: Repositório (MEDIUM, informativo) run: | - cid=$(docker create --network host -w /src aquasec/trivy:0.70.0 \ + cid=$(docker create --network host -v trivy-cache:/root/.cache/ -w /src aquasec/trivy:0.70.0 \ fs --scanners vuln,secret --severity MEDIUM --exit-code 0 \ --skip-dirs services/auth/node_modules,services/storage/node_modules,services/webmail/node_modules,services/webmail/.next .) docker cp . "$cid:/src" @@ -214,18 +235,21 @@ jobs: steps: - uses: actions/checkout@v7 - - uses: docker/setup-buildx-action@v4 - # Built but not pushed: this proves each Dockerfile still works on every # branch, while publishing stays in release.yml. + # + # Plain `docker build` on purpose. setup-buildx would start a fresh + # BuildKit container per job and throw its layer cache away; the daemon's + # own builder keeps the cache in the runner's persistent volume, and the + # "Stack completa" job reuses it for the same Dockerfiles. - name: Build - uses: docker/build-push-action@v7 - with: - context: ${{ (matrix.service == 'migrations' || matrix.service == 'rspamd') && '.' || format('services/{0}', matrix.service) }} - file: services/${{ matrix.service }}/Dockerfile - push: false - load: true - tags: lambdamail-${{ matrix.service }}:ci + run: | + case '${{ matrix.service }}' in + migrations|rspamd) context=. ;; + *) context=services/${{ matrix.service }} ;; + esac + docker build -f services/${{ matrix.service }}/Dockerfile \ + -t lambdamail-${{ matrix.service }}:ci "$context" # Scanned only for the images whose contents we author. migrations and # rspamd add .sql and .conf files to an upstream image and nothing else, @@ -236,7 +260,7 @@ jobs: - name: Trivy na imagem if: matrix.service != 'migrations' && matrix.service != 'rspamd' run: | - docker run --rm --network host -e DOCKER_HOST=tcp://127.0.0.1:2375 aquasec/trivy:0.70.0 \ + docker run --rm --network host -v trivy-cache:/root/.cache/ -e DOCKER_HOST=tcp://127.0.0.1:2375 aquasec/trivy:0.70.0 \ image --severity HIGH,CRITICAL --exit-code 1 lambdamail-${{ matrix.service }}:ci compose-up: From 1aac8be531a1bd00430baaf8ce615fd44d46c668 Mon Sep 17 00:00:00 2001 From: Lucas Rayan Date: Fri, 30 Oct 2026 11:22:08 -0300 Subject: [PATCH 2/4] ci: create a Gitea release when a v* tag is pushed The Release workflow only ever published container images; there was no entry under Releases. The new job runs after every image is published and writes the image list and the commit log since the previous tag. Co-Authored-By: Claude Sonnet 5.5 --- .gitea/workflows/release.yml | 40 ++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 995e1cd..e54dc8e 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -165,3 +165,43 @@ jobs: echo "${{ steps.meta.outputs.tags }}" echo '```' } >> "$GITHUB_STEP_SUMMARY" + + # Cria a entrada em "Releases" do repositório. Só roda quando uma tag v* é + # enviada, depois que todas as imagens foram publicadas. Usa o token do próprio + # Actions, então não precisa de segredo extra. + gitea-release: + name: Cria a release no Gitea + needs: [publish, prepare] + if: startsWith(gitea.ref, 'refs/tags/v') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - name: Release + env: + TAG: ${{ gitea.ref_name }} + TOKEN: ${{ gitea.token }} + API: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }} + NAMESPACE: ${{ env.REGISTRY }}/${{ needs.prepare.outputs.namespace }} + run: | + prev=$(git describe --tags --abbrev=0 "$TAG^" 2>/dev/null || true) + range=${prev:+$prev..}$TAG + { + echo "## Imagens" + echo + echo '```' + for s in protocols auth storage webmail migrations rspamd; do + echo "$NAMESPACE/$s:${TAG#v}" + done + echo '```' + echo + echo "## Mudanças${prev:+ desde $prev}" + echo + git log --no-merges --format='- %s (%h)' "$range" + } > notes.md + pre=false; case "$TAG" in *-*) pre=true ;; esac + jq -n --arg tag "$TAG" --arg name "LambdaMail ${TAG#v}" --rawfile body notes.md --argjson pre "$pre" \ + '{tag_name:$tag, name:$name, body:$body, prerelease:$pre}' > release.json + curl -fsS -X POST "$API/releases" -H "Authorization: token $TOKEN" \ + -H 'Content-Type: application/json' -d @release.json From f500534ac662e6a25c8960697df1634b6c9f5ebf Mon Sep 17 00:00:00 2001 From: Lucas Rayan Date: Fri, 30 Oct 2026 11:23:00 -0300 Subject: [PATCH 3/4] chore: keep .superset out of the repository It is local workspace configuration for a tool, not part of the project. Co-Authored-By: Claude Sonnet 5.5 --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index ddad1f7..8f64352 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,6 @@ Thumbs.db *.json.gz *.xml.gz *.zip.gz + +# Configuração local do Superset +.superset/ From 5bbbb8512884bc8fcb1dd39ff45c145159ecf257 Mon Sep 17 00:00:00 2001 From: Lucas Rayan Date: Fri, 30 Oct 2026 11:36:22 -0300 Subject: [PATCH 4/4] release: 1.0.0, a changelog, one .env.example and the docs to match - CHANGELOG.md and CHANGELOG.pt-BR.md, from the project history. - .env.example now serves development and production; scripts/init-env.sh (make env) creates .env with random secrets. .env.production.example is gone. - docs/RELEASING.md: the pipeline, how a version is cut and deployed, the runners. - DEPLOYMENT.md and both READMEs updated for Gitea and the new env flow. - Service versions set to 1.0.0. - CI: the proxy network step is idempotent and the stack is torn down always, since the runner's Docker daemon now outlives a job. Co-Authored-By: Claude Sonnet 5.5 --- .env.example | 139 ++++++++++++++++++++--------- .env.production.example | 104 --------------------- .gitea/workflows/ci.yml | 15 +++- CHANGELOG.md | 105 ++++++++++++++++++++++ CHANGELOG.pt-BR.md | 106 ++++++++++++++++++++++ Makefile | 6 +- README.md | 14 +-- README.pt-BR.md | 13 +-- docs/DEPLOYMENT.md | 33 ++++--- docs/RELEASING.md | 88 ++++++++++++++++++ scripts/init-env.sh | 65 ++++++++++++++ services/auth/package-lock.json | 4 +- services/auth/package.json | 2 +- services/storage/package-lock.json | 4 +- services/storage/package.json | 2 +- services/webmail/package-lock.json | 4 +- services/webmail/package.json | 2 +- 17 files changed, 524 insertions(+), 182 deletions(-) delete mode 100644 .env.production.example create mode 100644 CHANGELOG.md create mode 100644 CHANGELOG.pt-BR.md create mode 100644 docs/RELEASING.md create mode 100755 scripts/init-env.sh diff --git a/.env.example b/.env.example index e0cf2e1..dff3346 100644 --- a/.env.example +++ b/.env.example @@ -1,81 +1,132 @@ -# --- Container images --- -# Published to the Gitea registry by .gitea/workflows/release.yml, multi-architecture -# (linux/amd64 and linux/arm64). The deploy host pulls these instead of -# compiling: `docker compose pull && docker compose up -d`. Running -# `docker compose build` locally still overrides them with a local build. -# Pin IMAGE_TAG to a released version (e.g. 1.2.3) or a sha- tag to roll back. +# LambdaMail - environment +# +# One file for local development and for production. +# +# scripts/init-env.sh copy this to .env and generate the secrets +# scripts/init-env.sh --production the same, plus a list of what you still +# have to set before this is a real server +# +# Values below are the local-development defaults. Lines marked PRODUCTION say +# what a real deployment needs. In Coolify, paste the finished .env into the +# environment editor instead of keeping a file on the host. +# +# The secrets are left empty on purpose. The script fills them with random +# values, and compose refuses to start while one is empty, because a default +# secret in a published repository is not a secret. + +# --- Container images ------------------------------------------------------- +# Published to the Gitea registry by .gitea/workflows/release.yml for +# linux/amd64 and linux/arm64. The deploy host pulls instead of compiling: +# `docker compose pull && docker compose up -d`. `make up` builds from source +# through the build overlay and ignores these. +# PRODUCTION: pin IMAGE_TAG to a release (1.0.0) or to a sha- tag. Never +# leave `latest` on a server: a redeploy then silently changes what runs. IMAGE_NAMESPACE=git.lucasrguerra.dev.br/lucasrguerra/lambdamail IMAGE_TAG=latest -# --- Postgres --- -POSTGRES_DB=lambdamail -POSTGRES_USER=lambdamail -POSTGRES_PASSWORD=change-me-in-local-dev - -# --- Redis --- -REDIS_PASSWORD=change-me-in-local-dev - -# --- Shared secrets (local dev values — never reuse in production) --- -JWT_SECRET=local-dev-jwt-secret-change-me-at-least-32-chars -# Encrypts DKIM private keys and provider tokens at rest. Losing it makes every -# stored secret unrecoverable — back it up separately from the database. -LAMBDAMAIL_MASTER_KEY=local-dev-master-key-32-bytes-minimum-value - -# --- Host identity --- -PRIMARY_MAIL_HOST=mail.localhost +# --- Identity --------------------------------------------------------------- +# MAIL_DOMAIN is the domain you send and receive as. +# PRIMARY_MAIL_HOST is the host the MX record points at; its PTR must match it. +# PRODUCTION: MAIL_DOMAIN=example.com PRIMARY_MAIL_HOST=mail.example.com +# WEBMAIL_HOST=mail.example.com PUBLIC_IPV4= MAIL_DOMAIN=localhost +PRIMARY_MAIL_HOST=mail.localhost +WEBMAIL_HOST=localhost:3000 +# Optional separate host for /admin. +ADMIN_HOST= PUBLIC_IPV4=127.0.0.1 +# Leave empty unless IPv6 has a matching PTR and is covered by SPF. A half +# configured IPv6 is a common cause of rejection (PLAN.md premises). PUBLIC_IPV6= -# --- DNS automation (Cloudflare) --- +# --- Secrets ---------------------------------------------------------------- +# Filled in by scripts/init-env.sh. To do it by hand: +# +# POSTGRES_PASSWORD, REDIS_PASSWORD openssl rand -hex 24 +# JWT_SECRET, LAMBDAMAIL_MASTER_KEY openssl rand -hex 32 +# +# The first two are substituted into connection URIs (postgres://user:PASS@db/... +# and redis://:PASS@redis:6379), so they must hold no character that means +# something in a URL: use hex, NOT `openssl rand -base64`, whose alphabet has "/" +# and "+". A "/" ends the authority component, the host is parsed as part of the +# password and every connection fails authentication - which shows up as the +# migrations container exiting 1 while Postgres itself reports healthy. +POSTGRES_DB=lambdamail +POSTGRES_USER=lambdamail +POSTGRES_PASSWORD= +REDIS_PASSWORD= +# Signs webmail and admin sessions. At least 32 characters; the auth service +# refuses to start below that. +JWT_SECRET= +# Encrypts DKIM private keys, TOTP secrets and provider tokens at rest. +# LOSING THIS MAKES EVERY STORED SECRET UNRECOVERABLE: back it up separately +# from the database (PLAN.md risk R9). +LAMBDAMAIL_MASTER_KEY= + +# --- DNS automation (Cloudflare) -------------------------------------------- # The token needs Zone.DNS:Edit plus Zone.Zone:Read on the zone. Leave it empty -# to disable reconciliation; the service then logs that DNS sync is off. +# to manage the records by hand; the service then logs that DNS sync is off. CLOUDFLARE_API_TOKEN= DNS_SYNC_INTERVAL_SECONDS=21600 -# --- TLS --- -# traefik: read the certificate Traefik manages (default, DANE not possible). -# acme: obtain the certificate ourselves via DNS-01. Required for DANE, -# because only this mode can reuse a pre-generated key. +# --- TLS -------------------------------------------------------------------- +# traefik: read the certificate the proxy already manages (default; DANE is not +# possible in this mode). +# acme: obtain the certificate here via DNS-01. Required for DANE, because +# only this mode can reuse a pre-generated key. TLS_MODE=traefik -# Used only when TLS_MODE=acme. Leave ACME_DIRECTORY_URL empty for Let's -# Encrypt production; point it at the staging directory while testing, since -# production rate limits can lock a domain out for a week. +# Used only when TLS_MODE=acme. Leave ACME_DIRECTORY_URL empty for Let's Encrypt +# production; point it at the staging directory while testing, since production +# rate limits can lock a domain out for a week. ACME_EMAIL= ACME_DIRECTORY_URL= +# Where the proxy keeps its certificates, and the Docker network it listens on. +# PRODUCTION (Coolify): COOLIFY_PROXY_DIR=/data/coolify/proxy COOLIFY_PROXY_DIR=./local/acme +COOLIFY_PROXY_NETWORK=coolify TRAEFIK_ACME_FILENAME=acme.json CERT_POLL_INTERVAL_SECONDS=60 -# Static certificate files, used when no ACME store is available (local dev). +# Static certificate files, used when no ACME store is available (local dev; +# `make gen-dev-cert` writes them). PROTOCOLS_CERT_PATH=/etc/lambdamail/certs/cert.pem PROTOCOLS_KEY_PATH=/etc/lambdamail/certs/key.pem PROTOCOLS_SPOOL_DIR=/var/lambdamail/spool -# --- Transport security policy --- +# --- Transport security ----------------------------------------------------- # Start at "testing": senders honour the policy but do not hard-fail on it. -# Promote to "enforce" only once TLS-RPT reports come back clean. +# Promote to "enforce" only once TLS-RPT reports come back clean; enforce with a +# broken certificate silently blackholes inbound mail. MTA_STS_MODE=testing OUTBOUND_MTA_STS_ENABLED=true -# DANE stays off unless TLS_MODE=acme. Under Traefik-managed certificates the -# key changes at every renewal and the published TLSA record stops matching, -# which permanently rejects mail at every validating MTA. +# DANE stays off unless TLS_MODE=acme. Under proxy-managed certificates the key +# changes at every renewal and the published TLSA record stops matching, which +# permanently rejects mail at every validating MTA (PLAN.md section 5.1). OUTBOUND_DANE_ENABLED=false DNSSEC_RESOLVER=1.1.1.1:53 MAX_MESSAGE_BYTES=52428800 - # ARC seals accepted messages so a downstream receiver can still tell that the # mail authenticated at the first hop. Only worth enabling on a host that # forwards mail onward. ARC_SEAL_ENABLED=false -# --- Smarthost (for hosts where outbound port 25 is blocked) --- -# Setting RELAY_HOST routes all delivery through the relay and adds it to the -# published SPF record, without which every relayed message fails SPF. +# --- Smarthost (hosts where outbound port 25 is blocked) -------------------- +# Most VPS providers block port 25. If `make preflight` reports it blocked, set a +# relay here: all delivery goes through it and it is added to the published SPF, +# without which every relayed message fails SPF. RELAY_HOST= RELAY_PORT=587 RELAY_USER= RELAY_PASS= +# The SPF term that authorises the relay, exactly as the provider documents it. +# It cannot be derived from RELAY_HOST, and getting it wrong is worse than +# leaving it out. Brevo's relay is smtp-relay.brevo.com, but brevo.com publishes +# the SPF of their corporate mail: the sending ranges are at spf.brevo.com, so +# the value there is "include:spf.brevo.com". +RELAY_SPF_INCLUDE= -# --- Webmail --- -WEBMAIL_HOST=localhost:3000 -ADMIN_HOST= +# --- Storage ---------------------------------------------------------------- +STORAGE_DRIVER=local +S3_ENDPOINT= +S3_BUCKET= +S3_ACCESS_KEY= +S3_SECRET_KEY= diff --git a/.env.production.example b/.env.production.example deleted file mode 100644 index 6d3f0e3..0000000 --- a/.env.production.example +++ /dev/null @@ -1,104 +0,0 @@ -# LambdaMail - production environment -# -# Copy this into Coolify's environment editor, or to .env on the host, and -# replace every value marked CHANGE. The stack refuses to start without the -# required ones rather than falling back to a default, because a default -# secret in a published repository is not a secret. -# -# Generate each secret with: openssl rand -base64 36 -# except POSTGRES_PASSWORD and REDIS_PASSWORD, which go inside connection URIs -# and must be URL-safe - see the note above them. - -# --- Container images ------------------------------------------------------- -# Published by .gitea/workflows/release.yml for linux/amd64 and linux/arm64. -# Pin IMAGE_TAG to a released version or a sha- tag to roll back. -IMAGE_NAMESPACE=git.lucasrguerra.dev.br/lucasrguerra/lambdamail -IMAGE_TAG=latest - -# --- Identity --------------------------------------------------------------- -# MAIL_DOMAIN is the domain you send and receive as. -# PRIMARY_MAIL_HOST is the host the MX record points at; its PTR must match. -MAIL_DOMAIN=example.com # CHANGE -PRIMARY_MAIL_HOST=mail.example.com # CHANGE -WEBMAIL_HOST=mail.example.com # CHANGE - where the web UI is served -ADMIN_HOST= # optional separate host for /admin -PUBLIC_IPV4=203.0.113.10 # CHANGE - the VPS public IPv4 -# Leave empty unless IPv6 has a matching PTR and is covered by SPF. Half -# configured IPv6 is a common cause of rejection (PLAN.md premises). -PUBLIC_IPV6= - -# --- Secrets ---------------------------------------------------------------- -# These two are substituted into connection URIs - postgres://user:PASS@db/... and -# redis://:PASS@redis:6379 - so they must contain no character that means -# something in a URL. Use hex: -# -# openssl rand -hex 24 -# -# NOT `openssl rand -base64`, whose alphabet includes "/" and "+". A "/" ends -# the authority component, so the host is parsed as part of the password and -# every connection fails authentication - which surfaces as the migrations -# container exiting 1 while Postgres itself reports healthy. -POSTGRES_DB=lambdamail -POSTGRES_USER=lambdamail -POSTGRES_PASSWORD=CHANGE-generate-with-openssl-rand-hex-24 -REDIS_PASSWORD=CHANGE-generate-with-openssl-rand-hex-24 -# Signs webmail and admin sessions. Minimum 32 characters; the auth service -# refuses to start below that. -JWT_SECRET=CHANGE-generate-with-openssl-rand-at-least-32-chars -# Encrypts DKIM private keys, TOTP secrets and provider tokens at rest. -# LOSING THIS MAKES EVERY STORED SECRET UNRECOVERABLE - back it up separately -# from the database (PLAN.md risk R9). -LAMBDAMAIL_MASTER_KEY=CHANGE-generate-with-openssl-rand-at-least-32-chars - -# --- DNS automation --------------------------------------------------------- -# Token scope: Zone.DNS:Edit plus Zone.Zone:Read on the zone. Leave empty to -# manage the 13 records by hand; the service then logs that sync is off. -CLOUDFLARE_API_TOKEN= -DNS_SYNC_INTERVAL_SECONDS=21600 - -# --- TLS -------------------------------------------------------------------- -# traefik: read the certificate Coolify's proxy already manages (default). -# acme: obtain it here via DNS-01. Required for DANE, and only then. -TLS_MODE=traefik -COOLIFY_PROXY_DIR=/data/coolify/proxy -COOLIFY_PROXY_NETWORK=coolify -TRAEFIK_ACME_FILENAME=acme.json -CERT_POLL_INTERVAL_SECONDS=60 -PROTOCOLS_SPOOL_DIR=/var/lambdamail/spool -# Used only when TLS_MODE=acme. -ACME_EMAIL= -ACME_DIRECTORY_URL= - -# --- Transport security ----------------------------------------------------- -# Start at testing. Promote to enforce only once TLS-RPT reports come back -# clean: enforce with a broken certificate silently blackholes inbound mail. -MTA_STS_MODE=testing -OUTBOUND_MTA_STS_ENABLED=true -# DANE stays off under TLS_MODE=traefik. The key changes at every renewal and -# the published TLSA record stops matching, which permanently rejects mail at -# every validating MTA (PLAN.md section 5.1). -OUTBOUND_DANE_ENABLED=false -DNSSEC_RESOLVER=1.1.1.1:53 -MAX_MESSAGE_BYTES=52428800 -ARC_SEAL_ENABLED=false - -# --- Smarthost -------------------------------------------------------------- -# Most VPS providers block outbound port 25. If the preflight reports it -# blocked, set a relay here; it is added to the published SPF automatically. -RELAY_HOST= -RELAY_PORT=587 -RELAY_USER= -RELAY_PASS= -# The SPF term that authorises the relay, exactly as your provider documents -# it. Set this: it cannot be derived from RELAY_HOST, and getting it wrong is -# worse than leaving it out. Brevo's relay is smtp-relay.brevo.com, but -# brevo.com publishes the SPF of their corporate mail - the sending ranges are -# at spf.brevo.com, so the value here is "include:spf.brevo.com". -RELAY_SPF_INCLUDE= - -# --- Storage ---------------------------------------------------------------- -STORAGE_DRIVER=local -S3_ENDPOINT= -S3_BUCKET= -S3_ACCESS_KEY= -S3_SECRET_KEY= diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 2e27a75..4817271 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -269,14 +269,23 @@ jobs: needs: [go-protocols, ts-auth, ts-storage, webmail-build] steps: - uses: actions/checkout@v7 - - run: cp .env.example .env + - run: scripts/init-env.sh # The proxy network is declared external because Coolify owns it on the # deploy host. Nothing creates it here, and without it compose refuses to # start the stack at all. + # + # Idempotent on purpose: the runner's Docker daemon outlives the job, so + # the network may already exist from an earlier run. - name: Cria a rede do proxy - run: docker network create "${COOLIFY_PROXY_NETWORK:-coolify}" + run: | + net="${COOLIFY_PROXY_NETWORK:-coolify}" + docker network inspect "$net" >/dev/null 2>&1 || docker network create "$net" # The build overlay is explicit here: CI has to exercise the code in the # branch, not the images already published from main. - run: docker compose -f docker-compose.yaml -f docker-compose.build.yaml --env-file .env up --build --wait - run: docker compose -f docker-compose.yaml -f docker-compose.build.yaml ps - - run: docker compose -f docker-compose.yaml -f docker-compose.build.yaml down -v + # Always, or a failed `up` leaves containers and volumes behind in the + # daemon the next run starts from. + - name: Derruba a stack + if: always() + run: docker compose -f docker-compose.yaml -f docker-compose.build.yaml down -v diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..847c8a8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,105 @@ +# Changelog + +All notable changes to LambdaMail are listed here. The format follows +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project follows +[Semantic Versioning](https://semver.org/). + +[Português](CHANGELOG.pt-BR.md) + +## [Unreleased] + +## [1.0.0] - 2026-10-30 + +The first tagged release. Earlier work was never tagged, so this entry sums up +everything from the first commit on 2026-07-23 to the move to Gitea. Gaps that +remain are listed in the README under +[What is not finished](README.md#what-is-not-finished). + +### Mail server + +- SMTP MX and Submission (587 STARTTLS, 465 implicit TLS), IMAP with UID, flags, + IDLE, CONDSTORE, `SEARCH`, `EXPUNGE`, `COPY` and `MOVE`, POP3 and ManageSieve. +- SPF, DKIM (Ed25519 and RSA dual signature), DMARC, ARC sealing, MTA-STS and + DANE on delivery, and TLS-RPT report ingestion. +- Rspamd scoring with greylisting, RBLs and rate limiting, and ClamAV. +- A durable outbound queue in Postgres with the retry ladder and RFC 3464 + bounces, a smarthost fallback for hosts with port 25 blocked, and a + configurable SPF term for the relay (`RELAY_SPF_INCLUDE`). +- Mail addressed to this server is delivered locally instead of going out to the + internet and back. +- Every submitted message is kept in the sender's Sent folder; app passwords + sign in over SMTP, IMAP and POP3. +- Sieve rules evaluated at delivery, custom folders, and an RFC 5230 vacation + responder that replies in thread and once per sender per week. + +### Webmail and console + +- A complete redesign of both surfaces (the "Nocturne" interface): dark and light + themes, a command menu on `Ctrl K` / `Cmd K`, `c` and `/` shortcuts, an + installable app with its own icon, and a drawn logo. +- Conversations in cascade, with who wrote, how many, and whether you have + answered; reply, reply all, important mail, undo send, and drafts that replace + the previous autosave. +- A reader that suits both themes: plain messages take the app's colours, + designed ones keep their white sheet, quoted history folds away, remote images + are blocked until allowed. +- Trusted senders, kept on the server per mailbox: their mail skips spam and + always shows images. +- Webmail sessions of 30 days that renew while used, several accounts signed in + at once and switchable from the sidebar; the console stays at 8 hours. +- Profile photos with a circular crop editor, and per-domain BIMI logo hosting + and validation, published to DNS and kept in step with the logo. +- Admin console with server-side paging and filtering, user editing (role, + quota, language, password, second factor), aliases, a diagnostics page, and + isolation between the user and admin surfaces. Domain admins are confined to + their own domain. +- English, Portuguese and Spanish, with a test that every string exists + in all of them. + +### DNS and TLS + +- Cloudflare reconciliation of the expected records for every active domain, + idempotent, with per-record verification against public resolvers; a DKIM + record is never published with an empty key. +- A TLS panel that states the digest the TLSA record must carry, and documented + steps for turning DANE on. `TLS_MODE=traefik` (default) or `acme`. + +### Security + +- Two-factor sign-in with TOTP and recovery codes; enrolment can no longer + replace a secret the authenticator already has, enable MFA without recovery + codes, or kill the auth service. `make reset-mfa` recovers a broken enrolment. +- Argon2 concurrency is bounded, so a login flood cannot take the auth service + down. +- All service containers run as non-root; images are scanned with Trivy and the + build fails on HIGH or CRITICAL findings. +- Dependencies updated for two published advisories: `next` 15.5.28 + (unauthenticated remote code execution in image optimisation, + GHSA-2464-fhr4-w9xw) and `go-jose` 4.1.6 (denial of service). + +### Deployment and CI + +- The deploy host pulls images that CI built and tested; nothing is compiled on + the server. The compose file pins Traefik to the proxy network by name. +- **Moved to Gitea.** The source, the workflows (`.gitea/workflows`) and the + container images now live on `git.lucasrguerra.dev.br`; the images are + published for `linux/amd64` and `linux/arm64` under + `git.lucasrguerra.dev.br/lucasrguerra/lambdamail/` and the `v*` tag + creates a release with notes. See [docs/RELEASING.md](docs/RELEASING.md). +- CI caches npm, Go modules, the embedded Postgres, Trivy's database and image + layers. +- One `.env.example` for development and production, and `scripts/init-env.sh` + to create `.env` with random secrets (`make env`). It replaces + `.env.production.example`. + +### Fixed + +- Webmail healthcheck: Next binds IPv4 only and `wget` resolved `localhost` to + `::1`. +- The admin lockout, a draft that did not discard, folder counters, a vacation + reply that was not a reply, DNS panels that reported published records as + missing, and the reconcile button that created nothing. +- Migrations: a default partition for `email_messages`. + +[Unreleased]: https://git.lucasrguerra.dev.br/lucasrguerra/LambdaMail/compare/v1.0.0...HEAD +[1.0.0]: https://git.lucasrguerra.dev.br/lucasrguerra/LambdaMail/releases/tag/v1.0.0 diff --git a/CHANGELOG.pt-BR.md b/CHANGELOG.pt-BR.md new file mode 100644 index 0000000..a3448ae --- /dev/null +++ b/CHANGELOG.pt-BR.md @@ -0,0 +1,106 @@ +# Registro de mudanças + +Tudo o que muda de relevante no LambdaMail está aqui. O formato segue o +[Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/) e o projeto segue o +[Versionamento Semântico](https://semver.org/lang/pt-BR/). + +[English](CHANGELOG.md) + +## [Não lançado] + +## [1.0.0] - 2026-10-30 + +A primeira versão com tag. O trabalho anterior nunca foi tagueado, então esta +entrada resume tudo desde o primeiro commit, em 2026-07-23, até a mudança para o +Gitea. O que ainda falta está no README, em +[O que não está pronto](README.pt-BR.md#o-que-não-está-pronto). + +### Servidor de e-mail + +- SMTP MX e Submission (587 STARTTLS, 465 TLS implícito), IMAP com UID, flags, + IDLE, CONDSTORE, `SEARCH`, `EXPUNGE`, `COPY` e `MOVE`, POP3 e ManageSieve. +- SPF, DKIM (assinatura dupla Ed25519 e RSA), DMARC, selo ARC, MTA-STS e DANE na + entrega, e ingestão de relatórios TLS-RPT. +- Pontuação do Rspamd com greylisting, RBLs e limite de taxa, e ClamAV. +- Fila de saída durável no Postgres, com a escada de tentativas e devoluções RFC + 3464, retransmissão por smarthost para hosts com a porta 25 bloqueada, e termo + SPF do relay configurável (`RELAY_SPF_INCLUDE`). +- O e-mail endereçado a este servidor é entregue localmente, sem sair para a + internet e voltar. +- Toda mensagem enviada fica na pasta Enviados de quem enviou; as senhas de + aplicativo funcionam em SMTP, IMAP e POP3. +- Regras Sieve avaliadas na entrega, pastas próprias e resposta de ausência RFC + 5230, que responde dentro da conversa e uma vez por remetente por semana. + +### Webmail e console + +- Redesenho completo das duas interfaces ("Nocturne"): temas escuro e claro, + menu de comandos em `Ctrl K` / `Cmd K`, atalhos `c` e `/`, app instalável com + ícone próprio e um logo desenhado. +- Conversas em cascata, com quem escreveu, quantas mensagens e se você já + respondeu; responder, responder a todos, importantes, desfazer o envio, e + rascunhos que substituem o salvamento automático anterior. +- Um leitor que combina com os dois temas: mensagens simples usam as cores do + app, as desenhadas mantêm a folha branca, o histórico citado se recolhe e as + imagens remotas ficam bloqueadas até serem liberadas. +- Remetentes confiáveis, guardados no servidor por caixa: o e-mail deles não vai + para o spam e sempre mostra as imagens. +- Sessões do webmail de 30 dias que se renovam com o uso, várias contas abertas + ao mesmo tempo e trocáveis pela barra lateral; o console continua em 8 horas. +- Foto de perfil com editor de recorte circular, e hospedagem e validação do + logotipo BIMI por domínio, publicado no DNS e mantido em dia com o logo. +- Console de administração com paginação e filtros no servidor, edição de + usuários (papel, cota, idioma, senha, segundo fator), apelidos, uma página de + diagnóstico e isolamento entre as superfícies de usuário e de administração. + Administradores de domínio ficam restritos ao próprio domínio. +- Inglês, português e espanhol, com um teste de que toda string existe nos três. + +### DNS e TLS + +- Reconciliação com a Cloudflare dos registros esperados de cada domínio ativo, + idempotente, com verificação de cada registro em resolvedores públicos; um + registro DKIM nunca é publicado com a chave vazia. +- Um painel de TLS que mostra o digest que o registro TLSA precisa carregar, e + passos documentados para ligar o DANE. `TLS_MODE=traefik` (padrão) ou `acme`. + +### Segurança + +- Login em duas etapas com TOTP e códigos de recuperação; o cadastro não + substitui mais um segredo que o autenticador já tem, não ativa o MFA sem + códigos de recuperação e não derruba o serviço de autenticação. O + `make reset-mfa` recupera um cadastro quebrado. +- A concorrência do Argon2 é limitada, então uma enxurrada de logins não derruba + o serviço de autenticação. +- Todos os containers de serviço rodam sem root; as imagens passam pelo Trivy e + o build falha em achados HIGH ou CRITICAL. +- Dependências atualizadas por dois avisos publicados: `next` 15.5.28 (execução + remota de código sem autenticação na otimização de imagens, + GHSA-2464-fhr4-w9xw) e `go-jose` 4.1.6 (negação de serviço). + +### Implantação e CI + +- O servidor de produção só puxa imagens que o CI construiu e testou; nada é + compilado nele. O compose fixa o Traefik na rede do proxy pelo nome. +- **Mudança para o Gitea.** O código, os workflows (`.gitea/workflows`) e as + imagens de container agora ficam em `git.lucasrguerra.dev.br`; as imagens são + publicadas para `linux/amd64` e `linux/arm64` em + `git.lucasrguerra.dev.br/lucasrguerra/lambdamail/`, e a tag `v*` cria + uma release com notas. Veja [docs/RELEASING.md](docs/RELEASING.md). +- O CI guarda em cache o npm, os módulos Go, o Postgres embutido, o banco do + Trivy e as camadas das imagens. +- Um único `.env.example` para desenvolvimento e produção, e o + `scripts/init-env.sh` para criar o `.env` com segredos aleatórios + (`make env`). Ele substitui o `.env.production.example`. + +### Corrigido + +- Healthcheck do webmail: o Next escuta só em IPv4 e o `wget` resolvia + `localhost` para `::1`. +- O bloqueio do administrador, o rascunho que não descartava, os contadores de + pastas, uma resposta de ausência que não era resposta, painéis de DNS que + diziam faltar registros já publicados e o botão de reconciliar que não criava + nada. +- Migrações: partição padrão para `email_messages`. + +[Não lançado]: https://git.lucasrguerra.dev.br/lucasrguerra/LambdaMail/compare/v1.0.0...HEAD +[1.0.0]: https://git.lucasrguerra.dev.br/lucasrguerra/LambdaMail/releases/tag/v1.0.0 diff --git a/Makefile b/Makefile index 96a0cf2..6fe498e 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,8 @@ -.PHONY: up down logs test test-db-up test-db-down lint migrate-up migrate-down gen-dev-cert seed preflight create-admin reset-password reset-mfa +.PHONY: env up down logs test test-db-up test-db-down lint migrate-up migrate-down gen-dev-cert seed preflight create-admin reset-password reset-mfa + +# .env from .env.example with fresh secrets; refuses to overwrite an existing one. +env: + scripts/init-env.sh # Local development builds from source; production pulls what CI published. up: diff --git a/README.md b/README.md index beab4cb..9efd11b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@

LambdaMail

Your own mail server, with a webmail that is a pleasure to use.
SMTP, IMAP and POP3 · SPF, DKIM, DMARC and ARC · Webmail and admin console · One docker-compose.yaml, made for Coolify.

-

Webmail · Deploying · Operating · Mailbox core · License

+

Webmail · Deploying · Operating · Mailbox core · Releasing · Changelog · License

English · Português

@@ -18,7 +18,7 @@ SMTP, IMAP, POP3 and ManageSieve, with SPF, DKIM, DMARC, ARC, MTA-STS and TLS-RPT, DNS automation through Cloudflare, and spam and virus filtering. No paid tiers and no artificial limits on domains or mailboxes. -> **Status: pre-release.** The mail path, the security posture and the web +> **Version 1.0.0** ([changelog](CHANGELOG.md)). The mail path, the security posture and the web > interfaces work end to end, and there is an automated test suite behind them. > See [What is not finished](#what-is-not-finished) before putting real mail > through it. @@ -107,7 +107,7 @@ getting port 25 unblocked — and the order the rest has to happen in. The short version: ```bash -cp .env.production.example .env # then replace every CHANGE +scripts/init-env.sh --production # .env with random secrets; lists what to set make preflight # checks port 25, PTR, RBLs, token scope docker compose up -d make create-admin EMAIL=you@example.com PASSWORD='...' @@ -166,7 +166,7 @@ more thing to keep patched. ## Running locally ```bash -cp .env.example .env +make env # .env from .env.example, with random secrets make gen-dev-cert # self-signed certificate for the listeners make up # builds from source via the build overlay make seed # a postmaster@example.test fixture @@ -204,8 +204,8 @@ checked in a real browser; see ## Configuration Every variable is documented inline in -[`.env.production.example`](.env.production.example). The ones without a safe -default: +[`.env.example`](.env.example), the one file for development and production. +The ones without a safe default (`scripts/init-env.sh` generates the secrets): | Variable | | | :--- | :--- | @@ -383,6 +383,8 @@ Stated plainly, because a mail server that half works loses mail: the first. - Tests come with the change that needs them; the suites above must stay green. - Commits reference their issue: `#N: what changed and why`. +- A change someone would notice goes under *Unreleased* in [CHANGELOG.md](CHANGELOG.md) + (and its Portuguese twin). How a version is cut: [docs/RELEASING.md](docs/RELEASING.md). ## License diff --git a/README.pt-BR.md b/README.pt-BR.md index 6500717..03fd0d0 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -2,7 +2,7 @@

LambdaMail

Seu próprio servidor de e-mail, com um webmail que dá gosto de usar.
SMTP, IMAP e POP3 · SPF, DKIM, DMARC e ARC · Webmail e console de administração · Um único docker-compose.yaml, feito para o Coolify.

-

Webmail · Implantação · Operação · Núcleo da caixa · Licença

+

Webmail · Implantação · Operação · Núcleo da caixa · Lançamentos · Mudanças · Licença

English · Português

@@ -18,7 +18,7 @@ SMTP, IMAP, POP3 e ManageSieve, com SPF, DKIM, DMARC, ARC, MTA-STS e TLS-RPT, automação de DNS pela Cloudflare e filtro de spam e de vírus. Sem planos pagos e sem limites artificiais de domínios ou caixas. -> **Estado: pré-lançamento.** O caminho do e-mail, a postura de segurança e as +> **Versão 1.0.0** ([mudanças](CHANGELOG.pt-BR.md)). O caminho do e-mail, a postura de segurança e as > interfaces web funcionam de ponta a ponta, e há uma suíte de testes > automatizados por trás. Leia [O que não está pronto](#o-que-não-está-pronto) > antes de colocar e-mail de verdade nele. @@ -109,7 +109,7 @@ precisa acontecer. A versão curta: ```bash -cp .env.production.example .env # depois troque cada CHANGE +scripts/init-env.sh --production # .env com segredos aleatórios; lista o que definir make preflight # confere porta 25, PTR, RBLs, escopo do token docker compose up -d make create-admin EMAIL=voce@exemplo.com PASSWORD='...' @@ -166,7 +166,7 @@ coisa para manter atualizada. ## Rodando localmente ```bash -cp .env.example .env +make env # .env a partir do .env.example, com segredos aleatórios make gen-dev-cert # certificado autoassinado para os listeners make up # compila a partir do código pelo overlay de build make seed # uma caixa postmaster@example.test de teste @@ -204,7 +204,8 @@ navegador de verdade; veja [docs/WEBMAIL.md](docs/WEBMAIL.md#testing-it). ## Configuração Cada variável está documentada em linha em -[`.env.production.example`](.env.production.example). As que não têm padrão seguro: +[`.env.example`](.env.example), o arquivo único para desenvolvimento e produção. +As que não têm padrão seguro (o `scripts/init-env.sh` gera os segredos): | Variável | | | :--- | :--- | @@ -385,6 +386,8 @@ e-mail: - Os testes acompanham a mudança que precisa deles; as suítes acima precisam continuar verdes. - Os commits citam a issue: `#N: o que mudou e por quê`. +- Uma mudança que alguém vai notar entra em *Não lançado* no [CHANGELOG.pt-BR.md](CHANGELOG.pt-BR.md) + (e no equivalente em inglês). Como se lança uma versão: [docs/RELEASING.md](docs/RELEASING.md). ## Licença diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 7b4d411..3f6cd22 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -70,17 +70,28 @@ Cloudflare token, or you can add them by hand later. ## Step 2 — Create the application in Coolify -1. **New Resource → Docker Compose**, pointed at this repository. +1. **New Resource → Docker Compose**, pointed at this repository. It lives on + Gitea: choose **Private Repository (with Deploy Key)** and use + `git@git.lucasrguerra.dev.br:lucasrguerra/LambdaMail.git` with a key that is + registered in Gitea. An application that was created from a GitHub source + keeps cloning from GitHub until its source is changed in the Coolify UI; the + API cannot change it. 2. Coolify will read `docker-compose.yaml`. Leave the compose file alone; all configuration is environment. -3. Under **Environment Variables**, paste the contents of - `.env.production.example` and replace every value marked `CHANGE`. +3. Create the environment on your own machine, then paste it into + **Environment Variables**: -Generate each secret separately: + ```bash + scripts/init-env.sh --production # writes .env with random secrets + ``` -```bash -openssl rand -base64 36 -``` + The script lists every value that is still at a development default + (`MAIL_DOMAIN`, `PRIMARY_MAIL_HOST`, `WEBMAIL_HOST`, `PUBLIC_IPV4`, + `IMAGE_TAG`); set those in `.env`, then paste. Every variable is explained + inline in [`.env.example`](../.env.example). + +`POSTGRES_PASSWORD` and `REDIS_PASSWORD` are generated as hex on purpose: they +go inside connection URIs, and the `/` and `+` of base64 break them. `LAMBDAMAIL_MASTER_KEY` deserves particular care. It encrypts every DKIM private key, TOTP secret and provider token in the database. **Store a copy @@ -176,8 +187,9 @@ have propagated and the IP has some sending history. ## Upgrading -Images are published to the Gitea container registry for `linux/amd64` and `linux/arm64` on every merge -to `main`. +Images are published to the Gitea container registry for `linux/amd64` and +`linux/arm64` on every merge to `main`, and again under the version number when +a `v*` tag is pushed (see [RELEASING.md](RELEASING.md)). ```bash docker compose pull @@ -189,7 +201,8 @@ Deploys pull the images CI published rather than building on the server; source instead. Migrations run automatically on start, before the services that depend on them. -**Pin `IMAGE_TAG` to a `sha-` tag rather than leaving it on `latest`.** Not +**Pin `IMAGE_TAG` to a release (`1.0.0`) or a `sha-` tag rather than leaving it +on `latest`.** Not only for rollback: `docker compose up` does not re-pull a tag it already has locally, so a deployment left on `latest` will happily keep running the image it pulled the first time while reporting success. An immutable tag changes on diff --git a/docs/RELEASING.md b/docs/RELEASING.md new file mode 100644 index 0000000..6e93f8a --- /dev/null +++ b/docs/RELEASING.md @@ -0,0 +1,88 @@ +# Releasing LambdaMail + +How a version gets from a commit to a server, and what runs where. The history +of what changed is in the [CHANGELOG](../CHANGELOG.md). + +## The pipeline + +Everything runs on **Gitea Actions** (`.gitea/workflows/`), on runners hosted on +the Oracle server. + +| Workflow | Runs on | Does | +| :--- | :--- | :--- | +| `ci.yml` | every push and pull request | language lint, Go and TypeScript suites against a real Postgres, webmail tests, a Trivy scan of the repository and of each image, and the whole stack brought up with compose | +| `release.yml` | a push to `main`, a `v*` tag, or by hand | runs `ci.yml` again, builds the six images for `linux/amd64` and `linux/arm64`, publishes them, and on a tag creates the release | + +Nothing is published unless the full suite passes first. + +Images go to the Gitea registry, one package per service: + +``` +git.lucasrguerra.dev.br/lucasrguerra/lambdamail/{protocols,auth,storage,webmail,migrations,rspamd} +``` + +Tags on every image: `latest`, `sha-`, and on a version tag also +`1.2.3`, `1.2` and `1`. The packages are public, so a server pulls them without +logging in. + +## Cutting a release + +1. Update [CHANGELOG.md](../CHANGELOG.md) and [CHANGELOG.pt-BR.md](../CHANGELOG.pt-BR.md): + move what is under *Unreleased* to a new `## [X.Y.Z] - date` section. +2. Set `"version"` in `services/{auth,storage,webmail}/package.json` and their + lockfiles to the same number. +3. Merge to `main` and wait for the `Release` run of that merge to go green. +4. Tag the merge commit and push the tag: + + ```bash + git tag -a v1.2.3 -m "LambdaMail 1.2.3" + git push gitea v1.2.3 + ``` + +The tag triggers `release.yml` once more. It republishes the images under +`1.2.3`, `1.2` and `1` (layers are cached, so this is quick) and creates the +entry under **Releases** with the image list and the commit log since the +previous tag. A tag with a hyphen (`v1.3.0-rc.1`) is marked as a pre-release. + +Versions follow [Semantic Versioning](https://semver.org/): a change to a +migration, a required environment variable, or a published port is at least a +minor release, and anything that needs manual steps on upgrade is called out in +the changelog. + +## Deploying a version + +Coolify pulls; it does not build. See the *Upgrading* section of +[DEPLOYMENT.md](DEPLOYMENT.md) for the two-step deploy (set `IMAGE_TAG`, then +deploy). In short: + +1. `IMAGE_TAG` = the release (`1.2.3`) or `sha-`. Never `latest`. +2. Deploy. Migrations run before the services that need them. +3. Check `GET /health` on `protocols` and sign in to the webmail. + +To roll back, set `IMAGE_TAG` back to the previous release and deploy again. +Migrations are not reverted: write them so the previous version still runs +against the new schema. + +## Runners + +Two `act_runner` instances run on the Oracle server (Ampere ARM, 4 cores), each +with its own Docker-in-Docker daemon, so two jobs never share a network or a +port. The jobs run unprivileged. + +- `linux/arm64` images build natively; `linux/amd64` images build under QEMU + emulation, which is slower. Layers are cached in the registry. +- The Docker daemon of each runner keeps its layer cache between runs, and the + toolcache volume keeps downloaded toolchains. +- The workspace of a job is not visible to the Docker daemon, so workflows copy + files into containers with `docker cp` instead of mounting them with `-v`. +- Windows and macOS jobs cannot run here. + +## Why some actions are not used + +- `aquasecurity/trivy-action` clones Trivy from the workflow's own server, + which is Gitea; Trivy runs from its official image instead. +- `actions/upload-artifact` v4+ and the `gha` build cache are GitHub services; + per-architecture images are told apart by tag, and the cache lives in the + registry. +- Workflows read `GITHUB_OUTPUT` and `GITHUB_STEP_SUMMARY` under those names: + the `GITEA_*` equivalents are not set by the runner. diff --git a/scripts/init-env.sh b/scripts/init-env.sh new file mode 100755 index 0000000..7998f37 --- /dev/null +++ b/scripts/init-env.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# Creates .env from .env.example and fills the empty secrets with random values. +# +# scripts/init-env.sh local development +# scripts/init-env.sh --production also lists what is still at a dev default +# scripts/init-env.sh --force overwrite an existing .env +# +# An existing .env is never touched without --force: regenerating +# LAMBDAMAIL_MASTER_KEY makes every stored secret unrecoverable. +set -euo pipefail + +cd "$(dirname "$0")/.." + +production=0 +force=0 +for arg in "$@"; do + case "$arg" in + --production) production=1 ;; + --force) force=1 ;; + -h|--help) sed -n '2,10p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + *) echo "unknown option: $arg" >&2; exit 2 ;; + esac +done + +if [ -e .env ] && [ "$force" -ne 1 ]; then + echo ".env already exists; leaving it alone (use --force to overwrite)." >&2 + exit 1 +fi + +command -v openssl >/dev/null || { echo "openssl is required to generate secrets" >&2; exit 1; } + +cp .env.example .env +chmod 600 .env + +# set_var KEY VALUE: replaces the line KEY=... in .env. The value is hex, so it +# needs no escaping for sed. +set_var() { + sed -i.bak "s|^$1=.*|$1=$2|" .env && rm -f .env.bak +} + +set_var POSTGRES_PASSWORD "$(openssl rand -hex 24)" +set_var REDIS_PASSWORD "$(openssl rand -hex 24)" +set_var JWT_SECRET "$(openssl rand -hex 32)" +set_var LAMBDAMAIL_MASTER_KEY "$(openssl rand -hex 32)" + +echo ".env written (mode 600) with fresh secrets." + +if [ "$production" -eq 1 ]; then + set_var COOLIFY_PROXY_DIR /data/coolify/proxy + echo + echo "Production: still at their development defaults, set these in .env:" + pending=0 + check() { + if grep -qE "^$1=$2\$" .env; then echo " - $1 (now $2)"; pending=1; fi + } + check MAIL_DOMAIN localhost + check PRIMARY_MAIL_HOST mail.localhost + check WEBMAIL_HOST 'localhost:3000' + check PUBLIC_IPV4 127.0.0.1 + check IMAGE_TAG latest + [ "$pending" -eq 0 ] && echo " (nothing)" + echo + echo "Back up LAMBDAMAIL_MASTER_KEY separately from the database." + echo "Next: make preflight, then docker compose up -d (docs/DEPLOYMENT.md)." +fi diff --git a/services/auth/package-lock.json b/services/auth/package-lock.json index 47042f1..386f40b 100644 --- a/services/auth/package-lock.json +++ b/services/auth/package-lock.json @@ -1,12 +1,12 @@ { "name": "@lambdamail/auth", - "version": "0.1.0", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@lambdamail/auth", - "version": "0.1.0", + "version": "1.0.0", "dependencies": { "hash-wasm": "^4.12.0", "ioredis": "^5.4.1", diff --git a/services/auth/package.json b/services/auth/package.json index 9fe0304..335f01e 100644 --- a/services/auth/package.json +++ b/services/auth/package.json @@ -1,6 +1,6 @@ { "name": "@lambdamail/auth", - "version": "0.1.0", + "version": "1.0.0", "private": true, "type": "module", "scripts": { diff --git a/services/storage/package-lock.json b/services/storage/package-lock.json index 9cb0676..803821d 100644 --- a/services/storage/package-lock.json +++ b/services/storage/package-lock.json @@ -1,12 +1,12 @@ { "name": "@lambdamail/storage", - "version": "0.1.0", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@lambdamail/storage", - "version": "0.1.0", + "version": "1.0.0", "dependencies": { "ioredis": "^5.4.1", "pg": "^8.12.0" diff --git a/services/storage/package.json b/services/storage/package.json index 845a829..b59c0ce 100644 --- a/services/storage/package.json +++ b/services/storage/package.json @@ -1,6 +1,6 @@ { "name": "@lambdamail/storage", - "version": "0.1.0", + "version": "1.0.0", "private": true, "type": "module", "scripts": { diff --git a/services/webmail/package-lock.json b/services/webmail/package-lock.json index 472a32f..69a9494 100644 --- a/services/webmail/package-lock.json +++ b/services/webmail/package-lock.json @@ -1,12 +1,12 @@ { "name": "@lambdamail/webmail", - "version": "0.1.0", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@lambdamail/webmail", - "version": "0.1.0", + "version": "1.0.0", "dependencies": { "@fontsource-variable/geist": "^5.3.0", "clsx": "^2.1.1", diff --git a/services/webmail/package.json b/services/webmail/package.json index 668f513..5a531da 100644 --- a/services/webmail/package.json +++ b/services/webmail/package.json @@ -1,6 +1,6 @@ { "name": "@lambdamail/webmail", - "version": "0.1.0", + "version": "1.0.0", "private": true, "scripts": { "dev": "next dev",