Claude Code for poplib: Python POP3 Email Retrieval — Claude Skills 360 Blog
Blog / AI / Claude Code for poplib: Python POP3 Email Retrieval
AI

Claude Code for poplib: Python POP3 Email Retrieval

Published: November 15, 2028
Read time: 5 min read
By: Claude Skills 360

Python’s poplib module implements the POP3 protocol for retrieving email from a mail server. import poplib. Connect: poplib.POP3(host, port=110, timeout=60) — cleartext; poplib.POP3_SSL(host, port=995, keyfile=None, certfile=None, context=None, timeout=60) — TLS. Auth: conn.user("alice") then conn.pass_("secret") — two-step; conn.apop("alice", "secret") — APOP challenge-response (server must support). Mailbox: conn.stat()(msg_count, total_octets); conn.list(which=None)(response, [b"1 3456", b"2 789", ...], octets) — list all or one message with sizes. Retrieve: conn.retr(which)(response, [line_bytes, ...], octets) — full message; conn.top(which, maxlines) → same format, headers + N body lines only. UIDs: conn.uidl(which=None)(response, [b"1 uid", ...], octets) — unique identifiers stable across sessions. Delete: conn.dele(which) — marks message; conn.rset() — cancels all pending deletes; conn.quit() — commits deletes and closes. Keepalive: conn.noop(). Use as context manager with with poplib.POP3_SSL(host) as conn:. Claude Code generates mail downloaders, duplicate detectors, inbox monitors, and message archive builders.

CLAUDE.md for poplib

## poplib Stack
- Stdlib: import poplib, email
- Connect: conn = poplib.POP3_SSL("mail.example.com")
-           conn.user("alice"); conn.pass_("s3cr3t")
- Stat:    count, size = conn.stat()
- List:    resp, msgs, total = conn.list()
- Retr:    resp, lines, octets = conn.retr(msg_num)
-           msg = email.message_from_bytes(b"\n".join(lines))
- UID:     resp, uids, _ = conn.uidl()
- Quit:    conn.quit()   # commits deletes

poplib POP3 Email Retrieval Pipeline

# app/poplibutil.py — connect, download, parse, deduplicate, archive
from __future__ import annotations

import email
import email.message
import email.policy
import io
import poplib
import ssl
from dataclasses import dataclass, field
from pathlib import Path


# ─────────────────────────────────────────────────────────────────────────────
# 1. Connection helpers
# ─────────────────────────────────────────────────────────────────────────────

def connect(
    host: str,
    port: int = 995,
    username: str = "",
    password: str = "",
    use_ssl: bool = True,
    timeout: int = 30,
) -> poplib.POP3 | poplib.POP3_SSL:
    """
    Open and authenticate a POP3 connection.
    Returns the connected POP3 object.

    Example:
        conn = connect("mail.example.com", username="alice", password="s3cr3t")
        count, _ = conn.stat()
        print(f"{count} messages")
        conn.quit()
    """
    if use_ssl:
        ctx = ssl.create_default_context()
        conn = poplib.POP3_SSL(host, port=port, context=ctx, timeout=timeout)
    else:
        conn = poplib.POP3(host, port=port, timeout=timeout)
    if username:
        conn.user(username)
    if password:
        conn.pass_(password)
    return conn


def mailbox_stat(conn: poplib.POP3) -> tuple[int, int]:
    """
    Return (message_count, total_bytes) for the mailbox.

    Example:
        count, size = mailbox_stat(conn)
        print(f"{count} messages, {size/1024:.1f} KB total")
    """
    return conn.stat()


# ─────────────────────────────────────────────────────────────────────────────
# 2. Message listing and UID tracking
# ─────────────────────────────────────────────────────────────────────────────

@dataclass
class MessageEntry:
    number: int       # 1-based POP3 message number (session-local)
    size:   int       # bytes
    uid:    str       # UIDL unique identifier (persistent across sessions)

    def __str__(self) -> str:
        return f"#{self.number:4d}  uid={self.uid:<30s}  {self.size:>8d}B"


