feat(core): the e-mail server after Java, measured against Java
SMTP on port 25 stores mails for the domain's accounts as Java reads them anew and tells a local sender of unknown recipients; POP3 on port 110 hands them out. The mailboxes are in the snapshot and go back into the project. EmailRun plays four stories over the network; MailCases feeds Java's workers without one (26 cases, de and en). 18 cases name Java bugs that the core does not take. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
# Cases for tools/java-oracle's MailCases: FILIUS' SMTP and POP3 workers fed
|
||||
# with a client's messages, one per line. Read by emailServer.test.ts.
|
||||
case smtpPlain smtp
|
||||
HELO 192.168.0.20
|
||||
MAIL FROM: <bob@filius.de>
|
||||
RCPT TO:<anna@filius.de>
|
||||
DATA
|
||||
From: Bob Beispiel <bob@filius.de>\u000aTo: <anna@filius.de>\u000aSubject: Gru\u00df\u000a\u000aHallo Anna,\u000awie geht's?\u000d\u000a.\u000d\u000a
|
||||
QUIT
|
||||
case smtpTwice smtp
|
||||
EHLO client
|
||||
MAIL FROM: <bob@filius.de>
|
||||
RCPT TO:<anna@filius.de>
|
||||
DATA
|
||||
From: <bob@filius.de>\u000aTo: <anna@filius.de>\u000aSubject: Eins\u000a\u000aerster Teil\u000a
|
||||
zweiter Teil\u000a.\u000a
|
||||
MAIL FROM: <anna@filius.de>
|
||||
MAIL FROM: <anna@filius.de>
|
||||
RCPT TO:<bob@filius.de>
|
||||
DATA
|
||||
From: <anna@filius.de>\u000aTo: <bob@filius.de>\u000aSubject: Zwei\u000a\u000aan Bob\u000a.\u000a
|
||||
QUIT
|
||||
case smtpStart smtp
|
||||
DATA
|
||||
MAIL FROM:<bob@filius.de>
|
||||
noop
|
||||
helo
|
||||
Quit
|
||||
case smtpHelo smtp
|
||||
EHLO viele Leerzeichen
|
||||
NOOP
|
||||
HELO wieder
|
||||
case smtpEnvelope smtp
|
||||
HELO x
|
||||
MAIL FROM:bob
|
||||
MAIL FROM:<bob@filius.de>
|
||||
mail from : <bob@filius.de>
|
||||
RCPT TO:anna
|
||||
rcpt to: <ANNA@filius.de>
|
||||
RCPT TO:<anna@filius.de> <bob@filius.de>
|
||||
HELO noch einmal
|
||||
DATA
|
||||
Subject: Kopf\u000a\u000aText\u000a.\u000a
|
||||
case smtpDomainCase smtp
|
||||
HELO x
|
||||
MAIL FROM:<bob@filius.de>
|
||||
RCPT TO:<anna@FILIUS.DE>
|
||||
DATA
|
||||
Subject: Gross\u000a\u000aText\u000a.\u000a
|
||||
case smtpHeaders smtp
|
||||
HELO x
|
||||
MAIL FROM:<bob@filius.de>
|
||||
RCPT TO:<anna@filius.de>
|
||||
DATA
|
||||
FROM: Bob <bob@filius.de>\u000ato: a@b.de, Carla <carla@andere.de>,\u000aCc: <chef@filius.de>\u000aBcc: geheim@filius.de\u000aSubject: Zeit: 10 Uhr\u000a\u000aText mit: Doppelpunkt\u000a\u000aAbsatz\u000a.\u000a
|
||||
case smtpOddHeaders smtp
|
||||
HELO x
|
||||
MAIL FROM:<bob@filius.de>
|
||||
RCPT TO:<anna@filius.de>
|
||||
DATA
|
||||
To: x>y@z.de\u000aCc: ,\u000aSubject : mit Leerzeichen\u000a\u000aText\u000a.\u000a
|
||||
case smtpColonFirst smtp
|
||||
HELO x
|
||||
MAIL FROM:<bob@filius.de>
|
||||
RCPT TO:<anna@filius.de>
|
||||
DATA
|
||||
Subject: vorn\u000a: leer\u000a\u000aText\u000a.\u000a
|
||||
case smtpUnknownHeader smtp
|
||||
HELO x
|
||||
MAIL FROM:<bob@filius.de>
|
||||
RCPT TO:<anna@filius.de>
|
||||
DATA
|
||||
From: <bob@filius.de>\u000aX-Mailer: Filius\u000aSubject: Betreff\u000a\u000aText\u000a.\u000a
|
||||
case smtpNoHeader smtp
|
||||
HELO x
|
||||
MAIL FROM:<bob@filius.de>
|
||||
RCPT TO:<anna@filius.de>
|
||||
DATA
|
||||
Hallo\u000aWelt\u000a.\u000a
|
||||
case smtpDataPieces smtp
|
||||
HELO x
|
||||
MAIL FROM:<bob@filius.de>
|
||||
RCPT TO:<anna@filius.de>
|
||||
DATA
|
||||
Subject: Teile\u000a\u000aeins
|
||||
zwei\u000a
|
||||
.
|
||||
case smtpQuitInData smtp
|
||||
HELO x
|
||||
MAIL FROM:<bob@filius.de>
|
||||
RCPT TO:<anna@filius.de>
|
||||
DATA
|
||||
Subject: Abbruch\u000a\u000avorher\u000a
|
||||
quit
|
||||
case smtpUnknownRecipient smtp
|
||||
HELO x
|
||||
MAIL FROM:<bob@filius.de>
|
||||
RCPT TO:<niemand@filius.de>
|
||||
RCPT TO:<anna@filius.de>
|
||||
RCPT TO:<Keiner@filius.de>
|
||||
DATA
|
||||
From: Bob Beispiel <bob@filius.de>\u000aTo: <niemand@filius.de>\u000aSubject: Wer?\u000a\u000aIst da jemand?\u000a.\u000a
|
||||
case smtpEmptySender smtp
|
||||
HELO x
|
||||
MAIL FROM:<>
|
||||
RCPT TO:<niemand@filius.de>
|
||||
DATA
|
||||
Subject: Leer\u000a\u000aText\u000a.\u000a
|
||||
case smtpWithoutAt smtp
|
||||
HELO x
|
||||
MAIL FROM:<bob@filius.de>
|
||||
RCPT TO:<anna>
|
||||
DATA
|
||||
Subject: Ohne\u000a\u000aText\u000a.\u000a
|
||||
case pop3Plain pop3
|
||||
USER anna
|
||||
PASS geheim
|
||||
STAT
|
||||
LIST
|
||||
RETR 0
|
||||
RETR 1
|
||||
DELE 0
|
||||
STAT
|
||||
RSET
|
||||
DELE 0
|
||||
QUIT
|
||||
case pop3Again pop3
|
||||
USER anna
|
||||
PASS geheim
|
||||
RETR 1
|
||||
RETR 1
|
||||
case pop3Numbers pop3
|
||||
USER anna
|
||||
PASS geheim
|
||||
DELE 0
|
||||
LIST
|
||||
RETR 0
|
||||
RETR 1
|
||||
QUIT
|
||||
case pop3EmptyList pop3
|
||||
USER bob
|
||||
PASS passwort
|
||||
STAT
|
||||
LIST
|
||||
QUIT
|
||||
case pop3Before pop3
|
||||
STAT
|
||||
LIST
|
||||
RETR 0
|
||||
DELE 0
|
||||
RSET
|
||||
NOOP
|
||||
QUIT
|
||||
case pop3Users pop3
|
||||
USER niemand
|
||||
PASS x
|
||||
user ANNA
|
||||
PASS falsch
|
||||
PASS geheim
|
||||
USER bob
|
||||
PASS passwort
|
||||
NOOP
|
||||
case pop3UserSwitch pop3
|
||||
USER anna
|
||||
USER niemand
|
||||
PASS geheim
|
||||
USER anna
|
||||
PASS GEHEIM
|
||||
PASS geheim
|
||||
case pop3BadArguments pop3
|
||||
USER anna
|
||||
PASS geheim
|
||||
RETR 7
|
||||
DELE 7
|
||||
RETR -1
|
||||
LIST 0
|
||||
STAT extra
|
||||
NOOP
|
||||
RETR x
|
||||
case pop3NoArgument pop3
|
||||
USER
|
||||
case pop3TwoSpaces pop3
|
||||
USER anna
|
||||
PASS geheim
|
||||
RETR 0
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,160 @@
|
||||
// The e-mail server as Java FILIUS runs it. tools/java-oracle's EmailRun let
|
||||
// FILIUS' generic client talk SMTP and POP3 to an e-mail server on
|
||||
// mail-lan's network and wrote every frame each card saw, Java's tables of
|
||||
// them, the lines of the server's and the client's windows, and the
|
||||
// server's accounts at the end (email-java-<story>.java.json). The engine
|
||||
// plays the same stories with its programs (apps/emailServer.ts, the
|
||||
// generic client of apps/echo.ts): each card's frames in the same order,
|
||||
// with the same content and rows, at about the same time; each window's
|
||||
// lines as Java's, at about the same time; the same accounts at the end.
|
||||
// Java's errors in these dialogues are measured without a network
|
||||
// (emailServer.test.ts); these stories keep to what Java does right.
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { 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 { MAIL_ACCOUNTS } from './mailAccounts.js';
|
||||
import { clientText, emailServerText, isPolled, POLL_MILLISECONDS } from './programLines.js';
|
||||
import { snapshotOf, type Scenario } from './scenario.js';
|
||||
|
||||
const scenarioDirectory = join(dirname(fileURLToPath(import.meta.url)), 'scenarios');
|
||||
|
||||
const CLIENT = '00000000-0000-0000-0000-000000000201';
|
||||
const SERVER = '00000000-0000-0000-0000-000000000203';
|
||||
|
||||
type Program = 'server' | 'client';
|
||||
|
||||
interface JavaRun extends JavaTables {
|
||||
frames: Record<string, JavaFrame[]>;
|
||||
events: [number, Program, string][];
|
||||
/** Each account's name and its messages: Email.toString, new, marked for deletion. */
|
||||
accounts: [string, ...[string, string, string][]][];
|
||||
}
|
||||
|
||||
type Act = (engine: NetworkRuntimeEngine) => void;
|
||||
|
||||
/** EmailRun's calls at their times: connecting takes 2.5 s, each message 1.5 s, from 500 ms on. */
|
||||
function story(...parts: (readonly ['connect', number] | readonly ['send', ...string[]] | readonly ['wait', number])[]): [number, Act][] {
|
||||
const acts: [number, Act][] = [];
|
||||
let at = 500;
|
||||
for (const [kind, ...rest] of parts) {
|
||||
if (kind === 'connect') {
|
||||
const port = rest[0] as number;
|
||||
acts.push([at, (engine) => engine.simpleClient(CLIENT)!.connect('192.168.0.30', port)]);
|
||||
at += 2_500;
|
||||
} else if (kind === 'send') {
|
||||
for (const text of rest as string[]) {
|
||||
acts.push([at, (engine) => engine.simpleClient(CLIENT)!.send(text)]);
|
||||
at += 1_500;
|
||||
}
|
||||
} else {
|
||||
at += rest[0] as number;
|
||||
}
|
||||
}
|
||||
return acts;
|
||||
}
|
||||
|
||||
const STORIES: Record<string, [number, Act][]> = {
|
||||
smtp: story(
|
||||
['connect', 25],
|
||||
[
|
||||
'send',
|
||||
'HELO 192.168.0.20',
|
||||
'MAIL FROM: <bob@filius.de>',
|
||||
'RCPT TO:<anna@filius.de>',
|
||||
'DATA',
|
||||
"From: Bob Beispiel <bob@filius.de>\nTo: <anna@filius.de>\nSubject: Gruß\n\nHallo Anna,\nwie geht's?\r\n.\r\n",
|
||||
'QUIT',
|
||||
],
|
||||
),
|
||||
smtpErrors: story(
|
||||
['connect', 25],
|
||||
[
|
||||
'send',
|
||||
'NOOP',
|
||||
'EHLO',
|
||||
'DATA',
|
||||
'MAIL FROM: bob@filius.de',
|
||||
'RCPT TO:<niemand@filius.de>',
|
||||
'DATA',
|
||||
'MAIL FROM:<bob@filius.de>',
|
||||
'RCPT TO: anna',
|
||||
'NOOP',
|
||||
'DATA',
|
||||
'From: <bob@filius.de>\nTo: <niemand@filius.de>\nSubject: Wer?\n\nIst da jemand?\n.\n',
|
||||
'QUIT',
|
||||
],
|
||||
),
|
||||
pop3: story(
|
||||
['connect', 110],
|
||||
['send', 'USER anna', 'PASS geheim', 'STAT', 'LIST', 'RETR 0', 'RETR 1', 'DELE 0', 'DELE 1', 'QUIT'],
|
||||
['wait', 1_000],
|
||||
['connect', 110],
|
||||
['send', 'USER anna', 'PASS geheim', 'STAT', 'QUIT'],
|
||||
),
|
||||
pop3Errors: story(['connect', 110], ['send', 'NOOP', 'USER niemand', 'PASS x', 'USER anna', 'PASS falsch', 'PASS geheim', 'NOOP', 'USER bob', 'QUIT']),
|
||||
};
|
||||
|
||||
/** A notice's date: Java's in its locale and the time it ran. */
|
||||
const withoutDate = (text: string) => text.replace(/\nDate: [^\n]*\n/g, '\nDate: <date>\n');
|
||||
|
||||
type Lines = Record<Program, [number, string][]>;
|
||||
|
||||
function javaLines(java: JavaRun): Lines {
|
||||
const lines: Lines = { server: [], client: [] };
|
||||
for (const [time, program, text] of java.events) {
|
||||
// Anwendung.starten and the server's refreshes tell their observers nothing (null), which the windows skip.
|
||||
if (text === 'null' || text.startsWith('call ')) continue;
|
||||
lines[program].push([time, withClientPortText(text)]);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
function engineLines(engine: NetworkRuntimeEngine): Lines {
|
||||
const lines = <E>(log: readonly LogLine<E>[], text: (entry: E) => string): [number, string][] =>
|
||||
log.map((line) => [line.timeMilliseconds, withClientPortText(text(line))]);
|
||||
return { server: lines(engine.emailServer(SERVER)!.log.all, (entry) => emailServerText(entry)), client: lines(engine.simpleClient(CLIENT)!.log.all, clientText) };
|
||||
}
|
||||
|
||||
describe('the e-mail server as Java FILIUS runs it', () => {
|
||||
const scenario = JSON.parse(readFileSync(join(scenarioDirectory, 'mail-lan.scenario.json'), 'utf8')) as Scenario;
|
||||
const snapshot = withSnapshot(snapshotOf(scenario), {
|
||||
emailServersByNodeId: new Map([[SERVER, { domain: 'filius.de', accounts: MAIL_ACCOUNTS, isActive: true }]]),
|
||||
simpleClientsByNodeId: new Map([[CLIENT, { targetAddress: '', targetPort: 25 }]]),
|
||||
});
|
||||
|
||||
for (const [name, acts] of Object.entries(STORIES)) {
|
||||
it(name, () => {
|
||||
const java = JSON.parse(readFileSync(join(scenarioDirectory, `email-java-${name}.java.json`), 'utf8')) as JavaRun;
|
||||
const { engine } = watchedEngine(scenario, snapshot);
|
||||
for (const [time, act] of acts) {
|
||||
engine.handle({ type: 'advance', toMilliseconds: time });
|
||||
act(engine);
|
||||
}
|
||||
engine.handle({ type: 'advance', toMilliseconds: Math.max(...java.events.map(([time]) => time)) + 5_000 });
|
||||
|
||||
expectSameCaptures(engine, scenario, java.frames, { text: (text) => withoutDate(withClientPortText(text)), tables: java, cell: withoutDate });
|
||||
|
||||
const expected = javaLines(java);
|
||||
const actual = engineLines(engine);
|
||||
for (const program of ['server', 'client'] as const) {
|
||||
expect(actual[program].map(([, text]) => text), `${program}'s lines`).toEqual(expected[program].map(([, text]) => text));
|
||||
for (const [index, [javaTime, text]] of expected[program].entries()) {
|
||||
const late = javaTime - actual[program][index]![0];
|
||||
const most = (isPolled(text) ? POLL_MILLISECONDS : 0) + GAP_TOLERANCE_MILLISECONDS;
|
||||
expect(late, `${program}'s line ${JSON.stringify(text.slice(0, 60))}`).toBeGreaterThanOrEqual(-GAP_TOLERANCE_MILLISECONDS);
|
||||
expect(late, `${program}'s line ${JSON.stringify(text.slice(0, 60))}`).toBeLessThanOrEqual(most);
|
||||
}
|
||||
}
|
||||
|
||||
const accounts = engine.emailServer(SERVER)!.state.accounts.map((account) => [
|
||||
account.username,
|
||||
...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])]));
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,389 @@
|
||||
// The e-mail server's SMTP and POP3 dialogues (apps/emailServer.ts) against
|
||||
// Java FILIUS' own: tools/java-oracle's MailCases fed Java's workers with the
|
||||
// messages of email-server.cases.txt (email-server.<de|en>.java.json). After
|
||||
// each message: what was sent, the window's lines, whether the connection
|
||||
// was closed, and the accounts must be Java's, in both languages, except
|
||||
// where Java errs.
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
addressEntry,
|
||||
deviceConfiguration,
|
||||
NetworkRuntimeEngine,
|
||||
topologySnapshot,
|
||||
type TopologySnapshotInput,
|
||||
addressText,
|
||||
formatMail,
|
||||
mailboxEntry,
|
||||
MailServerState,
|
||||
mailSize,
|
||||
parseMail,
|
||||
Pop3Session,
|
||||
SmtpSession,
|
||||
type EmailServerLogEntry,
|
||||
type MailAccount,
|
||||
type PageLanguage,
|
||||
type Steps,
|
||||
} from '../src/index.js';
|
||||
import { link, pc, uuid } from './fixtures.js';
|
||||
import { MAIL_ACCOUNTS } from './mailAccounts.js';
|
||||
import { emailServerText } from './programLines.js';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
type JavaAccounts = [string, [string, boolean, boolean][]][];
|
||||
|
||||
interface JavaStep {
|
||||
message: string | null;
|
||||
sent: string[];
|
||||
lines: string[];
|
||||
closed: boolean;
|
||||
thrown: string | null;
|
||||
accounts: JavaAccounts;
|
||||
}
|
||||
|
||||
interface JavaCase {
|
||||
name: string;
|
||||
protocol: 'smtp' | 'pop3';
|
||||
steps: JavaStep[];
|
||||
}
|
||||
|
||||
const CLIENT = '192.168.0.20';
|
||||
|
||||
/** Runs a dialogue's steps to their end: nothing waits here, the connection sends at once. */
|
||||
function run(steps: Steps): void {
|
||||
for (let next = steps.next(); next.done !== true; next = steps.next()) throw new Error('a step waited');
|
||||
}
|
||||
|
||||
/** The session's own run of a case: each step as MailCases writes Java's. */
|
||||
function play(javaCase: JavaCase, language: PageLanguage): JavaStep[] {
|
||||
const lines: EmailServerLogEntry[] = [];
|
||||
const accounts: MailAccount[] = MAIL_ACCOUNTS.map((account) => ({ ...account, mailbox: account.mailbox.map(mailboxEntry) }));
|
||||
const state = new MailServerState(
|
||||
'filius.de',
|
||||
accounts,
|
||||
language,
|
||||
(entry) => lines.push(entry),
|
||||
() => undefined,
|
||||
() => '192.168.0.30',
|
||||
() => 0,
|
||||
);
|
||||
const sent: string[] = [];
|
||||
const connection = {
|
||||
address: CLIENT,
|
||||
*send(text: string): Steps<boolean> {
|
||||
sent.push(text);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
const session = javaCase.protocol === 'smtp' ? new SmtpSession(state, connection) : new Pop3Session(state, connection);
|
||||
const step = (text: string | null): JavaStep => {
|
||||
const result: JavaStep = {
|
||||
message: text,
|
||||
sent: [...sent],
|
||||
lines: lines.map((entry) => emailServerText(entry, language)),
|
||||
closed: session.closed,
|
||||
thrown: null,
|
||||
accounts: accounts.map((account) => [account.username, account.mailbox.map((entry) => [formatMail(entry.mail), entry.isNew, entry.isMarkedForDeletion])]),
|
||||
};
|
||||
sent.length = 0;
|
||||
lines.length = 0;
|
||||
return result;
|
||||
};
|
||||
run(session.begin());
|
||||
const steps = [step(null)];
|
||||
for (const each of javaCase.steps.slice(1)) {
|
||||
run(session.receive(each.message!));
|
||||
steps.push(step(each.message));
|
||||
}
|
||||
return steps;
|
||||
}
|
||||
|
||||
/** Java's date of a notice, in its locale and time: a placeholder. */
|
||||
const withoutDate = (text: string) => text.replace(/\nDate: [^\n]*\n/g, '\nDate: <date>\n');
|
||||
|
||||
const stored = (username: string, language: PageLanguage) => emailServerText({ kind: 'stored', username }, language);
|
||||
|
||||
/**
|
||||
* A step as Java means it: the answers given, the window's lines rebuilt
|
||||
* from them (received, sent, then the mails stored), the accounts as given.
|
||||
*/
|
||||
function fixed(
|
||||
step: JavaStep,
|
||||
language: PageLanguage,
|
||||
changes: { sent?: string[]; stored?: string[]; accounts?: JavaAccounts; closed?: boolean },
|
||||
): JavaStep {
|
||||
const sent = changes.sent ?? step.sent;
|
||||
const storedLines = changes.stored?.map((username) => stored(username, language)) ?? step.lines.filter((line) => !line.startsWith(`${CLIENT}`));
|
||||
return {
|
||||
...step,
|
||||
sent,
|
||||
lines: [`${CLIENT}< ${step.message}`, ...sent.map((text) => `${CLIENT}> ${text}`), ...storedLines],
|
||||
closed: changes.closed ?? step.closed,
|
||||
thrown: null,
|
||||
accounts: changes.accounts ?? step.accounts,
|
||||
};
|
||||
}
|
||||
|
||||
type Fix = (steps: JavaStep[], language: PageLanguage) => JavaStep[];
|
||||
|
||||
/** Each step from `from` on with the accounts given (a mail Java stores differently changes every later step). */
|
||||
const accountsFrom = (steps: JavaStep[], from: number, accounts: JavaAccounts) => steps.map((step, index) => (index >= from ? { ...step, accounts } : step));
|
||||
|
||||
const order = (language: PageLanguage) => (language === 'de' ? '503 Falsche Reihenfolge der Kommandos.' : '503 Wrong order of commands.');
|
||||
const queued = '250 Mail queued for delivery';
|
||||
const noSuchMessage = '-ERR no such message';
|
||||
const unknownCommand = '-ERR unknown command';
|
||||
|
||||
function addTo(accounts: JavaAccounts, username: string, ...mails: [string, boolean, boolean][]): JavaAccounts {
|
||||
return accounts.map(([name, mailbox]) => [name, name === username ? [...mailbox, ...mails] : mailbox]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Where Java errs, the case as Java means it (the core's departures, see
|
||||
* emailServer.ts).
|
||||
*/
|
||||
const JAVA_BUGS: Record<string, Fix> = {
|
||||
// After a mail Java answers the next command 503 and keeps sender,
|
||||
// recipients and text: the second mail went to anna too, with the first's text.
|
||||
smtpTwice: (steps, language) => {
|
||||
const start = steps[0]!.accounts;
|
||||
const first = steps[6]!.accounts.find(([name]) => name === 'anna')![1].at(-1)!;
|
||||
const second: [string, boolean, boolean] = ['From: <anna@filius.de>\nTo: <bob@filius.de>\nSubject: Zwei\n\nan Bob', true, false];
|
||||
const result = steps.map((step, index) => (index === 7 ? fixed(step, language, { sent: ['250 Sender OK'] }) : step));
|
||||
result[11] = fixed(steps[11]!, language, { stored: ['bob'] });
|
||||
return accountsFrom(result, 11, addTo(addTo(start, 'anna', first), 'bob', second));
|
||||
},
|
||||
// A command the state does not take: Java answers nothing.
|
||||
smtpStart: (steps, language) => steps.map((step, index) => (index === 1 || index === 2 ? fixed(step, language, { sent: [order(language)] }) : step)),
|
||||
// HELO again: Java answers nothing; the core begins anew.
|
||||
smtpHelo: (steps, language) => steps.map((step, index) => (index === 3 ? fixed(step, language, { sent: ['250 Hello wieder'] }) : step)),
|
||||
// "MAIL FROM" (two spaces) and HELO again: Java answers nothing. After
|
||||
// HELO the envelope is new: DATA and the text are out of order, nothing is stored.
|
||||
smtpEnvelope: (steps, language) => {
|
||||
const result = steps.map((step, index) => {
|
||||
if (index === 3) return fixed(step, language, { sent: [order(language)] });
|
||||
if (index === 8) return fixed(step, language, { sent: ['250 Hello noch einmal'] });
|
||||
if (index === 9 || index === 10) return fixed(step, language, { sent: [order(language)], stored: [] });
|
||||
return step;
|
||||
});
|
||||
return accountsFrom(result, 10, steps[0]!.accounts);
|
||||
},
|
||||
// Java compares the domain exactly and sends the mail on; anna@FILIUS.DE is anna's.
|
||||
smtpDomainCase: (steps, language) => {
|
||||
const mail: [string, boolean, boolean] = ['Subject: Gross\n\nText', true, false];
|
||||
const result = steps.map((step, index) => (index === 5 ? fixed(step, language, { stored: ['anna'] }) : step));
|
||||
return accountsFrom(result, 5, addTo(steps[0]!.accounts, 'anna', mail));
|
||||
},
|
||||
// A field Java does not know ends its header, and Subject became text.
|
||||
smtpUnknownHeader: (steps) => accountsFrom(steps, 5, addTo(steps[0]!.accounts, 'anna', ['From: <bob@filius.de>\nSubject: Betreff\n\nText', true, false])),
|
||||
// A line without a field name (": leer") ends Java's header and is dropped; the core's text begins with it.
|
||||
smtpColonFirst: (steps) => accountsFrom(steps, 5, addTo(steps[0]!.accounts, 'anna', ['Subject: vorn\n\n: leer\n\nText', true, false])),
|
||||
// Without a header Java drops the first line.
|
||||
smtpNoHeader: (steps) => accountsFrom(steps, 5, addTo(steps[0]!.accounts, 'anna', ['\nHallo\nWelt', true, false])),
|
||||
// "quit" during DATA: Java ends the connection; it is the mail's text.
|
||||
smtpQuitInData: (steps, language) => steps.map((step, index) => (index === 6 ? { ...fixed(step, language, { sent: [], closed: false }), lines: [`${CLIENT}< quit`] } : step)),
|
||||
// An empty sender: Java's notice throws (NullPointerException); none is sent.
|
||||
smtpEmptySender: (steps, language) => steps.map((step, index) => (index === 5 ? fixed(step, language, {}) : step)),
|
||||
// A recipient without "@": Java's delivery throws; the core sends the sender a notice, to the
|
||||
// envelope's sender, as the mail has no From.
|
||||
smtpWithoutAt: (steps, language) => {
|
||||
const notice: [string, boolean, boolean] = [
|
||||
'From: <noreply@filius.de>\nTo: <bob@filius.de>\nSubject: Non Delivery Notification\n\n' +
|
||||
'The mail could not be delivered either because user or mail domain is unknown.\nMessage could not be delivered to the following recipients:\n\tanna\n\n' +
|
||||
'Date: <date>\nMail-Server: 192.168.0.30\n\n______________________________________________________________________\nSubject: Ohne\n\nText',
|
||||
true,
|
||||
false,
|
||||
];
|
||||
const result = steps.map((step, index) => (index === 5 ? fixed(step, language, { sent: [queued], stored: ['bob'] }) : step));
|
||||
return accountsFrom(result, 5, addTo(steps[0]!.accounts, 'bob', notice));
|
||||
},
|
||||
// RETR of a message retrieved before: Java refuses.
|
||||
pop3Again: (steps, language) => steps.map((step, index) => (index === 4 ? fixed(step, language, { sent: steps[3]!.sent }) : step)),
|
||||
// After DELE 0, Java's LIST numbers the other message 0; RETR 0 still means the deleted one.
|
||||
pop3Numbers: (steps, language) => steps.map((step, index) => (index === 4 ? fixed(step, language, { sent: ['+OK 1 89\n1 89\n'] }) : step)),
|
||||
// LIST without messages: Java's list fails.
|
||||
pop3EmptyList: (steps, language) => steps.map((step, index) => (index === 4 ? fixed(step, language, { sent: ['+OK 0 0\n'] }) : step)),
|
||||
// Before log-in Java answers empty.
|
||||
pop3Before: (steps, language) =>
|
||||
steps.map((step, index) => (index >= 1 && index <= 5 ? fixed(step, language, { sent: [unknownCommand] }) : index === 7 ? fixed(step, language, { sent: ['+OK'] }) : step)),
|
||||
// A message that is not there: Java sends an empty mail or an empty
|
||||
// answer; LIST 0 lists all; RETR x throws and ends the connection.
|
||||
pop3BadArguments: (steps, language) =>
|
||||
steps.map((step, index) => {
|
||||
if (index === 3 || index === 4 || index === 5 || index === 9) return fixed(step, language, { sent: [noSuchMessage] });
|
||||
if (index === 6) return fixed(step, language, { sent: ['+OK 0 80'] });
|
||||
return step;
|
||||
}),
|
||||
// USER without a name: Java throws and ends the connection.
|
||||
pop3NoArgument: (steps, language) => steps.map((step, index) => (index === 1 ? fixed(step, language, { sent: ['-ERR user or password wrong'] }) : step)),
|
||||
// Two spaces before the number: Java throws and ends the connection.
|
||||
pop3TwoSpaces: (steps, language) => steps.map((step, index) => (index === 3 ? fixed(step, language, { sent: [noSuchMessage] }) : step)),
|
||||
};
|
||||
|
||||
function javaCases(language: PageLanguage): JavaCase[] {
|
||||
return JSON.parse(readFileSync(join(here, `email-server.${language}.java.json`), 'utf8')) as JavaCase[];
|
||||
}
|
||||
|
||||
const placeholders = (step: JavaStep): JavaStep => ({
|
||||
...step,
|
||||
accounts: step.accounts.map(([name, mailbox]) => [name, mailbox.map(([text, isNew, deleted]) => [withoutDate(text), isNew, deleted])]),
|
||||
});
|
||||
|
||||
describe("the e-mail server's dialogues as Java FILIUS answers them", () => {
|
||||
for (const language of ['de', 'en'] as const) {
|
||||
const cases = javaCases(language);
|
||||
|
||||
it(`corrects Java only where it errs (${language})`, () => {
|
||||
for (const [name, fix] of Object.entries(JAVA_BUGS)) {
|
||||
const javaCase = cases.find((each) => each.name === name)!;
|
||||
expect(JSON.stringify(fix(javaCase.steps, language)), name).not.toBe(JSON.stringify(javaCase.steps));
|
||||
}
|
||||
});
|
||||
|
||||
for (const javaCase of cases) {
|
||||
it(`${javaCase.name} (${language})`, () => {
|
||||
const expected = (JAVA_BUGS[javaCase.name]?.(javaCase.steps, language) ?? javaCase.steps).map(placeholders);
|
||||
const actual = play(javaCase, language).map(placeholders);
|
||||
for (const [index, step] of expected.entries()) expect(actual[index], `step ${index}: ${JSON.stringify(step.message)}`).toEqual(step);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe('e-mail as Java writes it', () => {
|
||||
it('reads addresses as AddressEntry does', () => {
|
||||
expect(addressEntry(' Anna Muster <anna@filius.de> ')).toEqual({ name: 'Anna Muster', mailAddress: 'anna@filius.de' });
|
||||
expect(addressEntry('<anna@filius.de>')).toEqual({ name: null, mailAddress: 'anna@filius.de' });
|
||||
expect(addressEntry('anna@filius.de')).toEqual({ name: null, mailAddress: 'anna@filius.de' });
|
||||
expect(addressEntry('Anna')).toEqual({ name: 'Anna', mailAddress: null });
|
||||
expect(addressEntry('a>b<c')).toEqual({ name: 'a>b<c', mailAddress: null });
|
||||
expect(addressText({ name: null, mailAddress: null })).toBeNull();
|
||||
});
|
||||
|
||||
it('writes no Bcc, and counts a size as Java does', () => {
|
||||
const mail = parseMail('From: A <a@b.de>\nTo: x@y.de, Y <y@z.de>\nBcc: geheim@b.de\nSubject: Hallo \n\nText\n');
|
||||
expect(mail.bcc).toEqual([{ name: null, mailAddress: 'geheim@b.de' }]);
|
||||
expect(formatMail(mail)).toBe('From: A <a@b.de>\nTo: <x@y.de>, Y <y@z.de>\nSubject: Hallo\n\nText');
|
||||
expect(mailSize(mail)).toBe('Text\n'.length + 'A <a@b.de>'.length + 'Hallo'.length + '[<x@y.de>, Y <y@z.de>]'.length);
|
||||
expect(mailSize(parseMail('\nx'))).toBe('x\n'.length + 'null'.length * 2 + '[]'.length);
|
||||
});
|
||||
});
|
||||
|
||||
const SERVER = uuid(0x5001);
|
||||
const HOST = uuid(0x5002);
|
||||
|
||||
function mailNetwork(): TopologySnapshotInput {
|
||||
return {
|
||||
nodes: [pc(SERVER, uuid(0x5011)), pc(HOST, uuid(0x5012))],
|
||||
links: [link(SERVER, uuid(0x5011), HOST, uuid(0x5012))],
|
||||
deviceConfigurations: new Map([
|
||||
[SERVER, deviceConfiguration('10.0.0.1', '255.255.255.0', '', '')],
|
||||
[HOST, deviceConfiguration('10.0.0.2', '255.255.255.0', '', '')],
|
||||
]),
|
||||
emailServersByNodeId: new Map([[SERVER, { domain: 'filius.de', accounts: MAIL_ACCOUNTS, isActive: true }]]),
|
||||
};
|
||||
}
|
||||
|
||||
/** Connects to the server's port and sends each message, each after the answer to the one before; the answers, the greeting first. */
|
||||
function talk(engine: NetworkRuntimeEngine, port: number, ...messages: string[]): string[] {
|
||||
const socket = engine.openTcpClientSocket(HOST, '10.0.0.1', port)!;
|
||||
if (engine.connectTcpWithResult(socket) !== 'connected') return [];
|
||||
const answers: string[] = [];
|
||||
const receive = () => {
|
||||
engine.handle({ type: 'advance', toMilliseconds: engine.state.currentTimeMilliseconds + 2_000 });
|
||||
const answer = engine.receiveTcp(socket);
|
||||
if (answer !== undefined) answers.push(new TextDecoder().decode(answer));
|
||||
};
|
||||
receive();
|
||||
for (const message of messages) {
|
||||
engine.sendTcp(socket, new TextEncoder().encode(message));
|
||||
receive();
|
||||
}
|
||||
return answers;
|
||||
}
|
||||
|
||||
describe('the e-mail server beyond the cases', () => {
|
||||
it('writes a failed answer as Java does (sw_smtpmitarbeiter_msg14)', () => {
|
||||
const lines: EmailServerLogEntry[] = [];
|
||||
const state = new MailServerState('filius.de', [], 'de', (entry) => lines.push(entry), () => undefined, () => '10.0.0.1', () => 0);
|
||||
const session = new SmtpSession(state, {
|
||||
address: '10.0.0.2',
|
||||
*send(): Steps<boolean> {
|
||||
return false;
|
||||
},
|
||||
});
|
||||
run(session.begin());
|
||||
expect(lines.map((entry) => emailServerText(entry))).toEqual(['----', 'Fehler beim Senden an 10.0.0.2']);
|
||||
});
|
||||
|
||||
it("takes a model's message as Java loads it from its file: the date, the subject trimmed when written, no sender as an empty name", () => {
|
||||
const entry = mailboxEntry({ ...MAIL_ACCOUNTS[0]!.mailbox[0]!, subject: ' Termin ', receivedAtMilliseconds: 1_000, from: { name: null, mailAddress: null } });
|
||||
expect(formatMail(entry.mail)).toBe('From: \nTo: <anna@filius.de>\nSubject: Termin\nDate Received: 1000\n\nErste Zeile\nZweite Zeile');
|
||||
expect(entry.mail.subject).toBe(' Termin ');
|
||||
});
|
||||
});
|
||||
|
||||
describe('the e-mail server in the engine', () => {
|
||||
it('switches SMTP and POP3 on and off together', () => {
|
||||
const engine = new NetworkRuntimeEngine(14);
|
||||
engine.handle({ type: 'start', snapshot: topologySnapshot(mailNetwork()), seed: 14, initialTimeMilliseconds: 0 });
|
||||
const server = engine.emailServer(SERVER)!;
|
||||
expect(server.isActive).toBe(true);
|
||||
server.setActive(false);
|
||||
engine.handle({ type: 'advance', toMilliseconds: 1_000 });
|
||||
expect(server.isActive).toBe(false);
|
||||
expect(talk(engine, 25)).toEqual([]);
|
||||
expect(talk(engine, 110)).toEqual([]);
|
||||
server.setActive(true);
|
||||
expect(talk(engine, 25, 'QUIT')).toEqual(['220 Willkommen bei filius.de', '221 Server beendet Verbindung.']);
|
||||
expect(talk(engine, 110, 'QUIT')).toEqual(['+OK POP3 server ready', '+OK']);
|
||||
});
|
||||
|
||||
it('keeps its mailboxes in the snapshot: a mail taken, a mail deleted; a new start begins from there', () => {
|
||||
const engine = new NetworkRuntimeEngine(14);
|
||||
engine.handle({ type: 'start', snapshot: topologySnapshot({ ...mailNetwork(), language: 'en' }), seed: 14, initialTimeMilliseconds: 0 });
|
||||
talk(engine, 25, 'HELO x', 'MAIL FROM:<anna@filius.de>', 'RCPT TO:<bob@filius.de>', 'DATA', 'Bcc: <bob@filius.de>\nSubject: Neu\n\nText\r\n.\r\n', 'QUIT');
|
||||
talk(engine, 25, 'HELO x', 'MAIL FROM:<anna@filius.de>', 'RCPT TO:<anna@filius.de>', 'DATA', 'Ohne Kopf\n.\n', 'QUIT');
|
||||
const mailbox = (username = 'bob') => engine.state.topologySnapshot.emailServersByNodeId.get(SERVER)!.accounts.find((account) => account.username === username)!.mailbox;
|
||||
// Stored as Java reads the mail anew: no Bcc, no "\r".
|
||||
expect(mailbox()).toEqual([expect.objectContaining({ id: 4, bcc: [], subject: 'Neu', body: 'Text\n', isNew: true, isMarkedForDeletion: false })]);
|
||||
expect(mailbox('anna').at(-1)).toEqual(expect.objectContaining({ from: { name: null, mailAddress: null }, subject: '', body: 'Ohne Kopf\n' }));
|
||||
expect(talk(engine, 110, 'USER bob', 'PASS passwort', 'RETR 0', 'DELE 0')).toEqual([
|
||||
'+OK POP3 server ready',
|
||||
'+OK enter password',
|
||||
'+OK Mailbox locked and ready',
|
||||
'+OK message follows \nSubject: Neu\n\nText',
|
||||
'+OK message marked for delete',
|
||||
]);
|
||||
expect(mailbox()).toEqual([expect.objectContaining({ isNew: false, isMarkedForDeletion: true })]);
|
||||
engine.handle({ type: 'stop' });
|
||||
engine.handle({ type: 'start', snapshot: engine.state.topologySnapshot, seed: 14, initialTimeMilliseconds: 0 });
|
||||
// As Java loads its file: every message new, none deleted; no sender is an empty name ("Text\n", "", "Neu", "[]").
|
||||
expect(talk(engine, 110, 'USER bob', 'PASS passwort', 'STAT', 'DELE 0', 'QUIT')).toEqual([
|
||||
'+OK POP3 server ready',
|
||||
'+OK enter password',
|
||||
'+OK Mailbox locked and ready',
|
||||
'+OK 1 10',
|
||||
'+OK message marked for delete',
|
||||
'+OK',
|
||||
]);
|
||||
expect(mailbox()).toEqual([]);
|
||||
});
|
||||
|
||||
it("writes a notice's date in the project's language, at the wall clock's time", () => {
|
||||
const at = new Date(2026, 9, 1, 18, 5).getTime();
|
||||
const notice = (language: PageLanguage) => {
|
||||
const accounts: MailAccount[] = MAIL_ACCOUNTS.map((account) => ({ ...account, mailbox: [] }));
|
||||
const state = new MailServerState('filius.de', accounts, language, () => undefined, () => undefined, () => '10.0.0.1', () => at);
|
||||
state.deliver(parseMail('Subject: x\n\ny\n'), 'anna@filius.de', ['niemand@filius.de']);
|
||||
return formatMail(accounts[0]!.mailbox[0]!.mail);
|
||||
};
|
||||
expect(notice('de')).toContain('\nDate: 01.10.26, 18:05\nMail-Server: 10.0.0.1\n');
|
||||
expect(notice('en')).toContain('\nDate: 01/10/2026, 18:05\n');
|
||||
expect(notice('de')).toContain('To: <anna@filius.de>\n');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
// The accounts of tools/java-oracle's EmailRun and MailCases (EmailRun.ACCOUNTS,
|
||||
// Java's /mailserver/konten.txt) as the model holds them: anna with a mail
|
||||
// from bob and one from carla, bob without mail.
|
||||
|
||||
import type { EmailServerAccount } from '../src/index.js';
|
||||
|
||||
const message = (fields: Partial<EmailServerAccount['mailbox'][number]>): EmailServerAccount['mailbox'][number] => ({
|
||||
id: 0,
|
||||
from: { name: null, mailAddress: null },
|
||||
to: [],
|
||||
cc: [],
|
||||
bcc: [],
|
||||
subject: '',
|
||||
body: '',
|
||||
isNew: true,
|
||||
isMarkedForDeletion: false,
|
||||
isSent: false,
|
||||
...fields,
|
||||
});
|
||||
|
||||
export const MAIL_ACCOUNTS: readonly EmailServerAccount[] = [
|
||||
{
|
||||
username: 'anna',
|
||||
password: 'geheim',
|
||||
firstName: 'Anna',
|
||||
lastName: 'Muster',
|
||||
mailbox: [
|
||||
message({ id: 1, from: { name: 'Bob Beispiel', mailAddress: 'bob@filius.de' }, to: [{ name: null, mailAddress: 'anna@filius.de' }], subject: 'Hallo Anna', body: 'Erste Zeile\nZweite Zeile' }),
|
||||
message({
|
||||
id: 2,
|
||||
from: { name: 'Carla', mailAddress: 'carla@andere.de' },
|
||||
to: [
|
||||
{ name: 'Anna Muster', mailAddress: 'anna@filius.de' },
|
||||
{ name: null, mailAddress: 'bob@filius.de' },
|
||||
],
|
||||
cc: [{ name: null, mailAddress: 'chef@filius.de' }],
|
||||
subject: 'Termin',
|
||||
body: 'Morgen um 10?',
|
||||
}),
|
||||
],
|
||||
},
|
||||
{ username: 'bob', password: 'passwort', firstName: 'Bob', lastName: 'Beispiel', mailbox: [] },
|
||||
];
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// message bundle says them: for comparing the core's structured lines with
|
||||
// what the Java oracle recorded (echoJava.test.ts, webJava.test.ts).
|
||||
|
||||
import type { EchoServerLogEntry, SendFailure, SimpleClientLogEntry, WebBrowserLogEntry } from '../src/index.js';
|
||||
import type { EchoServerLogEntry, EmailServerLogEntry, PageLanguage, SendFailure, SimpleClientLogEntry, WebBrowserLogEntry } from '../src/index.js';
|
||||
|
||||
/** Java's client and server look every 100 ms whether they are connected, or switched off; the core at once. */
|
||||
export const POLL_MILLISECONDS = 100;
|
||||
@@ -88,3 +88,19 @@ export function browserText(entry: WebBrowserLogEntry): string | undefined {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The e-mail server's line as Java's window shows it (sw_smtpmitarbeiter_msg12 to msg14 in both languages). */
|
||||
export function emailServerText(entry: EmailServerLogEntry, language: PageLanguage = 'de'): string {
|
||||
switch (entry.kind) {
|
||||
case 'connectionOpened':
|
||||
return '----';
|
||||
case 'received':
|
||||
return `${entry.address}< ${entry.text}`;
|
||||
case 'sent':
|
||||
return `${entry.address}> ${entry.text}`;
|
||||
case 'sendFailed':
|
||||
return language === 'de' ? `Fehler beim Senden an ${entry.address}` : `Error while sending ${entry.address}`;
|
||||
case 'stored':
|
||||
return language === 'de' ? `E-Mail für ${entry.username} im Postfach gespeichert.` : `E-mail for ${entry.username} saved in mailbox.`;
|
||||
}
|
||||
}
|
||||
|
||||
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
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"seed": 61,
|
||||
"nodes": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000201",
|
||||
"kind": "pc",
|
||||
"ports": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000211",
|
||||
"label": "eth0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000202",
|
||||
"kind": "pc",
|
||||
"ports": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000212",
|
||||
"label": "eth0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000203",
|
||||
"kind": "pc",
|
||||
"ports": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000213",
|
||||
"label": "eth0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000204",
|
||||
"kind": "pc",
|
||||
"ports": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000214",
|
||||
"label": "eth0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000205",
|
||||
"kind": "networkSwitch",
|
||||
"ports": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000221",
|
||||
"label": "sw1"
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000222",
|
||||
"label": "sw2"
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000223",
|
||||
"label": "sw3"
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000224",
|
||||
"label": "sw4"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-00000000F201",
|
||||
"sourceNode": "00000000-0000-0000-0000-000000000201",
|
||||
"sourcePort": "00000000-0000-0000-0000-000000000211",
|
||||
"targetNode": "00000000-0000-0000-0000-000000000205",
|
||||
"targetPort": "00000000-0000-0000-0000-000000000221"
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-00000000F202",
|
||||
"sourceNode": "00000000-0000-0000-0000-000000000202",
|
||||
"sourcePort": "00000000-0000-0000-0000-000000000212",
|
||||
"targetNode": "00000000-0000-0000-0000-000000000205",
|
||||
"targetPort": "00000000-0000-0000-0000-000000000222"
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-00000000F203",
|
||||
"sourceNode": "00000000-0000-0000-0000-000000000203",
|
||||
"sourcePort": "00000000-0000-0000-0000-000000000213",
|
||||
"targetNode": "00000000-0000-0000-0000-000000000205",
|
||||
"targetPort": "00000000-0000-0000-0000-000000000223"
|
||||
},
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-00000000F204",
|
||||
"sourceNode": "00000000-0000-0000-0000-000000000204",
|
||||
"sourcePort": "00000000-0000-0000-0000-000000000214",
|
||||
"targetNode": "00000000-0000-0000-0000-000000000205",
|
||||
"targetPort": "00000000-0000-0000-0000-000000000224"
|
||||
}
|
||||
],
|
||||
"devices": {
|
||||
"00000000-0000-0000-0000-000000000201": {
|
||||
"ip": "192.168.0.20",
|
||||
"mask": "255.255.255.0",
|
||||
"dns": "192.168.0.10"
|
||||
},
|
||||
"00000000-0000-0000-0000-000000000202": {
|
||||
"ip": "192.168.0.10",
|
||||
"mask": "255.255.255.0"
|
||||
},
|
||||
"00000000-0000-0000-0000-000000000203": {
|
||||
"ip": "192.168.0.30",
|
||||
"mask": "255.255.255.0"
|
||||
},
|
||||
"00000000-0000-0000-0000-000000000204": {
|
||||
"ip": "192.168.0.31",
|
||||
"mask": "255.255.255.0"
|
||||
}
|
||||
},
|
||||
"dnsServers": {
|
||||
"00000000-0000-0000-0000-000000000202": {
|
||||
"records": [
|
||||
"www.filius.de A 192.168.0.30",
|
||||
"seite.filius.de A 192.168.0.30",
|
||||
"andere.filius.de A 192.168.0.30",
|
||||
"leer.filius.de A 192.168.0.31"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user