diff --git a/.env.example b/.env.example index b60902a..73d7430 100644 --- a/.env.example +++ b/.env.example @@ -18,7 +18,6 @@ SMTP_USER=mailer@example.de SMTP_PASSWORD=change-me SMTP_USE_TLS=true MAIL_FROM=contacts-sync@example.de -MAIL_TO=du@example.de MAIL_SEND_HOUR=7 # --- Web-Ansicht / API (nur relevant für den zweiten Container) --- diff --git a/README.md b/README.md index b334aa1..0c93246 100644 --- a/README.md +++ b/README.md @@ -33,10 +33,10 @@ vim config/accounts.json ``` Trage für jede Apple-ID einen Eintrag mit eindeutigem `name`, -`apple_email`, `apple_app_password` und `authelia_user` ein. Optional -kann pro Account eine `healthcheck_url` konfiguriert werden, die nach -jedem erfolgreichen Sync aufgerufen wird (z.B. für Uptime-Monitoring). -Diese Datei +`apple_email`, `apple_app_password`, `authelia_user` und (optional) +`birthday_mail_to` ein. Optional kann pro Account eine `healthcheck_url` +konfiguriert werden, die nach jedem erfolgreichen Sync aufgerufen wird +(z.B. für Uptime-Monitoring). Diese Datei bleibt lokal auf dem Host, sie ist in `.gitignore` ausgeschlossen und wird nur als Volume in den Container gemountet. @@ -58,7 +58,9 @@ vim .env ``` Trage mindestens `MARIADB_USER`, `MARIADB_PASSWORD` sowie (falls du den -Mailer nutzen willst) `SMTP_HOST`, `MAIL_FROM` und `MAIL_TO` ein. +Mailer nutzen willst) `SMTP_HOST` und `MAIL_FROM` ein. Die +Empfänger-Adresse wird pro Account in `accounts.json` unter +`birthday_mail_to` konfiguriert. ## 5. Image beziehen @@ -106,7 +108,7 @@ SELECT account, sync_token, updated_at FROM sync_state; Versandhistorie der Geburtstagsmails: ```sql -SELECT sent_date, contacts_count, sent_at FROM birthday_mail_log +SELECT account, sent_date, contacts_count, sent_at FROM birthday_mail_log ORDER BY sent_date DESC LIMIT 10; ``` @@ -114,16 +116,18 @@ ORDER BY sent_date DESC LIMIT 10; - Läuft automatisch täglich um die in `MAIL_SEND_HOUR` konfigurierte Stunde (Default 7 Uhr) innerhalb desselben Containers. -- Versendet eine HTML-E-Mail mit stylisierten Geburtstagskarten und - Links zur Kontakt-Detailseite (falls `WEB_URL` gesetzt). +- Versendet pro Account mit gesetztem `birthday_mail_to` eine eigene + HTML-E-Mail mit stylisierten Geburtstagskarten und Links zur + Kontakt-Detailseite (falls `WEB_URL` gesetzt). - Über `MAILER_ENABLED=false` lässt sich der Mailer ganz abschalten, ohne den Kontakt-Sync zu beeinträchtigen. - Manueller Testlauf im laufenden Container: ``` docker exec -it icloud-contacts-sync python3 /app/mailer.py ``` -- Ein zweiter manueller Lauf am selben Tag versendet keine zweite Mail, - solange bereits ein Eintrag in `birthday_mail_log` für heute existiert. +- Ein zweiter manueller Lauf am selben Tag versendet keine zweite Mail + pro Account, solange bereits ein Eintrag in `birthday_mail_log` für + heute und diesen Account existiert. ## 9. Lokale Entwicklung (ohne Docker) diff --git a/SPEC.md b/SPEC.md index cd090c1..8f234f4 100644 --- a/SPEC.md +++ b/SPEC.md @@ -39,8 +39,8 @@ außerhalb des Apple-Ökosystems. ```json { "accounts": [ - { "name": "markus", "apple_email": "markus@icloud.com", "apple_app_password": "xxxx-xxxx-xxxx-xxxx", "authelia_user": "mmustermann", "healthcheck_url": "https://healthchecks.example.de/ping/abc123" }, - { "name": "partner", "apple_email": "partner@icloud.com", "apple_app_password": "yyyy-yyyy-yyyy-yyyy", "authelia_user": "pmustermann" } + { "name": "markus", "apple_email": "markus@icloud.com", "apple_app_password": "xxxx-xxxx-xxxx-xxxx", "authelia_user": "mmustermann", "birthday_mail_to": "markus@example.de", "healthcheck_url": "https://healthchecks.example.de/ping/abc123" }, + { "name": "partner", "apple_email": "partner@icloud.com", "apple_app_password": "yyyy-yyyy-yyyy-yyyy", "authelia_user": "pmustermann", "birthday_mail_to": "partner@example.de" } ], "admins": ["mmustermann"] } @@ -96,8 +96,8 @@ Siehe `sql/schema.sql`. Wichtigste Änderungen gegenüber v1: aktuellen `sync_token`. - `sync_runs` erweitert um `account`, `sync_type` (`initial`/`delta`), `contacts_upserted`, `contacts_deleted`. -- Neue Tabelle `birthday_mail_log`: ein Datensatz pro Tag, an dem - erfolgreich eine Geburtstagsmail versendet wurde, verhindert +- Neue Tabelle `birthday_mail_log`: ein Datensatz pro Tag und Account, + an dem erfolgreich eine Geburtstagsmail versendet wurde, verhindert Doppelversand bei mehrfachem Container-Neustart am selben Tag. - Neue Tabellen `groups` und `group_members`: Speichert iCloud-Kontaktgruppen (vCards mit `X-ADDRESSBOOKSERVER-KIND:group`) @@ -110,20 +110,23 @@ Siehe `sql/schema.sql`. Wichtigste Änderungen gegenüber v1: - Eigenständiges Skript `src/mailer.py`, läuft im selben Container über einen zweiten Cron-Eintrag, täglich zur in `MAIL_SEND_HOUR` konfigurierten Stunde (Default 7 Uhr). -- Query: alle Kontakte über alle Accounts hinweg, deren `birthday` +- Pro Account mit gesetztem `birthday_mail_to` wird eine eigene E-Mail + versendet, die nur Geburtstage aus diesem Account enthält. +- Query: Kontakte des jeweiligen Accounts, deren `birthday` (Monat/Tag) auf das heutige Datum fällt. - Versand nur bei existierenden Geburtstagen: Wird keine E-Mail versendet, wenn die Abfrage keine Treffer liefert. - Versand per SMTP mit STARTTLS (`smtplib`), Konfiguration über - `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASSWORD`, `MAIL_FROM`, - `MAIL_TO`. + `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASSWORD`, `MAIL_FROM`. + Die Empfänger-Adresse (`birthday_mail_to`) wird pro Account in + `accounts.json` konfiguriert. - Idempotenz: vor dem Versand wird `birthday_mail_log` auf einen - Eintrag für den heutigen Tag geprüft; existiert bereits einer, wird - der Lauf ohne erneuten Versand beendet. + Eintrag für den heutigen Tag und den jeweiligen Account geprüft; + existiert bereits einer, wird der Lauf ohne erneuten Versand beendet. - Feature-Flag `MAILER_ENABLED` erlaubt das komplette Deaktivieren ohne Codeänderung (Default: `false`). - E-Mail-Inhalt: HTML-E-Mail mit stylisierten Geburtstagskarten - (Name, Alter, Account, Link zur Kontakt-Detailseite falls `WEB_URL` + (Name, Alter, Link zur Kontakt-Detailseite falls `WEB_URL` gesetzt). Zusätzlich reiner Text-Alternative als Fallback. ## 7. Konfiguration (Umgebungsvariablen) @@ -144,13 +147,15 @@ Siehe `sql/schema.sql`. Wichtigste Änderungen gegenüber v1: | SMTP_PASSWORD | nein | leer, falls Relay ohne Auth | | SMTP_USE_TLS | nein | Default: true | | MAIL_FROM | ja (Mailer) | Absenderadresse | -| MAIL_TO | ja (Mailer) | Empfängeradresse(n) | | MAIL_SEND_HOUR | nein | Default: 7, Stunde (0-23) für täglichen Mailversand | | WEB_URL | nein | Web-URL für Links in Geburtstags-Mails (z.B. https://kontakte.example.de) | | AUTH_REMOTE_USER_HEADER | nein | Default: Remote-User, Header-Name für Authelia-User | | API_HOST | nein | Default: 0.0.0.0, Bindungs-Adresse des API-Services | | API_PORT | nein | Default: 8000, Port des API-Services | +Empfänger-Adresse für Geburtstags-Mails: `birthday_mail_to` pro Account +in `accounts.json` (keine globale Umgebungsvariable mehr nötig). + Secrets werden weiterhin als klassische Umgebungsvariablen übergeben, mit Ausnahme der Multi-Account-Zugangsdaten, die aus `accounts.json` gelesen werden (per Volume-Mount, nicht im Image, nicht im Git). diff --git a/config/README.md b/config/README.md index cebddf7..bfbd7c1 100644 --- a/config/README.md +++ b/config/README.md @@ -15,6 +15,9 @@ Felder pro Account: `Remote-User`-Header an die Web-Ansicht/API durchreicht. Dieser Wert bestimmt, welchen Account ein eingeloggter Benutzer in der Web-Ansicht sieht. +- `birthday_mail_to` (optional): Empfänger-Adresse für die tägliche + Geburtstagsmail. Fehlt das Feld oder ist leer, wird für diesen Account + keine Geburtstagsmail versendet. - `healthcheck_url` (optional): URL, die nach jedem erfolgreichen Sync-Lauf dieses Accounts aufgerufen wird (z.B. für Uptime-Monitoring wie Healthchecks.io). Bei Sync-Fehlern wird die URL nicht aufgerufen. diff --git a/config/accounts.json.example b/config/accounts.json.example index 1d663ff..d0a4b44 100644 --- a/config/accounts.json.example +++ b/config/accounts.json.example @@ -5,6 +5,7 @@ "apple_email": "markus@icloud.com", "apple_app_password": "xxxx-xxxx-xxxx-xxxx", "authelia_user": "mmustermann", + "birthday_mail_to": "markus@example.de", "custom_links": [ {"label": "Google Suche", "url": "https://www.google.com/search?q=[fullname]"}, {"label": "LinkedIn", "url": "https://www.linkedin.com/search/results/all/?keywords=[fullname]"}, @@ -18,6 +19,7 @@ "apple_email": "partner@icloud.com", "apple_app_password": "yyyy-yyyy-yyyy-yyyy", "authelia_user": "partner.user", + "birthday_mail_to": "partner@example.de", "custom_links": [], "healthcheck_url": "" } diff --git a/docker-compose.yml b/docker-compose.yml index b9fa7f5..12c47c4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,7 +20,6 @@ services: SMTP_PASSWORD: "${SMTP_PASSWORD}" SMTP_USE_TLS: "${SMTP_USE_TLS:-true}" MAIL_FROM: "${MAIL_FROM}" - MAIL_TO: "${MAIL_TO}" MAIL_SEND_HOUR: "${MAIL_SEND_HOUR:-7}" TIMEZONE: "${TIMEZONE:-Europe/Berlin}" WEB_URL: "${WEB_URL:-}" @@ -57,7 +56,6 @@ services: SMTP_PASSWORD: "${SMTP_PASSWORD}" SMTP_USE_TLS: "${SMTP_USE_TLS:-true}" MAIL_FROM: "${MAIL_FROM}" - MAIL_TO: "${MAIL_TO}" TIMEZONE: "${TIMEZONE:-Europe/Berlin}" WEB_URL: "${WEB_URL:-}" volumes: diff --git a/sql/schema.sql b/sql/schema.sql index 2a1ffac..9ef6be5 100644 --- a/sql/schema.sql +++ b/sql/schema.sql @@ -81,11 +81,12 @@ CREATE TABLE IF NOT EXISTS group_members ( KEY idx_group_members_member_uid (member_uid) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; --- Protokoll der Geburtstags-Mails, verhindert Doppelversand am selben Tag. +-- Protokoll der Geburtstags-Mails, verhindert Doppelversand am selben Tag pro Account. CREATE TABLE IF NOT EXISTS birthday_mail_log ( id INT AUTO_INCREMENT PRIMARY KEY, + account VARCHAR(100) NOT NULL, sent_date DATE NOT NULL, contacts_count INT NOT NULL, sent_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - UNIQUE KEY uq_birthday_mail_date (sent_date) + UNIQUE KEY uq_birthday_mail_date_account (account, sent_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/src/api/main.py b/src/api/main.py index baa5438..0dfea57 100644 --- a/src/api/main.py +++ b/src/api/main.py @@ -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).", }, ) diff --git a/src/api/templates/admin.html b/src/api/templates/admin.html index a687b3c..cb262ce 100644 --- a/src/api/templates/admin.html +++ b/src/api/templates/admin.html @@ -230,7 +230,7 @@