diff --git a/README.md b/README.md index c99e7031e..534c0c9ad 100644 --- a/README.md +++ b/README.md @@ -333,6 +333,12 @@ To expose Odysseus on a local network or Tailscale with HTTPS: | `PyMuPDF` | PDF page rendering in the side viewer panel and form-filling. (Note: AGPL-3.0) | | `markitdown` | Office/EPUB document text extraction (converts .docx/.xlsx/.pptx/.xls/.epub to Markdown). | +### Outlook / Office 365 email +Odysseus email accounts currently use IMAP/SMTP username-password auth. Outlook +and Microsoft 365 generally require OAuth instead, so normal Microsoft mailbox +passwords will fail. See [docs/email-outlook.md](docs/email-outlook.md) for the +current limitation and the planned integration direction. + ## Security Notes Odysseus is a self-hosted workspace with powerful local tools: shell access, file uploads, model downloads, web research, email/calendar integrations, and API tokens. Treat it like an admin console. diff --git a/docs/email-outlook.md b/docs/email-outlook.md new file mode 100644 index 000000000..1f8b97d5d --- /dev/null +++ b/docs/email-outlook.md @@ -0,0 +1,17 @@ +# Outlook / Office 365 email accounts + +Odysseus email accounts currently use IMAP and SMTP with username/password +authentication. That works for providers that still allow app passwords or +mailbox passwords for IMAP/SMTP. + +Microsoft disables basic authentication for Outlook and Microsoft 365 in most +modern accounts and tenants. If you try to add an Outlook account with a normal +password, Microsoft may return errors such as: + +- `IMAP: AUTHENTICATE failed` +- `SMTP: 535 5.7.139 Authentication unsuccessful, basic authentication is disabled` + +This is expected. Odysseus does not support Microsoft OAuth or Graph Mail yet, +so Outlook / Office 365 accounts cannot currently be added through the password +form. Use another email provider with app-password support, or track the future +Microsoft Graph OAuth integration. diff --git a/routes/email_helpers.py b/routes/email_helpers.py index 816aeea8e..6364c58d4 100644 --- a/routes/email_helpers.py +++ b/routes/email_helpers.py @@ -71,6 +71,38 @@ def _send_smtp_message(cfg: dict, from_addr: str, recipients: list[str], message smtp.sendmail(from_addr, recipients, message) +def _friendly_email_auth_error(protocol: str, host: str, error: object) -> str: + """Return a clearer setup error for known provider auth policies.""" + raw = str(error or "") + lower = raw.lower() + host_lower = (host or "").lower() + microsoft_host = any( + marker in host_lower + for marker in ( + "outlook.office365.com", + "smtp.office365.com", + "office365.com", + "outlook.com", + "hotmail.com", + "live.com", + ) + ) + microsoft_basic_auth_failure = ( + "5.7.139" in lower + or "basic authentication is disabled" in lower + or ("authenticate failed" in lower and microsoft_host) + or ("authentication unsuccessful" in lower and microsoft_host) + ) + if microsoft_basic_auth_failure: + return ( + "Microsoft no longer accepts normal mailbox passwords for " + "Outlook/Office 365 IMAP/SMTP in most accounts. Odysseus " + "does not support Microsoft OAuth/Graph mail yet, so Outlook " + "accounts cannot be added with this password form." + ) + return raw[:200] + + def _strip_think(text: str) -> str: """Email-flavored think strip — thin wrapper over the central helper. diff --git a/routes/email_routes.py b/routes/email_routes.py index 1a1f9b701..8441605ea 100644 --- a/routes/email_routes.py +++ b/routes/email_routes.py @@ -48,6 +48,7 @@ from routes.email_helpers import ( _extract_attachment_to_disk, _extract_html, _extract_text, _fetch_sender_thread_context, _pre_retrieve_context, _EMAIL_REPLY_SYS_PROMPT_BASE, _POOL_HOOKS, + _friendly_email_auth_error, SendEmailRequest, ExtractStyleRequest, ATTACHMENTS_DIR, COMPOSE_UPLOADS_DIR, SCHEDULED_DB, attachment_extract_dir, _email_cache_owner_clause, @@ -3163,7 +3164,7 @@ def setup_email_routes(): try: conn.logout() except Exception: pass except Exception as e: - imap_result = {"ok": False, "error": str(e)[:200]} + imap_result = {"ok": False, "error": _friendly_email_auth_error("IMAP", imap_host, e)} smtp_host = (body.get("smtp_host") or "").strip() if smtp_host: @@ -3185,7 +3186,7 @@ def setup_email_routes(): try: smtp.quit() except Exception: pass except Exception as e: - smtp_result = {"ok": False, "error": str(e)[:200]} + smtp_result = {"ok": False, "error": _friendly_email_auth_error("SMTP", smtp_host, e)} return { "ok": imap_result["ok"] and (smtp_result is None or smtp_result["ok"]), diff --git a/static/js/settings.js b/static/js/settings.js index c9e94722a..c6a1d1836 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -2736,13 +2736,14 @@ async function initEmailAccountsSettings() {

${isEdit ? 'Edit Account' : 'New Account'}

+
IMAP (Receiving)
-
+
SMTP (Sending) — optional, leave blank for read-only
@@ -2750,7 +2751,7 @@ async function initEmailAccountsSettings() {
-
+
`; + const eafProviderNotes = { + outlook: { + title: 'Outlook / Office 365 needs OAuth', + body: 'Microsoft disables normal password login for IMAP/SMTP in most Outlook and Microsoft 365 accounts. Odysseus does not support Microsoft OAuth/Graph mail yet, so this preset is only a placeholder for future support.', + }, + }; + const eafNoteEl = el('eaf-provider-note'); + const _renderEafProviderNote = (key) => { + const n = eafProviderNotes[key]; + if (!eafNoteEl || !n) { + if (eafNoteEl) { + eafNoteEl.style.display = 'none'; + eafNoteEl.innerHTML = ''; + } + return; + } + eafNoteEl.style.display = ''; + eafNoteEl.innerHTML = `
${esc(n.title)}
${esc(n.body)}
`; + }; + // Provider preset → autofill host/port/STARTTLS for both halves. el('eaf-provider').addEventListener('change', (e) => { + _renderEafProviderNote(e.target.value); const p = PROVIDERS[e.target.value]; if (!p) return; el('eaf-imap-host').value = p.imap.host; @@ -4071,7 +4093,7 @@ async function initUnifiedIntegrations() {
-
+
SMTP (Sending) — optional, leave blank for read-only
@@ -4079,7 +4101,7 @@ async function initUnifiedIntegrations() {
-
+
Used when nothing else is selected