ENiGMA½ BBS Software
  • JavaScript 99%
  • Shell 0.5%
  • PEG.js 0.2%
  • HTML 0.2%
Find a file
Bryan Ashby 8a24dbcd3d
Native Binkp support (#682)
* Native BinkP mailer: inbound server, outbound caller, ftn_bso integration

Adds a fully native BinkP (FidoNet mail exchange) implementation:

Core protocol (core/binkp/):
- frame.js / commands.js  — wire framing and command constants
- cram.js                 — CRAM-MD5 challenge/response auth
- session.js              — answering and originating session state machine
- bso_spool.js            — BSO outbound spool reader/writer with BSY locking
- caller.js               — callNode() / pollNodes() for outbound dialing
- binkp_poll_module.js    — sysop menu module for on-demand polling (!BINKP)

Scanner/tosser (core/scanner_tossers/binkp.js):
- Inbound TCP server (port 24554 by default, configurable)
- Per-node CRAM-MD5 password lookup
- BSY lock management (sends M_BSY on collision)
- Scheduled outbound polling via later.js text schedule
- Emits NewInboundBSO after receiving files (triggers ftn_bso toss)

ftn_bso integration (core/scanner_tossers/ftn_bso.js):
- Subscribes to NewInboundBSO on startup; unsubscribes on shutdown
- Re-uses existing tryImportNow + 'importing' busy-guard so concurrent
  triggers are safely coalesced

Infrastructure:
- core/system_events.js: new NewInboundBSO event key
- core/config.js: _pushTestConfig/_popTestConfig for test isolation
- core/config_default.js: documented binkp defaults block
- core/enigma_assert.js: lazy config access (safe before Config.create())
- misc/menu_templates/main.in.hjson: !BINKP sysop command

Tests (112 passing):
- test/binkp_frame.test.js, binkp_cram.test.js — unit tests
- test/binkp_session.test.js — session state machine
- test/binkp_bso_spool.test.js — spool read/write/lock
- test/binkp_server.test.js — live server accept/auth/file-receive/BSY
- test/binkp_caller.test.js — callNode/pollNodes against live server
- test/binkp_ftn_bso_integration.test.js — NewInboundBSO wiring + path compat

Docs:
- docs/_docs/messageareas/binkp.md: new operator setup guide
- docs/_docs/messageareas/bso-import-export.md: note native BinkP option

* binkp/bso_spool: drop double-dispose, GC all-tilded flow files

Two coupled cleanups in the post-send disposition path.

1) Double-dispose

   BinkpSession._applyDisposition unlinks (or truncates) the queued file
   based on its disposition before emitting 'file-sent'. The spool layer
   was repeating that work in _applyFlowDisposition, which produced
   spurious "Could not delete sent file" ENOENT warnings on every send
   of a ^-prefixed (delete) flow entry — the exact spam visible against
   fsxnet and spooknet sessions in production logs.

   For direct-attach entries the same race was silent (the disposeFn's
   .catch(() => {}) swallowed it) but equally redundant.

   Fix: BinkpSession owns file lifecycle. The spool's disposeFn is now
   purely flow-file bookkeeping — it tildes the line and nothing else
   for direct-attach (where there's no flow file at all, disposeFn is
   simply null and is no longer registered in the session's disposeMap).

2) Garbage-collect all-tilded flow files

   _applyFlowDisposition rewrote sent lines with a leading ~ but never
   removed the file when every line was processed. Quiet nodes
   accumulated dead-marker .clo/.flo files indefinitely; busy nodes saw
   ftn_bso append fresh entries to a file that already had pages of
   stale ~-prefixed history.

   Fix: after rewriting, if no live (non-empty, non-tilde) lines remain,
   unlink the flow file. ftn_bso recreates it on the next outbound
   queue for the same node.

Tests updated to match the new disposeFn contract (does not touch the
underlying file; session does), plus three new cases:
- direct-attach disposeFn is null and the file is left in place
- multi-entry flow file: GC happens only when the LAST live line is
  tilded; partial state preserves the surviving entry
- concurrent-modification: disposeFn skips both rewrite and GC if the
  flow file changed out from under the captured line

