diff --git a/app.js b/app.js index 31bb52e..1bcee6f 100644 --- a/app.js +++ b/app.js @@ -1,4 +1,5 @@ #!/usr/bin/env node + /* eslint unicorn/no-process-exit: 0 */ const config = require('./application/config') @@ -6,7 +7,7 @@ const config = require('./application/config') // Until node 11 adds flatmap, we use this: require('array.prototype.flatmap').shim() -const {app, io, server} = require('./infrastructure/web/web') +const { app, io, server } = require('./infrastructure/web/web') const ClientNotification = require('./infrastructure/web/client-notification') const ImapService = require('./application/imap-service') const MailProcessingService = require('./application/mail-processing-service') @@ -17,58 +18,58 @@ clientNotification.use(io) const imapService = new ImapService(config) const mailProcessingService = new MailProcessingService( - new MailRepository(), - imapService, - clientNotification, - config + new MailRepository(), + imapService, + clientNotification, + config ) // Put everything together: imapService.on(ImapService.EVENT_NEW_MAIL, mail => - mailProcessingService.onNewMail(mail) + mailProcessingService.onNewMail(mail) ) imapService.on(ImapService.EVENT_INITIAL_LOAD_DONE, () => - mailProcessingService.onInitialLoadDone() + mailProcessingService.onInitialLoadDone() ) imapService.on(ImapService.EVENT_DELETED_MAIL, mail => - mailProcessingService.onMailDeleted(mail) + mailProcessingService.onMailDeleted(mail) ) mailProcessingService.on('error', err => { - console.error('error from mailProcessingService, stopping.', err) - process.exit(1) + console.error('Error from mailProcessingService, stopping.', err) + process.exit(1) }) imapService.on(ImapService.EVENT_ERROR, error => { - console.error('fatal error from imap service', error) - process.exit(1) + console.error('Fatal error from IMAP service', error) + process.exit(1) }) app.set('mailProcessingService', mailProcessingService) imapService.connectAndLoadMessages().catch(error => { - console.error('fatal error from imap service', error) - process.exit(1) + console.error('Fatal error from IMAP service', error) + process.exit(1) }) server.on('error', error => { - if (error.syscall !== 'listen') { - console.error('fatal web server error', error) - return - } + if (error.syscall !== 'listen') { + console.error('Fatal web server error', error) + return + } - // Handle specific listen errors with friendly messages - switch (error.code) { - case 'EACCES': - console.error( - 'Port ' + config.http.port + ' requires elevated privileges' - ) - process.exit(1) - case 'EADDRINUSE': - console.error('Port ' + config.http.port + ' is already in use') - process.exit(1) - default: - console.error('fatal web server error', error) - process.exit(1) - } -}) + // Handle specific listen errors with friendly messages + switch (error.code) { + case 'EACCES': + console.error( + 'Port ' + config.http.port + ' requires elevated privileges' + ) + process.exit(1) + case 'EADDRINUSE': + console.error('Port ' + config.http.port + ' is already in use') + process.exit(1) + default: + console.error('Fatal web server error', error) + process.exit(1) + } +}) \ No newline at end of file diff --git a/application/imap-service.js b/application/imap-service.js index b20a0c6..cc820d1 100644 --- a/application/imap-service.js +++ b/application/imap-service.js @@ -127,21 +127,18 @@ class ImapService extends EventEmitter { 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 - ) + console.error('got fatal error during imap operation, stop app.', err) this.emit('error', err) }) await this.connection.openBox('INBOX') - debug('connected to imap') + debug('Connected to imap') }, { retries: 5 } ) } catch (error) { - console.error('can not connect, even with retry, stop app', error) + console.error('Cant connect, even after retrying, stopping app', error) throw error } } @@ -226,7 +223,7 @@ class ImapService extends EventEmitter { }) if (uids.length === 0) { - debug('no mails to delete.') + debug('No mails to delete.') return } @@ -235,7 +232,7 @@ class ImapService extends EventEmitter { uids.forEach(uid => { this.emit(ImapService.EVENT_DELETED_MAIL, uid) }) - console.log(`deleted ${uids.length} old messages.`) + console.log(`Deleted ${uids.length} old messages.`) } /** @@ -243,10 +240,10 @@ class ImapService extends EventEmitter { * @param uid delete specific mail per UID */ async deleteSpecificEmail(uid) { - debug(`deleting mails ${uid}`) + debug(`Deleting mails ${uid}`) if (!this.config.email.examples.uids.includes(parseInt(uid))) { await this.connection.deleteMessage(uid) - debug(`deleted mail with UID: ${uid}.`) + debug(`Deleted mail with UID: ${uid}.`) this.emit(ImapService.EVENT_DELETED_MAIL, uid) } } @@ -297,10 +294,10 @@ class ImapService extends EventEmitter { 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') + 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 = [ @@ -344,7 +341,7 @@ class ImapService extends EventEmitter { } }) } catch (error) { - debug('can not fetch', error) + debug('Cant fetch', error) throw error } } diff --git a/application/mail-processing-service.js b/application/mail-processing-service.js index 305425b..c8d6caf 100644 --- a/application/mail-processing-service.js +++ b/application/mail-processing-service.js @@ -54,13 +54,13 @@ class MailProcessingService extends EventEmitter { onInitialLoadDone() { this.initialLoadDone = true - console.log(`initial load done, got ${this.mailRepository.mailCount()} mails`) + 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]) + debug('New mail for', mail.to[0]) } mail.to.forEach(to => { @@ -70,7 +70,7 @@ class MailProcessingService extends EventEmitter { } onMailDeleted(uid) { - debug('mail deleted with uid', uid) + debug('Mail deleted with uid', uid) this.mailRepository.removeUid(uid) } @@ -78,7 +78,7 @@ class MailProcessingService extends EventEmitter { try { await this.imapService.deleteOldMails(helper.purgeTimeStamp()) } catch (error) { - console.log('can not delete old messages', error) + console.log('Cant delete old messages', error) } } @@ -86,7 +86,7 @@ class MailProcessingService extends EventEmitter { const fs = require('fs') fs.writeFile(filename, JSON.stringify(mails), err => { if (err) { - console.error('can not save mails to file', err) + console.error('Cant save mails to file', err) } }) } diff --git a/domain/mail-repository.js b/domain/mail-repository.js index 0297d8d..a271b51 100644 --- a/domain/mail-repository.js +++ b/domain/mail-repository.js @@ -15,7 +15,7 @@ class MailRepository { 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) + debug('Prevented non-example email from being shown in example inbox', mail.uid) } }) return _.orderBy(mails, mail => Date.parse(mail.date), ['desc']) @@ -43,7 +43,7 @@ class MailRepository { .filter(mail => mail.uid === parseInt(uid) && (address ? to == address : true)) .forEach(mail => { this.mailSummaries.remove(to, mail) - debug('removed ', mail.date, to, mail.subject) + debug('Removed ', mail.date, to, mail.subject) deleted = true }) }) diff --git a/infrastructure/web/routes/inbox.js b/infrastructure/web/routes/inbox.js index 400383e..db399fd 100644 --- a/infrastructure/web/routes/inbox.js +++ b/infrastructure/web/routes/inbox.js @@ -1,6 +1,6 @@ const express = require('express') const router = new express.Router() -const {param} = require('express-validator') +const { param } = require('express-validator') const config = require('../../../application/config') const Helper = require('../../../application/helper') @@ -9,182 +9,179 @@ const helper = new(Helper) const purgeTime = helper.purgeTimeElemetBuilder() const sanitizeAddress = param('address').customSanitizer( - (value, {req}) => { - return req.params.address - .replace(/[^A-Za-z0-9_.+@-]/g, '') // Remove special characters - .toLowerCase() - } + (value, { req }) => { + return req.params.address + .replace(/[^A-Za-z0-9_.+@-]/g, '') // Remove special characters + .toLowerCase() + } ) router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, (req, res, _next) => { - const mailProcessingService = req.app.get('mailProcessingService') - res.render('inbox', { - title: `${config.http.branding[0]} | ` + req.params.address, - purgeTime: purgeTime, - address: req.params.address, - mailSummaries: mailProcessingService.getMailSummaries(req.params.address), - branding: config.http.branding, - }) + const mailProcessingService = req.app.get('mailProcessingService') + res.render('inbox', { + title: `${config.http.branding[0]} | ` + req.params.address, + purgeTime: purgeTime, + address: req.params.address, + mailSummaries: mailProcessingService.getMailSummaries(req.params.address), + branding: config.http.branding, + }) }) router.get( - '^/:address/:uid([0-9]+)', - sanitizeAddress, - async (req, res, next) => { - try { - const mailProcessingService = req.app.get('mailProcessingService') - const mail = await mailProcessingService.getOneFullMail( - req.params.address, - req.params.uid - ) - if (mail) { - // Set a default subject if none is present - if (!mail.subject) { - mail.subject = 'No Subject' - } + '^/:address/:uid([0-9]+)', + sanitizeAddress, + async(req, res, next) => { + try { + const mailProcessingService = req.app.get('mailProcessingService') + const mail = await mailProcessingService.getOneFullMail( + req.params.address, + req.params.uid + ) + if (mail) { + // Set a default subject if none is present + if (!mail.subject) { + mail.subject = 'No Subject' + } - // Emails are immutable, cache if found - res.set('Cache-Control', 'private, max-age=600') - res.render('mail', { - title: mail.subject + " | " + req.params.address, - purgeTime: purgeTime, - address: req.params.address, - mail, - uid: req.params.uid, - branding: config.http.branding, - }) - } else { - res.render( - 'error', - { - purgeTime: purgeTime, - address: req.params.address, - message: 'This mail could not be found. It either does not exist or has been deleted from our servers!', - branding: config.http.branding + // Emails are immutable, cache if found + res.set('Cache-Control', 'private, max-age=600') + res.render('mail', { + title: mail.subject + " | " + req.params.address, + purgeTime: purgeTime, + address: req.params.address, + mail, + uid: req.params.uid, + branding: config.http.branding, + }) + } else { + res.render( + 'error', { + purgeTime: purgeTime, + address: req.params.address, + message: 'This mail could not be found. It either does not exist or has been deleted from our servers!', + branding: config.http.branding - } - ) - } - } catch (error) { - console.error('error while fetching one email', error) - next(error) - } - } + } + ) + } + } catch (error) { + console.error('Error while fetching email', error) + next(error) + } + } ) router.get( - '^/:address/delete-all', - sanitizeAddress, - async (req, res, next) => { - try { - const mailProcessingService = req.app.get('mailProcessingService') - const mailSummaries = await mailProcessingService.getMailSummaries(req.params.address) - for (mail in mailSummaries) { - await mailProcessingService.deleteSpecificEmail(req.params.address, mailSummaries[mail].uid) - } - res.redirect(`/inbox/${req.params.address}`) - } catch (error) { - console.error('error while deleting email', error) - next(error) - } - } + '^/:address/delete-all', + sanitizeAddress, + async(req, res, next) => { + try { + const mailProcessingService = req.app.get('mailProcessingService') + const mailSummaries = await mailProcessingService.getMailSummaries(req.params.address) + for (mail in mailSummaries) { + await mailProcessingService.deleteSpecificEmail(req.params.address, mailSummaries[mail].uid) + } + res.redirect(`/inbox/${req.params.address}`) + } catch (error) { + console.error('Error while deleting email', error) + next(error) + } + } ) router.get( - '^/:address/:uid/delete', - sanitizeAddress, - async (req, res, next) => { - try { - const mailProcessingService = req.app.get('mailProcessingService') - await mailProcessingService.deleteSpecificEmail(req.params.address, req.params.uid) - res.redirect(`/inbox/${req.params.address}`) - } catch (error) { - console.error('error while deleting email', error) - next(error) - } - } + '^/:address/:uid/delete', + sanitizeAddress, + async(req, res, next) => { + try { + const mailProcessingService = req.app.get('mailProcessingService') + await mailProcessingService.deleteSpecificEmail(req.params.address, req.params.uid) + res.redirect(`/inbox/${req.params.address}`) + } catch (error) { + console.error('Error while deleting email', error) + next(error) + } + } ) router.get( - '^/:address/:uid/:checksum([a-f0-9]+)', - sanitizeAddress, - async (req, res, next) => { - try { - const mailProcessingService = req.app.get('mailProcessingService') - const mail = await mailProcessingService.getOneFullMail( - req.params.address, - req.params.uid - ) - var index = mail.attachments.findIndex(attachment => attachment.checksum === req.params.checksum); - const attachment = mail.attachments[index]; - if (attachment) { - try { - res.set('Content-Disposition', `attachment; filename=${attachment.filename}`); - res.set('Content-Type', attachment.contentType); - res.send(attachment.content); - return; - } catch (error) { - console.error('error while fetching attachment', error); - next(error); - } - } else { - res.render( - 'error', - { - purgeTime: purgeTime, - address: req.params.address, - message: 'This attachment could not be found. It either does not exist or has been deleted from our servers!', - branding: config.http.branding, - } - ) - } - res.redirect(`/inbox/${req.params.address}`) - } catch (error) { - console.error('error while deleting email', error) - next(error) - } - } + '^/:address/:uid/:checksum([a-f0-9]+)', + sanitizeAddress, + async(req, res, next) => { + try { + const mailProcessingService = req.app.get('mailProcessingService') + const mail = await mailProcessingService.getOneFullMail( + req.params.address, + req.params.uid + ) + var index = mail.attachments.findIndex(attachment => attachment.checksum === req.params.checksum); + const attachment = mail.attachments[index]; + if (attachment) { + try { + res.set('Content-Disposition', `attachment; filename=${attachment.filename}`); + res.set('Content-Type', attachment.contentType); + res.send(attachment.content); + return; + } catch (error) { + console.error('Error while fetching attachment', error); + next(error); + } + } else { + res.render( + 'error', { + purgeTime: purgeTime, + address: req.params.address, + message: 'This attachment could not be found. It either does not exist or has been deleted from our servers!', + branding: config.http.branding, + } + ) + } + res.redirect(`/inbox/${req.params.address}`) + } catch (error) { + console.error('Error while deleting email', error) + next(error) + } + } ) router.get( - '^/:address/:uid/raw', - sanitizeAddress, - async (req, res, next) => { - try { - const mailProcessingService = req.app.get('mailProcessingService') - mail = await mailProcessingService.getOneFullMail( - req.params.address, - req.params.uid, - true - ) - if (mail) { - mail = mail.replace(/(?:\r\n|\r|\n)/g, '
') - // Emails are immutable, cache if found - res.set('Cache-Control', 'private, max-age=600') - res.render('raw', { - title: req.params.uid + " | raw | " + req.params.address, - mail - }) - } else { - res.render( - 'error', - { - purgeTime: purgeTime, - address: req.params.address, - message: 'This mail could not be found. It either does not exist or has been deleted from our servers!', - branding: config.http.branding, - } - ) - } - } catch (error) { - console.error('error while fetching one email', error) - next(error) - } - } + '^/:address/:uid/raw', + sanitizeAddress, + async(req, res, next) => { + try { + const mailProcessingService = req.app.get('mailProcessingService') + mail = await mailProcessingService.getOneFullMail( + req.params.address, + req.params.uid, + true + ) + if (mail) { + mail = mail.replace(/(?:\r\n|\r|\n)/g, '
') + // Emails are immutable, cache if found + res.set('Cache-Control', 'private, max-age=600') + res.render('raw', { + title: req.params.uid + " | raw | " + req.params.address, + mail + }) + } else { + res.render( + 'error', { + purgeTime: purgeTime, + address: req.params.address, + message: 'This mail could not be found. It either does not exist or has been deleted from our servers!', + branding: config.http.branding, + } + ) + } + } catch (error) { + console.error('Error while fetching raw email', error) + next(error) + } + } ) -module.exports = router +module.exports = router \ No newline at end of file diff --git a/infrastructure/web/web.js b/infrastructure/web/web.js index 09531d4..e70561b 100644 --- a/infrastructure/web/web.js +++ b/infrastructure/web/web.js @@ -11,7 +11,7 @@ const socketio = require('socket.io') const config = require('../../application/config') const inboxRouter = require('./routes/inbox') const loginRouter = require('./routes/login') -const {sanitizeHtmlTwigFilter} = require('./views/twig-filters') +const { sanitizeHtmlTwigFilter } = require('./views/twig-filters') // Init express middleware const app = express() @@ -24,20 +24,20 @@ const io = socketio(server) app.set('socketio', io) app.use(logger('dev')) app.use(express.json()) -app.use(express.urlencoded({extended: false})) -// View engine setup +app.use(express.urlencoded({ extended: false })) + // View engine setup app.set('views', path.join(__dirname, 'views')) app.set('view engine', 'twig') app.set('twig options', { - autoescape: true + autoescape: true }) // Application code: app.use( - express.static(path.join(__dirname, 'public'), { - immutable: true, - maxAge: '1h' - }) + express.static(path.join(__dirname, 'public'), { + immutable: true, + maxAge: '1h' + }) ) Twig.extendFilter('sanitizeHtml', sanitizeHtmlTwigFilter) @@ -52,18 +52,18 @@ app.use('/inbox', inboxRouter) // Catch 404 and forward to error handler app.use((req, res, next) => { - next({message: 'page not found', status: 404}) + next({ message: 'Page not found', status: 404 }) }) // Error handler app.use((err, req, res, _next) => { - // Set locals, only providing error in development - res.locals.message = err.message - res.locals.error = req.app.get('env') === 'development' ? err : {} + // Set locals, only providing error in development + res.locals.message = err.message + res.locals.error = req.app.get('env') === 'development' ? err : {} - // Render the error page - res.status(err.status || 500) - res.render('error') + // Render the error page + res.status(err.status || 500) + res.render('error') }) /** @@ -77,9 +77,9 @@ app.set('port', config.http.port) */ server.listen(config.http.port) server.on('listening', () => { - const addr = server.address() - const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port - debug('Listening on ' + bind) + const addr = server.address() + const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port + debug('Listening on ' + bind) }) -module.exports = {app, io, server} +module.exports = { app, io, server } \ No newline at end of file