Skip to content

NNTP Server Support #357

@awehttam

Description

@awehttam

NNTP Server Interface for BinktermPHP

Status: DRAFT — Discussion Only
Generated: 2026-05-21


This proposal is a draft, was generated by AI, and may not have been reviewed for accuracy. It is intended as a discussion document only; no implementation is currently planned.


Overview

NNTP (Network News Transfer Protocol, RFC 3977) is the protocol that powers Usenet newsgroups. It provides a well-understood, widely-implemented interface for reading and posting threaded messages — and it maps surprisingly well onto FTN echomail. Adding an NNTP server to BinktermPHP would let users connect with any standard newsreader (Thunderbird, Forte Agent, slrn, tin, etc.) and read or post to FTN echoareas as if they were Usenet newsgroups.

This document explores the conceptual mapping between FTN echomail and NNTP, the mechanical challenges involved, and what a BinktermPHP-native NNTP interface might look like architecturally.

For the complementary design where BinktermPHP acts as an upstream NNTP client rather than a user-facing server, see docs/proposals/NNTPClientTransport.md. The two proposals should share helper logic for article parsing/building, Message-ID generation, header translation, threading, and charset handling rather than implementing those rules twice.


Background: Why NNTP?

Usenet and Fidonet grew up in the same era and solved the same problem — store-and-forward discussion networks — independently. Their data models are strikingly similar:

Concept Fidonet / FTN Usenet / NNTP
Message area Echoarea (e.g. BINKTERM) Newsgroup (e.g. alt.bbs)
Message Echomail message Article
Network address FTN address (z:n/f.p) Email address
Unique ID MSGID kludge Message-ID header
Thread link REPLY: kludge References: header
Transit node Hub / uplink NNTP server (peer)

NNTP gateways for Fidonet have existed since the early 1990s. Historically, tools like ifgate and various uucp/inews pipelines connected FTN hubs to Usenet feeding points. The goal here is a first-class, BinktermPHP-native gateway that serves echomail directly from the database.


Protocol Mechanics

Core NNTP Commands

An NNTP server only needs a subset of commands to be useful for reading:

  • LIST [ACTIVE] — return a list of available newsgroups with article counts
  • GROUP <newsgroup> — select a newsgroup; server returns article count and number range
  • ARTICLE <n> / HEAD <n> / BODY <n> — retrieve article by number or Message-ID
  • OVER / XOVER — overview data (subject, from, date, message-id, references) for a range
  • NEWNEWS / NEWGROUPS — incremental sync since a given timestamp

For posting, the server additionally needs:

  • POST — accept a new article from the client
  • AUTHINFO USER / AUTHINFO PASS — authenticate before posting

This is a modest surface area. The heavy lifting is in the translation layer, not the protocol itself.

SSL / TLS

Standard NNTP runs on port 119. NNTPS (implicit TLS) runs on port 563. The daemon could accept connections on 119 and support STARTTLS upgrade, similar to how the telnet server handles raw connections before handing them to the TUI stack.


Mapping FTN Echomail to NNTP

Newsgroup Name Translation

Synchronet's NNTP server uses the convention NetworkDisplayName.AreaTag in mixed case (e.g. LovelyBits.BinktermPHP), preserving the original area tag casing rather than lowercasing. NNTP newsgroup names are case-insensitive by spec, so either convention works. BinktermPHP should follow the same pattern: use the network's display name as the hierarchy prefix and the area tag as-is.

Network "LovelyBits", area LVLY_BINKTERMPHP  →  LovelyBits.LVLY_BINKTERMPHP
Network "DoveNet",    area DOVE-NET.GENERAL  →  DoveNet.DOVE-NET.GENERAL

The original area tag is also preserved verbatim in the X-FTN-AREA header (see below).

Collision handling — If two networks share an area tag, the network display name prefix differentiates them. The server should detect and log any collision at startup.

Message-ID Construction

Rather than a generic @ftn domain, the Message-ID should use the BBS's own FQDN, following Synchronet's convention. The raw MSGID serial and address are preserved in the X-FTN-MSGID: header for round-trip fidelity. The constructed Message-ID is scoped to this system and used only by NNTP clients.

