[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 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 --- # --- Purge configuration ---
EMAIL_PURGE_TIME=48 # Time value for when to purge EMAIL_PURGE_TIME=48 # Time value for when to purge
@ -24,7 +24,7 @@ IMAP_CONCURRENCY=6 # Number of conc
# --- HTTP / WEB CONFIGURATION --- # --- HTTP / WEB CONFIGURATION ---
HTTP_PORT=3000 # Port 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: HTTP_DISPLAY_SORT=2 # Domain display sorting:
# 0 = no change, # 0 = no change,
# 1 = alphabetical, # 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] github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: crazyco patreon: crazyco
open_collective: # Replace with a single Open Collective username 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 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 community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username 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 copilot-instructions.md
node_modules node_modules
db/* db/*
package-lock.json

View file

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

11
app.js
View file

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

151
app.json
View file

@ -1,71 +1,84 @@
{ {
"name": "48hr.email | Disposable email", "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.", "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", "repository": "https://github.com/Crazyco-xyz/48hr.email",
"logo": "https://github.com/Crazyco-xyz/48hr.email/blob/main/infrastructure/web/public/images/logo.png", "logo": "https://github.com/Crazyco-xyz/48hr.email/blob/main/infrastructure/web/public/images/logo.png",
"keywords": [ "keywords": [
"node", "node",
"disposable-mail" "disposable-mail"
], ],
"env": { "env": {
"EMAIL_DOMAINS": { "EMAIL_DOMAINS": {
"description": "Email domains" "description": "List of domains your service handles"
}, },
"EMAIL_PURGE_TIME": { "EMAIL_PURGE_TIME": {
"description": "Config for when to purge the emails", "description": "Time value for when to purge",
"value": { "value": 48
"time": 48, },
"unit": "hours", "EMAIL_PURGE_UNIT": {
"convert": true "description": "Time unit for purging (minutes, hours, days)",
} "value": "hours"
}, },
"EMAIL_EXAMPLES": { "EMAIL_PURGE_CONVERT": {
"description": "Examples of the domains", "description": "Convert to highest sensible unit and round",
"value": { "value": true
"account": "example@48hr.email", },
"uids": [1, 2, 3] "EMAIL_EXAMPLE_ACCOUNT": {
} "description": "Example email account to preserve",
}, "value": "example@48hr.email"
"IMAP_USER": { },
"description": "Username to login to the imap server" "EMAIL_EXAMPLE_UIDS": {
}, "description": "Example UIDs to preserve",
"IMAP_PASSWORD": { "value": [1, 2, 3]
"description": "Password to login to the imap server" },
}, "IMAP_USER": {
"IMAP_SERVER": { "description": "Username to login to the imap server"
"description": "Hostname of the server (usually imap.example.com or mx.example.com)" },
}, "IMAP_PASSWORD": {
"IMAP_PORT": { "description": "Password to login to the imap server"
"description": "Port of the server (usually 993)", },
"value": 993 "IMAP_SERVER": {
}, "description": "Hostname of the server (usually imap.example.com or mx.example.com)"
"IMAP_TLS": { },
"description": "Use tls or not", "IMAP_PORT": {
"value": true "description": "Port of the server (usually 993)",
}, "value": 993
"IMAP_AUTHTIMEOUT": { },
"description": "Timeout for the auth", "IMAP_TLS": {
"value": 3000 "description": "Use tls or not",
}, "value": true
"IMAP_REFRESH_INTERVAL_SECONDS": { },
"description": "How often to refresh the imap messages manually", "IMAP_AUTH_TIMEOUT": {
"value": 60 "description": "Timeout for the auth in milliseconds",
}, "value": 3000
"HTTP_PORT": { },
"description": "Port to listen on", "IMAP_REFRESH_INTERVAL_SECONDS": {
"value": 3000 "description": "How often to refresh the imap messages manually",
}, "value": 60
"HTTP_BRANDING": { },
"description": "The branding of the site", "IMAP_FETCH_CHUNK": {
"value": ["48hr.email", "Crazyco", "https://crazyco.xyz"] "description": "Number of UIDs per fetch chunk during initial load",
}, "value": 200
"HTTP_DISPLAY_SORT": { },
"description": "Sort the emails for use", "IMAP_CONCURRENCY": {
"value": 0 "description": "Number of concurrent fetch workers during initial load",
}, "value": 6
"HTTP_HIDE_OTHER": { },
"description": "Hide other emails from the list besides the first", "HTTP_PORT": {
"value": false "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 // config.js
require("dotenv").config({ quiet: true }); require("dotenv").config({ quiet: true });
const debug = require('debug')('48hr-email:config')
/** /**
* Safely parse a value from env. * Safely parse a value from env.
@ -63,12 +64,17 @@ const config = {
}; };
// validation // validation
debug('Validating configuration...')
if (!config.imap.user || !config.imap.password || !config.imap.host) { 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."); throw new Error("IMAP is not configured. Check IMAP_* env vars.");
} }
if (!config.email.domains.length) { if (!config.email.domains.length) {
debug('Email domains validation failed: no domains configured')
throw new Error("No EMAIL_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; module.exports = config;

View file

@ -1,5 +1,6 @@
const config = require('./config') const config = require('./config')
const moment = require('moment') const moment = require('moment')
const debug = require('debug')('48hr-email:helper')
class Helper { class Helper {
@ -8,9 +9,11 @@ class Helper {
* @returns {Date} * @returns {Date}
*/ */
purgeTimeStamp() { purgeTimeStamp() {
return moment() const cutoff = moment()
.subtract(config.email.purgeTime.time, config.email.purgeTime.unit) .subtract(config.email.purgeTime.time, config.email.purgeTime.unit)
.toDate() .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 nowMs = now instanceof Date ? now.getTime() : now;
const pastMs = past instanceof Date ? past.getTime() : new Date(past).getTime(); 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), * Convert time to highest possible unit (minutes hours days),
* rounding if necessary and prefixing "~" when rounded. * rounding if necessary and prefixing "~" when rounded.
@ -76,9 +81,8 @@ class Helper {
} }
const footer = `<label title="${Tooltip}"> const footer = `<label title="${Tooltip}">
<h4 style="display: inline;"><u><i>${time}</i></u></h4> <h4 style="display: inline;"><u><i>${time}</i></u></h4>
</Label>` </Label>`
return footer return footer
} }
@ -87,7 +91,6 @@ class Helper {
* @param {Array} array * @param {Array} array
* @returns {Array} * @returns {Array}
*/ */
shuffleArray(array) { shuffleArray(array) {
for (let i = array.length - 1; i >= 0; i--) { for (let i = array.length - 1; i >= 0; i--) {
const j = Math.floor(Math.random() * (i + 1)); const j = Math.floor(Math.random() * (i + 1));
@ -101,7 +104,6 @@ class Helper {
* @param {Array} array * @param {Array} array
* @returns {Array} * @returns {Array}
*/ */
shuffleFirstItem(array) { shuffleFirstItem(array) {
let first = array[Math.floor(Math.random() * array.length)] let first = array[Math.floor(Math.random() * array.length)]
array = array.filter((value) => value != first); array = array.filter((value) => value != first);
@ -126,17 +128,26 @@ class Helper {
* Get a domain list from config for use * Get a domain list from config for use
* @returns {Array} * @returns {Array}
*/ */
getDomains() { getDomains() {
debug(`Getting domains with displaySort: ${config.http.displaySort}`)
let result;
switch (config.http.displaySort) { switch (config.http.displaySort) {
case 0: 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: 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: 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: 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 addressparser = require('nodemailer/lib/addressparser')
const pSeries = require('p-series') const pSeries = require('p-series')
const retry = require('async-retry') const retry = require('async-retry')
const debug = require('debug')('48hr-email:imap') const debug = require('debug')('48hr-email:imap-manager')
const _ = require('lodash') const _ = require('lodash')
const moment = require('moment') const moment = require('moment')
const Mail = require('../domain/mail') const Mail = require('../domain/mail')
@ -133,7 +133,7 @@ class ImapService extends EventEmitter {
}) })
await this.connection.openBox('INBOX') await this.connection.openBox('INBOX')
debug('Connected to imap') debug('Connected to imap Server at ' + this.config.imap.host)
}, { }, {
retries: 5 retries: 5
} }
@ -173,8 +173,14 @@ class ImapService extends EventEmitter {
debug('Load skipped: another load already in progress') debug('Load skipped: another load already in progress')
return return
} }
this.loadingInProgress = true 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 uids = await this._getAllUids()
const newUids = uids.filter(uid => !this.loadedUids.has(uid)) 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}`) 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 this.loadingInProgress = false
debug('Load finished') debug('Finished updating mail summary list')
} }
/** /**

View file

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

View file

@ -4,26 +4,38 @@ const router = new express.Router()
const config = require('../../../application/config') const config = require('../../../application/config')
const Helper = require('../../../application/helper') const Helper = require('../../../application/helper')
const helper = new(Helper) const helper = new(Helper)
const debug = require('debug')('48hr-email:routes')
const purgeTime = helper.purgeTimeElemetBuilder() const purgeTime = helper.purgeTimeElemetBuilder()
router.get('/:address/:errorCode', async(req, res) => { router.get('/:address/:errorCode', async(req, res, next) => {
const mailProcessingService = req.app.get('mailProcessingService') try {
const count = await mailProcessingService.getCount() 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 debug(`Rendering error page ${errorCode} with message: ${message}`)
const message = req.query.message || (req.session && req.session.errorMessage) || 'An error occurred' res.status(errorCode)
res.render('error', {
res.status(errorCode) title: `${config.http.branding[0]} | ${errorCode}`,
res.render('error', { purgeTime: purgeTime,
title: `${config.http.branding[0]} | ${errorCode}`, address: req.params.address,
purgeTime: purgeTime, count: count,
address: req.params.address, message: message,
count: count, status: errorCode,
message: message, branding: config.http.branding
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 express = require('express')
const router = new express.Router() const router = new express.Router()
const { param } = require('express-validator') const { param } = require('express-validator')
const debug = require('debug')('48hr-email:routes')
const config = require('../../../application/config') const config = require('../../../application/config')
const Helper = require('../../../application/helper') const Helper = require('../../../application/helper')
@ -16,17 +17,28 @@ const sanitizeAddress = param('address').customSanitizer(
} }
) )
router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, async(req, res, _next) => { router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, async(req, res, next) => {
const mailProcessingService = req.app.get('mailProcessingService') try {
const count = await mailProcessingService.getCount() const mailProcessingService = req.app.get('mailProcessingService')
res.render('inbox', { if (!mailProcessingService) {
title: `${config.http.branding[0]} | ` + req.params.address, throw new Error('Mail processing service not available')
purgeTime: purgeTime, }
address: req.params.address, debug(`Inbox request for ${req.params.address}`)
count: count, const count = await mailProcessingService.getCount()
mailSummaries: mailProcessingService.getMailSummaries(req.params.address), debug(`Rendering inbox with ${count} total mails`)
branding: config.http.branding, 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( router.get(
@ -35,6 +47,7 @@ router.get(
async(req, res, next) => { async(req, res, next) => {
try { try {
const mailProcessingService = req.app.get('mailProcessingService') const mailProcessingService = req.app.get('mailProcessingService')
debug(`Viewing email ${req.params.uid} for ${req.params.address}`)
const count = await mailProcessingService.getCount() const count = await mailProcessingService.getCount()
const mail = await mailProcessingService.getOneFullMail( const mail = await mailProcessingService.getOneFullMail(
req.params.address, req.params.address,
@ -48,6 +61,7 @@ router.get(
// Emails are immutable, cache if found // Emails are immutable, cache if found
res.set('Cache-Control', 'private, max-age=600') res.set('Cache-Control', 'private, max-age=600')
debug(`Rendering email view for UID ${req.params.uid}`)
res.render('mail', { res.render('mail', {
title: mail.subject + " | " + req.params.address, title: mail.subject + " | " + req.params.address,
purgeTime: purgeTime, purgeTime: purgeTime,
@ -58,10 +72,12 @@ router.get(
branding: config.http.branding, branding: config.http.branding,
}) })
} else { } 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!' 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`) res.redirect(`/error/${req.params.address}/404`)
} }
} catch (error) { } catch (error) {
debug(`Error fetching email ${req.params.uid} for ${req.params.address}:`, error.message)
console.error('Error while fetching email', error) console.error('Error while fetching email', error)
next(error) next(error)
} }
@ -75,12 +91,15 @@ router.get(
async(req, res, next) => { async(req, res, next) => {
try { try {
const mailProcessingService = req.app.get('mailProcessingService') const mailProcessingService = req.app.get('mailProcessingService')
debug(`Deleting all emails for ${req.params.address}`)
const mailSummaries = await mailProcessingService.getMailSummaries(req.params.address) const mailSummaries = await mailProcessingService.getMailSummaries(req.params.address)
for (mail in mailSummaries) { for (mail in mailSummaries) {
await mailProcessingService.deleteSpecificEmail(req.params.address, mailSummaries[mail].uid) await mailProcessingService.deleteSpecificEmail(req.params.address, mailSummaries[mail].uid)
} }
debug(`Deleted all emails for ${req.params.address}`)
res.redirect(`/inbox/${req.params.address}`) res.redirect(`/inbox/${req.params.address}`)
} catch (error) { } catch (error) {
debug(`Error deleting all emails for ${req.params.address}:`, error.message)
console.error('Error while deleting email', error) console.error('Error while deleting email', error)
next(error) next(error)
} }
@ -95,9 +114,12 @@ router.get(
async(req, res, next) => { async(req, res, next) => {
try { try {
const mailProcessingService = req.app.get('mailProcessingService') 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) 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}`) res.redirect(`/inbox/${req.params.address}`)
} catch (error) { } catch (error) {
debug(`Error deleting email ${req.params.uid} for ${req.params.address}:`, error.message)
console.error('Error while deleting email', error) console.error('Error while deleting email', error)
next(error) next(error)
} }
@ -110,11 +132,13 @@ router.get(
async(req, res, next) => { async(req, res, next) => {
try { try {
const mailProcessingService = req.app.get('mailProcessingService') 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 uid = parseInt(req.params.uid, 10)
const count = await mailProcessingService.getCount() const count = await mailProcessingService.getCount()
// Validate UID is a valid integer // Validate UID is a valid integer
if (isNaN(uid) || uid <= 0) { if (isNaN(uid) || uid <= 0) {
debug(`Invalid UID provided: ${req.params.uid}`)
req.session.errorMessage = 'Invalid/Malformed UID provided.' req.session.errorMessage = 'Invalid/Malformed UID provided.'
return res.redirect(`/error/${req.params.address}/400`) return res.redirect(`/error/${req.params.address}/400`)
} }
@ -125,6 +149,7 @@ router.get(
) )
if (!mail || !mail.attachments) { 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!' 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`) return res.redirect(`/error/${req.params.address}/404`)
} }
@ -134,20 +159,24 @@ router.get(
if (attachment) { if (attachment) {
try { try {
debug(`Serving attachment: ${attachment.filename}`)
res.set('Content-Disposition', `attachment; filename=${attachment.filename}`); res.set('Content-Disposition', `attachment; filename=${attachment.filename}`);
res.set('Content-Type', attachment.contentType); res.set('Content-Type', attachment.contentType);
res.send(attachment.content); res.send(attachment.content);
return; return;
} catch (error) { } catch (error) {
debug(`Error serving attachment: ${error.message}`)
console.error('Error while fetching attachment', error); console.error('Error while fetching attachment', error);
next(error); next(error);
return; return;
} }
} else { } 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!' 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`) return res.redirect(`/error/${req.params.address}/404`)
} }
} catch (error) { } catch (error) {
debug(`Error fetching attachment: ${error.message}`)
console.error('Error while fetching attachment', error) console.error('Error while fetching attachment', error)
next(error) next(error)
} }
@ -162,11 +191,13 @@ router.get(
async(req, res, next) => { async(req, res, next) => {
try { try {
const mailProcessingService = req.app.get('mailProcessingService') 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 uid = parseInt(req.params.uid, 10)
const count = await mailProcessingService.getCount() const count = await mailProcessingService.getCount()
// Validate UID is a valid integer // Validate UID is a valid integer
if (isNaN(uid) || uid <= 0) { if (isNaN(uid) || uid <= 0) {
debug(`Invalid UID provided for raw view: ${req.params.uid}`)
req.session.errorMessage = 'Invalid/Malformed UID provided.' req.session.errorMessage = 'Invalid/Malformed UID provided.'
return res.redirect(`/error/${req.params.address}/400`) return res.redirect(`/error/${req.params.address}/400`)
} }
@ -180,15 +211,18 @@ router.get(
mail = mail.replace(/(?:\r\n|\r|\n)/g, '<br>') mail = mail.replace(/(?:\r\n|\r|\n)/g, '<br>')
// Emails are immutable, cache if found // Emails are immutable, cache if found
res.set('Cache-Control', 'private, max-age=600') res.set('Cache-Control', 'private, max-age=600')
debug(`Rendering raw email view for UID ${req.params.uid}`)
res.render('raw', { res.render('raw', {
title: req.params.uid + " | raw | " + req.params.address, title: req.params.uid + " | raw | " + req.params.address,
mail mail
}) })
} else { } 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!' 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`) res.redirect(`/error/${req.params.address}/404`)
} }
} catch (error) { } catch (error) {
debug(`Error fetching raw email ${req.params.uid}: ${error.message}`)
console.error('Error while fetching raw email', error) console.error('Error while fetching raw email', error)
next(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 express = require('express')
const router = new express.Router() const router = new express.Router()
const { check, validationResult } = require('express-validator') const { check, validationResult } = require('express-validator')
const debug = require('debug')('48hr-email:routes')
const randomWord = require('random-word') const randomWord = require('random-word')
const config = require('../../../application/config') const config = require('../../../application/config')
@ -9,21 +10,36 @@ const helper = new(Helper)
const purgeTime = helper.purgeTimeElemetBuilder() const purgeTime = helper.purgeTimeElemetBuilder()
router.get('/', async(req, res, _next) => { router.get('/', async(req, res, next) => {
const count = await req.app.get('mailProcessingService').getCount() try {
res.render('login', { const mailProcessingService = req.app.get('mailProcessingService')
title: `${config.http.branding[0]} | Your temporary Inbox`, if (!mailProcessingService) {
username: randomWord(), throw new Error('Mail processing service not available')
purgeTime: purgeTime, }
domains: helper.getDomains(), debug('Login page requested')
count: count, const count = await mailProcessingService.getCount()
branding: config.http.branding, debug(`Rendering login page with ${count} total mails`)
example: config.email.examples.account, 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) => { 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) => { router.get('/logout', (req, res, _next) => {
@ -40,22 +56,35 @@ router.post(
check('username').isLength({ min: 1 }), check('username').isLength({ min: 1 }),
check('domain').isIn(config.email.domains) check('domain').isIn(config.email.domains)
], ],
async(req, res) => { async(req, res, next) => {
const errors = validationResult(req) try {
const count = await req.app.get('mailProcessingService').getCount() const mailProcessingService = req.app.get('mailProcessingService')
if (!errors.isEmpty()) { if (!mailProcessingService) {
return res.render('login', { throw new Error('Mail processing service not available')
userInputError: true, }
title: `${config.http.branding[0]} | Your temporary Inbox`, const errors = validationResult(req)
purgeTime: purgeTime, const count = await mailProcessingService.getCount()
username: randomWord(), if (!errors.isEmpty()) {
domains: helper.getDomains(), debug(`Login validation failed for ${req.body.username}@${req.body.domain}: ${errors.array().map(e => e.msg).join(', ')}`)
count: count, return res.render('login', {
branding: config.http.branding, 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> <title>{{ title }}</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimal-ui"> <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 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"> <meta property="og:image" content="/images/logo.png">

View file

@ -3,8 +3,7 @@
<title>{{ title }}</title> <title>{{ title }}</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimal-ui"> <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 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"> <meta property="og:image" content="/images/logo.png">

View file

@ -19,6 +19,13 @@ const Helper = require('../../application/helper')
const helper = new(Helper) const helper = new(Helper)
const purgeTime = helper.purgeTimeElemetBuilder() 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 // Init express middleware
const app = express() const app = express()
app.use(helmet()) app.use(helmet())
@ -34,7 +41,7 @@ app.use(express.urlencoded({ extended: false }))
// Session middleware // Session middleware
app.use(session({ 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, resave: false,
saveUninitialized: true, saveUninitialized: true,
cookie: { maxAge: 1000 * 60 * 60 * 24 } // 24 hours cookie: { maxAge: 1000 * 60 * 60 * 24 } // 24 hours
@ -82,22 +89,29 @@ app.use((req, res, next) => {
// Error handler // Error handler
app.use(async(err, req, res, _next) => { app.use(async(err, req, res, _next) => {
const mailProcessingService = req.app.get('mailProcessingService') try {
const count = await mailProcessingService.getCount() debug('Error handler triggered:', err.message)
const mailProcessingService = req.app.get('mailProcessingService')
const count = await mailProcessingService.getCount()
// Set locals, only providing error in development // Set locals, only providing error in development
res.locals.message = err.message res.locals.message = err.message
res.locals.error = req.app.get('env') === 'development' ? err : {} res.locals.error = req.app.get('env') === 'development' ? err : {}
// Render the error page // Render the error page
res.status(err.status || 500) res.status(err.status || 500)
res.render('error', { res.render('error', {
purgeTime: purgeTime, purgeTime: purgeTime,
address: req.params.address, address: req.params && req.params.address,
count: count, count: count,
branding: config.http.branding 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) 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", "name": "48hr.email",
"version": "1.6.2", "version": "1.6.3",
"private": false, "private": false,
"description": "48hr.email is your favorite open-source tempmail client. ", "description": "48hr.email is your favorite open-source tempmail client. ",
"keywords": [ "keywords": [
"tempmail", "tempmail",
"48hr.email", "48hr.email",
"disposable-email" "disposable-email"
], ],
"homepage": "https://48hr.email/", "homepage": "https://48hr.email/",
"bugs": { "bugs": {
"url": "https://github.com/Crazyco-xyz/48hr.email/issues" "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": "^_"
}
]
}, },
"overrides": [ "repository": {
{ "type": "git",
"files": "public/javascripts/*.js", "url": "git+https://github.com/Crazyco-xyz/48hr.email.git"
"esnext": false, },
"env": [ "license": "GPL-3.0",
"browser" "author": "ClaraCrazy",
], "type": "commonjs",
"globals": [ "main": "app.js",
"io" "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"
]
}]
}
}