mirror of
https://github.com/Crazyco-xyz/48hr.email.git
synced 2026-01-09 11:19:36 +01:00
[Feat]: Add email forwarding
Uses seperate SMTP credentials for forwarding. This is just the raw system, validation will be in the next commit.
This commit is contained in:
parent
cdce7e1e46
commit
8daa0fefe9
14 changed files with 791 additions and 41 deletions
|
|
@ -23,6 +23,15 @@ IMAP_REFRESH_INTERVAL_SECONDS=60 # Refresh interv
|
||||||
IMAP_FETCH_CHUNK=200 # Number of UIDs per fetch chunk during initial load
|
IMAP_FETCH_CHUNK=200 # Number of UIDs per fetch chunk during initial load
|
||||||
IMAP_CONCURRENCY=6 # Number of concurrent fetch workers during initial load
|
IMAP_CONCURRENCY=6 # Number of concurrent fetch workers during initial load
|
||||||
|
|
||||||
|
# --- SMTP CONFIGURATION (for email forwarding) ---
|
||||||
|
SMTP_ENABLED=false # Enable SMTP forwarding functionality (default: false)
|
||||||
|
SMTP_HOST="smtp.example.com" # SMTP server address (e.g., smtp.gmail.com, smtp.sendgrid.net)
|
||||||
|
SMTP_PORT=465 # SMTP port (587 for TLS, 465 for SSL, 25 for unencrypted)
|
||||||
|
SMTP_SECURE=true # Use SSL (true for port 465, false for other ports)
|
||||||
|
SMTP_USER="noreply@48hr.email" # SMTP authentication username (also used as from address)
|
||||||
|
SMTP_PASSWORD="password" # SMTP authentication password
|
||||||
|
SMTP_FROM_NAME="48hr Email Service" # Display name for forwarded emails
|
||||||
|
|
||||||
# --- HTTP / WEB CONFIGURATION ---
|
# --- HTTP / WEB CONFIGURATION ---
|
||||||
HTTP_PORT=3000 # Port
|
HTTP_PORT=3000 # Port
|
||||||
HTTP_BRANDING=["48hr.email","CrazyCo","https://crazyco.xyz"] # ['service_title', 'company_name', 'company_url']
|
HTTP_BRANDING=["48hr.email","CrazyCo","https://crazyco.xyz"] # ['service_title', 'company_name', 'company_url']
|
||||||
|
|
|
||||||
11
README.md
11
README.md
|
|
@ -65,7 +65,7 @@ All data is being removed 48hrs after they have reached the mail server.
|
||||||
## How can I set this up myself?
|
## How can I set this up myself?
|
||||||
|
|
||||||
**Prerequisites:**
|
**Prerequisites:**
|
||||||
- Mail server with IMAP
|
- Mail server with IMAP (Optionally also SMTP for email forwarding feature)
|
||||||
- One or multiple domains dedicated to this
|
- One or multiple domains dedicated to this
|
||||||
- git & nodejs
|
- git & nodejs
|
||||||
|
|
||||||
|
|
@ -134,15 +134,6 @@ If desired, you can also move the config file somewhere else (change volume moun
|
||||||
|
|
||||||
-----
|
-----
|
||||||
|
|
||||||
## TODO (PRs welcome)
|
|
||||||
|
|
||||||
- Add user registration:
|
|
||||||
- Allow people to forward single emails, or an inbox in its current state
|
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
-----
|
|
||||||
|
|
||||||
## Support me
|
## Support me
|
||||||
|
|
||||||
If you find this project useful, consider supporting its development!
|
If you find this project useful, consider supporting its development!
|
||||||
|
|
|
||||||
10
app.js
10
app.js
|
|
@ -9,6 +9,7 @@ const { app, io, server } = require('./infrastructure/web/web')
|
||||||
const ClientNotification = require('./infrastructure/web/client-notification')
|
const ClientNotification = require('./infrastructure/web/client-notification')
|
||||||
const ImapService = require('./application/imap-service')
|
const ImapService = require('./application/imap-service')
|
||||||
const MailProcessingService = require('./application/mail-processing-service')
|
const MailProcessingService = require('./application/mail-processing-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')
|
||||||
|
|
||||||
|
|
@ -35,11 +36,16 @@ if (config.lock.enabled) {
|
||||||
|
|
||||||
const imapService = new ImapService(config, inboxLock)
|
const imapService = new ImapService(config, inboxLock)
|
||||||
debug('IMAP service initialized')
|
debug('IMAP service initialized')
|
||||||
|
|
||||||
|
const smtpService = new SmtpService(config)
|
||||||
|
debug('SMTP service initialized')
|
||||||
|
|
||||||
const mailProcessingService = new MailProcessingService(
|
const mailProcessingService = new MailProcessingService(
|
||||||
new MailRepository(),
|
new MailRepository(),
|
||||||
imapService,
|
imapService,
|
||||||
clientNotification,
|
clientNotification,
|
||||||
config
|
config,
|
||||||
|
smtpService
|
||||||
)
|
)
|
||||||
debug('Mail processing service initialized')
|
debug('Mail processing service initialized')
|
||||||
|
|
||||||
|
|
@ -113,4 +119,4 @@ server.on('error', error => {
|
||||||
console.error('Fatal web server error', error)
|
console.error('Fatal web server error', error)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,16 @@ const config = {
|
||||||
fetchConcurrency: Number(process.env.IMAP_CONCURRENCY) || 6
|
fetchConcurrency: Number(process.env.IMAP_CONCURRENCY) || 6
|
||||||
},
|
},
|
||||||
|
|
||||||
|
smtp: {
|
||||||
|
enabled: parseBool(process.env.SMTP_ENABLED) || false,
|
||||||
|
host: parseValue(process.env.SMTP_HOST),
|
||||||
|
port: Number(process.env.SMTP_PORT) || 465,
|
||||||
|
secure: parseBool(process.env.SMTP_SECURE) || true,
|
||||||
|
user: parseValue(process.env.SMTP_USER),
|
||||||
|
password: parseValue(process.env.SMTP_PASSWORD),
|
||||||
|
fromName: parseValue(process.env.SMTP_FROM_NAME) || '48hr.email Forwarding'
|
||||||
|
},
|
||||||
|
|
||||||
http: {
|
http: {
|
||||||
port: Number(process.env.HTTP_PORT),
|
port: Number(process.env.HTTP_PORT),
|
||||||
branding: parseValue(process.env.HTTP_BRANDING),
|
branding: parseValue(process.env.HTTP_BRANDING),
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,13 @@ const helper = new(Helper)
|
||||||
|
|
||||||
|
|
||||||
class MailProcessingService extends EventEmitter {
|
class MailProcessingService extends EventEmitter {
|
||||||
constructor(mailRepository, imapService, clientNotification, config) {
|
constructor(mailRepository, imapService, clientNotification, config, smtpService = 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
|
||||||
|
|
||||||
// Cached methods:
|
// Cached methods:
|
||||||
this._initCache()
|
this._initCache()
|
||||||
|
|
@ -214,6 +215,68 @@ class MailProcessingService extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forward an email to a destination address
|
||||||
|
* @param {string} address - The recipient address of the email to forward
|
||||||
|
* @param {number|string} uid - The UID of the email to forward
|
||||||
|
* @param {string} destinationEmail - The email address to forward to
|
||||||
|
* @returns {Promise<{success: boolean, error?: string, messageId?: string}>}
|
||||||
|
*/
|
||||||
|
async forwardEmail(address, uid, destinationEmail) {
|
||||||
|
// Check if SMTP service is available
|
||||||
|
if (!this.smtpService) {
|
||||||
|
debug('Forward attempt failed: SMTP service not configured')
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Email forwarding is not configured. Please configure SMTP settings.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if email exists in repository
|
||||||
|
const mailSummary = this.mailRepository.getForRecipient(address)
|
||||||
|
.find(mail => parseInt(mail.uid) === parseInt(uid))
|
||||||
|
|
||||||
|
if (!mailSummary) {
|
||||||
|
debug(`Forward attempt failed: Email not found (address: ${address}, uid: ${uid})`)
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Email not found'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch full email content using cached method
|
||||||
|
debug(`Fetching full email for forwarding (address: ${address}, uid: ${uid})`)
|
||||||
|
const fullMail = await this.getOneFullMail(address, uid, false)
|
||||||
|
|
||||||
|
if (!fullMail) {
|
||||||
|
debug('Forward attempt failed: Could not fetch full email')
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Could not retrieve email content'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward via SMTP service
|
||||||
|
debug(`Forwarding email to ${destinationEmail}`)
|
||||||
|
const result = await this.smtpService.forwardMail(fullMail, destinationEmail)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
debug(`Email forwarded successfully. MessageId: ${result.messageId}`)
|
||||||
|
} else {
|
||||||
|
debug(`Email forwarding failed: ${result.error}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
} catch (error) {
|
||||||
|
debug('Error forwarding email:', error.message)
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Failed to forward 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 => {
|
||||||
|
|
|
||||||
222
application/smtp-service.js
Normal file
222
application/smtp-service.js
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
const nodemailer = require('nodemailer')
|
||||||
|
const debug = require('debug')('48hr-email:smtp-service')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SMTP Service for forwarding emails
|
||||||
|
* Uses nodemailer to send forwarded emails via configured SMTP server
|
||||||
|
*/
|
||||||
|
class SmtpService {
|
||||||
|
constructor(config) {
|
||||||
|
this.config = config
|
||||||
|
this.transporter = null
|
||||||
|
|
||||||
|
// Only initialize transporter if SMTP is configured
|
||||||
|
if (this._isConfigured()) {
|
||||||
|
this._initializeTransporter()
|
||||||
|
} else {
|
||||||
|
debug('SMTP not configured - forwarding functionality will be unavailable')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if SMTP is properly configured
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
_isConfigured() {
|
||||||
|
return !!(
|
||||||
|
this.config.smtp.enabled &&
|
||||||
|
this.config.smtp.host &&
|
||||||
|
this.config.smtp.user &&
|
||||||
|
this.config.smtp.password
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the nodemailer transporter
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_initializeTransporter() {
|
||||||
|
try {
|
||||||
|
this.transporter = nodemailer.createTransport({
|
||||||
|
host: this.config.smtp.host,
|
||||||
|
port: this.config.smtp.port,
|
||||||
|
secure: this.config.smtp.secure,
|
||||||
|
auth: {
|
||||||
|
user: this.config.smtp.user,
|
||||||
|
pass: this.config.smtp.password
|
||||||
|
},
|
||||||
|
tls: {
|
||||||
|
// Allow self-signed certificates and skip verification
|
||||||
|
// This is useful for development or internal SMTP servers
|
||||||
|
rejectUnauthorized: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
debug(`SMTP transporter initialized: ${this.config.smtp.host}:${this.config.smtp.port}`)
|
||||||
|
} catch (error) {
|
||||||
|
debug('Failed to initialize SMTP transporter:', error.message)
|
||||||
|
throw new Error(`SMTP initialization failed: ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forward an email to a destination address
|
||||||
|
* @param {Object} mail - Parsed email object from mailparser
|
||||||
|
* @param {string} destinationEmail - Email address to forward to
|
||||||
|
* @returns {Promise<{success: boolean, error?: string, messageId?: string}>}
|
||||||
|
*/
|
||||||
|
async forwardMail(mail, destinationEmail) {
|
||||||
|
if (!this.transporter) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'SMTP is not configured. Please configure SMTP settings to enable forwarding.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mail) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Email not found'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
debug(`Forwarding email (Subject: "${mail.subject}") to ${destinationEmail}`)
|
||||||
|
|
||||||
|
const forwardMessage = this._buildForwardMessage(mail, destinationEmail)
|
||||||
|
|
||||||
|
const info = await this.transporter.sendMail(forwardMessage)
|
||||||
|
|
||||||
|
debug(`Email forwarded successfully. MessageId: ${info.messageId}`)
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
messageId: info.messageId
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
debug('Failed to forward email:', error.message)
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Failed to send email: ${error.message}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the forward message structure
|
||||||
|
* @param {Object} mail - Parsed email object
|
||||||
|
* @param {string} destinationEmail - Destination address
|
||||||
|
* @returns {Object} - Nodemailer message object
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_buildForwardMessage(mail, destinationEmail) {
|
||||||
|
// Extract original sender info
|
||||||
|
const originalFrom = (mail.from && mail.from.text) || 'Unknown Sender'
|
||||||
|
const originalTo = (mail.to && mail.to.text) || 'Unknown Recipient'
|
||||||
|
const originalDate = mail.date ? new Date(mail.date).toLocaleString() : 'Unknown Date'
|
||||||
|
const originalSubject = mail.subject || '(no subject)'
|
||||||
|
|
||||||
|
// Build forwarded message body
|
||||||
|
let forwardedBody = `
|
||||||
|
---------- Forwarded message ----------
|
||||||
|
From: ${originalFrom}
|
||||||
|
Date: ${originalDate}
|
||||||
|
Subject: ${originalSubject}
|
||||||
|
To: ${originalTo}
|
||||||
|
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
// Add original text body if available
|
||||||
|
if (mail.text) {
|
||||||
|
forwardedBody += mail.text
|
||||||
|
} else if (mail.html) {
|
||||||
|
// If only HTML is available, mention it
|
||||||
|
forwardedBody += '[This email contains HTML content. See attachment or HTML version below.]\n\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the message object
|
||||||
|
const message = {
|
||||||
|
from: {
|
||||||
|
name: this.config.smtp.fromName,
|
||||||
|
address: this.config.smtp.user
|
||||||
|
},
|
||||||
|
to: destinationEmail,
|
||||||
|
subject: `Fwd: ${originalSubject}`,
|
||||||
|
text: forwardedBody,
|
||||||
|
replyTo: originalFrom
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add HTML body if available
|
||||||
|
if (mail.html) {
|
||||||
|
const htmlForwardedBody = `
|
||||||
|
<div style="border-left: 2px solid #ccc; padding-left: 10px; margin: 10px 0;">
|
||||||
|
<p><strong>---------- Forwarded message ----------</strong><br>
|
||||||
|
<strong>From:</strong> ${this._escapeHtml(originalFrom)}<br>
|
||||||
|
<strong>Date:</strong> ${this._escapeHtml(originalDate)}<br>
|
||||||
|
<strong>Subject:</strong> ${this._escapeHtml(originalSubject)}<br>
|
||||||
|
<strong>To:</strong> ${this._escapeHtml(originalTo)}</p>
|
||||||
|
</div>
|
||||||
|
${mail.html}
|
||||||
|
`
|
||||||
|
message.html = htmlForwardedBody
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add attachments if present
|
||||||
|
if (mail.attachments && mail.attachments.length > 0) {
|
||||||
|
message.attachments = mail.attachments.map(att => ({
|
||||||
|
filename: att.filename || 'attachment',
|
||||||
|
content: att.content,
|
||||||
|
contentType: att.contentType,
|
||||||
|
contentDisposition: att.contentDisposition || 'attachment'
|
||||||
|
}))
|
||||||
|
|
||||||
|
debug(`Including ${mail.attachments.length} attachment(s) in forwarded email`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple HTML escape for email headers
|
||||||
|
* @param {string} text
|
||||||
|
* @returns {string}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_escapeHtml(text) {
|
||||||
|
if (!text) return ''
|
||||||
|
return String(text)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify SMTP connection
|
||||||
|
* @returns {Promise<{success: boolean, error?: string}>}
|
||||||
|
*/
|
||||||
|
async verifyConnection() {
|
||||||
|
if (!this.transporter) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'SMTP is not configured'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.transporter.verify()
|
||||||
|
debug('SMTP connection verified successfully')
|
||||||
|
return { success: true }
|
||||||
|
} catch (error) {
|
||||||
|
debug('SMTP connection verification failed:', error.message)
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SmtpService
|
||||||
|
|
@ -22,4 +22,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
if (window.utils && typeof window.utils.initRefreshCountdown === 'function' && refreshInterval) {
|
if (window.utils && typeof window.utils.initRefreshCountdown === 'function' && refreshInterval) {
|
||||||
window.utils.initRefreshCountdown(refreshInterval);
|
window.utils.initRefreshCountdown(refreshInterval);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
// Initialize forward all modal
|
||||||
|
if (window.utils && typeof window.utils.initForwardAllModal === 'function') {
|
||||||
|
window.utils.initForwardAllModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -361,6 +361,173 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initForwardModal() {
|
||||||
|
const forwardModal = document.getElementById('forwardModal');
|
||||||
|
const forwardBtn = document.getElementById('forwardBtn');
|
||||||
|
const closeForward = document.getElementById('closeForward');
|
||||||
|
const forwardForm = document.querySelector('#forwardModal form');
|
||||||
|
const forwardEmail = document.getElementById('forwardEmail');
|
||||||
|
const forwardError = document.getElementById('forwardError');
|
||||||
|
|
||||||
|
if (!forwardModal || !forwardBtn) return;
|
||||||
|
|
||||||
|
const openModal = (m) => { if (m) m.style.display = 'block'; };
|
||||||
|
const closeModal = (m) => { if (m) m.style.display = 'none'; };
|
||||||
|
|
||||||
|
forwardBtn.onclick = function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
openModal(forwardModal);
|
||||||
|
if (forwardEmail) forwardEmail.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (closeForward) {
|
||||||
|
closeForward.onclick = function() {
|
||||||
|
closeModal(forwardModal);
|
||||||
|
if (forwardError) forwardError.style.display = 'none';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forwardForm) {
|
||||||
|
forwardForm.addEventListener('submit', function(e) {
|
||||||
|
const email = forwardEmail ? forwardEmail.value.trim() : '';
|
||||||
|
|
||||||
|
// Basic client-side validation
|
||||||
|
if (!email) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (forwardError) {
|
||||||
|
forwardError.textContent = 'Please enter an email address.';
|
||||||
|
forwardError.style.display = 'block';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple email format validation
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
if (!emailRegex.test(email)) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (forwardError) {
|
||||||
|
forwardError.textContent = 'Please enter a valid email address.';
|
||||||
|
forwardError.style.display = 'block';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forwardError) forwardError.style.display = 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('click', function(e) {
|
||||||
|
if (e.target === forwardModal) {
|
||||||
|
closeModal(forwardModal);
|
||||||
|
if (forwardError) forwardError.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if there's a server error and re-open modal
|
||||||
|
const serverError = forwardModal.querySelector('.unlock-error');
|
||||||
|
if (serverError && serverError.textContent.trim() && !serverError.id) {
|
||||||
|
openModal(forwardModal);
|
||||||
|
if (forwardEmail) forwardEmail.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for success message in URL
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
if (urlParams.get('forwarded') === 'true') {
|
||||||
|
// Remove the query param from URL without reload
|
||||||
|
const newUrl = window.location.pathname;
|
||||||
|
window.history.replaceState({}, '', newUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initForwardAllModal() {
|
||||||
|
const forwardAllModal = document.getElementById('forwardAllModal');
|
||||||
|
const forwardAllBtn = document.getElementById('forwardAllBtn');
|
||||||
|
const closeForwardAll = document.getElementById('closeForwardAll');
|
||||||
|
const forwardAllForm = document.querySelector('#forwardAllModal form');
|
||||||
|
const forwardAllEmail = document.getElementById('forwardAllEmail');
|
||||||
|
const forwardAllError = document.getElementById('forwardAllError');
|
||||||
|
|
||||||
|
if (!forwardAllModal || !forwardAllBtn) return;
|
||||||
|
|
||||||
|
const openModal = (m) => { if (m) m.style.display = 'block'; };
|
||||||
|
const closeModal = (m) => { if (m) m.style.display = 'none'; };
|
||||||
|
|
||||||
|
forwardAllBtn.onclick = function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
openModal(forwardAllModal);
|
||||||
|
if (forwardAllEmail) forwardAllEmail.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (closeForwardAll) {
|
||||||
|
closeForwardAll.onclick = function() {
|
||||||
|
closeModal(forwardAllModal);
|
||||||
|
if (forwardAllError) forwardAllError.style.display = 'none';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forwardAllForm) {
|
||||||
|
forwardAllForm.addEventListener('submit', function(e) {
|
||||||
|
const email = forwardAllEmail ? forwardAllEmail.value.trim() : '';
|
||||||
|
|
||||||
|
// Basic client-side validation
|
||||||
|
if (!email) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (forwardAllError) {
|
||||||
|
forwardAllError.textContent = 'Please enter an email address.';
|
||||||
|
forwardAllError.style.display = 'block';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple email format validation
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
if (!emailRegex.test(email)) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (forwardAllError) {
|
||||||
|
forwardAllError.textContent = 'Please enter a valid email address.';
|
||||||
|
forwardAllError.style.display = 'block';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forwardAllError) forwardAllError.style.display = 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('click', function(e) {
|
||||||
|
if (e.target === forwardAllModal) {
|
||||||
|
closeModal(forwardAllModal);
|
||||||
|
if (forwardAllError) forwardAllError.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check for success message in URL
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
// Check if we just came from a forward all attempt with an error
|
||||||
|
// Look for error message in the page that might be related to forwarding
|
||||||
|
const pageErrorDiv = document.querySelector('.unlock-error');
|
||||||
|
if (pageErrorDiv && !urlParams.get('forwardedAll')) {
|
||||||
|
const errorText = pageErrorDiv.textContent.trim();
|
||||||
|
// Check if error is related to forwarding
|
||||||
|
if (errorText.includes('forward') || errorText.includes('email') || errorText.includes('25')) {
|
||||||
|
openModal(forwardAllModal);
|
||||||
|
if (forwardAllEmail) forwardAllEmail.focus();
|
||||||
|
// Move error into modal
|
||||||
|
if (forwardAllError) {
|
||||||
|
forwardAllError.textContent = errorText;
|
||||||
|
forwardAllError.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (urlParams.get('forwardedAll')) {
|
||||||
|
// Remove the query param from URL without reload
|
||||||
|
const newUrl = window.location.pathname;
|
||||||
|
window.history.replaceState({}, '', newUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function initRefreshCountdown(refreshInterval) {
|
function initRefreshCountdown(refreshInterval) {
|
||||||
const refreshTimer = document.getElementById('refreshTimer');
|
const refreshTimer = document.getElementById('refreshTimer');
|
||||||
if (!refreshTimer || !refreshInterval) return;
|
if (!refreshTimer || !refreshInterval) return;
|
||||||
|
|
@ -377,7 +544,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expose utilities and run them
|
// Expose utilities and run them
|
||||||
window.utils = { formatEmailDates, formatMailDate, initLockModals, initCopyAddress, initExpiryTimers, initQrModal, initHamburgerMenu, initThemeToggle, initRefreshCountdown, initCryptoKeysToggle };
|
window.utils = { formatEmailDates, formatMailDate, initLockModals, initCopyAddress, initExpiryTimers, initQrModal, initHamburgerMenu, initThemeToggle, initRefreshCountdown, initCryptoKeysToggle, initForwardModal, initForwardAllModal };
|
||||||
formatEmailDates();
|
formatEmailDates();
|
||||||
formatMailDate();
|
formatMailDate();
|
||||||
initLockModals();
|
initLockModals();
|
||||||
|
|
@ -385,4 +552,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
initQrModal();
|
initQrModal();
|
||||||
initHamburgerMenu();
|
initHamburgerMenu();
|
||||||
initThemeToggle();
|
initThemeToggle();
|
||||||
});
|
initForwardModal();
|
||||||
|
initCryptoKeysToggle();
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -931,6 +931,16 @@ label {
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-info {
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 0.8rem;
|
||||||
|
background: var(--overlay-purple-08);
|
||||||
|
border-left: 3px solid var(--color-accent-purple);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
.close {
|
.close {
|
||||||
float: right;
|
float: right;
|
||||||
font-size: 2.8rem;
|
font-size: 2.8rem;
|
||||||
|
|
@ -993,6 +1003,19 @@ label {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Success Messages */
|
||||||
|
|
||||||
|
.success-message {
|
||||||
|
color: #2ecc71;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: rgba(46, 204, 113, 0.1);
|
||||||
|
border-left: 3px solid #2ecc71;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Remove Lock Button Styles */
|
/* Remove Lock Button Styles */
|
||||||
|
|
||||||
.modal-button-danger {
|
.modal-button-danger {
|
||||||
|
|
@ -1289,4 +1312,4 @@ body.light-mode .theme-icon-light {
|
||||||
.qr-icon-btn {
|
.qr-icon-btn {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
const express = require('express')
|
const express = require('express')
|
||||||
const router = new express.Router()
|
const router = new express.Router()
|
||||||
const { param } = require('express-validator')
|
const { param, body, validationResult } = require('express-validator')
|
||||||
const debug = require('debug')('48hr-email:routes')
|
const debug = require('debug')('48hr-email:routes')
|
||||||
|
|
||||||
const config = require('../../../application/config')
|
const config = require('../../../application/config')
|
||||||
|
|
@ -40,6 +40,63 @@ const validateDomain = (req, res, next) => {
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Simple in-memory rate limiter for forwarding (5 requests per 15 minutes per IP)
|
||||||
|
const forwardRateLimitStore = new Map()
|
||||||
|
const forwardLimiter = (req, res, next) => {
|
||||||
|
const ip = req.ip || req.connection.remoteAddress
|
||||||
|
const now = Date.now()
|
||||||
|
const windowMs = 15 * 60 * 1000 // 15 minutes
|
||||||
|
const maxRequests = 5
|
||||||
|
|
||||||
|
// Clean up old entries
|
||||||
|
for (const [key, data] of forwardRateLimitStore.entries()) {
|
||||||
|
if (now - data.resetTime > windowMs) {
|
||||||
|
forwardRateLimitStore.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get or create entry for this IP
|
||||||
|
let ipData = forwardRateLimitStore.get(ip)
|
||||||
|
if (!ipData || now - ipData.resetTime > windowMs) {
|
||||||
|
ipData = { count: 0, resetTime: now }
|
||||||
|
forwardRateLimitStore.set(ip, ipData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if limit exceeded
|
||||||
|
if (ipData.count >= maxRequests) {
|
||||||
|
debug(`Rate limit exceeded for IP ${ip}`)
|
||||||
|
req.session.errorMessage = 'Too many forward requests. Please try again after 15 minutes.'
|
||||||
|
return res.redirect(`/inbox/${req.params.address}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment counter
|
||||||
|
ipData.count++
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email validation middleware for forwarding
|
||||||
|
const validateForwardRequest = [
|
||||||
|
sanitizeAddress,
|
||||||
|
body('destinationEmail')
|
||||||
|
.trim()
|
||||||
|
.isEmail()
|
||||||
|
.withMessage('Invalid email address format')
|
||||||
|
.normalizeEmail()
|
||||||
|
.custom((value) => {
|
||||||
|
// Prevent forwarding to temporary email addresses
|
||||||
|
const domain = value.split('@')[1]
|
||||||
|
if (!domain) {
|
||||||
|
throw new Error('Invalid email address')
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempDomains = config.email.domains.map(d => d.toLowerCase())
|
||||||
|
if (tempDomains.includes(domain.toLowerCase())) {
|
||||||
|
throw new Error('Cannot forward to temporary email addresses')
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
]
|
||||||
|
|
||||||
router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, validateDomain, checkLockAccess, async(req, res, next) => {
|
router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, validateDomain, checkLockAccess, async(req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const mailProcessingService = req.app.get('mailProcessingService')
|
const mailProcessingService = req.app.get('mailProcessingService')
|
||||||
|
|
@ -58,11 +115,16 @@ router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, validateDomain, checkLo
|
||||||
// Pull any lock error from session and clear it after reading
|
// Pull any lock error from session and clear it after reading
|
||||||
const lockError = req.session ? req.session.lockError : undefined
|
const lockError = req.session ? req.session.lockError : undefined
|
||||||
const unlockErrorSession = req.session ? req.session.unlockError : undefined
|
const unlockErrorSession = req.session ? req.session.unlockError : undefined
|
||||||
|
const errorMessage = req.session ? req.session.errorMessage : undefined
|
||||||
if (req.session) {
|
if (req.session) {
|
||||||
delete req.session.lockError
|
delete req.session.lockError
|
||||||
delete req.session.unlockError
|
delete req.session.unlockError
|
||||||
|
delete req.session.errorMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for forward all success flag
|
||||||
|
const forwardAllSuccess = req.query.forwardedAll ? parseInt(req.query.forwardedAll) : null
|
||||||
|
|
||||||
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,
|
||||||
|
|
@ -80,7 +142,9 @@ router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, validateDomain, checkLo
|
||||||
redirectTo: req.originalUrl,
|
redirectTo: req.originalUrl,
|
||||||
expiryTime: config.email.purgeTime.time,
|
expiryTime: config.email.purgeTime.time,
|
||||||
expiryUnit: config.email.purgeTime.unit,
|
expiryUnit: config.email.purgeTime.unit,
|
||||||
refreshInterval: config.imap.refreshIntervalSeconds
|
refreshInterval: config.imap.refreshIntervalSeconds,
|
||||||
|
errorMessage: errorMessage,
|
||||||
|
forwardAllSuccess: forwardAllSuccess
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debug(`Error loading inbox for ${req.params.address}:`, error.message)
|
debug(`Error loading inbox for ${req.params.address}:`, error.message)
|
||||||
|
|
@ -122,6 +186,15 @@ router.get(
|
||||||
const isLocked = inboxLock && inboxLock.isLocked(req.params.address)
|
const isLocked = inboxLock && inboxLock.isLocked(req.params.address)
|
||||||
const hasAccess = req.session && req.session.lockedInbox === req.params.address
|
const hasAccess = req.session && req.session.lockedInbox === req.params.address
|
||||||
|
|
||||||
|
// Pull error message from session and clear it
|
||||||
|
const errorMessage = req.session ? req.session.errorMessage : undefined
|
||||||
|
if (req.session) {
|
||||||
|
delete req.session.errorMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for forward success flag
|
||||||
|
const forwardSuccess = req.query.forwarded === 'true'
|
||||||
|
|
||||||
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,
|
||||||
|
|
@ -135,7 +208,9 @@ router.get(
|
||||||
branding: config.http.branding,
|
branding: config.http.branding,
|
||||||
lockEnabled: config.lock.enabled,
|
lockEnabled: config.lock.enabled,
|
||||||
isLocked: isLocked,
|
isLocked: isLocked,
|
||||||
hasAccess: hasAccess
|
hasAccess: hasAccess,
|
||||||
|
errorMessage: errorMessage,
|
||||||
|
forwardSuccess: forwardSuccess
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
debug(`Email ${req.params.uid} not found for ${req.params.address}`)
|
debug(`Email ${req.params.uid} not found for ${req.params.address}`)
|
||||||
|
|
@ -331,6 +406,131 @@ router.get(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// POST route for forwarding a single email
|
||||||
|
router.post(
|
||||||
|
'^/:address/:uid/forward',
|
||||||
|
forwardLimiter,
|
||||||
|
validateDomain,
|
||||||
|
checkLockAccess,
|
||||||
|
validateForwardRequest,
|
||||||
|
async(req, res, next) => {
|
||||||
|
try {
|
||||||
|
const errors = validationResult(req)
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
const firstError = errors.array()[0].msg
|
||||||
|
debug(`Forward validation failed for ${req.params.address}: ${firstError}`)
|
||||||
|
req.session.errorMessage = firstError
|
||||||
|
return res.redirect(`/inbox/${req.params.address}/${req.params.uid}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mailProcessingService = req.app.get('mailProcessingService')
|
||||||
|
const { destinationEmail } = req.body
|
||||||
|
const uid = parseInt(req.params.uid, 10)
|
||||||
|
|
||||||
|
debug(`Forwarding email ${uid} from ${req.params.address} to ${destinationEmail}`)
|
||||||
|
|
||||||
|
const result = await mailProcessingService.forwardEmail(
|
||||||
|
req.params.address,
|
||||||
|
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}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
debug(`Error forwarding email ${req.params.uid}: ${error.message}`)
|
||||||
|
console.error('Error while forwarding email', error)
|
||||||
|
req.session.errorMessage = 'An unexpected error occurred while forwarding the email.'
|
||||||
|
res.redirect(`/inbox/${req.params.address}/${req.params.uid}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// POST route for forwarding all emails in an inbox
|
||||||
|
router.post(
|
||||||
|
'^/:address/forward-all',
|
||||||
|
forwardLimiter,
|
||||||
|
validateDomain,
|
||||||
|
checkLockAccess,
|
||||||
|
validateForwardRequest,
|
||||||
|
async(req, res, next) => {
|
||||||
|
try {
|
||||||
|
const validationErrors = validationResult(req)
|
||||||
|
if (!validationErrors.isEmpty()) {
|
||||||
|
const firstError = validationErrors.array()[0].msg
|
||||||
|
debug(`Forward all validation failed for ${req.params.address}: ${firstError}`)
|
||||||
|
req.session.errorMessage = firstError
|
||||||
|
return res.redirect(`/inbox/${req.params.address}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mailProcessingService = req.app.get('mailProcessingService')
|
||||||
|
const { destinationEmail } = req.body
|
||||||
|
|
||||||
|
debug(`Forwarding all emails from ${req.params.address} to ${destinationEmail}`)
|
||||||
|
|
||||||
|
const mailSummaries = await mailProcessingService.getMailSummaries(req.params.address)
|
||||||
|
|
||||||
|
// Limit bulk forwarding to 25 emails
|
||||||
|
const MAX_FORWARD_ALL = 25
|
||||||
|
if (mailSummaries.length > MAX_FORWARD_ALL) {
|
||||||
|
debug(`Forward all blocked: ${mailSummaries.length} emails exceeds limit of ${MAX_FORWARD_ALL}`)
|
||||||
|
req.session.errorMessage = `Cannot forward more than ${MAX_FORWARD_ALL} emails at once. You have ${mailSummaries.length} emails.`
|
||||||
|
return res.redirect(`/inbox/${req.params.address}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mailSummaries.length === 0) {
|
||||||
|
debug(`No emails to forward for ${req.params.address}`)
|
||||||
|
req.session.errorMessage = 'No emails to forward.'
|
||||||
|
return res.redirect(`/inbox/${req.params.address}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
let successCount = 0
|
||||||
|
let failCount = 0
|
||||||
|
const failMessages = []
|
||||||
|
|
||||||
|
for (const mail of mailSummaries) {
|
||||||
|
const result = await mailProcessingService.forwardEmail(
|
||||||
|
req.params.address,
|
||||||
|
mail.uid,
|
||||||
|
destinationEmail
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
successCount++
|
||||||
|
debug(`Successfully forwarded email UID ${mail.uid}`)
|
||||||
|
} else {
|
||||||
|
failCount++
|
||||||
|
debug(`Failed to forward email UID ${mail.uid}: ${result.error}`)
|
||||||
|
failMessages.push(`UID ${mail.uid}: ${result.error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(`Forward all complete: ${successCount} succeeded, ${failCount} failed`)
|
||||||
|
|
||||||
|
if (successCount > 0 && failCount === 0) {
|
||||||
|
return res.redirect(`/inbox/${req.params.address}?forwardedAll=${successCount}`)
|
||||||
|
} else if (successCount > 0 && failCount > 0) {
|
||||||
|
req.session.errorMessage = `Forwarded ${successCount} email(s), but ${failCount} failed.`
|
||||||
|
return res.redirect(`/inbox/${req.params.address}`)
|
||||||
|
} else {
|
||||||
|
req.session.errorMessage = `Failed to forward emails: ${failMessages[0] || 'Unknown error'}`
|
||||||
|
return res.redirect(`/inbox/${req.params.address}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
debug(`Error forwarding all emails: ${error.message}`)
|
||||||
|
console.error('Error while forwarding all emails', error)
|
||||||
|
req.session.errorMessage = 'An unexpected error occurred while forwarding emails.'
|
||||||
|
res.redirect(`/inbox/${req.params.address}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// Final catch-all for invalid UIDs (non-numeric or unmatched patterns)
|
// Final catch-all for invalid UIDs (non-numeric or unmatched patterns)
|
||||||
router.get(
|
router.get(
|
||||||
'^/:address/:uid',
|
'^/:address/:uid',
|
||||||
|
|
@ -343,4 +543,4 @@ router.get(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
<a href="#" id="lockBtn" aria-label="Protect inbox with password">Protect Inbox</a>
|
<a href="#" id="lockBtn" aria-label="Protect inbox with password">Protect Inbox</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<a href="#" id="forwardAllBtn" aria-label="Forward all emails">Forward All</a>
|
||||||
<a href="/inbox/{{ address }}/delete-all" aria-label="Delete all emails">Wipe Inbox</a>
|
<a href="/inbox/{{ address }}/delete-all" aria-label="Delete all emails">Wipe Inbox</a>
|
||||||
{% if lockEnabled and hasAccess %}
|
{% if lockEnabled and hasAccess %}
|
||||||
<a href="/lock/logout" aria-label="Logout">Logout</a>
|
<a href="/lock/logout" aria-label="Logout">Logout</a>
|
||||||
|
|
@ -31,6 +32,16 @@
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<script src="/javascripts/qrcode.js"></script>
|
<script src="/javascripts/qrcode.js"></script>
|
||||||
<script src="/javascripts/inbox-init.js" defer data-address="{{ address }}" data-expiry-time="{{ expiryTime }}" data-expiry-unit="{{ expiryUnit }}" data-refresh-interval="{{ refreshInterval }}"></script>
|
<script src="/javascripts/inbox-init.js" defer data-address="{{ address }}" data-expiry-time="{{ expiryTime }}" data-expiry-unit="{{ expiryUnit }}" data-refresh-interval="{{ refreshInterval }}"></script>
|
||||||
|
{% if forwardAllSuccess %}
|
||||||
|
<div class="success-message">
|
||||||
|
✓ Successfully forwarded {{ forwardAllSuccess }} email(s)!
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if errorMessage %}
|
||||||
|
<div class="unlock-error">
|
||||||
|
{{ errorMessage }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="inbox-container">
|
<div class="inbox-container">
|
||||||
<div class="inbox-header">
|
<div class="inbox-header">
|
||||||
<h1 class="inbox-title" id="copyAddress" title="Click to copy address">{{ address }}</h1>
|
<h1 class="inbox-title" id="copyAddress" title="Click to copy address">{{ address }}</h1>
|
||||||
|
|
@ -162,4 +173,24 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Forward All Modal -->
|
||||||
|
<div id="forwardAllModal" class="modal" style="display: none;">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="close" id="closeForwardAll">×</span>
|
||||||
|
<h3>Forward All Emails</h3>
|
||||||
|
<p class="modal-description">Enter the email address to forward all emails in this inbox to. Limited to 25 emails maximum.</p>
|
||||||
|
{% if mailSummaries|length > 0 %}
|
||||||
|
<p class="modal-info">You have {{ mailSummaries|length }} email(s) in this inbox.</p>
|
||||||
|
{% endif %}
|
||||||
|
<p id="forwardAllError" class="unlock-error" style="display:none"></p>
|
||||||
|
<form method="POST" action="/inbox/{{ address }}/forward-all">
|
||||||
|
<fieldset>
|
||||||
|
<label for="forwardAllEmail" class="floating-label">Destination Email</label>
|
||||||
|
<input type="email" id="forwardAllEmail" name="destinationEmail"
|
||||||
|
placeholder="recipient@example.com" required class="modal-input">
|
||||||
|
<button type="submit" class="button-primary modal-button">Forward All</button>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
{% block header %}
|
{% block header %}
|
||||||
<div class="action-links">
|
<div class="action-links">
|
||||||
<a href="/inbox/{{ address }}" aria-label="Return to inbox">← Return to inbox</a>
|
<a href="/inbox/{{ address }}" aria-label="Return to inbox">← Return to inbox</a>
|
||||||
|
<a href="#" id="forwardBtn" aria-label="Forward this email">Forward Email</a>
|
||||||
<a href="/inbox/{{ address }}/{{ uid }}/delete" aria-label="Delete this email">Delete Email</a>
|
<a href="/inbox/{{ address }}/{{ uid }}/delete" aria-label="Delete this email">Delete Email</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>
|
||||||
{% if lockEnabled and isLocked and hasAccess %}
|
{% if lockEnabled and isLocked and hasAccess %}
|
||||||
|
|
@ -22,6 +23,11 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
{% if forwardSuccess %}
|
||||||
|
<div class="success-message">
|
||||||
|
✓ Email forwarded successfully!
|
||||||
|
</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>
|
||||||
|
|
@ -88,16 +94,29 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Forward Email Modal -->
|
||||||
|
<div id="forwardModal" class="modal" style="display: none;">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="close" id="closeForward">×</span>
|
||||||
|
<h3>Forward Email</h3>
|
||||||
|
<p class="modal-description">Enter the email address to forward this message to.</p>
|
||||||
|
{% if errorMessage %}
|
||||||
|
<p class="unlock-error">{{ errorMessage }}</p>
|
||||||
|
{% endif %}
|
||||||
|
<p id="forwardError" class="unlock-error" style="display:none"></p>
|
||||||
|
<form method="POST" action="/inbox/{{ address }}/{{ uid }}/forward">
|
||||||
|
<fieldset>
|
||||||
|
<label for="forwardEmail" class="floating-label">Destination Email</label>
|
||||||
|
<input type="email" id="forwardEmail" name="destinationEmail"
|
||||||
|
placeholder="recipient@example.com" required class="modal-input">
|
||||||
|
<button type="submit" class="button-primary modal-button">Forward</button>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block footer %}
|
{% block footer %}
|
||||||
{{ parent() }}
|
{{ parent() }}
|
||||||
<script>
|
|
||||||
// Initialize crypto keys toggle
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
if (window.utils && typeof window.utils.initCryptoKeysToggle === 'function') {
|
|
||||||
window.utils.initCryptoKeysToggle();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "48hr.email",
|
"name": "48hr.email",
|
||||||
"version": "1.7.5",
|
"version": "1.8.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "48hr.email",
|
"name": "48hr.email",
|
||||||
"version": "1.7.5",
|
"version": "1.8.1",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"async-retry": "^1.3.3",
|
"async-retry": "^1.3.3",
|
||||||
|
|
|
||||||
22
package.json
22
package.json
|
|
@ -67,15 +67,17 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"overrides": [{
|
"overrides": [
|
||||||
"files": "public/javascripts/*.js",
|
{
|
||||||
"esnext": false,
|
"files": "public/javascripts/*.js",
|
||||||
"env": [
|
"esnext": false,
|
||||||
"browser"
|
"env": [
|
||||||
],
|
"browser"
|
||||||
"globals": [
|
],
|
||||||
"io"
|
"globals": [
|
||||||
]
|
"io"
|
||||||
}]
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue