mirror of
https://github.com/Crazyco-xyz/48hr.email.git
synced 2025-12-14 13:56:32 +01:00
probably fix things?
This commit is contained in:
parent
9d991486ae
commit
f42fbd4e74
4 changed files with 381 additions and 364 deletions
|
|
@ -9,25 +9,26 @@ class Helper {
|
|||
*/
|
||||
purgeTimeStamp() {
|
||||
return moment()
|
||||
.subtract(config.email.purgeTime.time, config.email.purgeTime.unit)
|
||||
.toDate()
|
||||
.subtract(config.email.purgeTime.time, config.email.purgeTime.unit)
|
||||
.toDate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if time difference between now and purgeTimeStamp is more than one day
|
||||
* @param {Date} now
|
||||
* Check if time difference between now and purgeTimeStamp is more than one day
|
||||
* @param {number|Date} now
|
||||
* @param {Date} past
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
moreThanOneDay(now, past) {
|
||||
const DAY_IN_MS = 24 * 60 * 60 * 1000;
|
||||
if((now - past) / DAY_IN_MS >= 1){
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
||||
const nowMs = now instanceof Date ? now.getTime() : now;
|
||||
const pastMs = past instanceof Date ? past.getTime() : new Date(past).getTime();
|
||||
|
||||
return (nowMs - pastMs) >= DAY_IN_MS;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert time to highest possible unit (minutes, hours, days) where `time > 1` and `Number.isSafeInteger(time)` (whole number)
|
||||
* @param {Number} time
|
||||
|
|
@ -43,7 +44,8 @@ class Helper {
|
|||
if (convertedTime > 60) {
|
||||
convertedTime = convertedTime / 60
|
||||
convertedUnit = 'hours';
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
if (convertedUnit === 'hours') {
|
||||
if (convertedTime > 24) {
|
||||
|
|
@ -106,8 +108,8 @@ class Helper {
|
|||
*/
|
||||
|
||||
shuffleFirstItem(array) {
|
||||
let first = array[Math.floor(Math.random()*array.length)]
|
||||
array = array.filter((value)=>value!=first);
|
||||
let first = array[Math.floor(Math.random() * array.length)]
|
||||
array = array.filter((value) => value != first);
|
||||
array = [first].concat(array)
|
||||
return array
|
||||
}
|
||||
|
|
@ -144,4 +146,4 @@ class Helper {
|
|||
}
|
||||
}
|
||||
|
||||
module.exports = Helper
|
||||
module.exports = Helper
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
const EventEmitter = require('events')
|
||||
const imaps = require('imap-simple')
|
||||
const {simpleParser} = require('mailparser')
|
||||
const { simpleParser } = require('mailparser')
|
||||
const addressparser = require('nodemailer/lib/addressparser')
|
||||
const pSeries = require('p-series')
|
||||
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.
|
||||
* @memberof ImapSimple
|
||||
*/
|
||||
imaps.ImapSimple.prototype.deleteMessage = function (uid, callback) {
|
||||
imaps.ImapSimple.prototype.deleteMessage = function(uid, callback) {
|
||||
var self = this;
|
||||
|
||||
if (callback) {
|
||||
return nodeify(self.deleteMessage(uid), callback);
|
||||
}
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
self.imap.addFlags(uid, '\\Deleted', function (err) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
self.imap.addFlags(uid, '\\Deleted', function(err) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
self.imap.expunge( function (err) {
|
||||
self.imap.expunge(function(err) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
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`
|
||||
* @memberof ImapSimple
|
||||
*/
|
||||
imaps.ImapSimple.prototype.closeBox = function (autoExpunge=true, callback) {
|
||||
imaps.ImapSimple.prototype.closeBox = function(autoExpunge = true, callback) {
|
||||
var self = this;
|
||||
|
||||
if (typeof(autoExpunge) == 'function'){
|
||||
if (typeof(autoExpunge) == 'function') {
|
||||
callback = autoExpunge;
|
||||
autoExpunge = true;
|
||||
}
|
||||
|
|
@ -65,9 +65,9 @@ imaps.ImapSimple.prototype.closeBox = function (autoExpunge=true, 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) {
|
||||
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.
|
||||
*/
|
||||
class ImapService extends EventEmitter {
|
||||
constructor(config) {
|
||||
super()
|
||||
this.config = config
|
||||
constructor(config) {
|
||||
super()
|
||||
this.config = config
|
||||
|
||||
/**
|
||||
* Set of emitted UIDs. Listeners should get each email only once.
|
||||
* @type {Set<any>}
|
||||
*/
|
||||
this.loadedUids = new Set()
|
||||
/**
|
||||
* Set of emitted UIDs. Listeners should get each email only once.
|
||||
* @type {Set<any>}
|
||||
*/
|
||||
this.loadedUids = new Set()
|
||||
|
||||
this.connection = null
|
||||
this.initialLoadDone = false
|
||||
}
|
||||
this.connection = null
|
||||
this.initialLoadDone = false
|
||||
}
|
||||
|
||||
async connectAndLoadMessages() {
|
||||
const configWithListener = {
|
||||
...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: () => this._doOnNewMail()
|
||||
}
|
||||
async connectAndLoadMessages() {
|
||||
const configWithListener = {
|
||||
...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: () => this._doOnNewMail()
|
||||
}
|
||||
|
||||
this.once(ImapService.EVENT_INITIAL_LOAD_DONE, () =>
|
||||
this._doAfterInitialLoad()
|
||||
)
|
||||
this.once(ImapService.EVENT_INITIAL_LOAD_DONE, () =>
|
||||
this._doAfterInitialLoad()
|
||||
)
|
||||
|
||||
await this._connectWithRetry(configWithListener)
|
||||
await this._connectWithRetry(configWithListener)
|
||||
|
||||
// Load all messages in the background. (ASYNC)
|
||||
this._loadMailSummariesAndEmitAsEvents()
|
||||
}
|
||||
// Load all messages in the background. (ASYNC)
|
||||
this._loadMailSummariesAndEmitAsEvents()
|
||||
}
|
||||
|
||||
async _connectWithRetry(configWithListener) {
|
||||
try {
|
||||
await retry(
|
||||
async _bail => {
|
||||
// If anything throws, we retry
|
||||
this.connection = await imaps.connect(configWithListener)
|
||||
async _connectWithRetry(configWithListener) {
|
||||
try {
|
||||
await retry(
|
||||
async _bail => {
|
||||
// If anything throws, we retry
|
||||
this.connection = await imaps.connect(configWithListener)
|
||||
|
||||
this.connection.on('error', err => {
|
||||
// We assume that the app will be restarted after a crash.
|
||||
console.error(
|
||||
'got fatal error during imap operation, stop app.',
|
||||
err
|
||||
)
|
||||
this.emit('error', err)
|
||||
})
|
||||
this.connection.on('error', err => {
|
||||
// We assume that the app will be restarted after a crash.
|
||||
console.error(
|
||||
'got fatal error during imap operation, stop app.',
|
||||
err
|
||||
)
|
||||
this.emit('error', err)
|
||||
})
|
||||
|
||||
await this.connection.openBox('INBOX')
|
||||
debug('connected to imap')
|
||||
},
|
||||
{
|
||||
retries: 5
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('can not connect, even with retry, stop app', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
await this.connection.openBox('INBOX')
|
||||
debug('connected to imap')
|
||||
}, {
|
||||
retries: 5
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('can not connect, even with retry, stop app', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
_doOnNewMail() {
|
||||
// Only react to new mails after the initial load, otherwise it might load the same mails twice.
|
||||
if (this.initialLoadDone) {
|
||||
this._loadMailSummariesAndEmitAsEvents()
|
||||
}
|
||||
}
|
||||
_doOnNewMail() {
|
||||
// Only react to new mails after the initial load, otherwise it might load the same mails twice.
|
||||
if (this.initialLoadDone) {
|
||||
this._loadMailSummariesAndEmitAsEvents()
|
||||
}
|
||||
}
|
||||
|
||||
_doAfterInitialLoad() {
|
||||
// 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)
|
||||
this._loadMailSummariesAndEmitAsEvents()
|
||||
_doAfterInitialLoad() {
|
||||
// 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)
|
||||
this._loadMailSummariesAndEmitAsEvents()
|
||||
|
||||
// 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
|
||||
// first time. (Note: set the refresh higher than the time it takes to download the mails).
|
||||
if (this.config.imap.refreshIntervalSeconds) {
|
||||
setInterval(
|
||||
() => this._loadMailSummariesAndEmitAsEvents(),
|
||||
this.config.imap.refreshIntervalSeconds * 1000
|
||||
)
|
||||
}
|
||||
}
|
||||
// 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
|
||||
// first time. (Note: set the refresh higher than the time it takes to download the mails).
|
||||
if (this.config.imap.refreshIntervalSeconds) {
|
||||
setInterval(
|
||||
() => this._loadMailSummariesAndEmitAsEvents(),
|
||||
this.config.imap.refreshIntervalSeconds * 1000
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async _loadMailSummariesAndEmitAsEvents() {
|
||||
// UID: Unique id of a message.
|
||||
async _loadMailSummariesAndEmitAsEvents() {
|
||||
// UID: Unique id of a message.
|
||||
|
||||
const uids = await this._getAllUids()
|
||||
const newUids = uids.filter(uid => !this.loadedUids.has(uid))
|
||||
const uids = await this._getAllUids()
|
||||
const newUids = uids.filter(uid => !this.loadedUids.has(uid))
|
||||
|
||||
// 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
|
||||
// in the frontend. With a small chunk size we ensure that we see the newest emails after a few seconds after
|
||||
// restart.
|
||||
const uidChunks = _.chunk(newUids, 20)
|
||||
// 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
|
||||
// in the frontend. With a small chunk size we ensure that we see the newest emails after a few seconds after
|
||||
// restart.
|
||||
const uidChunks = _.chunk(newUids, 20)
|
||||
|
||||
// Creates an array of functions. We do not start the search now, we just create the function.
|
||||
const fetchFunctions = uidChunks.map(uidChunk => () =>
|
||||
this._getMailHeadersAndEmitAsEvents(uidChunk)
|
||||
)
|
||||
// Creates an array of functions. We do not start the search now, we just create the function.
|
||||
const fetchFunctions = uidChunks.map(uidChunk => () =>
|
||||
this._getMailHeadersAndEmitAsEvents(uidChunk)
|
||||
)
|
||||
|
||||
await pSeries(fetchFunctions)
|
||||
await pSeries(fetchFunctions)
|
||||
|
||||
if (!this.initialLoadDone) {
|
||||
this.initialLoadDone = true
|
||||
this.emit(ImapService.EVENT_INITIAL_LOAD_DONE)
|
||||
}
|
||||
}
|
||||
if (!this.initialLoadDone) {
|
||||
this.initialLoadDone = true
|
||||
this.emit(ImapService.EVENT_INITIAL_LOAD_DONE)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Date} deleteMailsBefore delete mails before this date instance
|
||||
*/
|
||||
async deleteOldMails(deleteMailsBefore) {
|
||||
let uids = []
|
||||
//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)) {
|
||||
uids = await this._searchWithoutFetch([
|
||||
['!DELETED'],
|
||||
['BEFORE', deleteMailsBefore]
|
||||
])
|
||||
} else {
|
||||
uids = await this._searchWithoutFetch([
|
||||
['!DELETED'],
|
||||
])
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {Date} deleteMailsBefore delete mails before this date instance
|
||||
*/
|
||||
async deleteOldMails(deleteMailsBefore) {
|
||||
let uids = []
|
||||
//fetch mails from date +1day (calculated in MS) to avoid wasting resources and to fix imaps missing time-awareness
|
||||
if (helper.moreThanOneDay(moment(), deleteMailsBefore)) {
|
||||
console.log("Deleting mails older than one day");
|
||||
uids = await this._searchWithoutFetch([
|
||||
['!DELETED'],
|
||||
['BEFORE', deleteMailsBefore]
|
||||
])
|
||||
} else {
|
||||
console.log("Deleting mails without date filter");
|
||||
uids = await this._searchWithoutFetch([
|
||||
['!DELETED'],
|
||||
])
|
||||
}
|
||||
|
||||
if (uids.length === 0) {
|
||||
return
|
||||
}
|
||||
if (uids.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const DeleteOlderThan = helper.purgeTimeStamp()
|
||||
const uidsWithHeaders = await this._getMailHeaders(uids)
|
||||
const DeleteOlderThan = helper.purgeTimeStamp()
|
||||
const uidsWithHeaders = await this._getMailHeaders(uids)
|
||||
console.log(`Fetched ${uidsWithHeaders.length} mails for deletion check.`);
|
||||
|
||||
uidsWithHeaders.forEach(mail => {
|
||||
if (mail['attributes'].date > DeleteOlderThan || this.config.email.examples.uids.includes(parseInt(mail['attributes'].uid))) {
|
||||
uids = uids.filter(uid => uid !== mail['attributes'].uid)
|
||||
}
|
||||
})
|
||||
uidsWithHeaders.forEach(mail => {
|
||||
if (mail['attributes'].date > DeleteOlderThan || this.config.email.examples.uids.includes(parseInt(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) {
|
||||
debug('no mails to delete.')
|
||||
return
|
||||
}
|
||||
if (uids.length === 0) {
|
||||
console.log("Length 0")
|
||||
debug('no mails to delete.')
|
||||
return
|
||||
}
|
||||
|
||||
debug(`deleting mails ${uids}`)
|
||||
await this.connection.deleteMessage(uids)
|
||||
uids.forEach(uid => this.emit(ImapService.EVENT_DELETED_MAIL, uid))
|
||||
console.log(`deleted ${uids.length} old messages.`)
|
||||
}
|
||||
debug(`deleting mails ${uids}`)
|
||||
await this.connection.deleteMessage(uids)
|
||||
uids.forEach(uid => {
|
||||
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
|
||||
*/
|
||||
async deleteSpecificEmail(uid) {
|
||||
debug(`deleting mails ${uid}`)
|
||||
if (!this.config.email.examples.uids.includes(parseInt(uid))) {
|
||||
await this.connection.deleteMessage(uid)
|
||||
console.log(`deleted mail with UID: ${uid}.`)
|
||||
this.emit(ImapService.EVENT_DELETED_MAIL, uid)
|
||||
}
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param uid delete specific mail per UID
|
||||
*/
|
||||
async deleteSpecificEmail(uid) {
|
||||
debug(`deleting mails ${uid}`)
|
||||
if (!this.config.email.examples.uids.includes(parseInt(uid))) {
|
||||
await this.connection.deleteMessage(uid)
|
||||
console.log(`deleted mail with UID: ${uid}.`)
|
||||
this.emit(ImapService.EVENT_DELETED_MAIL, uid)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method because ImapSimple#search also fetches each message. We just need the uids here.
|
||||
*
|
||||
* @param {Object} searchCriteria (see ImapSimple#search)
|
||||
* @returns {Promise<Array<Int>>} Array of UIDs
|
||||
* @private
|
||||
*/
|
||||
async _searchWithoutFetch(searchCriteria) {
|
||||
const imapUnderlying = this.connection.imap
|
||||
/**
|
||||
* Helper method because ImapSimple#search also fetches each message. We just need the uids here.
|
||||
*
|
||||
* @param {Object} searchCriteria (see ImapSimple#search)
|
||||
* @returns {Promise<Array<Int>>} Array of UIDs
|
||||
* @private
|
||||
*/
|
||||
async _searchWithoutFetch(searchCriteria) {
|
||||
const imapUnderlying = this.connection.imap
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
imapUnderlying.search(searchCriteria, (err, uids) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(uids || [])
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
imapUnderlying.search(searchCriteria, (err, uids) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(uids || [])
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
_createMailSummary(message) {
|
||||
const headerPart = message.parts[0].body
|
||||
const to = headerPart.to
|
||||
.flatMap(to => addressparser(to))
|
||||
// The address also contains the name, just keep the email
|
||||
.map(addressObj => addressObj.address)
|
||||
_createMailSummary(message) {
|
||||
const headerPart = message.parts[0].body
|
||||
const to = headerPart.to
|
||||
.flatMap(to => addressparser(to))
|
||||
// The address also contains the name, just keep the email
|
||||
.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.
|
||||
let subject = "No Subject"
|
||||
try {
|
||||
subject = headerPart.subject[0]
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
const date = headerPart.date[0]
|
||||
const {uid} = message.attributes
|
||||
// Specify default subject, in case none exists.
|
||||
let subject = "No Subject"
|
||||
try {
|
||||
subject = headerPart.subject[0]
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
const date = headerPart.date[0]
|
||||
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) {
|
||||
if (!this.connection) {
|
||||
// Here we 'fail fast' instead of waiting for the connection.
|
||||
throw new Error('imap connection not ready')
|
||||
}
|
||||
async fetchOneFullMail(to, uid, raw = false) {
|
||||
if (!this.connection) {
|
||||
// Here we 'fail fast' instead of waiting for the connection.
|
||||
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.
|
||||
const searchCriteria = [['UID', uid], ['TO', to]]
|
||||
const fetchOptions = {
|
||||
bodies: ['HEADER', ''], // Empty string means full body
|
||||
markSeen: false
|
||||
}
|
||||
// For security we also filter TO, so it is harder to just enumerate all messages.
|
||||
const searchCriteria = [
|
||||
['UID', uid],
|
||||
['TO', to]
|
||||
]
|
||||
const fetchOptions = {
|
||||
bodies: ['HEADER', ''], // Empty string means full body
|
||||
markSeen: false
|
||||
}
|
||||
|
||||
const messages = await this.connection.search(searchCriteria, fetchOptions)
|
||||
if (messages.length === 0) {
|
||||
return false
|
||||
} else if (!raw) {
|
||||
const fullBody = await _.find(messages[0].parts, {which: ''})
|
||||
return simpleParser(fullBody.body)
|
||||
} else {
|
||||
return messages[0].parts[1].body
|
||||
}
|
||||
}
|
||||
const messages = await this.connection.search(searchCriteria, fetchOptions)
|
||||
if (messages.length === 0) {
|
||||
return false
|
||||
} else if (!raw) {
|
||||
const fullBody = await _.find(messages[0].parts, { which: '' })
|
||||
return simpleParser(fullBody.body)
|
||||
} else {
|
||||
return messages[0].parts[1].body
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async _getAllUids() {
|
||||
// We ignore mails that are flagged as DELETED, but have not been removed (expunged) yet.
|
||||
const uids = await this._searchWithoutFetch([['!DELETED']])
|
||||
// Create copy to not mutate the original array. Sort with newest first (DESC).
|
||||
return [...uids].sort().reverse()
|
||||
}
|
||||
async _getAllUids() {
|
||||
// We ignore mails that are flagged as DELETED, but have not been removed (expunged) yet.
|
||||
const uids = await this._searchWithoutFetch([
|
||||
['!DELETED']
|
||||
])
|
||||
// Create copy to not mutate the original array. Sort with newest first (DESC).
|
||||
return [...uids].sort().reverse()
|
||||
}
|
||||
|
||||
async _getMailHeadersAndEmitAsEvents(uids) {
|
||||
try {
|
||||
const mails = await this._getMailHeaders(uids)
|
||||
mails.forEach(mail => {
|
||||
this.loadedUids.add(mail.attributes.uid)
|
||||
// Some broadcast messages have no TO field. We have to ignore those messages.
|
||||
if (mail.parts[0].body.to) {
|
||||
this.emit(ImapService.EVENT_NEW_MAIL, this._createMailSummary(mail))
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
debug('can not fetch', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
async _getMailHeadersAndEmitAsEvents(uids) {
|
||||
try {
|
||||
const mails = await this._getMailHeaders(uids)
|
||||
mails.forEach(mail => {
|
||||
this.loadedUids.add(mail.attributes.uid)
|
||||
// Some broadcast messages have no TO field. We have to ignore those messages.
|
||||
if (mail.parts[0].body.to) {
|
||||
this.emit(ImapService.EVENT_NEW_MAIL, this._createMailSummary(mail))
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
debug('can not fetch', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async _getMailHeaders(uids) {
|
||||
const fetchOptions = {
|
||||
envelope: true,
|
||||
bodies: ['HEADER.FIELDS (FROM TO SUBJECT DATE)'],
|
||||
struct: false
|
||||
}
|
||||
const searchCriteria = [['UID', ...uids]]
|
||||
return this.connection.search(searchCriteria, fetchOptions)
|
||||
}
|
||||
async _getMailHeaders(uids) {
|
||||
const fetchOptions = {
|
||||
envelope: true,
|
||||
bodies: ['HEADER.FIELDS (FROM TO SUBJECT DATE)'],
|
||||
struct: false
|
||||
}
|
||||
const searchCriteria = [
|
||||
['UID', ...uids]
|
||||
]
|
||||
return this.connection.search(searchCriteria, fetchOptions)
|
||||
}
|
||||
}
|
||||
|
||||
// Consumers should use these constants:
|
||||
|
|
@ -360,4 +374,4 @@ ImapService.EVENT_DELETED_MAIL = 'mailDeleted'
|
|||
ImapService.EVENT_INITIAL_LOAD_DONE = 'initial load done'
|
||||
ImapService.EVENT_ERROR = 'error'
|
||||
|
||||
module.exports = ImapService
|
||||
module.exports = ImapService
|
||||
|
|
@ -8,86 +8,87 @@ const helper = new(Helper)
|
|||
|
||||
|
||||
class MailProcessingService extends EventEmitter {
|
||||
constructor(mailRepository, imapService, clientNotification, config) {
|
||||
super()
|
||||
this.mailRepository = mailRepository
|
||||
this.clientNotification = clientNotification
|
||||
this.imapService = imapService
|
||||
this.config = config
|
||||
constructor(mailRepository, imapService, clientNotification, config) {
|
||||
super()
|
||||
this.mailRepository = mailRepository
|
||||
this.clientNotification = clientNotification
|
||||
this.imapService = imapService
|
||||
this.config = config
|
||||
|
||||
// Cached methods:
|
||||
this.cachedFetchFullMail = mem(
|
||||
this.imapService.fetchOneFullMail.bind(this.imapService),
|
||||
{maxAge: 10 * 60 * 1000}
|
||||
)
|
||||
// Cached methods:
|
||||
this.cachedFetchFullMail = mem(
|
||||
this.imapService.fetchOneFullMail.bind(this.imapService), { maxAge: 10 * 60 * 1000 }
|
||||
)
|
||||
|
||||
this.initialLoadDone = false
|
||||
this.initialLoadDone = false
|
||||
|
||||
// Delete old messages now and every few hours
|
||||
this.imapService.once(ImapService.EVENT_INITIAL_LOAD_DONE, () =>
|
||||
this._deleteOldMails()
|
||||
)
|
||||
setInterval(() => this._deleteOldMails(), 10 * 60 * 1000)
|
||||
}
|
||||
// Delete old messages now and every few hours
|
||||
this.imapService.once(ImapService.EVENT_INITIAL_LOAD_DONE, () =>
|
||||
this._deleteOldMails()
|
||||
)
|
||||
setInterval(() => {
|
||||
this._deleteOldMails()
|
||||
}, 60 * 1000)
|
||||
}
|
||||
|
||||
getMailSummaries(address) {
|
||||
return this.mailRepository.getForRecipient(address)
|
||||
}
|
||||
getMailSummaries(address) {
|
||||
return this.mailRepository.getForRecipient(address)
|
||||
}
|
||||
|
||||
deleteSpecificEmail(adress, uid) {
|
||||
if (this.mailRepository.removeUid(uid, adress) == true) {
|
||||
this.imapService.deleteSpecificEmail(uid)
|
||||
}
|
||||
}
|
||||
deleteSpecificEmail(adress, uid) {
|
||||
if (this.mailRepository.removeUid(uid, adress) == true) {
|
||||
this.imapService.deleteSpecificEmail(uid)
|
||||
}
|
||||
}
|
||||
|
||||
getOneFullMail(address, uid, raw = false) {
|
||||
return this.cachedFetchFullMail(address, uid, raw)
|
||||
}
|
||||
getOneFullMail(address, uid, raw = false) {
|
||||
return this.cachedFetchFullMail(address, uid, raw)
|
||||
}
|
||||
|
||||
getAllMailSummaries() {
|
||||
return this.mailRepository.getAll()
|
||||
}
|
||||
getAllMailSummaries() {
|
||||
return this.mailRepository.getAll()
|
||||
}
|
||||
|
||||
onInitialLoadDone() {
|
||||
this.initialLoadDone = true
|
||||
console.log(
|
||||
`initial load done, got ${this.mailRepository.mailCount()} mails`
|
||||
)
|
||||
}
|
||||
onInitialLoadDone() {
|
||||
this.initialLoadDone = true
|
||||
console.log(
|
||||
`initial load done, got ${this.mailRepository.mailCount()} mails`
|
||||
)
|
||||
}
|
||||
|
||||
onNewMail(mail) {
|
||||
if (this.initialLoadDone) {
|
||||
// For now, only log messages if they arrive after the initial load
|
||||
debug('new mail for', mail.to[0])
|
||||
}
|
||||
onNewMail(mail) {
|
||||
if (this.initialLoadDone) {
|
||||
// For now, only log messages if they arrive after the initial load
|
||||
debug('new mail for', mail.to[0])
|
||||
}
|
||||
|
||||
mail.to.forEach(to => {
|
||||
this.mailRepository.add(to, mail)
|
||||
return this.clientNotification.emit(to)
|
||||
})
|
||||
}
|
||||
mail.to.forEach(to => {
|
||||
this.mailRepository.add(to, mail)
|
||||
return this.clientNotification.emit(to)
|
||||
})
|
||||
}
|
||||
|
||||
onMailDeleted(uid) {
|
||||
debug('mail deleted with uid', uid)
|
||||
this.mailRepository.removeUid(uid)
|
||||
}
|
||||
onMailDeleted(uid) {
|
||||
debug('mail deleted with uid', uid)
|
||||
this.mailRepository.removeUid(uid)
|
||||
}
|
||||
|
||||
async _deleteOldMails() {
|
||||
try {
|
||||
await this.imapService.deleteOldMails(helper.purgeTimeStamp())
|
||||
} catch (error) {
|
||||
console.log('can not delete old messages', error)
|
||||
}
|
||||
}
|
||||
async _deleteOldMails() {
|
||||
try {
|
||||
await this.imapService.deleteOldMails(helper.purgeTimeStamp())
|
||||
} catch (error) {
|
||||
console.log('can not delete old messages', error)
|
||||
}
|
||||
}
|
||||
|
||||
_saveToFile(mails, filename) {
|
||||
const fs = require('fs')
|
||||
fs.writeFile(filename, JSON.stringify(mails), err => {
|
||||
if (err) {
|
||||
console.error('can not save mails to file', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
_saveToFile(mails, filename) {
|
||||
const fs = require('fs')
|
||||
fs.writeFile(filename, JSON.stringify(mails), err => {
|
||||
if (err) {
|
||||
console.error('can not save mails to file', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MailProcessingService
|
||||
module.exports = MailProcessingService
|
||||
|
|
@ -4,57 +4,57 @@ const _ = require('lodash')
|
|||
const config = require('../application/config')
|
||||
|
||||
class MailRepository {
|
||||
constructor() {
|
||||
// MultiMap docs: https://yomguithereal.github.io/mnemonist/multi-map
|
||||
this.mailSummaries = new MultiMap()
|
||||
this.config = config
|
||||
}
|
||||
constructor() {
|
||||
// MultiMap docs: https://yomguithereal.github.io/mnemonist/multi-map
|
||||
this.mailSummaries = new MultiMap()
|
||||
this.config = config
|
||||
}
|
||||
|
||||
getForRecipient(address) {
|
||||
let mails = this.mailSummaries.get(address) || []
|
||||
mails.forEach(mail => {
|
||||
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)
|
||||
debug('prevented non-example email from being shown in example inbox', mail.uid)
|
||||
}
|
||||
})
|
||||
return _.orderBy(mails, mail => Date.parse(mail.date), ['desc'])
|
||||
}
|
||||
getForRecipient(address) {
|
||||
let mails = this.mailSummaries.get(address) || []
|
||||
mails.forEach(mail => {
|
||||
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)
|
||||
console.log('prevented non-example email from being shown in example inbox', mail.uid)
|
||||
}
|
||||
})
|
||||
return _.orderBy(mails, mail => Date.parse(mail.date), ['desc'])
|
||||
}
|
||||
|
||||
getAll() {
|
||||
const mails = [...this.mailSummaries.values()]
|
||||
return _.orderBy(mails, mail => Date.parse(mail.date), ['desc'])
|
||||
}
|
||||
getAll() {
|
||||
const mails = [...this.mailSummaries.values()]
|
||||
return _.orderBy(mails, mail => Date.parse(mail.date), ['desc'])
|
||||
}
|
||||
|
||||
add(to, mailSummary) {
|
||||
if (to !== undefined) {
|
||||
this.mailSummaries.set(to.toLowerCase(), mailSummary)
|
||||
} else {
|
||||
debug('IMAP reported no recipient for mail, ignoring', mailSummary)
|
||||
}
|
||||
}
|
||||
add(to, mailSummary) {
|
||||
if (to !== undefined) {
|
||||
this.mailSummaries.set(to.toLowerCase(), mailSummary)
|
||||
} else {
|
||||
debug('IMAP reported no recipient for mail, ignoring', mailSummary)
|
||||
}
|
||||
}
|
||||
|
||||
removeUid(uid, address) {
|
||||
if (!this.config.email.examples.uids.includes(parseInt(uid))) {
|
||||
var deleted = false
|
||||
// TODO: make this more efficient, looping through each email is not cool.
|
||||
this.mailSummaries.forEachAssociation((mails, to) => {
|
||||
mails
|
||||
.filter(mail => mail.uid === parseInt(uid) && (address ? to == address : true))
|
||||
.forEach(mail => {
|
||||
this.mailSummaries.remove(to, mail)
|
||||
debug('removed ', mail.date, to, mail.subject)
|
||||
deleted = true
|
||||
})
|
||||
})
|
||||
return deleted
|
||||
}
|
||||
return false
|
||||
}
|
||||
removeUid(uid, address) {
|
||||
if (!this.config.email.examples.uids.includes(parseInt(uid))) {
|
||||
var deleted = false
|
||||
// TODO: make this more efficient, looping through each email is not cool.
|
||||
this.mailSummaries.forEachAssociation((mails, to) => {
|
||||
mails
|
||||
.filter(mail => mail.uid === parseInt(uid) && (address ? to == address : true))
|
||||
.forEach(mail => {
|
||||
this.mailSummaries.remove(to, mail)
|
||||
debug('removed ', mail.date, to, mail.subject)
|
||||
deleted = true
|
||||
})
|
||||
})
|
||||
return deleted
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
mailCount() {
|
||||
return this.mailSummaries.size
|
||||
}
|
||||
mailCount() {
|
||||
return this.mailSummaries.size
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MailRepository
|
||||
module.exports = MailRepository
|
||||
Loading…
Add table
Reference in a new issue