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:
- 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.
- 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).
- 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.
- Cancel / supersede — NNTP supports cancel and supersede control messages. FTN has no equivalent. These should be silently dropped at the gateway boundary.
- 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
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
NNTP Server Interface for BinktermPHP
Status: DRAFT — Discussion Only
Generated: 2026-05-21
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-IDgeneration, 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:
BINKTERM)alt.bbs)MSGIDkludgeMessage-IDheaderREPLY:kludgeReferences:headerNNTP gateways for Fidonet have existed since the early 1990s. Historically, tools like
ifgateand variousuucp/inewspipelines 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 countsGROUP <newsgroup>— select a newsgroup; server returns article count and number rangeARTICLE <n>/HEAD <n>/BODY <n>— retrieve article by number or Message-IDOVER/XOVER— overview data (subject, from, date, message-id, references) for a rangeNEWNEWS/NEWGROUPS— incremental sync since a given timestampFor posting, the server additionally needs:
POST— accept a new article from the clientAUTHINFO USER/AUTHINFO PASS— authenticate before postingThis 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
STARTTLSupgrade, 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.AreaTagin 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.The original area tag is also preserved verbatim in the
X-FTN-AREAheader (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
@ftndomain, the Message-ID should use the BBS's own FQDN, following Synchronet's convention. The rawMSGIDserial and address are preserved in theX-FTN-MSGID:header for round-trip fidelity. The constructedMessage-IDis scoped to this system and used only by NNTP clients.Messages without a
MSGIDkludge (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:
From:(name)From:"Name" (z:n/f.p) <user@fN.nN.zN.fidonet>— see belowSubject:Subject:To:(name)X-Comment-To:To:header is omittedMSGID:kludgeMessage-ID:X-FTN-MSGID:REPLY:kludgeReferences:X-FTN-REPLY:date_writtenDate:Newsgroups:X-FTN-AREA:PID:kludgeX-FTN-PID:CHRS:kludgeX-FTN-CHRS:Content-Typealways declaresUTF-8SEEN-BY:linesX-FTN-SEEN-BY:PATH:kludgeX-FTN-PATH:Path:hostname!not-for-mailContent-Type:text/plain; charset=UTF-8; format=flowedFrom address
Synchronet encodes the FTN address into a structured email-like address using the
.fidonetpseudo-domain: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
@ftnplaceholder, which produces IDs that are scoped to the originating system and less likely to collide: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_writtenfrom the FTN packet is used for theDate:header because that is what newsreaders display and sort by. However,date_writtencan be wrong if the remote sysop's clock is skewed. The reliabledate_receivedtimestamp should be included asX-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 separatenntp_article_numbersequence. 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-Typeheader on served articles should declarecharset=UTF-8.Threading
FTN threading is subject-based by convention — replies share the same
Subject:and setREPLY:to theMSGIDof the parent. This maps directly to NNTPReferences:. However, FTN only tracks a single parent (REPLY:), while NNTPReferences: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-parentReferences:already enables threaded view in most newsreaders.Bidirectional Posting
Read-only access is straightforward. Two-way posting adds complexity:
POSTcommand. BinktermPHP's existing user accounts and passwords are the natural auth source.MSGIDwith a valid FTN address. The BBS's own node address is used (the same address used for netmail).Architecture
Daemon Model
Following the pattern of
telnet/andssh/, the NNTP server would be a standalone PHP daemon: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
Write Path
Configuration
NNTP server settings would live in
config/bbs.json(or a dedicatedconfig/nntp.json), managed through the BBS admin interface:ftn.,araknet., etc.)Design Decisions
Authentication required; subscribed areas only — Anonymous access is not supported. Users must authenticate with their BinktermPHP credentials before any
LISTorGROUPcommand is honored. The newsgroup list returned byLIST ACTIVEis filtered to the echoareas the authenticated user is subscribed to, so two users may see different sets of groups.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.
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.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.Posting name policy enforced — The
posting_name_policyconfigured for the echoarea's network is enforced on inbound NNTP posts, exactly as it would be on a post submitted through the web interface.Message-ID collisions — Handled gracefully: the duplicate is logged and skipped; the existing article is not overwritten.
Prior Art
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 becomeMessage-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 theFrom:header, Message-ID uniqueness for older messages without aMSGIDkludge, 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.mdshould be treated as the companion document. Shared concerns such asMessage-IDconstruction,From:synthesis, header naming, and threading behavior should come from common code paths.