log updates

This commit is contained in:
ClaraCrazy 2025-12-12 19:38:47 +01:00
parent 075940b0d8
commit 0ad3e40fcc
No known key found for this signature in database
GPG key ID: EBBC896ACB497011
6 changed files with 221 additions and 226 deletions

67
app.js
View file

@ -1,4 +1,5 @@
#!/usr/bin/env node #!/usr/bin/env node
/* eslint unicorn/no-process-exit: 0 */ /* eslint unicorn/no-process-exit: 0 */
const config = require('./application/config') const config = require('./application/config')
@ -6,7 +7,7 @@ const config = require('./application/config')
// Until node 11 adds flatmap, we use this: // Until node 11 adds flatmap, we use this:
require('array.prototype.flatmap').shim() 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 ClientNotification = require('./infrastructure/web/client-notification')
const ImapService = require('./application/imap-service') const ImapService = require('./application/imap-service')
const MailProcessingService = require('./application/mail-processing-service') const MailProcessingService = require('./application/mail-processing-service')
@ -17,58 +18,58 @@ clientNotification.use(io)
const imapService = new ImapService(config) const imapService = new ImapService(config)
const mailProcessingService = new MailProcessingService( const mailProcessingService = new MailProcessingService(
new MailRepository(), new MailRepository(),
imapService, imapService,
clientNotification, clientNotification,
config config
) )
// Put everything together: // Put everything together:
imapService.on(ImapService.EVENT_NEW_MAIL, mail => imapService.on(ImapService.EVENT_NEW_MAIL, mail =>
mailProcessingService.onNewMail(mail) mailProcessingService.onNewMail(mail)
) )
imapService.on(ImapService.EVENT_INITIAL_LOAD_DONE, () => imapService.on(ImapService.EVENT_INITIAL_LOAD_DONE, () =>
mailProcessingService.onInitialLoadDone() mailProcessingService.onInitialLoadDone()
) )
imapService.on(ImapService.EVENT_DELETED_MAIL, mail => imapService.on(ImapService.EVENT_DELETED_MAIL, mail =>
mailProcessingService.onMailDeleted(mail) mailProcessingService.onMailDeleted(mail)
) )
mailProcessingService.on('error', err => { mailProcessingService.on('error', err => {
console.error('error from mailProcessingService, stopping.', err) console.error('Error from mailProcessingService, stopping.', err)
process.exit(1) process.exit(1)
}) })
imapService.on(ImapService.EVENT_ERROR, error => { imapService.on(ImapService.EVENT_ERROR, error => {
console.error('fatal error from imap service', error) console.error('Fatal error from IMAP service', error)
process.exit(1) process.exit(1)
}) })
app.set('mailProcessingService', mailProcessingService) app.set('mailProcessingService', mailProcessingService)
imapService.connectAndLoadMessages().catch(error => { imapService.connectAndLoadMessages().catch(error => {
console.error('fatal error from imap service', error) console.error('Fatal error from IMAP service', error)
process.exit(1) process.exit(1)
}) })
server.on('error', error => { server.on('error', error => {
if (error.syscall !== 'listen') { if (error.syscall !== 'listen') {
console.error('fatal web server error', error) console.error('Fatal web server error', error)
return return
} }
// Handle specific listen errors with friendly messages // Handle specific listen errors with friendly messages
switch (error.code) { switch (error.code) {
case 'EACCES': case 'EACCES':
console.error( console.error(
'Port ' + config.http.port + ' requires elevated privileges' 'Port ' + config.http.port + ' requires elevated privileges'
) )
process.exit(1) process.exit(1)
case 'EADDRINUSE': case 'EADDRINUSE':
console.error('Port ' + config.http.port + ' is already in use') console.error('Port ' + config.http.port + ' is already in use')
process.exit(1) process.exit(1)
default: default:
console.error('fatal web server error', error) console.error('Fatal web server error', error)
process.exit(1) process.exit(1)
} }
}) })

View file

