#!/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
SMTP_AUTH_USER=admin@mail1.weicai168.com
SMTP_PASS=qwffHPPQ4966
ADMIN_EMAIL=admin@mail1.weicai168.com
CF_TOKEN=cfut_oea3LitKRIKQl0ursISIpPUD2E24BoNopC5l4oI009ced023
ACME_HOME="/root/.acme.sh"
ACME_CA=selfsigned
TLS_KEY_TYPE=rsa
ACME_CERT_DIR="$ACME_HOME/${MAIL_HOSTNAME}"
TLS_DIR="/etc/postfix/ssl"
DKIM_DIR="/etc/opendkim/keys/$MAIL_DOMAIN"

. /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_LOCK_WAIT_SECONDS=300
APT_FORCE_AFTER_SECONDS=180
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 40 "$@"; then
      return 0
    fi
    if [ "$attempt" -ge "$CURL_RETRY_COUNT" ]; then
      break
    fi
    sleep $((attempt * 2))
    attempt=$((attempt + 1))
  done
  return 1
}

optimize_apt_network() {
  cat > /etc/apt/apt.conf.d/99speed <<'APTCONF'
Acquire::Retries "5";
Acquire::http::Timeout "20";
Acquire::https::Timeout "20";
Acquire::ForceIPv4 "true";
Acquire::Languages "none";
APT::Get::Assume-Yes "true";
Dpkg::Use-Pty "0";
APTCONF
}

pick_fastest_ubuntu_mirror() {
  local mirrors=(
    "http://mirrors.aliyun.com/ubuntu/"
    "https://mirrors.tuna.tsinghua.edu.cn/ubuntu/"
    "https://mirrors.ustc.edu.cn/ubuntu/"
    "http://archive.ubuntu.com/ubuntu/"
  )
  local best_url=""
  local best_time="9999"
  local m t
  for m in "${mirrors[@]}"; do
    t="$(curl -4 -o /dev/null -sS -m 6 --connect-timeout 3 -w '%{time_total}' "$m" 2>/dev/null || echo 9999)"
    if awk "BEGIN {exit !($t < $best_time)}" >/dev/null 2>&1; then
      best_time="$t"
      best_url="$m"
    fi
  done
  if [ -n "$best_url" ] && [ "$best_time" != "9999" ]; then
    echo "$best_url"
    return 0
  fi
  return 1
}

switch_ubuntu_mirror_if_needed() {
  if [ ! -f /etc/apt/sources.list ]; then
    return 0
  fi
  local best
  best="$(pick_fastest_ubuntu_mirror || true)"
  if [ -n "$best" ]; then
    echo "ä½¿ç¨æå¿«ç Ubuntu éåï¼$best"
    sed -i -E "s|https?://archive.ubuntu.com/ubuntu/|$best|g" /etc/apt/sources.list || true
    sed -i -E "s|https?://security.ubuntu.com/ubuntu/|$best|g" /etc/apt/sources.list || true
  fi
}

stop_apt_auto_services() {
  timeout 20 systemctl stop apt-daily.timer apt-daily-upgrade.timer 2>/dev/null || true
  timeout 20 systemctl stop unattended-upgrades.service packagekit.service 2>/dev/null || true
  timeout 20 systemctl disable apt-daily.timer apt-daily-upgrade.timer 2>/dev/null || true
}

wait_for_cloud_init() {
  if command -v cloud-init >/dev/null 2>&1; then
    echo "ç­å¾ cloud-init å®æé¦æ¬¡å¯å¨åå§å..."
    timeout 420 cloud-init status --wait 2>/dev/null || true
  fi
}

apt_lock_pids() {
  local pids=""
  if command -v fuser >/dev/null 2>&1; then
    for lock in /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock /var/lib/apt/lists/lock /var/cache/apt/archives/lock; do
      [ -e "$lock" ] || continue
      fuser "$lock" 2>/dev/null || true
    done | tr ' ' '\n' | awk 'NF' | sort -u | tr '\n' ' '
  fi
  echo "$pids"
}

wait_for_apt_locks() {
  local elapsed=0
  local interval=10
  while true; do
    local pids
    pids="$(apt_lock_pids)"
    [ -n "$pids" ] || break
    if [ "$elapsed" -ge "$APT_LOCK_WAIT_SECONDS" ]; then
      echo "éè¯¯ï¼apt/dpkg å¨ $APT_LOCK_WAIT_SECONDS ç§åä»è¢«å ç¨"
      return 1
    fi
    echo "apt/dpkg éè¢«å ç¨ï¼ç­å¾ $interval ç§..."
    sleep "$interval"
    elapsed=$((elapsed + interval))
  done
}

prepare_apt_environment() {
  wait_for_cloud_init
  stop_apt_auto_services
  optimize_apt_network
  switch_ubuntu_mirror_if_needed
  wait_for_apt_locks
  dpkg --configure -a || true
}

apt_with_retry() {
  local attempt=1
  while [ "$attempt" -le "$APT_RETRY_COUNT" ]; do
    wait_for_apt_locks
    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
  echo "éè¯¯ï¼apt-get $* å¨ $APT_RETRY_COUNT æ¬¡å°è¯åä»å¤±è´¥"
  return 1
}

echo "æ­£å¨æ£æµ Ubuntu å¿è¦ç»ä»¶ï¼Postfix é®å±ï¼..."
prepare_apt_environment

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
}


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

debconf-set-selections <<< "postfix postfix/mailname string $MAIL_HOSTNAME"
debconf-set-selections <<< "postfix postfix/main_mailer_type string 'Internet Site'"
REQUIRED_PACKAGES=(
  curl
  gnupg
  ca-certificates
  openssl
  dnsutils
  iproute2
  procps
  iptables
  nftables
  rsyslog
  libmilter1.0.1
  libmilter-dev
  python3-dev
  python3-pip
  postfix
  opendkim
  opendkim-tools
  libsasl2-modules
  libsasl2-modules-db
  sasl2-bin
)

echo "[è¿åº¦] æ­£å¨å®è£ç³»ç»ä¾èµï¼aptï¼..."
apt_quiet_with_retry update
DEBIAN_FRONTEND=noninteractive apt_quiet_with_retry install -y "${REQUIRED_PACKAGES[@]}"

echo "æ­£å¨å®è£ pymilterï¼é®ä»¶å¤´é­æ¹ milterï¼..."
if ! python3 -c "import Milter" >/dev/null 2>&1; then
  pip3 install pymilter --break-system-packages 2>/dev/null || pip3 install pymilter
