diff --git a/send_mail.sh b/send_mail.sh new file mode 100755 index 0000000..a6d8194 --- /dev/null +++ b/send_mail.sh @@ -0,0 +1,684 @@ +#!/usr/bin/env bash +# +# send_mail.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: send_mail.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 "$@"