mirror of
https://github.com/skoelle/icloud-contacts-sync.git
synced 2026-09-17 23:40:24 +00:00
birthday mail for each account
This commit is contained in:
+33
-10
@@ -29,7 +29,7 @@ from api.schemas import (
|
||||
SyncRunOut,
|
||||
)
|
||||
from config import Config
|
||||
from mailer import build_message, fetch_birthdays_for_date, send_message
|
||||
from mailer import build_message, fetch_todays_birthdays_for_account, send_message
|
||||
from utils import fmt_birthday_age, fmt_birthday_short, is_unknown_year
|
||||
|
||||
logging.basicConfig(level=Config.LOG_LEVEL, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
@@ -439,20 +439,43 @@ def admin_test_send(
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
with db.get_connection() as conn:
|
||||
birthdays = fetch_birthdays_for_date(conn, target)
|
||||
|
||||
msg = build_message(birthdays, target_date=target)
|
||||
try:
|
||||
send_message(msg)
|
||||
except Exception as exc:
|
||||
accounts = Config.load_accounts()
|
||||
mail_accounts = [a for a in accounts if a.birthday_mail_to]
|
||||
if not mail_accounts:
|
||||
return templates.TemplateResponse(
|
||||
"admin.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user,
|
||||
"show_all": request.session.get("show_all", False),
|
||||
"error": f"Versand fehlgeschlagen: {exc}",
|
||||
"error": "Keine Accounts mit birthday_mail_to konfiguriert",
|
||||
},
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
sent_count = 0
|
||||
errors = []
|
||||
with db.get_connection() as conn:
|
||||
for account in mail_accounts:
|
||||
birthdays = fetch_todays_birthdays_for_account(conn, account.name, target)
|
||||
if not birthdays:
|
||||
continue
|
||||
msg = build_message(account.name, birthdays, target_date=target)
|
||||
msg["To"] = account.birthday_mail_to
|
||||
try:
|
||||
send_message(msg)
|
||||
sent_count += 1
|
||||
except Exception as exc:
|
||||
errors.append(f"{account.name}: {exc}")
|
||||
|
||||
if errors:
|
||||
return templates.TemplateResponse(
|
||||
"admin.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user,
|
||||
"show_all": request.session.get("show_all", False),
|
||||
"error": f"Versand fehlgeschlagen: {'; '.join(errors)}",
|
||||
},
|
||||
status_code=500,
|
||||
)
|
||||
@@ -463,7 +486,7 @@ def admin_test_send(
|
||||
"request": request,
|
||||
"current_user": current_user,
|
||||
"show_all": request.session.get("show_all", False),
|
||||
"success": f"Test-Mail für {target.strftime('%d.%m.%Y')} gesendet ({len(birthdays)} Kontakte).",
|
||||
"success": f"Test-Mails für {target.strftime('%d.%m.%Y')} gesendet ({sent_count} Accounts).",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -230,7 +230,7 @@
|
||||
<button type="submit" class="btn">Test-Geburtstagsmail senden (06.08.)</button>
|
||||
</form>
|
||||
<div class="test-info">
|
||||
Sendet eine Test-Geburtstagsmail an die konfigurierte Adresse (MAIL_TO) mit allen Kontakten, die am 6. August Geburtstag haben. Das Layout entspricht der täglichen Geburtstagsmail.
|
||||
Sendet eine Test-Geburtstagsmail an die im Account hinterlegte Adresse (birthday_mail_to) mit allen Kontakten, die am 6. August Geburtstag haben. Das Layout entspricht der täglichen Geburtstagsmail.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+5
-4
@@ -13,13 +13,15 @@ ICLOUD_BASE_URL = "https://contacts.icloud.com/"
|
||||
|
||||
class Account:
|
||||
def __init__(self, name: str, apple_email: str, apple_app_password: str, authelia_user: str | None,
|
||||
custom_links: list[dict] | None = None, healthcheck_url: str = ""):
|
||||
custom_links: list[dict] | None = None, healthcheck_url: str = "",
|
||||
birthday_mail_to: str | None = None):
|
||||
self.name = name
|
||||
self.apple_email = apple_email
|
||||
self.apple_app_password = apple_app_password
|
||||
self.authelia_user = authelia_user
|
||||
self.custom_links = custom_links or []
|
||||
self.healthcheck_url = healthcheck_url
|
||||
self.birthday_mail_to = birthday_mail_to
|
||||
|
||||
|
||||
class Config:
|
||||
@@ -43,7 +45,6 @@ class Config:
|
||||
SMTP_PASSWORD = os.environ.get("SMTP_PASSWORD", "")
|
||||
SMTP_USE_TLS = os.environ.get("SMTP_USE_TLS", "true").lower() == "true"
|
||||
MAIL_FROM = os.environ.get("MAIL_FROM", "")
|
||||
MAIL_TO = os.environ.get("MAIL_TO", "")
|
||||
|
||||
# Authelia liefert den eingeloggten Benutzer per Header, der vom
|
||||
# vorgeschalteten nginx/traefik als Remote-User weitergereicht wird.
|
||||
@@ -67,7 +68,6 @@ class Config:
|
||||
missing = [n for n, v in [
|
||||
("SMTP_HOST", cls.SMTP_HOST),
|
||||
("MAIL_FROM", cls.MAIL_FROM),
|
||||
("MAIL_TO", cls.MAIL_TO),
|
||||
] if not v]
|
||||
if missing:
|
||||
raise RuntimeError(f"Fehlende Mailer-Umgebungsvariablen: {', '.join(missing)}")
|
||||
@@ -103,7 +103,8 @@ class Config:
|
||||
seen_names.add(name)
|
||||
custom_links = entry.get("custom_links", [])
|
||||
healthcheck_url = entry.get("healthcheck_url", "")
|
||||
accounts.append(Account(name, email, pwd, authelia_user, custom_links, healthcheck_url))
|
||||
birthday_mail_to = entry.get("birthday_mail_to") or None
|
||||
accounts.append(Account(name, email, pwd, authelia_user, custom_links, healthcheck_url, birthday_mail_to))
|
||||
return accounts
|
||||
|
||||
@classmethod
|
||||
|
||||
+54
-39
@@ -1,10 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
|
||||
# Licensed under the MIT License. See LICENSE file in project root for details.
|
||||
"""Sendet eine tägliche E-Mail mit allen heutigen Geburtstagskindern aus der
|
||||
contacts-Tabelle (über alle Accounts hinweg). Wird per Cron einmal täglich
|
||||
um MAIL_SEND_HOUR aufgerufen. Verhindert Doppelversand am selben Tag über
|
||||
die Tabelle birthday_mail_log."""
|
||||
"""Sendet eine tägliche E-Mail mit Geburtstagskindern pro Account aus der
|
||||
contacts-Tabelle. Wird per Cron einmal täglich um MAIL_SEND_HOUR aufgerufen.
|
||||
Verhindert Doppelversand am selben Tag pro Account über die Tabelle
|
||||
birthday_mail_log."""
|
||||
import logging
|
||||
import smtplib
|
||||
import sys
|
||||
@@ -19,17 +19,18 @@ logging.basicConfig(level=Config.LOG_LEVEL, format="%(asctime)s [%(levelname)s]
|
||||
logger = logging.getLogger("mailer")
|
||||
|
||||
|
||||
def fetch_birthdays_for_date(conn, target_date: date) -> list[dict]:
|
||||
def fetch_todays_birthdays_for_account(conn, account_name: str, target_date: date) -> list[dict]:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""SELECT id, account, full_name, given_name, middle_name, family_name,
|
||||
prefix, suffix, birthday, organization, photo_url
|
||||
FROM contacts
|
||||
WHERE birthday IS NOT NULL
|
||||
WHERE account = %s
|
||||
AND birthday IS NOT NULL
|
||||
AND MONTH(birthday) = %s
|
||||
AND DAY(birthday) = %s
|
||||
ORDER BY given_name, family_name, full_name""",
|
||||
(target_date.month, target_date.day),
|
||||
(account_name, target_date.month, target_date.day),
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
for b in rows:
|
||||
@@ -38,32 +39,30 @@ def fetch_birthdays_for_date(conn, target_date: date) -> list[dict]:
|
||||
return rows
|
||||
|
||||
|
||||
def fetch_todays_birthdays(conn) -> list[dict]:
|
||||
return fetch_birthdays_for_date(conn, datetime.now(Config.TIMEZONE).date())
|
||||
|
||||
|
||||
def already_sent_today(conn) -> bool:
|
||||
today = datetime.now(Config.TIMEZONE).date()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT 1 FROM birthday_mail_log WHERE sent_date = %s", (today,))
|
||||
return cur.fetchone() is not None
|
||||
|
||||
|
||||
def log_sent(conn, count: int):
|
||||
def already_sent_today(conn, account: str) -> bool:
|
||||
today = datetime.now(Config.TIMEZONE).date()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"INSERT INTO birthday_mail_log (sent_date, contacts_count) VALUES (%s, %s)",
|
||||
(today, count),
|
||||
"SELECT 1 FROM birthday_mail_log WHERE account = %s AND sent_date = %s",
|
||||
(account, today),
|
||||
)
|
||||
return cur.fetchone() is not None
|
||||
|
||||
|
||||
def log_sent(conn, account: str, count: int):
|
||||
today = datetime.now(Config.TIMEZONE).date()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"INSERT INTO birthday_mail_log (account, sent_date, contacts_count) VALUES (%s, %s, %s)",
|
||||
(account, today, count),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def build_message(birthdays: list[dict], target_date: date | None = None) -> EmailMessage:
|
||||
def build_message(account_name: str, birthdays: list[dict], target_date: date | None = None) -> EmailMessage:
|
||||
today = target_date or datetime.now(Config.TIMEZONE).date()
|
||||
msg = EmailMessage()
|
||||
msg["From"] = Config.MAIL_FROM
|
||||
msg["To"] = Config.MAIL_TO
|
||||
|
||||
if not birthdays:
|
||||
msg["Subject"] = f"Geburtstage heute ({today.isoformat()}): keine"
|
||||
@@ -199,26 +198,42 @@ def main() -> int:
|
||||
logger.error(str(exc))
|
||||
return 1
|
||||
|
||||
accounts = Config.load_accounts()
|
||||
mail_accounts = [a for a in accounts if a.birthday_mail_to]
|
||||
if not mail_accounts:
|
||||
logger.info("Keine Accounts mit birthday_mail_to konfiguriert, überspringe Lauf")
|
||||
return 0
|
||||
|
||||
today = datetime.now(Config.TIMEZONE).date()
|
||||
sent_count = 0
|
||||
errors = 0
|
||||
|
||||
with db.get_connection() as conn:
|
||||
if already_sent_today(conn):
|
||||
logger.info("Geburtstagsmail wurde heute bereits versendet, überspringe")
|
||||
return 0
|
||||
for account in mail_accounts:
|
||||
if already_sent_today(conn, account.name):
|
||||
logger.info("Geburtstagsmail für Account '%s' wurde heute bereits versendet, überspringe", account.name)
|
||||
continue
|
||||
|
||||
birthdays = fetch_todays_birthdays(conn)
|
||||
if not birthdays:
|
||||
logger.info("Keine Geburtstage heute, überspringe Mailversand")
|
||||
return 0
|
||||
birthdays = fetch_todays_birthdays_for_account(conn, account.name, today)
|
||||
if not birthdays:
|
||||
logger.info("Keine Geburtstage heute für Account '%s', überspringe", account.name)
|
||||
continue
|
||||
|
||||
msg = build_message(birthdays)
|
||||
try:
|
||||
send_message(msg)
|
||||
except Exception:
|
||||
logger.exception("Versand der Geburtstagsmail fehlgeschlagen")
|
||||
return 1
|
||||
msg = build_message(account.name, birthdays, today)
|
||||
msg["To"] = account.birthday_mail_to
|
||||
try:
|
||||
send_message(msg)
|
||||
except Exception:
|
||||
logger.exception("Versand der Geburtstagsmail für Account '%s' fehlgeschlagen", account.name)
|
||||
errors += 1
|
||||
continue
|
||||
|
||||
log_sent(conn, len(birthdays))
|
||||
logger.info("Geburtstagsmail versendet: %d Kontakte", len(birthdays))
|
||||
return 0
|
||||
log_sent(conn, account.name, len(birthdays))
|
||||
sent_count += 1
|
||||
logger.info("Geburtstagsmail versendet für Account '%s': %d Kontakte", account.name, len(birthdays))
|
||||
|
||||
logger.info("Mailer-Lauf abgeschlossen: %d Mails versendet, %d Fehler", sent_count, errors)
|
||||
return 1 if errors > 0 else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user