// 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): 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 ' })).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 ' })).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 \nTo: \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: \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); }); });