feat(fls): import the e-mail programs into the editor model

The e-mail client and server of every host come into the model (core:
EmailClientConfiguration, EmailServerConfiguration, EmailMessage), from
Java's view:

- the server's accounts are those of /mailserver/konten.txt, as Java reads
  them at every simulation start; without the file those of the bean list,
  which Java would drop;
- the client's account is that of /konten.txt; without it the first of an
  older file's kontoListe, which FILIUS 2.14 no longer reads;
- messages are numbered per program in folder order, as Swift does; the
  sent folder's are sent; a date is taken only when the file gives one.

Departures from Swift: accounts and messages stay where Swift drops what
fails its checks; first and last name stay apart; a port that is no number
is the default, with a warning. On the corpus the model matches Swift's on
all 13 files; the one Swift error is named (it numbers a mailbox's messages
as their received time and writes that into the konten.txt it rewrites).

The Swift dump now carries the e-mail state, and the file system
comparison leaves out only Swift's hidden state files (and the directories
it makes for them) instead of all of /mail, /mailserver and /peer2peer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 07:36:05 +00:00
co-authored by Claude Opus 5.5
parent 8f9930f1ff
commit 1efbb8a660
11 changed files with 1404 additions and 27 deletions
+21 -4
View File
@@ -33,10 +33,10 @@
// - a web server's files stay in /webserver (its document root), where Swift
// moves them to /www; and no program state is written into a host's files
// (Swift keeps Gnutella and e-mail state there as hidden JSON files).
// - e-mail: see importEmail.ts;
// - a DNS server's records are what Java's server answers from /dns/hosts
// (see dnsHosts.ts); Swift reads the file its own way — tabs, comments,
// "IN", names with a leading digit — and drops Java's duplicates.
// Not here yet: e-mail accounts.
import {
canonicalMacAddress,
@@ -65,6 +65,8 @@ import {
type DhcpClientConfiguration,
type DhcpServerConfiguration,
type DnsServerConfiguration,
type EmailClientConfiguration,
type EmailServerConfiguration,
type EchoServerConfiguration,
type GnutellaConfiguration,
type InstallableProgram,
@@ -92,6 +94,7 @@ import {
type VirtualFileContent,
} from '@mdg-filius/core';
import { dnsRecordsOf, javaHostsFileContent, readDnsHosts } from './dnsHosts.js';
import { emailClientConfiguration, emailServerConfiguration } from './importEmail.js';
import type { FlsApplicationClass, FlsFileTree, FlsFirewall } from './systemSoftware.js';
import type { FlsDocumentationItem, FlsInterface, FlsNode, FlsNodeClass, FlsTopology } from './topology.js';
@@ -119,6 +122,8 @@ export interface ImportedProject {
webServerConfigurations: Map<Id, WebServerConfiguration>;
gnutellaConfigurations: Map<Id, GnutellaConfiguration>;
dnsServerConfigurations: Map<Id, DnsServerConfiguration>;
emailClientConfigurations: Map<Id, EmailClientConfiguration>;
emailServerConfigurations: Map<Id, EmailServerConfiguration>;
echoServerConfigurations: Map<Id, EchoServerConfiguration>;
simpleClientConfigurations: Map<Id, SimpleClientConfiguration>;
/** In render order: rectangles below text, then by order. */
@@ -162,6 +167,8 @@ export function importProject(topology: FlsTopology, newId: () => Id): ImportedP
webServerConfigurations: new Map(),
gnutellaConfigurations: new Map(),
dnsServerConfigurations: new Map(),
emailClientConfigurations: new Map(),
emailServerConfigurations: new Map(),
echoServerConfigurations: new Map(),
simpleClientConfigurations: new Map(),
};
@@ -369,6 +376,8 @@ type SystemConfigurations = Pick<
| 'webServerConfigurations'
| 'gnutellaConfigurations'
| 'dnsServerConfigurations'
| 'emailClientConfigurations'
| 'emailServerConfigurations'
| 'echoServerConfigurations'
| 'simpleClientConfigurations'
>;
@@ -393,6 +402,10 @@ export const PROGRAM_OF_CLASS: Readonly<Record<FlsApplicationClass, InstallableP
/** The web server's document root: where Java keeps its files. */
export const JAVA_WEB_ROOT = '/webserver';
function warnAt(node: TopologyNode, warnings: string[]): (message: string) => void {
return (message) => warnings.push(`node '${node.displayName}': ${message}`);
}
/** The program parts of Swift's finalizeCurrentNodeCandidate, for a PC or notebook. */
function registerPrograms(source: FlsNode, node: TopologyNode, system: SystemConfigurations, warnings: string[]): void {
const installed = new Set<InstallableProgram>();
@@ -420,9 +433,7 @@ function registerPrograms(source: FlsNode, node: TopologyNode, system: SystemCon
const primary = trimmedOrEmpty(source.interfaces[0]?.ip);
const forwarding = recursive && forwarder !== '' && forwarder !== primary;
const hosts = javaHostsFileContent(source.system.fileSystem);
const records = dnsRecordsOf(readDnsHosts(hosts ?? ''), (message) =>
warnings.push(`node '${node.displayName}': ${message}`),
);
const records = dnsRecordsOf(readDnsHosts(hosts ?? ''), warnAt(node, warnings));
system.dnsServerConfigurations.set(
node.id,
forwarding
@@ -431,6 +442,12 @@ function registerPrograms(source: FlsNode, node: TopologyNode, system: SystemCon
);
break;
}
case 'emailClient':
system.emailClientConfigurations.set(node.id, emailClientConfiguration(application, warnAt(node, warnings)));
break;
case 'emailServer':
system.emailServerConfigurations.set(node.id, emailServerConfiguration(application, warnAt(node, warnings)));
break;
case 'firewall':
if (application.firewall !== undefined) {
system.firewallConfigurationsByNodeId.set(node.id, firewallConfigurationOf(application.firewall, `node '${node.displayName}'`, warnings));