Add email threading, snooze, bulk actions, scheduled send, stars, and attachments; v1.1.21
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -439,8 +439,14 @@ export function register(app) {
|
||||
|
||||
const patch = {}
|
||||
if (typeof req.body?.archived === 'boolean') patch.archived = req.body.archived
|
||||
if (typeof req.body?.starred === 'boolean') patch.starred = req.body.starred
|
||||
if (typeof req.body?.name === 'string') patch.name = req.body.name.trim().slice(0, 200)
|
||||
if (typeof req.body?.notes === 'string') patch.notes = req.body.notes.trim().slice(0, 2000)
|
||||
if ('snoozedUntil' in (req.body ?? {})) {
|
||||
const v = req.body.snoozedUntil
|
||||
patch.snoozedUntil = v === null ? null : (typeof v === 'string' && !isNaN(Date.parse(v)) ? v : undefined)
|
||||
if (patch.snoozedUntil === undefined) delete patch.snoozedUntil
|
||||
}
|
||||
|
||||
let found = false
|
||||
state.contactSubmissions = state.contactSubmissions.map(item => {
|
||||
@@ -473,6 +479,46 @@ export function register(app) {
|
||||
res.json({ ok: true })
|
||||
})
|
||||
|
||||
app.post('/api/admin-contact-submissions/bulk', requireAdminAuth, (req, res) => {
|
||||
const { ids, action } = req.body ?? {}
|
||||
if (!Array.isArray(ids) || !['archive', 'unarchive', 'delete', 'star', 'unstar'].includes(action)) {
|
||||
res.status(400).json({ message: 'Invalid bulk action.' }); return
|
||||
}
|
||||
const idSet = new Set(ids.filter(id => typeof id === 'string'))
|
||||
if (idSet.size === 0) { res.json({ ok: true, affected: 0 }); return }
|
||||
|
||||
let affected = 0
|
||||
if (action === 'delete') {
|
||||
const before = state.contactSubmissions.length
|
||||
state.contactSubmissions = state.contactSubmissions.filter(s => !idSet.has(s.id))
|
||||
affected = before - state.contactSubmissions.length
|
||||
} else {
|
||||
const patch = action === 'archive' ? { archived: true }
|
||||
: action === 'unarchive' ? { archived: false }
|
||||
: action === 'star' ? { starred: true }
|
||||
: { starred: false }
|
||||
state.contactSubmissions = state.contactSubmissions.map(s => {
|
||||
if (!idSet.has(s.id)) return s
|
||||
affected++
|
||||
return { ...s, ...patch }
|
||||
})
|
||||
}
|
||||
queueContactSubmissionsWrite()
|
||||
res.json({ ok: true, affected })
|
||||
})
|
||||
|
||||
app.get('/api/admin-contact-submissions/:id/attachments/:attachmentId', requireAdminAuth, (req, res) => {
|
||||
const { id, attachmentId } = req.params
|
||||
const submission = state.contactSubmissions.find(s => s.id === id)
|
||||
if (!submission) { res.status(404).send('Not found.'); return }
|
||||
const attachment = (submission.attachments ?? []).find(a => a.id === attachmentId)
|
||||
if (!attachment) { res.status(404).send('Attachment not found.'); return }
|
||||
const safe = attachment.filename.replace(/[^\w.\-]/g, '_')
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${safe}"`)
|
||||
res.setHeader('Content-Type', attachment.contentType || 'application/octet-stream')
|
||||
res.send(Buffer.from(attachment.data, 'base64'))
|
||||
})
|
||||
|
||||
app.get('/api/admin-reply-config', requireAdminAuth, (_req, res) => {
|
||||
res.json({
|
||||
fromEmail: getResendReplyToAddress(),
|
||||
@@ -510,6 +556,7 @@ export function register(app) {
|
||||
const subject = typeof req.body?.subject === 'string' ? req.body.subject.trim() : ''
|
||||
const message = typeof req.body?.message === 'string' ? req.body.message.trim() : ''
|
||||
const requestedFrom = typeof req.body?.fromAddress === 'string' ? req.body.fromAddress.trim() : ''
|
||||
const scheduledAt = typeof req.body?.scheduledAt === 'string' && !isNaN(Date.parse(req.body.scheduledAt)) && new Date(req.body.scheduledAt) > new Date() ? req.body.scheduledAt : null
|
||||
|
||||
if (!id || typeof id !== 'string') {
|
||||
res.status(400).json({ message: 'Invalid submission id.' }); return
|
||||
@@ -546,6 +593,7 @@ export function register(app) {
|
||||
to: [submission.email],
|
||||
subject,
|
||||
replyTo: replyToAddress,
|
||||
...(scheduledAt ? { scheduledAt } : {}),
|
||||
tags: [
|
||||
{ name: 'flow', value: 'admin-reply' },
|
||||
{ name: 'message_type', value: submission.messageType ?? 'general' },
|
||||
@@ -574,11 +622,12 @@ export function register(app) {
|
||||
subject,
|
||||
preview: message.slice(0, 500),
|
||||
sentAt: new Date().toISOString(),
|
||||
scheduledAt: scheduledAt ?? null,
|
||||
})
|
||||
state.replyHistory = state.replyHistory.slice(0, 500)
|
||||
queueReplyHistoryWrite()
|
||||
|
||||
res.json({ ok: true })
|
||||
res.json({ ok: true, scheduled: Boolean(scheduledAt), scheduledAt: scheduledAt ?? null })
|
||||
} catch (err) {
|
||||
if (typeof req.params?.id === 'string' && req.params.id.trim()) {
|
||||
upsertContactEmailStatus(req.params.id.trim(), 'adminReply', {
|
||||
@@ -602,6 +651,7 @@ export function register(app) {
|
||||
const subject = typeof req.body?.subject === 'string' ? req.body.subject.trim() : ''
|
||||
const message = typeof req.body?.message === 'string' ? req.body.message.trim() : ''
|
||||
const requestedFrom = typeof req.body?.fromAddress === 'string' ? req.body.fromAddress.trim() : ''
|
||||
const scheduledAt = typeof req.body?.scheduledAt === 'string' && !isNaN(Date.parse(req.body.scheduledAt)) && new Date(req.body.scheduledAt) > new Date() ? req.body.scheduledAt : null
|
||||
|
||||
if (!to || !/^[a-zA-Z0-9._%+\-]{1,64}@[a-zA-Z0-9.\-]{1,253}\.[a-zA-Z]{2,}$/.test(to)) {
|
||||
res.status(400).json({ message: 'A valid recipient email address is required.' }); return
|
||||
@@ -630,6 +680,7 @@ export function register(app) {
|
||||
to: [to],
|
||||
subject,
|
||||
replyTo: replyToAddress,
|
||||
...(scheduledAt ? { scheduledAt } : {}),
|
||||
tags: [{ name: 'flow', value: 'admin-reply' }],
|
||||
text,
|
||||
html,
|
||||
@@ -645,11 +696,12 @@ export function register(app) {
|
||||
subject,
|
||||
preview: message.slice(0, 500),
|
||||
sentAt: new Date().toISOString(),
|
||||
scheduledAt: scheduledAt ?? null,
|
||||
})
|
||||
state.replyHistory = state.replyHistory.slice(0, 500)
|
||||
queueReplyHistoryWrite()
|
||||
|
||||
res.json({ ok: true })
|
||||
res.json({ ok: true, scheduled: Boolean(scheduledAt), scheduledAt: scheduledAt ?? null })
|
||||
} catch (err) {
|
||||
console.error('[admin-compose] send error:', err)
|
||||
res.status(500).json({ message: 'Failed to send email.' })
|
||||
|
||||
Reference in New Issue
Block a user