#!/bin/bash
# alpha-mail — privileged mailbox manager for the Alpha panel. Root-owned; called
# by www-data via NOPASSWD sudoers. Mailboxes are restricted to the panel's domains.
# Dovecot passwd-file entries are full 8-field (uid/gid/home + per-user quota).
#   add <email> <password> | passwd <email> <password> | del <email> | setquota <email> <MB>
#   rcpasswd            (reads "email:newpass" on stdin — Roundcube chpasswd driver)
#   list | listaliases | listdomains | syncdomains | dkim <domain>
#   addalias <alias@domain> <dest> | delalias <alias@domain> | migrate
set -euo pipefail
[ -r /etc/alpha-panel.conf ] && . /etc/alpha-panel.conf
DOMAIN="${PRIMARY_DOMAIN:-$(hostname -d 2>/dev/null || true)}"; DOMAIN="${DOMAIN:-localhost}"   # the owner's domain, never a literal
DRE="${DOMAIN//./\\.}"   # regex-escaped domain
ACTION="${1:-}"
USERS=/etc/dovecot/users
MBOX=/etc/postfix/vmail_mailboxes
ALIASES=/etc/postfix/vmail_aliases
DOMAINS=/etc/postfix/vmail_domains
VBASE=/var/mail/vhosts
DEFQ=2048   # default quota MB

valid_domain() { grep -qxF "$1" "$DOMAINS"; }
valid_email() {
  [[ "$1" =~ ^[a-z0-9]([a-z0-9._%+-]{0,62})@[a-z0-9.-]+$ ]] || return 1
  valid_domain "${1##*@}"
}
# full passwd-file line: $1=email $2=hash $3=quotaMB
entry_line() {
  local email="$1" hash="$2" q="$3" d u
  d="${email##*@}"; u="${email%@*}"
  printf '%s:%s:5000:5000::%s/%s/%s::userdb_quota_rule=*:storage=%sM' "$email" "$hash" "$VBASE" "$d" "$u" "$q"
}
line_for() { grep -iE "^${1//./\\.}:" "$USERS" 2>/dev/null | head -1; }
quota_of() { local q; q=$(printf '%s' "$1" | grep -oE 'storage=[0-9]+M' | grep -oE '[0-9]+'); echo "${q:-$DEFQ}"; }
hash_of()  { printf '%s' "$1" | cut -d: -f2; }
# Dovecot's passwd-file driver reloads $USERS only when its mtime or size changes, and
# stats it at most once a second. A password change rewrites the line at the SAME size
# (fixed-length hash), so a change landing in the same second as Dovecot's last read
# kept the OLD password live — the user's next login with the new one failed. Push the
# mtime one second ahead after every write so the next lookup always reloads.
bump_users() { touch -d "@$(( $(date +%s) + 1 ))" "$USERS" 2>/dev/null || true; }

set_password() {  # $1=email $2=newpass ; preserves quota
  local email="$1" pass="$2" old hash q
  old=$(line_for "$email"); [ -n "$old" ] || { echo "NO_BOX"; exit 5; }
  q=$(quota_of "$old"); hash=$(doveadm pw -s SHA512-CRYPT -p "$pass")
  sed -i "\#^${email}:#d" "$USERS"
  entry_line "$email" "$hash" "$q" >> "$USERS"; echo >> "$USERS"
  bump_users
}

