383 lines
18 KiB
Bash
383 lines
18 KiB
Bash
#!/bin/bash
|
|
# =============================================================
|
|
# PLEX32 — Daily Update Check & Notification
|
|
# KingDezigns Infrastructure
|
|
# Schedule: Daily at 7:00 AM
|
|
# Location: /usr/scripts/plex32/plex32_update_check.sh
|
|
# =============================================================
|
|
|
|
HOSTNAME="plex32"
|
|
SMTP_SERVER="smtppro.zoho.com"
|
|
SMTP_PORT="465"
|
|
SMTP_USER="rufus.king@kingdezigns.com"
|
|
SMTP_PASS="Valerie8545"
|
|
EMAIL_TO="rufus.king@kingdezigns.com"
|
|
EMAIL_FROM="rufus.king@kingdezigns.com"
|
|
TIMESTAMP=$(date "+%A, %B %d, %Y at %I:%M %p")
|
|
DATE_SHORT=$(date "+%Y-%m-%d")
|
|
LOG_DIR="/var/log/plex32-updates"
|
|
LOG_FILE="$LOG_DIR/update-check-$DATE_SHORT.log"
|
|
|
|
mkdir -p "$LOG_DIR"
|
|
exec > >(tee -a "$LOG_FILE") 2>&1
|
|
echo "[$TIMESTAMP] Starting update check..."
|
|
|
|
# =============================================================
|
|
# STEP 1 — CHECK OS/APT UPDATES
|
|
# =============================================================
|
|
echo "[INFO] Checking apt updates..."
|
|
apt_output=$(apt-get update -qq 2>&1)
|
|
apt_check_status=$?
|
|
|
|
if [ $apt_check_status -ne 0 ]; then
|
|
APT_FAILED=true
|
|
APT_UPDATES=0
|
|
APT_DETAILS="apt-get update failed:\n$apt_output"
|
|
else
|
|
APT_FAILED=false
|
|
apt_list=$(apt list --upgradable 2>/dev/null | grep -v "Listing..." | grep -v "^$")
|
|
APT_UPDATES=$(echo "$apt_list" | grep -c "/" || true)
|
|
APT_DETAILS="$apt_list"
|
|
fi
|
|
|
|
echo "[INFO] APT updates available: $APT_UPDATES"
|
|
|
|
# =============================================================
|
|
# STEP 2 — CHECK PLEX DOCKER IMAGE UPDATE
|
|
# =============================================================
|
|
echo "[INFO] Checking Plex Docker image..."
|
|
PLEX_FAILED=false
|
|
PLEX_UPDATE_AVAILABLE=false
|
|
PLEX_ERROR_HTML=""
|
|
|
|
PLEX_PULL_OUTPUT=$(docker pull lscr.io/linuxserver/plex:latest 2>&1)
|
|
PLEX_PULL_STATUS=$?
|
|
|
|
PLEX_IMAGE_VERSION=$(docker inspect lscr.io/linuxserver/plex:latest --format '{{index .Config.Labels "build_version"}}' 2>/dev/null || echo "unknown")
|
|
CURRENT_IMAGE_DATE=$(docker inspect --format='{{.Created}}' lscr.io/linuxserver/plex:latest 2>/dev/null | cut -d'T' -f1)
|
|
|
|
if [ $PLEX_PULL_STATUS -ne 0 ]; then
|
|
PLEX_FAILED=true
|
|
PLEX_STATUS_TEXT="Check failed — see error detail below"
|
|
echo "[ERROR] docker pull exited $PLEX_PULL_STATUS. Full output:"
|
|
echo "$PLEX_PULL_OUTPUT"
|
|
PLEX_ERROR_HTML="<div style='margin-top:8px;padding:10px 14px;background:#fff1f2;border-left:4px solid #b91c1c;border-radius:4px;'><div style='font-size:11px;font-weight:700;color:#b91c1c;text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px;'>⚠ Docker Pull Error (exit $PLEX_PULL_STATUS)</div><pre style='margin:0;font-family:monospace;font-size:12px;color:#7f1d1d;white-space:pre-wrap;'>$(echo "$PLEX_PULL_OUTPUT" | sed 's/&/\&/g; s/</\</g; s/>/\>/g')</pre></div>"
|
|
elif echo "$PLEX_PULL_OUTPUT" | grep -q "Pull complete"; then
|
|
PLEX_UPDATE_AVAILABLE=true
|
|
PLEX_STATUS_TEXT="Update available — new image pulled"
|
|
else
|
|
PLEX_STATUS_TEXT="Up to date"
|
|
fi
|
|
|
|
echo "[INFO] Plex status: $PLEX_STATUS_TEXT"
|
|
|
|
# =============================================================
|
|
# STEP 3 — CHECK FAIL2BAN VERSION
|
|
# =============================================================
|
|
echo "[INFO] Checking Fail2Ban..."
|
|
F2B_UPDATE_AVAILABLE=false
|
|
|
|
F2B_CANDIDATE=$(apt-cache policy fail2ban 2>/dev/null | grep Candidate | awk '{print $2}' | cut -d'-' -f1)
|
|
F2B_INSTALLED=$(apt-cache policy fail2ban 2>/dev/null | grep Installed | awk '{print $2}' | cut -d'-' -f1)
|
|
|
|
if [ "$F2B_CANDIDATE" != "$F2B_INSTALLED" ] && [ -n "$F2B_CANDIDATE" ]; then
|
|
F2B_UPDATE_AVAILABLE=true
|
|
F2B_STATUS_TEXT="Update available: $F2B_INSTALLED → $F2B_CANDIDATE"
|
|
else
|
|
F2B_STATUS_TEXT="Up to date ($F2B_INSTALLED)"
|
|
fi
|
|
|
|
echo "[INFO] Fail2Ban status: $F2B_STATUS_TEXT"
|
|
|
|
# =============================================================
|
|
# STEP 4 — CHECK DOCKER ENGINE UPDATE
|
|
# =============================================================
|
|
echo "[INFO] Checking Docker..."
|
|
DOCKER_UPDATE_AVAILABLE=false
|
|
|
|
DOCKER_CANDIDATE=$(apt-cache policy docker-ce 2>/dev/null | grep Candidate | awk '{print $2}')
|
|
DOCKER_INSTALLED=$(apt-cache policy docker-ce 2>/dev/null | grep Installed | awk '{print $2}')
|
|
|
|
if [ "$DOCKER_CANDIDATE" != "$DOCKER_INSTALLED" ] && [ -n "$DOCKER_CANDIDATE" ]; then
|
|
DOCKER_UPDATE_AVAILABLE=true
|
|
DOCKER_STATUS_TEXT="Update available: $DOCKER_INSTALLED → $DOCKER_CANDIDATE"
|
|
else
|
|
DOCKER_STATUS_TEXT="Up to date ($DOCKER_INSTALLED)"
|
|
fi
|
|
|
|
echo "[INFO] Docker status: $DOCKER_STATUS_TEXT"
|
|
|
|
# =============================================================
|
|
# STEP 5 — DETERMINE OVERALL STATUS
|
|
# =============================================================
|
|
ANY_FAILED=false
|
|
ANY_UPDATES=false
|
|
|
|
[ "$APT_FAILED" = true ] && ANY_FAILED=true
|
|
[ "$PLEX_FAILED" = true ] && ANY_FAILED=true
|
|
[ "$APT_UPDATES" -gt 0 ] && ANY_UPDATES=true
|
|
[ "$PLEX_UPDATE_AVAILABLE" = true ] && ANY_UPDATES=true
|
|
[ "$F2B_UPDATE_AVAILABLE" = true ] && ANY_UPDATES=true
|
|
[ "$DOCKER_UPDATE_AVAILABLE" = true ] && ANY_UPDATES=true
|
|
|
|
if [ "$ANY_FAILED" = true ]; then
|
|
STATUS_COLOR="#b45309"
|
|
STATUS_BADGE="⚠ CHECK FAILED"
|
|
STATUS_BANNER_MSG="⚠ One or more update checks failed — manual verification required"
|
|
SUBJECT="⚠ PLEX32 — Update Check Failed — $DATE_SHORT"
|
|
elif [ "$ANY_UPDATES" = true ]; then
|
|
STATUS_COLOR="#1d6fa8"
|
|
STATUS_BADGE="🔄 UPDATES AVAILABLE"
|
|
STATUS_BANNER_MSG="🔄 Updates are available for one or more components"
|
|
SUBJECT="🔄 PLEX32 — Updates Available — $DATE_SHORT"
|
|
else
|
|
STATUS_COLOR="#1a7f4b"
|
|
STATUS_BADGE="✅ ALL CURRENT"
|
|
STATUS_BANNER_MSG="✅ All components are up to date — no action required"
|
|
SUBJECT="✅ PLEX32 — All Systems Current — $DATE_SHORT"
|
|
fi
|
|
|
|
# =============================================================
|
|
# STEP 6 — BUILD COMMAND BLOCKS (click to select)
|
|
# =============================================================
|
|
make_cmd_block() {
|
|
local step_num="$1"
|
|
local step_label="$2"
|
|
local cmd="$3"
|
|
local cmd_html=$(echo "$cmd" | sed 's/&/\&/g; s/</\</g; s/>/\>/g')
|
|
echo "<div style='margin-bottom:16px;'>
|
|
<div style='font-size:12px;font-weight:600;color:#374151;margin-bottom:4px;'>$step_num. $step_label</div>
|
|
<div style='font-size:11px;color:#6b7280;margin-bottom:4px;'>👆 Click inside to select all — then Ctrl+C</div>
|
|
<pre style='margin:0;background:#1e293b;color:#e2e8f0;padding:14px;border-radius:6px;font-size:12px;line-height:1.6;white-space:pre-wrap;word-break:break-all;cursor:pointer;user-select:all;-webkit-user-select:all;-moz-user-select:all;'>$cmd_html</pre>
|
|
</div>"
|
|
}
|
|
|
|
UPDATE_COMMANDS_HTML=""
|
|
if [ "$ANY_UPDATES" = true ]; then
|
|
UPDATE_COMMANDS_HTML="<div style='margin-top:24px;'><div style='font-size:13px;font-weight:700;color:#374151;margin-bottom:12px;text-transform:uppercase;letter-spacing:.5px;'>🛠 Commands to Apply Updates (run in order)</div>"
|
|
|
|
STEP=1
|
|
if [ "$APT_UPDATES" -gt 0 ]; then
|
|
CMD="sudo apt update && sudo apt upgrade -y"
|
|
UPDATE_COMMANDS_HTML+=$(make_cmd_block "$STEP" "OS & Package Updates" "$CMD")
|
|
STEP=$((STEP+1))
|
|
fi
|
|
|
|
if [ "$PLEX_UPDATE_AVAILABLE" = true ]; then
|
|
CMD="cd ~/docker/plex
|
|
docker compose up -d"
|
|
UPDATE_COMMANDS_HTML+=$(make_cmd_block "$STEP" "Plex Media Server (Docker)" "$CMD")
|
|
STEP=$((STEP+1))
|
|
fi
|
|
|
|
if [ "$DOCKER_UPDATE_AVAILABLE" = true ]; then
|
|
CMD="sudo apt update && sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
|
docker --version"
|
|
UPDATE_COMMANDS_HTML+=$(make_cmd_block "$STEP" "Docker Engine" "$CMD")
|
|
STEP=$((STEP+1))
|
|
fi
|
|
|
|
if [ "$F2B_UPDATE_AVAILABLE" = true ]; then
|
|
CMD="sudo apt update && sudo apt install -y fail2ban
|
|
sudo systemctl restart fail2ban
|
|
sudo systemctl status fail2ban"
|
|
UPDATE_COMMANDS_HTML+=$(make_cmd_block "$STEP" "Fail2Ban" "$CMD")
|
|
fi
|
|
|
|
UPDATE_COMMANDS_HTML+="</div>"
|
|
fi
|
|
|
|
# =============================================================
|
|
# STEP 7 — BUILD APT PACKAGES TABLE
|
|
# =============================================================
|
|
APT_PACKAGES_HTML=""
|
|
if [ "$APT_UPDATES" -gt 0 ] && [ "$APT_FAILED" = false ]; then
|
|
APT_PACKAGES_HTML='<div style="margin-top:24px;">
|
|
<div style="font-size:13px;font-weight:700;color:#374151;margin-bottom:12px;text-transform:uppercase;letter-spacing:.5px;">📦 Packages Awaiting Update</div>
|
|
<table width="100%" cellspacing="0" cellpadding="0" style="background:white;border-radius:8px;border:1px solid #e5e7eb;overflow:hidden;">
|
|
<tr style="background:#f8fafc;">
|
|
<th style="text-align:left;padding:8px 14px;font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid #e5e7eb;">Package</th>
|
|
<th style="text-align:left;padding:8px 14px;font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid #e5e7eb;">Available Version</th>
|
|
</tr>'
|
|
|
|
while IFS= read -r line; do
|
|
[ -z "$line" ] && continue
|
|
pkg=$(echo "$line" | awk '{print $1}' | cut -d'/' -f1)
|
|
ver=$(echo "$line" | awk '{print $2}')
|
|
APT_PACKAGES_HTML+="<tr><td style='padding:8px 14px;font-size:13px;color:#111827;border-bottom:1px solid #f3f4f6;font-family:monospace;'>$pkg</td><td style='padding:8px 14px;font-size:13px;color:#1d6fa8;font-weight:600;border-bottom:1px solid #f3f4f6;'>$ver</td></tr>"
|
|
done <<< "$APT_DETAILS"
|
|
|
|
APT_PACKAGES_HTML+='</table></div>'
|
|
fi
|
|
|
|
# =============================================================
|
|
# STEP 8 — BADGES
|
|
# =============================================================
|
|
make_badge() {
|
|
local update="$1"
|
|
local failed="$2"
|
|
if [ "$failed" = true ]; then
|
|
echo "<span style='background:#b45309;color:white;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700;margin-left:8px;'>CHECK FAILED</span>"
|
|
elif [ "$update" = true ]; then
|
|
echo "<span style='background:#1d6fa8;color:white;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700;margin-left:8px;'>UPDATE AVAILABLE</span>"
|
|
else
|
|
echo "<span style='background:#1a7f4b;color:white;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700;margin-left:8px;'>CURRENT</span>"
|
|
fi
|
|
}
|
|
|
|
PLEX_BADGE=$(make_badge "$PLEX_UPDATE_AVAILABLE" "$PLEX_FAILED")
|
|
F2B_BADGE=$(make_badge "$F2B_UPDATE_AVAILABLE" "false")
|
|
DOCKER_BADGE=$(make_badge "$DOCKER_UPDATE_AVAILABLE" "false")
|
|
|
|
# =============================================================
|
|
# STEP 9 — BUILD & SEND EMAIL
|
|
# =============================================================
|
|
python3 << PYEOF
|
|
import smtplib, ssl
|
|
from email.mime.multipart import MIMEMultipart
|
|
from email.mime.text import MIMEText
|
|
|
|
apt_updates = $APT_UPDATES
|
|
plex_update = "$PLEX_UPDATE_AVAILABLE" == "true"
|
|
f2b_update = "$F2B_UPDATE_AVAILABLE" == "true"
|
|
docker_update = "$DOCKER_UPDATE_AVAILABLE" == "true"
|
|
any_failed = "$ANY_FAILED" == "true"
|
|
status_color = "$STATUS_COLOR"
|
|
status_badge = "$STATUS_BADGE"
|
|
status_banner = "$STATUS_BANNER_MSG"
|
|
subject = "$SUBJECT"
|
|
plex_status = "$PLEX_STATUS_TEXT"
|
|
f2b_status = "$F2B_STATUS_TEXT"
|
|
docker_status = "$DOCKER_STATUS_TEXT"
|
|
plex_badge = """$PLEX_BADGE"""
|
|
f2b_badge = """$F2B_BADGE"""
|
|
docker_badge = """$DOCKER_BADGE"""
|
|
update_commands = """$UPDATE_COMMANDS_HTML"""
|
|
apt_packages_html = """$APT_PACKAGES_HTML"""
|
|
plex_error_html = """$PLEX_ERROR_HTML"""
|
|
timestamp = "$TIMESTAMP"
|
|
plex_image_ver = "$PLEX_IMAGE_VERSION"
|
|
current_image_date= "$CURRENT_IMAGE_DATE"
|
|
from_email = "$EMAIL_FROM"
|
|
|
|
total_updates = apt_updates + (1 if plex_update else 0) + (1 if f2b_update else 0) + (1 if docker_update else 0)
|
|
apt_card_color = "#1d6fa8" if apt_updates > 0 else "#111827"
|
|
plex_card_color = "#1d6fa8" if plex_update else "#111827"
|
|
total_card_color = "#1d6fa8" if total_updates > 0 else "#1a7f4b"
|
|
other_update = f2b_update or docker_update
|
|
other_card_color = "#1d6fa8" if other_update else "#111827"
|
|
other_count = sum([1 if f2b_update else 0, 1 if docker_update else 0])
|
|
|
|
html = f"""<!DOCTYPE html>
|
|
<html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"></head>
|
|
<body style="margin:0;padding:0;background:#f1f5f9;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;">
|
|
<table width="100%" cellspacing="0" cellpadding="0" style="background:#f1f5f9;padding:32px 16px;">
|
|
<tr><td align="center">
|
|
<table width="100%" cellspacing="0" cellpadding="0" style="max-width:720px;background:white;border-radius:10px;box-shadow:0 1px 3px rgba(0,0,0,.1);border:1px solid #e5e7eb;overflow:hidden;">
|
|
|
|
<tr><td style="background:#0f172a;border-radius:10px 10px 0 0;padding:28px 32px;">
|
|
<table width="100%" cellspacing="0" cellpadding="0"><tr>
|
|
<td>
|
|
<div style="font-size:11px;color:#94a3b8;letter-spacing:1px;text-transform:uppercase;">Update Monitor</div>
|
|
<div style="font-size:26px;font-weight:800;color:white;margin-top:4px;">🖥 PLEX32</div>
|
|
<div style="font-size:13px;color:#94a3b8;margin-top:4px;">{timestamp} | VLAN 50 — 192.168.150.45</div>
|
|
</td>
|
|
<td align="right">
|
|
<div style="background:{status_color};color:white;padding:10px 20px;border-radius:8px;font-size:14px;font-weight:700;">{status_badge}</div>
|
|
</td>
|
|
</tr></table>
|
|
</td></tr>
|
|
|
|
<tr><td style="background:{status_color};padding:12px 32px;">
|
|
<span style="color:white;font-size:14px;font-weight:600;">{status_banner}</span>
|
|
</td></tr>
|
|
|
|
<tr><td style="padding:28px 32px;">
|
|
|
|
<table width="100%" cellspacing="0" cellpadding="0">
|
|
<tr>
|
|
<td style="width:24%;padding:8px 6px 8px 0;">
|
|
<div style="background:#f9fafb;border-radius:6px;padding:12px;text-align:center;border:1px solid #e5e7eb;">
|
|
<div style="font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;">Total Updates</div>
|
|
<div style="font-size:28px;font-weight:700;color:{total_card_color};margin-top:4px;">{total_updates}</div>
|
|
</div>
|
|
</td>
|
|
<td style="width:24%;padding:8px 6px;">
|
|
<div style="background:#f9fafb;border-radius:6px;padding:12px;text-align:center;border:1px solid #e5e7eb;">
|
|
<div style="font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;">OS Packages</div>
|
|
<div style="font-size:28px;font-weight:700;color:{apt_card_color};margin-top:4px;">{apt_updates}</div>
|
|
</div>
|
|
</td>
|
|
<td style="width:24%;padding:8px 6px;">
|
|
<div style="background:#f9fafb;border-radius:6px;padding:12px;text-align:center;border:1px solid #e5e7eb;">
|
|
<div style="font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;">Plex</div>
|
|
<div style="font-size:28px;font-weight:700;color:{plex_card_color};margin-top:4px;">{'1' if plex_update else '0'}</div>
|
|
</div>
|
|
</td>
|
|
<td style="width:24%;padding:8px 0 8px 6px;">
|
|
<div style="background:#f9fafb;border-radius:6px;padding:12px;text-align:center;border:1px solid #e5e7eb;">
|
|
<div style="font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;">Other</div>
|
|
<div style="font-size:28px;font-weight:700;color:{other_card_color};margin-top:4px;">{other_count}</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
|
|
<div style="margin-top:24px;">
|
|
<div style="font-size:13px;font-weight:700;color:#374151;margin-bottom:12px;text-transform:uppercase;letter-spacing:.5px;">📋 Component Status</div>
|
|
<table width="100%" cellspacing="0" cellpadding="0" style="background:white;border-radius:8px;border:1px solid #e5e7eb;overflow:hidden;">
|
|
<tr style="background:#f8fafc;">
|
|
<th style="text-align:left;padding:8px 14px;font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid #e5e7eb;">Component</th>
|
|
<th style="text-align:left;padding:8px 14px;font-size:11px;color:#6b7280;text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid #e5e7eb;">Status</th>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding:10px 14px;font-size:13px;color:#374151;font-weight:600;border-bottom:1px solid #f3f4f6;">🐧 Ubuntu Server (apt)</td>
|
|
<td style="padding:10px 14px;font-size:13px;color:#374151;border-bottom:1px solid #f3f4f6;">{apt_updates} package(s) available</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding:10px 14px;font-size:13px;color:#374151;font-weight:600;border-bottom:1px solid #f3f4f6;">🎬 Plex Media Server {plex_badge}</td>
|
|
<td style="padding:10px 14px;font-size:13px;color:#374151;border-bottom:1px solid #f3f4f6;">{plex_status}<br><span style="font-size:11px;color:#9ca3af;">Image: {plex_image_ver} | Built: {current_image_date}</span>{plex_error_html}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding:10px 14px;font-size:13px;color:#374151;font-weight:600;border-bottom:1px solid #f3f4f6;">🛡 Fail2Ban {f2b_badge}</td>
|
|
<td style="padding:10px 14px;font-size:13px;color:#374151;border-bottom:1px solid #f3f4f6;">{f2b_status}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding:10px 14px;font-size:13px;color:#374151;font-weight:600;">🐳 Docker Engine {docker_badge}</td>
|
|
<td style="padding:10px 14px;font-size:13px;color:#374151;">{docker_status}</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
|
|
{update_commands}
|
|
{apt_packages_html}
|
|
|
|
</td></tr>
|
|
|
|
<tr><td style="background:#0f172a;border-radius:0 0 10px 10px;padding:16px 32px;text-align:center;">
|
|
<span style="font-size:11px;color:#64748b;">Update Monitor | PLEX32 | KingDezigns Infrastructure</span>
|
|
</td></tr>
|
|
|
|
</table>
|
|
</td></tr>
|
|
</table>
|
|
</body></html>"""
|
|
|
|
msg = MIMEMultipart("alternative")
|
|
msg["Subject"] = subject
|
|
msg["From"] = f"KingDezigns PLEX32 <{from_email}>"
|
|
msg["To"] = "$EMAIL_TO"
|
|
msg.attach(MIMEText(html, "html"))
|
|
|
|
context = ssl.create_default_context()
|
|
try:
|
|
with smtplib.SMTP_SSL("$SMTP_SERVER", $SMTP_PORT, context=context, timeout=15) as server:
|
|
server.login("$SMTP_USER", "$SMTP_PASS")
|
|
server.sendmail("$EMAIL_FROM", "$EMAIL_TO", msg.as_string())
|
|
print("[$TIMESTAMP] Email sent successfully.")
|
|
except Exception as e:
|
|
print(f"[$TIMESTAMP] Email send failed: {e}")
|
|
exit(1)
|
|
PYEOF
|
|
|
|
echo "[$TIMESTAMP] Update check complete."
|