open
https://gitlab.synchro.net/main/sbbs/-/work_items/1318
## Background
`show_msghdr()` and `list_msg()` (src/sbbs3/getmsg.cpp) display a message header line by formatting a text.dat template (`MsgSubj`, `MsgTo`, `MsgFrom`, `SubMsgLstFmt`, `MailWaitingLstFmt`, `MailOnSystemLstFmt`) together with one or more message header fields through `bprintf()`, which merges them into a single string and emits it under a single `bputs()` mode.
text.dat strings are always CP437. Header fields are CP437 or UTF-8 depending on the message (`MSG_HFIELDS_UTF8`). Because the two are merged before output, they must be in the same charset first, so since 99b0b63767 (2019-08-03) `msghdr_text()` converts the template to UTF-8 into an `sbbs_t` buffer whenever the fields are UTF-8, and `msghdr_field()` converts fields to CP437 for non-UTF-8 terminals.
That conversion is what broke in the case fixed by 620c500c87 (2026-10-31): the stock `MsgSubj` template is exactly 128 bytes as UTF-8, the buffer was 128 bytes, and after bb5ff19c18 (2026-07-29) the conversion failed, so the CP437 template was sent in `P_UTF8` mode and rendered as mojibake on UTF-8 terminals. The fix enlarged the buffer and made a failed conversion fall back to an all-CP437 line, but the design still requires a template to be re-encoded (and a fallback for when it can't be) purely so it can share a byte string with the field.
## Proposal
Stop merging template and field into one string. Add a header-line formatter that walks the text.dat format string itself:
- literal segments are emitted through `outchar()`, which already translates CP437 to the terminal's charset (UTF-8, PETSCII, ASCII);
- each `%s` argument is emitted with its own print mode (`P_UTF8` when the message's header fields are UTF-8, otherwise none), honoring any `%.Ns` precision as a column count rather than a byte count.
Consequences:
- `msghdr_text()`, `sbbs_t::msghdr_utf8_text` and the CP437 fallback path go away; no text.dat string is ever re-encoded.
- `msghdr_field()`'s UTF-8-to-CP437 conversion remains only for non-UTF-8 terminals, where it is a genuine lossy translation and not an artifact of the merge.
- `%.70s`-style truncation stops cutting multi-byte UTF-8 fields at a byte boundary and stops mis-sizing the column when the field is UTF-8.
The formatter only needs to understand the handful of conversions the header templates use (`%s`, `%.Ns`, `%u`, `%d`, `%c`, `%%`); anything else can be rejected at load time or passed through untouched.
Affected callers: `show_msghdr()`, `list_msg()`, and `msgtoqwk.cpp`'s `QWKTagLineFmt` conversion, which reuses the same buffer for the same reason.
— *Authored by Claude (Claude Code), on behalf of @rswindell*
---
■ Synchronet ■ Vertrauen ■ Home of Synchronet ■ [vert/cvs/bbs].synchro.net