fi
python3 -c "import Milter"

mkdir -p /opt/postfix-milter
cat > /opt/postfix-milter/normalize_headers.py <<'MILTERPYEOF'
#!/usr/bin/env python3
"""Normalize outbound headers for Postfix (align with Haraka mail_rewrite Received é­æ¹)."""
from __future__ import annotations

import email.utils
import random
import re
import sys
import time

try:
    import Milter
except ImportError:
    sys.stderr.write("ç¼ºå° pymilterï¼è¯·æ§è¡ï¼pip3 install pymilter\n")
    raise

EHLO_DOMAIN = "mail1.weicai168.com"
RECEIVED_EHLO_DOMAINS = ["amazon.co.jp", "rakuten.co.jp", "yahoo.co.jp", "docomo.ne.jp", "softbank.ne.jp", "ntt.com", "aeon.co.jp", "paypay.ne.jp", "line.me", "mercari.jp", "i.softbank.jp", "ezweb.ne.jp", "au.com", "kddi.com", "nifty.com", "so-net.ne.jp", "plala.or.jp", "ocn.ne.jp", "biglobe.ne.jp", "nifty.ne.jp", "dti.ne.jp", "jcom.zaq.ne.jp", "asahi.com", "mainichi.jp", "yomiuri.co.jp", "sony.jp", "uniqlo.com", "cookpad.com", "zozo.jp", "pixiv.net", "nicovideo.jp", "sbisec.co.jp", "mufg.jp", "smbc.co.jp", "japanpost.jp", "outlook.jp", "gmail.com", "excite.co.jp", "infoseek.jp", "goo.ne.jp", "sakura.ne.jp", "gmobb.jp", "eonet.ne.jp", "commufa.jp", "hi-ho.ne.jp", "ymobile.ne.jp", "uqmobile.jp", "povo.jp", "ahamo.com", "linemo.jp", "mineo.jp", "iijmio.jp", "biccamera.com", "yodobashi.com", "yamada-denki.jp", "nitori-net.jp", "muji.com", "dmm.co.jp", "fancl.co.jp", "shiseido.co.jp", "tabelog.com", "hotpepper.jp", "jalan.net", "ekiten.jp", "livedoor.com", "fc2.com", "ameba.jp", "hatena.ne.jp", "mixi.jp", "cyberagent.co.jp", "dena.com", "gree.net", "cygames.co.jp", "square-enix.com", "nintendo.co.jp", "bandai.co.jp", "panasonic.com", "sharp.co.jp", "toshiba.co.jp", "hitachi.co.jp", "fujitsu.com", "nec.com", "canon.jp", "nikon.co.jp", "toyota.co.jp", "honda.co.jp", "ana.co.jp", "jal.co.jp", "jr-central.co.jp", "eki-net.com", "suica.com", "sevenbank.co.jp", "jcb.co.jp", "viewcard.co.jp", "eposcard.co.jp", "life.co.jp", "ntt-east.co.jp", "ntt-west.co.jp", "kagoya.net", "xserver.jp", "lolipop.jp", "heteml.jp", "conoha.jp", "starbucks.co.jp", "matsuyafoods.co.jp", "sukiya.jp", "mos.co.jp", "lawson.co.jp", "family.co.jp", "sej.co.jp", "tokyo-gas.co.jp", "tepco.co.jp", "kepco.co.jp", "suumo.jp", "homes.co.jp", "kakaku.com", "qoo10.jp", "shop-list.com", "montbell.com", "gu-global.com", "shimamura.gr.jp", "donki.com", "matsukiyococokara.com", "edion.com", "kojima.net", "nojima.co.jp", "askul.co.jp", "monotaro.com", "kakuyasu.co.jp", "oisix.com", "zexy.net", "mynavi.jp", "rikunabi.com", "recruit.co.jp", "wantedly.com", "benesse.co.jp", "nissan.co.jp", "mazda.co.jp", "subaru.jp", "suzuki.co.jp", "mitsubishi-motors.com", "lexus.com", "resona-gr.co.jp", "shinseibank.com", "aeonfinancial.co.jp", "orico.co.jp", "cedyna.co.jp", "rakuten-card.co.jp", "auone.jp", "willcom.com", "emobile.ne.jp", "disney.co.jp", "animate.co.jp", "suruga-ya.jp", "tsutaya.co.jp", "booklive.jp", "cmoa.jp", "itmedia.co.jp", "ascii.jp", "oricon.co.jp", "rocketnews24.com", "mamastar.jp", "kodansha.co.jp", "shueisha.co.jp", "shogakukan.co.jp", "mail.yahoo.co.jp", "mail.goo.ne.jp", "mediba.jp", "gunosy.com", "smartnews.com", "news.yahoo.co.jp", "nhk.or.jp", "tbs.co.jp", "ntv.co.jp", "fujitv.co.jp", "tv-asahi.co.jp", "bs11.jp", "wowow.co.jp", "hulu.jp", "abema.tv", "dazn.com", "spotify.com", "netflix.com", "uber.com", "menu.inc", "demae-can.com", "wolt.com", "coconala.com", "crowdworks.jp", "lancers.jp", "freee.co.jp", "moneyforward.com", "yayoi-kk.co.jp", "sansan.com", "chatwork.com", "cybozu.co.jp", "kintone.com", "backlog.com", "qiita.com", "zenn.dev", "note.com", "base.shop", "stores.jp", "shopify.com", "future-shop.jp", "make.shop", "colorme.shop", "peachjohn.co.jp", "gelatopique.com", "beams.co.jp", "united-arrows.co.jp", "world.co.jp", "magaseek.com", "locondo.jp", "cosme.com", "dhc.co.jp", "kose.co.jp", "suntory.co.jp", "kirin.co.jp", "kikkoman.co.jp", "ajinomoto.co.jp", "meiji.co.jp", "morinaga.co.jp", "glico.com", "lotte.co.jp", "calbee.co.jp", "yakult.co.jp", "nissin.com", "isetan.mistore.jp", "takashimaya.co.jp", "lumine.jp", "parco.jp", "odakyu-deptstore.jp", "welcia.co.jp", "tsuruha.co.jp", "sundrug.co.jp", "matsumoto-kiyoshi.jp", "loft.co.jp", "right-on.co.jp", "mercari-shops.com", "hands.net", "sogo-seibu.com", "kobeshi.co.jp", "kuronekoyamato.co.jp", "sagawa-exp.co.jp", "japanpost-group.jp", "skylark.co.jp"]
RECEIVED_MTA_LABELS = ["DOCOMO Mail Server", "MIZUHO Bank Mail", "Mitsubishi UFJ Mail", "SMBC Mail", "Resona Bank Mail", "Rakuten Group Mail", "NTT Communications", "SOFTBANK Mail", "KDDI Mail", "Japan Post Mail", "Yahoo! JAPAN Mail", "Amazon.co.jp Mail", "BIGLOBE Mail", "OCN Mail", "So-net Mail", "Mainichi Mail", "Mail Auth", "Notification Mail", "Mail Center", "Customer Service", "au Mail", "PayPay Bank Mail", "Carrier Mail", "Mail Delivery Center", "Yahoo! JAPAN", "Amazon.co.jp", "LINEMO", "T-Online", "Qoo10", "ZOZOTOWN", "Gunosy", "Uber Eats", "Wolt", "kintone", "STORES", "futureshop"]
STRIP_LIST_UNSUBSCRIBE = True
REWRITE_RECEIVED = True
STRIP_HEADERS_CFG = ["X-Mailer", "List-Unsubscribe", "List-Unsubscribe-Post"]
STRIP_HEADERS = list(STRIP_HEADERS_CFG)
HEADER_REWRITE_RULES = []
MSGID_DOMAIN = "mail1.weicai168.com"
OUTBOUND_CLIENT_MODE = "mobile"
RECEIVED_MODE = "single"
MOBILE_CLIENT_HELOS = ["smtpclient.apple", "smtpclient.apple", "smtpclient.apple", "iphone.local", "iphone.local", "iphone", "iphone", "iphone.lan", "android-mail", "android.local", "mail-client"]
MOBILE_CLIENT_USER_AGENTS = ["iPhone Mail (21F90)", "iPhone Mail (21F90)", "iPhone Mail (21E236)", "iPhone Mail (20G75)", "iPhone Mail (20F66)", "iPhone Mail (19H349)", "iOS-Mail/17.5.1 (21F90)", "iOS-Mail/17.4.1 (21E236)", "iOS-Mail/16.6 (20G75)", "iPad Mail (21F90)"]
MOBILE_CLIENT_X_MAILERS = ["iPhone Mail 17.5", "iPhone Mail 17.5", "iPhone Mail 17.4", "iPhone Mail 16.6", "iPhone Mail 16.5", "iPad Mail 17.5", "iOS Mail 17.5"]
MOBILE_CLIENT_MTA_LABELS = ["Apple Mail", "Apple Mail", "iPhone Mail", "iPhone Mail", "iOS Mail", "Mobile Mail", "Android Mail"]
HEADER_REWRITE_RULES = []
MSGID_DOMAIN = "mail1.weicai168.com"
RECEIVED_MODE = "single"
OUTBOUND_CLIENT_MODE = "mobile"
RECEIVED_MODE = "single"
MOBILE_CLIENT_HELOS = ["smtpclient.apple", "smtpclient.apple", "smtpclient.apple", "iphone.local", "iphone.local", "iphone", "iphone", "iphone.lan", "android-mail", "android.local", "mail-client"]
MOBILE_CLIENT_USER_AGENTS = ["iPhone Mail (21F90)", "iPhone Mail (21F90)", "iPhone Mail (21E236)", "iPhone Mail (20G75)", "iPhone Mail (20F66)", "iPhone Mail (19H349)", "iOS-Mail/17.5.1 (21F90)", "iOS-Mail/17.4.1 (21E236)", "iOS-Mail/16.6 (20G75)", "iPad Mail (21F90)"]
MOBILE_CLIENT_X_MAILERS = ["iPhone Mail 17.5", "iPhone Mail 17.5", "iPhone Mail 17.4", "iPhone Mail 16.6", "iPhone Mail 16.5", "iPad Mail 17.5", "iOS Mail 17.5"]
MOBILE_CLIENT_MTA_LABELS = ["Apple Mail", "Apple Mail", "iPhone Mail", "iPhone Mail", "iOS Mail", "Mobile Mail", "Android Mail"]

