import { ImapFlow } from 'imapflow'; import { simpleParser } from 'mailparser'; import db from '../db.js'; import { generateToken } from '../middleware/auth.js'; import { v4 as uuidv4 } from 'uuid'; let imapClient = null; let pollInterval = null; function getConfig() { return db.prepare('SELECT * FROM email_config WHERE id = 1').get(); } function getBoardByPrefix(prefix) { const boards = db.prepare('SELECT id, title FROM boards WHERE is_archived = 0').all(); return boards.find(b => b.title.toLowerCase().replace(/\s+/g, '-').startsWith(prefix.toLowerCase())); } function getCardByToken(token) { const record = db.prepare(` SELECT et.*, c.title as card_title, l.board_id, b.title as board_title, u.name as user_name FROM email_tokens et JOIN cards c ON c.id = et.card_id JOIN lists l ON l.id = c.list_id JOIN boards b ON b.id = l.board_id LEFT JOIN users u ON u.id = et.user_id WHERE et.token = ? AND et.expires_at > datetime('now') `).get(token); return record; } async function processMessage(msg) { const config = getConfig(); if (!config || !config.inbound_enabled) return; const parsed = await simpleParser(msg.source); const fromAddr = parsed.from?.value?.[0]?.address || ''; const subject = parsed.subject || '(no subject)'; const bodyText = parsed.text || ''; const bodyHtml = parsed.html || ''; const messageId = parsed.messageId || ''; const references = parsed.references || ''; const inReplyTo = parsed.inReplyTo || ''; const sender = db.prepare('SELECT id, name, email FROM users WHERE email = ? AND is_active = 1').get(fromAddr); if (!sender) return; // Check if this is a reply to an existing card if (inReplyTo || references) { const refStr = (inReplyTo + ' ' + references).toLowerCase(); const tokens = db.prepare('SELECT * FROM email_tokens').all(); for (const t of tokens) { if (refStr.includes(t.token.toLowerCase())) { const card = getCardByToken(t.token); if (card) { db.prepare(`INSERT INTO card_comments (card_id, user_id, content, is_email_reply) VALUES (?, ?, ?, 1)`) .run(card.card_id, sender.id, `📧 ${bodyText.trim()}`); db.prepare(`INSERT INTO card_activity (card_id, user_id, action, details) VALUES (?, ?, 'email_reply', ?)`) .run(card.card_id, sender.id, `Replied via email`); return; } } } } // Check if subject matches board prefix pattern (e.g., [tf-board-name] Card title) const prefixMatch = subject.match(/^\[(.+?)\]\s*(.+)/); if (prefixMatch) { const board = getBoardByPrefix(prefixMatch[1]); if (board) { const firstList = db.prepare('SELECT id FROM lists WHERE board_id = ? AND is_archived = 0 ORDER BY position LIMIT 1').get(board.id); if (firstList) { const result = db.prepare(`INSERT INTO cards (list_id, title, description, position, created_by) VALUES (?, ?, ?, ?, ?)`) .run(firstList.id, prefixMatch[2].trim(), bodyText.substring(0, 2000), Date.now(), sender.id); db.prepare(`INSERT INTO card_activity (card_id, user_id, action, details) VALUES (?, ?, 'created_via_email', ?)`) .run(result.lastInsertRowid, sender.id, `Created via email from ${fromAddr}`); db.prepare(`INSERT INTO email_log (from_email, subject, card_id, direction, status) VALUES (?, ?, ?, 'received', 'processed')`) .run(fromAddr, subject, result.lastInsertRowid); return; } } } } async function pollInbox() { const config = getConfig(); if (!config || !config.inbound_enabled) { return; } try { if (!imapClient) { imapClient = new ImapFlow({ host: config.imap_host, port: config.imap_port, secure: true, auth: { user: config.email, pass: config.app_password, }, logger: false, }); await imapClient.connect(); } const lock = await imapClient.getMailboxLock(config.inbound_folder); try { const lastPoll = db.prepare("SELECT value FROM app_state WHERE key = 'last_imap_poll'").get(); let since = new Date(Date.now() - 24 * 60 * 60 * 1000); if (lastPoll) { const d = new Date(lastPoll.value); if (!isNaN(d)) since = d; } for await (const msg of imapClient.fetch({ since }, { source: true })) { try { await processMessage(msg); } catch (err) { console.error('Error processing email:', err.message); } } db.prepare("INSERT OR REPLACE INTO app_state (key, value) VALUES ('last_imap_poll', ?)") .run(new Date().toISOString()); } finally { lock.release(); } } catch (err) { console.error('IMAP poll error:', err.message); imapClient = null; } } export function startImapPolling() { if (pollInterval) clearInterval(pollInterval); const config = getConfig(); if (config?.inbound_enabled) { pollInterval = setInterval(pollInbox, 60000); pollInbox(); } } export function stopImapPolling() { if (pollInterval) { clearInterval(pollInterval); pollInterval = null; } if (imapClient) { imapClient.logout().catch(() => {}); imapClient = null; } } export { pollInbox };