release: 1.1.0 - YAML config, web server, web auth and UI improvements
This commit is contained in:
+64
@@ -0,0 +1,64 @@
|
||||
.email-summary {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.email-summary:focus-visible {
|
||||
outline: 2px solid #9ca3af;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.email-content-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.email-content-frame {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
|
||||
border: none;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: inline-block;
|
||||
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.toggle.rotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.download-icon {
|
||||
display: inline-block;
|
||||
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
line-height: 1;
|
||||
|
||||
transition:
|
||||
transform 0.1s ease,
|
||||
opacity 0.1s ease;
|
||||
}
|
||||
|
||||
.download-icon:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.download-icon:active {
|
||||
transform: scale(0.85);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.download-icon:focus-visible {
|
||||
outline: 2px solid #9ca3af;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
+391
@@ -0,0 +1,391 @@
|
||||
'use strict';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
initializeEmailList();
|
||||
});
|
||||
|
||||
function initializeEmailList() {
|
||||
var emailItems = document.querySelectorAll('.email-item');
|
||||
|
||||
emailItems.forEach(function (item) {
|
||||
var summary = item.querySelector('.email-summary');
|
||||
var downloadButton = item.querySelector('.download-icon');
|
||||
|
||||
if (summary) {
|
||||
summary.addEventListener('click', function (event) {
|
||||
if (event.target.closest('.download-icon')) {
|
||||
return;
|
||||
}
|
||||
|
||||
toggleEmail(item);
|
||||
});
|
||||
|
||||
summary.addEventListener('keydown', function (event) {
|
||||
if (
|
||||
event.key !== 'Enter' &&
|
||||
event.key !== ' '
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target.closest('.download-icon')) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
toggleEmail(item);
|
||||
});
|
||||
}
|
||||
|
||||
if (downloadButton) {
|
||||
downloadButton.addEventListener('click', function (event) {
|
||||
event.stopPropagation();
|
||||
|
||||
var id = item.dataset.id;
|
||||
|
||||
if (!id) {
|
||||
console.error('Email ID is missing');
|
||||
return;
|
||||
}
|
||||
|
||||
downloadEmail(event, id);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toggleEmail(item) {
|
||||
var id = item.dataset.id;
|
||||
|
||||
if (!id) {
|
||||
console.error('Email ID is missing');
|
||||
return;
|
||||
}
|
||||
|
||||
var summary = item.querySelector('.email-summary');
|
||||
var detail = item.querySelector('.email-detail');
|
||||
var toggleIcon = item.querySelector('.toggle');
|
||||
var iframe = item.querySelector('.email-content-frame');
|
||||
|
||||
if (!detail || !iframe) {
|
||||
console.error(
|
||||
'Email UI elements are missing for:',
|
||||
id
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var isHidden = detail.classList.contains('hidden');
|
||||
|
||||
if (isHidden) {
|
||||
detail.classList.remove('hidden');
|
||||
|
||||
if (toggleIcon) {
|
||||
toggleIcon.classList.add('rotated');
|
||||
}
|
||||
|
||||
if (summary) {
|
||||
summary.setAttribute(
|
||||
'aria-expanded',
|
||||
'true'
|
||||
);
|
||||
}
|
||||
|
||||
loadEmailContent(
|
||||
id,
|
||||
iframe
|
||||
);
|
||||
} else {
|
||||
detail.classList.add('hidden');
|
||||
|
||||
if (toggleIcon) {
|
||||
toggleIcon.classList.remove('rotated');
|
||||
}
|
||||
|
||||
if (summary) {
|
||||
summary.setAttribute(
|
||||
'aria-expanded',
|
||||
'false'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadEmailContent(id, iframe) {
|
||||
if (iframe.dataset.loaded === 'true') {
|
||||
resizeIframe(iframe);
|
||||
return;
|
||||
}
|
||||
|
||||
iframe.dataset.loading = 'true';
|
||||
|
||||
fetch(
|
||||
'/api/view?id=' +
|
||||
encodeURIComponent(id),
|
||||
{
|
||||
method: 'GET',
|
||||
credentials: 'same-origin'
|
||||
}
|
||||
)
|
||||
.then(function (response) {
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
'HTTP error ' +
|
||||
response.status
|
||||
);
|
||||
}
|
||||
|
||||
return response.text();
|
||||
})
|
||||
.then(function (data) {
|
||||
writeEmailToIframe(
|
||||
iframe,
|
||||
data
|
||||
);
|
||||
|
||||
iframe.dataset.loaded = 'true';
|
||||
|
||||
delete iframe.dataset.loading;
|
||||
|
||||
resizeIframe(iframe);
|
||||
})
|
||||
.catch(function (error) {
|
||||
delete iframe.dataset.loading;
|
||||
|
||||
writeErrorToIframe(
|
||||
iframe,
|
||||
error.message
|
||||
);
|
||||
|
||||
console.error(
|
||||
'Failed to load email:',
|
||||
id,
|
||||
error
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function writeEmailToIframe(iframe, emailBody) {
|
||||
var doc = iframe.contentWindow.document;
|
||||
|
||||
doc.open();
|
||||
|
||||
doc.write(
|
||||
'<!DOCTYPE html>' +
|
||||
'<html>' +
|
||||
'<head>' +
|
||||
'<meta charset="UTF-8">' +
|
||||
'<meta name="viewport" content="width=device-width, initial-scale=1.0">' +
|
||||
|
||||
'<base target="_blank">' +
|
||||
|
||||
'<style>' +
|
||||
|
||||
'html {' +
|
||||
'margin: 0 !important;' +
|
||||
'padding: 0 !important;' +
|
||||
'background: white !important;' +
|
||||
'}' +
|
||||
|
||||
'body {' +
|
||||
'margin: 0 !important;' +
|
||||
'padding: 15px !important;' +
|
||||
'font-family: monospace !important;' +
|
||||
'font-size: 12px !important;' +
|
||||
'line-height: 1.4 !important;' +
|
||||
'background: white !important;' +
|
||||
'color: #333 !important;' +
|
||||
'overflow-wrap: anywhere !important;' +
|
||||
'}' +
|
||||
|
||||
'* {' +
|
||||
'box-sizing: border-box !important;' +
|
||||
'max-width: 100% !important;' +
|
||||
'}' +
|
||||
|
||||
'img {' +
|
||||
'max-width: 100% !important;' +
|
||||
'height: auto !important;' +
|
||||
'}' +
|
||||
|
||||
'table {' +
|
||||
'max-width: 100% !important;' +
|
||||
'}' +
|
||||
|
||||
'pre {' +
|
||||
'margin: 0 !important;' +
|
||||
'font-family: inherit !important;' +
|
||||
'white-space: pre-wrap !important;' +
|
||||
'overflow-wrap: anywhere !important;' +
|
||||
'}' +
|
||||
|
||||
'a {' +
|
||||
'color: #2563eb !important;' +
|
||||
'text-decoration: underline !important;' +
|
||||
'}' +
|
||||
|
||||
'a:visited {' +
|
||||
'color: #7c3aed !important;' +
|
||||
'}' +
|
||||
|
||||
'a:hover {' +
|
||||
'color: #1d4ed8 !important;' +
|
||||
'}' +
|
||||
|
||||
'</style>' +
|
||||
|
||||
'</head>' +
|
||||
|
||||
'<body>' +
|
||||
emailBody +
|
||||
'</body>' +
|
||||
|
||||
'</html>'
|
||||
);
|
||||
|
||||
doc.close();
|
||||
}
|
||||
|
||||
function writeErrorToIframe(iframe, message) {
|
||||
var doc = iframe.contentWindow.document;
|
||||
|
||||
doc.open();
|
||||
|
||||
doc.write(
|
||||
'<!DOCTYPE html>' +
|
||||
'<html>' +
|
||||
'<head>' +
|
||||
'<meta charset="UTF-8">' +
|
||||
'</head>' +
|
||||
'<body style="' +
|
||||
'padding:15px;' +
|
||||
'font-family:monospace;' +
|
||||
'font-size:12px;' +
|
||||
'color:#b91c1c;' +
|
||||
'">' +
|
||||
'Error loading email: ' +
|
||||
escapeHTML(message) +
|
||||
'</body>' +
|
||||
'</html>'
|
||||
);
|
||||
|
||||
doc.close();
|
||||
|
||||
resizeIframe(iframe);
|
||||
}
|
||||
|
||||
function resizeIframe(iframe) {
|
||||
if (!iframe || !iframe.contentWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var doc = iframe.contentWindow.document;
|
||||
|
||||
if (!doc || !doc.body) {
|
||||
return;
|
||||
}
|
||||
|
||||
var height = Math.max(
|
||||
doc.body.scrollHeight,
|
||||
doc.documentElement
|
||||
? doc.documentElement.scrollHeight
|
||||
: 0
|
||||
);
|
||||
|
||||
iframe.style.height =
|
||||
Math.max(height + 20, 200) +
|
||||
'px';
|
||||
|
||||
if (
|
||||
!iframe.dataset.resizeObserver &&
|
||||
typeof ResizeObserver !== 'undefined'
|
||||
) {
|
||||
var observer = new ResizeObserver(
|
||||
function () {
|
||||
try {
|
||||
var body =
|
||||
iframe.contentWindow.document.body;
|
||||
|
||||
var html =
|
||||
iframe.contentWindow.document.documentElement;
|
||||
|
||||
var newHeight = Math.max(
|
||||
body
|
||||
? body.scrollHeight
|
||||
: 0,
|
||||
html
|
||||
? html.scrollHeight
|
||||
: 0
|
||||
);
|
||||
|
||||
iframe.style.height =
|
||||
Math.max(
|
||||
newHeight + 20,
|
||||
200
|
||||
) +
|
||||
'px';
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
observer.observe(doc.body);
|
||||
|
||||
iframe.dataset.resizeObserver =
|
||||
'true';
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'Cannot resize email iframe:',
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function downloadEmail(event, id) {
|
||||
var icon = event.currentTarget;
|
||||
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
var originalTransform =
|
||||
icon.style.transform;
|
||||
|
||||
var originalOpacity =
|
||||
icon.style.opacity;
|
||||
|
||||
icon.style.transform =
|
||||
'scale(0.8)';
|
||||
|
||||
icon.style.opacity =
|
||||
'0.5';
|
||||
|
||||
window.location.href =
|
||||
'/api/download?id=' +
|
||||
encodeURIComponent(id);
|
||||
|
||||
window.setTimeout(
|
||||
function () {
|
||||
icon.style.transform =
|
||||
originalTransform;
|
||||
|
||||
icon.style.opacity =
|
||||
originalOpacity;
|
||||
},
|
||||
150
|
||||
);
|
||||
}
|
||||
|
||||
function escapeHTML(value) {
|
||||
return String(value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0"
|
||||
>
|
||||
|
||||
<title>{{.Title}}</title>
|
||||
|
||||
<script src="/static/tailwind.js"></script>
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="/static/app.css"
|
||||
>
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-100">
|
||||
<div class="max-w-6xl mx-auto bg-white min-h-screen shadow-sm">
|
||||
|
||||
<!-- Header -->
|
||||
<header class="bg-gray-800 text-white px-6 py-4 border-b-4 border-red-600">
|
||||
<h1 class="text-2xl font-normal">
|
||||
Fake SMTP Server
|
||||
</h1>
|
||||
|
||||
<p class="text-gray-400 text-sm mt-1">
|
||||
Email storage
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- Content -->
|
||||
<main class="p-4">
|
||||
|
||||
{{range .Days}}
|
||||
|
||||
<section class="mb-6 border border-gray-200">
|
||||
|
||||
<!-- Day header -->
|
||||
<div class="bg-gray-50 px-4 py-2 border-b border-gray-200 flex justify-between items-center">
|
||||
<h2 class="text-base font-semibold text-gray-700">
|
||||
{{.Date}}
|
||||
</h2>
|
||||
|
||||
<span class="bg-gray-400 text-white px-2 py-0.5 text-xs">
|
||||
{{len .Emails}} emails
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Emails -->
|
||||
<div class="divide-y divide-gray-100">
|
||||
|
||||
{{range .Emails}}
|
||||
|
||||
<article
|
||||
class="email-item"
|
||||
data-id="{{.ID}}"
|
||||
>
|
||||
|
||||
<!-- Email summary -->
|
||||
<div
|
||||
class="email-summary px-4 py-2 hover:bg-gray-50 cursor-pointer flex justify-between items-center"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<div class="flex-1 flex flex-wrap gap-4 items-baseline">
|
||||
|
||||
<div class="font-mono text-sm font-semibold text-gray-700 min-w-[180px]">
|
||||
{{.From}}
|
||||
</div>
|
||||
|
||||
<div class="font-mono text-xs text-gray-400 min-w-[180px]">
|
||||
→ {{.To}}
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-gray-600 flex-1">
|
||||
{{.Subject}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 text-gray-400 text-xs">
|
||||
|
||||
<span class="font-mono">
|
||||
{{.ModTime.Format "15:04:05"}}
|
||||
</span>
|
||||
|
||||
<span class="font-mono">
|
||||
{{.Size}} bytes
|
||||
</span>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="download-icon text-base cursor-pointer hover:text-gray-600"
|
||||
title="Download EML"
|
||||
aria-label="Download email"
|
||||
>
|
||||
💾
|
||||
</button>
|
||||
|
||||
<span
|
||||
class="toggle text-xs"
|
||||
aria-hidden="true"
|
||||
>
|
||||
▼
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Email detail -->
|
||||
<div class="email-detail hidden bg-gray-50 border-t border-gray-200 p-4">
|
||||
<div class="email-content-wrapper">
|
||||
|
||||
<iframe
|
||||
class="email-content-frame"
|
||||
title="Email content"
|
||||
sandbox="allow-same-origin allow-popups"
|
||||
srcdoc="<html><body style='margin:0;padding:15px;font-family:monospace;font-size:12px;'>Loading...</body></html>"
|
||||
></iframe>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</article>
|
||||
|
||||
{{end}}
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{else}}
|
||||
|
||||
<div class="text-center py-16 text-gray-400">
|
||||
|
||||
<div class="text-4xl mb-3">
|
||||
📭
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg text-gray-500 mb-2">
|
||||
No emails
|
||||
</h3>
|
||||
|
||||
<p class="text-sm">
|
||||
No emails have been saved yet
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
{{end}}
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/static/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user