RECEIVED_REWRITE_RE = re.compile(
    r"kumomta|zonemta|zone-mta|msmtp|mapi|localhost|127\.0\.0\.1|\[::1\]|undisclosed|"
    r"smtp\.client|esmtp\.mailer|mailer-daemon|postfix|esmtpsa",
    re.IGNORECASE,
)
BY_SELF_RE = re.compile("by\\s+mail1\\.weicai168\\.com", re.IGNORECASE)


def flatten_received_value(value: str) -> str:
    flat = str(value or "").replace("\r\n", " ").replace("\n", " ")
    return re.sub(r"\s+", " ", flat).strip()


def strip_tls_from_with_clause(clause: str) -> str:
    flat = flatten_received_value(clause)
    flat = re.sub(r"\s*\([^)]*[Tt][Ll][Ss][^)]*\)", "", flat)
    flat = re.sub(r"\s+", " ", flat).strip()
    return flat or "with ESMTP"


def extract_with_clause(original_value: str) -> str:
    flat = flatten_received_value(original_value)
    match = re.search(r"(with\s+.+?)\s+id\s+", flat, flags=re.IGNORECASE)
    if match:
        return strip_tls_from_with_clause(match.group(1))
    match = re.search(r"(with\s+[^;]+)", flat, flags=re.IGNORECASE)
    if match:
        return strip_tls_from_with_clause(match.group(1))
    return "with ESMTP"


def sanitize_received_line(line: str) -> str:
    out = flatten_received_value(line)
    out = re.sub(
        r"\s+id\s+[A-Za-z0-9._-]+\s+for\s+<[^>]+>\s*;",
        ";",
        out,
        flags=re.IGNORECASE,
    )
    out = re.sub(r"\s*;\s*;+", "; ", out)
    return re.sub(r"\s+", " ", out).strip()


def should_drop_received(line: str) -> bool:
    return bool(re.search(r"haraka|\(haraka\s+outbound\)", line, re.IGNORECASE))


def should_rewrite_received(line: str) -> bool:
    if should_drop_received(line):
        return False
    if re.search(r"\([Pp]ostfix\)", line):
        return True
    if RECEIVED_REWRITE_RE.search(line):
        return True
    if BY_SELF_RE.search(line) and re.search(
        r"\(127\.|localhost|::1\)|\(Postfix\)|ESMTPSA",
        line,
        re.IGNORECASE,
    ):
        return True
    return False


