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:
| Item | Required | Description |
|---|---|---|
password | No | TIC packet password, if required. Compared without regard to case, as other FTN software does |
uploadBy | No | Sets the “uploaded by” field for TIC attachments, for example “AgoraNet TIC” |
allowReplace | No | Set to true to allow TIC attachments to replace each other. This is especially handy for things like weekly node list attachments |
descPriority | No | Where the file description comes from: diz (default) prefers a FILE_ID.DIZ shipped inside the file, tic prefers the TIC’s own Ldesc |
exportType | No | Flavour of the outbound queued for this downlink: crash (default), hold, direct or normal. HTick calls this fileEchoFlavour |
noTic | No | Send the file with no companion TIC. HTick’s noTIC |
longNames | No | Emit Lfile (long file names). Defaults to true |
passUnknownKeywords | No | Pass keywords we do not recognise through unchanged. Defaults to true; set false for a peer in FSC-87 subset mode |
sha256 | No | Pass a Sha256 line through. Defaults to false |
addressDimensions | No | Dimensions 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 |
fileCase | No | Case of generated .tic filenames — lower (default) or upper |
allowUnverifiedForward | No | Forward files received from this node even though it has no password, i.e. was never authenticated. Defaults to false |
network | No | Address 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:
| Item | Required | Description |
|---|---|---|
secureInOnly | No | Only 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 |
requireAreaAuthorization | No | Also 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 |
holdMaxAgeMs | No | How 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 |
Files arriving after their TIC
Section titled “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:
| Item | Required | Description |
|---|---|---|
areaTag | Usually | Specifies the local areaTag in which to place TIC attachments. Not needed for a passthrough area, which stores nothing locally |
storageTag | No | Optionally, set a specific storageTag. If not set, the default for this area will be used. |
hashTags | No | One or more optional hash tags to assign TIC attachments in this area. |
downlinks | No | Addresses to forward this area’s files on to. See Forwarding to Downlinks |
uplinks | No | Addresses permitted to publish into this area. Required if downlinks is set — an area with downlinks and no uplinks forwards nothing |
network | No | Pins 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 |
passthrough | No | Set 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 |
areaDesc | No | Description 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
Section titled “Example Configuration”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 } } }}Restricting who may import
Section titled “Restricting who may import”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.
Forwarding to Downlinks
Section titled “Forwarding to Downlinks”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 } }}What gets sent
Section titled “What gets sent”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.
Who gets skipped
Section titled “Who gets skipped”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.
Who may publish
Section titled “Who may publish”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.
When nothing is forwarded at all
Section titled “When nothing is forwarded at all”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.secureInOnlytofalseto import from there. - The sending node has no
tic.passwordconfigured, so it was never actually authenticated. Set one, or setallowUnverifiedForwardon that node. - The TIC’s
Tonames 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 nouplinksat all.
Each of these is logged at warn with the reason.
Replaced files
Section titled “Replaced files”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.
Checking your configuration
Section titled “Checking your configuration”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.
Areas on More Than One Network
Section titled “Areas on More Than One Network”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 net | Best — you are a neighbour in their own net |
| Same zone | Same network, a different net |
| Neither | A 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 |
Overriding it
Section titled “Overriding it”Most specific wins:
nodes.<address>.tic.network— this link, for file echoes onlyticAreas.<tag>.network— every downlink of this area- 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.
What this does not change
Section titled “What this does not change”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
Section titled “Seenby”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.
Passthrough (Transit) Areas
Section titled “Passthrough (Transit) Areas”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 a transit file is deleted
Section titled “When a transit file is deleted”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.
A re-announced file
Section titled “A re-announced file”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
Replacesmatched 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.
Replaces
Section titled “Replaces”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.
When a downlink is busy
Section titled “When a downlink is busy”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.
Hatching — Originating a File
Section titled “Hatching — Originating a File”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.
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.
| Argument | Purpose |
|---|---|
--desc TEXT | Short description; becomes the TIC’s Desc |
--ldesc TEXT | Long description. Repeat for more than one line — Ldesc is multi-line and blank lines are kept |
--replaces PATTERN | 8.3 pattern this file supersedes, e.g. NODELIST.* |
--tags TAG1,TAG2 | Hashtags for the file base entry |
--storage-tag TAG | Storage location; default is the area’s first, or the ticAreas entry’s storageTag |
--dry-run | Print what would be hatched, and the TIC, without writing anything |
A weekly nodelist
Section titled “A weekly nodelist”--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.
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
--replacesmatched 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. --descand--ldesccome 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
Fileannounces; the long name travels asLfilefor receivers that honour it. The two must agree: BinkP offers a file by its basename and htick pairs a payload strictly byFile, so announcing one name while shipping another leaves the downlink an orphan it can never pair up.fsxnet-infopack.zipis therefore stored and announced asFSXNET-I.ZIP. - A symlink source is resolved before copying, so hatching from a
nodelist.latestpointer stores the file it points at rather than a link that will dangle. The name still comes from the path you gave. --dry-runwrites nothing at all and prints the TIC it would have produced.