Python imaplib: Read Email with IMAP

Published on: August 22, 2026
Reading time: 5 minutes
Close-up of hands typing on a laptop keyboard, Python book in sight, coding in progress.

The imaplib module implements an IMAP client in Python’s standard library. It can list mailboxes, search messages, fetch headers and bodies, copy messages, change flags, and monitor new activity. It is useful for support automation, attachment processing, archiving, and enterprise email integrations.

IMAP operates directly on a real mailbox. A careless command can mark messages as read, add the \Deleted flag, or permanently remove messages after EXPUNGE. Begin with a dedicated test account, select mailboxes in read-only mode, and use stable UIDs instead of changing sequence numbers.

Secure connection with IMAP4_SSL

import imaplib
import ssl

context = ssl.create_default_context()

with imaplib.IMAP4_SSL(
    "imap.example.com",
    port=993,
    ssl_context=context,
    timeout=15,
) as client:
    client.login("user@example.com", "password")
    print(client.noop())

The Python documentation notes that the internal default SSL context encrypts the connection but does not necessarily verify the certificate and hostname. Pass a context created by ssl.create_default_context() explicitly. Do not send a password over plain IMAP on port 143 before STARTTLS.

For private certificate authorities and minimum protocol versions, read Python ssl.

Credentials and authentication

Do not hard-code passwords. Load credentials from environment variables, a secret manager, or an OAuth flow when required by the provider. The authenticate() method supports SASL mechanisms advertised by the server.

Many providers disable ordinary password login. Inspect capabilities and follow the service documentation. Never print tokens, passwords, authentication challenges, or complete protocol transcripts.

Inspecting capabilities

print(client.capabilities)

Capabilities may include IMAP4REV1, IDLE, UIDPLUS, MOVE, or authentication mechanisms. Do not assume that every server implements the same extensions. Add an explicit fallback or refuse an operation when a required capability is absent.

Listing mailboxes

status, lines = client.list()
if status != "OK":
    raise RuntimeError("unable to list mailboxes")

for line in lines or []:
    print(line.decode("utf-8", errors="replace"))

Mailbox names may use modified UTF-7 or provider-specific delimiters. Avoid parsing every LIST response with a naive split(). A higher-level IMAP library can be worthwhile when broad international mailbox-name compatibility is a requirement.

Selecting INBOX as read-only

status, data = client.select("INBOX", readonly=True)
if status != "OK":
    raise RuntimeError("failed to open INBOX")

message_count = int(data[0])
print("messages:", message_count)

readonly=True reduces the chance of changing flags or deleting content. Re-open the mailbox deliberately in writable mode only when the business operation truly requires mutation.

Use UIDs instead of sequence numbers

Message sequence numbers change whenever the mailbox changes, especially after an EXPUNGE. UIDs are more stable inside the same mailbox.

status, data = client.uid("search", None, "ALL")
if status != "OK":
    raise RuntimeError("search failed")

uids = data[0].split()
print(uids[-10:])

UIDs are not globally permanent identifiers. If a mailbox is recreated, its UIDVALIDITY changes. A synchronization database should store the mailbox identity, UIDVALIDITY, and UID together.

Searching messages

status, data = client.uid(
    "search",
    None,
    "UNSEEN",
    "SINCE",
    "01-Jul-2026",
)

Search criteria are interpreted by the server. IMAP dates contain no time-of-day component. Sender, subject, and text searches also involve quoting and charset rules. Do not concatenate arbitrary user input into a raw search expression without validation.

Fetching headers without marking messages as read

status, data = client.uid(
    "fetch",
    uid,
    "(BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE MESSAGE-ID)])",
)

BODY.PEEK requests message data without adding \Seen on compliant servers. Fetching BODY[] may mark the message as read. Test the behavior against the actual provider and keep read-only selection enabled for passive workflows.

Parsing FETCH responses correctly

A FETCH response may include additional or unsolicited data. The documentation warns against assuming that the message bytes are always located at data[0][1]. Iterate through tuples and verify the literal type.

def fetch_literals(items):
    for item in items or []:
        if isinstance(item, tuple) and len(item) == 2:
            metadata, literal = item
            if isinstance(literal, bytes):
                yield metadata, literal

Request only the sections you need and enforce maximum sizes. For very large messages, fetch headers first, inspect metadata, and retrieve selected parts or bounded chunks.

Parsing an email message

from email import policy
from email.parser import BytesParser

message = BytesParser(policy=policy.default).parsebytes(raw_email)

print(message.get("Subject"))
print(message.get("From"))

Headers and bodies are untrusted data. Do not render HTML email without sanitization. Attachment filenames must never become local filesystem paths directly.

Extracting text with limits

def text_parts(message, limit=1_000_000):
    total = 0
    for part in message.walk():
        if part.get_content_maintype() == "multipart":
            continue
        if part.get_content_disposition() == "attachment":
            continue
        content = part.get_payload(decode=True) or b""
        total += len(content)
        if total > limit:
            raise ValueError("message exceeds limit")
        yield part.get_content_type(), content

Decode text using the declared charset and a safe fallback. The guide to Python codecs covers incremental decoding and error strategies.

Saving attachments safely

Generate an internal filename, enforce per-file and total size limits, inspect the actual format, and store files outside executable directories. A MIME filename can contain traversal sequences, control characters, reserved names, or duplicate names.

