Ken Johnson bbd9da8211 Bump to 0.6.1 -- kludge fixes from NR hpt-compare load
Four interlocking fixes that NR's 16-year archive load through
the library surfaced.  See CHANGELOG.md#0.6.1 for the full
per-bug writeup with JAM-001 / FTSC references.

1. ParseKludgeLine: first-wins on singleton kludges (msgid /
   reply / pid / tid / flags / chrs / tzutc / intl / fmpt / topt
   / area).  Quoted SOH-preserving body lines can no longer
   overwrite the prefix kludges -- stops tossers flagging
   legitimate messages as dupes of the quoted parent.

2. SplitKludgeBlob + BuildKludgePrefix added to Hudson, GoldBase,
   PCBoard, EzyCom, Wildcat uni adapters (all five were previously
   doing u.Body := native.Body verbatim, so msgid / pid / seen-by
   / path / chrs / tzutc / intl / vendor X-* kludges were all
   getting dropped on Read AND Write through those formats).

3. JAM-001 spec compliance:
   - TJamBase.CalcMsgIdCRC (new); JamFromUni auto-computes
     MsgIdCRC and ReplyCRC per message so external readers can
     walk reply chains (was writing 0 everywhere, collapsed
     threading on GoldED / MsgEd / hptlink).
   - PasswordCRC / ReplyCRC default to 0xFFFFFFFF sentinel, not
     0 (which is indistinguishable from a real CRC match).
   - InitHeader: DateCreated := Now, BaseMsgNum := 1,
     PasswordCRC := 0xFFFFFFFF (was all zeros).
   - JamFromUni appends trailing CR to body (FTS-0001 requires
     CR-terminated lines; PKT-sourced bodies have it, manually-
     built bodies may not -- library now ensures it).
   - CHRS and TID emitted as JAM_FTSKLUDGE subfields (were
     being dropped entirely; CHRS is load-bearing for non-ASCII
     display).
   - OADDRESS/DADDRESS no longer emitted (spec-valid but adds
     34 bytes/msg; HPT/fmail/GoldED don't; callers that need
     them can set jam.subfield.0 / .1 explicitly).
   - .jlr not created on base-init (HPT creates lazily on first
     lastread write; matching removes a file-tree diff).

4. Tests:
   - test_fuzz_kludge gains F-KL-4 (quoted ^AMSGID in body
     doesn't clobber prefix) and F-KL-5 (SOH mid-line is body
     text, enforcing the position-0 rule).
   - test_consumer_round1 gains TestJamCrcComputed,
     TestJamBodyTrailingCR, TestJamChrsTidRoundTrip,
     TestHudsonKludgeRoundTrip, TestGoldBaseKludgeRoundTrip.
   - test_roundtrip_attrs updated: Hudson now advertises
     msgid/seen-by via the new round-trip path.

All 21 consumer tests pass; 6-target build clean.
2026-04-20 10:07:50 -07:00

fpc-msgbase

A unified Free Pascal library for reading and writing classic BBS message bases.

Implements every supported format from the FTSC specifications and the original format authors' published documentation, behind one polymorphic API (TMessageBase). BBS software, mail tossers, message editors, and utilities can target a single interface regardless of the underlying format on disk.

Supported formats

Format Files Backend unit
Hudson MSGINFO/IDX/HDR/TXT/TOIDX.BBS mb.fmt.hudson.pas
JAM *.JHR *.JDT *.JDX *.JLR mb.fmt.jam.pas
Squish *.SQD *.SQI *.SQL mb.fmt.squish.pas
FTS-1 MSG numbered *.MSG per directory mb.fmt.msg.pas
PCBoard *.MSG + *.IDX mb.fmt.pcboard.pas
EzyCom MH#####.BBS / MT#####.BBS mb.fmt.ezycom.pas
GoldBase MSGINFO/IDX/HDR/TXT/TOIDX.DAT mb.fmt.goldbase.pas
Wildcat 4 WC SDK databases mb.fmt.wildcat.pas

FTN PKT is a transport-tier wire format and lives in the sibling fpc-ftn-transport library (units tt.pkt.reader, tt.pkt.writer, tt.pkt.format, tt.pkt.batch). The mbfPkt factory enum stays in fpc-msgbase so consumers wanting to iterate .pkt files via the unified API (MessageBaseOpen(mbfPkt, ...)) just add uses tt.pkt.reader alongside fpc-msgbase. PKT was here through 0.3.x; moved out in 0.4.0 because PKT is a wire format, not a storage format.

