Merge release/v1.0.0: changelog, one .env.example, release docs, faster CI, Gitea release job
CI / Lint de idioma (R9) (push) Successful in 3s
CI / Stack completa (push) Canceled after 0s
CI / Imagem (webmail) (push) Canceled after 0s
CI / Imagem (storage) (push) Canceled after 0s
CI / Imagem (rspamd) (push) Canceled after 0s
CI / Imagem (protocols) (push) Canceled after 0s
CI / Imagem (migrations) (push) Canceled after 0s
CI / Imagem (auth) (push) Canceled after 0s
CI / Trivy (dependências, segredos e Dockerfiles) (push) Canceled after 0s
CI / Testes (webmail) (push) Canceled after 0s
CI / TypeScript (storage) (push) Canceled after 0s
CI / TypeScript (auth) (push) Canceled after 2m6s
CI / Go (protocols) (push) Canceled after 2m8s
Release / ci (push) Canceled after 2m9s
Release / Namespace das imagens (push) Canceled after 0s
Release / Build auth (linux/amd64) (push) Canceled after 0s
Release / Build auth (linux/arm64) (push) Canceled after 0s
Release / Build migrations (linux/amd64) (push) Canceled after 0s
Release / Build migrations (linux/arm64) (push) Canceled after 0s
Release / Build protocols (linux/amd64) (push) Canceled after 0s
Release / Build protocols (linux/arm64) (push) Canceled after 0s
Release / Build rspamd (linux/amd64) (push) Canceled after 0s
Release / Build rspamd (linux/arm64) (push) Canceled after 0s
Release / Build storage (linux/amd64) (push) Canceled after 0s
Release / Build storage (linux/arm64) (push) Canceled after 0s
Release / Build webmail (linux/amd64) (push) Canceled after 0s
Release / Build webmail (linux/arm64) (push) Canceled after 0s
Release / Publica auth (push) Canceled after 0s
Release / Publica migrations (push) Canceled after 0s
Release / Publica protocols (push) Canceled after 0s
Release / Publica rspamd (push) Canceled after 0s
Release / Publica storage (push) Canceled after 0s
Release / Publica webmail (push) Canceled after 0s
Release / Cria a release no Gitea (push) Canceled after 0s
CI / Lint de idioma (R9) (push) Successful in 3s
CI / Stack completa (push) Canceled after 0s
CI / Imagem (webmail) (push) Canceled after 0s
CI / Imagem (storage) (push) Canceled after 0s
CI / Imagem (rspamd) (push) Canceled after 0s
CI / Imagem (protocols) (push) Canceled after 0s
CI / Imagem (migrations) (push) Canceled after 0s
CI / Imagem (auth) (push) Canceled after 0s
CI / Trivy (dependências, segredos e Dockerfiles) (push) Canceled after 0s
CI / Testes (webmail) (push) Canceled after 0s
CI / TypeScript (storage) (push) Canceled after 0s
CI / TypeScript (auth) (push) Canceled after 2m6s
CI / Go (protocols) (push) Canceled after 2m8s
Release / ci (push) Canceled after 2m9s
Release / Namespace das imagens (push) Canceled after 0s
Release / Build auth (linux/amd64) (push) Canceled after 0s
Release / Build auth (linux/arm64) (push) Canceled after 0s
Release / Build migrations (linux/amd64) (push) Canceled after 0s
Release / Build migrations (linux/arm64) (push) Canceled after 0s
Release / Build protocols (linux/amd64) (push) Canceled after 0s
Release / Build protocols (linux/arm64) (push) Canceled after 0s
Release / Build rspamd (linux/amd64) (push) Canceled after 0s
Release / Build rspamd (linux/arm64) (push) Canceled after 0s
Release / Build storage (linux/amd64) (push) Canceled after 0s
Release / Build storage (linux/arm64) (push) Canceled after 0s
Release / Build webmail (linux/amd64) (push) Canceled after 0s
Release / Build webmail (linux/arm64) (push) Canceled after 0s
Release / Publica auth (push) Canceled after 0s
Release / Publica migrations (push) Canceled after 0s
Release / Publica protocols (push) Canceled after 0s
Release / Publica rspamd (push) Canceled after 0s
Release / Publica storage (push) Canceled after 0s
Release / Publica webmail (push) Canceled after 0s
Release / Cria a release no Gitea (push) Canceled after 0s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
2db5915061
19 files changed
+608
-199
No files matched your search
+95
-44
@@ -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-<commit> 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=<the VPS 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=
|
||||
@@ -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=
|
||||
+54
-21
@@ -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
|
||||
@@ -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
|
||||
@@ -46,3 +46,6 @@ Thumbs.db
|
||||
*.json.gz
|
||||
*.xml.gz
|
||||
*.zip.gz
|
||||
|
||||
# Configuração local do Superset
|
||||
.superset/
|
||||
+105
@@ -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/<service>` 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
|
||||
@@ -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/<serviço>`, 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
|
||||
@@ -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:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<h1 align="center">LambdaMail</h1>
|
||||
<p align="center">Your own mail server, with a webmail that is a pleasure to use.<br>SMTP, IMAP and POP3 · SPF, DKIM, DMARC and ARC · Webmail and admin console · One <code>docker-compose.yaml</code>, made for Coolify.</p>
|
||||
|
||||
<p align="center"><a href="docs/WEBMAIL.md">Webmail</a> · <a href="docs/DEPLOYMENT.md">Deploying</a> · <a href="docs/OPERATIONS.md">Operating</a> · <a href="docs/MAILBOX-CORE.md">Mailbox core</a> · <a href="LICENSE">License</a></p>
|
||||
<p align="center"><a href="docs/WEBMAIL.md">Webmail</a> · <a href="docs/DEPLOYMENT.md">Deploying</a> · <a href="docs/OPERATIONS.md">Operating</a> · <a href="docs/MAILBOX-CORE.md">Mailbox core</a> · <a href="docs/RELEASING.md">Releasing</a> · <a href="CHANGELOG.md">Changelog</a> · <a href="LICENSE">License</a></p>
|
||||
|
||||
<p align="center"><b>English</b> · <a href="README.pt-BR.md">Português</a></p>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+8
-5
@@ -2,7 +2,7 @@
|
||||
<h1 align="center">LambdaMail</h1>
|
||||
<p align="center">Seu próprio servidor de e-mail, com um webmail que dá gosto de usar.<br>SMTP, IMAP e POP3 · SPF, DKIM, DMARC e ARC · Webmail e console de administração · Um único <code>docker-compose.yaml</code>, feito para o Coolify.</p>
|
||||
|
||||
<p align="center"><a href="docs/WEBMAIL.md">Webmail</a> · <a href="docs/DEPLOYMENT.md">Implantação</a> · <a href="docs/OPERATIONS.md">Operação</a> · <a href="docs/MAILBOX-CORE.md">Núcleo da caixa</a> · <a href="LICENSE">Licença</a></p>
|
||||
<p align="center"><a href="docs/WEBMAIL.md">Webmail</a> · <a href="docs/DEPLOYMENT.md">Implantação</a> · <a href="docs/OPERATIONS.md">Operação</a> · <a href="docs/MAILBOX-CORE.md">Núcleo da caixa</a> · <a href="docs/RELEASING.md">Lançamentos</a> · <a href="CHANGELOG.pt-BR.md">Mudanças</a> · <a href="LICENSE">Licença</a></p>
|
||||
|
||||
<p align="center"><a href="README.md">English</a> · <b>Português</b></p>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+22
-9
@@ -70,18 +70,29 @@ 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`.
|
||||
|
||||
Generate each secret separately:
|
||||
3. Create the environment on your own machine, then paste it into
|
||||
**Environment Variables**:
|
||||
|
||||
```bash
|
||||
openssl rand -base64 36
|
||||
scripts/init-env.sh --production # writes .env with random secrets
|
||||
```
|
||||
|
||||
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
|
||||
somewhere other than the database backup.** If you lose it, those secrets are
|
||||
@@ -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
|
||||
|
||||
@@ -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-<full commit>`, 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-<commit>`. 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.
|
||||
Executable
+65
@@ -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
|
||||
Generated
+2
-2
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@lambdamail/auth",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
Generated
+2
-2
@@ -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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@lambdamail/storage",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
Generated
+2
-2
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@lambdamail/webmail",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
Reference in new issue
Block a user