Skip to content

TIC Support

ENiGMA½ supports FidoNet-Style TIC file attachments by mapping external TIC area tags to local file areas.

Files can be received from an uplink and, if you declare downlinks, passed on to them — see Forwarding to Downlinks.

Under a given node defined in the ftn_bso config section in config.hjson (see BSO Import/Export), TIC configuration may be supplied:

{
scannerTossers: {
ftn_bso: {
nodes: {
"46:*": {
packetPassword: mypass
encoding: cp437
archiveType: zip
tic: { // <--- General TIC config for 46:*
password: TESTY-TEST // see tip below for keeping this out of plain text
uploadBy: AgoraNet TIC
allowReplace: true
}
}
}
}
}
}

Valid tic members:

ItemRequiredDescription
passwordNoTIC packet password, if required. Compared without regard to case, as other FTN software does
uploadByNoSets the “uploaded by” field for TIC attachments, for example “AgoraNet TIC”
allowReplaceNoSet to true to allow TIC attachments to replace each other. This is especially handy for things like weekly node list attachments
descPriorityNoWhere the file description comes from: diz (default) prefers a FILE_ID.DIZ shipped inside the file, tic prefers the TIC’s own Ldesc
exportTypeNoFlavour of the outbound queued for this downlink: crash (default), hold, direct or normal. HTick calls this fileEchoFlavour
noTicNoSend the file with no companion TIC. HTick’s noTIC
longNamesNoEmit Lfile (long file names). Defaults to true
passUnknownKeywordsNoPass keywords we do not recognise through unchanged. Defaults to true; set false for a peer in FSC-87 subset mode
sha256NoPass a Sha256 line through. Defaults to false
addressDimensionsNoDimensions to write From and To in — 3D, 4D (default) or 5D. Seenby is always written 4D regardless: it is the loop guard, and some processors match it by exact string, so a @domain there can cause a downlink to send a file back to a system that already has it
fileCaseNoCase of generated .tic filenames — lower (default) or upper
allowUnverifiedForwardNoForward files received from this node even though it has no password, i.e. was never authenticated. Defaults to false
networkNoAddress this link from your AKA in the named network, whatever its own zone suggests. htick’s per-link ourAka. This is tic.network; the node-level network key is NetMail’s and is not consulted. See Areas on more than one network

The password, uploadBy, allowReplace and descPriority members may also be set once for all nodes under scannerTossers.ftn_bso.tic, where the following system-wide members live as well:

ItemRequiredDescription
secureInOnlyNoOnly process TIC files found in the secure inbound (paths.secInbound). Defaults to true. TIC files in the unsecure inbound are left where they are, not imported and not deleted
requireAreaAuthorizationNoAlso require the sender to be an uplinks entry of the area when importing, not only when forwarding. Defaults to false. See Restricting who may import
holdMaxAgeMsNoHow long to keep a TIC whose file has not arrived yet. Defaults to 48 hours; set to 0 to hold indefinitely. See Files arriving after their TIC

A .tic and the file it announces frequently arrive in separate mailer sessions, sometimes many minutes apart. ENiGMA½ handles this the way other FTN file processors do: a TIC whose file is not present yet — or is still being written into the inbound — is held and retried on later import passes rather than rejected. Nothing is deleted while a TIC is being held.

A held TIC is retried on every import cycle, including the one triggered immediately when a BinkP session delivers files, so the file is normally picked up within seconds of arriving. If it never arrives, the TIC is given up on after holdMaxAgeMs and archived to paths.reject as before.

The announced name is matched case-insensitively against the inbound, so a TIC naming NODELIST.Z34 still finds a nodelist.z34 on disk.

Next, we need to configure the mapping between TIC areas you want to carry, and the file base area (and, optionally, specific storage tag) for them to be tossed to. You can also add hashtags to the tossed files to assist users in searching for files:

ticAreas: {
agn_node: {
areaTag: msgNetworks
storageTag: msg_network
hashTags: agoranet,nodelist
}
}

