Init Mailwolt installer

main
boksbc 2025-10-16 10:38:34 +02:00
commit 8cad8eceb8
15 changed files with 893 additions and 0 deletions

View File

@ -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__

38
scripts/10-provision.sh Normal file
View File

@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -euo pipefail
source ./lib.sh
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 \
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 \
ca-certificates rsyslog sudo openssl monit acl netcat-openbsd
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"
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"
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 …"
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"
grep -qE '^\s*#?\s*requirepass ' "$REDIS_CONF" \
&& sed -i "s/^\s*#\?\s*requirepass .*/requirepass ${REDIS_PASS}/" "$REDIS_CONF" \
|| printf "\nrequirepass %s\n" "${REDIS_PASS}" >> "$REDIS_CONF"
systemctl enable --now redis-server
systemctl restart redis-server || true

42
scripts/20-ssl.sh Normal file
View File

@ -0,0 +1,42 @@
#!/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" <<CFG
[req]
default_bits=2048
prompt=no
default_md=sha256
req_extensions=req_ext
distinguished_name=dn
[dn]
CN=${SERVER_PUBLIC_IPV4}
O=${APP_NAME}
C=DE
[req_ext]
subjectAltName=@alt_names
[alt_names]
IP.1=${SERVER_PUBLIC_IPV4}
CFG
openssl req -x509 -newkey rsa:2048 -days 825 -nodes -keyout "$KEY" -out "$CERT" -config "$OSSL_CFG"
chgrp www-data "$CERT" "$KEY" || true
chmod 640 "$KEY" "$CERT"
fi
install -d -m 0755 "$UI_SSL_DIR" "$WEBMAIL_SSL_DIR" "$MAIL_SSL_DIR"
ln -sf "$CERT" "$UI_CERT"; ln -sf "$KEY" "$UI_KEY"
ln -sf "$CERT" "$WEBMAIL_CERT";ln -sf "$KEY" "$WEBMAIL_KEY"
ln -sf "$CERT" "$MAIL_CERT"; ln -sf "$KEY" "$MAIL_KEY"

14
scripts/30-db.sh Normal file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
source ./lib.sh
log "MariaDB vorbereiten …"
systemctl enable --now mariadb
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}';
CREATE USER IF NOT EXISTS '${DB_USER}'@'127.0.0.1' IDENTIFIED BY '${DB_PASS}';
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'localhost';
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'127.0.0.1';
FLUSH PRIVILEGES;
SQL

81
scripts/40-postfix.sh Normal file
View File

@ -0,0 +1,81 @@
#!/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 "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 "smtpd_tls_cert_file=${MAIL_CERT}"
postconf -e "smtpd_tls_key_file=${MAIL_KEY}"
postconf -e "smtpd_tls_security_level = may"
postconf -e "smtp_tls_security_level = may"
postconf -e "smtpd_tls_received_header = yes"
postconf -e "smtpd_tls_protocols=!SSLv2,!SSLv3"
postconf -e "smtpd_tls_mandatory_protocols=!SSLv2,!SSLv3"
postconf -e "smtpd_tls_loglevel=1"
postconf -e "smtp_tls_loglevel=1"
postconf -e "disable_vrfy_command = yes"
postconf -e "smtpd_helo_required = yes"
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"
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"
postconf -e "smtpd_relay_restrictions = permit_mynetworks, reject_unauth_destination"
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_tls_auth_only=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 "smtps/inet=smtps inet n - n - - smtpd -o syslog_name=postfix/smtps -o smtpd_peername_lookup=no -o smtpd_tls_wrappermode=yes -o smtpd_tls_auth_only=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"
install -d -o root -g postfix -m 750 /etc/postfix/sql
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 u
JOIN domains d ON d.id = u.domain_id
WHERE u.email = '%s' AND u.is_active = 1 AND d.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
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 a
JOIN domains d ON d.id = a.domain_id
WHERE a.source = '%s' AND a.is_active = 1 AND d.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
# Nur aktivieren Start/Reload erst nach App/DB in 90-services.sh
systemctl enable postfix >/dev/null 2>&1 || true

