Headless FastAPI-Backend (server.py + core/engine.py) mit Mobile-PWA (web/), Strategie-/Backtest-Suite und Doku. Secrets, DB, Logs und Laufzeit-State sind via .gitignore ausgeschlossen; Config-Vorlage: oil_widget_config.ini.example. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
61 lines
2.6 KiB
Python
61 lines
2.6 KiB
Python
"""
|
|
core/mailer.py — E-Mail via Microsoft Graph (client-credentials)
|
|
=================================================================
|
|
Sendet HTML-Mails über die Graph-App-Registrierung in `[graph]`
|
|
(tenant_id/client_id/client_secret). Absender = `[graph] sender` oder
|
|
Default mailagent@hocks.eu. Ersetzt den PowerShell-Umweg (send_daily_report.ps1)
|
|
durch reines Python — so kann die Engine den Tagesreport selbst verschicken.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from core.logger import get_logger
|
|
|
|
log = get_logger("mail")
|
|
|
|
_TOKEN_URL = "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token"
|
|
_SENDMAIL = "https://graph.microsoft.com/v1.0/users/{sender}/sendMail"
|
|
_DEFAULT_SENDER = "mailagent@hocks.eu"
|
|
|
|
|
|
def send_graph_mail(graph_cfg, subject: str, html: str, to_addr: str) -> bool:
|
|
"""True bei Erfolg. `to_addr` darf mehrere Adressen kommagetrennt enthalten.
|
|
Fail-safe: loggt + gibt False zurück, wirft nie."""
|
|
import requests
|
|
g = graph_cfg or {}
|
|
tenant = (g.get("tenant_id") or "").strip()
|
|
cid = (g.get("client_id") or "").strip()
|
|
secret = (g.get("client_secret") or "").strip()
|
|
sender = (g.get("sender") or _DEFAULT_SENDER).strip()
|
|
recipients = [{"emailAddress": {"address": a.strip()}}
|
|
for a in (to_addr or "").split(",") if a.strip()]
|
|
if not (tenant and cid and secret) or not recipients:
|
|
log.warning("Graph-Config/Empfänger unvollständig — keine E-Mail")
|
|
return False
|
|
try:
|
|
tok = requests.post(
|
|
_TOKEN_URL.format(tenant=tenant),
|
|
data={"client_id": cid, "client_secret": secret,
|
|
"scope": "https://graph.microsoft.com/.default",
|
|
"grant_type": "client_credentials"}, timeout=30)
|
|
tok.raise_for_status()
|
|
access = tok.json().get("access_token")
|
|
if not access:
|
|
log.warning("Graph: kein access_token"); return False
|
|
r = requests.post(
|
|
_SENDMAIL.format(sender=sender),
|
|
headers={"Authorization": "Bearer " + access},
|
|
json={"message": {
|
|
"subject": subject,
|
|
"body": {"contentType": "HTML", "content": html},
|
|
"toRecipients": recipients,
|
|
"from": {"emailAddress": {"address": sender}}},
|
|
"saveToSentItems": True}, timeout=30)
|
|
if r.status_code in (200, 202):
|
|
log.info(f"E-Mail an {to_addr}: {subject}")
|
|
return True
|
|
log.warning(f"Graph sendMail {r.status_code}: {r.text[:200]}")
|
|
return False
|
|
except Exception as e:
|
|
log.warning(f"send_graph_mail: {e}")
|
|
return False
|