Valid ticAreas members under a given node mapping are as follows:

ItemRequiredDescription
areaTagUsuallySpecifies the local areaTag in which to place TIC attachments. Not needed for a passthrough area, which stores nothing locally
storageTagNoOptionally, set a specific storageTag. If not set, the default for this area will be used.
hashTagsNoOne or more optional hash tags to assign TIC attachments in this area.
downlinksNoAddresses to forward this area’s files on to. See Forwarding to Downlinks
uplinksNoAddresses permitted to publish into this area. Required if downlinks is set — an area with downlinks and no uplinks forwards nothing
networkNoPins every downlink of this area to one of your messageNetworks.ftn.networks. Without it each downlink is addressed from your closest AKA — see Areas on more than one network
passthroughNoSet to true to carry the echo for downlinks without storing it locally. See Passthrough areas. Must be set explicitly — an absent areaTag does not imply it
areaDescNoDescription of the echo, written as Areadesc into TICs you hatch. A forwarded TIC carries whatever the hatching system wrote, untouched

💡 Multiple TIC areas can be mapped to a single file base area.

Example configuration fragments mapping file base areas, FTN BSO node configuration and TIC area configuration.

fileBase: {
areaStoragePrefix: /home/bbs/file_areas/
storageTags: {
msg_network: "msg_network"
}
areas: {
msgNetworks: {
name: Message Networks
desc: Message networks news & info
storageTags: [
"msg_network"
]
}
}
}
scannerTossers: {
ftn_bso: {
nodes: {
"46:*": {
packetPassword: mypass
encoding: cp437
archiveType: zip
tic: {
password: TESTY-TEST
uploadBy: Agoranet TIC
allowReplace: true
}
}
}
ticAreas: {
// here we map AgoraNet AGN_NODE -> local msgNetworks file area
agn_node: {
areaTag: msgNetworks
storageTag: msg_network
hashTags: agoranet,nodelist
}
agn_info: {
areaTag: msgNetworks
storageTag: msg_network
hashTags: agoranet,infopack
}
}
}
}

By default, any node in nodes may announce a file into any area you carry. Authentication is not per-area: From is checked against your node list and the Area is checked against the areas you carry, but nothing correlates the two.

Setting tic.requireAreaAuthorization to true requires the sender to be listed in that area’s uplinks before the file is imported — the same check that already governs forwarding.

tic: {
requireAreaAuthorization: true
}

It is off by default only for compatibility; nothing in an existing configuration says who is entitled to which echo. A forwarding hub already has the uplinks lists this needs, so for one it usually costs nothing to enable.

By default ENiGMA½ is a leaf node: files arrive from an uplink, are imported, and go no further. Add downlinks to a ticAreas entry and it will also pass them on, generating a TIC for each downlink.

ticAreas: {
fsx_gen: {
areaTag: fsxGeneral
storageTag: fsx_gen
// which of your networks signs the outgoing Path and Seenby
network: fsxnet
// who may publish into this echo — required when forwarding
uplinks: [ "21:1/100" ]
downlinks: [ "21:1/200", "21:2/150" ]
}
}

Each downlink also needs an entry in nodes — that is where its TIC password and any per-link options live:

nodes: {
"21:1/200": {
tic: {
password: THEIRPASS // written as "Pw" in the TIC we send them
}
}
}

For each downlink that should receive the file, ENiGMA½ queues the file itself followed by a generated .tic, in that order — FSC-0087 requires the file to be sent first so a failed session cannot orphan a TIC. The file is sent from your file base and left there; the generated TIC is deleted once sent.

The outgoing TIC carries your address in From and a new Path line, the downlink in To, that downlink’s password in Pw, and the full Seenby list. Keywords ENiGMA½ does not recognise are passed through unchanged, as both FTS-5006 and FSC-0087 require.

