'use strict' /** * email-queue.js — admin view of ERPNext's outbound Email Queue. * * During the migration, ERPNext email is globally muted (mute_emails=1) and the * scheduler is paused, so nothing flushes. This gives ops a window to SEE what * is queued (sender, recipients, reference doc, status, error) and DELETE/PURGE * stale entries — without opening the ERPNext desk. * * Backed by the ERPNext REST API (same token the hub already uses). * * Routes: * GET /email-queue?status=Not%20Sent&limit=200 → { rows, count } * DELETE /email-queue/:name → { ok, deleted } * POST /email-queue/purge { status } → { ok, deleted } */ const { json, parseBody, erpFetch } = require('./helpers') async function listQueue (status, limit) { const params = new URLSearchParams({ fields: JSON.stringify(['name', 'status', 'sender', 'reference_doctype', 'reference_name', 'creation', 'error', 'retry']), order_by: 'creation desc', limit_page_length: String(Math.min(500, limit || 200)), }) if (status) params.set('filters', JSON.stringify([['status', '=', status]])) const res = await erpFetch('/api/resource/Email Queue?' + params) if (res.status !== 200) throw new Error('ERP list failed: ' + res.status) const rows = res.data?.data || [] // ERPNext ignores `fields` when listing a child doctype via REST (returns // only `name`), so read each row's recipients from its full doc instead. // Capped to keep the call snappy on a large queue. const CAP = 60 await Promise.all(rows.slice(0, CAP).map(async (r) => { try { const d = await erpFetch('/api/resource/Email Queue/' + encodeURIComponent(r.name)) r.recipients = (d.data?.data?.recipients || []).map(x => x.recipient).filter(Boolean) } catch { r.recipients = [] } })) return rows } async function deleteOne (name) { const r = await erpFetch('/api/resource/Email Queue/' + encodeURIComponent(name), { method: 'DELETE' }) return r.status === 202 || r.status === 200 } async function handle (req, res, method, path, url) { // GET /email-queue if (path === '/email-queue' && method === 'GET') { const rows = await listQueue(url.searchParams.get('status') || '', parseInt(url.searchParams.get('limit') || '200', 10)) const byStatus = {} for (const r of rows) byStatus[r.status] = (byStatus[r.status] || 0) + 1 return json(res, 200, { rows, count: rows.length, by_status: byStatus }) } // POST /email-queue/purge { status } — checked before the generic :name route if (path === '/email-queue/purge' && method === 'POST') { const body = await parseBody(req) const status = body.status || 'Not Sent' const rows = await listQueue(status, 500) let deleted = 0 for (const r of rows) { if (await deleteOne(r.name)) deleted++ } return json(res, 200, { ok: true, deleted, status }) } // DELETE /email-queue/:name const m = path.match(/^\/email-queue\/(.+)$/) if (m && method === 'DELETE') { const name = decodeURIComponent(m[1]) if (await deleteOne(name)) return json(res, 200, { ok: true, deleted: name }) return json(res, 500, { error: 'delete failed', name }) } return json(res, 404, { error: 'Not found' }) } module.exports = { handle }