Compare commits
16
Commits
fa2dcb578e
...
v1.0.0
No files matched your search
+96
-45
@@ -1,81 +1,132 @@
|
|||||||
# --- Container images ---
|
# LambdaMail - environment
|
||||||
# Published to GHCR by .github/workflows/release.yml, multi-architecture
|
#
|
||||||
# (linux/amd64 and linux/arm64). The deploy host pulls these instead of
|
# One file for local development and for production.
|
||||||
# compiling: `docker compose pull && docker compose up -d`. Running
|
#
|
||||||
# `docker compose build` locally still overrides them with a local build.
|
# scripts/init-env.sh copy this to .env and generate the secrets
|
||||||
# Pin IMAGE_TAG to a released version (e.g. 1.2.3) or a sha- tag to roll back.
|
# scripts/init-env.sh --production the same, plus a list of what you still
|
||||||
IMAGE_NAMESPACE=ghcr.io/lucasrguerra/lambdamail
|
# 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
|
IMAGE_TAG=latest
|
||||||
|
|
||||||
# --- Postgres ---
|
# --- Identity ---------------------------------------------------------------
|
||||||
POSTGRES_DB=lambdamail
|
# MAIL_DOMAIN is the domain you send and receive as.
|
||||||
POSTGRES_USER=lambdamail
|
# PRIMARY_MAIL_HOST is the host the MX record points at; its PTR must match it.
|
||||||
POSTGRES_PASSWORD=change-me-in-local-dev
|
# PRODUCTION: MAIL_DOMAIN=example.com PRIMARY_MAIL_HOST=mail.example.com
|
||||||
|
# WEBMAIL_HOST=mail.example.com PUBLIC_IPV4=<the VPS public IPv4>
|
||||||
# --- 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
|
|
||||||
MAIL_DOMAIN=localhost
|
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
|
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=
|
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
|
# 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=
|
CLOUDFLARE_API_TOKEN=
|
||||||
DNS_SYNC_INTERVAL_SECONDS=21600
|
DNS_SYNC_INTERVAL_SECONDS=21600
|
||||||
|
|
||||||
# --- TLS ---
|
# --- TLS --------------------------------------------------------------------
|
||||||
# traefik: read the certificate Traefik manages (default, DANE not possible).
|
# traefik: read the certificate the proxy already manages (default; DANE is not
|
||||||
# acme: obtain the certificate ourselves via DNS-01. Required for DANE,
|
# possible in this mode).
|
||||||
# because only this mode can reuse a pre-generated key.
|
# acme: obtain the certificate here via DNS-01. Required for DANE, because
|
||||||
|
# only this mode can reuse a pre-generated key.
|
||||||
TLS_MODE=traefik
|
TLS_MODE=traefik
|
||||||
# Used only when TLS_MODE=acme. Leave ACME_DIRECTORY_URL empty for Let's
|
# Used only when TLS_MODE=acme. Leave ACME_DIRECTORY_URL empty for Let's Encrypt
|
||||||
# Encrypt production; point it at the staging directory while testing, since
|
# production; point it at the staging directory while testing, since production
|
||||||
# production rate limits can lock a domain out for a week.
|
# rate limits can lock a domain out for a week.
|
||||||
ACME_EMAIL=
|
ACME_EMAIL=
|
||||||
ACME_DIRECTORY_URL=
|
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_DIR=./local/acme
|
||||||
|
COOLIFY_PROXY_NETWORK=coolify
|
||||||
TRAEFIK_ACME_FILENAME=acme.json
|
TRAEFIK_ACME_FILENAME=acme.json
|
||||||
CERT_POLL_INTERVAL_SECONDS=60
|
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_CERT_PATH=/etc/lambdamail/certs/cert.pem
|
||||||
PROTOCOLS_KEY_PATH=/etc/lambdamail/certs/key.pem
|
PROTOCOLS_KEY_PATH=/etc/lambdamail/certs/key.pem
|
||||||
PROTOCOLS_SPOOL_DIR=/var/lambdamail/spool
|
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.
|
# 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
|
MTA_STS_MODE=testing
|
||||||
OUTBOUND_MTA_STS_ENABLED=true
|
OUTBOUND_MTA_STS_ENABLED=true
|
||||||
# DANE stays off unless TLS_MODE=acme. Under Traefik-managed certificates the
|
# DANE stays off unless TLS_MODE=acme. Under proxy-managed certificates the key
|
||||||
# key changes at every renewal and the published TLSA record stops matching,
|
# changes at every renewal and the published TLSA record stops matching, which
|
||||||
# which permanently rejects mail at every validating MTA.
|
# permanently rejects mail at every validating MTA (PLAN.md section 5.1).
|
||||||
OUTBOUND_DANE_ENABLED=false
|
OUTBOUND_DANE_ENABLED=false
|
||||||
DNSSEC_RESOLVER=1.1.1.1:53
|
DNSSEC_RESOLVER=1.1.1.1:53
|
||||||
MAX_MESSAGE_BYTES=52428800
|
MAX_MESSAGE_BYTES=52428800
|
||||||
|
|
||||||
# ARC seals accepted messages so a downstream receiver can still tell that the
|
# 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
|
# mail authenticated at the first hop. Only worth enabling on a host that
|
||||||
# forwards mail onward.
|
# forwards mail onward.
|
||||||
ARC_SEAL_ENABLED=false
|
ARC_SEAL_ENABLED=false
|
||||||
|
|
||||||
# --- Smarthost (for hosts where outbound port 25 is blocked) ---
|
# --- Smarthost (hosts where outbound port 25 is blocked) --------------------
|
||||||
# Setting RELAY_HOST routes all delivery through the relay and adds it to the
|
# Most VPS providers block port 25. If `make preflight` reports it blocked, set a
|
||||||
# published SPF record, without which every relayed message fails SPF.
|
# 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_HOST=
|
||||||
RELAY_PORT=587
|
RELAY_PORT=587
|
||||||
RELAY_USER=
|
RELAY_USER=
|
||||||
RELAY_PASS=
|
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 ---
|
# --- Storage ----------------------------------------------------------------
|
||||||
WEBMAIL_HOST=localhost:3000
|
STORAGE_DRIVER=local
|
||||||
ADMIN_HOST=
|
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 .github/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=ghcr.io/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=
|
|
||||||
@@ -15,7 +15,7 @@ on:
|
|||||||
workflow_call:
|
workflow_call:
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ci-${{ github.ref }}
|
group: ci-${{ gitea.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -57,16 +57,41 @@ jobs:
|
|||||||
cache-dependency-path: services/protocols/go.sum
|
cache-dependency-path: services/protocols/go.sum
|
||||||
- name: Run migrations
|
- name: Run migrations
|
||||||
run: |
|
run: |
|
||||||
docker run --rm --network host \
|
# The job's workspace is a volume of its own, invisible to the Docker
|
||||||
-v "$(pwd)/../../migrations:/migrations" \
|
# daemon the job talks to, so `-v "$PWD:..."` would mount an empty
|
||||||
migrate/migrate:v4.17.1 \
|
# directory. Files are copied into the container instead.
|
||||||
|
cid=$(docker create --network host migrate/migrate:v4.17.1 \
|
||||||
-path=/migrations \
|
-path=/migrations \
|
||||||
-database "postgres://lambdamail_test:lambdamail_test@localhost:5432/lambdamail_test?sslmode=disable" \
|
-database "postgres://lambdamail_test:lambdamail_test@localhost:5432/lambdamail_test?sslmode=disable" \
|
||||||
up
|
up)
|
||||||
|
docker cp "$(pwd)/../../migrations/." "$cid:/migrations"
|
||||||
|
docker start -a "$cid"
|
||||||
|
status=$(docker inspect -f '{{.State.ExitCode}}' "$cid")
|
||||||
|
docker rm "$cid" >/dev/null
|
||||||
|
exit "$status"
|
||||||
- run: go vet ./...
|
- run: go vet ./...
|
||||||
- run: go test ./... -v
|
# 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).
|
||||||
|
- name: Testes
|
||||||
env:
|
env:
|
||||||
TEST_DATABASE_URL: postgres://lambdamail_test:lambdamail_test@localhost:5432/lambdamail_test?sslmode=disable
|
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" /home/tester
|
||||||
|
runuser -u tester -- env "HOME=/home/tester" "PATH=$PATH" \
|
||||||
|
"TEST_DATABASE_URL=$TEST_DATABASE_URL" go test ./... -v
|
||||||
|
|
||||||
ts-auth:
|
ts-auth:
|
||||||
name: TypeScript (auth)
|
name: TypeScript (auth)
|
||||||
@@ -91,18 +116,27 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
- uses: actions/setup-node@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,
|
# The auth suite asserts against real rows: password verification,
|
||||||
# lockout, TOTP replay and recovery-code consumption are all decided in
|
# lockout, TOTP replay and recovery-code consumption are all decided in
|
||||||
# the database, so a mock would prove nothing.
|
# the database, so a mock would prove nothing.
|
||||||
- name: Run migrations
|
- name: Run migrations
|
||||||
run: |
|
run: |
|
||||||
docker run --rm --network host \
|
# The job's workspace is a volume of its own, invisible to the Docker
|
||||||
-v "$(pwd)/../../migrations:/migrations" \
|
# daemon the job talks to, so `-v "$PWD:..."` would mount an empty
|
||||||
migrate/migrate:v4.17.1 \
|
# directory. Files are copied into the container instead.
|
||||||
|
cid=$(docker create --network host migrate/migrate:v4.17.1 \
|
||||||
-path=/migrations \
|
-path=/migrations \
|
||||||
-database "postgres://lambdamail_test:lambdamail_test@localhost:5432/lambdamail_test?sslmode=disable" \
|
-database "postgres://lambdamail_test:lambdamail_test@localhost:5432/lambdamail_test?sslmode=disable" \
|
||||||
up
|
up)
|
||||||
|
docker cp "$(pwd)/../../migrations/." "$cid:/migrations"
|
||||||
|
docker start -a "$cid"
|
||||||
|
status=$(docker inspect -f '{{.State.ExitCode}}' "$cid")
|
||||||
|
docker rm "$cid" >/dev/null
|
||||||
|
exit "$status"
|
||||||
- run: npm install
|
- run: npm install
|
||||||
- run: npm run lint
|
- run: npm run lint
|
||||||
- run: npm test
|
- run: npm test
|
||||||
@@ -119,13 +153,16 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
- uses: actions/setup-node@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 install
|
||||||
- run: npm run lint
|
- run: npm run lint
|
||||||
- run: npm test
|
- run: npm test
|
||||||
|
|
||||||
webmail-build:
|
webmail-build:
|
||||||
name: Build (webmail)
|
name: Testes (webmail)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -133,11 +170,15 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
- uses: actions/setup-node@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 install
|
||||||
- run: npm run lint
|
- run: npm run lint
|
||||||
- run: npm test
|
- 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:
|
security:
|
||||||
name: Trivy (dependências, segredos e Dockerfiles)
|
name: Trivy (dependências, segredos e Dockerfiles)
|
||||||
@@ -148,14 +189,20 @@ jobs:
|
|||||||
# A mail server holds credentials for every mailbox it serves, so the
|
# A mail server holds credentials for every mailbox it serves, so the
|
||||||
# secret scanner matters as much as the vulnerability one here: a DKIM
|
# secret scanner matters as much as the vulnerability one here: a DKIM
|
||||||
# key or a Cloudflare token committed by accident is a full compromise.
|
# key or a Cloudflare token committed by accident is a full compromise.
|
||||||
|
#
|
||||||
|
# Trivy runs from its official image rather than aquasecurity/trivy-action:
|
||||||
|
# that action clones the Trivy repository from the workflow's own server,
|
||||||
|
# which on Gitea is not GitHub.
|
||||||
- name: Repositório
|
- name: Repositório
|
||||||
uses: aquasecurity/trivy-action@v0.36.0
|
run: |
|
||||||
with:
|
cid=$(docker create --network host -v trivy-cache:/root/.cache/ -w /src aquasec/trivy:0.70.0 \
|
||||||
scan-type: fs
|
fs --scanners vuln,secret,misconfig --severity HIGH,CRITICAL --exit-code 1 \
|
||||||
scanners: vuln,secret,misconfig
|
--skip-dirs services/auth/node_modules,services/storage/node_modules,services/webmail/node_modules,services/webmail/.next .)
|
||||||
severity: HIGH,CRITICAL
|
docker cp . "$cid:/src"
|
||||||
exit-code: '1'
|
docker start -a "$cid"
|
||||||
skip-dirs: services/auth/node_modules,services/storage/node_modules,services/webmail/node_modules,services/webmail/.next
|
status=$(docker inspect -f '{{.State.ExitCode}}' "$cid")
|
||||||
|
docker rm "$cid" >/dev/null
|
||||||
|
exit "$status"
|
||||||
|
|
||||||
# MEDIUM is reported but does not fail the build.
|
# MEDIUM is reported but does not fail the build.
|
||||||
#
|
#
|
||||||
@@ -168,13 +215,15 @@ jobs:
|
|||||||
# Reporting rather than gating, because failing on every MEDIUM makes the
|
# Reporting rather than gating, because failing on every MEDIUM makes the
|
||||||
# scanner something people learn to skip.
|
# scanner something people learn to skip.
|
||||||
- name: Repositório (MEDIUM, informativo)
|
- name: Repositório (MEDIUM, informativo)
|
||||||
uses: aquasecurity/trivy-action@v0.36.0
|
run: |
|
||||||
with:
|
cid=$(docker create --network host -v trivy-cache:/root/.cache/ -w /src aquasec/trivy:0.70.0 \
|
||||||
scan-type: fs
|
fs --scanners vuln,secret --severity MEDIUM --exit-code 0 \
|
||||||
scanners: vuln,secret
|
--skip-dirs services/auth/node_modules,services/storage/node_modules,services/webmail/node_modules,services/webmail/.next .)
|
||||||
severity: MEDIUM
|
docker cp . "$cid:/src"
|
||||||
exit-code: '0'
|
docker start -a "$cid"
|
||||||
skip-dirs: services/auth/node_modules,services/storage/node_modules,services/webmail/node_modules,services/webmail/.next
|
status=$(docker inspect -f '{{.State.ExitCode}}' "$cid")
|
||||||
|
docker rm "$cid" >/dev/null
|
||||||
|
exit "$status"
|
||||||
|
|
||||||
image:
|
image:
|
||||||
name: Imagem (${{ matrix.service }})
|
name: Imagem (${{ matrix.service }})
|
||||||
@@ -186,20 +235,21 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- uses: docker/setup-buildx-action@v4
|
|
||||||
|
|
||||||
# Built but not pushed: this proves each Dockerfile still works on every
|
# Built but not pushed: this proves each Dockerfile still works on every
|
||||||
# branch, while publishing stays in release.yml.
|
# 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
|
- name: Build
|
||||||
uses: docker/build-push-action@v7
|
run: |
|
||||||
with:
|
case '${{ matrix.service }}' in
|
||||||
context: ${{ (matrix.service == 'migrations' || matrix.service == 'rspamd') && '.' || format('services/{0}', matrix.service) }}
|
migrations|rspamd) context=. ;;
|
||||||
file: services/${{ matrix.service }}/Dockerfile
|
*) context=services/${{ matrix.service }} ;;
|
||||||
push: false
|
esac
|
||||||
load: true
|
docker build -f services/${{ matrix.service }}/Dockerfile \
|
||||||
tags: lambdamail-${{ matrix.service }}:ci
|
-t lambdamail-${{ matrix.service }}:ci "$context"
|
||||||
cache-from: type=gha,scope=${{ matrix.service }}
|
|
||||||
cache-to: type=gha,mode=max,scope=${{ matrix.service }}
|
|
||||||
|
|
||||||
# Scanned only for the images whose contents we author. migrations and
|
# Scanned only for the images whose contents we author. migrations and
|
||||||
# rspamd add .sql and .conf files to an upstream image and nothing else,
|
# rspamd add .sql and .conf files to an upstream image and nothing else,
|
||||||
@@ -209,11 +259,9 @@ jobs:
|
|||||||
# that their base images have to be watched.
|
# that their base images have to be watched.
|
||||||
- name: Trivy na imagem
|
- name: Trivy na imagem
|
||||||
if: matrix.service != 'migrations' && matrix.service != 'rspamd'
|
if: matrix.service != 'migrations' && matrix.service != 'rspamd'
|
||||||
uses: aquasecurity/trivy-action@v0.36.0
|
run: |
|
||||||
with:
|
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-ref: lambdamail-${{ matrix.service }}:ci
|
image --severity HIGH,CRITICAL --exit-code 1 lambdamail-${{ matrix.service }}:ci
|
||||||
severity: HIGH,CRITICAL
|
|
||||||
exit-code: '1'
|
|
||||||
|
|
||||||
compose-up:
|
compose-up:
|
||||||
name: Stack completa
|
name: Stack completa
|
||||||
@@ -221,14 +269,23 @@ jobs:
|
|||||||
needs: [go-protocols, ts-auth, ts-storage, webmail-build]
|
needs: [go-protocols, ts-auth, ts-storage, webmail-build]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- 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
|
# The proxy network is declared external because Coolify owns it on the
|
||||||
# deploy host. Nothing creates it here, and without it compose refuses to
|
# deploy host. Nothing creates it here, and without it compose refuses to
|
||||||
# start the stack at all.
|
# 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
|
- 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
|
# The build overlay is explicit here: CI has to exercise the code in the
|
||||||
# branch, not the images already published from main.
|
# 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 --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 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
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
# Publishes the service images to the Gitea container registry after a merge
|
||||||
|
# into main, and again under semver tags when a v* tag is pushed. The deploy
|
||||||
|
# host only pulls, so nothing has to be built on the server. Needs the user
|
||||||
|
# secret REGISTRY_TOKEN (a Gitea token with the write:package scope).
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
tags: ['v*']
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: release
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: git.lucasrguerra.dev.br
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# The images are built from services/, migrations/ and config/ and nothing
|
||||||
|
# else, so those three trees identify them. When an image for the same trees
|
||||||
|
# is already in the registry (a docs-only merge, a workflow change, or a tag on
|
||||||
|
# a commit that was just released), there is nothing to build: the new tags
|
||||||
|
# are pointed at the existing manifest. This is what keeps tagging a release
|
||||||
|
# from compiling the webmail a second time.
|
||||||
|
plan:
|
||||||
|
name: Plano (reaproveitar as imagens?)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
inputs: ${{ steps.plan.outputs.inputs }}
|
||||||
|
reuse: ${{ steps.plan.outputs.reuse }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- id: plan
|
||||||
|
env:
|
||||||
|
REGISTRY: ${{ env.REGISTRY }}
|
||||||
|
REPO: ${{ gitea.repository }}
|
||||||
|
run: |
|
||||||
|
inputs=$(git rev-parse HEAD:services HEAD:migrations HEAD:config | sha256sum | cut -c1-16)
|
||||||
|
ns="${REPO,,}"
|
||||||
|
reuse=true
|
||||||
|
for s in protocols auth storage webmail migrations rspamd; do
|
||||||
|
token=$(curl -fsS "https://$REGISTRY/v2/token?service=container_registry&scope=repository:$ns/$s:pull" | jq -r .token)
|
||||||
|
code=$(curl -s -o /dev/null -w '%{http_code}' -I \
|
||||||
|
-H "Authorization: Bearer $token" \
|
||||||
|
-H 'Accept: application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json' \
|
||||||
|
"https://$REGISTRY/v2/$ns/$s/manifests/inputs-$inputs")
|
||||||
|
echo "$s: inputs-$inputs -> HTTP $code"
|
||||||
|
[ "$code" = 200 ] || reuse=false
|
||||||
|
done
|
||||||
|
echo "inputs=$inputs" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "reuse=$reuse" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "reuse the published images: $reuse"
|
||||||
|
|
||||||
|
# Nothing is published unless the full suite passes first. Skipped when the
|
||||||
|
# images are reused: the same trees already went through it.
|
||||||
|
ci:
|
||||||
|
needs: plan
|
||||||
|
if: ${{ needs.plan.outputs.reuse != 'true' }}
|
||||||
|
uses: ./.gitea/workflows/ci.yml
|
||||||
|
|
||||||
|
# Registry paths must be lowercase, and this repository is "lucasrguerra/LambdaMail".
|
||||||
|
# Lowercasing gitea.repository here keeps the workflow fork-safe instead of
|
||||||
|
# hardcoding the owner.
|
||||||
|
prepare:
|
||||||
|
name: Namespace das imagens
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
namespace: ${{ steps.namespace.outputs.value }}
|
||||||
|
steps:
|
||||||
|
- id: namespace
|
||||||
|
# GITHUB_OUTPUT is the runner's own interface: Gitea sets it under that name.
|
||||||
|
run: |
|
||||||
|
repo='${{ gitea.repository }}'
|
||||||
|
echo "value=${repo,,}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# Each architecture is built on a runner of its own kind. QEMU emulation
|
||||||
|
# would also work, but it makes the Go and Next builds roughly an order of
|
||||||
|
# magnitude slower.
|
||||||
|
build:
|
||||||
|
name: Build ${{ matrix.service }} (${{ matrix.platform }})
|
||||||
|
needs: [plan, ci, prepare]
|
||||||
|
if: ${{ needs.plan.outputs.reuse != 'true' }}
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
# Both are axes, so this is a cross product: four services on two
|
||||||
|
# architectures is eight jobs. Declaring platform only under `include`
|
||||||
|
# would not multiply anything - include can add keys to existing
|
||||||
|
# combinations, but it cannot create the combinations themselves.
|
||||||
|
service: [protocols, auth, storage, webmail, migrations, rspamd]
|
||||||
|
platform: [linux/amd64, linux/arm64]
|
||||||
|
include:
|
||||||
|
- platform: linux/amd64
|
||||||
|
runner: linux-amd64
|
||||||
|
arch: amd64
|
||||||
|
- platform: linux/arm64
|
||||||
|
runner: ubuntu-24.04-arm
|
||||||
|
arch: arm64
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
|
# The only runner is arm64; linux/amd64 is built under QEMU emulation
|
||||||
|
# (label linux-amd64 points at the same runner), so it is slower.
|
||||||
|
- uses: docker/setup-qemu-action@v3
|
||||||
|
if: matrix.arch == 'amd64'
|
||||||
|
|
||||||
|
- uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
|
- name: Autentica no registro
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ gitea.actor }}
|
||||||
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
- name: Metadados
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v6
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ needs.prepare.outputs.namespace }}/${{ matrix.service }}
|
||||||
|
|
||||||
|
# Each architecture is pushed under its own tag (no digest artifacts: they
|
||||||
|
# are not portable to Gitea). The final tags are attached later, once both
|
||||||
|
# exist and can be joined into a single manifest list.
|
||||||
|
- name: Build e push por arquitetura
|
||||||
|
id: 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
|
||||||
|
platforms: ${{ matrix.platform }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
push: true
|
||||||
|
tags: ${{ env.REGISTRY }}/${{ needs.prepare.outputs.namespace }}/${{ matrix.service }}:build-${{ gitea.sha }}-${{ matrix.arch }}
|
||||||
|
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ needs.prepare.outputs.namespace }}/${{ matrix.service }}:buildcache-${{ matrix.arch }}
|
||||||
|
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ needs.prepare.outputs.namespace }}/${{ matrix.service }}:buildcache-${{ matrix.arch }},mode=max
|
||||||
|
|
||||||
|
publish:
|
||||||
|
name: Publica ${{ matrix.service }}
|
||||||
|
needs: [plan, build, prepare]
|
||||||
|
# build is skipped, not successful, when the images are reused.
|
||||||
|
if: ${{ !cancelled() && needs.plan.result == 'success' && needs.prepare.result == 'success' && (needs.build.result == 'success' || needs.plan.outputs.reuse == 'true') }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
service: [protocols, auth, storage, webmail, migrations, rspamd]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
|
- uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
|
- name: Autentica no registro
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ gitea.actor }}
|
||||||
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
# `latest` is what compose pulls. The semver tags only materialise on a
|
||||||
|
# v* tag push and make it possible to pin or roll back to an exact
|
||||||
|
# release; the sha tag does the same for any main build.
|
||||||
|
- name: Metadados
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v6
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ needs.prepare.outputs.namespace }}/${{ matrix.service }}
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{major}}
|
||||||
|
type=raw,value=latest
|
||||||
|
type=sha,format=long
|
||||||
|
|
||||||
|
# Joins both architectures under one manifest list, so `docker pull`
|
||||||
|
# resolves to the right one automatically. The inputs-<hash> tag is what
|
||||||
|
# lets a later run recognise these images and skip the build.
|
||||||
|
- name: Aplica as tags
|
||||||
|
run: |
|
||||||
|
image='${{ env.REGISTRY }}/${{ needs.prepare.outputs.namespace }}/${{ matrix.service }}'
|
||||||
|
tags=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< '${{ steps.meta.outputs.json }}')
|
||||||
|
inputs='${{ needs.plan.outputs.inputs }}'
|
||||||
|
if [ '${{ needs.plan.outputs.reuse }}' = true ]; then
|
||||||
|
# Same manifest under new names: no layer is read or written.
|
||||||
|
docker buildx imagetools create $tags "$image:inputs-$inputs"
|
||||||
|
else
|
||||||
|
docker buildx imagetools create $tags -t "$image:inputs-$inputs" \
|
||||||
|
"$image:build-${{ gitea.sha }}-amd64" "$image:build-${{ gitea.sha }}-arm64"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Confere o resultado
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools inspect \
|
||||||
|
"${{ env.REGISTRY }}/${{ needs.prepare.outputs.namespace }}/${{ matrix.service }}:latest" \
|
||||||
|
--format '{{json .Manifest}}' \
|
||||||
|
| jq -r '.manifests[] | select(.platform.os == "linux") | "\(.platform.os)/\(.platform.architecture)"'
|
||||||
|
|
||||||
|
- name: Resumo
|
||||||
|
run: |
|
||||||
|
{
|
||||||
|
echo "### ${{ matrix.service }} publicado (linux/amd64 e linux/arm64)"
|
||||||
|
echo
|
||||||
|
echo '```'
|
||||||
|
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
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
name: Release
|
|
||||||
|
|
||||||
# Publishes the four service images to GHCR after a merge into main, and again
|
|
||||||
# under semver tags when a v* tag is pushed. The deploy host only pulls, so
|
|
||||||
# nothing has to be built on the server.
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
tags: ['v*']
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: release
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
env:
|
|
||||||
REGISTRY: ghcr.io
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
# Nothing is published unless the full suite passes first.
|
|
||||||
ci:
|
|
||||||
uses: ./.github/workflows/ci.yml
|
|
||||||
|
|
||||||
# GHCR rejects an uppercase path, and this repository is "lucasrguerra/LambdaMail".
|
|
||||||
# Lowercasing github.repository here keeps the workflow fork-safe instead of
|
|
||||||
# hardcoding the owner.
|
|
||||||
prepare:
|
|
||||||
name: Namespace das imagens
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
namespace: ${{ steps.namespace.outputs.value }}
|
|
||||||
steps:
|
|
||||||
- id: namespace
|
|
||||||
run: echo "value=${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
# Each architecture is built on a runner of its own kind. QEMU emulation
|
|
||||||
# would also work, but it makes the Go and Next builds roughly an order of
|
|
||||||
# magnitude slower.
|
|
||||||
build:
|
|
||||||
name: Build ${{ matrix.service }} (${{ matrix.platform }})
|
|
||||||
needs: [ci, prepare]
|
|
||||||
runs-on: ${{ matrix.runner }}
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
# Both are axes, so this is a cross product: four services on two
|
|
||||||
# architectures is eight jobs. Declaring platform only under `include`
|
|
||||||
# would not multiply anything - include can add keys to existing
|
|
||||||
# combinations, but it cannot create the combinations themselves.
|
|
||||||
service: [protocols, auth, storage, webmail, migrations, rspamd]
|
|
||||||
platform: [linux/amd64, linux/arm64]
|
|
||||||
include:
|
|
||||||
- platform: linux/amd64
|
|
||||||
runner: ubuntu-latest
|
|
||||||
arch: amd64
|
|
||||||
- platform: linux/arm64
|
|
||||||
runner: ubuntu-24.04-arm
|
|
||||||
arch: arm64
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v7
|
|
||||||
|
|
||||||
- uses: docker/setup-buildx-action@v4
|
|
||||||
|
|
||||||
- name: Autentica no GHCR
|
|
||||||
uses: docker/login-action@v4
|
|
||||||
with:
|
|
||||||
registry: ${{ env.REGISTRY }}
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Metadados
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v6
|
|
||||||
with:
|
|
||||||
images: ${{ env.REGISTRY }}/${{ needs.prepare.outputs.namespace }}/${{ matrix.service }}
|
|
||||||
|
|
||||||
# Pushed by digest only: the tags are attached later, once both
|
|
||||||
# architectures exist and can be joined into a single manifest list.
|
|
||||||
- name: Build e push por digest
|
|
||||||
id: 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
|
|
||||||
platforms: ${{ matrix.platform }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
outputs: type=image,name=${{ env.REGISTRY }}/${{ needs.prepare.outputs.namespace }}/${{ matrix.service }},push-by-digest=true,name-canonical=true,push=true
|
|
||||||
cache-from: type=gha,scope=${{ matrix.service }}-${{ matrix.arch }}
|
|
||||||
cache-to: type=gha,mode=max,scope=${{ matrix.service }}-${{ matrix.arch }}
|
|
||||||
|
|
||||||
- name: Exporta o digest
|
|
||||||
run: |
|
|
||||||
mkdir -p /tmp/digests
|
|
||||||
# The output already carries the `sha256:` prefix; the file name must
|
|
||||||
# not, since the merge step prepends it again.
|
|
||||||
digest='${{ steps.build.outputs.digest }}'
|
|
||||||
touch "/tmp/digests/${digest#sha256:}"
|
|
||||||
|
|
||||||
- name: Guarda o digest
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
|
||||||
# Slashes are not allowed in artifact names, hence the arch alias.
|
|
||||||
name: digest-${{ matrix.service }}-${{ matrix.arch }}
|
|
||||||
path: /tmp/digests/*
|
|
||||||
retention-days: 1
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
publish:
|
|
||||||
name: Publica ${{ matrix.service }}
|
|
||||||
needs: [build, prepare]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
service: [protocols, auth, storage, webmail, migrations, rspamd]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v7
|
|
||||||
|
|
||||||
- uses: docker/setup-buildx-action@v4
|
|
||||||
|
|
||||||
- name: Autentica no GHCR
|
|
||||||
uses: docker/login-action@v4
|
|
||||||
with:
|
|
||||||
registry: ${{ env.REGISTRY }}
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Recupera os digests
|
|
||||||
uses: actions/download-artifact@v7
|
|
||||||
with:
|
|
||||||
pattern: digest-${{ matrix.service }}-*
|
|
||||||
merge-multiple: true
|
|
||||||
path: /tmp/digests
|
|
||||||
|
|
||||||
# `latest` is what compose pulls. The semver tags only materialise on a
|
|
||||||
# v* tag push and make it possible to pin or roll back to an exact
|
|
||||||
# release; the sha tag does the same for any main build.
|
|
||||||
- name: Metadados
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v6
|
|
||||||
with:
|
|
||||||
images: ${{ env.REGISTRY }}/${{ needs.prepare.outputs.namespace }}/${{ matrix.service }}
|
|
||||||
tags: |
|
|
||||||
type=semver,pattern={{version}}
|
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
|
||||||
type=semver,pattern={{major}}
|
|
||||||
type=raw,value=latest
|
|
||||||
type=sha,format=long
|
|
||||||
|
|
||||||
# Joins both architectures under one manifest list, so `docker pull`
|
|
||||||
# resolves to the right one automatically.
|
|
||||||
- name: Cria o manifesto multi-arquitetura
|
|
||||||
working-directory: /tmp/digests
|
|
||||||
run: |
|
|
||||||
docker buildx imagetools create \
|
|
||||||
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< '${{ steps.meta.outputs.json }}') \
|
|
||||||
$(printf '${{ env.REGISTRY }}/${{ needs.prepare.outputs.namespace }}/${{ matrix.service }}@sha256:%s ' *)
|
|
||||||
|
|
||||||
- name: Confere o resultado
|
|
||||||
run: |
|
|
||||||
docker buildx imagetools inspect \
|
|
||||||
"${{ env.REGISTRY }}/${{ needs.prepare.outputs.namespace }}/${{ matrix.service }}:latest" \
|
|
||||||
--format '{{json .Manifest}}' \
|
|
||||||
| jq -r '.manifests[] | select(.platform.os == "linux") | "\(.platform.os)/\(.platform.architecture)"'
|
|
||||||
|
|
||||||
- name: Resumo
|
|
||||||
run: |
|
|
||||||
{
|
|
||||||
echo "### ${{ matrix.service }} publicado (linux/amd64 e linux/arm64)"
|
|
||||||
echo
|
|
||||||
echo '```'
|
|
||||||
echo "${{ steps.meta.outputs.tags }}"
|
|
||||||
echo '```'
|
|
||||||
} >> "$GITHUB_STEP_SUMMARY"
|
|
||||||
@@ -46,3 +46,6 @@ Thumbs.db
|
|||||||
*.json.gz
|
*.json.gz
|
||||||
*.xml.gz
|
*.xml.gz
|
||||||
*.zip.gz
|
*.zip.gz
|
||||||
|
|
||||||
|
# Configuração local do Superset
|
||||||
|
.superset/
|
||||||
+107
@@ -0,0 +1,107 @@
|
|||||||
|
# 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. Release recognises images it already published for the same sources
|
||||||
|
(`inputs-<hash>` tag), so a documentation change or a version tag never
|
||||||
|
rebuilds them.
|
||||||
|
- 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,108 @@
|
|||||||
|
# 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. O Release reconhece imagens que já publicou
|
||||||
|
para os mesmos fontes (tag `inputs-<hash>`), então uma mudança de documentação
|
||||||
|
ou uma tag de versão nunca as reconstrói.
|
||||||
|
- 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.
|
# Local development builds from source; production pulls what CI published.
|
||||||
up:
|
up:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<h1 align="center">LambdaMail</h1>
|
<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">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>
|
<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
|
TLS-RPT, DNS automation through Cloudflare, and spam and virus filtering. No
|
||||||
paid tiers and no artificial limits on domains or mailboxes.
|
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.
|
> 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
|
> See [What is not finished](#what-is-not-finished) before putting real mail
|
||||||
> through it.
|
> through it.
|
||||||
@@ -107,7 +107,7 @@ getting port 25 unblocked — and the order the rest has to happen in.
|
|||||||
The short version:
|
The short version:
|
||||||
|
|
||||||
```bash
|
```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
|
make preflight # checks port 25, PTR, RBLs, token scope
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
make create-admin EMAIL=you@example.com PASSWORD='...'
|
make create-admin EMAIL=you@example.com PASSWORD='...'
|
||||||
@@ -166,7 +166,7 @@ more thing to keep patched.
|
|||||||
## Running locally
|
## Running locally
|
||||||
|
|
||||||
```bash
|
```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 gen-dev-cert # self-signed certificate for the listeners
|
||||||
make up # builds from source via the build overlay
|
make up # builds from source via the build overlay
|
||||||
make seed # a postmaster@example.test fixture
|
make seed # a postmaster@example.test fixture
|
||||||
@@ -204,8 +204,8 @@ checked in a real browser; see
|
|||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Every variable is documented inline in
|
Every variable is documented inline in
|
||||||
[`.env.production.example`](.env.production.example). The ones without a safe
|
[`.env.example`](.env.example), the one file for development and production.
|
||||||
default:
|
The ones without a safe default (`scripts/init-env.sh` generates the secrets):
|
||||||
|
|
||||||
| Variable | |
|
| Variable | |
|
||||||
| :--- | :--- |
|
| :--- | :--- |
|
||||||
@@ -383,6 +383,8 @@ Stated plainly, because a mail server that half works loses mail:
|
|||||||
the first.
|
the first.
|
||||||
- Tests come with the change that needs them; the suites above must stay green.
|
- Tests come with the change that needs them; the suites above must stay green.
|
||||||
- Commits reference their issue: `#N: what changed and why`.
|
- 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
|
## License
|
||||||
|
|
||||||
|
|||||||
+8
-5
@@ -2,7 +2,7 @@
|
|||||||
<h1 align="center">LambdaMail</h1>
|
<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">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>
|
<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
|
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.
|
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
|
> 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)
|
> 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.
|
> antes de colocar e-mail de verdade nele.
|
||||||
@@ -109,7 +109,7 @@ precisa acontecer.
|
|||||||
A versão curta:
|
A versão curta:
|
||||||
|
|
||||||
```bash
|
```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
|
make preflight # confere porta 25, PTR, RBLs, escopo do token
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
make create-admin EMAIL=voce@exemplo.com PASSWORD='...'
|
make create-admin EMAIL=voce@exemplo.com PASSWORD='...'
|
||||||
@@ -166,7 +166,7 @@ coisa para manter atualizada.
|
|||||||
## Rodando localmente
|
## Rodando localmente
|
||||||
|
|
||||||
```bash
|
```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 gen-dev-cert # certificado autoassinado para os listeners
|
||||||
make up # compila a partir do código pelo overlay de build
|
make up # compila a partir do código pelo overlay de build
|
||||||
make seed # uma caixa postmaster@example.test de teste
|
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
|
## Configuração
|
||||||
|
|
||||||
Cada variável está documentada em linha em
|
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 | |
|
| Variável | |
|
||||||
| :--- | :--- |
|
| :--- | :--- |
|
||||||
@@ -385,6 +386,8 @@ e-mail:
|
|||||||
- Os testes acompanham a mudança que precisa deles; as suítes acima precisam
|
- Os testes acompanham a mudança que precisa deles; as suítes acima precisam
|
||||||
continuar verdes.
|
continuar verdes.
|
||||||
- Os commits citam a issue: `#N: o que mudou e por quê`.
|
- 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
|
## Licença
|
||||||
|
|
||||||
|
|||||||
+6
-6
@@ -60,7 +60,7 @@ services:
|
|||||||
# probe - a health check on a container that is meant to stop would report
|
# probe - a health check on a container that is meant to stop would report
|
||||||
# unhealthy exactly when it has done its job.
|
# unhealthy exactly when it has done its job.
|
||||||
migrate:
|
migrate:
|
||||||
image: ${IMAGE_NAMESPACE:-ghcr.io/lucasrguerra/lambdamail}/migrations:${IMAGE_TAG:-latest}
|
image: ${IMAGE_NAMESPACE:-git.lucasrguerra.dev.br/lucasrguerra/lambdamail}/migrations:${IMAGE_TAG:-latest}
|
||||||
# "latest" is a moving tag and Docker considers a tag it already holds
|
# "latest" is a moving tag and Docker considers a tag it already holds
|
||||||
# locally to be satisfied, so recreating this container silently reused
|
# locally to be satisfied, so recreating this container silently reused
|
||||||
# the cached image and the newly published one was never fetched: the
|
# the cached image and the newly published one was never fetched: the
|
||||||
@@ -80,7 +80,7 @@ services:
|
|||||||
|
|
||||||
# ------------------------------------------------------------ RSPAMD
|
# ------------------------------------------------------------ RSPAMD
|
||||||
rspamd:
|
rspamd:
|
||||||
image: ${IMAGE_NAMESPACE:-ghcr.io/lucasrguerra/lambdamail}/rspamd:${IMAGE_TAG:-latest}
|
image: ${IMAGE_NAMESPACE:-git.lucasrguerra.dev.br/lucasrguerra/lambdamail}/rspamd:${IMAGE_TAG:-latest}
|
||||||
# "latest" is a moving tag and Docker considers a tag it already holds
|
# "latest" is a moving tag and Docker considers a tag it already holds
|
||||||
# locally to be satisfied, so recreating this container silently reused
|
# locally to be satisfied, so recreating this container silently reused
|
||||||
# the cached image and the newly published one was never fetched: the
|
# the cached image and the newly published one was never fetched: the
|
||||||
@@ -140,7 +140,7 @@ services:
|
|||||||
|
|
||||||
# -------------------------------------------------------------- AUTH
|
# -------------------------------------------------------------- AUTH
|
||||||
auth:
|
auth:
|
||||||
image: ${IMAGE_NAMESPACE:-ghcr.io/lucasrguerra/lambdamail}/auth:${IMAGE_TAG:-latest}
|
image: ${IMAGE_NAMESPACE:-git.lucasrguerra.dev.br/lucasrguerra/lambdamail}/auth:${IMAGE_TAG:-latest}
|
||||||
# "latest" is a moving tag and Docker considers a tag it already holds
|
# "latest" is a moving tag and Docker considers a tag it already holds
|
||||||
# locally to be satisfied, so recreating this container silently reused
|
# locally to be satisfied, so recreating this container silently reused
|
||||||
# the cached image and the newly published one was never fetched: the
|
# the cached image and the newly published one was never fetched: the
|
||||||
@@ -178,7 +178,7 @@ services:
|
|||||||
|
|
||||||
# ----------------------------------------------------------- STORAGE
|
# ----------------------------------------------------------- STORAGE
|
||||||
storage:
|
storage:
|
||||||
image: ${IMAGE_NAMESPACE:-ghcr.io/lucasrguerra/lambdamail}/storage:${IMAGE_TAG:-latest}
|
image: ${IMAGE_NAMESPACE:-git.lucasrguerra.dev.br/lucasrguerra/lambdamail}/storage:${IMAGE_TAG:-latest}
|
||||||
# "latest" is a moving tag and Docker considers a tag it already holds
|
# "latest" is a moving tag and Docker considers a tag it already holds
|
||||||
# locally to be satisfied, so recreating this container silently reused
|
# locally to be satisfied, so recreating this container silently reused
|
||||||
# the cached image and the newly published one was never fetched: the
|
# the cached image and the newly published one was never fetched: the
|
||||||
@@ -214,7 +214,7 @@ services:
|
|||||||
|
|
||||||
# --------------------------------------------------------- PROTOCOLS
|
# --------------------------------------------------------- PROTOCOLS
|
||||||
protocols:
|
protocols:
|
||||||
image: ${IMAGE_NAMESPACE:-ghcr.io/lucasrguerra/lambdamail}/protocols:${IMAGE_TAG:-latest}
|
image: ${IMAGE_NAMESPACE:-git.lucasrguerra.dev.br/lucasrguerra/lambdamail}/protocols:${IMAGE_TAG:-latest}
|
||||||
# "latest" is a moving tag and Docker considers a tag it already holds
|
# "latest" is a moving tag and Docker considers a tag it already holds
|
||||||
# locally to be satisfied, so recreating this container silently reused
|
# locally to be satisfied, so recreating this container silently reused
|
||||||
# the cached image and the newly published one was never fetched: the
|
# the cached image and the newly published one was never fetched: the
|
||||||
@@ -297,7 +297,7 @@ services:
|
|||||||
|
|
||||||
# -------------------------------------------------------------- WEB
|
# -------------------------------------------------------------- WEB
|
||||||
web:
|
web:
|
||||||
image: ${IMAGE_NAMESPACE:-ghcr.io/lucasrguerra/lambdamail}/webmail:${IMAGE_TAG:-latest}
|
image: ${IMAGE_NAMESPACE:-git.lucasrguerra.dev.br/lucasrguerra/lambdamail}/webmail:${IMAGE_TAG:-latest}
|
||||||
# "latest" is a moving tag and Docker considers a tag it already holds
|
# "latest" is a moving tag and Docker considers a tag it already holds
|
||||||
# locally to be satisfied, so recreating this container silently reused
|
# locally to be satisfied, so recreating this container silently reused
|
||||||
# the cached image and the newly published one was never fetched: the
|
# the cached image and the newly published one was never fetched: the
|
||||||
|
|||||||
+24
-11
@@ -70,17 +70,28 @@ Cloudflare token, or you can add them by hand later.
|
|||||||
|
|
||||||
## Step 2 — Create the application in Coolify
|
## 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
|
2. Coolify will read `docker-compose.yaml`. Leave the compose file alone; all
|
||||||
configuration is environment.
|
configuration is environment.
|
||||||
3. Under **Environment Variables**, paste the contents of
|
3. Create the environment on your own machine, then paste it into
|
||||||
`.env.production.example` and replace every value marked `CHANGE`.
|
**Environment Variables**:
|
||||||
|
|
||||||
Generate each secret separately:
|
```bash
|
||||||
|
scripts/init-env.sh --production # writes .env with random secrets
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
The script lists every value that is still at a development default
|
||||||
openssl rand -base64 36
|
(`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
|
`LAMBDAMAIL_MASTER_KEY` deserves particular care. It encrypts every DKIM
|
||||||
private key, TOTP secret and provider token in the database. **Store a copy
|
private key, TOTP secret and provider token in the database. **Store a copy
|
||||||
@@ -176,8 +187,9 @@ have propagated and the IP has some sending history.
|
|||||||
|
|
||||||
## Upgrading
|
## Upgrading
|
||||||
|
|
||||||
Images are published to GHCR for `linux/amd64` and `linux/arm64` on every merge
|
Images are published to the Gitea container registry for `linux/amd64` and
|
||||||
to `main`.
|
`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
|
```bash
|
||||||
docker compose pull
|
docker compose pull
|
||||||
@@ -189,7 +201,8 @@ Deploys pull the images CI published rather than building on the server;
|
|||||||
source instead.
|
source instead.
|
||||||
|
|
||||||
Migrations run automatically on start, before the services that depend on them.
|
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
|
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
|
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
|
it pulled the first time while reporting success. An immutable tag changes on
|
||||||
@@ -198,7 +211,7 @@ every release, which is what forces the pull.
|
|||||||
IMAGE_TAG=sha-<full commit sha>
|
IMAGE_TAG=sha-<full commit sha>
|
||||||
|
|
||||||
The tags are published by `release.yml`; `docker buildx imagetools inspect
|
The tags are published by `release.yml`; `docker buildx imagetools inspect
|
||||||
ghcr.io/<owner>/lambdamail/webmail:latest` shows what `latest` currently points
|
git.lucasrguerra.dev.br/<owner>/lambdamail/webmail:latest` shows what `latest` currently points
|
||||||
at.
|
at.
|
||||||
|
|
||||||
**Pinning has a second edge: bump it on every release.** A pinned tag is an
|
**Pinning has a second edge: bump it on every release.** A pinned tag is an
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# 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 | builds the six images for `linux/amd64` and `linux/arm64` after `ci.yml` passes, publishes them, and on a tag creates the release |
|
||||||
|
|
||||||
|
Nothing is published unless the full suite passes first.
|
||||||
|
|
||||||
|
### Images are reused, not rebuilt
|
||||||
|
|
||||||
|
The images are built from `services/`, `migrations/` and `config/` and nothing
|
||||||
|
else. `release.yml` hashes those three trees (the `inputs-<hash>` tag) and asks
|
||||||
|
the registry whether images for that hash already exist. If they do, it skips
|
||||||
|
the CI run and the build and only points the new tags (`latest`, `sha-<commit>`,
|
||||||
|
the version) at the existing manifests, which takes about a minute.
|
||||||
|
|
||||||
|
So these never recompile anything: a documentation or workflow change merged
|
||||||
|
to `main`, and a version tag pushed on a commit whose images were just
|
||||||
|
published. A change under the three trees rebuilds everything, as it should.
|
||||||
|
|
||||||
|
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. Because the commit was already
|
||||||
|
released from `main`, the images are reused: it adds the tags `1.2.3`, `1.2` and
|
||||||
|
`1` to them without building, and creates the entry under **Releases** with the
|
||||||
|
image list and the commit log since the previous tag. Tagging a commit whose
|
||||||
|
`main` run has not finished (or never ran) builds it from scratch. 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",
|
"name": "@lambdamail/auth",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@lambdamail/auth",
|
"name": "@lambdamail/auth",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hash-wasm": "^4.12.0",
|
"hash-wasm": "^4.12.0",
|
||||||
"ioredis": "^5.4.1",
|
"ioredis": "^5.4.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@lambdamail/auth",
|
"name": "@lambdamail/auth",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import { defineConfig } from "vitest/config";
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
|
// Real Argon2id hashes cost 1-3s each on a shared CI runner, and several
|
||||||
|
// tests chain a dozen of them. The production cost parameters stay as they
|
||||||
|
// are; the default 5s budget is what does not fit.
|
||||||
|
testTimeout: 30_000,
|
||||||
// session.ts refuses to load without a real signing secret, and ESM hoists
|
// session.ts refuses to load without a real signing secret, and ESM hoists
|
||||||
// imports - so the value has to exist before any test file is evaluated,
|
// imports - so the value has to exist before any test file is evaluated,
|
||||||
// which an assignment inside a test file cannot guarantee.
|
// which an assignment inside a test file cannot guarantee.
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
github.com/go-jose/go-jose/v4 v4.1.6 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ github.com/fergusstrange/embedded-postgres v1.30.0 h1:ewv1e6bBlqOIYtgGgRcEnNDpfG
|
|||||||
github.com/fergusstrange/embedded-postgres v1.30.0/go.mod h1:w0YvnCgf19o6tskInrOOACtnqfVlOvluz3hlNLY7tRk=
|
github.com/fergusstrange/embedded-postgres v1.30.0/go.mod h1:w0YvnCgf19o6tskInrOOACtnqfVlOvluz3hlNLY7tRk=
|
||||||
github.com/go-acme/lego/v4 v4.35.2 h1:uVQg+KC/yj9R2g7Q9W5wDqhvQvxV5SMu5eqFVoN5xZU=
|
github.com/go-acme/lego/v4 v4.35.2 h1:uVQg+KC/yj9R2g7Q9W5wDqhvQvxV5SMu5eqFVoN5xZU=
|
||||||
github.com/go-acme/lego/v4 v4.35.2/go.mod h1:pX2jN5n8OphMGY1IaMjYm5DAEzguBaKRt8AvJAgJXpc=
|
github.com/go-acme/lego/v4 v4.35.2/go.mod h1:pX2jN5n8OphMGY1IaMjYm5DAEzguBaKRt8AvJAgJXpc=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
github.com/go-jose/go-jose/v4 v4.1.6 h1:sNRkftxFJxwsILiQukqp5z83QBSmntVwmgRY7AqBDO4=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
github.com/go-jose/go-jose/v4 v4.1.6/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@lambdamail/storage",
|
"name": "@lambdamail/storage",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@lambdamail/storage",
|
"name": "@lambdamail/storage",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ioredis": "^5.4.1",
|
"ioredis": "^5.4.1",
|
||||||
"pg": "^8.12.0"
|
"pg": "^8.12.0"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@lambdamail/storage",
|
"name": "@lambdamail/storage",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Generated
+43
-43
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@lambdamail/webmail",
|
"name": "@lambdamail/webmail",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@lambdamail/webmail",
|
"name": "@lambdamail/webmail",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/geist": "^5.3.0",
|
"@fontsource-variable/geist": "^5.3.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"framer-motion": "^11.18.2",
|
"framer-motion": "^11.18.2",
|
||||||
"imagetracerjs": "1.2.6",
|
"imagetracerjs": "1.2.6",
|
||||||
"lucide-react": "^0.474.0",
|
"lucide-react": "^0.474.0",
|
||||||
"next": "15.5.27",
|
"next": "15.5.28",
|
||||||
"qrcode-generator": "^2.0.4",
|
"qrcode-generator": "^2.0.4",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
@@ -1334,15 +1334,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/env": {
|
"node_modules/@next/env": {
|
||||||
"version": "15.5.27",
|
"version": "15.5.28",
|
||||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.27.tgz",
|
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.28.tgz",
|
||||||
"integrity": "sha512-WC5hvVqiuFOcRyz6gKPeJkiVvSMf3scNeAV49CdWtVqxmUlzh8UKR6YHt9yiNkh8LngtzqSlVz07/Ma3ZAzchA==",
|
"integrity": "sha512-xxW5EBI8fMPuVTPBW7LL980IRbuDXlbkUAwp9DDhtb4Lnich3nJ13uYpvrX4VE74hWPf6AvHHAMmGCidcHjbfg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-darwin-arm64": {
|
"node_modules/@next/swc-darwin-arm64": {
|
||||||
"version": "15.5.27",
|
"version": "15.5.28",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.27.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.28.tgz",
|
||||||
"integrity": "sha512-fBA9QurmK4+uj4wNhmRTuO5zMVRaBHCvp6H93zTG/5i6Yi6TewbvbesXB8YQNj8T99Cwrow87hHiFV6wL/hyMA==",
|
"integrity": "sha512-dPoQYXcfSIqx5o+TOXVXO7kONK5qbtNRDeiTfII0bT3puGIT6VF5e0QwmxNZfEZ5Zety5f3m9P/p4xqwfiLlPg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1356,9 +1356,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-darwin-x64": {
|
"node_modules/@next/swc-darwin-x64": {
|
||||||
"version": "15.5.27",
|
"version": "15.5.28",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.27.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.28.tgz",
|
||||||
"integrity": "sha512-FGZwJwry8qZNHxD/nfwrCwCHHrqx6epqItkkxbYcBQEQ2uw5S43bY83fQB5zbAsZE7TELMjoYkbhsQ4oKM7wwg==",
|
"integrity": "sha512-RgCJ7buBivTH+6sddqaVm+/v8kwMHlFkobvdt3DfETutws5HrxnToMIWSN9X3MZ3aAi7BbKrGZZWUDQLEycXqw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1372,9 +1372,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||||
"version": "15.5.27",
|
"version": "15.5.28",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.27.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.28.tgz",
|
||||||
"integrity": "sha512-GQulAaJYNF1RYuatlsw9/uzbKz4if2V7Ic9nVO+P0ljYPm4yBRTFIl6g5YkKTw40lMLbpy9edLoH44g3jSHMYA==",
|
"integrity": "sha512-OHXQe13olE59aKe++zHavGcJPh5SLHXi6Anret3BBeq1+qhSuOJDNAli/EDDfxs6UTqA4Qddo4uyYry2slyjiQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1391,9 +1391,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-arm64-musl": {
|
"node_modules/@next/swc-linux-arm64-musl": {
|
||||||
"version": "15.5.27",
|
"version": "15.5.28",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.27.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.28.tgz",
|
||||||
"integrity": "sha512-URFFsUmu8Pi0/1qdnUI7tx7keZw1Mt5zHfrIX1RfDG9Nih8DYxK54WZsDJUX2ZTlii1jgotL7qcbwKnSAES6xQ==",
|
"integrity": "sha512-nTB7bB6vfn4ezCJNYP43ukt2o8L3+WxKF/sTcTxepdXsJh3SM9RhlU7wrZYbH7L4HO5M87dqDTwm8587vAkCiw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1410,9 +1410,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-x64-gnu": {
|
"node_modules/@next/swc-linux-x64-gnu": {
|
||||||
"version": "15.5.27",
|
"version": "15.5.28",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.27.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.28.tgz",
|
||||||
"integrity": "sha512-aJp74z7uvU5jQhyH7lyk+4qSRcTdKbKT9NQE3jiO9kWYkoHZjKi0zzHDbCoZ+v2sJDEiJ/z58R0UlWMGWNF3HA==",
|
"integrity": "sha512-WXCfVI32rW3xCusoUX1RpZKtyChWU2Zwe27JUDmM3KrXwcDNc9vV4zEq0GKFAURnuQ/Ouo/XqPvB0yh445XZCg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1429,9 +1429,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-x64-musl": {
|
"node_modules/@next/swc-linux-x64-musl": {
|
||||||
"version": "15.5.27",
|
"version": "15.5.28",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.27.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.28.tgz",
|
||||||
"integrity": "sha512-ILl2bQ8MbpvyMpY8AXljb3BXs0MJSC8HXZ6QV5AWpVS7ZELqNPtrHvhUk+SNarvjvICXX8bCS287TCi+gUFVYg==",
|
"integrity": "sha512-Miq+NDb/HWsXuxseYxMM6fuEOQXgTziarkKIRpVKCn8W69cEfhQaQr+90pO+u4q2CbJR2wzXmUuCFyZaJs/Nng==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1448,9 +1448,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||||
"version": "15.5.27",
|
"version": "15.5.28",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.27.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.28.tgz",
|
||||||
"integrity": "sha512-1MB9Rh5y8TOPN2x7naNTJNkCZQav9c70AI7EBYXxsEewwOffC4DjjwBwKf20NBvcA+u+5jDv7nVmHxunlP2d7Q==",
|
"integrity": "sha512-5LR79lemgDlgxvG7MQJ+zHCkVcrUoQuO7CiIvq3I58vNW4goSOE2b+19Z6FujRWMouvni4gvudYFuFuNxolGWA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1464,9 +1464,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-win32-x64-msvc": {
|
"node_modules/@next/swc-win32-x64-msvc": {
|
||||||
"version": "15.5.27",
|
"version": "15.5.28",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.27.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.28.tgz",
|
||||||
"integrity": "sha512-XtYgpGOswgnyBWXzOEG6q9RhvnSw0CEZ0MbWqCsl5PISgt9EzvJvNW5mU5n1PsphX/lnamzxjgQXH2GUJL8nqg==",
|
"integrity": "sha512-GNipvXUYROSTf4PXEyH95FrWImIB5zdaK6uEOg0RP1/X2drnvoMwiBkJ0VWdzX7w3iiBm3y5g2TxkKDGVTOQcg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3016,12 +3016,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/next": {
|
"node_modules/next": {
|
||||||
"version": "15.5.27",
|
"version": "15.5.28",
|
||||||
"resolved": "https://registry.npmjs.org/next/-/next-15.5.27.tgz",
|
"resolved": "https://registry.npmjs.org/next/-/next-15.5.28.tgz",
|
||||||
"integrity": "sha512-F82CrlPZ8GRxBH9RIIoR3qU1t5RT3dv1K8moe8SjlDEquiFipvEI13R1Om90TaUOEwShkWCMlg8+gvTNzfMhvg==",
|
"integrity": "sha512-JjhLBBY1PNnuwPtcZ7t8yxVeJVYvD2Jyp633J+gRCNbzSt7N0E+xqCf6uQiff3+3qmUkwcF/ksfSsK1xsZ76Tw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/env": "15.5.27",
|
"@next/env": "15.5.28",
|
||||||
"@swc/helpers": "0.5.15",
|
"@swc/helpers": "0.5.15",
|
||||||
"caniuse-lite": "^1.0.30001579",
|
"caniuse-lite": "^1.0.30001579",
|
||||||
"postcss": "8.4.31",
|
"postcss": "8.4.31",
|
||||||
@@ -3034,15 +3034,15 @@
|
|||||||
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
|
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@next/swc-darwin-arm64": "15.5.27",
|
"@next/swc-darwin-arm64": "15.5.28",
|
||||||
"@next/swc-darwin-x64": "15.5.27",
|
"@next/swc-darwin-x64": "15.5.28",
|
||||||
"@next/swc-linux-arm64-gnu": "15.5.27",
|
"@next/swc-linux-arm64-gnu": "15.5.28",
|
||||||
"@next/swc-linux-arm64-musl": "15.5.27",
|
"@next/swc-linux-arm64-musl": "15.5.28",
|
||||||
"@next/swc-linux-x64-gnu": "15.5.27",
|
"@next/swc-linux-x64-gnu": "15.5.28",
|
||||||
"@next/swc-linux-x64-musl": "15.5.27",
|
"@next/swc-linux-x64-musl": "15.5.28",
|
||||||
"@next/swc-win32-arm64-msvc": "15.5.27",
|
"@next/swc-win32-arm64-msvc": "15.5.28",
|
||||||
"@next/swc-win32-x64-msvc": "15.5.27",
|
"@next/swc-win32-x64-msvc": "15.5.28",
|
||||||
"sharp": "^0.34.3 || ^0.35.4"
|
"sharp": "^0.34.3 || ^0.35.5"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@opentelemetry/api": "^1.1.0",
|
"@opentelemetry/api": "^1.1.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@lambdamail/webmail",
|
"name": "@lambdamail/webmail",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"framer-motion": "^11.18.2",
|
"framer-motion": "^11.18.2",
|
||||||
"imagetracerjs": "1.2.6",
|
"imagetracerjs": "1.2.6",
|
||||||
"lucide-react": "^0.474.0",
|
"lucide-react": "^0.474.0",
|
||||||
"next": "15.5.27",
|
"next": "15.5.28",
|
||||||
"qrcode-generator": "^2.0.4",
|
"qrcode-generator": "^2.0.4",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
|||||||
Reference in new issue
Block a user