A configured downlink is not sent the file when it is already listed in the TIC’s Seenby (this is the loop guard), is the node that sent you the file, is the TIC’s To, is the file’s Origin, or is one of your own addresses. Skips are logged at debug.

Only an address listed in that area’s uplinks may cause a file to be forwarded. A sender is matched allowing for the address-dimension differences that are normal in FTN control data, so an uplink written 21:1/100 still matches a TIC saying 21:1/100@fsxnet. An entry containing * is treated as a wildcard pattern, as in nodes — convenient, but naming a concrete address is the point of the list.

This is a forwarding control. Importing a TIC into your own file base is unchanged and is not area-scoped; see Forwarding to Downlinks above for why the two differ.

Forwarding is gated more tightly than importing, because importing affects only your own file base while forwarding makes other systems receive traffic your Path and Seenby lines vouch for. Nothing is forwarded when:

  • The TIC arrived in the unsecure inbound. This holds even if you set tic.secureInOnly to false to import from there.
  • The sending node has no tic.password configured, so it was never actually authenticated. Set one, or set allowUnverifiedForward on that node.
  • The TIC’s To names a system that is not you — the file is in transit through you. It is still imported, but re-announcing it under your name would be wrong. Routing such files onward is not implemented.
  • The file collided with one you already hold and was stored under a different name. Announcing one name while sending another leaves the downlink with a file it cannot pair up.
  • The sender is not listed in the area’s uplinks, or the area names no uplinks at all.

Each of these is logged at warn with the reason.

The same applies to a file you hatch yourself — see Hatching.

When a TIC’s Replaces supersedes a file you have already queued for a downlink that has not yet collected it, the old file and its TIC are removed from that downlink’s outbound. Anything already sent is left alone. Without this a downlink that polls infrequently would receive both, and the older one would in any case have been deleted locally by then.

The same pairing holds anywhere a reference leaves the outbound: oputil bso prune removes a payload and the .tic announcing it together. A payload removed on its own would leave the downlink an announcement for a file it never receives.

Problems that would otherwise be silent are reported at startup — an area with downlinks but no uplinks (which forwards nothing), an area with no resolvable network, a downlink missing from nodes, a downlink with no tic.password (its TICs will carry no Pw line), a downlink no address of yours shares a zone with, or an area whose zone more than one of your networks claims. If an area imports fine but never forwards, look there first.

If you have a single FTN address, none of this applies and nothing here changes what you see.

If you have several — a file echo carried on two networks, or a hub with several AKAs — each downlink is addressed from your address in its network. The From line, the Path line you add, and the outbound directory the file is queued in all follow that choice together, so a downlink never receives a TIC from an address it does not know you by.

The AKA is chosen by closeness to the downlink, which is what Synchronet’s tickit does and what htick does per link:

Same zone and same netBest — you are a neighbour in their own net
Same zoneSame network, a different net
NeitherA last resort, and reported at startup: you are introducing yourself with an address from another network entirely, which only works if the link already knows you by it

Most specific wins:

  1. nodes.<address>.tic.network — this link, for file echoes only
  2. ticAreas.<tag>.network — every downlink of this area
  3. Closest AKA, as above

ticAreas.<tag>.network keeps meaning exactly what it always did: it pins the whole area. A configuration that already uses it is not second-guessed by closeness matching. nodes.<address>.tic.network is more specific and overrides it, which is how you carve one link out of a pinned area.

nodes.<address>.network is not consulted. That key exists for NetMail routing, and treating it as a file-echo setting would change the identity announced to a link under a configuration nobody edited. Say tic.network if you mean file echoes.

A network name that is not configured falls back to the closest AKA rather than dropping the file, and says so. A downlink written 2D (103/999, with no zone) matches no network by zone and is addressed from the area’s own address rather than being skipped.

Only the identity — From, your Path line, and your entry in Seenby. The outbound directory a downlink’s files are queued in is unaffected: it follows the downlink’s zone, which is the one canonical answer the mailer can derive from an address alone, and the FTS-5005 .bsy lock lives beside it. A per-link directory would mean the tosser and a live mail session taking different lock files.