def build_received_trace_line(original_value: str) -> str:
    if OUTBOUND_CLIENT_MODE == "mobile":
        helo = random.choice(MOBILE_CLIENT_HELOS)
        # CGNAT / phone-like client IP
        if random.random() < 0.7:
            ip = "100.%d.%d.%d" % (random.randint(64, 127), random.randint(0, 255), random.randint(1, 254))
        else:
            ip = "192.168.%d.%d" % (random.randint(0, 255), random.randint(1, 254))
        return (
            f"from {helo} ([{ip}]) "
            f"by {EHLO_DOMAIN} "
            f"with ESMTPSA; "
            f"{email.utils.formatdate(localtime=True)}"
        )
    return (
        f"from {random.choice(RECEIVED_EHLO_DOMAINS)} (127.0.0.1) "
        f"by {EHLO_DOMAIN} ({random.choice(RECEIVED_MTA_LABELS)}) "
        f"{extract_with_clause(original_value)}; "
        f"{email.utils.formatdate(localtime=True)}"
    )


def process_received_list(values: list[str]) -> list[str]:
    # mobile / single: wipe PC MTA chain, keep exactly one keitai submission hop
    if OUTBOUND_CLIENT_MODE == "mobile" or RECEIVED_MODE == "single":
        return [sanitize_received_line(build_received_trace_line("with ESMTPSA"))]
    rewritten: list[str] = []
    for line in values:
        flat = flatten_received_value(line)
        if should_drop_received(flat):
            continue
        if should_rewrite_received(flat):
            rewritten.append(sanitize_received_line(build_received_trace_line(flat)))
        else:
            rewritten.append(sanitize_received_line(flat))
    if not rewritten:
        rewritten.append(sanitize_received_line(build_received_trace_line("with ESMTP")))
    return rewritten


class HeaderNormalizeMilter(Milter.Milter):
    def __init__(self) -> None:
        super().__init__()
        self.received_values: list[str] = []
        self.strip_counts: dict[str, int] = {}

    def header(self, name: str, value: str):
        lowered = name.lower()
        if lowered == "received":
            self.received_values.append(value)
        for strip_name in STRIP_HEADERS:
            if lowered == strip_name.lower():
                self.strip_counts[strip_name] = self.strip_counts.get(strip_name, 0) + 1
        return Milter.CONTINUE

    def eom(self):
        random.seed(int(time.time()) % 2147483647)
        for strip_name, count in list(self.strip_counts.items()):
            # mobile mode: keep User-Agent/X-Mailer if client sent real ones; still strip junk
            if OUTBOUND_CLIENT_MODE == "mobile" and strip_name.lower() in ("user-agent", "x-mailer", "x-mailer-trace"):
                continue
            for idx in range(count, 0, -1):
                self.chgheader(strip_name, idx, None)
        orig_count = len(self.received_values)
        if REWRITE_RECEIVED or OUTBOUND_CLIENT_MODE == "mobile":
            if OUTBOUND_CLIENT_MODE == "mobile" or RECEIVED_MODE == "single":
                for idx in range(orig_count, 0, -1):
                    try:
                        self.chgheader("Received", idx, None)
                    except Exception:
                        pass
                try:
                    self.addheader("Received", build_received_trace_line("with ESMTPSA"))
                except Exception:
                    pass
            else:
                processed = process_received_list(self.received_values)
                for idx, new_line in enumerate(processed, start=1):
                    if idx <= orig_count:
                        self.chgheader("Received", idx, new_line)
                if orig_count > len(processed):
                    for idx in range(orig_count, len(processed), -1):
                        self.chgheader("Received", idx, None)
        if OUTBOUND_CLIENT_MODE == "mobile":
            for pc in ("X-MSMail-Priority", "Thread-Index", "X-Priority", "X-MimeOLE", "X-MS-Has-Attach"):
                try:
                    self.chgheader(pc, 1, None)
                except Exception:
                    pass
        return Milter.CONTINUE



def main() -> None:
    Milter.set_flags(Milter.ADDHDRS | Milter.CHGHDRS)
    Milter.runmilter("postfix-header-normalize", "inet:8892@127.0.0.1", 240)


if __name__ == "__main__":
    main()
MILTERPYEOF
chmod 755 /opt/postfix-milter/normalize_headers.py

cat > /etc/postfix/header_checks <<'HEADERCHKSEOF'
/^X\-Mailer:/ IGNORE
/^List\-Unsubscribe:/ IGNORE
/^List\-Unsubscribe\-Post:/ IGNORE
/^Received:.* \([Pp]ostfix\)/ REPLACE Received: from android.local ([10.246.114.100]) by mail1.weicai168.com with ESMTPSA; Fri, 07 Aug 2026 19:18:56 +0000
HEADERCHKSEOF
cat > /etc/postfix/smtp_header_checks <<'SMTPHEADERCHKSEOF'
/^X\-Mailer:/ IGNORE
/^List\-Unsubscribe:/ IGNORE
/^List\-Unsubscribe\-Post:/ IGNORE
SMTPHEADERCHKSEOF
TEST_HDR='Received: from test (test [127.0.0.1]) by host (Postfix) with ESMTPSA id X;'
HDR_ACTION="$(postmap -q "$TEST_HDR" regexp:/etc/postfix/header_checks 2>/dev/null || true)"
# Accept REPLACE (rewrite), IGNORE (mobile: milter injects Received), or FILTER
case "$HDR_ACTION" in
  REPLACE\ *|IGNORE|IGNORE\ *|FILTER|FILTER\ *)
    echo "header_checks matched Postfix Received -> $HDR_ACTION"
    ;;
  *)
    echo "ERROR: header_checks did not match Postfix Received (postmap=$HDR_ACTION)"
    grep -E 'Received|Postfix|REPLACE|IGNORE' /etc/postfix/header_checks 2>/dev/null || true
    exit 1
    ;;
esac


cat > /etc/systemd/system/postfix-header-milter.service <<'UNITEOF'
[Unit]
Description=Postfix Header Normalize Milter
After=network.target

[Service]
Type=simple
ExecStart=/usr/bin/python3 /opt/postfix-milter/normalize_headers.py
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
UNITEOF
systemctl daemon-reload
systemctl enable postfix-header-milter
if ! systemctl restart postfix-header-milter; then
  echo "éè¯¯ï¼postfix-header-milter éå¯å¤±è´¥"
  systemctl --no-pager --full status postfix-header-milter || true
  journalctl -u postfix-header-milter --no-pager -n 80 || true
  exit 1
