[Chore]: Add extensive debug logging and improve config clarity

Introduces detailed debug logging throughout the application to aid troubleshooting and monitoring, unifying the debug namespace usage. Refactors configuration files for clarity, adds missing environment variables, and updates example values and documentation. Enhances screenshots management by hosting assets locally. Updates scripts for better development and production workflows. Improves comments for maintainability and adjusts minor UI meta tags.
This commit is contained in:
ClaraCrazy 2025-12-25 17:46:02 +01:00
parent ba6d97c7fe
commit 994142fc29
No known key found for this signature in database
GPG key ID: EBBC896ACB497011
21 changed files with 9034 additions and 8985 deletions

View file

@ -1,5 +1,5 @@
# --- EMAIL CONFIGURATION ---
EMAIL_DOMAINS=["example.com","example.net"] # List of domains your service handles (list)
EMAIL_DOMAINS=["example.com","example.net"] # List of domains your service handles ['example.com', 'example.net']
# --- Purge configuration ---
EMAIL_PURGE_TIME=48 # Time value for when to purge
@ -24,7 +24,7 @@ IMAP_CONCURRENCY=6 # Number of conc
# --- HTTP / WEB CONFIGURATION ---
HTTP_PORT=3000 # Port
HTTP_BRANDING=["48hr.email","CrazyCo","https://crazyco.xyz"] # [service_title, company_name, company_url]
HTTP_BRANDING=["48hr.email","CrazyCo","https://crazyco.xyz"] # ['service_title', 'company_name', 'company_url']
HTTP_DISPLAY_SORT=2 # Domain display sorting:
# 0 = no change,
# 1 = alphabetical,

2
.github/FUNDING.yml vendored
View file

@ -3,7 +3,7 @@
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: crazyco
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
ko_fi: crazyco
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username

BIN
.github/assets/html.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

BIN
.github/assets/inbox.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

BIN
.github/assets/raw.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

1
.gitignore vendored
View file

@ -5,4 +5,3 @@
copilot-instructions.md
node_modules
db/*
package-lock.json

View file

@ -71,7 +71,7 @@ User=user
Group=user
WorkingDirectory=/opt/48hr-email
ExecStart=npm run start
ExecStart=npm run prod
Restart=on-failure
TimeoutStartSec=0
@ -113,13 +113,13 @@ WantedBy=multi-user.target
### Screenshots:
- #### Inbox:
<img align="center" src="https://i.imgur.com/JJmSe7S.png">
<img align="center" src=".github/assets/inbox.png">
- #### Email with CSS:
<img align="center" src="https://i.imgur.com/x8OBoI7.png">
- #### Email using HTML and CSS:
<img align="center" src=".github/assets/html.png">
- #### Email without CSS:
<img align="center" src="https://i.imgur.com/VPZ8IG6.png">
<img align="center" src=".github/assets/raw.png">
<br><br>

11
app.js
View file

@ -3,6 +3,7 @@
/* eslint unicorn/no-process-exit: 0 */
const config = require('./application/config')
const debug = require('debug')('48hr-email:app')
// Until node 11 adds flatmap, we use this:
require('array.prototype.flatmap').shim()
@ -14,40 +15,50 @@ const MailProcessingService = require('./application/mail-processing-service')
const MailRepository = require('./domain/mail-repository')
const clientNotification = new ClientNotification()
debug('Client notification service initialized')
clientNotification.use(io)
const imapService = new ImapService(config)
debug('IMAP service initialized')
const mailProcessingService = new MailProcessingService(
new MailRepository(),
imapService,
clientNotification,
config
)
debug('Mail processing service initialized')
// Put everything together:
imapService.on(ImapService.EVENT_NEW_MAIL, mail =>
mailProcessingService.onNewMail(mail)
)
debug('Bound IMAP new mail event handler')
imapService.on(ImapService.EVENT_INITIAL_LOAD_DONE, () =>
mailProcessingService.onInitialLoadDone()
)
debug('Bound IMAP initial load done event handler')
imapService.on(ImapService.EVENT_DELETED_MAIL, mail =>
mailProcessingService.onMailDeleted(mail)
)
debug('Bound IMAP deleted mail event handler')
mailProcessingService.on('error', err => {
debug('Fatal error from mail processing service:', err.message)
console.error('Error from mailProcessingService, stopping.', err)
process.exit(1)
})
imapService.on(ImapService.EVENT_ERROR, error => {
debug('Fatal error from IMAP service:', error.message)
console.error('Fatal error from IMAP service', error)
process.exit(1)
})
app.set('mailProcessingService', mailProcessingService)
debug('Starting IMAP connection and message loading')
imapService.connectAndLoadMessages().catch(error => {
debug('Failed to connect to IMAP:', error.message)
console.error('Fatal error from IMAP service', error)
process.exit(1)
})

