commit 466b474f4072177d9771778928780c48e722f68e Author: boban Date: Fri Apr 24 15:10:18 2026 +0200 Initial: mailwolt-installer in eigenes Repo ausgelagert Modularer Installer für Mailwolt mit 25 Scripts. Ausgelagert aus boban/mailwolt Hauptrepo. Co-Authored-By: Claude Sonnet 4.6 diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..d721d4f Binary files /dev/null and b/.DS_Store differ diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/copilot.data.migration.agent.xml b/.idea/copilot.data.migration.agent.xml new file mode 100644 index 0000000..4ea72a9 --- /dev/null +++ b/.idea/copilot.data.migration.agent.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.ask.xml b/.idea/copilot.data.migration.ask.xml new file mode 100644 index 0000000..7ef04e2 --- /dev/null +++ b/.idea/copilot.data.migration.ask.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.ask2agent.xml b/.idea/copilot.data.migration.ask2agent.xml new file mode 100644 index 0000000..1f2ea11 --- /dev/null +++ b/.idea/copilot.data.migration.ask2agent.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.edit.xml b/.idea/copilot.data.migration.edit.xml new file mode 100644 index 0000000..8648f94 --- /dev/null +++ b/.idea/copilot.data.migration.edit.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/mailwolt-installer.iml b/.idea/mailwolt-installer.iml new file mode 100644 index 0000000..c956989 --- /dev/null +++ b/.idea/mailwolt-installer.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..3ce3588 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..2492f5c --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml new file mode 100644 index 0000000..f324872 --- /dev/null +++ b/.idea/php.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/config/.DS_Store b/config/.DS_Store new file mode 100644 index 0000000..f4a98d2 Binary files /dev/null and b/config/.DS_Store differ diff --git a/config/nginx/site.conf.tmpl b/config/nginx/site.conf.tmpl new file mode 100644 index 0000000..935dfe0 --- /dev/null +++ b/config/nginx/site.conf.tmpl @@ -0,0 +1,17 @@ +# ===================== HTTP (Port 80) ===================== +server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + + # ACME HTTP-01 + location ^~ /.well-known/acme-challenge/ { + root /var/www/letsencrypt; + allow all; + } + + __HTTP_BODY__ +} + +# ===================== HTTPS (Port 443) ==================== +__SSL_SERVER_BLOCK__ \ No newline at end of file diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..d1e7429 --- /dev/null +++ b/install.sh @@ -0,0 +1,909 @@ +#!/usr/bin/env bash +( +export HISTFILE= +set +o history +set -euo pipefail + +############################################## +# MailWolt # +# Bootstrap Installer v1.0 # +############################################## + +# ===== CLI-Flags (-dev / -stag) ===== +APP_ENV="${APP_ENV:-production}" +APP_DEBUG="${APP_DEBUG:-false}" + +DEV_MODE=0 +STAG_MODE=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + -dev) + DEV_MODE=1 + APP_ENV="local" + APP_DEBUG="true" + ;; + -stag|-staging) + STAG_MODE=1 + APP_ENV="staging" + APP_DEBUG="false" + ;; + esac + shift +done + +# ===== Branding & Pfade ===== +APP_NAME="${APP_NAME:-MailWolt}" + +APP_USER="${APP_USER:-mailwolt}" +APP_GROUP="${APP_GROUP:-www-data}" + +APP_DIR="/var/www/${APP_USER}" + +ADMIN_USER="${APP_USER}" +ADMIN_EMAIL="admin@localhost" +ADMIN_PASS="ChangeMe" + +CONF_BASE="/etc/${APP_USER}" +CERT_DIR="${CONF_BASE}/ssl" +CERT="${CERT_DIR}/cert.pem" +KEY="${CERT_DIR}/key.pem" + +NGINX_SITE="/etc/nginx/sites-available/${APP_USER}.conf" +NGINX_SITE_LINK="/etc/nginx/sites-enabled/${APP_USER}.conf" + +DB_NAME="${DB_NAME:-${APP_USER}}" +DB_USER="${DB_USER:-${APP_USER}}" +DB_PASS="${DB_PASS:-$(openssl rand -hex 16)}" + +GIT_REPO="${GIT_REPO:-http://10.10.20.81:3000/boban/mailwolt.git}" +GIT_BRANCH="${GIT_BRANCH:-main}" + +NODE_SETUP="${NODE_SETUP:-deb}" + +# ===== Styling ===== +GREEN="\033[1;32m"; YELLOW="\033[1;33m"; RED="\033[1;31m"; CYAN="\033[1;36m"; GREY="\033[0;90m"; NC="\033[0m" +BAR="──────────────────────────────────────────────────────────────────────────────" + +header() { + echo -e "${CYAN}${BAR}${NC}" + echo -e "${CYAN} 888b d888 d8b 888 888 888 888 888 ${NC}" + echo -e "${CYAN} 8888b d8888 Y8P 888 888 o 888 888 888 ${NC}" + echo -e "${CYAN} 88888b.d88888 888 888 d8b 888 888 888 ${NC}" + echo -e "${CYAN} 888Y88888P888 8888b. 888 888 888 d888b 888 .d88b. 888 888888 ${NC}" + echo -e "${CYAN} 888 Y888P 888 '88b 888 888 888d88888b888 d88''88b 888 888 ${NC}" + echo -e "${CYAN} 888 Y8P 888 .d888888 888 888 88888P Y88888 888 888 888 888 ${NC}" + echo -e "${CYAN} 888 ' 888 888 888 888 888 8888P Y8888 Y88..88P 888 Y88b. ${NC}" + echo -e "${CYAN} 888 888 'Y888888 888 888 888P Y888 'Y88P' 888 'Y888 ${NC}" + echo -e "${CYAN}${BAR}${NC}" + echo +} + +print_bootstrap_summary() { + local ip="$1" + local admin_user="$2" + local admin_pass="$3" + local GREEN="\033[1;32m" + local CYAN="\033[1;36m" + local GREY="\033[0;90m" + local YELLOW="\033[1;33m" + local RED="\033[1;31m" + local NC="\033[0m" + local BAR="${BAR:-──────────────────────────────────────────────────────────────────────────────}" + local scheme="http" + if [ -s "${CERT}" ] && [ -s "${KEY}" ]; then scheme="https"; fi + + echo + echo -e "${GREEN}${BAR}${NC}" + echo -e "${GREEN} ✔ ${APP_NAME} Bootstrap erfolgreich abgeschlossen${NC}" + echo -e "${GREEN}${BAR}${NC}" + echo -e " Bootstrap-Login (nur für ERSTEN Login & Wizard):" + echo -e " User: ${YELLOW}${admin_user}${NC}" + echo -e " Passwort: ${RED}${admin_pass}${NC}" + echo + echo -e " Aufruf: ${CYAN}${scheme}://${ip}${NC}" + echo -e " Laravel Root: ${GREY}${APP_DIR}${NC}" + echo -e " Nginx Site: ${GREY}${NGINX_SITE}${NC}" + echo -e " Self-signed Cert: ${GREY}${CERT_DIR}/{cert.pem,key.pem}${NC}" + echo -e " Postfix/Dovecot Ports aktiv: ${GREY}25, 465, 587, 110, 995, 143, 993${NC}" + echo -e " Rspamd/OpenDKIM: ${GREY}aktiv (DKIM-Keys später im Wizard)${NC}" + echo -e " Monit (Watchdog): ${GREY}installiert, NICHT aktiviert${NC}" + echo -e "${GREEN}${BAR}${NC}" + echo +} + +log() { echo -e "${GREEN}[+]${NC} $*"; } +warn() { echo -e "${YELLOW}[!]${NC} $*"; } +err() { echo -e "${RED}[x]${NC} $*"; } +require_root() { [ "$(id -u)" -eq 0 ] || { err "Bitte als root ausführen."; exit 1; }; } + +detect_ip() { + local ip + ip="$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for (i=1;i<=NF;i++) if ($i=="src") {print $(i+1); exit}}')" || true + [[ -n "${ip:-}" ]] || ip="$(hostname -I 2>/dev/null | awk '{print $1}')" + [[ -n "${ip:-}" ]] || { err "Konnte Server-IP nicht ermitteln."; exit 1; } + echo "$ip" +} + +gen() { head -c 512 /dev/urandom | tr -dc 'A-Za-z0-9' | head -c "${1:-28}" || true; } +pw() { gen 28; } +short() { gen 16; } + +# ===== Start ===== +require_root +header + +SERVER_IP="$(detect_ip)" +MAIL_HOSTNAME="${MAIL_HOSTNAME:-"bootstrap.local"}" +TZ="${TZ:-""}" + +echo -e "${GREY}Server-IP erkannt: ${SERVER_IP}${NC}" +[ -n "$TZ" ] && { ln -fs "/usr/share/zoneinfo/${TZ}" /etc/localtime || true; } + +log "Paketquellen aktualisieren…" +export DEBIAN_FRONTEND=noninteractive +apt-get update -y + +# ---- MariaDB-Workaround ---- +log "MariaDB-Workaround vorbereiten…" +mkdir -p /etc/mysql /etc/mysql/mariadb.conf.d +[ -f /etc/mysql/mariadb.cnf ] || echo '!include /etc/mysql/mariadb.conf.d/*.cnf' > /etc/mysql/mariadb.cnf + +# ---- Basis-Pakete installieren ---- +log "Pakete installieren… (dies kann einige Minuten dauern)" +export DEBIAN_FRONTEND=noninteractive +apt-get -y -o Dpkg::Options::="--force-confdef" \ + -o Dpkg::Options::="--force-confold" install \ + postfix postfix-mysql \ + dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd dovecot-mysql \ + mariadb-server mariadb-client \ + redis-server \ + rspamd \ + opendkim opendkim-tools \ + nginx \ + php php-fpm php-cli php-mbstring php-xml php-curl php-zip php-mysql php-redis php-gd unzip curl \ + composer git \ + certbot python3-certbot-nginx \ + fail2ban \ + ca-certificates rsyslog sudo openssl netcat-openbsd monit acl + +NGINX_HTTP2_SUPPORTED=0 +if nginx -V 2>&1 | grep -q http_v2; then + NGINX_HTTP2_SUPPORTED=1 + log "Nginx: HTTP/2-Unterstützung vorhanden ✅" +else + warn "Nginx: HTTP/2-Modul nicht gefunden – wechsle auf 'nginx-full'…" + apt-get install -y nginx-full || true + systemctl restart nginx || true + if nginx -V 2>&1 | grep -q http_v2; then + NGINX_HTTP2_SUPPORTED=1 + log "Nginx: HTTP/2 jetzt verfügbar ✅" + else + warn "HTTP/2 weiterhin nicht verfügbar (verwende SSL ohne http2)." + fi +fi + +if [ "$NGINX_HTTP2_SUPPORTED" = "1" ]; then + NGINX_HTTP2_SUFFIX=" http2" +else + NGINX_HTTP2_SUFFIX="" +fi + +# ===== Verzeichnisse / User ===== +log "Verzeichnisse und Benutzer anlegen…" +mkdir -p "${CERT_DIR}" /etc/postfix/sql /etc/dovecot/conf.d /etc/rspamd/local.d /var/mail/vhosts +id vmail >/dev/null 2>&1 || adduser --system --group --home /var/mail vmail +chown -R vmail:vmail /var/mail + +id "$APP_USER" >/dev/null 2>&1 || adduser --disabled-password --gecos "" "$APP_USER" +usermod -a -G "$APP_GROUP" "$APP_USER" + +# ===== Self-signed TLS (SAN = IP) ===== +OSSL_CFG="${CERT_DIR}/openssl.cnf" +if [ ! -s "$CERT" ] || [ ! -s "$KEY" ]; then + log "Erzeuge Self-Signed TLS Zertifikat (SAN=IP:${SERVER_IP})…" + install -d -m 0750 -o root -g "${APP_USER}" "${CERT_DIR}" + cat > "$OSSL_CFG" </dev/null 2>&1; then + setfacl -m u:${DEV_USER}:x "${CONF_BASE}" "${CERT_DIR}" || true + setfacl -m u:${DEV_USER}:r "$CERT" "$KEY" || true +fi + +# ===== MariaDB ===== +log "MariaDB vorbereiten…" +systemctl enable --now mariadb +mysql -uroot < /etc/postfix/sql/mysql-virtual-mailbox-maps.cf < /etc/postfix/sql/mysql-virtual-alias-maps.cf < /etc/dovecot/dovecot.conf <<'CONF' +!include_try /etc/dovecot/conf.d/*.conf +CONF + +cat > /etc/dovecot/conf.d/10-mail.conf <<'CONF' +protocols = imap pop3 lmtp +mail_location = maildir:/var/mail/vhosts/%d/%n +namespace inbox { inbox = yes } +mail_privileged_group = mail +CONF + +cat > /etc/dovecot/conf.d/10-auth.conf <<'CONF' +disable_plaintext_auth = yes +auth_mechanisms = plain login +!include_try auth-sql.conf.ext +CONF + +cat > /etc/dovecot/dovecot-sql.conf.ext < /etc/dovecot/conf.d/auth-sql.conf.ext <<'CONF' +passdb { driver = sql args = /etc/dovecot/dovecot-sql.conf.ext } +userdb { driver = static args = uid=vmail gid=vmail home=/var/mail/vhosts/%d/%n } +CONF +sudo chown root:dovecot /etc/dovecot/conf.d/auth-sql.conf.ext +sudo chmod 640 /etc/dovecot/conf.d/auth-sql.conf.ext + +cat > /etc/dovecot/conf.d/10-master.conf <<'CONF' +service lmtp { + unix_listener /var/spool/postfix/private/dovecot-lmtp { mode = 0600 user = postfix group = postfix } +} +service auth { + unix_listener /var/spool/postfix/private/auth { mode = 0660 user = postfix group = postfix } +} +service imap-login { + inet_listener imap { port = 143 } + inet_listener imaps { port = 993 ssl = yes } +} +service pop3-login { + inet_listener pop3 { port = 110 } + inet_listener pop3s { port = 995 ssl = yes } +} +CONF + +cat > /etc/dovecot/conf.d/10-ssl.conf < /etc/rspamd/local.d/worker-controller.inc <<'CONF' +password = "admin"; +bind_socket = "127.0.0.1:11334"; +CONF +systemctl enable --now rspamd || true + +cat > /etc/opendkim.conf <<'CONF' +Syslog yes +UMask 002 +Mode sv +Socket inet:8891@127.0.0.1 +Canonicalization relaxed/simple +On-BadSignature accept +On-Default accept +On-KeyNotFound accept +On-NoSignature accept +LogWhy yes +OversignHeaders From +# KeyTable / SigningTable werden nach dem Wizard gesetzt +CONF +systemctl enable --now opendkim || true + +# ===== Redis ===== +log "Redis absichern (Passwort setzen & nur localhost)…" +REDIS_CONF="/etc/redis/redis.conf" +REDIS_PASS="${REDIS_PASS:-$(openssl rand -hex 16)}" +sed -i 's/^\s*#\?\s*bind .*/bind 127.0.0.1/' "$REDIS_CONF" +sed -i 's/^\s*#\?\s*protected-mode .*/protected-mode yes/' "$REDIS_CONF" +if grep -qE '^\s*#?\s*requirepass ' "$REDIS_CONF"; then + sed -i "s/^\s*#\?\s*requirepass .*/requirepass ${REDIS_PASS}/" "$REDIS_CONF" +else + printf "\nrequirepass %s\n" "${REDIS_PASS}" >> "$REDIS_CONF" +fi +systemctl enable --now redis-server +systemctl restart redis-server + +# ===== Nginx ===== +log "Nginx konfigurieren…" +rm -f /etc/nginx/sites-enabled/default /etc/nginx/sites-available/default || true + +detect_php_fpm_sock() { + for v in 8.3 8.2 8.1 8.0 7.4; do s="/run/php/php${v}-fpm.sock"; [ -S "$s" ] && { echo "$s"; return; }; done + [ -S "/run/php/php-fpm.sock" ] && { echo "/run/php/php-fpm.sock"; return; } + echo "127.0.0.1:9000" +} +PHP_FPM_SOCK="$(detect_php_fpm_sock)" +install -d -m 0755 /var/www/letsencrypt + +if [ -s "${CERT}" ] && [ -s "${KEY}" ]; then + cat > "${NGINX_SITE}" < "${NGINX_SITE}" </dev/null || true)" ]; then + sudo -u "$APP_USER" -H bash -lc "cd /var/www && COMPOSER_ALLOW_SUPERUSER=0 composer create-project laravel/laravel ${APP_USER} --no-interaction" + fi +else + if [ ! -d "${APP_DIR}/.git" ]; then + sudo -u "$APP_USER" -H bash -lc "git clone --depth=1 -b ${GIT_BRANCH} ${GIT_REPO} ${APP_DIR}" + else + sudo -u "$APP_USER" -H bash -lc " + set -e + cd ${APP_DIR} + git checkout ${GIT_BRANCH} 2>/dev/null || git checkout -B ${GIT_BRANCH} + git fetch --depth=1 origin ${GIT_BRANCH} + if git merge-base --is-ancestor HEAD origin/${GIT_BRANCH}; then + git pull --ff-only + else + echo '[i] Non-fast-forward erkannt – setze hart auf origin/${GIT_BRANCH}.' >&2 + git reset --hard origin/${GIT_BRANCH} + git clean -fd + fi + " + fi + if [ -f "${APP_DIR}/composer.json" ]; then + if [ "${DEV_MODE}" = "1" ]; then + sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && composer install --no-interaction --prefer-dist" + else + sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && composer install --no-interaction --prefer-dist --no-dev" + fi + fi +fi + +# ===== Node / Frontend ===== +if [ -f "${APP_DIR}/package.json" ]; then + log "Node/NPM installieren…" + if command -v node >/dev/null 2>&1; then + NODE_MAJ=$(node -v | sed 's/^v//' | cut -d. -f1) + NODE_MIN=$(node -v | sed 's/^v//' | cut -d. -f2) + if [ "$NODE_MAJ" -lt 20 ] || { [ "$NODE_MAJ" -eq 20 ] && [ "$NODE_MIN" -lt 19 ]; }; then + curl -fsSL https://deb.nodesource.com/setup_22.x | bash - + apt-get install -y nodejs + fi + else + curl -fsSL https://deb.nodesource.com/setup_22.x | bash - + apt-get install -y nodejs + fi + + # .env anlegen & APP_KEY + sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && cp -n .env.example .env || true" + if ! grep -q '^APP_KEY=' "${APP_DIR}/.env"; then echo "APP_KEY=" >> "${APP_DIR}/.env"; fi + sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan key:generate --force || true" +fi + +# ===== .env füllen ===== +ENV_FILE="${APP_DIR}/.env" +upsert_env () { + local key="$1" val="$2" + local esc_key esc_val + esc_key="$(printf '%s' "$key" | sed -e 's/[.[\*^$(){}+?|/]/\\&/g')" + esc_val="$(printf '%s' "$val" | sed -e 's/[&/]/\\&/g')" + if grep -qE "^[#[:space:]]*${esc_key}=" "$ENV_FILE"; then + sed -Ei "s|^[#[:space:]]*${esc_key}=.*|${key}=${esc_val}|g" "$ENV_FILE" + else + printf '%s=%s\n' "$key" "$val" >> "$ENV_FILE" + fi +} + +if [ -s "${CERT}" ] && [ -s "${KEY}" ]; then + upsert_env APP_URL "\"https://\${APP_HOST}\"" +else + upsert_env APP_URL "\"http://\${APP_HOST}\"" +fi + +upsert_env APP_HOST "${SERVER_IP}" +upsert_env APP_ADMIN_USER "${ADMIN_USER}" +upsert_env APP_ADMIN_EMAIL "${ADMIN_EMAIL}" +upsert_env APP_ADMIN_PASS "${ADMIN_PASS}" +upsert_env APP_NAME "${APP_NAME}" +upsert_env APP_ENV "${APP_ENV}" +upsert_env APP_DEBUG "${APP_DEBUG}" + +upsert_env DB_CONNECTION "mysql" +upsert_env DB_HOST "127.0.0.1" +upsert_env DB_PORT "3306" +upsert_env DB_DATABASE "${DB_NAME}" +upsert_env DB_USERNAME "${DB_USER}" +upsert_env DB_PASSWORD "${DB_PASS}" + +# -------- WICHTIG: Cache-Store auf REDIS setzen (verhindert DB-Tabellenfehler) -------- +upsert_env CACHE_SETTINGS_STORE "redis" +upsert_env CACHE_STORE "redis" # <- HIER geändert (vorher: database) +upsert_env CACHE_DRIVER "redis" +upsert_env CACHE_PREFIX "${APP_USER}_cache" + +upsert_env SESSION_DRIVER "redis" +upsert_env REDIS_CLIENT "phpredis" +upsert_env REDIS_HOST "127.0.0.1" +upsert_env REDIS_PORT "6379" +upsert_env REDIS_PASSWORD "${REDIS_PASS}" +upsert_env REDIS_DB "0" +upsert_env REDIS_CACHE_DB "1" +upsert_env REDIS_CACHE_CONNECTION "cache" +upsert_env REDIS_CACHE_LOCK_CONNECTION "default" + +# Reverb / Vite +upsert_env BROADCAST_DRIVER "reverb" +upsert_env QUEUE_CONNECTION "redis" +upsert_env REVERB_APP_ID "${APP_USER}" +upsert_env REVERB_APP_KEY "${APP_USER}-yhp47tbt1aebhr1fgvgj" +upsert_env REVERB_APP_SECRET "${APP_USER}-ulrdt9agwzkqwqsunbnb" +upsert_env REVERB_HOST "127.0.0.1" +upsert_env REVERB_PORT "443" +upsert_env REVERB_SCHEME "https" +upsert_env REVERB_PATH "/ws" +upsert_env VITE_REVERB_APP_KEY "\${REVERB_APP_KEY}" +upsert_env VITE_REVERB_PORT "\${REVERB_PORT}" +upsert_env VITE_REVERB_SCHEME "\${REVERB_SCHEME}" +upsert_env VITE_REVERB_PATH "\${REVERB_PATH}" + +if [ "${DEV_MODE}" = "1" ]; then + sed -i '/^# --- MailWolt DEV/,/^# --- \/MailWolt DEV/d' "${ENV_FILE}" + cat >> "${ENV_FILE}" < "${APP_DIR}/vite.config.js" <<'JS' +import { defineConfig, loadEnv } from 'vite' +import laravel from 'laravel-vite-plugin' +import tailwindcss from '@tailwindcss/vite' +export default ({ mode }) => { + const env = loadEnv(mode, process.cwd(), '') + const host = env.VITE_DEV_HOST || '127.0.0.1' + const port = Number(env.VITE_DEV_PORT || 5173) + const origin = env.VITE_DEV_ORIGIN || env.APP_URL || 'https://localhost' + const hmrHost = env.VITE_HMR_HOST || (new URL(origin)).hostname + return defineConfig({ + plugins: [laravel({ input: ['resources/css/app.css','resources/js/app.js'], refresh: true }), tailwindcss()], + server: { host, port, https:false, strictPort:true, + hmr:{ protocol: env.VITE_HMR_PROTOCOL || 'wss', host: hmrHost, clientPort:Number(env.VITE_HMR_CLIENT_PORT||443) }, + origin + } + }) +} +JS + chown "${APP_USER}:${APP_GROUP}" "${APP_DIR}/vite.config.js" +fi + +# ===== Frontend Build ===== +if [ -f "${APP_DIR}/package.json" ]; then + log "Frontend Build…" + if [ -f "${APP_DIR}/package-lock.json" ] || [ -f "${APP_DIR}/npm-shrinkwrap.json" ]; then + sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm ci --no-audit --no-fund || npm install" + else + sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm install" + fi + sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm run build || true" + rm -f ${APP_DIR}/bootstrap/cache/*.php +fi + +# ===== Rechte / PHP-FPM ===== +APP_PW="${APP_PW:-changeme}" +if ! id -u "$APP_USER" >/dev/null 2>&1; then + adduser --disabled-password --gecos "" "$APP_USER" + echo "${APP_USER}:${APP_PW}" | chpasswd +fi +usermod -a -G "$APP_GROUP" "$APP_USER" + +# Sichert, dass alle nötigen Ordner existieren (idempotent) +install -d -m 0775 "${APP_DIR}/storage" \ + "${APP_DIR}/storage/framework" \ + "${APP_DIR}/storage/framework/cache" \ + "${APP_DIR}/storage/framework/cache/data" \ + "${APP_DIR}/storage/framework/sessions" \ + "${APP_DIR}/storage/framework/views" \ + "${APP_DIR}/bootstrap/cache" + +# Besitz & Rechte +chown -R "$APP_USER":"$APP_GROUP" "$APP_DIR" +find "$APP_DIR" -type d -exec chmod 775 {} \; +find "$APP_DIR" -type f -exec chmod 664 {} \; + +[ -f "$APP_DIR/artisan" ] && chmod 755 "$APP_DIR/artisan" +[ -d "$APP_DIR/vendor/bin" ] && chmod -R 755 "$APP_DIR/vendor/bin" +[ -d "$APP_DIR/node_modules/.bin" ] && chmod -R 755 "$APP_DIR/node_modules/.bin" +[ -f "$APP_DIR/node_modules/vite/bin/vite.js" ] && chmod 755 "$APP_DIR/node_modules/vite/bin/vite.js" +find "$APP_DIR" -type f -name "*.sh" -exec chmod 755 {} \; + +chmod -R 775 "$APP_DIR"/storage "$APP_DIR"/bootstrap/cache + +if command -v setfacl >/dev/null 2>&1; then + setfacl -R -m u:${APP_USER}:rwX,g:${APP_GROUP}:rwX "${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" || true + setfacl -dR -m u:${APP_USER}:rwX,g:${APP_GROUP}:rwX "${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" || true +fi + +grep -q 'umask 002' /home/${APP_USER}/.profile 2>/dev/null || echo 'umask 002' >> /home/${APP_USER}/.profile +grep -q 'umask 002' /home/${APP_USER}/.bashrc 2>/dev/null || echo 'umask 002' >> /home/${APP_USER}/.bashrc +sudo -u "$APP_USER" -H bash -lc "npm config set umask 0002" >/dev/null 2>&1 || true + +PHPV=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;') +FPM_POOL="/etc/php/${PHPV}/fpm/pool.d/www.conf" +if [ -f "$FPM_POOL" ]; then + sed -i 's/^;*listen\.owner.*/listen.owner = www-data/' "$FPM_POOL" + sed -i 's/^;*listen\.group.*/listen.group = www-data/' "$FPM_POOL" + sed -i 's/^;*listen\.mode.*/listen.mode = 0660/' "$FPM_POOL" + systemctl restart php${PHPV}-fpm || true +fi + +IDE_USER="${SUDO_USER:-}" +if [ -n "$IDE_USER" ] && id "$IDE_USER" >/dev/null 2>&1 && command -v setfacl >/dev/null 2>&1; then + usermod -a -G "$APP_GROUP" "$IDE_USER" || true + setfacl -R -m u:${IDE_USER}:rwX "$APP_DIR" + setfacl -dR -m u:${IDE_USER}:rwX "$APP_DIR" + echo -e "${GREEN}[i]${NC} Benutzer '${IDE_USER}' wurde für Schreibzugriff freigeschaltet (ACL + Gruppe ${APP_GROUP})." +fi + +# Webstack neu laden +systemctl reload nginx || true +systemctl restart php*-fpm || true + +# ===== Reverb systemd ===== +cat > /etc/systemd/system/mailwolt-ws.service </dev/null | grep -qE '(^| )reverb:start( |$)'"; then + systemctl enable --now mailwolt-ws +else + systemctl disable --now mailwolt-ws >/dev/null 2>&1 || true +fi + +# ===== Monit ===== +log "Monit konfigurieren & starten…" +cat > /etc/monit/monitrc <<'EOF' +set daemon 60 +set logfile syslog facility log_daemon + +check process postfix with pidfile /var/spool/postfix/pid/master.pid + start program = "/bin/systemctl start postfix" + stop program = "/bin/systemctl stop postfix" + if failed port 25 protocol smtp then restart + if failed port 465 type tcp ssl then restart + if failed port 587 type tcp then restart + +check process dovecot with pidfile /var/run/dovecot/master.pid + start program = "/bin/systemctl start dovecot" + stop program = "/bin/systemctl stop dovecot" + if failed port 143 type tcp then restart + if failed port 993 type tcp ssl then restart + if failed port 110 type tcp then restart + if failed port 995 type tcp ssl then restart + +check process mariadb with pidfile /var/run/mysqld/mysqld.pid + start program = "/bin/systemctl start mariadb" + stop program = "/bin/systemctl stop mariadb" + if failed port 3306 type tcp then restart + +check process redis-server with pidfile /run/redis/redis-server.pid + start program = "/bin/systemctl start redis-server" + stop program = "/bin/systemctl stop redis-server" + if failed port 6379 type tcp then restart + +check process rspamd with pidfile /run/rspamd/rspamd.pid + start program = "/bin/systemctl start rspamd" + stop program = "/bin/systemctl stop rspamd" + if failed port 11332 type tcp then restart + +check process opendkim with pidfile /run/opendkim/opendkim.pid + start program = "/bin/systemctl start opendkim" + stop program = "/bin/systemctl stop opendkim" + if failed port 8891 type tcp then restart + +check process nginx with pidfile /run/nginx.pid + start program = "/bin/systemctl start nginx" + stop program = "/bin/systemctl stop nginx" + if failed port 80 type tcp then restart + if failed port 443 type tcp ssl then restart + +check process mailwolt-ws matching "reverb:start" + start program = "/bin/systemctl start mailwolt-ws" + stop program = "/bin/systemctl stop mailwolt-ws" + if failed host 127.0.0.1 port 8080 type tcp for 2 cycles then restart + if 5 restarts within 5 cycles then timeout +EOF +chmod 600 /etc/monit/monitrc +monit -t && systemctl enable --now monit +monit reload +monit summary || true + +# ===== Healthchecks ===== +GREEN="\033[1;32m"; RED="\033[1;31m"; GREY="\033[0;90m"; NC="\033[0m" +ok(){ echo -e " [${GREEN}OK${NC}]"; } +fail(){ echo -e " [${RED}FAIL${NC}]"; } + +echo "[+] Quick-Healthchecks…" +printf " • MariaDB … " ; mysqladmin ping --silent >/dev/null 2>&1 && ok || fail +printf " • Redis … " ; if command -v redis-cli >/dev/null 2>&1; then + if [ -n "${REDIS_PASS:-}" ] && [ "${REDIS_PASS}" != "null" ]; then + redis-cli -a "${REDIS_PASS}" ping 2>/dev/null | grep -q PONG && ok || fail + else + redis-cli ping 2>/dev/null | grep -q PONG && ok || fail + fi +else fail; fi +printf " • PHP-FPM … " ; if [[ "$PHP_FPM_SOCK" == 127.0.0.1:9000 ]]; then ss -ltn | grep -q ":9000 " && ok || fail; else [ -S "$PHP_FPM_SOCK" ] && ok || fail; fi +printf " • App … " ; if command -v curl >/dev/null 2>&1; then + if [ -s "${CERT}" ] && [ -s "${KEY}" ]; then curl -skI "https://127.0.0.1" >/dev/null 2>&1 && ok || fail; else curl -sI "http://127.0.0.1" >/dev/null 2>&1 && ok || fail; fi +else echo -e " ${GREY}(curl fehlt)${NC}"; fi +check_port(){ local label="$1" cmd="$2"; printf " • %-5s … " "$label"; timeout 8s bash -lc "$cmd" >/dev/null 2>&1 && ok || fail; } +check_port "25" 'printf "QUIT\r\n" | nc -w 3 127.0.0.1 25' +check_port "465" 'printf "QUIT\r\n" | openssl s_client -connect 127.0.0.1:465 -quiet -ign_eof' +check_port "587" 'printf "EHLO x\r\nSTARTTLS\r\nQUIT\r\n" | openssl s_client -starttls smtp -connect 127.0.0.1:587 -quiet -ign_eof' +check_port "110" 'printf "QUIT\r\n" | nc -w 3 127.0.0.1 110' +check_port "995" 'printf "QUIT\r\n" | openssl s_client -connect 127.0.0.1:995 -quiet -ign_eof' +check_port "143" 'printf ". LOGOUT\r\n" | nc -w 3 127.0.0.1 143' +check_port "993" 'printf ". LOGOUT\r\n" | openssl s_client -connect 127.0.0.1:993 -quiet -ign_eof' + +print_bootstrap_summary "$SERVER_IP" "$ADMIN_USER" "$ADMIN_PASS" + +# ===== MOTD ===== +install -d /usr/local/bin +cat >/usr/local/bin/mw-motd <<'SH' +#!/usr/bin/env bash +set -euo pipefail +NC="\033[0m"; CY="\033[1;36m"; GR="\033[1;32m"; YE="\033[1;33m"; RD="\033[1;31m"; GY="\033[0;90m" +printf "\033[1;36m" +cat <<'ASCII' +:::: :::: ::: ::::::::::: ::: ::: ::: :::::::: ::: ::::::::::: ++:+:+: :+:+:+ :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: ++:+ +:+:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ ++#+ +:+ +#+ +#++:++#++: +#+ +#+ +#+ +:+ +#+ +#+ +:+ +#+ +#+ ++#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+#+ +#+ +#+ +#+ +#+ +#+ +#+# #+# #+# #+# #+# #+# #+#+# #+#+# #+# #+# #+# #+# +### ### ### ### ########### ########## ### ### ######## ########## ### +ASCII +printf "\033[0m\n" +now="$(date '+%Y-%m-%d %H:%M:%S %Z')" +fqdn="$(hostname -f 2>/dev/null || hostname)" +ip_int="$(hostname -I 2>/dev/null | awk '{print $1}')" +ip_ext=""; command -v curl >/dev/null 2>&1 && ip_ext="$(curl -s --max-time 1 https://ifconfig.me || true)" +upt="$(uptime -p 2>/dev/null || true)" +cores="$(nproc 2>/dev/null || echo -n '?')" +mhz="$(LC_ALL=C lscpu 2>/dev/null | awk -F: '/MHz/{gsub(/ /,"",$2); printf("%.0f MHz",$2); exit}')" +[ -z "$mhz" ] && mhz="$(awk -F: '/cpu MHz/{printf("%.0f MHz",$2); exit}' /proc/cpuinfo 2>/dev/null)" +load="$(awk '{print $1" / "$2" / "$3}' /proc/loadavg 2>/dev/null)" +mem_total="$(awk '/MemTotal/{printf "%.2f GB",$2/1024/1024}' /proc/meminfo)" +mem_free="$(awk '/MemAvailable/{printf "%.2f GB",$2/1024/1024}' /proc/meminfo)" +svc_status(){ systemctl is-active --quiet "$1" && echo -e "${GR}OK${NC}" || echo -e "${RD}FAIL${NC}"; } +printf "${CY}Information as of:${NC} ${YE}%s${NC}\n" "$now" +printf "${GY}FQDN :${NC} %s\n" "$fqdn" +if [ -n "$ip_ext" ]; then printf "${GY}IP :${NC} %s ${GY}(external:${NC} %s${GY})${NC}\n" "${ip_int:-?}" "$ip_ext"; else printf "${GY}IP :${NC} %s\n" "${ip_int:-?}"; fi +printf "${GY}Uptime :${NC} %s\n" "${upt:-?}" +printf "${GY}Core(s) :${NC} %s core(s) at ${CY}%s${NC}\n" "$cores" "${mhz:-?}" +printf "${GY}Load :${NC} %s (1 / 5 / 15)\n" "${load:-?}" +printf "${GY}Memory :${NC} ${RD}%s${NC} ${GY}(free)${NC} / ${CY}%s${NC} ${GY}(total)${NC}\n" "${mem_free:-?}" "${mem_total:-?}" +echo +printf "${GY}Services :${NC} postfix: $(svc_status postfix) dovecot: $(svc_status dovecot) nginx: $(svc_status nginx) mariadb: $(svc_status mariadb) redis: $(svc_status redis)\n" +SH +chmod +x /usr/local/bin/mw-motd + +if [ -d /etc/update-motd.d ]; then + cat >/etc/update-motd.d/10-mailwolt <<'SH' +#!/usr/bin/env bash +/usr/local/bin/mw-motd +SH + chmod +x /etc/update-motd.d/10-mailwolt + [ -f /etc/update-motd.d/50-motd-news ] && chmod -x /etc/update-motd.d/50-motd-news || true + [ -f /etc/update-motd.d/80-livepatch ] && chmod -x /etc/update-motd.d/80-livepatch || true +else + cat >/etc/profile.d/10-mailwolt-motd.sh <<'SH' +case "$-" in *i*) /usr/local/bin/mw-motd ;; esac +SH +fi +: > /etc/motd 2>/dev/null || true + +) \ No newline at end of file diff --git a/scripts/10-provision.sh b/scripts/10-provision.sh new file mode 100644 index 0000000..1697460 --- /dev/null +++ b/scripts/10-provision.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +set -euo pipefail +source ./lib.sh + +if [ -r /etc/mailwolt/installer.env ]; then + . /etc/mailwolt/installer.env +fi + +REDIS_PASS="${REDIS_PASS:-}" + +SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +find "$SCRIPTS_DIR/.." -type f -name "*.sh" -exec sed -i 's/\r$//' {} \; || true + +log "Pakete installieren …" +export DEBIAN_FRONTEND=noninteractive +apt-get update -y +# Minimal aber vollständig +apt-get -y -o Dpkg::Options::="--force-confdef" \ + -o Dpkg::Options::="--force-confold" install \ + postfix postfix-mysql dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd dovecot-mysql \ + mariadb-server mariadb-client redis-server rspamd opendkim opendkim-tools opendmarc clamav \ + clamav-daemon nginx php php-fpm php-cli php-mbstring php-xml php-curl php-zip php-mysql \ + php-redis php-gd unzip curl composer git certbot python3-certbot-nginx fail2ban ca-certificates \ + rsyslog sudo openssl monit acl netcat-openbsd jq sqlite3 + +# <<< Apache konsequent entfernen >>> +systemctl disable --now apache2 >/dev/null 2>&1 || true +apt-get -y purge 'apache2*' >/dev/null 2>&1 || true +apt-get -y autoremove >/dev/null 2>&1 || true + +log "Systemuser/Dirs …" +id vmail >/dev/null 2>&1 || adduser --system --group --home /var/mail vmail +id "$APP_USER" >/dev/null 2>&1 || adduser --disabled-password --gecos "" "$APP_USER" +# Systemuser/Dirs … +id vmail >/dev/null 2>&1 || adduser --system --group --home /var/mail vmail +id "$APP_USER" >/dev/null 2>&1 || adduser --disabled-password --gecos "" "$APP_USER" + +# --- FIX: Gruppen und Berechtigungen für Maildir und Dovecot-Zugriff --- +# vmail soll primär der Gruppe "mail" angehören, zusätzlich dovecot +usermod -g mail -a -G dovecot vmail || true + +# App-User in relevante Gruppen +usermod -a -G "$APP_GROUP" "$APP_USER" || true +usermod -a -G mail,dovecot "$APP_USER" || true + +# Maildir-Baum für Gruppe mail lesbar +chgrp -R mail /var/mail/vhosts || true +chmod -R g+rx /var/mail/vhosts || true + +# ACLs setzen, damit neue Verzeichnisse automatisch passende Rechte bekommen +setfacl -R -m g:mail:rx /var/mail/vhosts || true +setfacl -dR -m g:mail:rx /var/mail/vhosts || true +usermod -a -G "$APP_GROUP" "$APP_USER" || true +install -d -m 0755 -o root -g root /var/www +install -d -m 0775 -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR" + +SUDOERS_DKIM="/etc/sudoers.d/mailwolt-dkim" +cat > "${SUDOERS_DKIM}" <<'EOF' +Defaults!/usr/local/sbin/mailwolt-install-dkim !requiretty +Defaults!/usr/local/sbin/mailwolt-remove-dkim !requiretty +Defaults!/usr/bin/systemctl !requiretty +Defaults!/usr/bin/test !requiretty + +www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-install-dkim * +www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-remove-dkim * +www-data ALL=(root) NOPASSWD: /usr/bin/systemctl reload opendkim +www-data ALL=(root) NOPASSWD: /usr/bin/test * + +mailwolt ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-install-dkim * +mailwolt ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-remove-dkim * +mailwolt ALL=(root) NOPASSWD: /usr/bin/systemctl reload opendkim +mailwolt ALL=(root) NOPASSWD: /usr/bin/test * +EOF +chown root:root "${SUDOERS_DKIM}" +chmod 440 "${SUDOERS_DKIM}" + +if ! visudo -c -f "${SUDOERS_DKIM}" >/dev/null 2>&1; then + echo "[!] Ungültiger sudoers-Eintrag in ${SUDOERS_DKIM} – entferne Datei." + rm -f "${SUDOERS_DKIM}" +fi + +SUDOERS_DOVEADM="/etc/sudoers.d/mailwolt-doveadm" +cat > "${SUDOERS_DOVEADM}" <<'EOF' +Cmnd_Alias MW_DOVEADM_STATUS = /usr/bin/doveadm -f tab mailbox status -u * messages INBOX, \ + /usr/bin/doveadm mailbox status -u * messages INBOX +www-data ALL=(vmail) NOPASSWD: MW_DOVEADM_STATUS +mailwolt ALL=(vmail) NOPASSWD: MW_DOVEADM_STATUS +EOF +chown root:root "${SUDOERS_DOVEADM}" +chmod 440 "${SUDOERS_DOVEADM}" +visudo -c -f "${SUDOERS_DOVEADM}" || rm -f "${SUDOERS_DOVEADM}" + +log "MariaDB include-fix …" +mkdir -p /etc/mysql/mariadb.conf.d +[[ -f /etc/mysql/mariadb.cnf ]] || echo '!include /etc/mysql/mariadb.conf.d/*.cnf' > /etc/mysql/mariadb.cnf + +log "Redis absichern …" +if [[ -z "${REDIS_PASS:-}" || "${REDIS_PASS}" == "changeme" ]]; then + REDIS_PASS="$(openssl rand -hex 16)" + export REDIS_PASS + log "Neues Redis-Passwort generiert." +fi +# Aktiven Redis-Config-Pfad aus systemd holen (Fallback: Standard) +REDIS_CONF="$(systemctl show -p ExecStart redis-server \ + | sed -n 's/^ExecStart=.*redis-server[[:space:]]\+\([^[:space:]]\+\).*/\1/p')" +REDIS_CONF="${REDIS_CONF:-/etc/redis/redis.conf}" + +# Bind + protected-mode hart setzen +sed -i 's/^[[:space:]]*#\?[[:space:]]*bind .*/bind 127.0.0.1/' "$REDIS_CONF" +sed -i 's/^[[:space:]]*#\?[[:space:]]*protected-mode .*/protected-mode yes/' "$REDIS_CONF" + +# Vorherige requirepass-Zeilen entfernen (kommentiert/unkommentiert), dann neu schreiben +sed -i '/^[[:space:]]*#\?[[:space:]]*requirepass[[:space:]]\+/d' "$REDIS_CONF" +printf '\nrequirepass %s\n' "${REDIS_PASS}" >> "$REDIS_CONF" + +# Dienst aktivieren & neu starten +systemctl enable --now redis-server +systemctl restart redis-server || true + +# Sanity-Check (kein harter Exit, nur Log) +if redis-cli -a "${REDIS_PASS}" ping 2>/dev/null | grep -q PONG; then + log "Redis mit Passwort OK." +else + warn "Redis PING mit Passwort fehlgeschlagen – bitte /etc/redis/redis.conf prüfen." +fi \ No newline at end of file diff --git a/scripts/20-ssl.sh b/scripts/20-ssl.sh new file mode 100644 index 0000000..1dceed2 --- /dev/null +++ b/scripts/20-ssl.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -euo pipefail +source ./lib.sh + +CONF_BASE="/etc/${APP_USER}" +CERT_DIR="${CONF_BASE}/ssl" +UI_SSL_DIR="/etc/ssl/ui"; WEBMAIL_SSL_DIR="/etc/ssl/webmail"; MAIL_SSL_DIR="/etc/ssl/mail" +UI_CERT="${UI_SSL_DIR}/fullchain.pem"; UI_KEY="${UI_SSL_DIR}/privkey.pem" +WEBMAIL_CERT="${WEBMAIL_SSL_DIR}/fullchain.pem"; WEBMAIL_KEY="${WEBMAIL_SSL_DIR}/privkey.pem" +MAIL_CERT="${MAIL_SSL_DIR}/fullchain.pem"; MAIL_KEY="${MAIL_SSL_DIR}/privkey.pem" + +install -d -m 0750 "$CERT_DIR" +CERT="${CERT_DIR}/cert.pem"; KEY="${CERT_DIR}/key.pem" + +if [[ ! -s "$CERT" || ! -s "$KEY" ]]; then + log "Self-signed Zertifikat erzeugen …" + OSSL_CFG="${CERT_DIR}/openssl.cnf" + cat > "$OSSL_CFG" <&2 +fi + +# Optional: kurze Info, wohin verlinkt wurde +echo "[i] Mail TLS: $MAIL_CERT -> $CERT ; $MAIL_KEY -> $KEY" \ No newline at end of file diff --git a/scripts/21-le-deploy-hook.sh b/scripts/21-le-deploy-hook.sh new file mode 100644 index 0000000..fb37804 --- /dev/null +++ b/scripts/21-le-deploy-hook.sh @@ -0,0 +1,588 @@ +#!/usr/bin/env bash +set -euo pipefail +source ./lib.sh + +log "Let's Encrypt Deploy-Hooks und Wrapper anlegen …" + +# ------------------------------------------------------------------- +# 2) POSIX-kompatibler Deploy-Wrapper (von Certbot aufgerufen) +# ------------------------------------------------------------------- +cat >/usr/local/sbin/mailwolt-deploy.sh <<'WRAP' +#!/bin/sh +# POSIX-safe Certbot deploy-hook (ohne bashisms) +set -eu + +# Installer-ENV laden (liefert UI_HOST/WEBMAIL_HOST/MAIL_HOSTNAME etc.) +if [ -r /etc/mailwolt/installer.env ]; then + . /etc/mailwolt/installer.env +fi + +UI_HOST="${UI_HOST:-}" +WEBMAIL_HOST="${WEBMAIL_HOST:-}" +MAIL_HOSTNAME="${MAIL_HOSTNAME:-}" +ACME_BASE="/etc/letsencrypt/live" + +copy_cert() { + le_base="$1" # z.B. /etc/letsencrypt/live/ui.example.com + target_dir="$2" # z.B. /etc/ssl/ui + + cert="${le_base}/fullchain.pem" + key="${le_base}/privkey.pem" + + [ -s "$cert" ] || { echo "[deploy] missing $cert"; return 1; } + [ -s "$key" ] || { echo "[deploy] missing $key"; return 1; } + + mkdir -p "$target_dir" + + # echte Dateien (keine Symlinks), feste Rechte + install -m 0644 "$cert" "${target_dir}/fullchain.pem" + install -m 0600 "$key" "${target_dir}/privkey.pem" + + echo "[+] Copied ${target_dir}/fullchain.pem und privkey.pem ← ${le_base}" +} + +reload_services() { + kind="$1" # ui | mail + if command -v systemctl >/dev/null 2>&1; then + if [ "$kind" = "mail" ]; then + systemctl reload postfix 2>/dev/null || true + systemctl reload dovecot 2>/dev/null || true + else + systemctl reload nginx 2>/dev/null || true + fi + fi +} + +# Certbot-Kontext +LINEAGE="${RENEWED_LINEAGE:-}" +HOST="" +if [ -n "$LINEAGE" ]; then + HOST="$(basename "$LINEAGE")" +fi + +did_any=0 + +maybe_copy_for_host() { + host="$1" + dir="$2" + [ -n "$host" ] || return 0 + + # Fall A: Certbot liefert RENEWED_DOMAINS (Space-getrennt) + if [ -n "${RENEWED_DOMAINS:-}" ]; then + case " ${RENEWED_DOMAINS} " in + *" ${host} "*) copy_cert "${ACME_BASE}/${host}" "${dir}" && did_any=1 ;; + esac + return 0 + fi + + # Fall B: Erst-issue / kein RENEWED_DOMAINS → über LINEAGE matchen + if [ -n "$HOST" ] && [ "$HOST" = "$host" ]; then + copy_cert "${ACME_BASE}/${host}" "${dir}" && did_any=1 + fi +} + +# Gezieltes Kopieren +maybe_copy_for_host "$UI_HOST" "/etc/ssl/ui" +maybe_copy_for_host "$WEBMAIL_HOST" "/etc/ssl/webmail" +maybe_copy_for_host "$MAIL_HOSTNAME" "/etc/ssl/mail" + +# Fallback (Erstlauf): kopiere vorhandene Lineages +if [ "$did_any" -eq 0 ]; then + [ -n "$UI_HOST" ] && [ -d "${ACME_BASE}/${UI_HOST}" ] && copy_cert "${ACME_BASE}/${UI_HOST}" "/etc/ssl/ui" + [ -n "$WEBMAIL_HOST" ] && [ -d "${ACME_BASE}/${WEBMAIL_HOST}" ] && copy_cert "${ACME_BASE}/${WEBMAIL_HOST}" "/etc/ssl/webmail" + [ -n "$MAIL_HOSTNAME" ] && [ -d "${ACME_BASE}/${MAIL_HOSTNAME}" ] && copy_cert "${ACME_BASE}/${MAIL_HOSTNAME}" "/etc/ssl/mail" +fi + +# TLSA-Refresh (tolerant falls App noch nicht ready) +if command -v php >/dev/null 2>&1 && [ -f /var/www/mailwolt/artisan ]; then + (cd /var/www/mailwolt && php artisan dns:tlsa:refresh) || true +fi + +# Services neu laden +if [ -n "$HOST" ]; then + if [ -n "$MAIL_HOSTNAME" ] && [ "$HOST" = "$MAIL_HOSTNAME" ]; then + reload_services mail + else + reload_services ui + fi +else + reload_services ui +fi + +exit 0 +WRAP +chmod +x /usr/local/sbin/mailwolt-deploy.sh + +# ------------------------------------------------------------------- +# 3) Certbot deploy-hook, der den Wrapper aufruft +# ------------------------------------------------------------------- +install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy +cat >/etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-certs.sh <<'HOOK' +#!/bin/sh +exec /usr/local/sbin/mailwolt-deploy.sh +HOOK +chmod +x /etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-certs.sh + + +log "[✓] MailWolt Deploy-Hook eingerichtet" + +##!/usr/bin/env bash +#set -euo pipefail +#source ./lib.sh +# +## Persistente Installer-Variablen (werden vom Wrapper gelesen) +#install -d -m 0755 /etc/mailwolt +#cat >/etc/mailwolt/installer.env </usr/local/sbin/mw-deploy.sh <<'WRAP' +##!/usr/bin/env bash +#set -euo pipefail +# +## Installer-Variablen laden +#set +u +#[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env +#set -u +# +#UI_HOST="${UI_HOST:-}" +#WEBMAIL_HOST="${WEBMAIL_HOST:-}" +#MAIL_HOSTNAME="${MAIL_HOSTNAME:-}" +# +## --- Kopieren statt Symlinks (damit Laravel lesen kann) --------------------- +#copy_cert() { +# local le_base="$1" target_dir="$2" +# local cert="${le_base}/fullchain.pem" +# local key="${le_base}/privkey.pem" +# +# [[ -s "$cert" && -s "$key" ]] || return 0 +# +# install -d -m 0755 "$target_dir" +# +# # Vorhandene Symlinks entfernen, sonst kopierst du in die LE-Datei hinein +# [ -L "${target_dir}/fullchain.pem" ] && rm -f "${target_dir}/fullchain.pem" +# [ -L "${target_dir}/privkey.pem" ] && rm -f "${target_dir}/privkey.pem" +# +# # Echte Dateien ablegen +# install -m 0644 "$cert" "${target_dir}/fullchain.pem" +# install -m 0600 "$key" "${target_dir}/privkey.pem" +# +# echo "[+] Copied ${target_dir}/fullchain.pem und privkey.pem ← ${le_base}" +#} +# +## Nur Domains bearbeiten, die in diesem Lauf betroffen sind. +## Bei manchen Distros ist RENEWED_DOMAINS auf Erst-issue leer -> Fallback nutzen. +#RDOMS=" ${RENEWED_DOMAINS:-} " +#did_any=0 +# +#maybe_copy_for() { +# local host="$1" dir="$2" +# [[ -z "$host" ]] && return 0 +# if [[ "$RDOMS" == *" ${host} "* ]]; then +# copy_cert "/etc/letsencrypt/live/${host}" "${dir}" +# did_any=1 +# fi +#} +# +## 1) Normalfall: nur die vom Certbot gemeldeten Hosts kopieren +#maybe_copy_for "$UI_HOST" "/etc/ssl/ui" +#maybe_copy_for "$WEBMAIL_HOST" "/etc/ssl/webmail" +#maybe_copy_for "$MAIL_HOSTNAME" "/etc/ssl/mail" +# +## 2) Fallback: Beim Erstlauf/Edge-Cases alles kopieren, was bereits existiert +#if [[ "$did_any" -eq 0 ]]; then +# [[ -n "$UI_HOST" && -d "/etc/letsencrypt/live/${UI_HOST}" ]] && copy_cert "/etc/letsencrypt/live/${UI_HOST}" "/etc/ssl/ui" +# [[ -n "$WEBMAIL_HOST" && -d "/etc/letsencrypt/live/${WEBMAIL_HOST}" ]] && copy_cert "/etc/letsencrypt/live/${WEBMAIL_HOST}" "/etc/ssl/webmail" +# [[ -n "$MAIL_HOSTNAME" && -d "/etc/letsencrypt/live/${MAIL_HOSTNAME}"]] && copy_cert "/etc/letsencrypt/live/${MAIL_HOSTNAME}"/etc/ssl/mail +#fi +# +## Optional: TLSA via Laravel (tolerant, falls App noch nicht gebaut) +#if command -v php >/dev/null 2>&1 && [ -d /var/www/mailwolt ] && [ -f /var/www/mailwolt/artisan ]; then +# (cd /var/www/mailwolt && php artisan dns:tlsa:refresh) || true +#fi +# +## Nginx nur neu laden, wenn aktiv +#if systemctl is-active --quiet nginx; then +# systemctl reload nginx || true +#fi +#WRAP +#chmod +x /usr/local/sbin/mw-deploy.sh +# +## 2) Certbot-Deploy-Hook: ruft den Wrapper bei jeder erfolgreichen Ausstellung/Renew auf +#install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy +#cat >/etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-certs.sh <<'HOOK' +##!/usr/bin/env bash +#exec /usr/local/sbin/mw-deploy.sh +#HOOK +#chmod +x /etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-certs.sh +# +#log "[✓] MailWolt Deploy-Hook eingerichtet" + +##!/usr/bin/env bash +#set -euo pipefail +#source ./lib.sh +# +## Persistente Installer-Variablen (werden vom Wrapper gelesen) +#install -d -m 0755 /etc/mailwolt +#cat >/etc/mailwolt/installer.env </usr/local/sbin/mw-deploy.sh <<'WRAP' +##!/usr/bin/env bash +#set -euo pipefail +# +## Installer-Variablen laden +#set +u +#[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env +#set -u +# +#UI_HOST="${UI_HOST:-}" +#WEBMAIL_HOST="${WEBMAIL_HOST:-}" +#MAIL_HOSTNAME="${MAIL_HOSTNAME:-}" +# +## --- Kopieren statt Symlinks (damit Laravel lesen kann) --------------------- +#copy_cert() { +# local le_base="$1" target_dir="$2" +# local cert="${le_base}/fullchain.pem" +# local key="${le_base}/privkey.pem" +# +# [[ -s "$cert" && -s "$key" ]] || return 0 +# +# # Zielordner sicherstellen +# install -d -m 0755 "$target_dir" +# +# # Falls vorher Symlinks existieren → entfernen, sonst würde "install" das Ziel des Links überschreiben +# [ -L "${target_dir}/fullchain.pem" ] && rm -f "${target_dir}/fullchain.pem" +# [ -L "${target_dir}/privkey.pem" ] && rm -f "${target_dir}/privkey.pem" +# +# # KOPIEREN mit sauberen Rechten (Chain world-readable, Key nur root) +# install -m 0644 "$cert" "${target_dir}/fullchain.pem" +# install -m 0600 "$key" "${target_dir}/privkey.pem" +# +# echo "[+] Copied ${target_dir}/fullchain.pem und privkey.pem ← ${le_base}" +#} +# +## Nur für Domains arbeiten, die in diesem Lauf betroffen sind +#RDOMS=" ${RENEWED_DOMAINS:-} " +# +## UI +#if [[ -n "$UI_HOST" && "$RDOMS" == *" ${UI_HOST} "* ]]; then +# copy_cert "/etc/letsencrypt/live/${UI_HOST}" "/etc/ssl/ui" +#fi +## Webmail +#if [[ -n "$WEBMAIL_HOST" && "$RDOMS" == *" ${WEBMAIL_HOST} "* ]]; then +# copy_cert "/etc/letsencrypt/live/${WEBMAIL_HOST}" "/etc/ssl/webmail" +#fi +## MX +#if [[ -n "$MAIL_HOSTNAME" && "$RDOMS" == *" ${MAIL_HOSTNAME} "* ]]; then +# copy_cert "/etc/letsencrypt/live/${MAIL_HOSTNAME}" "/etc/ssl/mail" +#fi +# +## Optional: TLSA via Laravel (still tolerant, falls App noch nicht gebaut) +#if command -v php >/dev/null 2>&1 && [ -d /var/www/mailwolt ] && [ -f /var/www/mailwolt/artisan ]; then +# (cd /var/www/mailwolt && php artisan dns:tlsa:refresh) || true +#fi +# +## Nginx nur neu laden, wenn aktiv +#if systemctl is-active --quiet nginx; then +# systemctl reload nginx || true +#fi +#WRAP +#chmod +x /usr/local/sbin/mw-deploy.sh +# +## 2) Certbot-Deploy-Hook: ruft den Wrapper bei jeder erfolgreichen Ausstellung/Renew auf +#install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy +#cat >/etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh <<'HOOK' +##!/usr/bin/env bash +#exec /usr/local/sbin/mw-deploy.sh +#HOOK +#chmod +x /etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh +# +#log "[✓] MailWolt Deploy-Hook eingerichtet" + +##!/usr/bin/env bash +#set -euo pipefail +#source ./lib.sh +# +#install -d -m 0755 /etc/mailwolt +#cat >/etc/mailwolt/installer.env </usr/local/sbin/mw-deploy.sh <<'WRAP' +##!/usr/bin/env bash +#set -euo pipefail +# +## Installer-Variablen laden (UI_HOST, WEBMAIL_HOST, MAIL_HOSTNAME, optional LE_EMAIL etc.) +#set +u +#[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env +#set -u +# +#UI_HOST="${UI_HOST:-}" +#WEBMAIL_HOST="${WEBMAIL_HOST:-}" +#MAIL_HOSTNAME="${MAIL_HOSTNAME:-}" +# +#link_if() { +# local le_base="$1" target_dir="$2" +# local cert="${le_base}/fullchain.pem" +# local key="${le_base}/privkey.pem" +# [[ -s "$cert" && -s "$key" ]] || return 0 +# install -d -m 0755 "$target_dir" +# ln -sf "$cert" "${target_dir}/fullchain.pem" +# ln -sf "$key" "${target_dir}/privkey.pem" +# chmod 644 "${target_dir}/fullchain.pem" 2>/dev/null || true +# chmod 600 "${target_dir}/privkey.pem" 2>/dev/null || true +# echo "[+] Linked ${target_dir} -> ${le_base}" +#} +# +## Nur für Domains arbeiten, die im aktuellen Lauf erneuert/ausgestellt wurden +#RDOMS=" ${RENEWED_DOMAINS:-} " +# +## UI +#if [[ -n "$UI_HOST" && "$RDOMS" == *" ${UI_HOST} "* ]]; then +# link_if "/etc/letsencrypt/live/${UI_HOST}" "/etc/ssl/ui" +#fi +## Webmail +#if [[ -n "$WEBMAIL_HOST" && "$RDOMS" == *" ${WEBMAIL_HOST} "* ]]; then +# link_if "/etc/letsencrypt/live/${WEBMAIL_HOST}" "/etc/ssl/webmail" +#fi +## MX +#if [[ -n "$MAIL_HOSTNAME" && "$RDOMS" == *" ${MAIL_HOSTNAME} "* ]]; then +# link_if "/etc/letsencrypt/live/${MAIL_HOSTNAME}" "/etc/ssl/mail" +#fi +# +## Optional: TLSA via Laravel, falls App schon vorhanden (sonst still überspringen) +#if command -v php >/dev/null 2>&1 && [ -d /var/www/mailwolt ]; then +# (cd /var/www/mailwolt && php artisan dns:tlsa:refresh) || true +#fi +# +## Nginx nur neu laden, wenn aktiv +#if systemctl is-active --quiet nginx; then +# systemctl reload nginx || true +#fi +#WRAP +#chmod +x /usr/local/sbin/mw-deploy.sh +# +## 2) Certbot-Deploy-Hooks einrichten (ruft nur den Wrapper auf) +#install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy +#cat >/etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh <<'HOOK' +##!/usr/bin/env bash +#exec /usr/local/sbin/mw-deploy.sh +#HOOK +#chmod +x /etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh +# +#log "[✓] MailWolt Deploy-Hook eingerichtet" + +##!/usr/bin/env bash +#set -euo pipefail +#source ./lib.sh +# +#log "Let's Encrypt Deploy-Hooks und Wrapper anlegen …" +# +## 1) Wrapper-Skript, das Symlinks setzt und Nginx reloaded +#cat >/usr/local/sbin/mw-deploy.sh <<'WRAP' +##!/usr/bin/env bash +#set -euo pipefail +# +#link_if() { +# local le_base="$1" target_dir="$2" +# local cert="${le_base}/fullchain.pem" +# local key="${le_base}/privkey.pem" +# [[ -s "$cert" && -s "$key" ]] || return 0 +# install -d -m 0755 "$target_dir" +# ln -sf "$cert" "${target_dir}/fullchain.pem" +# ln -sf "$key" "${target_dir}/privkey.pem" +# chmod 644 "${target_dir}/fullchain.pem" 2>/dev/null || true +# chmod 600 "${target_dir}/privkey.pem" 2>/dev/null || true +# echo "[+] Linked ${target_dir} -> ${le_base}" +#} +# +#UI_HOST="${UI_HOST:-}" +#WEBMAIL_HOST="${WEBMAIL_HOST:-}" +#MAIL_HOSTNAME="${MAIL_HOSTNAME:-}" +# +#[[ -n "$UI_HOST" ]] && link_if "/etc/letsencrypt/live/${UI_HOST}" "/etc/ssl/ui" +#[[ -n "$WEBMAIL_HOST" ]] && link_if "/etc/letsencrypt/live/${WEBMAIL_HOST}" "/etc/ssl/webmail" +#[[ -n "$MAIL_HOSTNAME" ]] && link_if "/etc/letsencrypt/live/${MAIL_HOSTNAME}" "/etc/ssl/mail" +# +#if systemctl is-active --quiet nginx; then +# systemctl reload nginx || true +#fi +#WRAP +# +#chmod +x /usr/local/sbin/mw-deploy.sh +# +## 2) Certbot Deploy-Hook-Verzeichnis + Symlink für Renewals +#install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy +#cat >/etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh <<'HOOK' +##!/usr/bin/env bash +#exec /usr/local/sbin/mw-deploy.sh +#HOOK +#chmod +x /etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh +# +#log "[✓] MailWolt Deploy-Hook eingerichtet" +# +###!/usr/bin/env bash +##set -euo pipefail +##source ./lib.sh +## +### ──────────────────────────────────────────────────────────────────────────── +### 21-le-deploy-hook.sh +### • legt /etc/mailwolt/installer.env an (falls fehlt) +### • erzeugt Deploy-Hooks: +### - 50-mailwolt-symlinks.sh → verlinkt LE-Zerts nach /etc/ssl/{ui,webmail,mail} +### - 60-mailwolt-tlsa.sh → aktualisiert TLSA (3 1 1) für MX bei jedem Renew +### • KEIN Reload von Postfix/Dovecot (kommt später im Installer) +### ──────────────────────────────────────────────────────────────────────────── +## +### 0) Hostnamen persistent speichern (für spätere Deploys) +##install -d -m 0755 /etc/mailwolt +##if [[ ! -f /etc/mailwolt/installer.env ]]; then +## cat >/etc/mailwolt/installer.env </etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh </dev/null || true +## chmod 600 "\${target_dir}/privkey.pem" 2>/dev/null || true +## echo "[+] Linked \${target_dir} -> \${le_base}" +##} +## +### Verlinken (nur wenn Host konfiguriert) +##[[ -n "${UI_HOST}" ]] && link_if "\$UI_LE" "\$UI_SSL_DIR" +##[[ -n "${WEBMAIL_HOST}" ]] && link_if "\$WEBMAIL_LE" "\$WEBMAIL_SSL_DIR" +##[[ -n "${MAIL_HOSTNAME}" ]] && link_if "\$MX_LE" "\$MAIL_SSL_DIR" +## +### Nur reloaden, wenn Nginx aktiv ist (Installer startet ihn später erst) +##if systemctl is-active --quiet nginx; then +## systemctl reload nginx || true +##fi +##HOOK +##chmod +x /etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh +## +### ──────────────────────────────────────────────────────────────────────────── +### 3) 60-mailwolt-tlsa.sh +### → nutzt Laravel, falls vorhanden; sonst Fallback mit OpenSSL. +### → schreibt nur, wenn sich der Hash geändert hat (idempotent) +### ──────────────────────────────────────────────────────────────────────────── +##cat >/etc/letsencrypt/renewal-hooks/deploy/60-mailwolt-tlsa.sh <<'HOOK' +###!/usr/bin/env bash +##set -euo pipefail +## +### installer.env lesen +##set +u +##[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env +##set -u +## +##APP_ENV_VAL="${APP_ENV:-production}" +##BASE_DOMAIN_VAL="${BASE_DOMAIN:-example.com}" +## +##case "$APP_ENV_VAL" in +## local|dev|development) exit 0 ;; +##esac +##[ "$BASE_DOMAIN_VAL" = "example.com" ] && exit 0 +## +##MX_HOST="${MAIL_HOSTNAME:-}" +##SERVICE="_25._tcp" +##DNS_DIR="/etc/mailwolt/dns" +##OUT_FILE="${DNS_DIR}/${MX_HOST}.tlsa.txt" +## +### Nur reagieren, wenn MX-Zertifikat betroffen war +##case " ${RENEWED_DOMAINS:-} " in +## *" ${MX_HOST} "*) ;; +## *) exit 0 ;; +##esac +## +##CERT="${RENEWED_LINEAGE}/fullchain.pem" +##[ -s "$CERT" ] || exit 0 +## +### Wenn Laravel vorhanden ist → interner Command (DB + Datei idempotent) +##if command -v php >/dev/null 2>&1 && [ -d /var/www/mailwolt ]; then +## cd /var/www/mailwolt || exit 0 +## php artisan dns:tlsa:refresh || true +## exit 0 +##fi +## +### Fallback: nur Datei aktualisieren, wenn Hash sich ändert +##HASH="$(openssl x509 -in "$CERT" -noout -pubkey \ +## | openssl pkey -pubin -outform DER \ +## | openssl dgst -sha256 | sed 's/^.*= //')" +##NEW_LINE="${SERVICE}.${MX_HOST}. IN TLSA 3 1 1 ${HASH}" +## +##mkdir -p "$DNS_DIR" +## +##if [ -r "$OUT_FILE" ] && grep -q "IN TLSA" "$OUT_FILE"; then +## if grep -q "$HASH" "$OUT_FILE"; then +## echo "[TLSA] Unverändert – kein Update nötig." +## exit 0 +## fi +##fi +## +##echo "$NEW_LINE" > "$OUT_FILE" +##echo "[TLSA] Aktualisiert: $NEW_LINE" +##HOOK +##chmod +x /etc/letsencrypt/renewal-hooks/deploy/60-mailwolt-tlsa.sh +## +### ──────────────────────────────────────────────────────────────────────────── +##echo "[✓] Deploy-Hooks installiert." \ No newline at end of file diff --git a/scripts/22-dkim-helper.sh b/scripts/22-dkim-helper.sh new file mode 100644 index 0000000..2ef8c7d --- /dev/null +++ b/scripts/22-dkim-helper.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +set -euo pipefail +source ./lib.sh + +log "Installiere DKIM-Helper …" + +install -d -m 0755 /usr/local/sbin + +cat >/usr/local/sbin/mailwolt-install-dkim <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +DOMAIN="$1" # z.B. sysmail.toastra.com +SELECTOR="${2:-mwl1}" + +[[ -n "$DOMAIN" ]] || { echo "Usage: $0 [selector]"; exit 2; } + +KEYDIR="/etc/opendkim/keys/${DOMAIN}" +PRIV="${KEYDIR}/${SELECTOR}.private" +TXT="${KEYDIR}/${SELECTOR}.txt" + +install -d -m 0750 -o opendkim -g opendkim "$KEYDIR" + +if [[ ! -s "$PRIV" ]]; then + opendkim-genkey -b 2048 -s "$SELECTOR" -d "$DOMAIN" -D "$KEYDIR" + chown opendkim:opendkim "$PRIV" + chmod 600 "$PRIV" +fi + +grep -q "^${SELECTOR}\._domainkey\.${DOMAIN} " /etc/opendkim/KeyTable 2>/dev/null \ + || echo "${SELECTOR}._domainkey.${DOMAIN} ${DOMAIN}:${SELECTOR}:${PRIV}" >> /etc/opendkim/KeyTable + +grep -q "^\*@${DOMAIN} " /etc/opendkim/SigningTable 2>/dev/null \ + || echo "*@${DOMAIN} ${SELECTOR}._domainkey.${DOMAIN}" >> /etc/opendkim/SigningTable + +install -d -m 0755 /etc/mailwolt/dns +[[ -s "$TXT" ]] && cp -f "$TXT" "/etc/mailwolt/dns/dkim-${DOMAIN}.txt" || true + +systemctl restart opendkim +EOF + +log "[✓] DKIM-Helper installiert: /usr/local/sbin/mailwolt-install-dkim" \ No newline at end of file diff --git a/scripts/30-db.sh b/scripts/30-db.sh new file mode 100644 index 0000000..9781562 --- /dev/null +++ b/scripts/30-db.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail +source ./lib.sh + +log "MariaDB vorbereiten …" +systemctl enable --now mariadb +mysql -uroot < /etc/postfix/sql/mysql-virtual-domains.cf < /etc/postfix/sql/mysql-virtual-mailbox-maps.cf < /etc/postfix/sql/mysql-virtual-alias-maps.cf </dev/null 2>&1 || true diff --git a/scripts/50-dovecot.sh b/scripts/50-dovecot.sh new file mode 100644 index 0000000..bd2922f --- /dev/null +++ b/scripts/50-dovecot.sh @@ -0,0 +1,247 @@ +#!/usr/bin/env bash +set -euo pipefail +source ./lib.sh + +MAIL_SSL_DIR="/etc/ssl/mail" +MAIL_CERT="${MAIL_SSL_DIR}/fullchain.pem" +MAIL_KEY="${MAIL_SSL_DIR}/privkey.pem" + +log "Dovecot konfigurieren …" + +# ────────────────────────────────────────────────────────────────────────────── +# 1) vmail-Benutzer/Gruppe & Mailspool vorbereiten (DYNAMIC UID!) +# ────────────────────────────────────────────────────────────────────────────── + +# Sicherstellen, dass die Gruppe 'mail' existiert (auf Debian/Ubuntu idR vorhanden) +getent group mail >/dev/null || groupadd -g 8 mail || true + +# vmail anlegen, wenn er fehlt. Bevorzugt UID 109, falls frei – sonst automatisch. +if ! getent passwd vmail >/dev/null; then + if ! getent passwd 109 >/dev/null; then + useradd -u 109 -g mail -d /var/mail -M -s /usr/sbin/nologin vmail + else + useradd -g mail -d /var/mail -M -s /usr/sbin/nologin vmail + fi +fi + +# Tatsächliche vmail-UID ermitteln (wird unten in die Dovecot-Config geschrieben) +VMAIL_UID="$(id -u vmail)" + +# Mailspool-Basis +install -d -m 0770 -o vmail -g mail /var/mail/vhosts + +# ────────────────────────────────────────────────────────────────────────────── +# 2) Dovecot Grundgerüst +# ────────────────────────────────────────────────────────────────────────────── + +# Hauptdatei +install -d -m 0755 /etc/dovecot/conf.d +cat > /etc/dovecot/dovecot.conf <<'CONF' +!include_try /etc/dovecot/conf.d/*.conf +CONF + +# Mail-Location & Namespace + UID-Grenzen +cat > /etc/dovecot/conf.d/10-mail.conf < /etc/dovecot/conf.d/15-mailboxes.conf <<'CONF' +namespace inbox { + inbox = yes + + mailbox Drafts { + special_use = \Drafts + auto = subscribe + } + mailbox Junk { + special_use = \Junk + auto = subscribe + } + mailbox Trash { + special_use = \Trash + auto = subscribe + } + mailbox Sent { + special_use = \Sent + auto = subscribe + } + mailbox Archive { + special_use = \Archive + auto = create + } +} +CONF + +# Auth +cat > /etc/dovecot/conf.d/10-auth.conf <<'CONF' +disable_plaintext_auth = yes +auth_mechanisms = plain login +!include_try auth-sql.conf.ext + +auth_cache_size = 10M +auth_cache_ttl = 1 hour +CONF + +# SQL-Anbindung (Passwörter aus App-DB) +cat > /etc/dovecot/dovecot-sql.conf.ext < /etc/dovecot/conf.d/auth-sql.conf.ext <<'CONF' +passdb { + driver = sql + args = /etc/dovecot/dovecot-sql.conf.ext +} +userdb { + driver = static + args = uid=vmail gid=mail home=/var/mail/vhosts/%d/%n +} +CONF +chown root:dovecot /etc/dovecot/conf.d/auth-sql.conf.ext +chmod 640 /etc/dovecot/conf.d/auth-sql.conf.ext + +# ────────────────────────────────────────────────────────────────────────────── +# 3) IMAP Optimierung (iOS/IDLE-freundlich) +# ────────────────────────────────────────────────────────────────────────────── + +cat > /etc/dovecot/conf.d/20-imap.conf <<'CONF' +# IMAP-spezifische Einstellungen + +imap_idle_notify_interval = 2 mins +imap_hibernate_timeout = 0 + +protocol imap { + mail_max_userip_connections = 20 + imap_logout_format = in=%i out=%o deleted=%{deleted} expunged=%{expunged} +} +CONF + +# ────────────────────────────────────────────────────────────────────────────── +# 4) Master Services (LMTP, AUTH, IMAP, POP3, STATS) +# ────────────────────────────────────────────────────────────────────────────── + +cat > /etc/dovecot/conf.d/10-master.conf <<'CONF' +service lmtp { + unix_listener /var/spool/postfix/private/dovecot-lmtp { + mode = 0600 + user = postfix + group = postfix + } +} +service auth { + unix_listener /var/spool/postfix/private/auth { + mode = 0660 + user = postfix + group = postfix + } + unix_listener auth-userdb { + mode = 0660 + user = vmail + group = mail + } + process_limit = 1 +} +service imap-login { + inet_listener imap { + port = 143 + } + inet_listener imaps { + port = 993 + ssl = yes + } + process_limit = 128 + process_min_avail = 10 + service_count = 0 + vsz_limit = 512M +} +service pop3-login { + inet_listener pop3 { + port = 110 + } + inet_listener pop3s { + port = 995 + ssl = yes + } + process_limit = 50 + service_count = 0 +} +CONF + +# --- Dovecot: doveadm-server für App-Zugriff --- +cat >/etc/dovecot/conf.d/99-mailwolt-perms.conf <<'CONF' +service auth { + unix_listener auth-userdb { + mode = 0660 + user = vmail + group = mail + } +} + +service stats { + unix_listener stats-reader { + mode = 0660 + user = vmail + group = mail + } + unix_listener stats-writer { + mode = 0660 + user = vmail + group = mail + } +} +CONF + +# ────────────────────────────────────────────────────────────────────────────── +# 5) SSL-Konfiguration (ohne DH-Param-Erzeugung) +# ────────────────────────────────────────────────────────────────────────────── + +DOVECOT_SSL_CONF="/etc/dovecot/conf.d/10-ssl.conf" +touch "$DOVECOT_SSL_CONF" +grep -q '^ssl\s*=' "$DOVECOT_SSL_CONF" 2>/dev/null || echo "ssl = required" >> "$DOVECOT_SSL_CONF" +if grep -q '^\s*ssl_cert\s*=' "$DOVECOT_SSL_CONF"; then + sed -i "s|^\s*ssl_cert\s*=.*|ssl_cert = <${MAIL_CERT}|" "$DOVECOT_SSL_CONF" +else + echo "ssl_cert = <${MAIL_CERT}" >> "$DOVECOT_SSL_CONF" +fi +if grep -q '^\s*ssl_key\s*=' "$DOVECOT_SSL_CONF"; then + sed -i "s|^\s*ssl_key\s*=.*|ssl_key = <${MAIL_KEY}|" "$DOVECOT_SSL_CONF" +else + echo "ssl_key = <${MAIL_KEY}" >> "$DOVECOT_SSL_CONF" +fi +grep -q '^ssl_min_protocol' "$DOVECOT_SSL_CONF" || echo "ssl_min_protocol = TLSv1.2" >> "$DOVECOT_SSL_CONF" +grep -q '^ssl_prefer_server_ciphers' "$DOVECOT_SSL_CONF" || echo "ssl_prefer_server_ciphers = yes" >> "$DOVECOT_SSL_CONF" + +grep -q '^ssl_dh' "$DOVECOT_SSL_CONF" || echo "ssl_dh = > "$DOVECOT_SSL_CONF" + +# ────────────────────────────────────────────────────────────────────────────── +# 6) Verzeichnisse & Rechte prüfen +# ────────────────────────────────────────────────────────────────────────────── + +mkdir -p /var/spool/postfix/private +chown root:root /var/spool/postfix +chmod 0755 /var/spool/postfix +chown postfix:postfix /var/spool/postfix/private +chmod 0755 /var/spool/postfix/private + + +# ────────────────────────────────────────────────────────────────────────────── +# 7) Abschluss +# ────────────────────────────────────────────────────────────────────────────── + +log "Dovecot-Konfiguration abgeschlossen." diff --git a/scripts/60-rspamd-opendkim.sh b/scripts/60-rspamd-opendkim.sh new file mode 100644 index 0000000..9d7166a --- /dev/null +++ b/scripts/60-rspamd-opendkim.sh @@ -0,0 +1,319 @@ +#!/usr/bin/env bash +set -euo pipefail +source ./lib.sh + +log "Rspamd + OpenDKIM einrichten …" + +# ────────────────────────────────────────────────────────────── +# ENV laden +# ────────────────────────────────────────────────────────────── +set +u +[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env +set -u + +BASE_DOMAIN="${BASE_DOMAIN:-example.com}" +SYSMAIL_DOMAIN="${SYSMAIL_DOMAIN:-sysmail.${BASE_DOMAIN}}" # z.B. sysmail.example.com +DKIM_ENABLE="${DKIM_ENABLE:-1}" # 1=OpenDKIM aktiv +DKIM_SELECTOR="${DKIM_SELECTOR:-mwl1}" # z.B. mwl1 +DKIM_GENERATE="${DKIM_GENERATE:-0}" # 1=Key generieren, falls fehlt +RSPAMD_CONTROLLER_PASSWORD="${RSPAMD_CONTROLLER_PASSWORD:-admin}" + +# ────────────────────────────────────────────────────────────── +# Rspamd (Controller + Milter) +# ────────────────────────────────────────────────────────────── +install -d -m 0750 /etc/rspamd/local.d + +if command -v rspamadm >/dev/null 2>&1; then + RSPAMD_HASH="$(rspamadm pw -p "${RSPAMD_CONTROLLER_PASSWORD}")" +else + RSPAMD_HASH="${RSPAMD_CONTROLLER_PASSWORD}" +fi + +cat >/etc/rspamd/local.d/worker-controller.inc </etc/rspamd/local.d/statistic.conf </etc/rspamd/local.d/worker-proxy.inc <<'CONF' +worker "proxy" { + bind_socket = "127.0.0.1:11332"; + milter = yes; + timeout = 120s; + + upstream "scan" { + default = yes; + self_scan = yes; + servers = "127.0.0.1:11333"; + } +} +CONF + +cat >/etc/rspamd/local.d/worker-normal.inc <<'CONF' +worker "normal" { + bind_socket = "127.0.0.1:11333"; +} +CONF + +cat >/etc/rspamd/local.d/milter_headers.conf <<'CONF' +use = ["authentication-results"]; +header = "Authentication-Results"; +CONF + +cat >/etc/rspamd/local.d/options.inc <<'CONF' +dns { + servers = ["9.9.9.9:53", "1.1.1.1:53"]; + timeout = 5s; + retransmits = 2; +} +CONF + +# ────────────────────────────────────────────────────────────── +# Rspamd Redis-Konfiguration +# ────────────────────────────────────────────────────────────── +log "Rspamd Redis konfigurieren …" + +: "${REDIS_PASS:=}" + +cat >/etc/rspamd/local.d/redis.conf </dev/null 2>&1; then + if [[ -n "${REDIS_PASS}" ]]; then + if redis-cli -h 127.0.0.1 -p 6379 -a "${REDIS_PASS}" ping >/dev/null 2>&1; then + log "[✓] Redis erreichbar und Passwort akzeptiert." + else + log "[!] Warnung: Redis antwortet nicht oder Passwort falsch." + fi + else + if redis-cli -h 127.0.0.1 -p 6379 ping >/dev/null 2>&1; then + log "[✓] Redis erreichbar (ohne Passwort)." + else + log "[!] Warnung: Redis antwortet nicht." + fi + fi +fi + +systemctl enable --now rspamd || true + +# ────────────────────────────────────────────────────────────── +# OpenDKIM – nur wenn DKIM_ENABLE=1 +# ────────────────────────────────────────────────────────────── + +if [[ "${DKIM_ENABLE}" != "1" ]]; then + log "DKIM_ENABLE=0 → OpenDKIM wird übersprungen." + /usr/sbin/postconf -e "milter_default_action = accept" + /usr/sbin/postconf -e "milter_protocol = 6" + /usr/sbin/postconf -e "smtpd_milters = inet:127.0.0.1:11332" + /usr/sbin/postconf -e "non_smtpd_milters = inet:127.0.0.1:11332" + exit 0 +fi + + +install -d -m 0755 /etc/opendkim +install -d -m 0750 /etc/opendkim/keys +chown -R opendkim:opendkim /etc/opendkim +chmod 750 /etc/opendkim/keys + +# TrustedHosts +cat >/etc/opendkim/TrustedHosts <<'CONF' +127.0.0.1 +::1 +localhost +CONF +chown opendkim:opendkim /etc/opendkim/TrustedHosts +chmod 640 /etc/opendkim/TrustedHosts + +# ── Key-Verzeichnis für SYSMAIL_DOMAIN vorbereiten ─────────────────────────── +KEY_DIR="/etc/opendkim/keys/${SYSMAIL_DOMAIN}" +KEY_PRIV="${KEY_DIR}/${DKIM_SELECTOR}.private" +KEY_DNSTXT="${KEY_DIR}/${DKIM_SELECTOR}.txt" +install -d -m 0750 -o opendkim -g opendkim "${KEY_DIR}" + +# ── Key optional generieren (nur wenn gewünscht) ───────────────────────────── +if [[ ! -s "${KEY_PRIV}" && "${DKIM_GENERATE}" = "1" ]]; then + if command -v opendkim-genkey >/dev/null 2>&1; then + opendkim-genkey -b 2048 -s "${DKIM_SELECTOR}" -d "${SYSMAIL_DOMAIN}" -D "${KEY_DIR}" + chown opendkim:opendkim "${KEY_DIR}/${DKIM_SELECTOR}.private" || true + chmod 600 "${KEY_DIR}/${DKIM_SELECTOR}.private" || true + else + echo "[!] opendkim-genkey fehlt – kann DKIM-Key nicht generieren." + fi +fi + +# ── Key-/SigningTable nur anlegen, nicht leeren ─────────────────────────────── +touch /etc/opendkim/KeyTable /etc/opendkim/SigningTable +chown opendkim:opendkim /etc/opendkim/KeyTable /etc/opendkim/SigningTable +chmod 640 /etc/opendkim/KeyTable /etc/opendkim/SigningTable + +if [[ -s "${KEY_PRIV}" && "${BASE_DOMAIN}" != "example.com" ]]; then + LINE_KT="${DKIM_SELECTOR}._domainkey.${SYSMAIL_DOMAIN} ${SYSMAIL_DOMAIN}:${DKIM_SELECTOR}:${KEY_PRIV}" + LINE_ST="*@${SYSMAIL_DOMAIN} ${DKIM_SELECTOR}._domainkey.${SYSMAIL_DOMAIN}" + grep -Fqx "$LINE_KT" /etc/opendkim/KeyTable || echo "$LINE_KT" >> /etc/opendkim/KeyTable + grep -Fqx "$LINE_ST" /etc/opendkim/SigningTable || echo "$LINE_ST" >> /etc/opendkim/SigningTable +else + echo "[i] Kein Private Key unter ${KEY_PRIV} – App-Helper trägt später ein." +fi + +# ── Hauptkonfiguration ─────────────────────────────────────────────────────── +cat >/etc/opendkim.conf <<'CONF' +Syslog yes +UMask 002 +Mode sv +Socket inet:8891@127.0.0.1 +PidFile /run/opendkim/opendkim.pid +Canonicalization relaxed/simple + +On-BadSignature accept +On-Default accept +On-KeyNotFound accept +On-NoSignature accept + +LogWhy yes +OversignHeaders From + +KeyTable /etc/opendkim/KeyTable +SigningTable refile:/etc/opendkim/SigningTable +ExternalIgnoreList /etc/opendkim/TrustedHosts +InternalHosts /etc/opendkim/TrustedHosts + +UserID opendkim:opendkim +AutoRestart yes +AutoRestartRate 10/1h +Background yes +DNSTimeout 5 +SignatureAlgorithm rsa-sha256 +SyslogSuccess yes +CONF + +# ── systemd Drop-in: /run/opendkim sicherstellen ───────────────────────────── +install -d -m 0755 /etc/systemd/system/opendkim.service.d +cat >/etc/systemd/system/opendkim.service.d/override.conf <<'EOF' +[Service] +RuntimeDirectory=opendkim +RuntimeDirectoryMode=0755 +EOF + +install -d -o opendkim -g opendkim -m 0755 /run/opendkim + +# ────────────────────────────────────────────────────────────── +# Root-Helper: DKIM installieren / entfernen + sudoers-Regel +# ────────────────────────────────────────────────────────────── +install -d -m 0750 /usr/local/sbin + +# --- mailwolt-install-dkim ------------------------------------ +cat > /usr/local/sbin/mailwolt-install-dkim <<'EOSH' +#!/usr/bin/env bash +set -euo pipefail + +DOMAIN="$1" +SELECTOR="$2" +SRC_PRIV="$3" +SRC_TXT="${4:-}" + +OKDIR="/etc/opendkim" +KEYDIR="${OKDIR}/keys/${DOMAIN}" +KEYPRI="${KEYDIR}/${SELECTOR}.private" + +install -d -m 0750 -o opendkim -g opendkim "${KEYDIR}" +install -m 0600 -o opendkim -g opendkim "${SRC_PRIV}" "${KEYPRI}" + +KT="${OKDIR}/KeyTable" +ST="${OKDIR}/SigningTable" +touch "$KT" "$ST" +chown opendkim:opendkim "$KT" "$ST" +chmod 0640 "$KT" "$ST" + +LINE_KT="${SELECTOR}._domainkey.${DOMAIN} ${DOMAIN}:${SELECTOR}:${KEYPRI}" +LINE_ST="*@${DOMAIN} ${SELECTOR}._domainkey.${DOMAIN}" + +grep -Fqx "$LINE_KT" "$KT" || echo "$LINE_KT" >> "$KT" +grep -Fqx "$LINE_ST" "$ST" || echo "$LINE_ST" >> "$ST" + +if [[ -n "${SRC_TXT}" && -s "${SRC_TXT}" ]]; then + install -d -m 0755 /etc/mailwolt/dns + cp -f "${SRC_TXT}" "/etc/mailwolt/dns/dkim-${DOMAIN}.txt" +fi + +systemctl is-active --quiet opendkim && systemctl reload opendkim || true +echo "OK" +EOSH +chmod 0750 /usr/local/sbin/mailwolt-install-dkim +chown root:root /usr/local/sbin/mailwolt-install-dkim + +# --- 2) mailwolt-remove-dkim ---------------------------------- +cat >/usr/local/sbin/mailwolt-remove-dkim <<'EOSH' +#!/usr/bin/env bash +set -euo pipefail + +DOMAIN="$1" # z.B. kunden.tld oder sysmail.example.com +SELECTOR="$2" # z.B. mwl1 + +OKDIR="/etc/opendkim" +KEYDIR="${OKDIR}/keys/${DOMAIN}" +KEYPRI="${KEYDIR}/${SELECTOR}.private" +KT="${OKDIR}/KeyTable" +ST="${OKDIR}/SigningTable" + +# Key-Datei löschen (falls vorhanden) +[[ -f "${KEYPRI}" ]] && rm -f "${KEYPRI}" + +# Zeilen aus KeyTable und SigningTable entfernen +if [[ -f "$KT" ]]; then + tmp="$(mktemp)"; grep -v -F "${SELECTOR}._domainkey.${DOMAIN} ${DOMAIN}:${SELECTOR}:" "$KT" >"$tmp" && mv "$tmp" "$KT" + chown opendkim:opendkim "$KT"; chmod 0640 "$KT" +fi +if [[ -f "$ST" ]]; then + tmp="$(mktemp)"; grep -v -F "*@${DOMAIN} ${SELECTOR}._domainkey.${DOMAIN}" "$ST" >"$tmp" && mv "$tmp" "$ST" + chown opendkim:opendkim "$ST"; chmod 0640 "$ST" +fi + +# Verzeichnis ggf. aufräumen +rmdir "${KEYDIR}" 2>/dev/null || true + +# Dienst neu laden, falls aktiv +if systemctl is-active --quiet opendkim; then + systemctl reload opendkim || true +fi + +echo "OK" +EOSH +chown root:root /usr/local/sbin/mailwolt-remove-dkim +chmod 0750 /usr/local/sbin/mailwolt-remove-dkim + +# ── Dienst + Postfix-Milter aktivieren ───────────────────────── +systemctl daemon-reload +systemctl enable opendkim || true + +touch /run/mailwolt.need-apply-milters || true + +chgrp _rspamd /etc/rspamd/local.d/*.inc /etc/rspamd/local.d/*.conf || true +chmod 0640 /etc/rspamd/local.d/*.inc /etc/rspamd/local.d/*.conf || true + +#/usr/sbin/postconf -e "smtpd_milters = inet:127.0.0.1:11332, inet:127.0.0.1:8891" +#/usr/sbin/postconf -e "non_smtpd_milters = inet:127.0.0.1:11332, inet:127.0.0.1:8891" + +log "[✓] Rspamd + OpenDKIM eingerichtet (läuft; signiert, sobald Keys vorhanden sind)." diff --git a/scripts/61-opendmarc.sh b/scripts/61-opendmarc.sh new file mode 100644 index 0000000..5f48521 --- /dev/null +++ b/scripts/61-opendmarc.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +set -euo pipefail +source ./lib.sh + +log "OpenDMARC installieren/konfigurieren …" + +# Flags laden +set +u +[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env +set -u +OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-1}" + +# Paket sicherstellen +if ! dpkg -s opendmarc >/dev/null 2>&1; then + apt-get update -qq + apt-get install -y opendmarc +fi + +# Config-Verzeichnisse +install -d -m 0755 /etc/opendmarc +install -d -m 0755 /run/opendmarc + +# IgnoreHosts +cat >/etc/opendmarc/ignore.hosts <<'EOF' +127.0.0.1 +::1 +localhost +EOF +chmod 0644 /etc/opendmarc/ignore.hosts + +# Hauptkonfiguration +cat >/etc/opendmarc.conf <<'EOF' +AuthservID mailwolt +TrustedAuthservIDs mailwolt +IgnoreHosts /etc/opendmarc/ignore.hosts +Syslog true +SoftwareHeader true +Socket local:/run/opendmarc/opendmarc.sock +RejectFailures false +EOF +chmod 0644 /etc/opendmarc.conf + +# systemd Drop-in für RuntimeDirectory (robust nach Reboot) +install -d -m 0755 /etc/systemd/system/opendmarc.service.d +cat >/etc/systemd/system/opendmarc.service.d/override.conf <<'EOF' +[Service] +RuntimeDirectory=opendmarc +RuntimeDirectoryMode=0755 +EOF + +systemctl daemon-reload + +# Dienst nach Flag +if [[ "$OPENDMARC_ENABLE" = "1" ]]; then + systemctl enable --now opendmarc +else + systemctl disable --now opendmarc || true +fi + +# Postfix-Milter-Kette konsistent setzen (Rspamd + OpenDKIM + optional OpenDMARC) +touch /run/mailwolt.need-apply-milters || true + +log "[✓] OpenDMARC (ENABLE=${OPENDMARC_ENABLE}) bereit." \ No newline at end of file diff --git a/scripts/62-clamav.sh b/scripts/62-clamav.sh new file mode 100644 index 0000000..fd3cb07 --- /dev/null +++ b/scripts/62-clamav.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +set -euo pipefail +source ./lib.sh + +log "ClamAV (clamav-daemon) installieren/konfigurieren …" + +# Flags laden +set +u +[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env +set -u +CLAMAV_ENABLE="${CLAMAV_ENABLE:-0}" + +# Pakete +if ! dpkg -s clamav-daemon >/dev/null 2>&1; then + apt-get update -qq + apt-get install -y clamav clamav-daemon +fi + +# Signaturen aktualisieren (erst Freshclam starten) +systemctl stop clamav-freshclam 2>/dev/null || true +freshclam || true +systemctl start clamav-freshclam || true + +# clamd LocalSocket setzen +sed -i 's|^#\?LocalSocket .*|LocalSocket /run/clamav/clamd.ctl|' /etc/clamav/clamd.conf || true +install -d -m 0755 /run/clamav +chown clamav:clamav /run/clamav + +# Dienst nach Flag +if [[ "$CLAMAV_ENABLE" = "1" ]]; then + systemctl enable --now clamav-daemon +else + systemctl disable --now clamav-daemon || true +fi + +# Rspamd-Integration (nur wenn aktiv) +AV_CONF="/etc/rspamd/local.d/antivirus.conf" +if [[ "$CLAMAV_ENABLE" = "1" ]]; then + cat >"$AV_CONF" <<'EOF' +clamav { + symbol = "CLAM_VIRUS"; + type = "clamav"; + servers = "/run/clamav/clamd.ctl"; + scan_mime_parts = true; + scan_text_mime = true; + max_size = 50mb; + log_clean = false; + action = "reject"; +} +EOF + chown root:_rspamd "$AV_CONF" || true + chmod 0640 "$AV_CONF" || true + systemctl reload rspamd || systemctl restart rspamd +else + rm -f "$AV_CONF" || true + systemctl reload rspamd || true +fi + +log "[✓] ClamAV (ENABLE=${CLAMAV_ENABLE}) konfiguriert." \ No newline at end of file diff --git a/scripts/63-fail2ban.sh b/scripts/63-fail2ban.sh new file mode 100644 index 0000000..454b24f --- /dev/null +++ b/scripts/63-fail2ban.sh @@ -0,0 +1,230 @@ +#!/usr/bin/env bash +set -euo pipefail +source ./lib.sh + +log "Fail2Ban installieren/konfigurieren …" + +# Flags laden +set +u +[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env +set -u +FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}" + +# Paket +if ! dpkg -s fail2ban >/dev/null 2>&1; then + apt-get update -qq + apt-get install -y fail2ban sqlite3 +fi + +install -d -m 0755 /etc/fail2ban/jail.d + +# --------------------------------------------------------------- +# Basis-Jails (praxisnah) +# --------------------------------------------------------------- +cat >/etc/fail2ban/jail.d/mailwolt.conf <<'EOF' +[sshd] +enabled = true +port = ssh +logpath = /var/log/auth.log + +[postfix] +enabled = true +logpath = /var/log/mail.log +port = smtp,ssmtp,submission,465 + +[dovecot] +enabled = true +logpath = /var/log/mail.log +port = pop3,pop3s,imap,imaps,submission,465,587,993 + +[rspamd-controller] +enabled = true +port = 11334 +filter = rspamd +logpath = /var/log/rspamd/rspamd.log +maxretry = 5 +EOF + +# einfacher Filter für Rspamd-Controller +if [ ! -f /etc/fail2ban/filter.d/rspamd.conf ]; then + cat >/etc/fail2ban/filter.d/rspamd.conf <<'EOF' +[Definition] +failregex = .*Authentication failed for user.* from +ignoreregex = +EOF +fi + +# --------------------------------------------------------------- +# Fail2Ban-Backend auf SQLite umstellen +# --------------------------------------------------------------- +log "SQLite-Backend aktivieren …" + +cat >/etc/fail2ban/fail2ban.local <<'EOF' +[Definition] +loglevel = INFO +logtarget = /var/log/fail2ban.log +dbfile = /var/lib/fail2ban/fail2ban.sqlite3 +dbpurgeage = 86400 +EOF + +# Datenbankverzeichnis sicherstellen +install -d -o fail2ban -g fail2ban -m 0750 /var/lib/fail2ban + +# Falls DB nicht existiert, Dummy anlegen (wird vom Dienst erweitert) +if [ ! -f /var/lib/fail2ban/fail2ban.sqlite3 ]; then + sqlite3 /var/lib/fail2ban/fail2ban.sqlite3 "VACUUM;" +fi +chown fail2ban:fail2ban /var/lib/fail2ban/fail2ban.sqlite3 +chmod 0640 /var/lib/fail2ban/fail2ban.sqlite3 + +# --------------------------------------------------------------- +# sudoers für Web-UI +# --------------------------------------------------------------- +# Fail2Ban Blacklist-Jail +cat >/etc/fail2ban/jail.d/mailwolt-blacklist.local <<'EOF' +[mailwolt-blacklist] +enabled = true +filter = none +port = anyport +bantime = -1 +findtime = 1 +maxretry = 1 +EOF + +cat >/etc/fail2ban/filter.d/none.conf <<'EOF' +[Definition] +failregex = +ignoreregex = +EOF + +chmod 0640 /etc/fail2ban/filter.d/none.conf + + +SUDOERS_F2B="/etc/sudoers.d/mailwolt-fail2ban" +cat > "${SUDOERS_F2B}" <<'EOF' +Defaults:www-data !requiretty +www-data ALL=(root) NOPASSWD: \ + /usr/bin/fail2ban-client, \ + /usr/bin/fail2ban-client ping, \ + /usr/bin/fail2ban-client status, \ + /usr/bin/fail2ban-client status *, \ + /usr/bin/fail2ban-client get *, \ + /usr/bin/fail2ban-client set * banip *, \ + /usr/bin/fail2ban-client set * unbanip *, \ + /usr/bin/fail2ban-client reload, \ + /usr/bin/journalctl, \ + /bin/journalctl, \ + /usr/bin/zgrep, \ + /bin/zgrep, \ + /usr/bin/grep, \ + /bin/grep, \ + /usr/bin/tail, \ + /bin/tail, \ + /usr/bin/sqlite3, \ + /usr/bin/tee /etc/fail2ban/jail.d/* +EOF +chown root:root "${SUDOERS_F2B}" +chmod 440 "${SUDOERS_F2B}" + +if ! visudo -c -f "${SUDOERS_F2B}" >/dev/null 2>&1; then + echo "[!] Ungültiger sudoers-Eintrag in ${SUDOERS_F2B} – entferne Datei." + rm -f "${SUDOERS_F2B}" +fi + +# --------------------------------------------------------------- +# Dienst aktivieren/deaktivieren +# --------------------------------------------------------------- +if [[ "$FAIL2BAN_ENABLE" = "1" ]]; then + systemctl enable --now fail2ban +else + systemctl disable --now fail2ban || true +fi + +log "[✓] Fail2Ban (ENABLE=${FAIL2BAN_ENABLE}) bereit." + + +##!/usr/bin/env bash +#set -euo pipefail +#source ./lib.sh +# +#log "Fail2Ban installieren/konfigurieren …" +# +## Flags laden +#set +u +#[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env +#set -u +#FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}" +# +## Paket +#if ! dpkg -s fail2ban >/dev/null 2>&1; then +# apt-get update -qq +# apt-get install -y fail2ban +#fi +# +#install -d -m 0755 /etc/fail2ban/jail.d +# +## Basis-Jails (praxisnah) +#cat >/etc/fail2ban/jail.d/mailwolt.conf <<'EOF' +#[DEFAULT] +#bantime = 1h +#findtime = 10m +#maxretry = 5 +#backend = auto +# +#[sshd] +#enabled = true +#port = ssh +#logpath = /var/log/auth.log +# +#[postfix] +#enabled = true +#logpath = /var/log/mail.log +#port = smtp,ssmtp,submission,465 +# +#[dovecot] +#enabled = true +#logpath = /var/log/mail.log +#port = pop3,pop3s,imap,imaps,submission,465,587,993 +# +#[rspamd-controller] +#enabled = true +#port = 11334 +#filter = rspamd +#logpath = /var/log/rspamd/rspamd.log +#maxretry = 5 +#EOF +# +## einfacher Filter für Rspamd-Controller +#if [ ! -f /etc/fail2ban/filter.d/rspamd.conf ]; then +# cat >/etc/fail2ban/filter.d/rspamd.conf <<'EOF' +#[Definition] +#failregex = .*Authentication failed for user.* from +#ignoreregex = +#EOF +#fi +# +#SUDOERS_F2B="/etc/sudoers.d/mailwolt-fail2ban" +#cat > "${SUDOERS_F2B}" <<'EOF' +#www-data ALL=(root) NOPASSWD: /usr/bin/fail2ban-client status, /usr/bin/fail2ban-client status * +#EOF +#chown root:root "${SUDOERS_F2B}" +#chmod 440 "${SUDOERS_F2B}" +# +#if ! visudo -c -f "${SUDOERS_F2B}" >/dev/null 2>&1; then +# echo "[!] Ungültiger sudoers-Eintrag in ${SUDOERS_F2B} – entferne Datei." +# rm -f "${SUDOERS_F2B}" +#fi +# +#sudo tee /etc/sudoers.d/mailwolt-fail2ban >/dev/null <<'EOF' +#www-data ALL=(root) NOPASSWD: /usr/bin/fail2ban-client status, /usr/bin/fail2ban-client status * +#EOF +#sudo visudo -cf /etc/sudoers.d/mailwolt-fail2ban +# +## Dienst nach Flag +#if [[ "$FAIL2BAN_ENABLE" = "1" ]]; then +# systemctl enable --now fail2ban +#else +# systemctl disable --now fail2ban || true +#fi +# +#log "[✓] Fail2Ban (ENABLE=${FAIL2BAN_ENABLE}) bereit." \ No newline at end of file diff --git a/scripts/64-apply-milters.sh b/scripts/64-apply-milters.sh new file mode 100644 index 0000000..0927324 --- /dev/null +++ b/scripts/64-apply-milters.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail +source ./lib.sh + +# nur ausführen, wenn vorherige Schritte das Flag gesetzt haben +if [[ -f /run/mailwolt.need-apply-milters ]]; then + if command -v /usr/local/sbin/mailwolt-apply-milters >/dev/null 2>&1; then + log "Setze Postfix-Milter-Kette (Rspamd/OpenDKIM[/OpenDMARC]) …" + /usr/local/sbin/mailwolt-apply-milters || true + else + # Fallback (ident wie im Tool) + /usr/sbin/postconf -e "milter_default_action = accept" + /usr/sbin/postconf -e "milter_protocol = 6" + CHAIN="inet:127.0.0.1:11333, inet:127.0.0.1:8891" + systemctl is-active --quiet opendmarc && CHAIN="$CHAIN, inet:127.0.0.1:8893" || true + /usr/sbin/postconf -e "smtpd_milters = $CHAIN" + /usr/sbin/postconf -e "non_smtpd_milters = $CHAIN" + systemctl reload postfix || true + fi + rm -f /run/mailwolt.need-apply-milters || true + log "[✓] Milter-Kette angewandt." +else + log "Milter-Kette: kein Bedarf (Flag nicht gesetzt) – überspringe." +fi \ No newline at end of file diff --git a/scripts/70-nginx.sh b/scripts/70-nginx.sh new file mode 100644 index 0000000..822f1fd --- /dev/null +++ b/scripts/70-nginx.sh @@ -0,0 +1,528 @@ +#!/usr/bin/env bash +set -euo pipefail +source ./lib.sh + +log "Nginx konfigurieren …" + +# ── Flags/Umgebung (vom Bootstrap gesetzt; hier Fallbacks) ──────────────── +DEV_MODE="${DEV_MODE:-0}" # 1 = DEV (Vite-Proxy aktiv), 0 = PROD +PROXY_MODE="${PROXY_MODE:-0}" # 1 = NPM/Proxy davor, Backend spricht nur HTTP:80 +NPM_IP="${NPM_IP:-}" # z.B. 10.10.20.20 + +# Erwartet vom Bootstrap/Installer exportiert: +: "${UI_HOST:?UI_HOST fehlt}" +: "${WEBMAIL_HOST:?WEBMAIL_HOST fehlt}" +: "${APP_DIR:?APP_DIR fehlt}" + +ACME_ROOT="/var/www/letsencrypt" +install -d -m 0755 "$ACME_ROOT" + +# Default-Sites entfernen (verhindert doppelten default_server) +rm -f /etc/nginx/sites-enabled/default /etc/nginx/sites-available/default || true + +# HTTP/2-Unterstützung erkennen +NGINX_HTTP2_SUFFIX="" +if nginx -V 2>&1 | grep -q http_v2; then + NGINX_HTTP2_SUFFIX=" http2" +fi + +# PHP-FPM Socket/TCP finden → fastcgi_pass bauen +detect_php_fpm_sock(){ + for v in 8.3 8.2 8.1 8.0 7.4; do + s="/run/php/php${v}-fpm.sock" + [[ -S "$s" ]] && { echo "unix:${s}"; return; } + done + [[ -S "/run/php/php-fpm.sock" ]] && { echo "unix:/run/php/php-fpm.sock"; return; } + echo "127.0.0.1:9000" +} +PHP_FPM_TARGET="$(detect_php_fpm_sock)" +if [[ "$PHP_FPM_TARGET" == unix:* ]]; then + FASTCGI_PASS="fastcgi_pass ${PHP_FPM_TARGET};" +else + FASTCGI_PASS="fastcgi_pass ${PHP_FPM_TARGET};" +fi + +# ── Builder 1: HTTP-only (Proxy-Mode: TLS endet im NPM) ─────────────────── +## $1=host, $2=outfile +#build_site_http_only(){ +# local host="$1" outfile="$2" +# +# local def="" +# [[ "${DEV_MODE}" = "1" ]] && def=" default_server" +# [[ -z "${host}" || "${host}" = "_" ]] && host="_" +# +# cat > "$outfile" <> "$outfile" <<'CONF' +# # DEV: Vite-Proxy (HMR) +# location ^~ /@vite/ { proxy_pass http://127.0.0.1:5173/@vite/; proxy_set_header Host $host; } +# location ^~ /node_modules/ { proxy_pass http://127.0.0.1:5173/node_modules/; proxy_set_header Host $host; } +# location ^~ /resources/ { proxy_pass http://127.0.0.1:5173/resources/; proxy_set_header Host $host; } +#CONF +# fi +# +# echo "}" >> "$outfile" +#} + +#build_site_http_only(){ +# local host="$1" outfile="$2" +# +# # DEV: IP-Zugriff ohne Hostname → default_server + server_name _ +# local def="" +# if [[ "${DEV_MODE}" = "1" ]]; then +# def=" default_server" +# host="_" +# fi +# [[ -z "${host}" || "${host}" = "_" ]] && host="_" +# +# cat > "$outfile" <> "$outfile" <<'CONF' +# # DEV: Vite-Proxy (HMR) +# location ^~ /@vite/ { proxy_pass http://127.0.0.1:5173/@vite/; proxy_set_header Host $host; } +# location ^~ /node_modules/ { proxy_pass http://127.0.0.1:5173/node_modules/; proxy_set_header Host $host; } +# location ^~ /resources/ { proxy_pass http://127.0.0.1:5173/resources/; proxy_set_header Host $host; } +#CONF +# fi +# +# echo "}" >> "$outfile" +#} + +# $1=host, $2=outfile, $3=default_flag (default|nodefault) +build_site_http_only(){ + local host="$1" outfile="$2" def_flag="${3:-default}" + + local def="" + if [[ "${DEV_MODE}" = "1" && "${def_flag}" = "default" ]]; then + def=" default_server" + fi + [[ -z "${host}" || "${host}" = "_" ]] && host="_" + + cat > "$outfile" <> "$outfile" <<'CONF' + # DEV: Vite-Proxy (HMR) + location ^~ /@vite/ { proxy_pass http://127.0.0.1:5173/@vite/; proxy_set_header Host $host; } + location ^~ /node_modules/ { proxy_pass http://127.0.0.1:5173/node_modules/; proxy_set_header Host $host; } + location ^~ /resources/ { proxy_pass http://127.0.0.1:5173/resources/; proxy_set_header Host $host; } +CONF + fi + + echo "}" >> "$outfile" +} + +# ── Builder 2: 80→443 Redirect + 443/TLS (Live-Server) ──────────────────── +# $1=host, $2=cert_dir (/etc/ssl/ui | /etc/ssl/webmail), $3=outfile +build_site_tls(){ + local host="$1" cert_dir="$2" outfile="$3" + local cert="${cert_dir}/fullchain.pem" + local key="${cert_dir}/privkey.pem" + + cat > "$outfile" <> "$outfile" <<'CONF' + # DEV: Vite-Proxy (npm run dev) + location = /vite-hmr { + proxy_pass http://127.0.0.1:5173/vite-hmr; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + } + location ^~ /@vite/ { proxy_pass http://127.0.0.1:5173/@vite/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; } + location ^~ /node_modules/ { proxy_pass http://127.0.0.1:5173/node_modules/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; } + location ^~ /resources/ { proxy_pass http://127.0.0.1:5173/resources/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; } +CONF + fi + + echo "}" >> "$outfile" +} + +build_site_acme_only(){ + local host="$1" outfile="$2" + + cat > "$outfile" < "$outfile" < "$outfile" < /etc/nginx/conf.d/realip.conf </dev/null 2>&1 || true + systemctl reload nginx || true +else + die "nginx -t fehlgeschlagen – siehe /var/log/nginx/*.log" +fi diff --git a/scripts/75-le-issue.sh b/scripts/75-le-issue.sh new file mode 100644 index 0000000..02ae118 --- /dev/null +++ b/scripts/75-le-issue.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +set -euo pipefail +source ./lib.sh + +ACME_WEBROOT="/var/www/letsencrypt" +install -d -m 0755 "${ACME_WEBROOT}/.well-known/acme-challenge" + +# Staging optional (verbraucht kein Live-Limit) +CERTBOT_EXTRA=() +LE_STAGING="${LE_STAGING:-0}" +[[ "$LE_STAGING" = "1" ]] && CERTBOT_EXTRA+=(--test-cert) + +# Einheitliche LE-Mail (Fallback) +LE_MAIL="${LE_EMAIL:-admin@${BASE_DOMAIN}}" + +resolve_ok() { + local host="$1" + local pats=() + [[ -n "${SERVER_PUBLIC_IPV4:-}" ]] && pats+=("${SERVER_PUBLIC_IPV4//./\\.}") + [[ -n "${SERVER_PUBLIC_IPV6:-}" ]] && pats+=("${SERVER_PUBLIC_IPV6//:/\\:}") + [[ ${#pats[@]} -eq 0 ]] && return 0 + getent ahosts "$host" | awk '{print $1}' | sort -u \ + | grep -Eq "^($(IFS='|'; echo "${pats[*]}"))$" +} + +probe_http() { + local host="$1" + echo test > "${ACME_WEBROOT}/.well-known/acme-challenge/_probe" + curl -fsS --max-time 5 -4 "http://${host}/.well-known/acme-challenge/_probe" >/dev/null \ + || curl -fsS --max-time 5 -6 "http://${host}/.well-known/acme-challenge/_probe" >/dev/null +} + +issue() { + local host="${1:-}" + [[ -z "$host" ]] && return 0 + + echo "[i] Versuche LE für ${host} …" + + if ! resolve_ok "$host"; then + echo "[!] DNS zeigt (noch) nicht hierher – überspringe: ${host}" + return 0 + fi + + if ! probe_http "$host"; then + echo "[!] ACME-HTTP-Check für ${host} fehlgeschlagen (Port 80/IPv6/Firewall/Nginx prüfen)." + # wir versuchen trotzdem – Certbot meldet sich, falls es scheitert + fi + + EXTRA_ARGS=() + # Für MX den Key wiederverwenden → stabiler TLSA (3 1 1) + [[ "$host" == "${MAIL_HOSTNAME}" ]] && EXTRA_ARGS+=(--reuse-key) + + # WICHTIG: Deploy-Wrapper anhängen, damit Symlinks/Nginx gesetzt werden + certbot certonly \ + --agree-tos -m "${LE_MAIL}" --non-interactive \ + --webroot -w "${ACME_WEBROOT}" -d "${host}" \ + --deploy-hook /usr/local/sbin/mailwolt-deploy.sh \ + "${EXTRA_ARGS[@]}" "${CERTBOT_EXTRA[@]}" || true +} + +if [[ "${BASE_DOMAIN}" != "example.com" ]]; then + issue "${UI_HOST:-}" + issue "${WEBMAIL_HOST:-}" + issue "${MAIL_HOSTNAME:-}" + + # Nginx nur neu laden, wenn aktiv + if systemctl is-active --quiet nginx; then + systemctl reload nginx || true + fi +else + echo "[i] BASE_DOMAIN=example.com – LE wird übersprungen." +fi + +# ────────────────────────────────────────────────────────────────────────────── +# FIX: Validierung & Reparatur des Mail-Zertifikats +# ────────────────────────────────────────────────────────────────────────────── +MAIL_SSL_DIR="/etc/ssl/mail" +install -d -m 0755 "$MAIL_SSL_DIR" + +MAIL_CERT="${MAIL_SSL_DIR}/fullchain.pem" +MAIL_KEY="${MAIL_SSL_DIR}/privkey.pem" + +HOST="${MAIL_HOSTNAME:-}" +LE_DIR="" +[[ -n "$HOST" ]] && LE_DIR="/etc/letsencrypt/live/${HOST}" + +need_fix=0 + +# Ist der vorhandene Key gültig? (leer/nicht vorhanden/ungültig -> fix) +if [[ ! -s "$MAIL_KEY" ]] || ! openssl pkey -in "$MAIL_KEY" -noout >/dev/null 2>&1; then + need_fix=1 +fi + +# Wenn Fix nötig: aus Let's Encrypt Live kopieren +if [[ $need_fix -eq 1 ]]; then + echo "[!] Ungültiger oder fehlender Mail-Private-Key – versuche Reparatur …" + if [[ -n "$LE_DIR" && -r "${LE_DIR}/privkey.pem" && -r "${LE_DIR}/fullchain.pem" ]]; then + cp -f "${LE_DIR}/privkey.pem" "$MAIL_KEY" + cp -f "${LE_DIR}/fullchain.pem" "$MAIL_CERT" + chown root:root "$MAIL_CERT" "$MAIL_KEY" + chmod 600 "$MAIL_KEY" + chmod 644 "$MAIL_CERT" + echo "[+] Zertifikate neu kopiert aus ${LE_DIR}." + # Reload NICHT sofort – flaggen für 90-services + touch /run/mailwolt.need-dovecot-reload + else + echo "[!] Konnte ${LE_DIR}/privkey.pem oder fullchain.pem nicht lesen – bitte prüfen." + fi +else + echo "[✓] Mail-Zertifikat & -Key sind gültig." +fi + +# Optionaler Live-Check (nur wenn Host gesetzt) +if [[ -n "$HOST" ]]; then + if openssl s_client -connect "${HOST}:993" -servername "${HOST}" /dev/null \ + | grep -q "Verify return code: 0"; then + echo "[✓] TLS-Handshake erfolgreich auf imaps://${HOST}:993." + else + echo "[!] TLS-Handshake auf imaps://${HOST}:993 fehlgeschlagen (Dovecot Reload folgt in 90-services, falls Flag gesetzt)." + fi +fi \ No newline at end of file diff --git a/scripts/80-app.sh b/scripts/80-app.sh new file mode 100644 index 0000000..6fa99e4 --- /dev/null +++ b/scripts/80-app.sh @@ -0,0 +1,343 @@ +#!/usr/bin/env bash +set -euo pipefail +source ./lib.sh + +# --- Helper: sicherer Frontend-Build als APP_USER --------------------------- +safe_frontend_build() { + echo "[i] Frontend build …" + + # Verzeichnisse & Rechte vorbereiten (Gruppen-sticky & ACL) + install -d -m 2775 -o "$APP_USER" -g "$APP_GROUP" \ + "${APP_DIR}/public/build" "${APP_DIR}/node_modules" "${APP_DIR}/.npm-cache" + + chown -R "$APP_USER":"$APP_GROUP" "${APP_DIR}" + find "${APP_DIR}" -type d -exec chmod 2775 {} \; + find "${APP_DIR}" -type f -exec chmod 664 {} \; + setfacl -R -m g:"$APP_GROUP":rwX -m d:g:"$APP_GROUP":rwX "${APP_DIR}" || true + + # Vite-/Build-Reste bereinigen (falls mal root dort gebaut hat) + rm -rf "${APP_DIR}/node_modules/.vite" "${APP_DIR}/public/build/"* 2>/dev/null || true + + # npm auf projektlokales Cache konfigurieren + sudo -u "$APP_USER" -H bash -lc "cat > ~/.npmrc <<'RC' +fund=false +audit=false +prefer-offline=true +cache=${APP_DIR}/.npm-cache +RC" + + # Node ggf. installieren + if ! command -v node >/dev/null 2>&1; then + curl -fsSL https://deb.nodesource.com/setup_22.x | bash - + apt-get install -y nodejs + fi + + # Dependencies + Build (als App-User) + if sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && (npm ci --no-audit --no-fund || npm install --no-audit --no-fund) && npm run build"; then + return 0 + fi + + echo "[!] Build fehlgeschlagen – Rechtefix + Clean + Retry …" + rm -rf "${APP_DIR}/node_modules/.vite" "${APP_DIR}/public/build/"* 2>/dev/null || true + chown -R "$APP_USER":"$APP_GROUP" "${APP_DIR}" + find "${APP_DIR}" -type d -exec chmod 2775 {} \; + find "${APP_DIR}" -type f -exec chmod 664 {} \; + + sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm run build" +} + +relink_and_reload() { + if [[ -d /etc/letsencrypt/renewal-hooks/deploy ]]; then + run-parts /etc/letsencrypt/renewal-hooks/deploy || true + fi + if systemctl is-active --quiet nginx; then + systemctl reload nginx || true + fi +} + +log "App bereitstellen …" +mkdir -p "$(dirname "$APP_DIR")" +chown -R "$APP_USER":"$APP_GROUP" "$(dirname "$APP_DIR")" + +# Repo holen oder Laravel anlegen – passe GIT_REPO/GIT_BRANCH bei Bedarf an +GIT_REPO="${GIT_REPO:-https://git.nexlab.at/boban/mailwolt.git}" +GIT_BRANCH="${GIT_BRANCH:-main}" + +if [[ "${GIT_REPO}" == "https://example.com/your-repo-placeholder.git" ]]; then + [[ -d "$APP_DIR" && -n "$(ls -A "$APP_DIR" 2>/dev/null || true)" ]] || \ + sudo -u "$APP_USER" -H bash -lc "cd /var/www && composer create-project laravel/laravel ${APP_USER} --no-interaction" +else + if [[ ! -d "${APP_DIR}/.git" ]]; then + sudo -u "$APP_USER" -H bash -lc "git clone --depth=1 -b ${GIT_BRANCH} ${GIT_REPO} ${APP_DIR}" + else + sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && git fetch --depth=1 origin ${GIT_BRANCH} && git reset --hard origin/${GIT_BRANCH}" + fi + [[ -f "${APP_DIR}/composer.json" ]] && sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && composer install --no-interaction --prefer-dist" +fi + +ENV_FILE="${APP_DIR}/.env" +sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && cp -n .env.example .env || true" +grep -q '^APP_KEY=' "$ENV_FILE" || echo "APP_KEY=" >> "$ENV_FILE" +sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan key:generate --force || true" + +# --- App-URL/Hosts ---------------------------------------------------------- +#SERVER_PUBLIC_IPV4="${SERVER_PUBLIC_IPV4:-}" +#if [[ -z "$SERVER_PUBLIC_IPV4" ]] && command -v curl >/dev/null 2>&1; then +# SERVER_PUBLIC_IPV4="$(curl -fsS --max-time 2 https://ifconfig.me 2>/dev/null || true)" +# [[ "$SERVER_PUBLIC_IPV4" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || SERVER_PUBLIC_IPV4="" +#fi +#[[ -n "$SERVER_PUBLIC_IPV4" ]] || SERVER_PUBLIC_IPV4="$(detect_ip)" +# +#UI_CERT="/etc/ssl/ui/fullchain.pem" +#UI_KEY="/etc/ssl/ui/privkey.pem" +# +#if [[ -n "${UI_HOST:-}" ]]; then +# APP_HOST_VAL="$UI_HOST" +# APP_URL_VAL="https://${UI_HOST}" +#else +# APP_HOST_VAL="$SERVER_PUBLIC_IPV4" +# SCHEME="http" +# [[ -s "$UI_CERT" && -s "$UI_KEY" ]] && SCHEME="https" +# APP_URL_VAL="${SCHEME}://${SERVER_PUBLIC_IPV4}" +#fi + +SERVER_PUBLIC_IPV4="${SERVER_PUBLIC_IPV4:-}" +if [[ -z "$SERVER_PUBLIC_IPV4" ]] && command -v curl >/dev/null 2>&1; then + SERVER_PUBLIC_IPV4="$(curl -fsS --max-time 2 https://ifconfig.me 2>/dev/null || true)" + [[ "$SERVER_PUBLIC_IPV4" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || SERVER_PUBLIC_IPV4="" +fi +[[ -n "$SERVER_PUBLIC_IPV4" ]] || SERVER_PUBLIC_IPV4="$(detect_ip)" + +UI_CERT="/etc/ssl/ui/fullchain.pem" +UI_KEY="/etc/ssl/ui/privkey.pem" + +# DEV-Modus: immer IP als Host, http (kein example.com / keine Fake-Domain) +if [[ "${DEV_MODE:-0}" = "1" || "${APP_ENV:-production}" = "local" ]]; then + APP_HOST_VAL="$SERVER_PUBLIC_IPV4" + APP_URL_VAL="http://${APP_HOST_VAL}" +else + # PROD/normal: wenn UI_HOST gesetzt → benutzen, sonst IP + if [[ -n "${UI_HOST:-}" ]]; then + APP_HOST_VAL="$UI_HOST" + APP_URL_VAL="https://${UI_HOST}" + else + APP_HOST_VAL="$SERVER_PUBLIC_IPV4" + SCHEME="http" + [[ -s "$UI_CERT" && -s "$UI_KEY" ]] && SCHEME="https" + APP_URL_VAL="${SCHEME}://${APP_HOST_VAL}" + fi +fi + +SECURE=$([[ "${APP_ENV}" = "production" ]] && echo true || echo false) + +# --- .env schreiben --------------------------------------------------------- +upsert_env APP_URL "${APP_URL_VAL}" + +if [[ "${PROXY_MODE:-0}" -eq 1 ]]; then + TP_LIST="127.0.0.1,::1" + [[ -n "${NPM_IP:-}" ]] && TP_LIST="${TP_LIST},${NPM_IP}" + upsert_env TRUSTED_PROXIES "$TP_LIST" + upsert_env TRUSTED_HEADERS "x-forwarded-all" +else + upsert_env TRUSTED_PROXIES "" + upsert_env TRUSTED_HEADERS "x-forwarded-all" +fi + +upsert_env APP_HOST "${APP_HOST_VAL}" +upsert_env APP_NAME "${APP_NAME}" +upsert_env APP_ENV "${APP_ENV:-production}" +upsert_env APP_DEBUG "${APP_DEBUG:-false}" +upsert_env APP_TIMEZONE "${APP_TZ:-UTC}" + +upsert_env APP_LOCALE "${APP_LOCALE:-de}" +upsert_env APP_FALLBACK_LOCALE "en" + +upsert_env SERVER_PUBLIC_IPV4 "${SERVER_PUBLIC_IPV4}" +upsert_env SERVER_PUBLIC_IPV6 "${SERVER_PUBLIC_IPV6:-}" + +upsert_env SYSMAIL_SUB "${SYSMAIL_SUB}" +upsert_env SYSMAIL_DOMAIN "${SYSMAIL_DOMAIN}" +upsert_env DKIM_ENABLE "${DKIM_ENABLE}" +upsert_env DKIM_SELECTOR "${DKIM_SELECTOR}" +upsert_env DKIM_GENERATE "${DKIM_GENERATE}" + +upsert_env BASE_DOMAIN "${BASE_DOMAIN}" +upsert_env UI_SUB "${UI_SUB}" +upsert_env WEBMAIL_SUB "${WEBMAIL_SUB}" +upsert_env MTA_SUB "${MTA_SUB}" +upsert_env LE_EMAIL "${LE_EMAIL:-admin@${BASE_DOMAIN}}" + +upsert_env DB_CONNECTION "mysql" +upsert_env DB_HOST "127.0.0.1" +upsert_env DB_PORT "3306" +upsert_env DB_DATABASE "${DB_NAME}" +upsert_env DB_USERNAME "${DB_USER}" +upsert_env DB_PASSWORD "${DB_PASS}" + +upsert_env CACHE_SETTINGS_STORE "redis" +upsert_env CACHE_STORE "redis" +upsert_env CACHE_DRIVER "redis" +upsert_env CACHE_PREFIX "${APP_USER_PREFIX}_cache:" +upsert_env SESSION_DRIVER "redis" +upsert_env SESSION_SECURE_COOKIE "false" # wird nach SSL-Setup auf true gesetzt +upsert_env SESSION_SAMESITE "lax" +upsert_env REDIS_CLIENT "phpredis" +upsert_env REDIS_HOST "127.0.0.1" +upsert_env REDIS_PORT "6379" +upsert_env REDIS_PASSWORD "${REDIS_PASS}" +upsert_env REDIS_DB "0" +upsert_env REDIS_CACHE_DB "1" +upsert_env REDIS_CACHE_CONNECTION "cache" +upsert_env REDIS_CACHE_LOCK_CONNECTION "default" + +upsert_env BROADCAST_DRIVER "reverb" +upsert_env QUEUE_CONNECTION "redis" +upsert_env LOG_CHANNEL "daily" + +upsert_env REVERB_APP_ID "${APP_USER_PREFIX}" +grep -q '^REVERB_APP_KEY=' "$ENV_FILE" || upsert_env REVERB_APP_KEY "${APP_USER_PREFIX}_$(openssl rand -hex 16)" +grep -q '^REVERB_APP_SECRET=' "$ENV_FILE" || upsert_env REVERB_APP_SECRET "${APP_USER_PREFIX}_$(openssl rand -hex 32)" +upsert_env REVERB_HOST "\${APP_HOST}" +upsert_env REVERB_PORT "443" +upsert_env REVERB_SCHEME "https" +upsert_env REVERB_PATH "/ws" +upsert_env REVERB_SCALING_ENABLED "true" +upsert_env REVERB_SCALING_CHANNEL "reverb" + +upsert_env VITE_REVERB_APP_KEY "\${REVERB_APP_KEY}" +upsert_env VITE_REVERB_HOST "\${REVERB_HOST}" +upsert_env VITE_REVERB_PORT "\${REVERB_PORT}" +upsert_env VITE_REVERB_SCHEME "\${REVERB_SCHEME}" +upsert_env VITE_REVERB_PATH "\${REVERB_PATH}" + +upsert_env REVERB_SERVER_APP_KEY "\${REVERB_APP_KEY}" +upsert_env REVERB_SERVER_HOST "127.0.0.1" +upsert_env REVERB_SERVER_PORT "8080" +upsert_env REVERB_SERVER_PATH "" +upsert_env REVERB_SERVER_SCHEME "http" + +# --- DEV Block (optional) --------------------------------------------------- +DEV_MODE="${DEV_MODE:-0}" +if [[ "$DEV_MODE" = "1" ]]; then + sed -i '/^# --- MailWolt DEV/,/^# --- \/MailWolt DEV/d' "${ENV_FILE}" + cat >> "${ENV_FILE}" <make(Illuminate\\Contracts\\Console\\Kernel::class)->bootstrap(); + \$d = App\\Models\\Domain::firstOrCreate([\"domain\"=>\"${SYSMAIL_DOMAIN}\"],[\"is_active\"=>1,\"is_system\"=>1]); + \$r = app(App\\Services\\DkimService::class)->generateForDomain(\$d, 2048, \"${DKIM_SELECTOR}\"); + echo \$r[\"priv_path\"], \"\\n\"; + echo \$r[\"dns_txt\"], \"\\n\"; + ' + ")" + + PRIV_PATH="$(printf '%s\n' "$OUT" | sed -n '1p')" + DNS_TXT="$(printf '%s\n' "$OUT" | sed -n '2,$p')" + + if [[ -z "$PRIV_PATH" || ! -s "$PRIV_PATH" ]]; then + echo "[!] DKIM priv_path fehlt oder Datei leer: $PRIV_PATH" >&2 + exit 1 + fi + + TMP_TXT="$(mktemp /tmp/dkim_txt_XXXXXX.txt)" + printf '%s' "$DNS_TXT" >"$TMP_TXT" + + # 2) Root-Helper ausführen (hängt Key ein, pflegt Key/SigningTable, kopiert TXT) + if [[ -x /usr/local/sbin/mailwolt-install-dkim ]]; then + /usr/local/sbin/mailwolt-install-dkim "${SYSMAIL_DOMAIN}" "${DKIM_SELECTOR}" "${PRIV_PATH}" "${TMP_TXT}" + else + echo "[!] Helper /usr/local/sbin/mailwolt-install-dkim fehlt oder ist nicht ausführbar." >&2 + fi + + rm -f "$TMP_TXT" || true + + # 3) OpenDKIM neu laden + touch /run/mailwolt.need-opendkim-reload || true +else + log "DKIM übersprungen (DKIM_ENABLE=${DKIM_ENABLE}, SYSMAIL_DOMAIN='${SYSMAIL_DOMAIN}')." +fi + + +# --- TLSA aus App heraus (idempotent; läuft, wenn Zert lesbar ist) ---------- +sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan dns:tlsa:refresh || true" + +# --- Build Frontend (nur wenn nötig) ---------------------------------------- +if [[ -f "${APP_DIR}/package.json" && ! -f "${APP_DIR}/public/build/manifest.json" ]]; then + safe_frontend_build +fi + +# --- Abschluss: Caches + Rechte + Reloads ----------------------------------- +sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan optimize:clear && php artisan config:cache && php artisan optimize:clear" + +# Konsistente Rechte/ACL für das gesamte App-Verzeichnis +chown -R "$APP_USER":"$APP_GROUP" "$APP_DIR" +find "$APP_DIR" -type d -exec chmod 2775 {} \; +find "$APP_DIR" -type f -exec chmod 664 {} \; +setfacl -R -m g:"$APP_GROUP":rwX -m d:g:"$APP_GROUP":rwX "$APP_DIR" || true + +# Laravel-Write-Dirs sicherstellen (mit setgid & ACL) +install -d -m 2775 -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR/storage" "$APP_DIR/bootstrap/cache" +chgrp -R www-data "$APP_DIR/storage" "$APP_DIR/bootstrap/cache" || true +find "$APP_DIR/storage" "$APP_DIR/bootstrap/cache" -type d -exec chmod 2775 {} \; || true +find "$APP_DIR/storage" "$APP_DIR/bootstrap/cache" -type f -exec chmod 0664 {} \; || true +setfacl -R -m u:www-data:rwx,u:${APP_USER}:rwx "$APP_DIR/storage" "$APP_DIR/bootstrap/cache" || true +setfacl -dR -m u:www-data:rwx,u:${APP_USER}:rwx "$APP_DIR/storage" "$APP_DIR/bootstrap/cache" || true \ No newline at end of file diff --git a/scripts/88-update-wrapper.sh b/scripts/88-update-wrapper.sh new file mode 100644 index 0000000..6852861 --- /dev/null +++ b/scripts/88-update-wrapper.sh @@ -0,0 +1,264 @@ +#!/usr/bin/env bash +set -euo pipefail +source ./lib.sh + +log "Update-Wrapper & Sudoers …" + +WRAPPER="/usr/local/sbin/mailwolt-update" +LOGFILE="/var/log/mailwolt-update.log" +STATEDIR="/var/lib/mailwolt/update" +SUDOERS="/etc/sudoers.d/mailwolt-update" +VERSION_FILE="/var/lib/mailwolt/version" +SUDOERS_SERVICES="/etc/sudoers.d/mailwolt-services" +SUDOERS_ARTISAN="/etc/sudoers.d/mailwolt-artisan" + +# Kandidaten: wo liegt update.sh? +CANDIDATES=( + /opt/mailwolt-installer/scripts/update.sh + /mailwolt-installer/scripts/update.sh + /usr/local/lib/mailwolt/update.sh +) + +# State/Log vorbereiten +install -d -m 0755 "$(dirname "$LOGFILE")" +install -d -m 0755 "$STATEDIR" +: > "$LOGFILE" || true +chmod 0644 "$LOGFILE" + +# Wrapper erzeugen +cat > "$WRAPPER" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +LOG="/var/log/mailwolt-update.log" +STATE_DIR="/var/lib/mailwolt/update" +APP_DIR="/var/www/mailwolt" +WEB_USER="www-data" + +CANDIDATES=( + /opt/mailwolt-installer/scripts/update.sh + /mailwolt-installer/scripts/update.sh + /usr/local/lib/mailwolt/update.sh +) + +install -d -m 0755 "$(dirname "$LOG")" "$STATE_DIR" /var/lib/mailwolt +: > "$LOG" || true +chmod 0644 "$LOG" + +echo "running" > "$STATE_DIR/state" + +{ + echo "===== $(date -Is) :: Update gestartet =====" + + # --- Update-Script finden -------------------------------------------------- + SCRIPT="" + for p in "${CANDIDATES[@]}"; do + if [[ -x "$p" ]]; then SCRIPT="$p"; break; fi + if [[ -f "$p" && -r "$p" ]]; then SCRIPT="$p"; break; fi + done + + if [[ -z "$SCRIPT" ]]; then + echo "[!] update.sh nicht gefunden (versucht: ${CANDIDATES[*]})" + rc=127 + else + echo "[i] benutze: $SCRIPT" + if [[ "$(id -u)" -ne 0 ]]; then + echo "[!] Bitte als root ausführen" + rc=1 + else + if [[ -x "$SCRIPT" ]]; then + ALLOW_DIRTY=1 "$SCRIPT" + else + ALLOW_DIRTY=1 bash "$SCRIPT" + fi + rc=$? + fi + fi + + echo "===== $(date -Is) :: Update-Script beendet (rc=$rc) =====" + + # --- Nach dem Update: Assets neu bauen & Laravel optimieren --------------- + if [ -d "$APP_DIR" ]; then + cd "$APP_DIR" || exit 1 + + echo "[i] Führe Composer aus (falls vorhanden) ..." + if [ -f composer.json ]; then + sudo -u "$WEB_USER" composer install --no-dev --prefer-dist --no-interaction -q || true + fi + + echo "[i] Baue Frontend-Assets neu ..." + if command -v npm >/dev/null 2>&1 && [ -f package.json ]; then + sudo -u "$WEB_USER" npm ci --silent || true + sudo -u "$WEB_USER" npm run build --silent || true + fi + + echo "[i] Führe Migrationen & Cache-Optimierungen durch ..." + sudo -u "$WEB_USER" php artisan migrate --force || true + sudo -u "$WEB_USER" php artisan config:cache || true + sudo -u "$WEB_USER" php artisan optimize:clear || true + sudo -u "$WEB_USER" php artisan route:cache || true + sudo -u "$WEB_USER" php artisan view:cache || true + + echo "[i] Hebe Wartungsmodus auf ..." + sudo -u "$WEB_USER" php artisan up >/dev/null 2>&1 || true + fi + + # --- Version aktualisieren ------------------------------------------------- + echo "[i] Aktualisiere Version ..." + if command -v git >/dev/null 2>&1; then + SRC="/var/www/mailwolt" + if [ ! -d "$SRC/.git" ]; then + SRC="/opt/mailwolt-installer" + fi + + git config --global --add safe.directory "$SRC" || true + + if [ -f "$SRC/.git/shallow" ]; then + git -C "$SRC" fetch --unshallow --quiet || true + fi + git -C "$SRC" fetch --tags --quiet origin || true + + raw="$(git -C "$SRC" describe --tags --always --dirty 2>/dev/null || echo "unknown")" + norm="$(printf '%s' "$raw" | sed -E 's/^[vV]//; s/-.*$//')" + + printf '%s\n' "$raw" > /var/lib/mailwolt/version_raw + printf '%s\n' "$norm" > /var/lib/mailwolt/version + chmod 0644 /var/lib/mailwolt/version_raw /var/lib/mailwolt/version + + echo "[i] Version aktualisiert: raw=$raw norm=$norm (Quelle: $SRC)" + else + echo "unknown" > /var/lib/mailwolt/version_raw + echo "0.0.0" > /var/lib/mailwolt/version + chmod 0644 /var/lib/mailwolt/version_raw /var/lib/mailwolt/version + fi + + # --- Services neu starten -------------------------------------------------- + echo "[i] Starte MailWolt-Dienste neu ..." + sudo -u "$WEB_USER" php artisan mailwolt:restart-services || true + + # --- Abschluss ------------------------------------------------------------- + printf '%s\n' "$rc" > "$STATE_DIR/rc" + echo "done" > "$STATE_DIR/state" + echo "===== $(date -Is) :: Update beendet =====" + exit "$rc" + +} | tee -a "$LOG" +EOF + +chmod 0755 "$WRAPPER" +chown root:root "$WRAPPER" + +# Sudoers: www-data (Laravel) & mailwolt dürfen den Wrapper laufen lassen +cat > "$SUDOERS" <<'EOF' +Defaults!/usr/local/sbin/mailwolt-update !requiretty +www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-update +mailwolt ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-update +EOF + +chown root:root "$SUDOERS" +chmod 440 "$SUDOERS" + +if ! visudo -c -f "$SUDOERS" >/dev/null 2>&1; then + echo "[!] Ungültiger sudoers-Eintrag in $SUDOERS – entferne Datei." + rm -f "$SUDOERS" +fi + +cat > "$SUDOERS_SERVICES" <<'EOF' +Defaults!/usr/bin/systemctl !requiretty + +Cmnd_Alias MW_SERVICES = \ + /usr/bin/systemctl reload nginx.service, \ + /usr/bin/systemctl try-reload-or-restart nginx.service, \ + /usr/bin/systemctl try-reload-or-restart postfix.service, \ + /usr/bin/systemctl try-reload-or-restart dovecot.service, \ + /usr/bin/systemctl try-reload-or-restart rspamd.service, \ + /usr/bin/systemctl try-reload-or-restart opendkim.service, \ + /usr/bin/systemctl try-reload-or-restart opendmarc.service, \ + /usr/bin/systemctl try-reload-or-restart clamav-daemon.service, \ + /usr/bin/systemctl try-reload-or-restart redis-server.service + +www-data ALL=(root) NOPASSWD: MW_SERVICES +EOF + +chmod 440 "$SUDOERS_SERVICES" +chown root:root "$SUDOERS_SERVICES" + +# Prüfen, ob Syntax gültig ist +if ! visudo -c -f "$SUDOERS_SERVICES" >/dev/null 2>&1; then + echo "[!] Ungültiger sudoers-Eintrag in $SUDOERS_SERVICES – entferne Datei." + rm -f "$SUDOERS_SERVICES" +else + echo "[✓] Sudoers für Dienststeuerung angelegt: $SUDOERS_SERVICES" +fi + +# Version-File initial anlegen, falls nicht existiert +if [[ ! -f "$VERSION_FILE" ]]; then + echo "unknown" > "$VERSION_FILE" + chmod 0644 "$VERSION_FILE" +fi + +cat > "$SUDOERS_ARTISAN" <<'EOF' +# mailwolt darf artisan im App-Verzeichnis als www-data ausführen (ohne Passwort) +mailwolt ALL=(www-data) NOPASSWD: /usr/bin/php /var/www/mailwolt/artisan * +EOF + +chown root:root "$SUDOERS_ARTISAN" +chmod 440 "$SUDOERS_ARTISAN" + +if ! visudo -c -f "$SUDOERS_ARTISAN" >/dev/null 2>&1; then + echo "[!] Ungültiger sudoers-Eintrag in $SUDOERS_ARTISAN – entferne Datei." + rm -f "$SUDOERS_ARTISAN" +else + echo "[✓] Sudoers für Artisan-Kommandos angelegt: $SUDOERS_ARTISAN" +fi + +log "[✓] Update-Wrapper bereit: $WRAPPER" +log "[✓] Version wird unter $VERSION_FILE gespeichert" + +# ─── Installer-Wrapper ──────────────────────────────────────────────────────── +INSTALL_WRAPPER="/usr/local/sbin/mailwolt-install" +INSTALL_SUDOERS="/etc/sudoers.d/mailwolt-install" +INSTALL_STATE_DIR="/var/lib/mailwolt/install" +INSTALL_LOG="/var/log/mailwolt-install.log" + +# State/Log vorbereiten +install -d -m 0755 "$INSTALL_STATE_DIR" +: > "$INSTALL_LOG" || true +chmod 0644 "$INSTALL_LOG" + +# Installer-Wrapper aus scripts/install-wrapper.sh kopieren +INSTALL_SRC="" +for candidate in \ + "$(dirname "$0")/install-wrapper.sh" \ + /opt/mailwolt-installer/scripts/install-wrapper.sh \ + /var/www/mailwolt/mailwolt-installer/scripts/install-wrapper.sh; do + [[ -f "$candidate" ]] && INSTALL_SRC="$candidate" && break +done + +if [[ -n "$INSTALL_SRC" ]]; then + cp "$INSTALL_SRC" "$INSTALL_WRAPPER" + chmod 0755 "$INSTALL_WRAPPER" + chown root:root "$INSTALL_WRAPPER" + echo "[✓] Installer-Wrapper angelegt: $INSTALL_WRAPPER" +else + echo "[!] install-wrapper.sh nicht gefunden – Installer-Wrapper wird übersprungen." +fi + +# Sudoers: www-data & mailwolt dürfen den Installer-Wrapper laufen lassen +cat > "$INSTALL_SUDOERS" <<'SUDOEOF' +Defaults!/usr/local/sbin/mailwolt-install !requiretty +www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-install +www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-install * +mailwolt ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-install +mailwolt ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-install * +SUDOEOF + +chown root:root "$INSTALL_SUDOERS" +chmod 440 "$INSTALL_SUDOERS" + +if ! visudo -c -f "$INSTALL_SUDOERS" >/dev/null 2>&1; then + echo "[!] Ungültiger sudoers-Eintrag in $INSTALL_SUDOERS – entferne Datei." + rm -f "$INSTALL_SUDOERS" +else + echo "[✓] Sudoers für Installer angelegt: $INSTALL_SUDOERS" +fi \ No newline at end of file diff --git a/scripts/90-services.sh b/scripts/90-services.sh new file mode 100644 index 0000000..c28b982 --- /dev/null +++ b/scripts/90-services.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash +set -euo pipefail +source ./lib.sh + +log "systemd Units (Reverb / Scheduler / Queue / Mail) …" + +cat > /etc/systemd/system/${APP_USER}-ws.service < /etc/systemd/system/${APP_USER}-schedule.service < /etc/systemd/system/${APP_USER}-queue.service </dev/null 2>&1 || true +fi +systemctl enable --now ${APP_USER}-schedule +systemctl enable --now ${APP_USER}-queue + +# Mail-Dienste starten +systemctl enable --now rspamd opendkim postfix dovecot || true + +# PHP-FPM: Unit erkennen, enable + (re)load +enable_and_touch_php_fpm() { + for u in php8.3-fpm php8.2-fpm php8.1-fpm php8.0-fpm php7.4-fpm php-fpm; do + if systemctl list-unit-files | grep -q "^${u}\.service"; then + systemctl enable --now "$u" || true + systemctl reload "$u" || systemctl restart "$u" || true + echo "[i] PHP-FPM unit: $u" + return 0 + fi + done + echo "[!] Keine passende php-fpm Unit gefunden." +} +enable_and_touch_php_fpm + +# Falls in 80-app.sh DKIM installiert wurde: jetzt einmal reloaden +if [[ -e /run/mailwolt.need-opendkim-reload ]]; then + systemctl reload opendkim || true + rm -f /run/mailwolt.need-opendkim-reload || true +fi + +# Falls Zert-Fix markiert ist: Dovecot neu laden +if [[ -e /run/mailwolt.need-dovecot-reload ]]; then + systemctl reload dovecot || true + rm -f /run/mailwolt.need-dovecot-reload || true +fi + +# Falls DB-Migration schon durch: einmal reload +db_ready(){ mysql -u"${DB_USER}" -p"${DB_PASS}" -h 127.0.0.1 -D "${DB_NAME}" -e "SHOW TABLES LIKE 'migrations'\G" >/dev/null 2>&1; } +if db_ready; then + systemctl reload postfix || true +fi + +# Mini-Portcheck (hilft beim Installer-Output) +echo "Listening (25/465/587):" +ss -ltnp | awk '$4 ~ /:(25|465|587)$/ {print " " $0}' \ No newline at end of file diff --git a/scripts/92-sudoers-npm.sh b/scripts/92-sudoers-npm.sh new file mode 100644 index 0000000..6f07a93 --- /dev/null +++ b/scripts/92-sudoers-npm.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -euo pipefail +source ./lib.sh + +log "Sudoers: npm-Build ohne Passwort für user 'mailwolt' …" + +# 1) npm-Binary ermitteln (normal: /usr/bin/npm) +NPM_BIN="$(command -v npm || true)" + +if [[ -z "$NPM_BIN" ]]; then + warn "npm wurde nicht gefunden – sudoers wird vorbereitet, aber ohne Validierung. Stelle sicher, dass Node/npm installiert ist." + # Fallback – die meisten Distros legen hier an + NPM_BIN="/usr/bin/npm" +fi + +SUDOERS_FILE="/etc/sudoers.d/mailwolt-npm" + +# 2) Sudoers-Datei schreiben +cat > "$SUDOERS_FILE" </dev/null 2>&1; then + log "[✓] sudoers OK: ${SUDOERS_FILE} erlaubt 'mailwolt' → ${NPM_BIN} ohne Passwort." +else + echo "[!] Ungültiger sudoers-Eintrag in ${SUDOERS_FILE} – entferne Datei." + rm -f "$SUDOERS_FILE" +fi \ No newline at end of file diff --git a/scripts/93-backup-tools.sh b/scripts/93-backup-tools.sh new file mode 100644 index 0000000..ebfd7c4 --- /dev/null +++ b/scripts/93-backup-tools.sh @@ -0,0 +1,284 @@ +#!/usr/bin/env bash +set -euo pipefail +source ./lib.sh + +log "Backup/Restore – Tools, Config & Timer (installer.env) …" + +# ───────────────────────────────────────────────────────────── +# 1) installer.env laden (ENV > installer.env > Defaults) +# ───────────────────────────────────────────────────────────── +if [[ -f /etc/mailwolt/installer.env ]]; then + # automatisch exportieren, damit ${VAR} später überall wirkt + set -a + # shellcheck disable=SC1091 + source /etc/mailwolt/installer.env + set +a +else + log "[i] /etc/mailwolt/installer.env nicht gefunden – nutze Defaults." +fi + +# ───────────────────────────────────────────────────────────── +# 2) Pfade & Defaults (werden durch ENV/installer.env überschrieben) +# ───────────────────────────────────────────────────────────── +CONF_DIR="/etc/mailwolt" +CONF_FILE="${CONF_DIR}/backup.conf" +BIN_DIR="/usr/local/sbin" +UNIT_DIR="/etc/systemd/system" + +APP_DIR="${APP_DIR:-/var/www/mailwolt}" + +# DB-Parameter aus installer.env (bzw. ENV) oder Fallbacks +DB_HOST="${DB_HOST:-127.0.0.1}" +DB_NAME="${DB_NAME:-mailwolt}" +DB_USER="${DB_USER:-mailwolt}" +DB_PASS="${DB_PASS:-}" + +# Backup-Settings aus installer.env (bzw. ENV) +BACKUP_DIR="${BACKUP_DIR:-/var/backups/mailwolt}" +BACKUP_RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-7}" +BACKUP_USE_ZSTD="${BACKUP_USE_ZSTD:-1}" +BACKUP_ENABLED="${BACKUP_ENABLED:-0}" # 0|1 +BACKUP_INTERVAL="${BACKUP_INTERVAL:-daily}" # daily|weekly|monthly + +install -d -m 0755 "$CONF_DIR" "$BACKUP_DIR" + + +SUDOERS_BACKUP_FILE="/etc/sudoers.d/mailwolt-backup" +# 2) Sudoers-Datei schreiben +cat > "${SUDOERS_BACKUP_FILE} " </dev/null 2>&1; then + echo "[!] Ungültiger sudoers-Eintrag in ${SUDOERS_BACKUP_FILE} – entferne Datei." + rm -f "${SUDOERS_BACKUP_FILE}" +fi + +# ───────────────────────────────────────────────────────────── +# 3) /etc/mailwolt/backup.conf (von UI/APP überschreibbar) +# ───────────────────────────────────────────────────────────── +cat > "$CONF_FILE" < "${BIN_DIR}/mailwolt-backup" <<'EOSH' +#!/usr/bin/env bash +set -euo pipefail + +log(){ echo "[$(date -Is)] $*"; } + +# Konfiguration laden (ENV > Datei) +CONF="/etc/mailwolt/backup.conf" +[[ -f "$CONF" ]] && # shellcheck disable=SC1090 +source "$CONF" + +APP_DIR="${APP_DIR:-/var/www/mailwolt}" +BACKUP_DIR="${BACKUP_DIR:-/var/backups/mailwolt}" +RETENTION_DAYS="${RETENTION_DAYS:-7}" +USE_ZSTD="${USE_ZSTD:-1}" + +MYSQL_DB="${MYSQL_DB:-mailwolt}" +MYSQL_USER="${MYSQL_USER:-mailwolt}" +MYSQL_PASS="${MYSQL_PASS:-}" +MYSQL_HOST="${MYSQL_HOST:-127.0.0.1}" +MYSQL_PORT="${MYSQL_PORT:-3306}" + +STATE_DIR="/var/lib/mailwolt" +STATUS_FILE="${STATE_DIR}/backup.status" +install -d -m 0755 "$STATE_DIR" "$BACKUP_DIR" + +START_TS="$(date +%s)" +TS="$(date -u +%Y%m%dT%H%M%SZ)" +TMP="$(mktemp -d /tmp/mwbackup.XXXXXX)" +trap 'rm -rf "$TMP"' EXIT + +fail(){ + local msg="${1:-backup failed}" + local now="$(date -Is)" + { + echo "time=${now}" + echo "size=0" + echo "dur=$(( $(date +%s) - START_TS ))s" + echo "ok=0" + echo "error=${msg}" + } > "$STATUS_FILE" + echo "[$now] ${msg}" >&2 + exit 1 +} +trap 'fail "unexpected error (exit $?)"' ERR + +OUT="${BACKUP_DIR}/mailwolt-${TS}.tar" +log "⇒ starte Backup in $OUT …" + +# 1) DB +log " • mysqldump …" +MYSQL_PWD="$MYSQL_PASS" mysqldump \ + -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" \ + --single-transaction --routines --events --triggers \ + "$MYSQL_DB" > "$TMP/mysql.sql" + +# 2) Maildir +log " • Maildir …" +tar -C / -cf "$TMP/mail.tar" var/mail/vhosts 2>/dev/null || true + +# 3) App (ohne heavy dirs) +log " • App …" +tar -C / -cf "$TMP/app.tar" \ + --exclude='var/www/mailwolt/vendor' \ + --exclude='var/www/mailwolt/node_modules' \ + --exclude='var/www/mailwolt/public/build' \ + var/www/mailwolt + +# 4) Configs +log " • Configs …" +mkdir -p "$TMP/files" +cp -a /etc/mailwolt "$TMP/files/" 2>/dev/null || true +cp -a /etc/postfix "$TMP/files/" 2>/dev/null || true +cp -a /etc/dovecot "$TMP/files/" 2>/dev/null || true +cp -a /etc/opendkim "$TMP/files/" 2>/dev/null || true +cp -a /etc/opendmarc "$TMP/files/" 2>/dev/null || true +cp -a /etc/rspamd "$TMP/files/" 2>/dev/null || true +cp -a /etc/ssl/ui "$TMP/files/" 2>/dev/null || true +tar -C "$TMP" -cf "$TMP/files.tar" files + +# 5) Paket +log " • Archiviere …" +tar -C "$TMP" -cf "$OUT" mysql.sql mail.tar app.tar files.tar + +# 6) Komprimieren (optional) +if [[ "${USE_ZSTD:-1}" = "1" ]] && command -v zstd >/dev/null 2>&1; then + log " • komprimiere (zstd) …" + zstd -f --rm -19 "$OUT" + OUT="${OUT}.zst" +fi + +# 7) Retention +if [[ "$RETENTION_DAYS" =~ ^[0-9]+$ ]]; then + log " • Retention: lösche älter als ${RETENTION_DAYS} Tage …" + find "$BACKUP_DIR" -type f -mtime +"$RETENTION_DAYS" -name 'mailwolt-*' -delete || true +fi + +# 8) Statusfile fürs UI +SIZE_BYTES="$(stat -c '%s' "$OUT" 2>/dev/null || echo 0)" +{ + echo "time=$(date -Is)" + echo "size=${SIZE_BYTES}" + echo "dur=$(( $(date +%s) - START_TS ))s" + echo "ok=1" + echo "file=${OUT}" +} > "$STATUS_FILE" +chmod 0644 "$STATUS_FILE" 2>/dev/null || true + +log "[✓] Backup fertig: $OUT" +EOSH +chmod 0755 "${BIN_DIR}/mailwolt-backup" + +# ───────────────────────────────────────────────────────────── +# 5) /usr/local/sbin/mailwolt-restore +# ───────────────────────────────────────────────────────────── +cat > "${BIN_DIR}/mailwolt-restore" <<'EOSH' +#!/usr/bin/env bash +set -euo pipefail +log(){ echo "[$(date -Is)] $*"; } + +ARCHIVE="${1:-}" +[[ -n "$ARCHIVE" ]] || { echo "Usage: mailwolt-restore "; exit 1; } +[[ -f "$ARCHIVE" ]] || { echo "Backup nicht gefunden: $ARCHIVE"; exit 1; } + +TMP="$(mktemp -d /tmp/mwrestore.XXXXXX)" +trap 'rm -rf "$TMP"' EXIT + +case "$ARCHIVE" in + *.zst) zstd -d -c "$ARCHIVE" > "$TMP/backup.tar" ;; + *) cp -a "$ARCHIVE" "$TMP/backup.tar" ;; +esac + +log "⇒ entpacke …" +tar -C "$TMP" -xf "$TMP/backup.tar" + +# Reihenfolge: DB → App → Mail → Config +if [[ -f "$TMP/mysql.sql" ]]; then + log " • MySQL wiederherstellen …" + mysql < "$TMP/mysql.sql" +fi + +if [[ -f "$TMP/app.tar" ]]; then + log " • App → /var/www/mailwolt …" + tar -C / -xf "$TMP/app.tar" +fi + +if [[ -f "$TMP/mail.tar" ]]; then + log " • Maildir → /var/mail/vhosts …" + tar -C / -xf "$TMP/mail.tar" +fi + +if [[ -f "$TMP/files.tar" ]]; then + log " • Configs → /etc/* …" + tar -C / -xf "$TMP/files.tar" +fi + +log "[✓] Restore abgeschlossen." +EOSH +chmod 0755 "${BIN_DIR}/mailwolt-restore" + +log "[✓] Tools installiert: ${BIN_DIR}/mailwolt-backup, mailwolt-restore" + +# ───────────────────────────────────────────────────────────── +# 6) systemd Service + Timer (Timer default via installer.env) +# ───────────────────────────────────────────────────────────── +cat > "${UNIT_DIR}/mailwolt-backup.service" <<'EOSVC' +[Unit] +Description=MailWolt Backup + +[Service] +Type=oneshot +ExecStart=/usr/local/sbin/mailwolt-backup +Nice=10 +IOSchedulingClass=best-effort +IOSchedulingPriority=7 +EOSVC + +cat > "${UNIT_DIR}/mailwolt-backup.timer" </dev/null 2>&1 || true +fi + +log "[✓] Backup-Setup abgeschlossen." \ No newline at end of file diff --git a/scripts/95-monit.sh b/scripts/95-monit.sh new file mode 100644 index 0000000..6dca86e --- /dev/null +++ b/scripts/95-monit.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -euo pipefail +source ./lib.sh + +log "Monit konfigurieren …" +cat > /etc/monit/monitrc <<'EOF' +set daemon 60 +set logfile syslog facility log_daemon + +check process postfix with pidfile /var/spool/postfix/pid/master.pid + start program = "/bin/systemctl start postfix" + stop program = "/bin/systemctl stop postfix" + if failed port 25 protocol smtp then restart + if failed port 465 type tcp ssl then restart + if failed port 587 type tcp then restart + +check process dovecot with pidfile /run/dovecot/master.pid + start program = "/bin/systemctl start dovecot" + stop program = "/bin/systemctl stop dovecot" + if failed port 143 type tcp then restart + if failed port 993 type tcp ssl then restart + +check process mariadb with pidfile /run/mysqld/mysqld.pid + start program = "/bin/systemctl start mariadb" + stop program = "/bin/systemctl stop mariadb" + if failed port 3306 type tcp then restart + +check process redis-server with pidfile /run/redis/redis-server.pid + start program = "/bin/systemctl start redis-server" + stop program = "/bin/systemctl stop redis-server" + if failed port 6379 type tcp then restart + +check process nginx with pidfile /run/nginx.pid + start program = "/bin/systemctl start nginx" + stop program = "/bin/systemctl stop nginx" + if failed port 80 type tcp then restart + if failed port 443 type tcp ssl then restart +EOF +chmod 600 /etc/monit/monitrc +monit -t && systemctl enable --now monit +monit reload || true + +log "[✓] Monit konfiguriert und gestartet" + +# ── mailwolt-update ins System kopieren ───────────────────────────── +install -m 0750 -o root -g root scripts/update.sh /usr/local/sbin/mailwolt-update +log "[✓] mailwolt-update installiert → ausführbar via 'sudo mailwolt-update'" \ No newline at end of file diff --git a/scripts/95-woltguard.bak.sh b/scripts/95-woltguard.bak.sh new file mode 100644 index 0000000..d727918 --- /dev/null +++ b/scripts/95-woltguard.bak.sh @@ -0,0 +1,491 @@ +#!/usr/bin/env bash +set -euo pipefail +source ./lib.sh + +log "WoltGuard (Monit + Self-Heal) einrichten …" + +# ───────────────────────────────────────────────────────────── +# Env nur nachladen, wenn Flags nicht bereits exportiert sind +# ───────────────────────────────────────────────────────────── +INSTALLER_ENV="/etc/mailwolt/installer.env" +: "${CLAMAV_ENABLE:=}" ; : "${OPENDMARC_ENABLE:=}" ; : "${FAIL2BAN_ENABLE:=}" +if [[ -z "${CLAMAV_ENABLE}${OPENDMARC_ENABLE}${FAIL2BAN_ENABLE}" && -r "$INSTALLER_ENV" ]]; then + # shellcheck disable=SC1090 + . "$INSTALLER_ENV" +fi +CLAMAV_ENABLE="${CLAMAV_ENABLE:-0}" +OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-0}" +FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}" + +# ───────────────────────────────────────────────────────────── +# Monit installieren & aktivieren +# ───────────────────────────────────────────────────────────── +command -v monit >/dev/null || { apt-get update -qq; apt-get install -y monit; } +systemctl enable --now monit + +# ───────────────────────────────────────────────────────────── +# Helper-Skripte (laufen später eigenständig → Env selbst laden) +# ───────────────────────────────────────────────────────────── +install -d -m 0755 /usr/local/sbin + +# Redis-Ping (nimmt REDIS_PASSWORD aus installer.env oder .env) +cat >/usr/local/sbin/mailwolt-redis-ping.sh <<'EOSH' +#!/usr/bin/env bash +set -euo pipefail + +INSTALLER_ENV="/etc/mailwolt/installer.env" +APP_ENV="/var/www/mailwolt/.env" + +# Defaults +REDIS_HOST="${REDIS_HOST:-127.0.0.1}" +REDIS_PORT="${REDIS_PORT:-6379}" +REDIS_PASSWORD="${REDIS_PASSWORD:-}" +REDIS_PASS="${REDIS_PASS:-}" # Legacy + +# Installer-Env (falls vorhanden) +[[ -r "$INSTALLER_ENV" ]] && . "$INSTALLER_ENV" || true + +# Falls .env existiert: Werte ergänzen, die noch leer sind +if [[ -r "$APP_ENV" ]]; then + [[ -z "${REDIS_HOST}" ]] && REDIS_HOST="$(grep -m1 -E '^REDIS_HOST=' "$APP_ENV" | cut -d= -f2- || true)" + [[ -z "${REDIS_PORT}" ]] && REDIS_PORT="$(grep -m1 -E '^REDIS_PORT=' "$APP_ENV" | cut -d= -f2- || true)" + [[ -z "${REDIS_PASSWORD}" ]] && REDIS_PASSWORD="$(grep -m1 -E '^REDIS_PASSWORD=' "$APP_ENV" | cut -d= -f2- || true)" +fi + +# Legacy-Fallback: wenn PASSWORD leer, aber PASS gesetzt → übernehmen +[[ -z "${REDIS_PASSWORD}" && -n "${REDIS_PASS}" ]] && REDIS_PASSWORD="$REDIS_PASS" + +# Quotes strippen +strip(){ printf '%s' "$1" | sed -E 's/^"(.*)"$/\1/; s/^'\''(.*)'\''$/\1/'; } +REDIS_HOST="$(strip "${REDIS_HOST:-}")" +REDIS_PORT="$(strip "${REDIS_PORT:-}")" +REDIS_PASSWORD="$(strip "${REDIS_PASSWORD:-}")" + +# redis-cli muss vorhanden sein +command -v redis-cli >/dev/null 2>&1 || exit 1 + +BASE=(timeout 2 redis-cli --no-auth-warning --raw -h "$REDIS_HOST" -p "$REDIS_PORT") +if [[ -n "$REDIS_PASSWORD" ]]; then + CMD=("${BASE[@]}" -a "$REDIS_PASSWORD" ping) +else + CMD=("${BASE[@]}" ping) +fi + +# Erfolgreich nur bei exakt "PONG" +[[ "$("${CMD[@]}" 2>/dev/null || true)" == "PONG" ]] +EOSH +chmod 0755 /usr/local/sbin/mailwolt-redis-ping.sh + +# Rspamd-Heal (setzt Laufzeitverzeichnis, leert alte Socke, restarts rspamd) +cat >/usr/local/sbin/mailwolt-rspamd-heal.sh <<'EOSH' +#!/usr/bin/env bash +set -euo pipefail + +INSTALLER_ENV="/etc/mailwolt/installer.env" +APP_ENV="/var/www/mailwolt/.env" + +REDIS_HOST="${REDIS_HOST:-127.0.0.1}" +REDIS_PORT="${REDIS_PORT:-6379}" +REDIS_PASSWORD="${REDIS_PASSWORD:-}" + +[[ -r "$INSTALLER_ENV" ]] && . "$INSTALLER_ENV" +if [[ -z "${REDIS_PASSWORD}" && -r "$APP_ENV" ]]; then + REDIS_PASSWORD="$(grep -E '^REDIS_PASSWORD=' "$APP_ENV" | head -n1 | cut -d= -f2- || true)" +fi + +# Rspamd Runtime fixen +install -d -m 0755 -o _rspamd -g _rspamd /run/rspamd || true +[[ -S /var/lib/rspamd/rspamd.sock ]] && rm -f /var/lib/rspamd/rspamd.sock || true + +# Neustart +systemctl restart rspamd + +# Mini-Healthcheck +sleep 2 +ss -tln | grep -q ':11334' || echo "[WARN] Rspamd Controller Port 11334 nicht sichtbar" + +exit 0 +EOSH +chmod 0755 /usr/local/sbin/mailwolt-rspamd-heal.sh + +# ───────────────────────────────────────────────────────────── +# WoltGuard Wrapper + Unit +# ───────────────────────────────────────────────────────────── +cat >/usr/local/bin/woltguard <<'EOSH' +#!/usr/bin/env bash +set -euo pipefail +case "${1:-status}" in + start) systemctl enable --now monit ;; + stop) systemctl stop monit ;; + status) monit summary || systemctl status monit || true ;; + heal) monit reload || true; sleep 1; monit restart all || true ;; + monitor) monit monitor all || true ;; + unmonitor) monit unmonitor all || true ;; + *) echo "Usage: woltguard {start|stop|status|heal|monitor|unmonitor}"; exit 2;; +esac +EOSH +chmod 0755 /usr/local/bin/woltguard + +cat >/etc/systemd/system/woltguard.service <<'EOF' +[Unit] +Description=WoltGuard – Self-Healing Monitor for MailWolt +After=network.target +[Service] +Type=oneshot +ExecStart=/usr/local/bin/woltguard start +ExecStop=/usr/local/bin/woltguard stop +RemainAfterExit=yes +[Install] +WantedBy=multi-user.target +EOF +systemctl daemon-reload +systemctl enable --now woltguard + +# ───────────────────────────────────────────────────────────── +# Monit Basis + includes +# ───────────────────────────────────────────────────────────── +sed -i 's/^set daemon .*/set daemon 30/' /etc/monit/monitrc || true +grep -q 'include /etc/monit/conf.d/*' /etc/monit/monitrc || echo 'include /etc/monit/conf.d/*' >>/etc/monit/monitrc +install -d -m 0755 /etc/monit/conf.d + +# ───────────────────────────────────────────────────────────── +# Monit Checks +# ───────────────────────────────────────────────────────────── +# 10 – Redis zuerst (abhängig für rspamd) +cat >/etc/monit/conf.d/10-redis.conf <<'EOF' +check process redis with pidfile /run/redis/redis-server.pid + start program = "/bin/systemctl start redis-server" + stop program = "/bin/systemctl stop redis-server" + if failed host 127.0.0.1 port 6379 for 2 cycles then restart + if 5 restarts within 5 cycles then alert + +check program redis_ping path "/usr/local/sbin/mailwolt-redis-ping.sh" + if status != 0 for 2 cycles then exec "/bin/systemctl restart redis-server" +EOF + +# 20 – Rspamd (hängt von Redis ab), robust über process-matching +cat >/etc/monit/conf.d/20-rspamd.conf <<'EOF' +check process rspamd matching "/usr/bin/rspamd" + start program = "/bin/systemctl start rspamd" + stop program = "/bin/systemctl stop rspamd" + depends on redis + if failed host 127.0.0.1 port 11333 for 2 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh" + if failed host 127.0.0.1 port 11334 for 2 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh" + if 5 restarts within 5 cycles then alert +EOF + +# 30 – Maildienste +cat >/etc/monit/conf.d/30-postfix.conf <<'EOF' +check process postfix with pidfile /var/spool/postfix/pid/master.pid + start program = "/bin/systemctl start postfix" + stop program = "/bin/systemctl stop postfix" + if failed port 25 protocol smtp then restart + if failed port 465 type tcpssl then restart + if failed port 587 type tcp then restart + if 5 restarts within 5 cycles then alert +EOF + +cat >/etc/monit/conf.d/30-dovecot.conf <<'EOF' +check process dovecot with pidfile /run/dovecot/master.pid + start program = "/bin/systemctl start dovecot" + stop program = "/bin/systemctl stop dovecot" + if failed port 993 type tcpssl for 2 cycles then restart + if failed port 24 protocol lmtp for 2 cycles then restart + if 5 restarts within 5 cycles then alert +EOF + +# 40 – Web/PHP +cat >/etc/monit/conf.d/40-nginx.conf <<'EOF' +check process nginx with pidfile /run/nginx.pid + start program = "/bin/systemctl start nginx" + stop program = "/bin/systemctl stop nginx" + if failed port 80 type tcp then restart + if failed port 443 type tcpssl then restart + if 5 restarts within 5 cycles then alert +EOF + +# 50 – DKIM/DMARC +cat >/etc/monit/conf.d/50-opendkim.conf <<'EOF' +check process opendkim with pidfile /run/opendkim/opendkim.pid + start program = "/bin/systemctl start opendkim" + stop program = "/bin/systemctl stop opendkim" + if failed host 127.0.0.1 port 8891 type tcp for 2 cycles then restart + if 5 restarts within 5 cycles then alert +EOF + +# optional: OpenDMARC +if [[ "$OPENDMARC_ENABLE" = "1" ]]; then + cat >/etc/monit/conf.d/55-opendmarc.conf <<'EOF' +check process opendmarc with pidfile /run/opendmarc/opendmarc.pid + start program = "/bin/systemctl start opendmarc" + stop program = "/bin/systemctl stop opendmarc" + if 5 restarts within 5 cycles then alert +EOF +else + rm -f /etc/monit/conf.d/55-opendmarc.conf || true +fi + +# 60 – optional: ClamAV +if [[ "$CLAMAV_ENABLE" = "1" ]]; then + cat >/etc/monit/conf.d/60-clamav.conf <<'EOF' +check process clamd with pidfile /run/clamav/clamd.pid + start program = "/bin/systemctl start clamav-daemon" + stop program = "/bin/systemctl stop clamav-daemon" + if failed unixsocket /run/clamav/clamd.ctl for 3 cycles then restart + if 5 restarts within 5 cycles then timeout +EOF +else + rm -f /etc/monit/conf.d/60-clamav.conf || true +fi + +# 70 – Fail2Ban (optional, standardmäßig aktiv) +if [[ "$FAIL2BAN_ENABLE" = "1" ]]; then + cat >/etc/monit/conf.d/70-fail2ban.conf <<'EOF' +check process fail2ban with pidfile /run/fail2ban/fail2ban.pid + start program = "/bin/systemctl start fail2ban" + stop program = "/bin/systemctl stop fail2ban" + if 5 restarts within 5 cycles then alert +EOF +else + rm -f /etc/monit/conf.d/70-fail2ban.conf || true +fi +# ───────────────────────────────────────────────────────────── +# Monit neu laden +# ───────────────────────────────────────────────────────────── +monit -t +systemctl reload monit || systemctl restart monit +systemctl status monit --no-pager || true +log "[✓] WoltGuard aktiv." + +##!/usr/bin/env bash +#set -euo pipefail +#source ./lib.sh +# +#log "WoltGuard (Monit + Self-Heal) einrichten …" +# +#set +u +#[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env +#set -u +#CLAMAV_ENABLE="${CLAMAV_ENABLE:-0}" +#OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-0}" +#FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}" +# +## Pakete sicherstellen +#command -v monit >/dev/null || { apt-get update -qq; apt-get install -y monit; } +#systemctl enable --now monit +# +## Helper-Skripte +#install -d -m 0755 /usr/local/sbin +#cat >/usr/local/sbin/mailwolt-redis-ping.sh <<'EOSH' +##!/usr/bin/env bash +#set -euo pipefail +#PASS="" +#[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env || true +#if command -v redis-cli >/dev/null 2>&1; then +# [[ -n "${REDIS_PASS:-}" ]] \ +# && redis-cli -h 127.0.0.1 -p 6379 -a "$REDIS_PASS" ping | grep -q PONG \ +# || redis-cli -h 127.0.0.1 -p 6379 ping | grep -q PONG +#else +# exit 1 +#fi +#EOSH +#chmod 0755 /usr/local/sbin/mailwolt-redis-ping.sh +# +#cat >/usr/local/sbin/mailwolt-rspamd-heal.sh <<'EOSH' +##!/usr/bin/env bash +#set -euo pipefail +# +#REDIS_HOST="${REDIS_HOST:-127.0.0.1}" +#REDIS_PORT="${REDIS_PORT:-6379}" +#REDIS_PASSWORD="${REDIS_PASSWORD:-}" +# +#INSTALLER_ENV="/etc/mailwolt/installer.env" +#APP_ENV="/var/www/mailwolt/.env" +#REDIS_CLI="$(command -v redis-cli || true)" +#SYSTEMCTL="$(command -v systemctl || true)" +#RSPAMD_SERVICE="rspamd" +# +#if [ -r "$INSTALLER_ENV" ]; then . "$INSTALLER_ENV"; fi +#if [ -z "${REDIS_PASSWORD}" ] && [ -r "$APP_ENV" ]; then +# REDIS_PASSWORD="$(grep -E '^REDIS_PASSWORD=' "$APP_ENV" | head -n1 | cut -d= -f2- || true)" +#fi +# +#if [ -n "$REDIS_CLI" ]; then +# echo "[INFO] Prüfe Redis Verbindung..." +# if [ -n "${REDIS_PASSWORD}" ]; then +# if ! "$REDIS_CLI" -h "$REDIS_HOST" -p "$REDIS_PORT" -a "$REDIS_PASSWORD" ping | grep -q '^PONG$'; then +# echo "[WARN] Redis antwortet nicht oder Passwort falsch!" +# else +# echo "[OK] Redis antwortet (auth ok)." +# fi +# else +# if ! "$REDIS_CLI" -h "$REDIS_HOST" -p "$REDIS_PORT" ping | grep -q '^PONG$'; then +# echo "[WARN] Redis antwortet nicht (ohne Passwort)." +# else +# echo "[OK] Redis antwortet (kein Passwort)." +# fi +# fi +#else +# echo "[WARN] redis-cli nicht gefunden – überspringe Test." +#fi +# +#echo "[INFO] Prüfe Rspamd Socket & Verzeichnis..." +#install -d -m 0755 -o _rspamd -g _rspamd /run/rspamd || true +#[ -S /var/lib/rspamd/rspamd.sock ] && rm -f /var/lib/rspamd/rspamd.sock || true +# +#echo "[INFO] Starte Rspamd neu..." +#if [ -n "$SYSTEMCTL" ]; then +# "$SYSTEMCTL" restart "$RSPAMD_SERVICE" +# echo "[OK] Rspamd erfolgreich neu gestartet." +#else +# echo "[ERROR] systemctl nicht gefunden – kein Neustart möglich." +# exit 1 +#fi +# +#echo "[INFO] Healthcheck (Port 11334)..." +#sleep 3 +#if ss -tln | grep -q ':11334'; then +# echo "[OK] Rspamd Controller läuft auf Port 11334." +#else +# echo "[WARN] Rspamd Controller Port 11334 nicht erreichbar." +#fi +# +#echo "[DONE] Mailwolt Rspamd-Heal abgeschlossen." +#exit 0 +#EOSH +#chmod 0755 /usr/local/sbin/mailwolt-rspamd-heal.sh +# +## WoltGuard Wrapper + Unit +#cat >/usr/local/bin/woltguard <<'EOSH' +##!/usr/bin/env bash +#set -euo pipefail +#case "${1:-status}" in +# start) systemctl enable --now monit ;; +# stop) systemctl stop monit ;; +# status) monit summary || systemctl status monit || true ;; +# heal) monit reload || true; sleep 1; monit restart all || true ;; +# monitor) monit monitor all || true ;; +# unmonitor) monit unmonitor all || true ;; +# *) echo "Usage: woltguard {start|stop|status|heal|monitor|unmonitor}"; exit 2;; +#esac +#EOSH +#chmod 0755 /usr/local/bin/woltguard +# +#cat >/etc/systemd/system/woltguard.service <<'EOF' +#[Unit] +#Description=WoltGuard – Self-Healing Monitor for MailWolt +#After=network.target +#[Service] +#Type=oneshot +#ExecStart=/usr/local/bin/woltguard start +#ExecStop=/usr/local/bin/woltguard stop +#RemainAfterExit=yes +#[Install] +#WantedBy=multi-user.target +#EOF +#systemctl daemon-reload +#systemctl enable --now woltguard +# +## Monit Basis + include +#sed -i 's/^set daemon .*/set daemon 30/' /etc/monit/monitrc || true +#grep -q 'include /etc/monit/conf.d/*' /etc/monit/monitrc || echo 'include /etc/monit/conf.d/*' >>/etc/monit/monitrc +#install -d -m 0755 /etc/monit/conf.d +# +## Checks +#cat >/etc/monit/conf.d/postfix.conf <<'EOF' +#check process postfix with pidfile /var/spool/postfix/pid/master.pid +# start program = "/bin/systemctl start postfix" +# stop program = "/bin/systemctl stop postfix" +# if failed port 25 protocol smtp then restart +# if failed port 465 type tcpssl then restart +# if failed port 587 type tcp then restart +# if 5 restarts within 5 cycles then alert +#EOF +# +#cat >/etc/monit/conf.d/dovecot.conf <<'EOF' +#check process dovecot with pidfile /run/dovecot/master.pid +# start program = "/bin/systemctl start dovecot" +# stop program = "/bin/systemctl stop dovecot" +# if failed port 993 type tcpssl for 2 cycles then restart +# if failed port 24 protocol lmtp for 2 cycles then restart +# if 5 restarts within 5 cycles then alert +#EOF +# +#cat >/etc/monit/conf.d/nginx.conf <<'EOF' +#check process nginx with pidfile /run/nginx.pid +# start program = "/bin/systemctl start nginx" +# stop program = "/bin/systemctl stop nginx" +# if failed port 80 type tcp then restart +# if failed port 443 type tcpssl then restart +# if 5 restarts within 5 cycles then alert +#EOF +# +#cat >/etc/monit/conf.d/redis.conf <<'EOF' +#check process redis with pidfile /run/redis/redis-server.pid +# start program = "/bin/systemctl start redis-server" +# stop program = "/bin/systemctl stop redis-server" +# if failed host 127.0.0.1 port 6379 for 2 cycles then restart +# if 5 restarts within 5 cycles then alert +# +#check program redis_ping path "/usr/local/sbin/mailwolt-redis-ping.sh" +# if status != 0 for 2 cycles then exec "/bin/systemctl restart redis-server" +#EOF +# +#cat >/etc/monit/conf.d/rspamd.conf <<'EOF' +#check process rspamd with pidfile /run/rspamd/rspamd.pid +# start program = "/bin/systemctl start rspamd" +# stop program = "/bin/systemctl stop rspamd" +# if failed port 11333 for 2 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh" +# if failed port 11334 for 2 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh" +# if 5 restarts within 5 cycles then alert +#EOF +# +#cat >/etc/monit/conf.d/opendkim.conf <<'EOF' +#check process opendkim with pidfile /run/opendkim/opendkim.pid +# start program = "/bin/systemctl start opendkim" +# stop program = "/bin/systemctl stop opendkim" +# if failed host 127.0.0.1 port 8891 type tcp for 2 cycles then restart +# if 5 restarts within 5 cycles then alert +#EOF +# +## optional: OpenDMARC +#if [[ "$OPENDMARC_ENABLE" = "1" ]]; then +# cat >/etc/monit/conf.d/opendmarc.conf <<'EOF' +#check process opendmarc with pidfile /run/opendmarc/opendmarc.pid +# start program = "/bin/systemctl start opendmarc" +# stop program = "/bin/systemctl stop opendmarc" +# if 5 restarts within 5 cycles then alert +#EOF +#else +# rm -f /etc/monit/conf.d/opendmarc.conf || true +#fi +# +## optional: ClamAV +#if [[ "$CLAMAV_ENABLE" = "1" ]]; then +# cat >/etc/monit/conf.d/clamav.conf <<'EOF' +#check process clamd with pidfile /run/clamav/clamd.pid +# start program = "/bin/systemctl start clamav-daemon" +# stop program = "/bin/systemctl stop clamav-daemon" +# if failed unixsocket /run/clamav/clamd.ctl then restart +# if 5 restarts within 5 cycles then alert +#EOF +#else +# rm -f /etc/monit/conf.d/clamav.conf || true +#fi +# +## optional: Fail2Ban +#if [[ "$FAIL2BAN_ENABLE" = "1" ]]; then +# cat >/etc/monit/conf.d/fail2ban.conf <<'EOF' +#check process fail2ban with pidfile /run/fail2ban/fail2ban.pid +# start program = "/bin/systemctl start fail2ban" +# stop program = "/bin/systemctl stop fail2ban" +# if 5 restarts within 5 cycles then alert +#EOF +#else +# rm -f /etc/monit/conf.d/fail2ban.conf || true +#fi +# +#monit -t +#systemctl reload monit || systemctl restart monit +#systemctl status monit --no-pager || true +#log "[✓] WoltGuard aktiv." \ No newline at end of file diff --git a/scripts/95-woltguard.sh b/scripts/95-woltguard.sh new file mode 100644 index 0000000..b3f31f4 --- /dev/null +++ b/scripts/95-woltguard.sh @@ -0,0 +1,439 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Flags laden (falls vorhanden) +INSTALLER_ENV="/etc/mailwolt/installer.env" +: "${CLAMAV_ENABLE:=}"; : "${OPENDMARC_ENABLE:=}"; : "${FAIL2BAN_ENABLE:=}"; : "${MONIT_HTTP:=}" +if [[ -z "${CLAMAV_ENABLE}${OPENDMARC_ENABLE}${FAIL2BAN_ENABLE}" && -r "$INSTALLER_ENV" ]]; then + . "$INSTALLER_ENV" +fi +CLAMAV_ENABLE="${CLAMAV_ENABLE:-1}" +OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-1}" +FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}" +MONIT_HTTP="${MONIT_HTTP:-1}" + +# ── Monit so konfigurieren, dass NUR monitrc.d/* geladen wird ──────────────── +install -d -m 0755 /etc/monit/monitrc.d +install -d -m 0755 /etc/monit/conf.d # passiver Ablageort (NICHT includiert) + +# Poll-Intervall (30s) +sed -i 's/^set daemon .*/set daemon 30/' /etc/monit/monitrc || true +# alle alten include-Zeilen raus und monitrc.d setzen +sed -i 's|^#\?\s*include .*$||g' /etc/monit/monitrc +grep -q '^include /etc/monit/monitrc.d/\*' /etc/monit/monitrc \ + || echo 'include /etc/monit/monitrc.d/*' >> /etc/monit/monitrc + +# Optional: HTTP-UI nur einschalten, wenn explizit gewünscht +if [[ "$MONIT_HTTP" = "1" ]]; then + grep -q '^set httpd port 2812' /etc/monit/monitrc || cat >>/etc/monit/monitrc <<'HTTP' +set httpd port 2812 and + use address localhost + allow localhost +HTTP +fi + +# KEIN Löschen mehr der Dateien – wir verschieben je nach Status +# (vorher stand hier rm -rf /etc/monit/monitrc.d/* und rm -f /etc/monit/conf.d/*.conf) + +# ── Helper-Skripte ────────────────────────────────────────────────────────── +install -d -m 0755 /usr/local/sbin + +# Redis-Ping (Password: REDIS_PASSWORD aus installer.env oder .env) +cat >/usr/local/sbin/mailwolt-redis-ping.sh <<'EOSH' +#!/usr/bin/env bash +set -euo pipefail +INSTALLER_ENV="/etc/mailwolt/installer.env" +APP_ENV="/var/www/mailwolt/.env" +REDIS_HOST="${REDIS_HOST:-127.0.0.1}" +REDIS_PORT="${REDIS_PORT:-6379}" +REDIS_PASSWORD="${REDIS_PASSWORD:-}" +REDIS_PASS="${REDIS_PASS:-}" + +[[ -r "$INSTALLER_ENV" ]] && . "$INSTALLER_ENV" || true +if [[ -r "$APP_ENV" ]]; then + [[ -z "${REDIS_HOST}" ]] && REDIS_HOST="$(grep -m1 '^REDIS_HOST=' "$APP_ENV" | cut -d= -f2- || true)" + [[ -z "${REDIS_PORT}" ]] && REDIS_PORT="$(grep -m1 '^REDIS_PORT=' "$APP_ENV" | cut -d= -f2- || true)" + [[ -z "${REDIS_PASSWORD}" ]] && REDIS_PASSWORD="$(grep -m1 '^REDIS_PASSWORD=' "$APP_ENV" | cut -d= -f2- || true)" +fi +[[ -z "${REDIS_PASSWORD}" && -n "${REDIS_PASS}" ]] && REDIS_PASSWORD="$REDIS_PASS" + +strip(){ printf '%s' "$1" | sed -E 's/^"(.*)"$/\1/; s/^'"'"'(.*)'"'"'$/\1/'; } +REDIS_HOST="$(strip "${REDIS_HOST:-}")" +REDIS_PORT="$(strip "${REDIS_PORT:-}")" +REDIS_PASSWORD="$(strip "${REDIS_PASSWORD:-}")" + +command -v redis-cli >/dev/null 2>&1 || exit 1 +BASE=(timeout 2 redis-cli --no-auth-warning --raw -h "$REDIS_HOST" -p "$REDIS_PORT") +[[ -n "$REDIS_PASSWORD" ]] && CMD=("${BASE[@]}" -a "$REDIS_PASSWORD" ping) || CMD=("${BASE[@]}" ping) +[[ "$("${CMD[@]}" 2>/dev/null || true)" == "PONG" ]] +EOSH +chmod 0755 /usr/local/sbin/mailwolt-redis-ping.sh + +# Rspamd-Heal (Socke aufräumen, restart, Mini-Port-Check) +cat >/usr/local/sbin/mailwolt-rspamd-heal.sh <<'EOSH' +#!/usr/bin/env bash +set -euo pipefail + +INSTALLER_ENV="/etc/mailwolt/installer.env" +APP_ENV="/var/www/mailwolt/.env" + +REDIS_HOST="${REDIS_HOST:-127.0.0.1}" +REDIS_PORT="${REDIS_PORT:-6379}" +REDIS_PASS="${REDIS_PASS:-}" + +[[ -r "$INSTALLER_ENV" ]] && . "$INSTALLER_ENV" +if [[ -z "${REDIS_PASS}" && -r "$APP_ENV" ]]; then + REDIS_PASS="$(grep -E '^REDIS_PASS=' "$APP_ENV" | head -n1 | cut -d= -f2- || true)" +fi + +# Rspamd Runtime fixen +install -d -m 0755 -o _rspamd -g _rspamd /run/rspamd || true +[[ -S /var/lib/rspamd/rspamd.sock ]] && rm -f /var/lib/rspamd/rspamd.sock || true + +echo "$(date '+%F %T') heal run" >> /var/log/rspamd-heal.log + +# Neustart +systemctl restart rspamd + +# Mini-Healthcheck +sleep 2 +ss -tln | grep -q ':11334' || echo "[WARN] Rspamd Controller Port 11334 nicht sichtbar" + +exit 0 +EOSH +chmod 0755 /usr/local/sbin/mailwolt-rspamd-heal.sh + +# ── Monit-Checks (nummeriert) – fixe Dienste immer aktiv ──────────────────── +# 10 – Redis +cat >/etc/monit/monitrc.d/10-redis.conf <<'EOF' +check process redis with pidfile /run/redis/redis-server.pid + start program = "/bin/systemctl start redis-server" + stop program = "/bin/systemctl stop redis-server" + if failed host 127.0.0.1 port 6379 for 2 cycles then restart + if 5 restarts within 5 cycles then alert + +check program redis_ping path "/usr/local/sbin/mailwolt-redis-ping.sh" + if status != 0 for 2 cycles then exec "/bin/systemctl restart redis-server" +EOF + +# 20 – Rspamd (robust via process-matching + Heal) +cat >/etc/monit/monitrc.d/20-rspamd.conf <<'EOF' +check process rspamd matching "rspamd: main process" + start program = "/bin/systemctl start rspamd" with timeout 120 seconds + stop program = "/bin/systemctl stop rspamd" + depends on redis + if failed host 127.0.0.1 port 11333 for 3 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh" + if failed host 127.0.0.1 port 11334 for 3 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh" + if does not exist for 2 cycles then restart + if 5 restarts within 10 cycles then unmonitor +EOF + +# 30 – Postfix +cat >/etc/monit/monitrc.d/30-postfix.conf <<'EOF' +check process postfix with pidfile /var/spool/postfix/pid/master.pid + start program = "/bin/systemctl start postfix" + stop program = "/bin/systemctl stop postfix" + if failed host 127.0.0.1 port 25 type tcp with timeout 15 seconds for 3 cycles then restart + if failed host 127.0.0.1 port 465 type tcpssl with timeout 10 seconds then restart + if failed host 127.0.0.1 port 587 type tcp with timeout 10 seconds then restart + if 5 restarts within 5 cycles then alert +EOF + +# 30 – Dovecot (IMAPS; LMTP oft Unix-Socket → kein TCP-Fehlalarm) +cat >/etc/monit/monitrc.d/30-dovecot.conf <<'EOF' +check process dovecot with pidfile /run/dovecot/master.pid + start program = "/bin/systemctl start dovecot" + stop program = "/bin/systemctl stop dovecot" + if failed port 993 type tcpssl for 3 cycles then restart + if 5 restarts within 10 cycles then alert +EOF + +# 40 – Nginx +cat >/etc/monit/monitrc.d/40-nginx.conf <<'EOF' +check process nginx with pidfile /run/nginx.pid + start program = "/bin/systemctl start nginx" + stop program = "/bin/systemctl stop nginx" + if failed port 80 type tcp then restart + if failed port 443 type tcpssl then restart + if 5 restarts within 5 cycles then alert +EOF + +# 50 – OpenDKIM +cat >/etc/monit/monitrc.d/50-opendkim.conf <<'EOF' +check process opendkim with pidfile /run/opendkim/opendkim.pid + start program = "/bin/systemctl start opendkim" + stop program = "/bin/systemctl stop opendkim" + if failed host 127.0.0.1 port 8891 type tcp for 2 cycles then restart + if 5 restarts within 5 cycles then alert +EOF + +move_monit_conf() { + local name="$1" # z.B. 55-opendmarc + local enabled="$2" # "0" oder "1" + local src="/etc/monit/conf.d/${name}.conf" + local dst="/etc/monit/monitrc.d/${name}.conf" + + mkdir -p /etc/monit/conf.d /etc/monit/monitrc.d + + # Falls Datei nirgends existiert → in conf.d anlegen (lesbare Quelle) + if [[ ! -f "$src" && ! -f "$dst" ]]; then + cat >"$src" <<'EOF_PAYLOAD' +__PAYLOAD__ +EOF_PAYLOAD + fi + + if [[ "$enabled" = "1" ]]; then + # Aktiv: in monitrc.d haben + if [[ -f "$src" && ! -f "$dst" ]]; then + mv -f "$src" "$dst" + fi + else + # Inaktiv: in conf.d haben + if [[ -f "$dst" && ! -f "$src" ]]; then + mv -f "$dst" "$src" + fi + fi +} + +move_monit_conf "55-opendmarc" "${OPENDMARC_ENABLE:-0}" <<'EOF' +check process opendmarc with pidfile /run/opendmarc/opendmarc.pid + start program = "/bin/systemctl start opendmarc" + stop program = "/bin/systemctl stop opendmarc" + if 5 restarts within 5 cycles then alert +EOF + +move_monit_conf "60-clamav" "${CLAMAV_ENABLE:-0}" <<'EOF' +check process clamd matching "clamd" + start program = "/bin/systemctl start clamav-daemon" + stop program = "/bin/systemctl stop clamav-daemon" + if failed unixsocket /run/clamav/clamd.ctl for 3 cycles then restart + if 5 restarts within 10 cycles then unmonitor +EOF + +move_monit_conf "70-fail2ban" "${FAIL2BAN_ENABLE:-0}" <<'EOF' +check process fail2ban with pidfile /run/fail2ban/fail2ban.pid + start program = "/bin/systemctl start fail2ban" + stop program = "/bin/systemctl stop fail2ban" + if 5 restarts within 5 cycles then alert +EOF + +# ── Monit neu laden ───────────────────────────────────────────────────────── +monit -t +systemctl reload monit || systemctl restart monit + +# Optionaler Sichttest (CLI funktioniert auch ohne HTTP-UI) +# sleep 2 +# monit summary || true + +##!/usr/bin/env bash +#set -euo pipefail +# +## Flags laden (falls vorhanden) +#INSTALLER_ENV="/etc/mailwolt/installer.env" +#: "${CLAMAV_ENABLE:=}"; : "${OPENDMARC_ENABLE:=}"; : "${FAIL2BAN_ENABLE:=}"; : "${MONIT_HTTP:=}" +#if [[ -z "${CLAMAV_ENABLE}${OPENDMARC_ENABLE}${FAIL2BAN_ENABLE}" && -r "$INSTALLER_ENV" ]]; then +# . "$INSTALLER_ENV" +#fi +#CLAMAV_ENABLE="${CLAMAV_ENABLE:-1}" +#OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-1}" +#FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}" +#MONIT_HTTP="${MONIT_HTTP:-1}" +# +## ── Monit so konfigurieren, dass NUR monitrc.d/* geladen wird ──────────────── +#install -d -m 0755 /etc/monit/monitrc.d +## Poll-Intervall (30s) +#sed -i 's/^set daemon .*/set daemon 30/' /etc/monit/monitrc || true +## alle alten include-Zeilen raus und monitrc.d setzen +#sed -i 's|^#\?\s*include .*$||g' /etc/monit/monitrc +#grep -q '^include /etc/monit/monitrc.d/\*' /etc/monit/monitrc \ +# || echo 'include /etc/monit/monitrc.d/*' >> /etc/monit/monitrc +# +## Optional: HTTP-UI nur einschalten, wenn explizit gewünscht +#if [[ "$MONIT_HTTP" = "1" ]]; then +# grep -q '^set httpd port 2812' /etc/monit/monitrc || cat >>/etc/monit/monitrc <<'HTTP' +#set httpd port 2812 and +# use address localhost +# allow localhost +#HTTP +#fi +# +#sudo mkdir -p /etc/monit/monitrc.d +#sudo rm -rf /etc/monit/monitrc.d/* 2>/dev/null || true +#sudo rm -f /etc/monit/conf.d/*.conf 2>/dev/null || true +# +## ── Helper-Skripte ────────────────────────────────────────────────────────── +#install -d -m 0755 /usr/local/sbin +# +## Redis-Ping (Password: REDIS_PASSWORD aus installer.env oder .env) +#cat >/usr/local/sbin/mailwolt-redis-ping.sh <<'EOSH' +##!/usr/bin/env bash +#set -euo pipefail +#INSTALLER_ENV="/etc/mailwolt/installer.env" +#APP_ENV="/var/www/mailwolt/.env" +#REDIS_HOST="${REDIS_HOST:-127.0.0.1}" +#REDIS_PORT="${REDIS_PORT:-6379}" +#REDIS_PASSWORD="${REDIS_PASSWORD:-}" +#REDIS_PASS="${REDIS_PASS:-}" +# +#[[ -r "$INSTALLER_ENV" ]] && . "$INSTALLER_ENV" || true +#if [[ -r "$APP_ENV" ]]; then +# [[ -z "${REDIS_HOST}" ]] && REDIS_HOST="$(grep -m1 '^REDIS_HOST=' "$APP_ENV" | cut -d= -f2- || true)" +# [[ -z "${REDIS_PORT}" ]] && REDIS_PORT="$(grep -m1 '^REDIS_PORT=' "$APP_ENV" | cut -d= -f2- || true)" +# [[ -z "${REDIS_PASSWORD}" ]] && REDIS_PASSWORD="$(grep -m1 '^REDIS_PASSWORD=' "$APP_ENV" | cut -d= -f2- || true)" +#fi +#[[ -z "${REDIS_PASSWORD}" && -n "${REDIS_PASS}" ]] && REDIS_PASSWORD="$REDIS_PASS" +# +#strip(){ printf '%s' "$1" | sed -E 's/^"(.*)"$/\1/; s/^'"'"'(.*)'"'"'$/\1/'; } +#REDIS_HOST="$(strip "${REDIS_HOST:-}")" +#REDIS_PORT="$(strip "${REDIS_PORT:-}")" +#REDIS_PASSWORD="$(strip "${REDIS_PASSWORD:-}")" +# +#command -v redis-cli >/dev/null 2>&1 || exit 1 +#BASE=(timeout 2 redis-cli --no-auth-warning --raw -h "$REDIS_HOST" -p "$REDIS_PORT") +#[[ -n "$REDIS_PASSWORD" ]] && CMD=("${BASE[@]}" -a "$REDIS_PASSWORD" ping) || CMD=("${BASE[@]}" ping) +#[[ "$("${CMD[@]}" 2>/dev/null || true)" == "PONG" ]] +#EOSH +#chmod 0755 /usr/local/sbin/mailwolt-redis-ping.sh +# +## Rspamd-Heal (Socke aufräumen, restart, Mini-Port-Check) +#cat >/usr/local/sbin/mailwolt-rspamd-heal.sh <<'EOSH' +##!/usr/bin/env bash +#set -euo pipefail +# +#INSTALLER_ENV="/etc/mailwolt/installer.env" +#APP_ENV="/var/www/mailwolt/.env" +# +#REDIS_HOST="${REDIS_HOST:-127.0.0.1}" +#REDIS_PORT="${REDIS_PORT:-6379}" +#REDIS_PASS="${REDIS_PASS:-}" +# +#[[ -r "$INSTALLER_ENV" ]] && . "$INSTALLER_ENV" +#if [[ -z "${REDIS_PASS}" && -r "$APP_ENV" ]]; then +# REDIS_PASS="$(grep -E '^REDIS_PASS=' "$APP_ENV" | head -n1 | cut -d= -f2- || true)" +#fi +# +## Rspamd Runtime fixen +#install -d -m 0755 -o _rspamd -g _rspamd /run/rspamd || true +#[[ -S /var/lib/rspamd/rspamd.sock ]] && rm -f /var/lib/rspamd/rspamd.sock || true +# +#echo "$(date '+%F %T') heal run" >> /var/log/rspamd-heal.log +# +## Neustart +#systemctl restart rspamd +# +## Mini-Healthcheck +#sleep 2 +#ss -tln | grep -q ':11334' || echo "[WARN] Rspamd Controller Port 11334 nicht sichtbar" +# +#exit 0 +#EOSH +#chmod 0755 /usr/local/sbin/mailwolt-rspamd-heal.sh +# +## ── Monit-Checks (nummeriert) ─────────────────────────────────────────────── +## 10 – Redis +#cat >/etc/monit/monitrc.d/10-redis.conf <<'EOF' +#check process redis with pidfile /run/redis/redis-server.pid +# start program = "/bin/systemctl start redis-server" +# stop program = "/bin/systemctl stop redis-server" +# if failed host 127.0.0.1 port 6379 for 2 cycles then restart +# if 5 restarts within 5 cycles then alert +# +#check program redis_ping path "/usr/local/sbin/mailwolt-redis-ping.sh" +# if status != 0 for 2 cycles then exec "/bin/systemctl restart redis-server" +#EOF +# +## 20 – Rspamd (robust via process-matching + Heal) +#cat >/etc/monit/monitrc.d/20-rspamd.conf <<'EOF' +#check process rspamd matching "rspamd: main process" +# start program = "/bin/systemctl start rspamd" with timeout 120 seconds +# stop program = "/bin/systemctl stop rspamd" +# depends on redis +# if failed host 127.0.0.1 port 11333 for 3 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh" +# if failed host 127.0.0.1 port 11334 for 3 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh" +# if does not exist for 2 cycles then restart +# if 5 restarts within 10 cycles then unmonitor +#EOF +# +## 30 – Postfix +#cat >/etc/monit/monitrc.d/30-postfix.conf <<'EOF' +#check process postfix with pidfile /var/spool/postfix/pid/master.pid +# start program = "/bin/systemctl start postfix" +# stop program = "/bin/systemctl stop postfix" +# if failed host 127.0.0.1 port 25 type tcp with timeout 15 seconds for 3 cycles then restart +# if failed host 127.0.0.1 port 465 type tcpssl with timeout 10 seconds then restart +# if failed host 127.0.0.1 port 587 type tcp with timeout 10 seconds then restart +# if 5 restarts within 5 cycles then alert +#EOF +# +## 30 – Dovecot (IMAPS; LMTP oft Unix-Socket → kein TCP-Fehlalarm) +#cat >/etc/monit/monitrc.d/30-dovecot.conf <<'EOF' +#check process dovecot with pidfile /run/dovecot/master.pid +# start program = "/bin/systemctl start dovecot" +# stop program = "/bin/systemctl stop dovecot" +# if failed port 993 type tcpssl for 3 cycles then restart +# if 5 restarts within 10 cycles then alert +#EOF +# +## 40 – Nginx +#cat >/etc/monit/monitrc.d/40-nginx.conf <<'EOF' +#check process nginx with pidfile /run/nginx.pid +# start program = "/bin/systemctl start nginx" +# stop program = "/bin/systemctl stop nginx" +# if failed port 80 type tcp then restart +# if failed port 443 type tcpssl then restart +# if 5 restarts within 5 cycles then alert +#EOF +# +## 50 – OpenDKIM +#cat >/etc/monit/monitrc.d/50-opendkim.conf <<'EOF' +#check process opendkim with pidfile /run/opendkim/opendkim.pid +# start program = "/bin/systemctl start opendkim" +# stop program = "/bin/systemctl stop opendkim" +# if failed host 127.0.0.1 port 8891 type tcp for 2 cycles then restart +# if 5 restarts within 5 cycles then alert +#EOF +# +## 55 – OpenDMARC (optional) +#if [[ "$OPENDMARC_ENABLE" = "1" ]]; then +# cat >/etc/monit/monitrc.d/55-opendmarc.conf <<'EOF' +#check process opendmarc with pidfile /run/opendmarc/opendmarc.pid +# start program = "/bin/systemctl start opendmarc" +# stop program = "/bin/systemctl stop opendmarc" +# if 5 restarts within 5 cycles then alert +#EOF +#else +# rm -f /etc/monit/monitrc.d/55-opendmarc.conf || true +#fi +# +## 60 – ClamAV (über Socket) +#if [[ "$CLAMAV_ENABLE" = "1" ]]; then +# cat >/etc/monit/monitrc.d/60-clamav.conf <<'EOF' +#check process clamd matching "clamd" +# start program = "/bin/systemctl start clamav-daemon" +# stop program = "/bin/systemctl stop clamav-daemon" +# if failed unixsocket /run/clamav/clamd.ctl for 3 cycles then restart +# if 5 restarts within 10 cycles then unmonitor +#EOF +#else +# rm -f /etc/monit/monitrc.d/60-clamav.conf || true +#fi +# +## 70 – Fail2Ban (optional) +#if [[ "$FAIL2BAN_ENABLE" = "1" ]]; then +# cat >/etc/monit/monitrc.d/70-fail2ban.conf <<'EOF' +#check process fail2ban with pidfile /run/fail2ban/fail2ban.pid +# start program = "/bin/systemctl start fail2ban" +# stop program = "/bin/systemctl stop fail2ban" +# if 5 restarts within 5 cycles then alert +#EOF +#else +# rm -f /etc/monit/monitrc.d/70-fail2ban.conf || true +#fi +# +## ── Monit neu laden ───────────────────────────────────────────────────────── +#monit -t +#systemctl reload monit || systemctl restart monit +# +## Optionaler Sichttest (CLI funktioniert auch ohne HTTP-UI) +##sleep 2 +##monit summary || true \ No newline at end of file diff --git a/scripts/98-motd.sh b/scripts/98-motd.sh new file mode 100644 index 0000000..f5a7736 --- /dev/null +++ b/scripts/98-motd.sh @@ -0,0 +1,150 @@ +#!/usr/bin/env bash +set -euo pipefail +source ./lib.sh + +log "MOTD installieren …" +install -d /usr/local/bin + +cat >/usr/local/bin/mw-motd <<'SH' +#!/usr/bin/env bash +# MOTD – MailWolt +# bewusst KEIN "set -e" und KEIN pipefail; MOTD darf nie hart abbrechen +set -u + +# ---------- Farben ---------- +NC="\033[0m"; WH="\033[1;37m"; CY="\033[1;36m"; GY="\033[0;90m" +GR="\033[1;32m"; YE="\033[1;33m"; RD="\033[1;31m" + +# ---------- Breite / Zentrierung ---------- +W=110 +term_cols=$(tput cols 2>/dev/null || echo $W) +[ "$term_cols" -gt "$W" ] && pad=$(( (term_cols - W)/2 )) || pad=0 +sp(){ [ "$1" -gt 0 ] && printf "%${1}s" " " || true; } +center() { local s="$1"; local n=$(( (W - ${#s})/2 )); sp $((pad+n)); printf "%s\n" "$s"; } +rule(){ sp "$pad"; printf "%0.s=" $(seq 1 "$W"); printf "\n"; } +title(){ sp "$pad"; local t="$1"; local lf=$(( (W - ${#t} - 2)/2 )); local rf=$(( W - ${#t} - 2 - lf )); \ + printf "%s" "$(printf '─%.0s' $(seq 1 $lf))"; printf " %s " "$t"; printf "%s\n" "$(printf '─%.0s' $(seq 1 $rf))"; } +kv(){ sp "$pad"; printf "%-12s: %s\n" "$1" "$2"; } + +# ---------- Installer-/App-Variablen ---------- +UI_HOST=""; WEBMAIL_HOST=""; MAIL_HOSTNAME="" +[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env || true + +# ---------- Systemdaten ---------- +now="$(date '+%Y-%m-%d %H:%M:%S %Z' 2>/dev/null || echo '-')" +upt="$(uptime -p 2>/dev/null || echo '-')" +cores="$(nproc 2>/dev/null || echo 1)" +load_raw="$(awk '{printf "%s / %s / %s",$1,$2,$3}' /proc/loadavg 2>/dev/null || echo '0.00 / 0.00 / 0.00')" +load1="$(awk '{print $1}' /proc/loadavg 2>/dev/null || echo 0)" + +# RAM/SWAP +mem_total="$(awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || echo 0)" +mem_avail="$(awk '/MemAvailable/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || echo 0)" +mem_used=$(( mem_total - mem_avail )) +swap_total="$(awk '/SwapTotal/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || echo 0)" +swap_free="$(awk '/SwapFree/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || echo 0)" +swap_used=$(( swap_total - swap_free )) + +pct(){ local u="$1" t="$2"; [ "$t" -gt 0 ] || { echo 0; return; }; awk -v u="$u" -v t="$t" 'BEGIN{printf "%d",(u*100)/t}' ; } +ram_pct=$(pct "$mem_used" "$mem_total") +swap_pct=$(pct "$swap_used" "$swap_total") + +# Disks +df_line(){ df -hP "$1" 2>/dev/null | awk 'NR==2{printf "%s / %s (%s)",$3,$2,$5}'; } +df_pct(){ df -P "$1" 2>/dev/null | awk 'NR==2{gsub("%","",$5);print $5+0}'; } +disk_root="$(df_line /)"; pct_root="$(df_pct /)" +disk_var="$(df_line /var 2>/dev/null)"; [ -n "$disk_var" ] || disk_var="-" +pct_var="$(df_pct /var 2>/dev/null)"; [ -n "$pct_var" ] || pct_var=0 + +# IPs (int/ext) +ipv4_int="$(hostname -I 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i!~/:/){print $i;exit}}')" +ipv6_int="$(hostname -I 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i~/:/){print $i;exit}}')" +ipv4_ext="$(curl -4fsS --max-time 1 https://ifconfig.me 2>/dev/null || true)" +ipv6_ext="$(curl -6fsS --max-time 1 https://ifconfig.me 2>/dev/null || true)" + +# ---------- Status-Farben ---------- +mark(){ # value thresholdY thresholdR + local v="$1" y="$2" r="$3" + if [ "$v" -ge "$r" ]; then printf "${RD}[HIGH]${NC}" + elif [ "$v" -ge "$y" ]; then printf "${YE}[WARN]${NC}" + else printf "${GR}[OK]${NC}" + fi +} +# Load/CPU-Schwellen (pro Core) +load_pct=$(awk -v l="$load1" -v c="$cores" 'BEGIN{if(c<1)c=1; printf "%d", (l/c)*100}') +m_load="$(mark "$load_pct" 70 100)" +m_ram="$(mark "$ram_pct" 75 90)" +m_swap="$(mark "$swap_pct" 10 50)" +m_root="$(mark "$pct_root" 75 90)" +m_var="$(mark "$pct_var" 75 90)" + +# ---------- Header ---------- +rule +center "" +center ":::: :::: ::: ::::::::::: ::: ::: ::: :::::::: ::: :::::::::::" +center ":+:+:+ :+:+:+ :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: " +center ":+: +:+:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ " +center "+#+ +:+ +#+ +#++:++#++: +#+ +#+ +#+ +:+ +#+ +#+ +:+ +#+ +#+ " +center "+#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+#+ +#+ +#+ +#+ +#+ +#+ " +center "#+# #+# #+# #+# #+# #+# #+#+# #+#+# #+# #+# #+# #+# " +center "### ### ### ### ########### ########## ### ### ######## ########## ### " +center "" +rule + +# ---------- System ---------- +kv "Date / Time" "${YE}${now}${NC}" +sp "$pad"; printf "%-12s: int %-40s ext %s\n" "IPv4" "${ipv4_int:--}" "${ipv4_ext:--}" +sp "$pad"; printf "%-12s: int %-40s ext %s\n" "IPv6" "${ipv6_int:--}" "${ipv6_ext:--}" +kv "Uptime" "$upt" +sp "$pad"; printf "%-12s: %s cores, load %s %b\n" "CPU" "$cores" "$load_raw" "$m_load" +sp "$pad"; printf "%-12s: %s MiB / %s MiB (%d%%) %b %-5s %s MiB / %s MiB (%d%%) %b\n" \ + "RAM" "$mem_used" "$mem_total" "$ram_pct" "$m_ram" "SWAP:" "$swap_used" "$swap_total" "$swap_pct" "$m_swap" +sp "$pad"; printf "%-12s: / %s %b %-5s %s %b\n" \ + "Disk" "$disk_root" "$m_root" "/var:" "$disk_var" "$m_var" +echo + +# ---------- Domains ---------- +title "Domains" +[ -n "${UI_HOST:-}" ] && kv "UI" "${UI_HOST}" +[ -n "${WEBMAIL_HOST:-}" ] && kv "Webmail" "${WEBMAIL_HOST}" +[ -n "${MAIL_HOSTNAME:-}" ]&& kv "MX" "${MAIL_HOSTNAME}" +echo + +# ---------- Services (4 Spalten, bündig) ---------- +title "Services" +svc_state(){ systemctl is-active --quiet "$1" && printf "${GR}[OK]${NC}" || printf "${RD}[FAIL]${NC}"; } +SVC=( nginx mariadb redis-server postfix dovecot rspamd opendkim opendmarc clamav-daemon fail2ban mailwolt-ws mailwolt-queue mailwolt-schedule ) + +i=0; line="" +for s in "${SVC[@]}"; do + st="$(svc_state "$s")" + seg="$(printf "%-18s %-7s" "$s" "$st")" + line="$line$seg" + i=$((i+1)) + if [ $((i%4)) -eq 0 ]; then sp "$pad"; echo "$line"; line=""; else line="$line "; fi +done +[ -n "$line" ] && { sp "$pad"; echo "$line"; } +echo + +exit 0 +SH + +chmod 755 /usr/local/bin/mw-motd + +# update-motd Hook +if [[ -d /etc/update-motd.d ]]; then + cat >/etc/update-motd.d/10-mailwolt <<'SH' +#!/usr/bin/env bash +/usr/local/bin/mw-motd +SH + chmod +x /etc/update-motd.d/10-mailwolt + [[ -f /etc/update-motd.d/50-motd-news ]] && chmod -x /etc/update-motd.d/50-motd-news || true +else + # Fallback für Systeme ohne dynamic MOTD + cat >/etc/profile.d/10-mailwolt-motd.sh <<'SH' +case "$-" in *i*) /usr/local/bin/mw-motd ;; esac +SH +fi + +: > /etc/motd 2>/dev/null || true +log "[✓] MOTD installiert." \ No newline at end of file diff --git a/scripts/99-summary.sh b/scripts/99-summary.sh new file mode 100644 index 0000000..bf08778 --- /dev/null +++ b/scripts/99-summary.sh @@ -0,0 +1,212 @@ +#!/usr/bin/env bash +set -euo pipefail +source ./lib.sh + +# ───────────────────────────────────────────────────────────── +# MailWolt – Abschluss / Summary (Dienste, Zertifikate, Smoke-Test) +# ───────────────────────────────────────────────────────────── + +# Farben & Deko +NC="\033[0m"; BOLD="\033[1m"; DIM="\033[2m" +GREEN="\033[1;32m"; RED="\033[1;31m"; YELLOW="\033[1;33m"; CYAN="\033[1;36m"; GREY="\033[0;90m" +OKS="${GREEN}OK${NC}"; FAILS="${RED}FAIL${NC}" +bar(){ printf "${CYAN}%s${NC}\n" "──────────────────────────────────────────────────────────────────────────────"; } +ok(){ printf " [${OKS}]\n"; } +fail(){ printf " [${FAILS}]\n"; } + +# Installer-Variablen laden (falls vorhanden) +set +u +[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env +set -u + +# Defaults / Umgebung +APP_USER="${APP_USER:-mailwolt}" +APP_GROUP="${APP_GROUP:-www-data}" +APP_DIR="${APP_DIR:-/var/www/${APP_USER}}" + +BASE_DOMAIN="${BASE_DOMAIN:-example.com}" +UI_HOST="${UI_HOST:-}" +WEBMAIL_HOST="${WEBMAIL_HOST:-}" +MAIL_HOSTNAME="${MAIL_HOSTNAME:-}" + +APP_ENV="${APP_ENV:-production}" +PROXY_MODE="${PROXY_MODE:-}" # leer = nicht anzeigen; "1"=Proxy, "dev"=Dev, sonst "nein" +NPM_IP="${NPM_IP:-}" + +LE_EMAIL="${LE_EMAIL:-admin@${BASE_DOMAIN}}" +ACME_WEBROOT="/var/www/letsencrypt" + +# Zert-Pfade (werden via Hook nach /etc/ssl/* verlinkt) +UI_CERT="/etc/ssl/ui/fullchain.pem" +UI_KEY="/etc/ssl/ui/privkey.pem" +WEBMAIL_CERT="/etc/ssl/webmail/fullchain.pem" +MAIL_CERT="/etc/ssl/mail/fullchain.pem" + +# IPs (aus lib.sh) +SERVER_PUBLIC_IPV4="${SERVER_PUBLIC_IPV4:-$(detect_ip)}" +SERVER_PUBLIC_IPV6="${SERVER_PUBLIC_IPV6:-$(detect_ipv6)}" + +# URLs (https nur, wenn UI-Cert+Key vorhanden) +SCHEME="http" +[[ -s "$UI_CERT" && -s "$UI_KEY" ]] && SCHEME="https" +APP_URL="${SCHEME}://${UI_HOST:-$SERVER_PUBLIC_IPV4}" +WEBMAIL_URL="${SCHEME}://${WEBMAIL_HOST:-$SERVER_PUBLIC_IPV4}" + +# Ziel eines Symlinks auflösen +real_target(){ readlink -f -- "$1" 2>/dev/null || true; } + +# "LE" werten, wenn live/* ODER archive/* (auch fullchainN.pem) getroffen wird +is_le_path(){ + local p="$1" + [[ "$p" == /etc/letsencrypt/live/*/fullchain.pem || "$p" == /etc/letsencrypt/archive/*/fullchain*.pem ]] +} + +UI_CERT_TARGET="$(real_target "$UI_CERT")" +WEBMAIL_CERT_TARGET="$(real_target "$WEBMAIL_CERT")" +MAIL_CERT_TARGET="$(real_target "$MAIL_CERT")" + +is_le_path() { + case "$1" in + /etc/letsencrypt/live/*) return 0 ;; + *) return 1 ;; + esac +} + +# robust gegen set -u: immer ${var:-} +UI_LE="self-signed/none" +if [ -s "${UI_CERT:-}" ] && [ -n "${UI_CERT_TARGET:-}" ] && is_le_path "${UI_CERT_TARGET:-}"; then + UI_LE="LE" +fi + +WEBMAIL_LE="self-signed/none" +if [ -s "${WEBMAIL_CERT:-}" ] && [ -n "${WEBMAIL_CERT_TARGET:-}" ] && is_le_path "${WEBMAIL_CERT_TARGET:-}"; then + WEBMAIL_LE="LE" +fi + +MAIL_LE="self-signed/none" +if [ -s "${MAIL_CERT:-}" ] && [ -n "${MAIL_CERT_TARGET:-}" ] && is_le_path "${MAIL_CERT_TARGET:-}"; then + MAIL_LE="LE" +fi + +echo +bar +printf " %s\n" "✔ MailWolt Bootstrap fertig" +bar + +# Kopf-Infos +printf " %-14s %s\n" "Aufruf UI:" "${APP_URL}" +printf " %-14s %s\n" "Webmail:" "${WEBMAIL_URL}" +printf " %-14s %s\n" "App Root:" "${APP_DIR}" +printf " %-14s %s\n" "Mail-FQDN:" "${MAIL_HOSTNAME:-$SERVER_PUBLIC_IPV4}" +printf " %-14s %s\n" "BASE_DOMAIN:" "${BASE_DOMAIN}" +printf " %-14s %s\n" "LE-Email:" "${LE_EMAIL}" +printf " %-14s %s\n" "APP_ENV:" "${APP_ENV}" +# Proxy-Block nur anzeigen, wenn Variable gesetzt ist +if [[ -n "$PROXY_MODE" ]]; then + if [[ "$PROXY_MODE" == "1" ]]; then + printf " %-14s %s\n" "Proxy-Mode:" "ja (NPM: ${NPM_IP:-unbekannt})" + elif [[ "$PROXY_MODE" == "dev" ]]; then + printf " %-14s %s\n" "Proxy-Mode:" "Entwicklungsmodus" + else + printf " %-14s %s\n" "Proxy-Mode:" "nein" + fi +fi +printf " %-14s %s\n" "Server IPv6:" "${SERVER_PUBLIC_IPV6:-–}" +printf " %-14s %s\n" "ACME Webroot:" "${ACME_WEBROOT}" + +echo +printf " %-14s UI=%s, Webmail=%s, MX=%s\n" "Zertifikate:" "$UI_LE" "$WEBMAIL_LE" "$MAIL_LE" +echo + +echo " Anmeldung: Keine vordefinierten Admin-Daten." +echo " Bitte zuerst registrieren (Erst-User wird Admin, danach" +echo " wird die Registrierung automatisch gesperrt)." +echo + +# ── Dienste ──────────────────────────────────────────────────────────────── +bar +echo " Services" +bar + +OK_LIST=() +FAIL_LIST=() + +svc(){ + local unit="$1" label="${2:-$1}" + printf " • %-18s … " "$label" + if systemctl is-active --quiet "$unit"; then + ok + OK_LIST+=("$label") + else + fail + FAIL_LIST+=("$label") + fi +} + +# Kern-Services +svc nginx +svc mariadb +svc redis-server +svc postfix +svc dovecot +# App-Worker (tolerant) +svc "${APP_USER}-ws" "mailwolt-ws" || true +svc "${APP_USER}-schedule" "mailwolt-schedule" || true +svc "${APP_USER}-queue" "mailwolt-queue" || true + +echo +if ((${#OK_LIST[@]})); then + printf " ${GREEN}OK:${NC} %s\n" "$(IFS=', '; echo "${OK_LIST[*]}")" +fi +if ((${#FAIL_LIST[@]})); then + printf " ${RED}FAIL:${NC} %s\n" "$(IFS=', '; echo "${FAIL_LIST[*]}")" + echo " ${YELLOW}Hinweis:${NC} Details mit: journalctl -u -b --no-pager" +fi +echo + +# ── Smoke-Test ───────────────────────────────────────────────────────────── +bar +echo " Smoke-Test (SMTP/IMAP/POP3 mit/ohne TLS)" +bar + +check_port(){ + local tag="$1" cmd="$2" desc="$3" + printf " [%-3s] %-35s … " "$tag" "$desc" + if timeout 8s bash -lc "$cmd" >/dev/null 2>&1; then ok; else fail; fi +} + +# kleines Delay nach Erststart +sleep 6 || true + +# SMTP +check_port "25" 'printf "EHLO x\r\nQUIT\r\n" | nc -w 3 127.0.0.1 25' \ + "SMTP (EHLO)" +check_port "465" 'printf "QUIT\r\n" | openssl s_client -connect 127.0.0.1:465 -quiet -ign_eof' \ + "SMTPS (TLS + EHLO)" +check_port "587" 'printf "EHLO x\r\nSTARTTLS\r\nQUIT\r\n" | openssl s_client -starttls smtp -connect 127.0.0.1:587 -quiet -ign_eof' \ + "Submission (STARTTLS)" + +# POP/IMAP +check_port "110" 'printf "QUIT\r\n" | nc -w 3 127.0.0.1 110' \ + "POP3 (QUIT)" +check_port "995" 'printf "QUIT\r\n" | openssl s_client -connect 127.0.0.1:995 -quiet -ign_eof' \ + "POP3S (TLS + QUIT)" +check_port "143" 'printf ". CAPABILITY\r\n. LOGOUT\r\n" | nc -w 3 127.0.0.1 143' \ + "IMAP (CAPABILITY/LOGOUT)" +check_port "993" 'printf ". CAPABILITY\r\n. LOGOUT\r\n" | openssl s_client -connect 127.0.0.1:993 -quiet -ign_eof' \ + "IMAPS (TLS + CAPABILITY/LOGOUT)" + +echo + +# Hinweise nur ausgeben, wenn wirklich kein LE für UI/Webmail +if [[ "$UI_LE" != "LE" || "$WEBMAIL_LE" != "LE" ]]; then + echo -e " ${YELLOW}Hinweis:${NC} UI/Webmail verwenden noch kein Let's-Encrypt-Zertifikat." + echo -e " Prüfe Symlinks unter /etc/ssl/{ui,webmail} und den LE-Hook (21/75-Skripte)." + echo +fi + +# Proxy-Info (optional) +if [[ "$PROXY_MODE" == "1" ]]; then + echo -e " ${GREY}Proxy-Hinweis:${NC} App erwartet TLS am Proxy (Backend ohne https-Redirects)." + echo +fi \ No newline at end of file diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh new file mode 100644 index 0000000..2bfaf20 --- /dev/null +++ b/scripts/bootstrap.sh @@ -0,0 +1,633 @@ +#!/usr/bin/env bash +set -euo pipefail + +# --- Farbschema für whiptail (libnewt) – hohe Lesbarkeit (dunkler Input, schwarze Schrift) --- +export NEWT_COLORS=' +root=,blue +border=black,lightgray +window=black,lightgray +textbox=black,lightgray +label=black,lightgray +entry=black,cyan +button=black,cyan +actlistbox=black,cyan +actsellistbox=black,cyan +' + +# optionales Backtitle (erscheint oben) +export DIALOGOPTS="--backtitle MailWolt Setup" + +# ────────────────────────────────────────────────────────────── +# MailWolt – Interaktiver Bootstrap (whiptail + Fallback) +# ────────────────────────────────────────────────────────────── + +DEV_MODE=0 +PROXY_MODE=0 +NPM_IP="" +while [[ $# -gt 0 ]]; do + case "$1" in + -dev) DEV_MODE=1 ;; + -proxy) PROXY_MODE=1; NPM_IP="${2:-}"; shift ;; + esac + shift +done + +APP_ENV="${APP_ENV:-$([[ $DEV_MODE -eq 1 ]] && echo local || echo production)}" +APP_DEBUG="${APP_DEBUG:-$([[ $DEV_MODE -eq 1 ]] && echo true || echo false)}" +export DEV_MODE PROXY_MODE NPM_IP APP_ENV APP_DEBUG + +DB_PASS="${DB_PASS:-$(openssl rand -hex 16)}" +REDIS_PASS="${REDIS_PASS:-$(openssl rand -hex 16)}" +export DB_PASS REDIS_PASS + +cd "$(dirname "$0")" +source ./lib.sh +require_root +header + +# ── Defaults ────────────────────────────────────────────────── +APP_NAME="${APP_NAME:-MailWolt}" +APP_USER="${APP_USER:-mailwolt}" +APP_GROUP="${APP_GROUP:-www-data}" +APP_USER_PREFIX="${APP_USER_PREFIX:-mw}" +APP_DIR="${APP_DIR:-/var/www/${APP_USER}}" + +BASE_DOMAIN="${BASE_DOMAIN:-example.com}" +UI_SUB="${UI_SUB:-ui}" +WEBMAIL_SUB="${WEBMAIL_SUB:-webmail}" +MTA_SUB="${MTA_SUB:-mx}" + +DB_NAME="${DB_NAME:-${APP_USER}}" +DB_USER="${DB_USER:-${APP_USER}}" + +SERVER_PUBLIC_IPV4="$(detect_ip)" +SERVER_PUBLIC_IPV6="$(detect_ipv6)" +DEFAULT_TZ="$(detect_timezone)" +DEFAULT_LOCALE="$(guess_locale_from_tz "$DEFAULT_TZ")" + +echo -e "${GREY}Erkannte IP (v4): ${SERVER_PUBLIC_IPV4} v6: ${SERVER_PUBLIC_IPV6:-–}${NC}" + +# ── Helpers ─────────────────────────────────────────────────── +have_whiptail(){ command -v whiptail >/dev/null 2>&1; } + +#valid_fqdn(){ +# [[ "$1" =~ ^([a-z0-9]([-a-z0-9]*[a-z0-9])?\.)+[a-z]{2,}$ ]] +#} + +# ── Host-Validierung & DEV-Erkennung ──────────────────────────────────────── +valid_fqdn_prod(){ [[ "$1" =~ ^([a-z0-9]([-a-z0-9]*[a-z0-9])?\.)+[a-z]{2,}$ ]]; } +valid_host_dev(){ + # erlaubt: single-label (ui, webmail), FQDNs, IPv4 + [[ "$1" =~ ^([a-z0-9]([-a-z0-9]*[a-z0-9])?)(\.[a-z0-9-]+)*$ ]] || [[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] +} +is_local_like(){ + local h="$(echo "$1" | tr '[:upper:]' '[:lower:]')" + [[ "$h" =~ \.local$ || "$h" =~ \.loc$ || "$h" =~ \.dev$ || "$h" =~ \.test$ || "$h" = "localhost" ]] && return 0 + [[ "$h" =~ ^10\. || "$h" =~ ^192\.168\. || "$h" =~ ^172\.(1[6-9]|2[0-9]|3[0-1])\. || "$h" =~ ^127\. ]] && return 0 + return 1 +} +normalize_host(){ + # $1=input $2=default (nutzt DEV_MODE für die passende Prüflogik) + local inp="$1" def="$2" + if [[ "${DEV_MODE}" = "1" ]]; then + valid_host_dev "$inp" && { echo "$inp"; return; } + else + valid_fqdn_prod "$inp" && { echo "$inp"; return; } + fi + echo "$def" +} + +ask_tty_domain(){ + local label="$1" example="$2" def="$3" outvar="$4" inp + echo -e "${CYAN}${label}${NC}" + echo -e " z.B. ${YELLOW}${example}${NC}" + echo -e " Default: ${GREY}${def}${NC}" + read -r -p " Eingabe (Enter=Default): " inp || true + inp="${inp:-$def}" + if ! valid_fqdn "$inp"; then + echo -e "${YELLOW}[!] Ungültiger FQDN, nehme Default: ${def}${NC}" + inp="$def" + fi + eval "$outvar='$inp'" +} + +# ── Interaktive Eingaben (whiptail oder Fallback) ───────────── +MTA_DEFAULT="${MTA_SUB}.${BASE_DOMAIN}" +UI_DEFAULT="${UI_SUB}.${BASE_DOMAIN}" +WEBMAIL_DEFAULT="${WEBMAIL_SUB}.${BASE_DOMAIN}" + +CLAMAV_ENABLE=1 +OPENDMARC_ENABLE=1 +FAIL2BAN_ENABLE=1 + +if command -v whiptail >/dev/null 2>&1; then + TITLE="MailWolt Setup" + + # Hinweise zu erlaubten DEV-Hosts + MSG_SUFFIX="\n\nHinweis: Im DEV-Modus sind auch single-label Hosts (z.B. ui, webmail), *.local/*.dev und IPs erlaubt." + + _mta_in="$(whiptail --title "$TITLE" --inputbox "Mailserver-Host (MX)\nBeispiele: mx.domain.tld | mx.local | 10.0.0.10${MSG_SUFFIX}" 13 70 "$MTA_DEFAULT" 3>&1 1>&2 2>&3)" || exit 1 + _ui_in="$(whiptail --title "$TITLE" --inputbox "UI / Admin-Panel Host\nBeispiele: ui.domain.tld | ui.local | 10.0.0.10${MSG_SUFFIX}" 13 70 "$UI_DEFAULT" 3>&1 1>&2 2>&3)" || exit 1 + _wm_in="$(whiptail --title "$TITLE" --inputbox "Webmail Host\nBeispiele: webmail.domain.tld | web.local | 10.0.0.10${MSG_SUFFIX}" 13 70 "$WEBMAIL_DEFAULT" 3>&1 1>&2 2>&3)" || exit 1 + + # ZUERST provisorisch prüfen, ob „lokal“ → DEV erzwingen + if is_local_like "$_mta_in" || is_local_like "$_ui_in" || is_local_like "$_wm_in"; then + DEV_MODE=1; APP_ENV="local"; APP_DEBUG="true" + fi + export DEV_MODE APP_ENV APP_DEBUG + + # Jetzt mit passender Logik normalisieren + MTA_FQDN="$(normalize_host "$_mta_in" "$MTA_DEFAULT")" + UI_FQDN="$(normalize_host "$_ui_in" "$UI_DEFAULT")" + WEBMAIL_FQDN="$(normalize_host "$_wm_in" "$WEBMAIL_DEFAULT")" + + CHOICES="$(whiptail --title "$TITLE" --checklist "Optionale Dienste aktivieren" 15 70 6 \ + "ClamAV" "Virenscan (clamd/clamav-daemon)" ON \ + "OpenDMARC" "DMARC-Auswertung" ON \ + "Fail2Ban" "Brute-Force-Schutz" ON \ + 3>&1 1>&2 2>&3)" || true + CLAMAV_ENABLE=0; [[ "$CHOICES" == *"ClamAV"* ]] && CLAMAV_ENABLE=1 + OPENDMARC_ENABLE=0; [[ "$CHOICES" == *"OpenDMARC"* ]] && OPENDMARC_ENABLE=1 + FAIL2BAN_ENABLE=0; [[ "$CHOICES" == *"Fail2Ban"* ]] && FAIL2BAN_ENABLE=1 + +else + echo -e "${GREY}[i] whiptail nicht gefunden – TTY-Fallback.${NC}\n" + read -r -p "Mailserver-Host (MX) [${MTA_DEFAULT}]: " _mta_in; _mta_in="${_mta_in:-$MTA_DEFAULT}" + read -r -p "UI / Admin-Panel Host [${UI_DEFAULT}]: " _ui_in; _ui_in="${_ui_in:-$UI_DEFAULT}" + read -r -p "Webmail Host [${WEBMAIL_DEFAULT}]: " _wm_in; _wm_in="${_wm_in:-$WEBMAIL_DEFAULT}" + + if is_local_like "$_mta_in" || is_local_like "$_ui_in" || is_local_like "$_wm_in"; then + DEV_MODE=1; APP_ENV="local"; APP_DEBUG="true" + fi + export DEV_MODE APP_ENV APP_DEBUG + + MTA_FQDN="$(normalize_host "$_mta_in" "$MTA_DEFAULT")" + UI_FQDN="$(normalize_host "$_ui_in" "$UI_DEFAULT")" + WEBMAIL_FQDN="$(normalize_host "$_wm_in" "$WEBMAIL_DEFAULT")" + + read -r -p "ClamAV aktivieren? (1/0, Enter=1): " CLAMAV_ENABLE; CLAMAV_ENABLE="${CLAMAV_ENABLE:-1}" + read -r -p "OpenDMARC aktivieren? (1/0, Enter=1): " OPENDMARC_ENABLE; OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-1}" + read -r -p "Fail2Ban aktivieren? (1/0, Enter=1): " FAIL2BAN_ENABLE; FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}" +fi + +#if have_whiptail; then +# TITLE="MailWolt Setup" +# +# MTA_FQDN="$(whiptail --title "$TITLE" --inputbox "Mailserver-FQDN (MX)\nBeispiel: mx.domain.tld" 11 70 "$MTA_DEFAULT" 3>&1 1>&2 2>&3)" || exit 1 +# valid_fqdn "$MTA_FQDN" || MTA_FQDN="$MTA_DEFAULT" +# +# UI_FQDN="$(whiptail --title "$TITLE" --inputbox "UI / Admin-Panel FQDN\nBeispiel: ui.domain.tld" 11 70 "$UI_DEFAULT" 3>&1 1>&2 2>&3)" || exit 1 +# valid_fqdn "$UI_FQDN" || UI_FQDN="$UI_DEFAULT" +# +# WEBMAIL_FQDN="$(whiptail --title "$TITLE" --inputbox "Webmail FQDN\nBeispiel: webmail.domain.tld" 11 70 "$WEBMAIL_DEFAULT" 3>&1 1>&2 2>&3)" || exit 1 +# valid_fqdn "$WEBMAIL_FQDN" || WEBMAIL_FQDN="$WEBMAIL_DEFAULT" +# +# CHOICES="$(whiptail --title "$TITLE" --checklist "Optionale Dienste aktivieren" 15 70 6 \ +# "ClamAV" "Virenscan (clamd/clamav-daemon)" ON \ +# "OpenDMARC" "DMARC-Auswertung" ON \ +# "Fail2Ban" "Brute-Force-Schutz" ON \ +# 3>&1 1>&2 2>&3)" || true +# +# CLAMAV_ENABLE=0; [[ "$CHOICES" == *"ClamAV"* ]] && CLAMAV_ENABLE=1 +# OPENDMARC_ENABLE=0; [[ "$CHOICES" == *"OpenDMARC"* ]] && OPENDMARC_ENABLE=1 +# FAIL2BAN_ENABLE=0; [[ "$CHOICES" == *"Fail2Ban"* ]] && FAIL2BAN_ENABLE=1 +# +# whiptail --title "$TITLE" --msgbox "Zusammenfassung: +# +#MX : $MTA_FQDN +#UI : $UI_FQDN +#Webmail : $WEBMAIL_FQDN +# +#ClamAV : $([[ $CLAMAV_ENABLE -eq 1 ]] && echo Aktiv || echo Deaktiv) +#OpenDMARC : $([[ $OPENDMARC_ENABLE -eq 1 ]] && echo Aktiv || echo Deaktiv) +#Fail2Ban : $([[ $FAIL2BAN_ENABLE -eq 1 ]] && echo Aktiv || echo Deaktiv) +#" 16 70 +# +#else +# echo -e "${GREY}[i] whiptail nicht gefunden – nutze TTY-Prompts.${NC}\n" +# ask_tty_domain "Mailserver-FQDN (MX)" "mx.domain.tld" "$MTA_DEFAULT" MTA_FQDN +# ask_tty_domain "UI / Admin-Panel FQDN" "ui.domain.tld" "$UI_DEFAULT" UI_FQDN +# ask_tty_domain "Webmail FQDN" "webmail.domain.tld" "$WEBMAIL_DEFAULT" WEBMAIL_FQDN +# +# read -r -p "ClamAV aktivieren? (1/0, Enter=1): " CLAMAV_ENABLE; CLAMAV_ENABLE="${CLAMAV_ENABLE:-1}" +# read -r -p "OpenDMARC aktivieren? (1/0, Enter=1): " OPENDMARC_ENABLE; OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-1}" +# read -r -p "Fail2Ban aktivieren? (1/0, Enter=1): " FAIL2BAN_ENABLE; FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}" +#fi + +# ── Defaults/Kompatibilität ────────────────────────────────── +MTA_FQDN="${MTA_FQDN:-${MTA_DEFAULT}}" +UI_FQDN="${UI_FQDN:-${UI_DEFAULT}}" +WEBMAIL_FQDN="${WEBMAIL_FQDN:-${WEBMAIL_DEFAULT}}" +DKIM_ENABLE="${DKIM_ENABLE:-1}" +DKIM_SELECTOR="${DKIM_SELECTOR:-mwl1}" +DKIM_GENERATE="${DKIM_GENERATE:-1}" + +# BASE_DOMAIN/Subs aus FQDNs ableiten +if [[ "$MTA_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then MTA_SUB="${BASH_REMATCH[1]}"; BASE_DOMAIN="${BASH_REMATCH[2]}"; fi +if [[ "$UI_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then UI_SUB="${BASH_REMATCH[1]}"; fi +if [[ "$WEBMAIL_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then WEBMAIL_SUB="${BASH_REMATCH[1]}"; fi + +SYSMAIL_SUB="${SYSMAIL_SUB:-sysmail}" +SYSMAIL_DOMAIN="${SYSMAIL_SUB}.${BASE_DOMAIN}" + +MAIL_HOSTNAME="${MTA_FQDN}" +UI_HOST="${UI_FQDN}" +WEBMAIL_HOST="${WEBMAIL_FQDN}" + +APP_TZ="${APP_TZ:-$DEFAULT_TZ}" +APP_LOCALE="${APP_LOCALE:-$DEFAULT_LOCALE}" + +# ── Export & persist ───────────────────────────────────────── +export APP_NAME APP_USER APP_GROUP APP_USER_PREFIX APP_DIR +export BASE_DOMAIN UI_SUB WEBMAIL_SUB MTA_SUB +export SYSMAIL_SUB SYSMAIL_DOMAIN DKIM_ENABLE DKIM_SELECTOR DKIM_GENERATE +export UI_HOST WEBMAIL_HOST MAIL_HOSTNAME +export DB_NAME DB_USER +export SERVER_PUBLIC_IPV4 SERVER_PUBLIC_IPV6 APP_TZ APP_LOCALE +export CLAMAV_ENABLE OPENDMARC_ENABLE FAIL2BAN_ENABLE + +install -d -m 0755 /etc/mailwolt +cat >/etc/mailwolt/installer.env <>> Running ${STEP}.sh" + bash "./${STEP}.sh" +done + +##!/usr/bin/env bash +#set -euo pipefail +# +## --- Flags / Modi --- +#DEV_MODE=0 +#PROXY_MODE=0 +#NPM_IP="" +# +#while [[ $# -gt 0 ]]; do +# case "$1" in +# -dev) DEV_MODE=1 ;; +# -proxy) PROXY_MODE=1; NPM_IP="${2:-}"; shift ;; +# esac +# shift +#done +# +#APP_ENV="${APP_ENV:-$([[ $DEV_MODE -eq 1 ]] && echo local || echo production)}" +#APP_DEBUG="${APP_DEBUG:-$([[ $DEV_MODE -eq 1 ]] && echo true || echo false)}" +#export DEV_MODE PROXY_MODE NPM_IP APP_ENV APP_DEBUG +# +#DB_PASS="${DB_PASS:-$(openssl rand -hex 16)}" +#REDIS_PASS="${REDIS_PASS:-$(openssl rand -hex 16)}" +# +#export DB_PASS REDIS_PASS +# +#cd "$(dirname "$0")" +#source ./lib.sh +#require_root +#header +# +## ── Defaults ──────────────────────────────────────────────────────────────── +#APP_NAME="${APP_NAME:-MailWolt}" +#APP_USER="${APP_USER:-mailwolt}" +#APP_GROUP="${APP_GROUP:-www-data}" +#APP_USER_PREFIX="${APP_USER_PREFIX:-mw}" +#APP_DIR="${APP_DIR:-/var/www/${APP_USER}}" +# +#BASE_DOMAIN="${BASE_DOMAIN:-example.com}" +#UI_SUB="${UI_SUB:-ui}" +#WEBMAIL_SUB="${WEBMAIL_SUB:-webmail}" +#MTA_SUB="${MTA_SUB:-mx}" +# +#DB_NAME="${DB_NAME:-${APP_USER}}" +#DB_USER="${DB_USER:-${APP_USER}}" +# +#SERVER_PUBLIC_IPV4="$(detect_ip)" +#SERVER_PUBLIC_IPV6="$(detect_ipv6)" +#DEFAULT_TZ="$(detect_timezone)" +#DEFAULT_LOCALE="$(guess_locale_from_tz "$DEFAULT_TZ")" +# +#echo -e "${GREY}Erkannte IP (v4): ${SERVER_PUBLIC_IPV4} v6: ${SERVER_PUBLIC_IPV6:-–}${NC}" +# +## ── Schöne, farbige Abfragen ──────────────────────────────────────────────── +#echo -e "${CYAN}" +#echo "──────────────────────────────────────────────" +#echo -e " 📧 MailWolt Setup – Domain Konfiguration" +#echo "──────────────────────────────────────────────" +#echo -e "${NC}" +# +#MTA_DEFAULT="${MTA_SUB}.${BASE_DOMAIN}" +#UI_DEFAULT="${UI_SUB}.${BASE_DOMAIN}" +#WEBMAIL_DEFAULT="${WEBMAIL_SUB}.${BASE_DOMAIN}" +# +#ask_domain() { +# local __outvar="$1" label="$2" example="$3" defval="$4" input="" +# echo -e "${GREEN}[?]${NC} ${label}" +# echo -e " z.B. ${YELLOW}${example}${NC}" +# echo -e " Default: ${CYAN}${defval}${NC}" +# echo -ne " → Eingabe: ${CYAN}" +# read -r input +# echo -e "${NC}" +# if [[ -z "$input" ]]; then +# eval "$__outvar='$defval'" +# else +# eval "$__outvar='$input'" +# fi +#} +# +#ask_toggle() { +# local __outvar="$1" label="$2" defval="${3:-1}" input="" +# echo -ne "${GREEN}[?]${NC} ${label} (${CYAN}1${NC}=Ja / ${YELLOW}0${NC}=Nein) [Enter=${defval}]: " +# read -r input +# input="${input:-$defval}" +# case "$input" in +# 1|0) ;; +# *) echo -e "${YELLOW}Ungültig, nehme Default=${defval}.${NC}"; input="$defval" ;; +# esac +# eval "$__outvar='$input'" +#} +# +#ask_domain "MTA_FQDN" "Mailserver-FQDN (MX)" "mx.domain.tld" "$MTA_DEFAULT" +#ask_domain "UI_FQDN" "UI / Admin-Panel" "ui.domain.tld" "$UI_DEFAULT" +#ask_domain "WEBMAIL_FQDN" "Webmail-FQDN" "webmail.domain.tld" "$WEBMAIL_DEFAULT" +# +#echo -e "${CYAN}" +#echo "──────────────────────────────────────────────" +#echo -e " 🛡 Optionale Dienste" +#echo "──────────────────────────────────────────────" +#echo -e "${NC}" +# +#ask_toggle "CLAMAV_ENABLE" "ClamAV Virenscan aktivieren?" 1 +#ask_toggle "OPENDMARC_ENABLE" "OpenDMARC auswerten?" 1 +#ask_toggle "FAIL2BAN_ENABLE" "Fail2Ban aktivieren?" 1 +#echo +# +## Defaults, wenn Enter gedrückt (Abwärtskompatibilität) +#MTA_FQDN="${MTA_FQDN:-${MTA_SUB}.${BASE_DOMAIN}}" +#UI_FQDN="${UI_FQDN:-${UI_SUB}.${BASE_DOMAIN}}" +#WEBMAIL_FQDN="${WEBMAIL_FQDN:-${WEBMAIL_SUB}.${BASE_DOMAIN}}" +#DKIM_ENABLE="${DKIM_ENABLE:-1}" +#DKIM_SELECTOR="${DKIM_SELECTOR:-mwl1}" +#DKIM_GENERATE="${DKIM_GENERATE:-1}" +# +## BASE_DOMAIN und Sub-Labels aus MTA/UI/WEBMAIL ableiten (robust) +#if [[ "$MTA_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then +# MTA_SUB="${BASH_REMATCH[1]}" +# BASE_DOMAIN="${BASH_REMATCH[2]}" +#fi +#if [[ "$UI_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then +# UI_SUB="${BASH_REMATCH[1]}" +#fi +#if [[ "$WEBMAIL_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then +# WEBMAIL_SUB="${BASH_REMATCH[1]}" +#fi +# +#SYSMAIL_SUB="${SYSMAIL_SUB:-sysmail}" +#SYSMAIL_DOMAIN="${SYSMAIL_SUB}.${BASE_DOMAIN}" +## Kanonische Host-Variablen (NIE wieder zusammenbauen – nimm die FQDNs) +#MAIL_HOSTNAME="${MTA_FQDN}" +#UI_HOST="${UI_FQDN}" +#WEBMAIL_HOST="${WEBMAIL_FQDN}" +# +## Zeitzone/Locale sinnvoll setzen +#APP_TZ="${APP_TZ:-$DEFAULT_TZ}" +#APP_LOCALE="${APP_LOCALE:-$DEFAULT_LOCALE}" +# +## ── Variablen exportieren ─────────────────────────────────────────────────── +#export APP_NAME APP_USER APP_GROUP APP_USER_PREFIX APP_DIR +#export BASE_DOMAIN UI_SUB WEBMAIL_SUB MTA_SUB +#export SYSMAIL_SUB SYSMAIL_DOMAIN DKIM_ENABLE DKIM_SELECTOR DKIM_GENERATE +#export UI_HOST WEBMAIL_HOST MAIL_HOSTNAME +#export DB_NAME DB_USER +#export SERVER_PUBLIC_IPV4 SERVER_PUBLIC_IPV6 APP_TZ APP_LOCALE +#export CLAMAV_ENABLE OPENDMARC_ENABLE FAIL2BAN_ENABLE +# +#install -d -m 0755 /etc/mailwolt +#cat >/etc/mailwolt/installer.env <>> Running ${STEP}.sh" +# bash "./${STEP}.sh" +#done +###!/usr/bin/env bash +##set -euo pipefail +## +### --- Flags / Modi --- +##DEV_MODE=0 +##PROXY_MODE=0 +##NPM_IP="" +## +##while [[ $# -gt 0 ]]; do +## case "$1" in +## -dev) DEV_MODE=1 ;; +## -proxy) PROXY_MODE=1; NPM_IP="${2:-}"; shift ;; +## esac +## shift +##done +## +##APP_ENV="${APP_ENV:-$([[ $DEV_MODE -eq 1 ]] && echo local || echo production)}" +##APP_DEBUG="${APP_DEBUG:-$([[ $DEV_MODE -eq 1 ]] && echo true || echo false)}" +##export DEV_MODE PROXY_MODE NPM_IP APP_ENV APP_DEBUG +## +##DB_PASS="${DB_PASS:-$(openssl rand -hex 16)}" +##REDIS_PASS="${REDIS_PASS:-$(openssl rand -hex 16)}" +## +##export DB_PASS REDIS_PASS +## +##cd "$(dirname "$0")" +##source ./lib.sh +##require_root +##header +## +### ── Defaults ──────────────────────────────────────────────────────────────── +##APP_NAME="${APP_NAME:-MailWolt}" +##APP_USER="${APP_USER:-mailwolt}" +##APP_GROUP="${APP_GROUP:-www-data}" +##APP_USER_PREFIX="${APP_USER_PREFIX:-mw}" +##APP_DIR="${APP_DIR:-/var/www/${APP_USER}}" +## +##BASE_DOMAIN="${BASE_DOMAIN:-example.com}" +##UI_SUB="${UI_SUB:-ui}" +##WEBMAIL_SUB="${WEBMAIL_SUB:-webmail}" +##MTA_SUB="${MTA_SUB:-mx}" +## +##DB_NAME="${DB_NAME:-${APP_USER}}" +##DB_USER="${DB_USER:-${APP_USER}}" +## +##SERVER_PUBLIC_IPV4="$(detect_ip)" +##SERVER_PUBLIC_IPV6="$(detect_ipv6)" +##DEFAULT_TZ="$(detect_timezone)" +##DEFAULT_LOCALE="$(guess_locale_from_tz "$DEFAULT_TZ")" +## +##echo -e "${GREY}Erkannte IP (v4): ${SERVER_PUBLIC_IPV4} v6: ${SERVER_PUBLIC_IPV6:-–}${NC}" +## +### ── FQDNs abfragen ─────────────────────────────────────────────────────────── +##read -r -p "Mailserver FQDN (MX, z.B. mx.domain.tld) [Enter=${MTA_SUB}.${BASE_DOMAIN}]: " MTA_FQDN +##read -r -p "UI / Admin-Panel FQDN (z.B. ui.domain.tld) [Enter=${UI_SUB}.${BASE_DOMAIN}]: " UI_FQDN +##read -r -p "Webmail FQDN (z.B. webmail.domain.tld) [Enter=${WEBMAIL_SUB}.${BASE_DOMAIN}]: " WEBMAIL_FQDN +## +### Defaults, wenn Enter gedrückt +##MTA_FQDN="${MTA_FQDN:-${MTA_SUB}.${BASE_DOMAIN}}" +##UI_FQDN="${UI_FQDN:-${UI_SUB}.${BASE_DOMAIN}}" +##WEBMAIL_FQDN="${WEBMAIL_FQDN:-${WEBMAIL_SUB}.${BASE_DOMAIN}}" +##DKIM_ENABLE="${DKIM_ENABLE:-1}" +##DKIM_SELECTOR="${DKIM_SELECTOR:-mwl1}" +##DKIM_GENERATE="${DKIM_GENERATE:-1}" +## +### BASE_DOMAIN und Sub-Labels aus MTA/UI/WEBMAIL ableiten (robust) +##if [[ "$MTA_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then +## MTA_SUB="${BASH_REMATCH[1]}" +## BASE_DOMAIN="${BASH_REMATCH[2]}" +##fi +##if [[ "$UI_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then +## UI_SUB="${BASH_REMATCH[1]}" +##fi +##if [[ "$WEBMAIL_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then +## WEBMAIL_SUB="${BASH_REMATCH[1]}" +##fi +## +##SYSMAIL_SUB="${SYSMAIL_SUB:-sysmail}" +##SYSMAIL_DOMAIN="${SYSMAIL_SUB}.${BASE_DOMAIN}" +### Kanonische Host-Variablen (NIE wieder zusammenbauen – nimm die FQDNs) +##MAIL_HOSTNAME="${MTA_FQDN}" +##UI_HOST="${UI_FQDN}" +##WEBMAIL_HOST="${WEBMAIL_FQDN}" +## +### Zeitzone/Locale sinnvoll setzen (könntest du auch noch abfragen) +##APP_TZ="${APP_TZ:-$DEFAULT_TZ}" +##APP_LOCALE="${APP_LOCALE:-$DEFAULT_LOCALE}" +## +### ── Variablen exportieren ─────────────────────────────────────────────────── +##export APP_NAME APP_USER APP_GROUP APP_USER_PREFIX APP_DIR +##export BASE_DOMAIN UI_SUB WEBMAIL_SUB MTA_SUB +##export SYSMAIL_SUB SYSMAIL_DOMAIN DKIM_ENABLE DKIM_SELECTOR DKIM_GENERATE +##export UI_HOST WEBMAIL_HOST MAIL_HOSTNAME +##export DB_NAME DB_USER +##export SERVER_PUBLIC_IPV4 SERVER_PUBLIC_IPV6 APP_TZ APP_LOCALE +## +##install -d -m 0755 /etc/mailwolt +##cat >/etc/mailwolt/installer.env <>> Running ${STEP}.sh" +## bash "./${STEP}.sh" +##done \ No newline at end of file diff --git a/scripts/install-wrapper.sh b/scripts/install-wrapper.sh new file mode 100644 index 0000000..9d8040c --- /dev/null +++ b/scripts/install-wrapper.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Mailwolt Installer-Wrapper +# Deploy to: /usr/local/sbin/mailwolt-install +# Permissions: chmod 0755, chown root:root +set -euo pipefail + +LOG="/var/log/mailwolt-install.log" +STATE_DIR="/var/lib/mailwolt/install" +INSTALLER_SCRIPT="/var/www/mailwolt/mailwolt-installer/install.sh" +APP_DIR="/var/www/mailwolt" + +install -d -m 0755 "$STATE_DIR" /var/lib/mailwolt /var/lib/mailwolt/wizard +chown www-data:www-data /var/lib/mailwolt/wizard +: > "$LOG" +chmod 0644 "$LOG" + +echo "running" > "$STATE_DIR/state" +: > "$STATE_DIR/rc" + +{ + echo "===== $(date -Is) :: Installation gestartet =====" + + if [[ "$(id -u)" -ne 0 ]]; then + echo "[!] Muss als root laufen" + printf '1\n' > "$STATE_DIR/rc" + echo "done" > "$STATE_DIR/state" + exit 1 + fi + + # Komponente aus $1, falls übergeben (z.B. "nginx", "postfix", "dovecot", "all") + COMPONENT="${1:-all}" + echo "[i] Komponente: $COMPONENT" + + RC=0 + if [[ -f "$INSTALLER_SCRIPT" ]]; then + APP_DIR="$APP_DIR" COMPONENT="$COMPONENT" bash "$INSTALLER_SCRIPT" || RC=$? + else + echo "[!] installer script nicht gefunden: $INSTALLER_SCRIPT" + RC=127 + fi + + echo "===== $(date -Is) :: Installation beendet (rc=$RC) =====" + printf '%s\n' "$RC" > "$STATE_DIR/rc" + echo "done" > "$STATE_DIR/state" + exit "$RC" +} 2>&1 | tee -a "$LOG" diff --git a/scripts/lib.sh b/scripts/lib.sh new file mode 100644 index 0000000..cfa30c4 --- /dev/null +++ b/scripts/lib.sh @@ -0,0 +1,189 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ -f /etc/mailwolt/installer.env ]; then + set -a + . /etc/mailwolt/installer.env + set +a +fi +# ── Styling ──────────────────────────────────────────────────────────────── +GREEN="$(printf '\033[1;32m')"; YELLOW="$(printf '\033[1;33m')" +RED="$(printf '\033[1;31m')"; CYAN="$(printf '\033[1;36m')" +GREY="$(printf '\033[0;90m')"; NC="$(printf '\033[0m')" +BAR="──────────────────────────────────────────────────────────────────────────────" +log(){ echo -e "${GREEN}[+]${NC} $*"; } +warn(){ echo -e "${YELLOW}[!]${NC} $*"; } +err(){ echo -e "${RED}[x]${NC} $*"; } +die(){ err "$*"; exit 1; } +require_root(){ [[ "$(id -u)" -eq 0 ]] || die "Bitte als root ausführen."; } + +# --- Defaults, nur wenn noch nicht gesetzt --------------------------------- +: "${APP_USER:=mailwolt}" +: "${APP_GROUP:=www-data}" +: "${APP_DIR:=/var/www/${APP_USER}}" + +: "${APP_NAME:=MailWolt}" + +: "${BASE_DOMAIN:=example.com}" +: "${UI_SUB:=ui}" +: "${WEBMAIL_SUB:=webmail}" +: "${MTA_SUB:=mx}" + +# DB / Redis (werden später durch .env überschrieben) +: "${DB_NAME:=${APP_USER}}" +: "${DB_USER:=${APP_USER}}" +: "${DB_PASS:=}" +: "${REDIS_PASS:=}" + +# Stabile Zert-Pfade (UI/WEBMAIL/MX → symlinked via 20-ssl.sh) +: "${MAIL_SSL_DIR:=/etc/ssl/mail}" +: "${UI_SSL_DIR:=/etc/ssl/ui}" +: "${WEBMAIL_SSL_DIR:=/etc/ssl/webmail}" +: "${UI_CERT:=${UI_SSL_DIR}/fullchain.pem}" +: "${UI_KEY:=${UI_SSL_DIR}/privkey.pem}" + +# Optional: E-Mail für LE +: "${LE_EMAIL:=admin@${BASE_DOMAIN}}" + +load_env_file(){ + local f="$1" + [[ -f "$f" ]] || return 0 + while IFS='=' read -r k v; do + [[ "$k" =~ ^[A-Z0-9_]+$ ]] || continue + export "$k=$v" + done < <(grep -E '^[A-Z0-9_]+=' "$f") +} + +header(){ echo -e "${CYAN}${BAR}${NC} +${CYAN} :::: :::: ::: ::::::::::: ::: ::: ::: :::::::: ::: ::::::::::: +${CYAN} +:+:+: :+:+:+ :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: +${CYAN} +:+ +:+:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +${CYAN} +#+ +:+ +#+ +#++:++#++: +#+ +#+ +#+ +:+ +#+ +#+ +:+ +#+ +#+ +${CYAN} +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+#+ +#+ +#+ +#+ +#+ +#+ +${CYAN} #+# #+# #+# #+# #+# #+# #+#+# #+#+# #+# #+# #+# #+# +${CYAN} ### ### ### ### ########### ########## ### ### ######## ########## ### +${CYAN} ${CYAN}${BAR}${NC}\n"; } + +#header(){ echo -e "${CYAN}${BAR}${NC} +#${CYAN} 888b d888 d8b 888 888 888 888 888 +#${CYAN} 8888b d8888 Y8P 888 888 o 888 888 888 +#${CYAN} 88888b.d88888 888 888 d8b 888 888 888 +#${CYAN} 888Y88888P888 8888b. 888 888 888 d888b 888 .d88b. 888 888888 +#${CYAN} 888 Y888P 888 '88b 888 888 888d88888b888 d88''88b 888 888 +#${CYAN} 888 Y8P 888 .d888888 888 888 88888P Y88888 888 888 888 888 +#${CYAN} 888 ' 888 888 888 888 888 8888P Y8888 Y88..88P 888 Y88b. +#${CYAN} 888 888 'Y888888 888 888 888P Y888 'Y88P' 888 'Y888 +#${CYAN}${BAR}${NC}\n"; } + +detect_ip(){ + local ip + ip="$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src"){print $(i+1); exit}}')" || true + [[ -n "${ip:-}" ]] || ip="$(hostname -I 2>/dev/null | awk '{print $1}')" + [[ -n "${ip:-}" ]] || die "Konnte Server-IP nicht ermitteln." + echo "$ip" +} +detect_ipv4() { + local ext="" + if command -v curl >/dev/null 2>&1; then + ext="$(curl -fsS --max-time 2 https://ifconfig.me 2>/dev/null || true)" + [[ "$ext" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || ext="" + fi + echo "$ext" +} +detect_ipv6(){ + local ip6 + ip6="$(ip -6 addr show scope global 2>/dev/null | awk '/inet6/{print $2}' | cut -d/ -f1 | head -n1)" || true + [[ -n "${ip6:-}" ]] || ip6="$(hostname -I 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i ~ /:/){print $i; exit}}')" || true + echo "${ip6:-}" +} +detect_timezone(){ + local tz + if command -v timedatectl >/dev/null 2>&1; then + tz="$(timedatectl show -p Timezone --value 2>/dev/null | tr -d '[:space:]')" || true + [[ -n "${tz:-}" && "$tz" == */* ]] && { echo "$tz"; return; } + fi + [[ -r /etc/timezone ]] && { tz="$(sed -n '1p' /etc/timezone | tr -d '[:space:]')" || true; [[ "$tz" == */* ]] && { echo "$tz"; return; }; } + if [[ -L /etc/localtime ]]; then + tz="$(readlink -f /etc/localtime 2>/dev/null || true)"; tz="${tz#/usr/share/zoneinfo/}" + [[ "$tz" == */* ]] && { echo "$tz"; return; } + fi + if command -v curl >/dev/null 2>&1; then + tz="$(curl -fsSL --max-time 3 https://ipapi.co/timezone 2>/dev/null || true)"; [[ "$tz" == */* ]] && { echo "$tz"; return; } + fi + echo "UTC" +} +guess_locale_from_tz(){ case "${1:-UTC}" in + Europe/Berlin|Europe/Vienna|Europe/Zurich|Europe/Luxembourg|Europe/Brussels|Europe/Amsterdam) echo "de";; + *) echo "en";; esac; } + +resolve_ok(){ local host="$1"; getent ahosts "$host" | awk '{print $1}' | sort -u | grep -q -F "${SERVER_PUBLIC_IPV4:-}" ; } +join_host(){ local sub="$1" base="$2"; [[ -z "$sub" ]] && echo "$base" || echo "$sub.$base"; } + +# dns_preflight HOST [HOST2 ...] +# Prüft: A-Record → SERVER_PUBLIC_IPV4, MX (nur wenn HOST == MAIL_HOSTNAME), PTR. +# Gibt strukturierte Zeilen aus: OK|WARN|FAIL +# Rückgabe 0 = alles OK; 1 = mind. ein FAIL. +dns_preflight(){ + local overall=0 + local server_ip="${SERVER_PUBLIC_IPV4:-}" + + _dns_line(){ local level="$1" host="$2" check="$3" detail="$4" + case "$level" in + OK) echo -e "${GREEN}[DNS OK ]${NC} ${host} ${GREY}${check}${NC} → ${detail}" ;; + WARN) echo -e "${YELLOW}[DNS WARN]${NC} ${host} ${GREY}${check}${NC} → ${detail}" ;; + FAIL) echo -e "${RED}[DNS FAIL]${NC} ${host} ${GREY}${check}${NC} → ${detail}"; overall=1 ;; + esac + } + + for host in "$@"; do + [[ -z "$host" || "$host" == "example.com" ]] && continue + + # A-Record + local a_ip + a_ip="$(dig +short A "$host" @1.1.1.1 2>/dev/null | grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -n1)" + if [[ -z "$a_ip" ]]; then + _dns_line FAIL "$host" "A-Record" "kein Eintrag gefunden" + elif [[ -n "$server_ip" && "$a_ip" != "$server_ip" ]]; then + _dns_line FAIL "$host" "A-Record" "${a_ip} ≠ ${server_ip} (Server-IP)" + else + _dns_line OK "$host" "A-Record" "${a_ip}" + fi + + # MX (nur für MAIL_HOSTNAME) + if [[ "$host" == "${MAIL_HOSTNAME:-}" ]]; then + local mx + mx="$(dig +short MX "$host" @1.1.1.1 2>/dev/null | awk '{print $2}' | head -n1)" + if [[ -z "$mx" ]]; then + _dns_line WARN "$host" "MX-Record" "kein Eintrag – ausgehende Mail ggf. eingeschränkt" + else + _dns_line OK "$host" "MX-Record" "${mx}" + fi + fi + + # PTR (nur wenn IP bekannt) + if [[ -n "$server_ip" && -n "$a_ip" && "$a_ip" == "$server_ip" ]]; then + local ptr + ptr="$(dig +short -x "$server_ip" @1.1.1.1 2>/dev/null | head -n1 | sed 's/\.$//')" + if [[ -z "$ptr" ]]; then + _dns_line WARN "$host" "PTR-Record" "kein Reverse-DNS – kann Spam-Score erhöhen" + elif [[ "$ptr" != "$host" && "$ptr" != "${MAIL_HOSTNAME:-}" ]]; then + _dns_line WARN "$host" "PTR-Record" "${ptr} (zeigt nicht auf ${host})" + else + _dns_line OK "$host" "PTR-Record" "${ptr}" + fi + fi + done + + return $overall +} + +upsert_env(){ # upsert in $ENV_FILE + local k="$1" v="$2" ek ev + ek="$(printf '%s' "$k" | sed -e 's/[.[\*^$(){}+?|/]/\\&/g')" + ev="$(printf '%s' "$v" | sed -e 's/[&/]/\\&/g')" + if grep -qE "^[#[:space:]]*${ek}=" "$ENV_FILE" 2>/dev/null; then + sed -Ei "s|^[#[:space:]]*${ek}=.*|${k}=${ev}|g" "$ENV_FILE" + else + printf '%s=%s\n' "$k" "$v" >> "$ENV_FILE" + fi +} diff --git a/scripts/phase2-go-live.sh b/scripts/phase2-go-live.sh new file mode 100755 index 0000000..af9692c --- /dev/null +++ b/scripts/phase2-go-live.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +# Phase 2: Go-Live — DNS-Check → LE-Zertifikate → Nginx → Postfix/Dovecot aktualisieren +# Muss als root ausgeführt werden, NACHDEM die DNS-Einträge gesetzt wurden. +# Liest Konfiguration aus /etc/mailwolt/installer.env (durch Phase 1 geschrieben). +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/lib.sh" + +require_root + +ENV_FILE="/etc/mailwolt/installer.env" +[[ -f "$ENV_FILE" ]] || die "Phase 1 noch nicht abgeschlossen – ${ENV_FILE} fehlt." + +set -a; . "$ENV_FILE"; set +a + +# ── Pflichtfelder ───────────────────────────────────────────────────────────── +: "${BASE_DOMAIN:?BASE_DOMAIN fehlt in ${ENV_FILE}}" +: "${UI_HOST:?UI_HOST fehlt in ${ENV_FILE}}" +: "${WEBMAIL_HOST:?WEBMAIL_HOST fehlt in ${ENV_FILE}}" +: "${MAIL_HOSTNAME:?MAIL_HOSTNAME fehlt in ${ENV_FILE}}" +: "${SERVER_PUBLIC_IPV4:?SERVER_PUBLIC_IPV4 fehlt in ${ENV_FILE}}" + +header +echo -e "${CYAN} Phase 2 – Go-Live${NC}" +echo -e "${CYAN}${BAR}${NC}" +echo + +# ── Schritt 1: DNS Preflight ────────────────────────────────────────────────── +log "DNS-Vorab-Check …" + +SKIP_DNS="${SKIP_DNS:-0}" +dns_ok=0 + +if [[ "$SKIP_DNS" != "1" ]]; then + if dns_preflight "$UI_HOST" "$WEBMAIL_HOST" "$MAIL_HOSTNAME"; then + dns_ok=1 + else + warn "Einige DNS-Einträge zeigen noch nicht auf diesen Server." + echo + echo -e " Möglichkeiten:" + echo -e " a) DNS reparieren und dieses Skript erneut ausführen." + echo -e " b) Trotzdem fortfahren: SKIP_DNS=1 bash phase2-go-live.sh" + echo + read -rp "Trotzdem fortfahren? [j/N] " _ans + [[ "${_ans,,}" == "j" ]] || die "Abgebrochen." + dns_ok=1 + fi +else + warn "DNS-Check übersprungen (SKIP_DNS=1)." + dns_ok=1 +fi + +echo + +# ── Schritt 2: Let's Encrypt Zertifikate ───────────────────────────────────── +log "Let's Encrypt Zertifikate ausstellen …" +bash "${SCRIPT_DIR}/75-le-issue.sh" +echo + +# ── Schritt 3: Nginx-Konfiguration neu schreiben (TLS) ─────────────────────── +log "Nginx-Konfiguration aktualisieren (TLS) …" +# Nginx-Builder aus 70-nginx.sh wiederverwenden +source "${SCRIPT_DIR}/70-nginx.sh" || true # sourcing setzt Variablen und führt aus +echo + +# ── Schritt 4: Postfix hostname + TLS-Zertifikate aktualisieren ────────────── +log "Postfix: myhostname = ${MAIL_HOSTNAME} …" +postconf -e "myhostname = ${MAIL_HOSTNAME}" +postconf -e "myorigin = \$myhostname" +postconf -e "smtpd_tls_cert_file = /etc/ssl/mail/fullchain.pem" +postconf -e "smtpd_tls_key_file = /etc/ssl/mail/privkey.pem" +systemctl reload postfix || true + +# ── Schritt 5: Dovecot TLS ─────────────────────────────────────────────────── +log "Dovecot: TLS-Zertifikate aktualisieren …" +if [[ -f /etc/dovecot/conf.d/10-ssl.conf ]]; then + sed -i "s|^ssl_cert =.*|ssl_cert = /dev/null || systemctl restart "$u" + return 0 + fi + done +} + +artisan_down(){ + as_app "cd ${APP_DIR} && php artisan down --retry=10 --render=errors.503" 2>/dev/null || true + echo "[i] Wartungsmodus aktiviert" +} + +artisan_up(){ + as_app "cd ${APP_DIR} && php artisan up" 2>/dev/null || true + echo "[i] Wartungsmodus beendet" +} + +git_safe(){ + # Falls nötig: Repo als safe markieren (z.B. wenn Root den Wrapper aufruft) + as_app "git -C ${APP_DIR} config --global --add safe.directory ${APP_DIR} >/dev/null 2>&1 || true" +} + +git_dirty_check(){ + if [[ "$ALLOW_DIRTY" != "1" ]]; then + local dirty + dirty="$(as_app "git -C ${APP_DIR} status --porcelain")" + if [[ -n "$dirty" ]]; then + echo "[!] Arbeitsbaum hat uncommitted Änderungen. Abbruch (ALLOW_DIRTY=1 zum Überschreiben)." + exit 2 + fi + fi +} + +get_version(){ + as_app "cd ${APP_DIR} && (git describe --tags --always 2>/dev/null || git rev-parse --short=7 HEAD)" +} + +write_build_info(){ + local ver="$1" rev="$2" + install -d -m 0755 /etc/mailwolt || true + printf "version=%s\nrev=%s\nupdated=%s\n" "$ver" "$rev" "$(date -Is)" > /etc/mailwolt/build.info || true +} + +# --- Frontend build (quiet & robust) ------------------------------------------ +frontend_build_quiet() { + local LOG="/var/log/mailwolt-frontend-build.log" + local NPM_ENV="CI=1 NPM_CONFIG_FUND=false NPM_CONFIG_AUDIT=false npm_config_loglevel=warn" + + # Logfile vorbereiten + install -d -m 0755 "$(dirname "$LOG")" + : > "$LOG" || true + chmod 0644 "$LOG" + + echo "[i] Frontend: vorbereiten …" + # Build-Ziele & Temp besitzbar machen + as_app "mkdir -p '${APP_DIR}/public/build' '${APP_DIR}/node_modules' '${APP_DIR}/.vite' '${APP_DIR}/.npm-cache'" + chown -R "$APP_USER":"$APP_GROUP" "${APP_DIR}/public/build" "${APP_DIR}/node_modules" "${APP_DIR}/.vite" "${APP_DIR}/.npm-cache" || true + chmod -R g+rwX "${APP_DIR}/public/build" "${APP_DIR}/node_modules" "${APP_DIR}/.vite" || true + + # evtl. störrische Reste entfernen (vermeidet EACCES beim Unlink/Rimraf) + rm -rf "${APP_DIR}/node_modules/.vite" "${APP_DIR}/public/build/"* 2>/dev/null || true + + # npm Config (kein Fund/Audit, eigener Cache unter App-User) + as_app "printf 'fund=false\naudit=false\ncache=${APP_DIR}/.npm-cache\n' > ~/.npmrc" >>"$LOG" 2>&1 || true + + echo "[i] Frontend: Dependencies … (Details: $LOG)" + if ! as_app "cd '${APP_DIR}' && ${NPM_ENV} npm ci --no-audit --no-fund --no-progress" >>"$LOG" 2>&1; then + if ! as_app "cd '${APP_DIR}' && ${NPM_ENV} npm install --no-audit --no-fund --no-progress" >>"$LOG" 2>&1; then + echo "[!] npm install fehlgeschlagen. Letzte 60 Zeilen:" + tail -n 60 "$LOG" || true + return 1 + fi + fi + + echo "[i] Frontend: Build … (Details: $LOG)" + if ! as_app "cd '${APP_DIR}' && ${NPM_ENV} npm run build --silent --loglevel=warn" >>"$LOG" 2>&1; then + echo "[!] Build fehlgeschlagen. Letzte 80 Zeilen:" + tail -n 80 "$LOG" || true + return 1 + fi + + # Nach dem Build noch einmal Rechte begradigen + chown -R "$APP_USER":"$APP_GROUP" "${APP_DIR}/public/build" || true + find "${APP_DIR}/public/build" -type d -exec chmod 2775 {} \; 2>/dev/null || true + find "${APP_DIR}/public/build" -type f -exec chmod 0664 {} \; 2>/dev/null || true + + echo "[✓] Frontend gebaut." +} + +# -------- Guards -------------------------------------------------------------- +[[ "$(id -u)" -eq 0 ]] || { echo "[!] Bitte als root ausführen"; exit 1; } +[[ -d "$APP_DIR/.git" ]] || { echo "[!] $APP_DIR scheint kein Git-Repo zu sein"; exit 1; } + +git_safe +git_dirty_check + +# -------- Git: neuen Stand holen --------------------------------------------- +echo "[i] Prüfe Repository …" +OLD_REV="$(as_app "git -C ${APP_DIR} rev-parse HEAD")" +OLD_VER="$(get_version)" +NEW_REV="$OLD_REV" + +if [[ "$MODE" = "tags" ]]; then + # → Neueste Tags holen + as_app "git -C ${APP_DIR} fetch --quiet origin && git -C ${APP_DIR} fetch --tags --quiet origin || true" + LATEST_TAG="$(as_app "git -C ${APP_DIR} tag --list | sort -V | tail -n1")" + if [[ -z "$LATEST_TAG" ]]; then + echo "[!] Keine Tags gefunden – falle auf origin/${BRANCH} zurück" + as_app "git -C ${APP_DIR} checkout -q ${BRANCH} && git -C ${APP_DIR} pull --ff-only origin ${BRANCH}" + else + TARGET_REV="$(as_app "git -C ${APP_DIR} rev-list -n1 ${LATEST_TAG}")" + if [[ "$TARGET_REV" = "$OLD_REV" ]]; then + echo "[✓] Bereits auf neuestem Release (${LATEST_TAG}) – nichts zu tun." + write_build_info "$(get_version)" "$OLD_REV" + exit 0 + fi + echo "[i] Checkout auf Release ${LATEST_TAG} (${TARGET_REV:0:7}) …" + as_app "git -C ${APP_DIR} checkout -q ${LATEST_TAG}" + fi + NEW_REV="$(as_app "git -C ${APP_DIR} rev-parse HEAD")" +else + # Rolling: branch folgen + as_app "git -C ${APP_DIR} fetch --quiet origin ${BRANCH}" + BEHIND="$(as_app "git -C ${APP_DIR} rev-list --count HEAD..origin/${BRANCH} || echo 0")" + if [[ "$BEHIND" -eq 0 ]]; then + echo "[✓] Branch origin/${BRANCH} ist bereits aktuell – nichts zu tun." + write_build_info "$(get_version)" "$OLD_REV" + exit 0 + fi + echo "[i] Es gibt ${BEHIND} neue Commit(s) – ziehe Änderungen …" + as_app "git -C ${APP_DIR} checkout -q ${BRANCH} && git -C ${APP_DIR} pull --ff-only origin ${BRANCH}" + NEW_REV="$(as_app "git -C ${APP_DIR} rev-parse HEAD")" +fi + +# -------- Änderungstypen ermitteln ------------------------------------------- +CHANGED_FILES="$(as_app "git -C ${APP_DIR} diff --name-only ${OLD_REV}..${NEW_REV}")" + +NEED_COMPOSER=0 +NEED_MIGRATIONS=0 +NEED_FRONTEND=0 +NEED_PHP_RESTART=0 + +echo "$CHANGED_FILES" | grep -qE '(^|/)composer\.(json|lock)$' && NEED_COMPOSER=1 +echo "$CHANGED_FILES" | grep -qE '^database/migrations/' && NEED_MIGRATIONS=1 +echo "$CHANGED_FILES" | grep -qE '^(package(-lock)?\.json|vite\.config(\.ts|\.js)?|resources/|public/.*\.(js|css))' && NEED_FRONTEND=1 +echo "$CHANGED_FILES" | grep -qE '^(app/|routes/|config/|resources/views/)' && NEED_PHP_RESTART=1 + +echo "[i] Zusammenfassung:" +echo " Composer : $([[ $NEED_COMPOSER -eq 1 ]] && echo JA || echo nein)" +echo " Migrations : $([[ $NEED_MIGRATIONS -eq 1 ]] && echo JA || echo nein)" +echo " Frontend : $([[ $NEED_FRONTEND -eq 1 ]] && echo JA || echo nein)" +echo " PHP restart : $([[ $NEED_PHP_RESTART -eq 1 ]] && echo JA || echo nein)" + +# Wenn gar nichts relevantes geändert wurde → sauber beenden +if [[ $NEED_COMPOSER -eq 0 && $NEED_MIGRATIONS -eq 0 && $NEED_FRONTEND -eq 0 && $NEED_PHP_RESTART -eq 0 ]]; then + echo "[✓] Code-Stand aktualisiert, aber keine Build/Runtime-Änderungen – keine Neustarts nötig." + NEW_VER="$(get_version)" + write_build_info "$NEW_VER" "$NEW_REV" + echo "[i] Version: ${OLD_VER} → ${NEW_VER}" + exit 0 +fi + +# -------- Composer (ohne Downtime) ------------------------------------------- +if [[ $NEED_COMPOSER -eq 1 ]]; then + echo "[i] Composer …" + as_app "cd ${APP_DIR} && composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev" +fi + +# -------- Frontend (ohne Downtime — Assets sind statisch) -------------------- +if [[ $NEED_FRONTEND -eq 1 ]]; then + echo "[i] Frontend-Änderungen erkannt – baue Assets …" + if ! frontend_build_quiet; then + echo "[!] Frontend-Build ist fehlgeschlagen (siehe /var/log/mailwolt-frontend-build.log)." + exit 1 + fi +fi + +# -------- Wartungsmodus: Migrations + Cache + PHP-FPM reload ----------------- +# Trap stellt sicher dass artisan up auch bei Fehler läuft +MAINTENANCE_ACTIVE=0 +cleanup_maintenance(){ + if [[ "$MAINTENANCE_ACTIVE" -eq 1 ]]; then + artisan_up + fi +} +trap cleanup_maintenance EXIT INT TERM + +if [[ $NEED_MIGRATIONS -eq 1 || $NEED_PHP_RESTART -eq 1 || $NEED_COMPOSER -eq 1 ]]; then + artisan_down + MAINTENANCE_ACTIVE=1 +fi + +if [[ $NEED_MIGRATIONS -eq 1 ]]; then + echo "[i] DB-Migrationen …" + as_app "cd ${APP_DIR} && php artisan migrate --force" +fi + +if [[ $NEED_PHP_RESTART -eq 1 || $NEED_COMPOSER -eq 1 || $NEED_MIGRATIONS -eq 1 ]]; then + echo "[i] Cache/Optimierungen …" + as_app "cd ${APP_DIR} && php artisan optimize:clear || true" + as_app "cd ${APP_DIR} && php artisan config:cache || true" + as_app "cd ${APP_DIR} && php artisan route:cache || true" + as_app "cd ${APP_DIR} && php artisan queue:restart || true" +fi + +# -------- Dienste neu laden (gezielt) ---------------------------------------- +echo "[i] Dienste neu laden …" +if [[ $NEED_PHP_RESTART -eq 1 || $NEED_COMPOSER -eq 1 || $NEED_MIGRATIONS -eq 1 ]]; then + restart_php_fpm + restart_if_exists "${APP_USER}-queue" + restart_if_exists "${APP_USER}-schedule" + restart_if_exists "${APP_USER}-ws" +fi +if [[ $NEED_FRONTEND -eq 1 || $NEED_PHP_RESTART -eq 1 ]]; then + reload_if_active nginx +fi + +# -------- Wartungsmodus beenden ---------------------------------------------- +if [[ "$MAINTENANCE_ACTIVE" -eq 1 ]]; then + artisan_up + MAINTENANCE_ACTIVE=0 +fi + +# -------- Build-Info ablegen -------------------------------------------------- +NEW_VER="$(get_version)" +write_build_info "$NEW_VER" "$NEW_REV" + +echo "[✓] Update abgeschlossen: ${OLD_REV:0:7} → ${NEW_REV:0:7} (Version: ${NEW_VER})" \ No newline at end of file diff --git a/tools/push.sh b/tools/push.sh new file mode 100644 index 0000000..67456ba --- /dev/null +++ b/tools/push.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -euo pipefail + +MSG="" +BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo main)" +REMOTE="${REMOTE:-origin}" + +usage(){ echo "Usage: $0 -m \"commit message\" [-b branch]"; exit 1; } + +while getopts ":m:b:" opt; do + case "$opt" in + m) MSG="$OPTARG" ;; + b) BRANCH="$OPTARG" ;; + *) usage ;; + esac +done +shift $((OPTIND-1)) + +[[ -n "$MSG" ]] || usage +git rev-parse --git-dir >/dev/null 2>&1 || { echo "[x] kein Git-Repo"; exit 1; } + +echo "[i] Add/Commit → $BRANCH" +git add -A +# Kein Commit, wenn nichts geändert ist +if ! git diff --cached --quiet; then + git commit -m "$MSG" +else + echo "[i] Nichts zu committen (Index leer) – pushe nur." +fi + +echo "[i] Push → $REMOTE/$BRANCH" +git push "$REMOTE" "$BRANCH" +echo "[✓] Done." \ No newline at end of file diff --git a/tools/release.sh b/tools/release.sh new file mode 100644 index 0000000..75869cb --- /dev/null +++ b/tools/release.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +set -euo pipefail + +REMOTE="${REMOTE:-origin}" +BRANCH="${BRANCH:-main}" +ALLOW_DIRTY=0 +VERSION="" +MSG="" + +usage(){ + echo "Usage: $0 [-m \"release notes\"] [-b branch] [--allow-dirty]" + exit 1 +} + +# Args +[[ $# -ge 1 ]] || usage +VERSION="$1"; shift || true +[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "[x] Ungültige Version: $VERSION (erwarte SemVer X.Y.Z)"; exit 1; } + +while [[ $# -gt 0 ]]; do + case "$1" in + -m) MSG="$2"; shift 2 ;; + -b) BRANCH="$2"; shift 2 ;; + --allow-dirty) ALLOW_DIRTY=1; shift ;; + *) usage ;; + esac +done + +git rev-parse --git-dir >/dev/null 2>&1 || { echo "[x] kein Git-Repo"; exit 1; } + +# Clean tree? +if [[ $ALLOW_DIRTY -eq 0 ]]; then + if ! git diff --quiet || ! git diff --cached --quiet; then + echo "[x] Arbeitsbaum nicht sauber. Committe oder nutze --allow-dirty." + exit 1 + fi +fi + +# Aktuellen Stand holen +git fetch --quiet "$REMOTE" --tags +# Branch check-out/sync +git checkout -q "$BRANCH" +git pull --ff-only "$REMOTE" "$BRANCH" + +TAG="v${VERSION}" +if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then + echo "[x] Tag ${TAG} existiert bereits." + exit 1 +fi + +LAST_TAG="$(git describe --tags --abbrev=0 2>/dev/null || true)" +if [[ -n "$LAST_TAG" ]]; then + CHANGELOG="$(git log --pretty=format:'- %s (%h)' "${LAST_TAG}..HEAD" || true)" +else + CHANGELOG="$(git log --pretty=format:'- %s (%h)' || true)" +fi +[[ -n "$CHANGELOG" ]] || CHANGELOG="- initial release content" + +# VERSION bumpen +echo -n "$VERSION" > VERSION +git add VERSION +git commit -m "chore(release): v${VERSION}" + +# Tag-Message bauen +if [[ -z "$MSG" ]]; then +read -r -d '' MSG <