fi
sleep 1
systemctl is-active postfix-header-milter
ss -lntp | grep ':8892' || true


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


echo "æ­£å¨éç½® Postfix åºç¡åæ°..."
DEBIAN_FRONTEND=noninteractive dpkg-reconfigure -f noninteractive postfix || true

mkdir -p "$TLS_DIR" "$DKIM_DIR" /etc/postfix/sasl
chmod 755 "$TLS_DIR"

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 640 "$DKIM_DIR/$DKIM_SELECTOR.private"
chmod 644 "$DKIM_DIR/$DKIM_SELECTOR.pub"
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 640 "$DKIM_DIR/default2.private" 2>/dev/null || true
chmod 644 "$DKIM_DIR/default2.pub" 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 640 "$DKIM_DIR/default3.private" 2>/dev/null || true
chmod 644 "$DKIM_DIR/default3.pub" 2>/dev/null || true

cat > /etc/opendkim/TrustedHosts <<EOF
127.0.0.1
localhost
$MAIL_HOSTNAME
$MAIL_DOMAIN
EOF

printf '%s	%s:%s:%s/%s.private
' \
  "$DKIM_SELECTOR._domainkey.$MAIL_DOMAIN" "$MAIL_DOMAIN" "$DKIM_SELECTOR" "$DKIM_DIR" "$DKIM_SELECTOR" \
  > /etc/opendkim/KeyTable
printf '*@%s	%s._domainkey.%s
' "$MAIL_DOMAIN" "$DKIM_SELECTOR" "$MAIL_DOMAIN" \
  > /etc/opendkim/SigningTable
chown root:root /etc/opendkim /etc/opendkim.conf
chmod 755 /etc/opendkim
chown root:root /etc/opendkim/SigningTable /etc/opendkim/KeyTable /etc/opendkim/TrustedHosts 2>/dev/null || true
chmod 644 /etc/opendkim/SigningTable /etc/opendkim/KeyTable /etc/opendkim/TrustedHosts 2>/dev/null || true
chown -R opendkim:opendkim /etc/opendkim/keys
find /etc/opendkim/keys -type d -exec chmod 750 {} +
find /etc/opendkim/keys -name '*.private' -exec chmod 640 {} +
find /etc/opendkim/keys -name '*.pub' -exec chmod 644 {} +


if [ -f /etc/default/opendkim ]; then
  if grep -q '^SOCKET=' /etc/default/opendkim; then
    sed -i 's|^SOCKET=.*|SOCKET=inet:8891@127.0.0.1|' /etc/default/opendkim
  else
    echo 'SOCKET=inet:8891@127.0.0.1' >> /etc/default/opendkim
  fi
else
  echo 'SOCKET=inet:8891@127.0.0.1' > /etc/default/opendkim
fi
if [ -f /etc/default/opendkim ]; then
  sed -i 's|^RUNDIR=.*|RUNDIR=/run/opendkim|' /etc/default/opendkim 2>/dev/null || true
fi
mkdir -p /run/opendkim
chown opendkim:opendkim /run/opendkim 2>/dev/null || true

cat > /etc/opendkim.conf <<OPENDKIMEOF
Syslog                  yes
UMask                   002
Canonicalization        relaxed/relaxed
Mode                    sv
SubDomains              no
AutoRestart             yes
AutoRestartRate         10/1h
SignatureAlgorithm      rsa-sha256
SignHeaders             From, Subject, Date, Message-ID, MIME-Version
KeyTable                refile:/etc/opendkim/KeyTable
SigningTable            refile:/etc/opendkim/SigningTable
ExternalIgnoreList      /etc/opendkim/TrustedHosts
InternalHosts           /etc/opendkim/TrustedHosts
LogWhy                  yes
Socket                  inet:8891@127.0.0.1
OPENDKIMEOF

chown root:root /etc/opendkim /etc/opendkim.conf
chmod 755 /etc/opendkim
chown -R opendkim:opendkim /etc/opendkim/keys


echo "æ­£å¨éç½® Postfix TLS è¯ä¹¦ï¼OpenSSL èªç­¾åï¼..."
if [ -f "$TLS_DIR/cert.pem" ] && [ -f "$TLS_DIR/key.pem" ] && \
    openssl x509 -in "$TLS_DIR/cert.pem" -noout -checkend 2592000 >/dev/null 2>&1; then
  echo "Postfix èªç­¾å 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 "éè¯¯ï¼Postfix èªç­¾å TLS è¯ä¹¦çæå¤±è´¥"
  exit 1
fi
echo "Postfix èªç­¾å TLS è¯ä¹¦å°±ç»ª"

if [ ! -f "$TLS_DIR/cert.pem" ] || [ ! -f "$TLS_DIR/key.pem" ]; then
  echo "éè¯¯ï¼$TLS_DIR ä¸ç¼ºå°è¯ä¹¦æä»¶"
  exit 1
fi
chmod 644 "$TLS_DIR/cert.pem"
chmod 600 "$TLS_DIR/key.pem"


if ! dpkg -s libsasl2-modules-db >/dev/null 2>&1; then
  DEBIAN_FRONTEND=noninteractive apt-get update -qq
  DEBIAN_FRONTEND=noninteractive apt-get install -y libsasl2-modules-db libsasl2-modules
fi

mkdir -p /etc/postfix/sasl
cat > /etc/postfix/sasl/smtpd.conf <<'SASLEOF'
pwcheck_method: auxprop
auxprop_plugin: sasldb
mech_list: PLAIN LOGIN
sasldb_path: /etc/sasldb2
SASLEOF

# SMTP_AUTH_USER æ¥èª GUI/CSV ç Email åï¼å¦ admin@sq7.pvuwb.comï¼åç¼æ¯å°å¯ä¸åï¼
if [[ "$SMTP_AUTH_USER" == *@* ]]; then
  SMTP_AUTH_LOCAL="${SMTP_AUTH_USER%%@*}"
  SMTP_AUTH_REALM="${SMTP_AUTH_USER#*@}"
else
  SMTP_AUTH_LOCAL="$SMTP_AUTH_USER"
  SMTP_AUTH_REALM="$MAIL_DOMAIN"
fi

if [ -f /etc/sasldb2 ]; then
  rm -f /etc/sasldb2
fi
echo "$SMTP_PASS" | saslpasswd2 -p -c -u "$SMTP_AUTH_REALM" "$SMTP_AUTH_LOCAL"
chown postfix:postfix /etc/sasldb2
chmod 660 /etc/sasldb2

