From 07f5d80855d2499da9f94965d593b6c2d3ecee9b Mon Sep 17 00:00:00 2001 From: defiQUG Date: Tue, 12 May 2026 19:25:21 -0700 Subject: [PATCH] feat(mail): Mailcow provision wrappers, zone override for mail DNS/NPM - .env.master.example: CLOUDFLARE_ZONE_ID_OVERRIDE, Mailcow vars - provision-d-bis-mail-dns-and-npmplus: optional https upstream, override zone id - scripts/mailcow: defi-oracle.io DNS/NPM wrapper + Mailcow mailbox API helper Co-authored-by: Cursor --- .env.master.example | 10 ++ .../provision-d-bis-mail-dns-and-npmplus.sh | 29 ++-- .../provision-defi-oracle-mail-dns-npm.sh | 38 ++++ scripts/mailcow/provision-mailcow-mailbox.sh | 162 ++++++++++++++++++ 4 files changed, 226 insertions(+), 13 deletions(-) create mode 100755 scripts/mailcow/provision-defi-oracle-mail-dns-npm.sh create mode 100755 scripts/mailcow/provision-mailcow-mailbox.sh diff --git a/.env.master.example b/.env.master.example index bcc7ff5b..498159f0 100644 --- a/.env.master.example +++ b/.env.master.example @@ -29,6 +29,16 @@ CLOUDFLARE_ZONE_ID_D_BIS_ORG= CLOUDFLARE_ZONE_ID_MIM4U_ORG= CLOUDFLARE_ZONE_ID_SANKOFA_NEXUS= CLOUDFLARE_ZONE_ID_DEFI_ORACLE_IO= +# Optional: any zone when running scripts/cloudflare/provision-d-bis-mail-dns-and-npmplus.sh (takes precedence over *_D_BIS_ORG / CLOUDFLARE_ZONE_ID) +CLOUDFLARE_ZONE_ID_OVERRIDE= + +# --- Mailcow (VMID 10900 / mailcow-dbis; LAN 192.168.11.115) — scripts/mailcow/provision-mailcow-mailbox.sh --- +MAILCOW_BASE_URL= +MAILCOW_API_KEY= +IP_MAILCOW= +PORT_MAILCOW_HTTP= +PORT_MAILCOW_HTTPS= + CLOUDFLARE_TUNNEL_TOKEN= CLOUDFLARE_TUNNEL_ID= CLOUDFLARE_TUNNEL_ID_ALLTRA_HYBX= diff --git a/scripts/cloudflare/provision-d-bis-mail-dns-and-npmplus.sh b/scripts/cloudflare/provision-d-bis-mail-dns-and-npmplus.sh index f677d087..44658586 100755 --- a/scripts/cloudflare/provision-d-bis-mail-dns-and-npmplus.sh +++ b/scripts/cloudflare/provision-d-bis-mail-dns-and-npmplus.sh @@ -6,7 +6,7 @@ # - .env: CLOUDFLARE_API_TOKEN (or EMAIL+API_KEY) with Zone:DNS:Edit; for Origin CA also # SSL and Certificates (or use Global API Key for Origin CA — prefer scoped token with # Zone + SSL per Cloudflare dashboard). -# - CLOUDFLARE_ZONE_ID_D_BIS_ORG +# - Zone id: CLOUDFLARE_ZONE_ID_D_BIS_ORG (d-bis.org) or CLOUDFLARE_ZONE_ID_OVERRIDE / CLOUDFLARE_ZONE_ID for other zones # - NPM_URL, NPM_EMAIL, NPM_PASSWORD for NPMplus API # # Usage: @@ -25,6 +25,7 @@ # PROVISION_MX=0 1 = set apex MX to MX_TARGET (default 0: do not change — many zones use Zoho/365) # PROVISION_SPF=0 1 = upsert ONE apex TXT to SPF_TXT (default 0: do not clobber Zoho/legacy TXT) # PROVISION_DMARC=0 +# MAIL_UPSTREAM_SCHEME=http NPM upstream scheme # IP_MAIL_UPSTREAM=192.168.11.32 PMG / webmail HTTP backend # PORT_MAIL_UPSTREAM=8006 # MAIL_NPM_BLOCK_EXPLOITS=0 0 = ModSecurity off for finicky UIs (PMG admin) @@ -50,7 +51,8 @@ fi ZONE_NAME="${ZONE_NAME:-d-bis.org}" MAIL_SUB="${MAIL_SUBDOMAIN:-mail}" MAIL_FQDN="${MAIL_SUB}.${ZONE_NAME}" -ZONE_ID="${CLOUDFLARE_ZONE_ID_D_BIS_ORG:-${CLOUDFLARE_ZONE_ID:-}}" +# Prefer CLOUDFLARE_ZONE_ID_OVERRIDE when set (e.g. defi-oracle.io + CLOUDFLARE_ZONE_ID_DEFI_ORACLE_IO). +ZONE_ID="${CLOUDFLARE_ZONE_ID_OVERRIDE:-${CLOUDFLARE_ZONE_ID_D_BIS_ORG:-${CLOUDFLARE_ZONE_ID:-}}}" PUBLIC_IP="${PUBLIC_IP:-76.53.10.36}" MX_TARGET="${MX_TARGET:-$MAIL_FQDN}" MX_PRI="${MX_PRIORITY:-10}" @@ -62,6 +64,7 @@ PROVISION_DMARC="${PROVISION_DMARC:-0}" MAIL_NPM_BLOCK_EXPLOITS="${MAIL_NPM_BLOCK_EXPLOITS:-0}" IP_MAIL_UP="${IP_MAIL_UPSTREAM:-${IP_PMG:-192.168.11.32}}" PORT_MAIL_UP="${PORT_MAIL_UPSTREAM:-8006}" +MAIL_UPSTREAM_SCHEME="${MAIL_UPSTREAM_SCHEME:-http}" PROVISION_CF_ORIGIN_CERT="${PROVISION_CF_ORIGIN_CERT:-0}" PROVISION_NPM="${PROVISION_NPM:-1}" CERT_OUT_DIR="${CERT_OUT_DIR:-$PROJECT_ROOT/backups/certs}" @@ -79,7 +82,7 @@ log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_err() { echo -e "${RED}[ERR]${NC} $1"; } if [ -z "$ZONE_ID" ]; then - log_err "CLOUDFLARE_ZONE_ID_D_BIS_ORG (or CLOUDFLARE_ZONE_ID) is required" + log_err "Set zone id: CLOUDFLARE_ZONE_ID_OVERRIDE, or CLOUDFLARE_ZONE_ID_D_BIS_ORG / CLOUDFLARE_ZONE_ID (see script header)." exit 1 fi @@ -213,7 +216,7 @@ if [ "$PROVISION_NPM" != 1 ]; then exit 0 fi if [ "$DRY" = 1 ]; then - log_info "[dry-run] NPM: would ensure proxy host $MAIL_FQDN → http://${IP_MAIL_UP}:${PORT_MAIL_UP}" + log_info "[dry-run] NPM: would ensure proxy host $MAIL_FQDN → ${MAIL_UPSTREAM_SCHEME}://${IP_MAIL_UP}:${PORT_MAIL_UP}" log_ok "Done." exit 0 fi @@ -258,13 +261,13 @@ fi # Create or update proxy host if [ -z "$HOST_ID" ] || [ "$HOST_ID" = "null" ]; then - log_info "Creating proxy host $MAIL_FQDN → http://${IP_MAIL_UP}:${PORT_MAIL_UP} ..." + log_info "Creating proxy host $MAIL_FQDN → ${MAIL_UPSTREAM_SCHEME}://${IP_MAIL_UP}:${PORT_MAIL_UP} ..." if [ -n "$CERT_ID_TO_USE" ] && [ "$CERT_ID_TO_USE" != "null" ]; then - PH=$(jq -n --arg d "$MAIL_FQDN" --arg h "$IP_MAIL_UP" --argjson p "$PORT_MAIL_UP" --argjson be "$NPM_BLOCK_JSON" --argjson cid "$CERT_ID_TO_USE" \ - '{domain_names:[$d], forward_scheme:"http", forward_host:$h, forward_port:$p, allow_websocket_upgrade:true, block_exploits:$be, certificate_id:$cid, ssl_forced:true, http2_support:true, hsts_enabled:true, hsts_subdomains:false}') + PH=$(jq -n --arg d "$MAIL_FQDN" --arg s "$MAIL_UPSTREAM_SCHEME" --arg h "$IP_MAIL_UP" --argjson p "$PORT_MAIL_UP" --argjson be "$NPM_BLOCK_JSON" --argjson cid "$CERT_ID_TO_USE" \ + '{domain_names:[$d], forward_scheme:$s, forward_host:$h, forward_port:$p, allow_websocket_upgrade:true, block_exploits:$be, certificate_id:$cid, ssl_forced:true, http2_support:true, hsts_enabled:true, hsts_subdomains:false}') else - PH=$(jq -n --arg d "$MAIL_FQDN" --arg h "$IP_MAIL_UP" --argjson p "$PORT_MAIL_UP" --argjson be "$NPM_BLOCK_JSON" \ - '{domain_names:[$d], forward_scheme:"http", forward_host:$h, forward_port:$p, allow_websocket_upgrade:true, block_exploits:$be, certificate_id:null, ssl_forced:false}') + PH=$(jq -n --arg d "$MAIL_FQDN" --arg s "$MAIL_UPSTREAM_SCHEME" --arg h "$IP_MAIL_UP" --argjson p "$PORT_MAIL_UP" --argjson be "$NPM_BLOCK_JSON" \ + '{domain_names:[$d], forward_scheme:$s, forward_host:$h, forward_port:$p, allow_websocket_upgrade:true, block_exploits:$be, certificate_id:null, ssl_forced:false}') fi PR=$(curl_npm -X POST "$NPM_URL/api/nginx/proxy-hosts" -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" -d "$PH") @@ -278,12 +281,12 @@ else log_info "Updating proxy host id $HOST_ID" if [ -n "$CERT_ID_TO_USE" ] && [ "$CERT_ID_TO_USE" != "null" ]; then PAYLOAD=$(curl_npm -X GET "$NPM_URL/api/nginx/proxy-hosts/$HOST_ID" -H "Authorization: Bearer $TOK" | jq \ - --arg h "$IP_MAIL_UP" --argjson p "$PORT_MAIL_UP" --argjson be "$NPM_BLOCK_JSON" --argjson cid "$CERT_ID_TO_USE" \ - '.forward_host=$h | .forward_port=$p | .forward_scheme="http" | .block_exploits=$be | .certificate_id=$cid | .ssl_forced=true | .http2_support=true') + --arg s "$MAIL_UPSTREAM_SCHEME" --arg h "$IP_MAIL_UP" --argjson p "$PORT_MAIL_UP" --argjson be "$NPM_BLOCK_JSON" --argjson cid "$CERT_ID_TO_USE" \ + '{domain_names, forward_scheme:$s, forward_host:$h, forward_port:$p, allow_websocket_upgrade:(.allow_websocket_upgrade // true), block_exploits:$be, certificate_id:$cid, ssl_forced:true, http2_support:true, hsts_enabled:(.hsts_enabled // true), hsts_subdomains:(.hsts_subdomains // false), advanced_config:(.advanced_config // ""), locations:(.locations // [])}') else PAYLOAD=$(curl_npm -X GET "$NPM_URL/api/nginx/proxy-hosts/$HOST_ID" -H "Authorization: Bearer $TOK" | jq \ - --arg h "$IP_MAIL_UP" --argjson p "$PORT_MAIL_UP" --argjson be "$NPM_BLOCK_JSON" \ - '.forward_host=$h | .forward_port=$p | .forward_scheme="http" | .block_exploits=$be') + --arg s "$MAIL_UPSTREAM_SCHEME" --arg h "$IP_MAIL_UP" --argjson p "$PORT_MAIL_UP" --argjson be "$NPM_BLOCK_JSON" \ + '{domain_names, forward_scheme:$s, forward_host:$h, forward_port:$p, allow_websocket_upgrade:(.allow_websocket_upgrade // true), block_exploits:$be, certificate_id:(.certificate_id // null), ssl_forced:(.ssl_forced // false), http2_support:(.http2_support // true), hsts_enabled:(.hsts_enabled // false), hsts_subdomains:(.hsts_subdomains // false), advanced_config:(.advanced_config // ""), locations:(.locations // [])}') fi PUR=$(curl_npm -X PUT "$NPM_URL/api/nginx/proxy-hosts/$HOST_ID" -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" -d "$PAYLOAD") if echo "$PUR" | jq -e '.id' >/dev/null 2>&1; then diff --git a/scripts/mailcow/provision-defi-oracle-mail-dns-npm.sh b/scripts/mailcow/provision-defi-oracle-mail-dns-npm.sh new file mode 100755 index 00000000..16b9b4ad --- /dev/null +++ b/scripts/mailcow/provision-defi-oracle-mail-dns-npm.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Public mail for defi-oracle.io: Cloudflare A for mail.defi-oracle.io + NPMplus → Mailcow HTTP. +# Reuses scripts/cloudflare/provision-d-bis-mail-dns-and-npmplus.sh with zone override. +# +# .env needs: +# CLOUDFLARE_ZONE_ID_DEFI_ORACLE_IO (or set CLOUDFLARE_ZONE_ID_OVERRIDE) +# CLOUDFLARE_API_TOKEN or CLOUDFLARE_EMAIL + CLOUDFLARE_API_KEY +# NPM_EMAIL, NPM_PASSWORD when PROVISION_NPM=1 (default) +# +# Optional: +# PUBLIC_IP (default 76.53.10.36 from your stack) +# IP_MAIL_UPSTREAM (default 192.168.11.115 Mailcow) +# PORT_MAIL_UPSTREAM (default 443) +# +# Usage: +# ./scripts/mailcow/provision-defi-oracle-mail-dns-npm.sh --dry-run +# ./scripts/mailcow/provision-defi-oracle-mail-dns-npm.sh +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +# shellcheck source=/dev/null +source "${PROJECT_ROOT}/scripts/lib/load-project-env.sh" + +export ZONE_NAME="${ZONE_NAME:-defi-oracle.io}" +export MAIL_SUBDOMAIN="${MAIL_SUBDOMAIN:-mail}" +export CLOUDFLARE_ZONE_ID_OVERRIDE="${CLOUDFLARE_ZONE_ID_OVERRIDE:-${CLOUDFLARE_ZONE_ID_DEFI_ORACLE_IO:-}}" +export IP_MAIL_UPSTREAM="${IP_MAIL_UPSTREAM:-${IP_MAILCOW:-192.168.11.115}}" +export PORT_MAIL_UPSTREAM="${PORT_MAIL_UPSTREAM:-${PORT_MAILCOW_HTTPS:-443}}" +export MAIL_UPSTREAM_SCHEME="${MAIL_UPSTREAM_SCHEME:-https}" + +if [[ -z "$CLOUDFLARE_ZONE_ID_OVERRIDE" ]]; then + echo "ERROR: Set CLOUDFLARE_ZONE_ID_DEFI_ORACLE_IO in .env (or export CLOUDFLARE_ZONE_ID_OVERRIDE)." >&2 + exit 1 +fi + +exec "${PROJECT_ROOT}/scripts/cloudflare/provision-d-bis-mail-dns-and-npmplus.sh" "$@" diff --git a/scripts/mailcow/provision-mailcow-mailbox.sh b/scripts/mailcow/provision-mailcow-mailbox.sh new file mode 100755 index 00000000..265307f3 --- /dev/null +++ b/scripts/mailcow/provision-mailcow-mailbox.sh @@ -0,0 +1,162 @@ +#!/usr/bin/env bash +# Create a mailbox on Mailcow (VMID 10900 / mailcow-dbis) via REST API v1. +# +# Prerequisites: +# - Mailcow UI → System → API: create a read-write API key; allow this host's IP in API_ALLOW_FROM. +# - Root .env (or env): MAILCOW_API_KEY +# +# Optional env: +# MAILCOW_BASE_URL default https://192.168.11.115 +# MAIL_DOMAIN default defi-oracle.io +# MAIL_LOCAL_PART default theoracle +# MAIL_DISPLAY_NAME default "The Oracle" +# MAILBOX_PASSWORD if unset (non-dry-run), a random password is generated and printed once +# MAIL_QUOTA_MB default 3072 +# ENSURE_DOMAIN=1 add MAIL_DOMAIN on Mailcow when missing (default 1) +# +# Usage: +# ./scripts/mailcow/provision-mailcow-mailbox.sh --dry-run +# MAILBOX_PASSWORD='…' ./scripts/mailcow/provision-mailcow-mailbox.sh +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +# shellcheck source=/dev/null +source "${PROJECT_ROOT}/scripts/lib/load-project-env.sh" + +MAILCOW_BASE_URL="${MAILCOW_BASE_URL:-https://192.168.11.115}" +MAILCOW_BASE_URL="${MAILCOW_BASE_URL%/}" +MAIL_DOMAIN="${MAIL_DOMAIN:-defi-oracle.io}" +MAIL_LOCAL_PART="${MAIL_LOCAL_PART:-theoracle}" +MAIL_DISPLAY_NAME="${MAIL_DISPLAY_NAME:-The Oracle}" +MAIL_QUOTA_MB="${MAIL_QUOTA_MB:-3072}" +ENSURE_DOMAIN="${ENSURE_DOMAIN:-1}" + +DRY=0 +FORCE=0 +for a in "$@"; do + [[ "$a" == "--dry-run" ]] && DRY=1 + [[ "$a" == "--force" ]] && FORCE=1 +done + +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m' +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_ok() { echo -e "${GREEN}[OK]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_err() { echo -e "${RED}[ERR]${NC} $1"; } + +mc_curl() { + local method="$1" + local path="$2" + local data="${3:-}" + if [[ -n "$data" ]]; then + curl -skS --connect-timeout 10 --max-time 120 -X "$method" \ + -H "X-API-Key: ${MAILCOW_API_KEY}" \ + -H "Content-Type: application/json" \ + -d "$data" \ + "${MAILCOW_BASE_URL}/api/v1/${path}" + else + curl -skS --connect-timeout 10 --max-time 120 -X "$method" \ + -H "X-API-Key: ${MAILCOW_API_KEY}" \ + "${MAILCOW_BASE_URL}/api/v1/${path}" + fi +} + +response_ok() { + local json="$1" + echo "$json" | jq -e ' + if type == "array" then + [.[]? | objects | select(.type == "danger" or .type == "error")] | length == 0 + elif type == "object" then + (.type != "danger" and .type != "error") + else true end' >/dev/null 2>&1 +} + +if [[ "$DRY" != 1 ]] && [[ -z "${MAILCOW_API_KEY:-}" ]]; then + log_err "Set MAILCOW_API_KEY (Mailcow → System → API). Example: export MAILCOW_API_KEY='…'" + exit 1 +fi + +log_info "Mailcow: ${MAILCOW_BASE_URL} mailbox: ${MAIL_LOCAL_PART}@${MAIL_DOMAIN}" + +if [[ "$DRY" = 1 ]]; then + log_info "[dry-run] GET get/domain/all (check ${MAIL_DOMAIN})" + log_info "[dry-run] if missing and ENSURE_DOMAIN=1: POST add/domain …" + log_info "[dry-run] GET get/mailbox/all/${MAIL_DOMAIN} (check existing)" + log_info "[dry-run] POST add/mailbox for ${MAIL_LOCAL_PART}@${MAIL_DOMAIN}" + log_ok "Dry run complete." + exit 0 +fi + +# Auth probe +_auth=$(mc_curl GET "get/status/version" || echo '{"type":"error"}') +if echo "$_auth" | jq -e '.type == "error"' >/dev/null 2>&1; then + log_err "Mailcow API rejected the key or host is unreachable: $(echo "$_auth" | jq -c . 2>/dev/null || echo "$_auth")" + exit 1 +fi + +# Domain (list all — single-domain GET is inconsistent across versions) +_dom_all=$(mc_curl GET "get/domain/all" || echo '{"type":"error"}') +if echo "$_dom_all" | jq -e '.type == "error"' >/dev/null 2>&1; then + log_err "get/domain/all failed: $(echo "$_dom_all" | jq -c . 2>/dev/null || echo "$_dom_all")" + exit 1 +fi +if echo "$_dom_all" | jq -e --arg d "$MAIL_DOMAIN" ' + if type == "array" then + [(.[]? | objects) | select(.domain == $d or .domain_name == $d)] | length > 0 + else false end' >/dev/null 2>&1; then + log_ok "Domain already on Mailcow: ${MAIL_DOMAIN}" +elif [[ "$ENSURE_DOMAIN" == "1" ]]; then + log_info "Adding domain ${MAIL_DOMAIN} on Mailcow…" + _add_dom=$(jq -n \ + --arg d "$MAIL_DOMAIN" \ + '{active:"1",aliases:"400",backupmx:"0",defquota:"3072",description:"provision-mailcow-mailbox.sh",domain:$d,mailboxes:"50",maxquota:"102400",quota:"102400",relay_all_recipients:"0",restart_sogo:"1"}') + _dr=$(mc_curl POST "add/domain" "$_add_dom") + if ! response_ok "$_dr"; then + log_err "add/domain failed: $(echo "$_dr" | jq -c . 2>/dev/null || echo "$_dr")" + exit 1 + fi + log_ok "Domain added: ${MAIL_DOMAIN}" +else + log_err "Domain ${MAIL_DOMAIN} is not configured on Mailcow. Add it in the UI or set ENSURE_DOMAIN=1." + exit 1 +fi + +# Existing mailbox? +_mbs=$(mc_curl GET "get/mailbox/all/${MAIL_DOMAIN}" || echo '[]') +if echo "$_mbs" | jq -e 'type == "object" and .type == "error"' >/dev/null 2>&1; then + log_err "get/mailbox/all failed: $(echo "$_mbs" | jq -c .)" + exit 1 +fi +if echo "$_mbs" | jq -e --arg u "${MAIL_LOCAL_PART}@${MAIL_DOMAIN}" '[.[]? | select(.username == $u)] | length > 0' >/dev/null 2>&1; then + if [[ "$FORCE" == "1" ]]; then + log_warn "Mailbox exists; --force not implemented (use Mailcow UI to reset password). Exiting." + exit 1 + fi + log_ok "Mailbox already exists: ${MAIL_LOCAL_PART}@${MAIL_DOMAIN}" + exit 0 +fi + +_pw="${MAILBOX_PASSWORD:-}" +if [[ -z "$_pw" ]]; then + _pw="$(openssl rand -base64 24 | tr -d '\n' | tr '/+' 'Aa')" + log_warn "Generated MAILBOX_PASSWORD (save now; not stored): ${_pw}" +fi + +_add_mb=$(jq -n \ + --arg lp "$MAIL_LOCAL_PART" \ + --arg d "$MAIL_DOMAIN" \ + --arg n "$MAIL_DISPLAY_NAME" \ + --arg p "$_pw" \ + --argjson q "$MAIL_QUOTA_MB" \ + '{active:"1",domain:$d,local_part:$lp,name:$n,password:$p,password2:$p,quota:($q|tostring),force_pw_update:"0",tls_enforce_in:"0",tls_enforce_out:"0"}') + +_mr=$(mc_curl POST "add/mailbox" "$_add_mb") +if ! response_ok "$_mr"; then + log_err "add/mailbox failed: $(echo "$_mr" | jq -c . 2>/dev/null || echo "$_mr")" + exit 1 +fi + +log_ok "Mailbox created: ${MAIL_LOCAL_PART}@${MAIL_DOMAIN}" +log_info "Web UI: ${MAILCOW_BASE_URL}/"