Features

  • One TMessageBase abstract class — read, write, pack, reindex through the same methods regardless of format.
  • Lossless two-area message model. TUniMessage = Body (just the message text) + Attributes (key/value bag holding from/to/subject/dates/ addresses/MSGID/SEEN-BY/PATH and per-format extras). Same shape as RFC 822 email. Round-trip preservation enforced by the test suite.
  • Capabilities APIbase.SupportsAttribute('attr.returnreceipt') lets UIs hide controls the underlying backend has no slot for. Each backend publishes its key list via ClassSupportedAttributes. Full per-format matrix in docs/attributes-registry.md.
  • Per-user High-Water Markbase.GetHWM('NetReader') / base.SetHWM(...) plus auto-bump via base.ActiveUser. Native for JAM (.JLR), Squish (.SQL), Hudson + GoldBase (LASTREAD.BBS/DAT, with MapUser + Board context). Tossers and scanners register as named users in the format's native lastread file, so multiple consumers coexist without colliding. Unsupported formats return -1 honestly.
  • Layered locking: in-process TRTLCriticalSection + cross-process advisory lock (fpflock on Unix, LockFileEx on Windows, .LCK sentinel fallback)
    • the existing fmShareDenyWrite / fmShareDenyNone share modes.
  • Event hooks for logging, progress, and status reporting.
  • Path / filename auto-derivation per format from a base directory plus optional area tag (area attribute auto-populated on Read).
  • Shared FTSC kludge plumbing in mb.kludge — single source of truth for kludge-line parse/emit (ParseKludgeLine, SplitKludgeBlob, BuildKludgePrefix/Suffix). Unknown FTSC kludges round-trip uniformly as kludge.<lowername> regardless of which backend stored them, so consumers don't switch on format to find passthrough kludges.

Building

Use both the native and .uni adapter units in your uses clause — the .uni adapter's initialization block is what registers the backend with the unified-API factory. Forgetting it produces EMessageBase: No backend registered for <format>.

uses
  mb.types, mb.events, mb.api,
  mb.fmt.jam, mb.fmt.jam.uni;     { both — .uni registers }

Native Linux:

fpc -Fusrc -Fusrc/formats examples/example_read.pas

Lazarus package:

lazbuild fpc-msgbase.lpk

The repo includes a fpc.cfg template covering the multi-target build (i386-go32v2, i386-win32, i386-linux, i386-os2).

Layout

src/                mb.api, mb.types, mb.events, mb.lock, mb.paths, mb.kludge
src/formats/        mb.fmt.<format>.pas — one per supported format
docs/               architecture, locking semantics, format notes
tests/              FPCUnit tests, sample data
examples/           small CLI programs that double as smoke tests

Unit-name convention follows the Fimail style: ma.<category>.<name>.pas (ma. is the library's namespace prefix; the project was originally named message_api, hence ma). All units use {$mode objfpc}{$H+}.

Documentation

Status

Early development. APIs may move until 1.0.

Current: 0.6.1 — see CHANGELOG.md for the full release history. Downstream consumers should pin by tag (v0.6.1), not commit hash.

0.2 is a breaking change vs 0.1. TUniMessage lost its 13 named fields (WhoFrom/WhoTo/Subject/MsgNum/Attr/etc.) in favour of a strict Body + Attributes two-area model. Migration:

{ before }                       { after }
msg.WhoFrom                       msg.Attributes.Get('from')
msg.Subject := 'foo';             msg.Attributes.SetValue('subject', 'foo');
msg.Attr := MSG_ATTR_LOCAL;       msg.Attributes.SetBool('attr.local', true);
msg.OrigAddr                      msg.Attributes.GetAddr('addr.orig')
msg.DateWritten                   msg.Attributes.GetDate('date.written')

This change makes the unified API lossless — kludges (MSGID, SEEN-BY, PATH, etc.) round-trip cleanly, where 0.1 silently dropped them. See docs/attributes-registry.md for the full key catalog.

Format backends are spec-driven implementations validated against real-world sample bases.

Description
Unified Free Pascal library for reading/writing classic BBS message bases: Hudson, JAM, Squish, FTS-1 *.MSG, PCBoard, EzyCom, GoldBase, Wildcat 4.
Readme 1.2 MiB
Languages
Pascal 99.5%
Shell 0.5%