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:
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user