Claude Code for nntplib: Python NNTP Usenet Client — Claude Skills 360 Blog
Blog / AI / Claude Code for nntplib: Python NNTP Usenet Client
AI

Claude Code for nntplib: Python NNTP Usenet Client

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

Python’s nntplib module implements the NNTP (Network News Transfer Protocol) client for reading and posting Usenet newsgroup articles. import nntplib. Connect: s = nntplib.NNTP(host, port=119, user=None, password=None, readermode=True) or nntplib.NNTP_SSL(host, port=563). Context manager: with nntplib.NNTP(host) as s:. List groups: resp, groups = s.list() — returns list of GroupInfo(group, last, first, flag). Select group: resp, count, first, last, name = s.group("comp.lang.python"). Get overviews: resp, overviews = s.over((first, last)) — returns list of (id, {subject, from, date, message-id, references, :bytes, :lines}). Fetch article: resp, info = s.article(id)info.message_id, info.number, info.lines (list of bytes). Headers only: s.head(id). Body only: s.body(id). New articles: resp, ids = s.newnews(group_wildmat, datetime_obj). Post: s.post(io.BytesIO(article_bytes)). Quit: s.quit(). Exceptions: nntplib.NNTPError, NNTPTemporaryError, NNTPPermanentError, NNTPProtocolError, NNTPDataError. Note: deprecated 3.11, removed 3.13 — include compatibility guard. Claude Code generates newsgroup browsers, article archivers, subject readers, and Usenet feed processors.

CLAUDE.md for nntplib

## nntplib Stack
- Stdlib: import nntplib  (deprecated 3.11, removed 3.13 — guard with try/except)
- Connect: s = nntplib.NNTP(host)           # port 119 plaintext
-          s = nntplib.NNTP_SSL(host)       # port 563 TLS
- Groups:  resp, groups = s.list()
- Select:  resp, count, first, last, name = s.group("comp.lang.python")
- Over:    resp, ovs = s.over((first, last))  # subject,from,date,message-id
- Article: resp, info = s.article(msg_id)     # info.lines = [bytes, ...]
- Post:    s.post(io.BytesIO(article_bytes))

nntplib NNTP Usenet Pipeline

# app/nntplibutil.py — connect, browse groups, fetch articles, archive, post
from __future__ import annotations

import email
import email.message
import email.policy
import io
import socket
import ssl
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Iterator

# Guard for Python 3.13+ where nntplib is removed
try:
    import nntplib as _nntplib
    _NNTPLIB_AVAILABLE = True
except ImportError:
    _NNTPLIB_AVAILABLE = False


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

def connect(
    host: str,
    port: int | None = None,
    use_ssl: bool = False,
    user: str | None = None,
    password: str | None = None,
    timeout: int = 30,
    readermode: bool = True,
):
    """
    Connect to an NNTP server. Returns an NNTP connection object.
    Use as a context manager or call .quit() when done.

    Example:
        with connect("news.eternal-september.org") as s:
            _, groups = s.list()
    """
    if not _NNTPLIB_AVAILABLE:
        raise ImportError("nntplib not available (Python 3.13+)")

    kwargs = dict(readermode=readermode)
    if user:
        kwargs["user"] = user
        kwargs["password"] = password

    if use_ssl:
        ctx = ssl.create_default_context()
        return _nntplib.NNTP_SSL(
            host, port=port or 563,
            ssl_context=ctx,
            timeout=timeout,
            **kwargs
        )
    return _nntplib.NNTP(
        host, port=port or 119,
        timeout=timeout,
        **kwargs
    )


# ─────────────────────────────────────────────────────────────────────────────
# 2. Group listing and selection
# ─────────────────────────────────────────────────────────────────────────────

@dataclass
class GroupInfo:
    name:  str
    first: int
    last:  int
    flag:  str     # 'y'=postable, 'n'=read-only, 'm'=moderated

    @property
    def article_count(self) -> int:
        return max(0, self.last - self.first + 1)

    def __str__(self) -> str:
        return (f"{self.name:<40s}  "
                f"first={self.first}  last={self.last}  "
                f"count={self.article_count}  flag={self.flag}")


def list_groups(s, pattern: str | None = None) -> list[GroupInfo]:
    """
    List all newsgroups (optionally filtered by name substring).

    Example:
        with connect(host) as s:
            python_groups = list_groups(s, "python")
            for g in python_groups:
                print(g)
    """
    _, raw_groups = s.list()
    groups = [
        GroupInfo(
            name=g.group,
            first=int(g.first),
            last=int(g.last),
            flag=g.flag,
        )
        for g in raw_groups
    ]
    if pattern:
        groups = [g for g in groups if pattern.lower() in g.name.lower()]
    return sorted(groups, key=lambda g: g.name)


