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
+41
View File
@@ -60,6 +60,7 @@ import { forwardsIPv4ByDefault, isPcClassEndpoint, stableGeneratedMacAddress } f
import type {
DeviceConfiguration,
DhcpServerConfiguration,
EmailServerAccount,
FirewallConfiguration,
GatewayPortForwardingRow,
FirewallRule,
@@ -115,6 +116,7 @@ import { RuntimeSpeed } from './speed.js';
import { ApplicationThreads, waitUntil, type Steps, type Wait } from './threads.js';
import { EchoServer, SimpleClient } from './apps/echo.js';
import { WebBrowser, WebServer, type WebServerPage } from './apps/web.js';
import { EmailServer } from './apps/emailServer.js';
import { firewallLogPage, forwardingTablePage, ripTablePage, routerIndexPage } from './apps/routerPages.js';
import { applyFirewallSettings, firewallSettingsPage } from './apps/firewallSettings.js';
import { VirtualFileSystem } from '../model/virtualFileSystem.js';
@@ -910,6 +912,9 @@ export class NetworkRuntimeEngine {
private readonly simpleClientsByNodeId = new Map<Id, SimpleClient>();
private readonly webServersByNodeId = new Map<Id, WebServer>();
private readonly webBrowsersByNodeId = new Map<Id, WebBrowser>();
private readonly emailServersByNodeId = new Map<Id, EmailServer>();
/** The time of day a program writes into a mail (Java's new Date()). */
private readonly wallClock: () => number;
/** Each node's firewall log; it outlives a stop and a new start, as Java's log page does. */
private readonly firewallLogsByNodeId = new Map<Id, FirewallLogEntry[]>();
/** Accepted connections a program has taken (acceptTcpSteps). */
@@ -929,6 +934,9 @@ export class NetworkRuntimeEngine {
* web administration answers without a socket), likewise.
*
* `captureRetention`: how many frames each card's packet capture keeps.
*
* `wallClock`: the time of day in milliseconds, which a program writes
* into a mail (the e-mail server's notice); the host's clock by default.
*/
constructor(
seed: bigint | number = DEFAULT_SEED,
@@ -939,8 +947,10 @@ export class NetworkRuntimeEngine {
readonly swiftSends?: boolean;
readonly swiftRouters?: boolean;
readonly captureRetention?: CaptureRetentionPolicy;
readonly wallClock?: () => number;
} = {},
) {
this.wallClock = options.wallClock ?? Date.now;
this.packetCapture = new PacketCapture(options.captureRetention ?? PRODUCTION_CAPTURE_RETENTION);
this.cablesCarryOneFrameAtATime = options.swiftCables !== true;
this.swiftArp = options.swiftArp === true;
@@ -1032,6 +1042,7 @@ export class NetworkRuntimeEngine {
this.simpleClientsByNodeId.clear();
this.webServersByNodeId.clear();
this.webBrowsersByNodeId.clear();
this.emailServersByNodeId.clear();
this.arpLookups = [];
this.protocolThreads.clear();
this.dnsQueriesBySocketId.clear();
@@ -1117,6 +1128,20 @@ export class NetworkRuntimeEngine {
return this.webBrowsersByNodeId.get(nodeId);
}
/** The node's e-mail server while the simulation runs, if it has one installed. */
emailServer(nodeId: Id): EmailServer | undefined {
return this.emailServersByNodeId.get(nodeId);
}
/** An e-mail server's accounts as a run changed them, into the snapshot (and so into the project saved from it). */
private setEmailServerAccounts(nodeId: Id, accounts: readonly EmailServerAccount[]): void {
const servers = new Map(this.stateValue.topologySnapshot.emailServersByNodeId);
const server = servers.get(nodeId);
if (server === undefined) return;
servers.set(nodeId, { ...server, accounts });
this.stateValue.topologySnapshot = withSnapshot(this.stateValue.topologySnapshot, { emailServersByNodeId: servers });
}
/**
* Each node's programs start with the simulation (Betriebssystem.starten),
* from the snapshot; what they show starts empty.
@@ -1127,6 +1152,7 @@ export class NetworkRuntimeEngine {
this.simpleClientsByNodeId.clear();
this.webServersByNodeId.clear();
this.webBrowsersByNodeId.clear();
this.emailServersByNodeId.clear();
for (const node of snapshot.nodes) {
const echoServer = snapshot.echoServersByNodeId.get(node.id);
if (echoServer !== undefined) this.echoServersByNodeId.set(node.id, new EchoServer(this, node.id, echoServer));
@@ -1137,6 +1163,21 @@ export class NetworkRuntimeEngine {
this.webServersByNodeId.set(node.id, new WebServer(this, node.id, webServer, snapshot.fileSystemsByNodeId.get(node.id)));
}
if (snapshot.webBrowserNodeIds.has(node.id)) this.webBrowsersByNodeId.set(node.id, new WebBrowser(this, node.id));
const emailServer = snapshot.emailServersByNodeId.get(node.id);
if (emailServer !== undefined) {
const nodeId = node.id;
this.emailServersByNodeId.set(
nodeId,
new EmailServer(
this,
nodeId,
emailServer,
(accounts) => this.setEmailServerAccounts(nodeId, accounts),
() => this.networkInterfaces(nodeId)[0]?.ipAddress ?? 'null',
this.wallClock,
),
);
}
if ((node.kind === 'router' || node.kind === 'gateway') && !this.swiftRouters) this.startRouterWebServer(node.id, node.kind);
}
}