Files
bin/html_mail_send.sh

685 lines
23 KiB
Bash
Raw Normal View History

2026-01-13 15:19:42 +01:00
#!/usr/bin/env bash
#
2026-01-13 16:31:24 +01:00
# html_mail_send.sh - A sophisticated HTML email sender
2026-01-13 15:19:42 +01:00
# 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<filled; i++)); do
if ((i < filled / 3)); then
bar+="${G1}${SYM_DOT}${RST}"
elif ((i < 2 * filled / 3)); then
bar+="${G3}${SYM_DOT}${RST}"
else
bar+="${G5}${SYM_DOT}${RST}"
fi
done
for ((i=0; i<empty; i++)); do
bar+="${DIM}${RST}"
done
printf "\r [%s] ${BWHT}%3d%%${RST}" "$bar" "$percent"
}
# ═══════════════════════════════════════════════════════════════════════════════
# USAGE & HELP
# ═══════════════════════════════════════════════════════════════════════════════
show_usage() {
print_header
echo -e "${BWHT}USAGE${RST}"
echo -e " ${GRN}$SCRIPT_NAME${RST} ${CYN}[OPTIONS]${RST} ${YLW}-s SERVER -u USER -p PASS -f FROM -t TO -j SUBJECT -h FILE${RST}"
echo ""
echo -e "${BWHT}REQUIRED OPTIONS${RST}"
echo -e " ${BCYN}-s, --server${RST} ${DIM}SERVER${RST} SMTP server hostname"
echo -e " ${BCYN}-u, --user${RST} ${DIM}USER${RST} SMTP username"
echo -e " ${BCYN}-p, --pass${RST} ${DIM}PASS${RST} SMTP password (or use SMTP_PASSWORD env var)"
echo -e " ${BCYN}-f, --from${RST} ${DIM}EMAIL${RST} Sender email address"
echo -e " ${BCYN}-t, --to${RST} ${DIM}EMAIL${RST} Recipient email address"
echo -e " ${BCYN}-j, --subject${RST} ${DIM}TEXT${RST} Email subject line"
echo -e " ${BCYN}-h, --html${RST} ${DIM}FILE${RST} Path to HTML file"
echo ""
echo -e "${BWHT}OPTIONAL${RST}"
echo -e " ${BCYN}-P, --port${RST} ${DIM}PORT${RST} SMTP port (default: 587)"
echo -e " ${BCYN}-n, --name${RST} ${DIM}NAME${RST} Sender display name"
echo -e " ${BCYN} --no-tls${RST} Disable TLS/STARTTLS"
echo -e " ${BCYN} --ssl${RST} Use SSL (port 465)"
echo -e " ${BCYN}-v, --verbose${RST} Verbose output"
echo -e " ${BCYN} --dry-run${RST} Show what would be sent without sending"
echo -e " ${BCYN} --help${RST} Show this help message"
echo -e " ${BCYN} --version${RST} Show version"
echo ""
echo -e "${BWHT}EXAMPLES${RST}"
echo -e " ${DIM}# Basic usage${RST}"
echo -e " ${GRN}$SCRIPT_NAME${RST} -s smtp.gmail.com -u user@gmail.com -p 'app-password' \\"
echo -e " -f user@gmail.com -t recipient@example.com \\"
echo -e " -j 'Hello World' -h email.html"
echo ""
echo -e " ${DIM}# Using environment variable for password${RST}"
echo -e " ${YLW}export SMTP_PASSWORD='your-password'${RST}"
echo -e " ${GRN}$SCRIPT_NAME${RST} -s smtp.example.com -u user -f sender@example.com \\"
echo -e " -t recipient@example.com -j 'Subject' -h template.html"
echo ""
echo -e " ${DIM}# With sender name and custom port${RST}"
echo -e " ${GRN}$SCRIPT_NAME${RST} -s mail.example.com -P 465 --ssl \\"
echo -e " -u user -p pass -f sender@example.com -n 'John Doe' \\"
echo -e " -t recipient@example.com -j 'Important' -h message.html"
echo ""
echo -e "${BWHT}ENVIRONMENT VARIABLES${RST}"
echo -e " ${BCYN}SMTP_PASSWORD${RST} SMTP password (alternative to -p flag)"
echo ""
echo -e "${DIM}─────────────────────────────────────────────────────${RST}"
echo -e "${DIM}Requires: curl with SMTP support${RST}"
echo ""
}
show_version() {
2026-01-13 16:31:24 +01:00
echo -e "${BMAG}${SYM_MAIL}${RST} ${BWHT}html_mail_send.sh${RST} ${ACC1}v${VERSION}${RST}"
2026-01-13 15:19:42 +01:00
}
# ═══════════════════════════════════════════════════════════════════════════════
# VALIDATION
# ═══════════════════════════════════════════════════════════════════════════════
validate_email() {
local email="$1"
if [[ "$email" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
return 0
fi
return 1
}
validate_file() {
local file="$1"
if [[ -f "$file" && -r "$file" ]]; then
return 0
fi
return 1
}
check_dependencies() {
if ! command -v curl &>/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"
2026-01-13 16:31:24 +01:00
printf 'X-Mailer: html_mail_send.sh/%s\r\n' "$VERSION"
2026-01-13 15:19:42 +01:00
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 "$@"