Adds per-(user, board) HWM for the QuickBBS-family multi-board
formats. The same physical Hudson/GoldBase base file set holds
ALL boards (1..200 for Hudson, 1..500 for GoldBase) in one
LASTREAD.BBS / LASTREAD.DAT file, indexed by user number with
one word slot per board. Caller has to provide both pieces of
context before HWM operations make sense:
- base.MapUser('NetReader', 60001) - pick a numeric user ID
- base.Board := 5 - which board this scan is for
src/ma.api.pas:
- New TMessageBase.Board property (longint, default 0).
- Single-area formats (JAM, Squish) ignore it.
- Multi-board formats return -1 from GetHWM when Board <= 0.
src/formats/ma.fmt.hudson.pas:
- New HudsonLastRead record matching QuickBBS LASTREAD.BBS layout.
- TJamBase pattern: FLrStream lazy + EnsureLrStream +
GetLastRead(user, board) + SetLastRead(user, board, msgnum).
- SetLastRead extends file with zeros to reach the user slot,
matching QuickBBS convention.
- Uses fpOpen on Unix (same FPC auto-flock workaround as Squish).
src/formats/ma.fmt.goldbase.pas:
- Same shape, GoldBaseLastRead with GOLDBASE_MAX_BOARDS (500)
slots, file is LASTREAD.DAT.
Both .uni adapters wire DoSupportsHWM/DoGetHWMById/DoSetHWMById
to the new native methods, gating on Board > 0.
tests/test_hwm.pas: 3 new tests covering Hudson + GoldBase:
- TestHudsonRequiresMapUserAndBoard verifies -1 returns when
MapUser missing, Board missing, or both.
- TestHudsonSetGetPersistence covers two users on two boards
with cross-session persistence.
- TestGoldBaseSetGet covers a high board number (250) to
exercise the wider GOLDBASE_MAX_BOARDS range.
Updated docs/architecture.md HWM coverage map: Hudson and
GoldBase moved from deferred to native. EzyCom still deferred
(per-area layout differs); Wildcat/PCBoard still -1.
Suite: 40/40 across 9 programs (test_hwm now 11/11).
NetReader and similar consumers can now register tossers as
high-numbered users (60000+) and walk per-board HWM the way
Allfix has historically done. Tossers coexist with human BBS
users in the same LASTREAD file (different user slots).
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 | ma.fmt.hudson.pas |
| JAM | *.JHR *.JDT *.JDX *.JLR |
ma.fmt.jam.pas |
| Squish | *.SQD *.SQI *.SQL |
ma.fmt.squish.pas |
| FTS-1 MSG | numbered *.MSG per directory |
ma.fmt.msg.pas |
| FTN PKT | *.pkt (Type-2 / 2+ / 2.2) |
ma.fmt.pkt.pas |
| PCBoard | *.MSG + *.IDX |
ma.fmt.pcboard.pas |
| EzyCom | MH#####.BBS / MT#####.BBS |
ma.fmt.ezycom.pas |
| GoldBase | MSGINFO/IDX/HDR/TXT/TOIDX.DAT | ma.fmt.goldbase.pas |
| Wildcat 4 | WC SDK databases | ma.fmt.wildcat.pas |
Features
- One
TMessageBaseabstract 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 API —
base.SupportsAttribute('attr.returnreceipt')lets UIs hide controls the underlying backend has no slot for. Each backend publishes its key list viaClassSupportedAttributes. Full per-format matrix indocs/attributes-registry.md. - Per-user High-Water Mark —
base.GetHWM('NetReader')/base.SetHWM(...)plus auto-bump viabase.ActiveUser. Tossers and scanners register as named users in the format's native lastread file (.JLRfor JAM,.SQLfor Squish), so multiple consumers coexist without colliding. Unsupported formats return -1 honestly. - Layered locking: in-process
TRTLCriticalSection+ cross-process advisory lock (fpflockon Unix,LockFileExon Windows,.LCKsentinel fallback)- the existing
fmShareDenyWrite/fmShareDenyNoneshare modes.
- the existing
- Event hooks for logging, progress, and status reporting.
TPacketBatchworker pool for tossers that need to process many.pktfiles concurrently while serialising writes per destination base.- Path / filename auto-derivation per format from a base directory plus optional area tag.
Building
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/ ma.api, ma.types, ma.events, ma.lock, ma.paths, ma.batch
src/formats/ ma.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
docs/API.md— full API reference with examplesdocs/architecture.md— layered design + Body/Attributes contractdocs/attributes-registry.md— canonical attribute keys + per-format support matrixdocs/ftsc-compliance.md— spec notesdocs/format-notes/— per-format quirks
Status
Early development. APIs may move until 1.0.
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.