mailwolt/installer.sh

689 lines
25 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#!/usr/bin/env bash
set -euo pipefail
##############################################
# MailWolt #
# Bootstrap Installer v1.0 #
##############################################
# ===== Branding & Pfade (einmal ändern, überall wirksam) =====
APP_NAME="${APP_NAME:-MailWolt}"
APP_ENV="production"
APP_DEBUG="false"
APP_USER="${APP_USER:-mailwolt}"
APP_GROUP="${APP_GROUP:-www-data}"
# Projektverzeichnis
APP_DIR="/var/www/${APP_USER}"
# Konfigbasis (statt /etc/falcomail -> /etc/${APP_USER})
CONF_BASE="/etc/${APP_USER}"
CERT_DIR="${CONF_BASE}/ssl"
CERT="${CERT_DIR}/cert.pem"
KEY="${CERT_DIR}/key.pem"
# Nginx vHost
NGINX_SITE="/etc/nginx/sites-available/${APP_USER}.conf"
NGINX_SITE_LINK="/etc/nginx/sites-enabled/${APP_USER}.conf"
# DB
DB_NAME="${DB_NAME:-${APP_USER}}"
DB_USER="${DB_USER:-${APP_USER}}"
# DB_PASS muss vorher generiert oder gesetzt werden
# Git (Platzhalter; später ersetzen)
GIT_REPO="${GIT_REPO:-https://git.nexlab.at/boban/mailwolt.git}"
GIT_BRANCH="${GIT_BRANCH:-main}"
# Node Setup (deb = apt Pakete; nodesource = NodeSource LTS)
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
}
footer_ok() {
local ip="$1"
local app_name="${2:-$APP_NAME}"
local app_dir="${3:-$APP_DIR}"
local cert_dir="${4:-$CERT_DIR}"
echo
echo -e "${GREEN}${BAR}${NC}"
echo -e "${GREEN}${app_name} Installation erfolgreich abgeschlossen${NC}"
echo -e "${GREEN}${BAR}${NC}"
echo -e ""
echo -e " ${CYAN}➜ Setup-Wizard jetzt öffnen:${NC}"
echo -e " ${CYAN}http://${ip}/setup${NC}"
echo -e " ${GREY}https://${ip}/setup${NC} (self-signed Zertifikat)"
echo -e ""
echo -e " Laravel Root: ${GREY}${app_dir}${NC}"
echo -e " Self-signed Cert: ${GREY}${cert_dir}/{cert.pem,key.pem}${NC}"
echo -e " Postfix/Dovecot: ${GREY}25, 465, 587, 110, 995, 143, 993${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; }; }
# ===== IP ermitteln =====
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"
}
# ===== Secrets =====
gen() { head -c 512 /dev/urandom | tr -dc 'A-Za-z0-9' | head -c "${1:-28}" || true; }
pw() { gen 28; }
short() { gen 16; }
# ===== Argument-Parsing =====
while [[ $# -gt 0 ]]; do
case "$1" in
-dev) APP_ENV="local"; APP_DEBUG="true" ;;
-stag|-staging) APP_ENV="staging"; APP_DEBUG="false" ;;
esac
shift
done
# ===== Start =====
require_root
header
SERVER_IP="$(detect_ip)"
APP_PW="${APP_PW:-$(pw)}"
MAIL_HOSTNAME="${MAIL_HOSTNAME:-"bootstrap.local"}" # Wizard setzt später FQDN
TZ="${TZ:-""}" # leer; Wizard setzt final
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 (fix für mariadb-common prompt) ----
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)"
#apt-get install -y \
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 php-sqlite3 unzip curl acl \
composer \
certbot python3-certbot-nginx \
fail2ban \
ca-certificates rsyslog sudo openssl netcat-openbsd monit git
# ===== Verzeichnisse / User =====
log "Verzeichnisse und Benutzer anlegen…"
mkdir -p ${CERT_DIR} /etc/postfix /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 www-data "$APP_USER"
# ===== Self-signed TLS (SAN = IP) =====
CERT="${CERT_DIR}/cert.pem"
KEY="${CERT_DIR}/key.pem"
OSSL_CFG="${CERT_DIR}/openssl.cnf"
if [ ! -s "$CERT" ] || [ ! -s "$KEY" ]; then
log "Erzeuge Self-Signed TLS Zertifikat (SAN=IP:${SERVER_IP})…"
cat > "$OSSL_CFG" <<CFG
[req]
default_bits = 2048
prompt = no
default_md = sha256
req_extensions = req_ext
distinguished_name = dn
[dn]
CN = ${SERVER_IP}
O = ${APP_NAME}
C = DE
[req_ext]
subjectAltName = @alt_names
[alt_names]
IP.1 = ${SERVER_IP}
CFG
openssl req -x509 -newkey rsa:2048 -days 825 -nodes \
-keyout "$KEY" -out "$CERT" -config "$OSSL_CFG"
chmod 600 "$KEY"; chmod 644 "$CERT"
fi
# ===== MariaDB vorbereiten =====
log "MariaDB vorbereiten…"
systemctl enable --now mariadb
DB_PASS="$(pw)"
mysql -uroot <<SQL
CREATE DATABASE IF NOT EXISTS ${DB_NAME} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASS}';
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'localhost';
FLUSH PRIVILEGES;
SQL
# ===== Postfix konfigurieren (25/465/587) =====
log "Postfix konfigurieren…"
postconf -e "myhostname = ${MAIL_HOSTNAME}"
postconf -e "myorigin = \$myhostname"
postconf -e "mydestination = "
postconf -e "inet_interfaces = all"
postconf -e "inet_protocols = ipv4"
postconf -e "smtpd_banner = \$myhostname ESMTP"
postconf -e "smtp_dns_support_level = disabled"
postconf -e "smtpd_tls_cert_file = ${CERT}"
postconf -e "smtpd_tls_key_file = ${KEY}"
postconf -e "smtpd_tls_security_level = may"
postconf -e "smtpd_tls_auth_only = yes"
postconf -e "smtp_tls_security_level = may"
# Rspamd + OpenDKIM als Milter (accept)
postconf -e "milter_default_action = accept"
postconf -e "milter_protocol = 6"
postconf -e "smtpd_milters = inet:127.0.0.1:11332, inet:127.0.0.1:8891"
postconf -e "non_smtpd_milters = inet:127.0.0.1:11332, inet:127.0.0.1:8891"
# Auth via Dovecot
postconf -e "smtpd_sasl_type = dovecot"
postconf -e "smtpd_sasl_path = private/auth"
postconf -e "smtpd_sasl_auth_enable = yes"
postconf -e "smtpd_sasl_security_options = noanonymous"
postconf -e "smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination"
# Master-Services
postconf -M "smtp/inet=smtp inet n - n - - smtpd \
-o smtpd_peername_lookup=no \
-o smtpd_timeout=30s"
postconf -M "submission/inet=submission inet n - n - - smtpd \
-o syslog_name=postfix/submission \
-o smtpd_peername_lookup=no \
-o smtpd_tls_security_level=encrypt \
-o smtpd_sasl_auth_enable=yes \
-o smtpd_relay_restrictions=permit_sasl_authenticated,reject \
-o smtpd_recipient_restrictions=permit_sasl_authenticated,reject"
postconf -M "smtps/inet=smtps inet n - n - - smtpd \
-o syslog_name=postfix/smtps \
-o smtpd_peername_lookup=no \
-o smtpd_tls_wrappermode=yes \
-o smtpd_sasl_auth_enable=yes \
-o smtpd_relay_restrictions=permit_sasl_authenticated,reject \
-o smtpd_recipient_restrictions=permit_sasl_authenticated,reject"
postconf -M "pickup/unix=pickup unix n - y 60 1 pickup"
postconf -M "cleanup/unix=cleanup unix n - y - 0 cleanup"
postconf -M "qmgr/unix=qmgr unix n - n 300 1 qmgr"
# --- Postfix SQL-Maps (Platzhalter) ---
install -d -o root -g postfix -m 750 /etc/postfix/sql
install -o root -g postfix -m 640 /dev/null /etc/postfix/sql/mysql-virtual-mailbox-maps.cf
cat > /etc/postfix/sql/mysql-virtual-mailbox-maps.cf <<CONF
hosts = 127.0.0.1
user = ${DB_USER}
password = ${DB_PASS}
dbname = ${DB_NAME}
# query = SELECT 1 FROM mail_users WHERE email = '%s' AND is_active = 1 LIMIT 1;
CONF
chown root:postfix /etc/postfix/sql/mysql-virtual-mailbox-maps.cf
chmod 640 /etc/postfix/sql/mysql-virtual-mailbox-maps.cf
install -o root -g postfix -m 640 /dev/null /etc/postfix/sql/mysql-virtual-alias-maps.cf
cat > /etc/postfix/sql/mysql-virtual-alias-maps.cf <<CONF
hosts = 127.0.0.1
user = ${DB_USER}
password = ${DB_PASS}
dbname = ${DB_NAME}
# query = SELECT destination FROM mail_aliases WHERE source = '%s' AND is_active = 1 LIMIT 1;
CONF
chown root:postfix /etc/postfix/sql/mysql-virtual-alias-maps.cf
chmod 640 /etc/postfix/sql/mysql-virtual-alias-maps.cf
systemctl enable --now postfix
# ===== Dovecot konfigurieren (IMAP/POP3 + SSL) =====
log "Dovecot konfigurieren…"
cat > /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 <<CONF
driver = mysql
connect = host=127.0.0.1 dbname=${DB_NAME} user=${DB_USER} password=${DB_PASS}
default_pass_scheme = BLF-CRYPT
# password_query = SELECT email AS user, password_hash AS password
# FROM mail_users
# WHERE email = '%u' AND is_active = 1
# LIMIT 1;
CONF
chown root:dovecot /etc/dovecot/dovecot-sql.conf.ext
chmod 640 /etc/dovecot/dovecot-sql.conf.ext
cat > /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
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 <<CONF
ssl = required
ssl_cert = <${CERT}
ssl_key = <${KEY}
CONF
systemctl enable --now dovecot
# ===== Rspamd & OpenDKIM =====
log "Rspamd + OpenDKIM aktivieren…"
cat > /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 später im Wizard
CONF
systemctl enable --now opendkim || true
# ===== Redis =====
systemctl enable --now redis-server
# ===== Nginx: Laravel vHost (80/443) =====
log "Nginx konfigurieren…"
rm -f /etc/nginx/sites-enabled/default /etc/nginx/sites-available/default || true
PHPV=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')
PHP_FPM_SOCK="/run/php/php${PHPV}-fpm.sock"
[ -S "$PHP_FPM_SOCK" ] || PHP_FPM_SOCK="/run/php/php-fpm.sock"
cat > ${NGINX_SITE} <<CONF
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
root ${APP_DIR}/public;
index index.php index.html;
access_log /var/log/nginx/${APP_USER}_access.log;
error_log /var/log/nginx/${APP_USER}_error.log;
location / {
try_files \$uri \$uri/ /index.php?\$query_string;
}
location ~ \.php\$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:${PHP_FPM_SOCK};
}
location ^~ /livewire/ {
try_files \$uri /index.php?\$query_string;
}
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ {
expires 30d;
access_log off;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name _;
ssl_certificate ${CERT};
ssl_certificate_key ${KEY};
ssl_protocols TLSv1.2 TLSv1.3;
root ${APP_DIR}/public;
index index.php index.html;
access_log /var/log/nginx/${APP_USER}_ssl_access.log;
error_log /var/log/nginx/${APP_USER}_ssl_error.log;
location / {
try_files \$uri \$uri/ /index.php?\$query_string;
}
location ~ \.php\$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:${PHP_FPM_SOCK};
}
location ^~ /livewire/ {
try_files \$uri /index.php?\$query_string;
}
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ {
expires 30d;
access_log off;
}
}
CONF
ln -sf ${NGINX_SITE} ${NGINX_SITE_LINK}
nginx -t && systemctl enable --now nginx
# ===== Node/NPM installieren (für Vite/Tailwind Build) =====
log "Node/NPM installieren…"
if [ "$NODE_SETUP" = "nodesource" ]; then
curl -fsSL https://deb.nodesource.com/setup_22.x -o /tmp/nodesource_setup.sh
bash /tmp/nodesource_setup.sh
rm -f /tmp/nodesource_setup.sh
apt-get install -y nodejs
else
apt-get install -y nodejs npm
fi
# ===== Projekt aus Git holen =====
log "Projekt aus Git klonen…"
mkdir -p "$(dirname "$APP_DIR")"
chown "$APP_USER":$APP_GROUP "$(dirname "$APP_DIR")"
if [ ! -d "${APP_DIR}/.git" ]; then
rm -rf "${APP_DIR}"
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 checkout ${GIT_BRANCH} && git pull --ff-only"
fi
# ===== .env erstellen und befüllen =====
log ".env konfigurieren…"
APP_URL="http://${SERVER_IP}"
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && cp -n .env.example .env || true"
sed -i "s|^APP_NAME=.*|APP_NAME=${APP_NAME}|g" "${APP_DIR}/.env"
sed -i "s|^APP_URL=.*|APP_URL=${APP_URL}|g" "${APP_DIR}/.env"
sed -i "s|^APP_ENV=.*|APP_ENV=${APP_ENV}|g" "${APP_DIR}/.env"
sed -i "s|^APP_DEBUG=.*|APP_DEBUG=${APP_DEBUG}|g" "${APP_DIR}/.env"
sed -i "s|^DB_CONNECTION=.*|DB_CONNECTION=mysql|g" "${APP_DIR}/.env"
sed -i -E "s|^[#[:space:]]*DB_HOST=.*|DB_HOST=127.0.0.1|g" "${APP_DIR}/.env"
sed -i -E "s|^[#[:space:]]*DB_PORT=.*|DB_PORT=3306|g" "${APP_DIR}/.env"
sed -i -E "s|^[#[:space:]]*DB_DATABASE=.*|DB_DATABASE=${DB_NAME}|g" "${APP_DIR}/.env"
sed -i -E "s|^[#[:space:]]*DB_USERNAME=.*|DB_USERNAME=${DB_USER}|g" "${APP_DIR}/.env"
sed -i -E "s|^[#[:space:]]*DB_PASSWORD=.*|DB_PASSWORD=${DB_PASS}|g" "${APP_DIR}/.env"
sed -i "s|^CACHE_DRIVER=.*|CACHE_DRIVER=redis|g" "${APP_DIR}/.env"
sed -i -E "s|^[#[:space:]]*CACHE_PREFIX=.*|CACHE_PREFIX=${APP_USER}|g" "${APP_DIR}/.env"
sed -i "s|^SESSION_DRIVER=.*|SESSION_DRIVER=redis|g" "${APP_DIR}/.env"
sed -i "s|^REDIS_HOST=.*|REDIS_HOST=127.0.0.1|g" "${APP_DIR}/.env"
sed -i "s|^REDIS_PASSWORD=.*|REDIS_PASSWORD=null|g" "${APP_DIR}/.env"
sed -i "s|^REDIS_PORT=.*|REDIS_PORT=6379|g" "${APP_DIR}/.env"
REVERB_APP_ID="$(short)"
REVERB_APP_KEY="$(short)"
REVERB_APP_SECRET="$(short)"
grep -q '^REVERB_APP_ID=' "${APP_DIR}/.env" \
|| printf '\nBROADCAST_CONNECTION=reverb\nREVERB_APP_ID=%s\nREVERB_APP_KEY=%s\nREVERB_APP_SECRET=%s\nREVERB_HOST=127.0.0.1\nREVERB_PORT=8080\nREVERB_SCHEME=http\nVITE_REVERB_APP_KEY=%s\nVITE_REVERB_HOST=%s\nVITE_REVERB_PORT=8080\nVITE_REVERB_SCHEME=http\n' \
"$REVERB_APP_ID" "$REVERB_APP_KEY" "$REVERB_APP_SECRET" "$REVERB_APP_KEY" "$SERVER_IP" >> "${APP_DIR}/.env"
# Bootstrap-Admin für den ersten Login
BOOTSTRAP_USER="${APP_USER}"
BOOTSTRAP_EMAIL="${APP_USER}@localhost"
BOOTSTRAP_PASS="$(openssl rand -base64 18 | LC_ALL=C tr -dc 'A-Za-z0-9' | head -c 12)"
BOOTSTRAP_HASH="$(php -r 'echo password_hash($argv[1], PASSWORD_BCRYPT);' "$BOOTSTRAP_PASS")"
grep -q '^SETUP_PHASE=' "${APP_DIR}/.env" \
|| echo "SETUP_PHASE=bootstrap" >> "${APP_DIR}/.env"
sed -i "s|^SETUP_PHASE=.*|SETUP_PHASE=bootstrap|g" "${APP_DIR}/.env"
grep -q '^BOOTSTRAP_ADMIN_USER=' "${APP_DIR}/.env" \
|| echo "BOOTSTRAP_ADMIN_USER=${BOOTSTRAP_USER}" >> "${APP_DIR}/.env"
sed -i "s|^BOOTSTRAP_ADMIN_USER=.*|BOOTSTRAP_ADMIN_USER=${BOOTSTRAP_USER}|g" "${APP_DIR}/.env"
grep -q '^BOOTSTRAP_ADMIN_EMAIL=' "${APP_DIR}/.env" \
|| echo "BOOTSTRAP_ADMIN_EMAIL=${BOOTSTRAP_EMAIL}" >> "${APP_DIR}/.env"
sed -i "s|^BOOTSTRAP_ADMIN_EMAIL=.*|BOOTSTRAP_ADMIN_EMAIL=${BOOTSTRAP_EMAIL}|g" "${APP_DIR}/.env"
grep -q '^BOOTSTRAP_ADMIN_PASSWORD_HASH=' "${APP_DIR}/.env" \
|| echo "BOOTSTRAP_ADMIN_PASSWORD_HASH=${BOOTSTRAP_HASH}" >> "${APP_DIR}/.env"
sed -i "s|^BOOTSTRAP_ADMIN_PASSWORD_HASH=.*|BOOTSTRAP_ADMIN_PASSWORD_HASH=${BOOTSTRAP_HASH}|g" "${APP_DIR}/.env"
# ===== Composer Dependencies =====
log "Composer install…"
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && composer install --no-dev --optimize-autoloader --no-interaction"
# ===== App-Key, Migrations & Caches =====
log "App-Key generieren, Datenbank migrieren, Caches bauen…"
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan key:generate --force"
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan migrate --force"
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan storage:link --force || true"
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan config:cache"
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan route:cache"
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan view:cache"
# ===== Frontend Build =====
if [ -f "${APP_DIR}/package.json" ]; then
log "Frontend bauen…"
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm ci --no-audit --no-fund || npm install"
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm run build"
fi
# ===== Wizard State-Verzeichnis =====
mkdir -p /var/lib/mailwolt/wizard
chown "$APP_USER":"$APP_GROUP" /var/lib/mailwolt/wizard
chmod 775 /var/lib/mailwolt/wizard
# git safe.directory damit spätere pulls als root möglich sind
git config --global --add safe.directory "${APP_DIR}" || true
# ===== App-User/Gruppen & Rechte (am ENDE ausführen) =====
# User anlegen (nur falls noch nicht vorhanden) + Passwort setzen + Gruppe
if ! id -u "$APP_USER" >/dev/null 2>&1; then
adduser --disabled-password --gecos "" "$APP_USER"
fi
echo "${APP_USER}:${APP_PW}" | chpasswd
usermod -a -G "$APP_GROUP" "$APP_USER"
# 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 {} \;
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
# 7) npm respektiert umask zur Sicherheit direkt setzen (für APP_USER)
sudo -u "$APP_USER" -H bash -lc "npm config set umask 0002" >/dev/null 2>&1 || true
# PHP-FPM-Socket group-writable machen
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
# 9) Optional: deinem Shell-/IDE-User ebenfalls Schreibrechte geben
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 "${YELLOW}[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
# Hinweis zur neuen Gruppenzugehörigkeit
echo -e "${YELLOW}[i]${NC} SHELL: Du kannst dich nun als Benutzer '${APP_USER}' mit dem Passwort '${APP_PW}' anmelden."
echo -e "${YELLOW}[i]${NC} Hinweis: Nach dem ersten Login solltest du das Passwort mit 'passwd ${APP_USER}' ändern."
echo -e "${YELLOW}[i]${NC} Damit die Gruppenrechte (${APP_GROUP}) aktiv werden, bitte einmal ab- und wieder anmelden."
# ===== Monit (Watchdog) installiert, aber NICHT aktiviert =====
log "Monit (Watchdog) installieren (deaktiviert)"
cat > /etc/monit/monitrc <<'EOF'
set daemon 60
set logfile syslog facility log_daemon
# set mailserver + set alert werden im Wizard gesetzt
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
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
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 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
EOF
chmod 600 /etc/monit/monitrc
systemctl disable --now monit || true
apt-mark hold monit >/dev/null 2>&1 || true
# ===== Smoke-Test (alle Ports, mit Timeouts) =====
log "Smoke-Test (Ports & Banner):"
set +e
printf "[25] " && timeout 6s bash -lc 'printf "EHLO localhost\r\nQUIT\r\n" | nc -v -w 4 127.0.0.1 25 2>&1' || true
printf "[465] " && timeout 6s openssl s_client -connect 127.0.0.1:465 -brief -quiet </dev/null || echo "[465] Verbindung fehlgeschlagen"
printf "[587] " && timeout 6s openssl s_client -starttls smtp -connect 127.0.0.1:587 -brief -quiet </dev/null || echo "[587] Verbindung fehlgeschlagen"
printf "[110] " && timeout 6s bash -lc 'printf "QUIT\r\n" | nc -v -w 4 127.0.0.1 110 2>&1' || true
printf "[995] " && timeout 6s openssl s_client -connect 127.0.0.1:995 -brief -quiet </dev/null || echo "[995] Verbindung fehlgeschlagen"
printf "[143] " && timeout 6s bash -lc 'printf ". CAPABILITY\r\n. LOGOUT\r\n" | nc -v -w 4 127.0.0.1 143 2>&1' || true
printf "[993] " && timeout 6s openssl s_client -connect 127.0.0.1:993 -brief -quiet </dev/null || echo "[993] Verbindung fehlgeschlagen"
set -e
echo
echo "=============================================================="
echo " Bootstrap-Login (nur für ERSTEN Login & Wizard):"
echo " User: ${BOOTSTRAP_USER}"
echo " Passwort: ${BOOTSTRAP_PASS}"
echo "=============================================================="
echo
footer_ok "$SERVER_IP"