[Feat]: Add email validation function

Currently only used for forwarding
This commit is contained in:
ClaraCrazy 2026-01-02 16:13:22 +01:00
parent 8daa0fefe9
commit 2a08aa14a8
No known key found for this signature in database
GPG key ID: EBBC896ACB497011
15 changed files with 793 additions and 43 deletions

View file

@ -34,6 +34,7 @@ SMTP_FROM_NAME="48hr Email Service" # Display name f
# --- 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_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,

8
app.js
View file

@ -12,6 +12,7 @@ const MailProcessingService = require('./application/mail-processing-service')
const SmtpService = require('./application/smtp-service') const SmtpService = require('./application/smtp-service')
const MailRepository = require('./domain/mail-repository') const MailRepository = require('./domain/mail-repository')
const InboxLock = require('./domain/inbox-lock') const InboxLock = require('./domain/inbox-lock')
const VerificationStore = require('./domain/verification-store')
const clientNotification = new ClientNotification() const clientNotification = new ClientNotification()
debug('Client notification service initialized') debug('Client notification service initialized')
@ -40,12 +41,17 @@ debug('IMAP service initialized')
const smtpService = new SmtpService(config) const smtpService = new SmtpService(config)
debug('SMTP service initialized') debug('SMTP service initialized')
const verificationStore = new VerificationStore()
debug('Verification store initialized')
app.set('verificationStore', verificationStore)
const mailProcessingService = new MailProcessingService( const mailProcessingService = new MailProcessingService(
new MailRepository(), new MailRepository(),
imapService, imapService,
clientNotification, clientNotification,
config, config,
smtpService smtpService,
verificationStore
) )
debug('Mail processing service initialized') debug('Mail processing service initialized')

View file

@ -68,6 +68,7 @@ const config = {
http: { http: {
port: Number(process.env.HTTP_PORT), port: Number(process.env.HTTP_PORT),
baseUrl: parseValue(process.env.HTTP_BASE_URL) || 'http://localhost:3000',
branding: parseValue(process.env.HTTP_BRANDING), branding: parseValue(process.env.HTTP_BRANDING),
displaySort: Number(process.env.HTTP_DISPLAY_SORT), displaySort: Number(process.env.HTTP_DISPLAY_SORT),
hideOther: parseBool(process.env.HTTP_HIDE_OTHER) hideOther: parseBool(process.env.HTTP_HIDE_OTHER)

View file

@ -1,5 +1,6 @@
const config = require('./config') const config = require('./config')
const debug = require('debug')('48hr-email:helper') const debug = require('debug')('48hr-email:helper')
const crypto = require('crypto')
class Helper { class Helper {
@ -180,6 +181,57 @@ class Helper {
</label>` </label>`
return handling return handling
} }
/**
* Generate a cryptographically secure random verification token
* @returns {string} - 32-byte hex token (64 characters)
*/
generateVerificationToken() {
const token = crypto.randomBytes(32).toString('hex')
debug('Generated verification token')
return token
}
/**
* Sign an email address for use in a cookie
* Uses HMAC-SHA256 with the session secret
* @param {string} email - Email address to sign
* @returns {string} - HMAC signature (hex)
*/
signCookie(email) {
const secret = config.lock.sessionSecret
const hmac = crypto.createHmac('sha256', secret)
hmac.update(email.toLowerCase())
const signature = hmac.digest('hex')
debug(`Signed cookie for email: ${email}`)
return signature
}
/**
* Verify a cookie signature for an email address
* @param {string} email - Email address to verify
* @param {string} signature - HMAC signature to verify
* @returns {boolean} - True if signature is valid
*/
verifyCookieSignature(email, signature) {
if (!email || !signature) {
return false
}
const expectedSignature = this.signCookie(email)
// Use timing-safe comparison to prevent timing attacks
try {
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expectedSignature, 'hex')
)
} catch (error) {
// timingSafeEqual throws if buffers are different lengths
debug(`Cookie signature verification failed: ${error.message}`)
return false
}
}
} }
module.exports = Helper module.exports = Helper

View file