@ -127,21 +127,18 @@ class ImapService extends EventEmitter {
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.', err)
'got fatal error during imap operation, stop app.',
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('Cant connect, even after retrying, stopping app', error)
throw error throw error
} }
} }
@ -226,7 +223,7 @@ class ImapService extends EventEmitter {
}) })
if (uids.length === 0) { if (uids.length === 0) {
debug('no mails to delete.') debug('No mails to delete.')
return return
} }
@ -235,7 +232,7 @@ class ImapService extends EventEmitter {
uids.forEach(uid => { uids.forEach(uid => {
this.emit(ImapService.EVENT_DELETED_MAIL, 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 * @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)
debug(`deleted mail with UID: ${uid}.`) debug(`Deleted mail with UID: ${uid}.`)
this.emit(ImapService.EVENT_DELETED_MAIL, uid) this.emit(ImapService.EVENT_DELETED_MAIL, uid)
} }
} }
@ -297,10 +294,10 @@ class ImapService extends EventEmitter {
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 = [ const searchCriteria = [
@ -344,7 +341,7 @@ class ImapService extends EventEmitter {
} }
}) })
} catch (error) { } catch (error) {
debug('can not fetch', error) debug('Cant fetch', error)
throw error throw error
} }
} }

View file

@ -54,13 +54,13 @@ class MailProcessingService extends EventEmitter {
onInitialLoadDone() { onInitialLoadDone() {
this.initialLoadDone = true 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) { 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 => {
@ -70,7 +70,7 @@ class MailProcessingService extends EventEmitter {
} }
onMailDeleted(uid) { onMailDeleted(uid) {
debug('mail deleted with uid', uid) debug('Mail deleted with uid', uid)
this.mailRepository.removeUid(uid) this.mailRepository.removeUid(uid)
} }
@ -78,7 +78,7 @@ class MailProcessingService extends EventEmitter {
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('Cant delete old messages', error)
} }
} }
@ -86,7 +86,7 @@ class MailProcessingService extends EventEmitter {
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('Cant save mails to file', err)
} }
}) })
} }

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)
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']) 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)) .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
}) })
}) })

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 {param} = require('express-validator') const { param } = require('express-validator')
const config = require('../../../application/config') const config = require('../../../application/config')
const Helper = require('../../../application/helper') const Helper = require('../../../application/helper')
@ -9,182 +9,179 @@ const helper = new(Helper)
const purgeTime = helper.purgeTimeElemetBuilder() const purgeTime = helper.purgeTimeElemetBuilder()
const sanitizeAddress = param('address').customSanitizer( const sanitizeAddress = param('address').customSanitizer(
(value, {req}) => { (value, { req }) => {
return req.params.address return req.params.address
.replace(/[^A-Za-z0-9_.+@-]/g, '') // Remove special characters .replace(/[^A-Za-z0-9_.+@-]/g, '') // Remove special characters
.toLowerCase() .toLowerCase()
} }
) )
router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, (req, res, _next) => { router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, (req, res, _next) => {
const mailProcessingService = req.app.get('mailProcessingService') const mailProcessingService = req.app.get('mailProcessingService')
res.render('inbox', { res.render('inbox', {
title: `${config.http.branding[0]} | ` + req.params.address, title: `${config.http.branding[0]} | ` + req.params.address,
purgeTime: purgeTime, purgeTime: purgeTime,
address: req.params.address, address: req.params.address,
mailSummaries: mailProcessingService.getMailSummaries(req.params.address), mailSummaries: mailProcessingService.getMailSummaries(req.params.address),
branding: config.http.branding, branding: config.http.branding,
}) })
}) })
router.get( router.get(
'^/:address/:uid([0-9]+)', '^/:address/:uid([0-9]+)',
sanitizeAddress, sanitizeAddress,
async (req, res, next) => { async(req, res, next) => {
try { try {
const mailProcessingService = req.app.get('mailProcessingService') const mailProcessingService = req.app.get('mailProcessingService')
const mail = await mailProcessingService.getOneFullMail( const mail = await mailProcessingService.getOneFullMail(
req.params.address, req.params.address,
req.params.uid req.params.uid
) )
if (mail) { if (mail) {
// Set a default subject if none is present // Set a default subject if none is present
if (!mail.subject) { if (!mail.subject) {
mail.subject = 'No Subject' mail.subject = 'No Subject'
} }
// Emails are immutable, cache if found // Emails are immutable, cache if found
res.set('Cache-Control', 'private, max-age=600') res.set('Cache-Control', 'private, max-age=600')
res.render('mail', { res.render('mail', {
title: mail.subject + " | " + req.params.address, title: mail.subject + " | " + req.params.address,
purgeTime: purgeTime, purgeTime: purgeTime,
address: req.params.address, address: req.params.address,
mail, mail,
uid: req.params.uid, uid: req.params.uid,
branding: config.http.branding, branding: config.http.branding,
}) })
} else { } else {
res.render( res.render(
'error', 'error', {
{ purgeTime: purgeTime,
purgeTime: purgeTime, address: req.params.address,
address: req.params.address, message: 'This mail could not be found. It either does not exist or has been deleted from our servers!',
message: 'This mail could not be found. It either does not exist or has been deleted from our servers!', branding: config.http.branding
branding: config.http.branding
} }
) )
} }
} catch (error) { } catch (error) {
console.error('error while fetching one email', error) console.error('Error while fetching email', error)
next(error) next(error)
} }
} }
) )
router.get( router.get(
'^/:address/delete-all', '^/:address/delete-all',
sanitizeAddress, sanitizeAddress,
async (req, res, next) => { async(req, res, next) => {
try { try {
const mailProcessingService = req.app.get('mailProcessingService') const mailProcessingService = req.app.get('mailProcessingService')
const mailSummaries = await mailProcessingService.getMailSummaries(req.params.address) const mailSummaries = await mailProcessingService.getMailSummaries(req.params.address)
for (mail in mailSummaries) { for (mail in mailSummaries) {
await mailProcessingService.deleteSpecificEmail(req.params.address, mailSummaries[mail].uid) await mailProcessingService.deleteSpecificEmail(req.params.address, mailSummaries[mail].uid)
} }
res.redirect(`/inbox/${req.params.address}`) res.redirect(`/inbox/${req.params.address}`)
} catch (error) { } catch (error) {
console.error('error while deleting email', error) console.error('Error while deleting email', error)
next(error) next(error)
} }
} }
) )
router.get( router.get(
'^/:address/:uid/delete', '^/:address/:uid/delete',
sanitizeAddress, sanitizeAddress,
async (req, res, next) => { async(req, res, next) => {
try { try {
const mailProcessingService = req.app.get('mailProcessingService') const mailProcessingService = req.app.get('mailProcessingService')
await mailProcessingService.deleteSpecificEmail(req.params.address, req.params.uid) await mailProcessingService.deleteSpecificEmail(req.params.address, req.params.uid)
res.redirect(`/inbox/${req.params.address}`) res.redirect(`/inbox/${req.params.address}`)
} catch (error) { } catch (error) {
console.error('error while deleting email', error) console.error('Error while deleting email', error)
next(error) next(error)
} }
} }
) )
router.get( router.get(
'^/:address/:uid/:checksum([a-f0-9]+)', '^/:address/:uid/:checksum([a-f0-9]+)',
sanitizeAddress, sanitizeAddress,
async (req, res, next) => { async(req, res, next) => {
try { try {
const mailProcessingService = req.app.get('mailProcessingService') const mailProcessingService = req.app.get('mailProcessingService')
const mail = await mailProcessingService.getOneFullMail( const mail = await mailProcessingService.getOneFullMail(
req.params.address, req.params.address,
req.params.uid req.params.uid
) )
var index = mail.attachments.findIndex(attachment => attachment.checksum === req.params.checksum); var index = mail.attachments.findIndex(attachment => attachment.checksum === req.params.checksum);
const attachment = mail.attachments[index]; const attachment = mail.attachments[index];
if (attachment) { if (attachment) {
try { try {
res.set('Content-Disposition', `attachment; filename=${attachment.filename}`); res.set('Content-Disposition', `attachment; filename=${attachment.filename}`);
res.set('Content-Type', attachment.contentType); res.set('Content-Type', attachment.contentType);
res.send(attachment.content); res.send(attachment.content);
return; return;
} catch (error) { } catch (error) {
console.error('error while fetching attachment', error); console.error('Error while fetching attachment', error);
next(error); next(error);
} }
} else { } else {
res.render( res.render(
'error', 'error', {
{ purgeTime: purgeTime,
purgeTime: purgeTime, address: req.params.address,
address: req.params.address, message: 'This attachment could not be found. It either does not exist or has been deleted from our servers!',
message: 'This attachment could not be found. It either does not exist or has been deleted from our servers!', branding: config.http.branding,
branding: config.http.branding, }
} )
) }
} res.redirect(`/inbox/${req.params.address}`)
res.redirect(`/inbox/${req.params.address}`) } catch (error) {
} catch (error) { console.error('Error while deleting email', error)
console.error('error while deleting email', error) next(error)
next(error) }
} }
}
) )
router.get( router.get(
'^/:address/:uid/raw', '^/:address/:uid/raw',
sanitizeAddress, sanitizeAddress,
async (req, res, next) => { async(req, res, next) => {
try { try {
const mailProcessingService = req.app.get('mailProcessingService') const mailProcessingService = req.app.get('mailProcessingService')
mail = await mailProcessingService.getOneFullMail( mail = await mailProcessingService.getOneFullMail(
req.params.address, req.params.address,
req.params.uid, req.params.uid,
true true
) )
if (mail) { if (mail) {
mail = mail.replace(/(?:\r\n|\r|\n)/g, '<br>') mail = mail.replace(/(?:\r\n|\r|\n)/g, '<br>')
// Emails are immutable, cache if found // Emails are immutable, cache if found
res.set('Cache-Control', 'private, max-age=600') res.set('Cache-Control', 'private, max-age=600')
res.render('raw', { res.render('raw', {
title: req.params.uid + " | raw | " + req.params.address, title: req.params.uid + " | raw | " + req.params.address,
mail mail
}) })
} else { } else {
res.render( res.render(
'error', 'error', {
{ purgeTime: purgeTime,
purgeTime: purgeTime, address: req.params.address,
address: req.params.address, message: 'This mail could not be found. It either does not exist or has been deleted from our servers!',
message: 'This mail could not be found. It either does not exist or has been deleted from our servers!', branding: config.http.branding,
branding: config.http.branding, }
} )
) }
} } catch (error) {
} catch (error) { console.error('Error while fetching raw email', error)
console.error('error while fetching one email', error) next(error)
next(error) }
} }
}
) )
module.exports = router module.exports = router

