#!/usr/bin/env bash # ============================================================================== # Nextxch - Official One-Line Terminal Installer & Updater (Linux/macOS) # Engineered in Germany by iqmeta GmbH - https://nextxch.com/ # # Quick Interactive Install: # curl -fsSL https://nextxch.com/install.sh | sudo bash # # Non-Interactive / Automation Example: # curl -fsSL https://nextxch.com/install.sh | sudo bash -s -- \ # --mode docker --domain example.com --hostname mail.example.com \ # --admin admin@example.com --license "sml1..." # # In-Place Update: # curl -fsSL https://nextxch.com/install.sh | sudo bash -s -- --update # ============================================================================== set -euo pipefail VERSION="1.3.0" PORTAL_URL="https://nextxch.com" MODE="" DOMAIN="" MAIL_HOSTNAME="" ADMIN_EMAIL="" ADMIN_PASSWORD="" LICENSE_TOKEN="" INSTALL_DIR="/opt/nextxch" UPDATE_ONLY=0 NON_INTERACTIVE=0 while [ $# -gt 0 ]; do case "$1" in --mode) MODE="$2"; shift 2 ;; --domain) DOMAIN="$2"; shift 2 ;; --hostname) MAIL_HOSTNAME="$2"; shift 2 ;; --admin) ADMIN_EMAIL="$2"; shift 2 ;; --password) ADMIN_PASSWORD="$2"; shift 2 ;; --license) LICENSE_TOKEN="$2"; shift 2 ;; --dir) INSTALL_DIR="$2"; shift 2 ;; --update) UPDATE_ONLY=1; shift ;; -y|--yes) NON_INTERACTIVE=1; shift ;; *) echo "Unknown argument: $1"; exit 1 ;; esac done echo "" echo "======================================================================" echo " Nextxch v${VERSION} - Enterprise Mail, Groupware & GoBD" echo " Engineered in Germany by iqmeta GmbH (free up to 6 user accounts for testing)" echo "======================================================================" echo "" OS="$(uname -s)" ARCH="$(uname -m)" case "${ARCH}" in x86_64|amd64) DEB_ARCH="amd64"; RID_ARCH="x64" ;; aarch64|arm64) DEB_ARCH="arm64"; RID_ARCH="arm64" ;; *) echo "Unsupported CPU architecture: ${ARCH}"; exit 1 ;; esac prompt_tty() { local prompt_text="$1" local default_val="$2" local reply="" if [ "${NON_INTERACTIVE}" -eq 0 ] && [ -r /dev/tty ]; then printf "%s [%s]: " "${prompt_text}" "${default_val}" > /dev/tty IFS= read -r reply < /dev/tty || true fi if [ -z "${reply}" ]; then printf "%s" "${default_val}" else printf "%s" "${reply}" fi } verify_sha256() { local file_path="$1" local artifact_name="$2" if command -v sha256sum >/dev/null 2>&1; then local expected="" expected="$(curl -fsSL "${PORTAL_URL}/downloads/SHA256SUMS.txt" 2>/dev/null | awk -v f="${artifact_name}" '$2 == f || $2 == "*"f { print $1; exit }' || true)" if [ -n "${expected}" ]; then local actual="" actual="$(sha256sum "${file_path}" | awk '{ print $1 }')" if [ "${actual}" != "${expected}" ]; then echo "ERROR: SHA-256 verification failed for ${artifact_name}!" >&2 echo " Expected: ${expected}" >&2 echo " Actual: ${actual}" >&2 exit 1 fi echo "-> Verified SHA-256 checksum for ${artifact_name} (${actual:0:16}...)" fi fi } ensure_docker_image() { local image_ref="nextxch/server:${VERSION}" echo "-> Resolving OCI container image ${image_ref}..." if docker pull "${image_ref}" >/dev/null 2>&1; then echo "-> Pulled ${image_ref} from registry." return 0 fi echo "-> Public Docker Hub mirror not yet active; building ${image_ref} locally from verified release archive..." local tmp_ctx="" tmp_ctx="$(mktemp -d /tmp/nextxch-oci-XXXXXX)" local archive_name="nextxch_${VERSION}_linux-${RID_ARCH}.tar.gz" curl -fL "${PORTAL_URL}/downloads/${archive_name}" -o "${tmp_ctx}/${archive_name}" verify_sha256 "${tmp_ctx}/${archive_name}" "${archive_name}" mkdir -p "${tmp_ctx}/app" tar -xzf "${tmp_ctx}/${archive_name}" -C "${tmp_ctx}/app" cat > "${tmp_ctx}/Dockerfile" < /usr/local/bin/nextxch-cli \\ && chmod +x /usr/local/bin/nextxch-cli VOLUME ["/app/data"] EXPOSE 80 443 25 465 587 143 993 4190 ENV ASPNETCORE_URLS="http://0.0.0.0:80;https://0.0.0.0:443" \\ Storage__DataDirectory="/app/data" \\ MALLOC_ARENA_MAX="2" \\ MALLOC_MMAP_THRESHOLD_="131072" ENTRYPOINT ["/app/Nextxch.Server"] EOF docker build -t "${image_ref}" "${tmp_ctx}" rm -rf "${tmp_ctx}" } # Handle seamless in-place update (--update) if [ "${UPDATE_ONLY}" -eq 1 ]; then if [ -f "${INSTALL_DIR}/docker-compose.yml" ] && command -v docker >/dev/null 2>&1; then echo "-> Updating Nextxch Docker Compose stack in ${INSTALL_DIR} to v${VERSION}..." ensure_docker_image if [ -f "${INSTALL_DIR}/.env" ]; then sed -i.bak "s/^NEXTXCH_TAG=.*/NEXTXCH_TAG=${VERSION}/" "${INSTALL_DIR}/.env" && rm -f "${INSTALL_DIR}/.env.bak" fi (cd "${INSTALL_DIR}" && docker compose up -d) cat > /usr/local/bin/nextxch-cli <<'EOFCLI' #!/usr/bin/env bash exec docker exec -i nextxch /app/Nextxch.Server cli "$@" EOFCLI chmod +x /usr/local/bin/nextxch-cli echo "Update complete (v${VERSION})." exit 0 elif [ "${OS}" = "Linux" ] && command -v dpkg >/dev/null 2>&1 && dpkg -s nextxch >/dev/null 2>&1; then DEB_NAME="nextxch_${VERSION}_${DEB_ARCH}.deb" echo "-> Updating native Debian/Ubuntu package (${DEB_NAME})..." TMP_DEB="$(mktemp /tmp/nextxch-XXXXXX.deb)" curl -fL "${PORTAL_URL}/downloads/${DEB_NAME}" -o "${TMP_DEB}" verify_sha256 "${TMP_DEB}" "${DEB_NAME}" dpkg -i "${TMP_DEB}" rm -f "${TMP_DEB}" echo "Update complete (v${VERSION})." exit 0 elif [ "${OS}" = "Linux" ] && [ -f "/opt/nextxch/Nextxch.Server" ]; then TAR_NAME="nextxch_${VERSION}_linux-${RID_ARCH}.tar.gz" echo "-> Updating native Linux systemd installation in /opt/nextxch..." TMP_TAR="$(mktemp /tmp/nextxch-XXXXXX.tar.gz)" curl -fL "${PORTAL_URL}/downloads/${TAR_NAME}" -o "${TMP_TAR}" verify_sha256 "${TMP_TAR}" "${TAR_NAME}" systemctl stop nextxch.service || true tar -xzf "${TMP_TAR}" -C /opt/nextxch rm -f "${TMP_TAR}" chmod +x /opt/nextxch/Nextxch.Server cat > /usr/local/bin/nextxch-cli <<'EOFCLI' #!/usr/bin/env bash exec /opt/nextxch/Nextxch.Server cli "$@" EOFCLI chmod +x /usr/local/bin/nextxch-cli chown -R nextxch:nextxch /opt/nextxch 2>/dev/null || true systemctl start nextxch.service echo "Update complete (v${VERSION})." exit 0 fi fi # Auto-detect best mode if not explicitly provided if [ -z "${MODE}" ]; then if [ "${OS}" = "Darwin" ]; then MODE="macos" elif command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then DEFAULT_MODE="docker" MODE="$(prompt_tty "Deployment mode ('docker' for Docker Compose + PostgreSQL 18, or 'native' for systemd/.deb)" "${DEFAULT_MODE}")" else DEFAULT_MODE="native" MODE="$(prompt_tty "Deployment mode ('native' for systemd/.deb, or 'docker' for Docker Compose)" "${DEFAULT_MODE}")" fi fi if [ -z "${DOMAIN}" ]; then DETECTED_DOMAIN="$(hostname -d 2>/dev/null || true)" [ -z "${DETECTED_DOMAIN}" ] && DETECTED_DOMAIN="example.com" DOMAIN="$(prompt_tty "Primary Mail Domain" "${DETECTED_DOMAIN}")" fi if [ -z "${MAIL_HOSTNAME}" ]; then MAIL_HOSTNAME="$(prompt_tty "Public MX / TLS Hostname" "mail.${DOMAIN}")" fi if [ -z "${ADMIN_EMAIL}" ]; then ADMIN_EMAIL="$(prompt_tty "Initial ServerAdmin Email" "admin@${DOMAIN}")" fi if [ -z "${ADMIN_PASSWORD}" ]; then GEN_PASS="Sm!$(head -c 12 /dev/urandom | od -An -tx1 | tr -d ' \n')" ADMIN_PASSWORD="$(prompt_tty "Initial ServerAdmin Password" "${GEN_PASS}")" fi if [ -z "${LICENSE_TOKEN}" ] && [ "${NON_INTERACTIVE}" -eq 0 ]; then LICENSE_TOKEN="$(prompt_tty "Optional License Token (leave empty for Free Tier <= 10 accounts)" "")" fi if [ "${MODE}" = "docker" ]; then if ! command -v docker >/dev/null 2>&1; then echo "Installing Docker Engine..." curl -fsSL https://get.docker.com | sh fi ensure_docker_image mkdir -p "${INSTALL_DIR}" PG_PASS="Pg!$(head -c 14 /dev/urandom | od -An -tx1 | tr -d ' \n')" cat > "${INSTALL_DIR}/.env" < "${INSTALL_DIR}/docker-compose.yml" <<'EOF' services: postgres: image: postgres:18-alpine container_name: nextxch-postgres restart: unless-stopped environment: POSTGRES_DB: nextxch POSTGRES_USER: nextxch POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} volumes: - pgdata:/var/lib/postgresql healthcheck: test: ["CMD-SHELL", "pg_isready -U nextxch -d nextxch"] interval: 5s timeout: 5s retries: 10 nextxch: image: nextxch/server:${NEXTXCH_TAG:-1.3.0} container_name: nextxch restart: unless-stopped depends_on: postgres: condition: service_healthy ports: - "25:25" - "80:80" - "143:143" - "443:443" - "465:465" - "587:587" - "993:993" - "4190:4190" environment: ASPNETCORE_URLS: "http://0.0.0.0:80;https://0.0.0.0:443" MALLOC_ARENA_MAX: "2" MALLOC_MMAP_THRESHOLD_: "131072" Storage__Provider: "Postgres" Storage__PostgresConnectionString: "Host=postgres;Port=5432;Database=nextxch;Username=nextxch;Password=${POSTGRES_PASSWORD}" Storage__DataDirectory: "/app/data" Cluster__Enabled: "true" Bootstrap__DefaultDomain: "${NEXTXCH_DOMAIN}" Bootstrap__AdminEmail: "${NEXTXCH_ADMIN_EMAIL}" Bootstrap__AdminPassword: "${NEXTXCH_ADMIN_PASSWORD}" License__Token: "${NEXTXCH_LICENSE_TOKEN}" Smtp__Enabled: "true" Smtp__ServerHostName: "${NEXTXCH_HOSTNAME}" Smtp__OutboundWorkerEnabled: "true" Imap__Enabled: "true" Imap__ServerHostName: "${NEXTXCH_HOSTNAME}" ManageSieve__Enabled: "true" ManageSieve__ServerHostName: "${NEXTXCH_HOSTNAME}" volumes: - maildata:/app/data volumes: pgdata: maildata: EOF echo "-> Starting Nextxch v${VERSION} + PostgreSQL 18 Docker Compose stack..." (cd "${INSTALL_DIR}" && docker compose up -d) cat > /usr/local/bin/nextxch-cli <<'EOFCLI' #!/usr/bin/env bash exec docker exec -i nextxch /app/Nextxch.Server cli "$@" EOFCLI chmod +x /usr/local/bin/nextxch-cli elif [ "${OS}" = "Darwin" ] || [ "${MODE}" = "macos" ]; then TAR_NAME="nextxch_${VERSION}_osx-${RID_ARCH}.tar.gz" echo "-> Installing Nextxch native macOS launchd service (osx-${RID_ARCH})..." mkdir -p /usr/local/nextxch /usr/local/var/nextxch/logs TMP_TAR="$(mktemp /tmp/nextxch-XXXXXX.tar.gz)" curl -fL "${PORTAL_URL}/downloads/${TAR_NAME}" -o "${TMP_TAR}" tar -xzf "${TMP_TAR}" -C /usr/local/nextxch rm -f "${TMP_TAR}" bash /usr/local/nextxch/install-macos.sh cat > /usr/local/bin/nextxch-cli <<'EOFCLI' #!/usr/bin/env bash exec /usr/local/nextxch/Nextxch.Server cli "$@" EOFCLI chmod +x /usr/local/bin/nextxch-cli elif command -v dpkg >/dev/null 2>&1 && [ -f /etc/debian_version ]; then DEB_NAME="nextxch_${VERSION}_${DEB_ARCH}.deb" echo "-> Installing native Debian/Ubuntu package (${DEB_NAME})..." TMP_DEB="$(mktemp /tmp/nextxch-XXXXXX.deb)" curl -fL "${PORTAL_URL}/downloads/${DEB_NAME}" -o "${TMP_DEB}" verify_sha256 "${TMP_DEB}" "${DEB_NAME}" NEXTXCH_DOMAIN="${DOMAIN}" \ NEXTXCH_ADMIN_EMAIL="${ADMIN_EMAIL}" \ NEXTXCH_ADMIN_PASSWORD="${ADMIN_PASSWORD}" \ NEXTXCH_LICENSE_TOKEN="${LICENSE_TOKEN}" \ dpkg -i "${TMP_DEB}" rm -f "${TMP_DEB}" if [ -x /opt/nextxch/Nextxch.Server ] && [ ! -f /usr/local/bin/nextxch-cli ]; then cat > /usr/local/bin/nextxch-cli <<'EOFCLI' #!/usr/bin/env bash exec /opt/nextxch/Nextxch.Server cli "$@" EOFCLI chmod +x /usr/local/bin/nextxch-cli fi else TAR_NAME="nextxch_${VERSION}_linux-${RID_ARCH}.tar.gz" echo "-> Installing native Linux systemd release (linux-${RID_ARCH})..." getent group nextxch >/dev/null 2>&1 || groupadd -r nextxch id -u nextxch >/dev/null 2>&1 || useradd -r -g nextxch -d /var/lib/nextxch -s /sbin/nologin nextxch mkdir -p /opt/nextxch /var/lib/nextxch TMP_TAR="$(mktemp /tmp/nextxch-XXXXXX.tar.gz)" curl -fL "${PORTAL_URL}/downloads/${TAR_NAME}" -o "${TMP_TAR}" verify_sha256 "${TMP_TAR}" "${TAR_NAME}" tar -xzf "${TMP_TAR}" -C /opt/nextxch rm -f "${TMP_TAR}" chown -R nextxch:nextxch /var/lib/nextxch chmod +x /opt/nextxch/Nextxch.Server cat > /usr/local/bin/nextxch-cli <<'EOFCLI' #!/usr/bin/env bash exec /opt/nextxch/Nextxch.Server cli "$@" EOFCLI chmod +x /usr/local/bin/nextxch-cli cp /opt/nextxch/nextxch.service /etc/systemd/system/nextxch.service systemctl daemon-reload systemctl enable --now nextxch.service fi echo "" echo "======================================================================" echo " Nextxch v${VERSION} is installed and running!" echo " - Admin & GoBD Cockpit: https://${MAIL_HOSTNAME}/admin/ (http://localhost/admin/)" echo " - Nextxch Webmail: https://${MAIL_HOSTNAME}/mail/ (http://localhost/mail/)" echo " - CalDAV / CardDAV: https://${MAIL_HOSTNAME}/dav/" echo " - Management & IaC CLI: nextxch-cli describe" echo " - Admin Login: ${ADMIN_EMAIL}" echo " - DNS & Reverse-DNS: https://nextxch.com/dns-advisor/?domain=${DOMAIN}" echo "======================================================================"