115 BinkP-suite tests passing (was 113 prior to this change).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: scope cross-file pollution from root hooks/stubs

Two pre-existing latent issues in the suite were masked by a third
that hid the symptom — together they caused 10 BinkP tests to fail
when run as part of the full suite (passing in isolation).

Root cause
----------

test/user_db.test.js stubbed Events.emit and Events.listenerCount at
*top level*:

    Events.emit = () => {};
    Events.listenerCount = () => 0;

Because that runs at file-load time, mocha installs the stubs the
moment it requires user_db.test.js — and they live on the Events
singleton, so they persist for the entire suite. Every subsequent
test that depends on Events.emit() (NewInboundBSO, NewOutboundBSO,
crashmail dispatch, ftn_bso's import-on-inbound listener) silently
saw nothing fire.

The user_db file similarly had top-level before()/after() hooks
stubbing StatLog functions, which mocha treats as *root* hooks that
wrap every test in every file.

Fixes
-----

- test/user_db.test.js: wrap the suite in a describe('user_db', …)
  and move all stubbing (StatLog AND Events) into that describe's
  before(), saving originals first. after() restores. The stubs are
  now scoped to user_db's own tests.

- test/message_area.test.js: same shape — top-level before/after
  was acting as a root hook. Wrapped in describe('message_area —
  hideFromBrowse', …).

- core/events.js: look up logger.log on each call instead of
  capturing it at module-load time. Tests that mock loggerModule.log
  after events.js loads (i.e. all of them, since Log.init() is never
  called in tests) used to crash with "Cannot read properties of
  undefined (reading 'trace')" on the first Events.addListener.
  Lazy lookup is also a more honest pattern — logger.log IS supposed
  to be re-bindable via init().

- core/message_area.js: same lazy-Config-lookup change. Fixes a
  test-only leak where the captured Config getter would freeze on a
  stale fixture if the module had been reloaded via require.cache
  invalidation.

Suite goes from 1228 passing / 10 failing to 1238 passing / 0
failing. No production behavior change — all four edits are either
lazy lookups (semantically identical to the eager capture in normal
runtime) or test-file scoping (no production effect at all).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* binkp: crashmail send-on-export, separate pull schedule

Replaces the single 5-minute "look at the spool, dial nodes that
have pending mail" timer with two distinct triggers — eliminating
the wait-to-send latency for outbound and the no-fetch silence for
quiet peers.

1) Crashmail (event-driven, automatic, no config)

   ftn_bso.flowFileAppendRefs now takes a destAddress arg and emits
   a new NewOutboundBSO system event (with payload { address }) on
   successful append. scanner_tossers/binkp.js subscribes to that
   event, debounces back-to-back exports for the same node into one
   session (default 500 ms; tunable via crashmailDebounceMs), and
   dispatches a targeted pollNodes([addr]) call.

   Effect: a posted message is on the wire to the destination peer
   within ~half a second instead of waiting up to 5 minutes for the
   next scheduled poll.

2) Pull cycle (periodic, configured) for quiet peers

   The previous binkp.schedule key (a 5-min timer that only dialed
   nodes with pending mail) is replaced by binkp.pullSchedule. On
   each tick the new behavior dials EVERY configured peer in
   binkp.nodes regardless of pending state — so echo-mail flows in
   from hubs that wait for the spoke to call. Per-node opt-out via
   "pull": false in the node's config block.

   Default pullSchedule: every 15 minutes. The old "won't dial
   without pending mail" model meant spooknet/zer0net delivery only
   happened when the local user happened to post into one of their
   echos.

3) pollNodes(forceAddrs, cb) honors forceAddrs

   Previous signature accepted but ignored 'args'. Now it unions
   forceAddrs (Address instances or address strings) with the
   spool's pending-mail set, dedupes by zone:net/node, and dials
   each. Used by both new triggers above plus the existing sysop
   "poll now" menu module (which still passes []).

4) Bonus fix: BinkpSession 'disconnect' propagation in callNode

   When a remote drops the socket mid-handshake, BinkpSession emits
   'disconnect' (not 'error'). The caller's session-result promise
   now also listens for 'disconnect' and rejects, so callNode
   returns promptly instead of hanging until the 5-min
   SESSION_TIMEOUT. Also moves listener registration BEFORE the
   awaited attachSpoolToSession so events that fire during that
   await don't get missed.

