From ec5523d7c750b9280ffd264eefd7bc98b5fdd36f Mon Sep 17 00:00:00 2001 From: Julian Kiedaisch Date: Wed, 30 Sep 2026 07:06:55 +0000 Subject: [PATCH] feat(fls): Java's view of the e-mail programs The Java view now reads what FILIUS 2.14 keeps for its e-mail programs: the server's domain, switch and bean accounts with their mailboxes, and the accounts of /mailserver/konten.txt, which Java reads at every simulation start and which so win over the bean list; the client's three folders and address book, and its account from /konten.txt (no bean holds it). javaEmail.ts ports the bean setters and both file readers (stringZuListeBenutzerkonten, EmailKonto(String)); javaString.ts Java's trim, split and StringTokenizer; javaFileSystem.ts the file lookup, now shared with the DNS server, with the four non-ASCII units Java's equalsIgnoreCase also takes for an ASCII letter (measured over the BMP). Checked against Java on the corpus (the oracle dumps the e-mail state) and on 43 account files read by EmailServer.kontenLaden and EmailAnwendung.laden (EmailFileCases). Found: older files keep recipients as strings in the typed lists; a getter copy of the sender is filled and set back; one short server line makes Java throw for the whole file; a message with an empty text is lost on reading; a broken last client line empties the account. Co-Authored-By: Claude Opus 5.5 --- CHANGES.md | 3 + corpus/corpus.java.json | 1078 ++++++++++++++++++++- packages/fls/src/dnsHosts.ts | 45 +- packages/fls/src/index.ts | 3 + packages/fls/src/javaEmail.ts | 304 ++++++ packages/fls/src/javaFileSystem.ts | 51 + packages/fls/src/javaString.ts | 24 + packages/fls/src/systemSoftware.ts | 58 +- packages/fls/test/dnsHosts.test.ts | 24 +- packages/fls/test/email-client.cases.txt | 16 + packages/fls/test/email-client.java.json | 220 +++++ packages/fls/test/email-server.cases.txt | 27 + packages/fls/test/email-server.java.json | 740 ++++++++++++++ packages/fls/test/javaEmail.test.ts | 55 ++ packages/fls/test/javaEmailNames.ts | 37 + packages/fls/test/javaFileSystem.test.ts | 35 + packages/fls/test/topology.test.ts | 56 ++ scripts/code_language_allow.txt | 24 + tools/java-oracle/README.md | 15 +- tools/java-oracle/run.sh | 11 +- tools/java-oracle/src/EmailFileCases.java | 65 ++ tools/java-oracle/src/FlsOracle.java | 103 +- 22 files changed, 2900 insertions(+), 94 deletions(-) create mode 100644 packages/fls/src/javaEmail.ts create mode 100644 packages/fls/src/javaFileSystem.ts create mode 100644 packages/fls/src/javaString.ts create mode 100644 packages/fls/test/email-client.cases.txt create mode 100644 packages/fls/test/email-client.java.json create mode 100644 packages/fls/test/email-server.cases.txt create mode 100644 packages/fls/test/email-server.java.json create mode 100644 packages/fls/test/javaEmail.test.ts create mode 100644 packages/fls/test/javaEmailNames.ts create mode 100644 packages/fls/test/javaFileSystem.test.ts create mode 100644 tools/java-oracle/src/EmailFileCases.java diff --git a/CHANGES.md b/CHANGES.md index c5a2f76..5c9f5b0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -46,6 +46,9 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand | `test/javabeans.test.ts` | — | neu: je XMLDecoder-Regel ein Test; alle 23 Korpus-Dateien, Klasse und Position jedes Geräts wie Java | | `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 | +| `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` wie Javas Leser (StringTokenizer überspringt leere Felder, `split` verliert leere Felder am Ende); Empfänger älterer Dateien als Zeichenketten; gegen Java auf dem Korpus und an 43 Fällen geprüft | +| `src/javaFileSystem.ts`, `src/javaString.ts` | Java `Dateisystem.holeDatei`, `String.trim`/`split`/`equalsIgnoreCase`, `StringTokenizer` | 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/dnsHosts.test.ts`, `test/dns-hosts.*` | — | neu: `/dns/hosts`-Fälle, Javas Sicht wie Java, das Modell wie Swift bis auf 33 benannte Fälle in zwölf Gründen | | `test/zip.test.ts` | `TopologyProjectPersistenceTests.swift` (Archiv-Ablehnungstests, `makeMinimalZIP`, `makePayloadZIP`) | übertragen, ergänzt um DEFLATE-Grenzfälle | | `test/corpus.test.ts` | — | neu: 23 Korpus-Dateien, gegengeprüft mit `fflate.unzipSync` | diff --git a/corpus/corpus.java.json b/corpus/corpus.java.json index 99e5be8..80b1361 100644 --- a/corpus/corpus.java.json +++ b/corpus/corpus.java.json @@ -1820,7 +1820,23 @@ "applications": [ { "key": "filius.software.email.EmailAnwendung", - "class": "filius.software.email.EmailAnwendung" + "class": "filius.software.email.EmailAnwendung", + "received": [], + "sent": [], + "drafts": [], + "addressBook": [], + "fileAccount": { + "benutzername": "herbert", + "passwort": "herbert1234", + "nachname": "Schmidt", + "vorname": "Herbert", + "emailAdresse": "herbert@gmx.com", + "pop3server": "mail.gmx.com", + "pop3port": "110", + "smtpserver": "mail.gmx.com", + "smtpport": "25", + "nachrichten": [] + } }, { "key": "filius.software.lokal.Terminal", @@ -1923,7 +1939,23 @@ "applications": [ { "key": "filius.software.email.EmailAnwendung", - "class": "filius.software.email.EmailAnwendung" + "class": "filius.software.email.EmailAnwendung", + "received": [], + "sent": [], + "drafts": [], + "addressBook": [], + "fileAccount": { + "benutzername": "frimp", + "passwort": "frimp1234", + "nachname": "Mueller", + "vorname": "Frimp", + "emailAdresse": "frimp@webmail.com", + "pop3server": "mail.webmail.com", + "pop3port": "110", + "smtpserver": "mail.webmail.com", + "smtpport": "25", + "nachrichten": [] + } }, { "key": "filius.software.lokal.Terminal", @@ -2662,7 +2694,61 @@ "applications": [ { "key": "filius.software.email.EmailServer", - "class": "filius.software.email.EmailServer" + "class": "filius.software.email.EmailServer", + "aktiv": true, + "mailDomain": "gmx.com", + "beanAccounts": [ + { + "benutzername": "herbert", + "passwort": "herbert1234", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + }, + { + "benutzername": "george", + "passwort": "george1234", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + } + ], + "fileAccounts": [ + { + "benutzername": "herbert", + "passwort": "herbert1234", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + }, + { + "benutzername": "george", + "passwort": "george1234", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + } + ] } ], "dhcpClient": false, @@ -2721,7 +2807,37 @@ "applications": [ { "key": "filius.software.email.EmailServer", - "class": "filius.software.email.EmailServer" + "class": "filius.software.email.EmailServer", + "aktiv": true, + "mailDomain": "webmail.com", + "beanAccounts": [ + { + "benutzername": "frimp", + "passwort": "frimp1234", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + } + ], + "fileAccounts": [ + { + "benutzername": "frimp", + "passwort": "frimp1234", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + } + ] } ], "dhcpClient": false, @@ -2947,7 +3063,23 @@ "applications": [ { "key": "filius.software.email.EmailAnwendung", - "class": "filius.software.email.EmailAnwendung" + "class": "filius.software.email.EmailAnwendung", + "received": [], + "sent": [], + "drafts": [], + "addressBook": [], + "fileAccount": { + "benutzername": "george", + "passwort": "george1234", + "nachname": "Brown", + "vorname": "George", + "emailAdresse": "george@gmx.com", + "pop3server": "mail.gmx.com", + "pop3port": "110", + "smtpserver": "mail.gmx.com", + "smtpport": "25", + "nachrichten": [] + } }, { "key": "filius.software.lokal.Terminal", @@ -3009,7 +3141,23 @@ "applications": [ { "key": "filius.software.email.EmailAnwendung", - "class": "filius.software.email.EmailAnwendung" + "class": "filius.software.email.EmailAnwendung", + "received": [], + "sent": [], + "drafts": [], + "addressBook": [], + "fileAccount": { + "benutzername": "herbert", + "passwort": "herbert1234", + "nachname": "Schmidt", + "vorname": "Herbert", + "emailAdresse": "herbert@gmx.de", + "pop3server": "mail.gmx.de", + "pop3port": "110", + "smtpserver": "mail.gmx.de", + "smtpport": "25", + "nachrichten": [] + } }, { "key": "filius.software.lokal.Terminal", @@ -4160,7 +4308,84 @@ "applications": [ { "key": "filius.software.email.EmailAnwendung", - "class": "filius.software.email.EmailAnwendung" + "class": "filius.software.email.EmailAnwendung", + "received": [], + "sent": [ + { + "absender": [ + "Herbert Schmidt", + "herbert@gmx.de" + ], + "empfaenger": [ + [ + null, + "frimp@webmail.de" + ] + ], + "cc": [], + "bcc": [], + "betreff": "Hallo Frimp!", + "text": "Huhu! \n\nHi Frimp!\n\nDein \nHerbert", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + }, + { + "absender": [ + "Herbert Schmidt", + "herbert@gmx.de" + ], + "empfaenger": [ + [ + null, + "frimp@webmail.de" + ] + ], + "cc": [], + "bcc": [], + "betreff": "RE: Hallo Herbert!", + "text": "Hallo Frimp!\n\nFrimp Müller schrieb:\n> Huhu! \n> \n> Hi Herbert!\n> \n> Dein \n> Frimp\n> ", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + }, + { + "absender": [ + "Herbert Schmidt", + "herbert@gmx.de" + ], + "empfaenger": [ + [ + "Frimp Mueller", + "frimp@webmail.de" + ] + ], + "cc": [], + "bcc": [], + "betreff": "RE: RE: RE: Hallo Herbert!", + "text": "jetzt?!\n\nFrimp Mueller schrieb:\n> Hi Herbert Again!\n> \n> Herbert Schmidt schrieb:\n> > Hallo Frimp!\n> > \n> > Frimp Müller schrieb:\n> > > Huhu! \n> > > \n> > > Hi Herbert!\n> > > \n> > > Dein \n> > > Frimp\n> > >\n> >\n> ", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + } + ], + "drafts": [], + "addressBook": [], + "fileAccount": { + "benutzername": "herbert", + "passwort": "herbert1234", + "nachname": "Schmidt", + "vorname": "Herbert", + "emailAdresse": "herbert@gmx.de", + "pop3server": "mail.gmx.de", + "pop3port": "110", + "smtpserver": "mail.gmx.de", + "smtpport": "25", + "nachrichten": [] + } }, { "key": "filius.software.lokal.Terminal", @@ -4263,7 +4488,84 @@ "applications": [ { "key": "filius.software.email.EmailAnwendung", - "class": "filius.software.email.EmailAnwendung" + "class": "filius.software.email.EmailAnwendung", + "received": [], + "sent": [ + { + "absender": [ + "Frimp Müller", + "frimp@webmail.de" + ], + "empfaenger": [ + [ + null, + "herbert@gmx.de" + ] + ], + "cc": [], + "bcc": [], + "betreff": "Hallo Herbert!", + "text": "Huhu! \n\nHi Herbert!\n\nDein \nFrimp", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + }, + { + "absender": [ + "Frimp Müller", + "frimp@webmail.de" + ], + "empfaenger": [ + [ + "Herbert Schmidt", + "herbert@gmx.de" + ] + ], + "cc": [], + "bcc": [], + "betreff": "RE: RE: Hallo Herbert!", + "text": "funktioniert es jetzt?!\n\nHerbert Schmidt schrieb:\n> Hallo Frimp!\n> \n> Frimp Müller schrieb:\n> > Huhu! \n> > \n> > Hi Herbert!\n> > \n> > Dein \n> > Frimp\n> >\n> ", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + }, + { + "absender": [ + "Frimp Mueller", + "frimp@webmail.de" + ], + "empfaenger": [ + [ + "Herbert Schmidt", + "herbert@gmx.de" + ] + ], + "cc": [], + "bcc": [], + "betreff": "RE: RE: Hallo Herbert!", + "text": "Hi Herbert Again!\n\nHerbert Schmidt schrieb:\n> Hallo Frimp!\n> \n> Frimp Müller schrieb:\n> > Huhu! \n> > \n> > Hi Herbert!\n> > \n> > Dein \n> > Frimp\n> >\n> ", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + } + ], + "drafts": [], + "addressBook": [], + "fileAccount": { + "benutzername": "frimp", + "passwort": "frimp1234", + "nachname": "Mueller", + "vorname": "Frimp", + "emailAdresse": "frimp@webmail.de", + "pop3server": "mail.webmail.de", + "pop3port": "110", + "smtpserver": "mail.webmail.de", + "smtpport": "25", + "nachrichten": [] + } }, { "key": "filius.software.lokal.Terminal", @@ -4995,7 +5297,61 @@ "applications": [ { "key": "filius.software.email.EmailServer", - "class": "filius.software.email.EmailServer" + "class": "filius.software.email.EmailServer", + "aktiv": true, + "mailDomain": "gmx.de", + "beanAccounts": [ + { + "benutzername": "herbert", + "passwort": "herbert1234", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + }, + { + "benutzername": "george", + "passwort": "george1234", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + } + ], + "fileAccounts": [ + { + "benutzername": "herbert", + "passwort": "herbert1234", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + }, + { + "benutzername": "george", + "passwort": "george1234", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + } + ] } ], "dhcpClient": false, @@ -5054,7 +5410,37 @@ "applications": [ { "key": "filius.software.email.EmailServer", - "class": "filius.software.email.EmailServer" + "class": "filius.software.email.EmailServer", + "aktiv": true, + "mailDomain": "webmail.de", + "beanAccounts": [ + { + "benutzername": "frimp", + "passwort": "frimp1234", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + } + ], + "fileAccounts": [ + { + "benutzername": "frimp", + "passwort": "frimp1234", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + } + ] } ], "dhcpClient": false, @@ -5280,7 +5666,23 @@ "applications": [ { "key": "filius.software.email.EmailAnwendung", - "class": "filius.software.email.EmailAnwendung" + "class": "filius.software.email.EmailAnwendung", + "received": [], + "sent": [], + "drafts": [], + "addressBook": [], + "fileAccount": { + "benutzername": "george", + "passwort": "george1234", + "nachname": "Brown", + "vorname": "George", + "emailAdresse": "george@gmx.de", + "pop3server": "mail.gmx.de", + "pop3port": "110", + "smtpserver": "mail.gmx.de", + "smtpport": "25", + "nachrichten": [] + } }, { "key": "filius.software.lokal.Terminal", @@ -5342,7 +5744,23 @@ "applications": [ { "key": "filius.software.email.EmailAnwendung", - "class": "filius.software.email.EmailAnwendung" + "class": "filius.software.email.EmailAnwendung", + "received": [], + "sent": [], + "drafts": [], + "addressBook": [], + "fileAccount": { + "benutzername": "herbert", + "passwort": "herbert1234", + "nachname": "Schmidt", + "vorname": "Herbert", + "emailAdresse": "herbert@gmx.de", + "pop3server": "mail.gmx.de", + "pop3port": "110", + "smtpserver": "mail.gmx.de", + "smtpport": "25", + "nachrichten": [] + } }, { "key": "filius.software.lokal.Terminal", @@ -9173,7 +9591,23 @@ "applications": [ { "key": "filius.software.email.EmailAnwendung", - "class": "filius.software.email.EmailAnwendung" + "class": "filius.software.email.EmailAnwendung", + "received": [], + "sent": [], + "drafts": [], + "addressBook": [], + "fileAccount": { + "benutzername": "rechner3", + "passwort": "rechner3", + "nachname": "3", + "vorname": "Rechner", + "emailAdresse": "rechner3@senior.de", + "pop3server": "141.99.50.233", + "pop3port": "110", + "smtpserver": "141.99.50.233", + "smtpport": "25", + "nachrichten": [] + } }, { "key": "filius.software.www.WebBrowser", @@ -9231,7 +9665,23 @@ "applications": [ { "key": "filius.software.email.EmailAnwendung", - "class": "filius.software.email.EmailAnwendung" + "class": "filius.software.email.EmailAnwendung", + "received": [], + "sent": [], + "drafts": [], + "addressBook": [], + "fileAccount": { + "benutzername": "rechner4", + "passwort": "rechner4", + "nachname": "4", + "vorname": "Rechner", + "emailAdresse": "rechner4@senior.de", + "pop3server": "141.99.50.233", + "pop3port": "110", + "smtpserver": "141.99.50.233", + "smtpport": "25", + "nachrichten": [] + } }, { "key": "filius.software.www.WebBrowser", @@ -9289,7 +9739,23 @@ "applications": [ { "key": "filius.software.email.EmailAnwendung", - "class": "filius.software.email.EmailAnwendung" + "class": "filius.software.email.EmailAnwendung", + "received": [], + "sent": [], + "drafts": [], + "addressBook": [], + "fileAccount": { + "benutzername": "rechner1", + "passwort": "rechner1", + "nachname": "1", + "vorname": "Rechner", + "emailAdresse": "rechner1@filius.de", + "pop3server": "141.99.50.232", + "pop3port": "110", + "smtpserver": "141.99.50.232", + "smtpport": "25", + "nachrichten": [] + } }, { "key": "filius.software.www.WebBrowser", @@ -9347,7 +9813,23 @@ "applications": [ { "key": "filius.software.email.EmailAnwendung", - "class": "filius.software.email.EmailAnwendung" + "class": "filius.software.email.EmailAnwendung", + "received": [], + "sent": [], + "drafts": [], + "addressBook": [], + "fileAccount": { + "benutzername": "rechner2", + "passwort": "rechner2", + "nachname": "2", + "vorname": "Rechner", + "emailAdresse": "rechner2@filius.de", + "pop3server": "141.99.50.232", + "pop3port": "110", + "smtpserver": "141.99.50.232", + "smtpport": "25", + "nachrichten": [] + } }, { "key": "filius.software.www.WebBrowser", @@ -9513,7 +9995,61 @@ "applications": [ { "key": "filius.software.email.EmailServer", - "class": "filius.software.email.EmailServer" + "class": "filius.software.email.EmailServer", + "aktiv": true, + "mailDomain": "filius.de", + "beanAccounts": [ + { + "benutzername": "rechner1", + "passwort": "rechner1", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + }, + { + "benutzername": "rechner2", + "passwort": "rechner2", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + } + ], + "fileAccounts": [ + { + "benutzername": "rechner1", + "passwort": "rechner1", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + }, + { + "benutzername": "rechner2", + "passwort": "rechner2", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + } + ] } ], "dhcpClient": false, @@ -9572,7 +10108,61 @@ "applications": [ { "key": "filius.software.email.EmailServer", - "class": "filius.software.email.EmailServer" + "class": "filius.software.email.EmailServer", + "aktiv": true, + "mailDomain": "senior.de", + "beanAccounts": [ + { + "benutzername": "rechner3", + "passwort": "rechner3", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + }, + { + "benutzername": "rechner4", + "passwort": "rechner4", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + } + ], + "fileAccounts": [ + { + "benutzername": "rechner3", + "passwort": "rechner3", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + }, + { + "benutzername": "rechner4", + "passwort": "rechner4", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + } + ] } ], "dhcpClient": false, @@ -10699,7 +11289,205 @@ "applications": [ { "key": "filius.software.email.EmailAnwendung", - "class": "filius.software.email.EmailAnwendung" + "class": "filius.software.email.EmailAnwendung", + "received": [ + { + "absender": [ + "Rechner 1", + "rechner1@filius.de" + ], + "empfaenger": [ + [ + "string", + "rechner3@senior.de" + ] + ], + "cc": [], + "bcc": [], + "betreff": "test", + "text": "test", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + }, + { + "absender": [ + "Bob 1", + "bob@filius.de" + ], + "empfaenger": [ + [ + "string", + "alice@senior.de" + ] + ], + "cc": [], + "bcc": [], + "betreff": "test3", + "text": "", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + }, + { + "absender": [ + "Bob 1", + "bob@filius.de" + ], + "empfaenger": [ + [ + null, + "alice@senior.de" + ] + ], + "cc": [], + "bcc": [], + "betreff": "test bl", + "text": "test bl\n", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + } + ], + "sent": [ + { + "absender": [ + "Alice 3", + "alice@senior.de" + ], + "empfaenger": [ + [ + "string", + "Bob@filius.de" + ] + ], + "cc": [], + "bcc": [], + "betreff": "Beste Grüße", + "text": "Hallo Bob,\n\nschön von dir zu hören.\n\nGruß\nAlice", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + }, + { + "absender": [ + "Alice 3", + "alice@senior.de" + ], + "empfaenger": [ + [ + "string", + "bob@filius.de" + ] + ], + "cc": [], + "bcc": [], + "betreff": "test", + "text": "test", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + }, + { + "absender": [ + "Alice 3", + "alice@senior.de" + ], + "empfaenger": [ + [ + "string", + "bob@filius.de" + ] + ], + "cc": [], + "bcc": [], + "betreff": "RE: test3", + "text": "\n\nBob 1 schrieb:\n> ", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + }, + { + "absender": [ + "Alice 3", + "alice@senior.de" + ], + "empfaenger": [ + [ + "string", + "bob@filius.de" + ] + ], + "cc": [], + "bcc": [], + "betreff": "RE: test3", + "text": "\n\nBob 1 schrieb:\n> ", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + }, + { + "absender": [ + "Alice 3", + "alice@senior.de" + ], + "empfaenger": [ + [ + "Bob 1", + "bob@filius.de" + ] + ], + "cc": [], + "bcc": [], + "betreff": "RE: test bl", + "text": "huhu bob!\n\nBob 1 schrieb:\n> test bl\n> ", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + }, + { + "absender": [ + "Alice 3", + "alice@senior.de" + ], + "empfaenger": [ + [ + null, + "bob@filius.de" + ] + ], + "cc": [], + "bcc": [], + "betreff": "Hi Bob!", + "text": "Hi BoB!", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + } + ], + "drafts": [], + "addressBook": [], + "fileAccount": { + "benutzername": "alice", + "passwort": "alice", + "nachname": "3", + "vorname": "Alice", + "emailAdresse": "alice@senior.de", + "pop3server": "141.99.50.233", + "pop3port": "110", + "smtpserver": "141.99.50.233", + "smtpport": "25", + "nachrichten": [] + } }, { "key": "filius.software.lokal.Terminal", @@ -10792,7 +11580,11 @@ }, { "key": "filius.software.email.EmailServer", - "class": "filius.software.email.EmailServer" + "class": "filius.software.email.EmailServer", + "aktiv": true, + "mailDomain": "eve.de", + "beanAccounts": [], + "fileAccounts": [] }, { "key": "filius.software.www.WebBrowser", @@ -10850,7 +11642,105 @@ "applications": [ { "key": "filius.software.email.EmailAnwendung", - "class": "filius.software.email.EmailAnwendung" + "class": "filius.software.email.EmailAnwendung", + "received": [ + { + "absender": [ + "Alice 3", + "alice@senior.de" + ], + "empfaenger": [ + [ + "string", + "bob@filius.de" + ] + ], + "cc": [], + "bcc": [], + "betreff": "RE: test3", + "text": "Bob 1 schrieb:\n>", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + } + ], + "sent": [ + { + "absender": [ + "Rechner 1", + "rechner1@filius.de" + ], + "empfaenger": [ + [ + "string", + "rechner3@senior.de" + ] + ], + "cc": [], + "bcc": [], + "betreff": "test", + "text": "test", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + }, + { + "absender": [ + "Bob 1", + "bob@filius.de" + ], + "empfaenger": [ + [ + "string", + "alice@senior.de" + ] + ], + "cc": [], + "bcc": [], + "betreff": "test3", + "text": "", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + }, + { + "absender": [ + "Bob 1", + "bob@filius.de" + ], + "empfaenger": [ + [ + null, + "alice@senior.de" + ] + ], + "cc": [], + "bcc": [], + "betreff": "test bl", + "text": "test bl", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + } + ], + "drafts": [], + "addressBook": [], + "fileAccount": { + "benutzername": "bob", + "passwort": "bob", + "nachname": "1", + "vorname": "Bob", + "emailAdresse": "bob@filius.de", + "pop3server": "141.99.50.232", + "pop3port": "110", + "smtpserver": "141.99.50.232", + "smtpport": "25", + "nachrichten": [] + } }, { "key": "filius.software.lokal.FileExplorer", @@ -11020,7 +11910,119 @@ "applications": [ { "key": "filius.software.email.EmailServer", - "class": "filius.software.email.EmailServer" + "class": "filius.software.email.EmailServer", + "aktiv": true, + "mailDomain": "filius.de", + "beanAccounts": [ + { + "benutzername": "bob", + "passwort": "bob", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [ + { + "absender": [ + "Alice 3", + "alice@senior.de" + ], + "empfaenger": [ + [ + "Bob 1", + "bob@filius.de" + ] + ], + "cc": [], + "bcc": [], + "betreff": "RE: test bl", + "text": "huhu bob!\n\nBob 1 schrieb:\n> test bl\n>\n", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + }, + { + "absender": [ + "Alice 3", + "alice@senior.de" + ], + "empfaenger": [ + [ + null, + "bob@filius.de" + ] + ], + "cc": [], + "bcc": [], + "betreff": "Hi Bob!", + "text": "Hi BoB!\n", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + } + ] + } + ], + "fileAccounts": [ + { + "benutzername": "bob", + "passwort": "bob", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [ + { + "absender": [ + "Alice 3", + "alice@senior.de" + ], + "empfaenger": [ + [ + "Bob 1", + "bob@filius.de" + ] + ], + "cc": [], + "bcc": [], + "betreff": "RE: test bl", + "text": "huhu bob!\n\nBob 1 schrieb:\n> test bl\n>\n", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + }, + { + "absender": [ + "Alice 3", + "alice@senior.de" + ], + "empfaenger": [ + [ + null, + "bob@filius.de" + ] + ], + "cc": [], + "bcc": [], + "betreff": "Hi Bob!", + "text": "Hi BoB!\n", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + } + ] + } + ] } ], "dhcpClient": false, @@ -11079,7 +12081,37 @@ "applications": [ { "key": "filius.software.email.EmailServer", - "class": "filius.software.email.EmailServer" + "class": "filius.software.email.EmailServer", + "aktiv": true, + "mailDomain": "senior.de", + "beanAccounts": [ + { + "benutzername": "alice", + "passwort": "alice", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + } + ], + "fileAccounts": [ + { + "benutzername": "alice", + "passwort": "alice", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + } + ] } ], "dhcpClient": false, diff --git a/packages/fls/src/dnsHosts.ts b/packages/fls/src/dnsHosts.ts index 80ed4df..1768154 100644 --- a/packages/fls/src/dnsHosts.ts +++ b/packages/fls/src/dnsHosts.ts @@ -11,7 +11,9 @@ // - duplicates stay. import type { DnsResourceRecord } from '@mdg-filius/core'; +import { javaFileContent } from './javaFileSystem.js'; import { javaIpCheck, javaParseInt } from './javaIp.js'; +import { javaTrim } from './javaString.js'; import type { FlsFileTree } from './systemSoftware.js'; /** A record as Java's ResourceRecord holds it (the getters). */ @@ -27,51 +29,14 @@ export const DNS_HOSTS_PATH = '/dns/hosts'; // EingabenUeberpruefung.musterDomain, including its quirks: labels start with // a letter, labels after the first may hold a backslash, and the empty // string is a valid domain. -const JAVA_DOMAIN = /^([a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*)?\.?$/; +export const JAVA_DOMAIN = /^([a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*)?\.?$/; // EingabenUeberpruefung.musterIpAdresse. const JAVA_IP_ADDRESS = /^(0*([1-9]?[0-9]?|1[0-9]{1,2}|2[0-4]?[0-9]|25[0-5]))\.((0*(1?[0-9]{1,2}|2[0-4]?[0-9]|25[0-5]))\.){2}(0*(1?[0-9]{1,2}|2[0-4]?[0-9]|25[0-5]))$/; -/** `String.trim()`: every UTF-16 unit up to U+0020 off both ends. */ -function javaTrim(text: string): string { - return text.replace(/^[\u0000-\u0020]+|[\u0000-\u0020]+$/g, ''); -} - -/** - * The content of /dns/hosts as `Dateisystem.holeDatei` finds it: the first - * node in preorder whose path equals it ignoring case, and only if that node - * is a file. Undefined when there is none; a file without content reads as - * empty (Java would fail on it). - */ +/** The content of /dns/hosts as Java's DNS server finds it (see javaFileSystem.ts). */ export function javaHostsFileContent(root: FlsFileTree | undefined): string | undefined { - if (root === undefined) return undefined; - const found = firstInPreorder(root, '', (path) => javaEqualsIgnoreCase(path, DNS_HOSTS_PATH)); - return found?.kind === 'file' ? (found.content ?? '') : undefined; -} - -/** Preorder below the root; a node's path is "/" plus each name after the root's. */ -function firstInPreorder(node: FlsFileTree, path: string, matches: (path: string) => boolean): FlsFileTree | undefined { - for (const child of node.children) { - const childPath = `${path}/${child.kind === 'other' ? child.className : String(child.name)}`; - if (matches(childPath)) return child; - const found = firstInPreorder(child, childPath, matches); - if (found !== undefined) return found; - } - return undefined; -} - -/** - * `String.equalsIgnoreCase` against an ASCII path, unit by unit: equal after - * Character.toUpperCase, so the long s (U+017F) is an "s". Java also - * compares the lower case of both; for the letters of /dns/hosts that adds - * no unit (measured over the BMP). - */ -function javaEqualsIgnoreCase(text: string, asciiPath: string): boolean { - if (text.length !== asciiPath.length) return false; - for (let index = 0; index < text.length; index++) { - if (text[index]!.toUpperCase() !== asciiPath[index]!.toUpperCase()) return false; - } - return true; + return javaFileContent(root, DNS_HOSTS_PATH); } /** `DNSServer.leseRecordListe` on the content of /dns/hosts. */ diff --git a/packages/fls/src/index.ts b/packages/fls/src/index.ts index 654e981..2d5d2f7 100644 --- a/packages/fls/src/index.ts +++ b/packages/fls/src/index.ts @@ -2,7 +2,10 @@ export * from './archive.js'; export * from './dnsHosts.js'; export * from './importProject.js'; +export * from './javaEmail.js'; +export * from './javaFileSystem.js'; export * from './javaIp.js'; +export * from './javaString.js'; export * from './javabeans.js'; export * from './systemSoftware.js'; export * from './topology.js'; diff --git a/packages/fls/src/javaEmail.ts b/packages/fls/src/javaEmail.ts new file mode 100644 index 0000000..1a38ba4 --- /dev/null +++ b/packages/fls/src/javaEmail.ts @@ -0,0 +1,304 @@ +// Java's e-mail programs as FILIUS 2.14 loads them. Two places hold their +// state: +// - the file's JavaBeans: the server's accounts (listeBenutzerkonten, with +// their mailboxes), the client's three folders and address book; +// - text files in the host's file system: /mailserver/konten.txt, which the +// server reads (kontenLaden) whenever a simulation starts and so wins over +// the bean list, and /konten.txt, the client's account, read (laden) when +// its window opens. The client's account is in no bean: its accessors +// (holeKontoListe, setzeKontoListe) are not bean properties. +// Checked against Java on the corpus (corpus.java.json, applications). + +import { isJavaObject, listItems, simpleClassName, type JavaObject, type JavaValue } from './javabeans.js'; +import { javaSplit, javaTokens, javaTrim } from './javaString.js'; + +/** AddressEntry: a display name, an address, or both. */ +export interface JavaAddressEntry { + readonly name: string | null; + readonly mailAddress: string | null; +} + +/** + * A recipient as the list holds it: older files keep recipients as plain + * strings, which the decoder puts into the typed list unchecked. + */ +export type JavaRecipient = JavaAddressEntry | string | null; + +export interface JavaEmail { + readonly sender: JavaAddressEntry; + readonly to: readonly JavaRecipient[]; + readonly cc: readonly JavaRecipient[]; + readonly bcc: readonly JavaRecipient[]; + readonly subject: string | null; + readonly text: string | null; + readonly dateReceived: string | null; + readonly isNew: boolean; + readonly isDeleted: boolean; + readonly isSent: boolean; +} + +/** EmailKonto: on the server an account with its mailbox, in the client the account it uses. */ +export interface JavaEmailAccount { + readonly username: string | null; + readonly password: string | null; + readonly lastName: string; + readonly firstName: string; + readonly emailAddress: string | null; + readonly pop3Server: string | null; + readonly pop3Port: string | null; + readonly smtpServer: string | null; + readonly smtpPort: string | null; + readonly messages: readonly JavaEmail[]; +} + +/** Kontakt: an address book entry. */ +export interface JavaContact { + readonly name: string | null; + readonly firstName: string | null; + readonly street: string | null; + readonly houseNumber: number; + readonly postalCode: number; + readonly city: string | null; + readonly email: string | null; + readonly phone: string | null; +} + +/** An exception Java throws while reading, by its simple class name. */ +export interface JavaFailure { + readonly exception: string; +} + +export const EMAIL_SERVER_FILE = '/mailserver/konten.txt'; +export const EMAIL_CLIENT_FILE = '/konten.txt'; + +type Warn = (message: string) => void; + +// JavaBeans + +export function javaEmailAccounts(list: JavaValue | undefined, warn: Warn): JavaEmailAccount[] { + return objectsOf(list, 'EmailKonto', warn).map((account) => { + const at = (property: string) => text(account.properties.get(property), `EmailKonto ${property}`, warn); + return { + username: at('benutzername'), + password: at('passwort'), + // setNachname and setVorname keep "" for null. + lastName: at('nachname') ?? '', + firstName: at('vorname') ?? '', + emailAddress: at('emailAdresse'), + pop3Server: at('pop3server'), + pop3Port: at('pop3port'), + smtpServer: at('smtpserver'), + smtpPort: at('smtpport'), + messages: javaEmails(account.properties.get('nachrichten'), warn), + }; + }); +} + +export function javaEmails(list: JavaValue | undefined, warn: Warn): JavaEmail[] { + return objectsOf(list, 'Email', warn).map((email) => { + const at = (property: string) => email.properties.get(property); + const flag = (property: string, fallback: boolean) => { + const value = at(property); + if (value === undefined) return fallback; + if (typeof value === 'boolean') return value; + warn(`Email ${property} is not a boolean, used ${fallback}`); + return fallback; + }; + return { + sender: senderOf(at('absender'), warn), + to: recipientsOf(at('empfaenger'), warn), + cc: recipientsOf(at('cc'), warn), + bcc: recipientsOf(at('bcc'), warn), + subject: text(at('betreff'), 'Email betreff', warn), + text: at('text') === undefined ? '' : text(at('text'), 'Email text', warn), + dateReceived: at('dateReceived') === undefined ? '' : text(at('dateReceived'), 'Email dateReceived', warn), + isNew: flag('neu', true), + isDeleted: flag('delete', false), + isSent: flag('versendet', false), + }; + }); +} + +export function javaContacts(list: JavaValue | undefined, warn: Warn): JavaContact[] { + return objectsOf(list, 'Kontakt', warn).map((contact) => { + const at = (property: string) => text(contact.properties.get(property), `Kontakt ${property}`, warn); + const number = (property: string) => { + const value = contact.properties.get(property); + if (value === undefined) return 0; + if (typeof value === 'number' && Number.isInteger(value)) return value; + warn(`Kontakt ${property} is not an int, used 0`); + return 0; + }; + return { + name: at('name'), + firstName: at('vorname'), + street: at('strasse'), + houseNumber: number('hausnr'), + postalCode: number('plz'), + city: at('wohnort'), + email: at('email'), + phone: at('telefon'), + }; + }); +} + +/** Email.getAbsender(): a copy, so never null — a missing sender has neither name nor address. */ +function senderOf(value: JavaValue | undefined, warn: Warn): JavaAddressEntry { + if (typeof value === 'string') return addressEntry(value); + if (isAddressEntry(value)) return beanAddressEntry(value, warn); + if (value !== undefined && value !== null) warn('Email absender is neither a string nor an AddressEntry, ignored'); + return { name: null, mailAddress: null }; +} + +function recipientsOf(list: JavaValue | undefined, warn: Warn): JavaRecipient[] { + return listItems(list).flatMap((item): JavaRecipient[] => { + if (item === null || typeof item === 'string') return [item]; + if (isAddressEntry(item)) return [beanAddressEntry(item, warn)]; + warn('an e-mail recipient is neither a string nor an AddressEntry, left out'); + return []; + }); +} + +/** + * An AddressEntry — also the copy a getter returns, which the file fills and + * then sets (``): the decoder + * names such an object by its id. + */ +function isAddressEntry(value: JavaValue | undefined): value is JavaObject { + return isJavaObject(value) && simpleClassName(value.className) === 'AddressEntry'; +} + +function beanAddressEntry(entry: JavaObject, warn: Warn): JavaAddressEntry { + return { + name: text(entry.properties.get('name'), 'AddressEntry name', warn), + mailAddress: text(entry.properties.get('mailAddress'), 'AddressEntry mailAddress', warn), + }; +} + +function objectsOf(list: JavaValue | undefined, simpleClass: string, warn: Warn): JavaObject[] { + return listItems(list).flatMap((item) => { + if (isJavaObject(item) && simpleClassName(item.className) === simpleClass) return [item]; + warn(`a list of ${simpleClass} holds something else, left out`); + return []; + }); +} + +function text(value: JavaValue | undefined, name: string, warn: Warn): string | null { + if (value === undefined || value === null) return null; + if (typeof value === 'string') return value; + warn(`${name} is not a string, ignored`); + return null; +} + +// Text files + +/** `new AddressEntry(text)`: "Name
", "
", a bare address with "@", else a name. */ +export function addressEntry(raw: string): JavaAddressEntry { + const entry = javaTrim(raw); + const start = entry.indexOf('<'); + const end = entry.indexOf('>'); + if (start >= 0 && end > start) { + return { name: start > 0 ? javaTrim(entry.slice(0, start)) : null, mailAddress: javaTrim(entry.slice(start + 1, end)) }; + } + if (!entry.includes('<') && !entry.includes('>') && entry.includes('@')) return { name: null, mailAddress: entry }; + return { name: entry, mailAddress: null }; +} + +/** `EmailUtils.stringToAddressEntryList`: comma-separated, blank parts skipped. */ +function addressEntries(raw: string): JavaAddressEntry[] { + return javaSplit(raw, ',') + .filter((part) => javaTrim(part) !== '') + .map(addressEntry); +} + +/** + * `EmailServer.stringZuListeBenutzerkonten`: one account a line — + * user;domain;password;last name;first name;messages — split by a + * StringTokenizer, which skips empty fields; an account with fewer than five + * fields makes Java throw. Messages are "#"-separated, seven "$"-separated + * fields each: sender, to, cc, bcc, date, subject, text. Java's split drops + * trailing empty fields, so a message with an empty text is lost. + */ +export function readJavaEmailServerFile(content: string): JavaEmailAccount[] | JavaFailure { + const accounts: JavaEmailAccount[] = []; + for (const line of javaTokens(content, '\n')) { + const fields = javaTokens(line, ';'); + if (fields.length < 5) return { exception: 'NoSuchElementException' }; + const [username, , password, lastName, firstName, messages] = fields as [string, string, string, string, string, string?]; + accounts.push({ + username, + password, + lastName, + firstName, + emailAddress: null, + pop3Server: null, + pop3Port: null, + smtpServer: null, + smtpPort: null, + messages: messages === undefined ? [] : fileMessages(messages), + }); + } + return accounts; +} + +function fileMessages(messages: string): JavaEmail[] { + return javaSplit(messages, '#').flatMap((message) => { + const fields = javaSplit(message, '$'); + if (fields.length !== 7) return []; + const [sender, to, cc, bcc, dateReceived, subject, body] = fields as [string, string, string, string, string, string, string]; + return [ + { + sender: addressEntry(sender), + to: addressEntries(to), + cc: addressEntries(cc), + bcc: addressEntries(bcc), + subject, + text: unescapedBody(body), + dateReceived, + isNew: true, + isDeleted: false, + isSent: false, + }, + ]; + }); +} + +/** brtonl, then undoSpecialChar: "|" is a line break, "&&00124&&" a "|". */ +function unescapedBody(body: string): string { + return body + .replaceAll('|', '\n') + .replaceAll('&&00036&&', '$') + .replaceAll('&&00059&&', ';') + .replaceAll('&&00124&&', '|') + .replaceAll('&&00035&&', '#'); +} + +/** + * `EmailAnwendung.laden`: every line makes an account, the last one stays. + * `new EmailKonto(line)` takes nine ";"-separated fields — POP3 server, SMTP + * server, POP3 port, SMTP port, user, password, last name, first name, + * address — and with fewer keeps none (trailing empty fields do not count). + * Null when the file has no line at all. + */ +export function readJavaEmailClientFile(content: string): JavaEmailAccount | null { + const lines = javaSplit(content, '\n'); + const line = lines[lines.length - 1]; + if (line === undefined) return null; + const fields = javaSplit(line, ';'); + const empty = { username: null, password: null, lastName: '', firstName: '', emailAddress: null }; + const servers = { pop3Server: null, pop3Port: null, smtpServer: null, smtpPort: null }; + if (fields.length <= 8) return { ...empty, ...servers, messages: [] }; + const [pop3Server, smtpServer, pop3Port, smtpPort, username, password, lastName, firstName, emailAddress] = fields as string[]; + return { + username: username!, + password: password!, + lastName: lastName!, + firstName: firstName!, + emailAddress: emailAddress!, + pop3Server: pop3Server!, + pop3Port: pop3Port!, + smtpServer: smtpServer!, + smtpPort: smtpPort!, + messages: [], + }; +} diff --git a/packages/fls/src/javaFileSystem.ts b/packages/fls/src/javaFileSystem.ts new file mode 100644 index 0000000..3bf5d61 --- /dev/null +++ b/packages/fls/src/javaFileSystem.ts @@ -0,0 +1,51 @@ +// How Java FILIUS finds a file by path (Dateisystem.holeDatei): the first +// node in preorder whose path equals the path ignoring case, and only if that +// node is a file. + +import type { FlsFileTree } from './systemSoftware.js'; + +/** + * The content of the file at an ASCII path as `Dateisystem.holeDatei` finds + * it. Undefined when there is none; a file without content reads as empty + * (Java would fail on it). + */ +export function javaFileContent(root: FlsFileTree | undefined, asciiPath: string): string | undefined { + if (root === undefined) return undefined; + const found = firstInPreorder(root, '', (path) => javaEqualsIgnoreCase(path, asciiPath)); + return found?.kind === 'file' ? (found.content ?? '') : undefined; +} + +/** Preorder below the root; a node's path is "/" plus each name after the root's. */ +function firstInPreorder(node: FlsFileTree, path: string, matches: (path: string) => boolean): FlsFileTree | undefined { + for (const child of node.children) { + const childPath = `${path}/${child.kind === 'other' ? child.className : String(child.name)}`; + if (matches(childPath)) return child; + const found = firstInPreorder(child, childPath, matches); + if (found !== undefined) return found; + } + return undefined; +} + +// Besides ASCII case, String.equalsIgnoreCase takes these BMP units for an +// ASCII letter (measured on the JDK over every unit): dotted and dotless I, +// the Kelvin sign and the long s. +const JAVA_ASCII_CASE_FOLDS = new Map([ + ['\u0130', 'i'], + ['\u0131', 'i'], + ['\u212A', 'k'], + ['\u017F', 's'], +]); + +/** `String.equalsIgnoreCase` against an ASCII path, unit by unit. */ +function javaEqualsIgnoreCase(text: string, asciiPath: string): boolean { + if (text.length !== asciiPath.length) return false; + for (let index = 0; index < text.length; index++) { + if (asciiFold(text[index]!) !== asciiFold(asciiPath[index]!)) return false; + } + return true; +} + +function asciiFold(unit: string): string { + if (unit >= 'A' && unit <= 'Z') return unit.toLowerCase(); + return JAVA_ASCII_CASE_FOLDS.get(unit) ?? unit; +} diff --git a/packages/fls/src/javaString.ts b/packages/fls/src/javaString.ts new file mode 100644 index 0000000..8c92290 --- /dev/null +++ b/packages/fls/src/javaString.ts @@ -0,0 +1,24 @@ +// java.lang.String and java.util.StringTokenizer as FILIUS's text formats +// use them — each differs from its JavaScript namesake. + +/** `String.trim()`: every UTF-16 unit up to U+0020 off both ends. */ +export function javaTrim(text: string): string { + return text.replace(/^[\u0000-\u0020]+|[\u0000-\u0020]+$/g, ''); +} + +/** + * `text.split(separator)` for a one-character separator: trailing empty + * parts are dropped — "a;b;;".split(";") is two parts, ";;" none — but a + * text without the separator is one part, even the empty text. + */ +export function javaSplit(text: string, separator: string): string[] { + if (!text.includes(separator)) return [text]; + const parts = text.split(separator); + while (parts.length > 0 && parts[parts.length - 1] === '') parts.pop(); + return parts; +} + +/** `new StringTokenizer(text, separator)`: the non-empty runs between separators. */ +export function javaTokens(text: string, separator: string): string[] { + return text.split(separator).filter((token) => token !== ''); +} diff --git a/packages/fls/src/systemSoftware.ts b/packages/fls/src/systemSoftware.ts index ca72d58..2ab6760 100644 --- a/packages/fls/src/systemSoftware.ts +++ b/packages/fls/src/systemSoftware.ts @@ -27,6 +27,21 @@ // not know Java's constructors and sees a placeholder per path; they are // merged here. +import { JAVA_DOMAIN } from './dnsHosts.js'; +import { + EMAIL_CLIENT_FILE, + EMAIL_SERVER_FILE, + javaContacts, + javaEmailAccounts, + javaEmails, + readJavaEmailClientFile, + readJavaEmailServerFile, + type JavaContact, + type JavaEmail, + type JavaEmailAccount, + type JavaFailure, +} from './javaEmail.js'; +import { javaFileContent } from './javaFileSystem.js'; import { isJavaObject, isUnresolved, @@ -114,12 +129,12 @@ const SERVER_APPLICATIONS = new Set([ 'filius.software.www.WebServer', ]); -/** An installed application with the settings the model reads; e-mail contents are a later layer. */ +/** An installed application with the settings the model reads. */ export interface FlsApplication { /** The key in installierteAnwendungen — the class name in every corpus file. */ key: string | null; javaClass: FlsApplicationClass; - /** Server applications: switched on (Java's aktiv) and the port. */ + /** Server applications: switched on (Java's aktiv) and the port; EmailServer: switched on. */ isActive?: boolean; port?: number; /** ClientBaustein: the server it talks to (Java's zielIPAdresse, zielPort). */ @@ -136,6 +151,23 @@ export interface FlsApplication { maxPeers?: number | null; /** Firewall: the personal firewall. */ firewall?: FlsFirewall; + /** EmailServer: its domain (Java keeps "filius.de" for one its pattern rejects). */ + mailDomain?: string; + /** EmailServer: the accounts of the bean list. */ + beanAccounts?: JavaEmailAccount[]; + /** + * EmailServer: the accounts of /mailserver/konten.txt, which Java reads at + * every simulation start — null without the file (Java then starts with + * none), a failure where Java throws. + */ + fileAccounts?: JavaEmailAccount[] | JavaFailure | null; + /** EmailAnwendung: its folders (Java's empfangene, gesendete, erstellte Nachrichten). */ + received?: JavaEmail[]; + sent?: JavaEmail[]; + drafts?: JavaEmail[]; + addressBook?: JavaContact[]; + /** EmailAnwendung: the account of /konten.txt, null without one. */ + fileAccount?: JavaEmailAccount | null; /** The application's JavaBean, for the e-mail layer. */ source: JavaObject; } @@ -188,7 +220,7 @@ export function readSystemSoftware(device: JavaObject, javaClass: FlsNodeClass, case 'Notebook': result.ssid = optionalString(value('ssid'), 'ssid', warn) ?? null; result.fileSystem = fileSystem(value('dateisystem'), warn); - result.applications = applications(value('installierteAnwendungen'), warn); + result.applications = applications(value('installierteAnwendungen'), result.fileSystem, warn); break; case 'Vermittlungsrechner': result.ripEnabled = boolean(value('ripEnabled'), false, 'ripEnabled', warn); @@ -234,7 +266,7 @@ function modemAddress(value: string | null | undefined): string | null { * a project's own application, compiled into projekt/anwendungen — is left * out: this reader cannot run Java classes. */ -function applications(map: JavaValue | undefined, warn: Warn): FlsApplication[] { +function applications(map: JavaValue | undefined, root: FlsFileTree, warn: Warn): FlsApplication[] { const result: FlsApplication[] = []; for (const [key, application] of mapEntries(map)) { if (!isJavaObject(application)) { @@ -278,6 +310,24 @@ function applications(map: JavaValue | undefined, warn: Warn): FlsApplication[] case 'filius.software.firewall.Firewall': entry.firewall = firewallOf([application], true, warn); break; + case 'filius.software.email.EmailServer': { + entry.isActive = boolean(at('aktiv'), false, 'EmailServer aktiv', warn); + const domain = optionalString(at('mailDomain'), 'EmailServer mailDomain', warn); + entry.mailDomain = typeof domain === 'string' && JAVA_DOMAIN.test(domain) ? domain : 'filius.de'; + entry.beanAccounts = javaEmailAccounts(at('listeBenutzerkonten'), warn); + const file = javaFileContent(root, EMAIL_SERVER_FILE); + entry.fileAccounts = file === undefined ? null : readJavaEmailServerFile(file); + break; + } + case 'filius.software.email.EmailAnwendung': { + entry.received = javaEmails(at('empfangeneNachrichten'), warn); + entry.sent = javaEmails(at('gesendeteNachrichten'), warn); + entry.drafts = javaEmails(at('erstellteNachrichten'), warn); + entry.addressBook = javaContacts(at('adressbuch'), warn); + const file = javaFileContent(root, EMAIL_CLIENT_FILE); + entry.fileAccount = file === undefined ? null : readJavaEmailClientFile(file); + break; + } } result.push(entry); } diff --git a/packages/fls/test/dnsHosts.test.ts b/packages/fls/test/dnsHosts.test.ts index 2d2ff3c..eca4d79 100644 --- a/packages/fls/test/dnsHosts.test.ts +++ b/packages/fls/test/dnsHosts.test.ts @@ -8,8 +8,7 @@ import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; -import type { FlsFileTree } from '../src/index.js'; -import { dnsRecordsOf, javaHostsFileContent, readJavaDnsHosts } from '../src/dnsHosts.js'; +import { dnsRecordsOf, readJavaDnsHosts } from '../src/dnsHosts.js'; const here = dirname(fileURLToPath(import.meta.url)); type Row = [string, string, number, string]; @@ -186,24 +185,3 @@ describe('the records Java answers', () => { expect(warnings).toEqual(["DNS record 'www.filius.de A .1.2.3' has no usable IPv4 address, left out (it stays in /dns/hosts)"]); }); }); - -describe('javaHostsFileContent', () => { - const file = (name: string, content: string | null): FlsFileTree => ({ kind: 'file', name, type: 'text', content, children: [] }); - const directory = (name: string, ...children: FlsFileTree[]): FlsFileTree => ({ kind: 'directory', name, children }); - - it('finds /dns/hosts ignoring case, as Java does', () => { - expect(javaHostsFileContent(directory('root', directory('DNS', file('Hosts', 'x'))))).toBe('x'); - expect(javaHostsFileContent(directory('root', directory('dns', file('ho\u017Fts', 'long s'))))).toBe('long s'); - }); - - it('takes the first match in preorder, and nothing when that is a directory', () => { - expect(javaHostsFileContent(directory('root', directory('dns', file('hosts', 'first')), directory('DNS', file('hosts', 'second'))))).toBe('first'); - expect(javaHostsFileContent(directory('root', directory('dns', directory('hosts'), file('HOSTS', 'later'))))).toBeUndefined(); - }); - - it('is undefined without the file, and empty for a file without content', () => { - expect(javaHostsFileContent(undefined)).toBeUndefined(); - expect(javaHostsFileContent(directory('root', file('hosts', 'x')))).toBeUndefined(); - expect(javaHostsFileContent(directory('root', directory('dns', file('hosts', null))))).toBe(''); - }); -}); diff --git a/packages/fls/test/email-client.cases.txt b/packages/fls/test/email-client.cases.txt new file mode 100644 index 0000000..2afc61b --- /dev/null +++ b/packages/fls/test/email-client.cases.txt @@ -0,0 +1,16 @@ +mail.gmx.de;mail.gmx.de;110;25;herbert;herbert1234;Schmidt;Herbert;herbert@gmx.de +mail.gmx.de;mail.gmx.de;110;25;herbert;herbert1234;Schmidt;Herbert;herbert@gmx.de\u000A +a;a;110;25;first;p;L;F;first@x\u000Ab;b;110;25;second;p;L;F;second@x\u000A +a;a;110;25;first;p;L;F;first@x\u000Abroken\u000A + +\u000A +\u000A\u000A\u000A +a;b;110;25;u;p;L;F;\u000A +a;b;110;25;u;p;L;F;u@x;extra\u000A +a;b;110;25;u;p;L;F;u@x\u000D\u000A +a;;110;25;u;p;;;u@x\u000A +a;b;110;25;u;p;L;F;u@x\u000A\u000A\u000A +\u000Aa;b;110;25;u;p;L;F;u@x + a ; b ;x;y; u ; p ; L ; F ; u@x \u000A +;;;;;;;; +;;;;;;;;x diff --git a/packages/fls/test/email-client.java.json b/packages/fls/test/email-client.java.json new file mode 100644 index 0000000..642669e --- /dev/null +++ b/packages/fls/test/email-client.java.json @@ -0,0 +1,220 @@ +[ + [ + "mail.gmx.de;mail.gmx.de;110;25;herbert;herbert1234;Schmidt;Herbert;herbert@gmx.de", + { + "benutzername": "herbert", + "passwort": "herbert1234", + "nachname": "Schmidt", + "vorname": "Herbert", + "emailAdresse": "herbert@gmx.de", + "pop3server": "mail.gmx.de", + "pop3port": "110", + "smtpserver": "mail.gmx.de", + "smtpport": "25", + "nachrichten": [] + } + ], + [ + "mail.gmx.de;mail.gmx.de;110;25;herbert;herbert1234;Schmidt;Herbert;herbert@gmx.de\\u000A", + { + "benutzername": "herbert", + "passwort": "herbert1234", + "nachname": "Schmidt", + "vorname": "Herbert", + "emailAdresse": "herbert@gmx.de", + "pop3server": "mail.gmx.de", + "pop3port": "110", + "smtpserver": "mail.gmx.de", + "smtpport": "25", + "nachrichten": [] + } + ], + [ + "a;a;110;25;first;p;L;F;first@x\\u000Ab;b;110;25;second;p;L;F;second@x\\u000A", + { + "benutzername": "second", + "passwort": "p", + "nachname": "L", + "vorname": "F", + "emailAdresse": "second@x", + "pop3server": "b", + "pop3port": "110", + "smtpserver": "b", + "smtpport": "25", + "nachrichten": [] + } + ], + [ + "a;a;110;25;first;p;L;F;first@x\\u000Abroken\\u000A", + { + "benutzername": null, + "passwort": null, + "nachname": "", + "vorname": "", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + } + ], + [ + "", + { + "benutzername": null, + "passwort": null, + "nachname": "", + "vorname": "", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + } + ], + [ + "\\u000A", + null + ], + [ + "\\u000A\\u000A\\u000A", + null + ], + [ + "a;b;110;25;u;p;L;F;\\u000A", + { + "benutzername": null, + "passwort": null, + "nachname": "", + "vorname": "", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + } + ], + [ + "a;b;110;25;u;p;L;F;u@x;extra\\u000A", + { + "benutzername": "u", + "passwort": "p", + "nachname": "L", + "vorname": "F", + "emailAdresse": "u@x", + "pop3server": "a", + "pop3port": "110", + "smtpserver": "b", + "smtpport": "25", + "nachrichten": [] + } + ], + [ + "a;b;110;25;u;p;L;F;u@x\\u000D\\u000A", + { + "benutzername": "u", + "passwort": "p", + "nachname": "L", + "vorname": "F", + "emailAdresse": "u@x\r", + "pop3server": "a", + "pop3port": "110", + "smtpserver": "b", + "smtpport": "25", + "nachrichten": [] + } + ], + [ + "a;;110;25;u;p;;;u@x\\u000A", + { + "benutzername": "u", + "passwort": "p", + "nachname": "", + "vorname": "", + "emailAdresse": "u@x", + "pop3server": "a", + "pop3port": "110", + "smtpserver": "", + "smtpport": "25", + "nachrichten": [] + } + ], + [ + "a;b;110;25;u;p;L;F;u@x\\u000A\\u000A\\u000A", + { + "benutzername": "u", + "passwort": "p", + "nachname": "L", + "vorname": "F", + "emailAdresse": "u@x", + "pop3server": "a", + "pop3port": "110", + "smtpserver": "b", + "smtpport": "25", + "nachrichten": [] + } + ], + [ + "\\u000Aa;b;110;25;u;p;L;F;u@x", + { + "benutzername": "u", + "passwort": "p", + "nachname": "L", + "vorname": "F", + "emailAdresse": "u@x", + "pop3server": "a", + "pop3port": "110", + "smtpserver": "b", + "smtpport": "25", + "nachrichten": [] + } + ], + [ + " a ; b ;x;y; u ; p ; L ; F ; u@x \\u000A", + { + "benutzername": " u ", + "passwort": " p ", + "nachname": " L ", + "vorname": " F ", + "emailAdresse": " u@x ", + "pop3server": " a ", + "pop3port": "x", + "smtpserver": " b ", + "smtpport": "y", + "nachrichten": [] + } + ], + [ + ";;;;;;;;", + { + "benutzername": null, + "passwort": null, + "nachname": "", + "vorname": "", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + } + ], + [ + ";;;;;;;;x", + { + "benutzername": "", + "passwort": "", + "nachname": "", + "vorname": "", + "emailAdresse": "x", + "pop3server": "", + "pop3port": "", + "smtpserver": "", + "smtpport": "", + "nachrichten": [] + } + ] +] diff --git a/packages/fls/test/email-server.cases.txt b/packages/fls/test/email-server.cases.txt new file mode 100644 index 0000000..cffeef5 --- /dev/null +++ b/packages/fls/test/email-server.cases.txt @@ -0,0 +1,27 @@ +bob;filius.de;bob;Nachname;Vorname;\u000A +bob;filius.de;bob;Nachname;Vorname;\u000Aalice;filius.de;alice;A;B;\u000A +bob;filius.de;bob;Nachname;Vorname +bob;;bob;Nachname;Vorname;\u000A +bob;filius.de;bob;Nachname\u000A +bob;filius.de;bob;Nachname;Vorname;\u000Abroken\u000A + bob ;filius.de; bob ;Nachname;Vorname;\u000A +bob;filius.de;bob;Nachname;Vorname\u000D\u000A + +\u000A\u000A +bob;filius.de;bob;Nachname;Vorname;#Alice $bob@filius.de$$$$Hi Bob!$Hi BoB!|\u000A +bob;filius.de;bob;Nachname;Vorname;#Alice $bob@filius.de$$$$Hi Bob!$Hi BoB!|#Alice $bob@filius.de$$$$Hi Bob!$Hi BoB!|\u000A +bob;filius.de;bob;Nachname;Vorname;Alice $bob@filius.de$$$$Hi$Text\u000A +bob;filius.de;bob;Nachname;Vorname;#Alice $bob@filius.de$$$$Hi$\u000A +bob;filius.de;bob;Nachname;Vorname;#Alice $bob@filius.de$$$$$\u000A +bob;filius.de;bob;Nachname;Vorname;#Alice $bob@filius.de$$$$Hi $5$Text\u000A +bob;filius.de;bob;Nachname;Vorname;#a@x$b@y$c@z$d@w$1234$Hi$Text\u000A +bob;filius.de;bob;Nachname;Vorname;#a@x$b@y, ,c@z,$$$$Hi$Text\u000A +bob;filius.de;bob;Nachname;Vorname;#Name $Bob , $$$$Hi$Text\u000A +bob;filius.de;bob;Nachname;Vorname;#Only Name$b@y$$$$Hi$Text\u000A +bob;filius.de;bob;Nachname;Vorname;#a@x$b@y$$$$Hi$l1||l2&&00124&&x&&00036&&y&&00059&&z&&00035&&w\u000A +bob;filius.de;bob;Nachname;Vorname;#a@x$b@y$$$$Hi$&&00124&&|\u000A +bob;filius.de;bob;Nachname;Vorname;#a@x$b@y$$$$Hi$Text;extra\u000A +bob;filius.de;bob;Nachname;Vorname;##a@x$b@y$$$$Hi$Text##\u000A +bob;filius.de;bob;Nachname;Vorname;#a@x$b@y$$$$Hi$Text\u000D\u000A +bob;filius.de;bob;Nachname;Vorname;#\u001Fa@x $b@y$$$$Hi$Text\u000A diff --git a/packages/fls/test/email-server.java.json b/packages/fls/test/email-server.java.json new file mode 100644 index 0000000..c785fca --- /dev/null +++ b/packages/fls/test/email-server.java.json @@ -0,0 +1,740 @@ +[ + [ + "bob;filius.de;bob;Nachname;Vorname;\\u000A", + [ + { + "benutzername": "bob", + "passwort": "bob", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + } + ] + ], + [ + "bob;filius.de;bob;Nachname;Vorname;\\u000Aalice;filius.de;alice;A;B;\\u000A", + [ + { + "benutzername": "bob", + "passwort": "bob", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + }, + { + "benutzername": "alice", + "passwort": "alice", + "nachname": "A", + "vorname": "B", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + } + ] + ], + [ + "bob;filius.de;bob;Nachname;Vorname", + [ + { + "benutzername": "bob", + "passwort": "bob", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + } + ] + ], + [ + "bob;;bob;Nachname;Vorname;\\u000A", + "!NoSuchElementException" + ], + [ + "bob;filius.de;bob;Nachname\\u000A", + "!NoSuchElementException" + ], + [ + "bob;filius.de;bob;Nachname;Vorname;\\u000Abroken\\u000A", + "!NoSuchElementException" + ], + [ + " bob ;filius.de; bob ;Nachname;Vorname;\\u000A", + [ + { + "benutzername": " bob ", + "passwort": " bob ", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + } + ] + ], + [ + "bob;filius.de;bob;Nachname;Vorname\\u000D\\u000A", + [ + { + "benutzername": "bob", + "passwort": "bob", + "nachname": "Nachname", + "vorname": "Vorname\r", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + } + ] + ], + [ + "", + [] + ], + [ + "\\u000A\\u000A", + [] + ], + [ + "bob;filius.de;bob;Nachname;Vorname;#Alice $bob@filius.de$$$$Hi Bob!$Hi BoB!|\\u000A", + [ + { + "benutzername": "bob", + "passwort": "bob", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [ + { + "absender": [ + "Alice", + "alice@senior.de" + ], + "empfaenger": [ + [ + null, + "bob@filius.de" + ] + ], + "cc": [], + "bcc": [], + "betreff": "Hi Bob!", + "text": "Hi BoB!\n", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + } + ] + } + ] + ], + [ + "bob;filius.de;bob;Nachname;Vorname;#Alice $bob@filius.de$$$$Hi Bob!$Hi BoB!|#Alice $bob@filius.de$$$$Hi Bob!$Hi BoB!|\\u000A", + [ + { + "benutzername": "bob", + "passwort": "bob", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [ + { + "absender": [ + "Alice", + "alice@senior.de" + ], + "empfaenger": [ + [ + null, + "bob@filius.de" + ] + ], + "cc": [], + "bcc": [], + "betreff": "Hi Bob!", + "text": "Hi BoB!\n", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + }, + { + "absender": [ + "Alice", + "alice@senior.de" + ], + "empfaenger": [ + [ + null, + "bob@filius.de" + ] + ], + "cc": [], + "bcc": [], + "betreff": "Hi Bob!", + "text": "Hi BoB!\n", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + } + ] + } + ] + ], + [ + "bob;filius.de;bob;Nachname;Vorname;Alice $bob@filius.de$$$$Hi$Text\\u000A", + [ + { + "benutzername": "bob", + "passwort": "bob", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [ + { + "absender": [ + "Alice", + "alice@senior.de" + ], + "empfaenger": [ + [ + null, + "bob@filius.de" + ] + ], + "cc": [], + "bcc": [], + "betreff": "Hi", + "text": "Text", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + } + ] + } + ] + ], + [ + "bob;filius.de;bob;Nachname;Vorname;#Alice $bob@filius.de$$$$Hi$\\u000A", + [ + { + "benutzername": "bob", + "passwort": "bob", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + } + ] + ], + [ + "bob;filius.de;bob;Nachname;Vorname;#Alice $bob@filius.de$$$$$\\u000A", + [ + { + "benutzername": "bob", + "passwort": "bob", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + } + ] + ], + [ + "bob;filius.de;bob;Nachname;Vorname;#Alice $bob@filius.de$$$$Hi $5$Text\\u000A", + [ + { + "benutzername": "bob", + "passwort": "bob", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [] + } + ] + ], + [ + "bob;filius.de;bob;Nachname;Vorname;#a@x$b@y$c@z$d@w$1234$Hi$Text\\u000A", + [ + { + "benutzername": "bob", + "passwort": "bob", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [ + { + "absender": [ + null, + "a@x" + ], + "empfaenger": [ + [ + null, + "b@y" + ] + ], + "cc": [ + [ + null, + "c@z" + ] + ], + "bcc": [ + [ + null, + "d@w" + ] + ], + "betreff": "Hi", + "text": "Text", + "dateReceived": "1234", + "neu": true, + "delete": false, + "versendet": false + } + ] + } + ] + ], + [ + "bob;filius.de;bob;Nachname;Vorname;#a@x$b@y, ,c@z,$$$$Hi$Text\\u000A", + [ + { + "benutzername": "bob", + "passwort": "bob", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [ + { + "absender": [ + null, + "a@x" + ], + "empfaenger": [ + [ + null, + "b@y" + ], + [ + null, + "c@z" + ] + ], + "cc": [], + "bcc": [], + "betreff": "Hi", + "text": "Text", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + } + ] + } + ] + ], + [ + "bob;filius.de;bob;Nachname;Vorname;#Name $Bob , $$$$Hi$Text\\u000A", + [ + { + "benutzername": "bob", + "passwort": "bob", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [ + { + "absender": [ + null, + "a@x" + ], + "empfaenger": [ + [ + "Bob", + "b@y" + ], + [ + null, + "c@z" + ] + ], + "cc": [], + "bcc": [], + "betreff": "Hi", + "text": "Text", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + } + ] + } + ] + ], + [ + "bob;filius.de;bob;Nachname;Vorname;#Only Name$b@y$$$$Hi$Text\\u000A", + [ + { + "benutzername": "bob", + "passwort": "bob", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [ + { + "absender": [ + "Only Name", + null + ], + "empfaenger": [ + [ + null, + "b@y" + ] + ], + "cc": [], + "bcc": [], + "betreff": "Hi", + "text": "Text", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + } + ] + } + ] + ], + [ + "bob;filius.de;bob;Nachname;Vorname;#a@x$b@y$$$$Hi$l1||l2&&00124&&x&&00036&&y&&00059&&z&&00035&&w\\u000A", + [ + { + "benutzername": "bob", + "passwort": "bob", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [ + { + "absender": [ + null, + "a@x" + ], + "empfaenger": [ + [ + null, + "b@y" + ] + ], + "cc": [], + "bcc": [], + "betreff": "Hi", + "text": "l1\n\nl2|x$y;z#w", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + } + ] + } + ] + ], + [ + "bob;filius.de;bob;Nachname;Vorname;#a@x$b@y$$$$Hi$&&00124&&|\\u000A", + [ + { + "benutzername": "bob", + "passwort": "bob", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [ + { + "absender": [ + null, + "a@x" + ], + "empfaenger": [ + [ + null, + "b@y" + ] + ], + "cc": [], + "bcc": [], + "betreff": "Hi", + "text": "|\n", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + } + ] + } + ] + ], + [ + "bob;filius.de;bob;Nachname;Vorname;#a@x$b@y$$$$Hi$Text;extra\\u000A", + [ + { + "benutzername": "bob", + "passwort": "bob", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [ + { + "absender": [ + null, + "a@x" + ], + "empfaenger": [ + [ + null, + "b@y" + ] + ], + "cc": [], + "bcc": [], + "betreff": "Hi", + "text": "Text", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + } + ] + } + ] + ], + [ + "bob;filius.de;bob;Nachname;Vorname;##a@x$b@y$$$$Hi$Text##\\u000A", + [ + { + "benutzername": "bob", + "passwort": "bob", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [ + { + "absender": [ + null, + "a@x" + ], + "empfaenger": [ + [ + null, + "b@y" + ] + ], + "cc": [], + "bcc": [], + "betreff": "Hi", + "text": "Text", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + } + ] + } + ] + ], + [ + "bob;filius.de;bob;Nachname;Vorname;#a@x$b@y$$$$Hi$Text\\u000D\\u000A", + [ + { + "benutzername": "bob", + "passwort": "bob", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [ + { + "absender": [ + null, + "a@x" + ], + "empfaenger": [ + [ + null, + "b@y" + ] + ], + "cc": [], + "bcc": [], + "betreff": "Hi", + "text": "Text\r", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + } + ] + } + ] + ], + [ + "bob;filius.de;bob;Nachname;Vorname;#\\u001Fa@x $b@y$$$$Hi$Text\\u000A", + [ + { + "benutzername": "bob", + "passwort": "bob", + "nachname": "Nachname", + "vorname": "Vorname", + "emailAdresse": null, + "pop3server": null, + "pop3port": null, + "smtpserver": null, + "smtpport": null, + "nachrichten": [ + { + "absender": [ + null, + "a@x" + ], + "empfaenger": [ + [ + null, + "b@y" + ] + ], + "cc": [], + "bcc": [], + "betreff": "Hi", + "text": "Text", + "dateReceived": "", + "neu": true, + "delete": false, + "versendet": false + } + ] + } + ] + ] +] diff --git a/packages/fls/test/javaEmail.test.ts b/packages/fls/test/javaEmail.test.ts new file mode 100644 index 0000000..d196c8a --- /dev/null +++ b/packages/fls/test/javaEmail.test.ts @@ -0,0 +1,55 @@ +// Java's readers of the e-mail account files: every case in +// email-server.cases.txt and email-client.cases.txt was read by Java +// (EmailServer.kontenLaden, EmailAnwendung.laden; *.java.json, +// tools/java-oracle) and must come out the same here. + +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; +import { addressEntry, readJavaEmailClientFile, readJavaEmailServerFile } from '../src/javaEmail.js'; +import { javaEmailAccount } from './javaEmailNames.js'; + +const here = dirname(fileURLToPath(import.meta.url)); + +/** The cases file's escapes: a backslash, `u` and four hex digits for one UTF-16 unit. */ +function unescape(line: string): string { + return line.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex: string) => String.fromCharCode(Number.parseInt(hex, 16))); +} + +function measured(kind: 'server' | 'client'): [string, unknown][] { + const cases = JSON.parse(readFileSync(join(here, `email-${kind}.java.json`), 'utf8')) as [string, unknown][]; + const lines = readFileSync(join(here, `email-${kind}.cases.txt`), 'ascii').split('\n').slice(0, -1); + it('covers every line of the cases file', () => { + expect(cases.map(([line]) => line)).toEqual(lines); + }); + return cases; +} + +describe('readJavaEmailServerFile against Java', () => { + for (const [line, expected] of measured('server')) { + it(JSON.stringify(line), () => { + const accounts = readJavaEmailServerFile(unescape(line)); + expect(Array.isArray(accounts) ? accounts.map(javaEmailAccount) : `!${accounts.exception}`).toEqual(expected); + }); + } +}); + +describe('readJavaEmailClientFile against Java', () => { + for (const [line, expected] of measured('client')) { + it(JSON.stringify(line), () => { + const account = readJavaEmailClientFile(unescape(line)); + expect(account === null ? null : javaEmailAccount(account)).toEqual(expected); + }); + } +}); + +describe('addressEntry', () => { + it('reads a name and an address, an address alone, or a name alone', () => { + expect(addressEntry(' Alice ')).toEqual({ name: 'Alice', mailAddress: 'alice@senior.de' }); + expect(addressEntry('')).toEqual({ name: null, mailAddress: 'alice@senior.de' }); + expect(addressEntry('alice@senior.de')).toEqual({ name: null, mailAddress: 'alice@senior.de' }); + expect(addressEntry('Alice + entry === null ? null : typeof entry === 'string' ? ['string', entry] : [entry.name, entry.mailAddress]; + return { + absender: [email.sender.name, email.sender.mailAddress], + empfaenger: email.to.map(recipient), + cc: email.cc.map(recipient), + bcc: email.bcc.map(recipient), + betreff: email.subject, + text: email.text, + dateReceived: email.dateReceived, + neu: email.isNew, + delete: email.isDeleted, + versendet: email.isSent, + }; +} + diff --git a/packages/fls/test/javaFileSystem.test.ts b/packages/fls/test/javaFileSystem.test.ts new file mode 100644 index 0000000..4aac6d4 --- /dev/null +++ b/packages/fls/test/javaFileSystem.test.ts @@ -0,0 +1,35 @@ +// How Java FILIUS finds a file by path (Dateisystem.holeDatei), as the DNS +// server and the e-mail programs use it. + +import { describe, expect, it } from 'vitest'; +import type { FlsFileTree } from '../src/index.js'; +import { javaFileContent } from '../src/javaFileSystem.js'; + +const file = (name: string, content: string | null): FlsFileTree => ({ kind: 'file', name, type: 'text', content, children: [] }); +const directory = (name: string, ...children: FlsFileTree[]): FlsFileTree => ({ kind: 'directory', name, children }); + +describe('javaFileContent', () => { + it('finds a path ignoring case, as Java does', () => { + expect(javaFileContent(directory('root', directory('DNS', file('Hosts', 'x'))), '/dns/hosts')).toBe('x'); + }); + + it('takes the four units Java also equals with an ASCII letter (measured on the JDK)', () => { + expect(javaFileContent(directory('root', directory('dns', file('ho\u017Fts', 'long s'))), '/dns/hosts')).toBe('long s'); + expect(javaFileContent(directory('root', file('\u212Aonten.txt', 'kelvin')), '/konten.txt')).toBe('kelvin'); + expect(javaFileContent(directory('root', directory('ma\u0130lserver', file('konten.txt', 'dotted'))), '/mailserver/konten.txt')).toBe('dotted'); + expect(javaFileContent(directory('root', directory('ma\u0131lserver', file('konten.txt', 'dotless'))), '/mailserver/konten.txt')).toBe('dotless'); + expect(javaFileContent(directory('root', file('k\u00F6nten.txt', 'umlaut')), '/konten.txt')).toBeUndefined(); + }); + + it('takes the first match in preorder, and nothing when that is a directory', () => { + const twice = directory('root', directory('dns', file('hosts', 'first')), directory('DNS', file('hosts', 'second'))); + expect(javaFileContent(twice, '/dns/hosts')).toBe('first'); + expect(javaFileContent(directory('root', directory('dns', directory('hosts'), file('HOSTS', 'later'))), '/dns/hosts')).toBeUndefined(); + }); + + it('is undefined without the file, and empty for a file without content', () => { + expect(javaFileContent(undefined, '/dns/hosts')).toBeUndefined(); + expect(javaFileContent(directory('root', file('hosts', 'x')), '/dns/hosts')).toBeUndefined(); + expect(javaFileContent(directory('root', directory('dns', file('hosts', null))), '/dns/hosts')).toBe(''); + }); +}); diff --git a/packages/fls/test/topology.test.ts b/packages/fls/test/topology.test.ts index 798a70d..09a55fc 100644 --- a/packages/fls/test/topology.test.ts +++ b/packages/fls/test/topology.test.ts @@ -18,8 +18,11 @@ import { type FlsModem, type FlsNodeClass, type FlsTopology, + type JavaEmailAccount, + type JavaFailure, } from '../src/index.js'; import { EXPECTED_WARNINGS } from './expectedWarnings.js'; +import { javaEmail, javaEmailAccount } from './javaEmailNames.js'; const corpusRoot = join(dirname(fileURLToPath(import.meta.url)), '../../../corpus'); const javaView = JSON.parse(readFileSync(join(corpusRoot, 'corpus.java.json'), 'utf8')) as Record; @@ -91,9 +94,39 @@ function javaApplications(applications: FlsApplication[]): unknown[] { filterUdp: application.firewall.filterUdp, rules: application.firewall.rules.length, }), + ...(application.mailDomain === undefined + ? {} + : { + mailDomain: application.mailDomain, + beanAccounts: application.beanAccounts!.map(javaEmailAccount), + fileAccounts: javaFileAccounts(application.fileAccounts!), + }), + ...(application.received === undefined + ? {} + : { + received: application.received.map(javaEmail), + sent: application.sent!.map(javaEmail), + drafts: application.drafts!.map(javaEmail), + addressBook: application.addressBook!.map((contact) => [ + contact.name, + contact.firstName, + contact.street, + contact.houseNumber, + contact.postalCode, + contact.city, + contact.email, + contact.phone, + ]), + fileAccount: application.fileAccount === null ? null : javaEmailAccount(application.fileAccount!), + }), })); } +function javaFileAccounts(accounts: JavaEmailAccount[] | JavaFailure | null): unknown { + if (accounts === null) return null; + return Array.isArray(accounts) ? accounts.map(javaEmailAccount) : `!${accounts.exception}`; +} + /** Java's property names, as FlsOracle.java writes them. */ function javaDhcpServer(server: FlsDhcpServer) { return { @@ -423,6 +456,29 @@ describe('readTopology rules', () => { expect(Object.keys(byClass.get('filius.software.lokal.Terminal')!).sort()).toEqual(['javaClass', 'key', 'source']); }); + it('reads the e-mail beans through their setters: null names, string senders, a checked domain', () => { + const put = (className: string, body = '') => + `${className}${body}`; + const { nodes } = topologyOf(`${nodeItem(` + + ${put('filius.software.email.EmailServer', ` + 1und1.de + + bob + + + Alice <alice@senior.de> + + `)} + `)}`); + const server = nodes[0]!.system.applications![0]!; + + // setMailDomain keeps "filius.de" for a domain Java's pattern rejects. + expect(server).toMatchObject({ isActive: false, mailDomain: 'filius.de' }); + expect(server.beanAccounts![0]).toMatchObject({ username: 'bob', lastName: '', firstName: '' }); + expect(server.beanAccounts![0]!.messages[0]!.sender).toEqual({ name: 'Alice', mailAddress: 'alice@senior.de' }); + }); + it("gives a modem Java's defaults and keeps an address the file sets to null", () => { const { nodes } = topologyOf(` ${nodeItem('')} diff --git a/scripts/code_language_allow.txt b/scripts/code_language_allow.txt index 1f11695..97c759f 100644 --- a/scripts/code_language_allow.txt +++ b/scripts/code_language_allow.txt @@ -32,3 +32,27 @@ packages/fls/::adresse packages/fls/::ziel packages/fls/::teilnehmer packages/fls/::zahl +# Property and method names of Java FILIUS's e-mail classes (EmailKonto: +# benutzername, passwort, nachname, vorname, smtpserver, smtpport, nachrichten; +# Email: absender, empfaenger, betreff, neu, versendet; EmailAnwendung: +# empfangene/gesendete/erstellte Nachrichten; EmailServer.kontenLaden), as the +# Java oracle writes them; our types call them username, password, lastName, +# firstName, smtpServer, smtpPort, messages, sender, to, subject, isNew, +# isSent, received, sent and drafts. +packages/fls/::benutzername +packages/fls/::passwort +packages/fls/::nachname +packages/fls/::vorname +packages/fls/::smtpserver +packages/fls/::smtpport +packages/fls/::nachrichten +packages/fls/::absender +packages/fls/::empfaenger +packages/fls/::betreff +packages/fls/::neu +packages/fls/::versendet +packages/fls/::empfangene +packages/fls/::gesendete +packages/fls/::erstellte +packages/fls/::konten +packages/fls/::laden diff --git a/tools/java-oracle/README.md b/tools/java-oracle/README.md index fd20b20..685a959 100644 --- a/tools/java-oracle/README.md +++ b/tools/java-oracle/README.md @@ -33,7 +33,7 @@ virtuellen Bildschirm statt „headless". eigenen Vorlagen, sie stehen nicht in der Datei) und die installierten Programme eines Rechners mit ihren Einstellungen (Klassen, die Filius nicht mitbringt — eigene Anwendungen im Archiv unter `projekt/anwendungen` - — fehlen, weil das Werkzeug nur `konfiguration.xml` lädt); Kabel (Enden als Gerät/Schnittstelle), + — fehlen, weil das Werkzeug nur `konfiguration.xml` lädt); E-Mail: Konten des Servers aus der Bean-Liste und aus `/mailserver/konten.txt` (`kontenLaden`), Ordner und Adressbuch des Clients und sein Konto aus `/konten.txt` (`laden`); Kabel (Enden als Gerät/Schnittstelle), Doku-Elemente. Systemsoftware, Dateisysteme und Anwendungen folgen schrittweise. @@ -77,3 +77,16 @@ Zeilen nur LF; ein Name, den Javas Muster ablehnt (`1und1.de`, `müller.de`), wird leer, der Eintrag bleibt trotzdem; Typen außer A/MX/NS (auch `a`, `CNAME`, eine Kommentarzeile) gelten als gültig; eine TTL, die kein `int` ist, wird still 3600. + +## E-Mail-Kontodateien + +`EmailFileCases server|client` legt für jede Zeile von +`packages/fls/test/email-server.cases.txt` bzw. `email-client.cases.txt` +einen Rechner mit dieser Datei an (`/mailserver/konten.txt` bzw. +`/konten.txt`) und schreibt, welche Konten `EmailServer.kontenLaden()` bzw. +`EmailAnwendung.laden()` daraus machen. Gemessen: Eine einzige Serverzeile +mit weniger als fünf Feldern lässt Java die ganze Datei verwerfen +(`NoSuchElementException` — die Simulation startet nicht); leere Felder +überspringt Java, statt sie zu zählen; eine Nachricht mit leerem Text geht +verloren, weil `split` leere Felder am Ende abschneidet. Beim Client gewinnt +die letzte Zeile, auch eine kaputte, die das Konto leert. diff --git a/tools/java-oracle/run.sh b/tools/java-oracle/run.sh index dd02e51..892673f 100755 --- a/tools/java-oracle/run.sh +++ b/tools/java-oracle/run.sh @@ -21,7 +21,7 @@ if [ ! -f "$upstream/target/filius.jar" ]; then fi classpath="$upstream/target/filius.jar:$upstream/target/lib/*" mkdir -p "$here/.build" -javac -encoding UTF-8 -d "$here/.build" -cp "$classpath" "$here/src/FlsOracle.java" "$here/src/GetterDefaults.java" "$here/src/IpCheckCases.java" "$here/src/DnsHostsCases.java" +javac -encoding UTF-8 -d "$here/.build" -cp "$classpath" "$here/src/FlsOracle.java" "$here/src/GetterDefaults.java" "$here/src/IpCheckCases.java" "$here/src/DnsHostsCases.java" "$here/src/EmailFileCases.java" # FILIUS writes its settings below $HOME; keep them out of the real one. mkdir -p "$here/.build/home" # Removed first: a crashed run must not leave the previous result standing. @@ -73,3 +73,12 @@ dnshosts="$root/packages/fls/test/dns-hosts.java.json" rm -f "$dnshosts" HOME="$here/.build/home" xvfb-run -a java -cp "$classpath:$here/.build" DnsHostsCases "$cases" "$dnshosts" 2>&1 | grep -v 'logback\|^$' || true test -s "$dnshosts" + +# EmailServer.kontenLaden and EmailAnwendung.laden on hand-picked account files (packages/fls/src/javaEmail.ts). +for kind in server client; do + cases="$root/packages/fls/test/email-$kind.cases.txt" + output="$root/packages/fls/test/email-$kind.java.json" + rm -f "$output" + HOME="$here/.build/home" xvfb-run -a java -cp "$classpath:$here/.build" EmailFileCases "$kind" "$cases" "$output" 2>&1 | grep -v 'logback\|^$' || true + test -s "$output" +done diff --git a/tools/java-oracle/src/EmailFileCases.java b/tools/java-oracle/src/EmailFileCases.java new file mode 100644 index 0000000..6a5b75d --- /dev/null +++ b/tools/java-oracle/src/EmailFileCases.java @@ -0,0 +1,65 @@ +// Runs FILIUS's own readers of the e-mail account files on every case of a +// list: EmailServer.kontenLaden on /mailserver/konten.txt ("server") and +// EmailAnwendung.laden on /konten.txt ("client"). Writes the accounts Java +// ends up with — the reference for packages/fls/src/javaEmail.ts. One case +// per line: a whole file, ASCII with Java-style unicode escapes (backslash, +// u, four hex digits). +// +// Usage: java -cp filius.jar:lib/*:classes EmailFileCases server|client + +import filius.hardware.knoten.Rechner; +import filius.software.email.EmailAnwendung; +import filius.software.email.EmailKonto; +import filius.software.email.EmailServer; +import filius.software.system.Datei; +import filius.software.system.Dateisystem; +import filius.software.system.InternetKnotenBetriebssystem; + +import java.io.FileOutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +public class EmailFileCases { + + public static void main(String[] args) throws Exception { + boolean server = args[0].equals("server"); + List result = new ArrayList<>(); + for (String line : Files.readAllLines(Path.of(args[1]), StandardCharsets.US_ASCII)) { + InternetKnotenBetriebssystem system = (InternetKnotenBetriebssystem) new Rechner().getSystemSoftware(); + Dateisystem fileSystem = system.getDateisystem(); + Datei file = new Datei(); + file.setName("konten.txt"); + file.setDateiInhalt(IpCheckCases.unescape(line)); + Object accounts; + try { + if (server) { + EmailServer mail = new EmailServer(); + mail.setSystemSoftware(system); + fileSystem.speicherDatei(fileSystem.holeRootPfad() + Dateisystem.FILE_SEPARATOR + "mailserver", file); + mail.kontenLaden(); + accounts = FlsOracle.emailAccounts(mail.getListeBenutzerkonten()); + } else { + EmailAnwendung mail = new EmailAnwendung(); + mail.setSystemSoftware(system); + fileSystem.speicherDatei(fileSystem.getRoot(), file); + mail.laden(); + Map konten = mail.holeKontoListe(); + accounts = konten.isEmpty() ? null : FlsOracle.emailAccounts(new ArrayList<>(konten.values())).get(0); + } + } catch (RuntimeException error) { + accounts = "!" + error.getClass().getSimpleName(); + } + result.add(Arrays.asList(line, accounts)); + } + try (FileOutputStream out = new FileOutputStream(args[2])) { + out.write((FlsOracle.Json.write(result, "") + "\n").getBytes(StandardCharsets.UTF_8)); + } + System.out.println("email " + args[0] + " cases: " + result.size()); + System.exit(0); + } +} diff --git a/tools/java-oracle/src/FlsOracle.java b/tools/java-oracle/src/FlsOracle.java index c25b6b3..5ddca22 100644 --- a/tools/java-oracle/src/FlsOracle.java +++ b/tools/java-oracle/src/FlsOracle.java @@ -21,6 +21,12 @@ import filius.software.clientserver.ServerAnwendung; import filius.software.dateiaustausch.PeerToPeerAnwendung; import filius.software.dhcp.DHCPServer; import filius.software.dns.DNSServer; +import filius.software.email.AddressEntry; +import filius.software.email.Email; +import filius.software.email.EmailAnwendung; +import filius.software.email.EmailKonto; +import filius.software.email.EmailServer; +import filius.software.email.Kontakt; import filius.software.www.WebServer; import filius.software.firewall.Firewall; import filius.software.firewall.FirewallRule; @@ -296,8 +302,11 @@ public class FlsOracle { /** * The installed applications of a PC or notebook, by key: the class, and - * the settings the topology layer reads. E-mail accounts and messages are - * a later layer. + * the settings the topology layer reads. E-mail: the server's accounts as + * the file's bean list has them and as kontenLaden() reads them from + * /mailserver/konten.txt (what Java uses from the start of a simulation); + * the client's folders and address book, and the account laden() reads + * from /konten.txt when its window opens. */ static List applications(InternetKnotenBetriebssystem system) { List result = new ArrayList<>(); @@ -318,6 +327,41 @@ public class FlsOracle { if (application instanceof DNSServer dns) json.put("recursiveResolutionEnabled", dns.isRecursiveResolutionEnabled()); if (application instanceof WebServer web) json.put("useVHost", web.isUseVHost()); if (application instanceof PeerToPeerAnwendung peer) json.put("maxTeilnehmerZahl", peer.getMaxTeilnehmerZahl()); + if (application instanceof EmailServer mail) { + json.put("aktiv", mail.isAktiv()); + json.put("mailDomain", mail.getMailDomain()); + json.put("beanAccounts", emailAccounts(mail.getListeBenutzerkonten())); + boolean hasFile = system.getDateisystem().holeDatei("/mailserver/konten.txt") != null; + Object fileAccounts = null; + if (hasFile) { + try { + mail.kontenLaden(); + fileAccounts = emailAccounts(mail.getListeBenutzerkonten()); + } catch (RuntimeException error) { + fileAccounts = "!" + error.getClass().getSimpleName(); + } + } + json.put("fileAccounts", fileAccounts); + } + if (application instanceof EmailAnwendung mail) { + json.put("received", emails(mail.getEmpfangeneNachrichten())); + json.put("sent", emails(mail.getGesendeteNachrichten())); + json.put("drafts", emails(mail.getErstellteNachrichten())); + List contacts = new ArrayList<>(); + for (Kontakt contact : mail.getAdressbuch()) { + contacts.add(Arrays.asList(contact.getName(), contact.getVorname(), contact.getStrasse(), + contact.getHausnr(), contact.getPlz(), contact.getWohnort(), contact.getEmail(), + contact.getTelefon())); + } + json.put("addressBook", contacts); + try { + mail.laden(); + Map accounts = mail.holeKontoListe(); + json.put("fileAccount", accounts.isEmpty() ? null : emailAccounts(new ArrayList<>(accounts.values())).get(0)); + } catch (RuntimeException error) { + json.put("fileAccount", "!" + error.getClass().getSimpleName()); + } + } if (application instanceof Firewall firewall) { json.put("activated", firewall.isActivated()); json.put("defaultPolicy", (int) firewall.getDefaultPolicy()); @@ -331,6 +375,61 @@ public class FlsOracle { return result; } + static List emailAccounts(List accounts) { + List result = new ArrayList<>(); + for (EmailKonto account : accounts) { + Map json = new LinkedHashMap<>(); + json.put("benutzername", account.getBenutzername()); + json.put("passwort", account.getPasswort()); + json.put("nachname", account.getNachname()); + json.put("vorname", account.getVorname()); + json.put("emailAdresse", account.getEmailAdresse()); + json.put("pop3server", account.getPop3server()); + json.put("pop3port", account.getPop3port()); + json.put("smtpserver", account.getSmtpserver()); + json.put("smtpport", account.getSmtpport()); + json.put("nachrichten", emails(account.getNachrichten())); + result.add(json); + } + return result; + } + + static List emails(List emails) { + List result = new ArrayList<>(); + for (Email email : emails) { + Map json = new LinkedHashMap<>(); + json.put("absender", address(email.getAbsender())); + json.put("empfaenger", addresses(email.getEmpfaenger())); + json.put("cc", addresses(email.getCc())); + json.put("bcc", addresses(email.getBcc())); + json.put("betreff", email.getBetreff()); + json.put("text", email.getText()); + json.put("dateReceived", email.getDateReceived()); + json.put("neu", email.getNeu()); + json.put("delete", email.getDelete()); + json.put("versendet", email.isVersendet()); + result.add(json); + } + return result; + } + + /** + * Older files keep recipients as strings: the list is typed, but the + * decoder does not check, so an element may be a String. + */ + static List addresses(List entries) { + List result = new ArrayList<>(); + for (Object entry : entries) { + if (entry == null) result.add(null); + else result.add(entry instanceof AddressEntry address ? address(address) : Arrays.asList("string", entry.toString())); + } + return result; + } + + static List address(AddressEntry entry) { + return entry == null ? null : Arrays.asList(entry.getName(), entry.getMailAddress()); + } + /** * A file system tree: a directory is a node whose user object is its name, * a file one whose user object is a Datei. Contents as SHA-256 and length