probably fix things?

This commit is contained in:
ClaraCrazy 2025-12-07 16:11:42 +01:00
parent 9d991486ae
commit f42fbd4e74
No known key found for this signature in database
GPG key ID: EBBC896ACB497011
4 changed files with 381 additions and 364 deletions

View file

@ -9,25 +9,26 @@ class Helper {
*/ */
purgeTimeStamp() { purgeTimeStamp() {
return moment() return moment()
.subtract(config.email.purgeTime.time, config.email.purgeTime.unit) .subtract(config.email.purgeTime.time, config.email.purgeTime.unit)
.toDate() .toDate()
} }
/** /**
* Check if time difference between now and purgeTimeStamp is more than one day * Check if time difference between now and purgeTimeStamp is more than one day
* @param {Date} now * @param {number|Date} now
* @param {Date} past * @param {Date} past
* @returns {Boolean} * @returns {Boolean}
*/ */
moreThanOneDay(now, past) { moreThanOneDay(now, past) {
const DAY_IN_MS = 24 * 60 * 60 * 1000; const DAY_IN_MS = 24 * 60 * 60 * 1000;
if((now - past) / DAY_IN_MS >= 1){
return true const nowMs = now instanceof Date ? now.getTime() : now;
} else { const pastMs = past instanceof Date ? past.getTime() : new Date(past).getTime();
return false
} return (nowMs - pastMs) >= DAY_IN_MS;
} }
/** /**
* Convert time to highest possible unit (minutes, hours, days) where `time > 1` and `Number.isSafeInteger(time)` (whole number) * Convert time to highest possible unit (minutes, hours, days) where `time > 1` and `Number.isSafeInteger(time)` (whole number)
* @param {Number} time * @param {Number} time
@ -43,7 +44,8 @@ class Helper {
if (convertedTime > 60) { if (convertedTime > 60) {
convertedTime = convertedTime / 60 convertedTime = convertedTime / 60
convertedUnit = 'hours'; convertedUnit = 'hours';
}} }
}
if (convertedUnit === 'hours') { if (convertedUnit === 'hours') {
if (convertedTime > 24) { if (convertedTime > 24) {
@ -106,8 +108,8 @@ class Helper {
*/ */
shuffleFirstItem(array) { shuffleFirstItem(array) {
let first = array[Math.floor(Math.random()*array.length)] let first = array[Math.floor(Math.random() * array.length)]
array = array.filter((value)=>value!=first); array = array.filter((value) => value != first);
array = [first].concat(array) array = [first].concat(array)
return array return array
} }
@ -144,4 +146,4 @@ class Helper {
} }
} }
module.exports = Helper module.exports = Helper

View file

