Files

178 lines
6.4 KiB
JavaScript

const Imap = require('imap');
const nodemailer = require('nodemailer');
const { simpleParser } = require('mailparser');
const { getDb } = require('./db');
const util = require('util');
function getAccount(accountId) {
const db = getDb();
return db.prepare('SELECT * FROM email_accounts WHERE id = ?').get(accountId);
}
function getUserAccounts(userId) {
const db = getDb();
return db.prepare('SELECT id, email, name, imap_host, smtp_host, active FROM email_accounts WHERE user_id = ?').all(userId);
}
function createImapConnection(account) {
return new Promise((resolve, reject) => {
const imap = new Imap({
user: account.imap_user,
password: account.imap_pass,
host: account.imap_host,
port: account.imap_port || 993,
tls: account.imap_secure !== 0,
tlsOptions: { rejectUnauthorized: false },
connTimeout: 10000,
authTimeout: 5000
});
imap.once('ready', () => resolve(imap));
imap.once('error', (err) => reject(err));
imap.connect();
});
}
function openMailbox(imap, folder) {
return new Promise((resolve, reject) => {
imap.openBox(folder || 'INBOX', true, (err, box) => {
if (err) return reject(err);
resolve(box);
});
});
}
function searchMessages(imap, criteria) {
return new Promise((resolve, reject) => {
imap.search(criteria, (err, uids) => {
if (err) return reject(err);
resolve(uids || []);
});
});
}
function fetchMessages(imap, uids) {
return new Promise((resolve, reject) => {
if (!uids || uids.length === 0) return resolve([]);
const f = imap.fetch(uids, { bodies: '', struct: true });
const messages = [];
f.on('message', (msg, seqno) => {
const message = { uid: null, seqno, body: '' };
msg.on('body', (stream, info) => {
let buffer = '';
stream.on('data', chunk => buffer += chunk.toString('utf8'));
stream.on('end', () => { message.body = buffer; });
});
msg.on('attributes', attrs => {
message.uid = attrs.uid;
message.flags = attrs.flags || [];
message.date = attrs.date;
message.size = attrs.size;
});
msg.on('end', () => { if (message.uid) messages.push(message); });
});
f.once('error', reject);
f.once('end', () => resolve(messages));
});
}
async function fetchAndStoreMessages(accountId, folder, limit) {
const account = getAccount(accountId);
if (!account) throw new Error('Account not found');
const db = getDb();
const imap = await createImapConnection(account);
const box = await openMailbox(imap, folder || 'INBOX');
const total = Math.min(box.messages.total, limit || 50);
const start = Math.max(1, box.messages.total - total + 1);
const uids = [];
for (let i = start; i <= box.messages.total; i++) uids.push(i);
if (uids.length === 0) { imap.end(); return []; }
const messages = await fetchMessages(imap, uids);
imap.end();
const results = [];
for (const msg of messages) {
try {
const parsed = await simpleParser(msg.body);
const seen = msg.flags.includes('\\Seen');
const flagged = msg.flags.includes('\\Flagged');
const existing = db.prepare('SELECT id FROM email_messages WHERE account_id = ? AND uid = ?').get(accountId, msg.uid);
if (!existing) {
db.prepare(`INSERT INTO email_messages (account_id, uid, folder, subject, from_name, from_addr, to_addr, date, body_text, body_html, attachments, seen, flagged, size)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(
accountId, msg.uid, folder || 'INBOX',
(parsed.subject || '').substring(0, 500),
(parsed.from && parsed.from.text || '').substring(0, 200),
(parsed.from && parsed.from.value && parsed.from.value[0] && parsed.from.value[0].address || ''),
(parsed.to && parsed.to.text || '').substring(0, 500),
(parsed.date && parsed.date.toISOString() || msg.date || ''),
(parsed.text || '').substring(0, 100000),
(parsed.html || '').substring(0, 100000),
(parsed.attachments && parsed.attachments.length) || 0,
seen ? 1 : 0, flagged ? 1 : 0, msg.size || 0
);
} else {
db.prepare('UPDATE email_messages SET seen=?, flagged=? WHERE id=?').run(seen ? 1 : 0, flagged ? 1 : 0, existing.id);
}
results.push({
uid: msg.uid, subject: parsed.subject || '', from: parsed.from ? parsed.from.text : '',
date: parsed.date || msg.date, seen, flagged, size: msg.size
});
} catch (e) {
console.error('Error parsing message:', e.message);
}
}
return results;
}
async function sendEmail(accountId, to, subject, text, html, cc, bcc) {
const account = getAccount(accountId);
if (!account) throw new Error('Account not found');
const transporter = nodemailer.createTransport({
host: account.smtp_host,
port: account.smtp_port || 587,
secure: account.smtp_secure === 1,
auth: { user: account.smtp_user || account.imap_user, pass: account.smtp_pass || account.imap_pass }
});
const info = await transporter.sendMail({
from: `"${account.name || account.email}" <${account.email}>`,
to, subject, text, html, cc, bcc
});
return info;
}
async function getMailboxes(accountId) {
const account = getAccount(accountId);
if (!account) return [];
const imap = await createImapConnection(account);
const list = await new Promise((resolve, reject) => {
imap.getBoxes((err, boxes) => {
if (err) return reject(err);
resolve(boxes);
});
});
imap.end();
function flatten(boxes, path) {
const result = [];
for (const [name, box] of Object.entries(boxes)) {
const fullPath = path ? `${path}${box.delimiter}${name}` : name;
result.push({ name: fullPath, delimiter: box.delimiter });
if (box.children) result.push(...flatten(box.children, fullPath));
}
return result;
}
return flatten(list, '');
}
async function getMessageBody(accountId, uid) {
const account = getAccount(accountId);
if (!account) return null;
const imap = await createImapConnection(account);
await openMailbox(imap, 'INBOX');
const messages = await fetchMessages(imap, [uid]);
imap.end();
if (messages.length === 0) return null;
const parsed = await simpleParser(messages[0].body);
return parsed;
}
module.exports = { getAccount, getUserAccounts, createImapConnection, openMailbox, searchMessages, fetchMessages, fetchAndStoreMessages, sendEmail, getMailboxes, getMessageBody };