• docs/v322_new.md src/sbbs

    From Rob Swindell (on Windows@VERT to Git commit to main/sbbs/m on Fri Jul 17 03:05:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/9fc5505cbc4dfc9aeaab00ff
    Modified Files:
    docs/v322_new.md src/sbbs3/ctrl/build.bat src/sbbs3/jsexec.vcxproj ntsvcs.vcxproj sbbscon.vcxproj
    Log Message:
    Windows: build all 32-bit Synchronet hosts /LARGEADDRESSAWARE

    Mark sbbs.exe (sbbscon.vcxproj), sbbsNTsvcs.exe (ntsvcs.vcxproj),
    jsexec.exe (jsexec.vcxproj), and sbbsctrl.exe (ctrl/build.bat) as large-address-aware, raising their usable address space from 2GB to
    ~4GB on 64-bit Windows.

    This is a stopgap for GitLab #1185: an access violation in mozjs185
    seen only on 32-bit Windows under JS heap pressure (a web-scrape
    attack), never on Linux-x64. It raises the ceiling; it does not fix
    the underlying pointer corruption, which the SM128 / 64-bit migration
    addresses properly.

    The MSVC projects get <LargeAddressAware>true</LargeAddressAware> in
    both Debug|Win32 and Release|Win32 link steps. Borland's ilink32 has
    no LAA option, so build.bat flips the PE-header bit post-build
    instead.

    Validated: the 2006-era Borland host runs stably under LAA with
    system-wide top-down allocation (Synchronet DLLs executing above 2GB,
    all servers listening, no errors), and jsexec exercised the mozjs GC
    and trace-JIT with ~3.4GB live above 2GB without failure. Sustained
    behavior under real scrape load is not yet proven.

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

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Windows@VERT to Git commit to main/sbbs/m on Sat Jul 18 22:02:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/3a5e9cb3628031f8812840c5
    Modified Files:
    docs/v322_new.md src/sbbs3/scfg/scfg.c scfg.h scfgmsg.c
    Log Message:
    Restore SCFG import/export of subs.txt, alongside subs.ini

    subs.txt import/export was removed in mayor-19-caps (e6acb3269a) and its
    menu slots later repurposed for *subs.ini in reef-31-feed (cfb32e03f5).
    Some sysops still rely on the legacy text format (especially for import),
    so this restores it as an additional format while keeping *subs.ini as
    the preferred format going forward.

    Message Areas > Export/Import Areas list subs.txt again, the -import=
    command line recognizes a subs.txt filename once more, and the
    line-oriented parser/writer are back.

    Rather than the old memcpy-over-the-existing-sub (which zeroed every
    field the format doesn't carry - qwk_conf, print modes, area_tag, ...),
    the import now copies only the fields subs.txt actually provides, leaving
    the rest of an over-written sub intact. ptridx is likewise preserved
    (the exported value is meaningless on the importing system).

    Partially addresses issue #1128.

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

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Sun Jul 19 15:25:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/8a79c1ff830f811d824cced5
    Modified Files:
    docs/v322_new.md src/sbbs3/nodedefs.h
    Log Message:
    Document node-rerun-on-recycle behavior; fix stale NODE_RRUN comment

    Add a v3.22a release note for the terminal-server change that flags
    active nodes to re-read their configuration when a recycle is signaled,
    so configuration changes reach new callers without waiting for every
    node to disconnect.

    Also correct the NODE_RRUN definition comment. Its only remaining
    effect in the threaded terminal server is to make a node re-read its configuration on the next logon; nothing is "re-run" and there is no logoff-time action (unlike its neighbors NODE_EVENT and NODE_DOWN).
    The old "Re-run this node when log off" wording described the DOS/OS2 separate-process node model that no longer exists.

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

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Fri Jul 24 11:16:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/04b47329aa0cfbc839ec1a4c
    Modified Files:
    docs/v322_new.md src/sbbs3/zmodem.c
    Log Message:
    zmodem: fix SIGFPE when the transmit window is under 4 blocks (#1197)

    zmodem_send_from() picks the ack-request interval as

    subpkts_sent % (max_window_size / block_size / 4)

    which is a division by zero whenever the window is narrower than four
    blocks: with -8 -w8192 the divisor is 8192 / 8192 / 4, that is 1 / 4,
    that is 0. sexyz then dies with SIGFPE right after the ZRQINIT/ZRINIT handshake, having put about 3 KB of handshake on the wire and no file
    data at all. Also affected: -8 -w16384, -2 -w4096, and the same window
    sizes set through MaxWindowSize in sexyz.ini.

    Clamp the interval to a minimum of one, so a window narrower than four
    blocks requests an ACK on every subpacket, which is the only sensible
    reading of "every quarter window" at that size.

    Verified against lrz over the bench harness: -8 -w16384 and -2 -w4096
    both used to die on signal 8 and now complete with a matching SHA-256, at
    3.08 and 0.71 MB/s. Windows that already worked are unchanged, -w32768
    at 4.88 MB/s and -w65536 at 5.03, as is streaming without -w at 11.6.

    SyncTERM shares zmodem.c but never sets max_window_size, so it leaves the
    value at zero, short-circuits the branch, and cannot reach this. The
    exposure is sexyz only.

    A window exactly equal to the block size (-8 -w8192) no longer crashes
    but is still pathologically slow, roughly one subpacket per second with a one-second receive timeout each iteration. That is tracked separately in
    the issue; it is not a regression, since it used to be a crash.

    zmodem_ver bumped 2.3 -> 2.4. Release notes updated, including the already-shipped 2 GB windowed fix and Deuce's send-path speedups, which
    had no v322 entry.

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

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Fri Jul 24 23:28:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/aaf394a0d833acd89a64d3a9
    Modified Files:
    docs/v322_new.md src/sbbs3/sexyz.c
    Log Message:
    sexyz: keep the transmit window >= 4x the block size, like lsz (#1197)

    sexyz's -w set only the ZMODEM transmit window and left the block size independent (from -2/-4/-8, default 1 KB), so a window at or below the
    block size was configurable -- and it stalled the transfer to roughly one window per second. lsz and Forsberg sz never allow this: when -w is
    given they force the block down to a quarter of the window
    (blkopt = Txwspac = Txwindow/4), guaranteeing window >= 4*block so the ack-every-quarter-window scheme always has several blocks in flight.

    Do the same after argument parsing, so -8 -w8192 and -w8192 -8 both
    resolve the same way: floor the window to 256, round to a multiple of 64,
    and reduce max_block_size to window/4 when it exceeds it (and the start block_size with it). A one-line notice reports the reduction.

    Effect: the formerly-hanging window<=block configurations now complete --
    -8 -w8192 reduces the block to 2048 and transfers, -4 -w4096 to 1024,
    and so on -- while windows that already left four blocks of headroom
    (-w32768 and up with -8) are untouched and unchanged (4.75 and 4.88 MB/s
    here, streaming still 11.3). This also makes the divide-by-zero guarded
    in 04b47329aa unreachable by configuration, since window/block/4 is now
    always >= 1.

    This does not make sexyz's windowed throughput competitive with lsz --
    at the same block and window sexyz is still many times slower, and very
    small windows (which force tiny blocks) remain slow with occasional
    one-second stalls. That is the separate sexyz windowed-send overhead
    tracked in #1195; this change is only about not permitting the degenerate window<=block configuration that lsz forbids by construction.

    sexyz.c version 3.3 -> 3.4. No zmodem.c change, so SyncTERM is
    unaffected (it never sets a transmit window anyway).

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

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Windows@VERT to Git commit to main/sbbs/m on Sat Jul 25 22:24:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/b17b1d3e3e45bbbc4df47ae6
    Modified Files:
    docs/v322_new.md src/sbbs3/atcodes.cpp getkey.cpp js_bbs.cpp putmsg.cpp sbbs.h terminal.h
    Log Message:
    Unicode @-codes: return the expansion instead of printing it

    @U+XXXX@, @CHECKMARK@, @ELLIPSIS@, @COPY@, @SOUNDCOPY@, @REGISTERED@, @TRADEMARK@, @DEGREE_C@, @DEGREE_F@ and @WIDE:text@ wrote their glyph
    straight to the terminal from inside atcode() and returned an empty
    string. Format modifiers were therefore applied to "": @CHECKMARK-R10@
    emitted the glyph and then right-aligned nothing in a 10-column field.
    Callers that expand an @-code into a buffer for a non-printing reason -- bbs.atcode(), bbs.expand_atcodes(), format_text(), mnemonics(),
    putnode(), notify() and putmsg()'s @CENTER@ -- got an empty expansion
    plus stray output on the current node's terminal.

    These codes now encode into the caller's buffer and set P_UTF8, so show_atcode()'s existing display-width compensation makes alignment and truncation work. formatted_atcode() and expand_atcodes() take an
    optional pmode out-param so the buffer-expansion callers can learn the
    result is UTF-8; @CENTER@ and mnemonics() use it, the latter needing the
    P_UTF8 branch bputs() already has, since it emits byte-by-byte through outchar(). Terminal::center() measures with bstrlen(str, mode & P_UTF8):
    the other code sets bstrlen() recognizes are translated by putmsg(), not bputs(), and a centered line bypasses putmsg()'s character loop.

    The |W (doubled) modifier prints a UTF-8 expansion as-is rather than
    feeding it to wide(), which would double each byte rather than the glyph. Output buffers grew to fit @WIDE:, which triples byte length.

    Introduced in 9374bfe37e (pill-4-pencil), a9eea57362 (begun-3-ended) and 43c793a14b (hold-20-maps).

    Fixes #1198

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

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Windows@VERT to Git commit to main/sbbs/m on Sat Jul 25 22:24:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/dbc654505f7d822a8ece7c0d
    Modified Files:
    docs/v322_new.md src/sbbs3/con_out.cpp terminal.cpp
    Log Message:
    Count UTF-8 columns once, in Terminal::utf8_increment()

    outcp() wrote the codepoint with term_out() and then advanced the column itself, but term_out() -> parse_output() -> Terminal::utf8_increment()
    had already counted it -- so every codepoint outcp() emitted to a UTF-8 terminal advanced the column counter by twice the glyph's display width.
    The counter ran ahead of the cursor, making word-wrap, right-margin
    truncation, centering and the auto-pause line counter wrap early on any
    line containing such text. The call was also unconditional while
    term_out() returns early under CON_ECHO_OFF, so the column advanced for suppressed output too.

    utf8_increment() is now the only place that counts, and it passes sbbs->unicode_zerowidth rather than a hardcoded 0, preserving the
    zero-width quirk the ZWNBSP probe detects at logon. That applies the
    detected value to all UTF-8 output, bputs() included, not just outcp().

    outcp()'s inc_column() was correct when written in 9374bfe37e
    (pill-4-pencil): it paired with putcom(), which does no column
    accounting. It became a double-count in 374282bf81 (skins-28-shop),
    which switched the write to term_out() and introduced utf8_increment().

    Fixes #1200

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

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Windows@VERT to Git commit to main/sbbs/m on Sat Jul 25 23:32:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/005460da2b098c8ec796464a
    Modified Files:
    docs/v322_new.md src/sbbs3/useredit.cpp
    Log Message:
    Uedit: newline before the non-alpha commands that prompt

    getkeys() runs with K_NOCRLF and only alpha commands got a newline
    before their handler, so the '~' (leech), '+' (adjust credits), '*'
    (adjust minutes), '$' (credits) and '/' (AR string) prompts were echoed
    onto the tail of the "User edit (?=Menu) (n of m): " line.

    Present since ca499c9550 (silence-13-chorus) introduced the alpha-only
    newline.

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

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Tue Jul 28 18:06:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/58dc4d2b39ca0c227d5427aa
    Modified Files:
    docs/v322_new.md src/sbbs3/js_global.cpp src/xpdev/ini_file.h
    Log Message:
    Correct the .local rationale: ctrl/ is not overwritten by an upgrade

    The commit that added the .local filename variation justified it as protecting a sysop's edits from being overwritten by an upgrade. That is not true of the ctrl directory, which is where most of the configuration files it applies to live: ctrl is populated when the BBS is installed and an upgrade leaves it alone. The Windows upgrade script copies exactly one file into it, text.dat.

    What an upgrade does refresh is the distributed program directories -- exec, text and docs unconditionally, and xtrn when the sysop accepts the installer's "Upgrade Externals" option -- so the overwrite argument holds for a file we ship in one of those, and for any installation run from a source checkout, where updating means pulling the whole tree.

    The variation is still worth having for a ctrl file; the honest reason is separation rather than rescue. Reworded accordingly on the wiki (config:ini_files, howto:multihost) and in the release notes, where the claim was made. xpdev is a general-purpose library, so its header documents the search order and what .local means and leaves the rationale to those.

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

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Tue Jul 28 19:54:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/d91e670aaee817fea9820e9e
    Modified Files:
    docs/v322_new.md src/sbbs3/con_out.cpp str_util.c str_util.h terminal.h Log Message:
    bprintf: measure %s field widths in columns rather than bytes

    printf counts a field width in bytes, but a terminal renders columns.
    For a UTF-8 argument the two differ, so a byte-counted field prints
    short and whatever follows it slides left. Every columnar format string
    sent to a UTF-8 terminal is affected; message listings are the most
    visible case.

    Rewrite the numbers instead of the formatting. fmt_col_widths() walks
    the conversions in a format string, and for each %s carrying a literal
    width or precision re-states them in bytes such that printf's
    truncate-then-pad arrives at the requested number of columns: the new
    precision is the byte count of the leading N columns, and the new width
    is padded by the byte-minus-column difference. vsnprintf still does all
    the conversion. Sysop-customized text.dat files therefore keep working untouched, and output to a non-UTF-8 terminal is unchanged, the pre-pass running only for P_UTF8.

    A '*' width or precision is left alone: atcode() computes those itself
    and has already accounted for the difference, so rewriting them would compensate twice. An unrecognized conversion, a NULL argument, or a
    rewrite that would overflow the buffer all return the format string
    unmodified.

    Widths are measured with Terminal::bstrlen(), which already knows what
    occupies a column -- Ctrl-A codes, the PCBoard, Wildcat, Renegade,
    Celerity and WWIV sequences, backspace, and UTF-8 code-point widths.
    It gains an optional column limit and a byte-count out-parameter, which
    is what lets a field be truncated on a column boundary without splitting
    a multi-byte sequence.

    Issue #1204

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

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Fri Jul 31 21:43:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/72303693ef1b8dc2c11ff006
    Modified Files:
    docs/v322_new.md src/sbbs3/websrvr.cpp
    Log Message:
    websrvr: enforce download credits for unauthenticated file requests

    An anonymous request through the file-area virtual path
    (FileVPathPrefix) was authorized by user_can_download() alone, with no
    credit check, so every file in a directory with empty ARS downloaded
    free of charge regardless of its cost. The FTP server applies the
    credit gate to anonymous users, and issue #446 was closed with that
    being the intended policy.

    Both authorization paths now share check_file_download(), which applies
    the credit gate before the ARS check. Putting the two callers on one
    predicate is the point of the helper: the bypass existed because the
    anonymous branch and the authenticated one had drifted apart.

    The gate measures cost against user_available_credits() rather than
    user.cdt, so a user's free credits now count toward a download, as they
    already do on the Terminal and FTP servers, and as subtract_cdt()
    already assumed when charging: it spends free credits first.

    A refused anonymous request is logged at LOG_DEBUG. An insufficient
    credit refusal now reports NotEnoughCredits; both this and the existing authenticated failure log previously reported CantDownloadFromDir for
    it, naming the wrong cause.

    Fixes #1192

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

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Sat Aug 1 20:33:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/4de1032086f038890a75559e
    Modified Files:
    docs/v322_new.md src/sbbs3/websrvr.cpp
    Log Message:
    websrvr: load a file's cost before checking download credits (#1192)

    The credit gate added in 72303693ef (2026-07-31) compared against a
    cost that was always zero. resolve_vpath() loaded the file record at file_detail_index, and smb_getfile() skips smb_getmsghdr() entirely at
    that detail level, while cost is only ever set from the SMB_COST header
    field. Every comparison against it therefore passed: costed files
    downloaded free of charge for authenticated users as well as anonymous
    ones, while user_downloaded_file() still billed the account afterward
    (it reloads the record itself at file_detail_normal), so a user could
    go credit-negative.

    Read the header record when the directory's files can cost something. A
    free directory has none to report, since loadfile() zeroes the cost and download_is_free() short-circuits on the same DIR_FREE flag before any
    credit comparison, so the index record alone still suffices there and loadfile() is not asked to open the base twice.

    Verified against a live file base: an unauthenticated request for a 2,639,434-credit file is refused with 401, a zero-cost file in the same non-free directory returns 200, and a file in a free directory
    transfers in full.

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

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Sat Aug 1 23:40:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/7a0dabc5613b3f1a5e4ff8be
    Modified Files:
    docs/v322_new.md src/sbbs3/js_file.cpp
    Log Message:
    Parse an .ini once in File.iniGetAllObjects()

    It asked for each section's keys by name, and that lookup scans the file
    from the top -- so the cost was O(sections * lines). SyncArcade's lobby
    reads games.ini on every run for the cabinet titles, and at 4740 sections
    that read took two seconds on an idle machine; a loaded one paid seven,
    which reads as the lobby rescanning its ROMs when it is doing nothing of
    the kind.

    Parse the file into per-section lists once instead, the way scfglib1.c
    and scfglib2.c already read the configuration: 2005ms -> 66ms on that
    file. The section list still comes from iniGetSectionList(), so a
    duplicate section name is still collapsed to its first occurrence.

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

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Windows@VERT to Git commit to main/sbbs/m on Sun Aug 9 07:19:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/d02c34e2d43c073fb16fc610
    Modified Files:
    docs/v322_new.md src/sbbs3/xtrn_sec.cpp
    Log Message:
    Populate CHAIN.TXT lines 24 and 25 (call time and time used)

    These two WWIV drop-file fields have been hard-coded to 0 since the
    initial v3.00c check-in (7e3e47141a, 2000-10-10). Line 24 is the time
    of the call expressed as seconds past midnight; line 25 is the number
    of seconds the user has been on the system so far, so doors that show
    "time on" -- or recover the current time from the pair -- saw zeros.

    Line 25 uses timeon() rather than a raw now-starttime subtraction: it
    measures against the same logon timestamp line 24 is derived from, so
    the two stay consistent, and it clamps a negative result instead of
    printing it through an unsigned conversion.

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

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Windows@VERT to Git commit to main/sbbs/m on Sun Aug 9 07:19:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/abb64a33b74f9fcbe37e57ec
    Modified Files:
    docs/v322_new.md src/sbbs3/pcbdefs.hpp xtrn_sec.cpp
    Log Message:
    Fill in three PCBOARD.SYS fields left empty by the drop-file writer

    The struct-based PCBOARD.SYS writer added in 973b7f0a35 (2022-03-13)
    never assigned these, so each was written as whatever the value-
    initialized struct held:

    - Allowed K-Bytes for Download (offset 63) was 0, which tells a door
    the caller may download nothing. It now carries the user's
    available credits in kilobytes, the same source DOOR.SYS line 31
    and CALLINFO.BBS line 16 already use, saturated at 32767 -- the
    value PCBoard reads as "unlimited".

    - Conference Area user was in (offset 65) was 0. It now carries the
    current message group. The group number is used rather than the
    sub-board number (which offset 142 already carries) because it is
    an index into the user's group list and so always fits in a byte.

    - Language Extension (offset 80) was four NUL bytes; PCBoard writes
    four spaces when no alternate language is selected. The default is
    set in the structure definition alongside the other fields whose
    empty value is not zero.

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

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Sun Aug 9 23:03:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/2c49eafa1f3c5bfce980fbe4
    Modified Files:
    docs/v322_new.md src/sbbs3/xtrn_sec.cpp
    Log Message:
    Report GR in DOOR.SYS line 20 whenever ANSI is supported

    Line 20 required both ANSI and COLOR, so a caller using an ANSI
    terminal in monochrome mode was told "NG" and doors ran with ANSI
    disabled entirely.

    The monochrome distinction belongs to line 39 ("ANSI supported but
    NG mode"), which is still computed, but no door kit consults it.
    OpenDoors and xpdoor (both in this tree), MBSE's doorlib, and the
    Turbo Pascal DoorDriver Plus kit all decide ANSI solely from line
    20 -- DoorDriver Plus closes the file immediately after reading it,
    and doorlib names line 39 "allwaysN". Writers agree: WWIV emits GR
    on ANSI alone, and the published field definitions describe line 39
    as "always set to N".

    The ANSI-plus-COLOR test dates to the initial v3.00c check-in and
    was never revisited. 554c2ca395 (days-21-coins, 2020-12-11) moved
    it from useron.misc to the negotiated terminal flags for this same
    symptom -- doors losing ANSI for an ANSI-capable caller -- but kept
    the COLOR requirement.

    Affects both the 52-line and 31-line drop-file forms; line 20 is in
    the section common to both.

    Closes #1218

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

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Mon Aug 10 03:17:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/b8332a4618efd80300b671d6
    Modified Files:
    docs/v322_new.md src/sbbs3/mailsrvr.cpp mailsrvr.h
    Log Message:
    Treat the submission ports as an MSA, not as port 25 (#1221)

    The message submission ports (587, and 465 for implicit TLS) ran the same
    code path as the SMTP transfer port, so nothing downstream could tell the
    two apart. Three consequences, all reported by Deuce:

    - An authenticated user submitting an original message to an external
    recipient was judged by the port-25 rule and refused as an illegal
    relay unless the sysop enabled ALLOW_RELAY, a server-wide option.
    - The submission ports accepted unauthenticated mail, contrary to
    RFC 6409 section 4.3, which requires a 530 error response to MAIL on
    an unauthenticated session.
    - Once ALLOW_RELAY was set, every sender check was skipped for an
    authenticated session, so a user could submit with any From: address,
    including one at a domain the system does not own. The SMTP envelope
    sender was already derived from the user record, but the RFC822 From:
    header is emitted verbatim, and is now also DKIM-signed.

    Listening sockets previously passed their protocol name through xpms as
    the callback data, and the accept loop recovered each socket's properties
    by comparing that pointer against the name it was created from. Those comparisons work only because the names differ: the compiler pools
    identical string literals, so the transfer and submission agents, both
    named "SMTP", share one pointer and cannot be told apart. Pass a
    descriptor instead, carrying the display name alongside the properties
    the accept loop wants, so identity no longer depends on the name.

    An authenticated user sending to an external address via a submission
    port is a submission rather than a relay and no longer consults
    ALLOW_RELAY, which continues to govern the transfer port. The G and M restrictions still apply on every port, so a sysop retains per-user
    control over outbound mail.

    A sender address is authorized when its domain is one of this system's
    and it resolves to the authenticated user. Both questions are answered by smtp_resolve_user(), extracted from the RCPT TO handler, so any address
    that reaches a user -- alias, real name, alias.cfg entry, sub-address
    tag, user number -- also works as their sender address, and stays correct
    as the sysop edits alias.cfg. An alias resolving off-system is a forced
    relay rather than a local address, so it authorizes nobody, and the
    DefaultUser catch-all is suppressed: it exists to make unknown recipients deliverable, and honoring it would let that one account send as any
    address at the system's domains. MAIL FROM and the From: header are both checked. A null reverse-path is accepted, per RFC 6409 section 3.2.

    A refusal suggests the account's own address rather than prescribing it,
    since several addresses may be acceptable and the likeliest cause is a
    mail client configured with a misspelling of the sender's own address.

    The transfer port is unaffected in every respect, so only sysops who have deliberately enabled a submission port see any change.

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

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Tue Aug 18 22:04:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/f8d0bd2f64b441e91fe3e662
    Modified Files:
    docs/v322_new.md src/sbbs3/mailsrvr.cpp
    Log Message:
    mailsrvr: honor ALLOW_RELAY for foreign submission sender addresses

    Since b8332a4618 (chart-3-cause, 2026-08-10), the submission ports verify
    that an authenticated user's MAIL FROM and From: address is at one of the system's domains and resolves to that user. That rejects the sysop who
    points a mail client at another BBS as a smarthost and submits as their own domain -- which worked on 587 before that commit, and still works on port 25 wherever ALLOW_RELAY is set.

    Both of Vertrauen's authenticated submissions on 2026-08-18 were refused
    this way, one of them from a domain whose SPF record already authorizes Vertrauen to send for it. The reply named an address the submitter had no reason to want, so it read as a problem with the server hostname rather than with the sender address.

    A sender address in a foreign domain is a relay, so honor the permission the sysop already granted for relaying: with ALLOW_RELAY enabled, accept one
    from an authenticated user who is not restricted from relaying (G/M). An address in one of the system's own domains must still resolve to the authenticated user, so this is no license to forge a local address -- the
    case that would lend the system's DKIM reputation to the forgery.

    RFC 6409 section 6.1 (Enforce Submission Rights) is a MAY, in a section
    titled "Optional Actions", so declining to enforce it is conformant. The authentication requirement of section 4.3, a MUST, is unaffected.

    Issue #1221 asked for sysop-selectable strictness here. ALLOW_RELAY is a
    coarse stand-in for that -- a dedicated option or an ARS (issue #107) would express it properly -- but it does pick out the systems that mean it.

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

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Tue Aug 25 01:12:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/222dbfc7f6fd9921e2754adc
    Modified Files:
    docs/v322_new.md src/sbbs3/sexyz.c
    Log Message:
    sexyz: expose the ZMODEM file-management option, and -e for ESCCTL

    The engine has carried management_protect / management_clobber / management_newer for years and honors all three in zmodem_send_zfile(),
    but nothing ever set them -- not sexyz, not SyncTERM -- so every send
    fell through to the default and announced ZF1_ZMCRC ("transfer if
    different CRC or length"). The protocol work was already done; only
    the plumbing was missing.

    Add SendManagement to sexyz.ini's [ZMODEM] section, taking crc (the
    default, unchanged), clobber, protect or newer. One enumerated key
    rather than three booleans, because the engine treats them as mutually exclusive in a fixed precedence and three independent flags could be
    set to a combination that silently resolves to one of them.

    On the command line, -p and -n are new and match lsz. -y keeps its
    current meaning when receiving -- may we overwrite a file already here
    -- and gains the matching one when sending: tell the receiver to
    overwrite its own. That is exactly the lrzsz split, where lsz -y sets
    ZMCLOB and lrz -y clobbers locally, and -y was previously documented as applying only to receives, so nothing that worked before changes. Note
    the two act on different disks: receiving, the destination is this
    machine; sending, it is the user's. Synchronet's own receiver ignores
    a ZFILE's management field entirely, so this reaches third-party
    terminals only.

    Also add -e, the command-line equivalent of the existing
    EscapeCtrlChars ini key, for links that do not pass control characters
    intact. Verified against lrzsz: receiving with -e, the wire grows from 4,312,067 to 5,245,286 bytes for the same 4 MB file, within 0.005 % of
    what lrz -e negotiates from the same sender, and verifies
    byte-identical. It took the hex-header terminator fix in the preceding
    commit to work at all.

    When sending, -e is subject to the receiver's ZRINIT, which is what the
    engine derives escape_ctrl_chars from, so the usage text says the
    receiver's request governs rather than promising something the protocol
    layer overrides. Sending with ESCCTL actually negotiated is broken for
    a separate, pre-existing reason noted in the preceding commit.

    Options parse after the ini is read, so a flag overrides the file;
    verified with SendManagement=protect in the ini and -y on the command
    line yielding "overwriting destination".

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Tue Aug 25 01:12:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/fd038d9dd3cba68b4d95d026
    Modified Files:
    docs/v322_new.md src/sbbs3/zmodem.c
    Log Message:
    zmodem: escape CR unconditionally when ESCCTL is negotiated

    A sender with ESCCTL negotiated escaped every control character
    except carriage return, which it routed to a conditional rule --
    escape only when the previous byte sent was '@' -- and otherwise put
    on the wire raw. A receiver that requested ESCCTL discards unescaped
    control characters by definition, so it discarded those CRs, every
    subpacket carrying one failed its CRC, and the transfer never
    advanced: the sender ZRPOSed to zero until it spent its error budget
    and gave up. With random data that is any subpacket at all. A
    download to any receiver requesting ESCCTL was therefore impossible,
    not merely slow.

    lrzsz settles what the rule should be. In zsendline_init() (zm.c) the
    table entry for 015/0215 is:

    if (Zctlesc) tab = 1; /* always escape */
    else if (!turbo_escape) tab = 2; /* the conditional '@' rule */
    else tab = 0;

    So the conditional rule is the NON-ESCCTL case, and ESCCTL escapes CR
    like any other control character. This engine had the two swapped:
    the conditional rule was reachable only with ESCCTL on, where it is
    wrong, and never reachable with ESCCTL off, where it would be right.
    Since it could not fire correctly in either state, drop it -- fold CR
    into the control class and let the escape mask decide, which is
    exactly lrzsz's behaviour for both states. The '@' rule can be added
    properly later if wanted; the last_sent field it read is left in
    place, being public.

    The unescaped path is provably untouched: without ESCCTL neither the
    old CR class nor the new one is in the active mask, so CR is a normal
    byte either way. Confirmed byte-for-byte -- a 4 MB send still puts
    4,311,816 bytes on the wire, 256 MB still receives at 484.99 MB/s and
    sends at 206.24 MB/s, and the 3e-6 corruption gate still passes 5 of 5 receiving and 3 of 3 sending.

    With the fix, ESCCTL interoperates in every direction: sexyz to lrz,
    lrz to sexyz, and sexyz to itself all verify byte-identical. The
    strongest evidence it is now correct rather than merely accepted is
    the wire size -- sexyz puts 5,245,034 bytes on the wire where lsz puts 5,245,030 for the same file, so the two escape the same byte set.
    Random data exercises every one of the 256 values, so nothing is
    escaped by luck.

    Under injected errors the escaped path is a little less robust than
    the plain one: 4 of 5 at 3e-6 and 5 of 5 at 1e-6, against 5 of 5 for
    the unescaped path at 3e-6. It carries 25 % more bytes and so takes
    25 % more corruption at a given per-byte rate. No lrzsz baseline
    exists for that comparison -- lsz -e to lrz -e does not survive the
    harness's error injection at all, dying inside 0.04 s on every
    attempt.

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Windows@VERT to Git commit to main/sbbs/m on Sat Sep 5 15:45:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/fb3a6fff74c45ea1da0f8fdc
    Modified Files:
    docs/v322_new.md src/sbbs3/logon.cpp
    Log Message:
    Fix "Multiple Nodes" (G) user exemption being ignored at logon

    The logon-time check for a user already being on another node tests the
    user's G exemption ("Multiple Nodes"), but 02792b752c (2025-09-08)
    mechanically replaced that test with the new useron_is_guest() helper,
    which reads the G *restriction* (Guest) instead. Every other conversion
    in that commit was of a restriction test, so this lone exemption test
    changed behavior despite the commit's "No functional change" claim.

    The result, shipped in v3.21e: a user granted the G exemption was still disconnected with "You can't possibly be in two places at the same
    time", while Guest accounts gained an exemption they never had. Stock
    Guest accounts set both the G restriction and the G exemption (see exec/makeguest.js), so the swap was invisible there - only non-guest
    users holding the exemption saw the breakage.

    Fixes issue #1234.

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

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Windows@VERT to Git commit to main/sbbs/m on Sat Sep 5 18:40:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/64a15d67481a164b1db24e14
    Modified Files:
    docs/v322_new.md src/sbbs3/scansubs.cpp
    Log Message:
    Bug-fix: don't ask "Your Messages Only" during scan configuration for QWK nodes

    1f8a468b15 (2005-06-24) suppressed this prompt for QWK network nodes at
    the per-sub site but left the "all subs in group" branch testing
    useron.misc instead of useron.rest. FLAG('Q') is bit 16, which is
    unassigned in user.misc, so that test was always false and QWK nodes
    were still asked.

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

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From MRO@VERT/BBSESINF to Rob Swindell (on Windows on Sun Sep 6 02:35:00 2026
    Re: docs/v322_new.md src/sbbs3/scansubs.cpp
    By: Rob Swindell (on Windows 11) to Git commit to main/sbbs/master on Sat Sep 05 2026 06:40 pm

    https://gitlab.synchro.net/main/sbbs/-/commit/64a15d67481a164b1db24e14 Modified Files:
    docs/v322_new.md src/sbbs3/scansubs.cpp
    Log Message:
    Bug-fix: don't ask "Your Messages Only" during scan configuration for QWK nodes


    Why is it reporting your os version in your from: line?
    Is this a new feature?

    --
    https://i.imgur.com/MNKA5U0.png
    President of BBS Sysop's Union +++ https://bbses.info/union
    ---
    þ Synchronet þ ::: BBSES.info - free BBS services :::
  • From Digital Man@VERT to MRO on Mon Sep 7 14:41:00 2026
    Re: docs/v322_new.md src/sbbs3/scansubs.cpp
    By: MRO to Rob Swindell (on Windows on Sun Sep 06 2026 02:35 am

    Why is it reporting your os version in your from: line?

    My git commit messages have the OS of the system I made the commit from.

    Is this a new feature?

    No, it's been that way for years:
    https://git.synchro.net/gitpushlog.ssjs
    --
    digital man (rob)

    This Is Spinal Tap quote #40:
    Morty the Mime: Come on, don't talk back, mime is money, come on, move it. Norco, CA WX: 85.9øF, 60.0% humidity, 1 mph NNW wind, 0.04 inches rain/24hrs ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From MRO@VERT/BBSESINF to Digital Man on Mon Sep 7 19:58:00 2026
    Re: docs/v322_new.md src/sbbs3/scansubs.cpp
    By: Digital Man to MRO on Mon Sep 07 2026 02:41 pm

    Re: docs/v322_new.md src/sbbs3/scansubs.cpp
    By: MRO to Rob Swindell (on Windows on Sun Sep 06 2026 02:35 am

    Why is it reporting your os version in your from: line?

    My git commit messages have the OS of the system I made the commit from.

    Is this a new feature?

    No, it's been that way for years:
    https://git.synchro.net/gitpushlog.ssjs


    Oh, okay. I've never noticed it.

    --
    https://i.imgur.com/MNKA5U0.png
    President of BBS Sysop's Union +++ https://bbses.info/union
    ---
    þ Synchronet þ ::: BBSES.info - free BBS services :::
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Tue Sep 15 20:47:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/b7f7d2b3f506c3e449640733
    Modified Files:
    docs/v322_new.md src/sbbs3/chksmb.c writemsg.cpp src/smblib/smbadd.c smballoc.c smbdefs.h smblib.h smbtxt.c
    Log Message:
    smblib: raise the message data storage ceiling from 2GB to 4GB

    smb_allocdat() and smb_fallocdat() validated a data offset by testing (int)offset < 0, truncating a 64-bit off_t to 32 bits. Every allocation
    at or above 2GB read as negative and was refused with SMB_ERR_DAT_OFFSET,
    so a .sdt file could not grow past 2GB even though the format stores that offset in a uint32_t and so reaches 4GB - 1.

    The same truncation failed open at the top: at exactly 4GB, (int)offset
    is 0 and passes, and the header is then written with
    (uint32_t)4294967296 == 0, leaving the message's data at 4GB and its
    header pointing at the start of the file.

    Bound the end of the allocation against the format's own limit instead,
    named SMB_MAX_DAT_OFFSET/SMB_MAX_HDR_OFFSET, and repeat the check in smb_addmsg() where the offset is narrowed to uint32_t, which is the only
    guard for a hyper-allocated base. The data-length limit smb_addmsg()
    already enforced becomes SMB_MAX_DAT_LEN.

    Offsets on the way there were themselves 32-bit: ftell() is a 32-bit long
    on Windows, chsize() arguments were cast to int (one an int * int
    multiply that overflows at 2GB while truncating the data file), and the
    message text was read with a bare fseek() of hdr.offset + dfield.offset computed in 32-bit unsigned arithmetic.

    issue #12

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

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Tue Sep 15 20:47:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/aee485f478d18bf1aa97cb59
    Modified Files:
    docs/v322_new.md src/sbbs3/smbutil.c
    Log Message:
    smbutil: make pack error-safe

    packmsgs() renames the live .shd/.sdt/.sid aside before building their replacements, and three failure paths afterwards took a bare return with
    no message at all. A single unreadable message therefore left the base
    with no .shd, which a message server then treats as a base to create from scratch.

    Log those failures with the offending message number, skip a message
    whose data cannot be read rather than abandoning the pack, and restore
    the originals on any failure after the rename. The allocation tables are
    moved aside rather than truncated in place so that rollback is complete: against an emptied .sda, the next message written would land on top of
    live data. The replacements are flushed and checked before the swap, so a
    full disk no longer installs a truncated base, and the swap is now the
    last thing done rather than interleaved with the removals.

    Also stop discarding messages whose data exceeds 16MB. That limit was
    arbitrary and dropped the message from the base rather than leaving it uncompacted; the bound is now the format's own SMB_MAX_DAT_LEN.

    issue #1171

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

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net