release: 1.1.1 - session auth and security improvements
This commit is contained in:
+382
-129
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user