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() {
-
+