#!/usr/bin/env bash # # html_mail_send.sh - A sophisticated HTML email sender # Sends HTML emails via SMTP with style # set -euo pipefail # ═══════════════════════════════════════════════════════════════════════════════ # COLORS & STYLING # ═══════════════════════════════════════════════════════════════════════════════ # Reset RST='\033[0m' # Regular Colors BLK='\033[0;30m' RED='\033[0;31m' GRN='\033[0;32m' YLW='\033[0;33m' BLU='\033[0;34m' MAG='\033[0;35m' CYN='\033[0;36m' WHT='\033[0;37m' # Bold Colors BBLK='\033[1;30m' BRED='\033[1;31m' BGRN='\033[1;32m' BYLW='\033[1;33m' BBLU='\033[1;34m' BMAG='\033[1;35m' BCYN='\033[1;36m' BWHT='\033[1;37m' # Background Colors BGBLK='\033[40m' BGRED='\033[41m' BGGRN='\033[42m' BGYLW='\033[43m' BGBLU='\033[44m' BGMAG='\033[45m' BGCYN='\033[46m' BGWHT='\033[47m' # Special DIM='\033[2m' ITALIC='\033[3m' ULINE='\033[4m' BLINK='\033[5m' # Gradient colors (256 color mode) G1='\033[38;5;39m' # Light blue G2='\033[38;5;38m' # Cyan G3='\033[38;5;37m' # Teal G4='\033[38;5;36m' # Sea green G5='\033[38;5;35m' # Green # Accent colors ACC1='\033[38;5;213m' # Pink ACC2='\033[38;5;207m' # Hot pink ACC3='\033[38;5;171m' # Purple ACC4='\033[38;5;135m' # Violet # ═══════════════════════════════════════════════════════════════════════════════ # UNICODE SYMBOLS # ═══════════════════════════════════════════════════════════════════════════════ SYM_CHECK="✓" SYM_CROSS="✗" SYM_ARROW="→" SYM_MAIL="✉" SYM_LOCK="🔒" SYM_SEND="📤" SYM_DOT="●" SYM_STAR="★" SYM_BOLT="⚡" SYM_INFO="ℹ" SYM_WARN="⚠" # Box drawing characters BOX_TL="╭" BOX_TR="╮" BOX_BL="╰" BOX_BR="╯" BOX_H="─" BOX_V="│" BOX_HB="━" # ═══════════════════════════════════════════════════════════════════════════════ # CONFIGURATION # ═══════════════════════════════════════════════════════════════════════════════ VERSION="1.0.0" SCRIPT_NAME="$(basename "$0")" # Default values SMTP_SERVER="" SMTP_PORT="587" SMTP_USER="" SMTP_PASS="" SMTP_TLS="yes" FROM_EMAIL="" FROM_NAME="" TO_EMAIL="" SUBJECT="" HTML_FILE="" VERBOSE=false DRY_RUN=false # ═══════════════════════════════════════════════════════════════════════════════ # HELPER FUNCTIONS # ═══════════════════════════════════════════════════════════════════════════════ print_header() { local width=60 echo "" echo -e "${G1}${BOX_TL}$(printf '%*s' $((width-2)) '' | tr ' ' "$BOX_H")${BOX_TR}${RST}" echo -e "${G2}${BOX_V}${RST} ${BMAG}${SYM_MAIL}${RST} ${BWHT}S E N D _ M A I L${RST} ${ACC1}v${VERSION}${RST}$(printf '%*s' 23 '')${G2}${BOX_V}${RST}" echo -e "${G3}${BOX_V}${RST} ${DIM}${ITALIC}Sophisticated HTML Email Sender${RST}$(printf '%*s' 18 '')${G3}${BOX_V}${RST}" echo -e "${G4}${BOX_BL}$(printf '%*s' $((width-2)) '' | tr ' ' "$BOX_H")${BOX_BR}${RST}" echo "" } print_section() { local title="$1" echo -e "\n${ACC3}${SYM_STAR}${RST} ${BCYN}${title}${RST}" echo -e "${DIM}$(printf '%*s' 50 '' | tr ' ' '─')${RST}" } print_success() { echo -e "${BGRN}${SYM_CHECK}${RST} ${GRN}$1${RST}" } print_error() { echo -e "${BRED}${SYM_CROSS}${RST} ${RED}$1${RST}" >&2 } print_warning() { echo -e "${BYLW}${SYM_WARN}${RST} ${YLW}$1${RST}" } print_info() { echo -e "${BCYN}${SYM_INFO}${RST} ${CYN}$1${RST}" } print_step() { echo -e " ${ACC1}${SYM_ARROW}${RST} ${WHT}$1${RST}" } print_value() { local label="$1" local value="$2" printf " ${DIM}%-15s${RST} ${BWHT}%s${RST}\n" "$label:" "$value" } spinner() { local pid=$1 local message="${2:-Processing}" local spinchars='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏' local i=0 tput civis # Hide cursor while kill -0 "$pid" 2>/dev/null; do printf "\r ${ACC2}${spinchars:i++%${#spinchars}:1}${RST} ${WHT}%s...${RST}" "$message" sleep 0.1 done tput cnorm # Show cursor printf "\r%*s\r" 50 "" # Clear line } progress_bar() { local current=$1 local total=$2 local width=30 local percent=$((current * 100 / total)) local filled=$((current * width / total)) local empty=$((width - filled)) local bar="" for ((i=0; i/dev/null; then print_error "curl is required but not installed" echo -e " ${DIM}Install with: ${RST}${CYN}sudo apt install curl${RST}" exit 1 fi # Check if curl has SMTP support if ! curl -V | grep -q 'smtp'; then print_warning "curl may not have SMTP support" fi } validate_inputs() { local errors=0 print_section "Validating Configuration" # Check required fields if [[ -z "$SMTP_SERVER" ]]; then print_error "SMTP server is required (-s, --server)" ((errors++)) else print_success "SMTP server: $SMTP_SERVER" fi if [[ -z "$SMTP_USER" ]]; then print_error "SMTP username is required (-u, --user)" ((errors++)) else print_success "SMTP user: $SMTP_USER" fi if [[ -z "$SMTP_PASS" ]]; then # Try environment variable if [[ -n "${SMTP_PASSWORD:-}" ]]; then SMTP_PASS="$SMTP_PASSWORD" print_success "SMTP password: (from environment)" else print_error "SMTP password is required (-p, --pass or SMTP_PASSWORD env var)" ((errors++)) fi else print_success "SMTP password: ********" fi if [[ -z "$FROM_EMAIL" ]]; then print_error "From email is required (-f, --from)" ((errors++)) elif ! validate_email "$FROM_EMAIL"; then print_error "Invalid from email format: $FROM_EMAIL" ((errors++)) else print_success "From: ${FROM_NAME:+$FROM_NAME <}$FROM_EMAIL${FROM_NAME:+>}" fi if [[ -z "$TO_EMAIL" ]]; then print_error "To email is required (-t, --to)" ((errors++)) elif ! validate_email "$TO_EMAIL"; then print_error "Invalid to email format: $TO_EMAIL" ((errors++)) else print_success "To: $TO_EMAIL" fi if [[ -z "$SUBJECT" ]]; then print_error "Subject is required (-j, --subject)" ((errors++)) else print_success "Subject: $SUBJECT" fi if [[ -z "$HTML_FILE" ]]; then print_error "HTML file is required (-h, --html)" ((errors++)) elif ! validate_file "$HTML_FILE"; then print_error "HTML file not found or not readable: $HTML_FILE" ((errors++)) else local size size=$(stat -c%s "$HTML_FILE" 2>/dev/null || stat -f%z "$HTML_FILE" 2>/dev/null || echo "?") print_success "HTML file: $HTML_FILE (${size} bytes)" fi if ((errors > 0)); then echo "" print_error "Found $errors error(s). Please fix and try again." echo "" exit 1 fi echo "" print_success "All validations passed!" } # ═══════════════════════════════════════════════════════════════════════════════ # EMAIL SENDING # ═══════════════════════════════════════════════════════════════════════════════ generate_boundary() { echo "----=_Part_$(date +%s%N)_$(od -An -tx4 -N4 /dev/urandom | tr -d ' ')" } send_email() { print_section "Preparing Email" local boundary boundary=$(generate_boundary) # Build From header local from_header if [[ -n "$FROM_NAME" ]]; then from_header="$FROM_NAME <$FROM_EMAIL>" else from_header="$FROM_EMAIL" fi # Generate message ID local msg_id msg_id="<$(date +%s%N).$(od -An -tx4 -N4 /dev/urandom | tr -d ' ')@${SMTP_SERVER}>" # Current date in RFC 2822 format local date_header date_header=$(date -R) print_step "Building MIME message..." # Create temporary file for email content local tmp_email tmp_email=$(mktemp) # Store in global for cleanup trap _TMP_EMAIL="$tmp_email" trap 'rm -f "$_TMP_EMAIL"' EXIT # Build the email with proper CRLF line endings for SMTP { printf 'From: %s\r\n' "$from_header" printf 'To: %s\r\n' "$TO_EMAIL" printf 'Subject: %s\r\n' "$SUBJECT" printf 'Date: %s\r\n' "$date_header" printf 'Message-ID: %s\r\n' "$msg_id" printf 'MIME-Version: 1.0\r\n' printf 'Content-Type: multipart/alternative; boundary="%s"\r\n' "$boundary" printf 'X-Mailer: html_mail_send.sh/%s\r\n' "$VERSION" printf '\r\n' printf -- '--%s\r\n' "$boundary" printf 'Content-Type: text/plain; charset="UTF-8"\r\n' printf 'Content-Transfer-Encoding: 7bit\r\n' printf '\r\n' printf 'This email requires an HTML-capable email client.\r\n' printf '\r\n' printf -- '--%s\r\n' "$boundary" printf 'Content-Type: text/html; charset="UTF-8"\r\n' printf 'Content-Transfer-Encoding: base64\r\n' printf '\r\n' # Base64 encode with 76-char lines and convert LF to CRLF base64 -w 76 "$HTML_FILE" | sed 's/$/\r/' printf '\r\n' printf -- '--%s--\r\n' "$boundary" } > "$tmp_email" print_success "MIME message created" # Show summary print_section "Email Summary" print_value "From" "$from_header" print_value "To" "$TO_EMAIL" print_value "Subject" "$SUBJECT" print_value "SMTP Server" "${SMTP_SERVER}:${SMTP_PORT}" print_value "TLS/SSL" "$([[ "$SMTP_TLS" == "yes" ]] && echo "Enabled" || echo "Disabled")" if [[ "$DRY_RUN" == "true" ]]; then echo "" print_warning "DRY RUN - Email would be sent with the above settings" print_info "Email content saved to: $tmp_email" if [[ "$VERBOSE" == "true" ]]; then print_section "Email Content Preview" echo -e "${DIM}" head -50 "$tmp_email" echo -e "${RST}" echo -e "${DIM}[... truncated ...]${RST}" fi # Don't delete temp file in dry run trap - EXIT return 0 fi print_section "Sending Email" # Build curl command local curl_opts=() curl_opts+=(--url "smtp://${SMTP_SERVER}:${SMTP_PORT}") curl_opts+=(--user "${SMTP_USER}:${SMTP_PASS}") curl_opts+=(--mail-from "$FROM_EMAIL") curl_opts+=(--mail-rcpt "$TO_EMAIL") curl_opts+=(--upload-file "$tmp_email") if [[ "$SMTP_TLS" == "yes" ]]; then curl_opts+=(--ssl-reqd) elif [[ "$SMTP_TLS" == "ssl" ]]; then curl_opts=(--url "smtps://${SMTP_SERVER}:${SMTP_PORT}" "${curl_opts[@]:1}") fi if [[ "$VERBOSE" == "true" ]]; then curl_opts+=(--verbose) else curl_opts+=(--silent --show-error) fi # Send the email echo -ne " ${ACC2}⠋${RST} ${WHT}Connecting to SMTP server...${RST}" local start_time start_time=$(date +%s) if curl "${curl_opts[@]}" 2>&1; then local end_time end_time=$(date +%s) local duration=$((end_time - start_time)) printf "\r%*s\r" 50 "" echo "" echo -e " ${BGGRN}${BLK} ${SYM_CHECK} SUCCESS ${RST}" echo "" print_success "Email sent successfully!" print_info "Delivery time: ${duration}s" echo "" # Fun success animation echo -e " ${G1}${SYM_SEND}${RST} ${SYM_ARROW} ${G3}${SYM_ARROW}${RST} ${G5}${SYM_ARROW}${RST} ${ACC1}${SYM_MAIL}${RST} ${DIM}→ ${TO_EMAIL}${RST}" echo "" else printf "\r%*s\r" 50 "" echo "" echo -e " ${BGRED}${WHT} ${SYM_CROSS} FAILED ${RST}" echo "" print_error "Failed to send email" print_info "Try running with --verbose for more details" exit 1 fi } # ═══════════════════════════════════════════════════════════════════════════════ # ARGUMENT PARSING # ═══════════════════════════════════════════════════════════════════════════════ parse_args() { while [[ $# -gt 0 ]]; do case "$1" in -s|--server) SMTP_SERVER="$2" shift 2 ;; -P|--port) SMTP_PORT="$2" shift 2 ;; -u|--user) SMTP_USER="$2" shift 2 ;; -p|--pass) SMTP_PASS="$2" shift 2 ;; -f|--from) FROM_EMAIL="$2" shift 2 ;; -n|--name) FROM_NAME="$2" shift 2 ;; -t|--to) TO_EMAIL="$2" shift 2 ;; -j|--subject) SUBJECT="$2" shift 2 ;; -h|--html) HTML_FILE="$2" shift 2 ;; --no-tls) SMTP_TLS="no" shift ;; --ssl) SMTP_TLS="ssl" SMTP_PORT="${SMTP_PORT:-465}" shift ;; -v|--verbose) VERBOSE=true shift ;; --dry-run) DRY_RUN=true shift ;; --help) show_usage exit 0 ;; --version) show_version exit 0 ;; -*) print_error "Unknown option: $1" echo -e " Run ${CYN}$SCRIPT_NAME --help${RST} for usage" exit 1 ;; *) print_error "Unexpected argument: $1" echo -e " Run ${CYN}$SCRIPT_NAME --help${RST} for usage" exit 1 ;; esac done } # ═══════════════════════════════════════════════════════════════════════════════ # INTERACTIVE MODE # ═══════════════════════════════════════════════════════════════════════════════ prompt_input() { local prompt="$1" local var_name="$2" local default="${3:-}" local is_password="${4:-false}" local current_value="${!var_name}" if [[ -n "$default" ]]; then printf " ${ACC1}${SYM_ARROW}${RST} ${WHT}%s${RST} ${DIM}[%s]${RST}: " "$prompt" "$default" else printf " ${ACC1}${SYM_ARROW}${RST} ${WHT}%s${RST}: " "$prompt" fi local input if [[ "$is_password" == "true" ]]; then read -rs input echo "" else read -r input fi if [[ -z "$input" && -n "$default" ]]; then eval "$var_name='$default'" elif [[ -n "$input" ]]; then eval "$var_name='$input'" fi } interactive_mode() { print_header print_section "SMTP Configuration" prompt_input "SMTP Server" SMTP_SERVER prompt_input "SMTP Port" SMTP_PORT "587" prompt_input "SMTP Username" SMTP_USER if [[ -z "$SMTP_PASS" && -z "${SMTP_PASSWORD:-}" ]]; then prompt_input "SMTP Password" SMTP_PASS "" true else print_info "Using password from environment or argument" fi echo "" echo -e " ${DIM}TLS/SSL Options:${RST}" echo -e " ${CYN}1${RST}) STARTTLS (port 587) ${DIM}- recommended${RST}" echo -e " ${CYN}2${RST}) SSL/TLS (port 465)" echo -e " ${CYN}3${RST}) None (insecure)" printf " ${ACC1}${SYM_ARROW}${RST} ${WHT}Select encryption${RST} ${DIM}[1]${RST}: " read -r tls_choice case "${tls_choice:-1}" in 2) SMTP_TLS="ssl" SMTP_PORT="465" ;; 3) SMTP_TLS="no" ;; *) SMTP_TLS="yes" ;; esac print_section "Email Details" prompt_input "From Email" FROM_EMAIL prompt_input "From Name (optional)" FROM_NAME prompt_input "To Email" TO_EMAIL prompt_input "Subject" SUBJECT prompt_input "HTML File Path" HTML_FILE echo "" } # ═══════════════════════════════════════════════════════════════════════════════ # MAIN # ═══════════════════════════════════════════════════════════════════════════════ main() { # Parse command line arguments parse_args "$@" # If no arguments provided, run interactive mode if [[ -z "$SMTP_SERVER" && -z "$HTML_FILE" ]]; then interactive_mode else print_header fi # Check dependencies check_dependencies # Validate all inputs validate_inputs # Send the email send_email } # Run main function main "$@"