# smtpd é»è®¤ chroot å° /var/spool/postfixï¼é¡»å¨ jail åè½è¯»å° sasldb2 ä¸ smtpd.conf
mkdir -p /var/spool/postfix/etc/postfix/sasl
install -o postfix -g postfix -m 660 /etc/sasldb2 /var/spool/postfix/etc/sasldb2
install -o root -g root -m 644 /etc/postfix/sasl/smtpd.conf /var/spool/postfix/etc/postfix/sasl/smtpd.conf

postconf -e "cyrus_sasl_config_path = /etc/postfix/sasl"
postconf -e "smtpd_sasl_local_domain = $SMTP_AUTH_REALM"
postconf -e "smtpd_sasl_tls_security_options = noanonymous"

echo "Postfix SASL ç»å½åï¼$SMTP_AUTH_LOCAL@$SMTP_AUTH_REALM"
sasldblistusers2 2>/dev/null || true
ls -l /etc/sasldb2 /var/spool/postfix/etc/sasldb2 2>/dev/null || true

hostnamectl set-hostname "$MAIL_HOSTNAME" 2>/dev/null || hostname "$MAIL_HOSTNAME" || true
printf '%s\n' "$MAIL_HOSTNAME" > /etc/hostname
printf '%s\n' "$MAIL_HOSTNAME" > /etc/mailname
if grep -qE '^127\.0\.1\.1[[:space:]]' /etc/hosts 2>/dev/null; then
  sed -i -E "s|^127\.0\.1\.1[[:space:]].*|127.0.1.1\t$MAIL_HOSTNAME ${MAIL_HOSTNAME%%.*}|" /etc/hosts
else
  printf '127.0.1.1\t%s %s\n' "$MAIL_HOSTNAME" "${MAIL_HOSTNAME%%.*}" >> /etc/hosts
fi
postconf -e "myhostname = $MAIL_HOSTNAME"
postconf -e "mydomain = $MAIL_DOMAIN"
postconf -e "myorigin = \$mydomain"
postconf -e "smtp_helo_name = ${MAIL_HELO:-$MAIL_HOSTNAME}"
postconf -e "smtpd_banner = \$myhostname ESMTP"
postconf -e "mydestination = \$myhostname, localhost.\$mydomain, localhost"
postconf -e "inet_interfaces = all"
postconf -e "inet_protocols = ipv4"
postconf -e "smtpd_tls_cert_file = $TLS_DIR/cert.pem"
postconf -e "smtpd_tls_key_file = $TLS_DIR/key.pem"
postconf -e "smtpd_tls_security_level = may"
postconf -e "smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1"
postconf -e "smtpd_tls_auth_only = yes"
postconf -e "smtpd_sasl_auth_enable = yes"
postconf -e "smtpd_sasl_type = cyrus"
postconf -e "smtpd_sasl_path = smtpd"
postconf -e "smtpd_sasl_security_options = noanonymous"
postconf -e "broken_sasl_auth_clients = yes"
postconf -e "milter_protocol = 6"
postconf -e "milter_default_action = accept"
postconf -e "smtpd_milters = inet:127.0.0.1:8891"
postconf -e "non_smtpd_milters = inet:127.0.0.1:8892,inet:127.0.0.1:8891"
postconf -e "header_checks = regexp:/etc/postfix/header_checks"
postconf -e "smtp_header_checks = regexp:/etc/postfix/smtp_header_checks"
echo "æ­£å¨éç½® Postfix master.cfï¼587 submission + OpenDKIM milterï¼..."
postconf -e "milter_protocol = 6"
postconf -e "milter_default_action = accept"
postconf -e "milter_connect_macros = j {daemon_name} {daemon_flags} v {if_name} _"
postconf -e "milter_mail_macros = i {auth_type} {auth_authen} {auth_author} {mail_addr} {mail_host} {mail_mailer}"
sed -i '/^smtpd_milter_macros/d' /etc/postfix/main.cf
sed -i '/smtpd_milter_macros/d' /etc/postfix/master.cf
postconf -e "smtpd_milters = inet:127.0.0.1:8891"
postconf -e "non_smtpd_milters = inet:127.0.0.1:8892,inet:127.0.0.1:8891"

MASTER=/etc/postfix/master.cf
if [ ! -f "$MASTER" ]; then
  echo "éè¯¯ï¼ç¼ºå° $MASTER"
  exit 1
fi
cp -a "$MASTER" "${MASTER}.bak.$(date +%s)" 2>/dev/null || true
TMP="$(mktemp)"
awk '
  BEGIN { skip=0 }
  /^submission[[:space:]]+inet/ { skip=1; next }
  skip==1 {
    if ($0 ~ /^[[:space:]]/) next
    skip=0
  }
  skip==0 { print }
' "$MASTER" > "$TMP"
cat >> "$TMP" <<'SUBMISSIONBLOCK'
submission inet n       -       n       -       -       smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_tls_auth_only=yes
  -o smtpd_client_restrictions=permit_sasl_authenticated,reject
  -o smtpd_sender_restrictions=permit_mynetworks,permit_sasl_authenticated,reject_non_fqdn_sender
  -o smtpd_recipient_restrictions=permit_mynetworks,permit_sasl_authenticated,reject_unauth_destination
  -o milter_macro_daemon_name=ORIGINATING
  -o smtpd_milters=inet:127.0.0.1:8891
  -o non_smtpd_milters=inet:127.0.0.1:8892,inet:127.0.0.1:8891
SUBMISSIONBLOCK
mv "$TMP" "$MASTER"
SMTP_TMP="$(mktemp)"
awk '/^smtp[[:space:]]+inet/ {
  if ($5 == "y" || $5 == "-") $5="n"
  print
  next
}
{ print }' "$MASTER" > "$SMTP_TMP"
mv "$SMTP_TMP" "$MASTER"
if ! grep -Fq 'milter_macro_daemon_name=ORIGINATING' "$MASTER"; then
  echo "éè¯¯ï¼master.cf æªåå¥ submission ORIGINATING milter"
  exit 1
fi
postfix check || {
  echo "éè¯¯ï¼master.cf ä¿®æ¹å postfix check å¤±è´¥"
  postfix check
  exit 1
}

# åéä¸ç¨ï¼å³é­ååå¾/åçæ¯/åå®¹è¿æ»¤é¾è·¯ï¼é¿åæ¯å°ä¿¡èµ°æ«æå¨ææ¢éå
for svc in amavis amavis-new spamassassin clamav-daemon clamav-freshclam clamsmtp rspamd postgrey postsrsd opendmarc policyd-spf; do
  systemctl disable --now "$svc" >/dev/null 2>&1 || true
