diff --git a/lang/da.json b/lang/da.json index 0a20a36..d8b96fb 100644 --- a/lang/da.json +++ b/lang/da.json @@ -366,6 +366,7 @@ "m365_smtp_recipients_hint": "Adskil med komma eller semikolon", "m365_smtp_save": "Gem", "m365_smtp_auto_email_manual": "Send rapport efter manuel scanning", + "m365_smtp_prefer_smtp": "Send altid via SMTP (spring Microsoft Graph over)", "m365_smtp_send": "Send nu", "m365_smtp_saved": "Indstillinger gemt.", "m365_smtp_sending": "Sender…", diff --git a/lang/de.json b/lang/de.json index 2cc7a5a..80a880a 100644 --- a/lang/de.json +++ b/lang/de.json @@ -366,6 +366,7 @@ "m365_smtp_recipients_hint": "Komma- oder semikolongetrennt", "m365_smtp_save": "Speichern", "m365_smtp_auto_email_manual": "Bericht nach manueller Suche senden", + "m365_smtp_prefer_smtp": "Immer via SMTP senden (Microsoft Graph überspringen)", "m365_smtp_send": "Jetzt senden", "m365_smtp_saved": "Einstellungen gespeichert.", "m365_smtp_sending": "Senden…", diff --git a/lang/en.json b/lang/en.json index 2360725..3e17284 100644 --- a/lang/en.json +++ b/lang/en.json @@ -366,6 +366,7 @@ "m365_smtp_recipients_hint": "Comma or semicolon separated", "m365_smtp_save": "Save", "m365_smtp_auto_email_manual": "Email report after manual scan", + "m365_smtp_prefer_smtp": "Always send via SMTP (skip Microsoft Graph)", "m365_smtp_send": "Send now", "m365_smtp_saved": "Settings saved.", "m365_smtp_sending": "Sending…", diff --git a/routes/CLAUDE.md b/routes/CLAUDE.md index fec35bd..715004e 100644 --- a/routes/CLAUDE.md +++ b/routes/CLAUDE.md @@ -70,6 +70,7 @@ Exception hierarchy (all inherit `M365Error(Exception)`): - **Gmail vs Google Workspace** — auth error handlers check if SMTP username ends in `@gmail.com`/`@googlemail.com`; custom domains are treated as Google Workspace and error message points to the Workspace admin console. - **Canonical SMTP config keys are `username` and `use_tls`** — all backend readers (`smtp_test`, `_send_report_email`, `_send_email_graph`) use these. The Settings → E-mailrapport tab (`scheduler.js`) historically saved `user`/`starttls`, which left `username` empty so `server.login()` was skipped and the server rejected the send. Frontend now sends the canonical keys, and `_load_smtp_config()` normalises legacy `user`→`username` / `starttls`→`use_tls` for already-saved configs. The send-report modal (`scan.js`) already used the canonical keys. Keep both UIs and the backend on `username`/`use_tls`. - **Graph 202 ≠ delivered** — `_send_email_graph` returns on Graph's HTTP 202 (queued), and `smtp_test`/`send_report` treat that as success and never fall back to SMTP. A recipient on a domain Exchange Online considers an accepted/internal domain (e.g. a Google-hosted subdomain of the O365 domain) is silently dropped after the 202. There is no in-app fix for that routing; reaching such recipients requires SMTP (e.g. Google Workspace `smtp.gmail.com`/`smtp-relay.gmail.com`) or fixing Exchange Accepted Domains. +- **`prefer_smtp` config flag** — when truthy, `smtp_test`, `send_report`, and `_maybe_send_auto_email` (routes/scan.py) skip the Graph path entirely and send via SMTP. This is the in-app escape hatch for the Graph-202 routing trap above. The gate is `... and not smtp_cfg.get("prefer_smtp")` on each Graph branch — keep all three in sync. UI: `#st-smtpPreferSmtp` toggle (key `m365_smtp_prefer_smtp`), saved/loaded by `scheduler.js`. ## Scheduler — scan_scheduler.py + routes/scheduler.py diff --git a/routes/email.py b/routes/email.py index dbffe18..3894745 100644 --- a/routes/email.py +++ b/routes/email.py @@ -148,8 +148,12 @@ def smtp_test(): "" ) - # Try Graph API first - if state.connector and state.connector.is_authenticated(): + # Try Graph API first — unless the user opted to always use SMTP. Graph + # returns 202 (queued) even for recipients Exchange later silently drops + # (e.g. a Google-hosted subdomain of the O365 domain), so SMTP is the only + # reliable path for those; prefer_smtp forces it. + prefer_smtp = bool(saved.get("prefer_smtp")) + if state.connector and state.connector.is_authenticated() and not prefer_smtp: try: _send_email_graph(subject, body_html, recipients) return jsonify({"ok": True, "method": "graph", "recipients": recipients}) @@ -285,8 +289,8 @@ def send_report(): "" ) - # Try Graph API first - if state.connector and state.connector.is_authenticated(): + # Try Graph API first — unless prefer_smtp is set (see smtp_test for why). + if state.connector and state.connector.is_authenticated() and not smtp_cfg.get("prefer_smtp"): try: _send_email_graph(subject, body_html, recipients, attachment_bytes=xl_bytes, attachment_name=fname) diff --git a/routes/scan.py b/routes/scan.py index 30c8b7d..1c76f08 100644 --- a/routes/scan.py +++ b/routes/scan.py @@ -54,7 +54,7 @@ def _maybe_send_auto_email(): "" ) - if state.connector and state.connector.is_authenticated(): + if state.connector and state.connector.is_authenticated() and not smtp_cfg.get("prefer_smtp"): try: _send_email_graph(subject, body_html, recipients, attachment_bytes=xl_bytes, attachment_name=fname) diff --git a/static/js/scheduler.js b/static/js/scheduler.js index 3426e4b..bc7aa7e 100644 --- a/static/js/scheduler.js +++ b/static/js/scheduler.js @@ -323,6 +323,8 @@ function stLoadSmtp() { if (pw) pw.value = d.has_password ? '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022' : ''; const ae = document.getElementById('st-smtpAutoEmail'); if (ae) ae.checked = !!d.auto_email_manual; + const ps = document.getElementById('st-smtpPreferSmtp'); + if (ps) ps.checked = !!d.prefer_smtp; }).catch(function(){}); } @@ -341,6 +343,7 @@ async function stSmtpSave() { recipients: document.getElementById('st-smtpTo').value.split(/[,;]/).map(function(s){return s.trim();}).filter(Boolean), use_tls: document.getElementById('st-smtpTls').checked, auto_email_manual: !!(document.getElementById('st-smtpAutoEmail') || {}).checked, + prefer_smtp: !!(document.getElementById('st-smtpPreferSmtp') || {}).checked, }; if (pw !== null) body.password = pw; st.style.color = 'var(--muted)'; st.textContent = t('m365_smtp_saving','Saving...'); diff --git a/templates/index.html b/templates/index.html index 5f6fa9c..837b34c 100644 --- a/templates/index.html +++ b/templates/index.html @@ -845,6 +845,10 @@ document.addEventListener('DOMContentLoaded', applyI18n); +
+ + +