poplib Module Complexity¶
The poplib module is a synchronous POP3 client. Each command method sends its command and reads
the server's whole response before it returns; nothing is pipelined and nothing streams. A
command that returns lines - list(), uidl(), retr(), top() - builds the list in memory,
one bytes object per line, so retr() holds the whole message when it returns.
n is the bytes of the server's multi-line response to one call: one line per message for
list() and uidl(), the message for retr(), the headers and the requested body lines for
top(), the capability list for capa() and stls(). Every other response is a single line, and
a line longer than 2,048 bytes, counting its line ending, raises error_proto, so a single-line
reply is O(1). User names, passwords and message numbers are priced at O(1). The bounds price the
client's CPU work and memory; waiting for the server, connection setup, the TLS handshake and the
server's own work are outside every bound, and the Notes count the exchanges instead.
Complexity Reference¶
POP3¶
| Operation | Time | Space | Notes |
|---|---|---|---|
poplib.POP3(host, port=POP3_PORT[, timeout]) |
O(1) | O(1) | Connects and reads the greeting: one exchange, no command. Port 110 by default; timeout=0 raises ValueError. Not a context manager |
POP3.getwelcome() |
O(1) | O(1) | The greeting read at connection; no exchange |
POP3.set_debuglevel(level) |
O(1) | O(1) | 1 prints each command to standard output; 2 also prints every line read, a whole message for retr() |
POP3.quit() |
O(1) | O(1) | One exchange, then closes. The server deletes the marked messages and unlocks the mailbox |
POP3_SSL¶
| Operation | Time | Space | Notes |
|---|---|---|---|
poplib.POP3_SSL(host, port=POP3_SSL_PORT, *, timeout=..., context=None) |
O(1) | O(1) | A POP3 that speaks TLS from the first byte, on port 995 by default. Without context, the server's certificate is not verified |
Authentication and TLS¶
| Operation | Time | Space | Notes |
|---|---|---|---|
POP3.user(username), POP3.pass_(password) |
O(1) | O(1) | One exchange each. The server locks the mailbox from pass_() until quit() |
POP3.apop(user, secret) |
O(1) | O(1) | One exchange, sending an MD5 digest of the greeting's timestamp and secret instead of the password. Raises error_proto without sending anything when the greeting has no timestamp |
POP3.rpop(user) |
O(1) | O(1) | One exchange |
POP3.capa() |
O(n) | O(n) | One exchange; returns {name: [argument, ...]}. Raises error_proto when the server does not support CAPA |
POP3.stls(context=None) |
O(n) | O(n) | Calls capa(), then sends STLS: two exchanges, then the TLS handshake. Raises error_proto without sending STLS when the server does not advertise it, and at once on POP3_SSL or a second call. Without context, the certificate is not verified |
POP3.utf8() |
O(1) | O(1) | One exchange |
Mailbox commands¶
| Operation | Time | Space | Notes |
|---|---|---|---|
POP3.stat() |
O(1) | O(1) | One exchange; returns (message count, mailbox size) |
POP3.list(which=None) |
O(n) | O(n) | One exchange. Without which, one line per message; with it, one line |
POP3.uidl(which=None) |
O(n) | O(n) | As list(), with each message's unique id in place of its size |
POP3.retr(which) |
O(n) | O(n) | One exchange; returns (response, lines, octets) with the whole message as a list of lines. A message line over 2,048 bytes raises error_proto |
POP3.top(which, howmuch) |
O(n) | O(n) | One exchange; the headers and the first howmuch body lines, returned as retr() returns a message. Optional for servers |
POP3.dele(which) |
O(1) | O(1) | One exchange; the server marks the message, and deletes it at quit() |
POP3.rset() |
O(1) | O(1) | One exchange; the server clears every deletion mark |
POP3.noop() |
O(1) | O(1) | One exchange |
Exceptions¶
| Operation | Time | Space | Notes |
|---|---|---|---|
poplib.error_proto |
O(1) | O(1) | Raised for an -ERR reply, an over-long line, or a connection that closes while a reply is awaited. Subclasses Exception, not OSError: socket errors pass through as they are |
Reading a Mailbox¶
A Session¶
Connecting reads the greeting and nothing else, and each command after it waits for its reply.
quit() is what ends the session: POP3 is not a context manager, so close it in a finally.
import poplib
import ssl
context = ssl.create_default_context() # verifies the server; the default does not
pop = poplib.POP3_SSL('pop.example.com', context=context) # one exchange: the greeting
try:
pop.user('user@example.com') # O(1), one exchange
pop.pass_('app-password') # O(1), the mailbox is locked from here
count, size = pop.stat() # O(1)
assert count == 3
response, lines, octets = pop.retr(1) # O(n): the whole message, as lines
assert response.startswith(b'+OK')
assert lines[1] == b'Subject: Hello'
finally:
pop.quit() # O(1), unlocks the mailbox
Counting Messages¶
stat() asks for the count and gets one line back. list() gets one line per message, so
counting with it costs O(n), growing with the number of messages in the mailbox.
import poplib
pop = poplib.POP3('pop.example.com')
pop.user('user@example.com')
pop.pass_('app-password')
count, size = pop.stat() # O(1): one line back
response, listings, octets = pop.list() # O(n): one line per message
assert count == len(listings) == 3
assert listings[0] == b'1 57' # message number and size in octets
assert pop.list(2).startswith(b'+OK 2 ') # one message, one line: O(1)
pop.quit()
Retrieved Lines¶
retr() and top() return the message as a list of lines with their line endings removed and
any leading dot the server doubled taken off again; octets counts what the lines held with their
line endings. Joining the lines with b'\r\n' and ending with one more gives the message back
for the email parser.
import poplib
from email.parser import BytesParser
pop = poplib.POP3('pop.example.com')
pop.user('user@example.com')
pop.pass_('app-password')
response, lines, octets = pop.retr(2) # O(n)
assert b'.hidden dot line' in lines # sent as '..hidden dot line'
raw = b'\r\n'.join(lines) + b'\r\n'
assert len(raw) == octets
message = BytesParser().parsebytes(raw)
assert message['Subject'] == 'Report'
assert message.get_payload() == '.hidden dot line\r\nThe numbers are in.\r\n'
pop.quit()
Headers Without Bodies¶
top(which, 0) returns a message's headers alone, so a scan of what is in the mailbox reads each
message's headers instead of its body. Unlike retr(), it does not mark the message
seen. TOP is optional in POP3, and not every server implements it faithfully.
import poplib
pop = poplib.POP3('pop.example.com')
pop.user('user@example.com')
pop.pass_('app-password')
subjects = []
for number in range(1, pop.stat()[0] + 1):
response, lines, octets = pop.top(number, 0) # O(n): headers only
subjects += [line[9:] for line in lines if line.startswith(b'Subject: ')]
assert subjects == [b'Hello', b'Report', b'Lunch']
pop.quit()
Deleting Messages¶
dele() only marks a message. The server deletes the marked messages when quit() ends the
session, and rset() clears the marks before then. A connection that ends without QUIT should
delete nothing, but POP3 servers vary.
import poplib
pop = poplib.POP3('pop.example.com')
pop.user('user@example.com')
pop.pass_('app-password')
pop.dele(1) # O(1): marked, not yet deleted
assert pop.stat()[0] == 2 # marked messages are hidden from the session
pop.rset() # O(1): every mark cleared
assert pop.stat()[0] == 3
try:
pop.retr(99)
except poplib.error_proto as error:
assert error.args[0].startswith(b'-ERR')
else:
raise AssertionError('a missing message was retrieved')
pop.quit()
Upgrading with STLS¶
stls() calls capa() first and refuses to go on if STLS is not advertised, so it is two
exchanges before the handshake. Upgrade before user() and pass_(), or the password crosses the
network in clear text.
import poplib
import ssl
pop = poplib.POP3('pop.example.com') # port 110, clear text
assert 'STLS' in pop.capa() # O(n), one exchange
pop.stls(context=ssl.create_default_context()) # CAPA, STLS and the handshake
pop.user('user@example.com')
pop.pass_('app-password')
try:
pop.stls()
except poplib.error_proto as error:
assert 'already established' in str(error)
else:
raise AssertionError('a second STLS was sent')
pop.quit()
Common Patterns¶
Downloading Only New Messages¶
A message's number can change from one session to the next; its UIDL id does not. One uidl()
call lists every id, so a client that remembers which ids it has seen retrieves only the rest.
import poplib
seen = {b'uid-1'}
pop = poplib.POP3('pop.example.com')
pop.user('user@example.com')
pop.pass_('app-password')
response, listings, octets = pop.uidl() # O(n): one line per message
fresh = []
for listing in listings:
number, uid = listing.split()
if uid not in seen:
fresh.append(pop.retr(int(number))[1]) # O(n) for each new message
seen.add(uid)
assert len(fresh) == 2
assert seen == {b'uid-1', b'uid-2', b'uid-3'}
pop.quit()
Performance Best Practices¶
✅ Do:
- Count messages with
stat(): one line back, wherelist()returns one per message - Scan headers with
top(which, 0)where the server supports it, rather thanretr()on every message - Track
uidl()ids between sessions and retrieve only the messages you have not seen - Call
quit()in afinally: without it the server applies no deletions, and keeps the mailbox locked until it notices the connection is gone - Pass a
contextfromssl.create_default_context()toPOP3_SSLandstls()
❌ Avoid:
retr()on a message too large to hold in memory: it returns the whole message, and has no streaming formset_debuglevel(2)aroundretr(): it prints every line of every message- Sending the password over plain
POP3withoutstls()
Version Notes¶
- Python 3.12+:
POP3_SSLno longer acceptskeyfileandcertfile, and takestimeoutby keyword only; pass anssl.SSLContextascontext - All Python 3: Without
context,POP3_SSLandstls()do not verify the server's certificate or host name