'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 || `Re: [Ticket#${tn}] ${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) await sendMail({ to, cc, bcc: bccList, subject, bodyHtml: processedBodyHtml, attachments, inlineImages: finalInlineImages, inReplyTo, references, 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, messageId, 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;