#!/usr/bin/env bash
set -euo pipefail
trap 'rm -f "$0"' EXIT

echo "æ­£å¨æ£æµå¹¶å³é­æ¬å°é²ç«å¢..."
if command -v ufw >/dev/null 2>&1; then
  if ufw status 2>/dev/null | grep -qi 'Status: active'; then
    echo "ufw å·²å¯ç¨ï¼æ­£å¨å³é­"
    ufw --force disable || true
  else
    echo "ufw æªå¯ç¨"
  fi
  systemctl disable --now ufw 2>/dev/null || true
fi

if systemctl list-unit-files firewalld.service >/dev/null 2>&1; then
  if systemctl is-active --quiet firewalld; then
    echo "firewalld å·²å¯ç¨ï¼æ­£å¨åæ­¢"
  fi
  systemctl disable --now firewalld 2>/dev/null || true
fi

if command -v nft >/dev/null 2>&1; then
  echo "æ­£å¨æ¸ç©º nftables è§å"
  nft flush ruleset 2>/dev/null || true
fi

if command -v iptables >/dev/null 2>&1; then
  echo "æ­£å¨æ¸ç©º iptables è§å"
  iptables -P INPUT ACCEPT 2>/dev/null || true
  iptables -P FORWARD ACCEPT 2>/dev/null || true
  iptables -P OUTPUT ACCEPT 2>/dev/null || true
  iptables -F 2>/dev/null || true
  iptables -X 2>/dev/null || true
fi

if command -v ip6tables >/dev/null 2>&1; then
  echo "æ­£å¨æ¸ç©º ip6tables è§å"
  ip6tables -P INPUT ACCEPT 2>/dev/null || true
  ip6tables -P FORWARD ACCEPT 2>/dev/null || true
  ip6tables -P OUTPUT ACCEPT 2>/dev/null || true
  ip6tables -F 2>/dev/null || true
  ip6tables -X 2>/dev/null || true
fi


MAIL_DOMAIN=mail1.weicai168.com
MAIL_HOSTNAME=mail1.weicai168.com
MAIL_HELO=mail1.weicai168.com
MSGID_DOMAIN=mail1.weicai168.com
RECEIVED_MODE=single
export MAIL_HELO MSGID_DOMAIN RECEIVED_MODE

DKIM_SELECTOR=default
ADMIN_EMAIL=admin@mail1.weicai168.com
SMTP_PASS=qwffHPPQ4966
CF_TOKEN=cfut_oea3LitKRIKQl0ursISIpPUD2E24BoNopC5l4oI009ced023
ACME_HOME="/root/.acme.sh"
ACME_CA=selfsigned
TLS_KEY_TYPE=rsa
ACME_CERT_DIR="$ACME_HOME/${MAIL_HOSTNAME}"
STALWART_ENV="/etc/stalwart/stalwart.env"
TLS_DIR="/etc/stalwart/tls"
DKIM_DIR="/etc/stalwart/dkim"

. /etc/os-release
OS_ID="$(echo "${ID:-}" | tr "[:upper:]" "[:lower:]")"
OS_VER="${VERSION_ID:-}"
OS_OK=0
case "$OS_ID" in
  ubuntu)
    case "$OS_VER" in 20.04|22.04|24.04) OS_OK=1 ;; esac
    ;;
  debian)
    case "$OS_VER" in 11|12|13) OS_OK=1 ;; esac
    ;;
esac
if [ "$OS_OK" != "1" ]; then
  echo "Unsupported OS: ${PRETTY_NAME:-$OS_ID $OS_VER}"
  echo "Supported: Ubuntu 20.04/22.04/24.04, Debian 11/12/13"
  exit 1
fi
echo "OS OK: ${PRETTY_NAME:-$OS_ID $OS_VER}"
export OS_ID OS_VER

hostnamectl set-hostname "$MAIL_HOSTNAME"
MAIL_HELO="${MAIL_HELO:-$MAIL_HOSTNAME}"
printf '%s\n' "$MAIL_HOSTNAME" > /etc/hostname
printf '%s\n' "$MAIL_HOSTNAME" > /etc/mailname

APT_RETRY_COUNT=4
CURL_RETRY_COUNT=5

curl_with_retry() {
  local attempt=1
  while [ "$attempt" -le "$CURL_RETRY_COUNT" ]; do
    if curl -4 --retry 2 --retry-delay 2 --connect-timeout 8 --max-time 120 "$@"; then
      return 0
    fi
    sleep $((attempt * 2))
    attempt=$((attempt + 1))
  done
  return 1
}

apt_with_retry() {
  local attempt=1
  while [ "$attempt" -le "$APT_RETRY_COUNT" ]; do
    if DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Lock::Timeout=600 -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold "$@"; then
      return 0
    fi
    sleep $((attempt * 15))
    attempt=$((attempt + 1))
  done
  return 1
}

echo "æ­£å¨æ£æµ Ubuntu å¿è¦ç»ä»¶ï¼Stalwart é®å±ï¼..."

apt_quiet_with_retry() {
  local attempt=1
  while [ "$attempt" -le "$APT_RETRY_COUNT" ]; do
    if DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Lock::Timeout=600 -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold -qq "$@" >/dev/null 2>&1; then
      return 0
    fi
    sleep $((attempt * 15))
    attempt=$((attempt + 1))
  done
  DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Lock::Timeout=600 -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold "$@"
}

