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:
@@ -135,7 +135,67 @@ const FROM_JAVA: Readonly<Record<string, string>> = {
|
||||
'server.serverSocketFailed': 'sw_serveranwendung_msg6',
|
||||
'server.sendFailed.notConnected': 'sw_tcpsocket_msg6',
|
||||
'server.sendFailed.connectionLost': 'sw_tcpsocket_msg7',
|
||||
'server.sendFailed.timedOut': 'sw_tcpsocket_msg8',
|
||||
'server.sendFailed.timedOut': 'sw_tcpsocket_msg8', 'emailClient.sender': 'emailanwendung_msg1',
|
||||
'emailClient.subject': 'emailanwendung_msg2',
|
||||
'emailClient.recipient': 'emailanwendung_msg3',
|
||||
'emailClient.inbox': 'emailanwendung_msg5',
|
||||
'emailClient.sent': 'emailanwendung_msg6',
|
||||
'emailClient.fetch': 'emailanwendung_msg7',
|
||||
'emailClient.compose': 'emailanwendung_msg8',
|
||||
'emailClient.answer': 'emailanwendung_msg9',
|
||||
'emailClient.deleteHint': 'emailanwendung_msg10',
|
||||
'emailClient.accountHint': 'emailanwendung_msg11',
|
||||
'emailClient.composeTitle': 'emailanwendung_msg12',
|
||||
'emailClient.send': 'emailanwendung_msg13',
|
||||
'emailClient.toLabel': 'emailanwendung_msg14',
|
||||
'emailClient.ccLabel': 'emailanwendung_msg15',
|
||||
'emailClient.bccLabel': 'emailanwendung_msg16',
|
||||
'emailClient.subjectLabel': 'emailanwendung_msg17',
|
||||
'emailClient.re': 'emailanwendung_msg18',
|
||||
'emailClient.wrote': 'emailanwendung_msg19',
|
||||
'emailClient.invalidAddress': 'emailanwendung_msg20',
|
||||
'emailClient.noRecipient': 'emailanwendung_msg21',
|
||||
'emailClient.accountTitle': 'emailanwendung_msg25',
|
||||
'emailClient.email': 'emailanwendung_msg26',
|
||||
'emailClient.pop3Host': 'emailanwendung_msg27',
|
||||
'emailClient.pop3Port': 'emailanwendung_msg28',
|
||||
'emailClient.smtpHost': 'emailanwendung_msg29',
|
||||
'emailClient.smtpPort': 'emailanwendung_msg30',
|
||||
'emailClient.username': 'emailanwendung_msg31',
|
||||
'emailClient.password': 'emailanwendung_msg32',
|
||||
'emailClient.save': 'emailanwendung_msg33',
|
||||
'emailClient.cancel': 'emailanwendung_msg37',
|
||||
'emailClient.delete': 'emailanwendung_msg43',
|
||||
'emailClient.account': 'emailanwendung_msg44',
|
||||
'emailClient.name': 'emailanwendung_msg45',
|
||||
'emailClient.accountInvalid': 'emailanwendung_msg46',
|
||||
'emailClient.noAccount': 'emailanwendung_msg47',
|
||||
'emailServer.start': 'emailserver_msg1',
|
||||
'emailServer.started': 'emailserver_msg2',
|
||||
'emailServer.stopped': 'emailserver_msg3',
|
||||
'emailServer.domain': 'emailserver_msg4',
|
||||
'emailServer.deleteAccount': 'emailserver_msg8',
|
||||
'emailServer.yes': 'emailserver_msg9',
|
||||
'emailServer.cancel': 'emailserver_msg10',
|
||||
'emailServer.confirmTitle': 'emailserver_msg13',
|
||||
'emailServer.address': 'emailserver_msg14',
|
||||
'emailServer.mails': 'emailserver_msg15',
|
||||
'emailServer.username': 'emailserver_msg16',
|
||||
'emailServer.name': 'emailserver_msg17',
|
||||
'emailServer.password': 'emailserver_msg18',
|
||||
'emailServer.create': 'emailserver_msg19',
|
||||
'emailServer.lastNamePlaceholder': 'emailserver_msg20',
|
||||
'emailServer.firstNamePlaceholder': 'emailserver_msg21',
|
||||
'emailServer.whiteSpace': 'emailserver_msg24',
|
||||
'emailServer.missing': 'emailserver_msg25',
|
||||
'emailServer.newAccount': 'emailserver_msg26',
|
||||
'emailServer.accounts': 'emailserver_msg27',
|
||||
'emailServer.log': 'emailserver_msg28',
|
||||
'emailServer.stop': 'emailserver_msg29',
|
||||
'mail.failure.unresolved': 'sw_socket_msg1',
|
||||
'mail.failure.timedOut': 'sw_tcpsocket_msg2',
|
||||
'mail.failure.failed': 'sw_tcpsocket_msg3',
|
||||
'mail.failure.receiveFailed': 'sw_tcpsocket_msg6',
|
||||
};
|
||||
|
||||
/** Java's words, mended: the prompt says "Vermittlungsrechnerns" (guicontainer_msg1). */
|
||||
@@ -155,6 +215,8 @@ const FROM_JAVA_MENDED: Readonly<Record<string, readonly [string, (java: string)
|
||||
// Java's "\n" as line breaks; the last sentence of the German starts with a small letter there.
|
||||
'firewall.intro': ['jfirewalldialog_msg37', (java) => java.replaceAll('\\n', '\n').replace('\nüber', '\nÜber')],
|
||||
'firewall.filterSynHint': ['jfirewalldialog_msg43', (java) => java.replaceAll('\\n', '\n')],
|
||||
// Java's welcome without its bold tags (the panel writes it as text, Java's "!" after it).
|
||||
'emailClient.welcome': ['emailanwendung_msg4', (java) => java.replace(/<\/?b>/g, '')],
|
||||
// Without Java's key (Strg+D, Strg+R): in a browser they bookmark and reload the page.
|
||||
'mode.design': ['guimainmemu_msg3', (java) => java.replace(/ \((Strg|Ctrl)\+D\)$/, '')],
|
||||
'mode.simulation': ['guimainmemu_msg4', (java) => java.replace(/ \((Strg|Ctrl)\+R\)$/, '')],
|
||||
@@ -174,6 +236,18 @@ const FROM_JAVA_MENDED: Readonly<Record<string, readonly [string, (java: string)
|
||||
const FROM_JAVA_JOINED: Readonly<Record<string, readonly [string, string, string]>> = {
|
||||
'server.connectionAccepted': ['sw_serveranwendung_msg4', '{address}', 'sw_serveranwendung_msg5'],
|
||||
'server.connectionClosed': ['sw_servermitarbeiter_msg1', '{address}', 'sw_servermitarbeiter_msg2'],
|
||||
'emailServer.created': ['emailserver_msg22', '{name}', 'emailserver_msg23'],
|
||||
'mail.stored': ['sw_smtpmitarbeiter_msg12', '{name}', 'sw_smtpmitarbeiter_msg13'],
|
||||
'mail.connected': ['sw_smtpclient_msg1', '{address}', 'sw_smtpclient_msg2'],
|
||||
'mail.disconnected': ['sw_smtpclient_msg1', '{address}', 'sw_smtpclient_msg3'],
|
||||
};
|
||||
|
||||
/** Java's words before a value, joined as its programs join them (SMTPMitarbeiter, SMTPClient, the server window's question). */
|
||||
const FROM_JAVA_BEFORE: Readonly<Record<string, readonly [string, string, string?]>> = {
|
||||
'mail.sendFailed': ['sw_smtpmitarbeiter_msg14', ' {address}'],
|
||||
'mail.mailSent': ['sw_smtpclient_msg4', ' {recipients}'],
|
||||
// "'" + name + "'", the question's two halves written against it.
|
||||
'emailServer.confirmDelete': ['emailserver_msg11', '{name}', 'emailserver_msg12'],
|
||||
};
|
||||
|
||||
/** Java's page for a server it cannot reach (sw_webbrowser_msg1): its title and its heading. */
|
||||
@@ -216,6 +290,10 @@ describe('the catalogs', () => {
|
||||
expect(CATALOGS.de[key], key).toBe(`${de.get(before)} ${value} ${de.get(after)}`);
|
||||
expect(CATALOGS.en[key], key).toBe(`${en.get(before)} ${value} ${en.get(after)}`);
|
||||
}
|
||||
for (const [key, [before, value, after]] of Object.entries(FROM_JAVA_BEFORE)) {
|
||||
expect(CATALOGS.de[key], key).toBe(`${de.get(before)}${value}${after === undefined ? '' : de.get(after)}`);
|
||||
expect(CATALOGS.en[key], key).toBe(`${en.get(before)}${value}${after === undefined ? '' : en.get(after)}`);
|
||||
}
|
||||
for (const [key, [javaKey, part]] of Object.entries(FROM_JAVA_PAGE)) {
|
||||
expect(CATALOGS.de[key], key).toBe(part.exec(de.get(javaKey)!)![1]);
|
||||
expect(CATALOGS.en[key], key).toBe(part.exec(en.get(javaKey)!)![1]);
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// The e-mail windows' lines and checks against Java's
|
||||
// (packages/sim/test/mailWindow.java.json, MailWindowRun): each kind of line
|
||||
// the server's window got there in Java's words, what it leaves out, a
|
||||
// failure's message, and the checks of a new account.
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { EmailServerLogEntry } from '@mdg-filius/core';
|
||||
import { accountNames, newAccountProblem } from '../src/desktop/EmailServerPanel.js';
|
||||
import { emailServerLine, mailFailureText } from '../src/desktop/mailLines.js';
|
||||
import { translate } from '../src/i18n/i18n.js';
|
||||
|
||||
const java = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '../../sim/test/mailWindow.java.json'), 'utf8')) as { steps: { step: string; log?: string }[] };
|
||||
const log = (step: string) => java.steps.find((each) => each.step === step)!.log!;
|
||||
const de = (key: string, values?: Readonly<Record<string, string | number>>) => translate('de', key, values);
|
||||
const en = (key: string, values?: Readonly<Record<string, string | number>>) => translate('en', key, values);
|
||||
|
||||
describe("the e-mail server window's lines", () => {
|
||||
it("are Java's, line by line", () => {
|
||||
const after = log('server:after mails');
|
||||
const entries: EmailServerLogEntry[] = [
|
||||
{ kind: 'connectionOpened' },
|
||||
{ kind: 'received', address: '192.168.0.20', text: 'MAIL FROM: <anna@schule.de>' },
|
||||
{ kind: 'sent', address: '192.168.0.20', text: '250 Sender OK' },
|
||||
{ kind: 'stored', username: 'anna' },
|
||||
];
|
||||
for (const entry of entries) expect(after, entry.kind).toContain(`${emailServerLine(entry, de)}\n`);
|
||||
expect(log('server:start')).toBe(`${emailServerLine({ kind: 'switched', active: true }, de)}\n`);
|
||||
expect(log('server:stop').endsWith(`\n${emailServerLine({ kind: 'switched', active: false }, de)}\n`)).toBe(true);
|
||||
});
|
||||
|
||||
it("write the SMTP client's lines, and leave out its failures as Java's window does", () => {
|
||||
expect(emailServerLine({ kind: 'connectionStarting' }, de)).toBe('----');
|
||||
expect(emailServerLine({ kind: 'smtpConnected', address: '1.2.3.4', port: 25 }, de)).toBe('Verbindung zu 1.2.3.4:25 hergestellt');
|
||||
expect(emailServerLine({ kind: 'smtpDisconnected', address: '1.2.3.4', port: 25 }, en)).toBe('Connection to 1.2.3.4:25 disconnected');
|
||||
expect(emailServerLine({ kind: 'mailSent', recipients: 'a@b.de' }, de)).toBe('Mail versendet an a@b.de');
|
||||
expect(emailServerLine({ kind: 'sendFailed', address: '1.2.3.4' }, de)).toBe('Fehler beim Senden an 1.2.3.4');
|
||||
expect(emailServerLine({ kind: 'failed', failure: { kind: 'receiveFailed' } }, de)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("show a failure's message as Java's dialog does", () => {
|
||||
expect(mailFailureText({ kind: 'answer', text: '-ERR wrong password' }, de)).toBe('-ERR wrong password');
|
||||
expect(mailFailureText({ kind: 'connect', failure: 'unresolved' }, de)).toBe('Ziel konnte nicht bestimmt werden!');
|
||||
expect(mailFailureText({ kind: 'connect', failure: 'timedOut' }, en)).toBe('Timeout while connecting');
|
||||
expect(mailFailureText({ kind: 'connect', failure: 'failed' }, de)).toBe('Aktives Öffnen des Sockets fehlgeschlagen');
|
||||
expect(mailFailureText({ kind: 'receiveFailed' }, de)).toBe('Verbindung nicht aufgebaut!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('a new account', () => {
|
||||
it("is checked as Java's window checks it, and refused where the server has the name", () => {
|
||||
const accounts = [{ username: 'anna' }];
|
||||
expect(newAccountProblem('', 'x', accounts)).toBe('missing');
|
||||
expect(newAccountProblem('bob', '', accounts)).toBe('missing');
|
||||
for (const name of ['carl os', 'a\tb', 'a\nb']) expect(newAccountProblem(name, 'x', accounts), name).toBe('whiteSpace');
|
||||
expect(newAccountProblem('ANNA', 'x', accounts)).toBe('exists');
|
||||
expect(newAccountProblem('bob', 'x', accounts)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("takes the name typed, and Java's placeholders without one", () => {
|
||||
expect(accountNames('Neue Nutzerin', de)).toEqual({ firstName: 'Neue', lastName: 'Nutzerin' });
|
||||
expect(accountNames('Anna', de)).toEqual({ firstName: 'Anna', lastName: '' });
|
||||
expect(accountNames(' ', de)).toEqual({ firstName: 'Vorname', lastName: 'Nachname' });
|
||||
expect(accountNames('', en)).toEqual({ firstName: 'First name', lastName: 'Last name' });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user