1
0
mirror of git://f0xx.org/ac/ac-deploy synced 2026-07-29 03:38:07 +03:00

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 <cursoragent@cursor.com>
This commit is contained in:
Anton Afanasyeu
2026-06-23 18:24:10 +02:00
parent c554dc761d
commit 3d66edb405
9 changed files with 142 additions and 19 deletions

View File

@@ -62,6 +62,8 @@ FE_PROXY_TARGET=cast01.intra.raptor.org:80
# Lab DNS (2026-06): acl0.f0xx.org + c1c3.acl0.f0xx.org → FE (cast0103 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

View File

@@ -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

View File

@@ -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

View File

@@ -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 <noreply@acl0.f0xx.org>}"
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 <bestcastr@gmail.com>}"
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 <bestcastr@gmail.com>}"
fi
unset _MSMTP_PASS
mkdir -p "${COMPOSED}/config"
log "write composed config.php base_path=${ISSUES_BASE}"
cat > "${COMPOSED}/config/config.php" <<PHP

View File

@@ -0,0 +1,39 @@
#!/bin/sh
# Lab cluster outbound mail — msmtp + sendmail shim (same pattern as alpine-be).
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
# shellcheck source=/dev/null
. "$ROOT/scripts/lib/common.sh"
load_cluster_env
load_secrets_lab
FROM="${MSMTP_FROM:-bestcastr@gmail.com}"
USER="${MSMTP_USER:-bestcastr@gmail.com}"
PASS="$(read_secret MSMTP_APP_PASSWORD)"
[ -n "$PASS" ] || PASS="${MSMTP_APP_PASSWORD:-}"
[ -n "$PASS" ] || die "MSMTP_APP_PASSWORD missing in ${CAST_CLUSTER_ROOT}/secrets.lab.env"
apk add --no-cache msmtp mailx >/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}"

View File

@@ -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"

View File

@@ -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() {

View File

@@ -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"), "/");

View File

@@ -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 <noreply@acl0.f0xx.org>
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 <bestcastr@gmail.com>"
MAIL_REPLY_TO=bestcastr@gmail.com
MSMTP_FROM=bestcastr@gmail.com
MSMTP_USER=bestcastr@gmail.com
# MSMTP_APP_PASSWORD="xxxx xxxx xxxx xxxx"