View file

@ -11,7 +11,7 @@ const socketio = require('socket.io')
const config = require('../../application/config') const config = require('../../application/config')
const inboxRouter = require('./routes/inbox') const inboxRouter = require('./routes/inbox')
const loginRouter = require('./routes/login') const loginRouter = require('./routes/login')
const {sanitizeHtmlTwigFilter} = require('./views/twig-filters') const { sanitizeHtmlTwigFilter } = require('./views/twig-filters')
// Init express middleware // Init express middleware
const app = express() const app = express()
@ -24,20 +24,20 @@ const io = socketio(server)
app.set('socketio', io) app.set('socketio', io)
app.use(logger('dev')) app.use(logger('dev'))
app.use(express.json()) app.use(express.json())
app.use(express.urlencoded({extended: false})) app.use(express.urlencoded({ extended: false }))
// View engine setup // View engine setup
app.set('views', path.join(__dirname, 'views')) app.set('views', path.join(__dirname, 'views'))
app.set('view engine', 'twig') app.set('view engine', 'twig')
app.set('twig options', { app.set('twig options', {
autoescape: true autoescape: true
}) })
// Application code: // Application code:
app.use( app.use(
express.static(path.join(__dirname, 'public'), { express.static(path.join(__dirname, 'public'), {
immutable: true, immutable: true,
maxAge: '1h' maxAge: '1h'
}) })
) )
Twig.extendFilter('sanitizeHtml', sanitizeHtmlTwigFilter) Twig.extendFilter('sanitizeHtml', sanitizeHtmlTwigFilter)
@ -52,18 +52,18 @@ app.use('/inbox', inboxRouter)
// Catch 404 and forward to error handler // Catch 404 and forward to error handler
app.use((req, res, next) => { app.use((req, res, next) => {
next({message: 'page not found', status: 404}) next({ message: 'Page not found', status: 404 })
}) })
// Error handler // Error handler
app.use((err, req, res, _next) => { app.use((err, req, res, _next) => {
// Set locals, only providing error in development // Set locals, only providing error in development
res.locals.message = err.message res.locals.message = err.message
res.locals.error = req.app.get('env') === 'development' ? err : {} res.locals.error = req.app.get('env') === 'development' ? err : {}
// Render the error page // Render the error page
res.status(err.status || 500) res.status(err.status || 500)
res.render('error') res.render('error')
}) })
/** /**
@ -77,9 +77,9 @@ app.set('port', config.http.port)
*/ */
server.listen(config.http.port) server.listen(config.http.port)
server.on('listening', () => { server.on('listening', () => {
const addr = server.address() const addr = server.address()
const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port
debug('Listening on ' + bind) debug('Listening on ' + bind)
}) })
module.exports = {app, io, server} module.exports = { app, io, server }