@ -1,6 +1,6 @@
const EventEmitter = require('events') const EventEmitter = require('events')
const imaps = require('imap-simple') const imaps = require('imap-simple')
const {simpleParser} = require('mailparser') const { simpleParser } = require('mailparser')
const addressparser = require('nodemailer/lib/addressparser') const addressparser = require('nodemailer/lib/addressparser')
const pSeries = require('p-series') const pSeries = require('p-series')
const retry = require('async-retry') const retry = require('async-retry')
@ -21,20 +21,20 @@ const helper = new(Helper)
* @returns {undefined|Promise} Returns a promise when no callback is specified, resolving when the action succeeds. * @returns {undefined|Promise} Returns a promise when no callback is specified, resolving when the action succeeds.
* @memberof ImapSimple * @memberof ImapSimple
*/ */
imaps.ImapSimple.prototype.deleteMessage = function (uid, callback) { imaps.ImapSimple.prototype.deleteMessage = function(uid, callback) {
var self = this; var self = this;
if (callback) { if (callback) {
return nodeify(self.deleteMessage(uid), callback); return nodeify(self.deleteMessage(uid), callback);
} }
return new Promise(function (resolve, reject) { return new Promise(function(resolve, reject) {
self.imap.addFlags(uid, '\\Deleted', function (err) { self.imap.addFlags(uid, '\\Deleted', function(err) {
if (err) { if (err) {
reject(err); reject(err);
return; return;
} }
self.imap.expunge( function (err) { self.imap.expunge(function(err) {
if (err) { if (err) {
reject(err); reject(err);
return; return;
@ -53,10 +53,10 @@ imaps.ImapSimple.prototype.deleteMessage = function (uid, callback) {
* @returns {undefined|Promise} Returns a promise when no callback is specified, resolving to `boxName` * @returns {undefined|Promise} Returns a promise when no callback is specified, resolving to `boxName`
* @memberof ImapSimple * @memberof ImapSimple
*/ */
imaps.ImapSimple.prototype.closeBox = function (autoExpunge=true, callback) { imaps.ImapSimple.prototype.closeBox = function(autoExpunge = true, callback) {
var self = this; var self = this;
if (typeof(autoExpunge) == 'function'){ if (typeof(autoExpunge) == 'function') {
callback = autoExpunge; callback = autoExpunge;
autoExpunge = true; autoExpunge = true;
} }
@ -65,9 +65,9 @@ imaps.ImapSimple.prototype.closeBox = function (autoExpunge=true, callback) {
return nodeify(this.closeBox(autoExpunge), callback); return nodeify(this.closeBox(autoExpunge), callback);
} }
return new Promise(function (resolve, reject) { return new Promise(function(resolve, reject) {
self.imap.closeBox(autoExpunge, function (err, result) { self.imap.closeBox(autoExpunge, function(err, result) {
if (err) { if (err) {
reject(err); reject(err);
@ -87,271 +87,285 @@ imaps.ImapSimple.prototype.closeBox = function (autoExpunge=true, callback) {
* With this abstraction it would be easy to replace this with any inbound mail service like mailgun.com. * With this abstraction it would be easy to replace this with any inbound mail service like mailgun.com.
*/ */
class ImapService extends EventEmitter { class ImapService extends EventEmitter {
constructor(config) { constructor(config) {
super() super()
this.config = config this.config = config
/** /**
* Set of emitted UIDs. Listeners should get each email only once. * Set of emitted UIDs. Listeners should get each email only once.
* @type {Set<any>} * @type {Set<any>}
*/ */
this.loadedUids = new Set() this.loadedUids = new Set()
this.connection = null this.connection = null
this.initialLoadDone = false this.initialLoadDone = false
} }
async connectAndLoadMessages() { async connectAndLoadMessages() {
const configWithListener = { const configWithListener = {
...this.config, ...this.config,
// 'onmail' adds a callback when new mails arrive. With this we can keep the imap refresh interval very low (or even disable it). // 'onmail' adds a callback when new mails arrive. With this we can keep the imap refresh interval very low (or even disable it).
onmail: () => this._doOnNewMail() onmail: () => this._doOnNewMail()
} }
this.once(ImapService.EVENT_INITIAL_LOAD_DONE, () => this.once(ImapService.EVENT_INITIAL_LOAD_DONE, () =>
this._doAfterInitialLoad() this._doAfterInitialLoad()
) )
await this._connectWithRetry(configWithListener) await this._connectWithRetry(configWithListener)
// Load all messages in the background. (ASYNC) // Load all messages in the background. (ASYNC)
this._loadMailSummariesAndEmitAsEvents() this._loadMailSummariesAndEmitAsEvents()
} }
async _connectWithRetry(configWithListener) { async _connectWithRetry(configWithListener) {
try { try {
await retry( await retry(
async _bail => { async _bail => {
// If anything throws, we retry // If anything throws, we retry
this.connection = await imaps.connect(configWithListener) this.connection = await imaps.connect(configWithListener)
this.connection.on('error', err => { this.connection.on('error', err => {
// We assume that the app will be restarted after a crash. // We assume that the app will be restarted after a crash.
console.error( console.error(
'got fatal error during imap operation, stop app.', 'got fatal error during imap operation, stop app.',
err err
) )
this.emit('error', err) this.emit('error', err)
}) })
await this.connection.openBox('INBOX') await this.connection.openBox('INBOX')
debug('connected to imap') debug('connected to imap')
}, }, {
{ retries: 5
retries: 5 }
} )
) } catch (error) {
} catch (error) { console.error('can not connect, even with retry, stop app', error)
console.error('can not connect, even with retry, stop app', error) throw error
throw error }
} }
}
_doOnNewMail() { _doOnNewMail() {
// Only react to new mails after the initial load, otherwise it might load the same mails twice. // Only react to new mails after the initial load, otherwise it might load the same mails twice.
if (this.initialLoadDone) { if (this.initialLoadDone) {
this._loadMailSummariesAndEmitAsEvents() this._loadMailSummariesAndEmitAsEvents()
} }
} }
_doAfterInitialLoad() { _doAfterInitialLoad() {
// During initial load we ignored new incoming emails. In order to catch up with those, we have to refresh // During initial load we ignored new incoming emails. In order to catch up with those, we have to refresh
// the mails once after the initial load. (async) // the mails once after the initial load. (async)
this._loadMailSummariesAndEmitAsEvents() this._loadMailSummariesAndEmitAsEvents()
// If the above trigger on new mails does not work reliable, we have to regularly check // If the above trigger on new mails does not work reliable, we have to regularly check
// for new mails on the server. This is done only after all the mails have been loaded for the // for new mails on the server. This is done only after all the mails have been loaded for the
// first time. (Note: set the refresh higher than the time it takes to download the mails). // first time. (Note: set the refresh higher than the time it takes to download the mails).
if (this.config.imap.refreshIntervalSeconds) { if (this.config.imap.refreshIntervalSeconds) {
setInterval( setInterval(
() => this._loadMailSummariesAndEmitAsEvents(), () => this._loadMailSummariesAndEmitAsEvents(),
this.config.imap.refreshIntervalSeconds * 1000 this.config.imap.refreshIntervalSeconds * 1000
) )
} }
} }
async _loadMailSummariesAndEmitAsEvents() { async _loadMailSummariesAndEmitAsEvents() {
// UID: Unique id of a message. // UID: Unique id of a message.
const uids = await this._getAllUids() const uids = await this._getAllUids()
const newUids = uids.filter(uid => !this.loadedUids.has(uid)) const newUids = uids.filter(uid => !this.loadedUids.has(uid))
// Optimize by fetching several messages (but not all) with one 'search' call. // Optimize by fetching several messages (but not all) with one 'search' call.
// fetching all at once might be more efficient, but then it takes long until we see any messages // fetching all at once might be more efficient, but then it takes long until we see any messages
// in the frontend. With a small chunk size we ensure that we see the newest emails after a few seconds after // in the frontend. With a small chunk size we ensure that we see the newest emails after a few seconds after
// restart. // restart.
const uidChunks = _.chunk(newUids, 20) const uidChunks = _.chunk(newUids, 20)
// Creates an array of functions. We do not start the search now, we just create the function. // Creates an array of functions. We do not start the search now, we just create the function.
const fetchFunctions = uidChunks.map(uidChunk => () => const fetchFunctions = uidChunks.map(uidChunk => () =>
this._getMailHeadersAndEmitAsEvents(uidChunk) this._getMailHeadersAndEmitAsEvents(uidChunk)
) )
await pSeries(fetchFunctions) await pSeries(fetchFunctions)
if (!this.initialLoadDone) { if (!this.initialLoadDone) {
this.initialLoadDone = true this.initialLoadDone = true
this.emit(ImapService.EVENT_INITIAL_LOAD_DONE) this.emit(ImapService.EVENT_INITIAL_LOAD_DONE)
} }
} }
/** /**
* *
* @param {Date} deleteMailsBefore delete mails before this date instance * @param {Date} deleteMailsBefore delete mails before this date instance
*/ */
async deleteOldMails(deleteMailsBefore) { async deleteOldMails(deleteMailsBefore) {
let uids = [] let uids = []
//fetch mails from date +1day (calculated in MS) to avoid wasting resources and to fix imaps missing time-awareness //fetch mails from date +1day (calculated in MS) to avoid wasting resources and to fix imaps missing time-awareness
if (helper.moreThanOneDay(moment() + 24 * 60 * 60 * 1000, deleteMailsBefore)) { if (helper.moreThanOneDay(moment(), deleteMailsBefore)) {
uids = await this._searchWithoutFetch([ console.log("Deleting mails older than one day");
['!DELETED'], uids = await this._searchWithoutFetch([
['BEFORE', deleteMailsBefore] ['!DELETED'],
]) ['BEFORE', deleteMailsBefore]
} else { ])
uids = await this._searchWithoutFetch([ } else {
['!DELETED'], console.log("Deleting mails without date filter");
]) uids = await this._searchWithoutFetch([
} ['!DELETED'],
])
}
if (uids.length === 0) { if (uids.length === 0) {
return return
} }
const DeleteOlderThan = helper.purgeTimeStamp() const DeleteOlderThan = helper.purgeTimeStamp()
const uidsWithHeaders = await this._getMailHeaders(uids) const uidsWithHeaders = await this._getMailHeaders(uids)
console.log(`Fetched ${uidsWithHeaders.length} mails for deletion check.`);
uidsWithHeaders.forEach(mail => { uidsWithHeaders.forEach(mail => {
if (mail['attributes'].date > DeleteOlderThan || this.config.email.examples.uids.includes(parseInt(mail['attributes'].uid))) { if (mail['attributes'].date > DeleteOlderThan || this.config.email.examples.uids.includes(parseInt(mail['attributes'].uid))) {
uids = uids.filter(uid => uid !== mail['attributes'].uid) uids = uids.filter(uid => uid !== mail['attributes'].uid)
} console.log(mail['attributes'].date > DeleteOlderThan ? `Mail UID: ${mail['attributes'].uid} is newer than purge time.` : `Mail UID: ${mail['attributes'].uid} is an example mail.`);
}) }
})
if (uids.length === 0) { if (uids.length === 0) {
debug('no mails to delete.') console.log("Length 0")
return debug('no mails to delete.')
} return
}
debug(`deleting mails ${uids}`) debug(`deleting mails ${uids}`)
await this.connection.deleteMessage(uids) await this.connection.deleteMessage(uids)
uids.forEach(uid => this.emit(ImapService.EVENT_DELETED_MAIL, uid)) uids.forEach(uid => {
console.log(`deleted ${uids.length} old messages.`) this.emit(ImapService.EVENT_DELETED_MAIL, uid)
} console.log(`UID deleted: ${uid}`);
})
console.log(`deleted ${uids.length} old messages.`)
}
/** /**
* *
* @param uid delete specific mail per UID * @param uid delete specific mail per UID
*/ */
async deleteSpecificEmail(uid) { async deleteSpecificEmail(uid) {
debug(`deleting mails ${uid}`) debug(`deleting mails ${uid}`)
if (!this.config.email.examples.uids.includes(parseInt(uid))) { if (!this.config.email.examples.uids.includes(parseInt(uid))) {
await this.connection.deleteMessage(uid) await this.connection.deleteMessage(uid)
console.log(`deleted mail with UID: ${uid}.`) console.log(`deleted mail with UID: ${uid}.`)
this.emit(ImapService.EVENT_DELETED_MAIL, uid) this.emit(ImapService.EVENT_DELETED_MAIL, uid)
} }
} }
/** /**
* Helper method because ImapSimple#search also fetches each message. We just need the uids here. * Helper method because ImapSimple#search also fetches each message. We just need the uids here.
* *
* @param {Object} searchCriteria (see ImapSimple#search) * @param {Object} searchCriteria (see ImapSimple#search)
* @returns {Promise<Array<Int>>} Array of UIDs * @returns {Promise<Array<Int>>} Array of UIDs
* @private * @private
*/ */
async _searchWithoutFetch(searchCriteria) { async _searchWithoutFetch(searchCriteria) {
const imapUnderlying = this.connection.imap const imapUnderlying = this.connection.imap
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
imapUnderlying.search(searchCriteria, (err, uids) => { imapUnderlying.search(searchCriteria, (err, uids) => {
if (err) { if (err) {
reject(err) reject(err)
} else { } else {
resolve(uids || []) resolve(uids || [])
} }
}) })
}) })
} }
_createMailSummary(message) { _createMailSummary(message) {
const headerPart = message.parts[0].body const headerPart = message.parts[0].body
const to = headerPart.to const to = headerPart.to
.flatMap(to => addressparser(to)) .flatMap(to => addressparser(to))
// The address also contains the name, just keep the email // The address also contains the name, just keep the email
.map(addressObj => addressObj.address) .map(addressObj => addressObj.address)
const from = headerPart.from.flatMap(from => addressparser(from)) const from = headerPart.from.flatMap(from => addressparser(from))
// Specify default subject, in case none exists. // Specify default subject, in case none exists.
let subject = "No Subject" let subject = "No Subject"
try { try {
subject = headerPart.subject[0] subject = headerPart.subject[0]
} catch { } catch {
// Do nothing // Do nothing
} }
const date = headerPart.date[0] const date = headerPart.date[0]
const {uid} = message.attributes const { uid } = message.attributes
return Mail.create(to, from, date, subject, uid) return Mail.create(to, from, date, subject, uid)
} }
async fetchOneFullMail(to, uid, raw = false) { async fetchOneFullMail(to, uid, raw = false) {
if (!this.connection) { if (!this.connection) {
// Here we 'fail fast' instead of waiting for the connection. // Here we 'fail fast' instead of waiting for the connection.
throw new Error('imap connection not ready') throw new Error('imap connection not ready')
} }
debug(`fetching full message ${uid}`) debug(`fetching full message ${uid}`)
// For security we also filter TO, so it is harder to just enumerate all messages. // For security we also filter TO, so it is harder to just enumerate all messages.
const searchCriteria = [['UID', uid], ['TO', to]] const searchCriteria = [
const fetchOptions = { ['UID', uid],
bodies: ['HEADER', ''], // Empty string means full body ['TO', to]
markSeen: false ]
} const fetchOptions = {
bodies: ['HEADER', ''], // Empty string means full body
markSeen: false
}
const messages = await this.connection.search(searchCriteria, fetchOptions) const messages = await this.connection.search(searchCriteria, fetchOptions)
if (messages.length === 0) { if (messages.length === 0) {
return false return false
} else if (!raw) { } else if (!raw) {
const fullBody = await _.find(messages[0].parts, {which: ''}) const fullBody = await _.find(messages[0].parts, { which: '' })
return simpleParser(fullBody.body) return simpleParser(fullBody.body)
} else { } else {
return messages[0].parts[1].body return messages[0].parts[1].body
} }
} }
async _getAllUids() { async _getAllUids() {
// We ignore mails that are flagged as DELETED, but have not been removed (expunged) yet. // We ignore mails that are flagged as DELETED, but have not been removed (expunged) yet.
const uids = await this._searchWithoutFetch([['!DELETED']]) const uids = await this._searchWithoutFetch([
// Create copy to not mutate the original array. Sort with newest first (DESC). ['!DELETED']
return [...uids].sort().reverse() ])
} // Create copy to not mutate the original array. Sort with newest first (DESC).
return [...uids].sort().reverse()
}
async _getMailHeadersAndEmitAsEvents(uids) { async _getMailHeadersAndEmitAsEvents(uids) {
try { try {
const mails = await this._getMailHeaders(uids) const mails = await this._getMailHeaders(uids)
mails.forEach(mail => { mails.forEach(mail => {
this.loadedUids.add(mail.attributes.uid) this.loadedUids.add(mail.attributes.uid)
// Some broadcast messages have no TO field. We have to ignore those messages. // Some broadcast messages have no TO field. We have to ignore those messages.
if (mail.parts[0].body.to) { if (mail.parts[0].body.to) {
this.emit(ImapService.EVENT_NEW_MAIL, this._createMailSummary(mail)) this.emit(ImapService.EVENT_NEW_MAIL, this._createMailSummary(mail))
} }
}) })
} catch (error) { } catch (error) {
debug('can not fetch', error) debug('can not fetch', error)
throw error throw error
} }
} }
async _getMailHeaders(uids) { async _getMailHeaders(uids) {
const fetchOptions = { const fetchOptions = {
envelope: true, envelope: true,
bodies: ['HEADER.FIELDS (FROM TO SUBJECT DATE)'], bodies: ['HEADER.FIELDS (FROM TO SUBJECT DATE)'],
struct: false struct: false
} }
const searchCriteria = [['UID', ...uids]] const searchCriteria = [
return this.connection.search(searchCriteria, fetchOptions) ['UID', ...uids]
} ]
return this.connection.search(searchCriteria, fetchOptions)
}
} }
// Consumers should use these constants: // Consumers should use these constants:
@ -360,4 +374,4 @@ ImapService.EVENT_DELETED_MAIL = 'mailDeleted'
ImapService.EVENT_INITIAL_LOAD_DONE = 'initial load done' ImapService.EVENT_INITIAL_LOAD_DONE = 'initial load done'
ImapService.EVENT_ERROR = 'error' ImapService.EVENT_ERROR = 'error'
module.exports = ImapService module.exports = ImapService

View file

@ -8,86 +8,87 @@ const helper = new(Helper)
class MailProcessingService extends EventEmitter { class MailProcessingService extends EventEmitter {
constructor(mailRepository, imapService, clientNotification, config) { constructor(mailRepository, imapService, clientNotification, config) {
super() super()
this.mailRepository = mailRepository this.mailRepository = mailRepository
this.clientNotification = clientNotification this.clientNotification = clientNotification
this.imapService = imapService this.imapService = imapService
this.config = config this.config = config
// Cached methods: // Cached methods:
this.cachedFetchFullMail = mem( this.cachedFetchFullMail = mem(
this.imapService.fetchOneFullMail.bind(this.imapService), this.imapService.fetchOneFullMail.bind(this.imapService), { maxAge: 10 * 60 * 1000 }
{maxAge: 10 * 60 * 1000} )
)
this.initialLoadDone = false this.initialLoadDone = false
// Delete old messages now and every few hours // Delete old messages now and every few hours
this.imapService.once(ImapService.EVENT_INITIAL_LOAD_DONE, () => this.imapService.once(ImapService.EVENT_INITIAL_LOAD_DONE, () =>
this._deleteOldMails() this._deleteOldMails()
) )
setInterval(() => this._deleteOldMails(), 10 * 60 * 1000) setInterval(() => {
} this._deleteOldMails()
}, 60 * 1000)
}
getMailSummaries(address) { getMailSummaries(address) {
return this.mailRepository.getForRecipient(address) return this.mailRepository.getForRecipient(address)
} }
deleteSpecificEmail(adress, uid) { deleteSpecificEmail(adress, uid) {
if (this.mailRepository.removeUid(uid, adress) == true) { if (this.mailRepository.removeUid(uid, adress) == true) {
this.imapService.deleteSpecificEmail(uid) this.imapService.deleteSpecificEmail(uid)
} }
} }
getOneFullMail(address, uid, raw = false) { getOneFullMail(address, uid, raw = false) {
return this.cachedFetchFullMail(address, uid, raw) return this.cachedFetchFullMail(address, uid, raw)
} }
getAllMailSummaries() { getAllMailSummaries() {
return this.mailRepository.getAll() return this.mailRepository.getAll()
} }
onInitialLoadDone() { onInitialLoadDone() {
this.initialLoadDone = true this.initialLoadDone = true
console.log( console.log(
`initial load done, got ${this.mailRepository.mailCount()} mails` `initial load done, got ${this.mailRepository.mailCount()} mails`
) )
} }
onNewMail(mail) { onNewMail(mail) {
if (this.initialLoadDone) { if (this.initialLoadDone) {
// For now, only log messages if they arrive after the initial load // For now, only log messages if they arrive after the initial load
debug('new mail for', mail.to[0]) debug('new mail for', mail.to[0])
} }
mail.to.forEach(to => { mail.to.forEach(to => {
this.mailRepository.add(to, mail) this.mailRepository.add(to, mail)
return this.clientNotification.emit(to) return this.clientNotification.emit(to)
}) })
} }
onMailDeleted(uid) { onMailDeleted(uid) {
debug('mail deleted with uid', uid) debug('mail deleted with uid', uid)
this.mailRepository.removeUid(uid) this.mailRepository.removeUid(uid)
} }
async _deleteOldMails() { async _deleteOldMails() {
try { try {
await this.imapService.deleteOldMails(helper.purgeTimeStamp()) await this.imapService.deleteOldMails(helper.purgeTimeStamp())
} catch (error) { } catch (error) {
console.log('can not delete old messages', error) console.log('can not delete old messages', error)
} }
} }
_saveToFile(mails, filename) { _saveToFile(mails, filename) {
const fs = require('fs') const fs = require('fs')
fs.writeFile(filename, JSON.stringify(mails), err => { fs.writeFile(filename, JSON.stringify(mails), err => {
if (err) { if (err) {
console.error('can not save mails to file', err) console.error('can not save mails to file', err)
} }
}) })
} }
} }
module.exports = MailProcessingService module.exports = MailProcessingService

View file

@ -4,57 +4,57 @@ const _ = require('lodash')
const config = require('../application/config') const config = require('../application/config')
class MailRepository { class MailRepository {
constructor() { constructor() {
// MultiMap docs: https://yomguithereal.github.io/mnemonist/multi-map // MultiMap docs: https://yomguithereal.github.io/mnemonist/multi-map
this.mailSummaries = new MultiMap() this.mailSummaries = new MultiMap()
this.config = config this.config = config
} }
getForRecipient(address) { getForRecipient(address) {
let mails = this.mailSummaries.get(address) || [] let mails = this.mailSummaries.get(address) || []
mails.forEach(mail => { mails.forEach(mail => {
if (mail.to == this.config.email.examples.account && !this.config.email.examples.uids.includes(parseInt(mail.uid))) { if (mail.to == this.config.email.examples.account && !this.config.email.examples.uids.includes(parseInt(mail.uid))) {
mails = mails.filter(m => m.uid != mail.uid) mails = mails.filter(m => m.uid != mail.uid)
debug('prevented non-example email from being shown in example inbox', mail.uid) console.log('prevented non-example email from being shown in example inbox', mail.uid)
} }
}) })
return _.orderBy(mails, mail => Date.parse(mail.date), ['desc']) return _.orderBy(mails, mail => Date.parse(mail.date), ['desc'])
} }
getAll() { getAll() {
const mails = [...this.mailSummaries.values()] const mails = [...this.mailSummaries.values()]
return _.orderBy(mails, mail => Date.parse(mail.date), ['desc']) return _.orderBy(mails, mail => Date.parse(mail.date), ['desc'])
} }
add(to, mailSummary) { add(to, mailSummary) {
if (to !== undefined) { if (to !== undefined) {
this.mailSummaries.set(to.toLowerCase(), mailSummary) this.mailSummaries.set(to.toLowerCase(), mailSummary)
} else { } else {
debug('IMAP reported no recipient for mail, ignoring', mailSummary) debug('IMAP reported no recipient for mail, ignoring', mailSummary)
} }
} }
removeUid(uid, address) { removeUid(uid, address) {
if (!this.config.email.examples.uids.includes(parseInt(uid))) { if (!this.config.email.examples.uids.includes(parseInt(uid))) {
var deleted = false var deleted = false
// TODO: make this more efficient, looping through each email is not cool. // TODO: make this more efficient, looping through each email is not cool.
this.mailSummaries.forEachAssociation((mails, to) => { this.mailSummaries.forEachAssociation((mails, to) => {
mails mails
.filter(mail => mail.uid === parseInt(uid) && (address ? to == address : true)) .filter(mail => mail.uid === parseInt(uid) && (address ? to == address : true))
.forEach(mail => { .forEach(mail => {
this.mailSummaries.remove(to, mail) this.mailSummaries.remove(to, mail)
debug('removed ', mail.date, to, mail.subject) debug('removed ', mail.date, to, mail.subject)
deleted = true deleted = true
}) })
}) })
return deleted return deleted
} }
return false return false
} }
mailCount() { mailCount() {
return this.mailSummaries.size return this.mailSummaries.size
} }
} }
module.exports = MailRepository module.exports = MailRepository