48hr.email/infrastructure/web/routes/inbox.js
2026-01-01 00:37:14 +01:00

346 lines
No EOL
14 KiB
JavaScript

const express = require('express')
const router = new express.Router()
const { param } = require('express-validator')
const debug = require('debug')('48hr-email:routes')
const config = require('../../../application/config')
const Helper = require('../../../application/helper')
const CryptoDetector = require('../../../application/crypto-detector')
const helper = new(Helper)
const cryptoDetector = new CryptoDetector()
const { checkLockAccess } = require('../middleware/lock')
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()
}
)
// Middleware to validate domain is in allowed list
const validateDomain = (req, res, next) => {
const address = req.params.address
const domain = address.split('@')[1]
if (!domain) {
req.session.errorMessage = 'Invalid email address format.'
return res.redirect(`/error/${address}/400`)
}
const allowedDomains = config.email.domains.map(d => d.toLowerCase())
if (!allowedDomains.includes(domain.toLowerCase())) {
req.session.errorMessage = `Domain '${domain}' is not supported by this service.`
return res.redirect(`/error/${address}/403`)
}
next()
}
router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, validateDomain, checkLockAccess, async(req, res, next) => {
try {
const mailProcessingService = req.app.get('mailProcessingService')
if (!mailProcessingService) {
throw new Error('Mail processing service not available')
}
debug(`Inbox request for ${req.params.address}`)
const inboxLock = req.app.get('inboxLock')
const count = await mailProcessingService.getCount()
const largestUid = await req.app.locals.imapService.getLargestUid()
const totalcount = helper.countElementBuilder(count, largestUid)
debug(`Rendering inbox with ${count} total mails`)
const isLocked = inboxLock && inboxLock.isLocked(req.params.address)
const hasAccess = req.session && req.session.lockedInbox === req.params.address
// 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
if (req.session) {
delete req.session.lockError
delete req.session.unlockError
}
res.render('inbox', {
title: `${config.http.branding[0]} | ` + req.params.address,
purgeTime: purgeTime,
address: req.params.address,
count: count,
totalcount: totalcount,
mailSummaries: mailProcessingService.getMailSummaries(req.params.address),
branding: config.http.branding,
lockEnabled: config.lock.enabled,
isLocked: isLocked,
hasAccess: hasAccess,
unlockError: unlockErrorSession,
locktimer: config.lock.releaseHours,
error: lockError,
redirectTo: req.originalUrl,
expiryTime: config.email.purgeTime.time,
expiryUnit: config.email.purgeTime.unit,
refreshInterval: config.imap.refreshIntervalSeconds
})
} catch (error) {
debug(`Error loading inbox for ${req.params.address}:`, error.message)
console.error('Error while loading inbox', error)
next(error)
}
})
router.get(
'^/:address/:uid([0-9]+)',
sanitizeAddress,
validateDomain,
checkLockAccess,
async(req, res, next) => {
try {
const mailProcessingService = req.app.get('mailProcessingService')
debug(`Viewing email ${req.params.uid} for ${req.params.address}`)
const count = await mailProcessingService.getCount()
const largestUid = await req.app.locals.imapService.getLargestUid()
const totalcount = helper.countElementBuilder(count, largestUid)
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')
// Detect cryptographic keys in attachments
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 hasAccess = req.session && req.session.lockedInbox === req.params.address
debug(`Rendering email view for UID ${req.params.uid}`)
res.render('mail', {
title: mail.subject + " | " + req.params.address,
purgeTime: purgeTime,
address: req.params.address,
count: count,
totalcount: totalcount,
mail,
cryptoAttachments: cryptoAttachments,
uid: req.params.uid,
branding: config.http.branding,
lockEnabled: config.lock.enabled,
isLocked: isLocked,
hasAccess: hasAccess
})
} 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!'
res.redirect(`/error/${req.params.address}/404`)
}
} catch (error) {
debug(`Error fetching email ${req.params.uid} for ${req.params.address}:`, error.message)
console.error('Error while fetching email', error)
next(error)
}
}
)
// Catch-all for invalid UIDs (non-numeric)
router.get(
'^/:address/delete-all',
sanitizeAddress,
validateDomain,
checkLockAccess,
async(req, res, next) => {
try {
const mailProcessingService = req.app.get('mailProcessingService')
debug(`Deleting all emails for ${req.params.address}`)
const mailSummaries = await mailProcessingService.getMailSummaries(req.params.address)
// Create a copy of the array to avoid modification during iteration
const summariesToDelete = [...mailSummaries]
let deletedCount = 0
for (const mail of summariesToDelete) {
await mailProcessingService.deleteSpecificEmail(req.params.address, mail.uid)
deletedCount++
debug(`Successfully deleted UID ${mail.uid}`)
}
debug(`Deleted all ${deletedCount} emails for ${req.params.address}`)
res.redirect(`/inbox/${req.params.address}`)
} catch (error) {
debug(`Error deleting all emails for ${req.params.address}:`, error.message)
console.error('Error while deleting email', error)
next(error)
}
}
)
router.get(
'^/:address/:uid/delete',
sanitizeAddress,
validateDomain,
checkLockAccess,
async(req, res, next) => {
try {
const mailProcessingService = req.app.get('mailProcessingService')
mailProcessingService.deleteSpecificEmail(req.params.address, req.params.uid)
res.redirect(`/inbox/${req.params.address}`)
} catch (error) {
debug(`Error deleting email ${req.params.uid} for ${req.params.address}:`, error.message)
console.error('Error while deleting email', error)
next(error)
}
}
)
router.get(
'^/:address/:uid/:checksum([a-f0-9]+)',
sanitizeAddress,
validateDomain,
checkLockAccess,
async(req, res, next) => {
try {
const mailProcessingService = req.app.get('mailProcessingService')
debug(`Fetching attachment ${req.params.checksum} for email ${req.params.uid} (${req.params.address})`)
const uid = parseInt(req.params.uid, 10)
const count = await mailProcessingService.getCount()
// Validate UID is a valid integer
if (isNaN(uid) || uid <= 0) {
debug(`Invalid UID provided: ${req.params.uid}`)
req.session.errorMessage = 'Invalid/Malformed UID provided.'
return res.redirect(`/error/${req.params.address}/400`)
}
const mail = await mailProcessingService.getOneFullMail(
req.params.address,
uid
)
if (!mail || !mail.attachments) {
debug(`Email ${uid} or attachments not found for ${req.params.address}`)
req.session.errorMessage = 'This email could not be found. It either does not exist or has been deleted from our servers!'
return res.redirect(`/error/${req.params.address}/404`)
}
var index = mail.attachments.findIndex(attachment => attachment.checksum === req.params.checksum);
const attachment = mail.attachments[index];
if (attachment) {
try {
debug(`Serving attachment: ${attachment.filename}`)
res.set('Content-Disposition', `attachment; filename=${attachment.filename}`);
res.set('Content-Type', attachment.contentType);
res.send(attachment.content);
return;
} catch (error) {
debug(`Error serving attachment: ${error.message}`)
console.error('Error while fetching attachment', error);
next(error);
return;
}
} else {
debug(`Attachment ${req.params.checksum} not found in email ${uid}`)
req.session.errorMessage = 'This attachment could not be found. It either does not exist or has been deleted from our servers!'
return res.redirect(`/error/${req.params.address}/404`)
}
} catch (error) {
debug(`Error fetching attachment: ${error.message}`)
console.error('Error while fetching attachment', error)
next(error)
}
}
)
router.get(
'^/:address/:uid/raw',
sanitizeAddress,
validateDomain,
checkLockAccess,
async(req, res, next) => {
try {
const mailProcessingService = req.app.get('mailProcessingService')
debug(`Fetching raw email ${req.params.uid} for ${req.params.address}`)
const uid = parseInt(req.params.uid, 10)
const count = await mailProcessingService.getCount()
const largestUid = await req.app.locals.imapService.getLargestUid()
const totalcount = helper.countElementBuilder(count, largestUid)
// Validate UID is a valid integer
if (isNaN(uid) || uid <= 0) {
debug(`Invalid UID provided for raw view: ${req.params.uid}`)
req.session.errorMessage = 'Invalid/Malformed UID provided.'
return res.redirect(`/error/${req.params.address}/400`)
}
mail = await mailProcessingService.getOneFullMail(
req.params.address,
uid,
true
)
if (mail) {
const decodeQuotedPrintable = (input) => {
if (!input) return '';
// Remove soft line breaks
let cleaned = input.replace(/=\r?\n/g, '');
// Decode =XX hex escapes
cleaned = cleaned.replace(/=([0-9A-Fa-f]{2})/g, (_, hex) => {
try {
return String.fromCharCode(parseInt(hex, 16));
} catch {
return '=' + hex;
}
});
return cleaned;
};
const decodedMail = decodeQuotedPrintable(mail);
// Keep raw content but add literal newlines after <br> tags for readability
const rawMail = mail.replace(/<br\s*\/?\s*>/gi, '<br>\n');
// 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', {
title: req.params.uid + " | raw | " + req.params.address,
mail: rawMail,
decoded: decodedMail,
totalcount: totalcount
})
} 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!'
res.redirect(`/error/${req.params.address}/404`)
}
} catch (error) {
debug(`Error fetching raw email ${req.params.uid}: ${error.message}`)
console.error('Error while fetching raw email', error)
next(error)
}
}
)
// Final catch-all for invalid UIDs (non-numeric or unmatched patterns)
router.get(
'^/:address/:uid',
sanitizeAddress,
validateDomain,
async(req, res) => {
req.session.errorMessage = 'Invalid/Malformed UID provided.'
res.redirect(`/error/${req.params.address}/400`)
}
)
module.exports = router