Files
fastway-plugin-mailer/fw_getnode.pas
Ken Johnson 3aead38844
All checks were successful
Build & Release Plugin / build (push) Successful in 10s
v0.0.11: Xenia mailer conversion — full protocol stack + WFC redesign
25 new Pascal units (12,912 lines) converted from Xenia Mailer source:
- CRC-16/32 engine (all 4 variants, verified against test vectors)
- Zmodem: framing engine, send/receive with CRC-16/32, resume, WaZoo 8K
- EMSI: handshake engine with MD5 shared-secret, field parsing, DAT build
- Hydra: bidirectional engine with full rxpkt packet assembly (hex/bin/asc/uue)
- SEAlink: block transfer + YooHoo/2U2 handshake (FTS-0006)
- Session: protocol detection (EMSI/YooHoo/TSYNC/FTS-0001), negotiation
- Period scheduler: bitmask-based time windows (2020-2099)
- Event scheduler: Xenia-style named events with MAT_* behavior flags
- BSO outbound scanner with status tracking
- WaZoo send orchestration (mail bundles, file attaches, requests)
- File request processor with SRIF external processor support
- FAX Class 2 reception (ZFAX/QFX/raw formats) with T.30 parsing
- Modem AT command engine with response parsing, caller ID, MNP detection
- Login script engine (send/expect with sub-expects)
- WFC main loop, nodelist lookup, BBS caller detection
- All types/constants with platform ifdefs (Linux/FreeBSD/Windows/OS2)

WFC screen redesigned to match Xenia's classic layout:
- Blue top bar with system name, AKA address, live clock
- Schedule/Status/Stats/Outbound panels in Xenia's exact arrangement
- Activity panel with modem AT commands/responses
- Session overlay with mail/file counters and transfer progress bar
- CGA/VGA color scheme (cyan labels, green OK, red errors)
- Full real-time via WebSocket — no polling, incremental DOM updates

Events tab rebuilt with proper inline editor (checkboxes for days and
behavior flags, time/duration inputs) replacing prompt() dialogs.
Sessions and Nodes tabs auto-refresh via WebSocket events.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 07:22:11 -07:00

192 lines
5.8 KiB
ObjectPascal

