Compare commits

..

6 commits

Author SHA1 Message Date
ClaraCrazy
2ac2371963
Elaborate on vague env variable 2025-12-14 19:59:28 +01:00
ClaraCrazy
9e6f09077d
Update non-example mail handling to delete rather than ignore 2025-12-14 19:58:45 +01:00
ClaraCrazy
08c41fbf09
Bring back error handler from ye-ol f9a26cd631 2025-12-14 17:12:19 +01:00
ClaraCrazy
424fbf3930
shhhhh 2025-12-14 14:42:37 +01:00
ClaraCrazy
64550e351c
Move js into its own file 2025-12-14 14:24:44 +01:00
ClaraCrazy
9538e7be7a
Fix notifications 2025-12-14 14:18:37 +01:00
9 changed files with 128 additions and 105 deletions

View file

@ -5,6 +5,7 @@ EMAIL_DOMAINS=["example.com","example.net"] # List of domain
EMAIL_PURGE_TIME=48 # Time value for when to purge EMAIL_PURGE_TIME=48 # Time value for when to purge
EMAIL_PURGE_UNIT="hours" # minutes, hours, days EMAIL_PURGE_UNIT="hours" # minutes, hours, days
EMAIL_PURGE_CONVERT=true # Convert to highest sensible unit (and round) EMAIL_PURGE_CONVERT=true # Convert to highest sensible unit (and round)
# /\ Example: 120 minutes = 2 hours, 121 minutes = ~2 hours with added tooltip
# --- Example emails to keep clean --- # --- Example emails to keep clean ---
EMAIL_EXAMPLE_ACCOUNT="example@48hr.email" # example email to preserve EMAIL_EXAMPLE_ACCOUNT="example@48hr.email" # example email to preserve

2
.gitignore vendored
View file

