#!/usr/bin/env bash # ============================================================ # MailTidy Pro — Main Installer # Usage: curl -fsSL https://install.mailtidypro.com | bash # Tested on: Ubuntu 22.04 LTS, Ubuntu 24.04 LTS # ============================================================ set -euo pipefail IFS=$'\n\t' # ── Colours ───────────────────────────────────────────────── RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' BLUE='\033[0;34m'; BOLD='\033[1m'; RESET='\033[0m' ok() { echo -e "${GREEN} ✔ $*${RESET}"; } info() { echo -e "${BLUE} ▸ $*${RESET}"; } warn() { echo -e "${YELLOW} ⚠ $*${RESET}"; } die() { echo -e "${RED} ✘ $*${RESET}" >&2; exit 1; } step() { echo -e "\n${BOLD}${BLUE}── $* ──${RESET}\n"; } # ── Banner ─────────────────────────────────────────────────── clear echo -e "${BOLD}" cat << 'BANNER' __ __ _ _ _____ _ _ ____ | \/ | __ _(_) |_ _(_) __| |_ _ | _ \ _ __ ___ | |\/| |/ _` | | | | | | |/ _` | | | | | |_) | '__/ _ \ | | | | (_| | | | | | | | (_| | |_| | | __/| | | (_) | |_| |_|\__,_|_|_| |_| |_|\__,_|\__, | |_| |_| \___/ |___/ BANNER echo -e "${RESET}" echo -e " ${BOLD}MailTidy Pro — One-Line Installer${RESET}" echo -e " Email list verification SaaS · v1.0" echo -e " ${BLUE}https://mailtidypro.com${RESET}\n" echo -e " ${YELLOW}This will install: PHP 8.4, PostgreSQL 17, Redis, Node 24,${RESET}" echo -e " ${YELLOW}Python 3.10, Nginx, Supervisor, Certbot, and MailTidy Pro.${RESET}" echo -e " ${YELLOW}Estimated time: 10–15 minutes.${RESET}\n" # ── Root check ─────────────────────────────────────────────── [[ $EUID -ne 0 ]] && die "Run as root: sudo bash install.sh" # Always run from a stable directory cd /root || cd /tmp # ── Demo server guard ──────────────────────────────────────── # Prevent accidental install on the MailTidy demo/production server if hostname 2>/dev/null | grep -qi "mailtidy" || ip addr 2>/dev/null | grep -q "84.46.245.142"; then die "This appears to be the MailTidy demo server (84.46.245.142).\n Aborting to prevent data loss. Run this on your OWN VPS." fi # ── OS check ──────────────────────────────────────────────── if ! grep -qE 'Ubuntu (22|24)\.04' /etc/os-release 2>/dev/null; then warn "This installer is tested on Ubuntu 22.04 and 24.04." warn "Detected: $(grep PRETTY_NAME /etc/os-release | cut -d= -f2 | tr -d '"')" echo "" read -rp " Continue anyway? [y/N] " _yn < /dev/tty [[ "$_yn" =~ ^[Yy]$ ]] || die "Aborted." fi # ── Pre-flight: Port 25 ────────────────────────────────────── step "Pre-flight checks" if [[ "${SKIP_PORT_CHECK:-0}" == "1" ]]; then warn "SKIP_PORT_CHECK=1 — skipping port 25 verification" warn "Make sure your provider has confirmed port 25 is open, or verification will fail." else info "Testing outbound port 25 (required for SMTP verification)..." if timeout 10 bash -c 'echo -e "QUIT\r" > /dev/tcp/gmail-smtp-in.l.google.com/25 2>/dev/null'; then ok "Port 25 is open — SMTP verification will work" else echo "" die "Port 25 is BLOCKED on this server. MailTidy Pro's verification engine connects to recipient mail servers on port 25. Without it, every verification attempt will time out and the product is unusable. If your VPS provider has CONFIRMED port 25 is open and you believe this is a false negative, you can bypass this check by re-running: curl -fsSL https://install.mailtidypro.com | SKIP_PORT_CHECK=1 bash Providers that BLOCK port 25 (do not use): • DigitalOcean — permanently blocked, no path to unblock • AWS EC2 — blocked by default, rarely approved • Google Cloud — permanently blocked • Azure — permanently blocked Providers that ALLOW port 25 (use these): • Contabo — open by default (~\$6/mo) ← recommended • Vultr — request unlock via support ticket • RackNerd — open by default See: https://mailtidypro.com/docs#vps-providers Switch providers and re-run this installer." fi fi # ── Pre-flight: Public IP ──────────────────────────────────── PUBLIC_IP=$(curl -fsSL --connect-timeout 5 ifconfig.me 2>/dev/null || \ curl -fsSL --connect-timeout 5 api.ipify.org 2>/dev/null || \ echo "unknown") ok "Server public IP: ${PUBLIC_IP}" # ── Collect configuration ──────────────────────────────────── step "Configuration" printf "${YELLOW} ⚠ DNS A record must already point to this server${RESET}\n" printf "${YELLOW} ⚠ Use DNS-only mode in Cloudflare (grey cloud, not orange)${RESET}\n" printf "${BLUE} App domain: ${RESET}" read -r APP_DOMAIN < /dev/tty [[ -z "$APP_DOMAIN" ]] && die "App domain is required." printf "${BLUE} License key: ${RESET}" read -r LICENSE_KEY < /dev/tty [[ -z "$LICENSE_KEY" ]] && die "License key is required." printf "${BLUE} Admin email: ${RESET}" read -r ADMIN_EMAIL < /dev/tty [[ -z "$ADMIN_EMAIL" ]] && die "Admin email is required." printf "${BLUE} Admin password (min 12 chars): ${RESET}" ADMIN_PASS="" while IFS= read -r -s -n1 char < /dev/tty; do [[ -z "$char" ]] && break # Enter key if [[ "$char" == $'\177' || "$char" == $'\b' ]]; then # Backspace — remove last char and erase the asterisk if [[ ${#ADMIN_PASS} -gt 0 ]]; then ADMIN_PASS="${ADMIN_PASS%?}" printf "\b \b" fi else ADMIN_PASS+="$char" printf "*" fi done printf "\n" [[ ${#ADMIN_PASS} -lt 12 ]] && die "Password must be at least 12 characters." # ── Derived values ─────────────────────────────────────────── DB_NAME="mailtidy" DB_USER="mailtidy" DB_PASS=$(openssl rand -base64 32 | tr -d '=/+' | head -c 40) REDIS_PASS=$(openssl rand -base64 24 | tr -d '=/+' | head -c 32) APP_KEY="base64:$(openssl rand -base64 32)" ENGINE_SECRET=$(openssl rand -hex 32) WEBHOOK_SECRET=$(openssl rand -hex 32) # FIX: generate worker license token here so both Laravel and engine .env share it WORKER_LICENSE_TOKEN="lic_$(openssl rand -hex 16)" INSTALL_LOG="/var/log/mailtidy-install-$(date +%Y%m%d-%H%M%S).log" echo "" info "Configuration summary:" echo -e " App domain : ${BOLD}${APP_DOMAIN}${RESET}" echo -e " Admin email: ${BOLD}${ADMIN_EMAIL}${RESET}" echo -e " Public IP : ${BOLD}${PUBLIC_IP}${RESET}" echo -e " Log file : ${BOLD}${INSTALL_LOG}${RESET}" echo "" printf "${BLUE} Looks good? Press Enter to start installing, or Ctrl+C to abort... ${RESET}" read -r _ < /dev/tty # All subsequent output also goes to log exec > >(tee -a "$INSTALL_LOG") 2>&1 # ── Step 1: System packages ────────────────────────────────── step "1 / 9 System packages" export DEBIAN_FRONTEND=noninteractive apt-get update -q apt-get install -yq \ curl wget gnupg2 ca-certificates lsb-release \ software-properties-common apt-transport-https \ unzip zip git acl ufw fail2ban \ build-essential libpq-dev \ supervisor cron ok "Base packages installed" # ── Step 2: PHP 8.4 ───────────────────────────────────────── step "2 / 9 PHP 8.4" add-apt-repository -y ppa:ondrej/php apt-get update -q apt-get install -yq \ php8.4-fpm php8.4-cli php8.4-common \ php8.4-pgsql php8.4-redis php8.4-xml \ php8.4-curl php8.4-mbstring php8.4-zip \ php8.4-bcmath php8.4-intl php8.4-gd \ php8.4-tokenizer php8.4-fileinfo # Composer curl -fsSL https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer --quiet ok "PHP 8.4 + Composer installed" # PHP-FPM tuning PHP_FPM_CONF="/etc/php/8.4/fpm/pool.d/www.conf" sed -i 's/^pm = .*/pm = dynamic/' "$PHP_FPM_CONF" sed -i 's/^pm.max_children = .*/pm.max_children = 20/' "$PHP_FPM_CONF" sed -i 's/^pm.start_servers = .*/pm.start_servers = 4/' "$PHP_FPM_CONF" sed -i 's/^pm.min_spare_servers = .*/pm.min_spare_servers = 2/' "$PHP_FPM_CONF" sed -i 's/^pm.max_spare_servers = .*/pm.max_spare_servers = 6/' "$PHP_FPM_CONF" ok "PHP-FPM pool tuned" # ── Step 3: PostgreSQL 17 ──────────────────────────────────── step "3 / 9 PostgreSQL 17" curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \ | gpg --yes --dearmor -o /usr/share/keyrings/postgresql.gpg echo "deb [signed-by=/usr/share/keyrings/postgresql.gpg] https://apt.postgresql.org/pub/repos/apt \ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list apt-get update -q apt-get install -yq postgresql-17 postgresql-client-17 systemctl enable --now postgresql # Create DB and user — fully idempotent # ALTER ROLE updates password if role already exists sudo -u postgres psql <> "$REDIS_CONF" sed -i 's/^bind .*/bind 127.0.0.1 ::1/' "$REDIS_CONF" systemctl enable --now redis-server systemctl restart redis-server ok "Redis configured with password auth" # ── Step 5: Node.js 24 ────────────────────────────────────── step "5 / 9 Node.js 24 LTS" curl -fsSL https://deb.nodesource.com/setup_24.x | bash - apt-get install -yq nodejs ok "Node.js $(node --version) installed" # ── Step 6: Python 3.10 + engine deps ─────────────────────── step "6 / 9 Python engine" apt-get install -yq python3.10 python3.10-venv python3-pip # Create mailtidy system user id -u mailtidy &>/dev/null || useradd -r -s /sbin/nologin -d /opt/mailtidy-engine mailtidy # Install engine from the ZIP (bundled in /opt/mailtidy-engine by the ZIP unpacker below) # We set up the venv here; actual files come from the ZIP unpack in step 8 mkdir -p /opt/mailtidy-engine python3.10 -m venv /opt/mailtidy-engine/venv /opt/mailtidy-engine/venv/bin/pip install --quiet --upgrade pip # Dependencies installed from requirements.txt after engine files are unpacked (Step 8) mkdir -p /var/log/mailtidy-engine chown mailtidy:mailtidy /var/log/mailtidy-engine /opt/mailtidy-engine ok "Python 3.10 + engine venv ready" # ── Step 7: Nginx ──────────────────────────────────────────── step "7 / 9 Nginx" apt-get install -yq nginx systemctl enable nginx # Remove default and ALL configs mentioning this domain (fully idempotent) rm -f /etc/nginx/sites-enabled/default rm -f "/etc/nginx/sites-enabled/${APP_DOMAIN}" rm -f "/etc/nginx/sites-available/${APP_DOMAIN}" # Remove any other config files referencing this domain for f in /etc/nginx/sites-enabled/* /etc/nginx/sites-available/*; do [ -f "$f" ] && grep -q "server_name.*${APP_DOMAIN}" "$f" 2>/dev/null && rm -f "$f" || true done # Write app nginx config cat > "/etc/nginx/sites-available/${APP_DOMAIN}" << NGINXCONF server { listen 80; server_name ${APP_DOMAIN}; root /var/www/mailtidy/public; index index.php; # Security headers add_header X-Frame-Options "SAMEORIGIN"; add_header X-XSS-Protection "1; mode=block"; add_header X-Content-Type-Options "nosniff"; add_header Referrer-Policy "strict-origin-when-cross-origin"; # ── Engine proxy (FastAPI on 127.0.0.1:8000) ───────────────────────────── # Public worker endpoints — called by remote worker agents over the internet. location /workers/ { proxy_pass http://127.0.0.1:8000/workers/; proxy_http_version 1.1; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; proxy_read_timeout 60s; proxy_connect_timeout 10s; } # React SPA static assets (built frontend served by Nginx directly) location /assets/ { alias /var/www/mailtidy/public/assets/; expires 1y; add_header Cache-Control "public, immutable"; access_log off; } # Laravel API location /api/ { try_files \$uri \$uri/ /index.php?\$query_string; } location /sanctum/ { try_files \$uri \$uri/ /index.php?\$query_string; } # PHP-FPM location ~ \.php$ { fastcgi_pass unix:/var/run/php/php8.4-fpm.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME \$realpath_root\$fastcgi_script_name; include fastcgi_params; fastcgi_read_timeout 120; } # SPA fallback — serve index.php for everything else (React router handles it) location / { try_files \$uri \$uri/ /index.php?\$query_string; } # Upload limits client_max_body_size 64M; # Deny dot-files location ~ /\. { deny all; } } NGINXCONF ln -sf "/etc/nginx/sites-available/${APP_DOMAIN}" "/etc/nginx/sites-enabled/${APP_DOMAIN}" nginx -t nginx -s reload 2>/dev/null || systemctl start nginx ok "Nginx configured for ${APP_DOMAIN}" # ── Step 8: Unpack MailTidy Pro ────────────────────────────── step "8 / 9 Installing MailTidy Pro" # The ZIP is downloaded from the Envato token URL # The ZIP structure is: mailtidy-pro/ with sub-dirs: app/, engine/, frontend-dist/ DOWNLOAD_URL="https://app.mailtidypro.com/api/releases/latest?license=${LICENSE_KEY}" TMPDIR=$(mktemp -d) info "Downloading MailTidy Pro..." if ! curl -fsSL --connect-timeout 30 "$DOWNLOAD_URL" -o "${TMPDIR}/mailtidy.zip"; then die "Download failed. Check your license key and try again. If the problem persists, contact support@mailtidypro.com" fi info "Unpacking..." unzip -q "${TMPDIR}/mailtidy.zip" -d "${TMPDIR}/extracted" APP_SRC=$(find "${TMPDIR}/extracted" -maxdepth 1 -type d | tail -1) # Laravel app mkdir -p /var/www/mailtidy rsync -a --exclude='.env' --exclude='vendor' --exclude='node_modules' \ "${APP_SRC}/app/" /var/www/mailtidy/ # Python engine files rsync -a --exclude='.env' --exclude='.env.worker' --exclude='venv' \ "${APP_SRC}/engine/" /opt/mailtidy-engine/ # Install Python dependencies from requirements.txt (includes pydantic-settings etc.) /opt/mailtidy-engine/venv/bin/pip install --quiet -r /opt/mailtidy-engine/requirements.txt # Pre-built React frontend (no npm build needed by buyer) if [[ -d "${APP_SRC}/frontend-dist" ]]; then rsync -a "${APP_SRC}/frontend-dist/" /var/www/mailtidy/public/ ok "Pre-built React frontend deployed" fi # Install PHP dependencies cd /var/www/mailtidy composer install --no-dev --optimize-autoloader --quiet # ── Write Laravel .env ─────────────────────────────────────── cat > /var/www/mailtidy/.env << ENVFILE APP_NAME="MailTidy Pro" APP_ENV=production APP_KEY=${APP_KEY} APP_DEBUG=false APP_URL=https://${APP_DOMAIN} LOG_CHANNEL=daily LOG_LEVEL=error DB_CONNECTION=pgsql DB_HOST=127.0.0.1 DB_PORT=5432 DB_DATABASE=${DB_NAME} DB_USERNAME=${DB_USER} DB_PASSWORD=${DB_PASS} CACHE_STORE=redis QUEUE_CONNECTION=redis SESSION_DRIVER=redis SESSION_LIFETIME=120 SESSION_SECURE_COOKIE=true REDIS_HOST=127.0.0.1 REDIS_PASSWORD=${REDIS_PASS} REDIS_PORT=6379 # FIX: dedicated Redis connection for the engine (DB 2, same password) REDIS_ENGINE_HOST=127.0.0.1 REDIS_ENGINE_PORT=6379 REDIS_ENGINE_PASSWORD=${REDIS_PASS} REDIS_ENGINE_DB=2 MAIL_MAILER=log MAIL_FROM_ADDRESS="noreply@${APP_DOMAIN}" MAIL_FROM_NAME="MailTidy Pro" # Verification engine (internal) MAILTIDY_ENGINE_URL=http://127.0.0.1:8000 MAILTIDY_ENGINE_SECRET=${ENGINE_SECRET} MAILTIDY_WEBHOOK_SECRET=${WEBHOOK_SECRET} BILLING_STRIPE_ENABLED=false BILLING_PADDLE_ENABLED=false BILLING_RAZORPAY_ENABLED=false BILLING_PAYSTACK_ENABLED=false BILLING_FLUTTERWAVE_ENABLED=false DEMO_EXTENDED=false ENVFILE # ── Write Python engine .env ───────────────────────────────── cat > /opt/mailtidy-engine/.env << ENGINEENV ENVIRONMENT=production SERVICE_NAME=mailtidy-engine LARAVEL_SHARED_SECRET=${ENGINE_SECRET} LARAVEL_WEBHOOK_SECRET=${WEBHOOK_SECRET} LARAVEL_WEBHOOK_URL=https://${APP_DOMAIN}/api/python/result LARAVEL_PROGRESS_URL=https://${APP_DOMAIN}/api/python/progress # DB 2 reserved for engine. Must match REDIS_ENGINE_DB in Laravel .env REDIS_URL=redis://:${REDIS_PASS}@127.0.0.1:6379/2 REDIS_KEY_PREFIX=mtp:engine: SMTP_HELO_DOMAIN=mail.${APP_DOMAIN} SMTP_MAIL_FROM=verify@${APP_DOMAIN} SMTP_CONNECT_TIMEOUT_SECONDS=10.0 SMTP_COMMAND_TIMEOUT_SECONDS=8.0 MAX_CONCURRENT_PER_DOMAIN=5 MAX_CONCURRENT_GMAIL=3 MAX_CONCURRENT_YAHOO=3 MAX_CONCURRENT_OUTLOOK=3 DOMAIN_ERROR_WINDOW_SECONDS=60 DOMAIN_ERROR_THRESHOLD=0.30 DOMAIN_ERROR_MIN_SAMPLES=20 DOMAIN_COOLDOWN_SECONDS=1800 DEFAULT_BATCH_SIZE=200 DEFAULT_WORKER_CONCURRENCY=30 DEFAULT_WORKER_POLL_INTERVAL=5 BATCH_TIMEOUT_SECONDS=300 WORKER_STALE_AFTER_SECONDS=60 DNS_RESOLVERS=["1.1.1.1","8.8.8.8"] DNS_TIMEOUT_SECONDS=5.0 MX_CACHE_TTL_SECONDS=3600 WEBHOOK_MAX_RETRIES=5 WEBHOOK_INITIAL_BACKOFF_SECONDS=1.0 WEBHOOK_TIMEOUT_SECONDS=10.0 PROGRESS_WEBHOOK_EVERY_PCT=5.0 DISPOSABLE_DOMAINS_FILE=data/disposable_domains.txt ROLE_LOCALPARTS_FILE=data/role_localparts.txt ENGINEENV # ── Write worker agent .env.worker ─────────────────────────── # FIX: worker needs its own env file with license token + engine URL cat > /opt/mailtidy-engine/.env.worker << WORKERENV MAILTIDY_ENGINE_URL=http://127.0.0.1:8000 MAILTIDY_LICENSE_TOKEN=${WORKER_LICENSE_TOKEN} MAILTIDY_HOSTNAME=${APP_DOMAIN} MAILTIDY_PUBLIC_IP=${PUBLIC_IP} WORKERENV # ── Seed the worker license token into Redis (DB 2) ────────── # FIX: token must exist in Redis before worker starts or registration fails info "Seeding worker license token into Redis..." REDIS_TOKEN_KEY="mtp:engine:license:${WORKER_LICENSE_TOKEN}" REDIS_TOKEN_PAYLOAD="{\"token\":\"${WORKER_LICENSE_TOKEN}\",\"created_at\":$(date +%s),\"metadata\":{\"host\":\"${APP_DOMAIN}\",\"installer\":true}}" redis-cli -a "${REDIS_PASS}" -n 2 --no-auth-warning SET "${REDIS_TOKEN_KEY}" "${REDIS_TOKEN_PAYLOAD}" EX 31536000 > /dev/null ok "Worker license token seeded into Redis DB 2" # ── Add engine Redis connection to database.php ────────────── # FIX: Laravel needs a named 'engine' Redis connection to write tokens to DB 2 # Write to a temp file then execute — avoids php8.4 stdin issues on some servers cat > /tmp/db_patch.php << 'PHPEOF' [") !== false && strpos($contents, 'REDIS_ENGINE_HOST') !== false) { echo "database.php already patched — skipping\n"; exit(0); } $search = " 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),\n ],\n\n ],"; $replace = " 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),\n ],\n\n 'engine' => [\n 'url' => env('REDIS_ENGINE_URL'),\n 'host' => env('REDIS_ENGINE_HOST', '127.0.0.1'),\n 'username' => env('REDIS_ENGINE_USERNAME'),\n 'password' => env('REDIS_ENGINE_PASSWORD'),\n 'port' => env('REDIS_ENGINE_PORT', '6379'),\n 'database' => env('REDIS_ENGINE_DB', '2'),\n ],\n\n ],"; if (strpos($contents, $search) === false) { echo "WARNING: database.php format not matched — skipping patch\n"; exit(0); } file_put_contents($file, str_replace($search, $replace, $contents)); echo "database.php patched — engine Redis connection added\n"; PHPEOF php8.4 /tmp/db_patch.php rm -f /tmp/db_patch.php # Permissions chown -R www-data:www-data /var/www/mailtidy chmod -R 755 /var/www/mailtidy chmod -R 775 /var/www/mailtidy/storage /var/www/mailtidy/bootstrap/cache chown -R mailtidy:mailtidy /opt/mailtidy-engine chmod 600 /opt/mailtidy-engine/.env /opt/mailtidy-engine/.env.worker setfacl -R -m u:www-data:rwX /var/www/mailtidy/storage /var/www/mailtidy/bootstrap/cache 2>/dev/null || true # Run migrations and seed cd /var/www/mailtidy sudo -u www-data php8.4 artisan key:generate --force sudo -u www-data php8.4 artisan migrate:fresh --force sudo -u www-data php8.4 artisan db:seed --force sudo -u www-data php8.4 artisan storage:link --force 2>/dev/null || true sudo -u www-data php8.4 artisan config:cache sudo -u www-data php8.4 artisan optimize ok "MailTidy Pro application installed" # ── Create admin user ──────────────────────────────────────── info "Creating admin account..." ADMIN_PASS_HASH=$(php8.4 -r "echo password_hash('${ADMIN_PASS}', PASSWORD_BCRYPT, ['cost'=>12]);") sudo -u postgres psql -d mailtidy -c " INSERT INTO users (name, email, password, email_verified_at, role, created_at, updated_at) VALUES ('Admin', '${ADMIN_EMAIL}', '${ADMIN_PASS_HASH}', NOW(), 'admin', NOW(), NOW()) ON CONFLICT (email) DO UPDATE SET password = EXCLUDED.password, role = 'admin', updated_at = NOW(); " > /dev/null ok "Admin account created: ${ADMIN_EMAIL}" # Clean up download rm -rf "${TMPDIR}" # ── Step 9: Services ───────────────────────────────────────── step "9 / 9 Services" # Supervisor: Laravel queue workers + worker agent # Both managed by supervisor for consistency and simplicity. # mailtidy-agent sources .env.worker (NOT .env) — keeps engine config and # worker config cleanly separated. cat > /etc/supervisor/conf.d/mailtidy-queue.conf << 'SUPCONF' [program:mailtidy-queue] command=/usr/bin/php8.4 /var/www/mailtidy/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600 directory=/var/www/mailtidy user=www-data numprocs=2 process_name=%(program_name)s_%(process_num)02d autostart=true autorestart=true stopasgroup=true killasgroup=true stdout_logfile=/var/log/mailtidy-engine/queue.log stderr_logfile=/var/log/mailtidy-engine/queue-error.log [program:mailtidy-agent] command=/bin/bash -c 'set -a && source /opt/mailtidy-engine/.env.worker && set +a && exec /opt/mailtidy-engine/venv/bin/python3 /opt/mailtidy-engine/agent.py' directory=/opt/mailtidy-engine user=mailtidy numprocs=1 autostart=true autorestart=true stopasgroup=true killasgroup=true stdout_logfile=/var/log/mailtidy-engine/worker.log stderr_logfile=/var/log/mailtidy-engine/worker.log SUPCONF # systemd for Python engine only cat > /etc/systemd/system/mailtidy-engine.service << 'SYSDENG' [Unit] Description=MailTidy Pro Verification Engine After=network.target redis-server.service postgresql.service [Service] Type=simple User=mailtidy Group=mailtidy WorkingDirectory=/opt/mailtidy-engine EnvironmentFile=/opt/mailtidy-engine/.env ExecStart=/opt/mailtidy-engine/venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000 --workers 1 Restart=always RestartSec=5 StandardOutput=append:/var/log/mailtidy-engine/engine.log StandardError=append:/var/log/mailtidy-engine/engine.log [Install] WantedBy=multi-user.target SYSDENG systemctl daemon-reload systemctl enable --now mailtidy-engine || true sleep 5 # give engine time to boot before agent tries to register supervisorctl reread 2>/dev/null || true supervisorctl update 2>/dev/null || true ok "Verification engine started" ok "Worker agent started" ok "Queue workers started (×2)" # Log rotation cat > /etc/logrotate.d/mailtidy << 'LOGROT' /var/log/mailtidy-engine/*.log { daily missingok rotate 14 compress delaycompress notifempty sharedscripts postrotate systemctl reload mailtidy-engine >/dev/null 2>&1 || true endscript } LOGROT # Cron: Laravel scheduler (crontab -l 2>/dev/null | grep -v 'artisan schedule'; \ echo "* * * * * www-data /usr/bin/php8.4 /var/www/mailtidy/artisan schedule:run >> /dev/null 2>&1") \ | crontab - || true ok "Laravel scheduler added to cron" # UFW firewall — add rules idempotently (no reset — reset drops SSH) ufw default deny incoming 2>/dev/null || true ufw default allow outgoing 2>/dev/null || true ufw allow ssh 2>/dev/null || true ufw allow 'Nginx Full' 2>/dev/null || true ufw --force enable 2>/dev/null || true ok "UFW firewall: SSH + HTTP/HTTPS only" # Fail2ban systemctl enable --now fail2ban 2>/dev/null || true ok "Fail2ban enabled" # ── SSL with Certbot ───────────────────────────────────────── step "SSL Certificate" apt-get install -yq certbot python3-certbot-nginx info "Requesting Let's Encrypt certificate for ${APP_DOMAIN}..." info "(DNS A record must already be pointing to ${PUBLIC_IP})" if certbot --nginx -d "$APP_DOMAIN" --non-interactive --agree-tos -m "$ADMIN_EMAIL" --redirect 2>/dev/null; then ok "SSL certificate installed — HTTPS enabled" else warn "SSL certificate failed or already exists — skipping" warn "If needed, run: certbot --nginx -d ${APP_DOMAIN} -m ${ADMIN_EMAIL} --agree-tos --redirect" fi # ── Final restart of all services ──────────────────────────── systemctl restart php8.4-fpm 2>/dev/null || true systemctl enable --now nginx 2>/dev/null || true systemctl restart nginx 2>/dev/null || true sleep 2 systemctl restart mailtidy-engine 2>/dev/null || true sleep 5 # engine must be up before agent registers supervisorctl restart mailtidy-queue:* 2>/dev/null || true supervisorctl restart mailtidy-agent 2>/dev/null || true # ── Auto-activate license ───────────────────────────────────── info "Activating license..." cd /var/www/mailtidy 2>/dev/null || true ACTIVATE_OUT=$(sudo -u www-data php8.4 artisan license:activate-manual "${LICENSE_KEY}" "regular" --no-interaction 2>&1) && { ok "License activated successfully" } || { warn "License activation output: ${ACTIVATE_OUT}" warn "Activate manually at: https://${APP_DOMAIN}/settings/license" } # ── Health check ──────────────────────────────────────────── step "Health check" sleep 5 HTTP_STATUS=$(curl -fsSL -o /dev/null -w "%{http_code}" \ --connect-timeout 5 "http://127.0.0.1" 2>/dev/null || echo "000") if [[ "$HTTP_STATUS" =~ ^(200|301|302)$ ]]; then ok "Nginx responding (HTTP ${HTTP_STATUS})" else warn "Nginx returned HTTP ${HTTP_STATUS} — check /var/log/nginx/error.log" fi ENGINE_STATUS=$(curl -fsSL -o /dev/null -w "%{http_code}" \ --connect-timeout 5 "http://127.0.0.1:8000/health" 2>/dev/null || echo "000") if [[ "$ENGINE_STATUS" == "200" ]]; then ok "Verification engine healthy" else warn "Engine returned HTTP ${ENGINE_STATUS} — check /var/log/mailtidy-engine/engine.log" fi AGENT_STATUS=$(supervisorctl status mailtidy-agent 2>/dev/null | awk '{print $2}') if [[ "$AGENT_STATUS" == "RUNNING" ]]; then ok "Worker agent running" else warn "Worker agent not running — check /var/log/mailtidy-engine/worker.log" fi # ── Done ───────────────────────────────────────────────────── echo "" echo -e "${GREEN}${BOLD}╔══════════════════════════════════════════════════════╗${RESET}" echo -e "${GREEN}${BOLD}║ MailTidy Pro installed successfully! 🎉 ║${RESET}" echo -e "${GREEN}${BOLD}╚══════════════════════════════════════════════════════╝${RESET}" echo "" echo -e " ${BOLD}Your app:${RESET} https://${APP_DOMAIN}" echo -e " ${BOLD}Login with:${RESET} ${ADMIN_EMAIL}" echo -e " ${BOLD}Docs:${RESET} https://mailtidypro.com/docs" echo "" echo -e " ${YELLOW}Next steps:${RESET}" echo -e " 1. Log in and complete the setup wizard" echo -e " 2. Configure SMTP in Settings → Email (for notifications)" echo -e " 3. Add Stripe keys in Settings → Billing (to accept payments)" echo -e " 4. Upload a small test list to verify everything works" echo "" echo -e " ${YELLOW}Need more throughput?${RESET} Add worker VPS:" echo -e " ${BOLD}curl -fsSL https://install.mailtidypro.com/worker | bash -s TOKEN https://${APP_DOMAIN}${RESET}" echo "" echo -e " Install log saved to: ${INSTALL_LOG}" echo -e " Support: ${BLUE}support@mailtidypro.com${RESET}" echo ""