The poplib module implements a POP3 client in Python’s standard library. It can inspect a mailbox, list messages, retrieve content, and mark messages for deletion. POP3 still appears in older email accounts, embedded devices, and simple integrations that download mail from a server.
Python’s own documentation describes POP3 as obsolescent and recommends IMAP when available. POP3 usually provides a simple mailbox view without folders, rich server-side search, synchronized flags, or reliable partial access. Use it only when a provider or legacy system requires the protocol.
Secure connection with POP3_SSL
import poplib
import ssl
context = ssl.create_default_context()
client = poplib.POP3_SSL(
"pop.example.com",
port=995,
timeout=15,
context=context,
)
try:
client.user("user@example.com")
client.pass_("password")
print(client.stat())
finally:
client.quit()
Use POP3_SSL or STARTTLS before authentication. Plain POP3 can expose usernames, passwords, and message contents. The context created by ssl.create_default_context() verifies the certificate and hostname.
Do not disable verification to work around expired certificates or a hostname mismatch. Read Python ssl for trust-chain and hostname guidance.
STARTTLS on port 110
When a server uses explicit TLS upgrade, connect without authenticating, inspect capabilities, and call stls() with a verified context.
client = poplib.POP3("pop.example.com", timeout=15)
client.stls(context=context)
client.user(USER)
client.pass_(PASSWORD)
STARTTLS must happen before USER and PASS. Reject the connection if the expected capability is missing; never silently downgrade to plaintext.
Credentials
Do not hard-code passwords. Use environment variables, a secret manager, or provider-issued application credentials. Many modern services require OAuth and may reject ordinary POP3 passwords.
Do not run set_debuglevel(2) in production. Protocol debug output can reveal metadata, identifiers, server responses, and authentication details.
Inspecting capabilities
capabilities = client.capa()
for name, parameters in capabilities.items():
print(name, parameters)
Capabilities can advertise STLS, UIDL, TOP, UTF8, and authentication mechanisms. POP3 implementations vary widely. A method existing in Python does not guarantee that a server supports or correctly implements the related command.
Mailbox status
message_count, total_bytes = client.stat()
print(message_count, total_bytes)
stat() returns a count and the server-reported total size. Treat both as estimates and enforce independent limits. The mailbox can change between the status call and retrieval.
Listing messages
response, lines, octets = client.list()
messages = []
for line in lines:
number_text, size_text = line.split(maxsplit=1)
messages.append((int(number_text), int(size_text)))
Validate every line because the remote server can send unexpected data. Skip messages above the application limit and cap the number processed in one run.
Use UIDL to identify messages
POP3 message numbers are temporary positions for the current session. Use uidl() to get server-issued identifiers and avoid processing the same message repeatedly.
response, lines, octets = client.uidl()
uids = {}
for line in lines:
number, uid = line.split(maxsplit=1)
uids[int(number)] = uid.decode("ascii", errors="strict")
Persist completed UIDs in a transactional database or state file. The identifier is server-specific and does not replace the email’s Message-ID; both values can help deduplication.
Retrieving a complete message
response, lines, octets = client.retr(number)
if octets > MESSAGE_LIMIT:
raise ValueError("message is too large")
raw_email = b"\r\n".join(lines) + b"\r\n"
retr() returns lines without the final CRLF sequence. Reconstruct a valid byte stream carefully. The response may already occupy memory, so inspect LIST size first and impose per-run byte budgets.
Parsing with the email package
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"))
Subjects, senders, HTML bodies, and attachment names are untrusted. Escape headers before display and sanitize HTML if rendering is truly required.
TOP for headers and a body preview
top(number, lines) attempts to fetch headers and a number of body lines without setting a seen flag. The official documentation warns that TOP is poorly specified and often broken on non-mainstream servers.
response, lines, octets = client.top(number, 0)
headers = b"\r\n".join(lines) + b"\r\n\r\n"
Test TOP manually against every provider before depending on it. When behavior is inconsistent, retrieve the full bounded message or choose IMAP.
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
data = part.get_payload(decode=True) or b""
total += len(data)
if total > limit:
raise ValueError("content exceeds limit")
yield part.get_content_type(), data
Decode using the declared charset with a safe fallback. The guide to Python codecs covers decoding errors and incremental processing.
Safe attachments
Never use a MIME filename as a direct filesystem path. Generate an internal identifier, enforce per-file and total limits, inspect the actual format, and store outside executable directories.
Use Python tempfile during validation and Python hashlib for integrity and deduplication.
Deletion is committed by QUIT
dele(number) marks a message during the current session. On compliant servers, deletion becomes permanent when quit() completes.
client.dele(number)
# Other checks and persistence
client.quit() # commits changes
This creates an operational risk: code can mark the wrong messages and confirm all marks while closing. Separate download from deletion, confirm UID and domain state, and record a durable decision before calling DELE.
Undoing marks with RSET
client.rset()
rset() clears deletion marks for the session before they are committed. Call it when processing fails after one or more DELE commands.
Unexpected disconnects
Most compliant servers cancel pending deletion if the connection closes without QUIT, but Python’s documentation mentions historical implementations that violate this behavior. Do not treat a disconnect as a guaranteed rollback. The safest design delays DELE until the final stage.
A safer processing pipeline
A robust POP3 workflow can follow these steps:
- Connect using verified TLS.
- List UIDL identifiers and sizes.
- Skip UIDs already completed.
- Download at most N messages and M total bytes.
- Parse and validate content.
- Persist the result and commit local state.
- Only then, optionally mark messages for deletion.
- Call QUIT and record completion.
If local persistence fails, leave the server message untouched.
UTF-8 mode
The utf8() method requests the RFC 6856 mode when the server supports it. Check capabilities and handle error_proto. Even with UTF-8 at the POP3 layer, MIME parts may declare many different charsets.
Keep-alive and deadlines
noop() can keep a session active, but it does not replace a total deadline. Long processing should bound its duration and reconnect safely when needed.
Handling errors
poplib.error_proto represents POP3 protocol replies. Socket and TLS failures may arrive as OSError, TimeoutError, or ssl.SSLError.
try:
client = poplib.POP3_SSL(HOST, context=context, timeout=15)
client.user(USER)
client.pass_(PASSWORD)
except poplib.error_proto as error:
raise RuntimeError("POP3 server rejected the operation") from error
except OSError as error:
raise RuntimeError("POP3 transport failure") from error
Never include the password or full authentication reply in logs.
POP3 versus IMAP
POP3 focuses on downloading from a simple mailbox. IMAP supports folders, server-side search, stable UID tracking with UIDVALIDITY, flags, and synchronization. For workflows that preserve server state or use multiple folders, read Python imaplib.
Recommended tests
Test invalid certificates, missing STARTTLS, rejected login, unsupported UIDL, broken TOP, oversized messages, malformed MIME, dangerous attachments, DELE followed by RSET, failure before QUIT, timeout, empty mailboxes, and deduplication.
Common mistakes
Frequent mistakes include using plaintext POP3 with passwords, disabling TLS verification, trusting TOP, identifying messages only by session number, downloading an entire mailbox, keeping everything in memory, trusting attachment names, marking deletion before persistence, committing DELE automatically during cleanup, and logging sensitive content.
Conclusion
poplib provides direct POP3 access, but the protocol is limited and aging. Use POP3_SSL or STARTTLS with a verified context, identify messages through UIDL, impose strict limits, parse MIME as untrusted data, and make deletion a final explicitly authorized action.
Read the official poplib documentation and POP3 RFC 1939. When possible, prefer IMAP for more predictable synchronization and mailbox control.







