Use Sendico SMTP mailbox for local notifications (#57)

This commit is contained in:
Stephan D committed 2026-10-17 20:51:28 +02:00
1 parent 3e4e496037
commit b36eb72c23
18 files changed
+528 -36

No files matched your search

+1 -1
View File
@@ -5,7 +5,7 @@ RUN apk add --no-cache protobuf protobuf-dev make
WORKDIR /src
COPY Makefile ./
COPY backend ./backend
RUN make generate-notifications
RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg/mod make generate-notifications
ARG MODULE
WORKDIR /src/backend/${MODULE}
RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg/mod CGO_ENABLED=0 go build -trimpath -o /out/service .
+7 -3
View File
@@ -18,10 +18,14 @@ messaging:
reconnect_wait: 2
templates: "/etc/sxchnge/templates.json"
email:
driver: resend
driver: smtp
settings:
api_key_env: EMAIL_API_KEY
sender_address: "" # Set your verified Resend sender here, then run make build.
host: mail.sendico.io
port: 465
username: no-reply@sendico.io
password_env: SMTP_PASSWORD
network_timeout: 10
sender_address: no-reply@sendico.io
sender_name: "SXCHNGE"
# Use replicas: 3 and environment: production for a three-node NATS cluster.
# mode: ops validates resources without modifying them; app provisions them.
+7 -7
View File
@@ -61,7 +61,7 @@ provision_agents() {
mkdir -p "$directory"; chmod 755 "$directory"
case "$service" in
bff) paths='services/bff/jwt';;
notifications) paths='infrastructure/nats/auth services/notifications/email/resend';;
notifications) paths='infrastructure/nats/auth services/notifications/email/smtp';;
nats) paths=infrastructure/nats/auth;;
sendico) paths=services/bff/sendico;;
esac
@@ -105,13 +105,12 @@ up() {
random | jq -Rs --arg field "$field" '{($field):rtrimstr("\n")}' | put "$path"
fi
done
printf '{"api_key":""}' | ensure services/notifications/email/resend
provision_mongo
provision_agents
compose up -d --wait --wait-timeout 120 nats
retire_secrets
normalize_email_secret
kv services/notifications/email/resend | email_valid || fail 'Vault kv/services/notifications/email/resend needs api_key. Run make email; configure the verified sender in infra/local/notifications/runtime.yml, then make build && make up.'
kv services/notifications/email/smtp | smtp_valid || fail 'Vault kv/services/notifications/email/smtp needs the mailbox password. Run make email.'
compose up -d --wait --wait-timeout 180
export_ca
cat <<'MESSAGE'
@@ -134,7 +133,7 @@ MESSAGE
}
email() {
existing
printf 'Resend API key (hidden): ' >&2
printf 'SMTP mailbox password (hidden): ' >&2
tty_state=$(stty -g)
trap 'stty "$tty_state"; cleanup' EXIT
stty -echo
@@ -143,7 +142,8 @@ email() {
printf '\n' >&2
[ -n "$key" ] || fail 'A key is required'
printf '%s' "$key" > "$WORK/email-key"
normalize_email_secret
jq -n --rawfile key "$WORK/email-key" '{api_key:$key}' | put services/notifications/email/resend
echo 'API key saved in Vault. Set the verified sender in infra/local/notifications/runtime.yml, then make build && make up.'
jq -n --rawfile password "$WORK/email-key" '{password:$password}' > "$WORK/smtp-secret"
smtp_valid < "$WORK/smtp-secret" || fail 'Invalid SMTP mailbox password'
put services/notifications/email/smtp < "$WORK/smtp-secret"
echo 'SMTP password saved in Vault. The notifications Agent reloads it automatically.'
}
+1
View File
@@ -64,6 +64,7 @@ ensure() (
mongo() { compose exec -T mongo mongosh --quiet --file /dev/stdin > "$WORK/mongo-result" 2> "$WORK/mongo-error" || return 1; cat "$WORK/mongo-result"; }
admin_auth() { kv infrastructure/mongo/bootstrap | jq -r '"const admin=db.getSiblingDB(\"admin\"); admin.auth(\"local-admin\","+(.password|tojson)+");"'; }
email_valid() { jq -e '(.api_key|type=="string") and (.api_key|test("\\S"))' >/dev/null; }
smtp_valid() { jq -e 'keys==["password"] and (.password|type=="string" and length>0 and (explode|all(.>0 and .!=10 and .!=13)))' >/dev/null; }
export_ca() {
compose exec -T frontend cat /data/caddy/pki/authorities/local/root.crt | write "$STATE/ca.pem" 644
}
+4
View File
@@ -29,6 +29,10 @@ if images >/dev/null 2>&1; then fail 'Offline resolution succeeded'; fi
[ "$(cat "$STATE/images.env")" = 'previous build' ] || fail 'Failed resolution replaced pins'
if printf '{"api_key":"","sender_address":"notifications@localhost.test"}' | email_valid; then fail 'Empty email accepted'; fi
printf '{"api_key":"fixture-only","sender_address":"sender@example.test"}' | email_valid
printf '{"password":"fixture-only"}' | smtp_valid
for invalid in '{"password":""}' '{"password":"bad\nvalue"}' '{"password":"bad\u0000value"}' '{"password":"fixture","username":"not-a-secret"}'; do
if printf '%s' "$invalid" | smtp_valid; then fail 'Invalid SMTP secret accepted'; fi
done
# Initialized Vault without matching local keys must fail without initializing it again.
vault() { printf '{"initialized":true}'; }
if (initialize) > "$WORK/initialize-error" 2>&1; then fail 'Missing local keys accepted'; fi
+2 -2
View File
@@ -61,11 +61,11 @@ layout() {
if [ -f "$STATE/sendico/settings.env" ]; then
kv services/bff/sendico | jq -e 'keys==["client_secret","login","password"]' >/dev/null || fail 'Sendico secret contains non-secret configuration'
fi
kv services/notifications/email/resend | jq -e 'keys==["api_key"]' >/dev/null || fail 'Non-secret email configuration remains in Vault'
kv services/notifications/email/smtp | jq -e 'keys==["password"]' >/dev/null || fail 'SMTP secret contains non-secret configuration'
for old in bootstrap/mongo bff caller services/notifications/callers/bff email nats tls frontend-tls notifications-tls; do
[ "$(vault "kv/metadata/$old" GET yes)" = null ] || fail "Obsolete KV entry remains: $old"
done
for path in services/bff/jwt services/notifications/email/resend infrastructure/nats/auth infrastructure/mongo/bootstrap; do
for path in services/bff/jwt services/notifications/email/smtp infrastructure/nats/auth infrastructure/mongo/bootstrap; do
vault "kv/metadata/$path" | jq -e '.data.current_version>0' >/dev/null || fail "Missing canonical KV entry: $path"
done
echo 'PASS: canonical Vault hierarchy present; obsolete flat and TLS entries absent'
+2 -2
View File
@@ -22,8 +22,8 @@ env_template "SXCHNGE_NATS_URL" {
env_template "EMAIL_API_KEY" {
contents = "{{ with secret \"kv/data/services/notifications/email/resend\" }}{{ .Data.data.api_key }}{{ end }}"
env_template "SMTP_PASSWORD" {
contents = "{{ with secret \"kv/data/services/notifications/email/smtp\" }}{{ .Data.data.password }}{{ end }}"
error_on_missing_key = true
}