mirror of
https://github.com/Crazyco-xyz/48hr.email.git
synced 2026-01-09 11:19:36 +01:00
[Feat]: Add email validation function
Currently only used for forwarding
This commit is contained in:
parent
8daa0fefe9
commit
2a08aa14a8
15 changed files with 793 additions and 43 deletions
|
|
@ -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
8
app.js
|
|
@ -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')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 => {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
161
domain/verification-store.js
Normal file
161
domain/verification-store.js
Normal 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
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 }}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
143
infrastructure/web/views/verify-success.twig
Normal file
143
infrastructure/web/views/verify-success.twig
Normal 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 %}
|
||||||
|
|
@ -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
20
package-lock.json
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
25
package.json
25
package.json
|
|
@ -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"
|
]
|
||||||
]
|
}]
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue