const express = require('express') const router = new express.Router() const { param, body, validationResult } = 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() } // Simple in-memory rate limiter for forwarding (5 requests per 15 minutes per IP) const forwardRateLimitStore = new Map() const forwardLimiter = (req, res, next) => { const ip = req.ip || req.connection.remoteAddress const now = Date.now() const windowMs = 15 * 60 * 1000 // 15 minutes const maxRequests = 5 // Clean up old entries for (const [key, data] of forwardRateLimitStore.entries()) { if (now - data.resetTime > windowMs) { forwardRateLimitStore.delete(key) } } // Get or create entry for this IP let ipData = forwardRateLimitStore.get(ip) if (!ipData || now - ipData.resetTime > windowMs) { ipData = { count: 0, resetTime: now } forwardRateLimitStore.set(ip, ipData) } // Check if limit exceeded if (ipData.count >= maxRequests) { debug(`Rate limit exceeded for IP ${ip}`) req.session.errorMessage = 'Too many forward requests. Please try again after 15 minutes.' return res.redirect(`/inbox/${req.params.address}`) } // Increment counter ipData.count++ next() } // Email validation middleware for forwarding const validateForwardRequest = [ sanitizeAddress, body('destinationEmail') .trim() .isEmail() .withMessage('Invalid email address format') .normalizeEmail() .custom((value) => { // Prevent forwarding to temporary email addresses const domain = value.split('@')[1] if (!domain) { throw new Error('Invalid email address') } const tempDomains = config.email.domains.map(d => d.toLowerCase()) if (tempDomains.includes(domain.toLowerCase())) { throw new Error('Cannot forward to temporary email addresses') } return true }) ] 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 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 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, errorMessage: errorMessage, forwardAllSuccess: forwardAllSuccess }) } 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 // 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' 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, errorMessage: errorMessage, forwardSuccess: forwardSuccess }) } 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
tags for readability const rawMail = mail.replace(//gi, '
\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) } } ) // POST route for forwarding a single email router.post( '^/:address/:uid/forward', forwardLimiter, validateDomain, checkLockAccess, validateForwardRequest, async(req, res, next) => { try { const errors = validationResult(req) if (!errors.isEmpty()) { const firstError = errors.array()[0].msg debug(`Forward validation failed for ${req.params.address}: ${firstError}`) req.session.errorMessage = firstError return res.redirect(`/inbox/${req.params.address}/${req.params.uid}`) } const mailProcessingService = req.app.get('mailProcessingService') const { destinationEmail } = req.body const uid = parseInt(req.params.uid, 10) debug(`Forwarding email ${uid} from ${req.params.address} to ${destinationEmail}`) const result = await mailProcessingService.forwardEmail( req.params.address, uid, destinationEmail ) if (result.success) { debug(`Email ${uid} forwarded successfully to ${destinationEmail}`) return res.redirect(`/inbox/${req.params.address}/${uid}?forwarded=true`) } else { debug(`Failed to forward email ${uid}: ${result.error}`) req.session.errorMessage = result.error return res.redirect(`/inbox/${req.params.address}/${uid}`) } } catch (error) { debug(`Error forwarding email ${req.params.uid}: ${error.message}`) console.error('Error while forwarding email', error) req.session.errorMessage = 'An unexpected error occurred while forwarding the email.' res.redirect(`/inbox/${req.params.address}/${req.params.uid}`) } } ) // POST route for forwarding all emails in an inbox router.post( '^/:address/forward-all', forwardLimiter, validateDomain, checkLockAccess, validateForwardRequest, async(req, res, next) => { try { const validationErrors = validationResult(req) if (!validationErrors.isEmpty()) { const firstError = validationErrors.array()[0].msg debug(`Forward all validation failed for ${req.params.address}: ${firstError}`) req.session.errorMessage = firstError return res.redirect(`/inbox/${req.params.address}`) } const mailProcessingService = req.app.get('mailProcessingService') const { destinationEmail } = req.body debug(`Forwarding all emails from ${req.params.address} to ${destinationEmail}`) const mailSummaries = await mailProcessingService.getMailSummaries(req.params.address) // Limit bulk forwarding to 25 emails const MAX_FORWARD_ALL = 25 if (mailSummaries.length > MAX_FORWARD_ALL) { debug(`Forward all blocked: ${mailSummaries.length} emails exceeds limit of ${MAX_FORWARD_ALL}`) req.session.errorMessage = `Cannot forward more than ${MAX_FORWARD_ALL} emails at once. You have ${mailSummaries.length} emails.` return res.redirect(`/inbox/${req.params.address}`) } if (mailSummaries.length === 0) { debug(`No emails to forward for ${req.params.address}`) req.session.errorMessage = 'No emails to forward.' return res.redirect(`/inbox/${req.params.address}`) } let successCount = 0 let failCount = 0 const failMessages = [] for (const mail of mailSummaries) { const result = await mailProcessingService.forwardEmail( req.params.address, mail.uid, destinationEmail ) if (result.success) { successCount++ debug(`Successfully forwarded email UID ${mail.uid}`) } else { failCount++ debug(`Failed to forward email UID ${mail.uid}: ${result.error}`) failMessages.push(`UID ${mail.uid}: ${result.error}`) } } debug(`Forward all complete: ${successCount} succeeded, ${failCount} failed`) if (successCount > 0 && failCount === 0) { return res.redirect(`/inbox/${req.params.address}?forwardedAll=${successCount}`) } else if (successCount > 0 && failCount > 0) { req.session.errorMessage = `Forwarded ${successCount} email(s), but ${failCount} failed.` return res.redirect(`/inbox/${req.params.address}`) } else { req.session.errorMessage = `Failed to forward emails: ${failMessages[0] || 'Unknown error'}` return res.redirect(`/inbox/${req.params.address}`) } } catch (error) { debug(`Error forwarding all emails: ${error.message}`) console.error('Error while forwarding all emails', error) req.session.errorMessage = 'An unexpected error occurred while forwarding emails.' res.redirect(`/inbox/${req.params.address}`) } } ) // 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