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.