GOLDED FOR PYTHONSource on GitHub ↗
GOLDED FOR PYTHON / DOCUMENTATIONv1.2.0 · PYTHON 3.12+
Old messages. Ordinary Python.

Read FTN message bases.
Keep their history intact.

Read MSG, JAM, Squish and Hudson through a shared Python model. Get the message text, routing metadata and source location without writing a binary parser.

MSG / OPUSJAMSQUISHHUDSON
01

Your first message

Write a message into a temporary MSG area, then read it back. No old archive needed. This example cleans up after itself.

first_message.py · RUNNABLE
from pathlib import Path
from tempfile import TemporaryDirectory
from golded_ftn import OutgoingMessage
from golded_ftn_msg import MsgReader, MsgWriter

with TemporaryDirectory() as root:
    area = Path(root) / "area"
    writer = MsgWriter()
    writer.create(area)
    message = OutgoingMessage(
        from_name="Alice", to_name="Bob",
        subject="Hello, FidoNet", body_text="Still here.",
    )
    with writer.open(area) as session:
        result = session.append(message)
        assert result.identity.msgno == 1
    for received in MsgReader().read(area):
        print(received.subject)
        print(received.from_name)
OUTPUT
Hello, FidoNet
Alice

Append returns a message identity and revision token. The reader returns ParsedMessage objects. All four formats have session writers in the 1.2.0 release. GoldED interoperability tests are deferred, so concurrent mode remains disabled.

Install the packages first using the next section. Save a runnable example as a .py file and run it in that environment.

02

Install from PyPI

Use Python 3.12 or newer and uv. All five packages are available on PyPI at 1.2.0.

installation.sh · TERMINAL
mkdir ftn-examples
cd ftn-examples
uv venv --python 3.14
uv pip install --python .venv golded-ftn==1.2.0 golded-ftn-msg==1.2.0 golded-ftn-jam==1.2.0 golded-ftn-squish==1.2.0 golded-ftn-hudson==1.2.0
uv run --no-project --python .venv first_message.py

Put first_message.py inside ftn-examples/ before the last command. On Windows, run the same commands in PowerShell with uv installed.

For development inside a reader checkout, uv sync --locked uses the sibling golded-ftn-python directory. Built wheels keep the normal golded-ftn>=1.2.0,<2 dependency. The core has no runtime dependencies.

03

Choose a reader

Repository names end in -python. Distribution names don’t. Imports use underscores. Paths describe an area or base, not a single message.

FormatRepository / distribution / importPublic entry pointInput
Coregolded-ftn-python ↗
golded-ftn
golded_ftn
Models & helpersNo message-base files
MSG / Opusgolded-ftn-msg-python ↗
golded-ftn-msg
golded_ftn_msg
MsgReader / MsgWriterDirectory of numbered .MSG files
JAMgolded-ftn-jam-python ↗
golded-ftn-jam
golded_ftn_jam
JamReader / JamWriterBasename + .JHR, .JDT, .JDX
Squishgolded-ftn-squish-python ↗
golded-ftn-squish
golded_ftn_squish
SquishReader / SquishWriterBasename + .SQD, .SQI
Hudsongolded-ftn-hudson-python ↗
golded-ftn-hudson
golded_ftn_hudson
HudsonReader / HudsonWriterDirectory with MSGIDX.BBS, MSGHDR.BBS, MSGTXT.BBS
read_formats.py · USES YOUR FILES
from pathlib import Path
from golded_ftn import MessageBaseReader
from golded_ftn_msg import MsgReader
from golded_ftn_jam import JamReader
from golded_ftn_squish import SquishReader
from golded_ftn_hudson import HudsonReader

sources: list[tuple[MessageBaseReader, Path]] = [
    (MsgReader(), Path("archives/msg/general")),
    (MsgReader(header_format="opus"), Path("archives/opus/general")),
    (JamReader(), Path("archives/jam/general")),
    (SquishReader(), Path("archives/squish/general")),
    (HudsonReader(), Path("archives/hudson")),
]
for reader, source in sources:
    for message in reader.read(source):
        print(message.msgno, message.from_name, message.subject)

Replace these paths with your own stable copies. For JAM and Squish, pass general, not general.JHR or general.SQD. Hudson reads all boards in one base:

hudson_board.py · USES YOUR FILES
from golded_ftn_hudson import HudsonReader

for message in HudsonReader().read("archives/hudson"):
    if message.area_code == "BOARD7":
        print(message.subject)

BOARD7 is a synthetic board identifier. Hudson does not store your configured area names. FTSC and Opus MSG layouts are selected explicitly; the reader does not guess which header you have.

04

One message model

A MessageBaseReader exposes read(path, options=None). Call it through the same protocol regardless of the binary format. It returns an iterable; materialize it with list(...) when you need a collected result.

shared_reader.py · USES YOUR FILES
from os import PathLike
from golded_ftn import MessageBaseReader
from golded_ftn_jam import JamReader

def show_subjects(reader: MessageBaseReader, source: str | PathLike[str]) -> None:
    for message in reader.read(source):
        print(message.msgno, message.subject)
        if message.provenance is not None:
            print(message.provenance.source_type,
                  message.provenance.source_id,
                  message.provenance.source_offset)

show_subjects(JamReader(), "archives/jam/general")

Messages are frozen values. Sender and recipient addresses are decoded strings or None; FtnAddress is the structured address type used for outgoing messages. Collections in the core model are tuples.

msgno
The source message number. It is not globally unique across areas.
external_id
MSGID when available; otherwise a core-generated hash:sha256: ID over decoded, normalized fields. Keep provenance separately: an ID is not a file location.
provenance
Source type, actual file path, source identity and byte offset when known. Preserve original bytes separately if you need a byte-faithful archive.
control_lines
Parsed kludges, MSGID, REPLY, charset, SEEN-BY and PATH. Repeated metadata is retained in the model’s ordered sequences.
Reply links
reply_to_msgno, reply1st_msgno and reply_next_msgno are format-dependent. Missing or zero links become None.
control_metadata.py · RUNNABLE
from golded_ftn import parse_message

text = (
    "\x01MSGID: 2:236/77 demo\n"
    "\x01REPLY: 2:236/77 earlier\n"
    "Hello.\nSEEN-BY: 236/77 100\n"
    "PATH: 236/77\n"
)
controls = parse_message(text)
print(controls.msgid)
print(controls.seen_by)
print(controls.path)
OUTPUT
2:236/77 demo
('236/77 100',)
('236/77',)

parse_message() parses metadata; it does not remove controls from the input text. MSG and Hudson bodies can still contain control lines. JAM header controls and Squish control buffers are represented as metadata without being appended to the body.

05

Text, encodings and dates

Readers decode strictly. They use supported charset declarations and fall back to CP850 when no usable declaration is present. Configure another fallback with ReaderOptions(fallback_charset="UTF-8"). A fallback is a policy choice, not permission to decode with replacement characters.

text_helpers.py · RUNNABLE
from golded_ftn import detect_charset, to_utf8, parse_body, repair_mojibake

raw = b"\x01CHRS: IBMPC 2\r\nBruger m\x9bde\x00"
charset = detect_charset(raw)
text = parse_body(to_utf8(raw, charset))
print(charset)
print(text.splitlines()[-1])

# A separate, opt-in operation on decoded text.
repair = repair_mojibake("Bruger m°de")
print(repair.text)
OUTPUT
CP850
Bruger møde
Bruger møde

parse_body() normalizes line endings and removes trailing NULs. to_utf8() returns a Python string, not encoded bytes. Mojibake repair is never automatic; retain the decoded original if you apply it. Its confidence is a heuristic, not a probability.

Formatposted_at
FTSC MSG / OpusNaive datetime; Opus uses the packed written timestamp with a textual fallback.
JAMNaive datetime from epoch + DateWritten seconds; zero becomes None. No machine-local timezone conversion.
SquishValid packed written date is UTC-aware. A permitted textual fallback is naive. Invalid dates become None.
HudsonNaive MM-DD-YY HH:MM date; 00–79 maps to 2000–2079, 80–99 to 1980–1999.

Timezone controls remain metadata. Do not assume every posted_at has a timezone, or sort mixed aware and naive datetimes without an explicit policy.

06

Strict reading and archive mode

Strict reading is the default. Filesystem failures stay filesystem exceptions. Damaged records and decoding failures raise ParserException with source context and a chained cause.

strict_errors.py · USES YOUR FILES
from golded_ftn import ParserException
from golded_ftn_jam import JamReader

