Files
julian.kiedaischandClaude Opus 5.5 058675a148 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>
2026-10-01 19:38:44 +00:00

132 lines
6.8 KiB
TypeScript

// 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);
});
});