# Give a mail domain its own DKIM key if it has none, wire it into the tables and
# reload. Idempotent. Domains under $DOMAIN are already covered by the wildcard
# signing entry; this exists so an EXTERNAL domain added later cannot silently
# send unsigned mail, which Gmail scores as spam.
dkim_ensure() {
  local dom="$1" dir="/etc/opendkim/keys/$1"
  [[ "$dom" =~ ^[a-z0-9.-]+$ ]] || return 0
  case "$dom" in *".$DOMAIN"|"$DOMAIN") return 0 ;; esac   # wildcard already signs these
  [ -f "$dir/mail.private" ] && return 0
  mkdir -p "$dir"
  opendkim-genkey -b 2048 -d "$dom" -D "$dir" -s mail -v >/dev/null 2>&1 || return 0
  chown -R opendkim:opendkim "$dir"; chmod 600 "$dir/mail.private"
  grep -q "mail._domainkey.$dom" /etc/opendkim/key.table 2>/dev/null || echo "mail._domainkey.$dom $dom:mail:$dir/mail.private" >> /etc/opendkim/key.table
  grep -q "\*@$dom " /etc/opendkim/signing.table 2>/dev/null || echo "*@$dom mail._domainkey.$dom" >> /etc/opendkim/signing.table
  systemctl reload opendkim 2>/dev/null || systemctl restart opendkim 2>/dev/null || true
  return 0
}

sync_domains() {
  { grep -rhoE "server_name[^;]+;" /etc/nginx/sites-available/ 2>/dev/null | sed -E 's/server_name//; s/;//' | tr ' ' '\n'; echo "$DOMAIN"; } \
    | grep -E "${DRE}\$" | grep -vE "^_|mail\.${DRE}\$" | sed '/^$/d' | sort -u > "$DOMAINS"
}

# Deliver a "your mailbox is ready" email with the server connection settings.
send_welcome() {  # $1=email  $2=password (optional)
  local email="$1" pass="${2:-}" dom pw
  dom="${email##*@}"
  command -v sendmail >/dev/null 2>&1 || return 0
  [ -n "$pass" ] && pw="$pass" || pw="(the password set for this mailbox)"
  {
    echo "From: Alpha Mail <postmaster@${dom}>"
    echo "To: ${email}"
    echo "Subject: Your mailbox ${email} is ready - mail server settings"
    echo "Content-Type: text/plain; charset=utf-8"
    echo
    echo "Welcome - your email account is set up and ready to use."
    echo
    echo "  Email address:  ${email}"
    echo
    echo "WEBMAIL ----------------------------------------------"
    echo "  https://webmail.$DOMAIN"
    echo "  Sign in with your full email address and password."
    echo
    echo "IMAP  (reading mail in an app) -----------------------"
    echo "  Server:    mail.$DOMAIN"
    echo "  Port:      993       Security: SSL/TLS"
    echo "  Username:  ${email}"
    echo
    echo "SMTP  (sending mail / for websites & apps) -----------"
    echo "  Server:    mail.$DOMAIN"
    echo "  Port:      587 (STARTTLS)   or   465 (SSL/TLS)"
    echo "  Security:  STARTTLS / SSL, authentication required"
    echo "  Username:  ${email}"
    echo "  Password:  ${pw}"
    echo
    echo "USING SMTP IN A WEBSITE OR APP -----------------------"
    echo "  Any app that sends email (PHPMailer, contact forms, etc.)"
    echo "  can send through this mailbox:"
    echo
    echo "    Host        mail.$DOMAIN"
    echo "    Port        587"
    echo "    Encryption  TLS (STARTTLS)"
    echo "    Username    ${email}"
    echo "    Password    ${pw}"
    echo "    From        ${email}"
    echo
    echo "WORDPRESS - WP Mail SMTP plugin ----------------------"
    echo "  Mailer:          Other SMTP"
    echo "  SMTP Host:       mail.$DOMAIN"
    echo "  Encryption:      TLS"
    echo "  SMTP Port:       587"
    echo "  Auto TLS:        ON"
    echo "  Authentication:  ON"
    echo "  SMTP Username:   ${email}"
    echo "  SMTP Password:   ${pw}"
    echo "  From Email:      ${email}"
    echo
    echo "WORDPRESS - no plugin (add to functions.php / a mu-plugin):"
    echo
    echo "    add_action('phpmailer_init', function (\$phpmailer) {"
    echo "        \$phpmailer->isSMTP();"
    echo "        \$phpmailer->Host       = 'mail.$DOMAIN';"
    echo "        \$phpmailer->Port       = 587;"
    echo "        \$phpmailer->SMTPSecure = 'tls';"
    echo "        \$phpmailer->SMTPAuth   = true;"
    echo "        \$phpmailer->Username   = '${email}';"
    echo "        \$phpmailer->Password   = '${pw}';"
    echo "        \$phpmailer->setFrom('${email}', get_bloginfo('name'));"
    echo "    });"
    echo
    echo "Keep this message - it has everything you need to connect."
  } | sendmail -f "postmaster@${dom}" "${email}"
}

