import Imap, { Box } from "imap"; import { resolve } from "path"; import { getMailbox, getMailboxModseq, updateMailbox, updateMailboxModseq } from "../../db/imap/imap-db"; import { Attrs, AttrsWithEnvelope } from "../../interfaces/mail/attrs.interface"; import logger from "../../system/Logger"; import RegisterMessageInApp from "../message/saveMessage"; import { saveMessage } from "../message/storeMessage"; import updateMessage from "../message/updateMessage"; export interface ImapInfo { uid: number; modseq: string; flags: string[]; } export default class Mailbox { imap: Imap; boxName: string; id: number; box: Box; msgToSync: number; syncing: boolean; constructor(_imap: Imap, _boxId: number, _boxName: string) { this.imap = _imap; this.boxName = _boxName; this.id = _boxId; this.box; this.msgToSync = 0; this.syncing = false; this.init(); } async init() { // get mailbox from the database this.box = (await getMailbox(this.id))[0]; const isReadOnly = false; this.imap.openBox(this.boxName, isReadOnly, (err, box) => { if (err) logger.err(err); // sync messages and flags this.initSync(box); // wait for new mails this.imap.on("mail", (numNewMsgs: number) => { if (!this.syncing) { // if not syncing restart a sync this.syncManager(this.box.uidnext - 1, this.box.uidnext + numNewMsgs - 1); } else { // else save number of message to sync latter this.msgToSync += numNewMsgs; } }); // wait for flags update this.imap.on("update", (seqno: number, info: ImapInfo) => { logger.log(`Update message ${info.uid} with ${info.flags}`); const updateMsg = new updateMessage(info.uid, info.flags); updateMsg.updateFlags(); }); this.imap.on("expunge", (seqno: number) => { console.log("Message with sequence number " + seqno + " has been deleted from the server."); }); }); } async updateModseq(newModseq: number) { updateMailboxModseq(this.id, newModseq).then(() => { this.box.highestmodseq = newModseq.toString(); }); } async initSync(box: Box) { // sync mail only if has new messages if (this.box.uidnext < box.uidnext) { this.syncManager(this.box.uidnext, box.uidnext); } else { logger.log("Mail already up to date"); } // sync flags const lastModseq = (await getMailboxModseq(this.id))[0]?.modseq ?? 0; if (parseInt(box.highestmodseq) > lastModseq) { const fetchStream = this.imap.fetch("1:*", { bodies: "", modifiers: { changedsince: lastModseq } }); fetchStream.on("message", (message) => { message.once("attributes", (attrs) => { const updateMsg = new updateMessage(attrs.uid, attrs.flags); updateMsg.updateFlags(); }); }); fetchStream.once("error", function (err) { logger.err("Fetch error when syncing flags: " + err); }); fetchStream.once("end", function () { logger.log("Done fetching new flags"); }); } else { logger.log("Flags already up to date"); } this.updateModseq(parseInt(box.highestmodseq)); } syncManager = async (savedUid: number, currentUid: number) => { this.syncing = true; logger.log(`Fetching from ${savedUid} to ${currentUid} uid`); const nbMessageToSync = currentUid - savedUid; let STEP = nbMessageToSync > 300 ? Math.floor(nbMessageToSync / 7) : nbMessageToSync; let mails: AttrsWithEnvelope[] = []; for (let i = 0; i < nbMessageToSync; i += STEP) { mails = []; try { // fetch mails let secondUid = savedUid + STEP < currentUid ? savedUid + STEP : currentUid; await this.mailFetcher(savedUid, secondUid, mails) logger.log(`Fetched ${STEP} uids (${mails.length} messages)`); // save same in the database for (let k = 0; k < mails.length; k++) { try { const messageId = await saveMessage(mails[k], this.id, this.imap); const register = new RegisterMessageInApp(messageId, mails[k], this.id); await register.save(); } catch (error) { logger.err("Failed to save a message: " + error); } } savedUid = secondUid; this.box.uidnext += savedUid; updateMailbox(this.id, savedUid); } catch (error) { logger.err("Failed to sync message " + error); } logger.log(`Saved messages ${i + STEP > nbMessageToSync ? nbMessageToSync : i + STEP}/${nbMessageToSync}`); } // if has receive new msg during last sync then start a new sync if (this.msgToSync > 0) { const currentUid = this.box.uidnext; this.box.uidnext += this.msgToSync; // reset value to allow to detect new incoming message while syncing this.msgToSync = 0; await this.syncManager(currentUid, this.box.uidnext); } this.syncing = false; logger.log(`Finished syncing messages`); }; async mailFetcher(startUid: number, endUid: number, mails: Attrs[]): Promise { return new Promise((resolve, reject) => { const f = this.imap.seq.fetch(`${startUid}:${endUid}`, { size: true, envelope: true, }); f.on("message", (msg, seqno) => { msg.once("attributes", (attrs: AttrsWithEnvelope) => { mails.push(attrs); }); }); f.once("error", (err) => { logger.err("Fetch error: " + err); reject(1); }); f.once("end", async () => { resolve(0); }); }); } addFlag(source: string, flags: string[]): Promise { return new Promise((resolve, reject) => { this.imap.addFlags(source, flags, (err) => { if (err) { reject(err); } else { resolve(); } }); }); } removeFlag(source: string, flags: string[]): Promise { return new Promise((resolve, reject) => { this.imap.delFlags(source, flags, (err) => { if (err) { reject(err); } else { resolve(); } }); }); } }