feat(editor): F4.6e the e-mail program and e-mail server on a computer's desktop

The oracle MailWindowRun uses Java's e-mail windows as a person does and
answers their dialogs. The core sets and saves the program's account,
deletes a mail, checks a form as Java's window does, and gives the server
its domain, new and removed accounts and the window's own line; the room
sends both programs' views and takes their actions; the editor has both
windows. Better than Java: a name field for a new account, a user name the
server has is refused, deleting hits the folder shown, a new domain goes
into the accounts file at once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 16:44:36 +00:00
co-authored by Claude Opus 5.5
parent aa54e5400e
commit 11f13bca84
33 changed files with 2841 additions and 52 deletions
+84 -2
View File
@@ -19,8 +19,9 @@
// Java's do (TerminalWindow, BrowserWindow).
//
// A program's window may show more than its lines (plan §7.3): the browser's
// page, the web server's button and table go to the clients as a view,
// whole, when they change.
// page, the web server's button and table, the e-mail program's account and
// folders, the e-mail server's domain and accounts go to the clients as a
// view, whole, when they change.
//
// While the simulation runs, the document's programs are watched: a program
// installed or removed on the desktop (Java's software installation) is
@@ -29,6 +30,7 @@
import * as Y from 'yjs';
import {
accountOfForm,
DEFAULT_SPEED_PERCENT,
NetworkRuntimeEngine,
captureRows,
@@ -53,6 +55,8 @@ import {
type NodeSettings,
type SimMode,
type Topic,
type EmailClientView,
type EmailServerView,
type TraceFrame,
type WebServerView,
} from '@mdg-filius/protocol';
@@ -324,6 +328,68 @@ export class SimulationRoom {
return this.actInBrowser(input);
case 'webServer':
return this.actInWebServer(input);
case 'emailClient':
return this.actInEmailClient(input);
case 'emailServer':
return this.actInEmailServer(input);
default:
return 'unknownAction';
}
}
/** The account dialog (setAccount), the compose window (send), fetching and deleting a mail of a folder. */
private actInEmailClient(input: Extract<ClientMessage, { kind: 'input' }>): ErrorCode | undefined {
const client = this.engine.emailClient(input.node);
if (client === undefined) return 'unknownApp';
const { args } = input;
const text = (...names: string[]) => names.every((name) => typeof args[name] === 'string');
switch (input.action) {
case 'setAccount': {
if (!text('name', 'email', 'pop3Host', 'pop3Port', 'smtpHost', 'smtpPort', 'username', 'password')) return 'badArgs';
const account = accountOfForm(args as unknown as Parameters<typeof accountOfForm>[0]);
if (account === undefined) return 'badArgs';
client.setAccount(account);
return undefined;
}
case 'send':
if (!text('to', 'cc', 'bcc', 'subject', 'text')) return 'badArgs';
return client.send(args as unknown as Parameters<typeof client.send>[0]) === undefined ? undefined : 'badArgs';
case 'fetch':
// Without an account Java's button does nothing.
client.fetch();
return undefined;
case 'removeMail':
if ((args.folder !== 'inbox' && args.folder !== 'sent') || !Number.isInteger(args.index)) return 'badArgs';
client.removeMail(args.folder, args.index as number);
return undefined;
default:
return 'unknownAction';
}
}
/** The start button, the domain (only while the server is off, as Java's field), a new account and the removal of one. */
private actInEmailServer(input: Extract<ClientMessage, { kind: 'input' }>): ErrorCode | undefined {
const server = this.engine.emailServer(input.node);
if (server === undefined) return 'unknownApp';
const { args } = input;
switch (input.action) {
case 'setActive':
if (typeof args.active !== 'boolean') return 'badArgs';
server.setActive(args.active, true);
return undefined;
case 'setDomain':
if (typeof args.domain !== 'string') return 'badArgs';
return !server.isActive && server.setDomain(args.domain) ? undefined : 'badArgs';
case 'addAccount': {
const { username, password, firstName, lastName } = args;
if (typeof username !== 'string' || typeof password !== 'string' || typeof firstName !== 'string' || typeof lastName !== 'string') return 'badArgs';
// The window's checks: a user name and a password, the name without white space (musterKeineLeerzeichen).
if (username === '' || password === '' || /[ \t\n\v\f\r]/.test(username)) return 'badArgs';
return server.addAccount({ username, password, firstName, lastName }) ? undefined : 'badArgs';
}
case 'removeAccount':
if (typeof args.username !== 'string') return 'badArgs';
return server.removeAccount(args.username) ? undefined : 'badArgs';
default:
return 'unknownAction';
}
@@ -401,6 +467,22 @@ export class SimulationRoom {
const view: WebServerView = { active: server.isActive, useVirtualHosts: server.useVirtualHosts, virtualHosts: server.virtualHostTable.map(([host, folder]) => [host, folder] as const) };
return view;
}
case 'emailClient': {
const client = this.engine.emailClient(node);
if (client === undefined) return null;
const view: EmailClientView = { account: client.account, inbox: client.inbox.map((entry) => entry.mail), sent: client.sent.map((entry) => entry.mail) };
return view;
}
case 'emailServer': {
const server = this.engine.emailServer(node);
if (server === undefined) return null;
const view: EmailServerView = {
active: server.isActive,
domain: server.domain,
accounts: server.state.accounts.map((account) => ({ username: account.username, mails: account.mailbox.length })),
};
return view;
}
default:
return undefined;
}