1 parent
a116890e20
commit
02d0b13ec1
24 files changed
+245
-93
No files matched your search
@@ -274,6 +274,21 @@
|
||||
max-width: 760px;
|
||||
}
|
||||
|
||||
.compose-limit-summary {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.compose-limit-summary strong {
|
||||
color: var(--text);
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.appearance-presets {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
|
||||
+4
-7
@@ -885,13 +885,10 @@
|
||||
message text suggests a missing attachment</span></label>
|
||||
<label class="checkbox-row"><input id="compose-pref-spellcheck" type="checkbox"><span>Use browser
|
||||
spellcheck while composing</span></label>
|
||||
<label><span>Maximum attachment size</span><output
|
||||
id="compose-pref-max-attachment">__RUSTBOX_MAX_ATTACHMENT_MB__ MiB</output><small
|
||||
class="muted">Per-file upload limit set with <code>max_attachment_size_mb</code> in
|
||||
rustbox.toml.</small></label>
|
||||
<label><span>Maximum message size</span><output id="compose-pref-max-message">__RUSTBOX_MAX_MESSAGE_MB__
|
||||
MiB</output><small class="muted">Hard limit for the complete RFC822/MIME message sent to the MTA,
|
||||
including base64/MIME overhead.</small></label>
|
||||
<div class="compose-limit-summary" aria-label="Compose size limits">
|
||||
<span>Maximum attachment size: <strong id="compose-pref-max-attachment">__RUSTBOX_MAX_ATTACHMENT_MB__ MiB</strong></span>
|
||||
<span>Maximum message size: <strong id="compose-pref-max-message">__RUSTBOX_MAX_MESSAGE_MB__ MiB</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="muted">Markdown is stored as source text and sent as a safe rendered HTML alternative with a
|
||||
plain-text fallback.</p>
|
||||
|
||||
@@ -155,6 +155,21 @@
|
||||
return doc.body.innerHTML;
|
||||
}
|
||||
|
||||
function sanitizeMessagePreviewHtml(html) {
|
||||
const doc = new DOMParser().parseFromString(sanitizeMessageHtml(html), "text/html");
|
||||
doc.querySelectorAll("a[href]").forEach((node) => {
|
||||
if (!node.hasAttribute("target")) node.setAttribute("target", "_blank");
|
||||
const target = String(node.getAttribute("target") || "").toLowerCase();
|
||||
if (target && !["_self", "_parent", "_top"].includes(target)) {
|
||||
const rel = new Set(String(node.getAttribute("rel") || "").split(/\s+/).filter(Boolean));
|
||||
rel.add("noopener");
|
||||
rel.add("noreferrer");
|
||||
node.setAttribute("rel", [...rel].join(" "));
|
||||
}
|
||||
});
|
||||
return doc.body.innerHTML;
|
||||
}
|
||||
|
||||
function sanitizeAttachmentHtml(html) {
|
||||
const doc = new DOMParser().parseFromString(sanitizeMessageHtml(html), "text/html");
|
||||
doc.querySelectorAll("a[href]").forEach((node) => node.removeAttribute("href"));
|
||||
|
||||
@@ -458,10 +458,10 @@ function showApp() {
|
||||
$("#app-view").classList.remove("hidden");
|
||||
}
|
||||
|
||||
function finishAuthenticatedStartup() {
|
||||
function finishAuthenticatedStartup({ showLoginToast = false } = {}) {
|
||||
state.startupToastsMuted = false;
|
||||
armRealtimeNotifications();
|
||||
toast(tr("login_success", "Successfully logged in"), "success");
|
||||
if (showLoginToast) toast(tr("login_success", "Successfully logged in"), "success");
|
||||
}
|
||||
|
||||
function prepareAuthenticatedStartup() {
|
||||
@@ -735,7 +735,7 @@ async function login(event) {
|
||||
await loadTrustedSenders();
|
||||
startCalendarReminderWatcher();
|
||||
connectRealtime();
|
||||
finishAuthenticatedStartup();
|
||||
finishAuthenticatedStartup({ showLoginToast: true });
|
||||
} catch (error) {
|
||||
errorBox.textContent = error.message;
|
||||
errorBox.classList.remove("hidden");
|
||||
|
||||
@@ -688,8 +688,8 @@ function renderMessageBody(detail, forceRemote = false) {
|
||||
const imageSources = allowRemote ? "data: cid: https: http:" : "data: cid:";
|
||||
const csp = `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${imageSources}; style-src 'unsafe-inline'; font-src data:">`;
|
||||
const quoteStyle = state.mailPrefs.collapse_quotes !== false ? "blockquote{max-height:16em;overflow:auto;border-left:2px solid #d9ddda;padding-left:12px;margin-left:0}" : "blockquote{border-left:2px solid #d9ddda;padding-left:12px;margin-left:0}";
|
||||
const safeHtml = sanitizeMessageHtml(detail.html);
|
||||
iframe.srcdoc = `<!doctype html><html><head>${csp}<meta name="color-scheme" content="light"><style>body{margin:0;padding:22px;color:#202622;background:#fff;font:14px/1.6 Inter,system-ui,-apple-system,'Segoe UI',sans-serif}img{max-width:100%;height:auto}a{color:#356a56}pre{white-space:pre-wrap}${quoteStyle}</style></head><body>${safeHtml}</body></html>`;
|
||||
const safeHtml = sanitizeMessagePreviewHtml(detail.html);
|
||||
iframe.srcdoc = `<!doctype html><html><head><meta charset="utf-8">${csp}<meta name="color-scheme" content="light"><style>body{margin:0;padding:22px;color:#202622;background:#fff;font:14px/1.6 Inter,system-ui,-apple-system,'Segoe UI',sans-serif}img{max-width:100%;height:auto}a{color:#356a56}pre{white-space:pre-wrap}${quoteStyle}</style></head><body>${safeHtml}</body></html>`;
|
||||
body.append(iframe);
|
||||
}
|
||||
|
||||
@@ -722,6 +722,12 @@ function attachmentMime(attachment, blob = null) {
|
||||
return String(blob?.type || attachment?.content_type || attachment?.contentType || "application/octet-stream").toLowerCase();
|
||||
}
|
||||
|
||||
function attachmentContentUrl(folder, uid, attachment) {
|
||||
const path = apiPath(`/messages/${encodeURIComponent(folder)}/${uid}/attachments/${encodeURIComponent(attachment.id)}`);
|
||||
if (!state.accountId) return path;
|
||||
return `${path}${path.includes("?") ? "&" : "?"}account_id=${encodeURIComponent(state.accountId)}`;
|
||||
}
|
||||
|
||||
function attachmentPreviewLimitBytes(attachment) {
|
||||
const mime = attachmentMime(attachment);
|
||||
const filename = String(attachment?.filename || "").toLowerCase();
|
||||
@@ -759,9 +765,11 @@ async function openAttachment(folder, uid, attachment) {
|
||||
await offerAttachmentDownload(folder, uid, attachment, "This attachment is too large for preview. Download it instead?");
|
||||
return;
|
||||
}
|
||||
const blob = await fetchAttachmentBlob(folder, uid, attachment);
|
||||
const mime = attachmentMime(attachment, blob);
|
||||
const declaredMime = attachmentMime(attachment);
|
||||
const filename = String(attachment?.filename || "").toLowerCase();
|
||||
const isPdf = declaredMime === "application/pdf" || filename.endsWith(".pdf");
|
||||
const blob = isPdf ? null : await fetchAttachmentBlob(folder, uid, attachment);
|
||||
const mime = isPdf ? "application/pdf" : attachmentMime(attachment, blob);
|
||||
const isHtml = mime.includes("html") || /\.(html?|xhtml)$/i.test(filename);
|
||||
const isImage = mime.startsWith("image/") || /\.(png|jpe?g|gif|webp)$/i.test(filename);
|
||||
const isVideo = mime.startsWith("video/") || /\.(mp4|webm|ogv|mov)$/i.test(filename);
|
||||
@@ -771,7 +779,7 @@ async function openAttachment(folder, uid, attachment) {
|
||||
await offerAttachmentDownload(folder, uid, attachment, "Preview is not available for this attachment type. Download it instead?");
|
||||
return;
|
||||
}
|
||||
if (attachmentExceedsPreviewLimit(previewAttachment, blob.size)) {
|
||||
if (blob && attachmentExceedsPreviewLimit(previewAttachment, blob.size)) {
|
||||
await offerAttachmentDownload(folder, uid, attachment, "This attachment is too large for preview. Download it instead?");
|
||||
return;
|
||||
}
|
||||
@@ -794,12 +802,12 @@ async function openAttachment(folder, uid, attachment) {
|
||||
image.src = objectUrl;
|
||||
image.alt = attachment.filename || "Attachment image";
|
||||
body.append(image);
|
||||
} else if (mime === "application/pdf" || filename.endsWith(".pdf")) {
|
||||
objectUrl = URL.createObjectURL(blob);
|
||||
} else if (isPdf) {
|
||||
const frame = document.createElement("iframe");
|
||||
frame.src = objectUrl;
|
||||
frame.src = attachmentContentUrl(folder, uid, attachment);
|
||||
frame.title = attachment.filename || "PDF preview";
|
||||
frame.referrerPolicy = "no-referrer";
|
||||
frame.loading = "eager";
|
||||
body.append(frame);
|
||||
} else if (isVideo) {
|
||||
objectUrl = URL.createObjectURL(blob);
|
||||
@@ -815,7 +823,7 @@ async function openAttachment(folder, uid, attachment) {
|
||||
frame.referrerPolicy = "no-referrer";
|
||||
const safeHtml = sanitizeAttachmentHtml(await blob.text());
|
||||
const csp = `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src data:; style-src 'unsafe-inline'; font-src data:">`;
|
||||
frame.srcdoc = `<!doctype html><html><head>${csp}<meta name="color-scheme" content="light"><style>body{margin:0;padding:20px;color:#202622;background:#fff;font:14px/1.55 system-ui,sans-serif}img{max-width:100%;height:auto}pre{white-space:pre-wrap}</style></head><body>${safeHtml}</body></html>`;
|
||||
frame.srcdoc = `<!doctype html><html><head><meta charset="utf-8">${csp}<meta name="color-scheme" content="light"><style>body{margin:0;padding:20px;color:#202622;background:#fff;font:14px/1.55 system-ui,sans-serif}img{max-width:100%;height:auto}pre{white-space:pre-wrap}</style></head><body>${safeHtml}</body></html>`;
|
||||
body.append(frame);
|
||||
} else {
|
||||
const pre = document.createElement("pre");
|
||||
|
||||
Reference in new issue
Block a user