try:
    messages = list(JamReader().read("archives/jam/general"))
except OSError as error:
    print("Cannot access the source:", error)
except ParserException as error:
    print("Cannot parse the source:", error)
else:
    print("Messages:", len(messages))

Do not treat an exception as end-of-file. MSG can yield earlier messages before a later file fails. Strict JAM, Squish and Hudson validate their active records before exposing a result. Collecting with list() makes that boundary explicit for your caller.

Archive mode is a reported compromise

Use it when you need recoverable content from an imperfect archive. A callback is required. It receives metadata-only issues; it does not receive message contents.

archive_read.py · USES YOUR FILES
from golded_ftn import ReaderIssue, ReaderOptions
from golded_ftn_jam import JamReader

issues: list[ReaderIssue] = []
options = ReaderOptions(archive_mode=True, on_issue=issues.append)
messages = list(JamReader().read("archives/jam/general", options))

for issue in issues:
    print(issue.action, issue.code, issue.source_path,
          issue.source_id, issue.source_offset)

complete_traversal = not any(issue.action == "stopped" for issue in issues)
print("Messages:", len(messages))
print("Traversal completed:", complete_traversal)
# A completed traversal can still have skipped messages.
recovered
A supported deviation was handled and reported.
skipped
A record was not returned. Traversal can continue where the reader has a safe boundary.
stopped
Traversal could not safely continue. A validated prefix may be returned.

Issue counts are not message counts: one record can produce several issues. No stopped issue means traversal completed, not that every source message was recovered. Callback exceptions and filesystem errors still propagate. Other readers use the same ReaderOptions contract; each has its own bounded recovery rules.

07

Edit message bases

Create a base, open a session, then append, read, update and delete. Edits require the revision from a consistent session read. Stop GoldED before opening a writer session. The example uses FTSC MSG; JamWriter and SquishWriter use the same API. HudsonWriter.open also requires an explicit board.

write_netmail.py · RUNNABLE
from pathlib import Path
from tempfile import TemporaryDirectory
from golded_ftn import FtnAddress, MessagePatch, OutgoingMessage, WriterOptions
from golded_ftn_msg import MsgWriter

with TemporaryDirectory() as root:
    area = Path(root) / "area"
    writer = MsgWriter()
    writer.create(area)
    outgoing = OutgoingMessage(
        from_name="Alice", to_name="Bob", subject="Netmail",
        body_text="A message from Python.",
        from_address=FtnAddress.from_string("2:236/77.1"),
        to_address=FtnAddress.from_string("2:236/100"),
    )
    with writer.open(area, WriterOptions(target_charset="CP850")) as session:
        added = session.append(outgoing)
        received = session.read(added.identity.msgno)
        print(added.identity.msgno)
        print(received.message.from_address)
        print(received.message.to_address)
        changed = session.update(
            received.identity, MessagePatch(subject="Edited"), received.revision,
        )
        session.delete(changed.identity, changed.revision)
OUTPUT
1
2:236/77.1
2:236/100

Outgoing addresses use FtnAddress. The writer handles the corresponding INTL/FMPT/TOPT controls and charset metadata. Only use characters that the target charset can encode. Each operation is a commit unit. Earlier completed operations survive a later failure. See the writer contract and verification boundary and the complete golded-ftn API reference.

08

Limits and further reading

Read a stable copy. These readers do not lock a live base or take an atomic snapshot across its files. Stop the writer or use a consistent copy.

  • No area discovery, database adapters or automatic Nornir ingestion.
  • JAM and Squish use their indexes as authority; obsolete, unindexed headers are not returned.
  • JAM rejects compressed, encrypted or escaped active messages. Squish supports classic layouts, not compressed or in-progress frames.
  • Hudson supports classic .BBS files, not GoldBase .DAT layouts.
  • Unknown metadata stays unknown. The libraries do not invent configured area names or timezones.
  • Multi-file readers load their source files into memory. Allow for the size of the whole base.

Format reference: JAM-001 revision 1. Each package README links to its format references and GoldED structures.

JAM(mbp) - Copyright 1993 Joaquim Homrighausen, Andrew Milner, Mats Birch, Mats Wallin. ALL RIGHTS RESERVED.

The library code and this guide are MIT licensed. Format specifications and bundled fonts retain their own terms.