feat(core): the e-mail program after Java, measured against Java

The program sends to its account's SMTP server, one connection for each
recipient, and fetches its mails by POP3; the server now sends mails on
to other domains by MX and returns notices to their senders. The
program's folders are in the snapshot and go back into the project.
EmailClientRun plays six stories with two servers; the mail oracles run
in the language's locale, so Java's notice dates match the core's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 19:38:44 +00:00
co-authored by Claude Opus 5.5
parent 015635926f
commit 058675a148
30 changed files with 58319 additions and 1724 deletions
+39
View File
@@ -116,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 { EmailClient, type EmailClientFolders } from './apps/emailClient.js';
import { EmailServer } from './apps/emailServer.js';
import { firewallLogPage, forwardingTablePage, ripTablePage, routerIndexPage } from './apps/routerPages.js';
import { applyFirewallSettings, firewallSettingsPage } from './apps/firewallSettings.js';
@@ -913,6 +914,7 @@ export class NetworkRuntimeEngine {
private readonly webServersByNodeId = new Map<Id, WebServer>();
private readonly webBrowsersByNodeId = new Map<Id, WebBrowser>();
private readonly emailServersByNodeId = new Map<Id, EmailServer>();
private readonly emailClientsByNodeId = new Map<Id, EmailClient>();
/** 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. */
@@ -1043,6 +1045,7 @@ export class NetworkRuntimeEngine {
this.webServersByNodeId.clear();
this.webBrowsersByNodeId.clear();
this.emailServersByNodeId.clear();
this.emailClientsByNodeId.clear();
this.arpLookups = [];
this.protocolThreads.clear();
this.dnsQueriesBySocketId.clear();
@@ -1133,6 +1136,20 @@ export class NetworkRuntimeEngine {
return this.emailServersByNodeId.get(nodeId);
}
/** The node's e-mail program while the simulation runs, if it has one installed. */
emailClient(nodeId: Id): EmailClient | undefined {
return this.emailClientsByNodeId.get(nodeId);
}
/** An e-mail program's folders as a run changed them, into the snapshot (and so into the project saved from it). */
private setEmailClientFolders(nodeId: Id, folders: EmailClientFolders): void {
const clients = new Map(this.stateValue.topologySnapshot.emailClientsByNodeId);
const client = clients.get(nodeId);
if (client === undefined) return;
clients.set(nodeId, { ...client, ...folders });
this.stateValue.topologySnapshot = withSnapshot(this.stateValue.topologySnapshot, { emailClientsByNodeId: clients });
}
/** 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);
@@ -1153,6 +1170,7 @@ export class NetworkRuntimeEngine {
this.webServersByNodeId.clear();
this.webBrowsersByNodeId.clear();
this.emailServersByNodeId.clear();
this.emailClientsByNodeId.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));
@@ -1178,6 +1196,11 @@ export class NetworkRuntimeEngine {
),
);
}
const emailClient = snapshot.emailClientsByNodeId.get(node.id);
if (emailClient !== undefined) {
const nodeId = node.id;
this.emailClientsByNodeId.set(nodeId, new EmailClient(this, nodeId, emailClient, (folders) => this.setEmailClientFolders(nodeId, folders)));
}
if ((node.kind === 'router' || node.kind === 'gateway') && !this.swiftRouters) this.startRouterWebServer(node.id, node.kind);
}
}
@@ -1961,6 +1984,22 @@ export class NetworkRuntimeEngine {
return { type: 'resolved', address: exchange === undefined ? undefined : findDnsRecords(exchange.data, 'A', resolution.records)[0]?.data };
}
/** Resolver.holeIPAdresseMailServer in a program's thread (the e-mail server sending a mail on). */
*resolveDnsMailServerSteps(nodeId: Id, domain: string): Steps<DnsAddressResolution> {
const domainName = domain.endsWith('.') ? domain : `${domain}.`;
let lookup: DnsLookup | undefined;
const server = this.stateValue.topologySnapshot.deviceConfigurations.get(nodeId)?.dnsServer;
this.beginDnsResolution(nodeId, domainName, 'MX', server, (result) => {
lookup = result;
});
yield* waitUntil(() => lookup !== undefined || this.stateValue.phase !== 'running');
const result = lookup as DnsLookup | undefined;
if (result === undefined || result.type === 'timedOut') return { type: 'timedOut' };
const records = [...result.answers, ...result.authority];
const exchange = findDnsRecords(domainName, 'MX', records)[0];
return { type: 'resolved', address: exchange === undefined ? undefined : findDnsRecords(exchange.data, 'A', records)[0]?.data };
}
/**
* DNSQueryAgent.query: one question from a fresh socket; `done` with the
* first datagram back, or undefined after five round trips.