mailer weekly notifier

This commit is contained in:
2026-08-11 22:04:11 +02:00
parent 3635820eb8
commit 0f16aa45dc
5 changed files with 253 additions and 0 deletions
+190
View File
@@ -61,6 +61,13 @@ NOTIFY_EMAIL = os.environ.get("NOTIFY_EMAIL", "")
NOTIFY_TIME = int(os.environ.get("NOTIFY_TIME", "6"))
NOTIFY_TIMEZONE = os.environ.get("NOTIFY_TIMEZONE", "Europe/Berlin")
WEEKLY_NOTIFY_ENABLED = os.environ.get("WEEKLY_NOTIFY_ENABLED", "false").lower() == "true"
WEEKLY_NOTIFY_DAY = int(os.environ.get("WEEKLY_NOTIFY_DAY", "5"))
WEEKLY_NOTIFY_TIME = int(os.environ.get("WEEKLY_NOTIFY_TIME", "16"))
WEEKLY_NOTIFY_TIMEZONE = os.environ.get("WEEKLY_NOTIFY_TIMEZONE", "Europe/Berlin")
WEEKLY_NOTIFY_EMAIL = os.environ.get("WEEKLY_NOTIFY_EMAIL", "")
WEEKLY_SEARCHWORDS = os.environ.get("WEEKLY_SEARCHWORDS", "")
def bootstrap_database():
"""Legt DB_NAME und DB_USER an, falls sie noch nicht existieren.
@@ -131,6 +138,15 @@ def ensure_schema(conn):
UNIQUE KEY uk_date (notify_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
""")
cur.execute("""
CREATE TABLE IF NOT EXISTS weekly_notification_log (
id INT AUTO_INCREMENT PRIMARY KEY,
notify_date DATE NOT NULL,
sent_at DATETIME NOT NULL,
event_count INT NOT NULL,
UNIQUE KEY uk_date (notify_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
""")
conn.commit()
cur.close()
@@ -414,6 +430,179 @@ def check_and_send_notification():
conn.close()
def parse_weekly_searchwords():
if not WEEKLY_SEARCHWORDS:
return []
return [w.strip() for w in WEEKLY_SEARCHWORDS.split(",") if w.strip()]
def get_weekly_events(conn, calendar_label, searchwords):
tz = ZoneInfo(WEEKLY_NOTIFY_TIMEZONE)
now = datetime.now(tz)
today = now.replace(hour=0, minute=0, second=0, microsecond=0)
days_until_saturday = (5 - today.weekday()) % 7
if days_until_saturday == 0:
days_until_saturday = 7
week_start = today + timedelta(days=days_until_saturday)
week_end = week_start + timedelta(days=6, hours=23, minutes=59, seconds=59)
week_start_naive = week_start.replace(tzinfo=None)
week_end_naive = week_end.replace(tzinfo=None)
search_clauses = []
search_params = []
for word in searchwords:
like = f"%{word}%"
search_clauses.append("(summary LIKE %s OR description LIKE %s OR location LIKE %s)")
search_params.extend([like, like, like])
where_search = " OR ".join(search_clauses) if search_clauses else "1=1"
cur = conn.cursor(dictionary=True)
cur.execute(
f"""
SELECT summary, start_at, end_at, location
FROM calendar_events
WHERE calendar_label = %s
AND deleted = 0
AND all_day = 0
AND start_at >= %s
AND start_at <= %s
AND ({where_search})
ORDER BY start_at ASC
""",
(calendar_label, week_start_naive, week_end_naive, *search_params),
)
events = cur.fetchall()
cur.close()
return events, week_start, week_end
def should_send_weekly(conn):
if not WEEKLY_NOTIFY_ENABLED:
return False
if not parse_weekly_searchwords():
return False
tz = ZoneInfo(WEEKLY_NOTIFY_TIMEZONE)
now = datetime.now(tz)
if now.weekday() != WEEKLY_NOTIFY_DAY or now.hour != WEEKLY_NOTIFY_TIME:
return False
cur = conn.cursor()
cur.execute(
"SELECT COUNT(*) FROM weekly_notification_log WHERE notify_date = CURDATE()"
)
count = cur.fetchone()[0]
cur.close()
return count == 0
def log_weekly_notification(conn, event_count):
cur = conn.cursor()
cur.execute(
"INSERT INTO weekly_notification_log (notify_date, sent_at, event_count) VALUES (CURDATE(), NOW(), %s)",
(event_count,),
)
conn.commit()
cur.close()
def send_weekly_notification(events, searchwords, week_start, week_end):
email = WEEKLY_NOTIFY_EMAIL or NOTIFY_EMAIL
if not SMTP_HOST or not email:
log.warning("SMTP-Konfiguration unvollstaendig, ueberspringe Wochenbenachrichtigung")
return
tz = ZoneInfo(WEEKLY_NOTIFY_TIMEZONE)
count = len(events)
start_str = week_start.strftime("%a, %d.%m.")
end_str = week_end.strftime("%a, %d.%m.")
if count == 1:
event = events[0]
event_date = event["start_at"].replace(tzinfo=timezone.utc).astimezone(tz).strftime("%a, %d.%m.")
subject = f"Vorab-Info: Termin am {event_date}"
else:
subject = f"Vorab-Info: {count} Termine naechste Woche"
event_rows = ""
for event in events:
time_str = format_event_time(event["start_at"], event.get("end_at"))
event_date = event["start_at"].replace(tzinfo=timezone.utc).astimezone(tz).strftime("%a, %d.%m.")
summary = event["summary"]
location = f" ({event['location']})" if event.get("location") else ""
event_rows += f"""
<tr>
<td style="padding: 8px 12px; border-bottom: 1px solid #eee; font-weight: bold; white-space: nowrap;">{event_date}</td>
<td style="padding: 8px 12px; border-bottom: 1px solid #eee; white-space: nowrap;">{time_str}</td>
<td style="padding: 8px 12px; border-bottom: 1px solid #eee;">{summary}{location}</td>
</tr>"""
words_display = ", ".join(searchwords)
html = f"""
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
<h2 style="color: #333;">Vorab-Info: Naechste Woche</h2>
<p style="color: #555;">Vom <strong>{start_str}</strong> bis <strong>{end_str}</strong> stehen folgende Termine an:</p>
<table style="width: 100%; border-collapse: collapse; margin: 20px 0; background: #f9f9f9; border-radius: 8px; overflow: hidden;">
{event_rows}
</table>
<p style="color: #888; font-size: 12px;">Suchbegriffe: {words_display}</p>
</body>
</html>
"""
msg = MIMEMultipart("alternative")
msg["Subject"] = subject
msg["From"] = SMTP_FROM
msg["To"] = email
msg.attach(MIMEText(html, "html", "utf-8"))
try:
if SMTP_USE_TLS:
server = smtplib.SMTP(SMTP_HOST, SMTP_PORT)
server.starttls()
else:
server = smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT)
if SMTP_USER and SMTP_PASSWORD:
server.login(SMTP_USER, SMTP_PASSWORD)
server.sendmail(SMTP_FROM, [email], msg.as_string())
server.quit()
log.info("Wochenbenachrichtigung gesendet: %s", subject)
except Exception:
log.exception("Fehler beim Senden der Wochenbenachrichtigung")
def check_and_send_weekly_notification():
if not SMTP_HOST or not (WEEKLY_NOTIFY_EMAIL or NOTIFY_EMAIL):
return
conn = get_connection(autocommit=True)
try:
if should_send_weekly(conn):
searchwords = parse_weekly_searchwords()
events, week_start, week_end = get_weekly_events(conn, CALENDAR_LABEL, searchwords)
if events:
send_weekly_notification(events, searchwords, week_start, week_end)
log_weekly_notification(conn, len(events))
log.info("Wochenbenachrichtigung fuer %d Events gesendet", len(events))
else:
log.info("Keine passenden Termine naechste Woche, Wochenbenachrichtigung wird uebersprungen")
except Exception:
log.exception("Fehler bei der Wochenbenachrichtigung")
finally:
conn.close()
def main():
bootstrap_database()
log.info(
@@ -425,6 +614,7 @@ def main():
try:
run_sync_once()
check_and_send_notification()
check_and_send_weekly_notification()
except Exception:
log.exception("Sync-Durchlauf fehlgeschlagen, versuche es beim naechsten Intervall erneut")
time.sleep(SYNC_INTERVAL_MINUTES * 60)