Add "prefer SMTP" toggle to skip Microsoft Graph for email

When the M365 connector is connected the app always tries Graph first,
and a Graph 202 ends the send — so report mail to recipients Exchange
silently drops (Google-hosted subdomains of the O365 domain) never
reaches them, even with working SMTP configured.

New prefer_smtp flag gates all three Graph branches (smtp_test,
send_report, _maybe_send_auto_email) so they go straight to SMTP. UI
toggle #st-smtpPreferSmtp in Settings → E-mailrapport, saved/loaded by
scheduler.js, with da/de/en strings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
StyxX65 2026-06-22 11:30:45 +02:00
parent 526e2b0b78
commit 874c3ccec1
8 changed files with 20 additions and 5 deletions

View File

@ -366,6 +366,7 @@
"m365_smtp_recipients_hint": "Adskil med komma eller semikolon", "m365_smtp_recipients_hint": "Adskil med komma eller semikolon",
"m365_smtp_save": "Gem", "m365_smtp_save": "Gem",
"m365_smtp_auto_email_manual": "Send rapport efter manuel scanning", "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_send": "Send nu",
"m365_smtp_saved": "Indstillinger gemt.", "m365_smtp_saved": "Indstillinger gemt.",
"m365_smtp_sending": "Sender…", "m365_smtp_sending": "Sender…",

View File

@ -366,6 +366,7 @@
"m365_smtp_recipients_hint": "Komma- oder semikolongetrennt", "m365_smtp_recipients_hint": "Komma- oder semikolongetrennt",
"m365_smtp_save": "Speichern", "m365_smtp_save": "Speichern",
"m365_smtp_auto_email_manual": "Bericht nach manueller Suche senden", "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_send": "Jetzt senden",
"m365_smtp_saved": "Einstellungen gespeichert.", "m365_smtp_saved": "Einstellungen gespeichert.",
"m365_smtp_sending": "Senden…", "m365_smtp_sending": "Senden…",

View File

@ -366,6 +366,7 @@
"m365_smtp_recipients_hint": "Comma or semicolon separated", "m365_smtp_recipients_hint": "Comma or semicolon separated",
"m365_smtp_save": "Save", "m365_smtp_save": "Save",
"m365_smtp_auto_email_manual": "Email report after manual scan", "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_send": "Send now",
"m365_smtp_saved": "Settings saved.", "m365_smtp_saved": "Settings saved.",
"m365_smtp_sending": "Sending…", "m365_smtp_sending": "Sending…",

View File

@ -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. - **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`. - **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. - **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 ## Scheduler — scan_scheduler.py + routes/scheduler.py

View File

@ -148,8 +148,12 @@ def smtp_test():
"</body></html>" "</body></html>"
) )
# Try Graph API first # Try Graph API first — unless the user opted to always use SMTP. Graph
if state.connector and state.connector.is_authenticated(): # 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: try:
_send_email_graph(subject, body_html, recipients) _send_email_graph(subject, body_html, recipients)
return jsonify({"ok": True, "method": "graph", "recipients": recipients}) return jsonify({"ok": True, "method": "graph", "recipients": recipients})
@ -285,8 +289,8 @@ def send_report():
"</body></html>" "</body></html>"
) )
# Try Graph API first # Try Graph API first — unless prefer_smtp is set (see smtp_test for why).
if state.connector and state.connector.is_authenticated(): if state.connector and state.connector.is_authenticated() and not smtp_cfg.get("prefer_smtp"):
try: try:
_send_email_graph(subject, body_html, recipients, _send_email_graph(subject, body_html, recipients,
attachment_bytes=xl_bytes, attachment_name=fname) attachment_bytes=xl_bytes, attachment_name=fname)

View File

@ -54,7 +54,7 @@ def _maybe_send_auto_email():
"</body></html>" "</body></html>"
) )
if state.connector and state.connector.is_authenticated(): if state.connector and state.connector.is_authenticated() and not smtp_cfg.get("prefer_smtp"):
try: try:
_send_email_graph(subject, body_html, recipients, _send_email_graph(subject, body_html, recipients,
attachment_bytes=xl_bytes, attachment_name=fname) attachment_bytes=xl_bytes, attachment_name=fname)

View File

@ -323,6 +323,8 @@ function stLoadSmtp() {
if (pw) pw.value = d.has_password ? '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022' : ''; if (pw) pw.value = d.has_password ? '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022' : '';
const ae = document.getElementById('st-smtpAutoEmail'); const ae = document.getElementById('st-smtpAutoEmail');
if (ae) ae.checked = !!d.auto_email_manual; if (ae) ae.checked = !!d.auto_email_manual;
const ps = document.getElementById('st-smtpPreferSmtp');
if (ps) ps.checked = !!d.prefer_smtp;
}).catch(function(){}); }).catch(function(){});
} }
@ -341,6 +343,7 @@ async function stSmtpSave() {
recipients: document.getElementById('st-smtpTo').value.split(/[,;]/).map(function(s){return s.trim();}).filter(Boolean), recipients: document.getElementById('st-smtpTo').value.split(/[,;]/).map(function(s){return s.trim();}).filter(Boolean),
use_tls: document.getElementById('st-smtpTls').checked, use_tls: document.getElementById('st-smtpTls').checked,
auto_email_manual: !!(document.getElementById('st-smtpAutoEmail') || {}).checked, auto_email_manual: !!(document.getElementById('st-smtpAutoEmail') || {}).checked,
prefer_smtp: !!(document.getElementById('st-smtpPreferSmtp') || {}).checked,
}; };
if (pw !== null) body.password = pw; if (pw !== null) body.password = pw;
st.style.color = 'var(--muted)'; st.textContent = t('m365_smtp_saving','Saving...'); st.style.color = 'var(--muted)'; st.textContent = t('m365_smtp_saving','Saving...');

View File

@ -845,6 +845,10 @@ document.addEventListener('DOMContentLoaded', applyI18n);
<label data-i18n="m365_smtp_auto_email_manual">Email report after manual scan</label> <label data-i18n="m365_smtp_auto_email_manual">Email report after manual scan</label>
<label class="toggle" style="flex:unset"><input type="checkbox" id="st-smtpAutoEmail"><span class="toggle-slider"></span></label> <label class="toggle" style="flex:unset"><input type="checkbox" id="st-smtpAutoEmail"><span class="toggle-slider"></span></label>
</div> </div>
<div class="settings-row">
<label data-i18n="m365_smtp_prefer_smtp">Always send via SMTP (skip Microsoft Graph)</label>
<label class="toggle" style="flex:unset"><input type="checkbox" id="st-smtpPreferSmtp"><span class="toggle-slider"></span></label>
</div>
<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:4px"> <div style="display:flex;justify-content:flex-end;gap:8px;margin-top:4px">
<div id="st-smtpStatus" style="flex:1;font-size:11px;color:var(--muted);align-self:center"></div> <div id="st-smtpStatus" style="flex:1;font-size:11px;color:var(--muted);align-self:center"></div>
<button onclick="stSmtpSave()" style="background:none;border:1px solid var(--border);color:var(--muted);height:26px;padding:0 12px;border-radius:6px;font-size:12px;cursor:pointer;box-sizing:border-box" data-i18n="btn_save">Save</button> <button onclick="stSmtpSave()" style="background:none;border:1px solid var(--border);color:var(--muted);height:26px;padding:0 12px;border-radius:6px;font-size:12px;cursor:pointer;box-sizing:border-box" data-i18n="btn_save">Save</button>