IMAP API in Python: imaplib, IMAPClient & OAuth XOAUTH2 Guide
A complete guide to building Python IMAP email clients in 2026: from stdlib imaplib basics to IMAPClient production patterns, OAuth XOAUTH2 authentication for Gmail and Outlook, and when a unified email API saves you from the complexity entirely.
import imaplib, email
from email.header import decode_header
# Connect with OAuth XOAUTH2
mail = imaplib.IMAP4_SSL('imap.gmail.com')
auth_str = build_xoauth2(user, token)
mail.authenticate('XOAUTH2', lambda x: auth_str)
# Fetch unread messages
mail.select('INBOX')
_, ids = mail.search(None, 'UNSEEN')
for uid in ids[0].split():
_, data = mail.fetch(uid, '(RFC822)')
msg = email.message_from_bytes(data[0][1])What Is an IMAP Python Client?
An IMAP API Python client is any Python program that communicates with a mail server using the IMAP4 protocol (RFC 3501) to read, search, organize, and synchronize email messages. Unlike SMTP which only sends mail, IMAP gives your application bidirectional access to a remote mailbox: you can fetch headers, read bodies, set flags, move messages between folders, and listen for new arrivals in real time using IMAP IDLE.
IMAP (Internet Message Access Protocol) is a standard protocol that lets clients retrieve and manage email stored on a remote server. A Python IMAP client connects over TLS port 993, authenticates (Basic or OAuth XOAUTH2), then issues IMAP commands to fetch, search, flag, and organize messages. Python provides three levels of abstraction: the standard library imaplib, the higher-level IMAPClient wrapper, and fully async options like aioimaplib. For a complete overview of IMAP architecture and use cases, see our IMAP API developer guide.
Sync user inboxes into your CRM, ATS, or helpdesk. Python IMAP lets you poll or push (IDLE) new messages into your database without managing a full mail server.
Extract, classify, and route inbound emails using Python. Your IMAP client feeds a processing queue: invoice parsing, ticket creation, lead scoring, or LLM summarization.
Test email delivery, build inbox zero tools, or audit deliverability. Python IMAP gives you programmatic access to the same mailbox your users see. Check our email API guide for the full ecosystem.
The 3 Python IMAP Stack Options
Choosing the right Python IMAP library depends on your use case: a quick script, a production service, or an async pipeline. Here is how the main options compare before we dive into code examples for each.
| Library | Type | OAuth Support | Async | Parsing Helpers | Best For |
|---|---|---|---|---|---|
| imaplib | Stdlib (zero deps) | Manual | No | Minimal | Simple scripts, no external deps |
| IMAPClient | Third-party wrapper | Manual | No | Good | Production IMAP without async |
| aioimaplib | Async library | Manual | Yes | Limited | asyncio pipelines & IDLE at scale |
| Unipile APIBest | Unified REST/SDK | Built-in | Yes | Full | Multi-provider production at scale |
Skip the IMAP complexity. Unipile abstracts imaplib, OAuth, and multi-provider logic into a single REST API. One integration for Gmail, Outlook, and any IMAP server.
Start building freeQuickstart: Connecting to IMAP with imaplib
The Python standard library ships with imaplib - no install needed. Here is a complete walkthrough from connection to body fetch, with honest commentary on where the pain points are.
Use imaplib.IMAP4_SSL on port 993 for an encrypted connection. For Gmail and Outlook, see the server addresses in our IMAP server connection guide. Basic Auth (password) works for IMAP servers that still allow it - but Gmail and Outlook now require OAuth XOAUTH2 (covered in Section 5).
import imaplib
# Connect to Gmail IMAP (TLS port 993)
mail = imaplib.IMAP4_SSL('imap.gmail.com', 993)
# Basic auth (app password only - OAuth preferred)
mail.login('user@gmail.com', 'app-password')
# List all mailboxes
status, mailboxes = mail.list()
for box in mailboxes:
print(box.decode('utf-8'))
mail.logout()The select() call opens a mailbox and returns the message count. search() returns a space-separated byte string of message IDs - not UIDs. You need to decode and split manually.
# Select INBOX (or any folder)
status, count = mail.select('INBOX')
print(f'Messages: {count[0].decode()}')
# Search UNSEEN messages
status, msg_ids = mail.search(None, 'UNSEEN')
ids = msg_ids[0].split()
print(f'Unread count: {len(ids)}')
# Search by sender
_, from_ids = mail.search(None, 'FROM', 'boss@company.com')
# Search by date range
_, recent = mail.search(None, 'SINCE', '01-Jan-2026')This is where imaplib gets messy. The raw response is a list of tuples containing bytes. You must index data[0][1] to reach the raw message, then pass it to the email module for parsing. Headers are RFC 2047-encoded and require decode_header().
import email
from email.header import decode_header
def decode_str(value):
"""Decode RFC 2047 encoded header value."""
parts = decode_header(value)
decoded = []
for part, enc in parts:
if isinstance(part, bytes):
decoded.append(part.decode(enc or 'utf-8', errors='replace'))
else:
decoded.append(part)
return ' '.join(decoded)
for uid in ids[-5:]: # last 5 messages
_, data = mail.fetch(uid, '(RFC822)')
raw = data[0][1] # tuple unpacking pain
msg = email.message_from_bytes(raw)
subject = decode_str(msg['Subject'] or '')
sender = decode_str(msg['From'] or '')
# Walk MIME parts for plain text body
if msg.is_multipart():
for part in msg.walk():
if part.get_content_type() == 'text/plain':
body = part.get_payload(decode=True).decode('utf-8', errors='replace')
print(f'From: {sender} | Subject: {subject}')The imaplib pain points: raw byte responses require careful tuple indexing (data[0][1]), headers are RFC 2047-encoded and need decode_header(), multipart MIME bodies require a full walk(), and there are no built-in helpers for UIDs vs. sequence numbers. For production workloads, consider IMAPClient (Section 4) or a unified IMAP API instead.
Going Further: IMAPClient for Production
Once you hit production with imaplib, you quickly notice its fragile response parsing, inconsistent UID handling, and absence of helpers for common tasks. IMAPClient is a Pythonic wrapper that smooths out these rough edges while staying close to the IMAP spec.
data[0][1]uid() wrapper manually)use_uid=True)search(), fetch(), move(), copy() APIidle() / idle_check()from imapclient import IMAPClient
import pprint
# Connect with IMAPClient (use_uid=True by default)
server = IMAPClient('imap.gmail.com', ssl=True, use_uid=True)
server.login('user@gmail.com', 'app-password')
# Select and search
server.select_folder('INBOX')
messages = server.search(['UNSEEN'])
print(f'{len(messages)} unread messages')
# Fetch - returns structured dict, not raw bytes
response = server.fetch(messages[:10], ['ENVELOPE', 'BODY[]', 'FLAGS'])
for uid, data in response.items():
envelope = data[b'ENVELOPE']
print(f'UID {uid}: {envelope.subject}')
# Move messages to folder
server.move(messages[:5], 'Archive')
# IDLE - wait for new messages (up to 29 min on Gmail)
server.idle()
responses = server.idle_check(timeout=60)
print('IDLE response:', responses)
server.idle_done()
server.logout()Even IMAPClient requires manual OAuth token management and multi-provider handling. Unipile abstracts all of this - OAuth refresh, provider quirks, IDLE reconnects - behind a single REST API. See our complete email API guide.
Build it with UnipileOAuth XOAUTH2 in Python: Gmail and Microsoft 365
Basic password authentication is dead for major providers. Gmail has blocked it for standard accounts since May 2022. Microsoft 365 deprecated Basic Auth for IMAP in September 2024. XOAUTH2 (an OAuth 2.0 SASL mechanism) is now the only production-grade way to authenticate a Python IMAP client. For the full OAuth flow explanation, see our OAuth email API guide.
XOAUTH2 is a SASL authentication mechanism that encodes an OAuth Bearer token into a base64 string with the format: user={email}\x01auth=Bearer {token}\x01\x01. This string is passed to imaplib.IMAP4_SSL.authenticate('XOAUTH2', callback). The callback returns the base64-encoded string. You are responsible for obtaining and refreshing the OAuth access token through Google or Microsoft identity endpoints before making the IMAP connection. For the Gmail scopes required, see our Gmail API scopes guide.
import imaplib, base64, json
from google.oauth2.credentials import Credentials
from google.auth.transport.requests import Request
def build_xoauth2_string(user_email, access_token):
"""Build XOAUTH2 SASL string for IMAP authentication."""
auth_string = f'user={user_email}\x01auth=Bearer {access_token}\x01\x01'
return base64.b64encode(auth_string.encode('ascii'))
def get_valid_token(creds_dict):
"""Refresh token if expired using google-auth library."""
creds = Credentials.from_authorized_user_info(creds_dict)
if creds.expired and creds.refresh_token:
creds.refresh(Request())
return creds.token
# Required Gmail IMAP scope: https://mail.google.com/
user_email = 'user@gmail.com'
access_token = get_valid_token(creds_dict)
mail = imaplib.IMAP4_SSL('imap.gmail.com')
auth_string = build_xoauth2_string(user_email, access_token)
mail.authenticate('XOAUTH2', lambda x: auth_string)
# Now use as normal
mail.select('INBOX')
_, ids = mail.search(None, 'ALL')
print(f'Total messages: {len(ids[0].split())}')https://mail.google.com/ scope (not gmail.readonly alone - that scope does not grant IMAP access). See our Gmail API scopes guide for a full breakdown. You also need to enable IMAP in Gmail settings and use a Google Cloud OAuth 2.0 client ID with the correct redirect URI.
import imaplib, base64
import msal
def build_xoauth2_string(user_email, access_token):
auth_string = f'user={user_email}\x01auth=Bearer {access_token}\x01\x01'
return base64.b64encode(auth_string.encode('ascii'))
# MSAL - Microsoft Authentication Library for Python
CLIENT_ID = 'your-azure-app-client-id'
TENANT_ID = 'your-tenant-id'
# Scope for IMAP access (Outlook / Microsoft 365)
SCOPE = ['https://outlook.office365.com/IMAP.AccessAsUser.All']
app = msal.PublicClientApplication(CLIENT_ID, authority=f'https://login.microsoftonline.com/{TENANT_ID}')
# Acquire token (interactive or via refresh token)
result = app.acquire_token_by_authorization_code(code, scopes=SCOPE, redirect_uri=redirect_uri)
access_token = result['access_token']
# Connect to Outlook IMAP with XOAUTH2
user_email = 'user@outlook.com'
mail = imaplib.IMAP4_SSL('outlook.office365.com')
auth_string = build_xoauth2_string(user_email, access_token)
mail.authenticate('XOAUTH2', lambda x: auth_string)
mail.select('INBOX')OAuth XOAUTH2 is 50+ lines per provider. Unipile handles token refresh, XOAUTH2 handshake, and provider-specific quirks so your Python code stays focused on business logic - not auth plumbing.
Build your OAuth flowMulti-Provider Reality Check
Every provider has its own authentication quirks, server addresses, and rate limits. Here is what your Python IMAP client needs to handle for each. For server hostnames and TLS port details, see our dedicated IMAP server connection guide.
https://mail.google.com/IMAP.AccessAsUser.AllSupporting multiple providers means maintaining separate auth flows, server configs, and quirk workarounds. Unipile normalizes Gmail, Outlook, and any IMAP server into a single API. Read our email API providers comparison for context.
Build multi-provider emailWhen You Should Skip IMAP Entirely: Unified API Approach
After walking through imaplib, IMAPClient, and XOAUTH2, one truth becomes clear: raw IMAP in Python requires hundreds of lines of boilerplate for every provider you support. Unipile is a unified IMAP API that abstracts all of this into a single REST interface. You link user accounts (Gmail, Outlook, or any IMAP server) with OAuth, and Unipile handles every protocol detail on your behalf.
# 1. Get OAuth token (50+ lines)
# 2. Build XOAUTH2 string
# 3. Connect + authenticate
# 4. Select folder
# 5. Search messages
# 6. Fetch raw bytes
# 7. Parse tuple data[0][1]
# 8. Decode RFC 2047 headers
# 9. Walk MIME parts
# 10. Handle attachments
# 11. Manage IDLE timeouts
# 12. Reconnect on error
# 13. Repeat for Outlook...
# 14. Repeat for IMAP servers...import requests
# Fetch emails from any linked account
r = requests.get(
'https://api6.unipile.com/api/v1/emails',
headers={'X-API-KEY': api_key},
params={'account_id': account_id}
)
emails = r.json()['items']
# Works for Gmail, Outlook, any IMAPUnipile manages XOAUTH2 token acquisition, refresh, and expiry for Gmail and Outlook. No MSAL or google-auth setup needed in your code.
No raw bytes, no tuple unpacking, no RFC 2047 decoding. Every email comes back as clean structured JSON with parsed headers, body, and attachments.
Instead of managing IDLE sessions and reconnects, Unipile pushes new email events to your webhook endpoint. No polling, no socket management.
One API endpoint serves Gmail, Outlook, and any IMAP server. Add new providers by linking accounts - no code changes required on your side.
Unipile covers the full email lifecycle. Read, search, move, flag, and send - all from the same API. See our Python send email guide for the sending side.
Each of your users links their own account via OAuth. Unipile acts as an independent technical intermediary on behalf of each authenticated user - not a shared credential pool.
Common Pitfalls in Production
After getting a Python IMAP client working locally, production reveals edge cases that can cause silent data loss, connection drops, or broken character encoding. Here are the most common issues and how to fix them.
Microsoft completed the removal of Basic Authentication for IMAP on all Microsoft 365 tenants in September 2024. Any Python code using mail.login(user, password) against outlook.office365.com will now receive AUTHENTICATE failed. Migration to XOAUTH2 (Section 5) is mandatory for all Microsoft 365 / Outlook IMAP integrations. Exchange on-premises deployments may still support Basic Auth depending on server configuration.
Gmail silently terminates IDLE connections after approximately 29 minutes. If your code does not renew the IDLE session, new messages stop arriving without any exception being raised.
idle_done() every 25 minutes, then immediately call idle() again. Use select_folder('INBOX', readonly=False) to recheck message count on reconnect.Email headers like Subject and From use RFC 2047 encoding (=?UTF-8?B?...?=). Calling str(msg['Subject']) directly on a bytes value returns garbage or raises UnicodeDecodeError.
email.header.decode_header() and handle each part's encoding explicitly. Pass errors='replace' to .decode() to avoid crashes on malformed headers.Attachments live in nested MIME parts. Calling get_payload() on a multipart message returns a list of parts, not the content. Inline images are Content-Disposition: inline and have a Content-ID.
msg.walk(), check get_content_disposition() for attachment, and call get_payload(decode=True) on attachment parts to get decoded bytes.imaplib.IMAP4_SSL instances are NOT thread-safe. Using one connection across multiple threads causes garbled responses. Creating a new connection per request is expensive and rate-limited.
aioimaplib. Always implement exponential backoff for reconnect attempts after IMAP4.abort exceptions.Gmail limits IMAP connections to 15 concurrent connections per account and enforces bandwidth quotas (2,500 MB/day via IMAP). Fetching entire RFC822 bodies at scale will hit these limits quickly.
RFC822.HEADER), then fetch bodies selectively. Use BODY.PEEK[] instead of RFC822 to avoid auto-marking as read. Implement exponential backoff on [OVERQUOTA] responses.Access tokens from Google and Microsoft expire after 3,600 seconds (1 hour). If your IMAP session is still open when the token expires, the next command after expiry will silently fail or return NO.
expires_at timestamp. Before each batch operation, check if the token is within 5 minutes of expiry and refresh proactively. Store refresh tokens securely. This is exactly what Unipile handles automatically.Avoiding all 6 pitfalls requires significant infrastructure. Unipile handles IDLE reconnects, token refresh, rate limiting, and multi-provider normalization - so you can focus on your product logic. Part of our full email API guide.
Build without the pitfallsFrequently Asked Questions
Common questions about Python IMAP clients, imaplib, IMAPClient, XOAUTH2 authentication, and when to use a unified email API instead.
imap.gmail.com on port 993 using imaplib.IMAP4_SSL. However, Gmail no longer accepts plain username/password login for third-party apps. You must use OAuth XOAUTH2 authentication with an access token obtained from Google using the https://mail.google.com/ scope. See our Gmail API scopes guide for the full scope breakdown. App passwords still work for personal accounts with 2-step verification enabled, but OAuth is required for applications.user={email}\x01auth=Bearer {token}\x01\x01 in base64, then pass it to imaplib.IMAP4_SSL.authenticate('XOAUTH2', callback). This replaces the deprecated Basic Auth login for Gmail and Microsoft 365. Full implementation examples are in Section 5 of this guide. See also our OAuth email API guide.base64(f'user={email}\x01auth=Bearer {token}\x01\x01'). 3) Call mail.authenticate('XOAUTH2', lambda x: auth_string). For Gmail, use the google-auth library to manage token refresh. For Outlook/Microsoft 365, use the msal Python library. Both require registering an OAuth application in Google Cloud Console or Azure AD. The full imap python oauth code is in Section 5 above.search()/fetch()/move() methods, and includes built-in IDLE support. Both require you to handle OAuth token management yourself. Neither is async - for asyncio-based pipelines, consider aioimaplib. Install IMAPClient with pip install imapclient.outlook.office365.com on port 993 using imaplib.IMAP4_SSL. Since September 2024, Basic Auth is disabled for Microsoft 365 - you must use XOAUTH2. Register an Azure AD application, request the IMAP.AccessAsUser.All permission, obtain an access token using the msal Python library, then authenticate with mail.authenticate('XOAUTH2', ...). For personal Outlook.com accounts, the same OAuth flow applies. For server details and TLS configuration, see our IMAP server connection guide.outlook.office365.com:993. However, since September 2024, Microsoft has fully disabled Basic Authentication for IMAP on Microsoft 365 tenants. You must use OAuth 2.0 with the XOAUTH2 SASL mechanism. Your Azure AD administrator may also need to enable IMAP access in the Exchange admin center. An alternative is to use the Microsoft Graph API (REST) which avoids IMAP entirely and provides richer metadata.server.idle() to enter IDLE mode, then server.idle_check(timeout=60) to wait for events. Gmail silently disconnects after ~29 minutes, so you must renew: call server.idle_done() every 25 minutes, then server.idle() again. With raw imaplib, IDLE requires manual socket-level management - considerably more complex. For production real-time email, a webhook-based approach (as Unipile provides) is more reliable than maintaining IDLE sessions.aioimaplib library provides an asyncio-compatible IMAP4 client for Python. It supports standard IMAP commands and IDLE in an async context, making it suitable for high-concurrency pipelines where you manage many mailboxes simultaneously. However, aioimaplib has a smaller community than imaplib or IMAPClient, and OAuth support still requires manual XOAUTH2 implementation. For teams prioritizing developer speed over fine-grained control, a REST-based unified API delivers real-time email via webhooks without async socket management.