def list_messages(conn: poplib.POP3) -> list[MessageEntry]:
    """
    Return a list of MessageEntry objects for all messages in the mailbox.
    Merges LIST (sizes) and UIDL (uids) responses.

    Example:
        entries = list_messages(conn)
        for e in entries:
            print(e)
    """
    _, list_lines, _ = conn.list()
    _, uid_lines, _ = conn.uidl()

    sizes: dict[int, int] = {}
    for line in list_lines:
        parts = line.decode().split()
        if len(parts) >= 2:
            sizes[int(parts[0])] = int(parts[1])

    uids: dict[int, str] = {}
    for line in uid_lines:
        parts = line.decode().split()
        if len(parts) >= 2:
            uids[int(parts[0])] = parts[1]

    entries = []
    for num in sorted(sizes):
        entries.append(MessageEntry(
            number=num,
            size=sizes[num],
            uid=uids.get(num, ""),
        ))
    return entries


# ─────────────────────────────────────────────────────────────────────────────
# 3. Message retrieval and parsing
# ─────────────────────────────────────────────────────────────────────────────

@dataclass
class ParsedMessage:
    number:   int
    uid:      str
    raw:      bytes
    message:  email.message.EmailMessage

    @property
    def subject(self) -> str:
        return self.message.get("Subject", "(no subject)")

    @property
    def sender(self) -> str:
        return self.message.get("From", "")

    @property
    def date(self) -> str:
        return self.message.get("Date", "")

    @property
    def message_id(self) -> str:
        return self.message.get("Message-ID", "")

    def body_text(self) -> str:
        """Return the plain-text body, or empty string if none."""
        if self.message.is_multipart():
            for part in self.message.walk():
                if part.get_content_type() == "text/plain":
                    payload = part.get_payload(decode=True)
                    charset = part.get_content_charset() or "utf-8"
                    return payload.decode(charset, errors="replace")
        else:
            if self.message.get_content_type() == "text/plain":
                payload = self.message.get_payload(decode=True)
                charset = self.message.get_content_charset() or "utf-8"
                return payload.decode(charset, errors="replace")
        return ""

    def __str__(self) -> str:
        return (f"#{self.number}  From: {self.sender}  "
                f"Subject: {self.subject}  Date: {self.date}")


def fetch_message(conn: poplib.POP3, number: int, uid: str = "") -> ParsedMessage:
    """
    Retrieve and parse a POP3 message by its session number.

    Example:
        msg = fetch_message(conn, 1)
        print(msg.subject, msg.sender)
        print(msg.body_text()[:200])
    """
    _, lines, _ = conn.retr(number)
    raw = b"\r\n".join(lines)
    message = email.message_from_bytes(raw, policy=email.policy.default)
    return ParsedMessage(number=number, uid=uid, raw=raw, message=message)  # type: ignore[arg-type]


def fetch_headers(conn: poplib.POP3, number: int, uid: str = "") -> ParsedMessage:
    """
    Retrieve only headers + 0 body lines (fast peek without full download).

    Example:
        stub = fetch_headers(conn, 1)
        print(stub.subject, stub.sender)
    """
    _, lines, _ = conn.top(number, 0)
    raw = b"\r\n".join(lines)
    message = email.message_from_bytes(raw, policy=email.policy.default)
    return ParsedMessage(number=number, uid=uid, raw=raw, message=message)  # type: ignore[arg-type]


# ─────────────────────────────────────────────────────────────────────────────
# 4. Download session with deduplication
# ─────────────────────────────────────────────────────────────────────────────

def download_new(
    conn: poplib.POP3,
    seen_uids: set[str],
    headers_only: bool = False,
    max_messages: int | None = None,
) -> tuple[list[ParsedMessage], set[str]]:
    """
    Download messages whose UIDs are not in seen_uids.
    Returns (new_messages, updated_seen_uids).
    Pass headers_only=True for fast subject/sender preview without body download.

    Example:
        seen: set[str] = set()
        msgs, seen = download_new(conn, seen)
        for m in msgs:
            print(m)
    """
    entries = list_messages(conn)
    new_messages = []
    new_seen = set(seen_uids)

    for entry in entries:
        if entry.uid in seen_uids:
            continue
        if max_messages is not None and len(new_messages) >= max_messages:
            break
        if headers_only:
            msg = fetch_headers(conn, entry.number, entry.uid)
        else:
            msg = fetch_message(conn, entry.number, entry.uid)
        new_messages.append(msg)
        new_seen.add(entry.uid)

    return new_messages, new_seen


