feat(core): the e-mail program after Java, measured against Java
The program sends to its account's SMTP server, one connection for each recipient, and fetches its mails by POP3; the server now sends mails on to other domains by MX and returns notices to their senders. The program's folders are in the snapshot and go back into the project. EmailClientRun plays six stories with two servers; the mail oracles run in the language's locale, so Java's notice dates match the core's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,7 @@ export * from './runtime/apps/web.js';
|
||||
export * from './runtime/apps/routerPages.js';
|
||||
export * from './runtime/apps/firewallSettings.js';
|
||||
export * from './runtime/apps/emailServer.js';
|
||||
export * from './runtime/apps/emailClient.js';
|
||||
export * from './runtime/email.js';
|
||||
export * from './runtime/http.js';
|
||||
export * from './util/equal.js';
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
// FILIUS' e-mail program (filius.software.email: EmailAnwendung, SMTPClient,
|
||||
// POP3Client) and the SMTP client its server sends mails on with. The
|
||||
// program sends a mail to its account's SMTP server, one connection for each
|
||||
// recipient, and fetches the account's mails from its POP3 server (each
|
||||
// retrieved, then deleted). The window shows a failure's message; the
|
||||
// program also writes how each connection went, as Java tells its
|
||||
// observers.
|
||||
//
|
||||
// Departures from Java (emailClientJava.test.ts names each):
|
||||
// - a connection attempt sends three SYNs, as every TCP connection in the
|
||||
// core (Java's client sends one, tcpJava.test.ts);
|
||||
// - the account's SMTP port counts (Java always connects to 25).
|
||||
|
||||
import type { Id } from '../../ids.js';
|
||||
import type { EmailClientAccount, EmailMessage } from '../config.js';
|
||||
import { addressEntry, addressList, emailMessage, formatMail, isMailAddress, javaSplit, parseMail, type Mail, type MailboxEntry } from '../email.js';
|
||||
import type { NetworkRuntimeEngine } from '../engine.js';
|
||||
import { javaTrim } from '../http.js';
|
||||
import { pause, type Steps } from '../threads.js';
|
||||
import type { ConnectFailure } from './echo.js';
|
||||
import { CommandThread, ProgramLog } from './program.js';
|
||||
|
||||
export const SMTP_SERVER_PORT = 25;
|
||||
|
||||
/** Why a mail could not be sent or fetched: the server's answer, or the connection. */
|
||||
export type MailFailure =
|
||||
/** An answer the client did not expect (Java throws Exception(answer)). */
|
||||
| { readonly kind: 'answer'; readonly text: string }
|
||||
/** The connection could not be made (TCPSocket's exceptions). */
|
||||
| { readonly kind: 'connect'; readonly failure: ConnectFailure }
|
||||
/** The connection ended while the client waited for an answer (sw_tcpsocket_msg9). */
|
||||
| { readonly kind: 'receiveFailed' };
|
||||
|
||||
/** What the SMTP client tells the program it works for (EmailAnwendung or EmailServer). */
|
||||
export type SmtpClientLogEntry =
|
||||
/** EmailServer.LINE_SEPARATOR: a connection begins. */
|
||||
| { readonly kind: 'connectionStarting' }
|
||||
/** sw_smtpclient_msg1, msg2. */
|
||||
| { readonly kind: 'smtpConnected'; readonly address: string; readonly port: number }
|
||||
/** sw_smtpclient_msg4 and the recipient. */
|
||||
| { readonly kind: 'mailSent'; readonly recipients: string }
|
||||
/** sw_smtpclient_msg1, msg3. */
|
||||
| { readonly kind: 'smtpDisconnected'; readonly address: string; readonly port: number }
|
||||
| { readonly kind: 'failed'; readonly failure: MailFailure };
|
||||
|
||||
export type EmailClientLogEntry = SmtpClientLogEntry;
|
||||
|
||||
const encode = (text: string) => new TextEncoder().encode(text);
|
||||
const decode = (bytes: Uint8Array) => new TextDecoder().decode(bytes);
|
||||
|
||||
/** EingabenUeberpruefung.musterMindEinZeichen: at least one character, no line break. */
|
||||
const hasCharacters = (text: string) => /^[^\n\r\u0085\u2028\u2029]+$/.test(text);
|
||||
|
||||
/** A thrown failure of a dialogue (Java's exceptions). */
|
||||
class DialogueFailure extends Error {
|
||||
constructor(readonly failure: MailFailure) {
|
||||
super(failure.kind);
|
||||
}
|
||||
}
|
||||
|
||||
/** A client's connection to a server, as Java's TCPSocket of a ClientAnwendung. */
|
||||
class ClientConnection {
|
||||
private constructor(
|
||||
private readonly engine: NetworkRuntimeEngine,
|
||||
readonly socketId: Id,
|
||||
readonly address: string,
|
||||
readonly port: number,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* initialisiereSocket: the address (or name) resolved in the program's
|
||||
* thread, then connected by the new socket's thread, which begins after
|
||||
* what is already awake at this time (the last connection's close sends
|
||||
* its FIN first); the failure otherwise.
|
||||
*/
|
||||
static *open(engine: NetworkRuntimeEngine, nodeId: Id, host: string, port: number): Steps<ClientConnection | ConnectFailure> {
|
||||
const address = yield* engine.resolveHostSteps(nodeId, host);
|
||||
if (address === undefined) return 'unresolved';
|
||||
yield pause(engine.state.currentTimeMilliseconds);
|
||||
const socketId = engine.openTcpClientSocket(nodeId, address, port);
|
||||
const result = socketId === undefined ? 'invalidSocket' : yield* engine.connectTcpSteps(socketId);
|
||||
if (result !== 'connected') return result === 'timedOut' ? 'timedOut' : 'failed';
|
||||
return new ClientConnection(engine, socketId!, address, port);
|
||||
}
|
||||
|
||||
*send(text: string): Steps {
|
||||
yield* this.engine.sendTcpSteps(this.socketId, encode(text));
|
||||
}
|
||||
|
||||
/** TCPSocket.empfangen: the next message; a failure when the connection ends. */
|
||||
*receive(): Steps<string> {
|
||||
const received = yield* this.engine.receiveTcpSteps(this.socketId);
|
||||
if (received.type !== 'message') throw new DialogueFailure({ kind: 'receiveFailed' });
|
||||
return decode(received.payload);
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.engine.requestTcpClose(this.socketId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SMTPClient.initialisiereSocket, versenden, schliesseSocket: one mail to
|
||||
* one server for the recipients given (a comma list), told to `log`; true
|
||||
* when the server took it.
|
||||
*/
|
||||
export function* sendMailSteps(
|
||||
engine: NetworkRuntimeEngine,
|
||||
nodeId: Id,
|
||||
log: (entry: SmtpClientLogEntry) => void,
|
||||
server: string,
|
||||
port: number,
|
||||
mail: Mail,
|
||||
sender: string,
|
||||
recipients: string,
|
||||
): Steps<boolean> {
|
||||
log({ kind: 'connectionStarting' });
|
||||
const connection = yield* ClientConnection.open(engine, nodeId, server, port);
|
||||
if (typeof connection === 'string') {
|
||||
log({ kind: 'failed', failure: { kind: 'connect', failure: connection } });
|
||||
return false;
|
||||
}
|
||||
log({ kind: 'smtpConnected', address: connection.address, port: connection.port });
|
||||
let sent = false;
|
||||
try {
|
||||
sent = yield* smtpDialogue(engine, nodeId, connection, mail, sender, recipients);
|
||||
log({ kind: 'mailSent', recipients });
|
||||
} catch (error) {
|
||||
if (!(error instanceof DialogueFailure)) throw error;
|
||||
log({ kind: 'failed', failure: error.failure });
|
||||
}
|
||||
connection.close();
|
||||
log({ kind: 'smtpDisconnected', address: connection.address, port: connection.port });
|
||||
return sent;
|
||||
}
|
||||
|
||||
/** SMTPClient.versenden: HELO, MAIL FROM, RCPT TO, DATA, the mail, QUIT; false where Java gives up without a word. */
|
||||
function* smtpDialogue(engine: NetworkRuntimeEngine, nodeId: Id, connection: ClientConnection, mail: Mail, sender: string, recipients: string): Steps<boolean> {
|
||||
const expect = function* (command: string, accepted: (answer: string) => boolean): Steps {
|
||||
yield* connection.send(command);
|
||||
const answer = yield* connection.receive();
|
||||
if (!accepted(answer)) throw new DialogueFailure({ kind: 'answer', text: answer });
|
||||
};
|
||||
const code = (answer: string) => answer.slice(0, 3);
|
||||
if (!(yield* connection.receive()).startsWith('220')) return false;
|
||||
yield* expect(`HELO ${engine.networkInterfaces(nodeId)[0]?.ipAddress ?? 'null'}`, (answer) => answer.startsWith('250'));
|
||||
const senderAddress = addressEntry(sender).mailAddress;
|
||||
if (senderAddress === null || (senderAddress !== '' && !isMailAddress(senderAddress))) return false;
|
||||
yield* expect(`MAIL FROM: <${senderAddress}>`, (answer) => code(answer) === '250');
|
||||
for (const recipient of javaSplit(recipients, ',')) {
|
||||
if (!isMailAddress(recipient)) return false;
|
||||
yield* expect(`RCPT TO:<${recipient}>`, (answer) => code(answer) === '250' || code(answer) === '251');
|
||||
}
|
||||
yield* expect('DATA', (answer) => code(answer) === '354');
|
||||
yield* expect(`${formatMail(mail)}\r\n.\r\n`, (answer) => answer.startsWith('250'));
|
||||
yield* connection.send('QUIT');
|
||||
yield* connection.receive();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* POP3Client.emailsHolen: connected to the server, logged in, each mail
|
||||
* retrieved (and handed to `received`) and deleted, then QUIT; a failure
|
||||
* told to `log`.
|
||||
*/
|
||||
export function* fetchMailSteps(
|
||||
engine: NetworkRuntimeEngine,
|
||||
nodeId: Id,
|
||||
log: (entry: EmailClientLogEntry) => void,
|
||||
server: string,
|
||||
port: number,
|
||||
username: string,
|
||||
password: string,
|
||||
received: (mail: Mail) => void,
|
||||
): Steps {
|
||||
const connection = yield* ClientConnection.open(engine, nodeId, server, port);
|
||||
if (typeof connection === 'string') {
|
||||
log({ kind: 'failed', failure: { kind: 'connect', failure: connection } });
|
||||
return;
|
||||
}
|
||||
const expect = function* (command: string): Steps<string> {
|
||||
yield* connection.send(command);
|
||||
const answer = yield* connection.receive();
|
||||
if (!answer.startsWith('+OK')) throw new DialogueFailure({ kind: 'answer', text: answer });
|
||||
return answer;
|
||||
};
|
||||
try {
|
||||
// Java's chain of erfolg: a step that cannot be taken ends it without a word; QUIT comes in any case.
|
||||
if ((yield* connection.receive()).startsWith('+') && hasCharacters(username)) {
|
||||
yield* expect(`USER ${username}`);
|
||||
if (hasCharacters(password)) {
|
||||
yield* expect(`PASS ${password}`);
|
||||
const count = Number.parseInt(javaSplit(yield* expect('STAT'), ' ')[1] ?? '', 10);
|
||||
const numbers =
|
||||
count > 0
|
||||
? javaSplit(yield* expect('LIST'), '\n')
|
||||
.slice(1)
|
||||
.map((line) => Number.parseInt(javaSplit(line, ' ')[0] ?? '', 10))
|
||||
: [];
|
||||
for (const number of numbers) {
|
||||
const answer = yield* expect(`RETR ${number}`);
|
||||
const lineEnd = answer.indexOf('\n');
|
||||
if (lineEnd > 0) received(parseMail(answer.slice(lineEnd + 1)));
|
||||
yield* expect(`DELE ${number}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
yield* expect('QUIT');
|
||||
} catch (error) {
|
||||
if (!(error instanceof DialogueFailure)) throw error;
|
||||
log({ kind: 'failed', failure: error.failure });
|
||||
}
|
||||
connection.close();
|
||||
}
|
||||
|
||||
/** What the program's window asks for a mail (GUIApplicationEmailAnwendungWindow). */
|
||||
export interface MailForm {
|
||||
readonly to: string;
|
||||
readonly cc: string;
|
||||
readonly bcc: string;
|
||||
readonly subject: string;
|
||||
readonly text: string;
|
||||
}
|
||||
|
||||
/** Why the window does not send (emailanwendung_msg47, msg20, msg21). */
|
||||
export type MailFormProblem = 'noAccount' | 'invalidAddress' | 'noRecipient';
|
||||
|
||||
/** The window's mailPruefen: each address of the field, if any, as Java's pattern wants it. */
|
||||
const validField = (field: string) => javaTrim(field) === '' || javaSplit(field, ',').every((each) => isMailAddress(javaTrim(each)));
|
||||
|
||||
/** A mail of the program's folders, as the model holds it. */
|
||||
function entryOf(message: EmailMessage): MailboxEntry {
|
||||
const sender = message.from.name === null && message.from.mailAddress === null ? null : message.from;
|
||||
return {
|
||||
mail: {
|
||||
sender,
|
||||
to: message.to,
|
||||
cc: message.cc,
|
||||
bcc: message.bcc,
|
||||
subject: message.subject,
|
||||
text: message.body,
|
||||
dateReceived: message.receivedAtMilliseconds === undefined ? '' : String(message.receivedAtMilliseconds),
|
||||
},
|
||||
isNew: message.isNew,
|
||||
isMarkedForDeletion: message.isMarkedForDeletion,
|
||||
};
|
||||
}
|
||||
|
||||
/** The program's folders back in the model: numbered from 1, inbox, sent, drafts. */
|
||||
export interface EmailClientFolders {
|
||||
readonly inbox: readonly EmailMessage[];
|
||||
readonly sent: readonly EmailMessage[];
|
||||
readonly drafts: readonly EmailMessage[];
|
||||
readonly nextMessageID: number;
|
||||
}
|
||||
|
||||
/** EmailAnwendung: its account, inbox and sent mails. */
|
||||
export class EmailClient {
|
||||
readonly log: ProgramLog<EmailClientLogEntry>;
|
||||
readonly account: EmailClientAccount | null;
|
||||
readonly inbox: MailboxEntry[];
|
||||
readonly sent: MailboxEntry[];
|
||||
private readonly drafts: readonly EmailMessage[];
|
||||
/** The SMTP client's and the POP3 client's threads. */
|
||||
private readonly smtp: CommandThread;
|
||||
private readonly pop3: CommandThread;
|
||||
|
||||
constructor(
|
||||
private readonly engine: NetworkRuntimeEngine,
|
||||
private readonly nodeId: Id,
|
||||
configuration: { readonly account: EmailClientAccount | null; readonly inbox: readonly EmailMessage[]; readonly sent: readonly EmailMessage[]; readonly drafts: readonly EmailMessage[] },
|
||||
private readonly changed: (folders: EmailClientFolders) => void,
|
||||
) {
|
||||
this.log = new ProgramLog(engine);
|
||||
this.account = configuration.account;
|
||||
this.inbox = configuration.inbox.map(entryOf);
|
||||
this.sent = configuration.sent.map(entryOf);
|
||||
this.drafts = configuration.drafts;
|
||||
this.smtp = new CommandThread(engine);
|
||||
this.pop3 = new CommandThread(engine);
|
||||
}
|
||||
|
||||
/**
|
||||
* The window's send button: the mail made from the form (the account's
|
||||
* name and address as sender), sent to the account's SMTP server, one
|
||||
* connection for each recipient (EmailAnwendung.versendeEmail).
|
||||
*/
|
||||
send(form: MailForm): MailFormProblem | undefined {
|
||||
const account = this.account;
|
||||
if (account === null) return 'noAccount';
|
||||
if (![form.to, form.cc, form.bcc].every(validField)) return 'invalidAddress';
|
||||
const mail: Mail = {
|
||||
// AddressEntry trims the name: without a last name, Java's window leaves out the space.
|
||||
sender: addressEntry(`${account.firstName} ${account.lastName} <${account.email}>`),
|
||||
to: addressList(form.to),
|
||||
cc: addressList(form.cc),
|
||||
bcc: addressList(form.bcc),
|
||||
subject: form.subject,
|
||||
text: form.text,
|
||||
dateReceived: '',
|
||||
};
|
||||
if (mail.to.length + mail.cc.length + mail.bcc.length === 0) return 'noRecipient';
|
||||
// Java joins the addresses into a comma list and reads them back (EmailAnwendung.versendeEmail, SMTPClient.versendeEmail).
|
||||
const recipients = [...mail.to, ...mail.cc, ...mail.bcc].map((each) => each.mailAddress).filter((each): each is string => each !== null);
|
||||
const entry: MailboxEntry = { mail, isNew: true, isMarkedForDeletion: false };
|
||||
for (const recipient of recipients) {
|
||||
this.smtp.hand(() => this.sendTo(entry, recipient));
|
||||
}
|
||||
this.engine.resumeThreads();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private *sendTo(entry: MailboxEntry, recipient: string): Steps {
|
||||
const account = this.account!;
|
||||
const sent = yield* sendMailSteps(this.engine, this.nodeId, (each) => this.log.write(each), account.smtpHost, account.smtpPort, entry.mail, account.email, recipient);
|
||||
if (sent && !this.sent.includes(entry)) {
|
||||
this.sent.push(entry);
|
||||
this.changed(this.folders());
|
||||
}
|
||||
}
|
||||
|
||||
/** The window's fetch button: the account's mails from its POP3 server (EmailAnwendung.emailsAbholenEmails). */
|
||||
fetch(): boolean {
|
||||
const account = this.account;
|
||||
if (account === null) return false;
|
||||
this.pop3.hand(() =>
|
||||
fetchMailSteps(this.engine, this.nodeId, (each) => this.log.write(each), account.pop3Host, account.pop3Port, account.username, account.password, (mail) => {
|
||||
this.inbox.push({ mail, isNew: true, isMarkedForDeletion: false });
|
||||
this.changed(this.folders());
|
||||
}),
|
||||
);
|
||||
this.engine.resumeThreads();
|
||||
return true;
|
||||
}
|
||||
|
||||
/** The folders as the model keeps them. */
|
||||
folders(): EmailClientFolders {
|
||||
let id = 1;
|
||||
const inbox = this.inbox.map((entry) => emailMessage(entry, id++));
|
||||
const sent = this.sent.map((entry) => ({ ...emailMessage(entry, id++), isSent: true }));
|
||||
const drafts = this.drafts.map((message) => ({ ...message, id: id++ }));
|
||||
return { inbox, sent, drafts, nextMessageID: id };
|
||||
}
|
||||
}
|
||||
@@ -30,12 +30,13 @@
|
||||
// connection ends).
|
||||
|
||||
import type { Id } from '../../ids.js';
|
||||
import type { EmailMessage, EmailServerAccount } from '../config.js';
|
||||
import { addressEntry, addressList, addressText, formatMail, javaSplit, mailSize, parseMail, type Mail } from '../email.js';
|
||||
import type { EmailServerAccount } from '../config.js';
|
||||
import { addressEntry, addressText, emailMessage, formatMail, javaSplit, mailboxEntry, mailSize, parseMail, type Mail, type MailboxEntry } from '../email.js';
|
||||
import type { NetworkRuntimeEngine } from '../engine.js';
|
||||
import { javaTrim } from '../http.js';
|
||||
import type { Steps } from '../threads.js';
|
||||
import { ProgramLog } from './program.js';
|
||||
import { pause, type Steps } from '../threads.js';
|
||||
import { sendMailSteps, SMTP_SERVER_PORT, type SmtpClientLogEntry } from './emailClient.js';
|
||||
import { CommandThread, ProgramLog } from './program.js';
|
||||
import type { PageLanguage } from './routerPages.js';
|
||||
import type { EmailServerSnapshot } from '../snapshot.js';
|
||||
import { TcpServerProgram } from './server.js';
|
||||
@@ -54,14 +55,9 @@ export type EmailServerLogEntry =
|
||||
/** sw_smtpmitarbeiter_msg14: the answer could not be sent. */
|
||||
| { readonly kind: 'sendFailed'; readonly address: string }
|
||||
/** sw_smtpmitarbeiter_msg12, msg13: a mail stored in the account's mailbox. */
|
||||
| { readonly kind: 'stored'; readonly username: string };
|
||||
|
||||
/** A message in a mailbox, as POP3 sees it. */
|
||||
export interface MailboxEntry {
|
||||
mail: Mail;
|
||||
isNew: boolean;
|
||||
isMarkedForDeletion: boolean;
|
||||
}
|
||||
| { readonly kind: 'stored'; readonly username: string }
|
||||
/** What the SMTP client tells the server when it sends a mail on. */
|
||||
| SmtpClientLogEntry;
|
||||
|
||||
export interface MailAccount {
|
||||
readonly username: string;
|
||||
@@ -114,6 +110,19 @@ export interface MailConnection {
|
||||
send(text: string): Steps<boolean>;
|
||||
}
|
||||
|
||||
/** How the server sends a mail to other domains (EmailServer.weiterleiten with its SMTPClient). */
|
||||
export interface MailOutbox {
|
||||
/** EmailServer.emailWeiterleiten: later, by the server's own thread. */
|
||||
forward(mail: Mail, sender: string, recipients: readonly string[]): void;
|
||||
/**
|
||||
* SMTPClient.versendeEmail(null, …) in the calling thread: each
|
||||
* recipient's mail exchange looked up (Resolver.holeIPAdresseMailServer),
|
||||
* the mails then sent in a thread of their own, one after the other; the
|
||||
* recipients without a mail exchange.
|
||||
*/
|
||||
lookUpAndSend(mail: Mail, sender: string, recipients: readonly string[]): Steps<string[]>;
|
||||
}
|
||||
|
||||
/** What the server's sessions share: its domain, accounts and window. */
|
||||
export class MailServerState {
|
||||
constructor(
|
||||
@@ -126,6 +135,7 @@ export class MailServerState {
|
||||
/** The server's first address (primaryIPAdresse) and the wall clock, for a notice. */
|
||||
private readonly ipAddress: () => string,
|
||||
private readonly now: () => number,
|
||||
private readonly outbox: MailOutbox,
|
||||
) {}
|
||||
|
||||
/** EmailServer.sucheKonto: by name in any case. */
|
||||
@@ -140,9 +150,14 @@ export class MailServerState {
|
||||
return at >= 0 && address.slice(at + 1).toLowerCase() === this.domain.toLowerCase();
|
||||
}
|
||||
|
||||
/** SMTPMitarbeiter.verarbeiteEmail: each of the domain's recipients gets the mail as Java reads it anew. */
|
||||
deliver(mail: Mail, sender: string, recipients: readonly string[]): void {
|
||||
/**
|
||||
* SMTPMitarbeiter.verarbeiteEmail: each of the domain's recipients gets
|
||||
* the mail as Java reads it anew; the others' go on to their domains (by
|
||||
* the server's thread); the sender hears of the unknown ones.
|
||||
*/
|
||||
*deliver(mail: Mail, sender: string, recipients: readonly string[]): Steps {
|
||||
const unknown: string[] = [];
|
||||
const elsewhere: string[] = [];
|
||||
for (const recipient of recipients) {
|
||||
const account = this.isOwnDomain(recipient) ? this.account(javaSplit(recipient, '@')[0] ?? '') : undefined;
|
||||
if (account !== undefined) {
|
||||
@@ -150,20 +165,31 @@ export class MailServerState {
|
||||
this.log({ kind: 'stored', username: account.username });
|
||||
} else if (this.isOwnDomain(recipient) || !recipient.includes('@')) {
|
||||
unknown.push(recipient);
|
||||
} else {
|
||||
elsewhere.push(recipient);
|
||||
}
|
||||
// Other domains: sent on with the e-mail program (F3.3b).
|
||||
}
|
||||
this.changed();
|
||||
if (unknown.length > 0) this.notifyUnknown(mail, sender, unknown);
|
||||
// Java sends on the mail as it reads it anew; written for sending, that is the same text.
|
||||
if (elsewhere.length > 0) this.outbox.forward(mail, sender, elsewhere);
|
||||
if (unknown.length > 0) yield* this.notifyUnknown(mail, sender, unknown);
|
||||
}
|
||||
|
||||
/** EmailServer.sendUnknownReceiverResponse: a notice to a sender of the domain; none for an empty sender. */
|
||||
private notifyUnknown(mail: Mail, sender: string, unknown: readonly string[]): void {
|
||||
const address = addressEntry(sender).mailAddress;
|
||||
// An empty sender (<>) has no address of the domain.
|
||||
if (address === null || !this.isOwnDomain(address)) return;
|
||||
const account = this.account(javaSplit(address, '@')[0] ?? '');
|
||||
if (account === undefined) return;
|
||||
/** EmailServer.weiterleiten: the mail sent on; the sender hears of recipients without a mail exchange. */
|
||||
*sendOn(mail: Mail, sender: string, recipients: readonly string[]): Steps {
|
||||
const unknown = yield* this.outbox.lookUpAndSend(mail, sender, recipients);
|
||||
if (unknown.length > 0) yield* this.notifyUnknown(mail, sender, unknown);
|
||||
}
|
||||
|
||||
/**
|
||||
* EmailServer.sendUnknownReceiverResponse: a notice to the sender, stored
|
||||
* when the sender is the domain's, else sent on with an empty sender; none
|
||||
* for an empty sender.
|
||||
*/
|
||||
private *notifyUnknown(mail: Mail, sender: string, unknown: readonly string[]): Steps {
|
||||
const senderEntry = addressEntry(sender);
|
||||
const address = senderEntry.mailAddress;
|
||||
if (address === null || address === '') return;
|
||||
const text =
|
||||
'The mail could not be delivered either because user or mail domain is unknown.\n' +
|
||||
'Message could not be delivered to the following recipients:\n' +
|
||||
@@ -181,6 +207,12 @@ export class MailServerState {
|
||||
text,
|
||||
dateReceived: '',
|
||||
};
|
||||
if (!this.isOwnDomain(address)) {
|
||||
yield* this.sendOn(notice, '<>', [addressText(senderEntry)!]);
|
||||
return;
|
||||
}
|
||||
const account = this.account(javaSplit(address, '@')[0] ?? '');
|
||||
if (account === undefined) return;
|
||||
account.mailbox.push({ mail: notice, isNew: true, isMarkedForDeletion: false });
|
||||
this.log({ kind: 'stored', username: account.username });
|
||||
this.changed();
|
||||
@@ -236,7 +268,7 @@ export class SmtpSession {
|
||||
const recipients = this.recipients;
|
||||
this.state = 'envelope';
|
||||
this.resetEnvelope();
|
||||
this.server.deliver(mail, sender, recipients);
|
||||
yield* this.server.deliver(mail, sender, recipients);
|
||||
} else {
|
||||
this.source += message;
|
||||
}
|
||||
@@ -418,43 +450,6 @@ export class Pop3Session {
|
||||
}
|
||||
}
|
||||
|
||||
/** A message of the model as Java has it after loading the server's file: new, not deleted. */
|
||||
export function mailboxEntry(message: EmailMessage): MailboxEntry {
|
||||
const reread = (entries: EmailMessage['to']) => addressList(entries.map((entry) => addressText(entry) ?? '').join(','));
|
||||
return {
|
||||
mail: {
|
||||
sender: addressEntry(addressText(message.from) ?? ''),
|
||||
to: reread(message.to),
|
||||
cc: reread(message.cc),
|
||||
bcc: reread(message.bcc),
|
||||
subject: message.subject,
|
||||
text: message.body,
|
||||
dateReceived: message.receivedAtMilliseconds === undefined ? '' : String(message.receivedAtMilliseconds),
|
||||
},
|
||||
isNew: true,
|
||||
isMarkedForDeletion: false,
|
||||
};
|
||||
}
|
||||
|
||||
/** A mailbox's message back in the model, numbered by the caller. */
|
||||
export function emailMessage(entry: MailboxEntry, id: number): EmailMessage {
|
||||
const { mail } = entry;
|
||||
const received = /^[0-9]+$/.test(mail.dateReceived) ? Number(mail.dateReceived) : undefined;
|
||||
return {
|
||||
id,
|
||||
from: mail.sender ?? { name: null, mailAddress: null },
|
||||
to: mail.to,
|
||||
cc: mail.cc,
|
||||
bcc: mail.bcc,
|
||||
subject: mail.subject ?? '',
|
||||
body: mail.text,
|
||||
...(received === undefined ? {} : { receivedAtMilliseconds: received }),
|
||||
isNew: entry.isNew,
|
||||
isMarkedForDeletion: entry.isMarkedForDeletion,
|
||||
isSent: false,
|
||||
};
|
||||
}
|
||||
|
||||
const encode = (text: string) => new TextEncoder().encode(text);
|
||||
|
||||
/** SMTPServer, POP3Server: a server whose connections each hold a session. */
|
||||
@@ -497,10 +492,12 @@ export class EmailServer {
|
||||
readonly state: MailServerState;
|
||||
private readonly smtp: MailProtocolServer;
|
||||
private readonly pop3: MailProtocolServer;
|
||||
/** The server's own thread (Anwendung.ausfuehren): it sends mails on. */
|
||||
private readonly commands: CommandThread;
|
||||
|
||||
constructor(
|
||||
engine: NetworkRuntimeEngine,
|
||||
nodeId: Id,
|
||||
private readonly engine: NetworkRuntimeEngine,
|
||||
private readonly nodeId: Id,
|
||||
configuration: EmailServerSnapshot,
|
||||
changed: (accounts: readonly EmailServerAccount[]) => void,
|
||||
ipAddress: () => string,
|
||||
@@ -522,7 +519,15 @@ export class EmailServer {
|
||||
() => changed(this.accounts()),
|
||||
ipAddress,
|
||||
now,
|
||||
{
|
||||
forward: (mail, sender, recipients) => {
|
||||
this.commands.hand(() => this.state.sendOn(mail, sender, recipients));
|
||||
engine.resumeThreads();
|
||||
},
|
||||
lookUpAndSend: (mail, sender, recipients) => this.lookUpAndSend(mail, sender, recipients),
|
||||
},
|
||||
);
|
||||
this.commands = new CommandThread(engine);
|
||||
// POP3 first, as EmailServer.starten (its socket draws the first initial sequence number).
|
||||
this.pop3 = new MailProtocolServer(engine, nodeId, POP3_PORT, configuration.isActive, (connection) => new Pop3Session(this.state, connection));
|
||||
this.smtp = new MailProtocolServer(engine, nodeId, SMTP_PORT, configuration.isActive, (connection) => new SmtpSession(this.state, connection));
|
||||
@@ -537,6 +542,39 @@ export class EmailServer {
|
||||
this.smtp.setActive(active);
|
||||
}
|
||||
|
||||
/**
|
||||
* SMTPClient.versendeEmail(null, …): each recipient's mail exchange looked
|
||||
* up here, then a new SMTP client sends the mail to each, one after the
|
||||
* other, in its own thread; the recipients without a mail exchange.
|
||||
*/
|
||||
private *lookUpAndSend(mail: Mail, sender: string, recipients: readonly string[]): Steps<string[]> {
|
||||
const targets: [string, string][] = [];
|
||||
const unknown: string[] = [];
|
||||
for (const each of javaSplit(recipients.join(','), ',')) {
|
||||
if (javaTrim(each) === '') continue;
|
||||
const recipient = addressEntry(javaTrim(each)).mailAddress;
|
||||
if (recipient === null) continue;
|
||||
const domain = javaSplit(recipient, '@')[1];
|
||||
const resolution = domain === undefined ? undefined : yield* this.engine.resolveDnsMailServerSteps(this.nodeId, domain);
|
||||
const address = resolution?.type === 'resolved' ? resolution.address : undefined;
|
||||
if (address === undefined) unknown.push(recipient);
|
||||
else targets.push([address, recipient]);
|
||||
}
|
||||
if (targets.length > 0) {
|
||||
const engine = this.engine;
|
||||
const nodeId = this.nodeId;
|
||||
const log = (entry: SmtpClientLogEntry) => this.log.write(entry);
|
||||
engine.startThread(
|
||||
(function* (): Steps {
|
||||
// A new thread: it begins after the one that made it goes on (the worker reads its next message first).
|
||||
yield pause(engine.state.currentTimeMilliseconds);
|
||||
for (const [address, recipient] of targets) yield* sendMailSteps(engine, nodeId, log, address, SMTP_SERVER_PORT, mail, sender, recipient);
|
||||
})(),
|
||||
);
|
||||
}
|
||||
return unknown;
|
||||
}
|
||||
|
||||
/** The accounts as the model keeps them. */
|
||||
accounts(): EmailServerAccount[] {
|
||||
let id = 1;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
// and reads the fields after it as text); a line that is no field begins the
|
||||
// text (Java drops it).
|
||||
|
||||
import type { EmailAddress } from './config.js';
|
||||
import type { EmailAddress, EmailMessage } from './config.js';
|
||||
import { javaTrim } from './http.js';
|
||||
|
||||
/** An e-mail as Java's Email holds it while the programs run. */
|
||||
@@ -63,6 +63,14 @@ export function addressList(text: string): EmailAddress[] {
|
||||
.map(addressEntry);
|
||||
}
|
||||
|
||||
const RAW_MAIL_ADDRESS = '[a-zA-Z0-9]([\\-_\\.]{0,1}[a-zA-Z0-9])*@[a-zA-Z0-9]([\\-_\\.]{0,1}[a-zA-Z0-9])*\\.{0,1}';
|
||||
const MAIL_ADDRESS_PATTERN = new RegExp(`^(?:[^<]*<${RAW_MAIL_ADDRESS}>|${RAW_MAIL_ADDRESS})$`);
|
||||
|
||||
/** EingabenUeberpruefung.musterEmailAdresse: "Name <address>" or an address. */
|
||||
export function isMailAddress(text: string): boolean {
|
||||
return MAIL_ADDRESS_PATTERN.test(text);
|
||||
}
|
||||
|
||||
/** Email(String): the header fields Java knows, then the text, each line ended by "\n". */
|
||||
export function parseMail(source: string): Mail {
|
||||
let sender: EmailAddress | null = null;
|
||||
@@ -115,3 +123,47 @@ export function mailSize(mail: Mail): number {
|
||||
const sender = mail.sender === null ? 'null' : shown(mail.sender);
|
||||
return (mail.text + sender + (mail.subject ?? 'null') + `[${mail.to.map(shown).join(', ')}]` + mail.dateReceived).length;
|
||||
}
|
||||
|
||||
/** A message in a mailbox, as POP3 sees it. */
|
||||
export interface MailboxEntry {
|
||||
mail: Mail;
|
||||
isNew: boolean;
|
||||
isMarkedForDeletion: boolean;
|
||||
}
|
||||
|
||||
/** A message of the model as Java has it after loading the server's file: new, not deleted. */
|
||||
export function mailboxEntry(message: EmailMessage): MailboxEntry {
|
||||
const reread = (entries: EmailMessage['to']) => addressList(entries.map((entry) => addressText(entry) ?? '').join(','));
|
||||
return {
|
||||
mail: {
|
||||
sender: addressEntry(addressText(message.from) ?? ''),
|
||||
to: reread(message.to),
|
||||
cc: reread(message.cc),
|
||||
bcc: reread(message.bcc),
|
||||
subject: message.subject,
|
||||
text: message.body,
|
||||
dateReceived: message.receivedAtMilliseconds === undefined ? '' : String(message.receivedAtMilliseconds),
|
||||
},
|
||||
isNew: true,
|
||||
isMarkedForDeletion: false,
|
||||
};
|
||||
}
|
||||
|
||||
/** A mailbox's message back in the model, numbered by the caller. */
|
||||
export function emailMessage(entry: MailboxEntry, id: number): EmailMessage {
|
||||
const { mail } = entry;
|
||||
const received = /^[0-9]+$/.test(mail.dateReceived) ? Number(mail.dateReceived) : undefined;
|
||||
return {
|
||||
id,
|
||||
from: mail.sender ?? { name: null, mailAddress: null },
|
||||
to: mail.to,
|
||||
cc: mail.cc,
|
||||
bcc: mail.bcc,
|
||||
subject: mail.subject ?? '',
|
||||
body: mail.text,
|
||||
...(received === undefined ? {} : { receivedAtMilliseconds: received }),
|
||||
isNew: entry.isNew,
|
||||
isMarkedForDeletion: entry.isMarkedForDeletion,
|
||||
isSent: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -116,6 +116,7 @@ import { RuntimeSpeed } from './speed.js';
|
||||
import { ApplicationThreads, waitUntil, type Steps, type Wait } from './threads.js';
|
||||
import { EchoServer, SimpleClient } from './apps/echo.js';
|
||||
import { WebBrowser, WebServer, type WebServerPage } from './apps/web.js';
|
||||
import { EmailClient, type EmailClientFolders } from './apps/emailClient.js';
|
||||
import { EmailServer } from './apps/emailServer.js';
|
||||
import { firewallLogPage, forwardingTablePage, ripTablePage, routerIndexPage } from './apps/routerPages.js';
|
||||
import { applyFirewallSettings, firewallSettingsPage } from './apps/firewallSettings.js';
|
||||
@@ -913,6 +914,7 @@ export class NetworkRuntimeEngine {
|
||||
private readonly webServersByNodeId = new Map<Id, WebServer>();
|
||||
private readonly webBrowsersByNodeId = new Map<Id, WebBrowser>();
|
||||
private readonly emailServersByNodeId = new Map<Id, EmailServer>();
|
||||
private readonly emailClientsByNodeId = new Map<Id, EmailClient>();
|
||||
/** The time of day a program writes into a mail (Java's new Date()). */
|
||||
private readonly wallClock: () => number;
|
||||
/** Each node's firewall log; it outlives a stop and a new start, as Java's log page does. */
|
||||
@@ -1043,6 +1045,7 @@ export class NetworkRuntimeEngine {
|
||||
this.webServersByNodeId.clear();
|
||||
this.webBrowsersByNodeId.clear();
|
||||
this.emailServersByNodeId.clear();
|
||||
this.emailClientsByNodeId.clear();
|
||||
this.arpLookups = [];
|
||||
this.protocolThreads.clear();
|
||||
this.dnsQueriesBySocketId.clear();
|
||||
@@ -1133,6 +1136,20 @@ export class NetworkRuntimeEngine {
|
||||
return this.emailServersByNodeId.get(nodeId);
|
||||
}
|
||||
|
||||
/** The node's e-mail program while the simulation runs, if it has one installed. */
|
||||
emailClient(nodeId: Id): EmailClient | undefined {
|
||||
return this.emailClientsByNodeId.get(nodeId);
|
||||
}
|
||||
|
||||
/** An e-mail program's folders as a run changed them, into the snapshot (and so into the project saved from it). */
|
||||
private setEmailClientFolders(nodeId: Id, folders: EmailClientFolders): void {
|
||||
const clients = new Map(this.stateValue.topologySnapshot.emailClientsByNodeId);
|
||||
const client = clients.get(nodeId);
|
||||
if (client === undefined) return;
|
||||
clients.set(nodeId, { ...client, ...folders });
|
||||
this.stateValue.topologySnapshot = withSnapshot(this.stateValue.topologySnapshot, { emailClientsByNodeId: clients });
|
||||
}
|
||||
|
||||
/** An e-mail server's accounts as a run changed them, into the snapshot (and so into the project saved from it). */
|
||||
private setEmailServerAccounts(nodeId: Id, accounts: readonly EmailServerAccount[]): void {
|
||||
const servers = new Map(this.stateValue.topologySnapshot.emailServersByNodeId);
|
||||
@@ -1153,6 +1170,7 @@ export class NetworkRuntimeEngine {
|
||||
this.webServersByNodeId.clear();
|
||||
this.webBrowsersByNodeId.clear();
|
||||
this.emailServersByNodeId.clear();
|
||||
this.emailClientsByNodeId.clear();
|
||||
for (const node of snapshot.nodes) {
|
||||
const echoServer = snapshot.echoServersByNodeId.get(node.id);
|
||||
if (echoServer !== undefined) this.echoServersByNodeId.set(node.id, new EchoServer(this, node.id, echoServer));
|
||||
@@ -1178,6 +1196,11 @@ export class NetworkRuntimeEngine {
|
||||
),
|
||||
);
|
||||
}
|
||||
const emailClient = snapshot.emailClientsByNodeId.get(node.id);
|
||||
if (emailClient !== undefined) {
|
||||
const nodeId = node.id;
|
||||
this.emailClientsByNodeId.set(nodeId, new EmailClient(this, nodeId, emailClient, (folders) => this.setEmailClientFolders(nodeId, folders)));
|
||||
}
|
||||
if ((node.kind === 'router' || node.kind === 'gateway') && !this.swiftRouters) this.startRouterWebServer(node.id, node.kind);
|
||||
}
|
||||
}
|
||||
@@ -1961,6 +1984,22 @@ export class NetworkRuntimeEngine {
|
||||
return { type: 'resolved', address: exchange === undefined ? undefined : findDnsRecords(exchange.data, 'A', resolution.records)[0]?.data };
|
||||
}
|
||||
|
||||
/** Resolver.holeIPAdresseMailServer in a program's thread (the e-mail server sending a mail on). */
|
||||
*resolveDnsMailServerSteps(nodeId: Id, domain: string): Steps<DnsAddressResolution> {
|
||||
const domainName = domain.endsWith('.') ? domain : `${domain}.`;
|
||||
let lookup: DnsLookup | undefined;
|
||||
const server = this.stateValue.topologySnapshot.deviceConfigurations.get(nodeId)?.dnsServer;
|
||||
this.beginDnsResolution(nodeId, domainName, 'MX', server, (result) => {
|
||||
lookup = result;
|
||||
});
|
||||
yield* waitUntil(() => lookup !== undefined || this.stateValue.phase !== 'running');
|
||||
const result = lookup as DnsLookup | undefined;
|
||||
if (result === undefined || result.type === 'timedOut') return { type: 'timedOut' };
|
||||
const records = [...result.answers, ...result.authority];
|
||||
const exchange = findDnsRecords(domainName, 'MX', records)[0];
|
||||
return { type: 'resolved', address: exchange === undefined ? undefined : findDnsRecords(exchange.data, 'A', records)[0]?.data };
|
||||
}
|
||||
|
||||
/**
|
||||
* DNSQueryAgent.query: one question from a fresh socket; `done` with the
|
||||
* first datagram back, or undefined after five round trips.
|
||||
|
||||
@@ -22,6 +22,7 @@ import type {
|
||||
SwitchConfiguration,
|
||||
WebServerConfiguration,
|
||||
EmailServerAccount,
|
||||
EmailClientConfiguration,
|
||||
} from './config.js';
|
||||
import type { VirtualFileSystem } from '../model/virtualFileSystem.js';
|
||||
import type { PageLanguage } from './apps/routerPages.js';
|
||||
@@ -102,6 +103,8 @@ export interface TopologySnapshot {
|
||||
readonly webBrowserNodeIds: ReadonlySet<Id>;
|
||||
/** The installed e-mail servers, by node; a run's deliveries change their mailboxes here. */
|
||||
readonly emailServersByNodeId: ReadonlyMap<Id, EmailServerSnapshot>;
|
||||
/** The installed e-mail programs, by node; a run's sent and fetched mails change their folders here. */
|
||||
readonly emailClientsByNodeId: ReadonlyMap<Id, EmailClientConfiguration>;
|
||||
/** Each node's files, which its programs read (a web server its pages). */
|
||||
readonly fileSystemsByNodeId: ReadonlyMap<Id, VirtualFileSystem>;
|
||||
/** The language of the pages the devices write themselves (a router's), as Java's in the locale it runs in. */
|
||||
@@ -127,6 +130,7 @@ export interface TopologySnapshotInput {
|
||||
webServersByNodeId?: ReadonlyMap<Id, WebServerSnapshot>;
|
||||
webBrowserNodeIds?: ReadonlySet<Id>;
|
||||
emailServersByNodeId?: ReadonlyMap<Id, EmailServerSnapshot>;
|
||||
emailClientsByNodeId?: ReadonlyMap<Id, EmailClientConfiguration>;
|
||||
fileSystemsByNodeId?: ReadonlyMap<Id, VirtualFileSystem>;
|
||||
language?: PageLanguage;
|
||||
}
|
||||
@@ -151,6 +155,7 @@ export function topologySnapshot(input: TopologySnapshotInput): TopologySnapshot
|
||||
webServersByNodeId: input.webServersByNodeId ?? new Map(),
|
||||
webBrowserNodeIds: input.webBrowserNodeIds ?? new Set(),
|
||||
emailServersByNodeId: input.emailServersByNodeId ?? new Map(),
|
||||
emailClientsByNodeId: input.emailClientsByNodeId ?? new Map(),
|
||||
fileSystemsByNodeId: input.fileSystemsByNodeId ?? new Map(),
|
||||
language: input.language ?? 'de',
|
||||
};
|
||||
|
||||
@@ -3478,7 +3478,7 @@
|
||||
"bob",
|
||||
[
|
||||
[
|
||||
"From: <noreply@filius.de>\nTo: Bob Beispiel <bob@filius.de>\nSubject: Non Delivery Notification\n\nThe mail could not be delivered either because user or mail domain is unknown.\nMessage could not be delivered to the following recipients:\n\tniemand@filius.de\n\tKeiner@filius.de\n\nDate: 10/1/26, 6:55 PM\nMail-Server: 192.168.0.30\n\n______________________________________________________________________\nFrom: Bob Beispiel <bob@filius.de>\nTo: <niemand@filius.de>\nSubject: Wer?\n\nIst da jemand?",
|
||||
"From: <noreply@filius.de>\nTo: Bob Beispiel <bob@filius.de>\nSubject: Non Delivery Notification\n\nThe mail could not be delivered either because user or mail domain is unknown.\nMessage could not be delivered to the following recipients:\n\tniemand@filius.de\n\tKeiner@filius.de\n\nDate: 01.10.26, 19:29\nMail-Server: 192.168.0.30\n\n______________________________________________________________________\nFrom: Bob Beispiel <bob@filius.de>\nTo: <niemand@filius.de>\nSubject: Wer?\n\nIst da jemand?",
|
||||
true,
|
||||
false
|
||||
]
|
||||
|
||||
@@ -3478,7 +3478,7 @@
|
||||
"bob",
|
||||
[
|
||||
[
|
||||
"From: <noreply@filius.de>\nTo: Bob Beispiel <bob@filius.de>\nSubject: Non Delivery Notification\n\nThe mail could not be delivered either because user or mail domain is unknown.\nMessage could not be delivered to the following recipients:\n\tniemand@filius.de\n\tKeiner@filius.de\n\nDate: 10/1/26, 6:55 PM\nMail-Server: 192.168.0.30\n\n______________________________________________________________________\nFrom: Bob Beispiel <bob@filius.de>\nTo: <niemand@filius.de>\nSubject: Wer?\n\nIst da jemand?",
|
||||
"From: <noreply@filius.de>\nTo: Bob Beispiel <bob@filius.de>\nSubject: Non Delivery Notification\n\nThe mail could not be delivered either because user or mail domain is unknown.\nMessage could not be delivered to the following recipients:\n\tniemand@filius.de\n\tKeiner@filius.de\n\nDate: 01/10/2026, 19:29\nMail-Server: 192.168.0.30\n\n______________________________________________________________________\nFrom: Bob Beispiel <bob@filius.de>\nTo: <niemand@filius.de>\nSubject: Wer?\n\nIst da jemand?",
|
||||
true,
|
||||
false
|
||||
]
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
// The e-mail program (apps/emailClient.ts) where emailClientJava.test.ts's
|
||||
// stories do not go: what the window refuses, the account's SMTP port, Bcc,
|
||||
// and the folders as the model keeps them.
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
deviceConfiguration,
|
||||
formatMail,
|
||||
NetworkRuntimeEngine,
|
||||
topologySnapshot,
|
||||
type EmailClientAccount,
|
||||
type EmailClientConfiguration,
|
||||
type TopologySnapshotInput,
|
||||
} from '../src/index.js';
|
||||
import { link, pc, uuid } from './fixtures.js';
|
||||
import { MAIL_ACCOUNTS } from './mailAccounts.js';
|
||||
import { emailClientText } from './programLines.js';
|
||||
|
||||
const SERVER = uuid(0x6001);
|
||||
const HOST = uuid(0x6002);
|
||||
|
||||
const ACCOUNT: EmailClientAccount = {
|
||||
pop3Host: '10.0.0.1',
|
||||
pop3Port: 110,
|
||||
smtpHost: '10.0.0.1',
|
||||
smtpPort: 25,
|
||||
username: 'bob',
|
||||
password: 'passwort',
|
||||
firstName: 'Bob',
|
||||
lastName: '',
|
||||
email: 'bob@filius.de',
|
||||
};
|
||||
|
||||
function started(client: Partial<EmailClientConfiguration>): NetworkRuntimeEngine {
|
||||
const input: TopologySnapshotInput = {
|
||||
nodes: [pc(SERVER, uuid(0x6011)), pc(HOST, uuid(0x6012))],
|
||||
links: [link(SERVER, uuid(0x6011), HOST, uuid(0x6012))],
|
||||
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 }]]),
|
||||
emailClientsByNodeId: new Map([[HOST, { account: ACCOUNT, inbox: [], sent: [], drafts: [], addressBook: [], nextMessageID: 1, ...client }]]),
|
||||
};
|
||||
const engine = new NetworkRuntimeEngine(14);
|
||||
engine.handle({ type: 'start', snapshot: topologySnapshot(input), seed: 14, initialTimeMilliseconds: 0 });
|
||||
return engine;
|
||||
}
|
||||
|
||||
const form = { to: '', cc: '', bcc: '', subject: 'Betreff', text: 'Text' };
|
||||
|
||||
describe('the e-mail program', () => {
|
||||
it('refuses what its window refuses: no account, an address Java does not take, no recipient', () => {
|
||||
expect(started({ account: null }).emailClient(HOST)!.send({ ...form, to: 'anna@filius.de' })).toBe('noAccount');
|
||||
expect(started({ account: null }).emailClient(HOST)!.fetch()).toBe(false);
|
||||
const client = started({}).emailClient(HOST)!;
|
||||
expect(client.send({ ...form, to: 'anna@filius.de, anna@' })).toBe('invalidAddress');
|
||||
expect(client.send({ ...form, cc: 'Anna <anna>' })).toBe('invalidAddress');
|
||||
expect(client.send({ ...form, bcc: 'Anna Muster' })).toBe('invalidAddress');
|
||||
expect(client.send({ ...form, to: ' ' })).toBe('noRecipient');
|
||||
expect(client.send({ ...form, to: 'Anna Muster <anna@filius.de>' })).toBeUndefined();
|
||||
});
|
||||
|
||||
it('sends to a Bcc recipient too, who gets the mail without Bcc; the sent folder keeps it', () => {
|
||||
const engine = started({});
|
||||
expect(engine.emailClient(HOST)!.send({ ...form, to: 'anna@filius.de', bcc: 'bob@filius.de' })).toBeUndefined();
|
||||
engine.handle({ type: 'advance', toMilliseconds: 20_000 });
|
||||
const mailbox = (username: string) => engine.emailServer(SERVER)!.state.accounts.find((each) => each.username === username)!.mailbox;
|
||||
expect(formatMail(mailbox('bob')[0]!.mail)).toBe('From: Bob <bob@filius.de>\nTo: <anna@filius.de>\nSubject: Betreff\n\nText');
|
||||
expect(formatMail(mailbox('anna').at(-1)!.mail)).toBe(formatMail(mailbox('bob')[0]!.mail));
|
||||
expect(engine.emailClient(HOST)!.sent.map((entry) => entry.mail.bcc)).toEqual([[{ name: null, mailAddress: 'bob@filius.de' }]]);
|
||||
});
|
||||
|
||||
it("uses the account's SMTP port (Java always 25): without a server there, the connection fails", () => {
|
||||
const engine = started({ account: { ...ACCOUNT, smtpPort: 2525 } });
|
||||
engine.emailClient(HOST)!.send({ ...form, to: 'anna@filius.de' });
|
||||
engine.handle({ type: 'advance', toMilliseconds: 60_000 });
|
||||
expect(engine.emailClient(HOST)!.log.all.map((line) => emailClientText(line))).toEqual([
|
||||
'----',
|
||||
'filius.exception.TimeOutException: Zeitüberschreitung beim Verbindungsaufbau',
|
||||
]);
|
||||
expect(engine.emailClient(HOST)!.sent).toEqual([]);
|
||||
});
|
||||
|
||||
it('gives up without a word on an account address Java does not take (no MAIL FROM)', () => {
|
||||
const engine = started({ account: { ...ACCOUNT, email: 'bob@' } });
|
||||
engine.emailClient(HOST)!.send({ ...form, to: 'anna@filius.de' });
|
||||
engine.handle({ type: 'advance', toMilliseconds: 20_000 });
|
||||
expect(engine.emailClient(HOST)!.log.all.map((line) => emailClientText(line))).toEqual([
|
||||
'----',
|
||||
'Verbindung zu 10.0.0.1:25 hergestellt',
|
||||
'Mail versendet an anna@filius.de',
|
||||
'Verbindung zu 10.0.0.1:25 getrennt',
|
||||
]);
|
||||
expect(engine.emailServer(SERVER)!.log.all.filter((line) => line.kind === 'received').map((line) => line.kind === 'received' && line.text)).toEqual(['HELO 10.0.0.2']);
|
||||
expect(engine.emailClient(HOST)!.sent).toEqual([]);
|
||||
});
|
||||
|
||||
it('tells of a connection that ends while it waits for an answer', () => {
|
||||
const engine = started({});
|
||||
engine.emailClient(HOST)!.send({ ...form, to: 'anna@filius.de' });
|
||||
// Switched off when the server has taken the connection: it closes it before its greeting arrives.
|
||||
while (!engine.emailServer(SERVER)!.log.all.some((line) => line.kind === 'connectionOpened')) {
|
||||
engine.handle({ type: 'advance', toMilliseconds: engine.state.currentTimeMilliseconds + 1 });
|
||||
}
|
||||
engine.emailServer(SERVER)!.setActive(false);
|
||||
engine.handle({ type: 'advance', toMilliseconds: 20_000 });
|
||||
expect(engine.emailClient(HOST)!.log.all.map((line) => line.kind)).toEqual(['connectionStarting', 'smtpConnected', 'failed', 'smtpDisconnected']);
|
||||
expect(engine.emailClient(HOST)!.log.all[2]).toMatchObject({ failure: { kind: 'receiveFailed' } });
|
||||
});
|
||||
|
||||
it("takes a model's mail without a sender as Java's mail without one (no From)", () => {
|
||||
const engine = started({ inbox: [{ ...MAIL_ACCOUNTS[0]!.mailbox[0]!, from: { name: null, mailAddress: null } }] });
|
||||
expect(formatMail(engine.emailClient(HOST)!.inbox[0]!.mail)).toBe('To: <anna@filius.de>\nSubject: Hallo Anna\n\nErste Zeile\nZweite Zeile');
|
||||
});
|
||||
|
||||
it('keeps its folders in the snapshot: numbered inbox, sent, drafts; the drafts as they were', () => {
|
||||
const draft = { ...MAIL_ACCOUNTS[0]!.mailbox[1]!, id: 9 };
|
||||
const engine = started({ drafts: [draft] });
|
||||
engine.emailClient(HOST)!.send({ ...form, to: 'bob@filius.de' });
|
||||
engine.handle({ type: 'advance', toMilliseconds: 20_000 });
|
||||
engine.emailClient(HOST)!.fetch();
|
||||
engine.handle({ type: 'advance', toMilliseconds: 40_000 });
|
||||
const folders = engine.state.topologySnapshot.emailClientsByNodeId.get(HOST)!;
|
||||
expect(folders.inbox.map((message) => [message.id, message.subject, message.isSent])).toEqual([[1, 'Betreff', false]]);
|
||||
expect(folders.sent.map((message) => [message.id, message.subject, message.isSent])).toEqual([[2, 'Betreff', true]]);
|
||||
expect(folders.drafts).toEqual([{ ...draft, id: 3 }]);
|
||||
expect(folders.nextMessageID).toBe(4);
|
||||
expect(folders.account).toEqual(ACCOUNT);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,259 @@
|
||||
// The e-mail program as Java FILIUS runs it. tools/java-oracle's
|
||||
// EmailClientRun let FILIUS' e-mail program send and fetch mails with two
|
||||
// e-mail servers on mail-lan (filius.de, andere.de; the DNS server knows
|
||||
// both domains' MX) and wrote every frame each card saw, Java's tables of
|
||||
// them, what the program and each server told their windows, and at the end
|
||||
// the program's inbox and sent mails and each server's accounts
|
||||
// (email-client-java-<story>.java.json). The engine plays the same stories
|
||||
// with its programs (apps/emailClient.ts, apps/emailServer.ts): each card's
|
||||
// frames in the same order, with the same content and rows, at about the
|
||||
// same time; the same lines; the same mails at the end.
|
||||
|
||||
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 EmailClientAccount, type LogLine, type MailboxEntry, 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 { emailClientText, emailServerText, 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';
|
||||
const OTHER_SERVER = '00000000-0000-0000-0000-000000000204';
|
||||
|
||||
type Program = 'client' | 'server' | 'otherServer';
|
||||
type JavaMail = [string, string, string];
|
||||
|
||||
interface JavaRun extends JavaTables {
|
||||
frames: Record<string, JavaFrame[]>;
|
||||
events: [number, Program, string][];
|
||||
inbox: JavaMail[];
|
||||
sent: JavaMail[];
|
||||
accounts: [string, JavaMail[]][];
|
||||
otherAccounts: [string, JavaMail[]][];
|
||||
}
|
||||
|
||||
type Act = (engine: NetworkRuntimeEngine) => void;
|
||||
|
||||
const send =
|
||||
(to: string, cc: string, subject: string, text: string): Act =>
|
||||
(engine) => {
|
||||
expect(engine.emailClient(CLIENT)!.send({ to, cc, bcc: '', subject, text })).toBeUndefined();
|
||||
};
|
||||
const fetch: Act = (engine) => {
|
||||
expect(engine.emailClient(CLIENT)!.fetch()).toBe(true);
|
||||
};
|
||||
|
||||
interface Story {
|
||||
readonly host: string;
|
||||
readonly password?: string;
|
||||
readonly acts: readonly [number, Act][];
|
||||
/** The waits Java ends only at a look every 100 ms, one after the other: each connection opened, and the DNS answer of unknownHost (see tolerance). */
|
||||
readonly looks: number;
|
||||
}
|
||||
|
||||
const STORIES: Record<string, Story> = {
|
||||
send: {
|
||||
host: '192.168.0.30',
|
||||
looks: 3,
|
||||
acts: [[500, send('bob@filius.de', 'Carla <carla@andere.de>', 'Treffen', 'Hallo zusammen,\nbis morgen!')]],
|
||||
},
|
||||
fetch: { host: '192.168.0.30', looks: 1, acts: [[500, fetch]] },
|
||||
byName: {
|
||||
host: 'mail.filius.de',
|
||||
looks: 5,
|
||||
acts: [
|
||||
[500, send('niemand@andere.de', '', 'Hallo?', 'Ist da wer?')],
|
||||
[12_000, send('jemand@nirgendwo.de', '', 'Hallo?', 'Ist da wer?')],
|
||||
[25_000, fetch],
|
||||
],
|
||||
},
|
||||
wrongPass: {
|
||||
host: '192.168.0.30',
|
||||
password: 'falsch',
|
||||
looks: 1,
|
||||
acts: [[500, fetch]],
|
||||
},
|
||||
noServer: {
|
||||
host: '192.168.0.10',
|
||||
looks: 1,
|
||||
acts: [[500, send('bob@filius.de', '', 'Ins Leere', 'Niemand da')]],
|
||||
},
|
||||
unknownHost: { host: 'gibtsnicht.filius.de', looks: 1, acts: [[500, send('bob@filius.de', '', 'Ins Leere', 'Niemand da')]] },
|
||||
};
|
||||
|
||||
/** Java's wait for a SYN's answer (TCPSocket: three times 2.75 s, its timeout here). */
|
||||
const JAVA_TIMEOUT_MILLISECONDS = 3 * 2_750;
|
||||
|
||||
/**
|
||||
* Where Java errs: its run corrected, as Java means it. noServer: the
|
||||
* program's SYN goes once in Java, not three times (tcpJava.test.ts,
|
||||
* closedPort); its failure comes two attempts later in the engine.
|
||||
*/
|
||||
const JAVA_BUGS: Record<string, (java: JavaRun) => JavaRun> = {
|
||||
noServer: (java) => ({
|
||||
...java,
|
||||
events: java.events.map(([time, who, text]) => (who === 'client' && text.includes('Exception') ? [time + 2 * JAVA_TIMEOUT_MILLISECONDS, who, text] : [time, who, text])),
|
||||
}),
|
||||
};
|
||||
|
||||
/** Where the frames are compared only up to a time (Java's). noServer: the engine's second SYN, which Java does not send. */
|
||||
const UNTIL: Record<string, (java: JavaRun) => number> = {
|
||||
noServer: (java) => java.events.find(([, who, text]) => who === 'client' && text.includes('Exception'))![0] - 250,
|
||||
};
|
||||
|
||||
/**
|
||||
* Java's client and server look every 100 ms whether a connection is made
|
||||
* (TCPSocket.verbinden, ServerSocket.oeffnen); the engine knows at once. Java
|
||||
* falls behind by up to a look with each such wait of the story (Story.looks).
|
||||
*/
|
||||
const tolerance = (story: Story) => GAP_TOLERANCE_MILLISECONDS + story.looks * POLL_MILLISECONDS;
|
||||
|
||||
/**
|
||||
* byName: which socket draws the next initial sequence number, the program's
|
||||
* second or the server's to andere.de, is a race of Java's threads (both
|
||||
* are made within a few milliseconds); compared below the millions.
|
||||
*/
|
||||
const WITHOUT_INITIAL_NUMBERS = new Set(['byName']);
|
||||
const withoutInitialNumbers = (text: string) =>
|
||||
text
|
||||
.replace(/(seq|ack)=(\d+)/g, (_, name: string, value: string) => `${name}=${Number(value) % 1_000_000}`)
|
||||
.replace(/(SEQ|ACK): \d+\.(\d{3}\.\d{3})/g, '$1: n.$2');
|
||||
|
||||
/** 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');
|
||||
|
||||
/** A frame's text with placeholders: the client's port, a DNS query's random ID, a notice's date. */
|
||||
const frameText = (text: string) => withoutDate(withClientPortText(text)).replace(/ID=\d+/g, 'ID=x');
|
||||
|
||||
type Lines = Record<Program, [number, string][]>;
|
||||
|
||||
function javaLines(java: JavaRun): Lines {
|
||||
const lines: Lines = { client: [], server: [], otherServer: [] };
|
||||
for (const [time, program, text] of java.events) {
|
||||
// Anwendung.starten and the refreshes tell the 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 {
|
||||
client: lines(engine.emailClient(CLIENT)!.log.all, emailClientText),
|
||||
server: lines(engine.emailServer(SERVER)!.log.all, (entry) => emailServerText(entry)),
|
||||
otherServer: lines(engine.emailServer(OTHER_SERVER)!.log.all, (entry) => emailServerText(entry)),
|
||||
};
|
||||
}
|
||||
|
||||
const mails = (entries: readonly MailboxEntry[]): JavaMail[] =>
|
||||
entries.map((entry) => [withoutDate(formatMail(entry.mail)), String(entry.isNew), String(entry.isMarkedForDeletion)]);
|
||||
const javaMails = (list: JavaMail[]): JavaMail[] => list.map(([text, isNew, deleted]) => [withoutDate(text), isNew, deleted]);
|
||||
|
||||
describe('the e-mail program as Java FILIUS runs it', () => {
|
||||
it('corrects Java only where it errs, and cuts only after the story began', () => {
|
||||
for (const [name, correct] of Object.entries(JAVA_BUGS)) {
|
||||
const java = JSON.parse(readFileSync(join(scenarioDirectory, `email-client-java-${name}.java.json`), 'utf8')) as JavaRun;
|
||||
expect(JSON.stringify(correct(java)), name).not.toBe(JSON.stringify(java));
|
||||
expect(UNTIL[name]?.(java) ?? Number.POSITIVE_INFINITY, name).toBeGreaterThan(1_000);
|
||||
}
|
||||
});
|
||||
|
||||
const scenario = JSON.parse(readFileSync(join(scenarioDirectory, 'mail-lan.scenario.json'), 'utf8')) as Scenario;
|
||||
|
||||
for (const [name, story] of Object.entries(STORIES)) {
|
||||
it(name, () => {
|
||||
const measured = JSON.parse(readFileSync(join(scenarioDirectory, `email-client-java-${name}.java.json`), 'utf8')) as JavaRun;
|
||||
const java = JAVA_BUGS[name]?.(measured) ?? measured;
|
||||
const account: EmailClientAccount = {
|
||||
pop3Host: story.host,
|
||||
pop3Port: 110,
|
||||
smtpHost: story.host,
|
||||
smtpPort: 25,
|
||||
username: 'anna',
|
||||
password: story.password ?? 'geheim',
|
||||
firstName: 'Anna',
|
||||
lastName: 'Muster',
|
||||
email: 'anna@filius.de',
|
||||
};
|
||||
const snapshot = withSnapshot(snapshotOf(scenario), {
|
||||
emailServersByNodeId: new Map([
|
||||
[SERVER, { domain: 'filius.de', accounts: MAIL_ACCOUNTS, isActive: true }],
|
||||
[
|
||||
OTHER_SERVER,
|
||||
{
|
||||
domain: 'andere.de',
|
||||
accounts: [
|
||||
{
|
||||
username: 'carla',
|
||||
password: 'carla',
|
||||
firstName: 'Carla',
|
||||
lastName: 'Carla',
|
||||
mailbox: [],
|
||||
},
|
||||
],
|
||||
isActive: true,
|
||||
},
|
||||
],
|
||||
]),
|
||||
emailClientsByNodeId: new Map([
|
||||
[
|
||||
CLIENT,
|
||||
{
|
||||
account,
|
||||
inbox: [],
|
||||
sent: [],
|
||||
drafts: [],
|
||||
addressBook: [],
|
||||
nextMessageID: 1,
|
||||
},
|
||||
],
|
||||
]),
|
||||
});
|
||||
const { engine } = watchedEngine(scenario, snapshot);
|
||||
for (const [time, act] of story.acts) {
|
||||
engine.handle({ type: 'advance', toMilliseconds: time });
|
||||
act(engine);
|
||||
}
|
||||
engine.handle({
|
||||
type: 'advance',
|
||||
toMilliseconds: Math.max(...java.events.map(([time]) => time)) + 5_000,
|
||||
});
|
||||
|
||||
const numbers = WITHOUT_INITIAL_NUMBERS.has(name) ? withoutInitialNumbers : (text: string) => text;
|
||||
expectSameCaptures(engine, scenario, java.frames, {
|
||||
text: (text) => numbers(frameText(text)),
|
||||
tables: java,
|
||||
cell: (text) => numbers(withoutDate(text).replace(/ID=\d+/g, 'ID=x')),
|
||||
until: UNTIL[name]?.(measured),
|
||||
tolerance: tolerance(story),
|
||||
});
|
||||
|
||||
const expected = javaLines(java);
|
||||
const actual = engineLines(engine);
|
||||
for (const program of ['client', 'server', 'otherServer'] as const) {
|
||||
const lineText = (text: string) => numbers(withoutDate(text));
|
||||
expect(
|
||||
actual[program].map(([, text]) => lineText(text)),
|
||||
`${program}'s lines`,
|
||||
).toEqual(expected[program].map(([, text]) => lineText(text)));
|
||||
for (const [index, [javaTime, text]] of expected[program].entries()) {
|
||||
const late = javaTime - actual[program][index]![0];
|
||||
expect(Math.abs(late), `${program}'s line ${JSON.stringify(text.slice(0, 60))}: Java ${late} ms later`).toBeLessThanOrEqual(tolerance(story));
|
||||
}
|
||||
}
|
||||
|
||||
const client = engine.emailClient(CLIENT)!;
|
||||
expect(mails(client.inbox), 'inbox').toEqual(javaMails(java.inbox));
|
||||
expect(mails(client.sent), 'sent').toEqual(javaMails(java.sent));
|
||||
const accounts = (nodeId: string) => engine.emailServer(nodeId)!.state.accounts.map((each) => [each.username, mails(each.mailbox)]);
|
||||
expect(accounts(SERVER), 'filius.de').toEqual(java.accounts.map(([username, list]) => [username, javaMails(list)]));
|
||||
expect(accounts(OTHER_SERVER), 'andere.de').toEqual(java.otherAccounts.map(([username, list]) => [username, javaMails(list)]));
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
type TopologySnapshotInput,
|
||||
addressText,
|
||||
formatMail,
|
||||
isMailAddress,
|
||||
mailboxEntry,
|
||||
MailServerState,
|
||||
mailSize,
|
||||
@@ -53,6 +54,14 @@ interface JavaCase {
|
||||
|
||||
const CLIENT = '192.168.0.20';
|
||||
|
||||
/** Without a network, no mail goes to another domain (MailCases: Java's server thread never runs). */
|
||||
const NO_OUTBOX = {
|
||||
forward: () => undefined,
|
||||
*lookUpAndSend(): Steps<string[]> {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
|
||||
/** 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');
|
||||
@@ -70,6 +79,7 @@ function play(javaCase: JavaCase, language: PageLanguage): JavaStep[] {
|
||||
() => undefined,
|
||||
() => '192.168.0.30',
|
||||
() => 0,
|
||||
NO_OUTBOX,
|
||||
);
|
||||
const sent: string[] = [];
|
||||
const connection = {
|
||||
@@ -264,6 +274,15 @@ describe('e-mail as Java writes it', () => {
|
||||
expect(addressText({ name: null, mailAddress: null })).toBeNull();
|
||||
});
|
||||
|
||||
it("checks an address as Java's pattern does: an address, or a name and an address in angle brackets", () => {
|
||||
expect(isMailAddress('anna@filius.de')).toBe(true);
|
||||
expect(isMailAddress('Anna Muster <anna.m-x_1@filius.de.>')).toBe(true);
|
||||
expect(isMailAddress('anna@filius.de x')).toBe(false);
|
||||
expect(isMailAddress('Anna <anna@filius.de> x')).toBe(false);
|
||||
expect(isMailAddress('anna@')).toBe(false);
|
||||
expect(isMailAddress('anna..b@filius.de')).toBe(false);
|
||||
});
|
||||
|
||||
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' }]);
|
||||
@@ -309,7 +328,7 @@ function talk(engine: NetworkRuntimeEngine, port: number, ...messages: string[])
|
||||
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 state = new MailServerState('filius.de', [], 'de', (entry) => lines.push(entry), () => undefined, () => '10.0.0.1', () => 0, NO_OUTBOX);
|
||||
const session = new SmtpSession(state, {
|
||||
address: '10.0.0.2',
|
||||
*send(): Steps<boolean> {
|
||||
@@ -374,12 +393,29 @@ describe('the e-mail server in the engine', () => {
|
||||
expect(mailbox()).toEqual([]);
|
||||
});
|
||||
|
||||
it('sends no notice to an empty sender, also when the recipient is unknown (Java: <> gets none)', () => {
|
||||
const engine = new NetworkRuntimeEngine(14);
|
||||
engine.handle({ type: 'start', snapshot: topologySnapshot(mailNetwork()), seed: 14, initialTimeMilliseconds: 0 });
|
||||
expect(talk(engine, 25, 'HELO x', 'MAIL FROM:<>', 'RCPT TO:<niemand@filius.de>', 'DATA', 'Subject: x\n\ny\n.\n', 'QUIT').at(-1)).toBe('221 Server beendet Verbindung.');
|
||||
expect(engine.emailServer(SERVER)!.log.all.filter((line) => line.kind === 'stored')).toEqual([]);
|
||||
});
|
||||
|
||||
it("sends a notice to another domain's sender with an empty sender, and none back when that fails", () => {
|
||||
const engine = new NetworkRuntimeEngine(14);
|
||||
engine.handle({ type: 'start', snapshot: topologySnapshot(mailNetwork()), seed: 14, initialTimeMilliseconds: 0 });
|
||||
// nirgendwo.de has no mail exchange (no DNS server here): the notice goes nowhere, and its own failure tells no one.
|
||||
expect(talk(engine, 25, 'HELO x', 'MAIL FROM:<x@nirgendwo.de>', 'RCPT TO:<niemand@filius.de>', 'DATA', 'Subject: x\n\ny\n.\n', 'QUIT').at(-1)).toBe(
|
||||
'221 Server beendet Verbindung.',
|
||||
);
|
||||
expect(engine.emailServer(SERVER)!.log.all.filter((line) => line.kind === 'stored' || line.kind === 'connectionStarting')).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']);
|
||||
const state = new MailServerState('filius.de', accounts, language, () => undefined, () => undefined, () => '10.0.0.1', () => at, NO_OUTBOX);
|
||||
run(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');
|
||||
|
||||
@@ -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, EmailServerLogEntry, PageLanguage, SendFailure, SimpleClientLogEntry, WebBrowserLogEntry } from '../src/index.js';
|
||||
import type { EchoServerLogEntry, EmailClientLogEntry, 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;
|
||||
@@ -102,5 +102,34 @@ export function emailServerText(entry: EmailServerLogEntry, language: PageLangua
|
||||
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.`;
|
||||
default:
|
||||
return emailClientText(entry);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* What the SMTP and POP3 clients tell the program they work for, as Java's
|
||||
* observers get it (sw_smtpclient_msg1 to msg4; a failure as Java's
|
||||
* exception prints itself; the window shows its message).
|
||||
*/
|
||||
export function emailClientText(entry: EmailClientLogEntry): string {
|
||||
switch (entry.kind) {
|
||||
case 'connectionStarting':
|
||||
return '----';
|
||||
case 'smtpConnected':
|
||||
return `Verbindung zu ${entry.address}:${entry.port} hergestellt`;
|
||||
case 'mailSent':
|
||||
return `Mail versendet an ${entry.recipients}`;
|
||||
case 'smtpDisconnected':
|
||||
return `Verbindung zu ${entry.address}:${entry.port} getrennt`;
|
||||
case 'failed':
|
||||
switch (entry.failure.kind) {
|
||||
case 'answer':
|
||||
return `java.lang.Exception: ${entry.failure.text}`;
|
||||
case 'connect':
|
||||
return entry.failure.failure === 'timedOut' ? `filius.exception.TimeOutException: ${CONNECT_FAILURES.timedOut}` : `filius.exception.VerbindungsException: ${CONNECT_FAILURES[entry.failure.failure]}`;
|
||||
case 'receiveFailed':
|
||||
return 'filius.exception.VerbindungsException: Verbindung nicht aufgebaut!';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,457 @@
|
||||
{
|
||||
"frames": {
|
||||
"00000000-0000-0000-0000-000000000201": [
|
||||
{
|
||||
"time": 518,
|
||||
"source": "02:00:00:00:02:11",
|
||||
"destination": "FF:FF:FF:FF:FF:FF",
|
||||
"arp": 1,
|
||||
"senderIp": "192.168.0.20",
|
||||
"targetIp": "192.168.0.10"
|
||||
},
|
||||
{
|
||||
"time": 756,
|
||||
"source": "02:00:00:00:02:12",
|
||||
"destination": "02:00:00:00:02:11",
|
||||
"arp": 2,
|
||||
"senderIp": "192.168.0.10",
|
||||
"targetIp": "192.168.0.20"
|
||||
},
|
||||
{
|
||||
"time": 758,
|
||||
"source": "02:00:00:00:02:11",
|
||||
"destination": "02:00:00:00:02:12",
|
||||
"sourceIp": "192.168.0.20",
|
||||
"destinationIp": "192.168.0.10",
|
||||
"protocol": 6,
|
||||
"sourcePort": 56629,
|
||||
"destinationPort": 25,
|
||||
"payload": null,
|
||||
"tcpFlags": "S",
|
||||
"sequence": 5000000,
|
||||
"acknowledgement": 0
|
||||
}
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000202": [
|
||||
{
|
||||
"time": 642,
|
||||
"source": "02:00:00:00:02:11",
|
||||
"destination": "FF:FF:FF:FF:FF:FF",
|
||||
"arp": 1,
|
||||
"senderIp": "192.168.0.20",
|
||||
"targetIp": "192.168.0.10"
|
||||
},
|
||||
{
|
||||
"time": 644,
|
||||
"source": "02:00:00:00:02:12",
|
||||
"destination": "02:00:00:00:02:11",
|
||||
"arp": 2,
|
||||
"senderIp": "192.168.0.10",
|
||||
"targetIp": "192.168.0.20"
|
||||
},
|
||||
{
|
||||
"time": 872,
|
||||
"source": "02:00:00:00:02:11",
|
||||
"destination": "02:00:00:00:02:12",
|
||||
"sourceIp": "192.168.0.20",
|
||||
"destinationIp": "192.168.0.10",
|
||||
"protocol": 6,
|
||||
"sourcePort": 56629,
|
||||
"destinationPort": 25,
|
||||
"payload": null,
|
||||
"tcpFlags": "S",
|
||||
"sequence": 5000000,
|
||||
"acknowledgement": 0
|
||||
}
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000203": [
|
||||
{
|
||||
"time": 642,
|
||||
"source": "02:00:00:00:02:11",
|
||||
"destination": "FF:FF:FF:FF:FF:FF",
|
||||
"arp": 1,
|
||||
"senderIp": "192.168.0.20",
|
||||
"targetIp": "192.168.0.10"
|
||||
}
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000204": [
|
||||
{
|
||||
"time": 642,
|
||||
"source": "02:00:00:00:02:11",
|
||||
"destination": "FF:FF:FF:FF:FF:FF",
|
||||
"arp": 1,
|
||||
"senderIp": "192.168.0.20",
|
||||
"targetIp": "192.168.0.10"
|
||||
}
|
||||
]
|
||||
},
|
||||
"rows": {
|
||||
"00000000-0000-0000-0000-000000000201": [
|
||||
[
|
||||
"1",
|
||||
"02:00:00:00:02:11",
|
||||
"FF:FF:FF:FF:FF:FF",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x806"
|
||||
],
|
||||
[
|
||||
"1",
|
||||
"192.168.0.20",
|
||||
"192.168.0.10",
|
||||
"ARP",
|
||||
"Vermittlung",
|
||||
"Suche nach MAC für 192.168.0.10 [op=REQUEST, sender=02:00:00:00:02:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.10]"
|
||||
],
|
||||
[
|
||||
"2",
|
||||
"02:00:00:00:02:12",
|
||||
"02:00:00:00:02:11",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x806"
|
||||
],
|
||||
[
|
||||
"2",
|
||||
"192.168.0.10",
|
||||
"192.168.0.20",
|
||||
"ARP",
|
||||
"Vermittlung",
|
||||
"MAC ist 02:00:00:00:02:12 [op=REPLY, sender=02:00:00:00:02:12|192.168.0.10, target=02:00:00:00:02:11|192.168.0.20]"
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"02:00:00:00:02:11",
|
||||
"02:00:00:00:02:12",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x800"
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"192.168.0.20",
|
||||
"192.168.0.10",
|
||||
"IP",
|
||||
"Vermittlung",
|
||||
"Protokoll: 6, TTL: 64"
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"192.168.0.20:56629",
|
||||
"192.168.0.10:25",
|
||||
"TCP",
|
||||
"Transport",
|
||||
"SYN, SEQ: 5.000.000"
|
||||
]
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000202": [
|
||||
[
|
||||
"1",
|
||||
"02:00:00:00:02:11",
|
||||
"FF:FF:FF:FF:FF:FF",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x806"
|
||||
],
|
||||
[
|
||||
"1",
|
||||
"192.168.0.20",
|
||||
"192.168.0.10",
|
||||
"ARP",
|
||||
"Vermittlung",
|
||||
"Suche nach MAC für 192.168.0.10 [op=REQUEST, sender=02:00:00:00:02:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.10]"
|
||||
],
|
||||
[
|
||||
"2",
|
||||
"02:00:00:00:02:12",
|
||||
"02:00:00:00:02:11",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x806"
|
||||
],
|
||||
[
|
||||
"2",
|
||||
"192.168.0.10",
|
||||
"192.168.0.20",
|
||||
"ARP",
|
||||
"Vermittlung",
|
||||
"MAC ist 02:00:00:00:02:12 [op=REPLY, sender=02:00:00:00:02:12|192.168.0.10, target=02:00:00:00:02:11|192.168.0.20]"
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"02:00:00:00:02:11",
|
||||
"02:00:00:00:02:12",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x800"
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"192.168.0.20",
|
||||
"192.168.0.10",
|
||||
"IP",
|
||||
"Vermittlung",
|
||||
"Protokoll: 6, TTL: 64"
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"192.168.0.20:56629",
|
||||
"192.168.0.10:25",
|
||||
"TCP",
|
||||
"Transport",
|
||||
"SYN, SEQ: 5.000.000"
|
||||
]
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000203": [
|
||||
[
|
||||
"1",
|
||||
"02:00:00:00:02:11",
|
||||
"FF:FF:FF:FF:FF:FF",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x806"
|
||||
],
|
||||
[
|
||||
"1",
|
||||
"192.168.0.20",
|
||||
"192.168.0.10",
|
||||
"ARP",
|
||||
"Vermittlung",
|
||||
"Suche nach MAC für 192.168.0.10 [op=REQUEST, sender=02:00:00:00:02:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.10]"
|
||||
]
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000204": [
|
||||
[
|
||||
"1",
|
||||
"02:00:00:00:02:11",
|
||||
"FF:FF:FF:FF:FF:FF",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x806"
|
||||
],
|
||||
[
|
||||
"1",
|
||||
"192.168.0.20",
|
||||
"192.168.0.10",
|
||||
"ARP",
|
||||
"Vermittlung",
|
||||
"Suche nach MAC für 192.168.0.10 [op=REQUEST, sender=02:00:00:00:02:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.10]"
|
||||
]
|
||||
]
|
||||
},
|
||||
"layeredRows": {
|
||||
"00000000-0000-0000-0000-000000000201": [
|
||||
[
|
||||
"1",
|
||||
"02:00:00:00:02:11",
|
||||
"FF:FF:FF:FF:FF:FF",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x806"
|
||||
],
|
||||
[
|
||||
"1",
|
||||
"192.168.0.20",
|
||||
"192.168.0.10",
|
||||
"ARP",
|
||||
"Vermittlung",
|
||||
"Suche nach MAC für 192.168.0.10 [op=REQUEST, sender=02:00:00:00:02:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.10]"
|
||||
],
|
||||
[
|
||||
"2",
|
||||
"02:00:00:00:02:12",
|
||||
"02:00:00:00:02:11",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x806"
|
||||
],
|
||||
[
|
||||
"2",
|
||||
"192.168.0.10",
|
||||
"192.168.0.20",
|
||||
"ARP",
|
||||
"Vermittlung",
|
||||
"MAC ist 02:00:00:00:02:12 [op=REPLY, sender=02:00:00:00:02:12|192.168.0.10, target=02:00:00:00:02:11|192.168.0.20]"
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"02:00:00:00:02:11",
|
||||
"02:00:00:00:02:12",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x800"
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"192.168.0.20",
|
||||
"192.168.0.10",
|
||||
"IP",
|
||||
"Vermittlung",
|
||||
"Protokoll: 6, TTL: 64"
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"56629",
|
||||
"25",
|
||||
"TCP",
|
||||
"Transport",
|
||||
"SYN, SEQ: 5.000.000"
|
||||
]
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000202": [
|
||||
[
|
||||
"1",
|
||||
"02:00:00:00:02:11",
|
||||
"FF:FF:FF:FF:FF:FF",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x806"
|
||||
],
|
||||
[
|
||||
"1",
|
||||
"192.168.0.20",
|
||||
"192.168.0.10",
|
||||
"ARP",
|
||||
"Vermittlung",
|
||||
"Suche nach MAC für 192.168.0.10 [op=REQUEST, sender=02:00:00:00:02:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.10]"
|
||||
],
|
||||
[
|
||||
"2",
|
||||
"02:00:00:00:02:12",
|
||||
"02:00:00:00:02:11",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x806"
|
||||
],
|
||||
[
|
||||
"2",
|
||||
"192.168.0.10",
|
||||
"192.168.0.20",
|
||||
"ARP",
|
||||
"Vermittlung",
|
||||
"MAC ist 02:00:00:00:02:12 [op=REPLY, sender=02:00:00:00:02:12|192.168.0.10, target=02:00:00:00:02:11|192.168.0.20]"
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"02:00:00:00:02:11",
|
||||
"02:00:00:00:02:12",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x800"
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"192.168.0.20",
|
||||
"192.168.0.10",
|
||||
"IP",
|
||||
"Vermittlung",
|
||||
"Protokoll: 6, TTL: 64"
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"56629",
|
||||
"25",
|
||||
"TCP",
|
||||
"Transport",
|
||||
"SYN, SEQ: 5.000.000"
|
||||
]
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000203": [
|
||||
[
|
||||
"1",
|
||||
"02:00:00:00:02:11",
|
||||
"FF:FF:FF:FF:FF:FF",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x806"
|
||||
],
|
||||
[
|
||||
"1",
|
||||
"192.168.0.20",
|
||||
"192.168.0.10",
|
||||
"ARP",
|
||||
"Vermittlung",
|
||||
"Suche nach MAC für 192.168.0.10 [op=REQUEST, sender=02:00:00:00:02:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.10]"
|
||||
]
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000204": [
|
||||
[
|
||||
"1",
|
||||
"02:00:00:00:02:11",
|
||||
"FF:FF:FF:FF:FF:FF",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x806"
|
||||
],
|
||||
[
|
||||
"1",
|
||||
"192.168.0.20",
|
||||
"192.168.0.10",
|
||||
"ARP",
|
||||
"Vermittlung",
|
||||
"Suche nach MAC für 192.168.0.10 [op=REQUEST, sender=02:00:00:00:02:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.10]"
|
||||
]
|
||||
]
|
||||
},
|
||||
"events": [
|
||||
[
|
||||
212,
|
||||
"client",
|
||||
"null"
|
||||
],
|
||||
[
|
||||
214,
|
||||
"server",
|
||||
"null"
|
||||
],
|
||||
[
|
||||
218,
|
||||
"otherServer",
|
||||
"null"
|
||||
],
|
||||
[
|
||||
500,
|
||||
"client",
|
||||
"call versendeEmail bob@filius.de | | "
|
||||
],
|
||||
[
|
||||
503,
|
||||
"client",
|
||||
"----"
|
||||
],
|
||||
[
|
||||
8810,
|
||||
"client",
|
||||
"filius.exception.TimeOutException: Zeitüberschreitung beim Verbindungsaufbau"
|
||||
]
|
||||
],
|
||||
"inbox": [],
|
||||
"sent": [],
|
||||
"accounts": [
|
||||
[
|
||||
"anna",
|
||||
[
|
||||
[
|
||||
"From: Bob Beispiel <bob@filius.de>\nTo: <anna@filius.de>\nSubject: Hallo Anna\n\nErste Zeile\nZweite Zeile",
|
||||
"true",
|
||||
"false"
|
||||
],
|
||||
[
|
||||
"From: Carla <carla@andere.de>\nTo: Anna Muster <anna@filius.de>, <bob@filius.de>\nCc: <chef@filius.de>\nSubject: Termin\n\nMorgen um 10?",
|
||||
"true",
|
||||
"false"
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
"bob",
|
||||
[]
|
||||
]
|
||||
],
|
||||
"otherAccounts": [
|
||||
[
|
||||
"carla",
|
||||
[]
|
||||
]
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,633 @@
|
||||
{
|
||||
"frames": {
|
||||
"00000000-0000-0000-0000-000000000201": [
|
||||
{
|
||||
"time": 541,
|
||||
"source": "02:00:00:00:02:11",
|
||||
"destination": "FF:FF:FF:FF:FF:FF",
|
||||
"arp": 1,
|
||||
"senderIp": "192.168.0.20",
|
||||
"targetIp": "192.168.0.10"
|
||||
},
|
||||
{
|
||||
"time": 810,
|
||||
"source": "02:00:00:00:02:12",
|
||||
"destination": "02:00:00:00:02:11",
|
||||
"arp": 2,
|
||||
"senderIp": "192.168.0.10",
|
||||
"targetIp": "192.168.0.20"
|
||||
},
|
||||
{
|
||||
"time": 819,
|
||||
"source": "02:00:00:00:02:11",
|
||||
"destination": "02:00:00:00:02:12",
|
||||
"sourceIp": "192.168.0.20",
|
||||
"destinationIp": "192.168.0.10",
|
||||
"protocol": 17,
|
||||
"sourcePort": 52467,
|
||||
"destinationPort": 53,
|
||||
"payload": "ID=30279 QR=0 RCODE=0 QDCOUNT=1 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \ngibtsnicht.filius.de. A IN\n"
|
||||
},
|
||||
{
|
||||
"time": 1065,
|
||||
"source": "02:00:00:00:02:12",
|
||||
"destination": "02:00:00:00:02:11",
|
||||
"sourceIp": "192.168.0.10",
|
||||
"destinationIp": "192.168.0.20",
|
||||
"protocol": 17,
|
||||
"sourcePort": 53,
|
||||
"destinationPort": 52467,
|
||||
"payload": "ID=30279 QR=1 RCODE=0 QDCOUNT=0 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \n"
|
||||
}
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000202": [
|
||||
{
|
||||
"time": 687,
|
||||
"source": "02:00:00:00:02:11",
|
||||
"destination": "FF:FF:FF:FF:FF:FF",
|
||||
"arp": 1,
|
||||
"senderIp": "192.168.0.20",
|
||||
"targetIp": "192.168.0.10"
|
||||
},
|
||||
{
|
||||
"time": 692,
|
||||
"source": "02:00:00:00:02:12",
|
||||
"destination": "02:00:00:00:02:11",
|
||||
"arp": 2,
|
||||
"senderIp": "192.168.0.10",
|
||||
"targetIp": "192.168.0.20"
|
||||
},
|
||||
{
|
||||
"time": 937,
|
||||
"source": "02:00:00:00:02:11",
|
||||
"destination": "02:00:00:00:02:12",
|
||||
"sourceIp": "192.168.0.20",
|
||||
"destinationIp": "192.168.0.10",
|
||||
"protocol": 17,
|
||||
"sourcePort": 52467,
|
||||
"destinationPort": 53,
|
||||
"payload": "ID=30279 QR=0 RCODE=0 QDCOUNT=1 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \ngibtsnicht.filius.de. A IN\n"
|
||||
},
|
||||
{
|
||||
"time": 949,
|
||||
"source": "02:00:00:00:02:12",
|
||||
"destination": "02:00:00:00:02:11",
|
||||
"sourceIp": "192.168.0.10",
|
||||
"destinationIp": "192.168.0.20",
|
||||
"protocol": 17,
|
||||
"sourcePort": 53,
|
||||
"destinationPort": 52467,
|
||||
"payload": "ID=30279 QR=1 RCODE=0 QDCOUNT=0 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \n"
|
||||
}
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000203": [
|
||||
{
|
||||
"time": 690,
|
||||
"source": "02:00:00:00:02:11",
|
||||
"destination": "FF:FF:FF:FF:FF:FF",
|
||||
"arp": 1,
|
||||
"senderIp": "192.168.0.20",
|
||||
"targetIp": "192.168.0.10"
|
||||
}
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000204": [
|
||||
{
|
||||
"time": 688,
|
||||
"source": "02:00:00:00:02:11",
|
||||
"destination": "FF:FF:FF:FF:FF:FF",
|
||||
"arp": 1,
|
||||
"senderIp": "192.168.0.20",
|
||||
"targetIp": "192.168.0.10"
|
||||
}
|
||||
]
|
||||
},
|
||||
"rows": {
|
||||
"00000000-0000-0000-0000-000000000201": [
|
||||
[
|
||||
"1",
|
||||
"02:00:00:00:02:11",
|
||||
"FF:FF:FF:FF:FF:FF",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x806"
|
||||
],
|
||||
[
|
||||
"1",
|
||||
"192.168.0.20",
|
||||
"192.168.0.10",
|
||||
"ARP",
|
||||
"Vermittlung",
|
||||
"Suche nach MAC für 192.168.0.10 [op=REQUEST, sender=02:00:00:00:02:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.10]"
|
||||
],
|
||||
[
|
||||
"2",
|
||||
"02:00:00:00:02:12",
|
||||
"02:00:00:00:02:11",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x806"
|
||||
],
|
||||
[
|
||||
"2",
|
||||
"192.168.0.10",
|
||||
"192.168.0.20",
|
||||
"ARP",
|
||||
"Vermittlung",
|
||||
"MAC ist 02:00:00:00:02:12 [op=REPLY, sender=02:00:00:00:02:12|192.168.0.10, target=02:00:00:00:02:11|192.168.0.20]"
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"02:00:00:00:02:11",
|
||||
"02:00:00:00:02:12",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x800"
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"192.168.0.20",
|
||||
"192.168.0.10",
|
||||
"IP",
|
||||
"Vermittlung",
|
||||
"Protokoll: 17, TTL: 64"
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"192.168.0.20:52467",
|
||||
"192.168.0.10:53",
|
||||
"UDP",
|
||||
"Transport",
|
||||
""
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"192.168.0.20:52467",
|
||||
"192.168.0.10:53",
|
||||
"DNS",
|
||||
"Anwendung",
|
||||
"ID=30279 QR=0 RCODE=0 QDCOUNT=1 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \ngibtsnicht.filius.de. A IN\n"
|
||||
],
|
||||
[
|
||||
"4",
|
||||
"02:00:00:00:02:12",
|
||||
"02:00:00:00:02:11",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x800"
|
||||
],
|
||||
[
|
||||
"4",
|
||||
"192.168.0.10",
|
||||
"192.168.0.20",
|
||||
"IP",
|
||||
"Vermittlung",
|
||||
"Protokoll: 17, TTL: 64"
|
||||
],
|
||||
[
|
||||
"4",
|
||||
"192.168.0.10:53",
|
||||
"192.168.0.20:52467",
|
||||
"UDP",
|
||||
"Transport",
|
||||
""
|
||||
],
|
||||
[
|
||||
"4",
|
||||
"192.168.0.10:53",
|
||||
"192.168.0.20:52467",
|
||||
"DNS",
|
||||
"Anwendung",
|
||||
"ID=30279 QR=1 RCODE=0 QDCOUNT=0 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \n"
|
||||
]
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000202": [
|
||||
[
|
||||
"1",
|
||||
"02:00:00:00:02:11",
|
||||
"FF:FF:FF:FF:FF:FF",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x806"
|
||||
],
|
||||
[
|
||||
"1",
|
||||
"192.168.0.20",
|
||||
"192.168.0.10",
|
||||
"ARP",
|
||||
"Vermittlung",
|
||||
"Suche nach MAC für 192.168.0.10 [op=REQUEST, sender=02:00:00:00:02:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.10]"
|
||||
],
|
||||
[
|
||||
"2",
|
||||
"02:00:00:00:02:12",
|
||||
"02:00:00:00:02:11",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x806"
|
||||
],
|
||||
[
|
||||
"2",
|
||||
"192.168.0.10",
|
||||
"192.168.0.20",
|
||||
"ARP",
|
||||
"Vermittlung",
|
||||
"MAC ist 02:00:00:00:02:12 [op=REPLY, sender=02:00:00:00:02:12|192.168.0.10, target=02:00:00:00:02:11|192.168.0.20]"
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"02:00:00:00:02:11",
|
||||
"02:00:00:00:02:12",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x800"
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"192.168.0.20",
|
||||
"192.168.0.10",
|
||||
"IP",
|
||||
"Vermittlung",
|
||||
"Protokoll: 17, TTL: 64"
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"192.168.0.20:52467",
|
||||
"192.168.0.10:53",
|
||||
"UDP",
|
||||
"Transport",
|
||||
""
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"192.168.0.20:52467",
|
||||
"192.168.0.10:53",
|
||||
"DNS",
|
||||
"Anwendung",
|
||||
"ID=30279 QR=0 RCODE=0 QDCOUNT=1 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \ngibtsnicht.filius.de. A IN\n"
|
||||
],
|
||||
[
|
||||
"4",
|
||||
"02:00:00:00:02:12",
|
||||
"02:00:00:00:02:11",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x800"
|
||||
],
|
||||
[
|
||||
"4",
|
||||
"192.168.0.10",
|
||||
"192.168.0.20",
|
||||
"IP",
|
||||
"Vermittlung",
|
||||
"Protokoll: 17, TTL: 64"
|
||||
],
|
||||
[
|
||||
"4",
|
||||
"192.168.0.10:53",
|
||||
"192.168.0.20:52467",
|
||||
"UDP",
|
||||
"Transport",
|
||||
""
|
||||
],
|
||||
[
|
||||
"4",
|
||||
"192.168.0.10:53",
|
||||
"192.168.0.20:52467",
|
||||
"DNS",
|
||||
"Anwendung",
|
||||
"ID=30279 QR=1 RCODE=0 QDCOUNT=0 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \n"
|
||||
]
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000203": [
|
||||
[
|
||||
"1",
|
||||
"02:00:00:00:02:11",
|
||||
"FF:FF:FF:FF:FF:FF",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x806"
|
||||
],
|
||||
[
|
||||
"1",
|
||||
"192.168.0.20",
|
||||
"192.168.0.10",
|
||||
"ARP",
|
||||
"Vermittlung",
|
||||
"Suche nach MAC für 192.168.0.10 [op=REQUEST, sender=02:00:00:00:02:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.10]"
|
||||
]
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000204": [
|
||||
[
|
||||
"1",
|
||||
"02:00:00:00:02:11",
|
||||
"FF:FF:FF:FF:FF:FF",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x806"
|
||||
],
|
||||
[
|
||||
"1",
|
||||
"192.168.0.20",
|
||||
"192.168.0.10",
|
||||
"ARP",
|
||||
"Vermittlung",
|
||||
"Suche nach MAC für 192.168.0.10 [op=REQUEST, sender=02:00:00:00:02:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.10]"
|
||||
]
|
||||
]
|
||||
},
|
||||
"layeredRows": {
|
||||
"00000000-0000-0000-0000-000000000201": [
|
||||
[
|
||||
"1",
|
||||
"02:00:00:00:02:11",
|
||||
"FF:FF:FF:FF:FF:FF",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x806"
|
||||
],
|
||||
[
|
||||
"1",
|
||||
"192.168.0.20",
|
||||
"192.168.0.10",
|
||||
"ARP",
|
||||
"Vermittlung",
|
||||
"Suche nach MAC für 192.168.0.10 [op=REQUEST, sender=02:00:00:00:02:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.10]"
|
||||
],
|
||||
[
|
||||
"2",
|
||||
"02:00:00:00:02:12",
|
||||
"02:00:00:00:02:11",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x806"
|
||||
],
|
||||
[
|
||||
"2",
|
||||
"192.168.0.10",
|
||||
"192.168.0.20",
|
||||
"ARP",
|
||||
"Vermittlung",
|
||||
"MAC ist 02:00:00:00:02:12 [op=REPLY, sender=02:00:00:00:02:12|192.168.0.10, target=02:00:00:00:02:11|192.168.0.20]"
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"02:00:00:00:02:11",
|
||||
"02:00:00:00:02:12",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x800"
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"192.168.0.20",
|
||||
"192.168.0.10",
|
||||
"IP",
|
||||
"Vermittlung",
|
||||
"Protokoll: 17, TTL: 64"
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"52467",
|
||||
"53",
|
||||
"UDP",
|
||||
"Transport",
|
||||
""
|
||||
],
|
||||
[
|
||||
"3",
|
||||
null,
|
||||
null,
|
||||
"DNS",
|
||||
"Anwendung",
|
||||
"ID=30279 QR=0 RCODE=0 QDCOUNT=1 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \ngibtsnicht.filius.de. A IN\n"
|
||||
],
|
||||
[
|
||||
"4",
|
||||
"02:00:00:00:02:12",
|
||||
"02:00:00:00:02:11",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x800"
|
||||
],
|
||||
[
|
||||
"4",
|
||||
"192.168.0.10",
|
||||
"192.168.0.20",
|
||||
"IP",
|
||||
"Vermittlung",
|
||||
"Protokoll: 17, TTL: 64"
|
||||
],
|
||||
[
|
||||
"4",
|
||||
"53",
|
||||
"52467",
|
||||
"UDP",
|
||||
"Transport",
|
||||
""
|
||||
],
|
||||
[
|
||||
"4",
|
||||
null,
|
||||
null,
|
||||
"DNS",
|
||||
"Anwendung",
|
||||
"ID=30279 QR=1 RCODE=0 QDCOUNT=0 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \n"
|
||||
]
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000202": [
|
||||
[
|
||||
"1",
|
||||
"02:00:00:00:02:11",
|
||||
"FF:FF:FF:FF:FF:FF",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x806"
|
||||
],
|
||||
[
|
||||
"1",
|
||||
"192.168.0.20",
|
||||
"192.168.0.10",
|
||||
"ARP",
|
||||
"Vermittlung",
|
||||
"Suche nach MAC für 192.168.0.10 [op=REQUEST, sender=02:00:00:00:02:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.10]"
|
||||
],
|
||||
[
|
||||
"2",
|
||||
"02:00:00:00:02:12",
|
||||
"02:00:00:00:02:11",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x806"
|
||||
],
|
||||
[
|
||||
"2",
|
||||
"192.168.0.10",
|
||||
"192.168.0.20",
|
||||
"ARP",
|
||||
"Vermittlung",
|
||||
"MAC ist 02:00:00:00:02:12 [op=REPLY, sender=02:00:00:00:02:12|192.168.0.10, target=02:00:00:00:02:11|192.168.0.20]"
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"02:00:00:00:02:11",
|
||||
"02:00:00:00:02:12",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x800"
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"192.168.0.20",
|
||||
"192.168.0.10",
|
||||
"IP",
|
||||
"Vermittlung",
|
||||
"Protokoll: 17, TTL: 64"
|
||||
],
|
||||
[
|
||||
"3",
|
||||
"52467",
|
||||
"53",
|
||||
"UDP",
|
||||
"Transport",
|
||||
""
|
||||
],
|
||||
[
|
||||
"3",
|
||||
null,
|
||||
null,
|
||||
"DNS",
|
||||
"Anwendung",
|
||||
"ID=30279 QR=0 RCODE=0 QDCOUNT=1 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \ngibtsnicht.filius.de. A IN\n"
|
||||
],
|
||||
[
|
||||
"4",
|
||||
"02:00:00:00:02:12",
|
||||
"02:00:00:00:02:11",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x800"
|
||||
],
|
||||
[
|
||||
"4",
|
||||
"192.168.0.10",
|
||||
"192.168.0.20",
|
||||
"IP",
|
||||
"Vermittlung",
|
||||
"Protokoll: 17, TTL: 64"
|
||||
],
|
||||
[
|
||||
"4",
|
||||
"53",
|
||||
"52467",
|
||||
"UDP",
|
||||
"Transport",
|
||||
""
|
||||
],
|
||||
[
|
||||
"4",
|
||||
null,
|
||||
null,
|
||||
"DNS",
|
||||
"Anwendung",
|
||||
"ID=30279 QR=1 RCODE=0 QDCOUNT=0 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0 \n"
|
||||
]
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000203": [
|
||||
[
|
||||
"1",
|
||||
"02:00:00:00:02:11",
|
||||
"FF:FF:FF:FF:FF:FF",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x806"
|
||||
],
|
||||
[
|
||||
"1",
|
||||
"192.168.0.20",
|
||||
"192.168.0.10",
|
||||
"ARP",
|
||||
"Vermittlung",
|
||||
"Suche nach MAC für 192.168.0.10 [op=REQUEST, sender=02:00:00:00:02:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.10]"
|
||||
]
|
||||
],
|
||||
"00000000-0000-0000-0000-000000000204": [
|
||||
[
|
||||
"1",
|
||||
"02:00:00:00:02:11",
|
||||
"FF:FF:FF:FF:FF:FF",
|
||||
"",
|
||||
"Netzzugang",
|
||||
"0x806"
|
||||
],
|
||||
[
|
||||
"1",
|
||||
"192.168.0.20",
|
||||
"192.168.0.10",
|
||||
"ARP",
|
||||
"Vermittlung",
|
||||
"Suche nach MAC für 192.168.0.10 [op=REQUEST, sender=02:00:00:00:02:11|192.168.0.20, target=FF:FF:FF:FF:FF:FF|192.168.0.10]"
|
||||
]
|
||||
]
|
||||
},
|
||||
"events": [
|
||||
[
|
||||
284,
|
||||
"client",
|
||||
"null"
|
||||
],
|
||||
[
|
||||
304,
|
||||
"server",
|
||||
"null"
|
||||
],
|
||||
[
|
||||
317,
|
||||
"otherServer",
|
||||
"null"
|
||||
],
|
||||
[
|
||||
500,
|
||||
"client",
|
||||
"call versendeEmail bob@filius.de | | "
|
||||
],
|
||||
[
|
||||
508,
|
||||
"client",
|
||||
"----"
|
||||
],
|
||||
[
|
||||
1069,
|
||||
"client",
|
||||
"filius.exception.VerbindungsException: Ziel konnte nicht bestimmt werden!"
|
||||
]
|
||||
],
|
||||
"inbox": [],
|
||||
"sent": [],
|
||||
"accounts": [
|
||||
[
|
||||
"anna",
|
||||
[
|
||||
[
|
||||
"From: Bob Beispiel <bob@filius.de>\nTo: <anna@filius.de>\nSubject: Hallo Anna\n\nErste Zeile\nZweite Zeile",
|
||||
"true",
|
||||
"false"
|
||||
],
|
||||
[
|
||||
"From: Carla <carla@andere.de>\nTo: Anna Muster <anna@filius.de>, <bob@filius.de>\nCc: <chef@filius.de>\nSubject: Termin\n\nMorgen um 10?",
|
||||
"true",
|
||||
"false"
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
"bob",
|
||||
[]
|
||||
]
|
||||
],
|
||||
"otherAccounts": [
|
||||
[
|
||||
"carla",
|
||||
[]
|
||||
]
|
||||
]
|
||||
}
|
||||
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
File diff suppressed because it is too large
Load Diff
@@ -106,11 +106,13 @@
|
||||
},
|
||||
"00000000-0000-0000-0000-000000000203": {
|
||||
"ip": "192.168.0.30",
|
||||
"mask": "255.255.255.0"
|
||||
"mask": "255.255.255.0",
|
||||
"dns": "192.168.0.10"
|
||||
},
|
||||
"00000000-0000-0000-0000-000000000204": {
|
||||
"ip": "192.168.0.31",
|
||||
"mask": "255.255.255.0"
|
||||
"mask": "255.255.255.0",
|
||||
"dns": "192.168.0.10"
|
||||
}
|
||||
},
|
||||
"dnsServers": {
|
||||
@@ -119,7 +121,11 @@
|
||||
"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"
|
||||
"leer.filius.de A 192.168.0.31",
|
||||
"filius.de MX mail.filius.de",
|
||||
"mail.filius.de A 192.168.0.30",
|
||||
"andere.de MX mail.andere.de",
|
||||
"mail.andere.de A 192.168.0.31"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user