mirror of
https://github.com/Crazyco-xyz/48hr.email.git
synced 2026-01-09 11:19:36 +01:00
234 lines
7.3 KiB
JavaScript
234 lines
7.3 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 createApiRouter = require('../../api/router')
|
|
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()
|
|
})
|
|
|
|
// Mount API router (v1)
|
|
app.use('/api/v1', (req, res, next) => {
|
|
const apiTokenRepository = req.app.get('apiTokenRepository')
|
|
const dependencies = {
|
|
apiTokenRepository,
|
|
mailProcessingService: req.app.get('mailProcessingService'),
|
|
authService: req.app.get('authService'),
|
|
userRepository: req.app.get('userRepository'),
|
|
imapService: req.app.get('imapService'),
|
|
inboxLock: req.app.get('inboxLock'),
|
|
statisticsStore: req.app.get('statisticsStore'),
|
|
smtpService: req.app.get('smtpService'),
|
|
verificationStore: req.app.get('verificationStore'),
|
|
config: req.app.get('config')
|
|
}
|
|
const apiRouter = createApiRouter(dependencies)
|
|
apiRouter(req, res, next)
|
|
})
|
|
|
|
// Web routes
|
|
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 }
|