const nodemailer = require('nodemailer'); const { ImapFlow } = require('imapflow'); const { simpleParser } = require('mailparser'); function createSmtpTransport(channel, account) { return nodemailer.createTransport({ host: channel.smtp_host, port: channel.smtp_port, secure: Boolean(channel.smtp_secure), auth: { user: account.email, pass: account.auth_code, }, }); } function createImapClient(channel, account) { return new ImapFlow({ host: channel.imap_host, port: channel.imap_port, secure: Boolean(channel.imap_secure), auth: { user: account.email, pass: account.auth_code, }, }); } async function sendMail({ channel, account, payload }) { const transporter = createSmtpTransport(channel, account); const info = await transporter.sendMail({ from: payload.from || formatFrom(account), to: payload.to, cc: payload.cc, bcc: payload.bcc, subject: payload.subject, text: payload.text, html: payload.html, }); return { messageId: info.messageId, accepted: info.accepted, rejected: info.rejected, response: info.response, }; } function formatFrom(account) { if (account.display_name) { return `${account.display_name} <${account.email}>`; } return account.email; } function normalizeAddressList(list = []) { return list.map((item) => ({ name: item.name || '', address: item.address || '', })); } async function fetchLatestEmails({ channel, account, limit = 20, folder = 'INBOX', includeAllFolders = true }) { const client = createImapClient(channel, account); try { await client.connect(); const folders = includeAllFolders ? await listReadableFolders(client) : [folder]; const candidates = []; const perFolderLimit = Math.max(limit * 2, 20); for (const folderName of folders) { try { const folderCandidates = await fetchFolderCandidates(client, folderName, perFolderLimit); candidates.push(...folderCandidates); } catch (_error) { // Some providers expose special folders that are listed but not fetchable. continue; } } const targetMessages = candidates .sort((a, b) => compareDatesDesc(a.date, b.date)) .slice(0, limit > 0 ? limit : candidates.length); return hydrateMessages(client, targetMessages); } finally { await client.logout().catch(() => {}); } } async function listReadableFolders(client) { const folders = []; const listed = await client.list(); for (const mailbox of listed) { if (mailbox.flags?.has('\\Noselect')) { continue; } folders.push(mailbox.path); } if (!folders.length) { return ['INBOX']; } return folders; } async function fetchFolderCandidates(client, folder, limit) { const mailbox = await client.mailboxOpen(folder); const total = mailbox.exists || 0; if (!total) { return []; } const messages = []; const start = Math.max(total - limit + 1, 1); for await (const message of client.fetch(`${start}:${total}`, { uid: true, envelope: true, })) { messages.push({ uid: `${folder}:${message.uid}`, rawUid: String(message.uid), folder, subject: message.envelope?.subject || '', from: normalizeAddressList(message.envelope?.from || []), to: normalizeAddressList(message.envelope?.to || []), date: message.envelope?.date?.toISOString?.() || null, }); } return messages; } async function hydrateMessages(client, messages) { const result = []; for (const message of messages) { try { await client.mailboxOpen(message.folder); const downloaded = await client.download(message.rawUid, null, { uid: true }); const source = downloaded?.content ? await streamToString(downloaded.content) : ''; const simple = await simpleParser(source); result.push({ ...message, subject: simple.subject || message.subject || '', from: normalizeAddressList(simple.from?.value || message.from || []), to: normalizeAddressList(simple.to?.value || message.to || []), date: simple.date ? simple.date.toISOString() : message.date, text: simple.text || '', html: simple.html || '', }); } catch (_error) { result.push({ ...message, text: '', html: '', }); } } return result; } async function fetchMessageDetail({ channel, account, compositeUid }) { const client = createImapClient(channel, account); try { await client.connect(); const [folder, rawUid] = splitCompositeUid(compositeUid); await client.mailboxOpen(folder); const envelope = await client.fetchOne(rawUid, { uid: true, envelope: true }, { uid: true }); const downloaded = await client.download(rawUid, null, { uid: true }); const source = downloaded?.content ? await streamToString(downloaded.content) : ''; const simple = await simpleParser(source); return { uid: compositeUid, rawUid, folder, subject: simple.subject || envelope?.envelope?.subject || '', from: normalizeAddressList(simple.from?.value || envelope?.envelope?.from || []), to: normalizeAddressList(simple.to?.value || envelope?.envelope?.to || []), date: simple.date ? simple.date.toISOString() : envelope?.envelope?.date?.toISOString?.() || null, text: simple.text || '', html: simple.html || '', }; } finally { await client.logout().catch(() => {}); } } function splitCompositeUid(value) { const separatorIndex = String(value).indexOf(':'); if (separatorIndex === -1) { return ['INBOX', String(value)]; } return [ String(value).slice(0, separatorIndex), String(value).slice(separatorIndex + 1), ]; } function streamToString(stream) { return new Promise((resolve, reject) => { const chunks = []; stream.on('data', (chunk) => chunks.push(Buffer.from(chunk))); stream.on('error', reject); stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))); }); } function compareDatesDesc(a, b) { const left = a ? new Date(a).getTime() : 0; const right = b ? new Date(b).getTime() : 0; return right - left; } module.exports = { sendMail, fetchLatestEmails, fetchMessageDetail, };