Nothing is lost by that split — an outbound session presents every one of your addresses in the handshake, so where a file sits has never had anything to do with the identity you announce it under.

Seenby names every AKA you present in the echo, not just one. It is the loop guard, and a peer matches it against the address it knows you by — Synchronet’s tickit compares by literal string equality — so a peer that knows you by your Fidonet address would not recognise an fsxNet-only Seenby and would forward the file straight back at you.

Only the AKAs actually in play for that echo are listed. An address on a network the echo is not carried on has nothing to do with the file and would propagate as noise into every downstream TIC.

Selecting downlinks still treats all your addresses as you, across every network, so a downlinks list naming one of your own AKAs is recognised as a configuration error rather than fed.

A hub carrying forty file echoes for its downlinks does not necessarily want forty local file areas, forty storage directories, and forty areas’ worth of files its own users will never browse. A passthrough echo is relayed without being stored:

scannerTossers: {
ftn_bso: {
paths: {
// where transit payloads live; defaults to mail/ftn_tic_transit/
ticTransit: /enigma-bbs/mail/ftn_tic_transit/
}
ticAreas: {
fsx_gen: {
passthrough: true
network: fsxnet
uplinks: [ "21:1/100" ]
downlinks: [ "21:1/200", "21:1/300" ]
}
}
}
}

passthrough: true is required and is not inferred. Omitting areaTag looks like it should mean the same thing — there is no local area to store into — but the inference is not safe. This entry works today and stores into the file base, because a ticAreas key is matched against fileBase.areas as well:

fileBase: { areas: { fsx_gen: { ... } } }
ticAreas: { fsx_gen: { uplinks: [ "21:1/100" ], downlinks: [ "21:1/200" ] } }

Inferring passthrough would silently reinterpret that on upgrade as “forward these and then delete them”, and so would a typo in areaTag. When guessing wrong destroys files, the flag is explicit. An entry with neither a usable areaTag nor passthrough: true has nowhere to put a file and is reported at startup.

The payload goes to a per-echo directory under ticTransit and is queued for downlinks from there. Forwarding is otherwise identical: Origin stays whoever hatched the file, your Path line is appended, and Seenby gains you and your downlinks.

uplinks is not optional here. Nothing is stored, so forwarding under your name is the only thing a transit file is ever used for — an area with downlinks and no uplinks refuses everything, and says so at startup.

When no flow file references it any more. That is an exact answer, not a policy: a file a downlink has not collected is named by a live line in that node’s flow file and stays; one every link has collected is named by nothing and goes. The sweep runs at the end of each import pass.

A reference marked sent (~) still counts as a reference. Those lines are history and the flow file drains on its own; deleting underneath one buys nothing and makes “why is this gone?” unanswerable.

If any flow file cannot be read — a permissions problem, a volume that is not mounted — the sweep does not run at all that pass. A node whose queue we cannot read looks identical to one that owes nothing, and the difference decides whether a file is deleted.

This deliberately has no age threshold and needs none. A link that is merely offline still has live references, so its files are kept for exactly as long as it takes — which is the behaviour an expiry rule would be trying to approximate.

In a stored area, a second announcement of a name you already hold is caught by the file base: the file is stored under a renamed copy and forwarding refuses it, because announcing one name while shipping another leaves the downlink an orphan. A transit file must be stored under exactly the name it was announced as, so there is no rename to fall back on — and three cases are separated:

  • Superseding the file of that name, i.e. the TIC’s Replaces matched it. Not a duplicate at all: the uplink is telling you this replaces what you hold. Overwritten in place, exactly as a stored area does. This is the weekly-nodelist case and the most common thing in a file echo.
  • A different file, still queued for some downlink. Refused and archived to reject. Overwriting would change the bytes under a reference that is already queued, and the downlink would receive the new file under the old announcement’s CRC.
  • A different file, referenced by nobody. A leftover the sweep has not reached. Replaced.