def archive_messages(
    conn: poplib.POP3,
    archive_dir: str | Path,
    seen_uids: set[str] | None = None,
    delete_after_save: bool = False,
) -> tuple[list[Path], set[str]]:
    """
    Save new messages as .eml files in archive_dir.
    Returns (saved_paths, updated_seen_uids).

    Example:
        paths, seen = archive_messages(conn, "/var/mail/archive")
        for p in paths:
            print(f"  saved {p.name}")
    """
    dest = Path(archive_dir)
    dest.mkdir(parents=True, exist_ok=True)
    seen = set(seen_uids or [])

    entries = list_messages(conn)
    saved: list[Path] = []
    to_delete: list[int] = []

    for entry in entries:
        if entry.uid in seen:
            continue
        msg = fetch_message(conn, entry.number, entry.uid)
        safe_uid = "".join(c if c.isalnum() or c in "-_." else "_"
                           for c in entry.uid)
        path = dest / f"{safe_uid}.eml"
        path.write_bytes(msg.raw)
        saved.append(path)
        seen.add(entry.uid)
        if delete_after_save:
            to_delete.append(entry.number)

    for num in to_delete:
        conn.dele(num)

    return saved, seen


# ─────────────────────────────────────────────────────────────────────────────
# Demo (requires a real POP3 server — shows offline simulation)
# ─────────────────────────────────────────────────────────────────────────────

if __name__ == "__main__":
    print("=== poplib demo (offline simulation) ===")

    # Simulate a raw POP3 message
    raw_eml = b"""\
From: [email protected]\r
To: [email protected]\r
Subject: Hello from poplib\r
Date: Mon, 11 Nov 2028 10:00:00 +0000\r
Message-ID: <[email protected]>\r
Content-Type: text/plain; charset=utf-8\r
\r
Hi Alice,\r
\r
This is a test message fetched via POP3.\r
"""
    # Parse as if fetched from server
    import email.policy
    message = email.message_from_bytes(raw_eml, policy=email.policy.default)
    pm = ParsedMessage(number=1, uid="uid-001", raw=raw_eml, message=message)  # type: ignore[arg-type]

    print(f"\n--- ParsedMessage ---")
    print(f"  {pm}")
    print(f"  body: {pm.body_text()[:60]!r}")

    # ── Simulate mailbox stat ─────────────────────────────────────────────────
    print("\n--- Simulated deduplication ---")
    entries = [
        MessageEntry(1, 2048, "uid-001"),
        MessageEntry(2, 4096, "uid-002"),
        MessageEntry(3, 1024, "uid-003"),
    ]
    seen: set[str] = {"uid-001"}   # already downloaded
    new = [e for e in entries if e.uid not in seen]
    print(f"  {len(entries)} messages on server, {len(new)} new")
    for e in new:
        print(f"  would download: {e}")

    # ── Archive paths ─────────────────────────────────────────────────────────
    print("\n--- Archive naming ---")
    import tempfile
    with tempfile.TemporaryDirectory() as tmp:
        dest = Path(tmp) / "archive"
        dest.mkdir()
        for e in entries:
            safe = "".join(c if c.isalnum() or c in "-_." else "_"
                           for c in e.uid)
            path = dest / f"{safe}.eml"
            path.write_bytes(raw_eml)
            print(f"  saved {path.name}  ({e.size}B)")

    print("\nTo connect to a real server:")
    print("  conn = connect('mail.example.com', username='u', password='p')")
    print("  msgs, seen = download_new(conn, seen_uids=set())")
    print("  conn.quit()")
    print("\n=== done ===")

For the imaplib alternative — imaplib.IMAP4_SSL(host) plus conn.select("INBOX"), conn.search(None, "UNSEEN"), and conn.fetch(uid, "(RFC822)") provide full IMAP4 capabilities including server-side search, folder management, flag manipulation, partial message fetching, and persistent connections — use IMAP when you need to leave messages on the server, work with multiple folders, or sync mail across devices; use poplib only for simple download-and-delete workflows or legacy POP3-only servers (many modern email services have removed POP3 support in favor of IMAP). For the smtplib alternative — smtplib.SMTP_SSL(host, 465) with conn.sendmail(sender, recipients, message.as_bytes()) sends email — use smtplib for sending and poplib for receiving; the two modules are complementary and both support SSL/TLS with the standard ssl.create_default_context(). The Claude Skills 360 bundle includes poplib skill sets covering connect() with SSL and auth, MessageEntry with list_messages() mailbox lister, ParsedMessage with fetch_message()/fetch_headers() retrievers and body_text(), and download_new()/archive_messages() session managers with UID deduplication. Start with the free tier to try POP3 email patterns and poplib pipeline code generation.

Keep Reading

AI

Claude Code for email.contentmanager: Python Email Content Accessors

