openvibers/openvibe.mail Commits

Mail: verify owner senders for standing intents (#20)

OP OpenVibers committed 5 days ago · dd27762c7d · 2 files, +46 −0

* Verify owner addresses for agent mail triggers

* Exclude inactive mail aliases from owner check

* Cover inactive addresses in sender ownership test

Changes

ddiff --git a/server/http/agent-mail.js b/server/http/agent-mail.js
iindex b0b1815..1194e97 100644
---- a/server/http/agent-mail.js
++++ b/server/http/agent-mail.js
@@@ -107,6 +107,22 @@ function mountAgentMail(app, { db, config, stalwart, principal, keys, smtp = nul
return await findAgentInbox({ db, owner: who.subject, agentId }).catch(() => null);
}
++ // A standing intent may read a letter only as the owner's own words when Mail can prove the exact
++ // sender address belongs to that owner. A shared openvibe.email domain is not that proof.
++ app.get('/internal/mail/agents/:agentId/own-sender', guard('mail.agent.read'), wrap(async (req, res) => {
++ const inbox = await inboxFor(req);
++ if (!inbox) return res.status(404).json({ status: 404, code: 'mail.not_found', detail: 'That inbox does not exist.' });
++ const address = String(req.query.address || '').trim().toLowerCase();
++ if (!/^[^\s@]+@[^\s@]+$/.test(address) || address.length > 320) return res.status(422).json({ status: 422, code: 'mail.address.invalid', detail: 'Give one bare email address.' });
++ const owned = await db.maybe(`SELECT 1 AS owned FROM (
++ SELECT owner_subject, address FROM mail_mailboxes
++ UNION ALL SELECT owner_subject, address FROM mail_masks WHERE enabled AND (expires_at IS NULL OR expires_at > now())
++ UNION ALL SELECT owner_subject, address FROM mail_burners WHERE expires_at > now()
++ UNION ALL SELECT owner_subject, address FROM mail_agent_inboxes
++ ) a WHERE a.owner_subject = $1 AND lower(a.address) = $2 LIMIT 1`, [inbox.owner, address]);
++ return res.json({ owned: Boolean(owned) });
++ }));
++
app.get('/internal/mail/agents/:agentId/messages/:messageId', guard('mail.agent.read'), wrap(async (req, res) => {
const inbox = await inboxFor(req);
if (!inbox) return res.status(404).json({ status: 404, code: 'mail.not_found', detail: 'That inbox does not exist.' });
ddiff --git a/test/agent-mail.test.js b/test/agent-mail.test.js
iindex 6e52c91..8803f66 100644
---- a/test/agent-mail.test.js
++++ b/test/agent-mail.test.js
@@@ -44,6 +44,36 @@ const asActor = (t, user, capabilities) => ({ bearer: serviceToken(t, { capabili
const HOUR = 3600_000;
++test('the internal owner-address check accepts exact owned addresses only', async (t) => {
++ const s = await boot();
++ t.after(() => s.close());
++ const alice = s.network.addUser('alice');
++ const bob = s.network.addUser('bob');
++ const inbox = (await s.post('/api/v1/agent-inboxes', { as: alice, json: { agent_id: 'helper' } })).body.agent_inbox;
++ const mailbox = await s.db.value('SELECT address FROM mail_mailboxes WHERE owner_subject = $1', [alice.subject]);
++ const mask = (await s.post('/api/v1/masks', { as: alice, json: { label: 'shop' } })).body.mask;
++ const burner = (await s.post('/api/v1/burners', { as: alice, json: { expires: '1h' } })).body.burner;
++ const other = (await s.post('/api/v1/agent-inboxes', { as: bob, json: { agent_id: 'helper' } })).body.agent_inbox;
++ const url = (address) => `/internal/mail/agents/helper/own-sender?address=${encodeURIComponent(address)}`;
++ const auth = asActor(s, alice);
++ for (const address of [mailbox, mask.address, burner.address, inbox.address]) {
++ const found = await s.get(url(address), auth);
++ assert.strictEqual(found.status, 200, found.text);
++ assert.strictEqual(found.body.owned, true);
++ }
++ for (const address of [other.address, '[email protected]', '[email protected]']) {
++ const found = await s.get(url(address), auth);
++ assert.strictEqual(found.status, 200, found.text);
++ assert.strictEqual(found.body.owned, false);
++ }
++ await s.db.query('UPDATE mail_masks SET enabled = false WHERE id = $1', [mask.id]);
++ await s.db.query("UPDATE mail_burners SET expires_at = now() - interval '1 second' WHERE id = $1", [burner.id]);
++ assert.strictEqual((await s.get(url(mask.address), auth)).body.owned, false);
++ assert.strictEqual((await s.get(url(burner.address), auth)).body.owned, false);
++ assert.strictEqual((await s.get(url(mailbox), asActor(s, bob))).body.owned, false);
++ assert.strictEqual((await s.get(url(mailbox), asActor(s, alice, ['mail.agent.reply']))).status, 403);
++});
++
test('the switch is OFF by default and nothing is announced until the person turns it on', async (t) => {
const smtp = fakeSmtp();
const s = await boot({ smtp });