@ -7,13 +7,15 @@ const helper = new(Helper)
class MailProcessingService extends EventEmitter { class MailProcessingService extends EventEmitter {
constructor(mailRepository, imapService, clientNotification, config, smtpService = null) { constructor(mailRepository, imapService, clientNotification, config, smtpService = null, verificationStore = null) {
super() super()
this.mailRepository = mailRepository this.mailRepository = mailRepository
this.clientNotification = clientNotification this.clientNotification = clientNotification
this.imapService = imapService this.imapService = imapService
this.config = config this.config = config
this.smtpService = smtpService this.smtpService = smtpService
this.verificationStore = verificationStore
this.helper = new(Helper)
// Cached methods: // Cached methods:
this._initCache() this._initCache()
@ -277,6 +279,94 @@ class MailProcessingService extends EventEmitter {
} }
} }
/**
* Initiate email verification for forwarding
* Sends verification email to destination address
* @param {string} sourceAddress - The inbox address requesting forwarding
* @param {string} destinationEmail - The email address to verify and forward to
* @param {Array<number>} uids - Array of email UIDs to forward (optional, for context)
* @returns {Promise<{success: boolean, error?: string, cooldownSeconds?: number}>}
*/
async initiateForwardVerification(sourceAddress, destinationEmail, uids = []) {
// Check if verification store is available
if (!this.verificationStore) {
debug('Verification store not available')
return {
success: false,
error: 'Email verification is not configured'
}
}
// Check if SMTP service is available
if (!this.smtpService) {
debug('SMTP service not configured')
return {
success: false,
error: 'Email forwarding is not configured. Please configure SMTP settings.'
}
}
// Check rate limit (5-minute cooldown)
const canRequest = this.verificationStore.canRequestVerification(destinationEmail)
if (!canRequest) {
const lastRequest = this.verificationStore.getLastVerificationTime(destinationEmail)
const cooldownMs = 5 * 60 * 1000
const elapsed = Date.now() - lastRequest
const remainingSeconds = Math.ceil((cooldownMs - elapsed) / 1000)
debug(`Verification rate limit hit for ${destinationEmail}, ${remainingSeconds}s remaining`)
return {
success: false,
error: `Please wait ${remainingSeconds} seconds before requesting another verification email`,
cooldownSeconds: remainingSeconds
}
}
try {
// Generate verification token
const token = this.helper.generateVerificationToken()
// Store verification with metadata
this.verificationStore.createVerification(token, destinationEmail, {
sourceAddress,
uids,
createdAt: new Date().toISOString()
})
// Send verification email
const baseUrl = this.config.http.baseUrl
const branding = this.config.http.branding[0] || '48hr.email'
debug(`Sending verification email to ${destinationEmail} for source ${sourceAddress}`)
const result = await this.smtpService.sendVerificationEmail(
destinationEmail,
token,
baseUrl,
branding
)
if (result.success) {
debug(`Verification email sent successfully. MessageId: ${result.messageId}`)
return {
success: true,
messageId: result.messageId
}
} else {
debug(`Failed to send verification email: ${result.error}`)
return {
success: false,
error: result.error
}
}
} catch (error) {
debug('Error initiating verification:', error.message)
return {
success: false,
error: `Failed to send verification email: ${error.message}`
}
}
}
_saveToFile(mails, filename) { _saveToFile(mails, filename) {
const fs = require('fs') const fs = require('fs')
fs.writeFile(filename, JSON.stringify(mails), err => { fs.writeFile(filename, JSON.stringify(mails), err => {

View file

@ -217,6 +217,111 @@ ${mail.html}
} }
} }
} }
/**
* Send verification email to destination address
* @param {string} destinationEmail - Email address to verify
* @param {string} token - Verification token
* @param {string} baseUrl - Base URL for verification link
* @param {string} branding - Service branding name
* @returns {Promise<{success: boolean, error?: string, messageId?: string}>}
*/
async sendVerificationEmail(destinationEmail, token, baseUrl, branding = '48hr.email') {
if (!this.transporter) {
return {
success: false,
error: 'SMTP is not configured. Please configure SMTP settings to enable forwarding.'
}
}
const verificationLink = `${baseUrl}/inbox/verify?token=${token}`
const htmlContent = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: #2c3e50; color: white; padding: 20px; text-align: center; border-radius: 5px 5px 0 0; }
.content { background: #f9f9f9; padding: 30px; border: 1px solid #ddd; border-top: none; border-radius: 0 0 5px 5px; }
.button { display: inline-block; background: #3498db; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; margin: 20px 0; font-weight: bold; }
.button:hover { background: #2980b9; }
.warning { background: #fff3cd; border-left: 4px solid #ffc107; padding: 12px; margin: 20px 0; }
.footer { text-align: center; margin-top: 20px; color: #666; font-size: 0.9em; }
code { background: #e8e8e8; padding: 2px 6px; border-radius: 3px; font-family: monospace; }
</style>
</head>
<body>
<div class="header">
<h2>🔐 Verify Your Email Address</h2>
</div>
<div class="content">
<p>Hello,</p>
<p>You requested to use <strong>${this._escapeHtml(destinationEmail)}</strong> as a forwarding destination on <strong>${this._escapeHtml(branding)}</strong>.</p>
<p>To verify ownership of this email address and enable forwarding for 24 hours, please click the button below:</p>
<div style="text-align: center;">
<a href="${verificationLink}" class="button">Verify Email Address</a>
</div>
<p>Or copy and paste this link into your browser:</p>
<p><code>${verificationLink}</code></p>
<div class="warning">
<strong>Important:</strong> This verification link expires in <strong>15 minutes</strong>. Once verified, you'll be able to forward emails to this address for 24 hours.
</div>
<p>If you didn't request this verification, you can safely ignore this email.</p>
</div>
<div class="footer">
<p>This is an automated message from ${this._escapeHtml(branding)}</p>
</div>
</body>
</html>
`
const textContent = `
Verify Your Email Address
You requested to use ${destinationEmail} as a forwarding destination on ${branding}.
To verify ownership of this email address and enable forwarding for 24 hours, please visit:
${verificationLink}
IMPORTANT: This verification link expires in 15 minutes. Once verified, you'll be able to forward emails to this address for 24 hours.
If you didn't request this verification, you can safely ignore this email.
---
This is an automated message from ${branding}
`
try {
const info = await this.transporter.sendMail({
from: `"${branding} Forwarding Service" <${this.config.smtp.user}>`,
to: destinationEmail,
subject: `${branding} - Verify your email for forwarding`,
text: textContent,
html: htmlContent
})
debug(`Verification email sent to ${destinationEmail}, messageId: ${info.messageId}`)
return {
success: true,
messageId: info.messageId
}
} catch (error) {
debug(`Failed to send verification email: ${error.message}`)
return {
success: false,
error: `Failed to send verification email: ${error.message}`
}
}
}
} }
module.exports = SmtpService module.exports = SmtpService

View file

@ -0,0 +1,161 @@
const debug = require('debug')('48hr-email:verification-store')
/**
* In-memory store for email verification tokens
* Manages pending verifications with expiration and rate limiting
*/
class VerificationStore {
constructor() {
// Map of token -> verification data
this.verifications = new Map()
// Map of destinationEmail -> last verification request timestamp
this.lastVerificationRequests = new Map()
// Cleanup expired tokens every 5 minutes
this.cleanupInterval = setInterval(() => {
this.cleanup()
}, 5 * 60 * 1000)
debug('VerificationStore initialized')
}
/**
* Create a new verification entry
* @param {string} token - Unique verification token
* @param {string} destinationEmail - Email address being verified
* @param {Object} metadata - Additional data (sourceAddress, uids, etc.)
* @returns {Object} - Verification entry
*/
createVerification(token, destinationEmail, metadata = {}) {
const now = Date.now()
const expiresAt = now + (15 * 60 * 1000) // 15 minutes
const verification = {
token,
destinationEmail: destinationEmail.toLowerCase(),
createdAt: now,
expiresAt,
metadata
}
this.verifications.set(token, verification)
this.lastVerificationRequests.set(destinationEmail.toLowerCase(), now)
debug(`Created verification for ${destinationEmail}, token expires in 15 minutes`)
return verification
}
/**
* Verify a token and return the verification data if valid
* @param {string} token - Token to verify
* @returns {Object|null} - Verification data or null if invalid/expired
*/
verifyToken(token) {
const verification = this.verifications.get(token)
if (!verification) {
debug(`Token not found: ${token}`)
return null
}
const now = Date.now()
if (now > verification.expiresAt) {
debug(`Token expired: ${token}`)
this.verifications.delete(token)
return null
}
debug(`Token verified successfully for ${verification.destinationEmail}`)
// Remove token after successful verification (one-time use)
this.verifications.delete(token)
return verification
}
/**
* Get the last verification request time for an email
* @param {string} destinationEmail - Email address to check
* @returns {number|null} - Timestamp of last request or null
*/
getLastVerificationTime(destinationEmail) {
return this.lastVerificationRequests.get(destinationEmail.toLowerCase()) || null
}
/**
* Check if enough time has passed since last verification request
* @param {string} destinationEmail - Email address to check
* @param {number} cooldownMs - Cooldown period in milliseconds (default: 5 minutes)
* @returns {boolean} - True if can request verification, false if still in cooldown
*/
canRequestVerification(destinationEmail, cooldownMs = 5 * 60 * 1000) {
const lastRequest = this.getLastVerificationTime(destinationEmail)
if (!lastRequest) {
return true
}
const now = Date.now()
const timeSinceLastRequest = now - lastRequest
const canRequest = timeSinceLastRequest >= cooldownMs
if (!canRequest) {
const remainingSeconds = Math.ceil((cooldownMs - timeSinceLastRequest) / 1000)
debug(`Verification cooldown active for ${destinationEmail}, ${remainingSeconds}s remaining`)
}
return canRequest
}
/**
* Clean up expired tokens and old rate limit entries
*/
cleanup() {
const now = Date.now()
let expiredCount = 0
let rateLimitCleanupCount = 0
// Clean expired tokens
for (const [token, verification] of this.verifications.entries()) {
if (now > verification.expiresAt) {
this.verifications.delete(token)
expiredCount++
}
}
// Clean old rate limit entries (older than 1 hour)
for (const [email, timestamp] of this.lastVerificationRequests.entries()) {
if (now - timestamp > 60 * 60 * 1000) {
this.lastVerificationRequests.delete(email)
rateLimitCleanupCount++
}
}
if (expiredCount > 0 || rateLimitCleanupCount > 0) {
debug(`Cleanup: removed ${expiredCount} expired tokens, ${rateLimitCleanupCount} old rate limit entries`)
}
}
/**
* Get statistics about the store
* @returns {Object} - Store statistics
*/
getStats() {
return {
pendingVerifications: this.verifications.size,
rateLimitEntries: this.lastVerificationRequests.size
}
}
/**
* Destroy the store and cleanup interval
*/
destroy() {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval)
}
this.verifications.clear()
this.lastVerificationRequests.clear()
debug('VerificationStore destroyed')
}
}
module.exports = VerificationStore

View file

@ -1016,6 +1016,24 @@ label {
} }
/* Verification Messages */
.verification-message {
color: #3498db;
margin-bottom: 1.5rem;
padding: 1rem;
background: rgba(52, 152, 219, 0.1);
border-left: 3px solid #3498db;
border-radius: 4px;
font-weight: 500;
}
.verification-message strong {
color: #2980b9;
font-weight: 600;
}
/* Remove Lock Button Styles */ /* Remove Lock Button Styles */
.modal-button-danger { .modal-button-danger {

View file

@ -125,6 +125,10 @@ router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, validateDomain, checkLo
// Check for forward all success flag // Check for forward all success flag
const forwardAllSuccess = req.query.forwardedAll ? parseInt(req.query.forwardedAll) : null const forwardAllSuccess = req.query.forwardedAll ? parseInt(req.query.forwardedAll) : null
// Check for verification sent flag
const verificationSent = req.query.verificationSent === 'true'
const verificationEmail = req.query.email || ''
res.render('inbox', { res.render('inbox', {
title: `${config.http.branding[0]} | ` + req.params.address, title: `${config.http.branding[0]} | ` + req.params.address,
purgeTime: purgeTime, purgeTime: purgeTime,
@ -144,7 +148,9 @@ router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, validateDomain, checkLo
expiryUnit: config.email.purgeTime.unit, expiryUnit: config.email.purgeTime.unit,
refreshInterval: config.imap.refreshIntervalSeconds, refreshInterval: config.imap.refreshIntervalSeconds,
errorMessage: errorMessage, errorMessage: errorMessage,
forwardAllSuccess: forwardAllSuccess forwardAllSuccess: forwardAllSuccess,
verificationSent: verificationSent,
verificationEmail: verificationEmail
}) })
} catch (error) { } catch (error) {
debug(`Error loading inbox for ${req.params.address}:`, error.message) debug(`Error loading inbox for ${req.params.address}:`, error.message)
@ -195,6 +201,10 @@ router.get(
// Check for forward success flag // Check for forward success flag
const forwardSuccess = req.query.forwarded === 'true' const forwardSuccess = req.query.forwarded === 'true'
// Check for verification sent flag
const verificationSent = req.query.verificationSent === 'true'
const verificationEmail = req.query.email || ''
debug(`Rendering email view for UID ${req.params.uid}`) debug(`Rendering email view for UID ${req.params.uid}`)
res.render('mail', { res.render('mail', {
title: mail.subject + " | " + req.params.address, title: mail.subject + " | " + req.params.address,
@ -210,7 +220,9 @@ router.get(
isLocked: isLocked, isLocked: isLocked,
hasAccess: hasAccess, hasAccess: hasAccess,
errorMessage: errorMessage, errorMessage: errorMessage,
forwardSuccess: forwardSuccess forwardSuccess: forwardSuccess,
verificationSent: verificationSent,
verificationEmail: verificationEmail
}) })
} else { } else {
debug(`Email ${req.params.uid} not found for ${req.params.address}`) debug(`Email ${req.params.uid} not found for ${req.params.address}`)
@ -427,21 +439,48 @@ router.post(
const { destinationEmail } = req.body const { destinationEmail } = req.body
const uid = parseInt(req.params.uid, 10) const uid = parseInt(req.params.uid, 10)
debug(`Forwarding email ${uid} from ${req.params.address} to ${destinationEmail}`) // Check if destination email is verified via signed cookie
const verifiedEmail = req.signedCookies.verified_email
const result = await mailProcessingService.forwardEmail( if (verifiedEmail && verifiedEmail.toLowerCase() === destinationEmail.toLowerCase()) {
req.params.address, // Email is verified, proceed with forwarding
uid, debug(`Forwarding email ${uid} from ${req.params.address} to ${destinationEmail} (verified)`)
destinationEmail
)
if (result.success) { const result = await mailProcessingService.forwardEmail(
debug(`Email ${uid} forwarded successfully to ${destinationEmail}`) req.params.address,
return res.redirect(`/inbox/${req.params.address}/${uid}?forwarded=true`) uid,
destinationEmail
)
if (result.success) {
debug(`Email ${uid} forwarded successfully to ${destinationEmail}`)
return res.redirect(`/inbox/${req.params.address}/${uid}?forwarded=true`)
} else {
debug(`Failed to forward email ${uid}: ${result.error}`)
req.session.errorMessage = result.error
return res.redirect(`/inbox/${req.params.address}/${uid}`)
}
} else { } else {
debug(`Failed to forward email ${uid}: ${result.error}`) // Email not verified, initiate verification flow
req.session.errorMessage = result.error debug(`Email ${destinationEmail} not verified, initiating verification`)
return res.redirect(`/inbox/${req.params.address}/${uid}`)
const verificationResult = await mailProcessingService.initiateForwardVerification(
req.params.address,
destinationEmail, [uid]
)
if (verificationResult.success) {
debug(`Verification email sent to ${destinationEmail}`)
return res.redirect(`/inbox/${req.params.address}/${uid}?verificationSent=true&email=${encodeURIComponent(destinationEmail)}`)
} else if (verificationResult.cooldownSeconds) {
debug(`Verification rate limited for ${destinationEmail}`)
req.session.errorMessage = verificationResult.error
return res.redirect(`/inbox/${req.params.address}/${uid}`)
} else {
debug(`Failed to send verification email: ${verificationResult.error}`)
req.session.errorMessage = verificationResult.error || 'Failed to send verification email'
return res.redirect(`/inbox/${req.params.address}/${uid}`)
}
} }
} catch (error) { } catch (error) {
debug(`Error forwarding email ${req.params.uid}: ${error.message}`) debug(`Error forwarding email ${req.params.uid}: ${error.message}`)
@ -472,7 +511,38 @@ router.post(
const mailProcessingService = req.app.get('mailProcessingService') const mailProcessingService = req.app.get('mailProcessingService')
const { destinationEmail } = req.body const { destinationEmail } = req.body
debug(`Forwarding all emails from ${req.params.address} to ${destinationEmail}`) // Check if destination email is verified via signed cookie
const verifiedEmail = req.signedCookies.verified_email
if (!verifiedEmail || verifiedEmail.toLowerCase() !== destinationEmail.toLowerCase()) {
// Email not verified, initiate verification flow
debug(`Email ${destinationEmail} not verified, initiating verification for forward-all`)
const mailSummaries = await mailProcessingService.getMailSummaries(req.params.address)
const uids = mailSummaries.map(m => m.uid)
const verificationResult = await mailProcessingService.initiateForwardVerification(
req.params.address,
destinationEmail,
uids
)
if (verificationResult.success) {
debug(`Verification email sent to ${destinationEmail}`)
return res.redirect(`/inbox/${req.params.address}?verificationSent=true&email=${encodeURIComponent(destinationEmail)}`)
} else if (verificationResult.cooldownSeconds) {
debug(`Verification rate limited for ${destinationEmail}`)
req.session.errorMessage = verificationResult.error
return res.redirect(`/inbox/${req.params.address}`)
} else {
debug(`Failed to send verification email: ${verificationResult.error}`)
req.session.errorMessage = verificationResult.error || 'Failed to send verification email'
return res.redirect(`/inbox/${req.params.address}`)
}
}
// Email is verified, proceed with bulk forwarding
debug(`Forwarding all emails from ${req.params.address} to ${destinationEmail} (verified)`)
const mailSummaries = await mailProcessingService.getMailSummaries(req.params.address) const mailSummaries = await mailProcessingService.getMailSummaries(req.params.address)
@ -542,5 +612,78 @@ router.get(
} }
) )
// GET route for email verification (token verification)
router.get('/verify', async(req, res, next) => {
try {
const { token } = req.query
if (!token) {
debug('Verification attempt without token')
req.session.errorMessage = 'Verification token is required'
return res.redirect('/')
}
const verificationStore = req.app.get('verificationStore')
if (!verificationStore) {
debug('Verification store not available')
req.session.errorMessage = 'Email verification is not configured'
return res.redirect('/')
}
// Verify the token
const verification = verificationStore.verifyToken(token)
if (!verification) {
debug(`Invalid or expired verification token: ${token}`)
req.session.errorMessage = 'This verification link is invalid or has expired. Please request a new verification email.'
return res.redirect('/')
}
// Token is valid, set signed cookie
const destinationEmail = verification.destinationEmail
const cookieMaxAge = 24 * 60 * 60 * 1000 // 24 hours in milliseconds
res.cookie('verified_email', destinationEmail, {
maxAge: cookieMaxAge,
httpOnly: true,
signed: true,
sameSite: 'lax'
})
debug(`Email ${destinationEmail} verified successfully, cookie set for 24 hours`)
// Redirect to success page
return res.redirect(`/inbox/verify-success?email=${encodeURIComponent(destinationEmail)}`)
} catch (error) {
debug(`Error during verification: ${error.message}`)
console.error('Error during email verification', error)
req.session.errorMessage = 'An error occurred during verification'
res.redirect('/')
}
})
// GET route for verification success page
router.get('/verify-success', async(req, res) => {
const { email } = req.query
if (!email) {
return res.redirect('/')
}
const config = req.app.get('config')
const mailProcessingService = req.app.get('mailProcessingService')
const count = await mailProcessingService.getCount()
const largestUid = await req.app.locals.imapService.getLargestUid()
const totalcount = helper.countElementBuilder(count, largestUid)
res.render('verify-success', {
title: `Email Verified | ${config.http.branding[0]}`,
email: email,
branding: config.http.branding,
purgeTime: purgeTime,
totalcount: totalcount
})
})
module.exports = router module.exports = router

View file

@ -37,6 +37,11 @@
✓ Successfully forwarded {{ forwardAllSuccess }} email(s)! ✓ Successfully forwarded {{ forwardAllSuccess }} email(s)!
</div> </div>
{% endif %} {% endif %}
{% if verificationSent %}
<div class="verification-message">
📧 Verification email sent to <strong>{{ verificationEmail }}</strong>. Please check your inbox and click the verification link (expires in 15 minutes).
</div>
{% endif %}
{% if errorMessage %} {% if errorMessage %}
<div class="unlock-error"> <div class="unlock-error">
{{ errorMessage }} {{ errorMessage }}

View file

@ -28,6 +28,11 @@
✓ Email forwarded successfully! ✓ Email forwarded successfully!
</div> </div>
{% endif %} {% endif %}
{% if verificationSent %}
<div class="verification-message">
📧 Verification email sent to <strong>{{ verificationEmail }}</strong>. Please check your inbox and click the verification link (expires in 15 minutes).
</div>
{% endif %}
<div class="mail-container"> <div class="mail-container">
<div class="mail-header"> <div class="mail-header">
<h1 class="mail-subject">{{ mail.subject }}</h1> <h1 class="mail-subject">{{ mail.subject }}</h1>

View file

@ -0,0 +1,143 @@
{% extends 'layout.twig' %}
{% block header %}
<div class="action-links">
<a href="/" aria-label="Return to home">← Return to Home</a>
<button class="theme-toggle" id="themeToggle" aria-label="Toggle dark/light mode">
<svg class="theme-icon theme-icon-dark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg>
<svg class="theme-icon theme-icon-light" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg>
</button>
</div>
{% endblock %}
{% block body %}
<div class="verification-success-container">
<div class="verification-success-card">
<div class="verification-success-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
<polyline points="22 4 12 14.01 9 11.01"></polyline>
</svg>
</div>
<h1>Email Verified Successfully!</h1>
<div class="verification-details">
<p>You have verified ownership of:</p>
<p class="verified-email">{{ email }}</p>
</div>
<div class="verification-info">
<p>✓ You can now forward emails to this address for the next <strong>24 hours</strong></p>
<p>✓ After 24 hours, you'll need to verify again</p>
<p>✓ You can close this tab and return to your inbox</p>
</div>
<div class="verification-actions">
<a href="/" class="button button-primary">Return to {{ branding[0] }}</a>
</div>
</div>
</div>
<style>
.verification-success-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 60vh;
padding: 2rem;
}
.verification-success-card {
background: var(--background-color);
border: 2px solid var(--success-color, #28a745);
border-radius: 8px;
padding: 3rem 2rem;
max-width: 500px;
width: 100%;
text-align: center;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.verification-success-icon {
width: 80px;
height: 80px;
margin: 0 auto 1.5rem;
color: var(--success-color, #28a745);
}
.verification-success-icon svg {
width: 100%;
height: 100%;
}
.verification-success-card h1 {
color: var(--success-color, #28a745);
margin-bottom: 1.5rem;
font-size: 1.8rem;
}
.verification-details {
margin: 2rem 0;
padding: 1rem;
background: var(--code-background, #f5f5f5);
border-radius: 5px;
}
.verification-details p {
margin: 0.5rem 0;
}
.verified-email {
font-family: monospace;
font-size: 1.1rem;
font-weight: bold;
color: var(--primary-color);
word-break: break-all;
}
.verification-info {
margin: 2rem 0;
text-align: left;
padding: 0 1rem;
}
.verification-info p {
margin: 0.75rem 0;
line-height: 1.6;
}
.verification-actions {
margin-top: 2rem;
}
.button-primary {
display: inline-block;
background: var(--primary-color);
color: white;
padding: 0.75rem 2rem;
border-radius: 5px;
text-decoration: none;
font-weight: 600;
transition: background-color 0.2s;
}
.button-primary:hover {
background: var(--primary-hover-color, #0056b3);
}
@media (max-width: 600px) {
.verification-success-card {
padding: 2rem 1rem;
}
.verification-success-card h1 {
font-size: 1.5rem;
}
}
</style>
{% endblock %}

View file

@ -3,6 +3,7 @@ const http = require('http')
const debug = require('debug')('48hr-email:server') const debug = require('debug')('48hr-email:server')
const express = require('express') const express = require('express')
const session = require('express-session') const session = require('express-session')
const cookieParser = require('cookie-parser')
const logger = require('morgan') const logger = require('morgan')
const Twig = require('twig') const Twig = require('twig')
const compression = require('compression') const compression = require('compression')
@ -40,20 +41,20 @@ app.use(logger('dev'))
app.use(express.json()) app.use(express.json())
app.use(express.urlencoded({ extended: false })) app.use(express.urlencoded({ extended: false }))
// Session support for inbox locking // Cookie parser for signed cookies (email verification)
if (config.lock.enabled) { app.use(cookieParser(config.lock.sessionSecret))
const session = require('express-session')
app.use(session({ // Session support (always enabled for forward verification and inbox locking)
secret: config.lock.sessionSecret, app.use(session({
resave: false, secret: config.lock.sessionSecret,
saveUninitialized: false, resave: false,
cookie: { maxAge: 24 * 60 * 60 * 1000 } // 24 hours saveUninitialized: false,
})) cookie: { maxAge: 24 * 60 * 60 * 1000 } // 24 hours
} }))
// Clear session when user goes Home so locked inboxes require password again // Clear session when user goes Home so locked inboxes require password again
app.get('/', (req, res, next) => { app.get('/', (req, res, next) => {
if (config.lock.enabled && req.session) { if (req.session) {
req.session.destroy(() => next()) req.session.destroy(() => next())
} else { } else {
next() next()
@ -149,4 +150,4 @@ server.on('listening', () => {
debug('Listening on ' + bind) debug('Listening on ' + bind)
}) })
module.exports = { app, io, server } module.exports = { app, io, server }

20
package-lock.json generated
View file

@ -13,6 +13,7 @@
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"better-sqlite3": "^12.5.0", "better-sqlite3": "^12.5.0",
"compression": "^1.8.1", "compression": "^1.8.1",
"cookie-parser": "^1.4.7",
"debug": "^4.4.3", "debug": "^4.4.3",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"express": "^4.22.1", "express": "^4.22.1",
@ -1837,6 +1838,25 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/cookie-parser": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
"integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
"license": "MIT",
"dependencies": {
"cookie": "0.7.2",
"cookie-signature": "1.0.6"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/cookie-parser/node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"license": "MIT"
},
"node_modules/cookie-signature": { "node_modules/cookie-signature": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",

View file

@ -1,6 +1,6 @@
{ {
"name": "48hr.email", "name": "48hr.email",
"version": "1.8.1", "version": "1.8.2",
"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": [
@ -34,6 +34,7 @@
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"better-sqlite3": "^12.5.0", "better-sqlite3": "^12.5.0",
"compression": "^1.8.1", "compression": "^1.8.1",
"cookie-parser": "^1.4.7",
"debug": "^4.4.3", "debug": "^4.4.3",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"express": "^4.22.1", "express": "^4.22.1",
@ -67,17 +68,15 @@
} }
] ]
}, },
"overrides": [ "overrides": [{
{ "files": "public/javascripts/*.js",
"files": "public/javascripts/*.js", "esnext": false,
"esnext": false, "env": [
"env": [ "browser"
"browser" ],
], "globals": [
"globals": [ "io"
"io" ]
] }]
}
]
} }
} }