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
+509
View File
@@ -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"
}
]
}
+253
View File
@@ -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']);
});
});
+92 -2
View File
@@ -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']);
});
});