No description
  • Zig 92.3%
  • Nix 7.7%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Jeffrey C. Ollie f24bb382c2
All checks were successful
test / test (push) Successful in 2m31s
Reference RFC 4642 alongside the standards already listed
STARTTLS support went in without being recorded anywhere a reader looks for
what this library implements. It is now in the README's standards table and in
root.zig's list, and the doc comments for Client.startTls, Connection.startTls
and response code 580 carry section links the way the rest of the code already
did.

Also states in the table that RFC 8143 prefers the dedicated TLS port over
STARTTLS, since the library implements both and the choice between them is the
first thing a caller has to make.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkxDixvQUrBMuaMQPvQNMF
2026-08-30 22:08:03 -05:00
.forgejo/workflows Add a NixOS VM integration test for Connection 2026-08-30 20:44:25 -05:00
LICENSES Initial commit: NNTP client library for Zig 0.16 2026-08-30 11:44:52 -05:00
src Reference RFC 4642 alongside the standards already listed 2026-08-30 22:08:03 -05:00
test Add STARTTLS support 2026-08-30 22:03:04 -05:00
.gitignore Initial commit: NNTP client library for Zig 0.16 2026-08-30 11:44:52 -05:00
build.zig Add a NixOS VM integration test for Connection 2026-08-30 20:44:25 -05:00
build.zig.zon Initial commit: NNTP client library for Zig 0.16 2026-08-30 11:44:52 -05:00
flake.lock Run the integration test against INN instead of public-inbox 2026-08-30 21:50:56 -05:00
flake.nix Run the integration test against INN instead of public-inbox 2026-08-30 21:50:56 -05:00
README.md Reference RFC 4642 alongside the standards already listed 2026-08-30 22:08:03 -05:00
REUSE.toml Initial commit: NNTP client library for Zig 0.16 2026-08-30 11:44:52 -05:00

znntp

An NNTP client library for Zig (0.16).

Supported standards

Standard Title Support in znntp
RFC 3977 Network News Transfer Protocol (NNTP) Core protocol: session administration, group and article selection, retrieval, posting, LIST, OVER/HDR, and pipelining
RFC 4643 NNTP Extension for Authentication AUTHINFO USER/PASS
RFC 8054 NNTP Extension for Compression COMPRESS DEFLATE, both directions; interoperates with sync-flush and partial-flush servers
RFC 2980 Common NNTP Extensions XOVER, for servers that predate RFC 3977
RFC 4642 NNTP Extension for TLS STARTTLS, upgrading an established plaintext connection in place
RFC 8143 Using TLS with NNTP Implicit TLS (NNTPS on port 563) with system-root certificate verification, which the RFC prefers over STARTTLS
RFC 1951 DEFLATE Compressed Data Format Compression via std.compress.flate; decompression via a custom resumable inflate suited to interactive streams

Design

  • znntp.Client implements the protocol over any std.Io.Reader/std.Io.Writer pair. Transport is the caller's business — plain TCP, TLS, a proxy, or fixed buffers in tests.
  • znntp.Connection is the batteries-included transport: TCP or TLS (NNTPS, port 563) with system-root certificate verification, built on std.Io.
  • Zero-copy reads: response messages and multiline data lines borrow from the reader's buffer and are invalidated by the next read. Copy what you keep, or use the *Alloc variants.
  • Dot-stuffing/unstuffing and CRLF handling are dealt with in both directions; command arguments are checked against CRLF injection.

Usage

const std = @import("std");
const znntp = @import("znntp");

pub fn main() !void {
    var gpa_state: std.heap.DebugAllocator(.{}) = .init;
    defer _ = gpa_state.deinit();
    const gpa = gpa_state.allocator();

    var threaded: std.Io.Threaded = .init(gpa, .{});
    defer threaded.deinit();
    const io = threaded.io();

    const conn = try znntp.Connection.connect(gpa, io, "news.example.com", .{
        .tls = .on, // NNTPS on port 563; use .off for plain NNTP on 119
    });
    defer conn.destroy();
    var client = conn.client();

    const greeting = try client.readGreeting();
    std.debug.print("{s}\n", .{greeting.message});

    // try client.authenticate("user", "password");

    // Compress the rest of the session (COMPRESS DEFLATE, RFC 8054).
    // Works with both sync-flush and partial-flush servers (INN included):
    // the read side uses a resumable inflate that never blocks while
    // decodable data is pending.
    // try conn.enableDeflate(&client);

    const info = try client.group("misc.test");

    // Overview of the last few articles (lines borrow the read buffer).
    var it = try client.over(.{ .span = .{ .low = info.low, .high = info.high } });
    while (try it.next()) |line| {
        const entry = try znntp.OverviewEntry.parse(line);
        std.debug.print("{d} {s}\n", .{ entry.number, entry.subject });
    }

    // Fetch an article into owned memory (capped at 1 MiB).
    var art = try client.articleAlloc(gpa, .{ .number = info.high }, .limited(1024 * 1024));
    defer art.deinit(gpa);
    std.debug.print("{s}\n", .{art.data});

    try client.quit();
}

Pipelined bulk fetches

For downloading many articles, Pipeline keeps a bounded window of commands in flight (RFC 3977 pipelining) so the server overlaps its responses with your requests — without the unbounded-burst deadlock:

var pipe = client.pipeline(.body, .{ .range = .{ .low = info.low, .high = info.high } }, .{ .window = 16 });
while (try pipe.next()) |item| {
    switch (item.result) {
        .info => |art| {
            var data = item.data.?;
            while (try data.next()) |line| { ... }
        },
        .missing => |code| { ... }, // article expired or cancelled; run continues
    }
}

Sources can be a number range, a slice of numbers, or a slice of message-ids; verbs are .article, .head, .body, and .stat. Unconsumed article data is discarded automatically on the next iteration. Don't interleave other commands while a pipeline is running.

A pipeline can also start empty and be fed while it runs — enqueue new work between iterations as you discover it (following References headers, walking an NZB, ...), from any thread. Message-ids are copied, so it's fine to pass slices parsed out of the borrowed read buffer:

var pipe = client.pipelineDynamic(gpa, io, .article, .{ .window = 16 });
defer pipe.deinit();
try pipe.enqueueMessageId(root_id);
while (try pipe.next()) |item| {
    // ...parse the article, discover more...
    try pipe.enqueueMessageId(referenced_id);
    try pipe.enqueueRange(low, high);
}
// null means idle, not finished: enqueue more and call next() again.

Commands not wrapped by a helper can be sent through the escape hatch:

const r = try client.command("XFEATURE COMPRESS GZIP", .{});
if (!r.isSuccess()) return error.Unsupported;

Multiline responses must be consumed fully (iterate to null, or call discard()) before sending the next command.

Demo CLI

zig build run -- news.example.com misc.test --tls --compress

Prints the greeting, capabilities, and the last ten overview lines of the group.

Testing

zig build test

Protocol tests run against in-memory streams; no network access needed.