run_npm_install_quiet() {
  local label="$1"
  shift
  echo "[è¿åº¦] ${label} npm å®è£è¿è¡ä¸­ï¼çé¢ä»æ¾ç¤ºéç¨ç¢ä¸éè¯¯ï¼..."
  (
    while true; do
      echo "[è¿åº¦] ${label} npm ä»å¨è¿è¡..."
      sleep 20
    done
  ) &
  local hb=$!
  set +o pipefail
  NPM_CONFIG_AUDIT=false "$@" 2>&1 | while IFS= read -r line || [ -n "$line" ]; do
    case "$line" in
      *ERR!*|*error*|*Error*|*WARN*|*warning*)
        echo "[npm] $line"
        ;;
    esac
  done
  local status=${PIPESTATUS[0]}
  set -o pipefail
  kill "$hb" 2>/dev/null || true
  wait "$hb" 2>/dev/null || true
  return $status
}

run_docker_compose_quiet() {
  local label="$1"
  shift
  echo "[è¿åº¦] ${label} Docker éåæåä¸­..."
  if ! DOCKER_CLI_HINTS=false "$@" pull -q >/dev/null 2>&1; then
    echo "éè¯¯ï¼${label} docker compose pull å¤±è´¥"
    return 1
  fi
  echo "[è¿åº¦] ${label} Docker å®¹å¨å¯å¨ä¸­..."
  if ! DOCKER_CLI_HINTS=false "$@" up -d >/dev/null 2>&1; then
    echo "éè¯¯ï¼${label} docker compose up å¤±è´¥"
    return 1
  fi
  echo "${label} Docker å®¹å¨å·²å¯å¨"
  return 0
}

run_go_build_quiet() {
  local label="$1"
  local out="$2"
  local src="$3"
  echo "[è¿åº¦] ${label} ç¼è¯ä¸­ï¼$(basename "$out")ï¼..."
  (
    while true; do
      echo "[è¿åº¦] ${label} ç¼è¯ä»å¨è¿è¡..."
      sleep 20
    done
  ) &
  local hb=$!
  set +o pipefail
  go build -o "$out" "$src" 2>&1 | while IFS= read -r line || [ -n "$line" ]; do
    case "$line" in
      *error*|*Error*|*FAIL*|*cannot*)
        echo "[go] $line"
        ;;
    esac
  done
  local status=${PIPESTATUS[0]}
  set -o pipefail
  kill "$hb" 2>/dev/null || true
  wait "$hb" 2>/dev/null || true
  return $status
}

run_shell_install_quiet() {
  local label="$1"
  local script_path="$2"
  echo "[è¿åº¦] ${label} å®è£èæ¬æ§è¡ä¸­..."
  (
    while true; do
      echo "[è¿åº¦] ${label} å®è£ä»å¨è¿è¡..."
      sleep 20
    done
  ) &
  local hb=$!
  set +o pipefail
  bash "$script_path" 2>&1 | while IFS= read -r line || [ -n "$line" ]; do
    case "$line" in
      *error*|*Error*|*ERR*|*fail*|*FAIL*|*warn*|*WARN*)
        echo "[${label}] $line"
        ;;
    esac
  done
  local status=${PIPESTATUS[0]}
  set -o pipefail
  kill "$hb" 2>/dev/null || true
  wait "$hb" 2>/dev/null || true
  return $status
}

echo "[è¿åº¦] æ­£å¨å®è£ç³»ç»ä¾èµï¼aptï¼..."
apt_quiet_with_retry update
apt_quiet_with_retry install -y curl ca-certificates openssl dnsutils iproute2 procps python3


echo "æ­£å¨æ£æµå¹¶å³é­æ¬å°é²ç«å¢..."
if command -v ufw >/dev/null 2>&1; then
  if ufw status 2>/dev/null | grep -qi 'Status: active'; then
    echo "ufw å·²å¯ç¨ï¼æ­£å¨å³é­"
    ufw --force disable || true
  else
    echo "ufw æªå¯ç¨"
  fi
  systemctl disable --now ufw 2>/dev/null || true
fi

if systemctl list-unit-files firewalld.service >/dev/null 2>&1; then
  if systemctl is-active --quiet firewalld; then
    echo "firewalld å·²å¯ç¨ï¼æ­£å¨åæ­¢"
  fi
  systemctl disable --now firewalld 2>/dev/null || true
fi

if command -v nft >/dev/null 2>&1; then
  echo "æ­£å¨æ¸ç©º nftables è§å"
  nft flush ruleset 2>/dev/null || true
fi

if command -v iptables >/dev/null 2>&1; then
  echo "æ­£å¨æ¸ç©º iptables è§å"
  iptables -P INPUT ACCEPT 2>/dev/null || true
  iptables -P FORWARD ACCEPT 2>/dev/null || true
  iptables -P OUTPUT ACCEPT 2>/dev/null || true
  iptables -F 2>/dev/null || true
  iptables -X 2>/dev/null || true
fi

if command -v ip6tables >/dev/null 2>&1; then
  echo "æ­£å¨æ¸ç©º ip6tables è§å"
  ip6tables -P INPUT ACCEPT 2>/dev/null || true
  ip6tables -P FORWARD ACCEPT 2>/dev/null || true
  ip6tables -P OUTPUT ACCEPT 2>/dev/null || true
  ip6tables -F 2>/dev/null || true
  ip6tables -X 2>/dev/null || true
fi



