notify feature

This commit is contained in:
2026-08-10 22:43:06 +02:00
parent 43f2e96720
commit 70efc66696
7 changed files with 123 additions and 3 deletions
+8
View File
@@ -21,6 +21,14 @@ POLL_INTERVAL_MINUTES=15
# Healthchecks (self-hosted)
HEALTHCHECK_PING_URL=https://healthchecks.example.org/ping/change-me
# E-Mail-Notification bei IP-Änderung (optional)
SMTP_HOST=smtp.example.org
SMTP_PORT=587
SMTP_USER=change-me
SMTP_PASSWORD=change-me
NOTIFY_EMAIL_TO=you@example.org
NOTIFY_EMAIL_FROM=dyndns@example.org
# Sonstiges
TZ=Europe/Berlin
LOG_LEVEL=INFO
+1
View File
@@ -13,6 +13,7 @@
## Phase 2 - Kernmodule
- [x] config.py, state.py, fritzbox.py, cloudflare.py, freedns.py, healthcheck.py, main.py
- [x] notify.py (E-Mail-Notification bei IP-Aenderung)
## Phase 3 - Lokaler Test
- [ ] .env lokal befuellen (nicht committen)
+14
View File
@@ -39,6 +39,20 @@ Optional: `FREEDNS_UPDATE_URL`, `POLL_INTERVAL_MINUTES`,
`HEALTHCHECK_PING_URL`, `TZ`, `LOG_LEVEL`. Die IP kommt ausschließlich aus der
FritzBox, nie aus Webhook-Parametern.
### E-Mail-Notification (optional)
Bei IP-Änderung wird eine HTML-E-Mail mit Emojis versendet, wenn SMTP-Konfiguration
vorliegt:
| Variable | Zweck |
| --- | --- |
| `SMTP_HOST` | SMTP-Server (z.B. `smtp.gmail.com`) |
| `SMTP_PORT` | Port (Default: `587`) |
| `SMTP_USER` | SMTP-Login |
| `SMTP_PASSWORD` | SMTP-Passwort |
| `NOTIFY_EMAIL_TO` | Empfänger-Adresse |
| `NOTIFY_EMAIL_FROM` | Absender-Adresse |
## Betrieb
- **State (bewusst ohne Volume, ephemeral):** zuletzt bekannte IP liegt als
+10 -2
View File
@@ -26,7 +26,8 @@ die IP bei jedem Trigger selbst ueber die FritzBox TR-064-Schnittstelle.
2. IP_letzte = state.load()
3. Wenn gleich: Poll -> nur Heartbeat-Ping, kein Update. Webhook/Start -> nur Log.
4. Wenn unterschiedlich: Cloudflare-Records updaten, FreeDNS aufrufen, state speichern,
Healthcheck-Erfolgs-Ping. Bei Teilfehler: state NICHT speichern, Fail-Ping.
Healthcheck-Erfolgs-Ping, E-Mail-Notification (optional). Bei Teilfehler: state NICHT
speichern, Fail-Ping.
## 3. Cloudflare
@@ -46,7 +47,14 @@ die IP bei jedem Trigger selbst ueber die FritzBox TR-064-Schnittstelle.
- FritzBox-TR-064-User mit eingeschraenkten Rechten
- Alle Records bleiben DNS-only (kein Proxy-Mode in v1)
## 6. Offene Punkte
## 6. E-Mail-Notification (optional)
- HTML-E-Mail mit Emojis bei erfolgreicher IP-Aenderung
- SMTP-Auth mit TLS (Port 587)
- Nur wenn SMTP_HOST, NOTIFY_EMAIL_TO und NOTIFY_EMAIL_FROM gesetzt
- Plain-Text-Fallback fuer Clients ohne HTML-Unterstuetzung
## 7. Offene Punkte
- IPv6/AAAA
- Proxy-Mode fuer reine HTTP(S)-Subdomains
+11
View File
@@ -39,8 +39,19 @@ class Config:
self.healthcheck_ping_url = os.environ.get("HEALTHCHECK_PING_URL", "")
self.smtp_host = os.environ.get("SMTP_HOST", "")
self.smtp_port = int(os.environ.get("SMTP_PORT", "587"))
self.smtp_user = os.environ.get("SMTP_USER", "")
self.smtp_password = os.environ.get("SMTP_PASSWORD", "")
self.notify_email_to = os.environ.get("NOTIFY_EMAIL_TO", "")
self.notify_email_from = os.environ.get("NOTIFY_EMAIL_FROM", "")
self.state_path = os.environ.get("STATE_PATH", "/app/data/last-known-ip.json")
@property
def email_notification_enabled(self) -> bool:
return bool(self.smtp_host and self.notify_email_to and self.notify_email_from)
def validate(self):
missing = [v for v in REQUIRED_VARS if not os.environ.get(v)]
if missing:
+2 -1
View File
@@ -7,7 +7,7 @@ import threading
from apscheduler.schedulers.background import BackgroundScheduler
from flask import Flask
from app import cloudflare, freedns, healthcheck, state
from app import cloudflare, freedns, healthcheck, notify, state
from app.config import config
from app.fritzbox import FritzBoxError, get_external_ip
@@ -67,6 +67,7 @@ def run_cycle(trigger: str = "unknown"):
status="success",
message=f"IP updated to {current_ip}",
)
notify.send_ip_changed_email(last_ip, current_ip)
else:
failed = {k: v for k, v in cf_results.items() if v != "ok"}
log.error(
+77
View File
@@ -0,0 +1,77 @@
# Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
# Licensed under the MIT License. See LICENSE file in project root for details.
import logging
import smtplib
from email.message import EmailMessage
from app.config import config
log = logging.getLogger("notify")
def send_ip_changed_email(old_ip: str | None, new_ip: str) -> bool:
if not config.email_notification_enabled:
log.debug("E-Mail-Notification nicht konfiguriert, überspringe.")
return False
if old_ip is None:
old_ip = "unbekannt"
subject = f"🌐 IP-Update: {old_ip}{new_ip}"
html = f"""\
<html>
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f172a; color: #e2e8f0; padding: 32px; margin: 0;">
<div style="max-width: 480px; margin: 0 auto;">
<div style="text-align: center; font-size: 48px; margin-bottom: 16px;">🔄</div>
<h1 style="font-size: 20px; color: #38bdf8; text-align: center; margin-bottom: 24px;">
IP-Adresse geändert
</h1>
<div style="background: #1e293b; border-radius: 12px; padding: 24px; margin-bottom: 24px;">
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0; color: #94a3b8; font-size: 14px;">📍 Alte IP</td>
<td style="padding: 8px 0; text-align: right; font-family: monospace; font-size: 16px; color: #f87171;">
{old_ip}
</td>
</tr>
<tr>
<td colspan="2" style="padding: 4px 0; text-align: center; font-size: 20px;">⬇️</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #94a3b8; font-size: 14px;">🆕 Neue IP</td>
<td style="padding: 8px 0; text-align: right; font-family: monospace; font-size: 16px; color: #4ade80;">
{new_ip}
</td>
</tr>
</table>
</div>
<p style="font-size: 13px; color: #64748b; text-align: center; margin: 0;">
📡 DynDNS-Updater · Cloudflare + FreeDNS aktualisiert
</p>
</div>
</body>
</html>"""
text = f"IP-Adresse geändert: {old_ip}{new_ip}"
msg = EmailMessage()
msg["Subject"] = subject
msg["From"] = config.notify_email_from
msg["To"] = config.notify_email_to
msg.set_content(text)
msg.add_alternative(html, subtype="html")
try:
with smtplib.SMTP(config.smtp_host, config.smtp_port) as server:
server.ehlo()
server.starttls()
server.ehlo()
server.login(config.smtp_user, config.smtp_password)
server.send_message(msg)
log.info("E-Mail gesendet: %s -> %s", subject, config.notify_email_to)
return True
except Exception as exc:
log.error("E-Mail-Versand fehlgeschlagen: %s", exc)
return False