mirror of
https://github.com/Crazyco-xyz/48hr.email.git
synced 2026-01-08 10:49:35 +01:00
[Feat]: Update Config structure, add more feature flags, fix 302s
This is what makes a project a "Clara project"... going the extra mile for customizability <3
This commit is contained in:
parent
d454f91912
commit
345935f8b9
22 changed files with 435 additions and 97 deletions
|
|
@ -27,13 +27,14 @@ IMAP_CONCURRENCY=6 # Number of conc
|
|||
SMTP_ENABLED=false # Enable SMTP forwarding functionality (default: false)
|
||||
SMTP_USER="noreply@48hr.email" # SMTP authentication username (also used as from address)
|
||||
SMTP_PASSWORD="password" # SMTP authentication password
|
||||
SMTP_SERVER="smtp.example.com" # SMTP server address (e.g., smtp.gmail.com, smtp.sendgrid.net)
|
||||
SMTP_SERVER="smtp.example.com" # SMTP server address (e.g., smtp.gmail.com, smtp.sendgrid.net)
|
||||
SMTP_PORT=465 # SMTP port (587 for TLS, 465 for SSL, 25 for unencrypted)
|
||||
SMTP_SECURE=true # Use SSL (true for port 465, false for other ports)
|
||||
|
||||
# --- HTTP / WEB CONFIGURATION ---
|
||||
HTTP_PORT=3000 # Port
|
||||
HTTP_BASE_URL="http://localhost:3000" # Base URL for verification links (e.g., https://48hr.email)
|
||||
HTTP_SESSION_SECRET="change-this-secret-in-production" # Secret for session encryption (used for auth & locking)
|
||||
HTTP_BRANDING=["48hr.email","CrazyCo","https://crazyco.xyz"] # ['service_title', 'company_name', 'company_url']
|
||||
HTTP_DISPLAY_SORT=2 # Domain display sorting:
|
||||
# 0 = no change,
|
||||
|
|
@ -42,12 +43,11 @@ HTTP_DISPLAY_SORT=2 # Domain display
|
|||
# 3 = shuffle all
|
||||
HTTP_HIDE_OTHER=false # true = only show first domain, false = show all
|
||||
HTTP_STATISTICS_ENABLED=false # Enable statistics page at /stats (true/false)
|
||||
HTTP_SHOW_INFO_SECTION=true # Show info section on homepage (true/false)
|
||||
|
||||
# --- USER AUTHENTICATION & INBOX LOCKING ---
|
||||
USER_AUTH_ENABLED=false # Enable user registration/login system (default: false)
|
||||
USER_SESSION_SECRET="change-this-secret-in-production" # Secret for session encryption (used for auth & locking)
|
||||
USER_DATABASE_PATH="./db/data.db" # Path to application database (users, forwarding, locks)
|
||||
USER_MAX_FORWARD_EMAILS=5 # Maximum verified forwarding emails per user
|
||||
USER_MAX_LOCKED_INBOXES=5 # Maximum locked inboxes per user
|
||||
LOCK_RELEASE_HOURS=168 # Auto-release locked inboxes after X hours without login (default: 168 = 7 days)
|
||||
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,4 +1,5 @@
|
|||
.env
|
||||
.env.backup
|
||||
.idea
|
||||
.vscode
|
||||
.DS_Store
|
||||
|
|
|
|||
17
app.js
17
app.js
|
|
@ -2,6 +2,22 @@
|
|||
|
||||
/* eslint unicorn/no-process-exit: 0 */
|
||||
|
||||
// Check .env file permissions before loading config
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const envPath = path.resolve('.env')
|
||||
if (fs.existsSync(envPath)) {
|
||||
const mode = fs.statSync(envPath).mode
|
||||
const perms = (mode & parseInt('777', 8)).toString(8)
|
||||
const groupReadable = parseInt(perms[1], 10) >= 4
|
||||
const otherReadable = parseInt(perms[2], 10) >= 4
|
||||
if (groupReadable || otherReadable) {
|
||||
console.error(`\nSECURITY ERROR: .env file has insecure permissions (${perms})`)
|
||||
console.error(`Run: chmod 600 ${envPath}\n`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
const config = require('./application/config')
|
||||
const debug = require('debug')('48hr-email:app')
|
||||
const Helper = require('./application/helper')
|
||||
|
|
@ -90,6 +106,7 @@ if (config.user.authEnabled) {
|
|||
|
||||
const imapService = new ImapService(config, inboxLock)
|
||||
debug('IMAP service initialized')
|
||||
app.set('imapService', imapService)
|
||||
|
||||
const mailProcessingService = new MailProcessingService(
|
||||
new MailRepository(),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,13 @@
|
|||
require("dotenv").config({ quiet: true });
|
||||
const debug = require('debug')('48hr-email:config')
|
||||
|
||||
// Migration helper: warn about deprecated env vars
|
||||
if (process.env.USER_SESSION_SECRET && !process.env.HTTP_SESSION_SECRET) {
|
||||
console.warn('\nDEPRECATION WARNING: USER_SESSION_SECRET is deprecated.')
|
||||
console.warn(' Please rename it to HTTP_SESSION_SECRET in your .env file.')
|
||||
console.warn(' The old name still works but will be removed in a future version.\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely parse a value from env.
|
||||
* Returns `undefined` if the value is missing or invalid.
|
||||
|
|
@ -31,7 +38,7 @@ function parseBool(v) {
|
|||
|
||||
const config = {
|
||||
email: {
|
||||
domains: parseValue(process.env.EMAIL_DOMAINS),
|
||||
domains: parseValue(process.env.EMAIL_DOMAINS) || [],
|
||||
purgeTime: {
|
||||
time: Number(process.env.EMAIL_PURGE_TIME),
|
||||
unit: parseValue(process.env.EMAIL_PURGE_UNIT),
|
||||
|
|
@ -41,7 +48,16 @@ const config = {
|
|||
account: parseValue(process.env.EMAIL_EXAMPLE_ACCOUNT),
|
||||
uids: parseValue(process.env.EMAIL_EXAMPLE_UIDS)
|
||||
},
|
||||
blacklistedSenders: parseValue(process.env.EMAIL_BLACKLISTED_SENDERS) || []
|
||||
blacklistedSenders: parseValue(process.env.EMAIL_BLACKLISTED_SENDERS) || [],
|
||||
features: {
|
||||
imap: {
|
||||
enabled: true, // IMAP is always required
|
||||
refreshIntervalSeconds: Number(process.env.IMAP_REFRESH_INTERVAL_SECONDS),
|
||||
fetchChunkSize: Number(process.env.IMAP_FETCH_CHUNK) || 100,
|
||||
fetchConcurrency: Number(process.env.IMAP_CONCURRENCY) || 6
|
||||
},
|
||||
smtp: parseBool(process.env.SMTP_ENABLED) || false
|
||||
}
|
||||
},
|
||||
|
||||
imap: {
|
||||
|
|
@ -66,12 +82,19 @@ const config = {
|
|||
},
|
||||
|
||||
http: {
|
||||
// Server settings
|
||||
port: Number(process.env.HTTP_PORT),
|
||||
baseUrl: parseValue(process.env.HTTP_BASE_URL) || 'http://localhost:3000',
|
||||
branding: parseValue(process.env.HTTP_BRANDING),
|
||||
displaySort: Number(process.env.HTTP_DISPLAY_SORT),
|
||||
hideOther: parseBool(process.env.HTTP_HIDE_OTHER),
|
||||
statisticsEnabled: parseBool(process.env.HTTP_STATISTICS_ENABLED) || false
|
||||
sessionSecret: parseValue(process.env.HTTP_SESSION_SECRET) || parseValue(process.env.USER_SESSION_SECRET) || 'change-me-in-production',
|
||||
|
||||
// UI Features & Display
|
||||
features: {
|
||||
branding: parseValue(process.env.HTTP_BRANDING),
|
||||
displaySort: Number(process.env.HTTP_DISPLAY_SORT) || 0,
|
||||
hideOther: parseBool(process.env.HTTP_HIDE_OTHER),
|
||||
statistics: parseBool(process.env.HTTP_STATISTICS_ENABLED) || false,
|
||||
infoSection: parseBool(process.env.HTTP_SHOW_INFO_SECTION) !== false // default true
|
||||
}
|
||||
},
|
||||
|
||||
user: {
|
||||
|
|
@ -81,9 +104,6 @@ const config = {
|
|||
// Database
|
||||
databasePath: parseValue(process.env.USER_DATABASE_PATH) || './db/data.db',
|
||||
|
||||
// Session & Auth
|
||||
sessionSecret: parseValue(process.env.USER_SESSION_SECRET) || 'change-me-in-production',
|
||||
|
||||
// Feature Limits
|
||||
maxForwardEmails: Number(process.env.USER_MAX_FORWARD_EMAILS) || 5,
|
||||
maxLockedInboxes: Number(process.env.USER_MAX_LOCKED_INBOXES) || 5,
|
||||
|
|
|
|||
|
|
@ -109,11 +109,10 @@ class Helper {
|
|||
/**
|
||||
* Build a mail count html element with tooltip for the footer
|
||||
* @param {number} count - Current mail count
|
||||
* @param {number} largestUid - Largest UID from IMAP
|
||||
* @returns {String}
|
||||
*/
|
||||
mailCountBuilder(count) {
|
||||
const imapService = require('./imap-service')
|
||||
const largestUid = imapService.getLargestUid ? imapService.getLargestUid() : null
|
||||
mailCountBuilder(count, largestUid = null) {
|
||||
let tooltip = ''
|
||||
|
||||
if (largestUid && largestUid > 0) {
|
||||
|
|
@ -156,10 +155,10 @@ class Helper {
|
|||
* @returns {Array}
|
||||
*/
|
||||
hideOther(array) {
|
||||
if (config.http.hideOther) {
|
||||
return array[0]
|
||||
if (config.http.features.hideOther) {
|
||||
return array && array.length > 0 ? [array[0]] : []
|
||||
} else {
|
||||
return array
|
||||
return array || []
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -168,9 +167,13 @@ class Helper {
|
|||
* @returns {Array}
|
||||
*/
|
||||
getDomains() {
|
||||
debug(`Getting domains with displaySort: ${config.http.displaySort}`)
|
||||
debug(`Getting domains with displaySort: ${config.http.features.displaySort}`)
|
||||
if (!config.email.domains || !Array.isArray(config.email.domains) || !config.email.domains.length) {
|
||||
debug('ERROR: config.email.domains is not a valid array')
|
||||
return []
|
||||
}
|
||||
let result;
|
||||
switch (config.http.displaySort) {
|
||||
switch (config.http.features.displaySort) {
|
||||
case 0:
|
||||
result = this.hideOther(config.email.domains) // No modification
|
||||
debug(`Domain sort 0: no modification, ${result.length} domains`)
|
||||
|
|
@ -187,6 +190,9 @@ class Helper {
|
|||
result = this.hideOther(this.shuffleArray(config.email.domains)) // Shuffle all
|
||||
debug(`Domain sort 3: shuffle all, ${result.length} domains`)
|
||||
return result
|
||||
default:
|
||||
debug(`Unknown displaySort value: ${config.http.features.displaySort}, using case 0`)
|
||||
return this.hideOther(config.email.domains)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -212,7 +218,7 @@ class Helper {
|
|||
* @returns {string} - HMAC signature (hex)
|
||||
*/
|
||||
signCookie(email) {
|
||||
const secret = config.user.sessionSecret
|
||||
const secret = config.http.sessionSecret
|
||||
const hmac = crypto.createHmac('sha256', secret)
|
||||
hmac.update(email.toLowerCase())
|
||||
const signature = hmac.digest('hex')
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@ class MailProcessingService extends EventEmitter {
|
|||
|
||||
// Forward via SMTP service
|
||||
debug(`Forwarding email to ${destinationEmail}`)
|
||||
const branding = this.config.http.branding[0] || '48hr.email'
|
||||
const branding = this.config.http.features.branding[0] || '48hr.email'
|
||||
const result = await this.smtpService.forwardMail(fullMail, destinationEmail, branding)
|
||||
|
||||
if (result.success) {
|
||||
|
|
@ -354,7 +354,7 @@ class MailProcessingService extends EventEmitter {
|
|||
|
||||
// Send verification email
|
||||
const baseUrl = this.config.http.baseUrl
|
||||
const branding = this.config.http.branding[0] || '48hr.email'
|
||||
const branding = this.config.http.features.branding[0] || '48hr.email'
|
||||
|
||||
debug(`Sending verification email to ${destinationEmail} for source ${sourceAddress}`)
|
||||
const result = await this.smtpService.sendVerificationEmail(
|
||||
|
|
|
|||
|
|
@ -2887,7 +2887,7 @@ body.light-mode .theme-icon-light {
|
|||
}
|
||||
.action-dropdown .dropdown-menu a {
|
||||
padding: 10px 0;
|
||||
text-align: left;
|
||||
text-align: right;
|
||||
border: none !important;
|
||||
}
|
||||
.action-dropdown .dropdown-menu a:hover {
|
||||
|
|
@ -2924,7 +2924,7 @@ body.light-mode .theme-icon-light {
|
|||
.action-links.mobile-open>button:not(.hamburger-menu) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
text-align: right;
|
||||
padding: 10px 0;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ router.get('/account', requireAuth, async(req, res) => {
|
|||
forwardEmails,
|
||||
lockedInboxes,
|
||||
stats,
|
||||
branding: config.http.branding,
|
||||
branding: config.http.features.branding || ['48hr.email', 'Service', 'https://example.com'],
|
||||
purgeTime: purgeTime,
|
||||
successMessage: req.session.accountSuccess,
|
||||
errorMessage: req.session.accountError
|
||||
|
|
@ -94,7 +94,7 @@ router.post('/account/forward-email/add',
|
|||
|
||||
// Send verification email
|
||||
const baseUrl = config.http.baseUrl
|
||||
const branding = config.http.branding[0]
|
||||
const branding = (config.http.features.branding || ['48hr.email'])[0]
|
||||
|
||||
await smtpService.sendVerificationEmail(
|
||||
email,
|
||||
|
|
|
|||
|
|
@ -79,6 +79,20 @@ const loginRateLimiter = (req, res, next) => {
|
|||
next()
|
||||
}
|
||||
|
||||
// Middleware to capture redirect URL
|
||||
router.use((req, res, next) => {
|
||||
if (req.method === 'GET' && req.path === '/auth') {
|
||||
const referer = req.get('Referer')
|
||||
const redirectParam = req.query.redirect
|
||||
const redirectUrl = redirectParam || (referer && !referer.includes('/auth') ? referer : null)
|
||||
|
||||
if (redirectUrl) {
|
||||
req.session.redirectAfterLogin = redirectUrl
|
||||
}
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
// GET /auth - Show unified auth page (login or register)
|
||||
router.get('/auth', redirectIfAuthenticated, (req, res) => {
|
||||
const config = req.app.get('config')
|
||||
|
|
@ -90,8 +104,8 @@ router.get('/auth', redirectIfAuthenticated, (req, res) => {
|
|||
delete req.session.successMessage
|
||||
|
||||
res.render('auth', {
|
||||
title: `Login or Register | ${config.http.branding[0]}`,
|
||||
branding: config.http.branding,
|
||||
title: `Login or Register | ${(config.http.features.branding || ['48hr.email'])[0]}`,
|
||||
branding: config.http.features.branding || ['48hr.email', 'Service', 'https://example.com'],
|
||||
purgeTime: purgeTime,
|
||||
errorMessage,
|
||||
successMessage
|
||||
|
|
|
|||
|
|
@ -19,14 +19,15 @@ router.get('/:address/:errorCode', async(req, res, next) => {
|
|||
const message = req.query.message || (req.session && req.session.errorMessage) || 'An error occurred'
|
||||
|
||||
debug(`Rendering error page ${errorCode} with message: ${message}`)
|
||||
const branding = config.http.features.branding || ['48hr.email', 'Service', 'https://example.com']
|
||||
res.status(errorCode)
|
||||
res.render('error', {
|
||||
title: `${config.http.branding[0]} | ${errorCode}`,
|
||||
title: `${branding[0]} | ${errorCode}`,
|
||||
purgeTime: purgeTime,
|
||||
address: req.params.address,
|
||||
message: message,
|
||||
status: errorCode,
|
||||
branding: config.http.branding
|
||||
branding: branding
|
||||
})
|
||||
} catch (error) {
|
||||
debug('Error loading error page:', error.message)
|
||||
|
|
@ -36,4 +37,4 @@ router.get('/:address/:errorCode', async(req, res, next) => {
|
|||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
module.exports = router
|
||||
|
|
|
|||
|
|
@ -144,12 +144,13 @@ router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, validateDomain, optiona
|
|||
const verificationEmail = req.query.email || ''
|
||||
|
||||
res.render('inbox', {
|
||||
title: `${config.http.branding[0]} | ` + req.params.address,
|
||||
title: `${(config.http.features.branding || ['48hr.email'])[0]} | ` + req.params.address,
|
||||
purgeTime: purgeTime,
|
||||
address: req.params.address,
|
||||
mailSummaries: mailProcessingService.getMailSummaries(req.params.address),
|
||||
branding: config.http.branding,
|
||||
authEnabled: config.user.authEnabled,
|
||||
smtpEnabled: config.email.features.smtp,
|
||||
isAuthenticated: req.session && req.session.userId ? true : false,
|
||||
userForwardEmails: userForwardEmails,
|
||||
isLocked: isLocked,
|
||||
|
|
@ -240,8 +241,9 @@ router.get(
|
|||
mail,
|
||||
cryptoAttachments: cryptoAttachments,
|
||||
uid: req.params.uid,
|
||||
branding: config.http.branding,
|
||||
branding: config.http.features.branding || ['48hr.email', 'Service', 'https://example.com'],
|
||||
authEnabled: config.user.authEnabled,
|
||||
smtpEnabled: config.email.features.smtp,
|
||||
isAuthenticated: req.session && req.session.userId ? true : false,
|
||||
userForwardEmails: userForwardEmails,
|
||||
isLocked: isLocked,
|
||||
|
|
@ -440,10 +442,21 @@ router.get(
|
|||
}
|
||||
)
|
||||
|
||||
// Middleware to check if SMTP is enabled
|
||||
const smtpEnabled = (req, res, next) => {
|
||||
if (!config.email.features.smtp) {
|
||||
debug('SMTP forwarding is disabled')
|
||||
req.session.errorMessage = 'Email forwarding is currently disabled.'
|
||||
return res.redirect(`/inbox/${req.params.address}` + (req.params.uid ? `/${req.params.uid}` : ''))
|
||||
}
|
||||
next()
|
||||
}
|
||||
|
||||
// POST route for forwarding a single email (requires authentication)
|
||||
router.post(
|
||||
'^/:address/:uid/forward',
|
||||
requireAuth,
|
||||
smtpEnabled,
|
||||
forwardLimiter,
|
||||
validateDomain,
|
||||
checkLockAccess,
|
||||
|
|
@ -503,6 +516,7 @@ router.post(
|
|||
router.post(
|
||||
'^/:address/forward-all',
|
||||
requireAuth,
|
||||
smtpEnabled,
|
||||
forwardLimiter,
|
||||
validateDomain,
|
||||
checkLockAccess,
|
||||
|
|
|
|||
|
|
@ -2,13 +2,9 @@ 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')
|
||||
const Helper = require('../../../application/helper')
|
||||
const helper = new(Helper)
|
||||
|
||||
const purgeTime = helper.purgeTimeElemetBuilder()
|
||||
const templateContext = require('../template-context')
|
||||
|
||||
router.get('/', async(req, res, next) => {
|
||||
try {
|
||||
|
|
@ -17,14 +13,12 @@ router.get('/', async(req, res, next) => {
|
|||
throw new Error('Mail processing service not available')
|
||||
}
|
||||
debug('Login page requested')
|
||||
const context = templateContext.build(req, {
|
||||
username: randomWord()
|
||||
})
|
||||
res.render('login', {
|
||||
title: `${config.http.branding[0]} | Your temporary Inbox`,
|
||||
username: randomWord(),
|
||||
purgeTime: purgeTime,
|
||||
purgeTimeRaw: config.email.purgeTime,
|
||||
domains: helper.getDomains(),
|
||||
branding: config.http.branding,
|
||||
example: config.email.examples.account,
|
||||
...context,
|
||||
title: `${context.branding[0]} | Your temporary Inbox`
|
||||
})
|
||||
} catch (error) {
|
||||
debug('Error loading login page:', error.message)
|
||||
|
|
@ -56,14 +50,13 @@ router.post(
|
|||
const errors = validationResult(req)
|
||||
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', {
|
||||
const context = templateContext.build(req, {
|
||||
userInputError: true,
|
||||
title: `${config.http.branding[0]} | Your temporary Inbox`,
|
||||
purgeTime: purgeTime,
|
||||
purgeTimeRaw: config.email.purgeTime,
|
||||
username: randomWord(),
|
||||
domains: helper.getDomains(),
|
||||
branding: config.http.branding,
|
||||
username: randomWord()
|
||||
})
|
||||
return res.render('login', {
|
||||
...context,
|
||||
title: `${context.branding[0]} | Your temporary Inbox`
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,8 +8,12 @@ router.get('/', async(req, res) => {
|
|||
const config = req.app.get('config')
|
||||
|
||||
// Check if statistics are enabled
|
||||
if (!config.http.statisticsEnabled) {
|
||||
return res.status(404).send('Statistics are disabled')
|
||||
if (!config.http.features.statistics) {
|
||||
req.session.alertMessage = 'Statistics are disabled'
|
||||
const referer = req.get('Referer')
|
||||
// Don't redirect to /stats itself to avoid infinite loops
|
||||
const redirectUrl = (referer && !referer.includes('/stats')) ? referer : '/'
|
||||
return res.redirect(redirectUrl)
|
||||
}
|
||||
|
||||
const statisticsStore = req.app.get('statisticsStore')
|
||||
|
|
@ -33,12 +37,13 @@ router.get('/', async(req, res) => {
|
|||
|
||||
const stats = statisticsStore.getEnhancedStats()
|
||||
const purgeTime = helper.purgeTimeElemetBuilder()
|
||||
const branding = config.http.features.branding || ['48hr.email', 'Service', 'https://example.com']
|
||||
|
||||
debug(`Stats page requested: ${stats.currentCount} current, ${stats.allTimeTotal} all-time total, ${stats.historical.length} historical points`)
|
||||
|
||||
res.render('stats', {
|
||||
title: `Statistics | ${config.http.branding[0]}`,
|
||||
branding: config.http.branding,
|
||||
title: `Statistics | ${branding[0]}`,
|
||||
branding: branding,
|
||||
purgeTime: purgeTime,
|
||||
stats: stats,
|
||||
authEnabled: config.user.authEnabled,
|
||||
|
|
|
|||
57
infrastructure/web/template-context.js
Normal file
57
infrastructure/web/template-context.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
const config = require('../../application/config')
|
||||
const Helper = require('../../application/helper')
|
||||
|
||||
/**
|
||||
* Template Context Builder
|
||||
* Generates common variables for all template renders
|
||||
*/
|
||||
class TemplateContext {
|
||||
constructor() {
|
||||
this.helper = new Helper()
|
||||
this.purgeTime = this.helper.purgeTimeElemetBuilder()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get base context that should be available in all templates
|
||||
* @param {Object} req - Express request object
|
||||
* @returns {Object} Base template context
|
||||
*/
|
||||
getBaseContext(req) {
|
||||
return {
|
||||
// Config values
|
||||
config: config,
|
||||
branding: config.http.features.branding || ['48hr.email', 'Service', 'https://example.com'],
|
||||
purgeTime: this.purgeTime,
|
||||
purgeTimeRaw: config.email.purgeTime,
|
||||
|
||||
// Feature flags
|
||||
authEnabled: config.user.authEnabled,
|
||||
statisticsEnabled: config.http.features.statistics,
|
||||
smtpEnabled: config.email.features.smtp,
|
||||
showInfoSection: config.http.features.infoSection,
|
||||
|
||||
// User session
|
||||
currentUser: req.session && req.session.username ? req.session.username : null,
|
||||
|
||||
// Common data
|
||||
domains: this.helper.getDomains(),
|
||||
example: config.email.examples.account
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge base context with page-specific data
|
||||
* @param {Object} req - Express request object
|
||||
* @param {Object} pageData - Page-specific template data
|
||||
* @returns {Object} Complete template context
|
||||
*/
|
||||
build(req, pageData = {}) {
|
||||
return {
|
||||
...this.getBaseContext(req),
|
||||
...pageData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
module.exports = new TemplateContext()
|
||||
|
|
@ -4,22 +4,22 @@
|
|||
<div class="action-links">
|
||||
{% if currentUser %}
|
||||
<!-- Inbox Dropdown (multiple actions when logged in) -->
|
||||
<div class="action-dropdown">
|
||||
<button class="dropdown-toggle" aria-label="Inbox actions">Inbox ▾</button>
|
||||
<div class="dropdown-menu" data-section-title="Inbox Actions">
|
||||
<a href="#" id="forwardAllBtn" aria-label="Forward all emails">Forward All</a>
|
||||
{% if authEnabled %}
|
||||
{% if isLocked and hasAccess %}
|
||||
<a href="#" id="removeLockBtn" aria-label="Remove lock">Remove Lock</a>
|
||||
{% elseif not isLocked %}
|
||||
<a href="#" id="lockBtn" aria-label="Lock inbox to your account">Lock Inbox</a>
|
||||
<div class="action-dropdown">
|
||||
<button class="dropdown-toggle" aria-label="Inbox actions">Inbox ▾</button>
|
||||
<div class="dropdown-menu" data-section-title="Inbox Actions">
|
||||
{% if smtpEnabled %}
|
||||
<a href="#" id="forwardAllBtn" aria-label="Forward all emails">Forward All</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<a href="/inbox/{{ address }}/delete-all" aria-label="Delete all emails">Wipe Inbox</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Account Dropdown (logged in) -->
|
||||
{% if authEnabled %}
|
||||
{% if isLocked and hasAccess %}
|
||||
<a href="#" id="removeLockBtn" aria-label="Remove lock">Remove Lock</a>
|
||||
{% elseif not isLocked %}
|
||||
<a href="#" id="lockBtn" aria-label="Lock inbox to your account">Lock Inbox</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<a href="/inbox/{{ address }}/delete-all" aria-label="Delete all emails">Wipe Inbox</a>
|
||||
</div>
|
||||
</div> <!-- Account Dropdown (logged in) -->
|
||||
{% if authEnabled %}
|
||||
<div class="action-dropdown">
|
||||
<button class="dropdown-toggle" aria-label="Account menu">Account ▾</button>
|
||||
|
|
@ -178,6 +178,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Forward All Modal -->
|
||||
{% if smtpEnabled %}
|
||||
<div id="forwardAllModal" class="modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<span class="close" id="closeForwardAll">×</span>
|
||||
|
|
@ -216,4 +217,5 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -88,13 +88,20 @@
|
|||
</a>
|
||||
{% block header %}{% endblock %}
|
||||
</div>
|
||||
{% if alertMessage %}
|
||||
<div class="container" style="margin-top: 1rem;">
|
||||
<div class="alert alert-warning" style="font-size: 0.85rem;">
|
||||
{{ alertMessage }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% block body %}{% endblock %}
|
||||
</main>
|
||||
|
||||
{% block footer %}
|
||||
<section class="container footer">
|
||||
<hr>
|
||||
{% if config.http.statisticsEnabled %}
|
||||
{% if config.http.features.statistics %}
|
||||
<h4>{{ branding[0] }} offered by <a href="{{ branding[2] }}" style="text-decoration:underline" target="_blank">{{ branding[1] }}</a> | All Emails will be deleted after {{ purgeTime | raw }} | Check out our public <a href="/stats" style="text-decoration:underline">Statistics</a></h4>
|
||||
{% else %}
|
||||
<h4>{{ branding[0] }} offered by <a href="{{ branding[2] }}" style="text-decoration:underline" target="_blank">{{ branding[1] }}</a> | All Emails will be deleted after {{ purgeTime | raw }} | Currently handling {{ mailCount | raw }}</h4>
|
||||
|
|
|
|||
|
|
@ -89,22 +89,27 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<div class="features-grid">
|
||||
<div class="feature-card frosted-glass">
|
||||
<h3>Privacy First</h3>
|
||||
<p>No tracking, no bullshit. Your temporary email is completely anonymous.</p>
|
||||
</div>
|
||||
<div class="feature-card frosted-glass">
|
||||
<h3>Instant Access</h3>
|
||||
<p>Create unlimited temporary email addresses in seconds. No waiting, no verification.</p>
|
||||
</div>
|
||||
<div class="feature-card frosted-glass">
|
||||
<h3>Auto-Delete</h3>
|
||||
<p>All emails automatically purge after {{ purgeTimeRaw|readablePurgeTime }}. Clean and secure by default.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if showInfoSection %}
|
||||
<div class="info-section">
|
||||
<div class="features-grid">
|
||||
<div class="feature-card frosted-glass">
|
||||
<h3>Privacy First</h3>
|
||||
{% if statisticsEnabled %}
|
||||
<p>No tracking, no personal data collection. We only store email metadata for system analytics.</p>
|
||||
{% else %}
|
||||
<p>No tracking, no data collection. Your temporary email is completely anonymous.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="feature-card frosted-glass">
|
||||
<h3>Instant Access</h3>
|
||||
<p>Create unlimited temporary email addresses in seconds. No waiting, no verification.</p>
|
||||
</div>
|
||||
<div class="feature-card frosted-glass">
|
||||
<h3>Auto-Delete</h3>
|
||||
<p>All emails automatically purge after {{ purgeTimeRaw|readablePurgeTime }}. Clean and secure by default.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-content frosted-glass">
|
||||
<h2>What is a Temporary Email?</h2>
|
||||
<p>A temporary email (also known as disposable email or throwaway email) is a service that allows you to receive emails at a temporary address that self-destructs after a certain time. It's perfect for signing up to websites, testing services, or protecting your real inbox from spam.</p>
|
||||
|
|
@ -129,8 +134,9 @@
|
|||
<li><strong>{{ purgeTimeRaw|readablePurgeTime|title }} Retention:</strong> Emails stay accessible for the full duration before auto-deletion</li>
|
||||
</ul>
|
||||
|
||||
<p class="note">For extended features like email forwarding and inbox locking, you can optionally create a free account. But for basic temporary email needs, no registration is ever required.</p>
|
||||
<p class="note">{% if smtpEnabled or authEnabled %}For extended features like{% if smtpEnabled %} email forwarding{% endif %}{% if smtpEnabled and authEnabled %} and{% endif %}{% if authEnabled %} inbox locking{% endif %}, you can optionally create a free account. {% endif %}For basic temporary email needs, no registration is ever required.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@
|
|||
<div class="action-dropdown">
|
||||
<button class="dropdown-toggle" aria-label="Email actions">Email ▾</button>
|
||||
<div class="dropdown-menu" data-section-title="Email Actions">
|
||||
<a href="#" id="forwardBtn" aria-label="Forward this email">Forward</a>
|
||||
{% if smtpEnabled %}
|
||||
<a href="#" id="forwardBtn" aria-label="Forward this email">Forward</a>
|
||||
{% endif %}
|
||||
<a href="/inbox/{{ address }}/{{ uid }}/delete" aria-label="Delete this email">Delete</a>
|
||||
<a href="/inbox/{{ address }}/{{ uid }}/raw" target="_blank" aria-label="View raw email">View Raw</a>
|
||||
</div>
|
||||
|
|
@ -127,6 +129,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Forward Email Modal -->
|
||||
{% if smtpEnabled %}
|
||||
<div id="forwardModal" class="modal" style="display: none;">
|
||||
<div class="modal-content frosted-glass">
|
||||
<span class="close" id="closeForward">×</span>
|
||||
|
|
@ -165,6 +168,7 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,11 +45,11 @@ app.use(express.json())
|
|||
app.use(express.urlencoded({ extended: false }))
|
||||
|
||||
// Cookie parser for signed cookies (email verification)
|
||||
app.use(cookieParser(config.user.sessionSecret))
|
||||
app.use(cookieParser(config.http.sessionSecret))
|
||||
|
||||
// Session support (always enabled for forward verification and inbox locking)
|
||||
app.use(session({
|
||||
secret: config.user.sessionSecret,
|
||||
secret: config.http.sessionSecret,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: { maxAge: 24 * 60 * 60 * 1000 } // 24 hours
|
||||
|
|
@ -97,6 +97,13 @@ app.use((req, res, next) => {
|
|||
res.locals.authEnabled = config.user.authEnabled
|
||||
res.locals.config = config
|
||||
res.locals.currentUser = null
|
||||
res.locals.alertMessage = req.session ? req.session.alertMessage : null
|
||||
|
||||
// Clear alert after reading
|
||||
if (req.session && req.session.alertMessage) {
|
||||
delete req.session.alertMessage
|
||||
}
|
||||
|
||||
if (req.session && req.session.userId && req.session.username && req.session.isAuthenticated) {
|
||||
res.locals.currentUser = {
|
||||
id: req.session.userId,
|
||||
|
|
@ -107,14 +114,25 @@ app.use((req, res, next) => {
|
|||
})
|
||||
|
||||
// Middleware to expose mail count to all templates
|
||||
app.use((req, res, next) => {
|
||||
app.use(async(req, res, next) => {
|
||||
const mailProcessingService = req.app.get('mailProcessingService')
|
||||
const imapService = req.app.get('imapService')
|
||||
const Helper = require('../../application/helper')
|
||||
const helper = new Helper()
|
||||
|
||||
if (mailProcessingService) {
|
||||
const count = mailProcessingService.getCount()
|
||||
res.locals.mailCount = helper.mailCountBuilder(count)
|
||||
let largestUid = null
|
||||
|
||||
if (imapService) {
|
||||
try {
|
||||
largestUid = await imapService.getLargestUid()
|
||||
} catch (e) {
|
||||
debug('Error getting largest UID:', e.message)
|
||||
}
|
||||
}
|
||||
|
||||
res.locals.mailCount = helper.mailCountBuilder(count, largestUid)
|
||||
} else {
|
||||
res.locals.mailCount = ''
|
||||
}
|
||||
|
|
@ -159,7 +177,7 @@ app.use(async(err, req, res, _next) => {
|
|||
res.render('error', {
|
||||
purgeTime: purgeTime,
|
||||
address: req.params && req.params.address,
|
||||
branding: config.http.branding
|
||||
branding: config.http.features.branding || ['48hr.email', 'Service', 'https://example.com']
|
||||
})
|
||||
} catch (renderError) {
|
||||
debug('Error in error handler:', renderError.message)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "48hr.email",
|
||||
"version": "2.1.0",
|
||||
"version": "2.2.0",
|
||||
"private": false,
|
||||
"description": "48hr.email is your favorite open-source tempmail client.",
|
||||
"keywords": [
|
||||
|
|
@ -27,7 +27,8 @@
|
|||
"scripts": {
|
||||
"start": "node --trace-warnings ./app.js",
|
||||
"debug": "DEBUG=48hr-email:* node --nolazy --inspect-brk=9229 --trace-warnings ./app.js",
|
||||
"test": "xo"
|
||||
"test": "xo",
|
||||
"env:check": "node scripts/check-env.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"async-retry": "^1.3.3",
|
||||
|
|
|
|||
22
scripts/check-domains.js
Normal file
22
scripts/check-domains.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// Test script to verify domains are loaded correctly
|
||||
const helper = new Helper()
|
||||
const domains = helper.getDomains()
|
||||
console.log('\nDomains from helper.getDomains():', domains)
|
||||
console.log('Length:', domains ? domains.length : undefined)
|
||||
console.log('Type:', typeof config.email.domains)
|
||||
console.log('Is Array:', Array.isArray(config.email.domains))
|
||||
console.log('Length:', config.email.domains ? config.email.domains.length : undefined)
|
||||
|
||||
if (Array.isArray(config.email.domains) && config.email.domains.length > 0) {
|
||||
console.log('\nDomains list:')
|
||||
config.email.domains.forEach((d, i) => console.log(` ${i + 1}. ${d}`))
|
||||
} else {
|
||||
console.log('\nERROR: No domains configured!')
|
||||
}
|
||||
|
||||
console.log('\nHTTP Config:', JSON.stringify(config.http, null, 2))
|
||||
console.log('\nDomains from helper.getDomains():', domains)
|
||||
console.log('Length:', domains ? domains.length : undefined)
|
||||
process.exit(0)
|
||||
150
scripts/check-env.js
Normal file
150
scripts/check-env.js
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Environment Configuration Checker
|
||||
* Ensures .env has all required variables from .env.example
|
||||
* Adds missing variables with empty values at the correct position
|
||||
*/
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const ENV_PATH = path.resolve('.env')
|
||||
const EXAMPLE_PATH = path.resolve('.env.example')
|
||||
const BACKUP_PATH = path.resolve('.env.backup')
|
||||
|
||||
console.log('48hr.email Environment Configuration Checker\n')
|
||||
|
||||
// Check if .env.example exists
|
||||
if (!fs.existsSync(EXAMPLE_PATH)) {
|
||||
console.error('ERROR: .env.example not found!')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Create .env if it doesn't exist
|
||||
if (!fs.existsSync(ENV_PATH)) {
|
||||
console.log('INFO: .env not found, creating from .env.example...')
|
||||
fs.copyFileSync(EXAMPLE_PATH, ENV_PATH)
|
||||
console.log('SUCCESS: Created .env - please fill in your configuration values\n')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// Parse .env.example to get expected structure
|
||||
const exampleContent = fs.readFileSync(EXAMPLE_PATH, 'utf8')
|
||||
const exampleLines = exampleContent.split('\n')
|
||||
|
||||
// Parse current .env
|
||||
const envContent = fs.readFileSync(ENV_PATH, 'utf8')
|
||||
const envLines = envContent.split('\n')
|
||||
|
||||
// Extract variable names from .env
|
||||
const existingVars = new Set()
|
||||
envLines.forEach(line => {
|
||||
const trimmed = line.trim()
|
||||
if (trimmed && !trimmed.startsWith('#')) {
|
||||
const varName = trimmed.split('=')[0].trim()
|
||||
if (varName) existingVars.add(varName)
|
||||
}
|
||||
})
|
||||
|
||||
// Check for deprecated vars and auto-migrate
|
||||
const deprecatedVars = []
|
||||
const migrations = []
|
||||
|
||||
if (existingVars.has('USER_SESSION_SECRET')) {
|
||||
if (!existingVars.has('HTTP_SESSION_SECRET')) {
|
||||
// Migrate USER_SESSION_SECRET to HTTP_SESSION_SECRET
|
||||
const oldLine = envLines.find(l => l.trim().startsWith('USER_SESSION_SECRET='))
|
||||
if (oldLine) {
|
||||
const value = oldLine.split('=').slice(1).join('=')
|
||||
migrations.push({
|
||||
old: 'USER_SESSION_SECRET',
|
||||
new: 'HTTP_SESSION_SECRET',
|
||||
value: value,
|
||||
action: 'migrate'
|
||||
})
|
||||
}
|
||||
}
|
||||
deprecatedVars.push('USER_SESSION_SECRET → HTTP_SESSION_SECRET (will be removed)')
|
||||
}
|
||||
|
||||
// Find missing variables
|
||||
const missingVars = []
|
||||
const newLines = []
|
||||
let addedVars = 0
|
||||
|
||||
for (let i = 0; i < exampleLines.length; i++) {
|
||||
const line = exampleLines[i]
|
||||
const trimmed = line.trim()
|
||||
|
||||
// Preserve empty lines and section headers (comment lines)
|
||||
if (!trimmed || trimmed.startsWith('#')) {
|
||||
newLines.push(line)
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract variable name (before the = sign)
|
||||
const varName = trimmed.split('=')[0].trim()
|
||||
|
||||
// Skip if not a valid variable assignment
|
||||
if (!varName || !trimmed.includes('=')) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this var exists in current .env
|
||||
if (existingVars.has(varName)) {
|
||||
// Find and copy the existing line from .env
|
||||
const existingLine = envLines.find(l => l.trim().startsWith(varName + '='))
|
||||
newLines.push(existingLine || varName + '=')
|
||||
} else {
|
||||
// Check if there's a migration for this variable
|
||||
const migration = migrations.find(m => m.new === varName)
|
||||
if (migration) {
|
||||
// Use migrated value
|
||||
newLines.push(`${varName}=${migration.value}`)
|
||||
missingVars.push(`${varName} (migrated from ${migration.old})`)
|
||||
addedVars++
|
||||
} else {
|
||||
// Variable is missing - add it with empty value
|
||||
missingVars.push(varName)
|
||||
newLines.push(`${varName}=`)
|
||||
addedVars++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show results
|
||||
console.log('Configuration Status:\n')
|
||||
|
||||
if (migrations.length > 0) {
|
||||
console.log('Auto-migrations applied:')
|
||||
migrations.forEach(m => console.log(` ${m.old} → ${m.new}`))
|
||||
console.log()
|
||||
}
|
||||
|
||||
if (deprecatedVars.length > 0) {
|
||||
console.log('Deprecated variables found:')
|
||||
deprecatedVars.forEach(v => console.log(` ${v}`))
|
||||
console.log()
|
||||
}
|
||||
|
||||
if (missingVars.length > 0) {
|
||||
console.log(`Found ${missingVars.length} missing variable(s):`)
|
||||
missingVars.forEach(v => console.log(` * ${v}`))
|
||||
console.log()
|
||||
|
||||
// Create backup
|
||||
fs.copyFileSync(ENV_PATH, BACKUP_PATH)
|
||||
console.log('Created backup: .env.backup')
|
||||
|
||||
// Write updated .env
|
||||
fs.writeFileSync(ENV_PATH, newLines.join('\n'))
|
||||
console.log('Updated .env with empty placeholders')
|
||||
console.log('\nPlease fill in the missing values in your .env file!\n')
|
||||
} else if (deprecatedVars.length > 0) {
|
||||
console.log('All variables present (but some are deprecated)\n')
|
||||
} else {
|
||||
console.log('All variables present and up to date!\n')
|
||||
}
|
||||
|
||||
process.exit(0)
|
||||
Loading…
Add table
Reference in a new issue