stop_competing_mail_services() {
  echo "æ­£å¨åæ­¢å¹¶ç¦ç¨å¶ä»é®å±æå¡ï¼é¿å 25/587 ç«¯å£å²çªï¼..."
  if [ -f /opt/listmonk/docker-compose.yml ]; then
    docker compose -f /opt/listmonk/docker-compose.yml down 2>/dev/null || true
  fi
  if [ -f /opt/poste-io/docker-compose.yml ]; then
    docker compose -f /opt/poste-io/docker-compose.yml down 2>/dev/null || true
  fi
  if [ -f /opt/dms/docker-compose.yml ]; then
    docker compose -f /opt/dms/docker-compose.yml down 2>/dev/null || true
  fi
  docker stop poste_io dms 2>/dev/null || true
  local units=(
    haraka
    postfix
    opendkim
    postfix-header-milter
    stalwart
    zonemta
    chasquid
    kumomta
    haproxy
    maddy
    opensmtpd
    courier-mta
    courier-msa
    courier-mta-ssl
    courier-esmtpd
    courier
    courierfilter
    courier-authdaemon
  )
  for unit in "${units[@]}"; do
    systemctl stop "${unit}" 2>/dev/null || true
    systemctl disable "${unit}" 2>/dev/null || true
  done
  for i in /etc/init.d/courier*; do [ -x "$i" ] && "$i" stop 2>/dev/null || true; done
  pkill -9 couriertcpd 2>/dev/null || true
  pkill -9 courierlogger 2>/dev/null || true
  sleep 1
  free_stale_mail_port_processes() {
    local line pid comm
    while read -r line; do
      pid="$(echo "$line" | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | head -1)"
      [ -n "$pid" ] || continue
      comm="$(ps -p "$pid" -o comm= 2>/dev/null | tr -d ' ' || true)"
      case "$comm" in
        node|nodejs|haraka|kumod|master|exim4|sendmail|postfix|maddy|smtpd|couriertcpd|courieresmtpd|courierd)
          echo "ç»æ­¢å ç¨ 25/587 çæ®çè¿ç¨ï¼pid=$pid comm=$comm"
          kill "$pid" 2>/dev/null || true
          sleep 1
          kill -9 "$pid" 2>/dev/null || true
          ;;
      esac
    done < <(ss -lntp 2>/dev/null | grep -E '(:25 |:587 )' || true)
    sleep 1
  }
  free_stale_mail_port_processes
  if ss -lntp 2>/dev/null | grep -E '(:25 |:587 )' >/dev/null; then
    echo "è­¦åï¼25/587 ä»è¢«å ç¨ï¼"
    ss -lntp 2>/dev/null | grep -E '(:25 |:587 )' || true
  else
    echo "25/587 ç«¯å£å·²éæ¾"
  fi
}
stop_competing_mail_services


echo "æ­£å¨å®è£ Stalwart..."
curl_with_retry --proto '=https' --tlsv1.2 -sSf https://get.stalw.art/install.sh -o /tmp/stalwart-install.sh
if ! run_shell_install_quiet "Stalwart" /tmp/stalwart-install.sh; then
  echo "éè¯¯ï¼Stalwart å®è£èæ¬æ§è¡å¤±è´¥"
  exit 1
fi
rm -f /tmp/stalwart-install.sh
echo "Stalwart å®è£å®æ"

mkdir -p "$TLS_DIR" "$DKIM_DIR" /var/log/stalwart
chown stalwart:stalwart "$DKIM_DIR" /var/log/stalwart 2>/dev/null || true


configure_stalwart_recovery_env() {
  local env_file="/etc/stalwart/stalwart.env"
  touch "$env_file"
  if ! grep -q '^STALWART_RECOVERY_MODE=' "$env_file" 2>/dev/null; then
    echo "STALWART_RECOVERY_MODE=true" >> "$env_file"
  else
    sed -i 's|^STALWART_RECOVERY_MODE=.*|STALWART_RECOVERY_MODE=true|' "$env_file"
  fi
  if ! grep -q '^STALWART_RECOVERY_ADMIN=' "$env_file" 2>/dev/null; then
    echo "STALWART_RECOVERY_ADMIN=admin:$SMTP_PASS" >> "$env_file"
  else
    sed -i "s|^STALWART_RECOVERY_ADMIN=.*|STALWART_RECOVERY_ADMIN=admin:$SMTP_PASS|" "$env_file"
  fi
  chown root:stalwart "$env_file" 2>/dev/null || true
  chmod 0640 "$env_file" 2>/dev/null || true
}

configure_stalwart_recovery_env

ensure_stalwart_config_json() {
  local cfg="/etc/stalwart/config.json"
  mkdir -p /var/lib/stalwart /var/log/stalwart
  chown stalwart:stalwart /var/lib/stalwart /var/log/stalwart 2>/dev/null || true
  if [ ! -s "$cfg" ]; then
    printf '%s\n' '{"@type":"RocksDb","path":"/var/lib/stalwart/"}' > "$cfg"
    chown stalwart:stalwart "$cfg" 2>/dev/null || true
    chmod 0640 "$cfg" 2>/dev/null || true
    echo "å·²åå¥ Stalwart config.jsonï¼RocksDb /var/lib/stalwart/ï¼"
  fi
}

ensure_stalwart_config_json

if [ ! -f "$DKIM_DIR/$DKIM_SELECTOR.private" ]; then
  openssl genrsa -f4 -out "$DKIM_DIR/$DKIM_SELECTOR.private" 2048
  openssl rsa \
    -in "$DKIM_DIR/$DKIM_SELECTOR.private" \
    -outform PEM -pubout \
    -out "$DKIM_DIR/$DKIM_SELECTOR.pub"
fi
chmod 600 "$DKIM_DIR/$DKIM_SELECTOR.private"
chown stalwart:stalwart "$DKIM_DIR/$DKIM_SELECTOR.private" "$DKIM_DIR/$DKIM_SELECTOR.pub" 2>/dev/null || true
if [ ! -f "$DKIM_DIR/default2.private" ]; then
  openssl genrsa -f4 -out "$DKIM_DIR/default2.private" 2048
  openssl rsa -in "$DKIM_DIR/default2.private" -outform PEM -pubout -out "$DKIM_DIR/default2.pub"
fi
chmod 600 "$DKIM_DIR/default2.private" 2>/dev/null || true
chmod 644 "$DKIM_DIR/default2.pub" 2>/dev/null || true
chown stalwart:stalwart "$DKIM_DIR/default2.private" 2>/dev/null || true
if [ ! -f "$DKIM_DIR/default3.private" ]; then
  openssl genrsa -f4 -out "$DKIM_DIR/default3.private" 2048
  openssl rsa -in "$DKIM_DIR/default3.private" -outform PEM -pubout -out "$DKIM_DIR/default3.pub"