Tests
-----

13 new tests across:

- test/binkp_caller.test.js
    pollNodes forceAddrs: dials with no pending mail, accepts plain
    address strings, ignores invalid strings, dedupes against pending.

- test/binkp_ftn_bso_integration.test.js
    NewOutboundBSO emit: payload includes the destination address,
    no emit when destAddress is omitted, no emit when the underlying
    append fails.

- test/binkp_server.test.js
    _pullAddresses: returns concrete addresses, omits pull:false,
    skips wildcard patterns, returns [] when nodes is missing/empty.
    Crashmail dispatch: registers/removes the NewOutboundBSO
    listener on startup/shutdown, debounces same-address bursts to
    one dial, dispatches per-distinct-address within the window,
    discards events with missing address payloads.

These three test files also get their existing top-level
before/after hooks wrapped in describe blocks (the same pattern as
the test-isolation precursor commit) — defensive scoping so future
file additions can't easily reintroduce the kind of cross-file
pollution that ate the suite earlier.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Doc updates

* binkp: stale .bsy + inbound temp reapers, util consolidation

Operational hardening for the native BinkP mailer plus a small
cleanup pass over the call sites it grew during initial development.

Reapers (P0):

  - Stale .bsy locks: bso_spool acquireLock now reaps an EEXIST
    lock older than staleLockMaxAgeMs (default 30 min, 6× the
    session timeout) and retries once. BinkpModule.startup also
    sweeps every outbound directory unconditionally so a crashed
    prior run doesn't leave nodes permanently un-pollable.
  - Inbound temp files: BinkpSession tracks the binkp_in_*.dt
    files it owns and unlinks any partials in _destroy() on
    error/disconnect. BinkpModule.startup adds a startup sweep
    of tempDir as a safety net for hard process kills, gated by
    inboundTempMaxAgeMs (default 1 hr).
  - Bonus fix: writeStream now has an error handler so
    ERR_STREAM_DESTROYED from a destroy-during-write race no
    longer escapes to uncaughtException.

Cleanup (P1):

  - core/binkp/util.js consolidates localAddresses(config) and
    addressKey(addr) — duplicates removed from caller.js and
    scanner_tossers/binkp.js.
  - Crashmail listener now validates address.isValid() before
    queuing.
  - Address#getMatchScore (previously commented out) is restored;
    new findBestNodeMatch picks the most-specific matching pattern
    so a "21:1/100" override always wins over a "21:*" catch-all
    regardless of HJSON insertion order. Used by both nodeConfigFor
    and _lookupPassword.
  - Logger stub centralized in test/setup.js; removed the same
    ~10-line prelude from five test files.

Docs: docs/_docs/messageareas/binkp.md rewritten to cover
pullSchedule, crashmail, per-node pull:false, the new sweep
options, !BINKP sysop hotkey, and the pattern-specificity caveat
on nodes keys.

Tests: 1238 → 1266 (+28). Zero regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Pretty

* test: fix two binkp test flakes; ensure mocha exits cleanly

Three small fixes so `npm test` is deterministic and terminates.

binkp_caller "sends files from the outbound spool to the remote"
(pre-existing, ~10% flake): the test had the caller's localAddress
identical to the server's address, so both sides' spools — sharing
the same outbound dir — saw the same flow file and tried to send
it. The caller's delete-disposition unlinked the source mid-flight,
producing an intermittent file-received miss. Use distinct same-zone
addresses (caller=1:218/700, server=1:1/2) so each spool's queue
stays disjoint while keeping the outbound subdir as plain "outbound".

binkp_inbound_temp "unlinks a partial inbound temp file on _destroy()"
(introduced by the prior cleanup commit): the test waited one
setImmediate tick for fs.createWriteStream's open() to complete and
two more for _destroy()'s fire-and-forget unlink to land. Both races
become visible under load. Replace with poll-with-deadline helpers
(waitForFileExists / waitForFileGone) so timing is deterministic.

