openvibers/openvibe.mail Commits
Mail: verify owner senders for standing intents (#20)
Changes
@@@ -107,6 +107,22 @@ function mountAgentMail(app, { db, config, stalwart, principal, keys, smtp = nulreturn 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.' });@@@ -44,6 +44,36 @@ const asActor = (t, user, capabilities) => ({ bearer: serviceToken(t, { capabiliconst 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 });
@@ -107,6 +107,22 @@ function mountAgentMail(app, { db, config, stalwart, principal, keys, smtp = nul
@@ -107,6 +107,22 @@ function mountAgentMail(app, { db, config, stalwart, principal, keys, smtp = nul
return await findAgentInbox({ db, owner: who.subject, agentId }).catch(() => null);
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) => {
app.get('/internal/mail/agents/:agentId/messages/:messageId', guard('mail.agent.read'), wrap(async (req, res) => {
const inbox = await inboxFor(req);
const inbox = await inboxFor(req);
if (!inbox) return res.status(404).json({ status: 404, code: 'mail.not_found', detail: 'That inbox does not exist.' });
if (!inbox) return res.status(404).json({ status: 404, code: 'mail.not_found', detail: 'That inbox does not exist.' });
@@ -44,6 +44,36 @@ const asActor = (t, user, capabilities) => ({ bearer: serviceToken(t, { capabili
@@ -44,6 +44,36 @@ const asActor = (t, user, capabilities) => ({ bearer: serviceToken(t, { capabili
const HOUR = 3600_000;
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) => {
test('the switch is OFF by default and nothing is announced until the person turns it on', async (t) => {
const smtp = fakeSmtp();
const smtp = fakeSmtp();
const s = await boot({ smtp });
const s = await boot({ smtp });