feat(mail): Mailcow provision wrappers, zone override for mail DNS/NPM
Some checks failed
Deploy to Phoenix / validate (push) Failing after 1s
Deploy to Phoenix / deploy (push) Has been skipped
Deploy to Phoenix / deploy-atomic-swap-dapp (push) Has been skipped
Deploy to Phoenix / cloudflare (push) Has been skipped

- .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 <cursoragent@cursor.com>
This commit is contained in:
defiQUG
2026-05-12 19:25:21 -07:00
parent b388cd1fbc
commit 07f5d80855
4 changed files with 226 additions and 13 deletions

View File

@@ -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_FQDNhttp://${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_FQDNhttp://${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

View File

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

View File

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