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
- Client only; wildmat matching, overview formats beyond the default eight fields, and
LIST OVERVIEW.FMTare left to the application. - One command in flight at a time (no pipelining).
- Integration tests run against a scripted server inside the crate (
--features integration).