• Submission port (587/465) is not an MSA: authenticated submission requ

    From Rob Swindell@1:103/705 to GitLab issue in main/sbbs on Mon Aug 10 00:43:12 2026
    open https://gitlab.synchro.net/main/sbbs/-/issues/1221

    ## Summary

    The Mail Server listens on the submission port (587, `USE_SUBMISSION_PORT`) and on the implicit-TLS submission port (465, `TLS_SUBMISSION`), but it does not behave as a Message Submission Agent on them: those listeners run the exact same code path as port 25. Consequently:

    * An authenticated user cannot submit an original message to an external
    recipient without the sysop enabling `ALLOW_RELAY`, which is a server-wide
    option and not scoped to the submission port.
    * Once `ALLOW_RELAY` is enabled, an authenticated user may submit a message
    with an arbitrary `From:` address, including an address at a domain the
    system does not own. Nothing verifies the submitted sender against the
    authenticated user.
    * The submission port does not require authentication at all.

    Raised by Deuce on IRC after a submission from `deuce@bbsdev.net` to an external recipient, via the submission port, with a valid authenticated session, was refused and logged as `!ILLEGAL RELAY ATTEMPT`.

    ## What the RFCs say

    RFC 6409 (Oct 2011) obsoletes RFC 4409 (Apr 2006), which obsoletes RFC 2476 (Dec 1998). The submission/relay distinction is the *source* of the message, not the destination:

    **Message Submission Agent (MSA)**: A process that conforms to this specification. An MSA acts as a submission server to accept messages from MUAs, and it either delivers them or acts as an SMTP client to relay them to an MTA.

    **Message Transfer Agent (MTA)**: A process that conforms to [SMTP-MTA]. An MTA acts as an SMTP server to accept messages from an MSA or another MTA...

    -- RFC 6409 section 2.1

    So an authenticated user handing the server a new message addressed to `someone@example.com` is a **submission**, regardless of the recipient domain. It is a relay only when the message arrives from another MTA. This is the distinction port 587 exists for (RFC 6409 section 3.1: "Port 587 is reserved for email message submission").

    Relevant requirements:

    | Ref | Level | Requirement | Synchronet today |
    |---|---|---|---|
    | 6409 section 4.3 | **MUST** | "The MSA MUST, by default, issue an error response to the MAIL command if the session has not been authenticated using [SMTP-AUTH]", reply code 530 | Not implemented. An unauthenticated client on 587/465 is treated identically to one on 25. |
    | 6409 section 6.1 | MAY | "The MSA MAY issue an error response to a MAIL command if the address in MAIL FROM appears to have insufficient submission rights or is not authorized with the authentication used" | Not implemented. |
    | 6409 section 6.2 | MAY | "The MSA MAY issue an error response to a RCPT command if inconsistent with the permissions given to the user" | Partially: `ALLOW_RELAY` plus the `G`/`M` restrictions, but all-or-nothing and not per-port. |
    | 6409 section 7 | SHOULD | PIPELINING, ENHANCEDSTATUSCODES | Neither advertised (AUTH, which is MUST, is advertised). |
    | 6409 section 8.3 | SHOULD | Add or replace `Message-ID` if missing/invalid | Not implemented. |

    ## What the code actually does

    Verified in `src/sbbs3/mailsrvr.cpp` at master (2ff8d37d99):

    **1. The submission port is indistinguishable from port 25 after accept().** `servprot_submission` is defined at line 116 and referenced exactly once more, in the `xpms_add_list()` call at line 6523. The per-session struct records only `tls_port` (line 157), set at line 6734 as `(servprot == servprot_submissions)` -- i.e. only implicit-TLS-ness survives into the session. Nothing downstream can tell a submission-port session from a port-25 session, so no policy can be applied per-port even if one were written.

    **2. Submission to an external recipient is refused as a relay.** In the
    `RCPT TO:` handler (near line 4855):

    ```c
    if (p != alias_buf /* forced relay by alias */ &&
    (!(startup->options & MAIL_OPT_ALLOW_RELAY)
    || relay_user.number == 0
    || relay_user.rest & (FLAG('G') | FLAG('M'))) &&
    !find2strs(host_name, host_ip, relay_list, NULL)) {
    ... "!ILLEGAL RELAY ATTEMPT" ...
    ```

    With `ALLOW_RELAY` clear, an authenticated, unrestricted user is refused -- this is the reported symptom. The only lever is `ALLOW_RELAY`, which applies
    to every listener including port 25.

    **3. No sender authorization on an authenticated session.** Every sender check is gated on `relay_user.number == 0`, so authenticating *disables* them:

    * `MAIL FROM:` -- `chk_email_addr()` is skipped (near line 4595).
    * `From:` header -- `chk_email_addr()` is skipped (near line 3629).
    * The forged-From check (`compare_addrs(sender, sender_addr)`, near line 3730,
    which emits `!FORGED mail header 'FROM' field`) is skipped (near line 3729).

    Nothing compares the submitted address against the authenticated user's own address(es) or against the configured domain list.

    **4. The envelope sender is rewritten, but the `From:` header is not.** For an authenticated sender, `SENDERNETTYPE` is left `NET_NONE` (near line 3749), so `sendmail_thread()` derives the outbound `MAIL FROM:` from
    `usermailaddr(&scfg, str, msg.from)` (line 5821) rather than from the client-supplied reverse path. The visible `From:` header, however, is emitted verbatim:

    ```c
    if ((p = smb_get_hfield(msg, RFC822FROM, NULL)) != NULL)
    s = sockprintf(socket, prot, sess, "From: %s", p); /* use original RFC822 header field */
    ```

    (line 794). So the SMTP envelope cannot be forged by an authenticated user, but the header that recipients actually see can be.

    This has become more consequential now that the server signs outbound mail with DKIM: a message carrying a forged `From:` is signed with the system's key, lending its domain reputation to the forgery. It also means a legitimate submission whose `From:` is one of the system's domains can end up misaligned with an envelope sender derived from `usermailaddr()`, which matters for DMARC.

    ## Suggested direction

    Not a proposal for a specific patch, just the shape of it:

    1. **Record submission-ness in the session.** Add a field to `smtp_t` set from
    `servprot` at accept time, so per-port policy becomes expressible at all.
    This is one field; everything below depends on it.

    2. **Require authentication on the submission ports** (RFC 6409 section 4.3),
    with `530` on `MAIL` for an unauthenticated session. Default on, with an
    option to disable for sysops with existing unauthenticated automation
    pointed at 587.

    3. **Treat authenticated submission as submission, not relay.** Do not require
    `ALLOW_RELAY` for an authenticated session on the submission port.
    `ALLOW_RELAY` continues to govern port 25.

    4. **Verify the sender** (RFC 6409 section 6.1): reject `MAIL FROM:` and/or
    `From:` when the address does not belong to the authenticated user or is not
    at one of the system's domains, with `550 5.7.1`. Sysop-selectable strictness
    is probably needed -- at minimum: off / domain must be ours / address must be
    the user's.

    5. Optionally, per RFC 6409 section 8, add a `Message-ID` when the client omits
    one, and advertise `PIPELINING` and `ENHANCEDSTATUSCODES`.

    An ARS would be a natural way to express item 3/4's authority, which overlaps with issue #107 ("SMTP relay access requirements").

    -- *Authored by Claude (Claude Code), on behalf of @rswindell, from an IRC discussion between @rswindell and @Deuce*
    --- 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 Mon Aug 10 01:33:19 2026
    https://gitlab.synchro.net/main/sbbs/-/issues/1221#note_10134

    ## Scope of the change to `ALLOW_RELAY`

    Following up on item 3 of the suggested direction, since "does `ALLOW_RELAY` become port-25-only?" is the first question the fix raises.

    Effectively yes, but the precise framing is **`ALLOW_RELAY` keeps its current meaning and is only consulted on the transfer port (25)**. It is not redefined, narrowed, or given new semantics -- the submission ports simply stop asking it.

    ### What it gates today

    In the `RCPT TO:` handler, an external recipient is permitted if **any** of these hold:

    1. The address came from a forced relay by alias (`alias.cfg`).
    2. `ALLOW_RELAY` is set **and** the session is authenticated **and** the user
    lacks the `G`/`M` restrictions.
    3. The connecting host is listed in `relay.cfg`.

    Paths 1 and 3 bypass `ALLOW_RELAY` entirely. Note that path 3 permits *unauthenticated* relay from listed hosts -- a deliberate trusted-host feature, not something this option controls.

    So the only thing `ALLOW_RELAY` governs is **authenticated submission to an external destination**. That is already submission in the RFC 6409 sense; the problem is only that it can arrive on either port and is judged by the port-25 rule either way.

    ### What must stay port-independent

    * **The `G`/`M` restrictions.** "May this user send outbound internet mail?" is
    a property of the user, not of the port, and must keep applying on 587/465.
    Same for the ARS proposed in #107, if that lands.
    * **`relay.cfg`** -- trusted-host policy, orthogonal to submission.
    * **Alias-forced relay** -- routing, orthogonal to submission.

    Keeping these on their proper axis is what makes the change safe: the per-user "no outbound mail" control survives intact, so removing the `ALLOW_RELAY` gate from the submission port does not remove the sysop's ability to deny outbound mail to a given user.

    ### Behavior change to note in the release notes

    A sysop who deliberately left `ALLOW_RELAY` off to prevent users sending outbound internet mail, *and* who also enabled `USE_SUBMISSION_PORT`, would find
    their users able to send after this change.

    The exposure looks small: the submission port is opt-in, and a system with 587 enabled and `ALLOW_RELAY` clear currently has a submission port that cannot submit anything externally, which is close to pointless. Enabling 587 is already
    a statement of intent. Such a sysop also still has the correct tool -- user restrictions, or an ARS -- rather than a server-wide switch that happens to catch the case. Worth a release-note line rather than letting it be discovered.

    ### Naming and comment cleanup

    `ALLOW_RELAY` is already a misleading name: unauthenticated relay is never permitted by it. After this change it means "permit authenticated submission on port 25", which the word *relay* actively argues against.

    Its header comment in `mailsrvr.h` is also simply wrong today:

    ```c
    #define MAIL_OPT_ALLOW_RELAY (1 << 14) /* Allow relays from stored user IPs */ ```

    That describes `MAIL_OPT_SMTP_AUTH_VIA_IP` (bit 21), not this option.

    Suggest fixing the comment and the SCFG/docs wording, but keeping the ini key unchanged -- renaming it would break every existing `sbbs.ini` for no functional
    gain.

    -- *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 Mon Aug 10 01:45:08 2026
    https://gitlab.synchro.net/main/sbbs/-/issues/1221#note_10136

    ## Sender validation: why it is harder than it looks, and a way through

    Expanding item 4 of the suggested direction. Deuce's original ask had two levels, worth keeping distinct:

    * **Requirement:** "email received on the submission port must be from a valid
    BBS domain and allows authenticated users to send only those"
    * **Ideal:** "ideally it should verify the from is a valid email address of the
    authenticated user"

    ### The difficulty: the recipient side is deliberately permissive

    Synchronet accepts a wide set of forms as a local recipient address:

    | Form | Notes |
    |---|---|
    | Alias | via `matchuser()` -> `matchusername()` |
    | Real name, or `real.name` | only when `MM_REALNAME` is set |
    | `sysop`, `postmaster`, `sys_id` | when `ALLOW_SYSOP_ALIASES` is set |
    | `scfg.sys_op` (sysop's real name) | resolves to user 1 |
    | `name` plus `#tag` | subaddressing, via `smtp_splittag()` / `checktag()` |
    | User number | only when `alias.cfg` produced it (`p == alias_buf && IS_DIGIT(*p)`) |
    | `alias.cfg` entry | exact match or `*suffix` wildcard; the value may be a local name, a user number, or an arbitrary **external** address |
    | `sub:<code>` | posts to a sub-board |
    | `qwkid!user@host`, Fido `f1.n2.z3.fidonet` | routing forms |
    | Mail-processor `to` list | `mailproc_list[].to` |
    | `default_user` | catch-all: any otherwise-unknown recipient lands here |

    `matchusername()` is looser still: it walks both strings skipping every non-alphanumeric character, so `John Smith`, `John.Smith`, `John_Smith`, `JohnSmith` and `J.o.h.n.S.m.i.t.h` are all the same recipient.

    Reusing any of that for sender validation would be a mistake.

    ### Generate forward, do not look up in reverse

    The recipient question is "who does this deliver to?", where generosity is a feature. The sender question is "does this address belong to the authenticated user?", where every extra accepted form is another string a user can put in `From:` and have the server bless -- and, now, DKIM-sign.

    So build the small closed set of addresses that **are** the authenticated user, and compare against it:

    1. `usermailaddr()` of their alias, at `sys_inetaddr` and each domain in the
    domain list. Mandatory: this is exactly what the server itself puts in
    `From:` for that user.
    2. Their real name in dotted form at those domains, when `MM_REALNAME` is set
    (mirrors the recipient rule).
    3. The `alias` plus `#tag` variants, so subaddressing survives -- same mailbox,
    same person.
    4. Optionally their `user.netmail`, at the sysop's discretion.

    Compare the local part with `matchusername()` semantics, so dots and underscores remain irrelevant, and check the domain against `sys_inetaddr` plus the domain list. Cheap, closed, no reverse search.

    ### What must NOT feed sender validation

    The set above deliberately never consults `alias.cfg`, `default_user`, `sub:`, the mail-processor lists, or the QWK/Fido routing forms. All of those answer the
    delivery question. Two would be actively dangerous if reused:

    * **`default_user` makes every address valid.** On a catch-all system, any
    sender would validate.
    * **`alias.cfg` values may be external addresses.** Accepting those as senders
    is exactly the forgery being prevented.

    The one case that genuinely needs `alias.cfg`: a sysop who wants `sales@example.net` to route to user 5 *and* wants user 5 to send as it. Supporting that means consulting `alias.cfg` only for entries whose value resolves to the authenticated user's own number or alias -- never entries with external values, never `*` wildcards. That belongs behind an explicit strictness level, not in the default.

    ### Strictness levels and per-port defaults

    | Level | Meaning |
    |---|---|
    | off | No verification (today's behavior) |
    | domain | `From:` domain must be one of ours |
    | user | `From:` must be an address of the authenticated user (optionally plus self-resolving `alias.cfg` entries) |

    The only real argument for **off** is backward compatibility: some existing user
    legitimately sends through their BBS with an off-domain `From:`, which is the classic `ALLOW_RELAY` use case. That argument has no force on the submission port, which is opt-in and has no legacy behavior depending on it. Suggested defaults:

    * **Port 25:** `ALLOW_RELAY` as today, verification **off** by default. Nothing
    breaks.
    * **Ports 587/465:** verification always on, **user** by default, with **domain**
    available for sysops who need shared addresses.

    That makes the answer to Deuce's original question "enable the submission port" rather than "set these three options".

    It also resolves the disagreement rather than picking a side: sending from an arbitrary `From:` is long-standing port-25 behavior that cannot be yanked, and constraining submission to our own domains is what port 587 is *for*. Both hold once the policy is scoped per-port.

    ### Two spec details that bite a naive implementation

    * **`MAIL FROM:<>` must still be accepted.** RFC 6409 section 3.2: "a null
    return path, that is, MAIL FROM:<>, is permitted and MUST NOT, in itself, be
    cause for rejecting a message." A straight compare-to-user-address check
    rejects it.
    * **Adding `Sender:` is not a substitute for rejection.** RFC 6409 section 8.1
    lets the MSA stamp `Sender:` when `From:` is not the submitter, and that is
    the right behavior for a shared address *within our domains* -- `From:
    sales@example.net` submitted by user 5. It must **never** be used as a
    fallback that permits an off-domain `From:`: the message would still carry an
    arbitrary sender and still be DKIM-signed with the system's key, which is the
    outcome being prevented. Off-domain `From:` gets rejected, not annotated.

    Also note RFC 5322 permits multiple mailboxes in `From:`. Either require every one of them to pass, or reject the message.

    -- *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 Mon Aug 10 02:11:02 2026
    https://gitlab.synchro.net/main/sbbs/-/issues/1221#note_10137

    ## Implemented, with one correction to the analysis above

    Items 1-4 of the suggested direction are implemented and validated against
    a scratch mail server (isolated ctrl/data tree, loopback, high ports).

    ### Correction: `servprot` cannot identify the submission port

    The description above says the submission listener's identity "could be captured" from `servprot`, since `servprot_smtp` and `servprot_submission`
    are separate pointers. **That is wrong**, and item 1 as written does not
    work:

    ```c
    static const char* servprot_smtp = "SMTP";
    static const char* servprot_submission = "SMTP";
    ```

    Both initializers are the *same string literal*, and the compiler pools identical literals into one object, so `servprot_smtp == servprot_submission` is true. Verified with gcc at -O2: comparing the two yields "same pointer", while `servprot_submissions` ("SMTPS") is correctly distinct.

    The practical effect is that a `smtp->submission` flag set from a pointer comparison is **true on port 25 as well**. This was not visible by reading
    the code -- it showed up only when the test suite reported the transfer port answering `530 Authentication required.` to an unauthenticated `MAIL FROM`. `tls_port` is unaffected, because "SMTPS" is a distinct literal.

    The implementation instead derives it from the port actually bound, gated on the corresponding listener option, after the existing `getsockname()`:

    ```c
    server_port = inet_addrport(&server_addr);
    submission = ((startup->options & MAIL_OPT_USE_SUBMISSION_PORT) && server_port == startup->submission_port)
    || ((startup->options & MAIL_OPT_TLS_SUBMISSION) && server_port == startup->submissions_port);
    ```

    Anyone reading item 1 above should use this approach, not the pointer comparison.

    ### What landed

    * Submission ports require SMTP AUTH by default, `530` on `MAIL` otherwise.
    `[Mail]` `RequireSubmissionAuth` (default `true`) restores the old
    behavior for sysops with unauthenticated automation pointed at 587.
    * An authenticated user sending to an external recipient via a submission
    port no longer consults `ALLOW_RELAY`, which continues to govern port 25.
    The `G`/`M` restrictions still apply on every port.
    * Sender verification per port class: `[Mail]` `SenderValidation` (transfer
    port, default `None`) and `SubmissionSenderValidation` (submission ports,
    default `User`), each `None` / `Domain` / `User`. Both `MAIL FROM` and the
    `From:` header are checked. A null reverse-path is still accepted.
    * The acceptable-sender set is generated from the user record (alias, real
    name when `MM_REALNAME` is set, sub-address tags) rather than by reverse
    lookup, so `alias.cfg` entries with external values and a configured
    `DefaultUser` catch-all cannot authorize a sender.
    * `mail_startup_t` grows three fields, so `sbbsctrl.exe` needs a rebuild.

    ### Validation

    18 assertions over two configurations, all passing. Notably: unauthenticated `MAIL` gets `530` on 587 but `250` on 25; authenticated submission to an external recipient succeeds on 587 with `ALLOW_RELAY` clear but is still refused on 25; a foreign-domain or another local user's address is refused
    at `User` strictness and accepted at `Domain`; a forged `From:` header is caught during `DATA` even when `MAIL FROM` was valid.

    One early test failure turned out to be the test's fault rather than the code's: the scratch instance had inherited a `relay.cfg` listing `127.0.0.1`, and that trusted-host list legitimately bypasses the `ALLOW_RELAY` gate.

    ### Not done

    * **SCFG exposure.** The new settings are `sbbs.ini`-only for now. The Mail
    Server menu in `scfg/scfgsrvr.c` dispatches on positional `case` indices,
    so placing these next to "Allow Users to Relay Mail" means renumbering
    roughly fifteen subsequent cases -- worth doing as its own change.
    * **`Sender:` stamping** (RFC 6409 section 8.1) at `Domain` strictness, so a
    shared address records which user actually submitted. Without it, `Domain`
    permits one local user to send as another.
    * Item 5 (`Message-ID` generation, `PIPELINING`, `ENHANCEDSTATUSCODES`).
    Note that responses here use plain reply codes, without enhanced status
    codes, matching the rest of the file and the fact that
    `ENHANCEDSTATUSCODES` is not advertised.
    * Sessions authorized via `SMTP_AUTH_VIA_IP` resolve their user at `RCPT TO`,
    after `MAIL FROM` has been accepted, so sender validation does not apply to
    them. They still go through the existing `chk_email_addr()` checks.

    -- *Authored by Claude (Claude Code), on behalf of @rswindell*
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Deucе@1:103/705 to GitLab note in main/sbbs on Mon Aug 10 02:19:01 2026
    https://gitlab.synchro.net/main/sbbs/-/issues/1221#note_10138

    This seems overly complicated... there's code to decide which accout to deliver email to already, simply reusing that code and ensuring it resolves to the authenticated user should work.
    --- SBBSecho 3.37-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)
  • From Deucе@1:103/705 to GitLab note in main/sbbs on Mon Aug 10 02:23:07 2026
    https://gitlab.synchro.net/main/sbbs/-/issues/1221#note_10139

    I don't understand this concept that alias.cfg directed addresses don't belong to the user they deliver to. To my mind, if you receive eamil at a specific address, it is "your" address and you should be able to send email from it. The default_user argument also doesn't hold water.

    I do understand that the domain verification would still need to pass with an alias.cfg match, but that seems trivial compared to the effort you're going through to avoid re-using existing code.
    --- 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 Mon Aug 10 02:37:36 2026
    https://gitlab.synchro.net/main/sbbs/-/issues/1221#note_10140

    ## Settings collapsed to one

    The three settings described in the previous note are now one. Superseding
    that part of it:

    | Setting | Values | Default | Applies to |
    |---|---|---|---|
    | `[Mail]` `SubmissionSenderValidation` | `Domain` / `User` | `User` | 587/465 only |

    **`SenderValidation` is gone, and the transfer port no longer verifies
    senders at all.** Sender verification follows from the MSA role: RFC 6409 section 6.1 governs submission, while port 25 answers to RFC 5321, which asks for no such check. Extending it to the transfer port was scope the issue
    never called for.

    **`RequireSubmissionAuth` is gone, and authentication is now unconditional on the submission ports.** The only use for the escape hatch was pointing unauthenticated clients at 587, and port 25 already serves exactly that, so
    the option bought nothing the transfer port does not already offer. RFC 6409 section 4.3 says "MUST, by default", which would have permitted the knob;
    the redundancy is what rules it out.

    `None` is likewise gone as a validation level. On the submission ports the choice is `Domain` or `User` -- "no verification" is what port 25 is for.
    An unrecognized value, including a stale `None` from an earlier
    configuration, falls back to the default rather than disabling verification (verified against a running server, not just read out of `parseEnum`).

    This also shrinks the upgrade surface: port 25 behavior is now identical to before in every respect, so the only sysops affected are those who have deliberately enabled a submission port.

    ### Re-validated

    17 assertions over three configurations, all passing. Beyond the earlier
    set: the removed keys are inert when present in an `sbbs.ini` (an unauthenticated `MAIL` on 587 still gets `530` with `RequireSubmissionAuth=false` set, and a foreign `MAIL FROM` on port 25 is still accepted with `SenderValidation=User` set), and `SubmissionSenderValidation=Domain` accepts another local user's address
    while still refusing an off-domain one.

    -- *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 Mon Aug 10 02:53:14 2026
    https://gitlab.synchro.net/main/sbbs/-/issues/1221#note_10141

    ## Adopted, and the earlier objection was wrong

    @Deuce is right on both counts, and the code now works the way he describes.

    ### The alias.cfg objection

    My claim above -- that `alias.cfg`-directed addresses can't be trusted as senders because their values may be external -- does not survive tracing the recipient path. `alias()` substitutes first; if the result is an off-system address, `RCPT TO` takes the relay branch and never reaches the local-user lookup. So a helper that answers "which *local* user does this deliver to?" excludes external alias values by construction, with no special-casing. The risk I described isn't reachable, and I should have traced it before asserting it.

    The framing "if you receive email at a specific address, it is your address"
    is also just the better rule. It picks up user numbers, real-name forms, sub-address tags and every `matchusername()` equivalence for free, and it stays correct as the sysop edits `alias.cfg`, which a generated set does not.

    ### The default_user objection

    Half right, and it changed the design rather than the conclusion. Because the domain check still binds, honoring the catch-all would not open the system to arbitrary senders -- it would let one account send as anything at *your* domains. That is a narrower problem than I implied. But it is silent and surprising, and suppressing the fallback is a single argument to the lookup rather than a reason to avoid reuse. So: reuse the lookup, minus the `DefaultUser` fallback.

    ### One refinement to "reuse existing code"

    It wasn't callable as-is. The resolution lived inline in `smtp_client_thread`, interleaved with mail-processor matching, `sub:` handling, QWK routing and `rcptlst` writing. Reuse meant extracting it into `smtp_resolve_user()` and calling that from both paths, which is what landed.

    ### What this deletes

    The `SubmissionSenderValidation` setting is gone entirely, along with the `Domain` / `User` distinction. `Domain` existed to serve shared addresses like `sales@`, and an `alias.cfg` entry now covers that case at full strictness -- so there is nothing left to configure. `mail_startup_t` is back to its original layout, so the `sbbsctrl.exe` rebuild caveat is withdrawn.

    The rule is now simply: on a submission port, the `MAIL FROM` and `From:` address must be at one of this system's domains and must resolve to the authenticated user.

    ### Validated

    25 assertions. The recipient path is unchanged for every form it accepts (alias, dotted alias, `#tag`, `alias.cfg` to an alias, `alias.cfg` to a user number, sysop aliases, and the `DefaultUser` catch-all). On the sending side, `sales@` and `support@` from the scratch `alias.cfg` are now accepted for the user they deliver to, an alias pointing off-system is refused, and with `DefaultUser` set an arbitrary local address is still refused.

    `MAIL FROM:<>` remains accepted end-to-end, though a forged `From:` header on such a message is still caught -- RFC 6409 section 3.2 exempts the null return path "in itself", which covers the envelope, not the header.

    A rejection now names the address the session should be using, on the grounds that the likeliest cause is a mail client configured with a misspelling of the sender's own address rather than an actual forgery.

    -- *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 Mon Aug 10 03:18:36 2026
    https://gitlab.synchro.net/main/sbbs/-/issues/1221#note_10144

    Item 5 of the suggested direction (Message-ID generation, PIPELINING, ENHANCEDSTATUSCODES) is split out to #1222. All three are SHOULD-level, none depends on the others, and ENHANCEDSTATUSCODES in particular is a whole-file audit rather than a local change -- advertising it promises an enhanced code
    on every reply, which is why the submission work stuck to plain reply codes.

    Remaining here: sessions authorized via SMTP_AUTH_VIA_IP resolve their user at RCPT TO, after MAIL FROM has already been accepted, so sender validation does not reach them. They still go through the existing chk_email_addr() checks.

    -- *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 issue in main/sbbs on Mon Aug 10 10:18:15 2026
    close https://gitlab.synchro.net/main/sbbs/-/issues/1221
    --- 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 Mon Aug 10 10:18:16 2026
    https://gitlab.synchro.net/main/sbbs/-/issues/1221#note_10147

    Implemented in b8332a4618 (chart-3-cause, 2026-08-10).

    On the submission ports (587, and 465 for implicit TLS): SMTP authentication
    is always required (`530` otherwise, RFC 6409 section 4.3); an authenticated user sending to an external recipient is a submission rather than a relay and no longer needs `ALLOW_RELAY`, which continues to govern port 25; and the
    `MAIL FROM` and `From:` address must be at one of the system's domains and resolve to the authenticated user, via the same lookup that decides delivery. There is nothing to configure. The transfer port is unchanged in every
    respect.

    Split out rather than done here:

    * #1222 -- the remaining RFC 6409 SHOULDs (`Message-ID` generation,
    `PIPELINING`, `ENHANCEDSTATUSCODES`).
    * Sessions authorized via `SMTP_AUTH_VIA_IP` resolve their user at `RCPT TO`,
    after `MAIL FROM` has already been accepted, so sender validation does not
    reach them. They still go through the existing `chk_email_addr()` checks.
    Worth its own issue if that path matters to anyone; it is off by default.

    Thanks @Deuce -- the reuse-the-delivery-lookup approach is what made this
    land with no setting at all.

    -- *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 Mon Aug 10 21:15:48 2026
    https://gitlab.synchro.net/main/sbbs/-/issues/1221#note_10152

    Correction to the note above: the `SMTP_AUTH_VIA_IP` interaction is sharper than "sender validation does not reach them". On the submission ports those sessions cannot submit at all -- the IP lookup that establishes the authorization runs at `RCPT TO`, after the `MAIL FROM` gate that rejects them with `530`. Filed as #1224. Port 25 is unaffected.

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