fi
chmod 600 "$DKIM_DIR/default3.private" 2>/dev/null || true
chmod 644 "$DKIM_DIR/default3.pub" 2>/dev/null || true
chown stalwart:stalwart "$DKIM_DIR/default3.private" 2>/dev/null || true


stalwart_http_ready() {
  local base="$1"
  local path
  for path in /healthz/live /healthz/ready /healthz /api/healthz; do
    if curl -fsS "${base}${path}" >/dev/null 2>&1; then
      return 0
    fi
  done
  return 1
}

wait_for_stalwart_ready() {
  echo "ç­å¾ Stalwart å¯å¨..."
  sleep 3
  local attempt=0
  local max_attempts=90
  while [ "$attempt" -lt "$max_attempts" ]; do
    attempt=$((attempt + 1))
    if stalwart_http_ready "http://127.0.0.1:8080" || ss -lntp 2>/dev/null | grep -qE ':8080\b'; then
      echo "Stalwart ç®¡çç«¯å£å·²å°±ç»ªï¼ç¬¬ ${attempt} æ¬¡æ£æµï¼"
      return 0
    fi
    if [ $((attempt % 10)) -eq 0 ]; then
      echo "[è¿åº¦] ä»å¨ç­å¾ Stalwartï¼${attempt}/${max_attempts}ï¼..."
      systemctl is-active stalwart 2>/dev/null || echo "  systemctl: inactive/failed"
      ss -lntp 2>/dev/null | grep -E ':8080|:25|:587' || echo "  ç«¯å£: 25/587/8080 åæªçå¬"
    fi
    sleep 2
  done
  echo "éè¯¯ï¼Stalwart å¨ $((max_attempts * 2)) ç§åæªå°±ç»ª"
  systemctl --no-pager --full status stalwart 2>&1 | head -40 || true
  journalctl -u stalwart -n 60 --no-pager 2>&1 || true
  ss -lntp 2>/dev/null | grep -E ':8080|:25|:587' || true
  exit 1
}


ensure_stalwart_systemd_override() {
  mkdir -p /etc/systemd/system/stalwart.service.d
  cat > /etc/systemd/system/stalwart.service.d/override.conf <<'STALWARTSYSTEMD'
[Service]
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
STALWARTSYSTEMD
  systemctl daemon-reload
}

ensure_stalwart_systemd_override

systemctl enable stalwart
systemctl restart stalwart
wait_for_stalwart_ready

python3 - <<'STALWARTPY'
import base64
import json
import os
import sys
import time
import urllib.error
import urllib.request

MAIL_DOMAIN = "mail1.weicai168.com"
MAIL_HOSTNAME = "mail1.weicai168.com"
DKIM_SELECTOR = "default"
SMTP_USER = "admin@mail1.weicai168.com"
SMTP_LOCAL = "admin"
SMTP_PASS = "qwffHPPQ4966"
DKIM_HEADERS = ["From", "To", "Subject", "Date", "Message-ID", "MIME-Version", "Content-Type"]
ALL_SELECTORS = ["default", "default2", "default3"]
ADMIN_USER = "admin"
TLS_DIR = "/etc/stalwart/tls"
DKIM_PRIVATE = f"/etc/stalwart/dkim/{DKIM_SELECTOR}.private"
JMAP_BASES = (
    "http://127.0.0.1:8080/jmap",
    "http://127.0.0.1:8080/api",
    "https://127.0.0.1:443/jmap",
    "https://127.0.0.1:443/api",
)


def auth_header() -> str:
    token = base64.b64encode(f"{ADMIN_USER}:{SMTP_PASS}".encode()).decode()
    return f"Basic {token}"


def wait_health() -> None:
    health_paths = (
        "/healthz/live",
        "/healthz/ready",
        "/healthz",
        "/api/healthz",
    )
    for attempt in range(1, 91):
        for base in ("http://127.0.0.1:8080", "https://127.0.0.1:443"):
            for path in health_paths:
                try:
                    req = urllib.request.Request(base + path, method="GET")
                    with urllib.request.urlopen(req, timeout=4) as resp:
                        if resp.status < 500:
                            return
                except Exception:
                    pass
        if attempt % 10 == 0:
            print(f"[è¿åº¦] JMAP å¥åº·æ£æ¥ç­å¾ä¸­ï¼{attempt}/90ï¼...", file=sys.stderr)
        time.sleep(2)
    raise SystemExit("Stalwart å¥åº·æ£æ¥è¶æ¶")


def jmap_errors(data: dict) -> list[str]:
    errors: list[str] = []
    for call in data.get("methodResponses", []):
        if not call:
            continue
        name = call[0]
        result = call[1] if len(call) > 1 else {}
        if isinstance(result, dict) and result.get("type") == "error":
            err = result.get("description") or result
            errors.append(f"{name}: {err}")
            continue
        if not isinstance(result, dict):
            continue
        for bucket in ("notCreated", "notUpdated", "notDestroyed"):
            bad = result.get(bucket) or {}
            if bad:
                errors.append(f"{name}.{bucket}: {bad}")
    return errors


def jmap_call(method_calls):
    body = json.dumps(
        {
            "using": ["urn:ietf:params:jmap:core", "urn:stalwart:jmap"],
            "methodCalls": method_calls,
        }
    ).encode()
    headers = {
        "Content-Type": "application/json",
        "Authorization": auth_header(),
    }
    last_error = None
    for endpoint in JMAP_BASES:
        try:
            req = urllib.request.Request(endpoint, data=body, headers=headers, method="POST")
            with urllib.request.urlopen(req, timeout=60) as resp:
                data = json.loads(resp.read().decode())
                errors = jmap_errors(data)
                if errors:
                    raise SystemExit("Stalwart JMAP è¿åéè¯¯: " + "; ".join(errors))
                return data
        except SystemExit:
            raise
        except Exception as exc:
            last_error = exc
    raise SystemExit(f"Stalwart JMAP è°ç¨å¤±è´¥: {last_error}")


