{Feat]: Overhaul raw mail view

This commit is contained in:
ClaraCrazy 2025-12-27 22:09:45 +01:00
parent cfe5602d4a
commit 214e7d31eb
No known key found for this signature in database
GPG key ID: EBBC896ACB497011
3 changed files with 108 additions and 20 deletions

View file

@ -679,4 +679,60 @@ label {
.email-expiry .expiry-timer[style*="color: #b00"] {
color: #b00 !important;
}
/* Raw mail view */
.raw-container {
max-width: 1500px;
margin: 0 auto;
padding: 20px;
}
.raw-mail {
background: rgba(0, 0, 0, 0.35);
color: #e0e0e0;
padding: 20px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.08);
white-space: pre-wrap;
word-break: break-word;
overflow-x: auto;
font-family: Consolas, 'Courier New', monospace;
font-size: 0.95rem;
}
.raw-tabs {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.raw-tab-button {
padding: 8px 14px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.05);
color: #e0e0e0;
cursor: pointer;
transition: all 0.2s ease;
}
.raw-tab-button.active {
background: rgba(155, 77, 202, 0.25);
border-color: rgba(155, 77, 202, 0.4);
color: #fff;
}
.raw-tab-button:hover {
border-color: rgba(155, 77, 202, 0.5);
}
.raw-panels {
position: relative;
}
.raw-mail.hidden {
display: none;
}

View file

@ -252,13 +252,33 @@ router.get(
true
)
if (mail) {
mail = mail.replace(/(?:\r\n|\r|\n)/g, '<br>')
// Emails are immutable, cache if found
const decodeQuotedPrintable = (input) => {
if (!input) return '';
// Remove soft line breaks
let cleaned = input.replace(/=\r?\n/g, '');
// Decode =XX hex escapes
cleaned = cleaned.replace(/=([0-9A-Fa-f]{2})/g, (_, hex) => {
try {
return String.fromCharCode(parseInt(hex, 16));
} catch {
return '=' + hex;
}
});
return cleaned;
};
const decodedMail = decodeQuotedPrintable(mail);
// Keep raw content but add literal newlines after <br> tags for readability
const rawMail = mail.replace(/<br\s*\/?\s*>/gi, '<br>\n');
// Emails are immutable, cache if found
res.set('Cache-Control', 'private, max-age=600')
debug(`Rendering raw email view for UID ${req.params.uid}`)
res.render('raw', {
title: req.params.uid + " | raw | " + req.params.address,
mail,
mail: rawMail,
decoded: decodedMail,
totalcount: totalcount
})
} else {

View file

@ -1,19 +1,31 @@
{% extends 'layout.twig' %}
{% block header %}{% endblock %}
{% block body %}
<head>
<title>{{ title }}</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimal-ui">
<meta name="darkreader-lock">
<meta name="description" content="Dont give shady companies your real email. Use 48hr.email to protect your privacy!">
<meta property="og:image" content="/images/logo.png">
<link rel="shortcut icon" href="/images/logo.ico">
<link rel='stylesheet' href='/dependencies/milligram.css' />
<link rel='stylesheet' href='/stylesheets/custom.css' />
<script src="/socket.io/socket.io.js" defer="true"></script>
<script src="/javascripts/notifications.js" defer="true"></script>
</head>
{{ mail | raw }}
<div class="raw-container">
<div class="raw-tabs">
<button class="raw-tab-button active" data-target="raw">Raw (escaped)</button>
<button class="raw-tab-button" data-target="decoded">Decoded (quoted-printable)</button>
</div>
<div class="raw-panels">
<pre class="raw-mail" data-panel="raw">{{ mail | e }}</pre>
<pre class="raw-mail hidden" data-panel="decoded">{{ decoded | e }}</pre>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const buttons = document.querySelectorAll('.raw-tab-button');
const panels = document.querySelectorAll('.raw-mail[data-panel]');
buttons.forEach(btn => {
btn.addEventListener('click', () => {
const target = btn.dataset.target;
buttons.forEach(b => b.classList.toggle('active', b === btn));
panels.forEach(p => p.classList.toggle('hidden', p.dataset.panel !== target));
});
});
});
</script>
{% endblock %}
{% block footer %}{% endblock %}