151
app.json
View file

@ -1,71 +1,84 @@
{
"name": "48hr.email | Disposable email",
"description": "A simple and fast disposable mail service that works directly with your already existing imap server. No database required.",
"repository": "https://github.com/Crazyco-xyz/48hr.email",
"logo": "https://github.com/Crazyco-xyz/48hr.email/blob/main/infrastructure/web/public/images/logo.png",
"keywords": [
"node",
"disposable-mail"
],
"env": {
"EMAIL_DOMAINS": {
"description": "Email domains"
},
"EMAIL_PURGE_TIME": {
"description": "Config for when to purge the emails",
"value": {
"time": 48,
"unit": "hours",
"convert": true
}
},
"EMAIL_EXAMPLES": {
"description": "Examples of the domains",
"value": {
"account": "example@48hr.email",
"uids": [1, 2, 3]
}
},
"IMAP_USER": {
"description": "Username to login to the imap server"
},
"IMAP_PASSWORD": {
"description": "Password to login to the imap server"
},
"IMAP_SERVER": {
"description": "Hostname of the server (usually imap.example.com or mx.example.com)"
},
"IMAP_PORT": {
"description": "Port of the server (usually 993)",
"value": 993
},
"IMAP_TLS": {
"description": "Use tls or not",
"value": true
},
"IMAP_AUTHTIMEOUT": {
"description": "Timeout for the auth",
"value": 3000
},
"IMAP_REFRESH_INTERVAL_SECONDS": {
"description": "How often to refresh the imap messages manually",
"value": 60
},
"HTTP_PORT": {
"description": "Port to listen on",
"value": 3000
},
"HTTP_BRANDING": {
"description": "The branding of the site",
"value": ["48hr.email", "Crazyco", "https://crazyco.xyz"]
},
"HTTP_DISPLAY_SORT": {
"description": "Sort the emails for use",
"value": 0
},
"HTTP_HIDE_OTHER": {
"description": "Hide other emails from the list besides the first",
"value": false
"name": "48hr.email | Disposable email",
"description": "A simple and fast disposable mail service that works directly with your already existing imap server. No database required.",
"repository": "https://github.com/Crazyco-xyz/48hr.email",
"logo": "https://github.com/Crazyco-xyz/48hr.email/blob/main/infrastructure/web/public/images/logo.png",
"keywords": [
"node",
"disposable-mail"
],
"env": {
"EMAIL_DOMAINS": {
"description": "List of domains your service handles"
},
"EMAIL_PURGE_TIME": {
"description": "Time value for when to purge",
"value": 48
},
"EMAIL_PURGE_UNIT": {
"description": "Time unit for purging (minutes, hours, days)",
"value": "hours"
},
"EMAIL_PURGE_CONVERT": {
"description": "Convert to highest sensible unit and round",
"value": true
},
"EMAIL_EXAMPLE_ACCOUNT": {
"description": "Example email account to preserve",
"value": "example@48hr.email"
},
"EMAIL_EXAMPLE_UIDS": {
"description": "Example UIDs to preserve",
"value": [1, 2, 3]
},
"IMAP_USER": {
"description": "Username to login to the imap server"
},
"IMAP_PASSWORD": {
"description": "Password to login to the imap server"
},
"IMAP_SERVER": {
"description": "Hostname of the server (usually imap.example.com or mx.example.com)"
},
"IMAP_PORT": {
"description": "Port of the server (usually 993)",
"value": 993
},
"IMAP_TLS": {
"description": "Use tls or not",
"value": true
},
"IMAP_AUTH_TIMEOUT": {
"description": "Timeout for the auth in milliseconds",
"value": 3000
},
"IMAP_REFRESH_INTERVAL_SECONDS": {
"description": "How often to refresh the imap messages manually",
"value": 60
},
"IMAP_FETCH_CHUNK": {
"description": "Number of UIDs per fetch chunk during initial load",
"value": 200
},
"IMAP_CONCURRENCY": {
"description": "Number of concurrent fetch workers during initial load",
"value": 6
},
"HTTP_PORT": {
"description": "Port to listen on",
"value": 3000
},
"HTTP_BRANDING": {
"description": "The branding of the site",
"value": ["48hr.email", "Crazyco", "https://crazyco.xyz"]
},
"HTTP_DISPLAY_SORT": {
"description": "Domain display sorting: 0 = no change, 1 = alphabetical, 2 = alphabetical + first item shuffled, 3 = shuffle all",
"value": 2
},
"HTTP_HIDE_OTHER": {
"description": "Hide other emails from the list besides the first",
"value": false
}
}
}
}
}

