Compare commits

..

No commits in common. "1f45db188650b65cf041e2317b37d609d43a5846" and "63b30a37054fa324c9359602a457322293ae268d" have entirely different histories.

22 changed files with 8991 additions and 9404 deletions

View file

@ -1,5 +1,5 @@
# --- EMAIL CONFIGURATION ---
EMAIL_DOMAINS=["example.com","example.net"] # List of domains your service handles ['example.com', 'example.net']
EMAIL_DOMAINS=["example.com","example.net"] # List of domains your service handles (list)
# --- 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: crazyco
ko_fi: # Replace with a single Ko-fi username
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

BIN
.github/assets/raw.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

1
.gitignore vendored
View file

@ -5,3 +5,4 @@
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 prod
ExecStart=npm run start
Restart=on-failure
TimeoutStartSec=0
@ -113,13 +113,13 @@ WantedBy=multi-user.target
### Screenshots:
- #### Inbox:
<img align="center" src=".github/assets/inbox.png">
<img align="center" src="https://i.imgur.com/JJmSe7S.png">
- #### Email using HTML and CSS:
<img align="center" src=".github/assets/html.png">
- #### Email with CSS:
<img align="center" src="https://i.imgur.com/x8OBoI7.png">
- #### Email without CSS:
<img align="center" src=".github/assets/raw.png">
<img align="center" src="https://i.imgur.com/VPZ8IG6.png">
<br><br>

11
app.js
View file

@ -3,7 +3,6 @@
/* 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()
@ -15,50 +14,40 @@ 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)
})

149
app.json
View file

@ -1,84 +1,71 @@
{
"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
}
"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
}
}
}

View file

@ -1,6 +1,5 @@
// config.js
require("dotenv").config({ quiet: true });
const debug = require('debug')('48hr-email:config')
/**
* Safely parse a value from env.
@ -64,17 +63,12 @@ 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,6 +1,5 @@
const config = require('./config')
const moment = require('moment')
const debug = require('debug')('48hr-email:helper')
class Helper {
@ -9,11 +8,9 @@ class Helper {
* @returns {Date}
*/
purgeTimeStamp() {
const cutoff = moment()
return 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
}
/**
@ -28,12 +25,10 @@ class Helper {
const nowMs = now instanceof Date ? now.getTime() : now;
const pastMs = past instanceof Date ? past.getTime() : new Date(past).getTime();
const diffMs = nowMs - pastMs;
const result = diffMs >= DAY_IN_MS;
debug(`Time difference check: ${diffMs}ms >= ${DAY_IN_MS}ms = ${result}`)
return result;
return (nowMs - pastMs) >= DAY_IN_MS;
}
/**
* Convert time to highest possible unit (minutes hours days),
* rounding if necessary and prefixing "~" when rounded.
@ -81,8 +76,9 @@ 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
}
@ -91,6 +87,7 @@ 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));
@ -104,6 +101,7 @@ class Helper {
* @param {Array} array
* @returns {Array}
*/
shuffleFirstItem(array) {
let first = array[Math.floor(Math.random() * array.length)]
array = array.filter((value) => value != first);
@ -128,26 +126,17 @@ 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:
result = this.hideOther(config.email.domains) // No modification
debug(`Domain sort 0: no modification, ${result.length} domains`)
return result
return this.hideOther(config.email.domains) // No modification
case 1:
result = this.hideOther(config.email.domains.sort()) // Sort alphabetically
debug(`Domain sort 1: alphabetical sort, ${result.length} domains`)
return result
return this.hideOther(config.email.domains.sort()) // Sort alphabetically
case 2:
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
return this.hideOther(this.shuffleFirstItem(config.email.domains.sort())) // Sort alphabetically and shuffle first item
case 3:
result = this.hideOther(this.shuffleArray(config.email.domains)) // Shuffle all
debug(`Domain sort 3: shuffle all, ${result.length} domains`)
return result
return this.hideOther(this.shuffleArray(config.email.domains)) // Shuffle all
}
}
}

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-manager')
const debug = require('debug')('48hr-email:imap')
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 Server at ' + this.config.imap.host)
debug('Connected to imap')
}, {
retries: 5
}
@ -173,14 +173,8 @@ class ImapService extends EventEmitter {
debug('Load skipped: another load already in progress')
return
}
this.loadingInProgress = true
if (this.initialLoadDone) {
debug('Updating mail summaries from server...')
} else {
debug('Fetching mail summaries from server...')
}
debug('Starting load of mail summaries')
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}`)
@ -225,7 +219,7 @@ class ImapService extends EventEmitter {
}
this.loadingInProgress = false
debug('Finished updating mail summary list')
debug('Load finished')
}
/**

View file

@ -1,5 +1,5 @@
const EventEmitter = require('events')
const debug = require('debug')('48hr-email:imap-processor')
const debug = require('debug')('48hr-email:imap-manager')
const mem = require('mem')
const ImapService = require('./imap-service')
const Helper = require('./helper')
@ -33,36 +33,29 @@ 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() {
const count = this.mailRepository.mailCount()
debug('Mail count requested:', count)
return count
return this.mailRepository.mailCount()
}
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`)
@ -76,9 +69,7 @@ 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)
})
}
@ -90,11 +81,8 @@ 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

