"""Faellige Erinnerungen als Mail verschicken. Der Scheduler fragt die Datenbank ab, statt Timer im Speicher zu halten: laeuft der Server zum Zeitpunkt einer Erinnerung nicht, wird sie beim naechsten Start nachgeholt. """ import asyncio import logging from datetime import datetime, timezone from . import config, db, mailer log = logging.getLogger(__name__) INTERVAL_SECONDS = 60 def _now() -> str: return datetime.now(timezone.utc).isoformat() def _body(row) -> str: who = row["full_name"] or row["company"] or "Unbekannter Kontakt" lines = [row["text"], "", "-" * 40, f"Kontakt: {who}"] if row["company"] and row["full_name"]: lines.append(f"Firma: {row['company']}") if row["email"]: lines.append(f"E-Mail: {row['email']}") if row["phone"]: lines.append(f"Telefon: {row['phone']}") if row["mobile"]: lines.append(f"Mobil: {row['mobile']}") if row["place"]: lines.append(f"Kennengelernt in: {row['place']}") if config.PUBLIC_BASE_URL: lines += ["", f"{config.PUBLIC_BASE_URL}/#/karte/{row['card_id']}"] return "\n".join(lines) def due_reminders() -> list: return db.query( """ SELECT r.id, r.card_id, r.text, r.due_at, c.full_name, c.company, c.email, c.phone, c.mobile, c.place FROM reminders r JOIN cards c ON c.id = r.card_id WHERE r.sent_at IS NULL AND r.due_at <= ? ORDER BY r.due_at """, (_now(),), ) def process_due() -> int: """Alle faelligen Erinnerungen abarbeiten, Anzahl der Versendeten zurueck.""" rows = due_reminders() if not rows: return 0 if not mailer.configured(): log.warning("%d faellige Erinnerung(en), aber kein SMTP konfiguriert", len(rows)) return 0 sent = 0 for row in rows: who = row["full_name"] or row["company"] or "Kontakt" try: mailer.send(f"Erinnerung: {who}", _body(row)) except Exception as exc: log.exception("Erinnerung %s konnte nicht verschickt werden", row["id"]) db.execute( "UPDATE reminders SET send_error = ? WHERE id = ?", (f"{type(exc).__name__}: {exc}", row["id"]), ) continue db.execute( "UPDATE reminders SET sent_at = ?, send_error = NULL WHERE id = ?", (_now(), row["id"]), ) sent += 1 return sent async def run_forever() -> None: log.info("Erinnerungs-Scheduler gestartet (Intervall %ds)", INTERVAL_SECONDS) while True: try: await asyncio.to_thread(process_due) except Exception: log.exception("Scheduler-Durchlauf fehlgeschlagen") await asyncio.sleep(INTERVAL_SECONDS)