Compare commits

..

No commits in common. "f42fbd4e74f58eef1b81c3c3b6f6616d89e8bb51" and "e158fac4147449d8ab4b1b659d6cf0c4c7e3f0e7" have entirely different histories.

7 changed files with 400 additions and 417 deletions

View file

@ -15,19 +15,18 @@ class Helper {
/** /**
* 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 {number|Date} now * @param {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){
const nowMs = now instanceof Date ? now.getTime() : now; return true
const pastMs = past instanceof Date ? past.getTime() : new Date(past).getTime(); } else {
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)
@ -44,8 +43,7 @@ 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) {
@ -108,8 +106,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
} }

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);
@ -136,7 +136,8 @@ class ImapService extends EventEmitter {
await this.connection.openBox('INBOX') await this.connection.openBox('INBOX')
debug('connected to imap') debug('connected to imap')
}, { },
{
retries: 5 retries: 5
} }
) )
@ -201,14 +202,12 @@ class ImapService extends EventEmitter {
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(), deleteMailsBefore)) { if (helper.moreThanOneDay(moment() + 24 * 60 * 60 * 1000, deleteMailsBefore)) {
console.log("Deleting mails older than one day");
uids = await this._searchWithoutFetch([ uids = await this._searchWithoutFetch([
['!DELETED'], ['!DELETED'],
['BEFORE', deleteMailsBefore] ['BEFORE', deleteMailsBefore]
]) ])
} else { } else {
console.log("Deleting mails without date filter");
uids = await this._searchWithoutFetch([ uids = await this._searchWithoutFetch([
['!DELETED'], ['!DELETED'],
]) ])
@ -220,27 +219,21 @@ class ImapService extends EventEmitter {
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) {
console.log("Length 0")
debug('no mails to delete.') debug('no mails to delete.')
return return
} }
debug(`deleting mails ${uids}`) debug(`deleting mails ${uids}`)
await this.connection.deleteMessage(uids) await this.connection.deleteMessage(uids)
uids.forEach(uid => { uids.forEach(uid => this.emit(ImapService.EVENT_DELETED_MAIL, uid))
this.emit(ImapService.EVENT_DELETED_MAIL, uid)
console.log(`UID deleted: ${uid}`);
})
console.log(`deleted ${uids.length} old messages.`) console.log(`deleted ${uids.length} old messages.`)
} }
@ -295,7 +288,7 @@ class ImapService extends EventEmitter {
// 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)
} }
@ -309,10 +302,7 @@ class ImapService extends EventEmitter {
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 = [ const searchCriteria = [['UID', uid], ['TO', to]]
['UID', uid],
['TO', to]
]
const fetchOptions = { const fetchOptions = {
bodies: ['HEADER', ''], // Empty string means full body bodies: ['HEADER', ''], // Empty string means full body
markSeen: false markSeen: false
@ -322,7 +312,7 @@ class ImapService extends EventEmitter {
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
@ -332,9 +322,7 @@ class ImapService extends EventEmitter {
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([ const uids = await this._searchWithoutFetch([['!DELETED']])
['!DELETED']
])
// Create copy to not mutate the original array. Sort with newest first (DESC). // Create copy to not mutate the original array. Sort with newest first (DESC).
return [...uids].sort().reverse() return [...uids].sort().reverse()
} }
@ -361,9 +349,7 @@ class ImapService extends EventEmitter {
bodies: ['HEADER.FIELDS (FROM TO SUBJECT DATE)'], bodies: ['HEADER.FIELDS (FROM TO SUBJECT DATE)'],
struct: false struct: false
} }
const searchCriteria = [ const searchCriteria = [['UID', ...uids]]
['UID', ...uids]
]
return this.connection.search(searchCriteria, fetchOptions) return this.connection.search(searchCriteria, fetchOptions)
} }
} }

View file

@ -17,7 +17,8 @@ class MailProcessingService extends EventEmitter {
// Cached methods: // Cached methods:
this.cachedFetchFullMail = mem( this.cachedFetchFullMail = mem(
this.imapService.fetchOneFullMail.bind(this.imapService), { maxAge: 10 * 60 * 1000 } this.imapService.fetchOneFullMail.bind(this.imapService),
{maxAge: 10 * 60 * 1000}
) )
this.initialLoadDone = false this.initialLoadDone = false
@ -26,9 +27,7 @@ class MailProcessingService extends EventEmitter {
this.imapService.once(ImapService.EVENT_INITIAL_LOAD_DONE, () => this.imapService.once(ImapService.EVENT_INITIAL_LOAD_DONE, () =>
this._deleteOldMails() this._deleteOldMails()
) )
setInterval(() => { setInterval(() => this._deleteOldMails(), 10 * 60 * 1000)
this._deleteOldMails()
}, 60 * 1000)
} }
getMailSummaries(address) { getMailSummaries(address) {

View file

@ -15,7 +15,7 @@ class MailRepository {
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)
console.log('prevented non-example email from being shown in example inbox', mail.uid) debug('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'])

View file

@ -1,6 +1,6 @@
const express = require('express') const express = require('express')
const router = new express.Router() const router = new express.Router()
const { check, validationResult } = require('express-validator') const {check, validationResult} = require('express-validator')
const randomWord = require('random-word') const randomWord = require('random-word')
const config = require('../../../application/config') const config = require('../../../application/config')
@ -16,7 +16,6 @@ router.get('/', (req, res, _next) => {
purgeTime: purgeTime, purgeTime: purgeTime,
domains: helper.getDomains(), domains: helper.getDomains(),
branding: config.http.branding, branding: config.http.branding,
example: config.email.examples.account,
}) })
}) })
@ -34,8 +33,9 @@ router.get('/logout', (req, res, _next) => {
}) })
router.post( router.post(
'/', [ '/',
check('username').isLength({ min: 1 }), [
check('username').isLength({min: 1}),
check('domain').isIn(config.email.domains) check('domain').isIn(config.email.domains)
], ],
(req, res) => { (req, res) => {

View file

@ -3,7 +3,7 @@
{% block body %} {% block body %}
<div style="float: right; text-align: end;"> <div style="float: right; text-align: end;">
<a href="/inbox/{{ example }}">Example Inbox</a> <a href="/inbox/example@48hr.email">Example Inbox</a>
</div> </div>
<div id="login"> <div id="login">
<h1>Welcome!</h1> <h1>Welcome!</h1>

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "48hr.email", "name": "48hr.email",
"version": "1.6.1", "version": "1.5.4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "48hr.email", "name": "48hr.email",
"version": "1.6.1", "version": "1.5.4",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"array.prototype.flatmap": "^1.3.2", "array.prototype.flatmap": "^1.3.2",