@ -1,15 +1,11 @@
body {
margin: 0;
min-height: 100vh;
padding: 20px 20px 0;
display: flex;
padding: 10px 10px 0px;
flex-direction: column;
background: linear-gradient( 135deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.02)), #131516;
min-height: 100vh;
background-color: #131516;
color: #cccccc;
backdrop-filter: blur(18px) saturate(120%);
-webkit-backdrop-filter: blur(18px) saturate(120%);
border: 1px solid rgba(255, 255, 255, 0.12);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
body::-webkit-scrollbar {
@ -25,11 +21,6 @@ a {
color: #cccccc;
}
a:hover {
color: #9b4cda;
text-decoration: none;
}
h1 {
font-size: 3rem;
}
@ -52,14 +43,10 @@ p {
}
iframe {
background-color: #1D2021;
color: white;
width: 80%;
height: 60vh;
margin: 2rem auto;
display: block;
border: none;
border-radius: 30px;
border: 1px dotted black;
margin-left: 10%;
}
blockquote {
@ -84,33 +71,11 @@ text-muted {
.action-links {
float: right;
justify-content: flex-end;
/* aligns all links to the right */
flex-wrap: nowrap;
/* ensures they stay in one line */
text-align: right;
text-align: end;
}
.action-links a {
display: inline-block;
margin-bottom: 0.5rem;
padding: 10px 16px;
border-radius: 16px;
text-align: center;
background: rgba(155, 77, 202, 0.2);
border: 1px solid rgba(155, 77, 202, 0.35);
color: #fff;
text-decoration: none;
backdrop-filter: blur(12px) saturate(120%);
-webkit-backdrop-filter: blur(12px) saturate(120%);
box-shadow: 0 5px 15px rgba(155, 77, 202, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.08);
transition: transform 0.2s ease, background 0.2s ease;
}
.action-links a:hover {
background: rgba(155, 77, 202, 0.3);
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(155, 77, 202, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.12);
display: block;
}
@ -135,13 +100,6 @@ select:hover {
flex-direction: column;
max-width: 600px;
margin: auto;
background: linear-gradient( 135deg, rgba(155, 77, 202, 0.12), rgba(155, 77, 202, 0.04)), rgba(255, 255, 255, 0.04);
backdrop-filter: blur(20px) saturate(125%);
-webkit-backdrop-filter: blur(20px) saturate(125%);
border-radius: 22px;
border: 1px solid rgba(155, 77, 202, 0.25);
padding: 40px 36px;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(155, 77, 202, 0.08) inset;
}
#login h1,
@ -166,7 +124,7 @@ select:hover {
line-height: 1;
padding: 0 6px;
font-size: 1.4rem;
background: linear-gradient( 135deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.02)), #131516;
background-color: #131516;
z-index: 999;
}
@ -198,32 +156,16 @@ select:hover {
}
#login .buttons {
display: flex;
gap: 16px;
flex-wrap: wrap;
justify-content: flex-end;
/* keeps them aligned right like action-links */
margin-top: 1.5rem;
display: flex;
gap: 20px;
flex-wrap: wrap;
}
#login .buttons>* {
flex: 1 1 auto;
min-width: 120px;
border-radius: 20px;
color: #fff;
background: rgba(155, 77, 202, 0.2);
border: 1px solid rgba(155, 77, 202, 0.35);
backdrop-filter: blur(12px) saturate(120%);
-webkit-backdrop-filter: blur(12px) saturate(120%);
box-shadow: 0 8px 20px rgba(155, 77, 202, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.08);
cursor: pointer;
transition: transform 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
}
#login .buttons>*:hover {
background: rgba(155, 77, 202, 0.3);
transform: translateY(-2px);
box-shadow: 0 12px 25px rgba(155, 77, 202, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.12);
width: 100%;
flex: 1;
font-size: 1.3rem;
}
.mail_attachments {

View file

@ -4,38 +4,26 @@ 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, 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'
router.get('/:address/:errorCode', async(req, res) => {
const mailProcessingService = req.app.get('mailProcessingService')
const count = await mailProcessingService.getCount()
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')
}
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
})
})
module.exports = router

View file