done
postconf -e "content_filter ="
postconf -e "smtpd_proxy_filter ="
postconf -e "smtpd_proxy_options ="
postconf -e "receive_override_options ="
postconf -e "header_checks = regexp:/etc/postfix/header_checks"
postconf -e "smtp_header_checks = regexp:/etc/postfix/smtp_header_checks"

postconf -e "smtpd_banner = \$myhostname ESMTP"
postconf -e "smtpd_recipient_restrictions = permit_mynetworks,permit_sasl_authenticated,reject_unauth_destination"
postconf -e "smtpd_sender_restrictions = permit_mynetworks,permit_sasl_authenticated,reject_non_fqdn_sender"
# åºç«æéä¼åï¼æ¬æºä¾§å°½éä¸åééï¼ç¶é¢äº¤ç»å¯¹æ¹æå¡å¨ååºä¸ç³»ç»èµæºå³å®ã
postconf -e "default_process_limit = 1000"
postconf -e "default_destination_concurrency_limit = 200"
postconf -e "smtp_destination_concurrency_limit = 200"
postconf -e "initial_destination_concurrency = 50"
postconf -e "smtp_destination_recipient_limit = 0"
postconf -e "smtp_destination_rate_delay = 0s"
postconf -e "default_destination_rate_delay = 0s"
postconf -e "queue_run_delay = 10s"
postconf -e "minimal_backoff_time = 10s"
postconf -e "maximal_backoff_time = 60s"

postfix check || {
  echo "éè¯¯ï¼postfix éç½®æ£æ¥å¤±è´¥"
  postfix check
  exit 1
}
if [ -f /etc/opendkim.conf ]; then
  grep -q '^Socket' /etc/opendkim.conf 2>/dev/null || echo 'Socket                  inet:8891@127.0.0.1' >> /etc/opendkim.conf
  sed -i 's|^Socket.*|Socket                  inet:8891@127.0.0.1|' /etc/opendkim.conf
  grep -q '^Mode' /etc/opendkim.conf 2>/dev/null || echo 'Mode                    sv' >> /etc/opendkim.conf
  sed -i 's|^Mode[[:space:]].*|Mode                    sv|' /etc/opendkim.conf
  grep -q '^Canonicalization' /etc/opendkim.conf 2>/dev/null || echo 'Canonicalization        relaxed/relaxed' >> /etc/opendkim.conf
  grep -q '^SignatureAlgorithm' /etc/opendkim.conf 2>/dev/null || echo 'SignatureAlgorithm      rsa-sha256' >> /etc/opendkim.conf
  sed -i 's|^KeyTable.*|KeyTable                refile:/etc/opendkim/KeyTable|' /etc/opendkim.conf
  sed -i 's|^SigningTable.*|SigningTable            refile:/etc/opendkim/SigningTable|' /etc/opendkim.conf
  grep -q '^KeyTable' /etc/opendkim.conf 2>/dev/null || echo 'KeyTable                refile:/etc/opendkim/KeyTable' >> /etc/opendkim.conf
  grep -q '^SigningTable' /etc/opendkim.conf 2>/dev/null || echo 'SigningTable            refile:/etc/opendkim/SigningTable' >> /etc/opendkim.conf
  grep -q '^ExternalIgnoreList' /etc/opendkim.conf 2>/dev/null || echo 'ExternalIgnoreList      /etc/opendkim/TrustedHosts' >> /etc/opendkim.conf
  grep -q '^InternalHosts' /etc/opendkim.conf 2>/dev/null || echo 'InternalHosts           /etc/opendkim/TrustedHosts' >> /etc/opendkim.conf
  grep -q '^LogWhy' /etc/opendkim.conf 2>/dev/null || echo 'LogWhy                  yes' >> /etc/opendkim.conf
  awk '!/^MacroList/' /etc/opendkim.conf > /tmp/opendkim.conf.tmp
  mv /tmp/opendkim.conf.tmp /etc/opendkim.conf
  sed -i '/^User[[:space:]]/d; /^Group[[:space:]]/d' /etc/opendkim.conf 2>/dev/null || true
fi

opendkim -n || {
  echo "éè¯¯ï¼opendkim éç½®æ£æ¥å¤±è´¥"
  opendkim -n
  exit 1
}

mkdir -p /etc/systemd/system/opendkim.service.d
cat > /etc/systemd/system/opendkim.service.d/override.conf <<'OPENDKIMUNITEOF'
[Service]
Type=simple
PIDFile=
ExecStart=
ExecStart=/usr/sbin/opendkim -f -x /etc/opendkim.conf
User=opendkim
Group=opendkim
Restart=on-failure
RuntimeDirectory=opendkim
RuntimeDirectoryMode=0755
OPENDKIMUNITEOF
systemctl daemon-reload

systemctl enable rsyslog 2>/dev/null || true
systemctl restart rsyslog 2>/dev/null || true
systemctl enable opendkim
if ! systemctl restart opendkim; then
  echo "éè¯¯ï¼opendkim éå¯å¤±è´¥"
  systemctl --no-pager --full status opendkim || true
  journalctl -u opendkim --no-pager -n 120 || true
  exit 1