Read and write EmailMessage body content with Python's email.contentmanager module and Claude Code — email contentmanager ContentManager for the class that maps content types to get and set handler functions allowing EmailMessage to support get_content and set_content with type-specific behaviour, email contentmanager raw_data_manager for the ContentManager instance that handles raw bytes and str payloads without any conversion, email contentmanager content_manager for the standard ContentManager instance used by email.policy.default that intelligently handles text plain text html multipart and binary content types, email contentmanager get_content_text for the handler that returns the decoded text payload of a text-star message part as a str, email contentmanager get_content_binary for the handler that returns the raw decoded bytes payload of a non-text message part, email contentmanager get_data_manager for the get-handler lookup used by EmailMessage get_content to find the right reader function for the content type, email contentmanager set_content text for the handler that creates and sets a text part correctly choosing charset and transfer encoding, email contentmanager set_content bytes for the handler that creates and sets a binary part with base64 encoding and optional filename Content-Disposition, email contentmanager EmailMessage get_content for the method that reads the message body using the registered content manager handlers, email contentmanager EmailMessage set_content for the method that sets the message body and MIME headers in one call, email contentmanager EmailMessage make_alternative make_mixed make_related for the methods that convert a simple message into a multipart container, email contentmanager EmailMessage add_attachment for the method that attaches a file or bytes to a multipart message, and email contentmanager integration with email.message and email.policy and email.mime and io for building high-level email readers attachment extractors text body accessors HTML readers and policy-aware MIME construction pipelines.

5 min read Feb 12, 2029
AI

Claude Code for email.charset: Python Email Charset Encoding

Control header and body encoding for international email with Python's email.charset module and Claude Code — email charset Charset for the class that wraps a character set name with the encoding rules for header encoding and body encoding describing how to encode text for that charset in email messages, email charset Charset header_encoding for the attribute specifying whether headers using this charset should use QP quoted-printable encoding BASE64 encoding or no encoding, email charset Charset body_encoding for the attribute specifying the Content-Transfer-Encoding to use for message bodies in this charset such as QP or BASE64, email charset Charset output_codec for the attribute giving the Python codec name used to encode the string to bytes for the wire format, email charset Charset input_codec for the attribute giving the Python codec name used to decode incoming bytes to str, email charset Charset get_output_charset for returning the output charset name, email charset Charset header_encode for encoding a header string using the charset's header_encoding method, email charset Charset body_encode for encoding body content using the charset's body_encoding, email charset Charset convert for converting a string from the input_codec to the output_codec, email charset add_charset for registering a new charset with custom encoding rules in the global charset registry, email charset add_alias for adding an alias name that maps to an existing registered charset, email charset add_codec for registering a codec name mapping for use by the charset machinery, and email charset integration with email.message and email.mime and email.policy and email.encoders for building international email senders non-ASCII header encoders Content-Transfer-Encoding selectors charset-aware message constructors and MIME encoding pipelines.

5 min read Feb 11, 2029
AI

Claude Code for email.utils: Python Email Address and Header Utilities

Parse and format RFC 2822 email addresses and dates with Python's email.utils module and Claude Code — email utils parseaddr for splitting a display-name plus angle-bracket address string into a realname and email address tuple, email utils formataddr for combining a realname and address string into a properly quoted RFC 2822 address with angle brackets, email utils getaddresses for parsing a list of raw address header strings each potentially containing multiple comma-separated addresses into a list of realname address tuples, email utils parsedate for parsing an RFC 2822 date string into a nine-tuple compatible with time.mktime, email utils parsedate_tz for parsing an RFC 2822 date string into a ten-tuple that includes the UTC offset timezone in seconds, email utils parsedate_to_datetime for parsing an RFC 2822 date string into an aware datetime object with timezone, email utils formatdate for formatting a POSIX timestamp or the current time as an RFC 2822 date string with optional usegmt and localtime flags, email utils format_datetime for formatting a datetime object as an RFC 2822 date string, email utils make_msgid for generating a globally unique Message-ID string with optional idstring and domain components, email utils decode_rfc2231 for decoding an RFC 2231 encoded parameter value into a tuple of charset language and value, email utils encode_rfc2231 for encoding a string as an RFC 2231 encoded parameter value, email utils collapse_rfc2231_value for collapsing a decoded RFC 2231 tuple to a Unicode string, and email utils integration with email.message and email.headerregistry and datetime and time for building address parsers date formatters message-id generators header extractors and RFC-compliant email construction utilities.

5 min read Feb 10, 2029

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free