mirror of
https://github.com/Crazyco-xyz/48hr.email.git
synced 2026-02-14 17:19:35 +01:00
Compare commits
2 commits
2f7194e6bd
...
7ac2ef7b76
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ac2ef7b76 | ||
|
|
2c7b3ead3c |
5 changed files with 141 additions and 15 deletions
|
|
@ -6,9 +6,21 @@ require('events').defaultMaxListeners = 50;
|
||||||
* Receives sign-ins from users and notifies them when new mails are available.
|
* Receives sign-ins from users and notifies them when new mails are available.
|
||||||
*/
|
*/
|
||||||
class ClientNotification extends EventEmitter {
|
class ClientNotification extends EventEmitter {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.pendingNotifications = new Map(); // address -> count
|
||||||
|
}
|
||||||
|
|
||||||
use(io) {
|
use(io) {
|
||||||
io.on('connection', socket => {
|
io.on('connection', socket => {
|
||||||
socket.on('sign in', address => this._signIn(socket, address))
|
debug(`[SOCKET] New connection: id=${socket.id}`);
|
||||||
|
socket.on('sign in', address => {
|
||||||
|
debug(`[SOCKET] sign in received for address: ${address}, socket id: ${socket.id}`);
|
||||||
|
this._signIn(socket, address.toLowerCase())
|
||||||
|
});
|
||||||
|
socket.on('disconnect', reason => {
|
||||||
|
debug(`[SOCKET] Disconnected: id=${socket.id}, reason=${reason}`);
|
||||||
|
});
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -23,11 +35,33 @@ class ClientNotification extends EventEmitter {
|
||||||
|
|
||||||
this.on(address, newMailListener)
|
this.on(address, newMailListener)
|
||||||
|
|
||||||
|
// Deliver any pending notifications
|
||||||
|
const pending = this.pendingNotifications.get(address) || 0;
|
||||||
|
if (pending > 0) {
|
||||||
|
debug(`Delivering ${pending} pending notifications to ${address}`);
|
||||||
|
for (let i = 0; i < pending; i++) {
|
||||||
|
socket.emit('new emails');
|
||||||
|
}
|
||||||
|
this.pendingNotifications.delete(address);
|
||||||
|
}
|
||||||
|
|
||||||
socket.on('disconnect', reason => {
|
socket.on('disconnect', reason => {
|
||||||
debug(`client disconnect: ${address} (${reason})`)
|
debug(`client disconnect: ${address} (${reason})`)
|
||||||
this.removeListener(address, newMailListener)
|
this.removeListener(address, newMailListener)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emit(address) {
|
||||||
|
address = address.toLowerCase();
|
||||||
|
const hadListeners = super.emit(address);
|
||||||
|
if (!hadListeners) {
|
||||||
|
// Queue notification for later delivery
|
||||||
|
const prev = this.pendingNotifications.get(address) || 0;
|
||||||
|
this.pendingNotifications.set(address, prev + 1);
|
||||||
|
debug(`No listeners for ${address}, queued notification (${prev + 1} pending)`);
|
||||||
|
}
|
||||||
|
return hadListeners;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ClientNotification
|
module.exports = ClientNotification
|
||||||
|
|
@ -1,7 +1,63 @@
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const script = document.currentScript;
|
const script = document.querySelector('script[data-address]');
|
||||||
const address = script ? script.dataset.address : '';
|
const address = script ? script.dataset.address : '';
|
||||||
|
// Get expiry config from data attributes
|
||||||
|
const expiryTime = script && script.dataset.expiryTime ? Number(script.dataset.expiryTime) : 48;
|
||||||
|
const expiryUnit = script && script.dataset.expiryUnit ? script.dataset.expiryUnit : 'hours';
|
||||||
if (address) {
|
if (address) {
|
||||||
enableNewMessageNotifications(address, true);
|
enableNewMessageNotifications(address, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copy address on click
|
||||||
|
const copyAddress = document.getElementById('copyAddress');
|
||||||
|
const copyFeedback = document.getElementById('copyFeedback');
|
||||||
|
if (copyAddress) {
|
||||||
|
copyAddress.addEventListener('click', () => {
|
||||||
|
navigator.clipboard.writeText(copyAddress.textContent.trim()).then(() => {
|
||||||
|
if (copyFeedback) {
|
||||||
|
copyFeedback.style.display = 'inline';
|
||||||
|
setTimeout(() => { copyFeedback.style.display = 'none'; }, 1200);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expiry timer for each email
|
||||||
|
function getExpiryMs(time, unit) {
|
||||||
|
switch (unit) {
|
||||||
|
case 'minutes':
|
||||||
|
return time * 60 * 1000;
|
||||||
|
case 'hours':
|
||||||
|
return time * 60 * 60 * 1000;
|
||||||
|
case 'days':
|
||||||
|
return time * 24 * 60 * 60 * 1000;
|
||||||
|
default:
|
||||||
|
return 48 * 60 * 60 * 1000; // fallback 48h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateExpiryTimers() {
|
||||||
|
const timers = document.querySelectorAll('.expiry-timer');
|
||||||
|
timers.forEach(el => {
|
||||||
|
const dateStr = el.dataset.date;
|
||||||
|
if (!dateStr) return;
|
||||||
|
const mailDate = new Date(dateStr);
|
||||||
|
// Use config-driven expiry
|
||||||
|
const expiry = new Date(mailDate.getTime() + getExpiryMs(expiryTime, expiryUnit));
|
||||||
|
const now = new Date();
|
||||||
|
let diff = Math.floor((expiry - now) / 1000);
|
||||||
|
if (diff <= 0) {
|
||||||
|
el.textContent = 'Expired';
|
||||||
|
el.style.color = '#b00';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const hours = Math.floor(diff / 3600);
|
||||||
|
diff %= 3600;
|
||||||
|
const minutes = Math.floor(diff / 60);
|
||||||
|
const seconds = diff % 60;
|
||||||
|
el.textContent = `Expires in ${hours}h ${minutes}m ${seconds}s`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setInterval(updateExpiryTimers, 1000);
|
||||||
|
updateExpiryTimers();
|
||||||
});
|
});
|
||||||
|
|
@ -375,7 +375,8 @@ label {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.email-date {
|
.email-date,
|
||||||
|
.email-expiry {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: #666;
|
color: #666;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
@ -389,6 +390,23 @@ label {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Subject row with right-bound expiry */
|
||||||
|
|
||||||
|
.email-subject-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-date {
|
||||||
|
color: #666;
|
||||||
|
margin-left: auto;
|
||||||
|
text-align: right;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
@ -448,14 +466,6 @@ label {
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mail-meta {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mail-from {
|
.mail-from {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
color: #b366e6;
|
color: #b366e6;
|
||||||
|
|
@ -651,4 +661,22 @@ label {
|
||||||
|
|
||||||
.modal-button-cancel {
|
.modal-button-cancel {
|
||||||
background-color: #555;
|
background-color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Copy address feedback */
|
||||||
|
|
||||||
|
#copyAddress {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#copyFeedback {
|
||||||
|
display: none;
|
||||||
|
color: #9b4cda;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-expiry .expiry-timer[style*="color: #b00"] {
|
||||||
|
color: #b00 !important;
|
||||||
}
|
}
|
||||||
|
|
@ -56,7 +56,9 @@ router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, checkLockAccess, async(
|
||||||
unlockError: unlockErrorSession,
|
unlockError: unlockErrorSession,
|
||||||
locktimer: config.lock.releaseHours,
|
locktimer: config.lock.releaseHours,
|
||||||
error: lockError,
|
error: lockError,
|
||||||
redirectTo: req.originalUrl
|
redirectTo: req.originalUrl,
|
||||||
|
expiryTime: config.email.purgeTime.time,
|
||||||
|
expiryUnit: config.email.purgeTime.unit
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debug(`Error loading inbox for ${req.params.address}:`, error.message)
|
debug(`Error loading inbox for ${req.params.address}:`, error.message)
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,12 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<script src="/javascripts/inbox-init.js" defer data-address="{{ address }}"></script>
|
<script src="/javascripts/inbox-init.js" defer data-address="{{ address }}" data-expiry-time="{{ expiryTime }}" data-expiry-unit="{{ expiryUnit }}"></script>
|
||||||
<script src="/javascripts/lock-modals.js" defer></script>
|
<script src="/javascripts/lock-modals.js" defer></script>
|
||||||
<div class="inbox-container">
|
<div class="inbox-container">
|
||||||
<div class="inbox-header">
|
<div class="inbox-header">
|
||||||
<h1 class="inbox-title">{{ address }}</h1>
|
<h1 class="inbox-title" id="copyAddress" title="Click to copy address">{{ address }}</h1>
|
||||||
|
<span id="copyFeedback">Copied!</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="emails-container">
|
<div class="emails-container">
|
||||||
|
|
@ -39,7 +40,12 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="email-date">{{ mail.date | date }}</div>
|
<div class="email-date">{{ mail.date | date }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="email-subject">{{ mail.subject }}</div>
|
<div class="email-subject-row">
|
||||||
|
<div class="email-subject">{{ mail.subject }}</div>
|
||||||
|
<div class="email-expiry">
|
||||||
|
<span class="expiry-timer" data-date="{{ mail.date|date('Y-m-d\TH:i:s\Z') }}">Expires in ...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue