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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,509 @@
|
||||
{
|
||||
"steps": [
|
||||
{
|
||||
"step": "server:open",
|
||||
"button": "Starten",
|
||||
"domain": "filius.de",
|
||||
"domainEnabled": true,
|
||||
"accounts": [],
|
||||
"username": "",
|
||||
"log": "",
|
||||
"accountsFile": "",
|
||||
"dialogs": []
|
||||
},
|
||||
{
|
||||
"step": "server:domain ungültig domain!",
|
||||
"button": "Starten",
|
||||
"domain": "filius.de",
|
||||
"domainEnabled": true,
|
||||
"accounts": [],
|
||||
"username": "",
|
||||
"log": "",
|
||||
"accountsFile": "",
|
||||
"dialogs": []
|
||||
},
|
||||
{
|
||||
"step": "server:domain schule.de",
|
||||
"button": "Starten",
|
||||
"domain": "schule.de",
|
||||
"domainEnabled": true,
|
||||
"accounts": [],
|
||||
"username": "",
|
||||
"log": "",
|
||||
"accountsFile": "",
|
||||
"dialogs": []
|
||||
},
|
||||
{
|
||||
"step": "server:start",
|
||||
"button": "Anhalten",
|
||||
"domain": "schule.de",
|
||||
"domainEnabled": false,
|
||||
"accounts": [],
|
||||
"username": "",
|
||||
"log": "POP3- und SMTP-Server wurden gestartet.\n",
|
||||
"accountsFile": "",
|
||||
"dialogs": []
|
||||
},
|
||||
{
|
||||
"step": "server:add anna/geheim",
|
||||
"button": "Anhalten",
|
||||
"domain": "schule.de",
|
||||
"domainEnabled": false,
|
||||
"accounts": [
|
||||
[
|
||||
"anna@schule.de",
|
||||
"0"
|
||||
]
|
||||
],
|
||||
"username": "",
|
||||
"log": "POP3- und SMTP-Server wurden gestartet.\n",
|
||||
"accountsFile": "anna;schule.de;geheim;Nachname;Vorname;\n",
|
||||
"dialogs": [
|
||||
"Das Benutzerkonto anna wurde angelegt."
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "server:add /x",
|
||||
"button": "Anhalten",
|
||||
"domain": "schule.de",
|
||||
"domainEnabled": false,
|
||||
"accounts": [
|
||||
[
|
||||
"anna@schule.de",
|
||||
"0"
|
||||
]
|
||||
],
|
||||
"username": "",
|
||||
"log": "POP3- und SMTP-Server wurden gestartet.\n",
|
||||
"accountsFile": "anna;schule.de;geheim;Nachname;Vorname;\n",
|
||||
"dialogs": [
|
||||
"Geben Sie einen Benutzernamen und ein Passwort ein!"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "server:add carl os/x",
|
||||
"button": "Anhalten",
|
||||
"domain": "schule.de",
|
||||
"domainEnabled": false,
|
||||
"accounts": [
|
||||
[
|
||||
"anna@schule.de",
|
||||
"0"
|
||||
]
|
||||
],
|
||||
"username": "carl os",
|
||||
"log": "POP3- und SMTP-Server wurden gestartet.\n",
|
||||
"accountsFile": "anna;schule.de;geheim;Nachname;Vorname;\n",
|
||||
"dialogs": [
|
||||
"Der Benutzername darf keine Leerzeichen enthalten!"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "server:add ANNA/anders",
|
||||
"button": "Anhalten",
|
||||
"domain": "schule.de",
|
||||
"domainEnabled": false,
|
||||
"accounts": [
|
||||
[
|
||||
"anna@schule.de",
|
||||
"0"
|
||||
]
|
||||
],
|
||||
"username": "",
|
||||
"log": "POP3- und SMTP-Server wurden gestartet.\n",
|
||||
"accountsFile": "anna;schule.de;geheim;Nachname;Vorname;\n",
|
||||
"dialogs": [
|
||||
"Das Benutzerkonto ANNA wurde angelegt."
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "server:add bob/",
|
||||
"button": "Anhalten",
|
||||
"domain": "schule.de",
|
||||
"domainEnabled": false,
|
||||
"accounts": [
|
||||
[
|
||||
"anna@schule.de",
|
||||
"0"
|
||||
]
|
||||
],
|
||||
"username": "bob",
|
||||
"log": "POP3- und SMTP-Server wurden gestartet.\n",
|
||||
"accountsFile": "anna;schule.de;geheim;Nachname;Vorname;\n",
|
||||
"dialogs": [
|
||||
"Geben Sie einen Benutzernamen und ein Passwort ein!"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "server:add bob/pw",
|
||||
"button": "Anhalten",
|
||||
"domain": "schule.de",
|
||||
"domainEnabled": false,
|
||||
"accounts": [
|
||||
[
|
||||
"anna@schule.de",
|
||||
"0"
|
||||
],
|
||||
[
|
||||
"bob@schule.de",
|
||||
"0"
|
||||
]
|
||||
],
|
||||
"username": "",
|
||||
"log": "POP3- und SMTP-Server wurden gestartet.\n",
|
||||
"accountsFile": "anna;schule.de;geheim;Nachname;Vorname;\nbob;schule.de;pw;Nachname;Vorname;\n",
|
||||
"dialogs": [
|
||||
"Das Benutzerkonto bob wurde angelegt."
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "client:open",
|
||||
"inbox": [],
|
||||
"sent": [],
|
||||
"tab": 0,
|
||||
"preview": "html",
|
||||
"composing": false,
|
||||
"accountFile": null,
|
||||
"dialogs": []
|
||||
},
|
||||
{
|
||||
"step": "client:account an na 110",
|
||||
"inbox": [],
|
||||
"sent": [],
|
||||
"tab": 0,
|
||||
"preview": "html",
|
||||
"composing": false,
|
||||
"accountFile": null,
|
||||
"dialogs": [
|
||||
"Die Angaben sind entweder fehlerhaft oder unvollständig!"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "client:account anna 110x",
|
||||
"inbox": [],
|
||||
"sent": [],
|
||||
"tab": 0,
|
||||
"preview": "html",
|
||||
"composing": false,
|
||||
"accountFile": null,
|
||||
"dialogs": [
|
||||
"Die Angaben sind entweder fehlerhaft oder unvollständig!"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "client:account anna 110",
|
||||
"inbox": [],
|
||||
"sent": [],
|
||||
"tab": 0,
|
||||
"preview": "html",
|
||||
"composing": false,
|
||||
"accountFile": "192.168.0.30;192.168.0.30;110;25;anna;geheim;Muster;Anna Maria;anna@schule.de",
|
||||
"dialogs": []
|
||||
},
|
||||
{
|
||||
"step": "client:account shown",
|
||||
"fields": [
|
||||
"Anna Maria Muster",
|
||||
"anna@schule.de",
|
||||
"192.168.0.30",
|
||||
"110",
|
||||
"192.168.0.30",
|
||||
"25",
|
||||
"anna",
|
||||
"geheim"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "client:send Falsch",
|
||||
"inbox": [],
|
||||
"sent": [],
|
||||
"tab": 0,
|
||||
"preview": "html",
|
||||
"composing": true,
|
||||
"composeFields": [
|
||||
"bob@schule.de, kein",
|
||||
"",
|
||||
"",
|
||||
"Falsch"
|
||||
],
|
||||
"composeText": "x",
|
||||
"composeSender": "anna",
|
||||
"accountFile": "192.168.0.30;192.168.0.30;110;25;anna;geheim;Muster;Anna Maria;anna@schule.de",
|
||||
"dialogs": [
|
||||
"Mindestens eine eingegebene E-Mail-Adresse ist nicht korrekt!"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "client:send Leer",
|
||||
"inbox": [],
|
||||
"sent": [],
|
||||
"tab": 0,
|
||||
"preview": "html",
|
||||
"composing": true,
|
||||
"composeFields": [
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"Leer"
|
||||
],
|
||||
"composeText": "x",
|
||||
"composeSender": "anna",
|
||||
"accountFile": "192.168.0.30;192.168.0.30;110;25;anna;geheim;Muster;Anna Maria;anna@schule.de",
|
||||
"dialogs": [
|
||||
"Sie müssen mindestens eine Empfänger-Adresse eingeben!"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "client:send Hallo",
|
||||
"inbox": [],
|
||||
"sent": [
|
||||
[
|
||||
"<bob@schule.de>",
|
||||
"Hallo"
|
||||
]
|
||||
],
|
||||
"tab": 1,
|
||||
"preview": "html",
|
||||
"composing": false,
|
||||
"accountFile": "192.168.0.30;192.168.0.30;110;25;anna;geheim;Muster;Anna Maria;anna@schule.de",
|
||||
"dialogs": []
|
||||
},
|
||||
{
|
||||
"step": "client:send Selbst",
|
||||
"inbox": [],
|
||||
"sent": [
|
||||
[
|
||||
"<bob@schule.de>",
|
||||
"Hallo"
|
||||
],
|
||||
[
|
||||
"<anna@schule.de>",
|
||||
"Selbst"
|
||||
]
|
||||
],
|
||||
"tab": 1,
|
||||
"preview": "html",
|
||||
"composing": false,
|
||||
"accountFile": "192.168.0.30;192.168.0.30;110;25;anna;geheim;Muster;Anna Maria;anna@schule.de",
|
||||
"dialogs": []
|
||||
},
|
||||
{
|
||||
"step": "server:after mails",
|
||||
"button": "Anhalten",
|
||||
"domain": "schule.de",
|
||||
"domainEnabled": false,
|
||||
"accounts": [
|
||||
[
|
||||
"anna@schule.de",
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"bob@schule.de",
|
||||
"1"
|
||||
]
|
||||
],
|
||||
"username": "",
|
||||
"log": "POP3- und SMTP-Server wurden gestartet.\n----\n192.168.0.20> 220 Willkommen bei schule.de\n192.168.0.20< HELO 192.168.0.20\n192.168.0.20> 250 Hello 192.168.0.20\n192.168.0.20< MAIL FROM: <anna@schule.de>\n192.168.0.20> 250 Sender OK\n192.168.0.20< RCPT TO:<bob@schule.de>\n192.168.0.20> 250 Recipient OK\n192.168.0.20< DATA\n192.168.0.20> 354 End data with <CR><LF>.<CR><LF>\n192.168.0.20< From: Anna Maria Muster <anna@schule.de>\nTo: <bob@schule.de>\nSubject: Hallo\n\nZeile 1\nZeile 2\r\n.\r\n\n192.168.0.20> 250 Mail queued for delivery\nE-Mail für bob im Postfach gespeichert.\n192.168.0.20< QUIT\n192.168.0.20> 221 Server beendet Verbindung.\n----\n192.168.0.20> 220 Willkommen bei schule.de\n192.168.0.20< HELO 192.168.0.20\n192.168.0.20> 250 Hello 192.168.0.20\n192.168.0.20< MAIL FROM: <anna@schule.de>\n192.168.0.20> 250 Sender OK\n192.168.0.20< RCPT TO:<anna@schule.de>\n192.168.0.20> 250 Recipient OK\n192.168.0.20< DATA\n192.168.0.20> 354 End data with <CR><LF>.<CR><LF>\n192.168.0.20< From: Anna Maria Muster <anna@schule.de>\nTo: <anna@schule.de>\nSubject: Selbst\n\nErste Zeile\nZweite Zeile\r\n.\r\n\n192.168.0.20> 250 Mail queued for delivery\nE-Mail für anna im Postfach gespeichert.\n192.168.0.20< QUIT\n192.168.0.20> 221 Server beendet Verbindung.\n",
|
||||
"accountsFile": "anna;schule.de;geheim;Nachname;Vorname;#Anna Maria Muster <anna@schule.de>$<anna@schule.de>$$$$Selbst$Erste Zeile|Zweite Zeile|\nbob;schule.de;pw;Nachname;Vorname;#Anna Maria Muster <anna@schule.de>$<bob@schule.de>$$$$Hallo$Zeile 1|Zeile 2|\n",
|
||||
"dialogs": []
|
||||
},
|
||||
{
|
||||
"step": "client:fetch",
|
||||
"inbox": [
|
||||
[
|
||||
"Anna Maria Muster",
|
||||
"Selbst"
|
||||
]
|
||||
],
|
||||
"sent": [
|
||||
[
|
||||
"<bob@schule.de>",
|
||||
"Hallo"
|
||||
],
|
||||
[
|
||||
"<anna@schule.de>",
|
||||
"Selbst"
|
||||
]
|
||||
],
|
||||
"tab": 0,
|
||||
"preview": "html",
|
||||
"composing": false,
|
||||
"accountFile": "192.168.0.30;192.168.0.30;110;25;anna;geheim;Muster;Anna Maria;anna@schule.de",
|
||||
"dialogs": []
|
||||
},
|
||||
{
|
||||
"step": "client:choose inbox 0",
|
||||
"inbox": [
|
||||
[
|
||||
"Anna Maria Muster",
|
||||
"Selbst"
|
||||
]
|
||||
],
|
||||
"sent": [
|
||||
[
|
||||
"<bob@schule.de>",
|
||||
"Hallo"
|
||||
],
|
||||
[
|
||||
"<anna@schule.de>",
|
||||
"Selbst"
|
||||
]
|
||||
],
|
||||
"tab": 0,
|
||||
"preview": "Erste Zeile\nZweite Zeile\n",
|
||||
"composing": false,
|
||||
"accountFile": "192.168.0.30;192.168.0.30;110;25;anna;geheim;Muster;Anna Maria;anna@schule.de",
|
||||
"dialogs": []
|
||||
},
|
||||
{
|
||||
"step": "client:answer",
|
||||
"inbox": [
|
||||
[
|
||||
"Anna Maria Muster",
|
||||
"Selbst"
|
||||
]
|
||||
],
|
||||
"sent": [
|
||||
[
|
||||
"<bob@schule.de>",
|
||||
"Hallo"
|
||||
],
|
||||
[
|
||||
"<anna@schule.de>",
|
||||
"Selbst"
|
||||
]
|
||||
],
|
||||
"tab": 0,
|
||||
"preview": "Erste Zeile\nZweite Zeile\n",
|
||||
"composing": true,
|
||||
"composeFields": [
|
||||
"Anna Maria Muster <anna@schule.de>",
|
||||
"",
|
||||
"",
|
||||
"RE: Selbst"
|
||||
],
|
||||
"composeText": "\n\nAnna Maria Muster <anna@schule.de> schrieb:\n> Erste Zeile\n> Zweite Zeile\n> ",
|
||||
"composeSender": "anna",
|
||||
"accountFile": "192.168.0.30;192.168.0.30;110;25;anna;geheim;Muster;Anna Maria;anna@schule.de",
|
||||
"dialogs": []
|
||||
},
|
||||
{
|
||||
"step": "client:choose sent 0",
|
||||
"inbox": [
|
||||
[
|
||||
"Anna Maria Muster",
|
||||
"Selbst"
|
||||
]
|
||||
],
|
||||
"sent": [
|
||||
[
|
||||
"<bob@schule.de>",
|
||||
"Hallo"
|
||||
],
|
||||
[
|
||||
"<anna@schule.de>",
|
||||
"Selbst"
|
||||
]
|
||||
],
|
||||
"tab": 0,
|
||||
"preview": "Zeile 1\nZeile 2",
|
||||
"composing": false,
|
||||
"accountFile": "192.168.0.30;192.168.0.30;110;25;anna;geheim;Muster;Anna Maria;anna@schule.de",
|
||||
"dialogs": []
|
||||
},
|
||||
{
|
||||
"step": "client:delete inbox 0",
|
||||
"inbox": [
|
||||
[
|
||||
"Anna Maria Muster",
|
||||
"Selbst"
|
||||
]
|
||||
],
|
||||
"sent": [
|
||||
[
|
||||
"<anna@schule.de>",
|
||||
"Selbst"
|
||||
]
|
||||
],
|
||||
"tab": 0,
|
||||
"preview": " ",
|
||||
"composing": false,
|
||||
"accountFile": "192.168.0.30;192.168.0.30;110;25;anna;geheim;Muster;Anna Maria;anna@schule.de",
|
||||
"dialogs": []
|
||||
},
|
||||
{
|
||||
"step": "server:after fetch",
|
||||
"button": "Anhalten",
|
||||
"domain": "schule.de",
|
||||
"domainEnabled": false,
|
||||
"accounts": [
|
||||
[
|
||||
"anna@schule.de",
|
||||
"0"
|
||||
],
|
||||
[
|
||||
"bob@schule.de",
|
||||
"1"
|
||||
]
|
||||
],
|
||||
"username": "",
|
||||
"log": "POP3- und SMTP-Server wurden gestartet.\n----\n192.168.0.20> 220 Willkommen bei schule.de\n192.168.0.20< HELO 192.168.0.20\n192.168.0.20> 250 Hello 192.168.0.20\n192.168.0.20< MAIL FROM: <anna@schule.de>\n192.168.0.20> 250 Sender OK\n192.168.0.20< RCPT TO:<bob@schule.de>\n192.168.0.20> 250 Recipient OK\n192.168.0.20< DATA\n192.168.0.20> 354 End data with <CR><LF>.<CR><LF>\n192.168.0.20< From: Anna Maria Muster <anna@schule.de>\nTo: <bob@schule.de>\nSubject: Hallo\n\nZeile 1\nZeile 2\r\n.\r\n\n192.168.0.20> 250 Mail queued for delivery\nE-Mail für bob im Postfach gespeichert.\n192.168.0.20< QUIT\n192.168.0.20> 221 Server beendet Verbindung.\n----\n192.168.0.20> 220 Willkommen bei schule.de\n192.168.0.20< HELO 192.168.0.20\n192.168.0.20> 250 Hello 192.168.0.20\n192.168.0.20< MAIL FROM: <anna@schule.de>\n192.168.0.20> 250 Sender OK\n192.168.0.20< RCPT TO:<anna@schule.de>\n192.168.0.20> 250 Recipient OK\n192.168.0.20< DATA\n192.168.0.20> 354 End data with <CR><LF>.<CR><LF>\n192.168.0.20< From: Anna Maria Muster <anna@schule.de>\nTo: <anna@schule.de>\nSubject: Selbst\n\nErste Zeile\nZweite Zeile\r\n.\r\n\n192.168.0.20> 250 Mail queued for delivery\nE-Mail für anna im Postfach gespeichert.\n192.168.0.20< QUIT\n192.168.0.20> 221 Server beendet Verbindung.\n----\n192.168.0.20> +OK POP3 server ready\n192.168.0.20< USER anna\n192.168.0.20> +OK enter password\n192.168.0.20< PASS geheim\n192.168.0.20> +OK Mailbox locked and ready\n192.168.0.20< STAT\n192.168.0.20> +OK 1 83\n192.168.0.20< LIST\n192.168.0.20> +OK 1 83\n0 83\n\n192.168.0.20< RETR 0\n192.168.0.20> +OK message follows \nFrom: Anna Maria Muster <anna@schule.de>\nTo: <anna@schule.de>\nSubject: Selbst\n\nErste Zeile\nZweite Zeile\n192.168.0.20< DELE 0\n192.168.0.20> +OK message marked for delete\n192.168.0.20< QUIT\n192.168.0.20> +OK\n",
|
||||
"accountsFile": "anna;schule.de;geheim;Nachname;Vorname;\nbob;schule.de;pw;Nachname;Vorname;#Anna Maria Muster <anna@schule.de>$<bob@schule.de>$$$$Hallo$Zeile 1|Zeile 2|\n",
|
||||
"dialogs": []
|
||||
},
|
||||
{
|
||||
"step": "server:delete row 1",
|
||||
"button": "Anhalten",
|
||||
"domain": "schule.de",
|
||||
"domainEnabled": false,
|
||||
"accounts": [
|
||||
[
|
||||
"anna@schule.de",
|
||||
"0"
|
||||
]
|
||||
],
|
||||
"username": "",
|
||||
"log": "POP3- und SMTP-Server wurden gestartet.\n----\n192.168.0.20> 220 Willkommen bei schule.de\n192.168.0.20< HELO 192.168.0.20\n192.168.0.20> 250 Hello 192.168.0.20\n192.168.0.20< MAIL FROM: <anna@schule.de>\n192.168.0.20> 250 Sender OK\n192.168.0.20< RCPT TO:<bob@schule.de>\n192.168.0.20> 250 Recipient OK\n192.168.0.20< DATA\n192.168.0.20> 354 End data with <CR><LF>.<CR><LF>\n192.168.0.20< From: Anna Maria Muster <anna@schule.de>\nTo: <bob@schule.de>\nSubject: Hallo\n\nZeile 1\nZeile 2\r\n.\r\n\n192.168.0.20> 250 Mail queued for delivery\nE-Mail für bob im Postfach gespeichert.\n192.168.0.20< QUIT\n192.168.0.20> 221 Server beendet Verbindung.\n----\n192.168.0.20> 220 Willkommen bei schule.de\n192.168.0.20< HELO 192.168.0.20\n192.168.0.20> 250 Hello 192.168.0.20\n192.168.0.20< MAIL FROM: <anna@schule.de>\n192.168.0.20> 250 Sender OK\n192.168.0.20< RCPT TO:<anna@schule.de>\n192.168.0.20> 250 Recipient OK\n192.168.0.20< DATA\n192.168.0.20> 354 End data with <CR><LF>.<CR><LF>\n192.168.0.20< From: Anna Maria Muster <anna@schule.de>\nTo: <anna@schule.de>\nSubject: Selbst\n\nErste Zeile\nZweite Zeile\r\n.\r\n\n192.168.0.20> 250 Mail queued for delivery\nE-Mail für anna im Postfach gespeichert.\n192.168.0.20< QUIT\n192.168.0.20> 221 Server beendet Verbindung.\n----\n192.168.0.20> +OK POP3 server ready\n192.168.0.20< USER anna\n192.168.0.20> +OK enter password\n192.168.0.20< PASS geheim\n192.168.0.20> +OK Mailbox locked and ready\n192.168.0.20< STAT\n192.168.0.20> +OK 1 83\n192.168.0.20< LIST\n192.168.0.20> +OK 1 83\n0 83\n\n192.168.0.20< RETR 0\n192.168.0.20> +OK message follows \nFrom: Anna Maria Muster <anna@schule.de>\nTo: <anna@schule.de>\nSubject: Selbst\n\nErste Zeile\nZweite Zeile\n192.168.0.20< DELE 0\n192.168.0.20> +OK message marked for delete\n192.168.0.20< QUIT\n192.168.0.20> +OK\n",
|
||||
"accountsFile": "anna;schule.de;geheim;Nachname;Vorname;\n",
|
||||
"dialogs": [
|
||||
"Wollen Sie das Konto 'bob' wirklich entfernen?"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "server:stop",
|
||||
"button": "Starten",
|
||||
"domain": "schule.de",
|
||||
"domainEnabled": true,
|
||||
"accounts": [
|
||||
[
|
||||
"anna@schule.de",
|
||||
"0"
|
||||
]
|
||||
],
|
||||
"username": "",
|
||||
"log": "POP3- und SMTP-Server wurden gestartet.\n----\n192.168.0.20> 220 Willkommen bei schule.de\n192.168.0.20< HELO 192.168.0.20\n192.168.0.20> 250 Hello 192.168.0.20\n192.168.0.20< MAIL FROM: <anna@schule.de>\n192.168.0.20> 250 Sender OK\n192.168.0.20< RCPT TO:<bob@schule.de>\n192.168.0.20> 250 Recipient OK\n192.168.0.20< DATA\n192.168.0.20> 354 End data with <CR><LF>.<CR><LF>\n192.168.0.20< From: Anna Maria Muster <anna@schule.de>\nTo: <bob@schule.de>\nSubject: Hallo\n\nZeile 1\nZeile 2\r\n.\r\n\n192.168.0.20> 250 Mail queued for delivery\nE-Mail für bob im Postfach gespeichert.\n192.168.0.20< QUIT\n192.168.0.20> 221 Server beendet Verbindung.\n----\n192.168.0.20> 220 Willkommen bei schule.de\n192.168.0.20< HELO 192.168.0.20\n192.168.0.20> 250 Hello 192.168.0.20\n192.168.0.20< MAIL FROM: <anna@schule.de>\n192.168.0.20> 250 Sender OK\n192.168.0.20< RCPT TO:<anna@schule.de>\n192.168.0.20> 250 Recipient OK\n192.168.0.20< DATA\n192.168.0.20> 354 End data with <CR><LF>.<CR><LF>\n192.168.0.20< From: Anna Maria Muster <anna@schule.de>\nTo: <anna@schule.de>\nSubject: Selbst\n\nErste Zeile\nZweite Zeile\r\n.\r\n\n192.168.0.20> 250 Mail queued for delivery\nE-Mail für anna im Postfach gespeichert.\n192.168.0.20< QUIT\n192.168.0.20> 221 Server beendet Verbindung.\n----\n192.168.0.20> +OK POP3 server ready\n192.168.0.20< USER anna\n192.168.0.20> +OK enter password\n192.168.0.20< PASS geheim\n192.168.0.20> +OK Mailbox locked and ready\n192.168.0.20< STAT\n192.168.0.20> +OK 1 83\n192.168.0.20< LIST\n192.168.0.20> +OK 1 83\n0 83\n\n192.168.0.20< RETR 0\n192.168.0.20> +OK message follows \nFrom: Anna Maria Muster <anna@schule.de>\nTo: <anna@schule.de>\nSubject: Selbst\n\nErste Zeile\nZweite Zeile\n192.168.0.20< DELE 0\n192.168.0.20> +OK message marked for delete\n192.168.0.20< QUIT\n192.168.0.20> +OK\nPOP3- und SMTP-Server wurden angehalten.\n",
|
||||
"accountsFile": "anna;schule.de;geheim;Nachname;Vorname;\n",
|
||||
"dialogs": []
|
||||
},
|
||||
{
|
||||
"step": "server:domain neu.de",
|
||||
"button": "Starten",
|
||||
"domain": "neu.de",
|
||||
"domainEnabled": true,
|
||||
"accounts": [
|
||||
[
|
||||
"anna@neu.de",
|
||||
"0"
|
||||
]
|
||||
],
|
||||
"username": "",
|
||||
"log": "POP3- und SMTP-Server wurden gestartet.\n----\n192.168.0.20> 220 Willkommen bei schule.de\n192.168.0.20< HELO 192.168.0.20\n192.168.0.20> 250 Hello 192.168.0.20\n192.168.0.20< MAIL FROM: <anna@schule.de>\n192.168.0.20> 250 Sender OK\n192.168.0.20< RCPT TO:<bob@schule.de>\n192.168.0.20> 250 Recipient OK\n192.168.0.20< DATA\n192.168.0.20> 354 End data with <CR><LF>.<CR><LF>\n192.168.0.20< From: Anna Maria Muster <anna@schule.de>\nTo: <bob@schule.de>\nSubject: Hallo\n\nZeile 1\nZeile 2\r\n.\r\n\n192.168.0.20> 250 Mail queued for delivery\nE-Mail für bob im Postfach gespeichert.\n192.168.0.20< QUIT\n192.168.0.20> 221 Server beendet Verbindung.\n----\n192.168.0.20> 220 Willkommen bei schule.de\n192.168.0.20< HELO 192.168.0.20\n192.168.0.20> 250 Hello 192.168.0.20\n192.168.0.20< MAIL FROM: <anna@schule.de>\n192.168.0.20> 250 Sender OK\n192.168.0.20< RCPT TO:<anna@schule.de>\n192.168.0.20> 250 Recipient OK\n192.168.0.20< DATA\n192.168.0.20> 354 End data with <CR><LF>.<CR><LF>\n192.168.0.20< From: Anna Maria Muster <anna@schule.de>\nTo: <anna@schule.de>\nSubject: Selbst\n\nErste Zeile\nZweite Zeile\r\n.\r\n\n192.168.0.20> 250 Mail queued for delivery\nE-Mail für anna im Postfach gespeichert.\n192.168.0.20< QUIT\n192.168.0.20> 221 Server beendet Verbindung.\n----\n192.168.0.20> +OK POP3 server ready\n192.168.0.20< USER anna\n192.168.0.20> +OK enter password\n192.168.0.20< PASS geheim\n192.168.0.20> +OK Mailbox locked and ready\n192.168.0.20< STAT\n192.168.0.20> +OK 1 83\n192.168.0.20< LIST\n192.168.0.20> +OK 1 83\n0 83\n\n192.168.0.20< RETR 0\n192.168.0.20> +OK message follows \nFrom: Anna Maria Muster <anna@schule.de>\nTo: <anna@schule.de>\nSubject: Selbst\n\nErste Zeile\nZweite Zeile\n192.168.0.20< DELE 0\n192.168.0.20> +OK message marked for delete\n192.168.0.20< QUIT\n192.168.0.20> +OK\nPOP3- und SMTP-Server wurden angehalten.\n",
|
||||
"accountsFile": "anna;schule.de;geheim;Nachname;Vorname;\n",
|
||||
"dialogs": []
|
||||
},
|
||||
{
|
||||
"accounts": "1",
|
||||
"step": "server:accounts"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
// 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']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -196,7 +196,7 @@ describe('a simulation room', () => {
|
||||
expect(reason({ node: 'nobody', app: 'terminal', action: 'enter', args: { line: 'x' } })).toMatchObject({ code: 'unknownNode' });
|
||||
expect(reason({ node: node('Switch 1').id, app: 'terminal', action: 'enter', args: { line: 'x' } })).toMatchObject({ code: 'unknownApp' });
|
||||
expect(reason({ node: notebook, app: 'terminal', action: 'paint' })).toMatchObject({ code: 'unknownAction' });
|
||||
expect(reason({ node: notebook, app: 'emailClient', action: 'send' })).toMatchObject({ code: 'unknownAction' });
|
||||
expect(reason({ node: notebook, app: 'gnutella', action: 'search' })).toMatchObject({ code: 'unknownAction' });
|
||||
expect(reason({ node: notebook, app: 'terminal', action: 'enter', args: { line: 7 } })).toMatchObject({ code: 'badArgs' });
|
||||
expect(reason({ node: notebook, app: 'terminal', action: 'interrupt' })).toEqual({ v: 1, kind: 'ack', reqId: 'r' });
|
||||
send({ kind: 'nonsense', reqId: 'q' });
|
||||
@@ -458,7 +458,7 @@ describe("a simulation room's browser and web server windows", () => {
|
||||
[server, 'webServer', 'setVirtualHost', { row: 0, column: 1, value: 2 }, 'badArgs'],
|
||||
[server, 'webServer', 'restart', {}, 'unknownAction'],
|
||||
[notebook, 'webServer', 'setActive', { active: true }, 'unknownApp'],
|
||||
[server, 'emailServer', 'setActive', { active: true }, 'unknownAction'],
|
||||
[server, 'echoServer', 'setActive', { active: true }, 'unknownAction'],
|
||||
];
|
||||
for (const [target, app, action, args, code] of cases) {
|
||||
send({ kind: 'input', node: target, app, action, args, reqId: `${app}.${action}.${JSON.stringify(args)}` });
|
||||
@@ -467,3 +467,93 @@ describe("a simulation room's browser and web server windows", () => {
|
||||
expect(ofKind(got, 'ack')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("a simulation room's e-mail windows", () => {
|
||||
const views = (got: readonly HostMessage[], node: Id, app: string) => ofKind(got, 'view').filter((each) => each.node === node && each.app === app).map((each) => each.view);
|
||||
|
||||
it('sends the e-mail program and server as views, and takes their actions', () => {
|
||||
const { node, send, run, got } = setUp(exampleDoc('java-examples/email_komplett.fls'));
|
||||
const one = node('Rechner 1').id;
|
||||
const two = node('Rechner 2').id;
|
||||
const server = node('Mail-Server (filius.de)').id;
|
||||
send({ kind: 'sub', topics: [`app:${one}/emailClient`, `app:${two}/emailClient`, `app:${server}/emailServer`] });
|
||||
expect(views(got, server, 'emailServer')).toEqual([null]);
|
||||
send({ kind: 'start' });
|
||||
run(TICK_MILLISECONDS);
|
||||
expect(views(got, server, 'emailServer').at(-1)).toEqual({ active: true, domain: 'filius.de', accounts: [{ username: 'rechner1', mails: expect.any(Number) }, { username: 'rechner2', mails: expect.any(Number) }] });
|
||||
const mails = (views(got, server, 'emailServer').at(-1) as { accounts: { mails: number }[] }).accounts[1]!.mails;
|
||||
expect(views(got, one, 'emailClient').at(-1)).toMatchObject({ account: { username: 'rechner1', email: 'rechner1@filius.de' } });
|
||||
const act = (target: Id, app: string, action: string, args: object, reqId: string) => send({ kind: 'input', node: target, app, action, args, reqId });
|
||||
act(one, 'emailClient', 'send', { to: 'rechner2@filius.de', cc: '', bcc: '', subject: 'Hallo', text: 'Zeile' }, 'send');
|
||||
run(20_000);
|
||||
expect(ofKind(got, 'ack').map((each) => each.reqId)).toEqual(['send']);
|
||||
expect((views(got, one, 'emailClient').at(-1) as { sent: { subject: string }[] }).sent.map((mail) => mail.subject)).toContain('Hallo');
|
||||
expect((views(got, server, 'emailServer').at(-1) as { accounts: { mails: number }[] }).accounts[1]!.mails).toBe(mails + 1);
|
||||
act(two, 'emailClient', 'fetch', {}, 'fetch');
|
||||
run(20_000);
|
||||
const inbox = () => (views(got, two, 'emailClient').at(-1) as { inbox: { subject: string }[] }).inbox;
|
||||
expect(inbox().map((mail) => mail.subject)).toContain('Hallo');
|
||||
const before = inbox().length;
|
||||
act(two, 'emailClient', 'removeMail', { folder: 'inbox', index: 0 }, 'remove');
|
||||
expect(inbox()).toHaveLength(before - 1);
|
||||
// The program's lines go on as lines.
|
||||
expect(ofKind(got, 'app').some((each) => each.app === 'emailClient' && each.node === one && each.lines.length > 0)).toBe(true);
|
||||
act(two, 'emailClient', 'setAccount', { name: 'Zwei Zwo', email: 'zwei@filius.de', pop3Host: '141.99.50.232', pop3Port: '110', smtpHost: '141.99.50.232', smtpPort: '25', username: 'zwei', password: 'x' }, 'account');
|
||||
expect(views(got, two, 'emailClient').at(-1)).toMatchObject({ account: { firstName: 'Zwei', lastName: 'Zwo', username: 'zwei', pop3Port: 110 } });
|
||||
|
||||
// The server: switched off with the window's line, its domain set, an account made and removed.
|
||||
act(server, 'emailServer', 'setActive', { active: false }, 'off');
|
||||
act(server, 'emailServer', 'setDomain', { domain: 'schule.de' }, 'domain');
|
||||
act(server, 'emailServer', 'addAccount', { username: 'neu', password: 'pw', firstName: 'Vorname', lastName: 'Nachname' }, 'add');
|
||||
act(server, 'emailServer', 'removeAccount', { username: 'rechner1' }, 'removeAccount');
|
||||
expect(views(got, server, 'emailServer').at(-1)).toEqual({ active: false, domain: 'schule.de', accounts: [{ username: 'rechner2', mails: expect.any(Number) }, { username: 'neu', mails: 0 }] });
|
||||
const lines = ofKind(got, 'app').filter((each) => each.app === 'emailServer').flatMap((each) => each.lines as { kind: string }[]);
|
||||
expect(lines.at(-1)).toMatchObject({ kind: 'switched', active: false });
|
||||
});
|
||||
|
||||
it('names a wrong action in the e-mail windows', () => {
|
||||
const { node, send, run, got } = setUp(exampleDoc('java-examples/email_komplett.fls'));
|
||||
const one = node('Rechner 1').id;
|
||||
const server = node('Mail-Server (filius.de)').id;
|
||||
send({ kind: 'start' });
|
||||
run(TICK_MILLISECONDS);
|
||||
const account = { name: 'A', email: 'a@filius.de', pop3Host: 'h', pop3Port: '110', smtpHost: 'h', smtpPort: '25', username: 'a', password: 'p' };
|
||||
const cases: [string, string, string, Record<string, unknown>, string][] = [
|
||||
[one, 'emailClient', 'send', { to: 'kein', cc: '', bcc: '', subject: '', text: '' }, 'badArgs'],
|
||||
[one, 'emailClient', 'send', { to: '', cc: '', bcc: '', subject: '', text: '' }, 'badArgs'],
|
||||
[one, 'emailClient', 'send', { to: 'a@b.de', cc: '', bcc: '', subject: '' }, 'badArgs'],
|
||||
[one, 'emailClient', 'setAccount', { ...account, pop3Port: '0' }, 'badArgs'],
|
||||
[one, 'emailClient', 'setAccount', { ...account, password: 5 }, 'badArgs'],
|
||||
[one, 'emailClient', 'removeMail', { folder: 'drafts', index: 0 }, 'badArgs'],
|
||||
[one, 'emailClient', 'removeMail', { folder: 'inbox', index: 0.5 }, 'badArgs'],
|
||||
[one, 'emailClient', 'reply', {}, 'unknownAction'],
|
||||
[server, 'emailClient', 'fetch', {}, 'unknownApp'],
|
||||
[server, 'emailServer', 'setActive', { active: 1 }, 'badArgs'],
|
||||
[server, 'emailServer', 'setDomain', { domain: 'neu.de' }, 'badArgs'],
|
||||
[server, 'emailServer', 'setDomain', { domain: 3 }, 'badArgs'],
|
||||
[server, 'emailServer', 'addAccount', { username: 'rechner1', password: 'p', firstName: '', lastName: '' }, 'badArgs'],
|
||||
[server, 'emailServer', 'addAccount', { username: '', password: 'p', firstName: '', lastName: '' }, 'badArgs'],
|
||||
[server, 'emailServer', 'addAccount', { username: 'a', password: '', firstName: '', lastName: '' }, 'badArgs'],
|
||||
[server, 'emailServer', 'addAccount', { username: 'a b', password: 'p', firstName: '', lastName: '' }, 'badArgs'],
|
||||
[server, 'emailServer', 'addAccount', { username: 'a\tb', password: 'p', firstName: '', lastName: '' }, 'badArgs'],
|
||||
[server, 'emailServer', 'addAccount', { username: 'a', password: 'p', firstName: 1, lastName: '' }, 'badArgs'],
|
||||
[server, 'emailServer', 'removeAccount', { username: 'niemand' }, 'badArgs'],
|
||||
[server, 'emailServer', 'removeAccount', {}, 'badArgs'],
|
||||
[server, 'emailServer', 'restart', {}, 'unknownAction'],
|
||||
[one, 'emailServer', 'setActive', { active: true }, 'unknownApp'],
|
||||
];
|
||||
for (const [target, app, action, args, code] of cases) {
|
||||
const reqId = `${app}.${action}.${JSON.stringify(args)}`;
|
||||
send({ kind: 'input', node: target, app, action, args, reqId });
|
||||
expect(ofKind(got, 'error').at(-1), reqId).toMatchObject({ code, reqId });
|
||||
}
|
||||
expect(ofKind(got, 'ack')).toEqual([]);
|
||||
// Off, the server takes a domain, and only one Java's pattern takes.
|
||||
send({ kind: 'input', node: server, app: 'emailServer', action: 'setActive', args: { active: false }, reqId: 'off' });
|
||||
send({ kind: 'input', node: server, app: 'emailServer', action: 'setDomain', args: { domain: 'kein domain!' }, reqId: 'bad' });
|
||||
expect(ofKind(got, 'error').at(-1)).toMatchObject({ code: 'badArgs', reqId: 'bad' });
|
||||
send({ kind: 'input', node: server, app: 'emailServer', action: 'setDomain', args: { domain: 'neu.de' }, reqId: 'good' });
|
||||
expect(ofKind(got, 'ack').map((each) => each.reqId)).toEqual(['off', 'good']);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user