View file

@ -1,5 +1,6 @@
// config.js
require("dotenv").config({ quiet: true });
const debug = require('debug')('48hr-email:config')
/**
* Safely parse a value from env.
@ -63,12 +64,17 @@ const config = {
};
// validation
debug('Validating configuration...')
if (!config.imap.user || !config.imap.password || !config.imap.host) {
debug('IMAP configuration validation failed: missing user, password, or host')
throw new Error("IMAP is not configured. Check IMAP_* env vars.");
}
if (!config.email.domains.length) {
debug('Email domains validation failed: no domains configured')
throw new Error("No EMAIL_DOMAINS configured.");
}
debug(`Configuration validated successfully: ${config.email.domains.length} domains, IMAP host: ${config.imap.host}`)
module.exports = config;

View file

@ -1,5 +1,6 @@
const config = require('./config')
const moment = require('moment')
const debug = require('debug')('48hr-email:helper')
class Helper {
@ -8,9 +9,11 @@ class Helper {
* @returns {Date}
*/
purgeTimeStamp() {
return moment()
const cutoff = moment()
.subtract(config.email.purgeTime.time, config.email.purgeTime.unit)
.toDate()
debug(`Purge cutoff calculated: ${cutoff} (${config.email.purgeTime.time} ${config.email.purgeTime.unit} ago)`)
return cutoff
}
/**
@ -25,10 +28,12 @@ class Helper {
const nowMs = now instanceof Date ? now.getTime() : now;
const pastMs = past instanceof Date ? past.getTime() : new Date(past).getTime();
return (nowMs - pastMs) >= DAY_IN_MS;
const diffMs = nowMs - pastMs;
const result = diffMs >= DAY_IN_MS;
debug(`Time difference check: ${diffMs}ms >= ${DAY_IN_MS}ms = ${result}`)
return result;
}
/**
* Convert time to highest possible unit (minutes hours days),
* rounding if necessary and prefixing "~" when rounded.
@ -76,9 +81,8 @@ class Helper {
}
const footer = `<label title="${Tooltip}">
<h4 style="display: inline;"><u><i>${time}</i></u></h4>
</Label>`
<h4 style="display: inline;"><u><i>${time}</i></u></h4>
</Label>`
return footer
}
@ -87,7 +91,6 @@ class Helper {
* @param {Array} array
* @returns {Array}
*/
shuffleArray(array) {
for (let i = array.length - 1; i >= 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
@ -101,7 +104,6 @@ class Helper {
* @param {Array} array
* @returns {Array}
*/
shuffleFirstItem(array) {
let first = array[Math.floor(Math.random() * array.length)]
array = array.filter((value) => value != first);
@ -126,17 +128,26 @@ class Helper {
* Get a domain list from config for use
* @returns {Array}
*/
getDomains() {
debug(`Getting domains with displaySort: ${config.http.displaySort}`)
let result;
switch (config.http.displaySort) {
case 0:
return this.hideOther(config.email.domains) // No modification
result = this.hideOther(config.email.domains) // No modification
debug(`Domain sort 0: no modification, ${result.length} domains`)
return result
case 1:
return this.hideOther(config.email.domains.sort()) // Sort alphabetically
result = this.hideOther(config.email.domains.sort()) // Sort alphabetically
debug(`Domain sort 1: alphabetical sort, ${result.length} domains`)
return result
case 2:
return this.hideOther(this.shuffleFirstItem(config.email.domains.sort())) // Sort alphabetically and shuffle first item
result = this.hideOther(this.shuffleFirstItem(config.email.domains.sort())) // Sort alphabetically and shuffle first item
debug(`Domain sort 2: alphabetical + shuffle first, ${result.length} domains`)
return result
case 3:
return this.hideOther(this.shuffleArray(config.email.domains)) // Shuffle all
result = this.hideOther(this.shuffleArray(config.email.domains)) // Shuffle all
debug(`Domain sort 3: shuffle all, ${result.length} domains`)
return result
}
}
}

View file

@ -4,7 +4,7 @@ const { simpleParser } = require('mailparser')
const addressparser = require('nodemailer/lib/addressparser')
const pSeries = require('p-series')
const retry = require('async-retry')
const debug = require('debug')('48hr-email:imap')
const debug = require('debug')('48hr-email:imap-manager')
const _ = require('lodash')
const moment = require('moment')
const Mail = require('../domain/mail')
@ -133,7 +133,7 @@ class ImapService extends EventEmitter {
})
await this.connection.openBox('INBOX')
debug('Connected to imap')
debug('Connected to imap Server at ' + this.config.imap.host)
}, {
retries: 5
}
@ -173,8 +173,14 @@ class ImapService extends EventEmitter {
debug('Load skipped: another load already in progress')
return
}
this.loadingInProgress = true
debug('Starting load of mail summaries')
if (this.initialLoadDone) {
debug('Updating mail summaries from server...')
} else {
debug('Fetching mail summaries from server...')
}
const uids = await this._getAllUids()
const newUids = uids.filter(uid => !this.loadedUids.has(uid))
debug(`UIDs on server: ${uids.length}, new UIDs to fetch: ${newUids.length}, already loaded: ${this.loadedUids.size}`)
@ -219,7 +225,7 @@ class ImapService extends EventEmitter {
}
this.loadingInProgress = false
debug('Load finished')
debug('Finished updating mail summary list')
}
/**

View file

@ -1,5 +1,5 @@
const EventEmitter = require('events')
const debug = require('debug')('48hr-email:imap-manager')
const debug = require('debug')('48hr-email:imap-processor')
const mem = require('mem')
const ImapService = require('./imap-service')
const Helper = require('./helper')
@ -33,29 +33,36 @@ class MailProcessingService extends EventEmitter {
}
getMailSummaries(address) {
debug('Getting mail summaries for', address)
return this.mailRepository.getForRecipient(address)
}
deleteSpecificEmail(adress, uid) {
debug('Deleting specific email', adress, uid)
if (this.mailRepository.removeUid(uid, adress) == true) {
this.imapService.deleteSpecificEmail(uid)
}
}
getOneFullMail(address, uid, raw = false) {
debug('Cache lookup for', address + ':' + uid, raw ? '(raw)' : '(parsed)')
return this.cachedFetchFullMail(address, uid, raw)
}
getAllMailSummaries() {
debug('Getting all mail summaries')
return this.mailRepository.getAll()
}
getCount() {
return this.mailRepository.mailCount()
const count = this.mailRepository.mailCount()
debug('Mail count requested:', count)
return count
}
onInitialLoadDone() {
this.initialLoadDone = true
debug('Initial load completed, total mails:', this.mailRepository.mailCount())
console.log(`Initial load done, got ${this.mailRepository.mailCount()} mails`)
console.log(`Fetching and deleting mails every ${this.config.imap.refreshIntervalSeconds} seconds`)
console.log(`Mails older than ${this.config.email.purgeTime.time} ${this.config.email.purgeTime.unit} will be deleted`)
@ -69,7 +76,9 @@ class MailProcessingService extends EventEmitter {
}
mail.to.forEach(to => {
debug('Adding mail to repository for recipient:', to)
this.mailRepository.add(to, mail)
debug('Emitting notification for:', to)
return this.clientNotification.emit(to)
})
}
@ -81,8 +90,11 @@ class MailProcessingService extends EventEmitter {
async _deleteOldMails() {
try {
debug('Starting deletion of old mails')
await this.imapService.deleteOldMails(helper.purgeTimeStamp())
debug('Completed deletion of old mails')
} catch (error) {
debug('Error deleting old messages:', error.message)
console.log('Cant delete old messages', error)
}
}

View file

@ -4,26 +4,38 @@ const router = new express.Router()
const config = require('../../../application/config')
const Helper = require('../../../application/helper')
const helper = new(Helper)
const debug = require('debug')('48hr-email:routes')
const purgeTime = helper.purgeTimeElemetBuilder()
router.get('/:address/:errorCode', async(req, res) => {
const mailProcessingService = req.app.get('mailProcessingService')
const count = await mailProcessingService.getCount()
router.get('/:address/:errorCode', async(req, res, next) => {
try {
const mailProcessingService = req.app.get('mailProcessingService')
if (!mailProcessingService) {
throw new Error('Mail processing service not available')
}
debug(`Error page requested: ${req.params.errorCode} for ${req.params.address}`)
const count = await mailProcessingService.getCount()
const errorCode = parseInt(req.params.errorCode) || 404
const message = req.query.message || (req.session && req.session.errorMessage) || 'An error occurred'
const errorCode = parseInt(req.params.errorCode) || 404
const message = req.query.message || (req.session && req.session.errorMessage) || 'An error occurred'
res.status(errorCode)
res.render('error', {
title: `${config.http.branding[0]} | ${errorCode}`,
purgeTime: purgeTime,
address: req.params.address,
count: count,
message: message,
status: errorCode,
branding: config.http.branding
})
debug(`Rendering error page ${errorCode} with message: ${message}`)
res.status(errorCode)
res.render('error', {
title: `${config.http.branding[0]} | ${errorCode}`,
purgeTime: purgeTime,
address: req.params.address,
count: count,
message: message,
status: errorCode,
branding: config.http.branding
})
} catch (error) {
debug('Error loading error page:', error.message)
console.error('Error while loading error page', error)
// For error pages, we should still try to render something basic
res.status(500).send('Internal Server Error')
}
})
module.exports = router
module.exports = router

View file

@ -1,6 +1,7 @@
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')
@ -16,17 +17,28 @@ const sanitizeAddress = param('address').customSanitizer(
}
)
router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, async(req, res, _next) => {
const mailProcessingService = req.app.get('mailProcessingService')
const count = await mailProcessingService.getCount()
res.render('inbox', {
title: `${config.http.branding[0]} | ` + req.params.address,
purgeTime: purgeTime,
address: req.params.address,
count: count,
mailSummaries: mailProcessingService.getMailSummaries(req.params.address),
branding: config.http.branding,
})
router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, 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 count = await mailProcessingService.getCount()
debug(`Rendering inbox with ${count} total mails`)
res.render('inbox', {
title: `${config.http.branding[0]} | ` + req.params.address,
purgeTime: purgeTime,
address: req.params.address,
count: count,
mailSummaries: mailProcessingService.getMailSummaries(req.params.address),
branding: config.http.branding,
})
} catch (error) {
debug(`Error loading inbox for ${req.params.address}:`, error.message)
console.error('Error while loading inbox', error)
next(error)
}
})
router.get(
@ -35,6 +47,7 @@ router.get(
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 mail = await mailProcessingService.getOneFullMail(
req.params.address,
@ -48,6 +61,7 @@ router.get(
// Emails are immutable, cache if found
res.set('Cache-Control', 'private, max-age=600')
debug(`Rendering email view for UID ${req.params.uid}`)
res.render('mail', {
title: mail.subject + " | " + req.params.address,
purgeTime: purgeTime,
@ -58,10 +72,12 @@ router.get(
branding: config.http.branding,
})
} 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)
}
@ -75,12 +91,15 @@ router.get(
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)
for (mail in mailSummaries) {
await mailProcessingService.deleteSpecificEmail(req.params.address, mailSummaries[mail].uid)
}
debug(`Deleted all 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)
}
@ -95,9 +114,12 @@ router.get(
async(req, res, next) => {
try {
const mailProcessingService = req.app.get('mailProcessingService')
debug(`Deleting email ${req.params.uid} for ${req.params.address}`)
await mailProcessingService.deleteSpecificEmail(req.params.address, req.params.uid)
debug(`Successfully deleted email ${req.params.uid} for ${req.params.address}`)
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)
}
@ -110,11 +132,13 @@ router.get(
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`)
}
@ -125,6 +149,7 @@ router.get(
)
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`)
}
@ -134,20 +159,24 @@ router.get(
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)
}
@ -162,11 +191,13 @@ router.get(
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()
// 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`)
}
@ -180,15 +211,18 @@ router.get(
mail = mail.replace(/(?:\r\n|\r|\n)/g, '<br>')
// 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
})
} 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)
}
@ -206,4 +240,4 @@ router.get(
)
module.exports = router
module.exports = router

View file

@ -1,6 +1,7 @@
const express = require('express')
const router = new express.Router()
const { check, validationResult } = require('express-validator')
const debug = require('debug')('48hr-email:routes')
const randomWord = require('random-word')
const config = require('../../../application/config')
@ -9,21 +10,36 @@ const helper = new(Helper)
const purgeTime = helper.purgeTimeElemetBuilder()
router.get('/', async(req, res, _next) => {
const count = await req.app.get('mailProcessingService').getCount()
res.render('login', {
title: `${config.http.branding[0]} | Your temporary Inbox`,
username: randomWord(),
purgeTime: purgeTime,
domains: helper.getDomains(),
count: count,
branding: config.http.branding,
example: config.email.examples.account,
})
router.get('/', async(req, res, next) => {
try {
const mailProcessingService = req.app.get('mailProcessingService')
if (!mailProcessingService) {
throw new Error('Mail processing service not available')
}
debug('Login page requested')
const count = await mailProcessingService.getCount()
debug(`Rendering login page with ${count} total mails`)
res.render('login', {
title: `${config.http.branding[0]} | Your temporary Inbox`,
username: randomWord(),
purgeTime: purgeTime,
domains: helper.getDomains(),
count: count,
branding: config.http.branding,
example: config.email.examples.account,
})
} catch (error) {
debug('Error loading login page:', error.message)
console.error('Error while loading login page', error)
next(error)
}
})
router.get('/inbox/random', (req, res, _next) => {
res.redirect(`/inbox/${randomWord()}@${config.email.domains[Math.floor(Math.random() * config.email.domains.length)]}`)
const randomDomain = config.email.domains[Math.floor(Math.random() * config.email.domains.length)]
const inbox = `${randomWord()}@${randomDomain}`
debug(`Generated random inbox: ${inbox}`)
res.redirect(`/inbox/${inbox}`)
})
router.get('/logout', (req, res, _next) => {
@ -40,22 +56,35 @@ router.post(
check('username').isLength({ min: 1 }),
check('domain').isIn(config.email.domains)
],
async(req, res) => {
const errors = validationResult(req)
const count = await req.app.get('mailProcessingService').getCount()
if (!errors.isEmpty()) {
return res.render('login', {
userInputError: true,
title: `${config.http.branding[0]} | Your temporary Inbox`,
purgeTime: purgeTime,
username: randomWord(),
domains: helper.getDomains(),
count: count,
branding: config.http.branding,
})
}
async(req, res, next) => {
try {
const mailProcessingService = req.app.get('mailProcessingService')
if (!mailProcessingService) {
throw new Error('Mail processing service not available')
}
const errors = validationResult(req)
const count = await mailProcessingService.getCount()
if (!errors.isEmpty()) {
debug(`Login validation failed for ${req.body.username}@${req.body.domain}: ${errors.array().map(e => e.msg).join(', ')}`)
return res.render('login', {
userInputError: true,
title: `${config.http.branding[0]} | Your temporary Inbox`,
purgeTime: purgeTime,
username: randomWord(),
domains: helper.getDomains(),
count: count,
branding: config.http.branding,
})
}
res.redirect(`/inbox/${req.body.username}@${req.body.domain}`)
const inbox = `${req.body.username}@${req.body.domain}`
debug(`Login successful, redirecting to inbox: ${inbox}`)
res.redirect(`/inbox/${inbox}`)
} catch (error) {
debug('Error processing login:', error.message)
console.error('Error while processing login', error)
next(error)
}
}
)

View file

@ -4,8 +4,7 @@
<title>{{ title }}</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimal-ui">
<meta name="darkreader" content="stfu">
<meta name="darkreader-lock">
<meta name="description" content="Dont give shady companies your real email. Use 48hr.email to protect your privacy!">
<meta property="og:image" content="/images/logo.png">

View file

@ -3,8 +3,7 @@
<title>{{ title }}</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimal-ui">
<meta name="darkreader" content="stfu">
<meta name="darkreader-lock">
<meta name="description" content="Dont give shady companies your real email. Use 48hr.email to protect your privacy!">
<meta property="og:image" content="/images/logo.png">

View file

@ -19,6 +19,13 @@ 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)
console.error(`Error in ${context}`, error)
next(error)
}
// Init express middleware
const app = express()
app.use(helmet())
@ -34,7 +41,7 @@ app.use(express.urlencoded({ extended: false }))
// Session middleware
app.use(session({
secret: '1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ', // They will hate me for this
secret: '1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ', // They will hate me for this, its temporary tho, I swear!
resave: false,
saveUninitialized: true,
cookie: { maxAge: 1000 * 60 * 60 * 24 } // 24 hours
@ -82,22 +89,29 @@ app.use((req, res, next) => {
// Error handler
app.use(async(err, req, res, _next) => {
const mailProcessingService = req.app.get('mailProcessingService')
const count = await mailProcessingService.getCount()
try {
debug('Error handler triggered:', err.message)
const mailProcessingService = req.app.get('mailProcessingService')
const count = await mailProcessingService.getCount()
// Set locals, only providing error in development
res.locals.message = err.message
res.locals.error = req.app.get('env') === 'development' ? err : {}
// 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', {
purgeTime: purgeTime,
address: req.params.address,
count: count,
branding: config.http.branding
})
// Render the error page
res.status(err.status || 500)
res.render('error', {
purgeTime: purgeTime,
address: req.params && req.params.address,
count: count,
branding: config.http.branding
})
} 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')
}
})
/**
@ -116,4 +130,4 @@ server.on('listening', () => {
debug('Listening on ' + bind)
})
module.exports = { app, io, server }
module.exports = { app, io, server }

17364
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,85 +1,83 @@
{
"name": "48hr.email",
"version": "1.6.2",
"private": false,
"description": "48hr.email is your favorite open-source tempmail client. ",
"keywords": [
"tempmail",
"48hr.email",
"disposable-email"
],
"homepage": "https://48hr.email/",
"bugs": {
"url": "https://github.com/Crazyco-xyz/48hr.email/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Crazyco-xyz/48hr.email.git"
},
"license": "GPL-3.0",
"author": "ClaraCrazy",
"type": "commonjs",
"main": "app.js",
"scripts": {
"start": "node --trace-warnings ./app.js",
"test": "xo",
"debug": "node --nolazy --inspect-brk=9229 ./app.js"
},
"dependencies": {
"array.prototype.flatmap": "^1.3.2",
"async-retry": "^1.3.3",
"compression": "^1.7.4",
"debug": "^2.6.9",
"dotenv": "^17.2.3",
"encodings": "^1.0.0",
"express": "^4.21.1",
"express-session": "^1.18.2",
"express-validator": "^7.2.0",
"helmet": "^3.23.3",
"http-errors": "~1.6.2",
"imap-simple": "^1.6.3",
"lodash": "^4.17.21",
"mailparser": "^3.7.1",
"mem": "^4.3.0",
"mnemonist": "^0.27.2",
"moment": "^2.30.1",
"morgan": "^1.10.1",
"nodemailer": "^7.0.11",
"p-series": "^2.1.0",
"random-word": "^2.0.0",
"sanitize-html": "^2.13.0",
"semver": "^7.6.3",
"socket.io": "^4.8.0",
"twig": "^0.10.3"
},
"devDependencies": {
"xo": "^0.59.3"
},
"engines": {
"node": "22.x"
},
"xo": {
"semicolon": false,
"prettier": true,
"rules": {
"no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_"
}
]
"name": "48hr.email",
"version": "1.6.3",
"private": false,
"description": "48hr.email is your favorite open-source tempmail client. ",
"keywords": [
"tempmail",
"48hr.email",
"disposable-email"
],
"homepage": "https://48hr.email/",
"bugs": {
"url": "https://github.com/Crazyco-xyz/48hr.email/issues"
},
"overrides": [
{
"files": "public/javascripts/*.js",
"esnext": false,
"env": [
"browser"
],
"globals": [
"io"
]
}
]
}
}
"repository": {
"type": "git",
"url": "git+https://github.com/Crazyco-xyz/48hr.email.git"
},
"license": "GPL-3.0",
"author": "ClaraCrazy",
"type": "commonjs",
"main": "app.js",
"scripts": {
"start": "node --trace-warnings ./app.js",
"debug": "DEBUG=48hr-email:* node --nolazy --inspect-brk=9229 --trace-warnings ./app.js",
"test": "xo"
},
"dependencies": {
"array.prototype.flatmap": "^1.3.2",
"async-retry": "^1.3.3",
"compression": "^1.7.4",
"debug": "^4.4.3",
"dotenv": "^17.2.3",
"encodings": "^1.0.0",
"express": "^4.21.1",
"express-session": "^1.18.2",
"express-validator": "^7.2.0",
"helmet": "^3.23.3",
"http-errors": "~1.6.2",
"imap-simple": "^1.6.3",
"lodash": "^4.17.21",
"mailparser": "^3.7.1",
"mem": "^4.3.0",
"mnemonist": "^0.27.2",
"moment": "^2.30.1",
"morgan": "^1.10.1",
"nodemailer": "^7.0.11",
"p-series": "^2.1.0",
"random-word": "^2.0.0",
"sanitize-html": "^2.13.0",
"semver": "^7.6.3",
"socket.io": "^4.8.0",
"twig": "^0.10.3"
},
"devDependencies": {
"xo": "^0.59.3"
},
"engines": {
"node": "22.x"
},
"xo": {
"semicolon": false,
"prettier": true,
"rules": {
"no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_"
}
]
},
"overrides": [{
"files": "public/javascripts/*.js",
"esnext": false,
"env": [
"browser"
],
"globals": [
"io"
]
}]
}
}