Add --exit to the npm test script. Some test fixture is leaking a
handle (likely a half-closed socket) at suite end, leaving mocha
running indefinitely after "1266 passing". --exit is the standard
mocha mitigation; it doesn't hide production-code leaks, only test
fixture ones, and avoids contributors thinking the suite has hung.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Driveby AP fix

* Potential fix for pull request finding 'CodeQL / Unvalidated dynamic method call'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-05-03 18:28:06 -06:00
.devcontainer Sync up with master 2023-10-11 19:52:21 -06:00
.github fix: bump Ruby version to 3.1 in Jekyll deploy workflow (#2) 2026-02-26 15:45:05 -05:00
.husky Bump version 2025-09-23 19:08:26 -06:00
.vscode Sync up with master 2023-10-12 20:40:57 -06:00
art Native Binkp support (#682) 2026-05-03 18:28:06 -06:00
config ActivityPub Revamp (#668) 2026-04-20 16:47:07 -06:00
core Native Binkp support (#682) 2026-05-03 18:28:06 -06:00
dev_util ActivityPub Revamp (#668) 2026-04-20 16:47:07 -06:00
docker Added missing change directory 2023-10-15 20:43:31 +00:00
docs Native Binkp support (#682) 2026-05-03 18:28:06 -06:00
gopher Add gopher/README 2020-11-27 01:03:12 -07:00
misc Native Binkp support (#682) 2026-05-03 18:28:06 -06:00
mods Merge branch 'master' of ssh://numinibsd/git/base/enigma-bbs 2020-06-03 20:53:46 -06:00
test Native Binkp support (#682) 2026-05-03 18:28:06 -06:00
util First pass formatting with Prettier 2022-06-05 14:04:25 -06:00
www Retro style default profile, constant cleanup, some DRY, etc. 2023-01-29 16:52:01 -07:00
.dockerignore No need to copy the ephemeral directories 2023-08-26 20:03:48 -05:00
.gitattributes Use binary for ansi files, not CRLF 2026-04-15 15:29:26 -06:00
.gitignore ActivityPub Revamp (#668) 2026-04-20 16:47:07 -06:00
.lintstagedrc.json Added json to list of files to update 2023-01-13 09:23:03 -06:00
.prettierignore First pass formatting with Prettier 2022-06-05 14:04:25 -06:00
.prettierrc.json First pass formatting with Prettier 2022-06-05 14:04:25 -06:00
autoexec.sh autoexec.sh environment enhancement 2024-12-04 10:47:23 -05:00
CONTRIBUTING.md View system & FSE Revamp #1 (#641) 2026-03-31 18:52:05 -06:00
DEV.md View system & FSE Revamp #1 (#641) 2026-03-31 18:52:05 -06:00
eslint.config.mjs Pretty, fix up some types 2025-02-23 14:07:35 -07:00
LICENSE.TXT Update copyright 2026 2026-01-18 18:22:50 -07:00
main.js View system & FSE Revamp #1 (#641) 2026-03-31 18:52:05 -06:00
mise.toml Set Python to just 3.x 2025-09-22 21:17:17 -06:00
mkdocs.yml * Some doc changes 2015-11-20 11:56:57 -07:00
oputil.js * Split out oputil stuff into modules based on <command> 2017-02-15 20:27:16 -07:00
package-lock.json Fix IMAP error handler 2026-04-24 09:27:20 -06:00
package.json Native Binkp support (#682) 2026-05-03 18:28:06 -06:00
README.md Native Binkp support (#682) 2026-05-03 18:28:06 -06:00
TROUBLESHOOTING.md Organized and updated upgrade docs 2023-03-19 13:39:21 -06:00
UPGRADE.md Initial Wide Character support (#655) 2026-04-23 20:35:11 -06:00
watch.sh NodeJS Upgrade and watch.sh +x fix 2025-02-22 19:32:31 -05:00
WHATSNEW.md Native Binkp support (#682) 2026-05-03 18:28:06 -06:00
yarn.lock Some MLTEV fixes (#676) 2026-04-23 18:41:37 -06:00

ENiGMA½ BBS Software

ENiGMA½ BBS

ENiGMA½ is a modern BBS software with a nostalgic flair!

Features

Below are just some of the features ENiGMA½ supports out of the box:

  • Multi platform — Anywhere modern Node.js runs likely works (known to work under Linux, FreeBSD, OpenBSD, macOS and Windows)
  • Unlimited multi node support
  • Highly customizable via HJSON based configuration, menus, and themes in addition to JavaScript based mods
  • SQLite storage of users, message areas, etc.
  • Strong PBKDF2 backed password encryption.
  • Support for 2-Factor Authentication with One-Time-Passwords
  • Structured Bunyan logging!
  • Telnet, SSH, and both secure and non-secure WebSocket access built in! Additional servers are easy to implement
  • Built-in web server with HTTP(S) support — powers temporary download URLs, file browsing, and more
  • CP437 and UTF-8 output with wide character support — CJK, Hangul, fullwidth forms, and similar scripts display and edit correctly
  • SyncTERM style font support. Display PC/DOS and Amiga style artwork as it should be! In general, ANSI-BBS / cterm.txt / bansi.txt are followed for expected BBS behavior.
  • Baud emulation. View ANSI like the block gods intended.
  • Full SAUCE support.
  • Renegade style pipe color codes.
  • MCI support for lightbars, toggles, input areas, and so on plus many other bells and whistles
  • Message networks with FidoNet Type Network (FTN) + BinkleyTerm Style Outbound (BSO) message import/export, and MRC (Multi-Relay Chat)
  • Native BinkP/1.1 mailer — built-in inbound listener and outbound caller for FTN networks. No external binkd required; outbound mail ships immediately on export ("crashmail"), and a configurable pull cycle keeps echo mail flowing in from quiet hubs.
  • Internet mail — send and receive email directly from the BBS private message system via IMAP/SMTP. See Internet Mail
  • ActivityPub / Fediverse (experimental) — federated messaging with WebFinger, NodeInfo2, actor profiles, and common ActivityPub object types; PNG avatars with auto-generated defaults
  • Message bases exposed via Gopher and NNTP content servers
  • Gazelle (🪦) inspired File Bases including fast fully indexed full text search (FTS), #tags, and legacy X/Y/Z modem support
  • Upload processor supporting FILE_ID.DIZ and NFO extraction, year estimation, and more!
  • Door support including common dropfile formats for legacy DOS doors. Built in BBSLink, DoorParty, and Exodus!
  • Native x86/DOS door emulation via v86 — run classic DOS BBS doors (LORD, PimpWars, TradeWars, etc.) directly inside ENiGMA½ with zero external dependencies. No QEMU, DOSBox, or DOSEMU required. Includes oputil fat for managing FreeDOS disk images and oputil v86 for an interactive browser-based DOS desktop. See Local Doors — v86.
  • Z-Machine interactive fiction — run Infocom classics (Zork, Colossal Cave, and hundreds more from the IF Archive) natively in Node.js. No external emulator required. Supports Z-Machine versions 3, 4, 5, and 8. See Z-Machine Door.
  • Full Screen Editor (FSE) with ANSI art support, real-time cursor/mode indicators, inline find/search (Ctrl-F), file upload to body, and a fully modernized view engine
  • A remote accessible Waiting For Caller (WFC)!
  • Expandable achievement system — BBSing gamified!

...and much much more. Please check out the issue tracker and feel free to request features (or contribute!) features!

Documentation

Browse the docs online. Be sure to checkout the /docs/ folder as well for the latest and greatest documentation.

Installation

On most *nix systems simply run the following from your terminal:

curl -o- https://raw.githubusercontent.com/NuSkooler/enigma-bbs/master/misc/install.sh | bash

Please see Installation Methods for Windows, Docker, and so on...

Donating

If you feel the urge to donate, you can do so here

Donate using Liberapay

Support

Terminal Clients

ENiGMA has been tested with many terminals. However, the following are suggested for BBSing:

Some Boards

Special Thanks

(in no particular order)

...and so many others! This project would be nothing without the BBS and art scene communities!

Star History Chart

License

Released under BSD 2-clause. See LICENSE.TXT