case "$ACTION" in
  add)
    EMAIL="${2:-}"; PASS="${3:-}"
    valid_email "$EMAIL" || { echo "BAD_EMAIL"; exit 2; }
    [ -n "$PASS" ] || { echo "NO_PASS"; exit 3; }
    [ -n "$(line_for "$EMAIL")" ] && { echo "EXISTS"; exit 4; }
    D="${EMAIL##*@}"; U="${EMAIL%@*}"
    HASH=$(doveadm pw -s SHA512-CRYPT -p "$PASS")
    entry_line "$EMAIL" "$HASH" "$DEFQ" >> "$USERS"; echo >> "$USERS"; bump_users
    printf '%s %s/%s/\n' "$EMAIL" "$D" "$U" >> "$MBOX"; postmap "$MBOX"
    mkdir -p "${VBASE}/${D}/${U}"; chown -R vmail:vmail "${VBASE}/${D}"
    systemctl reload postfix
    send_welcome "$EMAIL" "$PASS" || true   # drop a settings/welcome email into the new inbox
    echo OK
    ;;
  welcome)
    EMAIL="${2:-}"
    valid_email "$EMAIL" || { echo "BAD_EMAIL"; exit 2; }
    [ -n "$(line_for "$EMAIL")" ] || { echo "NO_BOX"; exit 5; }
    send_welcome "$EMAIL" "${3:-}" || true
    echo OK
    ;;
  passwd)
    EMAIL="${2:-}"; PASS="${3:-}"
    valid_email "$EMAIL" || { echo "BAD_EMAIL"; exit 2; }
    [ -n "$PASS" ] || { echo "NO_PASS"; exit 3; }
    set_password "$EMAIL" "$PASS"; echo OK
    ;;
  rcpasswd)
    read -r LINE || true
    EMAIL="${LINE%%:*}"; PASS="${LINE#*:}"
    valid_email "$EMAIL" || { echo "BAD_EMAIL"; exit 2; }
    [ -n "$PASS" ] || { echo "NO_PASS"; exit 3; }
    set_password "$EMAIL" "$PASS"; echo OK
    ;;
  setquota)
    EMAIL="${2:-}"; MB="${3:-}"
    valid_email "$EMAIL" || { echo "BAD_EMAIL"; exit 2; }
    [[ "$MB" =~ ^[0-9]+$ ]] && [ "$MB" -ge 10 ] && [ "$MB" -le 1048576 ] || { echo "BAD_QUOTA"; exit 6; }
    OLD=$(line_for "$EMAIL"); [ -n "$OLD" ] || { echo "NO_BOX"; exit 5; }
    HASH=$(hash_of "$OLD")
    sed -i "\#^${EMAIL}:#d" "$USERS"
    entry_line "$EMAIL" "$HASH" "$MB" >> "$USERS"; echo >> "$USERS"; bump_users
    doveadm quota recalc -u "$EMAIL" >/dev/null 2>&1 || true
    echo OK
    ;;
  del)
    EMAIL="${2:-}"
    valid_email "$EMAIL" || { echo "BAD_EMAIL"; exit 2; }
    D="${EMAIL##*@}"; U="${EMAIL%@*}"
    sed -i "\#^${EMAIL}:#d" "$USERS" 2>/dev/null || true; bump_users
    sed -i "\#^${EMAIL} #d" "$MBOX" 2>/dev/null || true; postmap "$MBOX"
    rm -rf "${VBASE:?}/${D}/${U}"
    systemctl reload postfix
    echo OK
    ;;
  migrate)
    # upgrade any old-format (email:hash::::::) entries to the full 8-field format
    TMP=$(mktemp)
    while IFS= read -r l; do
      [ -n "$l" ] || continue
      em="${l%%:*}"
      if printf '%s' "$l" | grep -q 'userdb_quota_rule'; then echo "$l" >> "$TMP"; continue; fi
      h=$(printf '%s' "$l" | cut -d: -f2)
      entry_line "$em" "$h" "$DEFQ" >> "$TMP"; echo >> "$TMP"
    done < "$USERS"
    cat "$TMP" > "$USERS"; rm -f "$TMP"
    chown root:dovecot "$USERS"; chmod 640 "$USERS"; bump_users
    echo OK
    ;;
  addalias)
    ALIAS="${2:-}"; DEST="${3:-}"
    valid_email "$ALIAS" || { echo "BAD_EMAIL"; exit 2; }
    [[ "$DEST" =~ ^[a-z0-9._%+-]+@[a-z0-9.-]+$ ]] || { echo "BAD_DEST"; exit 6; }
    sed -i "\#^${ALIAS}[[:space:]]#d" "$ALIASES" 2>/dev/null || true
    printf '%s %s\n' "$ALIAS" "$DEST" >> "$ALIASES"; postmap "$ALIASES"
    systemctl reload postfix
    echo OK
    ;;
  delalias)
    ALIAS="${2:-}"
    valid_email "$ALIAS" || { echo "BAD_EMAIL"; exit 2; }
    sed -i "\#^${ALIAS}[[:space:]]#d" "$ALIASES" 2>/dev/null || true; postmap "$ALIASES"
    systemctl reload postfix
    echo OK
    ;;
  list)
    [ -f "$USERS" ] || exit 0
    while IFS=: read -r email rest; do
      [ -n "$email" ] || continue
      D="${email##*@}"; U="${email%@*}"; dir="${VBASE}/${D}/${U}"
      bytes=0; cnt=0
      if [ -d "$dir" ]; then
        bytes=$(du -sb "$dir" 2>/dev/null | cut -f1) || bytes=0
        cnt=$(find "$dir/new" "$dir/cur" -type f 2>/dev/null | wc -l) || cnt=0
      fi
      qmb=$(printf '%s' "$rest" | grep -oE 'storage=[0-9]+M' | grep -oE '[0-9]+'); qmb="${qmb:-$DEFQ}"
      printf '%s|%s|%s|%s\n' "$email" "${bytes:-0}" "${cnt:-0}" "$((qmb*1048576))"
    done < "$USERS"
    ;;
  listaliases)
    [ -f "$ALIASES" ] && grep -vE '^#|^$' "$ALIASES" || true
    ;;
  listdomains)
    sync_domains; cat "$DOMAINS"
    ;;
  syncdomains)
    sync_domains
    while read -r d; do [ -n "$d" ] && dkim_ensure "$d"; done < "$DOMAINS"
    systemctl reload postfix; echo OK
    ;;
  dkim-ensure)
    DOM="${2:-}"; [[ "$DOM" =~ ^[a-z0-9.-]+$ ]] || { echo BAD; exit 2; }
    dkim_ensure "$DOM"
    F="/etc/opendkim/keys/${DOM}/mail.txt"
    if [ -f "$F" ]; then grep -oE '"[^"]*"' "$F" | sed 's/"//g' | tr -d '\n'; echo
    else echo "COVERED_BY_WILDCARD"; fi ;;
  dkim)
    DOM="${2:-$DOMAIN}"
    [[ "$DOM" =~ ^[a-z0-9.-]+$ ]] || { echo "BAD"; exit 2; }
    F="/etc/opendkim/keys/${DOM}/mail.txt"
    [ -f "$F" ] || { echo "NO_KEY"; exit 7; }
    grep -oE '"[^"]*"' "$F" | sed 's/"//g' | tr -d '\n'; echo
    ;;
  *) echo "BAD_ACTION"; exit 1 ;;
esac
