import Imap from 'imap'; import { ObjectID } from 'mongodb' // import Message from './Message'; // import Conversation from './Conversation'; import Log from './Log'; import * as DB from './data/Data'; import ImapController from './imap/ImapController'; // interface Contact { // name: string; // addresses: Set; // } export default class Account { private address: string; private accountID: ObjectID; private conn: ImapController; // private contacts: Contact[] = []; // private conversations: Conversation[] = []; constructor(data: DB.Account) { this.accountID = data._id; this.address = data.address; Log.info('Created account %s', data.address); this.conn = new ImapController({ user: data.address, password: data.password, host: data.host, port: data.port, tls: data.tls }); } async init() { await this.conn.connect(); Log.perfStart('Synchronizing ' + this.address); const remoteBoxes = await this.getBoxes(); await this.synchronizeBoxes(remoteBoxes); await this.synchronizeMessages(remoteBoxes); Log.perfEnd('Synchronizing ' + this.address); // Log.info('Connected to %s', this.data.address); // await this.synchronizeData(); // Log.perfEnd('Synchronizing ' + this.data.address); // const messages = await this.fetchAllMessages(); // this.conversations = this.createConversations(messages).filter(c => c.active); // this.contacts = this.createContacts(messages); } async synchronizeBoxes(remoteBoxes: Map): Promise { const currentBoxes = await DB.MailboxModel.find({ account: this.accountID }); await Promise.all([ ...remoteBoxes.values() ].map(async box => { const existing = currentBoxes.filter(b => b.path === box.name)[0]; if (!existing) await this.addNewBox(box); else await this.refreshExistingBox(box, existing); })); } private async addNewBox(box: Imap.Box) { await DB.MailboxModel.create({ account: this.accountID, name: box.name, // TODO: This path: box.name, delimiter: '.', // TODO: and this type: DB.MailboxType.Inbox, treeTypes: new Set([ DB.MailboxType.Inbox ]), parent: undefined, // and this uidValidity: box.uidvalidity, uidNext: 1, } as DB.Create); } private async refreshExistingBox(remote: Imap.Box, _existing: DB.Mailbox) { Log.debug('existing box ' + remote.name); } async synchronizeMessages(remoteBoxes: Map): Promise { const currentBoxes = await DB.MailboxModel.find({ account: this.accountID }); await Promise.all(currentBoxes.map(async box => { const remote = remoteBoxes.get(box.path)!; console.log(box.uidNext, remote.uidnext); // if (box.uidValidity !== remote.uidvalidity) { // // Reacquire existing messages // } if (box.uidNext !== remote.uidnext) { // Get new messages const messages = await (await this.conn.get(box.path)).fetchMessagesByUID(`${box.uidNext}:*`); await DB.MailboxModel.updateOne({ _id: box._id }, { uidNext: remote.uidnext }); if (messages.size > 0) { console.log('adding ' + messages.size + ' messages.'); await DB.MessageModel.insertMany([ ...messages.keys() ].map(uid => { const message = messages.get(uid)!; const headers = this.parseHeaders(message.headers); return { account: this.accountID, box: box._id, uid: uid, messageId: headers.get('MESSAGE-ID') ?? '[!DATE:' + (+message.attrs.date) + ']', subject: this.cleanSubject(headers.get('SUBJECT')), date: message.attrs.date, } as DB.Message; })); } } })); } private async getBoxes(): Promise> { Log.perfStart('Getting boxes for ' + this.address); const remoteBoxes = await (await this.conn.get()).getBoxes(); const boxReqs: Promise[] = []; const boxes: Map = new Map(); const reqBoxesRecursively = (tree: Imap.MailBoxes, path: string = '') => { Object.keys(tree).forEach(name => { boxReqs.push((async () => { const box = (await this.conn.get(path + name)).getOpenBoxProps(); boxes.set(path + name, box); })()); if (tree[name].children) reqBoxesRecursively(tree[name].children, path + name + tree[name].delimiter); }); } reqBoxesRecursively(remoteBoxes); await Promise.all(boxReqs); Log.perfEnd('Getting boxes for ' + this.address); return boxes; } private parseHeaders(rawHeaders: string): Map { const headers: Map = new Map(); rawHeaders .split(/\r?\n(?=[A-z:\-_]+)/g) .map(h => h.trim()) .filter(h => h) .forEach(h => { const delimiter = h.indexOf(':'); const name = h.substr(0, delimiter).trim(); const value = h.substr(delimiter + 1).trim(); headers.set(name.toUpperCase(), value); }); return headers; } private cleanSubject(subject: string = '') { return subject.replace(/^((re|fwd?|b?cc)(:| ) *)*/gi, '').trim(); } // async synchronizeData(): Promise { // const existingBoxes = await DB.MailboxModel.find({ account: this.data._id }); // const remoteBoxes = (await this.imap.listBoxes()); // const newBoxes: (Mailbox & { uidValidity: number })[] = []; // const boxValidityChanged: { _id: ObjectID, uidValidity: number }[] = []; // const removedBoxes: Set = new Set(existingBoxes.map(box => box._id)); // for (let remote of remoteBoxes) { // if (!remote.treeTypes.has(DB.MailboxType.Inbox) && // !remote.treeTypes.has(DB.MailboxType.Sent) && // !remote.treeTypes.has(DB.MailboxType.Archives)) continue; // const existing = existingBoxes.filter(box => box.path === remote.path)[0]; // let uidValidity = (await this.imap.openBox(remote.path)).uidvalidity; // // Log.debug('Opened %s', remote.path); // if (!existing) newBoxes.push({ ...remote, uidValidity }); // else { // removedBoxes.delete(existing._id); // if (existing.uidValidity != uidValidity) boxValidityChanged.push({ _id: existing._id, uidValidity }); // } // }; // let createdIDs: Map = new Map(); // for (let box of newBoxes) { // createdIDs.set(box.path, (await DB.MailboxModel.create({ // name: box.name, // path: box.path, // account: this.data._id, // delimiter: box.delimiter, // type: box.type, // treeTypes: box.treeTypes, // parent: (await DB.MailboxModel.findOne({ path: box.parent }))?._id, // uidValidity: box.uidValidity, // uidNext: 1, // } as DB.Create)).id); // } // await Promise.all(boxValidityChanged.map(({ _id, uidValidity }) => // DB.MailboxModel.updateOne({ _id }, { uidValidity, uidNext: 1 }))); // await DB.MailboxModel.deleteMany({ _id: { $in: [ ...removedBoxes ] } }); // for (let box of await DB.MailboxModel.find({ account: this.data._id })) { // await this.imap.openBox(box.path); // const meta = Object.values(await this.imap.fetchMessages(box.uidNext + ':*')); // const contacts: { name: string; addresses: Set }[] = []; // for (let m of meta) { // [ m.from, ...m.to ].forEach(participant => { // for (let contact of contacts) { // if (contact.addresses.has(participant.address)) { // if (participant.name) contact.name = participant.name; // return; // } // } // contacts.push({ // name: participant.name ?? participant.address, // addresses: new Set([ participant.address ]) // }); // }); // } // await Promise.all(contacts.map(async contact => { // const addresses = [ ...contact.addresses ]; // await DB.ContactModel.updateOne( // { addresses: { $elemMatch: { $in: addresses } as any } }, // { // $set: { name: contact.name }, // $addToSet: { addresses } // }, // { upsert: true }); // })); // await Promise.all(meta.map(async meta => { // await DB.MessageModel.updateOne({ messageId: meta.messageId }, { // box: box._id, // uid: meta.boxId, // account: this.data._id, // $setOnInsert: { // subject: meta.subject, // date: meta.date, // from: (await DB.ContactModel.findOne({ addresses: meta.from.address }))!._id // } // } as any as DB.Message, // { upsert: true }); // })); // // await DB.MessageIDModel.insertMany(Object.values(meta).map(meta => // // ({ messageId: meta.messageId, uid: meta.boxId, account: this.data._id, box: box._id }))); // } // const boxes = (await this.imap.listBoxes()).filter(box => // !box.treeTypes.has(MailboxType.Spam) && !box.treeTypes.has(MailboxType.Trash)); // let allMeta: RawMessage[] = []; // for (let box of boxes) { // await this.imap.openBox(box.path); // const meta = await this.imap.fetchMessages('1:*'); // Object.keys(meta).forEach(id => allMeta.push(meta[id])); // } // } // getName() { // return this.name; // } // getAddress() { // return this.address; // } // getImage() { // return this.image; // } // hasUnreads() { // return this.unread; // } // getConversations() { // return this.conversations; // } // getContacts() { // return this.contacts; // } // getMessages(_messages: string[]): Message[] { // return [{ // id: 'AOUEOAEu', // date: new Date(), // from: 'me@auri.xyz', // to: [ 'nicole@aurailus.design' ], // content: '

