Five consumer-feedback items, one milestone:
(1) Shared FTSC kludge plumbing in src/ma.kludge.pas
ParseKludgeLine, SplitKludgeBlob, BuildKludgePrefix,
BuildKludgeSuffix. Single source of truth for kludge naming,
INTL/FMPT/TOPT recognition, and the kludge.<lowername>
forward-compat passthrough. Eliminates the four near-identical
parsers MSG/PKT/Squish were carrying; JAM's FTSKLUDGE subfield
walking also routes through ParseKludgeLine so its unknown
kludges land in the same `kludge.<name>` slot as the others.
Bug fix folded in: the parser previously split kludge name from
value at the first ':' it found, which broke INTL (the value
contains an FTN address with ':' in it). Now picks the earlier
of space and colon, which handles both colon-form ("MSGID: foo")
and space-form ("INTL <to> <from>") kludges correctly.
(2) INTL / FMPT / TOPT slots in attributes registry
FSC-4008 cross-zone routing kludges every netmail tosser carries.
Added to JAM/Squish/MSG/PKT capability lists, parsed natively,
emitted on Write. Round-trip covered by tests.
(3) Unified `kludge.*` namespace for unknown FTSC kludges
Squish's `squish.kludge.<name>`, MSG's `msg.kludge.<name>`, and
PKT's `pkt.kludge.<name>` all collapse to plain `kludge.<name>`.
Consumers find passthrough kludges without switching on format.
JAM's numeric `jam.subfield.<id>` stays — those are JAM-specific
binary subfields, not FTSC-form kludges.
(4) `area` auto-populated from base.AreaTag on Read
When the caller passes AAreaTag to MessageBaseOpen (or sets
the AreaTag property post-construction), every successful
ReadMessage fills msg.Attributes['area'] unless the adapter
already populated it from on-disk data (e.g. PKT AREA kludge).
Saves echomail consumers from copying AreaTag into every
message attribute manually.
(5) TMsgAttributes multi-line helpers
GetList / SetList / AppendListItem on TMsgAttributes for the
multi-instance attributes (seen-by, path, via, trace) that
store with #13 between entries. Consumers don't have to roll
their own split/join.
Plus two PKT polish items from the same feedback round:
(6) ma.fmt.pkt.uni.DoWriteMessage now raises EMessageBase
explicitly with a pointer to the Native API instead of
silently returning False.
(7) TPktFile.CreateFromStream / CreateNewToStream constructors
accept any TStream (with optional ownership), so unit tests
that round-trip via TMemoryStream don't have to tempfile-dance.
FStream is now TStream; FOwnsStream gates Free in destructor.
TStringDynArray moved from ma.api.pas to ma.types.pas so both
the capabilities API and the new attribute helpers can share it.
Docs sweep:
- docs/attributes-registry.md: intl/fmpt/topt added; unknown-kludge
convention documented; multi-line helper section added.
- docs/architecture.md: ma.kludge layer surfaced; .uni adapter
registration gotcha called out loudly with the recommended
uses clause; area auto-pop documented.
- docs/API.md: TUniMessage section rewritten for Body+Attributes
model (was still pre-0.2); HWM API documented; PKT cheat-sheet
notes Native + CreateFromStream; tests/programs list updated.
- README.md: Building section flags the .uni gotcha first
thing; ma.kludge added to features.
tests/test_consumer_round1.pas: 7 new tests covering INTL/FMPT/
TOPT round-trip on JAM/Squish/MSG, area auto-pop, GetList/SetList/
AppendListItem, PKT raise, and TPktFile in-memory stream
round-trip.
Suite: 47/47 across 10 programs (test_consumer_round1 adds 7).
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. 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.
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 (
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.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.