Matched by name inside the echo’s own transit directory rather than against file base metadata, because a transit file has no database row. The superseded file is dequeued from every downlink that has not collected it, its generated TIC is removed, and — when the replacement has a different name — the old file is deleted. A pattern matching more than one transit file is refused rather than guessed at.

The pattern comes from a remote peer, so it is matched with a linear scanner rather than a regular expression; a run of * in a pattern compiled to a regex backtracks catastrophically and would let an uplink stall the whole system.

An FTS-5005 .bsy lock means a node is mid-session and its flow file must not be touched. For a stored area that is merely a missed forward — the payload is in the file base and goes out next time. A transit file has no such copy, so if no downlink could be queued the import is deferred instead of completed: the transit copy is removed and the TIC and its payload stay in the inbound to be retried, bounded by tic.holdMaxAgeMs like any other hold. If some downlinks were queued and others were not, the file stays for the ones that have it queued and the rest miss it, exactly as a stored area behaves.

Forwarding passes on a file an uplink sent you. Hatching puts one of your own into an echo: your nodelist, your infopack, anything you produce. A hub that cannot hatch can only repeat what it is told.

Terminal window
oputil.js fb hatch AREA_TAG FILE [arguments]

AREA_TAG is the FTN area tag — a ticAreas key, i.e. what the echo is called on the network — not the local file base area tag. One local area may carry several echoes, and it is the echo you are publishing into.

The file is copied into that echo’s local file base area, scanned and persisted like any other upload, and then announced to every downlink configured for the echo. You are the Origin, the Path begins with your line, and the Seenby is you plus every downlink.

ArgumentPurpose
--desc TEXTShort description; becomes the TIC’s Desc
--ldesc TEXTLong description. Repeat for more than one line — Ldesc is multi-line and blank lines are kept
--replaces PATTERN8.3 pattern this file supersedes, e.g. NODELIST.*
--tags TAG1,TAG2Hashtags for the file base entry
--storage-tag TAGStorage location; default is the area’s first, or the ticAreas entry’s storageTag
--dry-runPrint what would be hatched, and the TIC, without writing anything

--replaces is what makes this work unattended. It supersedes the previous file in the same area and from the same origin, updates the file base entry in place rather than accumulating a second one, dequeues the old file and its TIC from any downlink that has not collected them, and removes the old physical file.

Terminal window
oputil.js fb hatch FSX_NODELIST /srv/staging/nodelist.246 --desc "fsxNet nodelist for day 246" --replaces "NODELIST.*"

Run that from cron each week and downlinks receive exactly one nodelist, never a backlog.

Only one match is accepted. A pattern matching several files is refused rather than guessed at, because picking wrong deletes a real file locally and pulls it out of every downlink’s queue. A pattern matching nothing is fine — that is the first hatch.

  • The echo must name downlinks, or there is nobody to announce to and the hatch is refused.
  • The echo must name an areaTag, since the payload has to live somewhere. Passthrough areas are not yet supported.
  • A name that already exists in the area is refused unless --replaces matched it. Storing under one name while announcing another leaves the downlink an orphan it can never pair up, which is the same reason a collision-renamed import is never forwarded.
  • --desc and --ldesc come off a shell; a line terminator in either is neutralised rather than becoming a keyword line of its own.
  • The file is stored and sent under its DOS 8.3 name, which is what the TIC’s File announces; the long name travels as Lfile for receivers that honour it. The two must agree: BinkP offers a file by its basename and htick pairs a payload strictly by File, so announcing one name while shipping another leaves the downlink an orphan it can never pair up. fsxnet-infopack.zip is therefore stored and announced as FSXNET-I.ZIP.
  • A symlink source is resolved before copying, so hatching from a nodelist.latest pointer stores the file it points at rather than a link that will dangle. The name still comes from the path you gave.
  • --dry-run writes nothing at all and prints the TIC it would have produced.

Message Networks