• sbbsecho: netmail addressed by user number bypasses the deleted-user c

    From Rob Swindell@1:103/705 to GitLab issue in main/sbbs on Thursday, July 16, 2026 13:38:06
    open https://gitlab.synchro.net/main/sbbs/-/issues/1187

    Inbound FidoNet netmail addressed to a bare user number is delivered to that number without checking whether the account is actually active, so a remote system can deposit mail into a `DELETED` (or `INACTIVE`) user's slot.

    In `import_netmail()` (`src/sbbs3/sbbsecho.c:4862`):

    ```c
    usernumber = atoi(hdr.to);
    if (!usernumber && strListFind(cfg.sysop_alias_list, hdr.to, /* case sensitive: */ false) >= 0)
    usernumber = 1;
    if (!usernumber)
    usernumber = lookup_user(&scfg, &user_list, hdr.to);
    ```

    `atoi(hdr.to)` is taken straight from the packet and **short-circuits `lookup_user()`** — which is the one function here that skips deleted users (`src/sbbs3/userdat.c:5051`). A netmail addressed `To: 42` therefore bypasses that check entirely.

    Nothing downstream catches it. `fmsgtosmsg()` calls `getuserdat()` (`sbbsecho.c:3576`), which bounds-checks the number but returns `USER_SUCCESS` for a `DELETED` record, and no `user.misc & DELETED` test guards the `smb_hfield_str(&msg, RECIPIENTEXT, str)` at line 3591.

    For contrast, the SMTP `RCPT TO` path (`src/sbbs3/mailsrvr.cpp:4976-5036`) is the model: it verifies a by-number recipient via `username()` / `"DELETED USER"`, then `getuserdat()`, then explicitly rejects `DELETED | INACTIVE` with a 550.

    ## Impact

    Mail lands in a deleted user's slot and is invisible until that slot is recycled. Combined with the slot-reuse mail-purge gap (see the companion issue), it then surfaces in an unrelated new user's inbox. It's remote-triggerable and needs no corruption or misconfiguration.

    ## Suggested fix

    Reject an inactive account at the point where the number is resolved, so the message falls through to the existing `lookup_user()` / `default_recipient` / "Unknown user" handling rather than changing `fmsgtosmsg()`'s error semantics (returning `SMB_FAILURE` there risks packet retry behavior):

    ```c
    usernumber = atoi(hdr.to);
    if (usernumber) { /* Addressed by user number: don't accept an inactive account */
    user_t user = { .number = usernumber };
    if (getuserdat(&scfg, &user) != USER_SUCCESS || !user_is_active(&user))
    usernumber = 0;
    }
    ```

    `user_is_active()` (`src/sbbs3/userdat.c:4108`) is the codebase's canonical `(misc & (DELETED | INACTIVE)) == 0` test — the same pair `total_users()` uses and that SMTP rejects on.

    ## Related observation

    `lookup_user()` skips `DELETED` but not `INACTIVE`, so netmail addressed **by name** to an inactive account is still accepted, where SMTP would reject it. Worth deciding separately whether that's intended.

    — *Authored by Claude (Claude Code), on behalf of @rswindell*
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Rob Swindell@1:103/705 to GitLab note in main/sbbs on Thursday, July 16, 2026 13:38:13
    https://gitlab.synchro.net/main/sbbs/-/issues/1187#note_9589

    Companion issue: #1186 (New user can inherit the previous (deleted) user's e-mail when their record slot is reused) — what makes mail left in a deleted user's slot user-visible.

    — *Authored by Claude (Claude Code), on behalf of @rswindell*
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Rob Swindell@1:103/705 to GitLab note in main/sbbs on Thursday, July 16, 2026 13:47:19
    https://gitlab.synchro.net/main/sbbs/-/issues/1187#note_9591

    Follow-up: `lookup_user()` now uses `user_is_active()` too, so the by-name path rejects an inactive account the same way the by-number path and SMTP's `RCPT TO` do — "can this account receive mail?" no longer depends on how the sender spelled the address.

    Verified against a probe calling `lookup_user()` directly, with one active and one `INACTIVE` account in an isolated ctrl/data tree:

    ```
    BEFORE: lookup_user("Active Guy") = 1 lookup_user("Dormant Guy") = 2 AFTER: lookup_user("Active Guy") = 1 lookup_user("Dormant Guy") = 0
    ```

    Behavior change worth noting for sysops: netmail addressed by name to a deactivated account now falls through to `default_recipient` / "Unknown user" handling rather than landing in that user's mailbox. Its other callers are the QWK (`un_qwk.cpp:346`, `un_rep.cpp:501`) and echomail (`sbbsecho.c:6449`) "message posted to you" notifiers, which now skip a deactivated user as well.

    — *Authored by Claude (Claude Code), on behalf of @rswindell*
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)