NR caught a real durability gap during migration prep: the sequence base.WriteMessage(msg); source.MarkSent(srcMsg); looks atomic, but the OS write buffer hasn't necessarily reached the platter when MarkSent runs. Crash in that window = silent message drop. Add `Sync` virtual on TMessageBase (default no-op for read-only and in-memory backends). Six writable backends override and flush every open stream they own via SysUtils.FileFlush (fpfsync on Unix, FlushFileBuffers on Windows): JAM .JHR / .JDT / .JDX / .JLR Squish .SQD / .SQI / .SQL Hudson msginfo / msgidx / msghdr / msgtxt / msgtoidx / LASTREAD.BBS PCBoard MSGS file + index EzyCom header + text GoldBase msginfo / msgidx / msghdr / msgtxt / msgtoidx / LASTREAD.DAT MSG inherits no-op (per-write open/close — buffer flushes on close, but dir entry isn't fsynced; future enhancement). Wildcat inherits no-op (legacy `file` IO, not TFileStream). Helper for backend authors: TMessageBase.FlushStream(S: TStream) class method that handles the TFileStream cast + nil-safety. `Sync` raises after Close (data is finalized; nothing to flush). Test: TestSyncWriteable in test_consumer_round1 -- writes a message via JAM and Squish, calls Sync (no raise), Close, calls Sync again (raise expected). docs/API.md: new "Sync (durability)" section explaining the commit-after-fsync pattern with the canonical example. Symmetric to fpc-ftn-transport TPktWriter.Sync (commit ee8c6ad) that NR's review prompted on the transport side. Suite: 48/48 (added TestSyncWriteable to test_consumer_round1).
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 |
| 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 |
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
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. Native for JAM (.JLR), Squish (.SQL), Hudson + GoldBase (LASTREAD.BBS/DAT, withMapUser+Boardcontext). 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 (fpflockon Unix,LockFileExon Windows,.LCKsentinel fallback)- the existing
fmShareDenyWrite/fmShareDenyNoneshare modes.
- the existing
- Event hooks for logging, progress, and status reporting.
- Path / filename auto-derivation per format from a base directory plus
optional area tag (
areaattribute auto-populated on Read). - Shared FTSC kludge plumbing in
ma.kludge— single source of truth for kludge-line parse/emit (ParseKludgeLine,SplitKludgeBlob,BuildKludgePrefix/Suffix). Unknown FTSC kludges round-trip uniformly askludge.<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
ma.types, ma.events, ma.api,
ma.fmt.jam, ma.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/ ma.api, ma.types, ma.events, ma.lock, ma.paths, ma.kludge
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.