def select_group(s, group_name: str) -> GroupInfo:
    """
    Select a newsgroup, returning updated GroupInfo with current counts.

    Example:
        g = select_group(s, "comp.lang.python")
        print(f"{g.article_count} articles available")
    """
    _, count, first, last, name = s.group(group_name)
    return GroupInfo(name=name, first=int(first), last=int(last), flag="y")


# ─────────────────────────────────────────────────────────────────────────────
# 3. Article overview / headers
# ─────────────────────────────────────────────────────────────────────────────

@dataclass
class ArticleOverview:
    number:      int
    message_id:  str
    subject:     str
    sender:      str
    date:        str
    references:  str
    bytes_:      int
    lines:       int

    def __str__(self) -> str:
        subj = self.subject[:60] + "…" if len(self.subject) > 60 else self.subject
        return f"  [{self.number:>8d}] {subj:<62s}  {self.sender[:30]}"


def get_overviews(
    s,
    group_name: str,
    max_articles: int = 100,
) -> list[ArticleOverview]:
    """
    Fetch the most recent article overviews for a newsgroup.

    Example:
        with connect(host) as s:
            for ov in get_overviews(s, "comp.lang.python", max_articles=20):
                print(ov)
    """
    g = select_group(s, group_name)
    if g.article_count == 0:
        return []

    start = max(g.first, g.last - max_articles + 1)
    _, overviews = s.over((start, g.last))

    results = []
    for num, headers in overviews:
        results.append(ArticleOverview(
            number=num,
            message_id=headers.get("message-id", ""),
            subject=headers.get("subject", "(no subject)"),
            sender=headers.get("from", ""),
            date=headers.get("date", ""),
            references=headers.get("references", ""),
            bytes_=int(headers.get(":bytes", 0)),
            lines=int(headers.get(":lines", 0)),
        ))
    return results


# ─────────────────────────────────────────────────────────────────────────────
# 4. Article fetch and decode
# ─────────────────────────────────────────────────────────────────────────────

@dataclass
class Article:
    number:      int
    message_id:  str
    headers:     dict[str, str]
    body_lines:  list[bytes]

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

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

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

    def body_text(self, encoding: str = "utf-8", errors: str = "replace") -> str:
        """Return the article body as a decoded string."""
        return b"\n".join(self.body_lines).decode(encoding, errors=errors)

    def as_email_message(self) -> email.message.Message:
        """Parse the article as an email.message.Message object."""
        raw = b"\n".join(self.body_lines)
        # Reconstruct headers + body
        hdr_lines = []
        for k, v in self.headers.items():
            hdr_lines.append(f"{k}: {v}".encode())
        full = b"\n".join(hdr_lines) + b"\n\n" + raw
        return email.message_from_bytes(full, policy=email.policy.default)


def fetch_article(s, article_id: str | int) -> Article:
    """
    Fetch a complete article (headers + body).

    Example:
        art = fetch_article(s, "<msgid@server>")
        print(art.subject)
        print(art.body_text())
    """
    _, info = s.article(str(article_id))
    raw_lines = info.lines

    # Split headers from body at blank line
    sep_idx = 0
    for i, line in enumerate(raw_lines):
        if line == b"":
            sep_idx = i
            break

    header_bytes = b"\n".join(raw_lines[:sep_idx])
    body_lines   = raw_lines[sep_idx + 1:]

    msg = email.message_from_bytes(header_bytes + b"\n\n",
                                   policy=email.policy.compat32)
    headers = {k.lower(): v for k, v in msg.items()}

    return Article(
        number=info.number,
        message_id=info.message_id,
        headers=headers,
        body_lines=body_lines,
    )


def fetch_headers_only(s, article_id: str | int) -> dict[str, str]:
    """
    Fetch only the headers of an article (no body download).

    Example:
        hdrs = fetch_headers_only(s, 12345)
        print(hdrs.get("subject"))
    """
    _, info = s.head(str(article_id))
    raw = b"\n".join(info.lines)
    msg = email.message_from_bytes(raw + b"\n\n",
                                   policy=email.policy.compat32)
    return {k.lower(): v for k, v in msg.items()}


# ─────────────────────────────────────────────────────────────────────────────
# 5. Archive and post
# ─────────────────────────────────────────────────────────────────────────────

