Files
bin/html_mail_send.sh
2026-01-13 16:31:24 +01:00

685 lines
23 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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<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() {
echo -e "${BMAG}${SYM_MAIL}${RST} ${BWHT}html_mail_send.sh${RST} ${ACC1}v${VERSION}${RST}"
}
# ═══════════════════════════════════════════════════════════════════════════════
# 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"
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 "$@"