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 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 07:06:55 +00:00
co-authored by Claude Opus 5.5
parent c80d212c90
commit ec5523d7c7
22 changed files with 2900 additions and 94 deletions
+3
View File
@@ -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/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/topology.test.ts` | — | neu: alle 23 Korpus-Dateien Feld für Feld wie Java (Geräte, Schnittstellen, Kabel, Doku-Elemente), dazu die Regeln, die der Korpus nicht berührt |
| `test/javaIp.test.ts`, `test/ip-check.*` | — | neu: `IP.ipCheck`-Fälle, Ergebnis aus Java | | `test/javaIp.test.ts`, `test/ip-check.*` | — | neu: `IP.ipCheck`-Fälle, Ergebnis aus Java |
| `src/javaEmail.ts` | Java `EmailServer` (Konten-Bean, `kontenLaden`/`stringZuListeBenutzerkonten`), `EmailAnwendung` (Ordner, Adressbuch, `laden`), `EmailKonto`, `Email`, `AddressEntry`, `EmailUtils` | neu nach Java: E-Mail-Beans über ihre Setter, `/mailserver/konten.txt` und `/konten.txt` 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/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/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` | | `test/corpus.test.ts` | — | neu: 23 Korpus-Dateien, gegengeprüft mit `fflate.unzipSync` |
+1055 -23
View File
File diff suppressed because it is too large Load Diff
+5 -40
View File
@@ -11,7 +11,9 @@
// - duplicates stay. // - duplicates stay.
import type { DnsResourceRecord } from '@mdg-filius/core'; import type { DnsResourceRecord } from '@mdg-filius/core';
import { javaFileContent } from './javaFileSystem.js';
import { javaIpCheck, javaParseInt } from './javaIp.js'; import { javaIpCheck, javaParseInt } from './javaIp.js';
import { javaTrim } from './javaString.js';
import type { FlsFileTree } from './systemSoftware.js'; import type { FlsFileTree } from './systemSoftware.js';
/** A record as Java's ResourceRecord holds it (the getters). */ /** 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 // EingabenUeberpruefung.musterDomain, including its quirks: labels start with
// a letter, labels after the first may hold a backslash, and the empty // a letter, labels after the first may hold a backslash, and the empty
// string is a valid domain. // 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. // EingabenUeberpruefung.musterIpAdresse.
const JAVA_IP_ADDRESS = 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]))$/; /^(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. */ /** The content of /dns/hosts as Java's DNS server finds it (see javaFileSystem.ts). */
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).
*/
export function javaHostsFileContent(root: FlsFileTree | undefined): string | undefined { export function javaHostsFileContent(root: FlsFileTree | undefined): string | undefined {
if (root === undefined) return undefined; return javaFileContent(root, DNS_HOSTS_PATH);
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;
} }
/** `DNSServer.leseRecordListe` on the content of /dns/hosts. */ /** `DNSServer.leseRecordListe` on the content of /dns/hosts. */
+3
View File
@@ -2,7 +2,10 @@
export * from './archive.js'; export * from './archive.js';
export * from './dnsHosts.js'; export * from './dnsHosts.js';
export * from './importProject.js'; export * from './importProject.js';
export * from './javaEmail.js';
export * from './javaFileSystem.js';
export * from './javaIp.js'; export * from './javaIp.js';
export * from './javaString.js';
export * from './javabeans.js'; export * from './javabeans.js';
export * from './systemSoftware.js'; export * from './systemSoftware.js';
export * from './topology.js'; export * from './topology.js';
+304
View File
@@ -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 (`<void id="AddressEntry0" property="absender">`): 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 <address>", "<address>", 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: [],
};
}
+51
View File
@@ -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;
}
+24
View File
@@ -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 !== '');
}
+54 -4
View File
@@ -27,6 +27,21 @@
// not know Java's constructors and sees a placeholder per path; they are // not know Java's constructors and sees a placeholder per path; they are
// merged here. // 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 { import {
isJavaObject, isJavaObject,
isUnresolved, isUnresolved,
@@ -114,12 +129,12 @@ const SERVER_APPLICATIONS = new Set<string>([
'filius.software.www.WebServer', '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 { export interface FlsApplication {
/** The key in installierteAnwendungen — the class name in every corpus file. */ /** The key in installierteAnwendungen — the class name in every corpus file. */
key: string | null; key: string | null;
javaClass: FlsApplicationClass; 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; isActive?: boolean;
port?: number; port?: number;
/** ClientBaustein: the server it talks to (Java's zielIPAdresse, zielPort). */ /** ClientBaustein: the server it talks to (Java's zielIPAdresse, zielPort). */
@@ -136,6 +151,23 @@ export interface FlsApplication {
maxPeers?: number | null; maxPeers?: number | null;
/** Firewall: the personal firewall. */ /** Firewall: the personal firewall. */
firewall?: FlsFirewall; 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. */ /** The application's JavaBean, for the e-mail layer. */
source: JavaObject; source: JavaObject;
} }
@@ -188,7 +220,7 @@ export function readSystemSoftware(device: JavaObject, javaClass: FlsNodeClass,
case 'Notebook': case 'Notebook':
result.ssid = optionalString(value('ssid'), 'ssid', warn) ?? null; result.ssid = optionalString(value('ssid'), 'ssid', warn) ?? null;
result.fileSystem = fileSystem(value('dateisystem'), warn); result.fileSystem = fileSystem(value('dateisystem'), warn);
result.applications = applications(value('installierteAnwendungen'), warn); result.applications = applications(value('installierteAnwendungen'), result.fileSystem, warn);
break; break;
case 'Vermittlungsrechner': case 'Vermittlungsrechner':
result.ripEnabled = boolean(value('ripEnabled'), false, 'ripEnabled', warn); 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 * a project's own application, compiled into projekt/anwendungen — is left
* out: this reader cannot run Java classes. * 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[] = []; const result: FlsApplication[] = [];
for (const [key, application] of mapEntries(map)) { for (const [key, application] of mapEntries(map)) {
if (!isJavaObject(application)) { if (!isJavaObject(application)) {
@@ -278,6 +310,24 @@ function applications(map: JavaValue | undefined, warn: Warn): FlsApplication[]
case 'filius.software.firewall.Firewall': case 'filius.software.firewall.Firewall':
entry.firewall = firewallOf([application], true, warn); entry.firewall = firewallOf([application], true, warn);
break; 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); result.push(entry);
} }
+1 -23
View File
@@ -8,8 +8,7 @@ import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path'; import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import type { FlsFileTree } from '../src/index.js'; import { dnsRecordsOf, readJavaDnsHosts } from '../src/dnsHosts.js';
import { dnsRecordsOf, javaHostsFileContent, readJavaDnsHosts } from '../src/dnsHosts.js';
const here = dirname(fileURLToPath(import.meta.url)); const here = dirname(fileURLToPath(import.meta.url));
type Row = [string, string, number, string]; 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)"]); 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('');
});
});
+16
View File
@@ -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
+220
View File
@@ -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": []
}
]
]
+27
View File
@@ -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 <alice@senior.de>$bob@filius.de$$$$Hi Bob!$Hi BoB!|\u000A
bob;filius.de;bob;Nachname;Vorname;#Alice <alice@senior.de>$bob@filius.de$$$$Hi Bob!$Hi BoB!|#Alice <alice@senior.de>$bob@filius.de$$$$Hi Bob!$Hi BoB!|\u000A
bob;filius.de;bob;Nachname;Vorname;Alice <alice@senior.de>$bob@filius.de$$$$Hi$Text\u000A
bob;filius.de;bob;Nachname;Vorname;#Alice <alice@senior.de>$bob@filius.de$$$$Hi$\u000A
bob;filius.de;bob;Nachname;Vorname;#Alice <alice@senior.de>$bob@filius.de$$$$$\u000A
bob;filius.de;bob;Nachname;Vorname;#Alice <alice@senior.de>$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 <a@x$b@y$$$$Hi$Text\u000A
bob;filius.de;bob;Nachname;Vorname;#<a@x>$Bob <b@y> , <c@z>$$$$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
+740
View File
@@ -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 <alice@senior.de>$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 <alice@senior.de>$bob@filius.de$$$$Hi Bob!$Hi BoB!|#Alice <alice@senior.de>$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 <alice@senior.de>$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 <alice@senior.de>$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 <alice@senior.de>$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 <alice@senior.de>$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 <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": [
"Name <a@x",
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>$Bob <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": [
[
"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
}
]
}
]
]
]
+55
View File
@@ -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 <alice@senior.de> ')).toEqual({ name: 'Alice', mailAddress: 'alice@senior.de' });
expect(addressEntry('<alice@senior.de>')).toEqual({ name: null, mailAddress: 'alice@senior.de' });
expect(addressEntry('alice@senior.de')).toEqual({ name: null, mailAddress: 'alice@senior.de' });
expect(addressEntry('Alice <alice@senior.de')).toEqual({ name: 'Alice <alice@senior.de', mailAddress: null });
expect(addressEntry('Alice')).toEqual({ name: 'Alice', mailAddress: null });
});
});
+37
View File
@@ -0,0 +1,37 @@
// The e-mail beans under Java's property names, as FlsOracle.java and
// EmailFileCases.java write them.
import type { JavaEmail, JavaEmailAccount, JavaRecipient } from '../src/index.js';
export function javaEmailAccount(account: JavaEmailAccount) {
return {
benutzername: account.username,
passwort: account.password,
nachname: account.lastName,
vorname: account.firstName,
emailAdresse: account.emailAddress,
pop3server: account.pop3Server,
pop3port: account.pop3Port,
smtpserver: account.smtpServer,
smtpport: account.smtpPort,
nachrichten: account.messages.map(javaEmail),
};
}
export function javaEmail(email: JavaEmail) {
const recipient = (entry: JavaRecipient) =>
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,
};
}
+35
View File
@@ -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('');
});
});
+56
View File
@@ -18,8 +18,11 @@ import {
type FlsModem, type FlsModem,
type FlsNodeClass, type FlsNodeClass,
type FlsTopology, type FlsTopology,
type JavaEmailAccount,
type JavaFailure,
} from '../src/index.js'; } from '../src/index.js';
import { EXPECTED_WARNINGS } from './expectedWarnings.js'; import { EXPECTED_WARNINGS } from './expectedWarnings.js';
import { javaEmail, javaEmailAccount } from './javaEmailNames.js';
const corpusRoot = join(dirname(fileURLToPath(import.meta.url)), '../../../corpus'); const corpusRoot = join(dirname(fileURLToPath(import.meta.url)), '../../../corpus');
const javaView = JSON.parse(readFileSync(join(corpusRoot, 'corpus.java.json'), 'utf8')) as Record<string, unknown>; const javaView = JSON.parse(readFileSync(join(corpusRoot, 'corpus.java.json'), 'utf8')) as Record<string, unknown>;
@@ -91,9 +94,39 @@ function javaApplications(applications: FlsApplication[]): unknown[] {
filterUdp: application.firewall.filterUdp, filterUdp: application.firewall.filterUdp,
rules: application.firewall.rules.length, 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. */ /** Java's property names, as FlsOracle.java writes them. */
function javaDhcpServer(server: FlsDhcpServer) { function javaDhcpServer(server: FlsDhcpServer) {
return { return {
@@ -423,6 +456,29 @@ describe('readTopology rules', () => {
expect(Object.keys(byClass.get('filius.software.lokal.Terminal')!).sort()).toEqual(['javaClass', 'key', 'source']); 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 = '') =>
`<void method="put"><string>${className}</string><object class="${className}">${body}</object></void>`;
const { nodes } = topologyOf(`<object class="java.util.LinkedList">${nodeItem(`<object class="filius.hardware.knoten.Rechner">
<void property="systemSoftware"><void property="installierteAnwendungen">
${put('filius.software.email.EmailServer', `
<void property="mailDomain"><string>1und1.de</string></void>
<void property="listeBenutzerkonten"><void method="add"><object class="filius.software.email.EmailKonto">
<void property="benutzername"><string>bob</string></void>
<void property="nachname"><null/></void>
<void property="nachrichten"><void method="add"><object class="filius.software.email.Email">
<void property="absender"><string> Alice &lt;alice@senior.de&gt; </string></void>
</object></void></void>
</object></void></void>`)}
</void></void></object>`)}</object>`);
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", () => { it("gives a modem Java's defaults and keeps an address the file sets to null", () => {
const { nodes } = topologyOf(`<object class="java.util.LinkedList"> const { nodes } = topologyOf(`<object class="java.util.LinkedList">
${nodeItem('<object class="filius.hardware.knoten.Modem"/>')} ${nodeItem('<object class="filius.hardware.knoten.Modem"/>')}
+24
View File
@@ -32,3 +32,27 @@ packages/fls/::adresse
packages/fls/::ziel packages/fls/::ziel
packages/fls/::teilnehmer packages/fls/::teilnehmer
packages/fls/::zahl 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
+14 -1
View File
@@ -33,7 +33,7 @@ virtuellen Bildschirm statt „headless".
eigenen Vorlagen, sie stehen nicht in der Datei) und die installierten eigenen Vorlagen, sie stehen nicht in der Datei) und die installierten
Programme eines Rechners mit ihren Einstellungen (Klassen, die Filius Programme eines Rechners mit ihren Einstellungen (Klassen, die Filius
nicht mitbringt — eigene Anwendungen im Archiv unter `projekt/anwendungen` 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. Doku-Elemente.
Systemsoftware, Dateisysteme und Anwendungen folgen schrittweise. 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`, 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` `CNAME`, eine Kommentarzeile) gelten als gültig; eine TTL, die kein `int`
ist, wird still 3600. 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.
+10 -1
View File
@@ -21,7 +21,7 @@ if [ ! -f "$upstream/target/filius.jar" ]; then
fi fi
classpath="$upstream/target/filius.jar:$upstream/target/lib/*" classpath="$upstream/target/filius.jar:$upstream/target/lib/*"
mkdir -p "$here/.build" 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. # FILIUS writes its settings below $HOME; keep them out of the real one.
mkdir -p "$here/.build/home" mkdir -p "$here/.build/home"
# Removed first: a crashed run must not leave the previous result standing. # 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" rm -f "$dnshosts"
HOME="$here/.build/home" xvfb-run -a java -cp "$classpath:$here/.build" DnsHostsCases "$cases" "$dnshosts" 2>&1 | grep -v 'logback\|^$' || true HOME="$here/.build/home" xvfb-run -a java -cp "$classpath:$here/.build" DnsHostsCases "$cases" "$dnshosts" 2>&1 | grep -v 'logback\|^$' || true
test -s "$dnshosts" 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
+65
View File
@@ -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 <cases txt> <output json>
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<Object> 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<String, EmailKonto> 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);
}
}
+101 -2
View File
@@ -21,6 +21,12 @@ import filius.software.clientserver.ServerAnwendung;
import filius.software.dateiaustausch.PeerToPeerAnwendung; import filius.software.dateiaustausch.PeerToPeerAnwendung;
import filius.software.dhcp.DHCPServer; import filius.software.dhcp.DHCPServer;
import filius.software.dns.DNSServer; 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.www.WebServer;
import filius.software.firewall.Firewall; import filius.software.firewall.Firewall;
import filius.software.firewall.FirewallRule; 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 installed applications of a PC or notebook, by key: the class, and
* the settings the topology layer reads. E-mail accounts and messages are * the settings the topology layer reads. E-mail: the server's accounts as
* a later layer. * 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<Object> applications(InternetKnotenBetriebssystem system) { static List<Object> applications(InternetKnotenBetriebssystem system) {
List<Object> result = new ArrayList<>(); List<Object> result = new ArrayList<>();
@@ -318,6 +327,41 @@ public class FlsOracle {
if (application instanceof DNSServer dns) json.put("recursiveResolutionEnabled", dns.isRecursiveResolutionEnabled()); if (application instanceof DNSServer dns) json.put("recursiveResolutionEnabled", dns.isRecursiveResolutionEnabled());
if (application instanceof WebServer web) json.put("useVHost", web.isUseVHost()); if (application instanceof WebServer web) json.put("useVHost", web.isUseVHost());
if (application instanceof PeerToPeerAnwendung peer) json.put("maxTeilnehmerZahl", peer.getMaxTeilnehmerZahl()); 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<Object> 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<String, EmailKonto> 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) { if (application instanceof Firewall firewall) {
json.put("activated", firewall.isActivated()); json.put("activated", firewall.isActivated());
json.put("defaultPolicy", (int) firewall.getDefaultPolicy()); json.put("defaultPolicy", (int) firewall.getDefaultPolicy());
@@ -331,6 +375,61 @@ public class FlsOracle {
return result; return result;
} }
static List<Object> emailAccounts(List<EmailKonto> accounts) {
List<Object> result = new ArrayList<>();
for (EmailKonto account : accounts) {
Map<String, Object> 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<Object> emails(List<Email> emails) {
List<Object> result = new ArrayList<>();
for (Email email : emails) {
Map<String, Object> 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<Object> addresses(List<AddressEntry> entries) {
List<Object> 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<Object> 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 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 * a file one whose user object is a Datei. Contents as SHA-256 and length