fi
mkdir -p /run/opendkim; chown opendkim:opendkim /run/opendkim 2>/dev/null || true
cat > /etc/opendkim/keytable_default2 <<EOF
default2._domainkey.$MAIL_DOMAIN $MAIL_DOMAIN:default2:$DKIM_DIR/default2.private
EOF
cat > /etc/opendkim/signingtable_default2 <<EOF
*@$MAIL_DOMAIN default2._domainkey.$MAIL_DOMAIN
EOF
cat > /etc/opendkim/extra_default2.conf <<EOF
Canonicalization relaxed/relaxed
Mode sv
SubDomains no
SignatureAlgorithm rsa-sha256
SignHeaders             From, Subject, Date, Message-ID, MIME-Version
KeyTable refile:/etc/opendkim/keytable_default2
SigningTable refile:/etc/opendkim/signingtable_default2
ExternalIgnoreList /etc/opendkim/TrustedHosts
InternalHosts /etc/opendkim/TrustedHosts
Socket inet:8901@127.0.0.1
PidFile /run/opendkim/extra_default2.pid
UserID opendkim
EOF
cat > /etc/systemd/system/opendkim-default2.service <<EOF
[Unit]
Description=OpenDKIM extra DKIM signer default2
After=network.target
[Service]
ExecStart=/usr/sbin/opendkim -f -x /etc/opendkim/extra_default2.conf
User=opendkim
RuntimeDirectory=opendkim
Restart=always
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable opendkim-default2 2>/dev/null || true
systemctl restart opendkim-default2
cat > /etc/opendkim/keytable_default3 <<EOF
default3._domainkey.$MAIL_DOMAIN $MAIL_DOMAIN:default3:$DKIM_DIR/default3.private
EOF
cat > /etc/opendkim/signingtable_default3 <<EOF
*@$MAIL_DOMAIN default3._domainkey.$MAIL_DOMAIN
EOF
cat > /etc/opendkim/extra_default3.conf <<EOF
Canonicalization relaxed/relaxed
Mode sv
SubDomains no
SignatureAlgorithm rsa-sha256
SignHeaders             From, Subject, Date, Message-ID, MIME-Version
KeyTable refile:/etc/opendkim/keytable_default3
SigningTable refile:/etc/opendkim/signingtable_default3
ExternalIgnoreList /etc/opendkim/TrustedHosts
InternalHosts /etc/opendkim/TrustedHosts
Socket inet:8902@127.0.0.1
PidFile /run/opendkim/extra_default3.pid
UserID opendkim
EOF
cat > /etc/systemd/system/opendkim-default3.service <<EOF
[Unit]
Description=OpenDKIM extra DKIM signer default3
After=network.target
[Service]
ExecStart=/usr/sbin/opendkim -f -x /etc/opendkim/extra_default3.conf
User=opendkim
RuntimeDirectory=opendkim
Restart=always
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable opendkim-default3 2>/dev/null || true
systemctl restart opendkim-default3
postconf -e "smtpd_milters=$(postconf -h smtpd_milters),inet:127.0.0.1:8901,inet:127.0.0.1:8902"
postconf -e "non_smtpd_milters=$(postconf -h non_smtpd_milters),inet:127.0.0.1:8901,inet:127.0.0.1:8902"
_dkim_waited=0
_dkim_restarted=0
while ! ss -lntp | grep -qE ':8891\b'; do
  if [ "$_dkim_waited" -ge 30 ]; then
    break
  fi
  if ! systemctl is-active --quiet opendkim && [ "$_dkim_restarted" -eq 0 ]; then
    systemctl restart opendkim || true
    _dkim_restarted=1
  fi
  sleep 1
  _dkim_waited=$((_dkim_waited+1))
done
if ! systemctl is-active --quiet opendkim; then
  echo "éè¯¯ï¼opendkim æªè¿è¡ï¼å·²ç­å¾ ${_dkim_waited}sï¼"
  systemctl --no-pager --full status opendkim || true
  journalctl -u opendkim --no-pager -n 40 || true
  exit 1
fi
if ! ss -lntp | grep -qE ':8891\b'; then
  echo "éè¯¯ï¼OpenDKIM æªçå¬ 8891ï¼å·²ç­å¾ ${_dkim_waited}sï¼"
  systemctl --no-pager --full status opendkim || true
  journalctl -u opendkim --no-pager -n 40 || true
  ss -lntp | grep -E ':8891|:8892' || true
  exit 1
fi

_hm_waited=0
_hm_restarted=0
while ! ss -lntp | grep -qE ':8892\b'; do
  if [ "$_hm_waited" -ge 30 ]; then
    break
  fi
  if ! systemctl is-active --quiet postfix-header-milter && [ "$_hm_restarted" -eq 0 ]; then
    systemctl restart postfix-header-milter || true
    _hm_restarted=1
  fi
  sleep 1
  _hm_waited=$((_hm_waited+1))
done
if ! systemctl is-active --quiet postfix-header-milter; then
  echo "éè¯¯ï¼postfix-header-milter æªè¿è¡ï¼å·²ç­å¾ ${_hm_waited}sï¼"
  systemctl --no-pager --full status postfix-header-milter || true
  journalctl -u postfix-header-milter --no-pager -n 40 || true
  exit 1
fi
if ! ss -lntp | grep -qE ':8892\b'; then
  echo "éè¯¯ï¼é®ä»¶å¤´é­æ¹ milter æªçå¬ 8892ï¼å·²ç­å¾ ${_hm_waited}sï¼"
  journalctl -u postfix-header-milter --no-pager -n 40 || true
  ss -lntp | grep -E ':8891|:8892' || true
  exit 1
fi
TEST_HDR='Received: from test (test [127.0.0.1]) by host (Postfix) with ESMTPSA id X;'
HDR_ACTION="$(postmap -q "$TEST_HDR" regexp:/etc/postfix/header_checks 2>/dev/null || true)"
# Accept REPLACE (rewrite), IGNORE (mobile: milter injects Received), or FILTER
case "$HDR_ACTION" in
  REPLACE\ *|IGNORE|IGNORE\ *|FILTER|FILTER\ *)
    echo "header_checks matched Postfix Received -> $HDR_ACTION"
    ;;
  *)
    echo "ERROR: header_checks did not match Postfix Received (postmap=$HDR_ACTION)"
    grep -E 'Received|Postfix|REPLACE|IGNORE' /etc/postfix/header_checks 2>/dev/null || true
    exit 1
    ;;
esac
smtpd_m="$(postconf -h smtpd_milters 2>/dev/null || true)"
case "$smtpd_m" in
  *8891*) ;;
  *)
    echo "éè¯¯ï¼smtpd_milters æªåå« OpenDKIM 8891ï¼å½å=$smtpd_mï¼"
    exit 1
    ;;
esac
if ! grep -Fq 'milter_macro_daemon_name=ORIGINATING' /etc/postfix/master.cf; then
  echo "éè¯¯ï¼master.cf ç¼ºå° submission ORIGINATING milter"
  exit 1
fi
echo "Postfix æå¡å°±ç»ªï¼smtpd_milters=$smtpd_m non_smtpd_milters=$(postconf -h non_smtpd_milters 2>/dev/null || true)"


systemctl enable postfix
if ! systemctl restart postfix; then
  echo "éè¯¯ï¼postfix éå¯å¤±è´¥"
  systemctl --no-pager --full status postfix || true
  journalctl -u postfix --no-pager -n 120 || true
  exit 1
fi

sleep 2
systemctl is-active opendkim
systemctl is-active postfix
ss -lntp | grep -E ':25|:587|:8891|:8892' || 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"
