feat(core): the e-mail server keeps /mailserver/konten.txt up to date at run time, measured against Java
The account file's writer moves from the export (packages/fls) into the core, so that the export and the running server write the same. The server writes the file at its start and whenever its accounts change. EmailRun now records Java's file as it left it and as Java writes the accounts it ended with; Java leaves the notice to a local sender out until its next write. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+3
-2
File diff suppressed because one or more lines are too long
@@ -29,6 +29,7 @@ export * from './runtime/apps/emailClient.js';
|
|||||||
export * from './runtime/apps/terminal.js';
|
export * from './runtime/apps/terminal.js';
|
||||||
export * from './runtime/apps/terminalTexts.js';
|
export * from './runtime/apps/terminalTexts.js';
|
||||||
export * from './runtime/email.js';
|
export * from './runtime/email.js';
|
||||||
|
export * from './runtime/emailAccountsFile.js';
|
||||||
export * from './runtime/http.js';
|
export * from './runtime/http.js';
|
||||||
export * from './util/equal.js';
|
export * from './util/equal.js';
|
||||||
export * from './util/javaBase64.js';
|
export * from './util/javaBase64.js';
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
// The e-mail server's account file, /mailserver/konten.txt, as Java writes
|
||||||
|
// it (EmailServer.listeBenutzerkontenZuString): Java's server reads it at
|
||||||
|
// every simulation start and writes it when its accounts change. Written by
|
||||||
|
// the server at run time and by the project export (packages/fls), from the
|
||||||
|
// accounts the model keeps. Java's errors are not copied: a field is written
|
||||||
|
// empty where Java writes "null" (a message's date, which Java never sets),
|
||||||
|
// and a character a field cannot carry in the format (a separator, a line
|
||||||
|
// break) becomes a space, with a warning.
|
||||||
|
|
||||||
|
import type { EmailAddress, EmailServerAccount } from './config.js';
|
||||||
|
|
||||||
|
export const EMAIL_SERVER_ACCOUNTS_FILE = '/mailserver/konten.txt';
|
||||||
|
|
||||||
|
/** A field's characters the format cannot carry, each a space, with a warning. */
|
||||||
|
export function accountFileField(value: string, forbidden: RegExp, what: string, warn: (message: string) => void): string {
|
||||||
|
if (!forbidden.test(value)) return value;
|
||||||
|
warn(`${what} holds a character Java's account file cannot carry, written there with a space`);
|
||||||
|
return value.replace(new RegExp(forbidden.source, 'g'), ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EmailServer.listeBenutzerkontenZuString: per account a line of user,
|
||||||
|
* domain, password, last and first name, then its messages, each "#" and
|
||||||
|
* seven fields joined by "$" — the text with Java's escapes
|
||||||
|
* (replaceSpecialChar, then line breaks as "|"), the others unescaped.
|
||||||
|
*/
|
||||||
|
export function serverAccountsFileText(domain: string, accounts: readonly EmailServerAccount[], warn: (message: string) => void = () => {}): string {
|
||||||
|
const where = (what: string) => `${EMAIL_SERVER_ACCOUNTS_FILE}: ${what}`;
|
||||||
|
const plain = (value: string, what: string) => accountFileField(value, /[;#$\r\n]/, where(what), warn);
|
||||||
|
const address = (entry: EmailAddress, what: string) => {
|
||||||
|
const name = entry.name === null ? null : accountFileField(entry.name, /[;#$,<>\r\n]/, where(what), warn);
|
||||||
|
const mail = entry.mailAddress === null ? null : accountFileField(entry.mailAddress, /[;#$,<>\r\n]/, where(what), warn);
|
||||||
|
if (name === null) return mail === null ? '' : `<${mail}>`;
|
||||||
|
return mail === null ? name : `${name} <${mail}>`;
|
||||||
|
};
|
||||||
|
const addresses = (entries: readonly EmailAddress[], what: string) => entries.map((entry) => address(entry, what)).join(',');
|
||||||
|
const escapedText = (text: string) =>
|
||||||
|
text.replace(/\$/g, '&&00036&&').replace(/;/g, '&&00059&&').replace(/\|/g, '&&00124&&').replace(/#/g, '&&00035&&').replace(/\n/g, '|');
|
||||||
|
return accounts
|
||||||
|
.map((account) => {
|
||||||
|
const messages = account.mailbox
|
||||||
|
.map(
|
||||||
|
(message) =>
|
||||||
|
'#' +
|
||||||
|
[
|
||||||
|
address(message.from, 'a sender'),
|
||||||
|
addresses(message.to, 'a recipient'),
|
||||||
|
addresses(message.cc, 'a recipient'),
|
||||||
|
addresses(message.bcc, 'a recipient'),
|
||||||
|
message.receivedAtMilliseconds === undefined ? '' : String(message.receivedAtMilliseconds),
|
||||||
|
plain(message.subject, 'a subject'),
|
||||||
|
accountFileField(escapedText(message.body), /\r$/, where('a text'), warn),
|
||||||
|
].join('$'),
|
||||||
|
)
|
||||||
|
.join('');
|
||||||
|
return [
|
||||||
|
plain(account.username, 'a user name'),
|
||||||
|
domain,
|
||||||
|
plain(account.password, 'a password'),
|
||||||
|
plain(account.lastName, 'a last name'),
|
||||||
|
plain(account.firstName, 'a first name'),
|
||||||
|
messages,
|
||||||
|
].join(';') + '\n';
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
@@ -121,7 +121,8 @@ import { EmailServer } from './apps/emailServer.js';
|
|||||||
import { Terminal } from './apps/terminal.js';
|
import { Terminal } from './apps/terminal.js';
|
||||||
import { firewallLogPage, forwardingTablePage, ripTablePage, routerIndexPage } from './apps/routerPages.js';
|
import { firewallLogPage, forwardingTablePage, ripTablePage, routerIndexPage } from './apps/routerPages.js';
|
||||||
import { applyFirewallSettings, firewallSettingsPage } from './apps/firewallSettings.js';
|
import { applyFirewallSettings, firewallSettingsPage } from './apps/firewallSettings.js';
|
||||||
import { VirtualFileSystem } from '../model/virtualFileSystem.js';
|
import { parentPath, VirtualFileSystem } from '../model/virtualFileSystem.js';
|
||||||
|
import { EMAIL_SERVER_ACCOUNTS_FILE, serverAccountsFileText } from './emailAccountsFile.js';
|
||||||
import {
|
import {
|
||||||
PRODUCTION_TRACE_RETENTION,
|
PRODUCTION_TRACE_RETENTION,
|
||||||
type HeaderField,
|
type HeaderField,
|
||||||
@@ -1181,6 +1182,33 @@ export class NetworkRuntimeEngine {
|
|||||||
if (server === undefined) return;
|
if (server === undefined) return;
|
||||||
servers.set(nodeId, { ...server, accounts });
|
servers.set(nodeId, { ...server, accounts });
|
||||||
this.stateValue.topologySnapshot = withSnapshot(this.stateValue.topologySnapshot, { emailServersByNodeId: servers });
|
this.stateValue.topologySnapshot = withSnapshot(this.stateValue.topologySnapshot, { emailServersByNodeId: servers });
|
||||||
|
this.writeEmailServerAccountsFile(nodeId, server.domain, accounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The server's account file (EmailServer.kontenSpeichern), written from its
|
||||||
|
* accounts at its start and whenever they change. Java makes it empty at
|
||||||
|
* its start when it is missing (and reads it), and writes it when an
|
||||||
|
* account is made or removed, after a POP3 command, after an SMTP mail was
|
||||||
|
* taken and when it stops — but before the notice to a local sender, which
|
||||||
|
* its file shows only at the next write.
|
||||||
|
*/
|
||||||
|
private writeEmailServerAccountsFile(nodeId: Id, domain: string, accounts: readonly EmailServerAccount[]): void {
|
||||||
|
const files = this.nodeFiles(nodeId);
|
||||||
|
const directory = parentPath(EMAIL_SERVER_ACCOUNTS_FILE);
|
||||||
|
if (!files.contains(directory)) files.createDirectory(directory, true);
|
||||||
|
files.writeTextFile(EMAIL_SERVER_ACCOUNTS_FILE, serverAccountsFileText(domain, accounts));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The node's files; a node without any gets an empty file system, kept in the snapshot. */
|
||||||
|
private nodeFiles(nodeId: Id): VirtualFileSystem {
|
||||||
|
const existing = this.stateValue.topologySnapshot.fileSystemsByNodeId.get(nodeId);
|
||||||
|
if (existing !== undefined) return existing;
|
||||||
|
const files = new VirtualFileSystem();
|
||||||
|
this.stateValue.topologySnapshot = withSnapshot(this.stateValue.topologySnapshot, {
|
||||||
|
fileSystemsByNodeId: new Map([...this.stateValue.topologySnapshot.fileSystemsByNodeId, [nodeId, files]]),
|
||||||
|
});
|
||||||
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1220,18 +1248,9 @@ export class NetworkRuntimeEngine {
|
|||||||
this.wallClock,
|
this.wallClock,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
this.writeEmailServerAccountsFile(nodeId, emailServer.domain, emailServer.accounts);
|
||||||
}
|
}
|
||||||
if (snapshot.terminalNodeIds.has(node.id)) {
|
if (snapshot.terminalNodeIds.has(node.id)) this.terminalsByNodeId.set(node.id, new Terminal(this, node.id, this.nodeFiles(node.id)));
|
||||||
let files = snapshot.fileSystemsByNodeId.get(node.id);
|
|
||||||
if (files === undefined) {
|
|
||||||
// A node without files gets an empty file system, kept in the snapshot.
|
|
||||||
files = new VirtualFileSystem();
|
|
||||||
this.stateValue.topologySnapshot = withSnapshot(this.stateValue.topologySnapshot, {
|
|
||||||
fileSystemsByNodeId: new Map([...this.stateValue.topologySnapshot.fileSystemsByNodeId, [node.id, files]]),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.terminalsByNodeId.set(node.id, new Terminal(this, node.id, files));
|
|
||||||
}
|
|
||||||
const emailClient = snapshot.emailClientsByNodeId.get(node.id);
|
const emailClient = snapshot.emailClientsByNodeId.get(node.id);
|
||||||
if (emailClient !== undefined) {
|
if (emailClient !== undefined) {
|
||||||
const nodeId = node.id;
|
const nodeId = node.id;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { readFileSync } from 'node:fs';
|
|||||||
import { dirname, join } from 'node:path';
|
import { dirname, join } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { formatMail, withSnapshot, type LogLine, type NetworkRuntimeEngine } from '../src/index.js';
|
import { EMAIL_SERVER_ACCOUNTS_FILE, formatMail, withSnapshot, type LogLine, type NetworkRuntimeEngine } from '../src/index.js';
|
||||||
import { expectSameCaptures, GAP_TOLERANCE_MILLISECONDS, watchedEngine, withClientPortText, type JavaFrame, type JavaTables } from './javaRun.js';
|
import { expectSameCaptures, GAP_TOLERANCE_MILLISECONDS, watchedEngine, withClientPortText, type JavaFrame, type JavaTables } from './javaRun.js';
|
||||||
import { MAIL_ACCOUNTS } from './mailAccounts.js';
|
import { MAIL_ACCOUNTS } from './mailAccounts.js';
|
||||||
import { clientText, emailServerText, isPolled, POLL_MILLISECONDS } from './programLines.js';
|
import { clientText, emailServerText, isPolled, POLL_MILLISECONDS } from './programLines.js';
|
||||||
@@ -32,6 +32,9 @@ interface JavaRun extends JavaTables {
|
|||||||
events: [number, Program, string][];
|
events: [number, Program, string][];
|
||||||
/** Each account's name and its messages: Email.toString, new, marked for deletion. */
|
/** Each account's name and its messages: Email.toString, new, marked for deletion. */
|
||||||
accounts: [string, ...[string, string, string][]][];
|
accounts: [string, ...[string, string, string][]][];
|
||||||
|
/** The account file as Java left it, and as Java writes the accounts it ended with (kontenSpeichern). */
|
||||||
|
file: string;
|
||||||
|
savedFile: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Act = (engine: NetworkRuntimeEngine) => void;
|
type Act = (engine: NetworkRuntimeEngine) => void;
|
||||||
@@ -155,6 +158,23 @@ describe('the e-mail server as Java FILIUS runs it', () => {
|
|||||||
...account.mailbox.map((entry) => [withoutDate(formatMail(entry.mail)), String(entry.isNew), String(entry.isMarkedForDeletion)]),
|
...account.mailbox.map((entry) => [withoutDate(formatMail(entry.mail)), String(entry.isNew), String(entry.isMarkedForDeletion)]),
|
||||||
]);
|
]);
|
||||||
expect(accounts).toEqual(java.accounts.map(([username, ...mails]) => [username, ...mails.map(([text, isNew, deleted]) => [withoutDate(text), isNew, deleted])]));
|
expect(accounts).toEqual(java.accounts.map(([username, ...mails]) => [username, ...mails.map(([text, isNew, deleted]) => [withoutDate(text), isNew, deleted])]));
|
||||||
|
|
||||||
|
// The account file at the end: as Java writes those accounts (kontenSpeichern), the engine at every change.
|
||||||
|
const file = snapshotFiles(engine).textFile(EMAIL_SERVER_ACCOUNTS_FILE);
|
||||||
|
expect(fileWithoutDate(file), 'account file').toBe(fileWithoutDate(java.savedFile));
|
||||||
|
expect(fileWithoutDate(java.file) === fileWithoutDate(java.savedFile), "Java's file as it left it is up to date").toBe(!STALE_FILE.has(name));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where Java left its account file behind its accounts: smtpErrors, the
|
||||||
|
* notice to the local sender (SMTPMitarbeiter saves the accounts before it
|
||||||
|
* sends the notice); the engine writes the file at every change.
|
||||||
|
*/
|
||||||
|
const STALE_FILE = new Set(['smtpErrors']);
|
||||||
|
|
||||||
|
/** A notice's date in the account file, where its line breaks are "|". */
|
||||||
|
const fileWithoutDate = (text: string) => text.replace(/\|Date: [^|]*\|/g, '|Date: <date>|');
|
||||||
|
|
||||||
|
const snapshotFiles = (engine: NetworkRuntimeEngine) => engine.state.topologySnapshot.fileSystemsByNodeId.get(SERVER)!;
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import { fileURLToPath } from 'node:url';
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import {
|
import {
|
||||||
addressEntry,
|
addressEntry,
|
||||||
|
EMAIL_SERVER_ACCOUNTS_FILE,
|
||||||
|
serverAccountsFileText,
|
||||||
|
VirtualFileSystem,
|
||||||
deviceConfiguration,
|
deviceConfiguration,
|
||||||
NetworkRuntimeEngine,
|
NetworkRuntimeEngine,
|
||||||
topologySnapshot,
|
topologySnapshot,
|
||||||
@@ -362,6 +365,15 @@ describe('the e-mail server in the engine', () => {
|
|||||||
expect(talk(engine, 110, 'QUIT')).toEqual(['+OK POP3 server ready', '+OK']);
|
expect(talk(engine, 110, 'QUIT')).toEqual(['+OK POP3 server ready', '+OK']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('writes its account file from its accounts when it starts, over one that says otherwise', () => {
|
||||||
|
const files = new VirtualFileSystem();
|
||||||
|
files.createDirectory('/mailserver');
|
||||||
|
files.writeTextFile(EMAIL_SERVER_ACCOUNTS_FILE, 'alt;filius.de;alt;Alt;Alt;\n');
|
||||||
|
const engine = new NetworkRuntimeEngine(14);
|
||||||
|
engine.handle({ type: 'start', snapshot: topologySnapshot({ ...mailNetwork(), fileSystemsByNodeId: new Map([[SERVER, files]]) }), seed: 14, initialTimeMilliseconds: 0 });
|
||||||
|
expect(files.textFile(EMAIL_SERVER_ACCOUNTS_FILE)).toBe(serverAccountsFileText('filius.de', MAIL_ACCOUNTS));
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps its mailboxes in the snapshot: a mail taken, a mail deleted; a new start begins from there', () => {
|
it('keeps its mailboxes in the snapshot: a mail taken, a mail deleted; a new start begins from there', () => {
|
||||||
const engine = new NetworkRuntimeEngine(14);
|
const engine = new NetworkRuntimeEngine(14);
|
||||||
engine.handle({ type: 'start', snapshot: topologySnapshot({ ...mailNetwork(), language: 'en' }), seed: 14, initialTimeMilliseconds: 0 });
|
engine.handle({ type: 'start', snapshot: topologySnapshot({ ...mailNetwork(), language: 'en' }), seed: 14, initialTimeMilliseconds: 0 });
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -74,6 +74,8 @@
|
|||||||
// Java FILIUS to the topology of the original up to those departures.
|
// Java FILIUS to the topology of the original up to those departures.
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
accountFileField,
|
||||||
|
serverAccountsFileText,
|
||||||
javaBase64,
|
javaBase64,
|
||||||
defaultSwitchConfiguration,
|
defaultSwitchConfiguration,
|
||||||
dhcpServerConfiguration,
|
dhcpServerConfiguration,
|
||||||
@@ -292,7 +294,7 @@ function exportSystem(node: TopologyNode, project: ProjectToExport, servingModem
|
|||||||
const client = project.installedPrograms.get(id)?.has('emailClient') === true ? project.emailClientConfigurations.get(id) : undefined;
|
const client = project.installedPrograms.get(id)?.has('emailClient') === true ? project.emailClientConfigurations.get(id) : undefined;
|
||||||
const server = project.installedPrograms.get(id)?.has('emailServer') === true ? project.emailServerConfigurations.get(id) : undefined;
|
const server = project.installedPrograms.get(id)?.has('emailServer') === true ? project.emailServerConfigurations.get(id) : undefined;
|
||||||
if (client?.account != null) putFile(root, EMAIL_CLIENT_FILE, 'text/txt', clientAccountFile(client, warn));
|
if (client?.account != null) putFile(root, EMAIL_CLIENT_FILE, 'text/txt', clientAccountFile(client, warn));
|
||||||
if (server !== undefined) putFile(root, EMAIL_SERVER_FILE, 'txt', serverAccountsFile(server, warn));
|
if (server !== undefined) putFile(root, EMAIL_SERVER_FILE, 'txt', serverAccountsFileText(server.domain, server.accounts, warn));
|
||||||
return {
|
return {
|
||||||
...system,
|
...system,
|
||||||
ssid: ssid === '' ? null : ssid,
|
ssid: ssid === '' ? null : ssid,
|
||||||
@@ -419,11 +421,7 @@ function javaContact(contact: EmailContact): JavaContact {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** A field of an account file: each character it cannot carry becomes a space, with a warning. */
|
/** A field of an account file: each character it cannot carry becomes a space, with a warning. */
|
||||||
function field(value: string, forbidden: RegExp, what: string, warn: Warn): string {
|
const field = accountFileField;
|
||||||
if (!forbidden.test(value)) return value;
|
|
||||||
warn(`${what} holds a character Java's account file cannot carry, written there with a space`);
|
|
||||||
return value.replace(new RegExp(forbidden.source, 'g'), ' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** EmailKonto.toString: nine fields; a field may hold no ';' and no line break. */
|
/** EmailKonto.toString: nine fields; a field may hold no ';' and no line break. */
|
||||||
function clientAccountFile(client: EmailClientConfiguration, warn: Warn): string {
|
function clientAccountFile(client: EmailClientConfiguration, warn: Warn): string {
|
||||||
@@ -442,53 +440,6 @@ function clientAccountFile(client: EmailClientConfiguration, warn: Warn): string
|
|||||||
].join(';');
|
].join(';');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* EmailServer.listeBenutzerkontenZuString: per account a line of user,
|
|
||||||
* domain, password, last and first name, then its messages, each "#" and
|
|
||||||
* seven fields joined by "$" — the text with Java's escapes
|
|
||||||
* (replaceSpecialChar, then line breaks as "|"), the others unescaped.
|
|
||||||
*/
|
|
||||||
function serverAccountsFile(server: EmailServerConfiguration, warn: Warn): string {
|
|
||||||
const where = (what: string) => `${EMAIL_SERVER_FILE}: ${what}`;
|
|
||||||
const plain = (value: string, what: string) => field(value, /[;#$\r\n]/, where(what), warn);
|
|
||||||
const address = (entry: EmailAddress, what: string) => {
|
|
||||||
const name = entry.name === null ? null : field(entry.name, /[;#$,<>\r\n]/, where(what), warn);
|
|
||||||
const mail = entry.mailAddress === null ? null : field(entry.mailAddress, /[;#$,<>\r\n]/, where(what), warn);
|
|
||||||
if (name === null) return mail === null ? '' : `<${mail}>`;
|
|
||||||
return mail === null ? name : `${name} <${mail}>`;
|
|
||||||
};
|
|
||||||
const addresses = (entries: readonly EmailAddress[], what: string) => entries.map((entry) => address(entry, what)).join(',');
|
|
||||||
const escapedText = (text: string) =>
|
|
||||||
text.replace(/\$/g, '&&00036&&').replace(/;/g, '&&00059&&').replace(/\|/g, '&&00124&&').replace(/#/g, '&&00035&&').replace(/\n/g, '|');
|
|
||||||
return server.accounts
|
|
||||||
.map((account) => {
|
|
||||||
const messages = account.mailbox
|
|
||||||
.map(
|
|
||||||
(message) =>
|
|
||||||
'#' +
|
|
||||||
[
|
|
||||||
address(message.from, 'a sender'),
|
|
||||||
addresses(message.to, 'a recipient'),
|
|
||||||
addresses(message.cc, 'a recipient'),
|
|
||||||
addresses(message.bcc, 'a recipient'),
|
|
||||||
message.receivedAtMilliseconds === undefined ? '' : String(message.receivedAtMilliseconds),
|
|
||||||
plain(message.subject, 'a subject'),
|
|
||||||
field(escapedText(message.body), /\r$/, where('a text'), warn),
|
|
||||||
].join('$'),
|
|
||||||
)
|
|
||||||
.join('');
|
|
||||||
return [
|
|
||||||
plain(account.username, 'a user name'),
|
|
||||||
server.domain,
|
|
||||||
plain(account.password, 'a password'),
|
|
||||||
plain(account.lastName, 'a last name'),
|
|
||||||
plain(account.firstName, 'a first name'),
|
|
||||||
messages,
|
|
||||||
].join(';') + '\n';
|
|
||||||
})
|
|
||||||
.join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The file at an absolute path of the root's tree, replacing one of the same
|
* The file at an absolute path of the root's tree, replacing one of the same
|
||||||
* name in any case (Java's holeDatei finds it so); directories are made.
|
* name in any case (Java's holeDatei finds it so); directories are made.
|
||||||
|
|||||||
@@ -726,6 +726,14 @@ Gemessen (01.10.2026):
|
|||||||
* Die Größe in `STAT` und `LIST` ist Javas eigene: die Länge von Text,
|
* Die Größe in `STAT` und `LIST` ist Javas eigene: die Länge von Text,
|
||||||
Absender, Betreff, Empfängerliste (`[a, b]`) und Datum.
|
Absender, Betreff, Empfängerliste (`[a, b]`) und Datum.
|
||||||
|
|
||||||
|
* Die Kontendatei `/mailserver/konten.txt` (seit F3.5a mitgeschrieben, wie
|
||||||
|
Java sie hinterlässt und wie `kontenSpeichern` die Konten am Ende
|
||||||
|
schreibt, 02.10.2026): Java schreibt sie nach einer angenommenen Mail,
|
||||||
|
nach POP3-Befehlen, bei Kontoänderungen und beim Stoppen; die
|
||||||
|
Benachrichtigung an einen lokalen Absender schickt der SMTP-Arbeiter erst
|
||||||
|
nach dem Speichern, sie fehlt in der Datei bis zum nächsten Mal. Felder
|
||||||
|
ohne Wert (das Datum) schreibt Java leer.
|
||||||
|
|
||||||
## E-Mail-Programm (F3.3b)
|
## E-Mail-Programm (F3.3b)
|
||||||
|
|
||||||
`EmailClientRun` lässt Javas E-Mail-Programm (`EmailAnwendung`) auf
|
`EmailClientRun` lässt Javas E-Mail-Programm (`EmailAnwendung`) auf
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
// really happens: every frame each card saw, every line the server wrote
|
// really happens: every frame each card saw, every line the server wrote
|
||||||
// into its window, every line of the generic client that talks to it, and
|
// into its window, every line of the generic client that talks to it, and
|
||||||
// the server's accounts at the end (each message as Email.toString, whether
|
// the server's accounts at the end (each message as Email.toString, whether
|
||||||
// it is new, whether it is marked for deletion).
|
// it is new, whether it is marked for deletion), and its account file, as
|
||||||
|
// Java left it and as Java writes those accounts (kontenSpeichern).
|
||||||
//
|
//
|
||||||
// EmailRun <scenario.json> <story> <milliseconds> <out.json>
|
// EmailRun <scenario.json> <story> <milliseconds> <out.json>
|
||||||
//
|
//
|
||||||
@@ -139,7 +140,10 @@ public class EmailRun {
|
|||||||
out.close(']');
|
out.close(']');
|
||||||
}
|
}
|
||||||
out.close(']');
|
out.close(']');
|
||||||
|
// The account file as Java left it, and as Java writes the accounts it ends with (kontenSpeichern).
|
||||||
out.key("file").string(files.holeDatei("/mailserver/konten.txt").getDateiInhalt());
|
out.key("file").string(files.holeDatei("/mailserver/konten.txt").getDateiInhalt());
|
||||||
|
server.kontenSpeichern();
|
||||||
|
out.key("savedFile").string(files.holeDatei("/mailserver/konten.txt").getDateiInhalt());
|
||||||
out.close('}');
|
out.close('}');
|
||||||
network.stop();
|
network.stop();
|
||||||
ScenarioNetwork.write(args[3], out);
|
ScenarioNetwork.write(args[3], out);
|
||||||
|
|||||||
Reference in New Issue
Block a user