release: 1.1.1 - session auth and security improvements

This commit is contained in:
2026-10-05 22:36:24 +03:00
parent 1cd3cb9a2e
commit b1cd985720
8 changed files with 2539 additions and 539 deletions
+382 -129
View File
@@ -1,56 +1,88 @@
'use strict';
document.addEventListener('DOMContentLoaded', function () {
initializeEmailList();
});
var iframeObservers = new WeakMap();
document.addEventListener(
'DOMContentLoaded',
function () {
initializeEmailList();
}
);
function initializeEmailList() {
var emailItems = document.querySelectorAll('.email-item');
var emailItems =
document.querySelectorAll('.email-item');
emailItems.forEach(function (item) {
var summary = item.querySelector('.email-summary');
var downloadButton = item.querySelector('.download-icon');
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;
summary.addEventListener(
'click',
function (event) {
if (
event.target.closest(
'.download-icon'
)
) {
return;
}
toggleEmail(item);
}
);
toggleEmail(item);
});
summary.addEventListener(
'keydown',
function (event) {
if (
event.key !== 'Enter' &&
event.key !== ' '
) {
return;
}
summary.addEventListener('keydown', function (event) {
if (
event.key !== 'Enter' &&
event.key !== ' '
) {
return;
if (
event.target.closest(
'.download-icon'
)
) {
return;
}
event.preventDefault();
toggleEmail(item);
}
if (event.target.closest('.download-icon')) {
return;
}
event.preventDefault();
toggleEmail(item);
});
);
}
if (downloadButton) {
downloadButton.addEventListener('click', function (event) {
event.stopPropagation();
downloadButton.addEventListener(
'click',
function (event) {
event.stopPropagation();
var id = item.dataset.id;
var id = item.dataset.id;
if (!id) {
console.error('Email ID is missing');
return;
if (!id) {
console.error(
'Email ID is missing'
);
return;
}
downloadEmail(
event,
id
);
}
downloadEmail(event, id);
});
);
}
});
}
@@ -59,14 +91,26 @@ function toggleEmail(item) {
var id = item.dataset.id;
if (!id) {
console.error('Email ID is missing');
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');
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(
@@ -77,13 +121,16 @@ function toggleEmail(item) {
return;
}
var isHidden = detail.classList.contains('hidden');
var isHidden =
detail.classList.contains('hidden');
if (isHidden) {
detail.classList.remove('hidden');
if (toggleIcon) {
toggleIcon.classList.add('rotated');
toggleIcon.classList.add(
'rotated'
);
}
if (summary) {
@@ -101,7 +148,9 @@ function toggleEmail(item) {
detail.classList.add('hidden');
if (toggleIcon) {
toggleIcon.classList.remove('rotated');
toggleIcon.classList.remove(
'rotated'
);
}
if (summary) {
@@ -114,11 +163,19 @@ function toggleEmail(item) {
}
function loadEmailContent(id, iframe) {
if (iframe.dataset.loaded === 'true') {
if (
iframe.dataset.loaded === 'true'
) {
resizeIframe(iframe);
return;
}
if (
iframe.dataset.loading === 'true'
) {
return;
}
iframe.dataset.loading = 'true';
fetch(
@@ -130,6 +187,8 @@ function loadEmailContent(id, iframe) {
}
)
.then(function (response) {
handleUnauthorized(response);
if (!response.ok) {
throw new Error(
'HTTP error ' +
@@ -140,12 +199,15 @@ function loadEmailContent(id, iframe) {
return response.text();
})
.then(function (data) {
resetIframeObserver(iframe);
writeEmailToIframe(
iframe,
data
);
iframe.dataset.loaded = 'true';
iframe.dataset.loaded =
'true';
delete iframe.dataset.loading;
@@ -154,6 +216,17 @@ function loadEmailContent(id, iframe) {
.catch(function (error) {
delete iframe.dataset.loading;
// При 401 уже выполняется redirect.
if (
error &&
error.message ===
'Session expired'
) {
return;
}
resetIframeObserver(iframe);
writeErrorToIframe(
iframe,
error.message
@@ -167,71 +240,94 @@ function loadEmailContent(id, iframe) {
});
}
function writeEmailToIframe(iframe, emailBody) {
var doc = iframe.contentWindow.document;
function handleUnauthorized(response) {
if (response.status !== 401) {
return;
}
window.location.href = '/login';
throw new Error(
'Session expired'
);
}
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">' +
'<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;' +
'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;' +
'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;' +
'box-sizing:border-box !important;' +
'max-width:100% !important;' +
'}' +
'img {' +
'max-width: 100% !important;' +
'height: auto !important;' +
'max-width:100% !important;' +
'height:auto !important;' +
'}' +
'table {' +
'max-width: 100% !important;' +
'max-width:100% !important;' +
'}' +
'pre {' +
'margin: 0 !important;' +
'font-family: inherit !important;' +
'white-space: pre-wrap !important;' +
'overflow-wrap: anywhere !important;' +
'margin:0 !important;' +
'font-family:inherit !important;' +
'white-space:pre-wrap !important;' +
'overflow-wrap:anywhere !important;' +
'}' +
'a {' +
'color: #2563eb !important;' +
'text-decoration: underline !important;' +
'color:#2563eb !important;' +
'text-decoration:underline !important;' +
'}' +
'a:visited {' +
'color: #7c3aed !important;' +
'color:#7c3aed !important;' +
'}' +
'a:hover {' +
'color: #1d4ed8 !important;' +
'color:#1d4ed8 !important;' +
'}' +
'</style>' +
@@ -239,7 +335,7 @@ function writeEmailToIframe(iframe, emailBody) {
'</head>' +
'<body>' +
emailBody +
emailBody +
'</body>' +
'</html>'
@@ -248,26 +344,36 @@ function writeEmailToIframe(iframe, emailBody) {
doc.close();
}
function writeErrorToIframe(iframe, message) {
var doc = iframe.contentWindow.document;
function writeErrorToIframe(
iframe,
message
) {
var doc =
iframe.contentWindow.document;
doc.open();
doc.write(
'<!DOCTYPE html>' +
'<html>' +
'<head>' +
'<meta charset="UTF-8">' +
'<meta charset="UTF-8">' +
'</head>' +
'<body style="' +
'padding:15px;' +
'font-family:monospace;' +
'font-size:12px;' +
'color:#b91c1c;' +
'">' +
'Error loading email: ' +
escapeHTML(message) +
'</body>' +
'</html>'
);
@@ -276,68 +382,81 @@ function writeErrorToIframe(iframe, message) {
resizeIframe(iframe);
}
function resetIframeObserver(iframe) {
var observer =
iframeObservers.get(iframe);
if (observer) {
observer.disconnect();
iframeObservers.delete(
iframe
);
}
}
function resizeIframe(iframe) {
if (!iframe || !iframe.contentWindow) {
if (
!iframe ||
!iframe.contentWindow
) {
return;
}
try {
var doc = iframe.contentWindow.document;
var doc =
iframe.contentWindow.document;
if (!doc || !doc.body) {
return;
}
var height = Math.max(
doc.body.scrollHeight,
doc.documentElement
? doc.documentElement.scrollHeight
: 0
updateIframeHeight(
iframe,
doc
);
iframe.style.height =
Math.max(height + 20, 200) +
'px';
if (
!iframe.dataset.resizeObserver &&
typeof ResizeObserver !== 'undefined'
typeof ResizeObserver ===
'undefined'
) {
var observer = new ResizeObserver(
return;
}
var existing =
iframeObservers.get(iframe);
if (existing) {
return;
}
var observer =
new ResizeObserver(
function () {
try {
var body =
iframe.contentWindow.document.body;
var currentDoc =
iframe
.contentWindow
.document;
var html =
iframe.contentWindow.document.documentElement;
var newHeight = Math.max(
body
? body.scrollHeight
: 0,
html
? html.scrollHeight
: 0
updateIframeHeight(
iframe,
currentDoc
);
iframe.style.height =
Math.max(
newHeight + 20,
200
) +
'px';
} catch (error) {
// Iframe may have been replaced.
}
}
);
observer.observe(doc.body);
observer.observe(
doc.body
);
iframe.dataset.resizeObserver =
'true';
}
iframeObservers.set(
iframe,
observer
);
} catch (error) {
console.warn(
'Cannot resize email iframe:',
@@ -346,39 +465,173 @@ function resizeIframe(iframe) {
}
}
function downloadEmail(event, id) {
var icon = event.currentTarget;
function updateIframeHeight(
iframe,
doc
) {
if (!doc || !doc.body) {
return;
}
var bodyHeight =
doc.body.scrollHeight;
var htmlHeight =
doc.documentElement
? doc.documentElement.scrollHeight
: 0;
var height = Math.max(
bodyHeight,
htmlHeight,
180
);
iframe.style.height =
height + 20 + 'px';
}
function downloadEmail(
event,
id
) {
var button =
event.currentTarget;
if (!id) {
return;
}
var originalTransform =
icon.style.transform;
button.style.transform;
var originalOpacity =
icon.style.opacity;
button.style.opacity;
icon.style.transform =
button.disabled = true;
button.style.transform =
'scale(0.8)';
icon.style.opacity =
button.style.opacity =
'0.5';
window.location.href =
fetch(
'/api/download?id=' +
encodeURIComponent(id);
encodeURIComponent(id),
{
method: 'GET',
credentials: 'same-origin'
}
)
.then(function (response) {
handleUnauthorized(response);
window.setTimeout(
function () {
icon.style.transform =
if (!response.ok) {
throw new Error(
'HTTP error ' +
response.status
);
}
return Promise.all([
response.blob(),
Promise.resolve(
getDownloadFilename(
response,
id
)
)
]);
})
.then(function (result) {
var blob = result[0];
var filename = result[1];
var objectURL =
URL.createObjectURL(blob);
var link =
document.createElement('a');
link.href = objectURL;
link.download = filename;
document.body.appendChild(
link
);
link.click();
link.remove();
window.setTimeout(
function () {
URL.revokeObjectURL(
objectURL
);
},
1000
);
})
.catch(function (error) {
if (
error &&
error.message ===
'Session expired'
) {
return;
}
console.error(
'Failed to download email:',
id,
error
);
window.alert(
'Failed to download email'
);
})
.finally(function () {
button.disabled = false;
button.style.transform =
originalTransform;
icon.style.opacity =
button.style.opacity =
originalOpacity;
},
150
);
});
}
function getDownloadFilename(
response,
id
) {
var contentDisposition =
response.headers.get(
'Content-Disposition'
);
if (contentDisposition) {
var match =
contentDisposition.match(
/filename="([^"]+)"/i
);
if (match && match[1]) {
return match[1];
}
}
var parts = id.split('/');
if (parts.length > 0) {
return parts[
parts.length - 1
];
}
return 'email.eml';
}
function escapeHTML(value) {