Messages without a MSGID kludge (which exist in older traffic) need a synthetic ID derived from a hash of their content. This is a known weakness of any FTN/NNTP bridge.

Header Translation

Synchronet's output is a useful reference for which headers to emit. BinktermPHP should follow the same conventions:

FTN field / kludge NNTP header Notes
From: (name) From: "Name" (z:n/f.p) <user@fN.nN.zN.fidonet> — see below
Subject: Subject:
To: (name) X-Comment-To: Synchronet convention; To: header is omitted
MSGID: kludge Message-ID: See format below; raw value also in X-FTN-MSGID:
REPLY: kludge References: Single parent; raw value also in X-FTN-REPLY:
date_written Date: May be wrong; see note below
Echoarea tag Newsgroups: Translated name; raw tag in X-FTN-AREA:
PID: kludge X-FTN-PID:
CHRS: kludge X-FTN-CHRS: Raw wire value; Content-Type always declares UTF-8
SEEN-BY: lines X-FTN-SEEN-BY:
PATH: kludge X-FTN-PATH:
BBS hostname Path: hostname!not-for-mail
Content-Type: text/plain; charset=UTF-8; format=flowed

From address

Synchronet encodes the FTN address into a structured email-like address using the .fidonet pseudo-domain:

"awehttam" (227:1/200) <awehttam@f200.n1.z227.fidonet>

The format is "DisplayName" (zone:net/node.point) <username@f{node}.n{net}.z{zone}.fidonet>. Point 0 is included literally. This is non-routable but unambiguous and round-trippable.

Message-ID format

Synchronet uses the BBS's own FQDN as the domain rather than a generic @ftn placeholder, which produces IDs that are scoped to the originating system and less likely to collide:

<MSGID_SERIAL.sequence.AREANAME_LOWERCASED@bbs.hostname>

Example from the observed header: <6A08C568.55.lovelybitslvly_binktermphp@revpol.lovelybits.org>

BinktermPHP should use Config::getSiteUrl() to derive the hostname component.

Date reliability

date_written from the FTN packet is used for the Date: header because that is what newsreaders display and sort by. However, date_written can be wrong if the remote sysop's clock is skewed. The reliable date_received timestamp should be included as X-Date-Received: so discrepancies are visible.

Article Numbering

NNTP article numbers are per-group integers that increase monotonically. BinktermPHP's echomail table already has a primary key (id) per message. Per-group article numbers can be derived from a simple ordered sequence within each echoarea — either by using the database id directly (with an offset per area) or by maintaining a separate nntp_article_number sequence. Article numbers must never be reused.

Encoding

BinktermPHP stores all message bodies as UTF-8 in PostgreSQL regardless of the original wire encoding, so the NNTP server can serve bodies directly without any conversion. Inbound posts from a newsreader arrive as UTF-8 (NNTP is UTF-8 native) and can be injected into the outbound queue as-is. The Content-Type header on served articles should declare charset=UTF-8.


Threading

FTN threading is subject-based by convention — replies share the same Subject: and set REPLY: to the MSGID of the parent. This maps directly to NNTP References:. However, FTN only tracks a single parent (REPLY:), while NNTP References: is a full ancestor chain. The gateway can build a synthetic chain by walking the database for the thread root, but this is an optional enhancement; a single-parent References: already enables threaded view in most newsreaders.


Bidirectional Posting

Read-only access is straightforward. Two-way posting adds complexity:

  1. Authentication — The NNTP daemon must authenticate the user before accepting a POST command. BinktermPHP's existing user accounts and passwords are the natural auth source.
  2. Address assignment — Posts injected into FTN need a MSGID with a valid FTN address. The BBS's own node address is used (the same address used for netmail).
  3. Injection path — The new message must be written to the outbound packet queue via the same code path used by web posting, not by writing raw FTN packets directly. This ensures kludge generation and dupe-check logic are applied consistently.
  4. Cancel / supersede — NNTP supports cancel and supersede control messages. FTN has no equivalent. These should be silently dropped at the gateway boundary.
  5. Flood and abuse — Because the NNTP client is any third-party tool, rate limiting per authenticated user is important to prevent the BBS's FTN node from flooding its uplinks.

Architecture

Daemon Model

