Fix IMAP Authentication Failed Error in Python

The attachment-intake script stops at login with one of these, although the same address and password work in the webmail interface:

imaplib.IMAP4.error: b'[AUTHENTICATIONFAILED] Invalid credentials (Failure)'
imaplib.IMAP4.error: b'[ALERT] Application-specific password required: https://support.google.com/accounts/answer/185833 (Failure)'
imaplib.IMAP4.error: b'LOGIN failed.'
imaplib.IMAP4.error: b'AUTHENTICATE failed.'

The last two are typical of Microsoft 365, the first two of Gmail, and generic variants come from every other provider. None of them means the script's IMAP code is wrong.

Root Cause

A browser login and an IMAP login are different authentication paths, and providers treat them differently. Mailboxes with multi-factor authentication cannot complete a second factor over IMAP's plain LOGIN command, so providers either reject the account password outright or require a separate app password generated for that one client. Large providers have gone further and disabled password-based ("basic") authentication for IMAP entirely: Microsoft 365 accepts only OAuth 2.0 tokens through the XOAUTH2 mechanism, and Google accepts app passwords only on accounts with 2-step verification and increasingly steers organisations toward OAuth. Beyond those policy reasons, three mundane causes produce the same message: IMAP access switched off for the mailbox or tenant, a username that is not the full address the server expects, and a password containing characters mangled by shell quoting or a .env parser.

Minimal Diagnostic

Ask the server which authentication mechanisms it offers, check the credentials are loaded as intended without printing them, and try the login with the exact server response captured.

# pip install python-dotenv
import imaplib
import os
from dotenv import load_dotenv

def diagnose_login() -> None:
    load_dotenv()
    host = os.environ.get("MAIL_HOST", "")
    user = os.environ.get("MAIL_USER", "")
    password = os.environ.get("MAIL_PASSWORD", "")
    print(f"host={host!r} user={user!r} password: {len(password)} chars, "
          f"leading/trailing space={password != password.strip()}, quotes={password[:1] in {chr(34), chr(39)}}")
    try:
        conn = imaplib.IMAP4_SSL(host, 993, timeout=20)
    except OSError as exc:
        raise SystemExit(f"cannot reach {host}:993 — network or host problem, not authentication: {exc}")
    caps = conn.capabilities
    print("server capabilities:", " ".join(c for c in caps if c.startswith("AUTH=") or c in ("IMAP4rev1", "IMAP4rev2")))
    print("LOGINDISABLED advertised:", "LOGINDISABLED" in caps)
    try:
        typ, data = conn.login(user, password)
        print("login OK:", typ, data)
    except imaplib.IMAP4.error as exc:
        print("login failed, server said:", exc)
    finally:
        try:
            conn.logout()
        except imaplib.IMAP4.error:
            pass

if __name__ == "__main__":
    diagnose_login()
host='outlook.office365.com' user='[email protected]' password: 16 chars, leading/trailing space=False, quotes=False
server capabilities: IMAP4rev1 AUTH=PLAIN AUTH=XOAUTH2
LOGINDISABLED advertised: False
login failed, server said: b'LOGIN failed.'

The server advertises AUTH=XOAUTH2, and plain login fails with correct credentials: basic authentication is disabled for this tenant. On Gmail the output would include the Application-specific password required alert, which names the fix directly.

Reading the login failure The root asks what the server replied. Application-specific password required means the Google account has 2-step verification, fixed with an app password. LOGIN failed or AUTHENTICATE failed on Microsoft 365 means basic authentication is disabled, fixed with OAuth2 XOAUTH2 or the Graph API. Invalid credentials with correct values means IMAP is disabled or the username format is wrong, fixed in the admin settings. A connection error before login means network or host problems rather than authentication. What did the server reply? exact text from IMAP4.error app password required Gmail with 2FA account password refused LOGIN failed (M365) Basic auth off passwords never work invalid credentials Config or policy IMAP off, wrong username no reply / timeout Not auth at all host, port, firewall App password 16 chars, no spaces OAuth2 XOAUTH2 or Graph API Admin settings full address

Fix 1: Use an App Password (Gmail and Other 2FA Mailboxes)

For Google Workspace or Gmail accounts with 2-step verification, and for providers such as Fastmail, iCloud and Yahoo that use the same model, generate an app password in the account's security settings and use it instead of the account password. Store it without the display spaces some providers show.

