Implement email support with per-user permission model (closes #2)

New modules:
- server/mailer.mjs: nodemailer transport wrapper
- server/mail_perms.mjs: runtime permission store, persisted to disk

New actions:
- send-email: checks (caller, to, topic) permission before sending
- set-mail-permission: grant/revoke permissions, gated by canApprove
- get-mail-permissions: list current permissions

Handler signature extended to handler(params, ctx) where ctx carries
caller, users, mail_perm_store and mailer_send. Existing handlers
ignore ctx so the change is backwards-compatible.

SMTP config lives in secrets.json under optional 'smtp' key.
Mail permissions path via --mail-perms or CONDUIT_MAIL_PERMS.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-17 22:34:26 +00:00
parent 5fb9d3ce07
commit b1ccbfef41
8 changed files with 190 additions and 12 deletions

49
server/mail_perms.mjs Normal file
View File

@@ -0,0 +1,49 @@
import { readFileSync, writeFileSync, existsSync } from 'fs';
export function load_mail_perms(file_path) {
let allowed = [];
if (file_path && existsSync(file_path)) {
try {
const parsed = JSON.parse(readFileSync(file_path, 'utf8'));
if (!Array.isArray(parsed.allowed)) {
throw new Error("'allowed' must be an array");
}
allowed = parsed.allowed;
} catch (err) {
throw new Error(`Cannot load mail permissions from ${file_path}: ${err.message}`);
}
}
function write() {
if (!file_path) {
return;
}
writeFileSync(file_path, JSON.stringify({ allowed }, null, '\t') + '\n', 'utf8');
}
function check(user, to, topic) {
return allowed.some(e => e.user === user && e.to === to && e.topic === topic);
}
function add(user, to, topic) {
if (!check(user, to, topic)) {
allowed.push({ user, to, topic });
write();
}
}
function remove(user, to, topic) {
const before = allowed.length;
allowed = allowed.filter(e => !(e.user === user && e.to === to && e.topic === topic));
if (allowed.length !== before) {
write();
}
}
function list() {
return [...allowed];
}
return { check, add, remove, list };
}