mirror of
https://github.com/Crazyco-xyz/48hr.email.git
synced 2026-01-09 11:19:36 +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_ENABLED=false # Enable SMTP forwarding functionality (default: false)
|
||||||
SMTP_USER="noreply@48hr.email" # SMTP authentication username (also used as from address)
|
SMTP_USER="noreply@48hr.email" # SMTP authentication username (also used as from address)
|
||||||
SMTP_PASSWORD="password" # SMTP authentication password
|
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_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)
|
SMTP_SECURE=true # Use SSL (true for port 465, false for other ports)
|
||||||
|
|
||||||
# --- HTTP / WEB CONFIGURATION ---
|
# --- HTTP / WEB CONFIGURATION ---
|
||||||
HTTP_PORT=3000 # Port
|
HTTP_PORT=3000 # Port
|
||||||
HTTP_BASE_URL="http://localhost:3000" # Base URL for verification links (e.g., https://48hr.email)
|
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_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,
|
||||||
|
|
@ -42,12 +43,11 @@ HTTP_DISPLAY_SORT=2 # Domain display
|
||||||
# 3 = shuffle all
|
# 3 = shuffle all
|
||||||
HTTP_HIDE_OTHER=false # true = only show first domain, false = show 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_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 AUTHENTICATION & INBOX LOCKING ---
|
||||||
USER_AUTH_ENABLED=false # Enable user registration/login system (default: false)
|
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_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_FORWARD_EMAILS=5 # Maximum verified forwarding emails per user
|
||||||
USER_MAX_LOCKED_INBOXES=5 # Maximum locked inboxes 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)
|
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
|
||||||
|
.env.backup
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
|
||||||
17
app.js
17
app.js
|
|
@ -2,6 +2,22 @@
|
||||||
|
|
||||||
/* eslint unicorn/no-process-exit: 0 */
|
/* 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 config = require('./application/config')
|
||||||
const debug = require('debug')('48hr-email:app')
|
const debug = require('debug')('48hr-email:app')
|
||||||
const Helper = require('./application/helper')
|
const Helper = require('./application/helper')
|
||||||
|
|
@ -90,6 +106,7 @@ if (config.user.authEnabled) {
|
||||||
|
|
||||||
const imapService = new ImapService(config, inboxLock)
|
const imapService = new ImapService(config, inboxLock)
|
||||||
debug('IMAP service initialized')
|
debug('IMAP service initialized')
|
||||||
|
app.set('imapService', imapService)
|
||||||
|
|
||||||
const mailProcessingService = new MailProcessingService(
|
const mailProcessingService = new MailProcessingService(
|
||||||
new MailRepository(),
|
new MailRepository(),
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,13 @@
|
||||||
require("dotenv").config({ quiet: true });
|
require("dotenv").config({ quiet: true });
|
||||||
const debug = require('debug')('48hr-email:config')
|
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.
|
* Safely parse a value from env.
|
||||||
* Returns `undefined` if the value is missing or invalid.
|
* Returns `undefined` if the value is missing or invalid.
|
||||||
|
|
@ -31,7 +38,7 @@ function parseBool(v) {
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
email: {
|
email: {
|
||||||
domains: parseValue(process.env.EMAIL_DOMAINS),
|
domains: parseValue(process.env.EMAIL_DOMAINS) || [],
|
||||||
purgeTime: {
|
purgeTime: {
|
||||||
time: Number(process.env.EMAIL_PURGE_TIME),
|
time: Number(process.env.EMAIL_PURGE_TIME),
|
||||||
unit: parseValue(process.env.EMAIL_PURGE_UNIT),
|
unit: parseValue(process.env.EMAIL_PURGE_UNIT),
|
||||||
|
|
@ -41,7 +48,16 @@ const config = {
|
||||||
account: parseValue(process.env.EMAIL_EXAMPLE_ACCOUNT),
|
account: parseValue(process.env.EMAIL_EXAMPLE_ACCOUNT),
|
||||||
uids: parseValue(process.env.EMAIL_EXAMPLE_UIDS)
|
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: {
|
imap: {
|
||||||
|
|
@ -66,12 +82,19 @@ const config = {
|
||||||
},
|
},
|
||||||
|
|
||||||
http: {
|
http: {
|
||||||
|
// Server settings
|
||||||
port: Number(process.env.HTTP_PORT),
|
port: Number(process.env.HTTP_PORT),
|
||||||
baseUrl: parseValue(process.env.HTTP_BASE_URL) || 'http://localhost:3000',
|
baseUrl: parseValue(process.env.HTTP_BASE_URL) || 'http://localhost:3000',
|
||||||
branding: parseValue(process.env.HTTP_BRANDING),
|
sessionSecret: parseValue(process.env.HTTP_SESSION_SECRET) || parseValue(process.env.USER_SESSION_SECRET) || 'change-me-in-production',
|
||||||
displaySort: Number(process.env.HTTP_DISPLAY_SORT),
|
|
||||||
hideOther: parseBool(process.env.HTTP_HIDE_OTHER),
|
// UI Features & Display
|
||||||
statisticsEnabled: parseBool(process.env.HTTP_STATISTICS_ENABLED) || false
|
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: {
|
user: {
|
||||||
|
|
@ -81,9 +104,6 @@ const config = {
|
||||||
// Database
|
// Database
|
||||||
databasePath: parseValue(process.env.USER_DATABASE_PATH) || './db/data.db',
|
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
|
// Feature Limits
|
||||||
maxForwardEmails: Number(process.env.USER_MAX_FORWARD_EMAILS) || 5,
|
maxForwardEmails: Number(process.env.USER_MAX_FORWARD_EMAILS) || 5,
|
||||||
maxLockedInboxes: Number(process.env.USER_MAX_LOCKED_INBOXES) || 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
|
* Build a mail count html element with tooltip for the footer
|
||||||
* @param {number} count - Current mail count
|
* @param {number} count - Current mail count
|
||||||
|
* @param {number} largestUid - Largest UID from IMAP
|
||||||
* @returns {String}
|
* @returns {String}
|
||||||
*/
|
*/
|
||||||
mailCountBuilder(count) {
|
mailCountBuilder(count, largestUid = null) {
|
||||||
const imapService = require('./imap-service')
|
|
||||||
const largestUid = imapService.getLargestUid ? imapService.getLargestUid() : null
|
|
||||||
let tooltip = ''
|
let tooltip = ''
|
||||||
|
|
||||||
if (largestUid && largestUid > 0) {
|
if (largestUid && largestUid > 0) {
|
||||||
|
|
@ -156,10 +155,10 @@ class Helper {
|
||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
*/
|
*/
|
||||||
hideOther(array) {
|
hideOther(array) {
|
||||||
if (config.http.hideOther) {
|
if (config.http.features.hideOther) {
|
||||||
return array[0]
|
return array && array.length > 0 ? [array[0]] : []
|
||||||
} else {
|
} else {
|
||||||
return array
|
return array || []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -168,9 +167,13 @@ class Helper {
|
||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
*/
|
*/
|
||||||
getDomains() {
|
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;
|
let result;
|
||||||
switch (config.http.displaySort) {
|
switch (config.http.features.displaySort) {
|
||||||
case 0:
|
case 0:
|
||||||
result = this.hideOther(config.email.domains) // No modification
|
result = this.hideOther(config.email.domains) // No modification
|
||||||
debug(`Domain sort 0: no modification, ${result.length} domains`)
|
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
|
result = this.hideOther(this.shuffleArray(config.email.domains)) // Shuffle all
|
||||||
debug(`Domain sort 3: shuffle all, ${result.length} domains`)
|
debug(`Domain sort 3: shuffle all, ${result.length} domains`)
|
||||||
return result
|
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)
|
* @returns {string} - HMAC signature (hex)
|
||||||
*/
|
*/
|
||||||
signCookie(email) {
|
signCookie(email) {
|
||||||
const secret = config.user.sessionSecret
|
const secret = config.http.sessionSecret
|
||||||
const hmac = crypto.createHmac('sha256', secret)
|
const hmac = crypto.createHmac('sha256', secret)
|
||||||
hmac.update(email.toLowerCase())
|
hmac.update(email.toLowerCase())
|
||||||
const signature = hmac.digest('hex')
|
const signature = hmac.digest('hex')
|
||||||
|
|
|
||||||
|
|
@ -274,7 +274,7 @@ class MailProcessingService extends EventEmitter {
|
||||||
|
|
||||||
// Forward via SMTP service
|
// Forward via SMTP service
|
||||||
debug(`Forwarding email to ${destinationEmail}`)
|
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)
|
const result = await this.smtpService.forwardMail(fullMail, destinationEmail, branding)
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
|
@ -354,7 +354,7 @@ class MailProcessingService extends EventEmitter {
|
||||||
|
|
||||||
// Send verification email
|
// Send verification email
|
||||||
const baseUrl = this.config.http.baseUrl
|
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}`)
|
debug(`Sending verification email to ${destinationEmail} for source ${sourceAddress}`)
|
||||||
const result = await this.smtpService.sendVerificationEmail(
|
const result = await this.smtpService.sendVerificationEmail(
|
||||||
|
|
|
||||||
|
|
@ -2887,7 +2887,7 @@ body.light-mode .theme-icon-light {
|
||||||
}
|
}
|
||||||
.action-dropdown .dropdown-menu a {
|
.action-dropdown .dropdown-menu a {
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
text-align: left;
|
text-align: right;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
}
|
}
|
||||||
.action-dropdown .dropdown-menu a:hover {
|
.action-dropdown .dropdown-menu a:hover {
|
||||||
|
|
@ -2924,7 +2924,7 @@ body.light-mode .theme-icon-light {
|
||||||
.action-links.mobile-open>button:not(.hamburger-menu) {
|
.action-links.mobile-open>button:not(.hamburger-menu) {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: left;
|
text-align: right;
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ router.get('/account', requireAuth, async(req, res) => {
|
||||||
forwardEmails,
|
forwardEmails,
|
||||||
lockedInboxes,
|
lockedInboxes,
|
||||||
stats,
|
stats,
|
||||||
branding: config.http.branding,
|
branding: config.http.features.branding || ['48hr.email', 'Service', 'https://example.com'],
|
||||||
purgeTime: purgeTime,
|
purgeTime: purgeTime,
|
||||||
successMessage: req.session.accountSuccess,
|
successMessage: req.session.accountSuccess,
|
||||||
errorMessage: req.session.accountError
|
errorMessage: req.session.accountError
|
||||||
|
|
@ -94,7 +94,7 @@ router.post('/account/forward-email/add',
|
||||||
|
|
||||||
// Send verification email
|
// Send verification email
|
||||||
const baseUrl = config.http.baseUrl
|
const baseUrl = config.http.baseUrl
|
||||||
const branding = config.http.branding[0]
|
const branding = (config.http.features.branding || ['48hr.email'])[0]
|
||||||
|
|
||||||
await smtpService.sendVerificationEmail(
|
await smtpService.sendVerificationEmail(
|
||||||
email,
|
email,
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,20 @@ const loginRateLimiter = (req, res, next) => {
|
||||||
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)
|
// GET /auth - Show unified auth page (login or register)
|
||||||
router.get('/auth', redirectIfAuthenticated, (req, res) => {
|
router.get('/auth', redirectIfAuthenticated, (req, res) => {
|
||||||
const config = req.app.get('config')
|
const config = req.app.get('config')
|
||||||
|
|
@ -90,8 +104,8 @@ router.get('/auth', redirectIfAuthenticated, (req, res) => {
|
||||||
delete req.session.successMessage
|
delete req.session.successMessage
|
||||||
|
|
||||||
res.render('auth', {
|
res.render('auth', {
|
||||||
title: `Login or Register | ${config.http.branding[0]}`,
|
title: `Login or Register | ${(config.http.features.branding || ['48hr.email'])[0]}`,
|
||||||
branding: config.http.branding,
|
branding: config.http.features.branding || ['48hr.email', 'Service', 'https://example.com'],
|
||||||
purgeTime: purgeTime,
|
purgeTime: purgeTime,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
successMessage
|
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'
|
const message = req.query.message || (req.session && req.session.errorMessage) || 'An error occurred'
|
||||||
|
|
||||||
debug(`Rendering error page ${errorCode} with message: ${message}`)
|
debug(`Rendering error page ${errorCode} with message: ${message}`)
|
||||||
|
const branding = config.http.features.branding || ['48hr.email', 'Service', 'https://example.com']
|
||||||
res.status(errorCode)
|
res.status(errorCode)
|
||||||
res.render('error', {
|
res.render('error', {
|
||||||
title: `${config.http.branding[0]} | ${errorCode}`,
|
title: `${branding[0]} | ${errorCode}`,
|
||||||
purgeTime: purgeTime,
|
purgeTime: purgeTime,
|
||||||
address: req.params.address,
|
address: req.params.address,
|
||||||
message: message,
|
message: message,
|
||||||
status: errorCode,
|
status: errorCode,
|
||||||
branding: config.http.branding
|
branding: branding
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debug('Error loading error page:', error.message)
|
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 || ''
|
const verificationEmail = req.query.email || ''
|
||||||
|
|
||||||
res.render('inbox', {
|
res.render('inbox', {
|
||||||
title: `${config.http.branding[0]} | ` + req.params.address,
|
title: `${(config.http.features.branding || ['48hr.email'])[0]} | ` + req.params.address,
|
||||||
purgeTime: purgeTime,
|
purgeTime: purgeTime,
|
||||||
address: req.params.address,
|
address: req.params.address,
|
||||||
mailSummaries: mailProcessingService.getMailSummaries(req.params.address),
|
mailSummaries: mailProcessingService.getMailSummaries(req.params.address),
|
||||||
branding: config.http.branding,
|
branding: config.http.branding,
|
||||||
authEnabled: config.user.authEnabled,
|
authEnabled: config.user.authEnabled,
|
||||||
|
smtpEnabled: config.email.features.smtp,
|
||||||
isAuthenticated: req.session && req.session.userId ? true : false,
|
isAuthenticated: req.session && req.session.userId ? true : false,
|
||||||
userForwardEmails: userForwardEmails,
|
userForwardEmails: userForwardEmails,
|
||||||
isLocked: isLocked,
|
isLocked: isLocked,
|
||||||
|
|
@ -240,8 +241,9 @@ router.get(
|
||||||
mail,
|
mail,
|
||||||
cryptoAttachments: cryptoAttachments,
|
cryptoAttachments: cryptoAttachments,
|
||||||
uid: req.params.uid,
|
uid: req.params.uid,
|
||||||
branding: config.http.branding,
|
branding: config.http.features.branding || ['48hr.email', 'Service', 'https://example.com'],
|
||||||
authEnabled: config.user.authEnabled,
|
authEnabled: config.user.authEnabled,
|
||||||
|
smtpEnabled: config.email.features.smtp,
|
||||||
isAuthenticated: req.session && req.session.userId ? true : false,
|
isAuthenticated: req.session && req.session.userId ? true : false,
|
||||||
userForwardEmails: userForwardEmails,
|
userForwardEmails: userForwardEmails,
|
||||||
isLocked: isLocked,
|
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)
|
// POST route for forwarding a single email (requires authentication)
|
||||||
router.post(
|
router.post(
|
||||||
'^/:address/:uid/forward',
|
'^/:address/:uid/forward',
|
||||||
requireAuth,
|
requireAuth,
|
||||||
|
smtpEnabled,
|
||||||
forwardLimiter,
|
forwardLimiter,
|
||||||
validateDomain,
|
validateDomain,
|
||||||
checkLockAccess,
|
checkLockAccess,
|
||||||
|
|
@ -503,6 +516,7 @@ router.post(
|
||||||
router.post(
|
router.post(
|
||||||
'^/:address/forward-all',
|
'^/:address/forward-all',
|
||||||
requireAuth,
|
requireAuth,
|
||||||
|
smtpEnabled,
|
||||||
forwardLimiter,
|
forwardLimiter,
|
||||||
validateDomain,
|
validateDomain,
|
||||||
checkLockAccess,
|
checkLockAccess,
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,9 @@ 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 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')
|
||||||
const Helper = require('../../../application/helper')
|
const templateContext = require('../template-context')
|
||||||
const helper = new(Helper)
|
|
||||||
|
|
||||||
const purgeTime = helper.purgeTimeElemetBuilder()
|
|
||||||
|
|
||||||
router.get('/', async(req, res, next) => {
|
router.get('/', async(req, res, next) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -17,14 +13,12 @@ router.get('/', async(req, res, next) => {
|
||||||
throw new Error('Mail processing service not available')
|
throw new Error('Mail processing service not available')
|
||||||
}
|
}
|
||||||
debug('Login page requested')
|
debug('Login page requested')
|
||||||
|
const context = templateContext.build(req, {
|
||||||
|
username: randomWord()
|
||||||
|
})
|
||||||
res.render('login', {
|
res.render('login', {
|
||||||
title: `${config.http.branding[0]} | Your temporary Inbox`,
|
...context,
|
||||||
username: randomWord(),
|
title: `${context.branding[0]} | Your temporary Inbox`
|
||||||
purgeTime: purgeTime,
|
|
||||||
purgeTimeRaw: config.email.purgeTime,
|
|
||||||
domains: helper.getDomains(),
|
|
||||||
branding: config.http.branding,
|
|
||||||
example: config.email.examples.account,
|
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debug('Error loading login page:', error.message)
|
debug('Error loading login page:', error.message)
|
||||||
|
|
@ -56,14 +50,13 @@ router.post(
|
||||||
const errors = validationResult(req)
|
const errors = validationResult(req)
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
debug(`Login validation failed for ${req.body.username}@${req.body.domain}: ${errors.array().map(e => e.msg).join(', ')}`)
|
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,
|
userInputError: true,
|
||||||
title: `${config.http.branding[0]} | Your temporary Inbox`,
|
username: randomWord()
|
||||||
purgeTime: purgeTime,
|
})
|
||||||
purgeTimeRaw: config.email.purgeTime,
|
return res.render('login', {
|
||||||
username: randomWord(),
|
...context,
|
||||||
domains: helper.getDomains(),
|
title: `${context.branding[0]} | Your temporary Inbox`
|
||||||
branding: config.http.branding,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,12 @@ router.get('/', async(req, res) => {
|
||||||
const config = req.app.get('config')
|
const config = req.app.get('config')
|
||||||
|
|
||||||
// Check if statistics are enabled
|
// Check if statistics are enabled
|
||||||
if (!config.http.statisticsEnabled) {
|
if (!config.http.features.statistics) {
|
||||||
return res.status(404).send('Statistics are disabled')
|
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')
|
const statisticsStore = req.app.get('statisticsStore')
|
||||||
|
|
@ -33,12 +37,13 @@ router.get('/', async(req, res) => {
|
||||||
|
|
||||||
const stats = statisticsStore.getEnhancedStats()
|
const stats = statisticsStore.getEnhancedStats()
|
||||||
const purgeTime = helper.purgeTimeElemetBuilder()
|
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`)
|
debug(`Stats page requested: ${stats.currentCount} current, ${stats.allTimeTotal} all-time total, ${stats.historical.length} historical points`)
|
||||||
|
|
||||||
res.render('stats', {
|
res.render('stats', {
|
||||||
title: `Statistics | ${config.http.branding[0]}`,
|
title: `Statistics | ${branding[0]}`,
|
||||||
branding: config.http.branding,
|
branding: branding,
|
||||||
purgeTime: purgeTime,
|
purgeTime: purgeTime,
|
||||||
stats: stats,
|
stats: stats,
|
||||||
authEnabled: config.user.authEnabled,
|
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">
|
<div class="action-links">
|
||||||
{% if currentUser %}
|
{% if currentUser %}
|
||||||
<!-- Inbox Dropdown (multiple actions when logged in) -->
|
<!-- Inbox Dropdown (multiple actions when logged in) -->
|
||||||
<div class="action-dropdown">
|
<div class="action-dropdown">
|
||||||
<button class="dropdown-toggle" aria-label="Inbox actions">Inbox ▾</button>
|
<button class="dropdown-toggle" aria-label="Inbox actions">Inbox ▾</button>
|
||||||
<div class="dropdown-menu" data-section-title="Inbox Actions">
|
<div class="dropdown-menu" data-section-title="Inbox Actions">
|
||||||
<a href="#" id="forwardAllBtn" aria-label="Forward all emails">Forward All</a>
|
{% if smtpEnabled %}
|
||||||
{% if authEnabled %}
|
<a href="#" id="forwardAllBtn" aria-label="Forward all emails">Forward All</a>
|
||||||
{% 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 %}
|
||||||
{% endif %}
|
{% if authEnabled %}
|
||||||
<a href="/inbox/{{ address }}/delete-all" aria-label="Delete all emails">Wipe Inbox</a>
|
{% if isLocked and hasAccess %}
|
||||||
</div>
|
<a href="#" id="removeLockBtn" aria-label="Remove lock">Remove Lock</a>
|
||||||
</div>
|
{% elseif not isLocked %}
|
||||||
|
<a href="#" id="lockBtn" aria-label="Lock inbox to your account">Lock Inbox</a>
|
||||||
<!-- Account Dropdown (logged in) -->
|
{% 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 authEnabled %}
|
||||||
<div class="action-dropdown">
|
<div class="action-dropdown">
|
||||||
<button class="dropdown-toggle" aria-label="Account menu">Account ▾</button>
|
<button class="dropdown-toggle" aria-label="Account menu">Account ▾</button>
|
||||||
|
|
@ -178,6 +178,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Forward All Modal -->
|
<!-- Forward All Modal -->
|
||||||
|
{% if smtpEnabled %}
|
||||||
<div id="forwardAllModal" class="modal" style="display: none;">
|
<div id="forwardAllModal" class="modal" style="display: none;">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<span class="close" id="closeForwardAll">×</span>
|
<span class="close" id="closeForwardAll">×</span>
|
||||||
|
|
@ -216,4 +217,5 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -88,13 +88,20 @@
|
||||||
</a>
|
</a>
|
||||||
{% block header %}{% endblock %}
|
{% block header %}{% endblock %}
|
||||||
</div>
|
</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 %}
|
{% block body %}{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{% block footer %}
|
{% block footer %}
|
||||||
<section class="container footer">
|
<section class="container footer">
|
||||||
<hr>
|
<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>
|
<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 %}
|
{% 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>
|
<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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="features-grid">
|
{% if showInfoSection %}
|
||||||
<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>
|
|
||||||
|
|
||||||
<div class="info-section">
|
<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">
|
<div class="info-content frosted-glass">
|
||||||
<h2>What is a Temporary Email?</h2>
|
<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>
|
<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>
|
<li><strong>{{ purgeTimeRaw|readablePurgeTime|title }} Retention:</strong> Emails stay accessible for the full duration before auto-deletion</li>
|
||||||
</ul>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,9 @@
|
||||||
<div class="action-dropdown">
|
<div class="action-dropdown">
|
||||||
<button class="dropdown-toggle" aria-label="Email actions">Email ▾</button>
|
<button class="dropdown-toggle" aria-label="Email actions">Email ▾</button>
|
||||||
<div class="dropdown-menu" data-section-title="Email Actions">
|
<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 }}/delete" aria-label="Delete this email">Delete</a>
|
||||||
<a href="/inbox/{{ address }}/{{ uid }}/raw" target="_blank" aria-label="View raw email">View Raw</a>
|
<a href="/inbox/{{ address }}/{{ uid }}/raw" target="_blank" aria-label="View raw email">View Raw</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -127,6 +129,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Forward Email Modal -->
|
<!-- Forward Email Modal -->
|
||||||
|
{% if smtpEnabled %}
|
||||||
<div id="forwardModal" class="modal" style="display: none;">
|
<div id="forwardModal" class="modal" style="display: none;">
|
||||||
<div class="modal-content frosted-glass">
|
<div class="modal-content frosted-glass">
|
||||||
<span class="close" id="closeForward">×</span>
|
<span class="close" id="closeForward">×</span>
|
||||||
|
|
@ -165,6 +168,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,11 +45,11 @@ app.use(express.json())
|
||||||
app.use(express.urlencoded({ extended: false }))
|
app.use(express.urlencoded({ extended: false }))
|
||||||
|
|
||||||
// Cookie parser for signed cookies (email verification)
|
// 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)
|
// Session support (always enabled for forward verification and inbox locking)
|
||||||
app.use(session({
|
app.use(session({
|
||||||
secret: config.user.sessionSecret,
|
secret: config.http.sessionSecret,
|
||||||
resave: false,
|
resave: false,
|
||||||
saveUninitialized: false,
|
saveUninitialized: false,
|
||||||
cookie: { maxAge: 24 * 60 * 60 * 1000 } // 24 hours
|
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.authEnabled = config.user.authEnabled
|
||||||
res.locals.config = config
|
res.locals.config = config
|
||||||
res.locals.currentUser = null
|
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) {
|
if (req.session && req.session.userId && req.session.username && req.session.isAuthenticated) {
|
||||||
res.locals.currentUser = {
|
res.locals.currentUser = {
|
||||||
id: req.session.userId,
|
id: req.session.userId,
|
||||||
|
|
@ -107,14 +114,25 @@ app.use((req, res, next) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Middleware to expose mail count to all templates
|
// 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 mailProcessingService = req.app.get('mailProcessingService')
|
||||||
|
const imapService = req.app.get('imapService')
|
||||||
const Helper = require('../../application/helper')
|
const Helper = require('../../application/helper')
|
||||||
const helper = new Helper()
|
const helper = new Helper()
|
||||||
|
|
||||||
if (mailProcessingService) {
|
if (mailProcessingService) {
|
||||||
const count = mailProcessingService.getCount()
|
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 {
|
} else {
|
||||||
res.locals.mailCount = ''
|
res.locals.mailCount = ''
|
||||||
}
|
}
|
||||||
|
|
@ -159,7 +177,7 @@ app.use(async(err, req, res, _next) => {
|
||||||
res.render('error', {
|
res.render('error', {
|
||||||
purgeTime: purgeTime,
|
purgeTime: purgeTime,
|
||||||
address: req.params && req.params.address,
|
address: req.params && req.params.address,
|
||||||
branding: config.http.branding
|
branding: config.http.features.branding || ['48hr.email', 'Service', 'https://example.com']
|
||||||
})
|
})
|
||||||
} catch (renderError) {
|
} catch (renderError) {
|
||||||
debug('Error in error handler:', renderError.message)
|
debug('Error in error handler:', renderError.message)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "48hr.email",
|
"name": "48hr.email",
|
||||||
"version": "2.1.0",
|
"version": "2.2.0",
|
||||||
"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": [
|
||||||
|
|
@ -27,7 +27,8 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node --trace-warnings ./app.js",
|
"start": "node --trace-warnings ./app.js",
|
||||||
"debug": "DEBUG=48hr-email:* node --nolazy --inspect-brk=9229 --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": {
|
"dependencies": {
|
||||||
"async-retry": "^1.3.3",
|
"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