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
+3 -2
View File
@@ -12,7 +12,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
| `src/model/node.ts` | `Model/TopologyNode.swift` | übertragen; Standard-Ports und -Namen wie Java; ein gegebener Name bleibt, auch leer (Java lässt Router unbenannt) | | `src/model/node.ts` | `Model/TopologyNode.swift` | übertragen; Standard-Ports und -Namen wie Java; ein gegebener Name bleibt, auch leer (Java lässt Router unbenannt) |
| `src/model/link.ts`, `src/model/graph.ts` | `Model/TopologyLink.swift`, `Model/TopologyGraph.swift` | übertragen, bisher nur die Daten (Abfragen folgen mit dem Editor) | | `src/model/link.ts`, `src/model/graph.ts` | `Model/TopologyLink.swift`, `Model/TopologyGraph.swift` | übertragen, bisher nur die Daten (Abfragen folgen mit dem Editor) |
| `src/model/documentation.ts` | `Model/TopologyDocumentation.swift` | übertragen; FNV-Kennung mit BigInt, gegen die IDs aus dem Swift-Import geprüft | | `src/model/documentation.ts` | `Model/TopologyDocumentation.swift` | übertragen; FNV-Kennung mit BigInt, gegen die IDs aus dem Swift-Import geprüft |
| `src/runtime/config.ts` | `State/TopologyEditorState.swift` (Laufzeit-Konfigurationen, WLAN eines Rechners, DHCP, Portweiterleitung, Remote Link ohne LAN-Transport, Programme und ihre Konfiguration, Firewall, DNS-Einträge aus `State/TopologyDNSResolver.swift`); dazu Echo-Server- und Client-Konfiguration, die Swift nicht speichert | übertragen; Java-Beschriftungen der Oberfläche weggelassen | | `src/runtime/config.ts` | `State/TopologyEditorState.swift` (Laufzeit-Konfigurationen, WLAN eines Rechners, DHCP, Portweiterleitung, Remote Link ohne LAN-Transport, Programme und ihre Konfiguration, Firewall, DNS-Einträge aus `State/TopologyDNSResolver.swift`, E-Mail-Konten und -Nachrichten aus `State/TopologyEmail.swift`); dazu Echo-Server- und Client-Konfiguration, die Swift nicht speichert | übertragen; Java-Beschriftungen der Oberfläche weggelassen |
| `src/runtime/engine.ts` | `State/TopologyNetworkRuntimeEngine.swift` Z. ~313–3585, ~4753–4850 | übertragen: Warteschlange, Uhr, Ethernet, Switch, ARP, IPv4, ICMP, Routing, Firewall. Klasse statt Werttyp. Noch nicht übertragen (wirft `NotPortedError`): Gateway-NAT, UDP, TCP, DHCP, DNS, RIP, Remote Link, WLAN | | `src/runtime/engine.ts` | `State/TopologyNetworkRuntimeEngine.swift` Z. ~313–3585, ~4753–4850 | übertragen: Warteschlange, Uhr, Ethernet, Switch, ARP, IPv4, ICMP, Routing, Firewall. Klasse statt Werttyp. Noch nicht übertragen (wirft `NotPortedError`): Gateway-NAT, UDP, TCP, DHCP, DNS, RIP, Remote Link, WLAN |
| `src/runtime/ipv4.ts` | Engine `parseIPv4`/`sameSubnet` | übertragen | | `src/runtime/ipv4.ts` | Engine `parseIPv4`/`sameSubnet` | übertragen |
| `src/runtime/packets.ts` | Engine, Paketmodelle | übertragen; `Data` → `Uint8Array` | | `src/runtime/packets.ts` | Engine, Paketmodelle | übertragen; `Data` → `Uint8Array` |
@@ -38,7 +38,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
| `src/archive.ts` | ebenda, Archiv-Teil von Import und Export | übertragen, ohne die XML-Ebene | | `src/archive.ts` | ebenda, Archiv-Teil von Import und Export | übertragen, ohne die XML-Ebene |
| `src/javabeans.ts`, `src/javaDefaults.json` | — | neu: XMLEncoder-Leser nach den Regeln von `java.beans.XMLDecoder` (Swift liest elementweise und verliert geteilte Werte); Getter-Standardwerte und statische Konstanten mit `tools/java-oracle` gemessen | | `src/javabeans.ts`, `src/javaDefaults.json` | — | neu: XMLEncoder-Leser nach den Regeln von `java.beans.XMLDecoder` (Swift liest elementweise und verliert geteilte Werte); Getter-Standardwerte und statische Konstanten mit `tools/java-oracle` gemessen |
| `src/importProject.ts` | `Persistence/TopologyProjectStore.swift` (Knoten, Ports, Kabel, IP-Konfiguration, Switch/WLAN, Routen, RIP, Firewall, DHCP, Portweiterleitung, Modem, Dateisysteme, Programme, Doku-Elemente des Imports) | übertragen, Eingabe ist Javas Sicht: Name und Port-Anzahl wie Java, abgelehnte Adresse → Swift-Standard, Router-Standardgateway wie Java (Swift verliert es), jede Router-Firewall eingetragen, Konfigurationstexte wie in Swift getrimmt (`swiftTrim`), Rechner ohne Dateisystem leer wie in Java (Swift: Demo-Dateien), alle Programme bleiben (Swift verwirft sechs), Webserver-Dateien bleiben in `/webserver` (Swift: `/www`), keine versteckten Zustandsdateien im Dateisystem, DNS-Einträge so, wie Javas Server sie aus `/dns/hosts` beantwortet; gegen Swifts importiertes Modell auf 13 Dateien geprüft | | `src/importProject.ts` | `Persistence/TopologyProjectStore.swift` (Knoten, Ports, Kabel, IP-Konfiguration, Switch/WLAN, Routen, RIP, Firewall, DHCP, Portweiterleitung, Modem, Dateisysteme, Programme, Doku-Elemente des Imports) | übertragen, Eingabe ist Javas Sicht: Name und Port-Anzahl wie Java, abgelehnte Adresse → Swift-Standard, Router-Standardgateway wie Java (Swift verliert es), jede Router-Firewall eingetragen, Konfigurationstexte wie in Swift getrimmt (`swiftTrim`), Rechner ohne Dateisystem leer wie in Java (Swift: Demo-Dateien), alle Programme bleiben (Swift verwirft sechs), Webserver-Dateien bleiben in `/webserver` (Swift: `/www`), keine versteckten Zustandsdateien im Dateisystem, DNS-Einträge so, wie Javas Server sie aus `/dns/hosts` beantwortet; gegen Swifts importiertes Modell auf 13 Dateien geprüft |
| `test/importProject.test.ts` | — | neu: Swifts importiertes Modell (13 Dateien, zwei begründete Abweichungen), Vollständigkeit aller 23, Pings im Kern (über einen Router; über drei Router mit manuellen Routen und Router-Gateways) | | `test/importProject.test.ts` | — | neu: Swifts importiertes Modell (13 Dateien, begründete Abweichungen; im Dateisystem ausgenommen nur Swifts versteckte Zustandsdateien), Vollständigkeit aller 23, Pings im Kern (über einen Router; über drei Router mit manuellen Routen und Router-Gateways) |
| `src/systemSoftware.ts` | Java `SwitchFirmware`, `Betriebssystem`, `VermittlungsrechnerBetriebssystem`, `GatewayFirmware`, `Weiterleitungstabelle`, `Firewall`, `FirewallRule`, `DHCPServer`, `ModemFirmware`, `Dateisystem`, `Datei` | neu nach Java: SSID, Lernzeit, RIP, manuelle Routen, Router-Firewall (an allen Stellen, an denen die Datei sie erreicht), DHCP (ein kaputter statischer Eintrag fällt allein weg, Java verliert alle folgenden), statische NAT, Modem, Dateisystem und installierte Programme von Rechner und Notebook; `Character.isWhitespace` am JDK gemessen; gegen Java geprüft | | `src/systemSoftware.ts` | Java `SwitchFirmware`, `Betriebssystem`, `VermittlungsrechnerBetriebssystem`, `GatewayFirmware`, `Weiterleitungstabelle`, `Firewall`, `FirewallRule`, `DHCPServer`, `ModemFirmware`, `Dateisystem`, `Datei` | neu nach Java: SSID, Lernzeit, RIP, manuelle Routen, Router-Firewall (an allen Stellen, an denen die Datei sie erreicht), DHCP (ein kaputter statischer Eintrag fällt allein weg, Java verliert alle folgenden), statische NAT, Modem, Dateisystem und installierte Programme von Rechner und Notebook; `Character.isWhitespace` am JDK gemessen; gegen Java geprüft |
| `src/javaIp.ts` | Java `IP.inetAton`/`ipCheck` | neu nach Java; an 46 Fällen mit Java gemessen | | `src/javaIp.ts` | Java `IP.inetAton`/`ipCheck` | neu nach Java; an 46 Fällen mit Java gemessen |
| `src/dnsHosts.ts` | Java `DNSServer.leseRecordListe`, `ResourceRecord`, `Dateisystem.holeDatei`; Swift `State/TopologyDNSResolver.swift` (`TopologyDNSHostsFileCodec`) als Vergleich | neu nach Java: Felder nur an einzelnen Leerzeichen, keine Kommentare, ungültige TTL → 3600, Doppelte bleiben; ins Modell kommt nur, was Java beantwortet (Typ A/MX/NS, Name nicht leer); Javas Fehler nicht übernommen: Label mit Ziffer vorn erlaubt, kein Backslash, letztes Label nicht rein numerisch (RFC 1123/3696), Typ in jeder Schreibung (RFC 4343), negative TTL ungültig (RFC 2181); an 76 Fällen mit Java und Swift gemessen | | `src/dnsHosts.ts` | Java `DNSServer.leseRecordListe`, `ResourceRecord`, `Dateisystem.holeDatei`; Swift `State/TopologyDNSResolver.swift` (`TopologyDNSHostsFileCodec`) als Vergleich | neu nach Java: Felder nur an einzelnen Leerzeichen, keine Kommentare, ungültige TTL → 3600, Doppelte bleiben; ins Modell kommt nur, was Java beantwortet (Typ A/MX/NS, Name nicht leer); Javas Fehler nicht übernommen: Label mit Ziffer vorn erlaubt, kein Backslash, letztes Label nicht rein numerisch (RFC 1123/3696), Typ in jeder Schreibung (RFC 4343), negative TTL ungültig (RFC 2181); an 76 Fällen mit Java und Swift gemessen |
@@ -47,6 +47,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
| `test/topology.test.ts` | — | neu: alle 23 Korpus-Dateien Feld für Feld wie Java (Geräte, Schnittstellen, Kabel, Doku-Elemente), dazu die Regeln, die der Korpus nicht berührt | | `test/topology.test.ts` | — | neu: alle 23 Korpus-Dateien Feld für Feld wie Java (Geräte, Schnittstellen, Kabel, Doku-Elemente), dazu die Regeln, die der Korpus nicht berührt |
| `test/javaIp.test.ts`, `test/ip-check.*` | — | neu: `IP.ipCheck`-Fälle, Ergebnis aus Java | | `test/javaIp.test.ts`, `test/ip-check.*` | — | neu: `IP.ipCheck`-Fälle, Ergebnis aus Java |
| `src/javaEmail.ts` | Java `EmailServer` (Konten-Bean, `kontenLaden`/`stringZuListeBenutzerkonten`), `EmailAnwendung` (Ordner, Adressbuch, `laden`), `EmailKonto`, `Email`, `AddressEntry`, `EmailUtils` | neu nach Java: E-Mail-Beans über ihre Setter, `/mailserver/konten.txt` und `/konten.txt` im Format von Javas Lesern, ohne ihre Fehler (leere Felder zählen, eine kurze Zeile fällt allein weg, Nachrichten mit leerem Text oder Betreff bleiben, `;` im Betreff bleibt, CRLF, beim Client die letzte vollständige Zeile); Empfänger älterer Dateien als Zeichenketten; gegen Java auf dem Korpus und an 43 Fällen geprüft (13 benannte Java-Fehler) | | `src/javaEmail.ts` | Java `EmailServer` (Konten-Bean, `kontenLaden`/`stringZuListeBenutzerkonten`), `EmailAnwendung` (Ordner, Adressbuch, `laden`), `EmailKonto`, `Email`, `AddressEntry`, `EmailUtils` | neu nach Java: E-Mail-Beans über ihre Setter, `/mailserver/konten.txt` und `/konten.txt` im Format von Javas Lesern, ohne ihre Fehler (leere Felder zählen, eine kurze Zeile fällt allein weg, Nachrichten mit leerem Text oder Betreff bleiben, `;` im Betreff bleibt, CRLF, beim Client die letzte vollständige Zeile); Empfänger älterer Dateien als Zeichenketten; gegen Java auf dem Korpus und an 43 Fällen geprüft (13 benannte Java-Fehler) |
| `src/importEmail.ts` | `Persistence/TopologyProjectStore.swift` (`registerJavaEmailConfigurations`, `assignedJavaEmailMessages`) | übertragen, Eingabe ist Javas Sicht: Server-Konten aus `/mailserver/konten.txt`, ohne Datei aus der Bean-Liste (Java verlöre sie); Client-Konto aus `/konten.txt`, ohne Datei aus der älteren `kontoListe` (Filius 2.14 liest sie nicht mehr); Konten und Nachrichten bleiben, wo Swift verwirft, was seine Prüfung nicht besteht; Vor- und Nachname getrennt; Empfangszeit nur aus der Datei (Swift nummeriert); gegen Swifts Modell geprüft |
| `src/javaFileSystem.ts`, `src/javaString.ts` | Java `Dateisystem.holeDatei`, `String.trim`/`split`/`equalsIgnoreCase` | neu nach Java; die Groß-/Kleinschreibung gegenüber ASCII am JDK über die ganze BMP gemessen | | `src/javaFileSystem.ts`, `src/javaString.ts` | Java `Dateisystem.holeDatei`, `String.trim`/`split`/`equalsIgnoreCase` | neu nach Java; die Groß-/Kleinschreibung gegenüber ASCII am JDK über die ganze BMP gemessen |
| `test/javaEmail.test.ts`, `test/email-*.cases.txt`, `test/email-*.java.json`, `test/javaEmailNames.ts`, `test/javaFileSystem.test.ts` | — | neu: Kontodateien von Server und Client, Ergebnis aus Java; Dateifund | | `test/javaEmail.test.ts`, `test/email-*.cases.txt`, `test/email-*.java.json`, `test/javaEmailNames.ts`, `test/javaFileSystem.test.ts` | — | neu: Kontodateien von Server und Client, Ergebnis aus Java; Dateifund |
| `test/dnsHosts.test.ts`, `test/dns-hosts.*` | — | neu: `/dns/hosts`-Fälle, wie Java bis auf 7 benannte Java-Fehler, das Modell wie Swift bis auf benannte Fälle mit Grund | | `test/dnsHosts.test.ts`, `test/dns-hosts.*` | — | neu: `/dns/hosts`-Fälle, wie Java bis auf 7 benannte Java-Fehler, das Modell wie Swift bis auf benannte Fälle mit Grund |
+842
View File
@@ -5644,6 +5644,26 @@
266 266
], ],
"system" : { "system" : {
"emailClient" : {
"drafts" : [
],
"email" : "rechner3@senior.de",
"inbox" : [
],
"name" : "Rechner 3",
"nextMessageID" : 1,
"password" : "rechner3",
"pop3Host" : "141.99.50.233",
"pop3Port" : 110,
"sent" : [
],
"smtpHost" : "141.99.50.233",
"smtpPort" : 25,
"username" : "rechner3"
},
"installedPrograms" : [ "installedPrograms" : [
"emailClient", "emailClient",
"webBrowser" "webBrowser"
@@ -5695,6 +5715,26 @@
375 375
], ],
"system" : { "system" : {
"emailClient" : {
"drafts" : [
],
"email" : "rechner4@senior.de",
"inbox" : [
],
"name" : "Rechner 4",
"nextMessageID" : 1,
"password" : "rechner4",
"pop3Host" : "141.99.50.233",
"pop3Port" : 110,
"sent" : [
],
"smtpHost" : "141.99.50.233",
"smtpPort" : 25,
"username" : "rechner4"
},
"installedPrograms" : [ "installedPrograms" : [
"emailClient", "emailClient",
"webBrowser" "webBrowser"
@@ -5746,6 +5786,26 @@
59 59
], ],
"system" : { "system" : {
"emailClient" : {
"drafts" : [
],
"email" : "rechner1@filius.de",
"inbox" : [
],
"name" : "Rechner 1",
"nextMessageID" : 1,
"password" : "rechner1",
"pop3Host" : "141.99.50.232",
"pop3Port" : 110,
"sent" : [
],
"smtpHost" : "141.99.50.232",
"smtpPort" : 25,
"username" : "rechner1"
},
"installedPrograms" : [ "installedPrograms" : [
"emailClient", "emailClient",
"webBrowser" "webBrowser"
@@ -5797,6 +5857,26 @@
161 161
], ],
"system" : { "system" : {
"emailClient" : {
"drafts" : [
],
"email" : "rechner2@filius.de",
"inbox" : [
],
"name" : "Rechner 2",
"nextMessageID" : 1,
"password" : "rechner2",
"pop3Host" : "141.99.50.232",
"pop3Port" : 110,
"sent" : [
],
"smtpHost" : "141.99.50.232",
"smtpPort" : 25,
"username" : "rechner2"
},
"installedPrograms" : [ "installedPrograms" : [
"emailClient", "emailClient",
"webBrowser" "webBrowser"
@@ -6094,6 +6174,29 @@
285 285
], ],
"system" : { "system" : {
"emailServer" : {
"accounts" : [
{
"mailbox" : [
],
"name" : "Vorname Nachname",
"password" : "rechner1",
"username" : "rechner1"
},
{
"mailbox" : [
],
"name" : "Vorname Nachname",
"password" : "rechner2",
"username" : "rechner2"
}
],
"domain" : "filius.de",
"nextMessageID" : 1,
"pop3Port" : 110
},
"installedPrograms" : [ "installedPrograms" : [
"emailServer" "emailServer"
] ]
@@ -6144,6 +6247,29 @@
414 414
], ],
"system" : { "system" : {
"emailServer" : {
"accounts" : [
{
"mailbox" : [
],
"name" : "Vorname Nachname",
"password" : "rechner3",
"username" : "rechner3"
},
{
"mailbox" : [
],
"name" : "Vorname Nachname",
"password" : "rechner4",
"username" : "rechner4"
}
],
"domain" : "senior.de",
"nextMessageID" : 1,
"pop3Port" : 110
},
"installedPrograms" : [ "installedPrograms" : [
"emailServer" "emailServer"
] ]
@@ -6575,6 +6701,26 @@
169 169
], ],
"system" : { "system" : {
"emailClient" : {
"drafts" : [
],
"email" : "herbert@gmx.com",
"inbox" : [
],
"name" : "Herbert Schmidt",
"nextMessageID" : 1,
"password" : "herbert1234",
"pop3Host" : "mail.gmx.com",
"pop3Port" : 110,
"sent" : [
],
"smtpHost" : "mail.gmx.com",
"smtpPort" : 25,
"username" : "herbert"
},
"installedPrograms" : [ "installedPrograms" : [
"emailClient", "emailClient",
"webBrowser" "webBrowser"
@@ -6791,6 +6937,26 @@
274 274
], ],
"system" : { "system" : {
"emailClient" : {
"drafts" : [
],
"email" : "frimp@webmail.com",
"inbox" : [
],
"name" : "Frimp Mueller",
"nextMessageID" : 1,
"password" : "frimp1234",
"pop3Host" : "mail.webmail.com",
"pop3Port" : 110,
"sent" : [
],
"smtpHost" : "mail.webmail.com",
"smtpPort" : 25,
"username" : "frimp"
},
"installedPrograms" : [ "installedPrograms" : [
"emailClient", "emailClient",
"webBrowser" "webBrowser"
@@ -7279,6 +7445,29 @@
417 417
], ],
"system" : { "system" : {
"emailServer" : {
"accounts" : [
{
"mailbox" : [
],
"name" : "Vorname Nachname",
"password" : "herbert1234",
"username" : "herbert"
},
{
"mailbox" : [
],
"name" : "Vorname Nachname",
"password" : "george1234",
"username" : "george"
}
],
"domain" : "gmx.com",
"nextMessageID" : 1,
"pop3Port" : 110
},
"installedPrograms" : [ "installedPrograms" : [
"emailServer" "emailServer"
] ]
@@ -7329,6 +7518,21 @@
684 684
], ],
"system" : { "system" : {
"emailServer" : {
"accounts" : [
{
"mailbox" : [
],
"name" : "Vorname Nachname",
"password" : "frimp1234",
"username" : "frimp"
}
],
"domain" : "webmail.com",
"nextMessageID" : 1,
"pop3Port" : 110
},
"installedPrograms" : [ "installedPrograms" : [
"emailServer" "emailServer"
] ]
@@ -7476,6 +7680,26 @@
840 840
], ],
"system" : { "system" : {
"emailClient" : {
"drafts" : [
],
"email" : "george@gmx.com",
"inbox" : [
],
"name" : "George Brown",
"nextMessageID" : 1,
"password" : "george1234",
"pop3Host" : "mail.gmx.com",
"pop3Port" : 110,
"sent" : [
],
"smtpHost" : "mail.gmx.com",
"smtpPort" : 25,
"username" : "george"
},
"installedPrograms" : [ "installedPrograms" : [
"emailClient", "emailClient",
"webBrowser" "webBrowser"
@@ -8406,6 +8630,103 @@
165 165
], ],
"system" : { "system" : {
"emailClient" : {
"drafts" : [
],
"email" : "herbert@gmx.de",
"inbox" : [
],
"name" : "Herbert Schmidt",
"nextMessageID" : 4,
"password" : "herbert1234",
"pop3Host" : "mail.gmx.de",
"pop3Port" : 110,
"sent" : [
{
"bcc" : [
],
"body" : "Huhu! \n\nHi Frimp!\n\nDein \nHerbert",
"cc" : [
],
"deliveryIdentity" : null,
"from" : [
"Herbert Schmidt",
"herbert@gmx.de"
],
"id" : 1,
"isMarkedForDeletion" : false,
"isNew" : true,
"isSent" : true,
"receivedAtMilliseconds" : null,
"subject" : "Hallo Frimp!",
"to" : [
[
null,
"frimp@webmail.de"
]
]
},
{
"bcc" : [
],
"body" : "Hallo Frimp!\n\nFrimp Müller <frimp@webmail.de> schrieb:\n> Huhu! \n> \n> Hi Herbert!\n> \n> Dein \n> Frimp\n> ",
"cc" : [
],
"deliveryIdentity" : null,
"from" : [
"Herbert Schmidt",
"herbert@gmx.de"
],
"id" : 2,
"isMarkedForDeletion" : false,
"isNew" : true,
"isSent" : true,
"receivedAtMilliseconds" : null,
"subject" : "RE: Hallo Herbert!",
"to" : [
[
null,
"frimp@webmail.de"
]
]
},
{
"bcc" : [
],
"body" : "jetzt?!\n\nFrimp Mueller <frimp@webmail.de> schrieb:\n> Hi Herbert Again!\n> \n> Herbert Schmidt <herbert@gmx.de> schrieb:\n> > Hallo Frimp!\n> > \n> > Frimp Müller <frimp@webmail.de> schrieb:\n> > > Huhu! \n> > > \n> > > Hi Herbert!\n> > > \n> > > Dein \n> > > Frimp\n> > >\n> >\n> ",
"cc" : [
],
"deliveryIdentity" : null,
"from" : [
"Herbert Schmidt",
"herbert@gmx.de"
],
"id" : 3,
"isMarkedForDeletion" : false,
"isNew" : true,
"isSent" : true,
"receivedAtMilliseconds" : null,
"subject" : "RE: RE: RE: Hallo Herbert!",
"to" : [
[
"Frimp Mueller",
"frimp@webmail.de"
]
]
}
],
"smtpHost" : "mail.gmx.de",
"smtpPort" : 25,
"username" : "herbert"
},
"installedPrograms" : [ "installedPrograms" : [
"emailClient", "emailClient",
"webBrowser" "webBrowser"
@@ -9054,6 +9375,29 @@
417 417
], ],
"system" : { "system" : {
"emailServer" : {
"accounts" : [
{
"mailbox" : [
],
"name" : "Vorname Nachname",
"password" : "herbert1234",
"username" : "herbert"
},
{
"mailbox" : [
],
"name" : "Vorname Nachname",
"password" : "george1234",
"username" : "george"
}
],
"domain" : "gmx.de",
"nextMessageID" : 1,
"pop3Port" : 110
},
"installedPrograms" : [ "installedPrograms" : [
"emailServer" "emailServer"
] ]
@@ -9104,6 +9448,21 @@
649 649
], ],
"system" : { "system" : {
"emailServer" : {
"accounts" : [
{
"mailbox" : [
],
"name" : "Vorname Nachname",
"password" : "frimp1234",
"username" : "frimp"
}
],
"domain" : "webmail.de",
"nextMessageID" : 1,
"pop3Port" : 110
},
"installedPrograms" : [ "installedPrograms" : [
"emailServer" "emailServer"
] ]
@@ -9251,6 +9610,26 @@
826 826
], ],
"system" : { "system" : {
"emailClient" : {
"drafts" : [
],
"email" : "george@gmx.de",
"inbox" : [
],
"name" : "George Brown",
"nextMessageID" : 1,
"password" : "george1234",
"pop3Host" : "mail.gmx.de",
"pop3Port" : 110,
"sent" : [
],
"smtpHost" : "mail.gmx.de",
"smtpPort" : 25,
"username" : "george"
},
"installedPrograms" : [ "installedPrograms" : [
"emailClient", "emailClient",
"webBrowser" "webBrowser"
@@ -11349,6 +11728,258 @@
264 264
], ],
"system" : { "system" : {
"emailClient" : {
"drafts" : [
],
"email" : "alice@senior.de",
"inbox" : [
{
"bcc" : [
],
"body" : "test",
"cc" : [
],
"deliveryIdentity" : null,
"from" : [
"Rechner 1",
"rechner1@filius.de"
],
"id" : 1,
"isMarkedForDeletion" : false,
"isNew" : true,
"isSent" : false,
"receivedAtMilliseconds" : null,
"subject" : "test",
"to" : [
[
null,
"rechner3@senior.de"
]
]
},
{
"bcc" : [
],
"body" : "",
"cc" : [
],
"deliveryIdentity" : null,
"from" : [
"Bob 1",
"bob@filius.de"
],
"id" : 2,
"isMarkedForDeletion" : false,
"isNew" : true,
"isSent" : false,
"receivedAtMilliseconds" : null,
"subject" : "test3",
"to" : [
[
null,
"alice@senior.de"
]
]
},
{
"bcc" : [
],
"body" : "test bl\n",
"cc" : [
],
"deliveryIdentity" : null,
"from" : [
"Bob 1",
"bob@filius.de"
],
"id" : 3,
"isMarkedForDeletion" : false,
"isNew" : true,
"isSent" : false,
"receivedAtMilliseconds" : null,
"subject" : "test bl",
"to" : [
[
null,
"alice@senior.de"
]
]
}
],
"name" : "Alice 3",
"nextMessageID" : 10,
"password" : "alice",
"pop3Host" : "141.99.50.233",
"pop3Port" : 110,
"sent" : [
{
"bcc" : [
],
"body" : "Hallo Bob,\n\nschön von dir zu hören.\n\nGruß\nAlice",
"cc" : [
],
"deliveryIdentity" : null,
"from" : [
"Alice 3",
"alice@senior.de"
],
"id" : 4,
"isMarkedForDeletion" : false,
"isNew" : true,
"isSent" : true,
"receivedAtMilliseconds" : null,
"subject" : "Beste Grüße",
"to" : [
[
null,
"Bob@filius.de"
]
]
},
{
"bcc" : [
],
"body" : "test",
"cc" : [
],
"deliveryIdentity" : null,
"from" : [
"Alice 3",
"alice@senior.de"
],
"id" : 5,
"isMarkedForDeletion" : false,
"isNew" : true,
"isSent" : true,
"receivedAtMilliseconds" : null,
"subject" : "test",
"to" : [
[
null,
"bob@filius.de"
]
]
},
{
"bcc" : [
],
"body" : "\n\nBob 1 schrieb:\n> ",
"cc" : [
],
"deliveryIdentity" : null,
"from" : [
"Alice 3",
"alice@senior.de"
],
"id" : 6,
"isMarkedForDeletion" : false,
"isNew" : true,
"isSent" : true,
"receivedAtMilliseconds" : null,
"subject" : "RE: test3",
"to" : [
[
null,
"bob@filius.de"
]
]
},
{
"bcc" : [
],
"body" : "\n\nBob 1 schrieb:\n> ",
"cc" : [
],
"deliveryIdentity" : null,
"from" : [
"Alice 3",
"alice@senior.de"
],
"id" : 7,
"isMarkedForDeletion" : false,
"isNew" : true,
"isSent" : true,
"receivedAtMilliseconds" : null,
"subject" : "RE: test3",
"to" : [
[
null,
"bob@filius.de"
]
]
},
{
"bcc" : [
],
"body" : "huhu bob!\n\nBob 1 <bob@filius.de> schrieb:\n> test bl\n> ",
"cc" : [
],
"deliveryIdentity" : null,
"from" : [
"Alice 3",
"alice@senior.de"
],
"id" : 8,
"isMarkedForDeletion" : false,
"isNew" : true,
"isSent" : true,
"receivedAtMilliseconds" : null,
"subject" : "RE: test bl",
"to" : [
[
"Bob 1",
"bob@filius.de"
]
]
},
{
"bcc" : [
],
"body" : "Hi BoB!",
"cc" : [
],
"deliveryIdentity" : null,
"from" : [
"Alice 3",
"alice@senior.de"
],
"id" : 9,
"isMarkedForDeletion" : false,
"isNew" : true,
"isSent" : true,
"receivedAtMilliseconds" : null,
"subject" : "Hi Bob!",
"to" : [
[
null,
"bob@filius.de"
]
]
}
],
"smtpHost" : "141.99.50.233",
"smtpPort" : 25,
"username" : "alice"
},
"installedPrograms" : [ "installedPrograms" : [
"emailClient", "emailClient",
"webBrowser" "webBrowser"
@@ -11444,6 +12075,14 @@
], ],
"recursiveResolutionEnabled" : false "recursiveResolutionEnabled" : false
}, },
"emailServer" : {
"accounts" : [
],
"domain" : "eve.de",
"nextMessageID" : 1,
"pop3Port" : 110
},
"installedPrograms" : [ "installedPrograms" : [
"dnsServer", "dnsServer",
"emailServer", "emailServer",
@@ -11496,6 +12135,128 @@
120 120
], ],
"system" : { "system" : {
"emailClient" : {
"drafts" : [
],
"email" : "bob@filius.de",
"inbox" : [
{
"bcc" : [
],
"body" : "Bob 1 schrieb:\n>",
"cc" : [
],
"deliveryIdentity" : null,
"from" : [
"Alice 3",
"alice@senior.de"
],
"id" : 1,
"isMarkedForDeletion" : false,
"isNew" : true,
"isSent" : false,
"receivedAtMilliseconds" : null,
"subject" : "RE: test3",
"to" : [
[
null,
"bob@filius.de"
]
]
}
],
"name" : "Bob 1",
"nextMessageID" : 5,
"password" : "bob",
"pop3Host" : "141.99.50.232",
"pop3Port" : 110,
"sent" : [
{
"bcc" : [
],
"body" : "test",
"cc" : [
],
"deliveryIdentity" : null,
"from" : [
"Rechner 1",
"rechner1@filius.de"
],
"id" : 2,
"isMarkedForDeletion" : false,
"isNew" : true,
"isSent" : true,
"receivedAtMilliseconds" : null,
"subject" : "test",
"to" : [
[
null,
"rechner3@senior.de"
]
]
},
{
"bcc" : [
],
"body" : "",
"cc" : [
],
"deliveryIdentity" : null,
"from" : [
"Bob 1",
"bob@filius.de"
],
"id" : 3,
"isMarkedForDeletion" : false,
"isNew" : true,
"isSent" : true,
"receivedAtMilliseconds" : null,
"subject" : "test3",
"to" : [
[
null,
"alice@senior.de"
]
]
},
{
"bcc" : [
],
"body" : "test bl",
"cc" : [
],
"deliveryIdentity" : null,
"from" : [
"Bob 1",
"bob@filius.de"
],
"id" : 4,
"isMarkedForDeletion" : false,
"isNew" : true,
"isSent" : true,
"receivedAtMilliseconds" : null,
"subject" : "test bl",
"to" : [
[
null,
"alice@senior.de"
]
]
}
],
"smtpHost" : "141.99.50.232",
"smtpPort" : 25,
"username" : "bob"
},
"installedPrograms" : [ "installedPrograms" : [
"emailClient", "emailClient",
"webBrowser" "webBrowser"
@@ -11793,6 +12554,72 @@
382 382
], ],
"system" : { "system" : {
"emailServer" : {
"accounts" : [
{
"mailbox" : [
{
"bcc" : [
],
"body" : "huhu bob!\n\nBob 1 <bob@filius.de> schrieb:\n> test bl\n>\n",
"cc" : [
],
"deliveryIdentity" : null,
"from" : [
"Alice 3",
"alice@senior.de"
],
"id" : 1,
"isMarkedForDeletion" : false,
"isNew" : true,
"isSent" : false,
"receivedAtMilliseconds" : 1,
"subject" : "RE: test bl",
"to" : [
[
"Bob 1",
"bob@filius.de"
]
]
},
{
"bcc" : [
],
"body" : "Hi BoB!\n",
"cc" : [
],
"deliveryIdentity" : null,
"from" : [
"Alice 3",
"alice@senior.de"
],
"id" : 2,
"isMarkedForDeletion" : false,
"isNew" : true,
"isSent" : false,
"receivedAtMilliseconds" : 2,
"subject" : "Hi Bob!",
"to" : [
[
null,
"bob@filius.de"
]
]
}
],
"name" : "Vorname Nachname",
"password" : "bob",
"username" : "bob"
}
],
"domain" : "filius.de",
"nextMessageID" : 3,
"pop3Port" : 110
},
"installedPrograms" : [ "installedPrograms" : [
"emailServer" "emailServer"
] ]
@@ -11843,6 +12670,21 @@
405 405
], ],
"system" : { "system" : {
"emailServer" : {
"accounts" : [
{
"mailbox" : [
],
"name" : "Vorname Nachname",
"password" : "alice",
"username" : "alice"
}
],
"domain" : "senior.de",
"nextMessageID" : 1,
"pop3Port" : 110
},
"installedPrograms" : [ "installedPrograms" : [
"emailServer" "emailServer"
] ]
+80
View File
@@ -201,6 +201,86 @@ export interface DnsServerConfiguration {
readonly records: readonly DnsResourceRecord[]; readonly records: readonly DnsResourceRecord[];
} }
/**
* An e-mail address as Java's AddressEntry holds it: a display name, an
* address, or both (Swift's TopologyRuntimeEmailAddress wants the address).
*/
export interface EmailAddress {
readonly name: string | null;
readonly mailAddress: string | null;
}
/** An e-mail (Swift's TopologyRuntimeEmailMessage without its run-time delivery identity). */
export interface EmailMessage {
/** Numbered from 1 per program, in folder order, as Swift numbers them. */
readonly id: number;
readonly from: EmailAddress;
readonly to: readonly EmailAddress[];
readonly cc: readonly EmailAddress[];
readonly bcc: readonly EmailAddress[];
readonly subject: string;
readonly body: string;
/** Only when the file gives one; Java never sets it. */
readonly receivedAtMilliseconds?: number;
readonly isNew: boolean;
readonly isMarkedForDeletion: boolean;
/** A message of the sent folder. */
readonly isSent: boolean;
}
/** The account an e-mail client uses. First and last name stay apart, as Java keeps them. */
export interface EmailClientAccount {
readonly pop3Host: string;
readonly pop3Port: number;
readonly smtpHost: string;
readonly smtpPort: number;
readonly username: string;
readonly password: string;
readonly firstName: string;
readonly lastName: string;
readonly email: string;
}
/** Java's Kontakt: an address book entry. */
export interface EmailContact {
readonly name: string;
readonly firstName: string;
readonly street: string;
readonly houseNumber: number;
readonly postalCode: number;
readonly city: string;
readonly email: string;
readonly phone: string;
}
export interface EmailClientConfiguration {
/** Null when the client has none (Java opens it without one). */
readonly account: EmailClientAccount | null;
readonly inbox: readonly EmailMessage[];
readonly sent: readonly EmailMessage[];
readonly drafts: readonly EmailMessage[];
readonly addressBook: readonly EmailContact[];
readonly nextMessageID: number;
}
export interface EmailServerAccount {
readonly username: string;
readonly password: string;
readonly firstName: string;
readonly lastName: string;
readonly mailbox: readonly EmailMessage[];
}
export interface EmailServerConfiguration {
readonly domain: string;
readonly pop3Port: number;
readonly accounts: readonly EmailServerAccount[];
readonly nextMessageID: number;
}
export const DEFAULT_POP3_PORT = 110;
export const DEFAULT_SMTP_PORT = 25;
/** /**
* Java's echo server (ServerBaustein) and generic client (ClientBaustein) * Java's echo server (ServerBaustein) and generic client (ClientBaustein)
* keep their port and target in the file; Swift asks for them at run time. * keep their port and target in the file; Swift asks for them at run time.
+133
View File
@@ -0,0 +1,133 @@
// The e-mail programs of a host in the editor model (Swift's
// registerJavaEmailConfigurations), from Java's view (javaEmail.ts). Java
// decides what the programs hold; its errors are not copied:
// - the server's accounts are those of /mailserver/konten.txt, which Java
// reads at every simulation start; without the file, those of the bean
// list, which Java would drop there;
// - 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;
// - a recipient an older file keeps as a string is read as an address.
// Departures from Swift (named in importProject.test.ts): accounts and
// messages stay as Java has them, 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 (Swift drops the account); a message's date is taken only
// when the file gives one (Swift numbers a mailbox's messages instead).
import {
DEFAULT_POP3_PORT,
DEFAULT_SMTP_PORT,
type EmailAddress,
type EmailClientAccount,
type EmailClientConfiguration,
type EmailContact,
type EmailMessage,
type EmailServerConfiguration,
} from '@mdg-filius/core';
import { addressEntry, type JavaEmail, type JavaEmailAccount, type JavaRecipient } from './javaEmail.js';
import { javaParseInt } from './javaIp.js';
import type { FlsApplication } from './systemSoftware.js';
type Warn = (message: string) => void;
export function emailClientConfiguration(application: FlsApplication, warn: Warn): EmailClientConfiguration {
const source = application.fileAccount ?? application.legacyAccounts?.[0] ?? null;
const numbering = { next: 1 };
const inbox = emailMessages(application.received ?? [], numbering, undefined, warn);
const sent = emailMessages(application.sent ?? [], numbering, true, warn);
const drafts = emailMessages(application.drafts ?? [], numbering, false, warn);
return {
account: source === null ? null : clientAccount(source, warn),
inbox,
sent,
drafts,
addressBook: (application.addressBook ?? []).map(
(contact): EmailContact => ({
name: contact.name ?? '',
firstName: contact.firstName ?? '',
street: contact.street ?? '',
houseNumber: contact.houseNumber,
postalCode: contact.postalCode,
city: contact.city ?? '',
email: contact.email ?? '',
phone: contact.phone ?? '',
}),
),
nextMessageID: numbering.next,
};
}
export function emailServerConfiguration(application: FlsApplication, warn: Warn): EmailServerConfiguration {
const accounts = Array.isArray(application.fileAccounts) ? application.fileAccounts : (application.beanAccounts ?? []);
const numbering = { next: 1 };
return {
domain: application.mailDomain ?? 'filius.de',
pop3Port: DEFAULT_POP3_PORT,
accounts: accounts.map((account) => ({
username: account.username ?? '',
password: account.password ?? '',
firstName: account.firstName,
lastName: account.lastName,
mailbox: emailMessages(account.messages, numbering, undefined, warn),
})),
nextMessageID: numbering.next,
};
}
function clientAccount(account: JavaEmailAccount, warn: Warn): EmailClientAccount {
return {
pop3Host: account.pop3Server ?? '',
pop3Port: port(account.pop3Port, DEFAULT_POP3_PORT, 'POP3', warn),
smtpHost: account.smtpServer ?? '',
smtpPort: port(account.smtpPort, DEFAULT_SMTP_PORT, 'SMTP', warn),
username: account.username ?? '',
password: account.password ?? '',
firstName: account.firstName,
lastName: account.lastName,
email: account.emailAddress ?? '',
};
}
/** A port as Java's client reads it (Integer.parseInt); none or no port is the default. */
function port(text: string | null, fallback: number, protocol: string, warn: Warn): number {
if (text === null) return fallback;
const value = javaParseInt(text);
if (value !== undefined && value >= 0 && value <= 65_535) return value;
warn(`e-mail client: ${protocol} port '${text}' is not a port, used ${fallback}`);
return fallback;
}
/** Numbered on from `numbering.next`; `isSent` is the folder's when it has one. */
function emailMessages(
messages: readonly JavaEmail[],
numbering: { next: number },
isSent: boolean | undefined,
warn: Warn,
): EmailMessage[] {
return messages.map((message) => {
const received = message.dateReceived !== null && /^[0-9]+$/.test(message.dateReceived) ? Number(message.dateReceived) : undefined;
return {
id: numbering.next++,
from: { name: message.sender.name, mailAddress: message.sender.mailAddress },
to: recipients(message.to, warn),
cc: recipients(message.cc, warn),
bcc: recipients(message.bcc, warn),
subject: message.subject ?? '',
body: message.text ?? '',
...(received === undefined ? {} : { receivedAtMilliseconds: received }),
isNew: message.isNew,
isMarkedForDeletion: message.isDeleted,
isSent: isSent ?? message.isSent,
};
});
}
function recipients(entries: readonly JavaRecipient[], warn: Warn): EmailAddress[] {
return entries.flatMap((entry) => {
if (entry === null) {
warn('an e-mail recipient is null, left out');
return [];
}
const address = typeof entry === 'string' ? addressEntry(entry) : entry;
return [{ name: address.name, mailAddress: address.mailAddress }];
});
}
+21 -4
View File
@@ -33,10 +33,10 @@
// - a web server's files stay in /webserver (its document root), where Swift // - 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 // 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). // (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 // - 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, // (see dnsHosts.ts); Swift reads the file its own way — tabs, comments,
// "IN", names with a leading digit — and drops Java's duplicates. // "IN", names with a leading digit — and drops Java's duplicates.
// Not here yet: e-mail accounts.
import { import {
canonicalMacAddress, canonicalMacAddress,
@@ -65,6 +65,8 @@ import {
type DhcpClientConfiguration, type DhcpClientConfiguration,
type DhcpServerConfiguration, type DhcpServerConfiguration,
type DnsServerConfiguration, type DnsServerConfiguration,
type EmailClientConfiguration,
type EmailServerConfiguration,
type EchoServerConfiguration, type EchoServerConfiguration,
type GnutellaConfiguration, type GnutellaConfiguration,
type InstallableProgram, type InstallableProgram,
@@ -92,6 +94,7 @@ import {
type VirtualFileContent, type VirtualFileContent,
} from '@mdg-filius/core'; } from '@mdg-filius/core';
import { dnsRecordsOf, javaHostsFileContent, readDnsHosts } from './dnsHosts.js'; import { dnsRecordsOf, javaHostsFileContent, readDnsHosts } from './dnsHosts.js';
import { emailClientConfiguration, emailServerConfiguration } from './importEmail.js';
import type { FlsApplicationClass, FlsFileTree, FlsFirewall } from './systemSoftware.js'; import type { FlsApplicationClass, FlsFileTree, FlsFirewall } from './systemSoftware.js';
import type { FlsDocumentationItem, FlsInterface, FlsNode, FlsNodeClass, FlsTopology } from './topology.js'; import type { FlsDocumentationItem, FlsInterface, FlsNode, FlsNodeClass, FlsTopology } from './topology.js';
@@ -119,6 +122,8 @@ export interface ImportedProject {
webServerConfigurations: Map<Id, WebServerConfiguration>; webServerConfigurations: Map<Id, WebServerConfiguration>;
gnutellaConfigurations: Map<Id, GnutellaConfiguration>; gnutellaConfigurations: Map<Id, GnutellaConfiguration>;
dnsServerConfigurations: Map<Id, DnsServerConfiguration>; dnsServerConfigurations: Map<Id, DnsServerConfiguration>;
emailClientConfigurations: Map<Id, EmailClientConfiguration>;
emailServerConfigurations: Map<Id, EmailServerConfiguration>;
echoServerConfigurations: Map<Id, EchoServerConfiguration>; echoServerConfigurations: Map<Id, EchoServerConfiguration>;
simpleClientConfigurations: Map<Id, SimpleClientConfiguration>; simpleClientConfigurations: Map<Id, SimpleClientConfiguration>;
/** In render order: rectangles below text, then by order. */ /** In render order: rectangles below text, then by order. */
@@ -162,6 +167,8 @@ export function importProject(topology: FlsTopology, newId: () => Id): ImportedP
webServerConfigurations: new Map(), webServerConfigurations: new Map(),
gnutellaConfigurations: new Map(), gnutellaConfigurations: new Map(),
dnsServerConfigurations: new Map(), dnsServerConfigurations: new Map(),
emailClientConfigurations: new Map(),
emailServerConfigurations: new Map(),
echoServerConfigurations: new Map(), echoServerConfigurations: new Map(),
simpleClientConfigurations: new Map(), simpleClientConfigurations: new Map(),
}; };
@@ -369,6 +376,8 @@ type SystemConfigurations = Pick<
| 'webServerConfigurations' | 'webServerConfigurations'
| 'gnutellaConfigurations' | 'gnutellaConfigurations'
| 'dnsServerConfigurations' | 'dnsServerConfigurations'
| 'emailClientConfigurations'
| 'emailServerConfigurations'
| 'echoServerConfigurations' | 'echoServerConfigurations'
| 'simpleClientConfigurations' | '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. */ /** The web server's document root: where Java keeps its files. */
export const JAVA_WEB_ROOT = '/webserver'; 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. */ /** The program parts of Swift's finalizeCurrentNodeCandidate, for a PC or notebook. */
function registerPrograms(source: FlsNode, node: TopologyNode, system: SystemConfigurations, warnings: string[]): void { function registerPrograms(source: FlsNode, node: TopologyNode, system: SystemConfigurations, warnings: string[]): void {
const installed = new Set<InstallableProgram>(); 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 primary = trimmedOrEmpty(source.interfaces[0]?.ip);
const forwarding = recursive && forwarder !== '' && forwarder !== primary; const forwarding = recursive && forwarder !== '' && forwarder !== primary;
const hosts = javaHostsFileContent(source.system.fileSystem); const hosts = javaHostsFileContent(source.system.fileSystem);
const records = dnsRecordsOf(readDnsHosts(hosts ?? ''), (message) => const records = dnsRecordsOf(readDnsHosts(hosts ?? ''), warnAt(node, warnings));
warnings.push(`node '${node.displayName}': ${message}`),
);
system.dnsServerConfigurations.set( system.dnsServerConfigurations.set(
node.id, node.id,
forwarding forwarding
@@ -431,6 +442,12 @@ function registerPrograms(source: FlsNode, node: TopologyNode, system: SystemCon
); );
break; 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': case 'firewall':
if (application.firewall !== undefined) { if (application.firewall !== undefined) {
system.firewallConfigurationsByNodeId.set(node.id, firewallConfigurationOf(application.firewall, `node '${node.displayName}'`, warnings)); system.firewallConfigurationsByNodeId.set(node.id, firewallConfigurationOf(application.firewall, `node '${node.displayName}'`, warnings));
+22 -6
View File
@@ -11,7 +11,7 @@
// the account files of test/email-*.cases.txt, where Java's errors in reading // the account files of test/email-*.cases.txt, where Java's errors in reading
// them are not copied (see readEmailServerFile, readEmailClientFile). // them are not copied (see readEmailServerFile, readEmailClientFile).
import { isJavaObject, listItems, simpleClassName, type JavaObject, type JavaValue } from './javabeans.js'; import { isJavaObject, listItems, mapEntries, simpleClassName, type JavaObject, type JavaValue } from './javabeans.js';
import { javaSplit, javaTrim } from './javaString.js'; import { javaSplit, javaTrim } from './javaString.js';
/** AddressEntry: a display name, an address, or both. */ /** AddressEntry: a display name, an address, or both. */
@@ -73,7 +73,23 @@ type Warn = (message: string) => void;
// JavaBeans // JavaBeans
export function javaEmailAccounts(list: JavaValue | undefined, warn: Warn): JavaEmailAccount[] { export function javaEmailAccounts(list: JavaValue | undefined, warn: Warn): JavaEmailAccount[] {
return objectsOf(list, 'EmailKonto', warn).map((account) => { return emailAccountsOf(listItems(list), warn);
}
/**
* The client accounts of older files (EmailAnwendung's kontoListe, a map by
* address). FILIUS 2.14 has no such property and loses them; they are read
* for files without /konten.txt.
*/
export function javaLegacyEmailAccounts(map: JavaValue | undefined, warn: Warn): JavaEmailAccount[] {
return emailAccountsOf(
mapEntries(map).map(([, account]) => account),
warn,
);
}
function emailAccountsOf(items: JavaValue[], warn: Warn): JavaEmailAccount[] {
return objectsOf(items, 'EmailKonto', warn).map((account) => {
const at = (property: string) => text(account.properties.get(property), `EmailKonto ${property}`, warn); const at = (property: string) => text(account.properties.get(property), `EmailKonto ${property}`, warn);
return { return {
username: at('benutzername'), username: at('benutzername'),
@@ -92,7 +108,7 @@ export function javaEmailAccounts(list: JavaValue | undefined, warn: Warn): Java
} }
export function javaEmails(list: JavaValue | undefined, warn: Warn): JavaEmail[] { export function javaEmails(list: JavaValue | undefined, warn: Warn): JavaEmail[] {
return objectsOf(list, 'Email', warn).map((email) => { return objectsOf(listItems(list), 'Email', warn).map((email) => {
const at = (property: string) => email.properties.get(property); const at = (property: string) => email.properties.get(property);
const flag = (property: string, fallback: boolean) => { const flag = (property: string, fallback: boolean) => {
const value = at(property); const value = at(property);
@@ -117,7 +133,7 @@ export function javaEmails(list: JavaValue | undefined, warn: Warn): JavaEmail[]
} }
export function javaContacts(list: JavaValue | undefined, warn: Warn): JavaContact[] { export function javaContacts(list: JavaValue | undefined, warn: Warn): JavaContact[] {
return objectsOf(list, 'Kontakt', warn).map((contact) => { return objectsOf(listItems(list), 'Kontakt', warn).map((contact) => {
const at = (property: string) => text(contact.properties.get(property), `Kontakt ${property}`, warn); const at = (property: string) => text(contact.properties.get(property), `Kontakt ${property}`, warn);
const number = (property: string) => { const number = (property: string) => {
const value = contact.properties.get(property); const value = contact.properties.get(property);
@@ -172,8 +188,8 @@ function beanAddressEntry(entry: JavaObject, warn: Warn): JavaAddressEntry {
}; };
} }
function objectsOf(list: JavaValue | undefined, simpleClass: string, warn: Warn): JavaObject[] { function objectsOf(items: JavaValue[], simpleClass: string, warn: Warn): JavaObject[] {
return listItems(list).flatMap((item) => { return items.flatMap((item) => {
if (isJavaObject(item) && simpleClassName(item.className) === simpleClass) return [item]; if (isJavaObject(item) && simpleClassName(item.className) === simpleClass) return [item];
warn(`a list of ${simpleClass} holds something else, left out`); warn(`a list of ${simpleClass} holds something else, left out`);
return []; return [];
+4
View File
@@ -34,6 +34,7 @@ import {
javaContacts, javaContacts,
javaEmailAccounts, javaEmailAccounts,
javaEmails, javaEmails,
javaLegacyEmailAccounts,
readEmailClientFile, readEmailClientFile,
readEmailServerFile, readEmailServerFile,
type JavaContact, type JavaContact,
@@ -167,6 +168,8 @@ export interface FlsApplication {
addressBook?: JavaContact[]; addressBook?: JavaContact[];
/** EmailAnwendung: the account of /konten.txt, null without one. */ /** EmailAnwendung: the account of /konten.txt, null without one. */
fileAccount?: JavaEmailAccount | null; fileAccount?: JavaEmailAccount | null;
/** EmailAnwendung: the accounts of an older file's kontoListe, which FILIUS 2.14 ignores. */
legacyAccounts?: JavaEmailAccount[];
/** The application's JavaBean, for the e-mail layer. */ /** The application's JavaBean, for the e-mail layer. */
source: JavaObject; source: JavaObject;
} }
@@ -325,6 +328,7 @@ function applications(map: JavaValue | undefined, root: FlsFileTree, warn: Warn)
entry.addressBook = javaContacts(at('adressbuch'), warn); entry.addressBook = javaContacts(at('adressbuch'), warn);
const file = javaFileContent(root, EMAIL_CLIENT_FILE); const file = javaFileContent(root, EMAIL_CLIENT_FILE);
entry.fileAccount = file === undefined ? null : readEmailClientFile(file, warn); entry.fileAccount = file === undefined ? null : readEmailClientFile(file, warn);
entry.legacyAccounts = javaLegacyEmailAccounts(at('kontoListe'), warn);
break; break;
} }
} }
+232 -14
View File
@@ -19,6 +19,8 @@ import {
portSnapshot, portSnapshot,
topologySnapshot, topologySnapshot,
VirtualFileSystem, VirtualFileSystem,
type EmailAddress,
type EmailMessage,
type Id, type Id,
type NodeKind, type NodeKind,
type VirtualFileEntry, type VirtualFileEntry,
@@ -29,6 +31,8 @@ import {
readFlsArchive, readFlsArchive,
readTopology, readTopology,
type FlsApplication, type FlsApplication,
type JavaEmail,
type JavaEmailAccount,
type FlsFileTree, type FlsFileTree,
type FlsNode, type FlsNode,
type FlsTopology, type FlsTopology,
@@ -148,6 +152,39 @@ function systemOf(project: ImportedProject, nodeId: Id) {
records: dns.records.map((record) => [record.name, record.type, record.ttlSeconds, record.target]), records: dns.records.map((record) => [record.name, record.type, record.ttlSeconds, record.target]),
}; };
} }
const emailClient = project.emailClientConfigurations.get(nodeId);
// Swift keeps no client without an account.
if (emailClient?.account) {
const { account } = emailClient;
result.emailClient = {
pop3Host: account.pop3Host,
pop3Port: account.pop3Port,
smtpHost: account.smtpHost,
smtpPort: account.smtpPort,
username: account.username,
password: account.password,
name: fullName(account),
email: account.email,
inbox: emailClient.inbox.map(swiftMessage),
sent: emailClient.sent.map(swiftMessage),
drafts: emailClient.drafts.map(swiftMessage),
nextMessageID: emailClient.nextMessageID,
};
}
const emailServer = project.emailServerConfigurations.get(nodeId);
if (emailServer !== undefined) {
result.emailServer = {
domain: emailServer.domain,
pop3Port: emailServer.pop3Port,
accounts: emailServer.accounts.map((account) => ({
username: account.username,
password: account.password,
name: fullName(account),
mailbox: account.mailbox.map(swiftMessage),
})),
nextMessageID: emailServer.nextMessageID,
};
}
const remoteLink = project.remoteLinkConfigurations.get(nodeId); const remoteLink = project.remoteLinkConfigurations.get(nodeId);
if (remoteLink !== undefined) result.remoteLink = remoteLink; if (remoteLink !== undefined) result.remoteLink = remoteLink;
const firewall = project.firewallConfigurationsByNodeId.get(nodeId); const firewall = project.firewallConfigurationsByNodeId.get(nodeId);
@@ -172,6 +209,29 @@ function systemOf(project: ImportedProject, nodeId: Id) {
return result; return result;
} }
/** Swift keeps one name, first and last joined. */
function fullName(person: { firstName: string; lastName: string }): string {
return [person.firstName, person.lastName].filter((part) => part !== '').join(' ');
}
function swiftMessage(message: EmailMessage) {
const address = (entry: EmailAddress) => [entry.name, entry.mailAddress];
return {
id: message.id,
from: address(message.from),
to: message.to.map(address),
cc: message.cc.map(address),
bcc: message.bcc.map(address),
subject: message.subject,
body: message.body,
deliveryIdentity: null,
receivedAtMilliseconds: message.receivedAtMilliseconds ?? null,
isNew: message.isNew,
isMarkedForDeletion: message.isMarkedForDeletion,
isSent: message.isSent,
};
}
/** The shape describeImportedState (tools/swift-oracle) writes: indices instead of random ids. */ /** The shape describeImportedState (tools/swift-oracle) writes: indices instead of random ids. */
function asSwiftState(project: ImportedProject) { function asSwiftState(project: ImportedProject) {
const { nodes, links } = project.graph; const { nodes, links } = project.graph;
@@ -241,9 +301,37 @@ function withJavaDefaultNames(state: unknown): unknown {
* Where the Swift import is wrong and Java (corpus.java.json) is right, per * Where the Swift import is wrong and Java (corpus.java.json) is right, per
* file and node: the corrected node. * file and node: the corrected node.
*/ */
type SwiftNode = { displayName: string; hostLabelPolicy: string; system: Record<string, unknown> }; type SwiftNode = { displayName: string; hostLabelPolicy: string; system: Record<string, unknown>; fileSystem: unknown };
const shownByIp = (displayName: string) => (node: SwiftNode) => ({ ...node, displayName, hostLabelPolicy: 'ipAddress' }); const shownByIp = (displayName: string) => (node: SwiftNode) => ({ ...node, displayName, hostLabelPolicy: 'ipAddress' });
/**
* Swift numbers a server mailbox's messages as their received time when
* the file has none (Java never sets one), and writes the numbers into the
* konten.txt it rewrites.
*/
const unnumberedMailbox = (numbered: string[]) => (node: SwiftNode) => {
const server = node.system.emailServer as { accounts: { mailbox: { receivedAtMilliseconds: unknown }[] }[] };
const fileSystem = (node.fileSystem as { path: string; text?: string }[]).map((entry) =>
entry.path === '/mailserver/konten.txt'
? { ...entry, text: numbered.reduce((text, number) => text.replace(`$$$${number}$`, () => '$$$$'), entry.text!) }
: entry,
);
return {
...node,
fileSystem,
system: {
...node.system,
emailServer: {
...server,
accounts: server.accounts.map((account) => ({
...account,
mailbox: account.mailbox.map((message) => ({ ...message, receivedAtMilliseconds: null })),
})),
},
},
};
};
const SWIFT_ERRORS: Record<string, Record<number, (node: SwiftNode) => SwiftNode>> = { const SWIFT_ERRORS: Record<string, Record<number, (node: SwiftNode) => SwiftNode>> = {
'java-examples/man-in-the-middle.fls': { 5: unnumberedMailbox(['1', '2']) },
'java-examples/multicast_server.fls': { 'java-examples/multicast_server.fls': {
// useIPAsName is `<object idref="Boolean0"/>`, the shared result of a // useIPAsName is `<object idref="Boolean0"/>`, the shared result of a
// button's isEnabled() — true. Swift's reader cannot follow it: it shows // button's isEnabled() — true. Swift's reader cannot follow it: it shows
@@ -263,20 +351,33 @@ function withSwiftErrorsCorrected(name: string, state: unknown): unknown {
} }
/** /**
* What applications do to a file system, which the applications step ports: * What Swift's programs do to a file system and ours do not: Swift keeps the
* Swift keeps the e-mail and Gnutella state as files (left out on both * e-mail and Gnutella state as hidden files too (left out here; the model
* sides here), and moves a web server's /webserver to /www (compared here * holds that state), and moves a web server's /webserver to /www (compared
* by reading our /webserver as /www where Swift has one). Everything else * here by reading our /webserver as /www where Swift has one). Everything
* in a file system must match. * else in a file system must match — also the /mail, /mailserver and
* /peer2peer directories Swift creates and the konten.txt files it rewrites.
*/ */
const APPLICATION_DIRECTORIES = ['/mail', '/mailserver', '/peer2peer']; const SWIFT_STATE_FILES = [
'/mail/.filiuspad-email-client.json',
'/mailserver/.filiuspad-email-server.json',
'/peer2peer/.filiuspad-gnutella.json',
];
/**
* Directories Swift creates for those files: left out of Swift's state when
* they hold nothing else and the file had none (ours has none then).
*/
const SWIFT_STATE_DIRECTORIES = ['/mail', '/mailserver', '/peer2peer'];
type FileEntries = { path: string }[] | null; type FileEntries = { path: string }[] | null;
function withoutApplicationDirectories(state: unknown, webRootOf?: (index: number) => boolean): unknown { function withoutApplicationDirectories(
state: unknown,
webRootOf?: (index: number) => boolean,
ours?: unknown,
): unknown {
const { nodes, ...rest } = state as { nodes: { fileSystem: FileEntries }[] }; const { nodes, ...rest } = state as { nodes: { fileSystem: FileEntries }[] };
const outside = (entry: { path: string }) => const ourNodes = (ours as { nodes: { fileSystem: FileEntries }[] } | undefined)?.nodes;
!APPLICATION_DIRECTORIES.some((directory) => entry.path === directory || entry.path.startsWith(`${directory}/`));
const asWebRoot = (entry: { path: string }) => const asWebRoot = (entry: { path: string }) =>
entry.path === '/webserver' || entry.path.startsWith('/webserver/') entry.path === '/webserver' || entry.path.startsWith('/webserver/')
? { ...entry, path: `/www${entry.path.slice('/webserver'.length)}` } ? { ...entry, path: `/www${entry.path.slice('/webserver'.length)}` }
@@ -285,7 +386,12 @@ function withoutApplicationDirectories(state: unknown, webRootOf?: (index: numbe
...rest, ...rest,
nodes: nodes.map((node, index) => { nodes: nodes.map((node, index) => {
if (node.fileSystem === null) return node; if (node.fileSystem === null) return node;
const entries = node.fileSystem.filter(outside).map((entry) => (webRootOf?.(index) ? asWebRoot(entry) : entry)); const files = node.fileSystem.filter((entry) => !SWIFT_STATE_FILES.includes(entry.path));
const holdsMore = (directory: string) => files.some((entry) => entry.path.startsWith(`${directory}/`));
const ourPaths = new Set((ourNodes?.[index]?.fileSystem ?? []).map((entry) => entry.path));
const outside = (entry: { path: string }) =>
!(ourNodes !== undefined && SWIFT_STATE_DIRECTORIES.includes(entry.path) && !holdsMore(entry.path) && !ourPaths.has(entry.path));
const entries = files.filter(outside).map((entry) => (webRootOf?.(index) ? asWebRoot(entry) : entry));
return { ...node, fileSystem: entries.sort((lhs, rhs) => (lhs.path < rhs.path ? -1 : lhs.path > rhs.path ? 1 : 0)) }; return { ...node, fileSystem: entries.sort((lhs, rhs) => (lhs.path < rhs.path ? -1 : lhs.path > rhs.path ? 1 : 0)) };
}), }),
}; };
@@ -313,9 +419,8 @@ describe('corpus: the imported model as Swift builds it', () => {
const project = importFile(file); const project = importFile(file);
const expected = withSwiftErrorsCorrected(name, withJavaDefaultNames(swiftView[name]!.state)); const expected = withSwiftErrorsCorrected(name, withJavaDefaultNames(swiftView[name]!.state));
expect(withoutApplicationDirectories(asSwiftState(project), swiftWebRoots(expected))).toEqual( const ours = withoutApplicationDirectories(asSwiftState(project), swiftWebRoots(expected));
withoutApplicationDirectories(expected), expect(ours).toEqual(withoutApplicationDirectories(expected, undefined, ours));
);
expect(project.warnings).toEqual(EXPECTED_WARNINGS[name] ?? []); expect(project.warnings).toEqual(EXPECTED_WARNINGS[name] ?? []);
}); });
} }
@@ -794,6 +899,119 @@ describe('importProject rules', () => {
]); ]);
}); });
describe('e-mail', () => {
const source = flsNode('Rechner').source;
const account = (username: string, fields: Partial<JavaEmailAccount> = {}): JavaEmailAccount => ({
username,
password: `${username}-pw`,
lastName: 'Lang',
firstName: 'Anna Lena',
emailAddress: `${username}@filius.de`,
pop3Server: '10.0.0.1',
pop3Port: '110',
smtpServer: '10.0.0.1',
smtpPort: '25',
messages: [],
...fields,
});
const email = (subject: string, fields: Partial<JavaEmail> = {}): JavaEmail => ({
sender: { name: 'Alice', mailAddress: 'alice@senior.de' },
to: ['Bob <bob@filius.de>'],
cc: [],
bcc: [],
subject,
text: 'Hi',
dateReceived: '',
isNew: true,
isDeleted: false,
isSent: false,
...fields,
});
const host = (application: Partial<FlsApplication>) =>
flsNode('Rechner', {
name: 'Host',
interfaces: [nic('10.0.0.2')],
system: { applications: [{ key: 'mail', javaClass: 'filius.software.email.EmailAnwendung', source, ...application }] },
});
const server = (application: Partial<FlsApplication>) =>
flsNode('Rechner', {
name: 'Server',
interfaces: [nic('10.0.0.1')],
system: {
applications: [{ key: 'mail', javaClass: 'filius.software.email.EmailServer', source, mailDomain: 'filius.de', beanAccounts: [], fileAccounts: null, ...application }],
},
});
const imported = (node: FlsNode) => {
const project = importProject(topology({ nodes: [node] }), counter());
const id = project.graph.nodes[0]!.id;
return { project, client: project.emailClientConfigurations.get(id), server: project.emailServerConfigurations.get(id) };
};
it("takes a server's accounts from /mailserver/konten.txt, and from the bean list without the file", () => {
expect(imported(server({ beanAccounts: [account('bean')], fileAccounts: [account('file')] })).server!.accounts.map((a) => a.username)).toEqual(['file']);
expect(imported(server({ beanAccounts: [account('bean')], fileAccounts: null })).server!.accounts.map((a) => a.username)).toEqual(['bean']);
expect(imported(server({ beanAccounts: [account('bean')], fileAccounts: [] })).server!.accounts).toEqual([]);
});
it("numbers a server's messages across its accounts and keeps names apart", () => {
const { server: configuration } = imported(
server({ fileAccounts: [account('a', { messages: [email('1'), email('2')] }), account('b', { messages: [email('3')] })] }),
);
expect(configuration!.accounts.map((a) => a.mailbox.map((m) => [m.id, m.subject]))).toEqual([[[1, '1'], [2, '2']], [[3, '3']]]);
expect(configuration!.accounts[0]).toMatchObject({ firstName: 'Anna Lena', lastName: 'Lang', password: 'a-pw' });
expect(configuration!.nextMessageID).toBe(4);
});
it("takes a client's account from /konten.txt, else from an older file's kontoListe, else none", () => {
expect(imported(host({ fileAccount: account('file'), legacyAccounts: [account('legacy')] })).client!.account?.username).toBe('file');
expect(imported(host({ fileAccount: null, legacyAccounts: [account('legacy'), account('second')] })).client!.account?.username).toBe('legacy');
expect(imported(host({ fileAccount: null, legacyAccounts: [] })).client!.account).toBeNull();
});
it('uses the default for a port that is no port, and says so', () => {
const { project, client } = imported(host({ fileAccount: account('u', { pop3Port: 'x', smtpPort: '70000' }) }));
expect(client!.account).toMatchObject({ pop3Port: 110, smtpPort: 25 });
expect(project.warnings).toEqual([
"node 'Host': e-mail client: POP3 port 'x' is not a port, used 110",
"node 'Host': e-mail client: SMTP port '70000' is not a port, used 25",
]);
});
it('numbers the folders in order, marks the sent folder sent, and reads string recipients as addresses', () => {
const { project, client } = imported(
host({
fileAccount: account('u'),
received: [email('in', { isSent: true, dateReceived: '1700000000000' })],
sent: [email('out', { to: [null, { name: null, mailAddress: 'x@y' }] })],
drafts: [email('draft', { isSent: true, subject: null, text: null })],
}),
);
expect(client!.inbox[0]).toMatchObject({ id: 1, isSent: true, receivedAtMilliseconds: 1_700_000_000_000, to: [{ name: 'Bob', mailAddress: 'bob@filius.de' }] });
expect(client!.sent[0]).toMatchObject({ id: 2, isSent: true, to: [{ name: null, mailAddress: 'x@y' }] });
expect(client!.sent[0]).not.toHaveProperty('receivedAtMilliseconds');
expect(client!.drafts[0]).toMatchObject({ id: 3, isSent: false, subject: '', body: '' });
expect(client!.nextMessageID).toBe(4);
expect(project.warnings).toEqual(["node 'Host': an e-mail recipient is null, left out"]);
});
it("keeps a message's flags and a date only when it is a number, and the address book", () => {
const { client } = imported(
host({
fileAccount: account('u'),
received: [email('read', { isNew: false, isDeleted: true, dateReceived: 'Mon, 1 Jan' })],
addressBook: [
{ name: 'Lang', firstName: null, street: 'Weg', houseNumber: 3, postalCode: 20095, city: 'Hamburg', email: 'a@b', phone: null },
],
}),
);
expect(client!.inbox[0]).toMatchObject({ isNew: false, isMarkedForDeletion: true });
expect(client!.inbox[0]).not.toHaveProperty('receivedAtMilliseconds');
expect(client!.addressBook).toEqual([
{ name: 'Lang', firstName: '', street: 'Weg', houseNumber: 3, postalCode: 20095, city: 'Hamburg', email: 'a@b', phone: '' },
]);
});
});
it("takes a DNS server's records from its /dns/hosts, and names the node in a warning", () => { it("takes a DNS server's records from its /dns/hosts, and names the node in a warning", () => {
const hosts = 'www.filius.de A 3600 141.99.2.10\nfilius.de MX 3600 mail.filius.de\nbroken.de A 3600 .1.2.3\n'; const hosts = 'www.filius.de A 3600 141.99.2.10\nfilius.de MX 3600 mail.filius.de\nbroken.de A 3600 .1.2.3\n';
const server = flsNode('Rechner', { const server = flsNode('Rechner', {
+21
View File
@@ -478,6 +478,27 @@ describe('readTopology rules', () => {
expect(server.beanAccounts![0]!.messages[0]!.sender).toEqual({ name: 'Alice', mailAddress: 'alice@senior.de' }); expect(server.beanAccounts![0]!.messages[0]!.sender).toEqual({ name: 'Alice', mailAddress: 'alice@senior.de' });
}); });
it("reads an older file's kontoListe, which FILIUS 2.14 ignores, in the order of its entries", () => {
const legacyAccount = (username: string) =>
`<void method="put"><string>${username}@senior.de</string><object class="filius.software.email.EmailKonto">
<void property="benutzername"><string>${username}</string></void>
<void property="pop3port"><string>110</string></void>
</object></void>`;
const { nodes } = topologyOf(`<object class="java.util.LinkedList">${nodeItem(`<object class="filius.hardware.knoten.Rechner">
<void property="systemSoftware"><void property="installierteAnwendungen">
<void method="put"><string>filius.software.email.EmailAnwendung</string><object class="filius.software.email.EmailAnwendung">
<void property="kontoListe">${legacyAccount('rechner3')}${legacyAccount('rechner4')}</void>
</object></void>
</void></void></object>`)}</object>`);
const client = nodes[0]!.system.applications![0]!;
expect(client.fileAccount).toBeNull();
expect(client.legacyAccounts!.map((account) => [account.username, account.pop3Port])).toEqual([
['rechner3', '110'],
['rechner4', '110'],
]);
});
it("gives a modem Java's defaults and keeps an address the file sets to null", () => { it("gives a modem Java's defaults and keeps an address the file sets to null", () => {
const { nodes } = topologyOf(`<object class="java.util.LinkedList"> const { nodes } = topologyOf(`<object class="java.util.LinkedList">
${nodeItem('<object class="filius.hardware.knoten.Modem"/>')} ${nodeItem('<object class="filius.hardware.knoten.Modem"/>')}
+1 -1
View File
@@ -41,7 +41,7 @@ der Port nicht übernimmt.
`--dns-hosts` liest dieselben `/dns/hosts`-Fälle wie das Java-Orakel `--dns-hosts` liest dieselben `/dns/hosts`-Fälle wie das Java-Orakel
(`packages/fls/test/dns-hosts.cases.txt`) mit Swifts (`packages/fls/test/dns-hosts.cases.txt`) mit Swifts
`TopologyDNSHostsFileCodec`; unter `state` stehen die DNS-Einträge jedes `TopologyDNSHostsFileCodec`; unter `state` stehen die DNS-Einträge jedes
Servers (`dnsServer.records`). Servers (`dnsServer.records`) und E-Mail-Client und -Server mit Konten und Nachrichten (`emailClient`, `emailServer`).
## Wie es gebaut wird ## Wie es gebaut wird
@@ -404,6 +404,30 @@ func describeSystem(_ state: TopologyEditorState, _ node: TopologyNode) -> JSON
"records": dns.typedRecords.map { dnsRecordJSON($0) }, "records": dns.typedRecords.map { dnsRecordJSON($0) },
] as JSON ] as JSON
} }
if let client = state.runtimeEmailClientConfigurationsByNodeID[node.id] {
json["emailClient"] = [
"pop3Host": client.pop3Host, "pop3Port": client.pop3Port,
"smtpHost": client.smtpHost, "smtpPort": client.smtpPort,
"username": client.username, "password": client.password,
"name": client.name, "email": client.email,
"inbox": client.inbox.map(emailMessageJSON),
"sent": client.sent.map(emailMessageJSON),
"drafts": client.drafts.map(emailMessageJSON),
"nextMessageID": Int(client.nextMessageID),
] as JSON
}
if let server = state.runtimeEmailServerConfigurationsByNodeID[node.id] {
json["emailServer"] = [
"domain": server.domain, "pop3Port": server.pop3Port,
"accounts": server.accounts.map { account in
[
"username": account.username, "password": account.password, "name": account.name,
"mailbox": account.mailbox.map(emailMessageJSON),
] as JSON
},
"nextMessageID": Int(server.nextMessageID),
] as JSON
}
if let link = state.remoteLinkConfigurationsByNodeID[node.id] { if let link = state.remoteLinkConfigurationsByNodeID[node.id] {
json["remoteLink"] = [ json["remoteLink"] = [
"pairIdentifier": link.pairIdentifier, "pairIdentifier": link.pairIdentifier,
@@ -615,6 +639,27 @@ func foldPaths(_ cases: [PathCase]) -> [JSON] {
} }
} }
func emailAddressJSON(_ address: TopologyRuntimeEmailAddress) -> [Any] {
[address.name ?? NSNull(), address.mailAddress]
}
func emailMessageJSON(_ message: TopologyRuntimeEmailMessage) -> JSON {
[
"id": Int(message.id),
"from": emailAddressJSON(message.from),
"to": message.to.map(emailAddressJSON),
"cc": message.cc.map(emailAddressJSON),
"bcc": message.bcc.map(emailAddressJSON),
"subject": message.subject,
"body": message.body,
"deliveryIdentity": message.deliveryIdentity ?? NSNull(),
"receivedAtMilliseconds": message.receivedAtMilliseconds.map { Int($0) } ?? NSNull(),
"isNew": message.isNew,
"isMarkedForDeletion": message.isMarkedForDeletion,
"isSent": message.isSent,
]
}
// MARK: - /dns/hosts cases (packages/fls/test/dns-hosts.cases.txt) // MARK: - /dns/hosts cases (packages/fls/test/dns-hosts.cases.txt)
func dnsRecordJSON(_ record: TopologyDNSResourceRecord) -> [Any] { func dnsRecordJSON(_ record: TopologyDNSResourceRecord) -> [Any] {