Lorem ipsum dolor sit amet.

' // }]; // } // async fetchAllMessages(): Promise { // const boxes = (await this.imap.listBoxes()).filter(box => // !box.treeTypes.has(MailboxType.Spam) && !box.treeTypes.has(MailboxType.Trash)); // let allMeta: RawMessage[] = []; // for (let box of boxes) { // await this.imap.openBox(box.path); // const meta = await this.imap.fetchMessages('1:*'); // Object.keys(meta).forEach(id => allMeta.push(meta[id])); // } // allMeta = allMeta.sort((a, b) => +a.date - +b.date); // return allMeta; // } // private createConversations(messages: RawMessage[]): Conversation[] { // const conversations: Conversation[] = []; // messages.forEach(message => { // if (message.replyTo) { // for (let conversation of conversations) { // for (let reference of [ ...message.references, message.replyTo ]) { // if (conversation.messages.has(reference)) { // conversation.date = message.date; // conversation.messages.add(message.messageId); // conversation.title = this.cleanSubjectLine(message.subject); // conversation.active = conversation.active || message.active; // message.to.forEach(p => conversation.participants.add(p.address)); // conversation.participants.add(message.from.address); // return; // } // } // } // } // conversations.push({ // title: this.cleanSubjectLine(message.subject), // messages: new Set([ message.messageId ]), // date: message.date, // active: message.active, // participants: new Set([ message.from.address, ...message.to.map(p => p.address) ]) // }); // }); // conversations.forEach(conversation => { // conversation.participants.delete(this.address); // }); // return conversations.sort((a, b) => +a.date - +b.date); // } // private createContacts(messages: RawMessage[]): Contact[] { // const contacts: Contact[] = []; // for (let message of messages) { // [ message.from, ...message.to ].forEach(participant => { // for (let contact of contacts) { // if (contact.addresses.has(participant.address)) { // if (participant.name) contact.name = participant.name; // return; // } // } // contacts.push({ // name: participant.name ?? participant.address, // addresses: new Set([ participant.address ]) // }); // }); // } // return contacts; // } };