#!/usr/bin/env bash# Loopback SMTP relay kit (euler-smtp): outbound mail, with its credentials scoped to the relay.# ==============================================================================================## Deploys / removes the small loopback submission relay through which the app# tier (first euler-auth: OTP + invite mail) sends email — so no app service needs# a direct-internet exception or the Gmail credentials. Sibling to frontend.sh /# egress.sh / ddns.sh / firewall.sh; see docs/web-server-guide.md § Egress.## Model:# - The relay (scripts/setup/euler-smtp.py, deployed to /usr/local/bin/euler-smtp)# listens on loopback 127.0.0.1:8025 and relays upstream to Gmail submission# (smtp.gmail.com:587, STARTTLS + app password). Stdlib-only, system python3.# - It runs as the dedicated `euler-smtp` user (own group, outside euler-web) and# is the sole holder of the Gmail creds and — per firewall.sh — the sole# euler-* uid permitted tcp :587.# - Clients (euler-auth) submit over plain loopback SMTP, no credentials; the# envelope sender is always forced to SMTP_ADDRESS.# - Config is the scoped /etc/euler/smtp.env (root:euler-smtp 0640), deployed by# the installer from ~/.euler/env (the authoring source: SMTP_ADDRESS /# SMTP_APP_PASSWORD). euler-smtp never reads the full ~/.euler/env.## /etc/euler/smtp.env root:euler-smtp 0640 (generated here)# /usr/local/bin/euler-smtp root:root 0755 (deployed relay)# /etc/systemd/system/euler-smtp.service (root-owned, boot-enabled)## Because the unit lives in root's systemd and runs as a locked-down user, lifecycle# (start/stop/restart) requires sudo.## Actions: deploy | remove | upgrade | status | test | help## Author: Vikas Munshi <vikas.munshi@gmail.com># Copyright (c) 2026. All rights reserved.# Licensed under the MIT License.set-euopipefailSCRIPT_DIR="$(cd"$(dirname"${BASH_SOURCE[0]}")"&&pwd)"PROJECT_ROOT="$(cd"${SCRIPT_DIR}/../.."&&pwd)"RELAY_SRC="${SCRIPT_DIR}/euler-smtp.py"# relay source in the repo (for deploy)# The authoring env (~/.euler/env, possibly vault-encrypted): ENV_FILE +# load_authoring_env, shared so every kit reads it one way.# shellcheck source=scripts/setup/authoring_env.sh."${SCRIPT_DIR}/authoring_env.sh"SYS_DIR="/etc/euler"SMTP_ENV="${SYS_DIR}/smtp.env"# scoped runtime config (root:euler-smtp 0640)RELAY_BIN="/usr/local/bin/euler-smtp"# deployed relay (euler-smtp runs this)SMTP_USER="euler-smtp"SMTP_GROUP="euler-smtp"# The loopback listener euler-auth submits to (EULER_SMTP_RELAY in auth.env).LISTEN_ADDR="${EULER_SMTP_LISTEN:-127.0.0.1:8025}"UPSTREAM_HOST="smtp.gmail.com"UPSTREAM_PORT="587"SERVICE_NAME="euler-smtp.service"SERVICE_DEST="/etc/systemd/system/${SERVICE_NAME}"# Set by load_config.SMTP_ADDRESS=""SMTP_APP_PASSWORD=""usage(){cat<<USAGEUsage: $0 [deploy|remove|upgrade|status|test|help] deploy Create the euler-smtp user, deploy the relay + scoped smtp.env, and install the root-owned, boot-enabled euler-smtp.service. Idempotent. remove Remove the service + relay (prompts before removing smtp.env + user). upgrade Alias of deploy: redeploy the relay, regenerate smtp.env + unit, restart. status Show the unit state and probe the loopback listener (EHLO). test Send a real test mail to SMTP_ADDRESS through the relay. Authoring config in ~/.euler/env: SMTP_ADDRESS + SMTP_APP_PASSWORD (Gmail app password). deploy installs a scoped copy to ${SMTP_ENV} for ${SMTP_USER}.USAGE}check_can_sudo(){if!command-vsudo&>/dev/null;thenecho"Error: sudo is not installed or not found in PATH">&2return1fiif!sudo-v2>/dev/null;thenecho"Error: current user does not have sudo privileges">&2return1fi}require_systemd(){if[!-d/run/systemd/system]||!command-vsystemctl&>/dev/null;thenecho"Error: systemd is required for the relay service; it is not active here.">&2return1fi}# Source the config and resolve the Gmail creds. Prefers the deployed scoped# /etc/euler/smtp.env (readable by root/euler-smtp); falls back to the repo ~/.euler/env# (operator). Fails if the creds are set in neither.load_config(){localsrc=""if[-r"${SMTP_ENV}"];thensrc="${SMTP_ENV}"elif[-r"${ENV_FILE}"];thensrc="${ENV_FILE}"fiif[-n"${src}"];thenload_authoring_env"${src}"||return1fiif[-z"${SMTP_ADDRESS:-}"]||[-z"${SMTP_APP_PASSWORD:-}"];thenecho"Error: SMTP_ADDRESS / SMTP_APP_PASSWORD not set in ${SMTP_ENV} or ${ENV_FILE}">&2return1fi}# Create the dedicated euler-smtp user (own group, nologin, outside euler-web).ensure_smtp_user(){if!getentgroup"${SMTP_GROUP}">/dev/null;thensudogroupadd--system"${SMTP_GROUP}"fiif!getentpasswd"${SMTP_USER}">/dev/null;thenecho"Creating system user ${SMTP_USER}..."sudouseradd--system--no-create-home--shell/usr/sbin/nologin\-g"${SMTP_GROUP}""${SMTP_USER}"fi}# Deploy the scoped runtime config euler-smtp reads (Gmail creds + listener only).deploy_smtp_env(){sudomkdir-p"${SYS_DIR}"sudotee"${SMTP_ENV}">/dev/null<<EOF# GENERATED by scripts/setup/smtp.sh — scoped runtime config for euler-smtp.# Authoring source: ~/.euler/env. Read ONLY by the relay — no app service sees these.EULER_SMTP_LISTEN=${LISTEN_ADDR}SMTP_HOST=${UPSTREAM_HOST}SMTP_PORT=${UPSTREAM_PORT}SMTP_ADDRESS=${SMTP_ADDRESS}SMTP_APP_PASSWORD=${SMTP_APP_PASSWORD}EOFsudochownroot:"${SMTP_GROUP}""${SMTP_ENV}"sudochmod0640"${SMTP_ENV}"}# ── install / uninstall (root-owned unit) ─────────────────────────────────────────do_deploy(){check_can_sudo||return1require_systemd||return1load_config||return1ensure_smtp_userdeploy_smtp_envecho"Deploying relay to ${RELAY_BIN}..."sudoinstall-m0755"${RELAY_SRC}""${RELAY_BIN}"echo"Installing ${SERVICE_NAME} (loopback ${LISTEN_ADDR} -> ${UPSTREAM_HOST}:${UPSTREAM_PORT}, as ${SMTP_USER})..."sudotee"${SERVICE_DEST}">/dev/null<<EOF[Unit]Description=euler loopback SMTP submission relayDocumentation=https://github.com/vikasmunshi/euler/blob/master/docs/web-server-guide.mdAfter=network-online.targetWants=network-online.target[Service]Type=simpleUser=${SMTP_USER}Group=${SMTP_GROUP}EnvironmentFile=${SMTP_ENV}ExecStart=/usr/bin/python3 ${RELAY_BIN}Restart=on-failureRestartSec=5s# Hardening — loopback listener, upstream :587 only (enforced by firewall.sh).NoNewPrivileges=trueProtectSystem=strictProtectHome=truePrivateTmp=truePrivateDevices=trueRestrictAddressFamilies=AF_INET AF_INET6CapabilityBoundingSet=LockPersonality=trueMemoryDenyWriteExecute=trueRestrictRealtime=trueSystemCallArchitectures=nativeProtectKernelTunables=trueProtectKernelModules=trueProtectKernelLogs=trueProtectControlGroups=trueProtectClock=trueProtectHostname=trueProtectProc=invisibleProcSubset=pidRestrictNamespaces=trueRestrictSUIDSGID=trueRemoveIPC=trueSystemCallFilter=@system-serviceSystemCallErrorNumber=EPERM[Install]WantedBy=multi-user.targetEOFsudosystemctldaemon-reloadsudosystemctlenable--now"${SERVICE_NAME}"sudosystemctlrestart"${SERVICE_NAME}"# The relay uid is part of the nftables egress ruleset — regenerate if installed.if[-f/etc/systemd/system/euler-firewall.service];thenecho"Reloading the egress firewall to include ${SMTP_USER}...""${SCRIPT_DIR}/firewall.sh"reloadfido_status}do_remove(){check_can_sudo||return1if[-f"${SERVICE_DEST}"];thensudosystemctldisable--now"${SERVICE_NAME}"2>/dev/null||truesudorm-f"${SERVICE_DEST}"sudosystemctldaemon-reloadfisudorm-f"${RELAY_BIN}"localreplyread-r-p"Remove ${SMTP_ENV} (Gmail creds) and the ${SMTP_USER} user? [y/N] "replyif[["${reply}"=~^[Yy]$]];thensudorm-f"${SMTP_ENV}"ifgetentpasswd"${SMTP_USER}">/dev/null;thensudouserdel"${SMTP_USER}"2>/dev/null||true;fiifgetentgroup"${SMTP_GROUP}">/dev/null;thensudogroupdel"${SMTP_GROUP}"2>/dev/null||true;fifiecho"SMTP relay uninstall complete."}# ── status / test ─────────────────────────────────────────────────────────────────# Probe the loopback listener: connect, expect 220, EHLO, QUIT.probe_listener(){python3-"${LISTEN_ADDR}"<<'PY'import smtplibimport syshost, _, port = sys.argv[1].rpartition(':')try: with smtplib.SMTP(host, int(port), timeout=5) as smtp: code, msg = smtp.ehlo() ok = code == 250 print(f"listener: {'✓' if ok else '✗'} {host}:{port} EHLO -> {code} {msg.decode().splitlines()[0]}") sys.exit(0 if ok else 1)except OSError as exc: print(f"listener: ✗ {host}:{port} unreachable ({exc})") sys.exit(1)PY}do_status(){echo"Relay: ${LISTEN_ADDR} -> ${UPSTREAM_HOST}:${UPSTREAM_PORT} (as ${SMTP_USER})"if[-f"${SERVICE_DEST}"]&&command-vsystemctl&>/dev/null;thenecho"${SERVICE_NAME}: $(systemctlis-active"${SERVICE_NAME}"2>/dev/null)/$(systemctlis-enabled"${SERVICE_NAME}"2>/dev/null)"elseecho"${SERVICE_NAME}: ✗ not installed"fiprobe_listener||true}# Send a real test mail (to SMTP_ADDRESS itself) through the relay.do_test(){load_config||return1echo"Sending test mail to ${SMTP_ADDRESS} via ${LISTEN_ADDR}..."python3-"${LISTEN_ADDR}""${SMTP_ADDRESS}"<<'PY'import smtplibimport sysfrom email.message import EmailMessagelisten, rcpt = sys.argv[1], sys.argv[2]host, _, port = listen.rpartition(':')msg = EmailMessage()msg['From'] = rcptmsg['To'] = rcptmsg['Subject'] = 'euler-smtp relay test'msg.set_content('Test mail from the euler-smtp loopback relay (scripts/setup/smtp.sh test).')with smtplib.SMTP(host, int(port), timeout=60) as smtp: smtp.send_message(msg)print('✓ relay accepted the message — check the inbox.')PY}# ── Dispatch ──────────────────────────────────────────────────────────────────────ACTION="${1:-status}"case"${ACTION}"indeploy)do_deploy;;upgrade)do_deploy;;remove)do_remove;;status)do_status;;test)do_test;;-h|--help|help)usage;;*)echo"Unknown action: ${ACTION}";usage;exit1;;esac