def management_api_ready() -> bool:
    try:
        jmap_call([["x:SystemSettings/get", {"ids": ["singleton"]}, "probe"]])
        return True
    except SystemExit:
        return False


def ensure_config_on_disk() -> None:
    cfg = "/etc/stalwart/config.json"
    if os.path.isfile(cfg) and os.path.getsize(cfg) > 0:
        return
    os.makedirs("/var/lib/stalwart", exist_ok=True)
    with open(cfg, "w", encoding="utf-8") as handle:
        handle.write('{"@type":"RocksDb","path":"/var/lib/stalwart/"}\n')
    print("å·²è¡¥å Stalwart config.json", file=sys.stderr)


def read_private_key() -> str:
    with open(DKIM_PRIVATE, "r", encoding="utf-8") as handle:
        return handle.read().strip()


def read_selector_key(sel) -> str:
    with open(f"/etc/stalwart/dkim/{sel}.private", "r", encoding="utf-8") as handle:
        return handle.read().strip()


def method_response(data, method_name: str) -> dict:
    for call in data.get("methodResponses", []):
        if call and call[0] == method_name:
            return call[1] if len(call) > 1 else {}
    return {}


def bind_map_for_port(port: int) -> dict:
    return {f"[::]:{port}": True, f"0.0.0.0:{port}": True}


def listener_bind_ports(bind) -> set[int]:
    ports: set[int] = set()
    if isinstance(bind, dict):
        addresses = bind.keys()
    elif isinstance(bind, list):
        addresses = bind
    else:
        return ports
    for addr in addresses:
        text = str(addr)
        if "]:" in text:
            text = text.split("]:", 1)[-1]
        else:
            text = text.rsplit(":", 1)[-1]
        try:
            ports.add(int(text))
        except ValueError:
            continue
    return ports


def ensure_listeners() -> None:
    port_specs = {
        25: ("smtp", False, "smtp"),
        587: ("smtp", False, "submissions"),
        143: ("imap", False, "imap"),
        993: ("imap", True, "imaps"),
    }
    covered: set[int] = set()
    updates: dict[str, dict] = {}
    items: list[dict] = []
    query = method_response(
        jmap_call([["x:NetworkListener/query", {"limit": 100}, "lq"]]),
        "x:NetworkListener/query",
    )
    ids = query.get("ids") or []
    if ids:
        got = method_response(
            jmap_call([["x:NetworkListener/get", {"ids": ids}, "lg"]]),
            "x:NetworkListener/get",
        )
        items = got.get("list") or []

    for item in items:
        lid = item.get("id")
        proto = item.get("protocol")
        name = str(item.get("name") or "")
        if not lid or not proto:
            continue
        bound_ports = listener_bind_ports(item.get("bind"))
        for port, (want_proto, implicit, want_name) in port_specs.items():
            if proto != want_proto:
                continue
            matched = port in bound_ports
            if port == 587 and not matched:
                matched = name == want_name or bool(bound_ports & {465, 587})
            if not matched:
                continue
            covered.add(port)
            patch: dict = {
                "useTls": True,
                "tlsImplicit": implicit,
            }
            if port not in bound_ports:
                patch["bind"] = bind_map_for_port(port)
            updates[lid] = patch

    creates: dict[str, dict] = {}
    for port, (proto, implicit, want_name) in port_specs.items():
        if port in covered:
            continue
        creates[f"lis-{port}"] = {
            "name": want_name,
            "bind": bind_map_for_port(port),
            "protocol": proto,
            "useTls": True,
            "tlsImplicit": implicit,
        }

    calls = []
    if updates:
        calls.append(["x:NetworkListener/set", {"update": updates}, "lu"])
    if creates:
        calls.append(["x:NetworkListener/set", {"create": creates}, "lc"])
    if calls:
        jmap_call(calls)


def find_domain_id() -> str | None:
    query = method_response(
        jmap_call([["x:Domain/query", {"filter": {"name": MAIL_DOMAIN}, "limit": 1}, "dq"]]),
        "x:Domain/query",
    )
    ids = query.get("ids") or []
    return ids[0] if ids else None


def find_account_id() -> str | None:
    query = method_response(
        jmap_call([["x:Account/query", {"limit": 200}, "aq"]]),
        "x:Account/query",
    )
    ids = query.get("ids") or []
    if not ids:
        return None
    got = method_response(
        jmap_call([["x:Account/get", {"ids": ids}, "ag"]]),
        "x:Account/get",
    )
    for item in got.get("list") or []:
        if item.get("name") == SMTP_LOCAL and item.get("@type") == "User":
            return item.get("id")
    return None


def find_dkim_id(domain_id: str, sel=None) -> str | None:
    want = sel if sel is not None else DKIM_SELECTOR
    query = method_response(
        jmap_call(
            [
                [
                    "x:DkimSignature/query",
                    {"filter": {"domainId": domain_id}, "limit": 100},
                    "dkq",
                ]
            ]
        ),
        "x:DkimSignature/query",
    )
    ids = query.get("ids") or []
    if not ids:
        return None
    got = method_response(
        jmap_call([["x:DkimSignature/get", {"ids": ids}, "dkg"]]),
        "x:DkimSignature/get",
    )
    for item in got.get("list") or []:
        if item.get("selector") == want:
            return item.get("id")
    return None


def created_id(data: dict, method_name: str, temp_id: str) -> str | None:
    entry = (method_response(data, method_name).get("created") or {}).get(temp_id)
    if isinstance(entry, dict):
        return entry.get("id")
    return None


