mirror of
https://github.com/Crazyco-xyz/48hr.email.git
synced 2026-01-09 11:19:36 +01:00
Adds UX debug mode while mocking the imap server and other critical parts of the service simply to test UI elements for faster development
213 lines
6.5 KiB
JavaScript
213 lines
6.5 KiB
JavaScript
const path = require('path')
|
|
const http = require('http')
|
|
const debug = require('debug')('48hr-email:server')
|
|
const express = require('express')
|
|
const session = require('express-session')
|
|
const cookieParser = require('cookie-parser')
|
|
const logger = require('morgan')
|
|
const Twig = require('twig')
|
|
const compression = require('compression')
|
|
const helmet = require('helmet')
|
|
const socketio = require('socket.io')
|
|
|
|
const config = require('../../application/config')
|
|
const inboxRouter = require('./routes/inbox')
|
|
const loginRouter = require('./routes/login')
|
|
const errorRouter = require('./routes/error')
|
|
const lockRouter = require('./routes/lock')
|
|
const authRouter = require('./routes/auth')
|
|
const accountRouter = require('./routes/account')
|
|
const statsRouter = require('./routes/stats')
|
|
const templateContext = require('./template-context')
|
|
const { sanitizeHtmlTwigFilter, readablePurgeTime } = require('./views/twig-filters')
|
|
|
|
// Utility function for consistent error handling in routes
|
|
const handleRouteError = (error, req, res, next, context = 'route') => {
|
|
debug(`Error in ${context}:`, error.message)
|
|
console.error(`Error in ${context}`, error)
|
|
next(error)
|
|
}
|
|
|
|
// Init express middleware
|
|
const app = express()
|
|
app.use(helmet())
|
|
app.use(compression())
|
|
app.set('config', config)
|
|
const server = http.createServer(app)
|
|
const io = socketio(server)
|
|
|
|
app.set('socketio', io)
|
|
|
|
// HTTP request logging - only enable with DEBUG environment variable
|
|
if (process.env.DEBUG && process.env.DEBUG.includes('48hr-email')) {
|
|
app.use(logger('dev'))
|
|
}
|
|
|
|
app.use(express.json())
|
|
app.use(express.urlencoded({ extended: false }))
|
|
|
|
// Cookie parser for signed cookies (email verification)
|
|
app.use(cookieParser(config.http.sessionSecret))
|
|
|
|
// Session support (always enabled for forward verification and inbox locking)
|
|
app.use(session({
|
|
secret: config.http.sessionSecret,
|
|
resave: false,
|
|
saveUninitialized: false,
|
|
cookie: { maxAge: 24 * 60 * 60 * 1000 } // 24 hours
|
|
}))
|
|
|
|
// Clear lock session data when user goes Home (but preserve authentication)
|
|
app.get('/', (req, res, next) => {
|
|
if (req.session && req.session.lockedInbox) {
|
|
// Only clear lock-related data, preserve user authentication
|
|
delete req.session.lockedInbox
|
|
req.session.save(() => next())
|
|
} else {
|
|
next()
|
|
}
|
|
})
|
|
|
|
// Remove trailing slash middleware (except for root)
|
|
app.use((req, res, next) => {
|
|
if (req.path.length > 1 && req.path.endsWith('/')) {
|
|
const query = req.url.slice(req.path.length) // preserve query string
|
|
return res.redirect(301, req.path.slice(0, -1) + query)
|
|
}
|
|
next()
|
|
})
|
|
|
|
// View engine setup
|
|
app.set('views', path.join(__dirname, 'views'))
|
|
app.set('view engine', 'twig')
|
|
app.set('twig options', {
|
|
autoescape: true
|
|
})
|
|
|
|
// Application code:
|
|
app.use(
|
|
express.static(path.join(__dirname, 'public'), {
|
|
immutable: true,
|
|
maxAge: '1h'
|
|
})
|
|
)
|
|
Twig.extendFilter('sanitizeHtml', sanitizeHtmlTwigFilter)
|
|
Twig.extendFilter('readablePurgeTime', readablePurgeTime)
|
|
|
|
// Middleware to expose user session to all templates
|
|
app.use((req, res, next) => {
|
|
res.locals.authEnabled = config.user.authEnabled
|
|
res.locals.config = config
|
|
res.locals.currentUser = null
|
|
res.locals.alertMessage = req.session ? req.session.alertMessage : null
|
|
|
|
// Clear alert after reading
|
|
if (req.session && req.session.alertMessage) {
|
|
delete req.session.alertMessage
|
|
}
|
|
|
|
if (req.session && req.session.userId && req.session.username && req.session.isAuthenticated) {
|
|
res.locals.currentUser = {
|
|
id: req.session.userId,
|
|
username: req.session.username
|
|
}
|
|
}
|
|
next()
|
|
})
|
|
|
|
// Middleware to expose mail count to all templates
|
|
app.use(async(req, res, next) => {
|
|
const mailProcessingService = req.app.get('mailProcessingService')
|
|
const imapService = req.app.get('imapService')
|
|
const Helper = require('../../application/helper')
|
|
const helper = new Helper()
|
|
|
|
if (mailProcessingService) {
|
|
const count = mailProcessingService.getCount()
|
|
let largestUid = null
|
|
|
|
if (imapService) {
|
|
try {
|
|
largestUid = await imapService.getLargestUid()
|
|
} catch (e) {
|
|
debug('Error getting largest UID:', e.message)
|
|
}
|
|
}
|
|
|
|
res.locals.mailCount = helper.mailCountBuilder(count, largestUid)
|
|
} else {
|
|
res.locals.mailCount = ''
|
|
}
|
|
next()
|
|
})
|
|
|
|
// Middleware to show loading page until IMAP is ready
|
|
app.use((req, res, next) => {
|
|
const isImapReady = req.app.get('isImapReady')
|
|
if (!isImapReady && !req.path.startsWith('/images') && !req.path.startsWith('/javascripts') && !req.path.startsWith('/stylesheets') && !req.path.startsWith('/dependencies')) {
|
|
return res.render('loading', templateContext.build(req, {
|
|
title: 'Loading...'
|
|
}))
|
|
}
|
|
next()
|
|
})
|
|
|
|
app.use('/', loginRouter)
|
|
if (config.user.authEnabled) {
|
|
app.use('/', authRouter)
|
|
app.use('/', accountRouter)
|
|
}
|
|
app.use('/inbox', inboxRouter)
|
|
app.use('/error', errorRouter)
|
|
app.use('/lock', lockRouter)
|
|
app.use('/stats', statsRouter)
|
|
|
|
// Catch 404 and forward to error handler
|
|
app.use((req, res, next) => {
|
|
next({ message: 'Page not found', status: 404 })
|
|
})
|
|
|
|
// Error handler
|
|
app.use(async(err, req, res, _next) => {
|
|
try {
|
|
debug('Error handler triggered:', err.message)
|
|
|
|
// 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', templateContext.build(req, {
|
|
title: 'Error',
|
|
message: err.message,
|
|
status: err.status || 500
|
|
}))
|
|
} catch (renderError) {
|
|
debug('Error in error handler:', renderError.message)
|
|
console.error('Critical error in error handler', renderError)
|
|
// Fallback: send plain text error if rendering fails
|
|
res.status(500).send('Internal Server Error')
|
|
}
|
|
})
|
|
|
|
/**
|
|
* Get port from environment and store in Express.
|
|
*/
|
|
|
|
app.set('port', config.http.port)
|
|
|
|
/**
|
|
* Listen on provided port, on all network interfaces.
|
|
*/
|
|
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)
|
|
|
|
// Emit event for app.js to display startup banner
|
|
server.emit('ready')
|
|
})
|
|
|
|
module.exports = { app, io, server }
|