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).
10 KiB
Attribute registry
This document is the source of truth for attribute key names used
across all fpc-msgbase backends.
TUniMessage has only two areas:
TUniMessage = record
Body: AnsiString; { only the message text }
Attributes: TMsgAttributes; { everything else, key/value }
end;
Every header field, kludge, control line, and per-format flag a
backend understands lives in Attributes under one of the keys
below. Backends drop keys they don't recognise on Write (RFC 822
X-header semantics). Callers can query base.SupportsAttribute(K)
before setting a key to know up-front which backends carry it.
Naming convention: lowercase, dot-namespaced. Universal /
FTSC-defined keys are unqualified (from, msgid). Format-
specific keys are prefixed with the format name (jam.msgidcrc,
squish.umsgid, pcb.confnum).
Tier 1 — Universal headers
Every Fido format sets these on Read; every backend reads them on Write. Equivalent to the always-present headers in any classic BBS message.
| Key | Type | Meaning |
|---|---|---|
msg.num |
int | Backend-assigned message number / index |
from |
string | Author name |
to |
string | Recipient name |
subject |
string | Message subject |
date.written |
date | When author wrote the message |
date.received |
date | When local system received |
addr.orig |
ftn | Originating FTN address (zone:net/node[.point]) |
addr.dest |
ftn | Destination FTN address |
area |
string | Echo area tag (echomail) |
board |
int | Conference / board number for multi-board formats |
cost |
int | Cost in cents (FTS-1 backends) |
Tier 2 — Canonical attribute bits
Boolean flags derived from the FTS-1 attribute word, plus extensions
some backends carry as separate bits. Always written via
SetBool(K, true); absent (or false) when not set.
| Key | FTS-1 | Meaning |
|---|---|---|
attr.private |
0x0001 | Private message |
attr.crash |
0x0002 | Crash priority |
attr.received |
0x0004 | Recipient has read |
attr.sent |
0x0008 | Sent to destination |
attr.fileattach |
0x0010 | File attached |
attr.intransit |
0x0020 | In transit |
attr.orphan |
0x0040 | No matching destination |
attr.killsent |
0x0080 | Kill after sending |
attr.local |
0x0100 | Originated locally |
attr.hold |
0x0200 | Hold for pickup |
attr.filereq |
0x0400 | File request |
attr.returnreceipt |
0x0800 | Return receipt requested |
attr.isreceipt |
0x1000 | This message is a receipt |
attr.auditreq |
0x2000 | Audit trail requested |
attr.fileupdreq |
0x4000 | File update request |
attr.deleted |
— | Tombstoned (per-base) |
attr.read |
— | Marked-read (per-user) |
attr.echo |
— | Echomail (vs netmail) |
attr.direct |
— | Direct flavour |
attr.immediate |
— | Immediate flavour |
attr.locked |
— | Locked (no edit) |
attr.netpending |
— | Pending netmail toss |
attr.echopending |
— | Pending echomail toss |
attr.nokill |
— | Protected from purge |
Tier 3 — FTSC kludges
Standard FTSC kludge lines, named after the kludge name without
the leading ^A. Multi-line attributes (SEEN-BY, PATH, Via) join
their lines with #13.
| Key | Type | Spec | Meaning |
|---|---|---|---|
msgid |
string | FTS-9 | Globally unique message ID |
replyid |
string | FTS-9 | Reply linkage to a previous MSGID |
pid |
string | FRL-1004 | Producer ID (creating tosser/editor) |
tid |
string | FSC-46 | Tosser ID |
flags |
string | FRL-1005 | Routing/handling flags |
chrs |
string | FTS-5003 | Character set declaration |
tzutc |
string | FTS-4001 | Time-zone offset from UTC |
seen-by |
multi-string | FTS-4 | SEEN-BY lines (one node-list per line) |
path |
multi-string | FTS-4 | PATH lines (one node-list per line) |
via |
multi-string | FTS-4009 | Via lines (one per relay) |
intl |
string | FSC-4008 | INTL kludge: <dest-zone:net/node> <orig-zone:net/node> for cross-zone netmail |
fmpt |
string | FSC-4008 | FMPT (origin point number) |
topt |
string | FSC-4008 | TOPT (destination point number) |
Unknown FTSC kludges
Any ^A<NAME>: <value> line whose <NAME> is not in the table
above is preserved as kludge.<lowername> regardless of which
backend stored it. Example: ^aXFOO: bar → kludge.xfoo =
'bar'.
This is the universal forward-compat slot for FTSC-form kludges
the library doesn't recognize natively. All four kludge-aware
backends (JAM, Squish, MSG, PKT) use the same kludge.*
namespace so a consumer can find passthrough kludges without
switching on format.
JAM's numeric SubField IDs that have no FTSC analogue stay
namespaced as jam.subfield.<id> (those aren't FTSC-form
kludges; they're JAM-specific binary subfields).
Multi-line attribute helpers
Attributes that store multiple lines (seen-by, path, via,
trace) join their lines with #13. Use the typed accessors
on TMsgAttributes to avoid manual splitting:
list := msg.Attributes.GetList('seen-by'); // TStringDynArray
msg.Attributes.SetList('path', list); // joins with #13
msg.Attributes.AppendListItem('seen-by', '3/777');
Tier 4 — Format-specific keys
These are namespaced and only meaningful to the format that produces them. Other backends ignore them on Write (silently dropped — fine).
JAM
| Key | Type | Meaning |
|---|---|---|
jam.msgidcrc |
int | Index fast-path CRC of MSGID |
jam.replycrc |
int | Index fast-path CRC of ReplyID |
jam.dateprocessed |
unix-int | Tosser timestamp |
jam.passwordcrc |
int | Per-message password CRC |
jam.cost |
int | JAM-level cost (separate from cost) |
jam.timesread |
int | Times-read counter |
jam.replyto |
int | Parent in reply chain |
jam.reply1st |
int | First child in reply chain |
jam.replynext |
int | Next sibling in reply chain |
jam.attribute2 |
int | Reserved JAM attribute2 word |
jam.subfield.<id> |
multi | Passthrough for JAM-numeric subfields with no FTSC kludge analogue |
(JAM's JAM_FTSKLUDGE subfields are parsed through the shared
kludge dispatcher, so their content lands in the canonical slot
— msgid, intl, etc., or kludge.<name> for unknowns —
rather than a JAM-specific bag.)
Squish
| Key | Type | Meaning |
|---|---|---|
squish.umsgid |
int | UMsgID (per-area unique number) |
squish.utcofs |
int | UTC offset in minutes |
squish.replyto |
int | Reply chain parent |
squish.kludge.<name> |
multi | Passthrough for unknown CtrlInfo kludges |
Hudson / GoldBase
| Key | Type | Meaning |
|---|---|---|
hudson.prevreply |
int | Previous message in reply chain |
hudson.nextreply |
int | Next message in reply chain |
hudson.timesread |
int | Times-read counter |
goldbase.prevreply |
int | (GoldBase variant) |
goldbase.nextreply |
int | (GoldBase variant) |
goldbase.timesread |
int | (GoldBase variant) |
EzyCom
| Key | Type | Meaning |
|---|---|---|
ezy.extattr |
int | EzyCom ExtAttr byte |
ezy.prevreply |
int | Reply chain prev |
ezy.nextreply |
int | Reply chain next |
PCBoard
| Key | Type | Meaning |
|---|---|---|
pcb.refnum |
int | RefNum field |
pcb.status |
int | Raw PCB status byte |
pcb.active |
int | Active flag |
pcb.echo |
int | Echo flag |
pcb.export |
int | Export flag |
pcb.extra2 |
int | Extra2 byte (incl. Allfix-sent bit 6) |
pcb.extra3 |
int | Extra3 byte |
pcb.hastags |
int | HasTags flag |
pcb.origin |
int | Origin flag |
pcb.readnum |
int | ReadNum word |
pcb.extendedstatus |
int | Extended status byte |
pcb.password |
string | Per-message password (max 12 chars) |
Wildcat
| Key | Type | Meaning |
|---|---|---|
wildcat.confnum |
int | Conference number this message lives in |
wildcat.mflags |
int | Raw mFlags word from TMsgHeader |
PKT
| Key | Type | Meaning |
|---|---|---|
pkt.cost |
int | Packet-level cost (separate from cost) |
pkt.kludge.<name> |
multi | Passthrough for unknown body kludges |
MSG (FTS-1)
| Key | Type | Meaning |
|---|---|---|
msg.replyto |
int | Reply chain parent |
msg.nextreply |
int | Reply chain next |
msg.timesread |
int | Times-read counter |
msg.kludge.<name> |
multi | Passthrough for unknown body kludges |
Per-format support matrix
X = the backend's ClassSupportedAttributes lists the key.
Blank = backend has no slot for it; setting the key has no effect
on Write, and the key won't appear in Attributes after a Read.
| Key | JAM | Squish | MSG | PKT | Hudson | GoldBase | EzyCom | PCB | WC |
|---|---|---|---|---|---|---|---|---|---|
msg.num |
X | X | X | X | X | X | X | X | X |
from / to / subject |
X | X | X | X | X | X | X | X | X |
addr.orig / addr.dest |
X | X | X | X | X | X | X | X | X |
date.written |
X | X | X | X | X | X | X | X | X |
date.received |
X | X | X | X | |||||
area |
X | X | X | X | X | X | X | X | X |
board |
X | X | X | ||||||
cost |
X | X | X | X | X | ||||
attr.private |
X | X | X | X | X | X | X | X | X |
attr.crash |
X | X | X | X | X | X | X | ||
attr.received |
X | X | X | X | X | X | X | X | |
attr.sent |
X | X | X | X | X | X | X | X | |
attr.killsent |
X | X | X | X | X | X | X | ||
attr.local |
X | X | X | X | X | X | X | X | |
attr.hold |
X | X | X | X | X | X | |||
attr.fileattach |
X | X | X | X | X | X | X | ||
attr.returnreceipt |
X | X | X | X | X | X | |||
attr.deleted |
X | X | X | X | X | X | X | ||
msgid |
X | X | X | X | |||||
replyid |
X | X | X | X | |||||
pid |
X | X | X | X | |||||
flags |
X | X | X | X | |||||
seen-by |
X | X | X | X | |||||
path |
X | X | X | X | |||||
Format-specific (<fmt>.*) |
X | X | X | X | X | X | X | X | X |
(Always check at runtime via SupportsAttribute(K) rather than
relying on this table — it can drift. The capability list each
backend ships in ClassSupportedAttributes is authoritative.)