Use Python tempfile during validation and Python hashlib for integrity and deduplication.

Flags

Common flags include \Seen, \Answered, \Flagged, \Deleted, and \Draft. Use UID STORE and silent commands when you do not need the expanded response.

client.uid(
    "store",
    uid,
    "+FLAGS.SILENT",
    r"(\Seen)",
)

Before changing flags, confirm that the UID and mailbox still identify the intended message. Record the domain decision in logs without copying sensitive message content.

Deletion happens in two stages

Traditional IMAP deletion adds \Deleted and later runs EXPUNGE. An EXPUNGE can permanently remove every message already marked for deletion in the selected mailbox, including messages marked by another client.

client.uid("store", uid, "+FLAGS.SILENT", r"(\Deleted)")
# Do not call expunge automatically without an explicit policy.

When available, UIDPLUS or MOVE extensions provide more predictable operations. Use unselect() to release a mailbox without expunging. Be aware that close() on a writable mailbox can remove messages marked as deleted.

Copying and moving

copy() copies messages. A legacy move often means COPY, add \Deleted, and EXPUNGE, which is risky under concurrency. If the server advertises MOVE, use the extension through uid("MOVE", ...) and verify the result.

IDLE in Python 3.14

Python 3.14 adds idle(), an iterable context manager that yields notifications such as EXISTS. Set a duration to avoid server inactivity limits.

with client.idle(duration=29 * 60) as idler:
    for response_type, response_data in idler:
        if response_type == "EXISTS":
            print("mailbox changed", response_data)

A notification is not a complete synchronization event. When a change arrives, perform a bounded UID-based search or sync. Reconnect with backoff when the IDLE connection closes.

Total deadlines and reconnecting

The constructor’s timeout covers connection establishment, but long workflows need a total deadline and a reconnect policy. An IMAP4.abort usually requires closing the object and opening a new connection.

Do not blindly retry write commands because a disconnect can occur after the server applied the change but before the client received confirmation.

Checking command results

Most methods return (status, data), where status is commonly OK, NO, or BAD. The absence of an exception does not always mean success.

status, data = client.uid("search", None, "ALL")
if status != "OK":
    detail = data[0] if data else b"no detail"
    raise RuntimeError(f"IMAP returned {status}: {detail!r}")

Logging and privacy

Do not enable imaplib.Debug in production. Protocol traces can reveal mailbox names, addresses, subjects, message identifiers, and authentication details. Log only the logical mailbox, operation, duration, item count, status, and a correlation identifier.

Test invalid certificates, rejected login, missing mailbox, read-only behavior, UIDs and UIDVALIDITY, multipart messages, invalid charsets, oversized attachments, flags, reconnection, IDLE, unsolicited FETCH data, and protection against accidental EXPUNGE.

Common mistakes

Frequent mistakes include omitting certificate verification, using sequence numbers, selecting a mailbox writable by default, fetching BODY[] and marking messages read, reading only the first FETCH tuple, trusting attachment filenames, calling EXPUNGE automatically, logging message contents, and retrying uncertain write operations after a timeout.

Conclusion

imaplib provides detailed control over IMAP mailboxes, but that control requires discipline. Use IMAP4_SSL with an explicitly verified context, select read-only, prefer UIDs, use BODY.PEEK, limit message and attachment sizes, inspect every status response, and make deletion a separately authorized workflow.

Read the official imaplib documentation and IMAP4rev2 RFC 9051. For critical automation, combine an isolated account, idempotent state tracking, observability, and backups.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Extreme close-up of computer code displaying various programming terms and elements.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python ftplib: Secure FTP and FTPS

    Learn Python ftplib to list, download, and upload files over FTP or FTPS with TLS, timeouts, limits, resume, and clear

    Ler mais

    Tempo de leitura: 5 minutos
    21/08/2026
    Server rack representing an endpoint built with Python xmlrpc.server
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    xmlrpc.server: Build XML-RPC Servers

    Learn Python xmlrpc.server to build XML-RPC servers, register functions, restrict methods and paths, and avoid unsafe exposure.

    Ler mais

    Tempo de leitura: 5 minutos
    21/08/2026
    Server cables representing remote calls with Python xmlrpc.client
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python xmlrpc.client: Remote Calls

    Learn Python xmlrpc.client to call XML-RPC services, handle Fault and ProtocolError, use TLS, compatible types, and safe limits.

    Ler mais

    Tempo de leitura: 5 minutos
    21/08/2026
    Error code over binary data representing failures handled with Python urllib.error
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python urllib.error: Handle HTTP Failures

    Learn Python urllib.error to handle URLError, HTTPError, incomplete downloads, selective retries, and clearer network diagnostics.

    Ler mais

    Tempo de leitura: 5 minutos
    21/08/2026
    HTML keycaps representing HTML entities with Python html.entities
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    html.entities: Convert HTML Entities

    Learn Python html.entities to inspect HTML entities, convert names and code points, and avoid confusing decoding with sanitization.

    Ler mais

    Tempo de leitura: 6 minutos
    21/08/2026
    Folder with files representing MIME types identified with Python mimetypes
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    mimetypes: Detect MIME Types for Files

    Learn Python mimetypes to identify file types, validate uploads, and set safer HTTP Content-Type headers.

    Ler mais

    Tempo de leitura: 6 minutos
    20/08/2026