def archive_group(
    s,
    group_name: str,
    max_articles: int = 50,
) -> list[Article]:
    """
    Fetch the most recent articles from a newsgroup and return them.

    Example:
        arts = archive_group(s, "comp.lang.python", max_articles=10)
        for art in arts:
            print(art.subject)
    """
    overviews = get_overviews(s, group_name, max_articles=max_articles)
    articles = []
    for ov in overviews:
        try:
            art = fetch_article(s, ov.message_id)
            articles.append(art)
        except Exception:
            pass
    return articles


def build_article(
    subject: str,
    body: str,
    newsgroups: str,
    sender: str,
    encoding: str = "utf-8",
) -> bytes:
    """
    Build an RFC 1036 news article bytes ready for posting.

    Example:
        art = build_article(
            subject="Re: Python 4.0 wishlist",
            body="I'd love walrus operators everywhere.\n",
            newsgroups="comp.lang.python",
            sender="Alice <[email protected]>",
        )
        with connect(host) as s:
            s.post(io.BytesIO(art))
    """
    now = datetime.now(timezone.utc).strftime("%a, %d %b %Y %H:%M:%S +0000")
    header = (
        f"From: {sender}\r\n"
        f"Newsgroups: {newsgroups}\r\n"
        f"Subject: {subject}\r\n"
        f"Date: {now}\r\n"
        f"MIME-Version: 1.0\r\n"
        f"Content-Type: text/plain; charset={encoding}\r\n"
        f"Content-Transfer-Encoding: 8bit\r\n"
        f"\r\n"
    )
    return header.encode(encoding) + body.encode(encoding)


# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────

if __name__ == "__main__":
    import sys
    print("=== nntplib demo ===")

    if not _NNTPLIB_AVAILABLE:
        print("  nntplib not available (Python 3.13+)")
        print("  Demonstrating build_article only:")
        art = build_article(
            subject="Test article",
            body="Hello Usenet.\n",
            newsgroups="test.test",
            sender="Demo <[email protected]>",
        )
        print(f"  article bytes ({len(art)}):\n  {art.decode()[:200]}")
        raise SystemExit(0)

    # Connection test using demo.eternal-september.org (public NNTP test server)
    HOST = "demo.eternal-september.org"
    print(f"\n  Attempting to connect to {HOST}:119 ...")
    print("  (If no network, skipping to article builder demo)")

    try:
        with connect(HOST, timeout=10) as s:
            # ── list groups matching "eternal" ─────────────────────────────────
            print("\n--- list_groups('eternal') ---")
            groups = list_groups(s, "eternal")
            for g in groups[:5]:
                print(f"  {g}")
            if len(groups) > 5:
                print(f"  ... and {len(groups) - 5} more")

            # ── overviews ─────────────────────────────────────────────────────
            if groups:
                gn = groups[0].name
                print(f"\n--- get_overviews({gn!r}, max=5) ---")
                overviews = get_overviews(s, gn, max_articles=5)
                for ov in overviews:
                    print(ov)

    except (socket.timeout, socket.gaierror, OSError) as e:
        print(f"  Network unavailable: {e}")

    # ── build_article demo (always runs) ──────────────────────────────────────
    print("\n--- build_article ---")
    art = build_article(
        subject="Re: Python 4.0 wishlist",
        body="I'd love pattern matching extensions.\r\n",
        newsgroups="comp.lang.python",
        sender="Demo User <[email protected]>",
    )
    print(f"  article ({len(art)} bytes):\n")
    print("  " + art.decode().replace("\r\n", "\n  "))

    print("\n=== done ===")

For the requests + news HTTP API alternative — many Usenet providers expose a REST/HTTP interface, and services like Giganews or Supernews provide web dashboards; using requests.get() to fetch articles via HTTPS is more firewall-friendly than raw NNTP — use HTTP APIs when your hosting environment blocks outbound TCP/119 or TCP/563; use nntplib when you need direct binary NNTP protocol access, bulk header downloads via the OVER command, or posting articles programmatically without a REST intermediary. For the email alternative — email.message_from_bytes() and email.header.decode_header() parse the RFC 2822 message format that NNTP articles use — nntplib handles the NNTP wire protocol while email handles the message payload; use them together: nntplib to retrieve info.lines, then email.message_from_bytes() for structured header access, rich MIME body extraction, and encoded-word decoding. Note that both nntplib and the article format utilities are deprecated in Python 3.11 and removed in 3.13; the Article.as_email_message() method above bridges to the stable email module which remains available. The Claude Skills 360 bundle includes nntplib skill sets covering connect() plain/SSL connection helper, GroupInfo with list_groups()/select_group(), ArticleOverview with get_overviews(), Article with fetch_article()/fetch_headers_only()/body_text()/as_email_message(), archive_group() archiver, and build_article() posting helper. Start with the free tier to try NNTP patterns and nntplib 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