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 053d5b3..4817271 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: @@ -245,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/.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 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/ 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",