2026-04-15 08:46:42 -07:00
|
|
|
{
|
|
|
|
|
test_write_existing.pas - write into copies of real populated
|
|
|
|
|
bases and verify integrity.
|
|
|
|
|
|
|
|
|
|
Never touches the source trees:
|
|
|
|
|
~/fidonet/msg/jam/10thamd.* (291 messages on disk)
|
|
|
|
|
~/fidonet/msg/netmail/ (27 numbered *.msg files)
|
|
|
|
|
|
|
|
|
|
Both are copied into /tmp scratch dirs first. Tests append
|
|
|
|
|
messages, reopen, verify counts and that pre-existing messages
|
|
|
|
|
are still readable.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
program test_write_existing;
|
|
|
|
|
|
|
|
|
|
{$mode objfpc}{$H+}
|
|
|
|
|
|
|
|
|
|
uses
|
|
|
|
|
SysUtils,
|
|
|
|
|
testutil,
|
0.7.0: extract TFTNAddress into leaf mb.address unit
Ask 1 from fpc-binkp consumer thread: non-storage libraries
(fpc-ftn-transport, fpc-binkp, future fpc-comet-proto / fpc-emsi,
SQL-backed messaging like Fastway) only need TFTNAddress, not the
full 1041-line mb.types. Extract to src/mb.address.pas (~90 lines,
only SysUtils) so they can cp a single file into their project.
mb.types continues to uses mb.address so existing callers see the
type transitively -- BUT FPC does not propagate record-field access
through re-export, so consumers that touch TFTNAddress.Zone/Net/
Node/Point directly must add mb.address to their own uses clause.
All 7 in-tree .uni adapters, 2 examples, 5 test harnesses updated.
No behavioural change. Full suite passes, multi-target build
green (x86_64-linux, i386-{linux,freebsd,win32,os2,go32v2}).
2026-04-21 10:56:58 -07:00
|
|
|
mb.address, mb.types, mb.events, mb.api,
|
Rename ma.* -> mb.* namespace (cosmetic, breaking)
Across-the-board rename so the unit prefix matches the repo
name (mb = msgbase). Brings naming into line with
fpc-ftn-transport's tt.* prefix and avoids the historical
"ma" abbreviation that meant nothing to new readers.
Files renamed via git mv:
src/ma.{api,events,kludge,lock,paths,types}.pas
-> src/mb.{...}.pas
src/formats/ma.fmt.{jam,squish,hudson,msg,pcboard,ezycom,
goldbase,wildcat,wcutil}{,.uni}.pas
-> src/formats/mb.fmt.*.pas
All `unit ma.X` declarations and `uses ma.X` clauses rewritten
to `mb.X` across src/, examples/, tests/.
Suite: 47/47 (read 7, hwm 11, lock 4, pack 4, write 5,
wildcat 5, consumer_round1 5, batch's gone w/ PKT relocation,
plus testutil).
Consumer impact: anyone with `uses ma.api;` etc. needs to
update to `uses mb.api;`. No semantic changes; a search/replace
on the consumer's source tree is the only migration step.
NR's notes (~/.MSGAPI_MSGS.md round 3) align this against
their already-pinned 8130b40; the next NR pin bump rolls in
both this rename and any further work in one step.
2026-04-18 13:19:15 -07:00
|
|
|
mb.fmt.jam, mb.fmt.jam.uni,
|
|
|
|
|
mb.fmt.msg, mb.fmt.msg.uni,
|
|
|
|
|
mb.fmt.hudson, mb.fmt.hudson.uni;
|
2026-04-15 08:46:42 -07:00
|
|
|
|
|
|
|
|
const
|
|
|
|
|
JAM_SRC = '/home/ken/fidonet/msg/jam/10thamd';
|
|
|
|
|
JAM_SCRATCH = '/tmp/ma_write_jam';
|
|
|
|
|
MSG_SRC = '/home/ken/fidonet/msg/netmail';
|
|
|
|
|
MSG_SCRATCH = '/tmp/ma_write_msg';
|
|
|
|
|
HUDSON_SCRATCH = '/tmp/ma_write_hudson';
|
|
|
|
|
HUDSON_SEED_N = 50; { seed from JAM source }
|
|
|
|
|
|
|
|
|
|
function RunShell(const Cmd: string): integer;
|
|
|
|
|
begin
|
|
|
|
|
Result := ExecuteProcess('/bin/sh', ['-c', Cmd]);
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
function MakeMsg(N: longint): TUniMessage;
|
|
|
|
|
begin
|
Lossless message model: Body + Attributes (showstopper fix)
Replaces TUniMessage's 13-field flat record with a strict two-area
model: Body holds only the message text; Attributes holds everything
else (from/to/subject/dates/addresses/MSGID/SEEN-BY/PATH/format-
specific fields) as namespaced key/value pairs.
Why this fix is required NOW: the previous JAM adapter dropped
MSGID, ReplyID, PID, Flags, SEEN-BY and PATH on every Read/Write
through the unified API. A NetReader parity test surfaced it (17/21
pass with 4 kludge failures). All 9 adapters had the same bug. For
tossers and scanners the impact is silent corruption: dropped MSGID
→ dupe storms, dropped PATH → mail loops, dropped SEEN-BY → broken
routing. Three downstream consumers (Fimail's codex-transport branch,
NetReader, future Allfix) had halted integration work pending this
fix. Without it, anyone vendoring fpc-msgbase 0.1 ships with a
known-corrupting adapter.
Design choice: per Ken's call, "message is just the message text;
everything else is an attribute, including from/to/subject/dates."
Same architecture as RFC 822 email (headers + body). Each backend
fills attributes it knows on Read; reads attributes it understands
on Write; ignores unknown attributes silently (RFC 822 X-header
semantics). Forward-compatible -- a new backend (e.g. a planned SQL
message store) just adds its own attribute keys; old backends ignore
them.
Composition is the consumer's job. The library never reassembles
Body + Attributes into kludge-laden display text. A BBS that wants
inline kludges walks Attributes and prepends ^aMSGID etc. to its
own display. A tosser that needs MSGID for dupe detection reads
Attributes.Get('msgid') directly -- no body parsing required.
src/ma.types.pas:
- New TMsgAttribute / TMsgAttributes records with Get/SetValue,
typed accessors (GetInt/GetBool/GetDate/GetAddr), Has/Remove,
iteration. Linear-search lookup, fine for the ~30-50 keys per
message. Switch to hash later if profiling shows need.
- Replaced TUniMessage with the minimal Body + Attributes record.
- New UniAttrBitsToAttributes / UniAttrBitsFromAttributes helpers
to bridge the canonical MSG_ATTR_* cardinal bitset to/from
individual `attr.*` boolean keys.
- {$modeswitch advancedrecords} added so records have methods.
src/ma.api.pas:
- New capabilities API: TStringDynArray return type,
ClassSupportedAttributes (virtual class fn, default empty),
SupportedAttributes (instance sugar), SupportsAttribute (per-key
query). Each backend overrides ClassSupportedAttributes with the
static list of keys it knows. Callers query before setting so a
BBS UI can hide controls the underlying backend has no slot for.
src/formats/ma.fmt.*.uni.pas (all 9):
- Rewrote each XxxToUni and XxxFromUni for the new model. Read
populates Attributes with universal/FTSC/format-specific keys per
the attribute registry (to be published in phase 5). Write reads
attributes back and writes native form.
- JAM walks SubFields[] for SEEN-BY/PATH/TZUTC/TRACE plus passthrough
of unknown subfield IDs as `jam.subfield.<id>` for round-trip
safety. Squish parses CtrlInfo (NUL-separated ^A lines) into
individual attributes, rebuilds on Write. MSG and PKT (which keep
kludges inline in body per FTS-1) parse leading ^A lines and
trailing SEEN-BY/PATH out of the body so TUniMessage.Body is
always plain user text; on Write they reassemble the on-disk form.
- Each backend ships ClassSupportedAttributes with its key list.
src/ma.batch.pas: PktToUni signature updated to (in,out var) form.
tests/* + examples/*: migrated all callers from Msg.WhoFrom (etc.)
to Msg.Attributes.Get('from'). MakeMsg helpers now use SetValue/
SetBool/SetAddr.
Verified: 24/24 tests pass across all 7 test programs (read,
roundtrip, lock, batch, wildcat, write_existing, pack). Wildcat
walks all 7 vendored conferences clean.
Out of scope (next phases):
- docs/attributes-registry.md publishing the full key list with
per-format support matrix
- cross-format round-trip + capabilities-driven copy test
- update architecture.md / PROPOSAL.md to reflect the new model
2026-04-17 14:11:15 -07:00
|
|
|
Result.Attributes.Clear;
|
|
|
|
|
Result.Attributes.SetValue('from', 'WriteTester' + IntToStr(N));
|
|
|
|
|
Result.Attributes.SetValue('to', 'Receiver' + IntToStr(N));
|
|
|
|
|
Result.Attributes.SetValue('subject', 'written #' + IntToStr(N));
|
|
|
|
|
Result.Attributes.SetDate('date.written',
|
|
|
|
|
EncodeDate(2026, 4, 15) + EncodeTime(10, N mod 60, 0, 0));
|
|
|
|
|
Result.Attributes.SetBool('attr.local', true);
|
|
|
|
|
Result.Attributes.SetBool('attr.echo', true);
|
|
|
|
|
Result.Attributes.SetAddr('addr.orig', MakeFTNAddress(1, 100, 200, 0));
|
|
|
|
|
Result.Attributes.SetAddr('addr.dest', MakeFTNAddress(1, 100, 300, 0));
|
|
|
|
|
Result.Attributes.SetValue('area', 'TEST');
|
|
|
|
|
Result.Body := 'Appended message body ' + IntToStr(N) + #13;
|
2026-04-15 08:46:42 -07:00
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TestJamAppend;
|
|
|
|
|
const
|
|
|
|
|
APPEND_COUNT = 10;
|
|
|
|
|
var
|
|
|
|
|
base: TMessageBase;
|
|
|
|
|
msg: TUniMessage;
|
|
|
|
|
i, preCount: longint;
|
|
|
|
|
begin
|
|
|
|
|
TestBegin(SysUtils.Format(
|
|
|
|
|
'JAM: append %d messages to real 291-msg base', [APPEND_COUNT]));
|
|
|
|
|
|
|
|
|
|
if not FileExists(JAM_SRC + '.jhr') then begin
|
|
|
|
|
WriteLn('SKIP (source missing)');
|
|
|
|
|
exit;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
{ Fresh scratch copy. }
|
|
|
|
|
if DirectoryExists(ExtractFilePath(JAM_SCRATCH + '.')) then
|
|
|
|
|
RunShell(SysUtils.Format('rm -f %s/10thamd.*', [ExtractFilePath(JAM_SCRATCH + '.')]));
|
|
|
|
|
ForceDirectories(ExtractFilePath(JAM_SCRATCH + '.'));
|
|
|
|
|
RunShell(SysUtils.Format('cp %s.jhr %s.jhr', [JAM_SRC, JAM_SCRATCH]));
|
|
|
|
|
RunShell(SysUtils.Format('cp %s.jdt %s.jdt', [JAM_SRC, JAM_SCRATCH]));
|
|
|
|
|
RunShell(SysUtils.Format('cp %s.jdx %s.jdx', [JAM_SRC, JAM_SCRATCH]));
|
|
|
|
|
RunShell(SysUtils.Format('cp %s.jlr %s.jlr', [JAM_SRC, JAM_SCRATCH]));
|
|
|
|
|
RunShell(SysUtils.Format('chmod u+w %s.*', [JAM_SCRATCH]));
|
|
|
|
|
|
|
|
|
|
{ Record pre-count. }
|
|
|
|
|
base := MessageBaseOpen(mbfJam, JAM_SCRATCH, momReadOnly);
|
|
|
|
|
try
|
|
|
|
|
AssertTrue('Pre-open RO', base.Open);
|
|
|
|
|
preCount := base.MessageCount;
|
|
|
|
|
AssertEquals('Pre-count', 291, preCount);
|
|
|
|
|
finally
|
|
|
|
|
base.Close;
|
|
|
|
|
base.Free;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
{ Append phase. }
|
|
|
|
|
base := MessageBaseOpen(mbfJam, JAM_SCRATCH, momReadWrite);
|
|
|
|
|
try
|
|
|
|
|
AssertTrue('Open RW', base.Open);
|
|
|
|
|
for i := 1 to APPEND_COUNT do begin
|
|
|
|
|
msg := MakeMsg(i);
|
|
|
|
|
AssertTrue('WriteMessage ' + IntToStr(i), base.WriteMessage(msg));
|
|
|
|
|
end;
|
|
|
|
|
AssertEquals('Count after append',
|
|
|
|
|
preCount + APPEND_COUNT, base.MessageCount);
|
|
|
|
|
finally
|
|
|
|
|
base.Close;
|
|
|
|
|
base.Free;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
{ Reopen + verify. }
|
|
|
|
|
base := MessageBaseOpen(mbfJam, JAM_SCRATCH, momReadOnly);
|
|
|
|
|
try
|
|
|
|
|
AssertTrue('Reopen RO', base.Open);
|
|
|
|
|
AssertEquals('Count after reopen',
|
|
|
|
|
preCount + APPEND_COUNT, base.MessageCount);
|
|
|
|
|
|
|
|
|
|
{ First original message should still be Robert Wolfe. }
|
|
|
|
|
AssertTrue('Read original msg 0', base.ReadMessage(0, msg));
|
Lossless message model: Body + Attributes (showstopper fix)
Replaces TUniMessage's 13-field flat record with a strict two-area
model: Body holds only the message text; Attributes holds everything
else (from/to/subject/dates/addresses/MSGID/SEEN-BY/PATH/format-
specific fields) as namespaced key/value pairs.
Why this fix is required NOW: the previous JAM adapter dropped
MSGID, ReplyID, PID, Flags, SEEN-BY and PATH on every Read/Write
through the unified API. A NetReader parity test surfaced it (17/21
pass with 4 kludge failures). All 9 adapters had the same bug. For
tossers and scanners the impact is silent corruption: dropped MSGID
→ dupe storms, dropped PATH → mail loops, dropped SEEN-BY → broken
routing. Three downstream consumers (Fimail's codex-transport branch,
NetReader, future Allfix) had halted integration work pending this
fix. Without it, anyone vendoring fpc-msgbase 0.1 ships with a
known-corrupting adapter.
Design choice: per Ken's call, "message is just the message text;
everything else is an attribute, including from/to/subject/dates."
Same architecture as RFC 822 email (headers + body). Each backend
fills attributes it knows on Read; reads attributes it understands
on Write; ignores unknown attributes silently (RFC 822 X-header
semantics). Forward-compatible -- a new backend (e.g. a planned SQL
message store) just adds its own attribute keys; old backends ignore
them.
Composition is the consumer's job. The library never reassembles
Body + Attributes into kludge-laden display text. A BBS that wants
inline kludges walks Attributes and prepends ^aMSGID etc. to its
own display. A tosser that needs MSGID for dupe detection reads
Attributes.Get('msgid') directly -- no body parsing required.
src/ma.types.pas:
- New TMsgAttribute / TMsgAttributes records with Get/SetValue,
typed accessors (GetInt/GetBool/GetDate/GetAddr), Has/Remove,
iteration. Linear-search lookup, fine for the ~30-50 keys per
message. Switch to hash later if profiling shows need.
- Replaced TUniMessage with the minimal Body + Attributes record.
- New UniAttrBitsToAttributes / UniAttrBitsFromAttributes helpers
to bridge the canonical MSG_ATTR_* cardinal bitset to/from
individual `attr.*` boolean keys.
- {$modeswitch advancedrecords} added so records have methods.
src/ma.api.pas:
- New capabilities API: TStringDynArray return type,
ClassSupportedAttributes (virtual class fn, default empty),
SupportedAttributes (instance sugar), SupportsAttribute (per-key
query). Each backend overrides ClassSupportedAttributes with the
static list of keys it knows. Callers query before setting so a
BBS UI can hide controls the underlying backend has no slot for.
src/formats/ma.fmt.*.uni.pas (all 9):
- Rewrote each XxxToUni and XxxFromUni for the new model. Read
populates Attributes with universal/FTSC/format-specific keys per
the attribute registry (to be published in phase 5). Write reads
attributes back and writes native form.
- JAM walks SubFields[] for SEEN-BY/PATH/TZUTC/TRACE plus passthrough
of unknown subfield IDs as `jam.subfield.<id>` for round-trip
safety. Squish parses CtrlInfo (NUL-separated ^A lines) into
individual attributes, rebuilds on Write. MSG and PKT (which keep
kludges inline in body per FTS-1) parse leading ^A lines and
trailing SEEN-BY/PATH out of the body so TUniMessage.Body is
always plain user text; on Write they reassemble the on-disk form.
- Each backend ships ClassSupportedAttributes with its key list.
src/ma.batch.pas: PktToUni signature updated to (in,out var) form.
tests/* + examples/*: migrated all callers from Msg.WhoFrom (etc.)
to Msg.Attributes.Get('from'). MakeMsg helpers now use SetValue/
SetBool/SetAddr.
Verified: 24/24 tests pass across all 7 test programs (read,
roundtrip, lock, batch, wildcat, write_existing, pack). Wildcat
walks all 7 vendored conferences clean.
Out of scope (next phases):
- docs/attributes-registry.md publishing the full key list with
per-format support matrix
- cross-format round-trip + capabilities-driven copy test
- update architecture.md / PROPOSAL.md to reflect the new model
2026-04-17 14:11:15 -07:00
|
|
|
AssertEquals('Msg[0].WhoFrom', 'Robert Wolfe', msg.Attributes.Get('from'));
|
2026-04-15 08:46:42 -07:00
|
|
|
|
|
|
|
|
{ Appended messages should be at indexes preCount..preCount+9. }
|
|
|
|
|
for i := 1 to APPEND_COUNT do begin
|
|
|
|
|
AssertTrue('Read appended ' + IntToStr(i),
|
|
|
|
|
base.ReadMessage(preCount + i - 1, msg));
|
|
|
|
|
AssertEquals('Appended WhoFrom',
|
Lossless message model: Body + Attributes (showstopper fix)
Replaces TUniMessage's 13-field flat record with a strict two-area
model: Body holds only the message text; Attributes holds everything
else (from/to/subject/dates/addresses/MSGID/SEEN-BY/PATH/format-
specific fields) as namespaced key/value pairs.
Why this fix is required NOW: the previous JAM adapter dropped
MSGID, ReplyID, PID, Flags, SEEN-BY and PATH on every Read/Write
through the unified API. A NetReader parity test surfaced it (17/21
pass with 4 kludge failures). All 9 adapters had the same bug. For
tossers and scanners the impact is silent corruption: dropped MSGID
→ dupe storms, dropped PATH → mail loops, dropped SEEN-BY → broken
routing. Three downstream consumers (Fimail's codex-transport branch,
NetReader, future Allfix) had halted integration work pending this
fix. Without it, anyone vendoring fpc-msgbase 0.1 ships with a
known-corrupting adapter.
Design choice: per Ken's call, "message is just the message text;
everything else is an attribute, including from/to/subject/dates."
Same architecture as RFC 822 email (headers + body). Each backend
fills attributes it knows on Read; reads attributes it understands
on Write; ignores unknown attributes silently (RFC 822 X-header
semantics). Forward-compatible -- a new backend (e.g. a planned SQL
message store) just adds its own attribute keys; old backends ignore
them.
Composition is the consumer's job. The library never reassembles
Body + Attributes into kludge-laden display text. A BBS that wants
inline kludges walks Attributes and prepends ^aMSGID etc. to its
own display. A tosser that needs MSGID for dupe detection reads
Attributes.Get('msgid') directly -- no body parsing required.
src/ma.types.pas:
- New TMsgAttribute / TMsgAttributes records with Get/SetValue,
typed accessors (GetInt/GetBool/GetDate/GetAddr), Has/Remove,
iteration. Linear-search lookup, fine for the ~30-50 keys per
message. Switch to hash later if profiling shows need.
- Replaced TUniMessage with the minimal Body + Attributes record.
- New UniAttrBitsToAttributes / UniAttrBitsFromAttributes helpers
to bridge the canonical MSG_ATTR_* cardinal bitset to/from
individual `attr.*` boolean keys.
- {$modeswitch advancedrecords} added so records have methods.
src/ma.api.pas:
- New capabilities API: TStringDynArray return type,
ClassSupportedAttributes (virtual class fn, default empty),
SupportedAttributes (instance sugar), SupportsAttribute (per-key
query). Each backend overrides ClassSupportedAttributes with the
static list of keys it knows. Callers query before setting so a
BBS UI can hide controls the underlying backend has no slot for.
src/formats/ma.fmt.*.uni.pas (all 9):
- Rewrote each XxxToUni and XxxFromUni for the new model. Read
populates Attributes with universal/FTSC/format-specific keys per
the attribute registry (to be published in phase 5). Write reads
attributes back and writes native form.
- JAM walks SubFields[] for SEEN-BY/PATH/TZUTC/TRACE plus passthrough
of unknown subfield IDs as `jam.subfield.<id>` for round-trip
safety. Squish parses CtrlInfo (NUL-separated ^A lines) into
individual attributes, rebuilds on Write. MSG and PKT (which keep
kludges inline in body per FTS-1) parse leading ^A lines and
trailing SEEN-BY/PATH out of the body so TUniMessage.Body is
always plain user text; on Write they reassemble the on-disk form.
- Each backend ships ClassSupportedAttributes with its key list.
src/ma.batch.pas: PktToUni signature updated to (in,out var) form.
tests/* + examples/*: migrated all callers from Msg.WhoFrom (etc.)
to Msg.Attributes.Get('from'). MakeMsg helpers now use SetValue/
SetBool/SetAddr.
Verified: 24/24 tests pass across all 7 test programs (read,
roundtrip, lock, batch, wildcat, write_existing, pack). Wildcat
walks all 7 vendored conferences clean.
Out of scope (next phases):
- docs/attributes-registry.md publishing the full key list with
per-format support matrix
- cross-format round-trip + capabilities-driven copy test
- update architecture.md / PROPOSAL.md to reflect the new model
2026-04-17 14:11:15 -07:00
|
|
|
'WriteTester' + IntToStr(i), msg.Attributes.Get('from'));
|
2026-04-15 08:46:42 -07:00
|
|
|
AssertEquals('Appended Subject',
|
Lossless message model: Body + Attributes (showstopper fix)
Replaces TUniMessage's 13-field flat record with a strict two-area
model: Body holds only the message text; Attributes holds everything
else (from/to/subject/dates/addresses/MSGID/SEEN-BY/PATH/format-
specific fields) as namespaced key/value pairs.
Why this fix is required NOW: the previous JAM adapter dropped
MSGID, ReplyID, PID, Flags, SEEN-BY and PATH on every Read/Write
through the unified API. A NetReader parity test surfaced it (17/21
pass with 4 kludge failures). All 9 adapters had the same bug. For
tossers and scanners the impact is silent corruption: dropped MSGID
→ dupe storms, dropped PATH → mail loops, dropped SEEN-BY → broken
routing. Three downstream consumers (Fimail's codex-transport branch,
NetReader, future Allfix) had halted integration work pending this
fix. Without it, anyone vendoring fpc-msgbase 0.1 ships with a
known-corrupting adapter.
Design choice: per Ken's call, "message is just the message text;
everything else is an attribute, including from/to/subject/dates."
Same architecture as RFC 822 email (headers + body). Each backend
fills attributes it knows on Read; reads attributes it understands
on Write; ignores unknown attributes silently (RFC 822 X-header
semantics). Forward-compatible -- a new backend (e.g. a planned SQL
message store) just adds its own attribute keys; old backends ignore
them.
Composition is the consumer's job. The library never reassembles
Body + Attributes into kludge-laden display text. A BBS that wants
inline kludges walks Attributes and prepends ^aMSGID etc. to its
own display. A tosser that needs MSGID for dupe detection reads
Attributes.Get('msgid') directly -- no body parsing required.
src/ma.types.pas:
- New TMsgAttribute / TMsgAttributes records with Get/SetValue,
typed accessors (GetInt/GetBool/GetDate/GetAddr), Has/Remove,
iteration. Linear-search lookup, fine for the ~30-50 keys per
message. Switch to hash later if profiling shows need.
- Replaced TUniMessage with the minimal Body + Attributes record.
- New UniAttrBitsToAttributes / UniAttrBitsFromAttributes helpers
to bridge the canonical MSG_ATTR_* cardinal bitset to/from
individual `attr.*` boolean keys.
- {$modeswitch advancedrecords} added so records have methods.
src/ma.api.pas:
- New capabilities API: TStringDynArray return type,
ClassSupportedAttributes (virtual class fn, default empty),
SupportedAttributes (instance sugar), SupportsAttribute (per-key
query). Each backend overrides ClassSupportedAttributes with the
static list of keys it knows. Callers query before setting so a
BBS UI can hide controls the underlying backend has no slot for.
src/formats/ma.fmt.*.uni.pas (all 9):
- Rewrote each XxxToUni and XxxFromUni for the new model. Read
populates Attributes with universal/FTSC/format-specific keys per
the attribute registry (to be published in phase 5). Write reads
attributes back and writes native form.
- JAM walks SubFields[] for SEEN-BY/PATH/TZUTC/TRACE plus passthrough
of unknown subfield IDs as `jam.subfield.<id>` for round-trip
safety. Squish parses CtrlInfo (NUL-separated ^A lines) into
individual attributes, rebuilds on Write. MSG and PKT (which keep
kludges inline in body per FTS-1) parse leading ^A lines and
trailing SEEN-BY/PATH out of the body so TUniMessage.Body is
always plain user text; on Write they reassemble the on-disk form.
- Each backend ships ClassSupportedAttributes with its key list.
src/ma.batch.pas: PktToUni signature updated to (in,out var) form.
tests/* + examples/*: migrated all callers from Msg.WhoFrom (etc.)
to Msg.Attributes.Get('from'). MakeMsg helpers now use SetValue/
SetBool/SetAddr.
Verified: 24/24 tests pass across all 7 test programs (read,
roundtrip, lock, batch, wildcat, write_existing, pack). Wildcat
walks all 7 vendored conferences clean.
Out of scope (next phases):
- docs/attributes-registry.md publishing the full key list with
per-format support matrix
- cross-format round-trip + capabilities-driven copy test
- update architecture.md / PROPOSAL.md to reflect the new model
2026-04-17 14:11:15 -07:00
|
|
|
'written #' + IntToStr(i), msg.Attributes.Get('subject'));
|
2026-04-15 08:46:42 -07:00
|
|
|
AssertTrue('Appended body contains marker',
|
|
|
|
|
Pos('Appended message body ' + IntToStr(i),
|
|
|
|
|
msg.Body) > 0);
|
|
|
|
|
end;
|
|
|
|
|
finally
|
|
|
|
|
base.Close;
|
|
|
|
|
base.Free;
|
|
|
|
|
end;
|
|
|
|
|
TestOK;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TestMsgAppend;
|
|
|
|
|
const
|
|
|
|
|
APPEND_COUNT = 5;
|
|
|
|
|
var
|
|
|
|
|
base: TMessageBase;
|
|
|
|
|
msg: TUniMessage;
|
|
|
|
|
i, preCount: longint;
|
|
|
|
|
begin
|
|
|
|
|
TestBegin(SysUtils.Format(
|
|
|
|
|
'*.MSG: append %d messages to 27-msg netmail dir', [APPEND_COUNT]));
|
|
|
|
|
|
|
|
|
|
if not DirectoryExists(MSG_SRC) then begin
|
|
|
|
|
WriteLn('SKIP (source missing)');
|
|
|
|
|
exit;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
{ Fresh scratch copy. }
|
|
|
|
|
if DirectoryExists(MSG_SCRATCH) then
|
|
|
|
|
RunShell(SysUtils.Format('rm -rf %s', [MSG_SCRATCH]));
|
|
|
|
|
ForceDirectories(MSG_SCRATCH);
|
|
|
|
|
RunShell(SysUtils.Format('cp -r %s/. %s/', [MSG_SRC, MSG_SCRATCH]));
|
|
|
|
|
RunShell(SysUtils.Format('chmod -R u+w %s', [MSG_SCRATCH]));
|
|
|
|
|
|
|
|
|
|
base := MessageBaseOpen(mbfMsg, MSG_SCRATCH, momReadOnly);
|
|
|
|
|
try
|
|
|
|
|
AssertTrue('Pre-open RO', base.Open);
|
|
|
|
|
preCount := base.MessageCount;
|
|
|
|
|
AssertEquals('Pre-count', 27, preCount);
|
|
|
|
|
finally
|
|
|
|
|
base.Close;
|
|
|
|
|
base.Free;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
base := MessageBaseOpen(mbfMsg, MSG_SCRATCH, momReadWrite);
|
|
|
|
|
try
|
|
|
|
|
AssertTrue('Open RW', base.Open);
|
|
|
|
|
for i := 1 to APPEND_COUNT do begin
|
|
|
|
|
msg := MakeMsg(100 + i);
|
|
|
|
|
AssertTrue('WriteMessage ' + IntToStr(i), base.WriteMessage(msg));
|
Lossless message model: Body + Attributes (showstopper fix)
Replaces TUniMessage's 13-field flat record with a strict two-area
model: Body holds only the message text; Attributes holds everything
else (from/to/subject/dates/addresses/MSGID/SEEN-BY/PATH/format-
specific fields) as namespaced key/value pairs.
Why this fix is required NOW: the previous JAM adapter dropped
MSGID, ReplyID, PID, Flags, SEEN-BY and PATH on every Read/Write
through the unified API. A NetReader parity test surfaced it (17/21
pass with 4 kludge failures). All 9 adapters had the same bug. For
tossers and scanners the impact is silent corruption: dropped MSGID
→ dupe storms, dropped PATH → mail loops, dropped SEEN-BY → broken
routing. Three downstream consumers (Fimail's codex-transport branch,
NetReader, future Allfix) had halted integration work pending this
fix. Without it, anyone vendoring fpc-msgbase 0.1 ships with a
known-corrupting adapter.
Design choice: per Ken's call, "message is just the message text;
everything else is an attribute, including from/to/subject/dates."
Same architecture as RFC 822 email (headers + body). Each backend
fills attributes it knows on Read; reads attributes it understands
on Write; ignores unknown attributes silently (RFC 822 X-header
semantics). Forward-compatible -- a new backend (e.g. a planned SQL
message store) just adds its own attribute keys; old backends ignore
them.
Composition is the consumer's job. The library never reassembles
Body + Attributes into kludge-laden display text. A BBS that wants
inline kludges walks Attributes and prepends ^aMSGID etc. to its
own display. A tosser that needs MSGID for dupe detection reads
Attributes.Get('msgid') directly -- no body parsing required.
src/ma.types.pas:
- New TMsgAttribute / TMsgAttributes records with Get/SetValue,
typed accessors (GetInt/GetBool/GetDate/GetAddr), Has/Remove,
iteration. Linear-search lookup, fine for the ~30-50 keys per
message. Switch to hash later if profiling shows need.
- Replaced TUniMessage with the minimal Body + Attributes record.
- New UniAttrBitsToAttributes / UniAttrBitsFromAttributes helpers
to bridge the canonical MSG_ATTR_* cardinal bitset to/from
individual `attr.*` boolean keys.
- {$modeswitch advancedrecords} added so records have methods.
src/ma.api.pas:
- New capabilities API: TStringDynArray return type,
ClassSupportedAttributes (virtual class fn, default empty),
SupportedAttributes (instance sugar), SupportsAttribute (per-key
query). Each backend overrides ClassSupportedAttributes with the
static list of keys it knows. Callers query before setting so a
BBS UI can hide controls the underlying backend has no slot for.
src/formats/ma.fmt.*.uni.pas (all 9):
- Rewrote each XxxToUni and XxxFromUni for the new model. Read
populates Attributes with universal/FTSC/format-specific keys per
the attribute registry (to be published in phase 5). Write reads
attributes back and writes native form.
- JAM walks SubFields[] for SEEN-BY/PATH/TZUTC/TRACE plus passthrough
of unknown subfield IDs as `jam.subfield.<id>` for round-trip
safety. Squish parses CtrlInfo (NUL-separated ^A lines) into
individual attributes, rebuilds on Write. MSG and PKT (which keep
kludges inline in body per FTS-1) parse leading ^A lines and
trailing SEEN-BY/PATH out of the body so TUniMessage.Body is
always plain user text; on Write they reassemble the on-disk form.
- Each backend ships ClassSupportedAttributes with its key list.
src/ma.batch.pas: PktToUni signature updated to (in,out var) form.
tests/* + examples/*: migrated all callers from Msg.WhoFrom (etc.)
to Msg.Attributes.Get('from'). MakeMsg helpers now use SetValue/
SetBool/SetAddr.
Verified: 24/24 tests pass across all 7 test programs (read,
roundtrip, lock, batch, wildcat, write_existing, pack). Wildcat
walks all 7 vendored conferences clean.
Out of scope (next phases):
- docs/attributes-registry.md publishing the full key list with
per-format support matrix
- cross-format round-trip + capabilities-driven copy test
- update architecture.md / PROPOSAL.md to reflect the new model
2026-04-17 14:11:15 -07:00
|
|
|
AssertTrue('New MsgNum > 0', msg.Attributes.GetInt('msg.num') > 0);
|
2026-04-15 08:46:42 -07:00
|
|
|
end;
|
|
|
|
|
AssertEquals('Count after append',
|
|
|
|
|
preCount + APPEND_COUNT, base.MessageCount);
|
|
|
|
|
finally
|
|
|
|
|
base.Close;
|
|
|
|
|
base.Free;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
base := MessageBaseOpen(mbfMsg, MSG_SCRATCH, momReadOnly);
|
|
|
|
|
try
|
|
|
|
|
AssertTrue('Reopen RO', base.Open);
|
|
|
|
|
AssertEquals('Count after reopen',
|
|
|
|
|
preCount + APPEND_COUNT, base.MessageCount);
|
|
|
|
|
finally
|
|
|
|
|
base.Close;
|
|
|
|
|
base.Free;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
TestOK;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
{ SeedHudsonFromJam - create a populated Hudson base by copying
|
|
|
|
|
the first N messages from the real JAM sample. Lets Hudson
|
|
|
|
|
tests run against realistic data without committing binary
|
|
|
|
|
samples to the repo. }
|
|
|
|
|
procedure SeedHudsonFromJam(const AHudsonPath: string; N: longint);
|
|
|
|
|
var
|
|
|
|
|
src, dst: TMessageBase;
|
|
|
|
|
msg: TUniMessage;
|
|
|
|
|
i, copied: longint;
|
|
|
|
|
begin
|
|
|
|
|
if DirectoryExists(AHudsonPath) then
|
|
|
|
|
RunShell(SysUtils.Format('rm -rf %s', [AHudsonPath]));
|
|
|
|
|
ForceDirectories(AHudsonPath);
|
|
|
|
|
|
|
|
|
|
dst := MessageBaseOpen(mbfHudson, AHudsonPath, momCreate);
|
|
|
|
|
try
|
|
|
|
|
if not dst.Open then exit;
|
|
|
|
|
src := MessageBaseOpen(mbfJam, JAM_SRC, momReadOnly);
|
|
|
|
|
try
|
|
|
|
|
if not src.Open then exit;
|
|
|
|
|
copied := 0;
|
|
|
|
|
i := 0;
|
|
|
|
|
while (copied < N) and (i < src.MessageCount) do begin
|
|
|
|
|
if src.ReadMessage(i, msg) then begin
|
|
|
|
|
dst.WriteMessage(msg);
|
|
|
|
|
Inc(copied);
|
|
|
|
|
end;
|
|
|
|
|
Inc(i);
|
|
|
|
|
end;
|
|
|
|
|
finally
|
|
|
|
|
src.Close;
|
|
|
|
|
src.Free;
|
|
|
|
|
end;
|
|
|
|
|
finally
|
|
|
|
|
dst.Close;
|
|
|
|
|
dst.Free;
|
|
|
|
|
end;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
procedure TestHudsonAppend;
|
|
|
|
|
const
|
|
|
|
|
APPEND_COUNT = 10;
|
|
|
|
|
var
|
|
|
|
|
base: TMessageBase;
|
|
|
|
|
msg: TUniMessage;
|
|
|
|
|
i, preCount: longint;
|
|
|
|
|
begin
|
|
|
|
|
TestBegin(SysUtils.Format(
|
|
|
|
|
'Hudson: seed %d msgs from JAM, append %d, verify',
|
|
|
|
|
[HUDSON_SEED_N, APPEND_COUNT]));
|
|
|
|
|
|
|
|
|
|
if not FileExists(JAM_SRC + '.jhr') then begin
|
|
|
|
|
WriteLn('SKIP (JAM source missing)');
|
|
|
|
|
exit;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
SeedHudsonFromJam(HUDSON_SCRATCH, HUDSON_SEED_N);
|
|
|
|
|
|
|
|
|
|
base := MessageBaseOpen(mbfHudson, HUDSON_SCRATCH, momReadOnly);
|
|
|
|
|
try
|
|
|
|
|
AssertTrue('Pre-open RO', base.Open);
|
|
|
|
|
preCount := base.MessageCount;
|
|
|
|
|
AssertEquals('Pre-count', HUDSON_SEED_N, preCount);
|
|
|
|
|
finally
|
|
|
|
|
base.Close;
|
|
|
|
|
base.Free;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
base := MessageBaseOpen(mbfHudson, HUDSON_SCRATCH, momReadWrite);
|
|
|
|
|
try
|
|
|
|
|
AssertTrue('Open RW', base.Open);
|
|
|
|
|
for i := 1 to APPEND_COUNT do begin
|
|
|
|
|
msg := MakeMsg(200 + i);
|
|
|
|
|
AssertTrue('WriteMessage ' + IntToStr(i), base.WriteMessage(msg));
|
|
|
|
|
end;
|
|
|
|
|
AssertEquals('Count after append',
|
|
|
|
|
preCount + APPEND_COUNT, base.MessageCount);
|
|
|
|
|
finally
|
|
|
|
|
base.Close;
|
|
|
|
|
base.Free;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
base := MessageBaseOpen(mbfHudson, HUDSON_SCRATCH, momReadOnly);
|
|
|
|
|
try
|
|
|
|
|
AssertTrue('Reopen RO', base.Open);
|
|
|
|
|
AssertEquals('Count after reopen',
|
|
|
|
|
preCount + APPEND_COUNT, base.MessageCount);
|
|
|
|
|
for i := 1 to APPEND_COUNT do begin
|
|
|
|
|
AssertTrue('Read appended ' + IntToStr(i),
|
|
|
|
|
base.ReadMessage(preCount + i - 1, msg));
|
|
|
|
|
AssertEquals('Appended WhoFrom',
|
Lossless message model: Body + Attributes (showstopper fix)
Replaces TUniMessage's 13-field flat record with a strict two-area
model: Body holds only the message text; Attributes holds everything
else (from/to/subject/dates/addresses/MSGID/SEEN-BY/PATH/format-
specific fields) as namespaced key/value pairs.
Why this fix is required NOW: the previous JAM adapter dropped
MSGID, ReplyID, PID, Flags, SEEN-BY and PATH on every Read/Write
through the unified API. A NetReader parity test surfaced it (17/21
pass with 4 kludge failures). All 9 adapters had the same bug. For
tossers and scanners the impact is silent corruption: dropped MSGID
→ dupe storms, dropped PATH → mail loops, dropped SEEN-BY → broken
routing. Three downstream consumers (Fimail's codex-transport branch,
NetReader, future Allfix) had halted integration work pending this
fix. Without it, anyone vendoring fpc-msgbase 0.1 ships with a
known-corrupting adapter.
Design choice: per Ken's call, "message is just the message text;
everything else is an attribute, including from/to/subject/dates."
Same architecture as RFC 822 email (headers + body). Each backend
fills attributes it knows on Read; reads attributes it understands
on Write; ignores unknown attributes silently (RFC 822 X-header
semantics). Forward-compatible -- a new backend (e.g. a planned SQL
message store) just adds its own attribute keys; old backends ignore
them.
Composition is the consumer's job. The library never reassembles
Body + Attributes into kludge-laden display text. A BBS that wants
inline kludges walks Attributes and prepends ^aMSGID etc. to its
own display. A tosser that needs MSGID for dupe detection reads
Attributes.Get('msgid') directly -- no body parsing required.
src/ma.types.pas:
- New TMsgAttribute / TMsgAttributes records with Get/SetValue,
typed accessors (GetInt/GetBool/GetDate/GetAddr), Has/Remove,
iteration. Linear-search lookup, fine for the ~30-50 keys per
message. Switch to hash later if profiling shows need.
- Replaced TUniMessage with the minimal Body + Attributes record.
- New UniAttrBitsToAttributes / UniAttrBitsFromAttributes helpers
to bridge the canonical MSG_ATTR_* cardinal bitset to/from
individual `attr.*` boolean keys.
- {$modeswitch advancedrecords} added so records have methods.
src/ma.api.pas:
- New capabilities API: TStringDynArray return type,
ClassSupportedAttributes (virtual class fn, default empty),
SupportedAttributes (instance sugar), SupportsAttribute (per-key
query). Each backend overrides ClassSupportedAttributes with the
static list of keys it knows. Callers query before setting so a
BBS UI can hide controls the underlying backend has no slot for.
src/formats/ma.fmt.*.uni.pas (all 9):
- Rewrote each XxxToUni and XxxFromUni for the new model. Read
populates Attributes with universal/FTSC/format-specific keys per
the attribute registry (to be published in phase 5). Write reads
attributes back and writes native form.
- JAM walks SubFields[] for SEEN-BY/PATH/TZUTC/TRACE plus passthrough
of unknown subfield IDs as `jam.subfield.<id>` for round-trip
safety. Squish parses CtrlInfo (NUL-separated ^A lines) into
individual attributes, rebuilds on Write. MSG and PKT (which keep
kludges inline in body per FTS-1) parse leading ^A lines and
trailing SEEN-BY/PATH out of the body so TUniMessage.Body is
always plain user text; on Write they reassemble the on-disk form.
- Each backend ships ClassSupportedAttributes with its key list.
src/ma.batch.pas: PktToUni signature updated to (in,out var) form.
tests/* + examples/*: migrated all callers from Msg.WhoFrom (etc.)
to Msg.Attributes.Get('from'). MakeMsg helpers now use SetValue/
SetBool/SetAddr.
Verified: 24/24 tests pass across all 7 test programs (read,
roundtrip, lock, batch, wildcat, write_existing, pack). Wildcat
walks all 7 vendored conferences clean.
Out of scope (next phases):
- docs/attributes-registry.md publishing the full key list with
per-format support matrix
- cross-format round-trip + capabilities-driven copy test
- update architecture.md / PROPOSAL.md to reflect the new model
2026-04-17 14:11:15 -07:00
|
|
|
'WriteTester' + IntToStr(200 + i), msg.Attributes.Get('from'));
|
2026-04-15 08:46:42 -07:00
|
|
|
end;
|
|
|
|
|
finally
|
|
|
|
|
base.Close;
|
|
|
|
|
base.Free;
|
|
|
|
|
end;
|
|
|
|
|
TestOK;
|
|
|
|
|
end;
|
|
|
|
|
|
|
|
|
|
begin
|
Rename to fpc-msgbase, scrub false-provenance Allfix references
Project renamed from message_api → fpc-msgbase. Folder, README title,
docs, build.sh, fpc.cfg, and test banners all updated for consistency
with the planned remote at kjgr.io:2222/kenjreno/fpc-msgbase.git.
Also scrubbed claims that backends were "ported from Allfix" or
"match Allfix's msgutil/domsg" — none of this code was ported from
Allfix; it was implemented from FTSC documents and the original
format authors' published specs (jam.txt, squish.doc, pcboard.doc,
EzyCom reference, WildCat 4 SDK headers). Author credits live in
docs/ftsc-compliance.md.
Real interop facts that mention Allfix-the-product stay documented:
the PCB Extra2 sent-bit ($40) Allfix sets when tossing, and the
FTSC-registered product code $EB. These describe external software
behavior we interoperate with, not provenance.
docs/format-notes/hudson.md removed — stale planning doc that
predates the working ma.fmt.hudson backend.
2026-04-17 12:47:43 -07:00
|
|
|
WriteLn('fpc-msgbase: write-to-existing-base tests');
|
2026-04-15 08:46:42 -07:00
|
|
|
WriteLn;
|
|
|
|
|
TestJamAppend;
|
|
|
|
|
TestMsgAppend;
|
|
|
|
|
TestHudsonAppend;
|
|
|
|
|
Halt(TestsSummary);
|
|
|
|
|
end.
|