#!/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" # ── 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 [[ "$_yn" =~ ^[Yy]$ ]] || die "Aborted." fi # ── Pre-flight: Port 25 ────────────────────────────────────── step "Pre-flight checks" info "Testing outbound port 25 (required for SMTP verification)..." if timeout 10 bash -c 'cat < /dev/tcp/gmail-smtp-in.l.google.com/25' &>/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. 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 See: https://mailtidypro.com/docs#vps-providers Switch providers and re-run this installer." 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" echo -e " Enter the domain for your app (e.g. ${BOLD}app.yourdomain.com${RESET}):" echo -e " ${YELLOW}⚠ DNS A record must already point ${PUBLIC_IP} → this domain${RESET}" echo -e " ${YELLOW}⚠ Use DNS-only mode in Cloudflare (grey cloud, not orange)${RESET}\n" read -rp " App domain: " APP_DOMAIN [[ -z "$APP_DOMAIN" ]] && die "App domain is required." echo "" echo -e " Enter your CodeCanyon purchase code (from your Downloads page):" read -rp " License key: " LICENSE_KEY [[ -z "$LICENSE_KEY" ]] && die "License key is required." echo "" echo -e " Admin email address (for your login + SSL cert notifications):" read -rp " Admin email: " ADMIN_EMAIL [[ -z "$ADMIN_EMAIL" ]] && die "Admin email is required." echo "" echo -e " Admin password (minimum 12 characters):" read -rsp " Admin password: " ADMIN_PASS echo "" [[ ${#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 -base64 32 | tr -d '=/+' | head -c 48) WEBHOOK_SECRET=$(openssl rand -base64 32 | tr -d '=/+' | head -c 48) 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 "" read -rp " Looks good? Press Enter to start installing, or Ctrl+C to abort... " _ # 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 --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 sudo -u postgres psql -v ON_ERROR_STOP=1 <> "$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 /opt/mailtidy-engine/venv/bin/pip install --quiet \ fastapi==0.115.0 \ uvicorn==0.30.6 \ aiosmtplib==3.0.1 \ dnspython==2.7.0 \ pydantic==2.9.2 \ httpx==0.27.2 \ redis==5.2.0 \ python-dotenv==1.0.1 \ structlog==24.4.0 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 site rm -f /etc/nginx/sites-enabled/default # 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"; # 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 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://api.mailtidypro.com/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='venv' \ "${APP_SRC}/engine/" /opt/mailtidy-engine/ # 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 MAIL_MAILER=log MAIL_FROM_ADDRESS="noreply@${APP_DOMAIN}" MAIL_FROM_NAME="MailTidy Pro" # Verification engine (internal) ENGINE_URL=http://127.0.0.1:8000 ENGINE_SECRET=${ENGINE_SECRET} 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 LARAVEL_WEBHOOK_URL=https://${APP_DOMAIN}/api/python/result LARAVEL_PROGRESS_URL=https://${APP_DOMAIN}/api/python/progress WEBHOOK_SECRET=${WEBHOOK_SECRET} ENGINE_SECRET=${ENGINE_SECRET} REDIS_URL=redis://:${REDIS_PASS}@127.0.0.1:6379/1 MAX_CONCURRENCY=30 MAX_PER_DOMAIN=5 BATCH_SIZE=200 BATCH_TIMEOUT=300 LOG_FILE=/var/log/mailtidy-engine/engine.log ENGINEENV # 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 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 php artisan key:generate --force sudo -u www-data php artisan migrate --force sudo -u www-data php artisan db:seed --force sudo -u www-data php artisan storage:link sudo -u www-data php artisan optimize ok "MailTidy Pro application installed" # ── Create admin user ──────────────────────────────────────── info "Creating admin account..." sudo -u www-data php artisan tinker --execute=" \$u = \\App\\Models\\User::where('email', '${ADMIN_EMAIL}')->first(); if (!\$u) { \$u = \\App\\Models\\User::create([ 'name' => 'Admin', 'email' => '${ADMIN_EMAIL}', 'password' => \\Illuminate\\Support\\Facades\\Hash::make('${ADMIN_PASS}'), 'email_verified_at' => now(), 'role' => 'admin', 'is_extended' => false, ]); } echo 'Admin: ' . \$u->email; " ok "Admin account created: ${ADMIN_EMAIL}" # Clean up download rm -rf "${TMPDIR}" # ── Step 9: Services ───────────────────────────────────────── step "9 / 9 Services" # Supervisor for Laravel queue workers cat > /etc/supervisor/conf.d/mailtidy-queue.conf << 'SUPCONF' [program:mailtidy-queue] command=php /var/www/mailtidy/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600 directory=/var/www/mailtidy user=www-data numprocs=2 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 SUPCONF # systemd for Python engine 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 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 # systemd for worker agent (runs on the main VPS too) cat > /etc/systemd/system/mailtidy-worker.service << 'SYSDWRK' [Unit] Description=MailTidy Pro Worker Agent After=mailtidy-engine.service [Service] Type=simple User=root WorkingDirectory=/opt/mailtidy-engine EnvironmentFile=/opt/mailtidy-engine/.env ExecStart=/opt/mailtidy-engine/venv/bin/python agent.py Restart=always RestartSec=10 StandardOutput=append:/var/log/mailtidy-engine/worker.log StandardError=append:/var/log/mailtidy-engine/worker.log [Install] WantedBy=multi-user.target SYSDWRK systemctl daemon-reload systemctl enable --now mailtidy-engine systemctl enable --now mailtidy-worker supervisorctl reread && supervisorctl update 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 php /var/www/mailtidy/artisan schedule:run >> /dev/null 2>&1") \ | crontab - ok "Laravel scheduler added to cron" # UFW firewall ufw --force reset ufw default deny incoming ufw default allow outgoing ufw allow ssh ufw allow 'Nginx Full' ufw --force enable ok "UFW firewall: SSH + HTTP/HTTPS only" # Fail2ban systemctl enable --now fail2ban 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; then ok "SSL certificate installed — HTTPS enabled" else warn "SSL certificate failed. This usually means the DNS A record" warn "for '${APP_DOMAIN}' hasn't propagated yet." warn "" warn "Once your DNS is pointed, run:" warn " certbot --nginx -d ${APP_DOMAIN} -m ${ADMIN_EMAIL} --agree-tos --redirect" fi # ── Final restart of all services ──────────────────────────── systemctl restart php8.4-fpm systemctl restart nginx systemctl restart mailtidy-engine systemctl restart mailtidy-worker supervisorctl restart mailtidy-queue:* # ── Health check ──────────────────────────────────────────── step "Health check" sleep 3 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 # ── 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 ""