@ -1,7 +1,6 @@
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')
@ -17,28 +16,17 @@ const sanitizeAddress = param('address').customSanitizer(
}
)
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('^/: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(
@ -47,7 +35,6 @@ 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,
@ -61,7 +48,6 @@ 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,
@ -72,12 +58,10 @@ 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)
}
@ -91,15 +75,12 @@ 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)
}
@ -114,12 +95,9 @@ 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)
}
@ -132,13 +110,11 @@ 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`)
}
@ -149,7 +125,6 @@ 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`)
}
@ -159,24 +134,20 @@ 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)
}
@ -191,13 +162,11 @@ 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`)
}
@ -211,18 +180,15 @@ 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)
}

View file

@ -1,7 +1,6 @@
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')
@ -10,36 +9,21 @@ const helper = new(Helper)
const purgeTime = helper.purgeTimeElemetBuilder()
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('/', 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('/inbox/random', (req, res, _next) => {
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}`)
res.redirect(`/inbox/${randomWord()}@${config.email.domains[Math.floor(Math.random() * config.email.domains.length)]}`)
})
router.get('/logout', (req, res, _next) => {
@ -56,35 +40,22 @@ router.post(
check('username').isLength({ min: 1 }),
check('domain').isIn(config.email.domains)
],
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,
})
}
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)
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,
})
}
res.redirect(`/inbox/${req.body.username}@${req.body.domain}`)
}
)

View file

@ -4,7 +4,8 @@
<title>{{ title }}</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimal-ui">
<meta name="darkreader-lock">
<meta name="darkreader" content="stfu">
<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,7 +3,8 @@
<title>{{ title }}</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimal-ui">
<meta name="darkreader-lock">
<meta name="darkreader" content="stfu">
<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,13 +19,6 @@ 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())
@ -41,7 +34,7 @@ app.use(express.urlencoded({ extended: false }))
// Session middleware
app.use(session({
secret: '1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ', // They will hate me for this, its temporary tho, I swear!
secret: '1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ', // They will hate me for this
resave: false,
saveUninitialized: true,
cookie: { maxAge: 1000 * 60 * 60 * 24 } // 24 hours
@ -89,29 +82,22 @@ app.use((req, res, next) => {
// Error handler
app.use(async(err, req, res, _next) => {
try {
debug('Error handler triggered:', err.message)
const mailProcessingService = req.app.get('mailProcessingService')
const count = await mailProcessingService.getCount()
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 && 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')
}
// Render the error page
res.status(err.status || 500)
res.render('error', {
purgeTime: purgeTime,
address: req.params.address,
count: count,
branding: config.http.branding
})
})
/**

17658
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.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"
"name": "48hr.email",
"version": "1.6.1",
"private": false,
"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-validator": "^7.2.0",
"helmet": "^3.23.3",
"http-errors": "~1.6.2",
"imap-simple": "^4.3.0",
"lodash": "^4.17.21",
"mailparser": "^3.7.1",
"mem": "^4.3.0",
"mnemonist": "^0.27.2",
"moment": "^2.30.1",
"morgan": "~1.9.0",
"nodemailer": "^6.9.15",
"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"
},
"xo": {
"semicolon": false,
"prettier": true,
"rules": {
"no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_"
}
]
},
"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.3",
"async-retry": "^1.3.3",
"compression": "^1.8.1",
"debug": "^4.4.3",
"dotenv": "^17.2.3",
"encodings": "^1.0.0",
"express": "^4.22.1",
"express-session": "^1.18.2",
"express-validator": "^7.3.1",
"helmet": "^3.23.3",
"http-errors": "~1.6.2",
"imap-simple": "^1.6.3",
"lodash": "^4.17.21",
"mailparser": "^3.9.1",
"mem": "^4.3.0",
"mnemonist": "^0.27.2",
"moment": "^2.30.1",
"morgan": "^1.10.1",
"nodemailer": "^7.0.12",
"p-series": "^2.1.0",
"random-word": "^2.0.0",
"sanitize-html": "^2.17.0",
"semver": "^7.7.3",
"socket.io": "^4.8.3",
"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"
]
}
"overrides": [
{
"files": "public/javascripts/*.js",
"esnext": false,
"env": [
"browser"
],
"globals": [
"io"
]
}
}
]
},
"engines": {
"node": "22.x"
},
"description": "48hr.email is your favorite open-source tempmail client. ",
"main": "app.js",
"repository": {
"type": "git",
"url": "git+https://github.com/Crazyco-xyz/48hr.email.git"
},
"keywords": [
"tempmail",
"48hr.email",
"disposable-email"
],
"author": "ClaraCrazy",
"license": "GPL-3.0",
"bugs": {
"url": "https://github.com/Crazyco-xyz/48hr.email/issues"
},
"homepage": "https://48hr.email/"
}