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

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Sonnet 5.5 committed 2026-10-30 11:44:19 -03:00
commit 2db5915061
19 files changed
+608 -199

No files matched your search

+95 -44
View File
@@ -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=
-104
View File
@@ -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
View File
@@ -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
+40
View File
@@ -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
+3
View File
@@ -46,3 +46,6 @@ Thumbs.db
*.json.gz
*.xml.gz
*.zip.gz
# Configuração local do Superset
.superset/
+105
View File
@@ -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
+106
View File
@@ -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
+5 -1
View File
@@ -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:
+8 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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
+88
View File
@@ -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.
+65
View File
@@ -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
+2 -2
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@lambdamail/auth",
"version": "0.1.0",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
+2 -2
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@lambdamail/storage",
"version": "0.1.0",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
+2 -2
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@lambdamail/webmail",
"version": "0.1.0",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",