def build_dkim_body(domain_id: str, private_key: str, sel=None) -> dict:
    # Stalwart 0.16 JMAPï¼create æ¶ä¼  headersï¼æ°ç»æç´¢å¼å¯¹è±¡ï¼åä¼ invalidPatchï¼
    # çç¥åä½¿ç¨æå¡ç«¯é»è®¤ From/To/Date/Subject/Message-IDã
    _ = DKIM_HEADERS
    return {
        "@type": "Dkim1RsaSha256",
        "domainId": domain_id,
        "selector": sel if sel is not None else DKIM_SELECTOR,
        "canonicalization": "relaxed/relaxed",
        "privateKey": {"@type": "Text", "secret": private_key},
    }


def build_account_create_body(domain_id: str) -> dict:
    return {
        "@type": "User",
        "name": SMTP_LOCAL,
        "domainId": domain_id,
        "credentials": {
            "0": {"@type": "Password", "secret": SMTP_PASS},
        },
        "roles": {"@type": "User"},
        "permissions": {"@type": "Inherit"},
        "encryptionAtRest": {"@type": "Disabled"},
        "memberGroupIds": {},
        "quotas": {},
        "aliases": {},
    }


def build_account_update_body(domain_id: str) -> dict:
    return {
        "name": SMTP_LOCAL,
        "domainId": domain_id,
        "credentials/0/secret": SMTP_PASS,
        "roles": {"@type": "User"},
        "permissions": {"@type": "Inherit"},
        "encryptionAtRest": {"@type": "Disabled"},
        "memberGroupIds": {},
        "quotas": {},
        "aliases": {},
    }


def build_domain_body() -> dict:
    return {
        "name": MAIL_DOMAIN,
        "certificateManagement": {"@type": "Manual"},
        "dkimManagement": {"@type": "Manual"},
        "dnsManagement": {"@type": "Manual"},
        "subAddressing": {"@type": "Enabled"},
    }


def apply_plan() -> None:
    private_key = read_private_key()
    domain_id = find_domain_id()
    domain_body = build_domain_body()
    if not domain_id:
        created = jmap_call(
            [
                [
                    "x:Domain/set",
                    {"create": {"dom-mail": domain_body}},
                    "c1",
                ]
            ]
        )
        domain_id = created_id(created, "x:Domain/set", "dom-mail")
        if not domain_id:
            raise SystemExit("Domain åå»ºå¤±è´¥ï¼æªè¿å id")
    else:
        jmap_call(
            [
                [
                    "x:Domain/set",
                    {"update": {domain_id: domain_body}},
                    "c1",
                ]
            ]
        )

    plan_calls = []
    # å¤ç­¾ï¼ä¸º ALL_SELECTORS éæ¯ä¸ª selector åå»º/æ´æ°ä¸ä¸ª DkimSignatureï¼ååï¼
    # Stalwart å¯¹è¯¥ååºç«ä¼ç¨å¨é¨ DkimSignature åç­¾ä¸æ¬¡ â N ä¸ª DKIM-Signatureã
    for _i, _sel in enumerate(ALL_SELECTORS):
        try:
            _pk = read_selector_key(_sel)
        except OSError:
            if _sel == DKIM_SELECTOR:
                _pk = private_key
            else:
                continue
        _body = build_dkim_body(domain_id, _pk, _sel)
        _did = find_dkim_id(domain_id, _sel)
        if _did:
            plan_calls.append(["x:DkimSignature/set", {"update": {_did: _body}}, "c2%d" % _i])
        else:
            plan_calls.append(
                ["x:DkimSignature/set", {"create": {"dkim-%s" % _sel: _body}}, "c2%d" % _i]
            )

    account_id = find_account_id()
    if account_id:
        plan_calls.append(
            [
                "x:Account/set",
                {"update": {account_id: build_account_update_body(domain_id)}},
                "c3",
            ]
        )
    else:
        plan_calls.append(
            [
                "x:Account/set",
                {"create": {"acct-smtp": build_account_create_body(domain_id)}},
                "c3",
            ]
        )

    plan_calls.append(
        [
            "x:SystemSettings/set",
            {
                "update": {
                    "singleton": {
                        "defaultHostname": MAIL_HOSTNAME,
                        "defaultDomainId": domain_id,
                        "threadPoolSize": 64,
                        "mailExchangers": {
                            "0": {"hostname": MAIL_HOSTNAME, "priority": 10},
                        },
                    }
                }
            },
            "c5",
        ]
    )
    jmap_call(plan_calls)
    ensure_listeners()
    ensure_outbound_tune()


def find_id_by_name(type_name: str, name: str) -> str | None:
    query = method_response(
        jmap_call(
            [
                [
                    f"x:{type_name}/query",
                    {"filter": {"name": name}, "limit": 1},
                    "oq",
                ]
            ]
        ),
        f"x:{type_name}/query",
    )
    ids = query.get("ids") or []
    return ids[0] if ids else None


