import assert from 'node:assert/strict'; import { mkdtempSync } from 'node:fs'; import net from 'node:net'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { test } from 'node:test'; import { createDomain, createInboundMailbox, createUser, createWebmailLoginTicket, exchangeWebmailLoginTicket, initDatabase, replaceInboundMailboxGrants } from '../src/db.js'; import { startSubmissionServer } from '../src/submission.js'; test('an established Webmail SMTP session rechecks live send permission', async () => { initDatabase(mkdtempSync(path.join(tmpdir(), 'mailhub-submission-webmail-')), 'webmail-submission-secret'); const owner = createUser({ username: 'webmail-owner', email: 'webmail-owner@example.com', password: 'password123' }); const delegate = createUser({ username: 'webmail-delegate', email: 'webmail-delegate@example.com', password: 'password123' }); createDomain(owner.id, { domain: 'webmail-submit.example', selector: 'mh', verificationToken: 'verify-webmail-submit', dkimPublic: 'public', dkimPrivate: 'private', senderHost: 'in.webmail-submit.example', sendingIp: '192.0.2.10', spfExtra: '', dmarcPolicy: 'none', dmarcRua: '' }); const mailbox = createInboundMailbox(owner.id, { address: 'shared@webmail-submit.example', password: 'mailbox-password' }); replaceInboundMailboxGrants(mailbox.id, [{ userId: delegate.id, permissions: { receive: true, send: true } }]); const ticket = createWebmailLoginTicket(delegate.id, mailbox.id, { audience: 'https://mail.us.ss5.xyz' }); const login = exchangeWebmailLoginTicket(ticket.ticket, { audience: 'https://mail.us.ss5.xyz' }); const [server] = startSubmissionServer({ enabled: true, listeners: [{ port: 0, protocol: 'smtp' }], hostname: 'in.webmail-submit.example', allowInsecureAuth: true, inboundEnabled: true }); await waitForListening(server); const client = await connectSmtp(server.address().port); try { assert.match(await client.readReply(), /^220 /); client.write('EHLO roundcube.example'); assert.match(await client.readReply(), /^250 HELP$/m); const auth = Buffer.from(`\u0000${mailbox.address}\u0000${login.credential}`).toString('base64'); client.write(`AUTH PLAIN ${auth}`); assert.match(await client.readReply(), /^235 /); replaceInboundMailboxGrants(mailbox.id, [{ userId: delegate.id, permissions: { receive: true, send: false } }]); client.write(`MAIL FROM:<${mailbox.address}>`); assert.match(await client.readReply(), /^530 Authentication no longer valid$/); client.write(`AUTH PLAIN ${auth}`); assert.match(await client.readReply(), /^535 Authentication failed$/); } finally { client.close(); await closeServer(server); } }); function waitForListening(server) { if (server.listening) return Promise.resolve(); return new Promise((resolve, reject) => { server.once('listening', resolve); server.once('error', reject); }); } function closeServer(server) { return new Promise((resolve) => server.close(resolve)); } function connectSmtp(port) { return new Promise((resolve, reject) => { const socket = net.connect(port, '127.0.0.1'); socket.setEncoding('utf8'); let buffer = ''; const pending = []; socket.on('data', (chunk) => { buffer += chunk; flush(); }); socket.once('error', reject); socket.once('connect', () => { socket.off('error', reject); resolve({ write(line) { socket.write(`${line}\r\n`); }, readReply() { return new Promise((replyResolve) => { pending.push(replyResolve); flush(); }); }, close() { socket.destroy(); } }); }); function flush() { if (!pending.length) return; const lines = buffer.split('\r\n'); if (lines.length < 2) return; const complete = lines.slice(0, -1); const finalIndex = complete.findIndex((line) => /^\d{3} /.test(line)); if (finalIndex === -1) return; const reply = complete.slice(0, finalIndex + 1).join('\n'); buffer = `${complete.slice(finalIndex + 1).join('\r\n')}${lines.at(-1) ? `\r\n${lines.at(-1)}` : ''}`; pending.shift()(reply); flush(); } }); }