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:
2026-10-01 18:58:26 +00:00
co-authored by Claude Opus 5.5
parent 8438b42018
commit 015635926f
26 changed files with 45188 additions and 14 deletions
+148
View File
@@ -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);
}
}
+161
View File
@@ -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;
}
}