From 3d66edb405144d9bdff937075acdf1066001a9d0 Mon Sep 17 00:00:00 2001 From: Anton Afanasyeu Date: Tue, 23 Jun 2026 18:24:10 +0200 Subject: [PATCH] cluster0: Gmail mail relay (msmtp + PHP SMTP), lab cron Mirror alpine-be msmtp for CLI; compose uses Gmail SMTP for php-fpm. Safe secrets.lab.env parser; deploy-mailrelay, install-lab-cron, verify-mail-lab. Co-authored-by: Cursor --- sim/cluster0/cluster.env | 2 ++ sim/cluster0/mail/msmtprc.template | 16 +++++++++ sim/cluster0/populate_lab_setup.sh | 4 +++ sim/cluster0/scripts/compose-lab-backend.sh | 18 +++++++--- sim/cluster0/scripts/deploy-mailrelay.sh | 39 +++++++++++++++++++++ sim/cluster0/scripts/install-lab-cron.sh | 28 +++++++++++++++ sim/cluster0/scripts/lib/common.sh | 32 ++++++++++++++--- sim/cluster0/scripts/verify-mail-lab.sh | 4 ++- sim/cluster0/secrets.lab.env.example | 18 +++++----- 9 files changed, 142 insertions(+), 19 deletions(-) create mode 100644 sim/cluster0/mail/msmtprc.template create mode 100755 sim/cluster0/scripts/deploy-mailrelay.sh create mode 100755 sim/cluster0/scripts/install-lab-cron.sh diff --git a/sim/cluster0/cluster.env b/sim/cluster0/cluster.env index 210fc26..efbcfc6 100644 --- a/sim/cluster0/cluster.env +++ b/sim/cluster0/cluster.env @@ -62,6 +62,8 @@ FE_PROXY_TARGET=cast01.intra.raptor.org:80 # Lab DNS (2026-06): acl0.f0xx.org + c1–c3.acl0.f0xx.org → FE (cast01–03 cluster) ACL0_CNAME=acl0.f0xx.org +MAIL_RELAY_ENABLE=1 +MAIL_GMAIL_DEST=bestcastr@gmail.com CAST01_PUBLIC=c1.acl0.f0xx.org CAST02_PUBLIC=c2.acl0.f0xx.org CAST03_PUBLIC=c3.acl0.f0xx.org diff --git a/sim/cluster0/mail/msmtprc.template b/sim/cluster0/mail/msmtprc.template new file mode 100644 index 0000000..e221246 --- /dev/null +++ b/sim/cluster0/mail/msmtprc.template @@ -0,0 +1,16 @@ +# Android Cast — outbound relay (Gmail app password via msmtp.secret) +# Installed by scripts/deploy-mailrelay.sh on cluster primary (cast01). +defaults +auth on +tls on +tls_trust_file /etc/ssl/certs/ca-certificates.crt +logfile /var/log/msmtp.log + +account gmail +host smtp.gmail.com +port 587 +from __MSMTP_FROM__ +user __MSMTP_USER__ +passwordeval cat /etc/androidcast/msmtp.secret + +account default : gmail diff --git a/sim/cluster0/populate_lab_setup.sh b/sim/cluster0/populate_lab_setup.sh index 0b6d04d..1ae730b 100644 --- a/sim/cluster0/populate_lab_setup.sh +++ b/sim/cluster0/populate_lab_setup.sh @@ -80,6 +80,10 @@ run_phase_app() { if [ "${GITEA_ENABLE:-0}" = "1" ] && is_primary_node; then run_script "$ROOT/scripts/deploy-gitea.sh" || journal_warn gitea "install failed (non-fatal)" fi + if [ "${MAIL_RELAY_ENABLE:-0}" = "1" ] && is_primary_node; then + run_script "$ROOT/scripts/deploy-mailrelay.sh" || journal_warn mailrelay "install failed (non-fatal)" + run_script "$ROOT/scripts/install-lab-cron.sh" || journal_warn lab_cron "install failed (non-fatal)" + fi run_script "$ROOT/scripts/configure-nginx.sh" rc-service php-fpm83 restart 2>/dev/null || true rc-service nginx reload 2>/dev/null || rc-service nginx restart 2>/dev/null || true diff --git a/sim/cluster0/scripts/compose-lab-backend.sh b/sim/cluster0/scripts/compose-lab-backend.sh index f40b585..8b76555 100755 --- a/sim/cluster0/scripts/compose-lab-backend.sh +++ b/sim/cluster0/scripts/compose-lab-backend.sh @@ -104,14 +104,24 @@ ISSUES_BASE="${APP_BASE_PATH:-/app/androidcast_project/issues}" PUBLIC_ORIGIN_CFG="${LAB_PUBLIC_ORIGIN:-https://acl0.f0xx.org}" load_secrets_lab AUTH_KEY="${AUTH_ENCRYPTION_KEY:-lab-cluster-dev-32-char-min-secret!!}" -MAIL_TRANSPORT="${MAIL_TRANSPORT:-smtp}" -MAIL_FROM="${MAIL_FROM:-Android Cast Lab }" -MAIL_REPLY="${MAIL_REPLY_TO:-info@apps.f0xx.org}" +_MSMTP_PASS="$(read_secret MSMTP_APP_PASSWORD 2>/dev/null || true)" +[ -n "$_MSMTP_PASS" ] || _MSMTP_PASS="${MSMTP_APP_PASSWORD:-}" +MAIL_TRANSPORT="${MAIL_TRANSPORT:-sendmail}" +MAIL_FROM="${MAIL_FROM:-Android Cast Lab }" +MAIL_REPLY="${MAIL_REPLY_TO:-bestcastr@gmail.com}" SMTP_HOST="${SMTP_HOST:-127.0.0.1}" SMTP_PORT="${SMTP_PORT:-587}" SMTP_ENC="${SMTP_ENCRYPTION:-tls}" -SMTP_USER="${SMTP_USER:-}" +SMTP_USER="${SMTP_USER:-${MSMTP_USER:-}}" SMTP_PASS="${SMTP_PASS:-}" +if [ -n "$_MSMTP_PASS" ]; then + MAIL_TRANSPORT=smtp + SMTP_HOST=smtp.gmail.com + SMTP_USER="${MSMTP_USER:-bestcastr@gmail.com}" + SMTP_PASS="$_MSMTP_PASS" + MAIL_FROM="${MAIL_FROM:-Android Cast }" +fi +unset _MSMTP_PASS mkdir -p "${COMPOSED}/config" log "write composed config.php base_path=${ISSUES_BASE}" cat > "${COMPOSED}/config/config.php" </dev/null 2>&1 || apk add --no-cache msmtp + +mkdir -p /etc/androidcast +umask 077 +printf '%s\n' "$PASS" > /etc/androidcast/msmtp.secret +chmod 600 /etc/androidcast/msmtp.secret + +sed -e "s|__MSMTP_FROM__|${FROM}|g" -e "s|__MSMTP_USER__|${USER}|g" \ + "$ROOT/mail/msmtprc.template" > /etc/msmtprc +chmod 644 /etc/msmtprc + +touch /var/log/msmtp.log +chmod 666 /var/log/msmtp.log 2>/dev/null || true + +rc-update add msmtp default 2>/dev/null || true +rc-service msmtp restart 2>/dev/null || true + +# sendmail → msmtp (php mail() / AuthMailer sendmail transport) +if [ ! -e /usr/sbin/sendmail ] || [ -L /usr/sbin/sendmail ]; then + ln -sf /usr/bin/msmtp /usr/sbin/sendmail 2>/dev/null || true +fi + +log "deploy-mailrelay_ok from=${FROM} user=${USER}" diff --git a/sim/cluster0/scripts/install-lab-cron.sh b/sim/cluster0/scripts/install-lab-cron.sh new file mode 100755 index 0000000..00c28f2 --- /dev/null +++ b/sim/cluster0/scripts/install-lab-cron.sh @@ -0,0 +1,28 @@ +#!/bin/sh +# Lab maintenance cron on primary (cast01) — mirrors alpine-be schedules where applicable. +set -eu +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +# shellcheck source=/dev/null +. "$ROOT/scripts/lib/common.sh" +load_cluster_env + +is_primary_node || { log "install-lab-cron: skip non-primary"; exit 0; } + +COMPOSED="${COMPOSE_BACKEND:-/var/www/ac/composed/backend}" +SHORTENER="/var/www/url-shortener" +CRON_FILE=/etc/crontabs/root +MARK="# ac-cluster0-lab" + +grep -v "$MARK" "$CRON_FILE" 2>/dev/null > /tmp/crontab.ac.new || : > /tmp/crontab.ac.new + +if [ -f "${COMPOSED}/scripts/purge_remote_access.php" ]; then + echo "0 * * * * cd ${COMPOSED} && php83 scripts/purge_remote_access.php --hours=24 ${MARK}" >> /tmp/crontab.ac.new +fi +if [ -f "${SHORTENER}/scripts/purge_url_shortener.php" ]; then + echo "0 4 * * * cd ${SHORTENER} && php83 scripts/purge_url_shortener.php ${MARK}" >> /tmp/crontab.ac.new +fi + +mv /tmp/crontab.ac.new "$CRON_FILE" +chmod 600 "$CRON_FILE" +rc-service crond restart 2>/dev/null || rc-service crond start 2>/dev/null || true +log "install-lab-cron_ok" diff --git a/sim/cluster0/scripts/lib/common.sh b/sim/cluster0/scripts/lib/common.sh index 88bd92d..76bf46d 100644 --- a/sim/cluster0/scripts/lib/common.sh +++ b/sim/cluster0/scripts/lib/common.sh @@ -189,10 +189,34 @@ read_cred() { load_secrets_lab() { _file="${CAST_CLUSTER_ROOT}/secrets.lab.env" [ -f "$_file" ] || return 0 - set -a - # shellcheck source=/dev/null - . "$_file" - set +a + # Export only simple KEY=value lines (no shell metacharacters in values via source). + while IFS= read -r _line || [ -n "$_line" ]; do + case "$_line" in + ''|\#*) continue ;; + *=*) + _k="${_line%%=*}" + _v="${_line#*=}" + case "$_k" in + MAIL_*|MSMTP_*|SMTP_*|AUTH_*|GITEA_*|WG_*|RSSH_*) + export "$_k=$_v" + ;; + esac + ;; + esac + done < "$_file" +} + +read_secret() { + _key="$1" + _file="${CAST_CLUSTER_ROOT}/secrets.lab.env" + [ -f "$_file" ] || return 1 + awk -v k="$_key" ' + $0 ~ "^" k "=" { + sub("^" k "=", "") + print + exit + } + ' "$_file" } ssh_node() { diff --git a/sim/cluster0/scripts/verify-mail-lab.sh b/sim/cluster0/scripts/verify-mail-lab.sh index 8bf6be0..67281ba 100755 --- a/sim/cluster0/scripts/verify-mail-lab.sh +++ b/sim/cluster0/scripts/verify-mail-lab.sh @@ -14,7 +14,9 @@ COMPOSED="${COMPOSE_BACKEND:-/var/www/ac/composed/backend}" [ -f "${COMPOSED}/config/config.php" ] || die "missing composed backend — run compose-lab-backend.sh" export MAIL_TEST_TO="$TO" -_out="$(php -r ' +PHP="${PHP_BIN:-php83}" +command -v "$PHP" >/dev/null 2>&1 || PHP=php +_out="$("$PHP" -r ' require "'"${COMPOSED}"'/src/bootstrap.php"; $to = getenv("MAIL_TEST_TO") ?: ""; $origin = rtrim((string) cfg("public_origin", "https://acl0.f0xx.org"), "/"); diff --git a/sim/cluster0/secrets.lab.env.example b/sim/cluster0/secrets.lab.env.example index 35fd4ff..0fb866f 100644 --- a/sim/cluster0/secrets.lab.env.example +++ b/sim/cluster0/secrets.lab.env.example @@ -18,13 +18,11 @@ AUTH_ENCRYPTION_KEY=lab-cluster-dev-32-char-min-secret!! # RSSH_BASTION_HOST=cast01.intra.raptor.org # RSSH_BASTION_PORT=22 -# SMTP (when lab mail is configured — prefer email recovery) -# Fill on NFS /shared/cluster/secrets.lab.env (mode 600); re-run compose-lab-backend.sh -MAIL_TRANSPORT=smtp -MAIL_FROM=Android Cast Lab -MAIL_REPLY_TO=info@apps.f0xx.org -# SMTP_HOST= -# SMTP_PORT=587 -# SMTP_ENCRYPTION=tls -# SMTP_USER= -# SMTP_PASS= +# Outbound mail (msmtp → Gmail — copy MSMTP_APP_PASSWORD from alpine-be /etc/androidcast/msmtp.secret) +# Use quotes if the app password contains spaces. +MAIL_TRANSPORT=sendmail +MAIL_FROM="Android Cast Lab " +MAIL_REPLY_TO=bestcastr@gmail.com +MSMTP_FROM=bestcastr@gmail.com +MSMTP_USER=bestcastr@gmail.com +# MSMTP_APP_PASSWORD="xxxx xxxx xxxx xxxx"