| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144 |
- 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();
- }
- });
- }
|