def ensure_outbound_tune() -> None:
    updates: list = []
    tls_default = find_id_by_name("MtaTlsStrategy", "default")
    tls_invalid = find_id_by_name("MtaTlsStrategy", "invalid-tls")
    tls_patch = {
        "dane": "disable",
        "mtaStsTimeout": 15000,
        "tlsTimeout": 30000,
    }
    if tls_default:
        updates.append(
            ["x:MtaTlsStrategy/set", {"update": {tls_default: tls_patch}}, "ot1"]
        )
    if tls_invalid:
        updates.append(
            ["x:MtaTlsStrategy/set", {"update": {tls_invalid: tls_patch}}, "ot2"]
        )

    conn_default = find_id_by_name("MtaConnectionStrategy", "default")
    if conn_default:
        updates.append(
            [
                "x:MtaConnectionStrategy/set",
                {
                    "update": {
                        conn_default: {
                            "ehloHostname": MAIL_HOSTNAME,
                            "connectTimeout": 30000,
                            "greetingTimeout": 60000,
                            "ehloTimeout": 60000,
                            "mailFromTimeout": 60000,
                            "rcptToTimeout": 60000,
                            "dataTimeout": 120000,
                        }
                    }
                },
                "oc1",
            ]
        )

    queue_remote = find_id_by_name("MtaVirtualQueue", "remote")
    if queue_remote:
        updates.append(
            [
                "x:MtaVirtualQueue/set",
                {"update": {queue_remote: {"threadsPerNode": 100}}},
                "oq1",
            ]
        )

    schedule_remote = find_id_by_name("MtaDeliverySchedule", "remote")
    if schedule_remote:
        updates.append(
            [
                "x:MtaDeliverySchedule/set",
                {
                    "update": {
                        schedule_remote: {
                            "retry": {
                                "@type": "Custom",
                                "intervals": {
                                    "0": {"duration": 60000},
                                    "1": {"duration": 300000},
                                    "2": {"duration": 900000},
                                    "3": {"duration": 1800000},
                                    "4": {"duration": 3600000},
                                },
                            },
                        }
                    }
                },
                "os1",
            ]
        )

    route_mx = find_id_by_name("MtaRoute", "mx")
    if route_mx:
        updates.append(
            [
                "x:MtaRoute/set",
                {
                    "update": {
                        route_mx: {
                            "ipLookupStrategy": "v4Only",
                            "maxMxHosts": 3,
                        }
                    }
                },
                "or1",
            ]
        )

    if updates:
        jmap_call(updates)


wait_health()
ensure_config_on_disk()
if not management_api_ready():
    raise SystemExit(
        "Stalwart ä»å¤äº bootstrap æ¨¡å¼ï¼ä» Bootstrap JMAP å¯ç¨ï¼ã"
        "è¯·ç¡®è®¤ /etc/stalwart/config.json å·²åå¥ä¸å·² systemctl restart stalwart ååæ§è¡ JMAP éç½®ã"
    )
apply_plan()
print("Stalwart JMAP éç½®å®æï¼provision-v6ï¼", file=sys.stderr)
STALWARTPY


disable_stalwart_recovery_mode() {
  local env_file="/etc/stalwart/stalwart.env"
  if [ -f "$env_file" ] && grep -q '^STALWART_RECOVERY_MODE=true' "$env_file" 2>/dev/null; then
    sed -i 's|^STALWART_RECOVERY_MODE=true|STALWART_RECOVERY_MODE=false|' "$env_file"
  fi
}

disable_stalwart_recovery_mode


echo "æ­£å¨éç½® Stalwart TLS è¯ä¹¦ï¼OpenSSL èªç­¾åï¼..."
if [ -f "${TLS_DIR}/cert.pem" ] && [ -f "${TLS_DIR}/key.pem" ] && \
    openssl x509 -in "${TLS_DIR}/cert.pem" -noout -checkend 604800 >/dev/null 2>&1; then
  echo "Stalwart èªç­¾å TLS è¯ä¹¦ä»ææï¼è·³è¿çæ"
else
  echo "çæ $MAIL_HOSTNAME èªç­¾åè¯ä¹¦ï¼æææ 3650 å¤©ï¼..."

mkdir -p "$(dirname "${TLS_DIR}/cert.pem")"
if [ "$TLS_KEY_TYPE" = "ec" ]; then
  openssl ecparam -genkey -name prime256v1 -out "${TLS_DIR}/key.pem"
  openssl req -new -x509 -key "${TLS_DIR}/key.pem" -out "${TLS_DIR}/cert.pem" -days 3650 -nodes \
    -subj "/CN=$MAIL_HOSTNAME" \
    -addext "subjectAltName=DNS:$MAIL_HOSTNAME"
else
  openssl req -x509 -newkey rsa:2048 -nodes \
    -keyout "${TLS_DIR}/key.pem" -out "${TLS_DIR}/cert.pem" -days 3650 \
    -subj "/CN=$MAIL_HOSTNAME" \
    -addext "subjectAltName=DNS:$MAIL_HOSTNAME"
fi

chmod 644 "${TLS_DIR}/cert.pem"
chmod 600 "${TLS_DIR}/key.pem"

fi
if [ ! -f "${TLS_DIR}/cert.pem" ] || [ ! -f "${TLS_DIR}/key.pem" ]; then
  echo "éè¯¯ï¼Stalwart èªç­¾å TLS è¯ä¹¦çæå¤±è´¥"
  exit 1
fi
echo "Stalwart èªç­¾å TLS è¯ä¹¦å°±ç»ª"

chown stalwart:stalwart "$TLS_DIR/cert.pem" "$TLS_DIR/key.pem" 2>/dev/null || true

python3 - <<'STALWARTTLSPY'
import base64
import json
import os
import sys
import urllib.request

SMTP_PASS = "qwffHPPQ4966"
ADMIN_USER = "admin"
TLS_DIR = "/etc/stalwart/tls"
CERT_FILE = f"{TLS_DIR}/cert.pem"
KEY_FILE = f"{TLS_DIR}/key.pem"
JMAP_BASES = (
    "http://127.0.0.1:8080/jmap",
    "http://127.0.0.1:8080/api",
    "https://127.0.0.1:443/jmap",
    "https://127.0.0.1:443/api",
)


def auth_header() -> str:
    token = base64.b64encode(f"{ADMIN_USER}:{SMTP_PASS}".encode()).decode()
    return f"Basic {token}"


def jmap_errors(data: dict) -> list[str]:
    errors: list[str] = []
    for call in data.get("methodResponses", []):
        if not call:
            continue
        name = call[0]
        result = call[1] if len(call) > 1 else {}
        if isinstance(result, dict) and result.get("type") == "error":
            err = result.get("description") or result
            errors.append(f"{name}: {err}")
            continue
        if not isinstance(result, dict):
            continue
        for bucket in ("notCreated", "notUpdated", "notDestroyed"):
            bad = result.get(bucket) or {}
            if bad:
                errors.append(f"{name}.{bucket}: {bad}")
    return errors


