BinkP Native Mailer
ENiGMA½ includes a built-in BinkP mailer that handles both inbound and outbound FidoNet packet transport without requiring an external daemon such as binkd.
BinkP is the TCP/IP session-layer protocol used by modern FidoNet nodes to exchange mail packets and files. The native mailer implements:
- Inbound server — listens for incoming BinkP connections (default port 24554)
- Outbound caller — dials configured nodes to deliver pending mail
- Crashmail — sub-second send-on-export, no scheduled wait
- Pull cycle — periodic dial of every configured peer to keep echo mail flowing in from quiet hubs
- CRAM-MD5 authentication (FTS-1027)
- NR (Non-Reliable) mode (FTS-1028) — safe resume after disconnect
- TLS / binkps — optional encrypted listener (port 24555) and TLS outbound per node
- GZ compression (FTS-1029) — transparent per-file compression negotiated via
EXTCMD; skips already-compressed types - Multi-batch sessions — continues exchanging files across multiple EOB cycles in a single TCP connection
- FREQ (File REQuest) — serves files to requesting nodes; supports magic names (e.g.
NODELIST) and versioned directory search - BSO spool integration — reads and writes the same BSO outbound/inbound directories that
ftn_bsouses for packet scanning and tossing
Configuration
Section titled “Configuration”All BinkP configuration lives inside the existing scannerTossers.ftn_bso block in config.hjson, under a binkp sub-key.
scannerTossers: { ftn_bso: { // ... existing ftn_bso config (paths, nodes, schedule, etc.) ...
binkp: { // Inbound server — accepts connections from other nodes inbound: { enabled: true port: 24554 // IANA-registered BinkP port address: "0.0.0.0" // listen on all interfaces; use "127.0.0.1" for local-only
// Optional TLS listener (binkps, port 24555). Both plain and TLS // listeners run simultaneously when enabled. // tls: { // enabled: true // port: 24555 // certFile: "/path/to/binkp.crt" // keyFile: "/path/to/binkp.key" // } }
// Pull cycle: dial every configured peer on this schedule, even // if we have no outbound for them. Keeps echo mail flowing in // from quiet hubs that wait for the spoke (us) to call. // Set to null/empty to disable; crashmail still works. pullSchedule: "every 15 minutes"
// Crashmail debounce window (ms). When ftn_bso queues outbound // mail, dialing is delayed this long so back-to-back exports to // the same peer coalesce into a single session. crashmailDebounceMs: 500
// FREQ (File REQuest) — serve files to requesting nodes. // When a remote node sends a .req file, ENiGMA resolves each // requested name and sends the files back in the same session. // Resolution order: magic → areas (file base) → dirs // // freq: { // enabled: true // // // magic — name → path or glob; newest glob match wins // // magic: { // // NODELIST: "/path/to/nl/NODELIST.*" // // ALLFIX: "/path/to/ALLFIX.NA" // // } // // // areas — file base area tags (best for TIC-imported files) // // areas: [ // // { areaTag: "nodelists" } // // ] // // // dirs — raw filesystem directories // // dirs: [ "/path/to/freq-files" ] // // maxFiles: 10 // cap on files returned per session (default 10) // requirePwd: false // if true, only honour FREQs from password-authenticated sessions // }
// Per-node outbound configuration nodes: { // Key is an FTN address (wildcards supported, e.g. "21:1/*") "1:218/700": { host: "bbs.example.com" port: 24554 // optional, defaults to 24554 sessionPassword: "s3cr3t" // optional CRAM-MD5 password — see tip below pull: true // optional, defaults to true // requestNR: true // optional; ask this node to send in NR mode // gz: false // optional; disable GZ compression for this node }
// TLS outbound example (binkps): // "1:218/701": { // host: "secure.example.com" // port: 24555 // sessionPassword: "s3cr3t" // tls: true // tlsAllowSelfSigned: true // or tlsFingerprint / tlsCertFile // } } } }}binkp.inbound
Section titled “binkp.inbound”| Key | Required | Default | Description |
|---|---|---|---|
enabled | No | false | Start the inbound listening server on startup |
port | No | 24554 | TCP port to listen on |
address | No | "0.0.0.0" | IP address to bind |
binkp.pullSchedule
Section titled “binkp.pullSchedule”Later.js text expression — for example "every 15 minutes" or "at 3:00 am". When the timer fires, ENiGMA½ dials every configured peer in binkp.nodes (excluding wildcard patterns and any peer with pull: false). This is independent of whether we have outbound mail queued — its job is to give quiet hubs a chance to push their pending echo mail down to us.
Set to null, "", or omit the key entirely to disable the pull cycle. Crashmail (event-driven dialing on outbound) still works without it.
If the expression fails to parse, the pull cycle is disabled and a warning is logged at startup.
binkp.crashmailDebounceMs
Section titled “binkp.crashmailDebounceMs”When ftn_bso writes a flow file (i.e. queues a packet for a remote peer), it emits a NewOutboundBSO event. The BinkP module dials the destination peer right away — within hundreds of milliseconds — so messages ship without waiting on the next pull cycle. To avoid one session per message during a multi-message export, dialing is delayed by crashmailDebounceMs (default 500) so back-to-back exports to the same peer coalesce into a single session.
Lower it to ship faster at the cost of more sessions per burst; raise it if your scanner emits big batched exports.
binkp.staleLockMaxAgeMs
Section titled “binkp.staleLockMaxAgeMs”When ENiGMA½ acquires a node lock (.bsy file in the outbound directory), it expects to release it cleanly at session end. If the BBS crashes mid-session the lock persists and that node becomes un-pollable until the file is removed.
staleLockMaxAgeMs (default 30 * 60 * 1000, i.e. 30 minutes) controls how long a .bsy file may live untouched before it’s reaped. Two paths use it:
- Startup sweep runs unconditionally on BinkP module startup (even when inbound is disabled, since outbound calls also acquire locks).
- Just-in-time check runs when
acquireLockhits anEEXIST: if the existing lock is older than the threshold, it’s unlinked and the lock is acquired.
The default is 6× the internal session timeout (5 minutes), giving a generous safety margin without making post-crash recovery slow. If you ever raise the session timeout, raise this in proportion.
The same .bsy files are honoured by external mailers such as Binkd, and by the ENiGMA½ tosser when it queues outbound — see flowLockTimeoutMs below.
binkp.flowRefWarnRepeatMs
Section titled “binkp.flowRefWarnRepeatMs”A BSO flow file stores an absolute path to each file queued for a node. If that file is later deleted or moved, the entry can never be sent — the node simply never receives it. ENiGMA½ warns about this, naming the node and the file.
flowRefWarnRepeatMs (default 60 * 60 * 1000, i.e. 1 hour) is how often that warning repeats for the same reference. It repeats because the condition is a standing fault rather than an event: nothing fixes it on its own, and saying it once per process meant it scrolled away long before anyone looked.
Raise it if you have a dangling reference you already know about and have not dealt with yet. To see what is affected and act on it, use oputil bso status — and note that a node whose queued entries have all gone missing is deliberately not polled, so that command is the only place it shows up.
flowLockTimeoutMs
Section titled “flowLockTimeoutMs”FTS-5005.003 §5.1 requires that any software touching a BSO flow file first take that file’s .bsy lock, and prohibits changes entirely while the lock is held:
A bsy is a main control file that must be used by any software dealing with flow files in BSO. […] Any software must check this file before doing any changes in flow files. If a bsy file exists all changes are prohibited in any corresponding flow files.
That applies to ENiGMA½ queueing outbound just as much as to a mailer sending it. Because the lock is a standard, per-node NNNNnnnn.bsy in the node’s own outbound directory, honouring it interlocks correctly with the native BinkP mailer and external mailers alike — Binkd takes the very same lock, so neither side needs to know about the other.
flowLockTimeoutMs (default 5000, i.e. 5 seconds) is how long the tosser waits for a busy node’s lock before giving up. Contention is normally momentary — a session finishing an entry — but the lock is held for the length of a whole session, and during that time the spec says the flow file must not be modified.
If the wait expires the outbound is not queued this pass, and you’ll see:
Flow file busy; outbound not queued this passNothing is lost or corrupted — the write simply did not happen, which is what the spec requires. Raise this on a busy hub with long sessions.
binkp.inboundTempMaxAgeMs
Section titled “binkp.inboundTempMaxAgeMs”Inbound files are buffered in tempDir (defaults to the OS temp dir) under names like binkp_in_*.dt, then renamed into the inbound spool on successful receipt. Two layers protect against leaks if a peer drops mid-transfer:
- In-session finalizer: each session tracks the temp files it owns; on socket error or disconnect they are unlinked immediately.
- Startup sweep: catches anything left after a hard process kill that prevented the in-session finalizer from running.
inboundTempMaxAgeMs(default60 * 60 * 1000, i.e. 1 hour) is the age threshold.
binkp.nodes
Section titled “binkp.nodes”Each key is an FTN address or wildcard pattern. Values:
| Key | Required | Default | Description |
|---|---|---|---|
host | Yes (for outbound) | — | Hostname or IP of the remote node |
port | No | 24554 | Remote port |
sessionPassword | No | — | Session password for CRAM-MD5 authentication (distinct from FTN packet password) |
pull | No | true | Include this node in the periodic pull cycle. Set false for write-only peers. Crashmail dispatch is unaffected — outbound queued for a pull: false peer still triggers an immediate session. |
requestNR | No | false | Ask this node to send files to us in NR mode. See below. |
gz | No | true | Set false to neither offer nor use GZ compression with this node. |
Wildcard patterns (e.g. "21:1/*") are valid for inbound password lookup but are skipped during pull cycles, since the pull cycle dials concrete addresses only. Put the most specific patterns first in your config — pattern matching uses first-match-wins on insertion order.
binkp.nodes[].requestNR — NR (non-reliable) mode
Section titled “binkp.nodes[].requestNR — NR (non-reliable) mode”NR mode is the binkp answer to a link that keeps dropping mid-transfer. Normally a sender starts every file at offset 0 and the receiver has to interrupt with M_GET to move it; on a bad line the connection is often gone again before that lands. In NR mode the sender instead offers the file with an offset of -1 and waits to be told where to start, so the resume position is settled before any data moves. See FTS-1028.
OPT NR is a request aimed at the remote: it asks the other side to send to us that way. It costs a round trip per file, and the standard is blunt that it “degrades performance over regular quality connections and it should be used only if absolutely necessary” — so ENiGMA½ does not send it unless a node opts in with requestNR: true. binkd behaves the same way, asking only when the sysop sets -nr or -nd on a node.
The other direction needs no configuration. If a peer asks us for NR mode we always honour it, which FTS-1028 makes a MUST.
binkp.nodes[].gz — compression
Section titled “binkp.nodes[].gz — compression”When both sides advertise GZ (and EXTCMD), file data is compressed on the wire. Files that are already compressed — ArcMail bundles (.mo0, .tu1, …), .zip, .arj and friends — are sent as-is, so a node using archiveType sees little of this either way.
The wire format is the zlib container (FTS-1029), which is what binkd, Mystic and the rest expect. Inbound data is decoded from either the zlib or the gzip container, so a peer running an ENiGMA½ older than 0.5.1-beta — which sent gzip — is still understood.
If a peer cannot decompress what we send, ENiGMA½ does not simply give up on the file: FTS-1029 allows a receiver to ask for it again uncompressed, and a peer that asks us that way is honoured for the rest of the session. Set gz: false to skip compression with a node entirely.
binkp.nodes[].tls / TLS outbound (binkps)
Section titled “binkp.nodes[].tls / TLS outbound (binkps)”To dial a node over TLS (binkps, typically port 24555) set tls: true in the node block. Exactly one of the following trust options is also required:
| Key | Description |
|---|---|
tlsAllowSelfSigned | true — accept any certificate (convenient, no MITM protection) |
tlsFingerprint | "SHA256:AA:BB:..." — pin to a specific certificate fingerprint |
tlsCertFile | "/path/to/ca.pem" — trust a specific CA or self-signed certificate file |
binkp.inbound.tls — TLS listener (binkps)
Section titled “binkp.inbound.tls — TLS listener (binkps)”Set inbound.tls.enabled: true to start a second, TLS-only listener. Both the plain (port 24554) and TLS (default 24555) listeners run simultaneously. Requires a certificate and matching private key:
| Key | Description |
|---|---|
enabled | true to start the TLS listener |
port | Port to listen on (default 24555) |
certFile | Absolute path to the PEM certificate |
keyFile | Absolute path to the PEM private key |
binkp.freq — File REQuest (FREQ)
Section titled “binkp.freq — File REQuest (FREQ)”When FREQ is configured, remote nodes can request named files by sending a .req file containing one name per line. ENiGMA½ resolves each name and sends the matching files back in the same BinkP session (no extra round-trip required).
Passwords in .req files (e.g. NODELIST!password) are stripped and ignored.
Resolution order (first match wins):
-
magic— case-insensitive name → path mapping. The path may contain glob wildcards (*,?); when it does, ENiGMA½ expands the glob at request time and returns the newest matching file. This meansNODELIST: "/path/to/nl/NODELIST.*"always serves the current segment without any config changes after each weekly update. -
areas— ENiGMA½ file base area tags. This is the preferred option when files arrive via TIC file echoes: TIC processing imports each received file into a configured file area, and the FREQ resolver queries that area by name (exact match first, then prefix — e.g.NODELISTmatchesNODELIST.365). The newest file by upload timestamp wins. -
dirs— plain filesystem directories. Useful for files managed outside the file base. Exact name match first, then prefix match; newest by filesystem mtime wins.
| Key | Default | Description |
|---|---|---|
enabled | false | Enable FREQ handling |
magic | — | Object mapping magic names → file paths or glob patterns |
areas | [] | Array of { areaTag } objects — file base areas to search |
dirs | [] | Array of directory paths to search |
maxFiles | 10 | Maximum files returned per session (across all resolvers combined) |
requirePwd | false | When true, only honour FREQs from sessions authenticated with a sessionPassword (CRAM-MD5) |
Typical setup for a TIC-fed nodelist:
// In scannerTossers.ftn_bso.ticAreas — map the hub's TIC area to a local file areaticAreas: { fidonet_nodelist: { areaTag: "nodelists" storageTag: "nodelists" }}
// In fileBase — define the storage tag and area// fileBase.storageTags: { nodelists: "nodelists" }// fileBase.areas: { nodelists: { name: "Nodelists", storageTags: ["nodelists"] } }
// In binkp — point FREQ at the same areafreq: { enabled: true areas: [ { areaTag: "nodelists" } ]}With this setup, every new nodelist that arrives via TIC is immediately FREQ-serveable — no path configuration to maintain.
Sessions and batches
Section titled “Sessions and batches”A binkp/1.1 session is a series of batches. Each ends when both sides have sent M_EOB, and another follows any batch that carried more than that pair — so a session that moved mail always ends on a short empty batch. Each new batch is also an opportunity: anything that turned up mid-session, such as a FREQ response or mail tossed from a packet that just arrived, goes out on the same connection instead of waiting for the next poll.
Both sides decide this the same way, by counting the command frames sent and received since the batch began, so they agree without negotiating. A peer that identifies itself as binkp/1.0, or that never sends a version at all, is not offered a second batch.
Reloading configuration
Section titled “Reloading configuration”config.hjson is watched and reloaded while the BBS runs. Almost everything under binkp is read at the moment it is used, so a reload applies on its own:
| Setting | Applies to | When it takes effect |
|---|---|---|
nodes (hosts, ports, passwords, pull, TLS) | next session, inbound or outbound | Immediately |
freq | next inbound session | Immediately |
crashmailDebounceMs | next crashmail burst | Immediately |
tempDir, staleLockMaxAgeMs, flowRefWarnRepeatMs | next session | Immediately |
flowLockTimeoutMs (under ftn_bso) | next outbound queue | Immediately |
ftn_bso paths and messageNetworks.ftn.networks | next session or poll | Immediately |
pullSchedule | the pull timer | On reload — the timer is rebuilt |
inbound.enabled, inbound.port, inbound.address, inbound.tls.* | the listening socket | Restart required |
The last row is bound into the socket when it starts listening; changing it under live sessions is not something a reload can do safely. A reload that touches those keys logs a warning naming them so the change isn’t silently lost:
[BinkP] Inbound listener settings changed; restart required for them to take effectHow it works with ftn_bso
Section titled “How it works with ftn_bso”The two modules share the same BSO spool directories (paths.outbound, paths.inbound, paths.secInbound):
Both resolve outbound subdirectories — which network owns the bare outbound/ directory, and how zones are suffixed — through the same shared logic, so they cannot disagree about where a given node’s mail lives. See Outbound Directory Layout.
Outbound flow:
ftn_bsoscans message areas → writes.pktfiles and flow file references into the outbound spoolftn_bsoemits aNewOutboundBSOevent with the destination address- The BinkP module receives the event, debounces briefly (
crashmailDebounceMs), then dials the destination - After a successful send, the flow file entry is rewritten as
~-prefixed and the packet file is deleted; once every line in a flow file is~-prefixed the flow file itself is removed - Independently, the pull-cycle timer dials every configured peer on
pullScheduleso quiet hubs get periodic touches even when we have nothing for them
Inbound flow:
- A remote node connects on port 24554 (or ENiGMA½ calls out and receives files in return)
- BinkP receives the files into
tempDirand on successful completion moves them intopaths.inbound(orpaths.secInboundfor password-authenticated sessions) ftn_bsois immediately triggered to toss the received packets into message areas — no waiting for the next scheduled import
External mailer compatibility:
If you use an external mailer (binkd, etc.) instead of or alongside the native BinkP mailer, ftn_bso continues to toss inbound files via its @watch / @sched mechanisms unchanged. The native BinkP mailer’s immediate-toss trigger is additive and does not interfere.
Sysop poll command
Section titled “Sysop poll command”Once BinkP is configured, sysops can trigger an immediate outbound poll from the main menu by typing !BINKP. The system dials every node that has pending mail and reports the result. The default menu template wires this for the sysop ACS group automatically; a fresh install gets the command for free.
To expose a poll option in a custom menu entry, point it to the binkp_poll module:
myBinkpPollMenu: { desc: "BinkP Poll" module: binkp_poll}Pre-existing custom menus that don’t include this entry will need it added manually.
Firewall / NAT notes
Section titled “Firewall / NAT notes”- Open TCP port 24554 inbound if you want other nodes to be able to call you
- The outbound caller initiates connections from an ephemeral port; no special firewall rules needed for outbound
- If you are behind NAT, configure your router to forward port 24554 to the BBS host
Migrating from external binkd
Section titled “Migrating from external binkd”- Stop
binkdand disable its startup service - Add the
binkpblock to yourscannerTossers.ftn_bsoconfig as shown above, withinbound.enabled: true - Copy your per-node passwords from your
binkd.cfgintobinkp.nodesassessionPasswordvalues - Remove or adjust any
@watch/@schedimport schedule fromftn_bso— the native mailer triggers toss immediately after each session, so a frequent scheduled import is no longer necessary (a slow fallback such as"every 60 minutes"is harmless) - Reload the config (
oputil.js config reload) or bounce the process
Renamed since binkd migration guides
Section titled “Renamed since binkd migration guides”If you’re following older notes or examples, two things have changed:
- The
binkp.schedulekey is nowbinkp.pullSchedulewith clearer semantics: it dials every configured peer regardless of pending mail, not just nodes with queued outbound. Outbound dispatch is now event-driven via crashmail and does not require a schedule. - There is no separate “outbound poll schedule” key — the pull cycle is the only schedule. Sub-second outbound dispatch happens automatically via crashmail.