# pip install python-dotenv
import imaplib
import os
from dotenv import load_dotenv

def login_with_app_password() -> imaplib.IMAP4_SSL:
    load_dotenv()
    host = os.environ["MAIL_HOST"]                                   # e.g. imap.gmail.com
    user = os.environ["MAIL_USER"].strip()                           # changed: full address, trimmed
    app_password = os.environ["MAIL_APP_PASSWORD"].replace(" ", "")  # changed: app password, spaces removed
    conn = imaplib.IMAP4_SSL(host, 993, timeout=30)
    try:
        conn.login(user, app_password)
    except imaplib.IMAP4.error as exc:
        conn.logout()
        raise RuntimeError(f"app password rejected for {user}: {exc}") from exc
    return conn

Check two account-level settings when an app password is still rejected: IMAP access must be enabled in the mail settings, and in Google Workspace the administrator must allow IMAP and must not restrict access to OAuth-only clients. App passwords are revoked when the account password changes, which is a common reason a job that ran for months suddenly fails.

Fix 2: OAuth 2.0 with XOAUTH2 (Microsoft 365)

Exchange Online requires an OAuth access token for IMAP. Register an application in Microsoft Entra ID, grant it the IMAP.AccessAsApp application permission for Office 365 Exchange Online, register its service principal in Exchange, and grant it access to the one mailbox it needs. Then request a token with MSAL and authenticate with XOAUTH2:

# pip install msal python-dotenv
import imaplib
import os
import msal
from dotenv import load_dotenv

def xoauth2_login() -> imaplib.IMAP4_SSL:
    load_dotenv()
    tenant, client_id = os.environ["AZURE_TENANT_ID"], os.environ["AZURE_CLIENT_ID"]
    secret, mailbox = os.environ["AZURE_CLIENT_SECRET"], os.environ["MAIL_USER"]
    app = msal.ConfidentialClientApplication(
        client_id, authority=f"https://login.microsoftonline.com/{tenant}", client_credential=secret)
    result = app.acquire_token_for_client(scopes=["https://outlook.office365.com/.default"])  # changed
    if "access_token" not in result:
        raise RuntimeError(f"token request failed: {result.get('error')}: {result.get('error_description')}")
    auth_string = f"user={mailbox}\x01auth=Bearer {result['access_token']}\x01\x01"          # changed
    conn = imaplib.IMAP4_SSL("outlook.office365.com", 993, timeout=30)
    try:
        conn.authenticate("XOAUTH2", lambda _: auth_string.encode())                          # changed
    except imaplib.IMAP4.error as exc:
        conn.logout()
        raise RuntimeError(f"XOAUTH2 rejected for {mailbox}: {exc}") from exc
    return conn

The .default scope on the Exchange resource asks for the application permissions already granted to the app. AUTHENTICATE failed at this stage — with a valid token — almost always means the Exchange service principal was not registered or not given mailbox permission; those steps happen in Exchange Online PowerShell (New-ServicePrincipal and Add-MailboxPermission), not in the Azure portal. For new work, the Microsoft Graph API avoids IMAP entirely and is simpler to scope — see download attachments from Microsoft 365 with the Graph API.

XOAUTH2 login sequence The script requests a token from Microsoft Entra ID with its client id and secret and the Exchange default scope, and receives an access token. It connects to Exchange Online IMAP, sends AUTHENTICATE XOAUTH2 with a base64 string containing the mailbox user and bearer token, and receives OK when the service principal has permission on that mailbox. Without that permission the same valid token receives AUTHENTICATE failed. Python script Entra ID Exchange IMAP client id + secret, .default scope access_token (60 min) AUTHENTICATE XOAUTH2 OK — mailbox permission present A valid token still fails if the Exchange service principal lacks access to that mailbox

Fix 3: Username, Encoding and IMAP Access Settings

When the provider does accept passwords, the remaining causes are configuration. Normalise what the script sends and state the expected formats in one place:

# pip install python-dotenv
import imaplib
import os
from dotenv import dotenv_values