@ -4,3 +4,5 @@
.DS_Store .DS_Store
copilot-instructions.md copilot-instructions.md
node_modules node_modules
db/*
package-lock.json

View file

@ -12,12 +12,22 @@ class MailRepository {
getForRecipient(address) { getForRecipient(address) {
let mails = this.mailSummaries.get(address) || [] let mails = this.mailSummaries.get(address) || []
const mailsToDelete = []
mails.forEach(mail => { mails.forEach(mail => {
if (mail.to == this.config.email.examples.account && !this.config.email.examples.uids.includes(parseInt(mail.uid))) { if (mail.to == this.config.email.examples.account && !this.config.email.examples.uids.includes(parseInt(mail.uid))) {
mails = mails.filter(m => m.uid != mail.uid) mailsToDelete.push(mail.uid)
debug('Prevented non-example email from being shown in example inbox', mail.uid) debug('Marking non-example email for deletion from example inbox', mail.uid)
} }
}) })
// Delete the non-example mails
mailsToDelete.forEach(uid => {
this.removeUid(uid, address)
})
// Get fresh list after deletions
mails = this.mailSummaries.get(address) || []
return _.orderBy(mails, mail => Date.parse(mail.date), ['desc']) return _.orderBy(mails, mail => Date.parse(mail.date), ['desc'])
} }

View file

@ -0,0 +1,7 @@
document.addEventListener('DOMContentLoaded', () => {
const script = document.currentScript;
const address = script ? script.dataset.address : '';
if (address) {
enableNewMessageNotifications(address, true);
}
});

View file

@ -11,8 +11,8 @@ function showNewMailsNotification(address, reloadPage) {
renotify: true renotify: true
}) })
notification.addEventListener('click', event => { notification.addEventListener('click', event => {
// TODO: does not work after reloading the page, see #1
event.preventDefault() event.preventDefault()
window.focus()
}) })
if (reloadPage) { if (reloadPage) {

View file

@ -0,0 +1,29 @@
const express = require('express')
const router = new express.Router()
const config = require('../../../application/config')
const Helper = require('../../../application/helper')
const helper = new(Helper)
const purgeTime = helper.purgeTimeElemetBuilder()
router.get('/:address/:errorCode', async(req, res) => {
const mailProcessingService = req.app.get('mailProcessingService')
const count = await mailProcessingService.getCount()
const errorCode = parseInt(req.params.errorCode) || 404
const message = req.query.message || (req.session && req.session.errorMessage) || 'An error occurred'
res.status(errorCode)
res.render('error', {
title: `${config.http.branding[0]} | ${errorCode}`,
purgeTime: purgeTime,
address: req.params.address,
count: count,
message: message,
status: errorCode,
branding: config.http.branding
})
})
module.exports = router

View file

@ -58,16 +58,8 @@ router.get(
branding: config.http.branding, branding: config.http.branding,
}) })
} else { } else {
res.render( req.session.errorMessage = 'This mail could not be found. It either does not exist or has been deleted from our servers!'
'error', { res.redirect(`/error/${req.params.address}/404`)
purgeTime: purgeTime,
address: req.params.address,
count: count,
message: 'This mail could not be found. It either does not exist or has been deleted from our servers!',
branding: config.http.branding
}
)
} }
} catch (error) { } catch (error) {
console.error('Error while fetching email', error) console.error('Error while fetching email', error)
@ -76,6 +68,16 @@ router.get(
} }
) )
// Catch-all for invalid UIDs (non-numeric)
router.get(
'^/:address/:uid',
sanitizeAddress,
async(req, res) => {
req.session.errorMessage = 'Invalid/Malformed UID provided.'
res.redirect(`/error/${req.params.address}/400`)
}
)
router.get( router.get(
'^/:address/delete-all', '^/:address/delete-all',
sanitizeAddress, sanitizeAddress,
@ -121,15 +123,8 @@ router.get(
// Validate UID is a valid integer // Validate UID is a valid integer
if (isNaN(uid) || uid <= 0) { if (isNaN(uid) || uid <= 0) {
return res.render( req.session.errorMessage = 'Invalid/Malformed UID provided.'
'error', { return res.redirect(`/error/${req.params.address}/400`)
purgeTime: purgeTime,
address: req.params.address,
count: count,
message: 'Invalid/Malformed UID provided.',
branding: config.http.branding,
}
)
} }
const mail = await mailProcessingService.getOneFullMail( const mail = await mailProcessingService.getOneFullMail(
@ -138,15 +133,8 @@ router.get(
) )
if (!mail || !mail.attachments) { if (!mail || !mail.attachments) {
return res.render( req.session.errorMessage = 'This email could not be found. It either does not exist or has been deleted from our servers!'
'error', { return res.redirect(`/error/${req.params.address}/404`)
purgeTime: purgeTime,
address: req.params.address,
count: count,
message: 'This email could not be found. It either does not exist or has been deleted from our servers!',
branding: config.http.branding,
}
)
} }
var index = mail.attachments.findIndex(attachment => attachment.checksum === req.params.checksum); var index = mail.attachments.findIndex(attachment => attachment.checksum === req.params.checksum);
@ -164,15 +152,8 @@ router.get(
return; return;
} }
} else { } else {
return res.render( req.session.errorMessage = 'This attachment could not be found. It either does not exist or has been deleted from our servers!'
'error', { return res.redirect(`/error/${req.params.address}/404`)
purgeTime: purgeTime,
address: req.params.address,
count: count,
message: 'This attachment could not be found. It either does not exist or has been deleted from our servers!',
branding: config.http.branding,
}
)
} }
} catch (error) { } catch (error) {
console.error('Error while fetching attachment', error) console.error('Error while fetching attachment', error)
@ -194,15 +175,8 @@ router.get(
// Validate UID is a valid integer // Validate UID is a valid integer
if (isNaN(uid) || uid <= 0) { if (isNaN(uid) || uid <= 0) {
return res.render( req.session.errorMessage = 'Invalid/Malformed UID provided.'
'error', { return res.redirect(`/error/${req.params.address}/400`)
purgeTime: purgeTime,
address: req.params.address,
count: count,
message: 'Invalid/Malformed UID provided.',
branding: config.http.branding,
}
)
} }
mail = await mailProcessingService.getOneFullMail( mail = await mailProcessingService.getOneFullMail(
@ -219,15 +193,8 @@ router.get(
mail mail
}) })
} else { } else {
res.render( req.session.errorMessage = 'This mail could not be found. It either does not exist or has been deleted from our servers!'
'error', { res.redirect(`/error/${req.params.address}/404`)
purgeTime: purgeTime,
address: req.params.address,
count: count,
message: 'This mail could not be found. It either does not exist or has been deleted from our servers!',
branding: config.http.branding,
}
)
} }
} catch (error) { } catch (error) {
console.error('Error while fetching raw email', error) console.error('Error while fetching raw email', error)

View file

@ -1,11 +1,7 @@
{% extends 'layout.twig' %} {% extends 'layout.twig' %}
{% block body %} {% block body %}
<script> <script src="/javascripts/inbox-init.js" defer data-address="{{ address }}"></script>
document.addEventListener("DOMContentLoaded", function () {
enableNewMessageNotifications('{{ address }}', true)
});
</script>
<div class="action-links"> <div class="action-links">
<a href="/inbox/{{ address }}/delete-all">Wipe Inbox</a> <a href="/inbox/{{ address }}/delete-all">Wipe Inbox</a>

View file

@ -2,6 +2,7 @@ const path = require('path')
const http = require('http') 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 logger = require('morgan') const logger = require('morgan')
const Twig = require('twig') const Twig = require('twig')
const compression = require('compression') const compression = require('compression')
@ -11,6 +12,7 @@ const socketio = require('socket.io')
const config = require('../../application/config') const config = require('../../application/config')
const inboxRouter = require('./routes/inbox') const inboxRouter = require('./routes/inbox')
const loginRouter = require('./routes/login') const loginRouter = require('./routes/login')
const errorRouter = require('./routes/error')
const { sanitizeHtmlTwigFilter } = require('./views/twig-filters') const { sanitizeHtmlTwigFilter } = require('./views/twig-filters')
const Helper = require('../../application/helper') const Helper = require('../../application/helper')
@ -30,6 +32,14 @@ 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 middleware
app.use(session({
secret: '1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ', // They will hate me for this
resave: false,
saveUninitialized: true,
cookie: { maxAge: 1000 * 60 * 60 * 24 } // 24 hours
}))
// Remove trailing slash middleware (except for root) // Remove trailing slash middleware (except for root)
app.use((req, res, next) => { app.use((req, res, next) => {
if (req.path.length > 1 && req.path.endsWith('/')) { if (req.path.length > 1 && req.path.endsWith('/')) {
@ -63,6 +73,7 @@ app.get('/', (req, res, _next) => {
app.use('/', loginRouter) app.use('/', loginRouter)
app.use('/inbox', inboxRouter) app.use('/inbox', inboxRouter)
app.use('/error', errorRouter)
// Catch 404 and forward to error handler // Catch 404 and forward to error handler
app.use((req, res, next) => { app.use((req, res, next) => {