Files
julian.kiedaischandClaude Opus 5.5 11f13bca84 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>
2026-10-03 16:44:36 +00:00

254 lines
14 KiB
TypeScript

// The e-mail program's and the e-mail server's windows against Java's
// (tools/java-oracle MailWindowRun) on mail-lan's network: the same server
// switched, its domain set, accounts made and removed; the same account set
// in the program, mails written, sent, fetched, answered and deleted — the
// same rows, previews, files and lines after each. What Java's window checks
// before it acts (a form's addresses, a user name), the editor's panel checks
// with the core's functions; here they are compared with Java's dialogs.
// Named where the windows are better than Java's:
// - a user name the server has already is refused (Java's window says it
// made it),
// - deleting deletes the mail chosen in the folder shown (Java's deleted a
// mail of the other folder, where one was chosen before),
// - a new domain is written into the accounts file at once (Java writes it
// with the next change of the accounts).
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { accountOfForm, mailFormProblem, NetworkRuntimeEngine, splitName, VirtualFileSystem, withSnapshot, type AccountForm, type MailForm } from '@mdg-filius/core';
import { inboxRow, replyForm, sentRow } from '@mdg-filius/protocol';
import { snapshotOf, type Scenario } from '../../core/test/scenario.js';
interface JavaStep {
readonly step: string;
readonly [field: string]: unknown;
}
const here = dirname(fileURLToPath(import.meta.url));
const java = JSON.parse(readFileSync(join(here, 'mailWindow.java.json'), 'utf8')) as { readonly steps: readonly JavaStep[] };
const scenario = JSON.parse(readFileSync(join(here, '../../core/test/scenarios/mail-lan.scenario.json'), 'utf8')) as Scenario;
const CLIENT = '00000000-0000-0000-0000-000000000201';
const SERVER = '00000000-0000-0000-0000-000000000203';
const step = (name: string) => java.steps.find((each) => each.step === name)!;
/** Java's window's placeholders for a new account's names (emailserver_msg21, msg20). */
const PLACEHOLDER = { firstName: 'Vorname', lastName: 'Nachname' };
function setUp() {
const engine = new NetworkRuntimeEngine(scenario.seed);
const serverFiles = new VirtualFileSystem();
const clientFiles = new VirtualFileSystem();
const snapshot = withSnapshot(snapshotOf(scenario), {
emailServersByNodeId: new Map([[SERVER, { domain: 'filius.de', accounts: [], isActive: false }]]),
emailClientsByNodeId: new Map([[CLIENT, { account: null, inbox: [], sent: [], drafts: [], addressBook: [], nextMessageID: 1 }]]),
fileSystemsByNodeId: new Map([
[SERVER, serverFiles],
[CLIENT, clientFiles],
]),
});
engine.handle({ type: 'start', snapshot, seed: scenario.seed, initialTimeMilliseconds: 0 });
const run = (milliseconds: number) => engine.handle({ type: 'advance', toMilliseconds: engine.state.currentTimeMilliseconds + milliseconds });
return { engine, server: engine.emailServer(SERVER)!, client: engine.emailClient(CLIENT)!, serverFiles, clientFiles, run };
}
/** The server's window as Java's shows it: button, domain field, rows of address and number of mails. */
function serverWindow(context: ReturnType<typeof setUp>) {
const { server } = context;
return {
button: server.isActive ? 'Anhalten' : 'Starten',
domain: server.domain,
domainEnabled: !server.isActive,
accounts: server.state.accounts.map((account) => [`${account.username}@${server.domain}`, String(account.mailbox.length)]),
accountsFile: context.serverFiles.textFile('/mailserver/konten.txt'),
};
}
const javaServerWindow = (each: JavaStep) => ({ button: each.button, domain: each.domain, domainEnabled: each.domainEnabled, accounts: each.accounts, accountsFile: each.accountsFile });
/** What Java's server window checks before it makes an account, as the editor's panel checks it (emailserver_msg25, msg24). */
function serverFormProblem(username: string, password: string): 'missing' | 'whiteSpace' | undefined {
if (username === '' || password === '') return 'missing';
return /[ \t\n\v\f\r]/.test(username) ? 'whiteSpace' : undefined;
}
const ACCOUNT_FORM: AccountForm = {
name: 'Anna Maria Muster',
email: 'anna@schule.de',
pop3Host: '192.168.0.30',
pop3Port: '110',
smtpHost: '192.168.0.30',
smtpPort: '25',
username: 'anna',
password: 'geheim',
};
const MAIL_FORMS: Record<string, MailForm> = {
invalid: { to: 'bob@schule.de, kein', cc: '', bcc: '', subject: 'Falsch', text: 'x' },
empty: { to: '', cc: '', bcc: '', subject: 'Leer', text: 'x' },
hello: { to: 'bob@schule.de', cc: '', bcc: '', subject: 'Hallo', text: 'Zeile 1\nZeile 2' },
self: { to: 'anna@schule.de', cc: '', bcc: '', subject: 'Selbst', text: 'Erste Zeile\nZweite Zeile' },
};
describe("the e-mail windows, against Java's", () => {
it("switch the server, set its domain and make its accounts as Java's window does", () => {
const context = setUp();
const { server } = context;
expect(serverWindow(context)).toEqual(javaServerWindow(step('server:open')));
expect(server.setDomain('ungültig domain!')).toBe(false);
expect(serverWindow(context)).toEqual(javaServerWindow(step('server:domain ungültig domain!')));
expect(server.setDomain('schule.de')).toBe(true);
expect(serverWindow(context)).toEqual({ ...javaServerWindow(step('server:domain schule.de')), accountsFile: '' });
server.setActive(true, true);
expect(serverWindow(context)).toEqual(javaServerWindow(step('server:start')));
expect(server.log.all.map((line) => line.kind)).toEqual(['switched']);
const dialogs: Record<string, string> = {
missing: 'Geben Sie einen Benutzernamen und ein Passwort ein!',
whiteSpace: 'Der Benutzername darf keine Leerzeichen enthalten!',
};
for (const [username, password] of [['anna', 'geheim'], ['', 'x'], ['carl os', 'x'], ['ANNA', 'anders'], ['bob', ''], ['bob', 'pw']] as const) {
const recorded = step(`server:add ${username}/${password}`);
const problem = serverFormProblem(username, password);
if (problem !== undefined) {
expect(recorded.dialogs, username).toEqual([dialogs[problem]]);
} else {
const made = server.addAccount({ username, password, ...PLACEHOLDER });
// Java's window says it made "ANNA"; the server has "anna" already and takes nothing.
expect(recorded.dialogs).toEqual([`Das Benutzerkonto ${username} wurde angelegt.`]);
expect(made, username).toBe(username !== 'ANNA');
}
expect(serverWindow(context), username).toEqual(javaServerWindow(recorded));
}
});
it("set the program's account, write and send its mails, fetch, answer and delete them as Java's window does", () => {
const context = setUp();
const { server, client, clientFiles, run } = context;
server.setDomain('schule.de');
server.setActive(true, true);
server.addAccount({ username: 'anna', password: 'geheim', ...PLACEHOLDER });
server.addAccount({ username: 'bob', password: 'pw', ...PLACEHOLDER });
expect(client.account).toBeNull();
for (const [username, port] of [['an na', '110'], ['anna', '110x'], ['anna', '110']] as const) {
const recorded = step(`client:account ${username} ${port}`);
const account = accountOfForm({ ...ACCOUNT_FORM, username, pop3Port: port });
if (account === undefined) expect(recorded.dialogs).toEqual(['Die Angaben sind entweder fehlerhaft oder unvollständig!']);
else client.setAccount(account);
expect(clientFiles.contains('/konten.txt') ? clientFiles.textFile('/konten.txt') : null, username + port).toBe(recorded.accountFile);
}
// Opened again, the dialog shows the account: first and last name with a space between.
const shown = client.account!;
expect([`${shown.firstName} ${shown.lastName}`, shown.email, shown.pop3Host, String(shown.pop3Port), shown.smtpHost, String(shown.smtpPort), shown.username, shown.password]).toEqual(
step('client:account shown').fields,
);
const problems: Record<string, string> = {
invalidAddress: 'Mindestens eine eingegebene E-Mail-Adresse ist nicht korrekt!',
noRecipient: 'Sie müssen mindestens eine Empfänger-Adresse eingeben!',
};
for (const form of Object.values(MAIL_FORMS)) {
const name = form.subject;
const recorded = step(`client:send ${name}`);
const problem = mailFormProblem(form, true);
if (problem !== undefined) {
expect(recorded.dialogs, name).toEqual([problems[problem]]);
// The compose window stays open with what was typed.
expect([recorded.composing, recorded.composeFields, recorded.composeText, recorded.composeSender]).toEqual([true, [form.to, form.cc, form.bcc, form.subject], form.text, 'anna']);
} else {
expect(client.send(form)).toBeUndefined();
run(20_000);
// Sent: the compose window closes, the window shows the sent mails.
expect([recorded.composing, recorded.tab]).toEqual([false, 1]);
}
expect(client.sent.map((entry) => sentRow(entry.mail)), name).toEqual(recorded.sent);
}
expect(serverWindow(context)).toEqual(javaServerWindow(step('server:after mails')));
client.fetch();
run(20_000);
const fetched = step('client:fetch');
expect([client.inbox.map((entry) => inboxRow(entry.mail)), fetched.tab]).toEqual([fetched.inbox, 0]);
expect(client.inbox[0]!.mail.text).toBe(step('client:choose inbox 0').preview);
const answer = replyForm(client.inbox[0]!.mail, { re: 'RE:', wrote: 'schrieb:' });
const answered = step('client:answer');
expect([[answer.to, '', '', answer.subject], answer.text, 'anna']).toEqual([answered.composeFields, answered.composeText, answered.composeSender]);
expect(client.sent[0]!.mail.text).toBe(step('client:choose sent 0').preview);
expect(serverWindow(context)).toEqual(javaServerWindow(step('server:after fetch')));
// The mail chosen in the inbox is deleted (Java deleted the sent mail chosen before).
client.removeMail('inbox', 0);
const deleted = step('client:delete inbox 0');
expect(client.inbox).toEqual([]);
expect(deleted.inbox).toEqual(fetched.inbox);
expect(client.sent.map((entry) => sentRow(entry.mail))).toEqual((fetched.sent as unknown[]).slice());
expect(deleted.sent).toEqual((fetched.sent as unknown[]).slice(1));
// A row it has not: nothing.
client.removeMail('sent', 5);
client.removeMail('sent', -1);
expect(client.sent).toHaveLength(2);
});
it("remove an account, stop the server and set its domain while it is off, as Java's window does", () => {
const context = setUp();
const { server } = context;
server.setDomain('schule.de');
server.setActive(true, true);
server.addAccount({ username: 'anna', password: 'geheim', ...PLACEHOLDER });
server.addAccount({ username: 'bob', password: 'pw', ...PLACEHOLDER });
expect(step('server:delete row 1').dialogs).toEqual(["Wollen Sie das Konto 'bob' wirklich entfernen?"]);
expect(server.removeAccount('bob')).toBe(true);
expect(server.removeAccount('bob')).toBe(false);
expect(server.removeAccount('ANNA')).toBe(false);
expect(serverWindow(context)).toEqual(javaServerWindow(step('server:delete row 1')));
server.setActive(false, true);
expect(serverWindow(context)).toEqual(javaServerWindow(step('server:stop')));
expect(server.log.all.map((line) => line.kind)).toEqual(['switched', 'switched']);
expect(server.log.all.map((line) => line.kind === 'switched' && line.active)).toEqual([true, false]);
expect(server.setDomain('neu.de')).toBe(true);
// The new domain goes into the file at once (Java's keeps the old one until the accounts change).
const recorded = step('server:domain neu.de');
expect(serverWindow(context)).toEqual({ ...javaServerWindow(recorded), accountsFile: 'anna;neu.de;geheim;Nachname;Vorname;\n' });
expect(recorded.accountsFile).toBe('anna;schule.de;geheim;Nachname;Vorname;\n');
expect(server.setDomain('')).toBe(false);
expect(server.domain).toBe('neu.de');
});
it("split a name as Java's account dialog does", () => {
expect(splitName('Anna Maria Muster')).toEqual({ firstName: 'Anna Maria', lastName: 'Muster' });
expect(splitName('Anna')).toEqual({ firstName: 'Anna', lastName: '' });
expect(splitName(' Anna ')).toEqual({ firstName: '', lastName: 'Anna' });
expect(splitName('Anna ')).toEqual({ firstName: 'Anna', lastName: '' });
expect(splitName(' ')).toEqual({ firstName: '', lastName: '' });
expect(accountOfForm({ ...ACCOUNT_FORM, username: '', smtpPort: '65535', pop3Port: '1' })).toMatchObject({ username: '', smtpPort: 65535, pop3Port: 1 });
for (const port of ['0', '65536', '', '080', ' 25']) expect(accountOfForm({ ...ACCOUNT_FORM, smtpPort: port }), port).toBeUndefined();
expect(accountOfForm({ ...ACCOUNT_FORM, username: 'a.b_c-d9' })).toBeDefined();
expect(accountOfForm({ ...ACCOUNT_FORM, username: 'anna@x' })).toBeUndefined();
});
it('deletes a sent mail, sends with the account a mail was written with, and keeps a new domain in the snapshot', () => {
const context = setUp();
const { engine, server, client, run } = context;
server.setDomain('schule.de');
expect(engine.state.topologySnapshot.emailServersByNodeId.get(SERVER)!.domain).toBe('schule.de');
server.setActive(true, true);
server.addAccount({ username: 'anna', password: 'geheim', ...PLACEHOLDER });
server.addAccount({ username: 'bob', password: 'pw', ...PLACEHOLDER });
client.setAccount(accountOfForm(ACCOUNT_FORM)!);
expect(client.send({ ...MAIL_FORMS.hello!, to: 'bob@schule.de, anna@schule.de' })).toBeUndefined();
// Changed before the mail goes to its second recipient: it still goes with the account it was written with.
client.setAccount(accountOfForm({ ...ACCOUNT_FORM, smtpHost: 'niemand.de' })!);
run(40_000);
expect(client.sent.map((entry) => entry.mail.subject)).toEqual(['Hallo']);
expect(server.state.accounts.map((account) => account.mailbox.length)).toEqual([1, 1]);
client.setAccount(accountOfForm(ACCOUNT_FORM)!);
expect(client.send(MAIL_FORMS.self!)).toBeUndefined();
run(20_000);
client.removeMail('sent', 0);
expect(client.sent.map((entry) => entry.mail.subject)).toEqual(['Selbst']);
expect(engine.state.topologySnapshot.emailClientsByNodeId.get(CLIENT)!.sent.map((message) => message.subject)).toEqual(['Selbst']);
});
});