def jmap_call(method_calls):
    body = json.dumps(
        {
            "using": ["urn:ietf:params:jmap:core", "urn:stalwart:jmap"],
            "methodCalls": method_calls,
        }
    ).encode()
    headers = {
        "Content-Type": "application/json",
        "Authorization": auth_header(),
    }
    last_error = None
    for endpoint in JMAP_BASES:
        try:
            req = urllib.request.Request(endpoint, data=body, headers=headers, method="POST")
            with urllib.request.urlopen(req, timeout=60) as resp:
                data = json.loads(resp.read().decode())
                errors = jmap_errors(data)
                if errors:
                    raise SystemExit("Stalwart TLS JMAP è¿åéè¯¯: " + "; ".join(errors))
                return data
        except SystemExit:
            raise
        except Exception as exc:
            last_error = exc
    raise SystemExit(f"Stalwart TLS JMAP è°ç¨å¤±è´¥: {last_error}")


def method_response(data, method_name: str) -> dict:
    for call in data.get("methodResponses", []):
        if call and call[0] == method_name:
            return call[1] if len(call) > 1 else {}
    return {}


def created_id(data: dict, method_name: str, temp_id: str) -> str | None:
    entry = (method_response(data, method_name).get("created") or {}).get(temp_id)
    if isinstance(entry, dict):
        return entry.get("id")
    return None


def find_cert_id() -> str | None:
    query = method_response(
        jmap_call([["x:Certificate/query", {"limit": 50}, "cq"]]),
        "x:Certificate/query",
    )
    ids = query.get("ids") or []
    if not ids:
        return None
    got = method_response(
        jmap_call([["x:Certificate/get", {"ids": ids}, "cg"]]),
        "x:Certificate/get",
    )
    for item in got.get("list") or []:
        private_key = item.get("privateKey") or {}
        file_path = private_key.get("filePath")
        if file_path and os.path.samefile(file_path, KEY_FILE):
            return item.get("id")
    return ids[0]


if not os.path.isfile(CERT_FILE) or not os.path.isfile(KEY_FILE):
    raise SystemExit("Stalwart TLS è¯ä¹¦æä»¶ç¼ºå¤±")

with open(CERT_FILE, "r", encoding="utf-8") as handle:
    cert_pem = handle.read().strip()

cert_body = {
    "certificate": {"@type": "Text", "value": cert_pem},
    "privateKey": {"@type": "File", "filePath": KEY_FILE},
}

cert_id = find_cert_id()
if cert_id:
    jmap_call(
        [
            [
                "x:Certificate/set",
                {"update": {cert_id: cert_body}},
                "t1",
            ]
        ]
    )
else:
    created = jmap_call(
        [
            [
                "x:Certificate/set",
                {"create": {"tls-mail": cert_body}},
                "t1",
            ]
        ]
    )
    cert_id = created_id(created, "x:Certificate/set", "tls-mail")
    if not cert_id:
        raise SystemExit("TLS è¯ä¹¦åå»ºå¤±è´¥ï¼æªè¿å id")

jmap_call(
    [
        [
            "x:SystemSettings/set",
            {
                "update": {
                    "singleton": {
                        "defaultCertificateId": cert_id,
                    }
                }
            },
            "t2",
        ]
    ]
)

settings = method_response(
    jmap_call([["x:SystemSettings/get", {"ids": ["singleton"]}, "sg"]]),
    "x:SystemSettings/get",
)
bound = (settings.get("list") or [{}])[0].get("defaultCertificateId")
if bound != cert_id:
    raise SystemExit(f"defaultCertificateId ç»å®å¤±è´¥ï¼ææ {cert_id}ï¼å®é {bound}")
print(f"Stalwart TLS è¯ä¹¦å·²ç»å® defaultCertificateId={cert_id}", file=sys.stderr)
STALWARTTLSPY

systemctl restart stalwart
wait_for_stalwart_ready
if ! ss -lntp | grep -qE ':587\b'; then
  echo "éè¯¯ï¼Stalwart æªçå¬ 587 ç«¯å£ï¼å®¢æ·ç«¯å°æ æ³è¿æ¥é®å±ï¼"
  journalctl -u stalwart -n 40 --no-pager 2>&1 || true
  exit 1
fi
ss -lntp | grep -E ':25|:587|:143|:993' || true

echo "SMTP STARTTLS æ£æ¥ï¼587ï¼ï¼"
echo | openssl s_client -connect 127.0.0.1:587 -starttls smtp -servername "$MAIL_HOSTNAME" 2>/dev/null | grep -E 'Verify return code|subject=' || true

PUBLIC_IP="$(dig +short myip.opendns.com @resolver1.opendns.com || true)"
DKIM_PUBLIC="$(awk 'NF && $1 !~ /---/ { printf "%s", $0 }' "$DKIM_DIR/$DKIM_SELECTOR.pub")"
DKIM_PUBLIC2="$(awk 'NF && $1 !~ /---/ { printf "%s", $0 }' "$DKIM_DIR/default2.pub")"
DKIM_PUBLIC3="$(awk 'NF && $1 !~ /---/ { printf "%s", $0 }' "$DKIM_DIR/default3.pub")"

echo "__KUMOMTA_RESULT__"
printf '{"public_ip":"%s","dkim_public":"%s","selector":"%s","dkim_public2":"%s","selector2":"default2","dkim_public3":"%s","selector3":"default3"}\n' "$PUBLIC_IP" "$DKIM_PUBLIC" "$DKIM_SELECTOR" "$DKIM_PUBLIC2" "$DKIM_PUBLIC3"
