Last active
October 28, 2025 15:59
-
-
Save gioxx/89a8520d1117114faaa1268f0c8f8740 to your computer and use it in GitHub Desktop.
SMTP interactive tester wrapper for swaks (https://jetmore.org/john/code/swaks/)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # SMTP interactive tester wrapper for swaks | |
| # Gioxx, 2025 - https://github.com/gioxx | |
| # - Checks and optionally installs swaks and Net::SSLeay (interactive) | |
| # - Prompts the user for SMTP parameters with sensible defaults (Office365) | |
| # - Builds a safe argument vector and runs swaks | |
| # - Appends an RFC-like date to the Subject | |
| set -euo pipefail | |
| # Prompt with default value | |
| prompt_default() { | |
| local var_name="$1"; shift | |
| local prompt_msg="$1"; shift | |
| local default="$1"; shift | |
| local reply | |
| if [ -t 0 ]; then | |
| read -r -p "$prompt_msg [$default]: " reply || reply="" | |
| else | |
| reply="$default" | |
| fi | |
| if [ -z "${reply}" ]; then | |
| eval "$var_name=\"\$default\"" | |
| else | |
| eval "$var_name=\"\$reply\"" | |
| fi | |
| } | |
| # Yes/No prompt (default yes = true) | |
| prompt_yesno() { | |
| local var_name="$1"; shift | |
| local prompt_msg="$1"; shift | |
| local default_yes="${1:-y}"; shift | |
| local reply | |
| if [ -t 0 ]; then | |
| read -r -p "$prompt_msg ($([ "$default_yes" = "y" ] && echo "Y/n" || echo "y/N")): " reply || reply="" | |
| else | |
| reply="$default_yes" | |
| fi | |
| reply="${reply,,}" # to lowercase | |
| if [ -z "$reply" ]; then | |
| reply="$default_yes" | |
| fi | |
| if [[ "$reply" == "y" || "$reply" == "yes" ]]; then | |
| eval "$var_name=true" | |
| else | |
| eval "$var_name=false" | |
| fi | |
| } | |
| # Root or sudo helper | |
| need_sudo() { | |
| if [ "$(id -u)" -eq 0 ]; then | |
| echo "" | |
| elif command -v sudo >/dev/null 2>&1; then | |
| echo "sudo" | |
| else | |
| echo "" | |
| fi | |
| } | |
| # Detect basic OS / package manager | |
| detect_pkg_env() { | |
| # Outputs two values: PKG_MGR and FAMILY | |
| # FAMILY: deb|rhel|alpine|mac|unknown | |
| if [ "$(uname -s)" = "Darwin" ]; then | |
| echo "brew mac" | |
| return | |
| fi | |
| if [ -f /etc/os-release ]; then | |
| . /etc/os-release | |
| ID_LIKE="${ID_LIKE:-}" | |
| case "$ID $ID_LIKE" in | |
| *debian*|*ubuntu*|*Debian*|*Ubuntu*) | |
| if command -v apt >/dev/null 2>&1 || command -v apt-get >/dev/null 2>&1; then | |
| echo "apt deb" | |
| return | |
| fi | |
| ;; | |
| *rhel*|*fedora*|*centos*|*rocky*|*almalinux*) | |
| if command -v dnf >/dev/null 2>&1; then | |
| echo "dnf rhel"; return | |
| elif command -v yum >/dev/null 2>&1; then | |
| echo "yum rhel"; return | |
| fi | |
| ;; | |
| *alpine*) | |
| if command -v apk >/dev/null 2>&1; then | |
| echo "apk alpine"; return | |
| fi | |
| ;; | |
| esac | |
| fi | |
| # Fallback unknown | |
| echo "unknown unknown" | |
| } | |
| # Install swaks | |
| install_swaks() { | |
| local SUDO; SUDO="$(need_sudo)" | |
| local PKG_MGR FAMILY; read -r PKG_MGR FAMILY < <(detect_pkg_env) | |
| case "$FAMILY" in | |
| deb) | |
| $SUDO apt update | |
| $SUDO apt install -y swaks | |
| ;; | |
| rhel) | |
| if [ "$PKG_MGR" = "dnf" ]; then | |
| $SUDO dnf install -y swaks || $SUDO dnf install -y perl-App-cpanminus | |
| else | |
| $SUDO yum install -y swaks || $SUDO yum install -y perl-App-cpanminus | |
| fi | |
| ;; | |
| alpine) | |
| $SUDO apk add --no-cache swaks | |
| ;; | |
| mac) | |
| if ! command -v brew >/dev/null 2>&1; then | |
| echo "Homebrew not found. Install it from https://brew.sh and re-run." >&2 | |
| return 1 | |
| fi | |
| brew install swaks | |
| ;; | |
| *) | |
| echo "Unsupported OS for auto-install of swaks. Please install swaks manually." >&2 | |
| return 1 | |
| ;; | |
| esac | |
| } | |
| # Install Net::SSLeay (and IO::Socket::SSL) | |
| install_net_ssleay() { | |
| local SUDO; SUDO="$(need_sudo)" | |
| local PKG_MGR FAMILY; read -r PKG_MGR FAMILY < <(detect_pkg_env) | |
| case "$FAMILY" in | |
| deb) | |
| $SUDO apt update | |
| $SUDO apt install -y libnet-ssleay-perl libio-socket-ssl-perl | |
| ;; | |
| rhel) | |
| if [ "$PKG_MGR" = "dnf" ]; then | |
| $SUDO dnf install -y perl-Net-SSLeay perl-IO-Socket-SSL | |
| else | |
| $SUDO yum install -y perl-Net-SSLeay perl-IO-Socket-SSL | |
| fi | |
| ;; | |
| alpine) | |
| # Package names can vary by repo; try common ones: | |
| $SUDO apk add --no-cache perl-net-ssleay perl-io-socket-ssl || { | |
| echo "Falling back to cpan for Alpine ..." >&2 | |
| $SUDO apk add --no-cache perl make gcc g++ openssl-dev perl-dev | |
| cpan -T -i Net::SSLeay IO::Socket::SSL | |
| } | |
| ;; | |
| mac) | |
| if ! command -v brew >/dev/null 2>&1; then | |
| echo "Homebrew not found. Install it from https://brew.sh and re-run." >&2 | |
| return 1 | |
| fi | |
| # Try system Perl via CPAN (often the most reliable for these modules) | |
| # Ensure Perl available (macOS has one), but we also prepare cpanminus if present | |
| if command -v cpanm >/dev/null 2>&1; then | |
| cpanm --notest Net::SSLeay IO::Socket::SSL | |
| else | |
| # cpan will ask first-time config; user interaction may be required | |
| cpan -T -i Net::SSLeay IO::Socket::SSL | |
| fi | |
| ;; | |
| *) | |
| echo "Unsupported OS for auto-install of Net::SSLeay. Please install it manually." >&2 | |
| return 1 | |
| ;; | |
| esac | |
| } | |
| # Checkers | |
| have_swaks() { command -v swaks >/dev/null 2>&1; } | |
| have_net_ssleay() { perl -MNet::SSLeay -e 'print $Net::SSLeay::VERSION' >/dev/null 2>&1; } | |
| if ! have_swaks; then | |
| echo "swaks not found." | |
| prompt_yesno INSTALL_SWAKS "Install swaks now?" y | |
| if [ "$INSTALL_SWAKS" = true ]; then | |
| install_swaks || { echo "Failed to install swaks." >&2; exit 2; } | |
| else | |
| echo "Cannot proceed without swaks." >&2 | |
| exit 2 | |
| fi | |
| fi | |
| # Check Net::SSLeay now; if missing and user selects TLS later, we'll re-check and offer install again. | |
| if ! have_net_ssleay; then | |
| echo "Perl module Net::SSLeay not found (required for TLS)." | |
| prompt_yesno INSTALL_SSL "Install Net::SSLeay (and IO::Socket::SSL) now?" y | |
| if [ "$INSTALL_SSL" = true ]; then | |
| install_net_ssleay || echo "Warning: failed to install Net::SSLeay. You can still use 'None' security." | |
| fi | |
| fi | |
| echo "=== SMTP interactive swaks wrapper ===" | |
| DEFAULT_HOST="smtp.office365.com" | |
| DEFAULT_PORT="587" | |
| DEFAULT_AUTH_METHOD="LOGIN" # LOGIN, PLAIN, CRAM-MD5 | |
| prompt_default HOST "SMTP server" "$DEFAULT_HOST" | |
| prompt_default PORT "Port" "$DEFAULT_PORT" | |
| echo "Choose security mode:" | |
| echo " 1) STARTTLS (recommended for port 587)" | |
| echo " 2) SMTPS (implicit TLS, typical port 465)" | |
| echo " 3) None (plain, dev only)" | |
| read -r -p "Select 1,2,3 [1]: " sec_choice | |
| sec_choice=${sec_choice:-1} | |
| case "$sec_choice" in | |
| 1) SECURITY="starttls" ;; | |
| 2) SECURITY="smtps" ;; | |
| 3) SECURITY="none" ;; | |
| *) SECURITY="starttls" ;; | |
| esac | |
| # If TLS was chosen, ensure Net::SSLeay is present (offer install if still missing) | |
| if [[ "$SECURITY" != "none" ]] && ! have_net_ssleay; then | |
| echo "TLS selected but Net::SSLeay is missing." | |
| prompt_yesno INSTALL_SSL2 "Install Net::SSLeay (and IO::Socket::SSL) now?" y | |
| if [ "$INSTALL_SSL2" = true ]; then | |
| install_net_ssleay || { echo "Failed to install Net::SSLeay; cannot proceed with TLS." >&2; exit 3; } | |
| else | |
| echo "Cannot proceed with TLS/SMTPS without Net::SSLeay. Choose 'None' or re-run." >&2 | |
| exit 3 | |
| fi | |
| fi | |
| prompt_yesno USE_AUTH "Use SMTP authentication?" y | |
| if [ "$USE_AUTH" = true ]; then | |
| read -r -p "Auth method (LOGIN/PLAIN/CRAM-MD5) [${DEFAULT_AUTH_METHOD}]: " AUTH_METHOD | |
| AUTH_METHOD=${AUTH_METHOD:-$DEFAULT_AUTH_METHOD} | |
| read -r -p "Username (user@domain): " USERNAME | |
| if [ -t 0 ]; then | |
| read -r -s -p "Password: " PASSWORD; echo | |
| else | |
| PASSWORD="" | |
| fi | |
| fi | |
| prompt_default FROM "From address" "${USERNAME:-noreply@contoso.com}" | |
| prompt_default TO "To address" "${FROM:-your.email@contoso.com}" | |
| prompt_default SUBJECT "Subject base text" "swaks SMTP test" | |
| echo "Enter message body — finish with Ctrl-D on an empty line:" | |
| BODY=$(cat) # Ctrl-D to end; empty -> default below | |
| DATE_STR=$(LC_TIME=C date +"%a, %d %b %Y %T %z") | |
| FINAL_SUBJECT="${SUBJECT} ${DATE_STR}" | |
| args=() | |
| args+=(--to "$TO") | |
| args+=(--from "$FROM") | |
| args+=(--server "$HOST") | |
| args+=(--port "$PORT") | |
| case "$SECURITY" in | |
| starttls) args+=(--tls) ;; | |
| smtps) args+=(--smtps) ;; | |
| none) : ;; | |
| esac | |
| if [ "$USE_AUTH" = true ]; then | |
| args+=(--auth "$AUTH_METHOD") | |
| args+=(--auth-user "$USERNAME") | |
| if [ -z "${PASSWORD:-}" ] && [ -t 0 ]; then | |
| read -r -s -p "Password (again): " PASSWORD; echo | |
| fi | |
| args+=(--auth-password "$PASSWORD") | |
| fi | |
| args+=(--header "Subject: $FINAL_SUBJECT") | |
| if [ -z "$BODY" ]; then | |
| BODY="Hello from swaks wrapper at $(date -u +"%Y-%m-%dT%H:%M:%SZ")" | |
| fi | |
| args+=(--body "$BODY") | |
| echo | |
| echo "=== Summary ===" | |
| echo "Server: $HOST:$PORT" | |
| echo "Security: $SECURITY" | |
| if [ "$USE_AUTH" = true ]; then | |
| echo "Auth: yes (method: $AUTH_METHOD, user: $USERNAME)" | |
| echo "Password: ******** (hidden)" | |
| else | |
| echo "Auth: no" | |
| fi | |
| echo "From: $FROM" | |
| echo "To: $TO" | |
| echo "Subject: $FINAL_SUBJECT" | |
| echo "Body length: ${#BODY} bytes" | |
| echo "================" | |
| echo | |
| prompt_yesno CONFIRM "Run swaks now?" y | |
| if [ "$CONFIRM" != true ]; then | |
| echo "Aborted by user." | |
| exit 0 | |
| fi | |
| echo "Running swaks ..." | |
| exec swaks "${args[@]}" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment