feat(core): the e-mail server after Java, measured against Java
SMTP on port 25 stores mails for the domain's accounts as Java reads them anew and tells a local sender of unknown recipients; POP3 on port 110 hands them out. The mailboxes are in the snapshot and go back into the project. EmailRun plays four stories over the network; MailCases feeds Java's workers without one (26 cases, de and en). 18 cases name Java bugs that the core does not take. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -684,3 +684,44 @@ Gemessen (01.10.2026):
|
||||
Regel „TCP 80 verwerfen“ über die erlaubende geschoben, und das SYN des
|
||||
Browsers bleibt an der Firewall hängen. Javas Router-Server lauschte auch
|
||||
hier in einem Lauf nie (`run.sh` wiederholt).
|
||||
|
||||
## E-Mail-Server (F3.3a)
|
||||
|
||||
`EmailRun` lässt Javas einfachen Client (`ClientBaustein`) auf `mail-lan`
|
||||
rohe SMTP- und POP3-Befehle an einen E-Mail-Server schicken (Domain
|
||||
`filius.de`, Konten aus `/mailserver/konten.txt`: anna mit zwei Mails, bob
|
||||
ohne). Aufgezeichnet werden die Frames, die Fensterzeilen von Server und
|
||||
Client und am Ende die Konten (jede Mail als `Email.toString`, neu,
|
||||
gelöscht). `MailCases` füttert Javas `SMTPMitarbeiter` und
|
||||
`POP3Mitarbeiter` ohne Netz über einen Socket, der nur mitschreibt, mit den
|
||||
Fällen aus `packages/core/test/email-server.cases.txt`, auf Deutsch und auf
|
||||
Englisch. Die Netz-Abläufe bleiben bei dem, was Java richtig macht; Javas
|
||||
Fehler zeigen die Fälle.
|
||||
|
||||
Gemessen (01.10.2026):
|
||||
|
||||
* Der Server startet POP3 (Port 110) vor SMTP (Port 25); POP3s Socket nimmt
|
||||
die erste TCP-Startnummer. Die SMTP-Antworten sind übersetzt („220
|
||||
Willkommen bei“), die POP3-Antworten englisch. SMTP schreibt seine
|
||||
Fensterzeile nach dem Senden, POP3 davor.
|
||||
* Eine angenommene Mail speichert Java so, wie es ihren Text neu liest
|
||||
(`new Email(email.toString())`): Zeilenenden ohne `\r`, kein Bcc.
|
||||
* SMTP-Fehler: Nach einer Mail beantwortet der Server den nächsten Befehl mit
|
||||
503 und behält Absender, Empfänger und Text (die zweite Mail ging auch an
|
||||
die erste Empfängerin, mit dem ersten Text); unbekannte oder verfrühte
|
||||
Befehle bleiben ohne Antwort; ein `quit` im Mailtext beendet die
|
||||
Verbindung; die Domain wird genau verglichen (`FILIUS.DE` ist fremd); ein
|
||||
leerer Absender mit unbekanntem Empfänger und ein Empfänger ohne `@`
|
||||
werfen im Mitarbeiter, die Verbindung endet; eine unbekannte Kopfzeile
|
||||
beendet den Kopf (der Betreff wird Text), ohne Leerzeile fehlt die erste
|
||||
Zeile; eine Benachrichtigung an eine Mail ohne From hat `To: null`.
|
||||
* Die Benachrichtigung über unbekannte Empfänger trägt Javas Datum im
|
||||
Gebietsschema der JVM (hier `10/1/26, 6:30 PM`) und die Adresse des
|
||||
Servers.
|
||||
* POP3 zählt Mails ab 0. Fehler: eine Mail lässt sich nur einmal abrufen;
|
||||
`LIST` ohne Mails gibt `-ERR`, `LIST 0` listet alle; vor der Anmeldung
|
||||
kommen leere Antworten; eine fehlende Mail kommt als leere `+OK`-Mail,
|
||||
`DELE` darauf leer; ein fehlendes oder falsches Argument wirft, die
|
||||
Verbindung endet; nach `DELE` nummeriert `LIST` neu, `RETR` nicht.
|
||||
* Die Größe in `STAT` und `LIST` ist Javas eigene: die Länge von Text,
|
||||
Absender, Betreff, Empfängerliste (`[a, b]`) und Datum.
|
||||
|
||||
@@ -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" "$here/src/EmailFileCases.java" "$here/src/BeansDump.java" "$here/src/Base64Cases.java" "$here/src/ScenarioNetwork.java" "$here/src/DhcpRun.java" "$here/src/TcpRun.java" "$here/src/DnsRun.java" "$here/src/ArpRun.java" "$here/src/RipRun.java" "$here/src/NatRun.java" "$here/src/WlanRun.java" "$here/src/ExampleRun.java" "$here/src/EchoRun.java" "$here/src/WebRun.java" "$here/src/RouterWebRun.java" "$here/src/KonfigCases.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" "$here/src/BeansDump.java" "$here/src/Base64Cases.java" "$here/src/ScenarioNetwork.java" "$here/src/DhcpRun.java" "$here/src/TcpRun.java" "$here/src/DnsRun.java" "$here/src/ArpRun.java" "$here/src/RipRun.java" "$here/src/NatRun.java" "$here/src/WlanRun.java" "$here/src/ExampleRun.java" "$here/src/EchoRun.java" "$here/src/WebRun.java" "$here/src/RouterWebRun.java" "$here/src/KonfigCases.java" "$here/src/EmailRun.java" "$here/src/MailCases.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.
|
||||
@@ -247,6 +247,24 @@ for language in de en; do
|
||||
test -s "$output"
|
||||
done
|
||||
|
||||
# The e-mail server as Java FILIUS runs it (EmailRun on mail-lan), and its
|
||||
# SMTP and POP3 workers fed without a network, in both languages (MailCases).
|
||||
for run in smtp:16000 smtpErrors:25000 pop3:36000 pop3Errors:20000; do
|
||||
story="${run%%:*}"
|
||||
output="$root/packages/core/test/scenarios/email-java-$story.java.json"
|
||||
rm -f "$output"
|
||||
HOME="$here/.build/home" xvfb-run -a java -cp "$classpath:$here/.build" EmailRun \
|
||||
"$root/packages/core/test/scenarios/mail-lan.scenario.json" "$story" "${run##*:}" "$output" 2>&1 | grep -v 'logback\|^$' || true
|
||||
test -s "$output"
|
||||
done
|
||||
for language in de en; do
|
||||
output="$root/packages/core/test/email-server.$language.java.json"
|
||||
rm -f "$output"
|
||||
HOME="$here/.build/home" xvfb-run -a java -cp "$classpath:$here/.build" MailCases "$language" \
|
||||
"$root/packages/core/test/email-server.cases.txt" "$output" 2>&1 | grep -v 'logback\|^$' || true
|
||||
test -s "$output"
|
||||
done
|
||||
|
||||
# FILIUS' own examples as Java FILIUS runs them, opened and started as FILIUS
|
||||
# does (ExampleRun): the acceptance of F2 (packages/fls/test/examplesJava.test.ts).
|
||||
for run in internetwork_komplett:routing:12000 einfaches_rechnernetz_komplett:dhcp:20000 dns_server:dns:12000 \
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
// Runs a story of FILIUS' e-mail server (EmailServer with its SMTPServer and
|
||||
// POP3Server) on a core scenario's network in Java FILIUS and records what
|
||||
// really happens: every frame each card saw, every line the server wrote
|
||||
// into its window, every line of the generic client that talks to it, and
|
||||
// the server's accounts at the end (each message as Email.toString, whether
|
||||
// it is new, whether it is marked for deletion).
|
||||
//
|
||||
// EmailRun <scenario.json> <story> <milliseconds> <out.json>
|
||||
//
|
||||
// The stories use mail-lan (client on 192.168.0.20, e-mail server on .30,
|
||||
// domain filius.de). The server's accounts come from /mailserver/konten.txt
|
||||
// as below (anna with two messages, from bob and from carla; bob without),
|
||||
// switched on.
|
||||
// FILIUS' generic client (ClientBaustein) on .20 sends the commands as they
|
||||
// are, one message each, after a pause for the answer.
|
||||
// smtp to anna, then QUIT
|
||||
// smtpErrors commands out of order, then a mail to a recipient the
|
||||
// domain does not know (a notice goes to the local sender)
|
||||
// pop3 log in, STAT, LIST, RETR and DELE both messages, QUIT;
|
||||
// again: log in, STAT, QUIT
|
||||
// pop3Errors unknown user, PASS before USER, wrong password, NOOP before
|
||||
// and after log-in, USER after log-in
|
||||
// Java's errors in these dialogues are measured without a network
|
||||
// (MailCases), so the stories keep to what Java does right.
|
||||
|
||||
import filius.software.clientserver.ClientBaustein;
|
||||
import filius.software.email.Email;
|
||||
import filius.software.email.EmailKonto;
|
||||
import filius.software.email.EmailServer;
|
||||
import filius.software.system.Datei;
|
||||
import filius.software.system.Dateisystem;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class EmailRun {
|
||||
static final String CLIENT = "00000000-0000-0000-0000-000000000201";
|
||||
static final String SERVER = "00000000-0000-0000-0000-000000000203";
|
||||
|
||||
/** The server's accounts before the run, in EmailServer's file format. */
|
||||
static final String ACCOUNTS = "anna;filius.de;geheim;Muster;Anna;#Bob Beispiel <bob@filius.de>$<anna@filius.de>$$$$Hallo Anna$Erste Zeile|Zweite Zeile"
|
||||
+ "#Carla <carla@andere.de>$Anna Muster <anna@filius.de>,<bob@filius.de>$<chef@filius.de>$$$Termin$Morgen um 10?\n"
|
||||
+ "bob;filius.de;passwort;Beispiel;Bob;\n";
|
||||
|
||||
static ScenarioNetwork network;
|
||||
static ClientBaustein client;
|
||||
static final List<Object[]> events = new ArrayList<>();
|
||||
|
||||
static synchronized void event(String who, String what) {
|
||||
events.add(new Object[] { System.currentTimeMillis() - network.start, who, what });
|
||||
}
|
||||
|
||||
static void sleepUntil(long milliseconds) throws InterruptedException {
|
||||
long rest = network.start + milliseconds - System.currentTimeMillis();
|
||||
if (rest > 0) Thread.sleep(rest);
|
||||
}
|
||||
|
||||
static long at;
|
||||
|
||||
static void connect(int port) throws InterruptedException {
|
||||
sleepUntil(at);
|
||||
client.setZielIPAdresse("192.168.0.30");
|
||||
client.setZielPort(port);
|
||||
event("client", "call verbinden " + port);
|
||||
client.verbinden();
|
||||
at += 2500;
|
||||
}
|
||||
|
||||
/** Each command after the answer to the one before. */
|
||||
static void send(String... messages) throws InterruptedException {
|
||||
for (String message : messages) {
|
||||
sleepUntil(at);
|
||||
event("client", "call senden " + message);
|
||||
client.senden(message);
|
||||
at += 1500;
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
network = ScenarioNetwork.read(args[0]);
|
||||
String story = args[1];
|
||||
long end = Long.parseLong(args[2]);
|
||||
|
||||
network.system(SERVER).installApp("filius.software.email.EmailServer", false);
|
||||
EmailServer server = (EmailServer) network.system(SERVER).holeSoftware("filius.software.email.EmailServer");
|
||||
Dateisystem files = network.system(SERVER).getDateisystem();
|
||||
files.speicherDatei("/mailserver", new Datei("konten.txt", "txt", ACCOUNTS));
|
||||
server.setMailDomain("filius.de");
|
||||
server.setAktiv(true);
|
||||
server.hinzuBeobachter((observable, data) -> event("server", String.valueOf(data)));
|
||||
network.system(CLIENT).installApp("filius.software.clientserver.ClientBaustein", false);
|
||||
client = (ClientBaustein) network.system(CLIENT).holeSoftware("filius.software.clientserver.ClientBaustein");
|
||||
client.hinzuBeobachter((observable, data) -> event("client", String.valueOf(data)));
|
||||
network.start();
|
||||
at = 500;
|
||||
|
||||
switch (story) {
|
||||
case "smtp":
|
||||
connect(25);
|
||||
send("HELO 192.168.0.20", "MAIL FROM: <bob@filius.de>", "RCPT TO:<anna@filius.de>", "DATA",
|
||||
"From: Bob Beispiel <bob@filius.de>\nTo: <anna@filius.de>\nSubject: Gruß\n\nHallo Anna,\nwie geht's?\r\n.\r\n", "QUIT");
|
||||
break;
|
||||
case "smtpErrors":
|
||||
connect(25);
|
||||
send("NOOP", "EHLO", "DATA", "MAIL FROM: bob@filius.de", "RCPT TO:<niemand@filius.de>", "DATA",
|
||||
"MAIL FROM:<bob@filius.de>", "RCPT TO: anna", "NOOP", "DATA",
|
||||
"From: <bob@filius.de>\nTo: <niemand@filius.de>\nSubject: Wer?\n\nIst da jemand?\n.\n", "QUIT");
|
||||
break;
|
||||
case "pop3":
|
||||
connect(110);
|
||||
send("USER anna", "PASS geheim", "STAT", "LIST", "RETR 0", "RETR 1", "DELE 0", "DELE 1", "QUIT");
|
||||
at += 1000;
|
||||
connect(110);
|
||||
send("USER anna", "PASS geheim", "STAT", "QUIT");
|
||||
break;
|
||||
case "pop3Errors":
|
||||
connect(110);
|
||||
send("NOOP", "USER niemand", "PASS x", "USER anna", "PASS falsch", "PASS geheim", "NOOP", "USER bob", "QUIT");
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("unknown story " + story);
|
||||
}
|
||||
sleepUntil(Math.max(end, at + 3000));
|
||||
|
||||
ScenarioNetwork.Json out = new ScenarioNetwork.Json();
|
||||
out.open('{');
|
||||
network.writeFrames(out);
|
||||
synchronized (EmailRun.class) {
|
||||
out.key("events").open('[');
|
||||
for (Object[] each : events) out.open('[').number((Long) each[0]).string((String) each[1]).string((String) each[2]).close(']');
|
||||
out.close(']');
|
||||
}
|
||||
out.key("accounts").open('[');
|
||||
for (EmailKonto account : server.getListeBenutzerkonten()) {
|
||||
out.open('[').string(account.getBenutzername());
|
||||
for (Email email : account.getNachrichten()) {
|
||||
out.open('[').string(email.toString()).string(String.valueOf(email.getNeu())).string(String.valueOf(email.getDelete())).close(']');
|
||||
}
|
||||
out.close(']');
|
||||
}
|
||||
out.close(']');
|
||||
out.key("file").string(files.holeDatei("/mailserver/konten.txt").getDateiInhalt());
|
||||
out.close('}');
|
||||
network.stop();
|
||||
ScenarioNetwork.write(args[3], out);
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// Feeds FILIUS' SMTP and POP3 workers (SMTPMitarbeiter, POP3Mitarbeiter of
|
||||
// an EmailServer) with messages from a client, one after the other, without
|
||||
// a network: the worker's socket only writes down what it is to send. After
|
||||
// each message it writes what the worker sent, the lines the e-mail server
|
||||
// wrote into its window, whether the socket was closed, and the accounts
|
||||
// (each message as Email.toString, new, marked for deletion). The reference
|
||||
// for the core's e-mail server (packages/core/src/runtime/apps/emailServer.ts).
|
||||
// Each case starts from a new server with the accounts of EmailRun (domain
|
||||
// filius.de, its address 192.168.0.30, the client 192.168.0.20). The cases
|
||||
// file is ASCII, with Java-style unicode escapes (backslash, u, four hex
|
||||
// digits) for anything else, a message per line:
|
||||
//
|
||||
// case <name> smtp|pop3
|
||||
// <message>
|
||||
//
|
||||
// Usage: java -cp filius.jar:lib/*:classes MailCases <de|en> <cases txt> <output json>
|
||||
|
||||
import filius.hardware.knoten.Rechner;
|
||||
import filius.rahmenprogramm.Information;
|
||||
import filius.software.email.Email;
|
||||
import filius.software.email.EmailKonto;
|
||||
import filius.software.email.EmailServer;
|
||||
import filius.software.email.POP3Mitarbeiter;
|
||||
import filius.software.email.POP3Server;
|
||||
import filius.software.email.SMTPMitarbeiter;
|
||||
import filius.software.email.SMTPServer;
|
||||
import filius.software.system.Datei;
|
||||
import filius.software.system.InternetKnotenBetriebssystem;
|
||||
import filius.software.transportschicht.TCPSocket;
|
||||
|
||||
import java.io.FileOutputStream;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
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.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
public class MailCases {
|
||||
|
||||
/** A socket that only writes down what is sent, and whether it was closed. */
|
||||
static class Recorder extends TCPSocket {
|
||||
final List<String> sent = new ArrayList<>();
|
||||
boolean closed = false;
|
||||
|
||||
Recorder(InternetKnotenBetriebssystem system) throws Exception {
|
||||
super(system, "192.168.0.20", 50000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void senden(String nachricht) {
|
||||
sent.add(nachricht);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void schliessen() {
|
||||
closed = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beenden() {
|
||||
closed = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean istVerbunden() {
|
||||
return !closed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String holeZielIPAdresse() {
|
||||
return "192.168.0.20";
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
// Before any class reads the message bundle (I18n.messages is read once).
|
||||
Information.getInformation().setLocale(args[0].equals("en") ? new Locale("en", "GB") : Locale.GERMANY);
|
||||
List<Object> cases = new ArrayList<>();
|
||||
List<Object> steps = null;
|
||||
EmailServer server = null;
|
||||
Recorder socket = null;
|
||||
Object worker = null;
|
||||
Method process = null;
|
||||
List<String> lines = new ArrayList<>();
|
||||
for (String line : Files.readAllLines(Path.of(args[1]), StandardCharsets.US_ASCII)) {
|
||||
if (line.isEmpty() || line.startsWith("#")) continue;
|
||||
if (line.startsWith("case ")) {
|
||||
String[] words = line.split(" ");
|
||||
Map<String, Object> current = new LinkedHashMap<>();
|
||||
steps = new ArrayList<>();
|
||||
current.put("name", words[1]);
|
||||
current.put("protocol", words[2]);
|
||||
current.put("steps", steps);
|
||||
cases.add(current);
|
||||
Rechner host = new Rechner();
|
||||
InternetKnotenBetriebssystem system = (InternetKnotenBetriebssystem) host.getSystemSoftware();
|
||||
system.setzeIPAdresse("192.168.0.30");
|
||||
system.installApp("filius.software.email.EmailServer", false);
|
||||
server = (EmailServer) system.holeSoftware("filius.software.email.EmailServer");
|
||||
system.getDateisystem().speicherDatei("/mailserver", new Datei("konten.txt", "txt", EmailRun.ACCOUNTS));
|
||||
server.setMailDomain("filius.de");
|
||||
server.kontenLaden();
|
||||
lines.clear();
|
||||
List<String> window = lines;
|
||||
server.hinzuBeobachter((observable, data) -> {
|
||||
if (data != null) window.add(String.valueOf(data));
|
||||
});
|
||||
socket = new Recorder(system);
|
||||
if (words[2].equals("smtp")) {
|
||||
worker = new SMTPMitarbeiter(socket, new SMTPServer(25, server));
|
||||
process = SMTPMitarbeiter.class.getDeclaredMethod("verarbeiteNachricht", String.class);
|
||||
} else {
|
||||
worker = new POP3Mitarbeiter(socket, new POP3Server(110, server));
|
||||
process = POP3Mitarbeiter.class.getDeclaredMethod("verarbeiteNachricht", String.class);
|
||||
}
|
||||
process.setAccessible(true);
|
||||
// The greeting the worker sends when it begins.
|
||||
steps.add(step(null, socket, lines, server, null));
|
||||
continue;
|
||||
}
|
||||
String message = IpCheckCases.unescape(line);
|
||||
String thrown = null;
|
||||
try {
|
||||
process.invoke(worker, message);
|
||||
} catch (InvocationTargetException error) {
|
||||
thrown = error.getCause().getClass().getSimpleName();
|
||||
}
|
||||
steps.add(step(message, socket, lines, server, thrown));
|
||||
}
|
||||
try (FileOutputStream out = new FileOutputStream(args[2])) {
|
||||
out.write((FlsOracle.Json.write(cases, "") + "\n").getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
System.out.println("mail cases (" + args[0] + "): " + cases.size());
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
static Map<String, Object> step(String message, Recorder socket, List<String> lines, EmailServer server, String thrown) {
|
||||
Map<String, Object> step = new LinkedHashMap<>();
|
||||
step.put("message", message);
|
||||
step.put("sent", new ArrayList<>(socket.sent));
|
||||
step.put("lines", new ArrayList<>(lines));
|
||||
step.put("closed", socket.closed);
|
||||
step.put("thrown", thrown);
|
||||
List<Object> accounts = new ArrayList<>();
|
||||
for (EmailKonto account : server.getListeBenutzerkonten()) {
|
||||
List<Object> messages = new ArrayList<>();
|
||||
for (Email email : account.getNachrichten()) messages.add(Arrays.asList(email.toString(), email.getNeu(), email.getDelete()));
|
||||
accounts.add(Arrays.asList(account.getBenutzername(), messages));
|
||||
}
|
||||
step.put("accounts", accounts);
|
||||
socket.sent.clear();
|
||||
lines.clear();
|
||||
return step;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user