diff --git a/infrastructure/web/middleware/lock.js b/infrastructure/web/middleware/lock.js
index a5b18eb..a8b5f2a 100644
--- a/infrastructure/web/middleware/lock.js
+++ b/infrastructure/web/middleware/lock.js
@@ -1,3 +1,5 @@
+const templateContext = require('../template-context')
+
function checkLockAccess(req, res, next) {
const inboxLock = req.app.get('inboxLock')
const address = req.params.address
@@ -21,14 +23,10 @@ function checkLockAccess(req, res, next) {
const unlockError = req.session ? req.session.unlockError : undefined
if (req.session) delete req.session.unlockError
- return res.render('error', {
- purgeTime: require('../../../application/helper').prototype.purgeTimeElemetBuilder(),
- address: address,
- message: 'This inbox is locked by another user. Only the owner can access it.',
- branding: req.app.get('config').http.branding,
- currentUser: req.session && req.session.username,
- authEnabled: req.app.get('config').user.authEnabled
- })
+ return res.render('error', templateContext.build(req, {
+ title: 'Access Denied',
+ message: 'This inbox is locked by another user. Only the owner can access it.'
+ }))
}
// Update last access if they have access and are authenticated
diff --git a/infrastructure/web/routes/account.js b/infrastructure/web/routes/account.js
index 141793d..7a30b62 100644
--- a/infrastructure/web/routes/account.js
+++ b/infrastructure/web/routes/account.js
@@ -3,6 +3,7 @@ const express = require('express')
const router = express.Router()
const { requireAuth } = require('../middleware/auth')
const { body, validationResult } = require('express-validator')
+const templateContext = require('../template-context')
// GET /account - Account dashboard
router.get('/account', requireAuth, async(req, res) => {
@@ -26,25 +27,20 @@ router.get('/account', requireAuth, async(req, res) => {
const config = req.app.get('config')
const stats = userRepository.getUserStats(req.session.userId, config.user)
- // Get purge time for footer
- const purgeTime = helper.purgeTimeElemetBuilder()
+ const successMessage = req.session.accountSuccess
+ const errorMessage = req.session.accountError
+ delete req.session.accountSuccess
+ delete req.session.accountError
- res.render('account', {
+ res.render('account', templateContext.build(req, {
title: 'Account Dashboard',
username: req.session.username,
forwardEmails,
lockedInboxes,
stats,
- branding: config.http.features.branding || ['48hr.email', 'Service', 'https://example.com'],
- purgeTime: purgeTime,
- smtpEnabled: config.email.features.smtp,
- successMessage: req.session.accountSuccess,
- errorMessage: req.session.accountError
- })
-
- // Clear flash messages
- delete req.session.accountSuccess
- delete req.session.accountError
+ successMessage,
+ errorMessage
+ }))
} catch (error) {
console.error('Account page error:', error)
res.status(500).render('error', {
diff --git a/infrastructure/web/routes/auth.js b/infrastructure/web/routes/auth.js
index 7d55bf1..b7323a1 100644
--- a/infrastructure/web/routes/auth.js
+++ b/infrastructure/web/routes/auth.js
@@ -4,10 +4,7 @@ const { body, validationResult } = require('express-validator')
const debug = require('debug')('48hr-email:auth-routes')
const { redirectIfAuthenticated } = require('../middleware/auth')
const config = require('../../../application/config')
-const Helper = require('../../../application/helper')
-const helper = new Helper()
-
-const purgeTime = helper.purgeTimeElemetBuilder()
+const templateContext = require('../template-context')
// Simple in-memory rate limiters for registration and login
const registrationRateLimitStore = new Map()
@@ -96,21 +93,13 @@ router.use((req, res, next) => {
// GET /auth - Show unified auth page (login or register)
router.get('/auth', redirectIfAuthenticated, (req, res) => {
const config = req.app.get('config')
- const errorMessage = req.session.errorMessage
const successMessage = req.session.successMessage
-
- // Clear messages after reading
- delete req.session.errorMessage
delete req.session.successMessage
- res.render('auth', {
+ res.render('auth', templateContext.build(req, {
title: `Login or Register | ${(config.http.features.branding || ['48hr.email'])[0]}`,
- branding: config.http.features.branding || ['48hr.email', 'Service', 'https://example.com'],
- purgeTime: purgeTime,
- smtpEnabled: config.email.features.smtp,
- errorMessage,
successMessage
- })
+ }))
})
// POST /register - Process registration
diff --git a/infrastructure/web/routes/error.js b/infrastructure/web/routes/error.js
index 7fda3f4..8e567ad 100644
--- a/infrastructure/web/routes/error.js
+++ b/infrastructure/web/routes/error.js
@@ -1,13 +1,9 @@
const express = require('express')
-
const router = new express.Router()
const config = require('../../../application/config')
-const Helper = require('../../../application/helper')
-const helper = new(Helper)
+const templateContext = require('../template-context')
const debug = require('debug')('48hr-email:routes')
-const purgeTime = helper.purgeTimeElemetBuilder()
-
router.get('/:address/:errorCode', async(req, res, next) => {
try {
const mailProcessingService = req.app.get('mailProcessingService')
@@ -21,14 +17,11 @@ router.get('/:address/:errorCode', async(req, res, next) => {
debug(`Rendering error page ${errorCode} with message: ${message}`)
const branding = config.http.features.branding || ['48hr.email', 'Service', 'https://example.com']
res.status(errorCode)
- res.render('error', {
+ res.render('error', templateContext.build(req, {
title: `${branding[0]} | ${errorCode}`,
- purgeTime: purgeTime,
- address: req.params.address,
message: message,
- status: errorCode,
- branding: branding
- })
+ status: errorCode
+ }))
} catch (error) {
debug('Error loading error page:', error.message)
console.error('Error while loading error page', error)
diff --git a/infrastructure/web/routes/inbox.js b/infrastructure/web/routes/inbox.js
index faa61ce..61f68d0 100644
--- a/infrastructure/web/routes/inbox.js
+++ b/infrastructure/web/routes/inbox.js
@@ -6,6 +6,7 @@ const debug = require('debug')('48hr-email:routes')
const config = require('../../../application/config')
const Helper = require('../../../application/helper')
const CryptoDetector = require('../../../application/crypto-detector')
+const templateContext = require('../template-context')
const helper = new(Helper)
const cryptoDetector = new CryptoDetector()
const { checkLockAccess } = require('../middleware/lock')
@@ -105,68 +106,11 @@ router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, validateDomain, optiona
throw new Error('Mail processing service not available')
}
debug(`Inbox request for ${req.params.address}`)
- const inboxLock = req.app.get('inboxLock')
- // Check lock status
- const isLocked = inboxLock && inboxLock.isLocked(req.params.address)
- const userId = req.session && req.session.userId
- const isAuthenticated = req.session && req.session.isAuthenticated
-
- // Check if user has access (either owns the lock or has session access)
- const hasAccess = isAuthenticated && userId && inboxLock ?
- (inboxLock.isLockedByUser(req.params.address, userId) || req.session.lockedInbox === req.params.address) :
- (req.session && req.session.lockedInbox === req.params.address)
-
- // Get user's verified emails if logged in
- let userForwardEmails = []
- if (req.session && req.session.userId) {
- const userRepository = req.app.get('userRepository')
- if (userRepository) {
- userForwardEmails = userRepository.getForwardEmails(req.session.userId)
- }
- }
-
- // Pull any lock error from session and clear it after reading
- const lockError = req.session ? req.session.lockError : undefined
- const unlockErrorSession = req.session ? req.session.unlockError : undefined
- const errorMessage = req.session ? req.session.errorMessage : undefined
- if (req.session) {
- delete req.session.lockError
- delete req.session.unlockError
- delete req.session.errorMessage
- }
-
- // Check for forward all success flag
- const forwardAllSuccess = req.query.forwardedAll ? parseInt(req.query.forwardedAll) : null
-
- // Check for verification sent flag
- const verificationSent = req.query.verificationSent === 'true'
- const verificationEmail = req.query.email || ''
-
- res.render('inbox', {
+ res.render('inbox', templateContext.build(req, {
title: `${(config.http.features.branding || ['48hr.email'])[0]} | ` + req.params.address,
- purgeTime: purgeTime,
- address: req.params.address,
- mailSummaries: mailProcessingService.getMailSummaries(req.params.address),
- branding: config.http.branding,
- authEnabled: config.user.authEnabled,
- smtpEnabled: config.email.features.smtp,
- isAuthenticated: req.session && req.session.userId ? true : false,
- userForwardEmails: userForwardEmails,
- isLocked: isLocked,
- hasAccess: hasAccess,
- unlockError: unlockErrorSession,
- locktimer: config.user.lockReleaseHours,
- error: lockError,
- redirectTo: req.originalUrl,
- expiryTime: config.email.purgeTime.time,
- expiryUnit: config.email.purgeTime.unit,
- refreshInterval: config.imap.refreshIntervalSeconds,
- errorMessage: errorMessage,
- forwardAllSuccess: forwardAllSuccess,
- verificationSent: verificationSent,
- verificationEmail: verificationEmail
- })
+ mailSummaries: mailProcessingService.getMailSummaries(req.params.address)
+ }))
} catch (error) {
debug(`Error loading inbox for ${req.params.address}:`, error.message)
console.error('Error while loading inbox', error)
@@ -201,58 +145,13 @@ router.get(
const cryptoAttachments = cryptoDetector.detectCryptoAttachments(mail.attachments)
debug(`Found ${cryptoAttachments.length} cryptographic attachments`)
- const inboxLock = req.app.get('inboxLock')
- const isLocked = inboxLock && inboxLock.isLocked(req.params.address)
- const userId = req.session && req.session.userId
- const isAuthenticated = req.session && req.session.isAuthenticated
-
- // Check if user has access (either owns the lock or has session access)
- const hasAccess = isAuthenticated && userId && inboxLock ?
- (inboxLock.isLockedByUser(req.params.address, userId) || req.session.lockedInbox === req.params.address) :
- (req.session && req.session.lockedInbox === req.params.address)
-
- // Get user's verified emails if logged in
- let userForwardEmails = []
- if (req.session && req.session.userId) {
- const userRepository = req.app.get('userRepository')
- if (userRepository) {
- userForwardEmails = userRepository.getForwardEmails(req.session.userId)
- }
- }
-
- // Pull error message from session and clear it
- const errorMessage = req.session ? req.session.errorMessage : undefined
- if (req.session) {
- delete req.session.errorMessage
- }
-
- // Check for forward success flag
- const forwardSuccess = req.query.forwarded === 'true'
-
- // Check for verification sent flag
- const verificationSent = req.query.verificationSent === 'true'
- const verificationEmail = req.query.email || ''
-
debug(`Rendering email view for UID ${req.params.uid}`)
- res.render('mail', {
+ res.render('mail', templateContext.build(req, {
title: mail.subject + " | " + req.params.address,
- purgeTime: purgeTime,
- address: req.params.address,
mail,
cryptoAttachments: cryptoAttachments,
- uid: req.params.uid,
- branding: config.http.features.branding || ['48hr.email', 'Service', 'https://example.com'],
- authEnabled: config.user.authEnabled,
- smtpEnabled: config.email.features.smtp,
- isAuthenticated: req.session && req.session.userId ? true : false,
- userForwardEmails: userForwardEmails,
- isLocked: isLocked,
- hasAccess: hasAccess,
- errorMessage: errorMessage,
- forwardSuccess: forwardSuccess,
- verificationSent: verificationSent,
- verificationEmail: verificationEmail
- })
+ uid: req.params.uid
+ }))
} else {
debug(`Email ${req.params.uid} not found for ${req.params.address}`)
req.session.errorMessage = 'This mail could not be found. It either does not exist or has been deleted from our servers!'
@@ -424,11 +323,11 @@ router.get(
// Emails are immutable, cache if found
res.set('Cache-Control', 'private, max-age=600')
debug(`Rendering raw email view for UID ${req.params.uid}`)
- res.render('raw', {
+ res.render('raw', templateContext.build(req, {
title: req.params.uid + " | raw | " + req.params.address,
mail: rawMail,
decoded: decodedMail
- })
+ }))
} else {
debug(`Raw email ${uid} not found for ${req.params.address}`)
req.session.errorMessage = 'This mail could not be found. It either does not exist or has been deleted from our servers!'
diff --git a/infrastructure/web/routes/stats.js b/infrastructure/web/routes/stats.js
index 621e60a..ae1f0fb 100644
--- a/infrastructure/web/routes/stats.js
+++ b/infrastructure/web/routes/stats.js
@@ -1,6 +1,7 @@
const express = require('express')
const router = new express.Router()
const debug = require('debug')('48hr-email:stats-routes')
+const templateContext = require('../template-context')
// GET /stats - Statistics page with lazy loading
router.get('/', async(req, res) => {
@@ -16,10 +17,7 @@ router.get('/', async(req, res) => {
return res.redirect(redirectUrl)
}
- const Helper = require('../../../application/helper')
- const helper = new Helper()
const branding = config.http.features.branding || ['48hr.email', 'Service', 'https://example.com']
- const purgeTime = helper.purgeTimeElemetBuilder()
// Return page with placeholder data immediately - real data loads via JS
const placeholderStats = {
@@ -48,15 +46,11 @@ router.get('/', async(req, res) => {
debug(`Stats page requested - returning with lazy loading`)
- res.render('stats', {
+ res.render('stats', templateContext.build(req, {
title: `Statistics | ${branding[0]}`,
- branding: branding,
- purgeTime: purgeTime,
stats: placeholderStats,
- authEnabled: config.user.authEnabled,
- currentUser: req.session && req.session.username,
lazyLoad: true
- })
+ }))
} catch (error) {
debug(`Error loading stats page: ${error.message}`)
console.error('Error while loading stats page', error)
diff --git a/infrastructure/web/template-context.js b/infrastructure/web/template-context.js
index d274c95..f492848 100644
--- a/infrastructure/web/template-context.js
+++ b/infrastructure/web/template-context.js
@@ -19,12 +19,36 @@ class TemplateContext {
* @returns {Object} Base template context
*/
getBaseContext(req) {
+ const inboxLock = req.app.get('inboxLock')
+ const address = req.params && req.params.address
+ const userId = req.session && req.session.userId
+ const isAuthenticated = !!(req.session && req.session.userId)
+
+ // Calculate lock status for current address
+ const isLocked = address && inboxLock ? inboxLock.isLocked(address) : false
+ const hasAccess = address && isAuthenticated && userId && inboxLock ?
+ (inboxLock.isLockedByUser(address, userId) || req.session.lockedInbox === address) :
+ (address && req.session && req.session.lockedInbox === address)
+
+ // Get user's verified forward emails if logged in
+ let userForwardEmails = []
+ if (isAuthenticated && userId) {
+ const userRepository = req.app.get('userRepository')
+ if (userRepository) {
+ userForwardEmails = userRepository.getForwardEmails(userId)
+ }
+ }
+
return {
// Config values
config: config,
branding: config.http.features.branding || ['48hr.email', 'Service', 'https://example.com'],
purgeTime: this.purgeTime,
purgeTimeRaw: config.email.purgeTime,
+ expiryTime: config.email.purgeTime.time,
+ expiryUnit: config.email.purgeTime.unit,
+ refreshInterval: config.imap.refreshIntervalSeconds,
+ locktimer: config.user.lockReleaseHours,
// Feature flags
authEnabled: config.user.authEnabled,
@@ -32,8 +56,29 @@ class TemplateContext {
smtpEnabled: config.email.features.smtp,
showInfoSection: config.http.features.infoSection,
- // User session
+ // User session & authentication
currentUser: req.session && req.session.username ? req.session.username : null,
+ isAuthenticated: isAuthenticated,
+ userForwardEmails: userForwardEmails,
+
+ // Lock status
+ isLocked: isLocked,
+ hasAccess: hasAccess,
+
+ // Session messages/errors (auto-clear after reading)
+ error: this._getAndClearSession(req, 'lockError'),
+ unlockError: this._getAndClearSession(req, 'unlockError'),
+ errorMessage: this._getAndClearSession(req, 'errorMessage'),
+
+ // Query parameters
+ verificationSent: req.query && req.query.verificationSent === 'true',
+ verificationEmail: req.query && req.query.email || '',
+ forwardSuccess: req.query && req.query.forwarded === 'true',
+ forwardAllSuccess: req.query && req.query.forwardedAll ? parseInt(req.query.forwardedAll) : null,
+
+ // Request info
+ redirectTo: req.originalUrl,
+ address: address,
// Common data
domains: this.cachedDomains,
@@ -41,6 +86,17 @@ class TemplateContext {
}
}
+ /**
+ * Helper to get and clear session value
+ * @private
+ */
+ _getAndClearSession(req, key) {
+ if (!req.session) return undefined
+ const value = req.session[key]
+ delete req.session[key]
+ return value
+ }
+
/**
* Merge base context with page-specific data
* @param {Object} req - Express request object
diff --git a/infrastructure/web/views/layout.twig b/infrastructure/web/views/layout.twig
index 645b738..9c70d4b 100644
--- a/infrastructure/web/views/layout.twig
+++ b/infrastructure/web/views/layout.twig
@@ -9,7 +9,7 @@
{% block metaTags %}
-
+
@@ -20,7 +20,7 @@
-
+
diff --git a/infrastructure/web/views/twig-filters.js b/infrastructure/web/views/twig-filters.js
index 14a8217..41e667a 100644
--- a/infrastructure/web/views/twig-filters.js
+++ b/infrastructure/web/views/twig-filters.js
@@ -71,12 +71,7 @@ function convertAndRound(time, unit) {
*/
exports.readablePurgeTime = function(purgeTime) {
if (!purgeTime || !purgeTime.time || !purgeTime.unit) {
- // Fallback to config if not provided
- if (config.email.purgeTime) {
- purgeTime = config.email.purgeTime
- } else {
- return '48 hours'
- }
+ purgeTime = config.email.purgeTime
}
let result = `${purgeTime.time} ${purgeTime.unit}`
diff --git a/infrastructure/web/web.js b/infrastructure/web/web.js
index 8690c85..8319e7f 100644
--- a/infrastructure/web/web.js
+++ b/infrastructure/web/web.js
@@ -18,12 +18,9 @@ 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')
-const Helper = require('../../application/helper')
-const helper = new(Helper)
-const purgeTime = helper.purgeTimeElemetBuilder()
-
// Utility function for consistent error handling in routes
const handleRouteError = (error, req, res, next, context = 'route') => {
debug(`Error in ${context}:`, error.message)
@@ -143,7 +140,9 @@ app.use(async(req, res, next) => {
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')
+ return res.render('loading', templateContext.build(req, {
+ title: 'Loading...'
+ }))
}
next()
})
@@ -174,11 +173,11 @@ app.use(async(err, req, res, _next) => {
// Render the error page
res.status(err.status || 500)
- res.render('error', {
- purgeTime: purgeTime,
- address: req.params && req.params.address,
- branding: config.http.features.branding || ['48hr.email', 'Service', 'https://example.com']
- })
+ 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)