108
scripts/50-dovecot.sh Normal file
View File

@ -0,0 +1,108 @@
#!/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 …"
# Hauptdatei
cat > /etc/dovecot/dovecot.conf <<'CONF'
!include_try /etc/dovecot/conf.d/*.conf
CONF
# Mail-Location & Namespace
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
# Auth
cat > /etc/dovecot/conf.d/10-auth.conf <<'CONF'
disable_plaintext_auth = yes
auth_mechanisms = plain login
!include_try auth-sql.conf.ext
CONF
# SQL-Anbindung
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
# Auth-SQL Einbindung
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
chown root:dovecot /etc/dovecot/conf.d/auth-sql.conf.ext
chmod 640 /etc/dovecot/conf.d/auth-sql.conf.ext
# Master-Services (LMTP + AUTH + Listener)
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
# SSL stabile Mail-Pfade
DOVECOT_SSL_CONF="/etc/dovecot/conf.d/10-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
# Postfix-Socket-Verzeichnis sicherstellen
mkdir -p /var/spool/postfix/private
chown postfix:postfix /var/spool/postfix /var/spool/postfix/private
chmod 0755 /var/spool/postfix /var/spool/postfix/private
# Nur aktivieren Start/Reload erst nach App/DB in 90-services.sh
systemctl enable dovecot >/dev/null 2>&1 || true

View File

@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
source ./lib.sh
log "Rspamd + OpenDKIM …"
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
CONF
systemctl enable --now opendkim || true

147
scripts/70-nginx.sh Normal file
View File

@ -0,0 +1,147 @@
#!/usr/bin/env bash
set -euo pipefail
source ./lib.sh
log "Nginx konfigurieren …"
NGINX_SITE="/etc/nginx/sites-available/${APP_USER}.conf"
NGINX_SITE_LINK="/etc/nginx/sites-enabled/${APP_USER}.conf"
ACME_ROOT="/var/www/letsencrypt"
install -d -m 0755 "$ACME_ROOT"
# HTTP/2 prüfen
NGINX_HTTP2_SUFFIX=""
if nginx -V 2>&1 | grep -q http_v2; then
NGINX_HTTP2_SUFFIX=" http2"
fi
# PHP-FPM Socket oder TCP ermitteln und 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#unix:};"
else
FASTCGI_PASS="fastcgi_pass ${PHP_FPM_TARGET};"
fi
# Prüfen, ob UI-Zert vorhanden ist
UI_CERT="/etc/ssl/ui/fullchain.pem"
UI_KEY="/etc/ssl/ui/privkey.pem"
SSL_ENABLED=0
[[ -s "$UI_CERT" && -s "$UI_KEY" ]] && SSL_ENABLED=1
TPL="config/nginx/site.conf.tmpl"
[[ -f "$TPL" ]] || die "Nginx-Template fehlt: $TPL"
render="$(cat "$TPL")"
# --------- Bausteine, die in das Template eingesetzt werden ---------
# (A) HTTP-Body, wenn KEIN SSL → App direkt über Port 80
HTTP_BODY_APP="$(cat <<'HTTP'
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;
client_max_body_size 25m;
location / { try_files $uri $uri/ /index.php?$query_string; }
location ~ \.php$ {
include snippets/fastcgi-php.conf;
__FASTCGI_PASS__
try_files $uri =404;
}
location ^~ /livewire/ { try_files $uri /index.php?$query_string; }
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)$ { expires 30d; access_log off; }
HTTP
)"
# (B) HTTP-Body, wenn SSL → nur Redirect auf 443
HTTP_BODY_REDIRECT='return 301 https://$host$request_uri;'
# (C) kompletter SSL-Serverblock (wird nur eingefügt, wenn SSL aktiv)
SSL_BLOCK="$(cat <<'SSL'
server {
listen 443 ssl${NGINX_HTTP2_SUFFIX};
listen [::]:443 ssl${NGINX_HTTP2_SUFFIX};
server_name _;
ssl_certificate ${UI_CERT};
ssl_certificate_key ${UI_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;
client_max_body_size 25m;
location / { try_files $uri $uri/ /index.php?$query_string; }
location ~ \.php$ {
include snippets/fastcgi-php.conf;
__FASTCGI_PASS__
try_files $uri =404;
}
location ^~ /livewire/ { try_files $uri /index.php?$query_string; }
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)$ { expires 30d; access_log off; }
# WebSocket: Laravel Reverb
location /ws/ {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
proxy_pass http://127.0.0.1:8080/;
}
# Reverb HTTP API
location /apps/ {
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
proxy_pass http://127.0.0.1:8080/apps/;
}
}
SSL
)"
# --------- Platzhalter ersetzen ---------
if [[ $SSL_ENABLED -eq 1 ]]; then
render="${render/__HTTP_BODY__/$HTTP_BODY_REDIRECT}"
render="${render/__SSL_SERVER_BLOCK__/$SSL_BLOCK}"
else
render="${render/__HTTP_BODY__/$HTTP_BODY_APP}"
render="${render/__SSL_SERVER_BLOCK__/}"
fi
# Variablen & __FASTCGI_PASS__ im fertigen Render ersetzen
render="$(echo "$render" \
| sed "s|\${APP_DIR}|${APP_DIR}|g; s|\${APP_USER}|${APP_USER}|g; \
s|\${UI_CERT}|${UI_CERT}|g; s|\${UI_KEY}|${UI_KEY}|g; \
s|\${NGINX_HTTP2_SUFFIX}|${NGINX_HTTP2_SUFFIX}|g; \
s|__FASTCGI_PASS__|${FASTCGI_PASS}|g")"
# Schreiben/aktivieren
echo "$render" > "$NGINX_SITE"
ln -sf "$NGINX_SITE" "$NGINX_SITE_LINK"
# Test & reload
if nginx -t; then
systemctl enable --now nginx >/dev/null 2>&1 || true
systemctl reload nginx || true
else
die "nginx -t fehlgeschlagen siehe /var/log/nginx/*.log"
fi

70
scripts/80-app.sh Normal file
View File

@ -0,0 +1,70 @@
#!/usr/bin/env bash
set -euo pipefail
source ./lib.sh
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://example.com/your-repo-placeholder.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 heuristisch
APP_URL="http://${SERVER_PUBLIC_IPV4}"
UI_CERT="/etc/ssl/ui/fullchain.pem"; UI_KEY="/etc/ssl/ui/privkey.pem"
if [[ -f "$UI_CERT" && -f "$UI_KEY" && resolve_ok "$UI_HOST" ]]; then
APP_URL="https://${UI_HOST}"
fi
upsert_env APP_NAME "${APP_NAME}"
upsert_env APP_URL "${APP_URL}"
upsert_env APP_ENV "production"
upsert_env APP_DEBUG "false"
upsert_env APP_LOCALE "${APP_LOCALE}"
upsert_env APP_FALLBACK_LOCALE "en"
upsert_env SERVER_PUBLIC_IPV4 "${SERVER_PUBLIC_IPV4}"
upsert_env SERVER_PUBLIC_IPV6 "${SERVER_PUBLIC_IPV6}"
upsert_env BASE_DOMAIN "${BASE_DOMAIN}"
upsert_env UI_SUB "${UI_SUB}"
upsert_env WEBMAIL_SUB "${WEBMAIL_SUB}"
upsert_env SYSTEM_SUB "${SYSTEM_SUB}"
upsert_env MTA_SUB "${MTA_SUB}"
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_DRIVER "redis"
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:-}"
chown -R "$APP_USER":"$APP_GROUP" "$APP_DIR"
chmod -R u=rwX,g=rwX,o=rX "$APP_DIR"
install -d -m 0775 -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR/storage" "$APP_DIR/bootstrap/cache"
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan optimize:clear && php artisan config:cache"

105
scripts/90-services.sh Normal file
View File

@ -0,0 +1,105 @@
#!/usr/bin/env bash
set -euo pipefail
source ./lib.sh
log "systemd Units (Reverb / Scheduler / Queue) …"
cat > /etc/systemd/system/${APP_USER}-ws.service <<EOF
[Unit]
Description=${APP_NAME} WebSocket Backend
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
Environment=NODE_ENV=production WS_PORT=8080
User=${APP_USER}
Group=${APP_GROUP}
WorkingDirectory=${APP_DIR}
ExecStartPre=/usr/bin/bash -lc 'test -f .env'
ExecStartPre=/usr/bin/bash -lc 'test -d vendor'
ExecStart=/usr/bin/php artisan reverb:start --host=127.0.0.1 --port=8080 --no-interaction
Restart=always
RestartSec=2
StandardOutput=append:/var/log/${APP_USER}-ws.log
StandardError=append:/var/log/${APP_USER}-ws.log
KillSignal=SIGINT
TimeoutStopSec=15
[Install]
WantedBy=multi-user.target
EOF
cat > /etc/systemd/system/${APP_USER}-schedule.service <<EOF
[Unit]
Description=${APP_NAME} Laravel Scheduler
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=${APP_USER}
Group=${APP_GROUP}
WorkingDirectory=${APP_DIR}
ExecStartPre=/usr/bin/bash -lc 'test -f .env'
ExecStartPre=/usr/bin/bash -lc 'test -d vendor'
ExecStart=/usr/bin/php artisan schedule:work
Restart=always
RestartSec=2
StandardOutput=append:/var/log/${APP_USER}-schedule.log
StandardError=append:/var/log/${APP_USER}-schedule.log
KillSignal=SIGINT
TimeoutStopSec=15
[Install]
WantedBy=multi-user.target
EOF
cat > /etc/systemd/system/${APP_USER}-queue.service <<EOF
[Unit]
Description=${APP_NAME} Queue Worker
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=${APP_USER}
Group=${APP_GROUP}
WorkingDirectory=${APP_DIR}
ExecStartPre=/usr/bin/bash -lc 'test -f .env'
ExecStartPre=/usr/bin/bash -lc 'test -d vendor'
ExecStart=/usr/bin/php artisan queue:work --queue=default,notify --tries=1
Restart=always
RestartSec=2
StandardOutput=append:/var/log/${APP_USER}-queue.log
StandardError=append:/var/log/${APP_USER}-queue.log
KillSignal=SIGINT
TimeoutStopSec=15
[Install]
WantedBy=multi-user.target
EOF
chown root:root /etc/systemd/system/${APP_USER}-*.service
chmod 644 /etc/systemd/system/${APP_USER}-*.service
touch /var/log/${APP_USER}-ws.log /var/log/${APP_USER}-schedule.log /var/log/${APP_USER}-queue.log
chown ${APP_USER}:${APP_GROUP} /var/log/${APP_USER}-*.log
chmod 664 /var/log/${APP_USER}-*.log
systemctl daemon-reload
# Optional: Reverb nur wenn vorhanden
if sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan list --no-ansi | grep -qE '(^| )reverb:start( |$)'"; then
systemctl enable --now ${APP_USER}-ws
else
systemctl disable --now ${APP_USER}-ws >/dev/null 2>&1 || true
fi
systemctl enable --now ${APP_USER}-schedule
systemctl enable --now ${APP_USER}-queue
# Web stack neu laden
systemctl reload nginx || true
systemctl restart php*-fpm || true
# Postfix/Dovecot erst nach Migration reloaden
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
systemctl reload dovecot || true
else
echo "[i] DB noch nicht migriert überspringe Postfix/Dovecot reload."
fi

41
scripts/95-monit.sh Normal file
View File

@ -0,0 +1,41 @@
#!/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

52
scripts/98-motd.sh Normal file
View File

@ -0,0 +1,52 @@
#!/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
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 '?')"
load="$(awk '{print $1" / "$2" / "$3}' /proc/loadavg 2>/dev/null)"
svc(){ 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}(ext:${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}Cores :${NC} %s\n" "$cores"
printf "${GY}Load :${NC} %s (1/5/15)\n" "${load:-?}"
printf "${GY}Svc :${NC} postfix: $(svc postfix) dovecot: $(svc dovecot) nginx: $(svc nginx) mariadb: $(svc mariadb) redis: $(svc 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
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

20
scripts/99-summary.sh Normal file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
source ./lib.sh
scheme="http"
[[ -f /etc/ssl/ui/fullchain.pem && -f /etc/ssl/ui/privkey.pem ]] && scheme="https"
echo -e "
${GREEN}${BAR}${NC}
${GREEN}${APP_NAME} Bootstrap fertig${NC}
${GREEN}${BAR}${NC}
Admin-User: ${YELLOW}${ADMIN_USER}${NC}
Admin-Mail: ${YELLOW}${ADMIN_EMAIL}${NC}
Passwort: ${RED}${ADMIN_PASS}${NC}
Aufruf UI: ${CYAN}${scheme}://${SERVER_PUBLIC_IPV4}${NC}
App Root: ${GREY}${APP_DIR}${NC}
Nginx Site: ${GREY}/etc/nginx/sites-available/${APP_USER}.conf${NC}
Mail-FQDN: ${GREY}${MAIL_HOSTNAME}${NC}
"

61
scripts/bootstrap.sh Normal file
View File

@ -0,0 +1,61 @@
#!/usr/bin/env bash
set -euo pipefail
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}"
SYSTEM_SUB="${SYSTEM_SUB:-system}"
ADMIN_USER="${ADMIN_USER:-${APP_USER}}"
ADMIN_EMAIL="${ADMIN_EMAIL:-admin@localhost}"
ADMIN_PASS="${ADMIN_PASS:-ChangeMe}"
DB_NAME="${DB_NAME:-${APP_USER}}"
DB_USER="${DB_USER:-${APP_USER}}"
DB_PASS="${DB_PASS:-$(openssl rand -hex 16)}"
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}"
read -r -p "Basisdomain (Enter=${BASE_DOMAIN}): " INP; BASE_DOMAIN="${INP:-$BASE_DOMAIN}"
read -r -p "UI Subdomain (Enter=${UI_SUB}): " INP; UI_SUB="${INP:-$UI_SUB}"
read -r -p "Webmail Subdomain (Enter=${WEBMAIL_SUB}): " INP; WEBMAIL_SUB="${INP:-$WEBMAIL_SUB}"
read -r -p "Mailserver Subdomain (Enter=${MTA_SUB}): " INP; MTA_SUB="${INP:-$MTA_SUB}"
read -r -p "Zeitzone (Enter=${DEFAULT_TZ}): " INP; APP_TZ="${INP:-$DEFAULT_TZ}"
read -r -p "Sprache [de/en] (Enter=${DEFAULT_LOCALE}): " INP; APP_LOCALE="${INP:-$DEFAULT_LOCALE}"
UI_HOST="$(join_host "$UI_SUB" "$BASE_DOMAIN")"
WEBMAIL_HOST="$(join_host "$WEBMAIL_SUB" "$BASE_DOMAIN")"
MAIL_HOSTNAME="$(join_host "$MTA_SUB" "$BASE_DOMAIN")"
SYSTEM_HOSTNAME="$(join_host "$SYSTEM_SUB" "$BASE_DOMAIN")"
export APP_NAME APP_USER APP_GROUP APP_USER_PREFIX APP_DIR
export BASE_DOMAIN UI_SUB WEBMAIL_SUB MTA_SUB SYSTEM_SUB
export UI_HOST WEBMAIL_HOST MAIL_HOSTNAME SYSTEM_HOSTNAME
export ADMIN_USER ADMIN_EMAIL ADMIN_PASS
export DB_NAME DB_USER DB_PASS
export SERVER_PUBLIC_IPV4 SERVER_PUBLIC_IPV6 APP_TZ APP_LOCALE
# ── Sequenz ────────────────────────────────────────────────────────────────
for STEP in 10-provision 20-ssl 30-db 40-postfix 50-dovecot 60-rspamd-opendkim 70-nginx 80-app 90-services 95-monit 98-motd 99-summary
do
log ">>> Running ${STEP}.sh"
bash "./${STEP}.sh"
done

71
scripts/lib.sh Normal file
View File

@ -0,0 +1,71 @@
#!/usr/bin/env bash
set -euo pipefail
# ── 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."; }
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_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"; }
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
}