NNTP

hopf-nntp is an NNTP / NNTPS async client (RFC 3977, STARTTLS per RFC 4642, AUTHINFO per RFC 4643). It dials, greets, reads CAPABILITIES, negotiates TLS and authenticates for you, then hands your handler a session that queues commands from any thread and answers through callbacks. There is no server in this crate.

Features

Area Support
Handshake Greeting (200 posting allowed / 201), CAPABILITIES (re-read after TLS), STARTTLS required or opportunistic, implicit NNTPS
Auth AUTHINFO SASL (SCRAM-SHA-256, CRAM-MD5, PLAIN, LOGIN — chosen from the SASL capability in that order of preference) or AUTHINFO USER / PASS; credentials go out only once TLS is as configured
Commands Typed helpers for LIST ACTIVE [wildmat], GROUP, OVER, ARTICLE, HEAD, POST, QUIT; NntpSession::command for anything else
Multi-line replies Delivered line by line, dot-unstuffed, as the bytes arrive; POST bodies are dot-stuffed and terminated for you
Timeouts DNS, connect, and a per-command quiet-time budget reset on every reply line

Session API

NntpClientHandler::on_connected(&NntpSession, &NntpGreeting) fires once the session is usable; the greeting carries the capabilities, whether posting is allowed, and whether the connection is secure and authenticated. Clone the NntpSession and keep it: commands may be queued from any thread, go out one at a time in queue order, and each completion callback receives the final NntpStatus (or the transport error that ended the session, which every still-queued command also hears). quit() sends QUIT and closes once it is answered; is_alive() is false after that or once the connection is gone.

Callbacks run on the connection's reactor thread: hand the data to another thread rather than doing slow work inside them.

Quick start

use std::sync::Arc;
use hopf_core::{public_trust_connector, Runtime, RuntimeConfig};
use hopf_nntp::{NntpClient, NntpClientHandler, NntpGreeting, NntpSession};

struct Headlines;

impl NntpClientHandler for Headlines {
    fn on_connected(&mut self, session: &NntpSession, greeting: &NntpGreeting) {
        println!("secure={} posting={}", greeting.secure, greeting.posting_allowed);
        let s = session.clone();
        session.group("comp.lang.rust", move |g| {
            let g = g.expect("GROUP");
            s.over(g.last.saturating_sub(20), g.last, |o| println!("{} {}", o.article_number, o.subject), |_| {});
            s.quit();
        });
    }
    fn on_error(&mut self, error: &std::io::Error) {
        eprintln!("nntp: {error}");
    }
}

let rt = Arc::new(Runtime::start(RuntimeConfig::default())?);
NntpClient::new("news.example.com", 563)
    .implicit_tls(public_trust_connector(&[]), "news.example.com")
    .credentials("alice", "secret")
    .connect_with(&rt, Box::new(Headlines))?;

Errors

NntpClientHandler::on_error reports a refused or timed-out dial, a TLS failure, a required STARTTLS the server did not offer, rejected credentials (PermissionDenied, with the server's 481/482 text), a command that got no reply within the budget, or the transport going away mid-session. A command the server rejects is not a session error: its own callback gets NntpClientError with the status code and text.

Limitations