{ ======================================================================== }
{ fw_getnode.pas — Nodelist lookup utilities }
{ Converted from Xenia Mailer getnode.c }
{ Original: (C) 1987-2001 Arjen G. Lentz }
{ }
{ FidoNet address parsing (z:n/n.p@domain format), password lookup, }
{ AKA alias selection, security level determination. }
{ }
{ Most of the heavy nodelist work is in fw_fido_nodelist.pas (already }
{ in the mailer plugin). This unit provides the session-level helpers. }
{ ======================================================================== }
unit fw_getnode;
{$mode objfpc}{$H+}
interface
uses
SysUtils, fw_fido_types, fw_mailer_types, fw_misc;
type
{ ── Security levels for file request processing ── }
TSecurityLevel = (
slUnlisted, { 0: Not in nodelist }
slListed, { 1: Listed node, no password }
slKnown, { 2: Known node (we have pw configured) }
slPassword, { 3: Password verified but not in pwd list }
slProtected, { 4: Password verified and in pwd list }
slTopSecurity { 5: Highest security }
);
{ ── Parse a FidoNet address string ── }
{ Supports formats: z:n/n.p, z:n/n, n/n.p, n/n, n:n/n@domain }
function ParseFidoAddress(const AStr: string;
out AZone, ANet, ANode, APoint: Integer;
ADefaultZone: Integer = 1): Boolean;
{ Format a FidoNet address as string }
function FormatFidoAddress(AZone, ANet, ANode, APoint: Integer;
const ADomain: string = ''): string;
{ ── Password lookup ── }
function FindPassword(const APasswords: TAddressPasswordArray;
AZone, ANet, ANode, APoint: Integer): string;
{ ── AKA alias selection ── }
{ Select which of our AKAs to present to a given remote address. }
function SelectAKA(const AAliases: TAKAAliasArray;
const AOurAKAs: array of TFidoAddress; ANumAKAs: Integer;
ARemZone, ARemNet, ARemNode, ARemPoint: Integer): Integer;
{ ── Security level determination ── }
function GetSecurityLevel(APasswordMatch: Boolean;
AWeHavePassword: Boolean;
ANodeListed: Boolean): Integer;
{ ── Remap point address ── }
{ Convert boss-routed point addresses to proper 4D representation }
procedure RemapAddress(var AZone, ANet, ANode, APoint, APointNet: Integer);
implementation
function ParseFidoAddress(const AStr: string;
out AZone, ANet, ANode, APoint: Integer;
ADefaultZone: Integer): Boolean;
var
S, Part: string;
ColonPos, SlashPos, DotPos, AtPos: Integer;
begin
Result := False;
AZone := ADefaultZone;
ANet := 0;
ANode := 0;
APoint := 0;
S := Trim(AStr);
if S = '' then Exit;
{ Strip domain (@domain) }
AtPos := Pos('@', S);
if AtPos > 0 then
S := Copy(S, 1, AtPos - 1);
{ Parse zone:net/node.point }
ColonPos := Pos(':', S);
SlashPos := Pos('/', S);
DotPos := Pos('.', S);
if SlashPos = 0 then Exit; { Must have at least net/node }
if ColonPos > 0 then
begin
AZone := StrToIntDef(Copy(S, 1, ColonPos - 1), 0);
if AZone = 0 then Exit;
S := Copy(S, ColonPos + 1, MaxInt);
SlashPos := Pos('/', S);
DotPos := Pos('.', S);
end;
ANet := StrToIntDef(Copy(S, 1, SlashPos - 1), -1);
if ANet < 0 then Exit;
if DotPos > SlashPos then
begin
ANode := StrToIntDef(Copy(S, SlashPos + 1, DotPos - SlashPos - 1), -1);
APoint := StrToIntDef(Copy(S, DotPos + 1, MaxInt), 0);
end
else
ANode := StrToIntDef(Copy(S, SlashPos + 1, MaxInt), -1);
if ANode < 0 then Exit;
Result := True;
end;
function FormatFidoAddress(AZone, ANet, ANode, APoint: Integer;
const ADomain: string): string;
begin
if APoint > 0 then
Result := Format('%d:%d/%d.%d', [AZone, ANet, ANode, APoint])
else
Result := Format('%d:%d/%d', [AZone, ANet, ANode]);
if ADomain <> '' then
Result := Result + '@' + ADomain;
end;
function FindPassword(const APasswords: TAddressPasswordArray;
AZone, ANet, ANode, APoint: Integer): string;
var
I: Integer;
begin
Result := '';
for I := 0 to High(APasswords) do
if (APasswords[I].Zone = AZone) and (APasswords[I].Net = ANet) and
(APasswords[I].Node = ANode) and (APasswords[I].Point = APoint) then
Exit(APasswords[I].Password);
end;
function SelectAKA(const AAliases: TAKAAliasArray;
const AOurAKAs: array of TFidoAddress; ANumAKAs: Integer;
ARemZone, ARemNet, ARemNode, ARemPoint: Integer): Integer;
var
I: Integer;
AddrStr: string;
begin
Result := 0; { Default to first AKA }
AddrStr := FormatFidoAddress(ARemZone, ARemNet, ARemNode, ARemPoint);
{ Check alias patterns }
for I := 0 to High(AAliases) do
begin
if PatternMatch(AAliases[I].Pattern, AddrStr) then
begin
if (AAliases[I].MyAKA >= 0) and (AAliases[I].MyAKA < ANumAKAs) then
Exit(AAliases[I].MyAKA);
end;
end;
{ Default: pick AKA in same zone }
for I := 0 to ANumAKAs - 1 do
if AOurAKAs[I].Zone = ARemZone then
Exit(I);
end;
function GetSecurityLevel(APasswordMatch: Boolean;
AWeHavePassword: Boolean;
ANodeListed: Boolean): Integer;
begin
if APasswordMatch and AWeHavePassword then
Result := Ord(slProtected)
else if AWeHavePassword then
Result := Ord(slKnown)
else if ANodeListed then
Result := Ord(slListed)
else
Result := Ord(slUnlisted);
end;
procedure RemapAddress(var AZone, ANet, ANode, APoint, APointNet: Integer);
begin
{ If node is -1 (0xFFFF), this is a point using fake net }
if ANode = -1 then
begin
APointNet := ANet;
APoint := ANode; { Actually the point number was in node field }
{ Need boss node info from nodelist to fully resolve }
end;
end;
end.