def login_checked(env_file: str = ".env") -> imaplib.IMAP4_SSL:
    values = dotenv_values(env_file)                         # changed: read raw values, no shell expansion
    host = (values.get("MAIL_HOST") or "").strip()
    user = (values.get("MAIL_USER") or "").strip()
    password = values.get("MAIL_PASSWORD") or ""
    if "@" not in user:
        raise ValueError("MAIL_USER must be the full email address for most providers")
    if password != password.strip():
        raise ValueError("MAIL_PASSWORD has leading or trailing whitespace")
    conn = imaplib.IMAP4_SSL(host, 993, timeout=30)
    try:
        conn.login(user, password)                            # imaplib quotes special characters itself
    except imaplib.IMAP4.error as exc:
        conn.logout()
        raise RuntimeError(f"login rejected by {host}: {exc}") from exc
    return conn

Passwords containing $ are a frequent trap when set through a shell (export MAIL_PASSWORD=pa$$word expands $$ to a process id) or through tools that interpolate variables in .env files. Wrap such values in single quotes in the .env file and read them with dotenv_values. Non-ASCII passwords can also fail with imaplib's LOGIN command, which sends ASCII; AUTHENTICATE PLAIN with UTF-8 encoding works where the server supports it.

Keeping Authentication Working Over Time

Authentication failures rarely happen on day one; they happen months later when a token expires, a password rotates, or a tenant policy changes. Build the job to fail loudly and specifically, and to refresh what can be refreshed automatically:

# pip install msal
import logging
import time
import msal

log = logging.getLogger("mail-auth")

class TokenProvider:
    """Caches an app-only token and refreshes it a few minutes before expiry."""
    def __init__(self, app: msal.ConfidentialClientApplication, scope: str):
        self.app, self.scope = app, scope
        self._token, self._expires = None, 0.0

    def get(self) -> str:
        if self._token is None or time.time() > self._expires - 300:
            result = self.app.acquire_token_for_client(scopes=[self.scope])
            if "access_token" not in result:
                log.error("token refresh failed: %s", result.get("error_description"))
                raise RuntimeError("cannot obtain access token")
            self._token = result["access_token"]
            self._expires = time.time() + int(result.get("expires_in", 3600))
        return self._token

For app-password setups, alert on the first authentication failure rather than retrying: repeated failed logins can lock the account or trigger security alerts, turning a rotated password into a locked mailbox. Put the credential's owner and rotation date in the job's documentation so whoever gets the alert knows who can issue a new one. The alerting side is covered in add retries and failure alerts to automation jobs.

Why working jobs start failing later A job that authenticates successfully can later fail when an OAuth access token expires after about an hour without refresh, when an account password change revokes existing app passwords, when a client secret reaches its configured expiry date, or when a tenant administrator disables basic authentication or IMAP access. Each event produces a generic authentication failure message. 60 minutes access token expires; refresh it password change all app passwords revoked secret expiry client secret end date reached policy change basic auth or IMAP disabled

Verification

Confirm authentication in the environment the job actually runs in — the scheduler, container or service account — not on your laptop, and confirm the session can do what the job needs, not merely log in.

# stdlib only
import imaplib

def verify_session(conn: imaplib.IMAP4_SSL, folders=("INBOX", "Processed", "Ignored")) -> None:
    typ, data = conn.list()
    assert typ == "OK", "LIST failed after login"
    names = {line.decode(errors="replace").split(' "/" ')[-1].strip('"') for line in data if line}
    missing = [f for f in folders if f not in names]
    assert not missing, f"folders missing (create them first): {missing}"
    typ, _ = conn.select("INBOX", readonly=True)
    assert typ == "OK", "cannot open INBOX"
    typ, data = conn.uid("SEARCH", None, "ALL")
    assert typ == "OK", "search not permitted"
    print(f"session verified: {len(data[0].split())} message(s) visible in INBOX")
    conn.logout()

Run the check as the first step of every scheduled execution. It turns "the job found no attachments" — which is indistinguishable from a quiet day — into an explicit failure when access is broken.

FAQ

Can I turn basic authentication back on for Microsoft 365? No. Microsoft has permanently disabled it for IMAP in Exchange Online. OAuth or the Graph API are the supported routes.

Do app passwords work with Google Workspace accounts? Only if the administrator allows them and the user has 2-step verification enabled. Workspace administrators can disable them organisation-wide, in which case OAuth is required.

Why does login work from my machine but not from the server? Conditional access or IP-based policies may block the server's network, or the server reads a different .env. Run the diagnostic on the server itself.

Is it safe to log the server's error message? Yes — the error text does not contain the password. Never log the password, the token or the XOAUTH2 string, which contains the bearer token.

Part of Processing Email Attachments Automatically.