Following the pattern of telnet/ and ssh/, the NNTP server would be a standalone PHP daemon:

nntp/
  nntp_server.php     — entry point, listens on TCP port 119/563
  src/
    NNTPSession.php   — per-connection state machine
    NNTPArticle.php   — echomail → NNTP article translation
    NNTPGroupList.php — area → newsgroup mapping, LIST support
    NNTPPost.php      — inbound article → echomail injection

Each client connection is a separate process or coroutine (matching the telnet server's model). The daemon reads echomail directly from PostgreSQL via Database::getInstance().

If BinktermPHP also implements the upstream polling/posting design from docs/proposals/NNTPClientTransport.md, the server and client sides should reuse shared NNTP helpers rather than maintaining separate implementations of article/header translation behavior.

Read Path

Newsreader → NNTP daemon → PostgreSQL (echomail table)
                        ↑
                  NNTPArticle translates rows to RFC 3977 wire format

Write Path

Newsreader → NNTP daemon → NNTPPost validates & authenticates
                        → injects via existing EchomailWriter / outbound queue
                        → BinkP mailer picks up on next poll

Configuration

NNTP server settings would live in config/bbs.json (or a dedicated config/nntp.json), managed through the BBS admin interface:

  • Listen port (default 119)
  • TLS certificate path
  • Newsgroup prefix (ftn., araknet., etc.)
  • Allow posting vs. read-only
  • Post rate limit: max posts per minute (default 10)
  • Post rate limit: max posts per hour (default 60)

Design Decisions

  1. Authentication required; subscribed areas only — Anonymous access is not supported. Users must authenticate with their BinktermPHP credentials before any LIST or GROUP command is honored. The newsgroup list returned by LIST ACTIVE is filtered to the echoareas the authenticated user is subscribed to, so two users may see different sets of groups.

  2. Flood control is configurable — Per-user post rate limits (e.g. max posts per minute and per hour) should be configurable via the admin interface with reasonable defaults (e.g. 10 posts/minute, 60 posts/hour). Posts that exceed the limit are rejected with a 441 response rather than silently queued.

  3. No peering — The server implements client-facing NNTP only. Server-to-server commands (IHAVE, CHECK, TAKETHIS) are not implemented. The BinktermPHP FTN mailer remains the sole mechanism for propagating messages to the wider network.

  4. Article expiry is a backend concern — The NNTP daemon does not send Expires: headers and does not manage expiry itself. If the BinktermPHP backend prunes old echomail rows, those articles simply disappear from the newsgroup's number range. Newsreaders that have already downloaded them will retain their local copies; requests for expired article numbers return 423.

  5. Posting name policy enforced — The posting_name_policy configured for the echoarea's network is enforced on inbound NNTP posts, exactly as it would be on a post submitted through the web interface.

  6. Message-ID collisions — Handled gracefully: the duplicate is logged and skipped; the existing article is not overwritten.


Prior Art

  • ifgate / UUCP gateways (1990s) — earliest FTN↔Usenet bridges, operated at the packet level
  • NewsGate — a mid-90s Windows tool that bridged Fidonet echoareas to a local NNTP server
  • Synchronet BBS — has a built-in NNTP server that serves its internal message bases; the architecture is similar to what is proposed here
  • MBSE BBS — open-source FTN BBS with NNTP gateway support; its source is a useful reference for the header translation edge cases

Summary

An NNTP interface for BinktermPHP is technically feasible and architecturally clean. The FTN ↔ NNTP data model mapping is good enough for practical use: echoareas become newsgroups, MSGID/REPLY: kludges become Message-ID/References: headers, and because BinktermPHP stores everything as UTF-8, there is no encoding gap to bridge. The main challenges are synthetic address construction for the From: header, Message-ID uniqueness for older messages without a MSGID kludge, and the write-path injection ensuring FTN protocol rules are respected when posting from a standard newsreader. A read-only implementation would be straightforward; full bidirectional posting adds meaningful complexity around authentication and abuse prevention.

If upstream NNTP peering is pursued as well, docs/proposals/NNTPClientTransport.md should be treated as the companion document. Shared concerns such as Message-ID construction, From: synthesis, header naming, and threading behavior should come from common code paths.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions