425 lines
15 KiB
JavaScript
425 lines
15 KiB
JavaScript
'use strict';
|
|
|
|
const express = require('express');
|
|
const router = express.Router();
|
|
const pool = require('../db');
|
|
const { db } = require('../activityDb');
|
|
const { sendMail } = require('../utils/mailer');
|
|
|
|
// Helper to get local timestamp in YYYY-MM-DD HH:mm:ss format
|
|
function getLocalTimestamp() {
|
|
const d = new Date();
|
|
const pad = (n) => String(n).padStart(2, '0');
|
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
}
|
|
|
|
// ─── SIGNATURES ────────────────────────────────────────────────────────────────
|
|
|
|
// GET /api/email/signatures — list signatures for a given agent
|
|
router.get('/signatures', (req, res) => {
|
|
try {
|
|
const { agent_id } = req.query;
|
|
if (!agent_id) return res.status(400).json({ error: 'agent_id è obbligatorio' });
|
|
|
|
const rows = db.prepare(`
|
|
SELECT id, name, body_html, is_default, created_at, updated_at
|
|
FROM email_signatures
|
|
WHERE agent_id = ?
|
|
ORDER BY is_default DESC, name ASC
|
|
`).all(parseInt(agent_id, 10));
|
|
|
|
res.json(rows);
|
|
} catch (err) {
|
|
console.error('[Email] Error listing signatures:', err);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// POST /api/email/signatures — create a new signature
|
|
router.post('/signatures', (req, res) => {
|
|
try {
|
|
const { agent_id, name, body_html, is_default = 0 } = req.body;
|
|
if (!agent_id || !name) return res.status(400).json({ error: 'agent_id e name sono obbligatori' });
|
|
|
|
// If new signature is default, reset others for this agent
|
|
if (is_default) {
|
|
db.prepare(`UPDATE email_signatures SET is_default = 0 WHERE agent_id = ?`).run(parseInt(agent_id, 10));
|
|
}
|
|
|
|
const result = db.prepare(`
|
|
INSERT INTO email_signatures (agent_id, name, body_html, is_default)
|
|
VALUES (?, ?, ?, ?)
|
|
`).run(parseInt(agent_id, 10), name, body_html || '', is_default ? 1 : 0);
|
|
|
|
res.json({ id: result.lastInsertRowid, agent_id, name, body_html, is_default });
|
|
} catch (err) {
|
|
console.error('[Email] Error creating signature:', err);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// PUT /api/email/signatures/:id — update a signature
|
|
router.put('/signatures/:id', (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { agent_id, name, body_html, is_default } = req.body;
|
|
|
|
// If new signature is default, reset others for this agent
|
|
if (is_default && agent_id) {
|
|
db.prepare(`UPDATE email_signatures SET is_default = 0 WHERE agent_id = ?`).run(parseInt(agent_id, 10));
|
|
}
|
|
|
|
db.prepare(`
|
|
UPDATE email_signatures
|
|
SET name = ?, body_html = ?, is_default = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
WHERE id = ?
|
|
`).run(name, body_html || '', is_default ? 1 : 0, parseInt(id, 10));
|
|
|
|
const updated = db.prepare(`SELECT * FROM email_signatures WHERE id = ?`).get(parseInt(id, 10));
|
|
res.json(updated);
|
|
} catch (err) {
|
|
console.error('[Email] Error updating signature:', err);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// PATCH /api/email/signatures/:id/default — set a signature as default
|
|
router.patch('/signatures/:id/default', (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { agent_id } = req.body;
|
|
|
|
if (agent_id) {
|
|
db.prepare(`UPDATE email_signatures SET is_default = 0 WHERE agent_id = ?`).run(parseInt(agent_id, 10));
|
|
}
|
|
|
|
db.prepare(`
|
|
UPDATE email_signatures SET is_default = 1, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
WHERE id = ?
|
|
`).run(parseInt(id, 10));
|
|
|
|
res.json({ success: true });
|
|
} catch (err) {
|
|
console.error('[Email] Error setting default signature:', err);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// DELETE /api/email/signatures/:id — delete a signature
|
|
router.delete('/signatures/:id', (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
db.prepare(`DELETE FROM email_signatures WHERE id = ?`).run(parseInt(id, 10));
|
|
res.json({ success: true });
|
|
} catch (err) {
|
|
console.error('[Email] Error deleting signature:', err);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// ─── ADDRESS GROUPS ────────────────────────────────────────────────────────────
|
|
|
|
// GET /api/email/address-groups — list groups for a given agent
|
|
router.get('/address-groups', (req, res) => {
|
|
try {
|
|
const { agent_id } = req.query;
|
|
if (!agent_id) return res.status(400).json({ error: 'agent_id è obbligatorio' });
|
|
|
|
const rows = db.prepare(`
|
|
SELECT id, name, emails, created_at, updated_at
|
|
FROM email_address_groups
|
|
WHERE agent_id = ?
|
|
ORDER BY name ASC
|
|
`).all(parseInt(agent_id, 10));
|
|
|
|
res.json(rows);
|
|
} catch (err) {
|
|
console.error('[Email] Error fetching address groups:', err);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// POST /api/email/address-groups — create a new group
|
|
router.post('/address-groups', (req, res) => {
|
|
try {
|
|
const { agent_id, name, emails } = req.body;
|
|
if (!agent_id || !name || !emails) {
|
|
return res.status(400).json({ error: 'Campi agent_id, name e emails sono obbligatori' });
|
|
}
|
|
|
|
const info = db.prepare(`
|
|
INSERT INTO email_address_groups (agent_id, name, emails)
|
|
VALUES (?, ?, ?)
|
|
`).run(parseInt(agent_id, 10), name.trim(), emails.trim());
|
|
|
|
res.json({ id: info.lastInsertRowid, success: true });
|
|
} catch (err) {
|
|
console.error('[Email] Error creating address group:', err);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// PUT /api/email/address-groups/:id — update a group
|
|
router.put('/address-groups/:id', (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { name, emails } = req.body;
|
|
if (!name || !emails) {
|
|
return res.status(400).json({ error: 'Campi name e emails sono obbligatori' });
|
|
}
|
|
|
|
db.prepare(`
|
|
UPDATE email_address_groups
|
|
SET name = ?, emails = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
WHERE id = ?
|
|
`).run(name.trim(), emails.trim(), parseInt(id, 10));
|
|
|
|
res.json({ success: true });
|
|
} catch (err) {
|
|
console.error('[Email] Error updating address group:', err);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// DELETE /api/email/address-groups/:id — delete a group
|
|
router.delete('/address-groups/:id', (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
db.prepare(`DELETE FROM email_address_groups WHERE id = ?`).run(parseInt(id, 10));
|
|
res.json({ success: true });
|
|
} catch (err) {
|
|
console.error('[Email] Error deleting address group:', err);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
// ─── SEND EMAIL ───────────────────────────────────────────────────────────────
|
|
|
|
// POST /api/email/send — send an email for a ticket
|
|
router.post('/send', async (req, res) => {
|
|
const {
|
|
ticketId,
|
|
to,
|
|
cc = [],
|
|
bcc = [],
|
|
subject: customSubject,
|
|
bodyHtml,
|
|
attachments = [],
|
|
inlineImages = [],
|
|
agentId,
|
|
agentName = 'Agente',
|
|
keepHelpdeskCopy = true,
|
|
inReplyTo,
|
|
references,
|
|
} = req.body;
|
|
|
|
if (!ticketId) return res.status(400).json({ error: 'ticketId è obbligatorio' });
|
|
if (!to || !to.length) return res.status(400).json({ error: 'Il campo "to" è obbligatorio' });
|
|
if (!bodyHtml) return res.status(400).json({ error: 'Il corpo della email è obbligatorio' });
|
|
|
|
try {
|
|
// 1. Fetch ticket number and title for subject
|
|
const ticketResult = await pool.query(
|
|
`SELECT tn, title FROM ticket WHERE id = $1`,
|
|
[ticketId]
|
|
);
|
|
if (!ticketResult.rows.length) return res.status(404).json({ error: 'Ticket non trovato' });
|
|
|
|
const { tn, title } = ticketResult.rows[0];
|
|
const subject = customSubject || `[Ticket#${tn}] Re: ${title}`;
|
|
|
|
// 2. Build BCC list (include OTRS system mailbox if keepHelpdeskCopy is true)
|
|
const bccList = [...bcc];
|
|
if (keepHelpdeskCopy) {
|
|
const otrsBcc = process.env.OTRS_MAIL_BCC;
|
|
if (otrsBcc && !bccList.includes(otrsBcc)) bccList.push(otrsBcc);
|
|
}
|
|
|
|
// Extract inline base64 images from bodyHtml and replace with CID references
|
|
const extractedInlineImages = [];
|
|
let processedBodyHtml = bodyHtml;
|
|
let cidCounter = 1;
|
|
processedBodyHtml = bodyHtml.replace(/src="data:([^;]+);base64,([^"]+)"/g, (match, contentType, base64Data) => {
|
|
let ext = 'png'; // default fallback
|
|
if (contentType) {
|
|
const parts = contentType.split('/');
|
|
if (parts.length === 2) {
|
|
ext = parts[1];
|
|
if (ext === 'jpeg') ext = 'jpg';
|
|
}
|
|
}
|
|
const cid = `inline-image-${Date.now()}-${cidCounter++}.${ext}`;
|
|
extractedInlineImages.push({
|
|
cid,
|
|
content: base64Data,
|
|
contentType
|
|
});
|
|
return `src="cid:${cid}"`;
|
|
});
|
|
|
|
const finalInlineImages = [...inlineImages, ...extractedInlineImages];
|
|
|
|
// Generate unique Message-ID
|
|
const messageId = `<${Date.now()}.${Math.random().toString(36).substring(2)}@pharmaidea.com>`;
|
|
|
|
// 3. Send via configured mailer (Graph API or SMTP)
|
|
const mailResult = await sendMail({ to, cc, bcc: bccList, subject, bodyHtml: processedBodyHtml, attachments, inlineImages: finalInlineImages, inReplyTo, references, messageId });
|
|
const finalMessageId = (mailResult && mailResult.internetMessageId) || messageId;
|
|
|
|
// 4. Log article in OTRS ticket via DB as a standard Email article
|
|
try {
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const agentLoginResult = agentId
|
|
? await pool.query(`SELECT login FROM users WHERE id = $1`, [agentId])
|
|
: null;
|
|
const agentLogin = agentLoginResult?.rows[0]?.login || 'system';
|
|
|
|
// Query email from user_preferences for agent
|
|
let agentEmail = '';
|
|
if (agentId) {
|
|
const prefRes = await pool.query(
|
|
`SELECT preferences_value FROM user_preferences WHERE user_id = $1 AND preferences_key = 'UserEmail'`,
|
|
[agentId]
|
|
);
|
|
if (prefRes.rows.length > 0) {
|
|
agentEmail = prefRes.rows[0].preferences_value || '';
|
|
}
|
|
}
|
|
const aFrom = agentEmail ? `"${agentLogin}" <${agentEmail}>` : `"${agentLogin}" <${process.env.AZURE_MAIL_SENDER || process.env.SMTP_FROM || 'helpdesk@example.com'}>`;
|
|
|
|
const toList = to.join(', ');
|
|
|
|
const localNow = getLocalTimestamp();
|
|
|
|
// Insert article via DB metadata (Email channel=1, Visible to customer=1)
|
|
const artInsert = await pool.query(`
|
|
INSERT INTO article (
|
|
ticket_id, article_sender_type_id, communication_channel_id,
|
|
is_visible_for_customer, create_time, create_by, change_time, change_by
|
|
) VALUES (
|
|
$1, 1, 1, 1, $3, $2, $3, $2
|
|
) RETURNING id`,
|
|
[ticketId, agentId || 1, localNow]
|
|
);
|
|
|
|
const articleId = artInsert.rows[0]?.id;
|
|
|
|
if (articleId) {
|
|
// Write standard HTML MIME data
|
|
await pool.query(`
|
|
INSERT INTO article_data_mime (article_id, a_from, a_to, a_cc, a_bcc, a_subject, a_body, a_content_type, a_message_id, incoming_time, create_time, create_by, change_time, change_by)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, 'text/html; charset=utf-8', $8, $9, $11, $10, $11, $10)`,
|
|
[articleId, aFrom, toList, cc.join(', '), bccList.join(', '), subject, processedBodyHtml, finalMessageId, now, agentId || 1, localNow]
|
|
);
|
|
|
|
// Helper to strip HTML tags
|
|
const stripHtml = (html) => {
|
|
if (!html) return '';
|
|
return html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
};
|
|
|
|
// Helper to clean search index values
|
|
const cleanSearchValue = (str) => {
|
|
if (!str) return '';
|
|
return str.toLowerCase()
|
|
.replace(/[^\w\s@.+-]/g, ' ')
|
|
.replace(/\s+/g, ' ')
|
|
.trim();
|
|
};
|
|
|
|
const plainBody = stripHtml(processedBodyHtml);
|
|
|
|
// 1. Write standard plain text version for client fallbacks
|
|
await pool.query(`
|
|
INSERT INTO article_data_mime_plain (article_id, body, create_time, create_by, change_time, change_by)
|
|
VALUES ($1, $2, $3, $4, $3, $4)`,
|
|
[articleId, plainBody, localNow, agentId || 1]
|
|
);
|
|
|
|
// 2. Populate OTRS fulltext search index (article_search_index)
|
|
const indexRows = [
|
|
{ key: 'MIMEBase_From', val: aFrom },
|
|
{ key: 'MIMEBase_To', val: toList },
|
|
{ key: 'MIMEBase_Subject', val: subject },
|
|
{ key: 'MIMEBase_Body', val: plainBody }
|
|
];
|
|
if (cc && cc.length) {
|
|
indexRows.push({ key: 'MIMEBase_Cc', val: cc.join(', ') });
|
|
}
|
|
|
|
for (const row of indexRows) {
|
|
if (row.val) {
|
|
await pool.query(`
|
|
INSERT INTO article_search_index (ticket_id, article_id, article_key, article_value)
|
|
VALUES ($1, $2, $3, $4)`,
|
|
[ticketId, articleId, row.key, cleanSearchValue(row.val)]
|
|
);
|
|
}
|
|
}
|
|
|
|
// 3. Write attachments (the special 'file-1' HTML body, normal ones, and inline images) to article_data_mime_attachment
|
|
const allAtts = [
|
|
{
|
|
filename: 'file-1',
|
|
contentType: 'text/html; charset="utf-8"',
|
|
content: Buffer.from(processedBodyHtml).toString('base64'),
|
|
disposition: '',
|
|
contentId: null
|
|
},
|
|
...attachments.map(a => ({
|
|
filename: a.filename,
|
|
contentType: a.contentType || 'application/octet-stream',
|
|
content: a.content, // base64
|
|
disposition: 'attachment',
|
|
contentId: null
|
|
})),
|
|
...extractedInlineImages.map(img => ({
|
|
filename: img.cid,
|
|
contentType: img.contentType || 'image/png',
|
|
content: img.content, // base64
|
|
disposition: 'inline',
|
|
contentId: `<${img.cid}>`
|
|
}))
|
|
];
|
|
|
|
for (const att of allAtts) {
|
|
try {
|
|
const byteSize = Buffer.from(att.content, 'base64').length;
|
|
await pool.query(`
|
|
INSERT INTO article_data_mime_attachment (
|
|
article_id, filename, content_size, content_type,
|
|
content_id, disposition, content,
|
|
create_time, create_by, change_time, change_by
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $9, $8, $9, $8)`,
|
|
[
|
|
articleId,
|
|
att.filename,
|
|
byteSize,
|
|
att.contentType,
|
|
att.contentId,
|
|
att.disposition,
|
|
att.content, // base64 text directly
|
|
agentId || 1,
|
|
localNow
|
|
]
|
|
);
|
|
} catch (attErr) {
|
|
console.warn('[Email] Allegato non inserito:', attErr.message);
|
|
}
|
|
}
|
|
}
|
|
} catch (noteErr) {
|
|
console.warn('[Email] Articolo OTRS non inserito a database o non indicizzato (non bloccante):', noteErr.message);
|
|
}
|
|
|
|
console.log(`[Email] ✅ Email inviata per ticket #${tn} a: ${to.join(', ')}`);
|
|
res.json({ success: true, subject, to });
|
|
|
|
} catch (err) {
|
|
console.error('[Email] ❌ Errore invio email:', err.message);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|