UNPKG

@limetech/lime-elements

Version:
350 lines (343 loc) • 17.5 kB
import { r as registerInstance, c as createEvent, h, H as Host } from './index-DBTJNfo7.js'; import { t as translate } from './translations-DVRaJQvC.js'; /** * Checks whether the given HTML contains any images with a `data-remote-src` * attribute, indicating they reference external (remote) resources. * * @param html - The HTML string to inspect. */ function containsRemoteImages(html) { const parser = new DOMParser(); const document = parser.parseFromString(html, 'text/html'); return document.querySelector('img[data-remote-src]') !== null; } /** * If `allowRemoteImages` is `true`, replaces the `src` attribute of every * `<img data-remote-src="...">` element with the value of `data-remote-src` * (provided it points to an http(s) URL) and removes the data attribute. * * When `allowRemoteImages` is `false` the HTML is returned unchanged. * * Returns an HTML fragment that is safe to assign to `innerHTML` on a regular * container element (not a full `<html>/<head>/<body>` document string). * * @param html - The HTML string to process. * @param allowRemoteImages - Whether to restore remote image sources. */ function applyRemoteImagesPolicy(html, allowRemoteImages) { if (!allowRemoteImages) { return html; } const parser = new DOMParser(); const document = parser.parseFromString(html, 'text/html'); const images = document.querySelectorAll('img[data-remote-src]'); for (const image of images) { const remoteSrc = image.dataset.remoteSrc; if (!remoteSrc || !isAllowedRemoteImageUrl(remoteSrc)) { delete image.dataset.remoteSrc; continue; } image.setAttribute('src', remoteSrc); delete image.dataset.remoteSrc; } const headStyles = [...document.head.querySelectorAll('style')] .map((style) => style.outerHTML) .join(''); return `${headStyles}${document.body.innerHTML}`; } function isAllowedRemoteImageUrl(url) { const trimmed = url.trim(); const lower = trimmed.toLowerCase(); return lower.startsWith('https://') || lower.startsWith('http://'); } /** * Splits a comma-separated email address list (e.g. `To:` / `Cc:`) into individual * recipient strings. * * In RFC 5322, address lists are comma-separated. However, commas can also appear * inside quoted display names (quoted-string) and must then be ignored as separators. * This splitter only treats a comma as a separator when it is outside quoted strings * and outside angle-bracketed address parts (`<...>`). * * Notes: * - If a display name contains a comma, it should be quoted or encoded to be * unambiguous, e.g. `"Doe, Jane" <jane.doe@example.com>` or * `=?UTF-8?Q?Doe,_Jane?= <jane.doe@example.com>`. * - Real-world `.eml` files are usually RFC-ish but not always perfectly compliant. * Malformed input with unquoted commas in display names may be split incorrectly. * * @param value - A comma-separated list of recipients. * @returns An array of trimmed recipient strings. * * @example * splitEmailAddressList('"Doe, Jane" <jane@example.com>, Team <team@example.com>'); * // => ['"Doe, Jane" <jane@example.com>', 'Team <team@example.com>'] */ function splitEmailAddressList(value) { const parts = []; let current = ''; const state = { inQuotes: false, escapeNext: false, angleDepth: 0, }; const append = (character) => { current += character; }; const flush = () => { const trimmed = current.trim(); if (trimmed) { parts.push(trimmed); } current = ''; }; for (const character of value) { if (consumeEscaped(character, state, append)) { continue; } if (beginEscape(character, state, append)) { continue; } if (toggleQuote(character, state, append)) { continue; } if (adjustAngleDepth(character, state, append)) { continue; } if (isAddressSeparator(character, state)) { flush(); continue; } append(character); } flush(); return parts; } function consumeEscaped(character, state, append) { if (!state.escapeNext) { return false; } append(character); state.escapeNext = false; return true; } function beginEscape(character, state, append) { if (!state.inQuotes || character !== '\\') { return false; } append(character); state.escapeNext = true; return true; } function toggleQuote(character, state, append) { if (character !== '"' || state.angleDepth !== 0) { return false; } append(character); state.inQuotes = !state.inQuotes; return true; } function adjustAngleDepth(character, state, append) { if (state.inQuotes) { return false; } if (character === '<') { append(character); state.angleDepth += 1; return true; } if (character !== '>' || state.angleDepth === 0) { return false; } append(character); state.angleDepth -= 1; return true; } function isAddressSeparator(character, state) { return character === ',' && !state.inQuotes && state.angleDepth === 0; } /** * Format a file size in bytes into a human readable string. * * Uses base 1024 units (binary prefixes without the "i" designation) * and applies adaptive precision: one decimal for values < 10 of the * chosen unit, otherwise no decimals. * * Examples: * - 0 => "0 B" * - 512 => "512 B" * - 1536 => "1.5 KB" * - 1048576 => "1 MB" * - 5368709120 => "5 GB" * - formatBytes(5347737600, 2) => "4.98 GB" (value < 10 so two decimals) * * @param bytes - the size in bytes * @param decimals - max number of decimals for small unit values (default: 1) * @returns formatted size string */ function formatBytes(bytes, decimals = 1) { if (bytes == null || Number.isNaN(bytes)) { return ''; } if (bytes < 0) { return ''; } if (bytes === 0) { return '0 B'; } const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const i = Math.max(0, Math.min(sizes.length - 1, Math.floor(Math.log(bytes) / Math.log(k)))); const value = bytes / Math.pow(k, i); const safeDecimals = Number.isFinite(decimals) ? Math.trunc(decimals) : 0; const precision = value < 10 ? Math.min(100, Math.max(0, safeDecimals)) : 0; // only keep decimals for small values const rounded = Number.parseFloat(value.toFixed(precision)); return `${rounded} ${sizes[i]}`; } const emailViewerCss = () => `@charset "UTF-8";:host(limel-email-viewer){display:block;width:100%;height:100%;box-sizing:border-box}*,*::before,*::after{box-sizing:border-box;min-width:0;min-height:0}.email{display:flex;flex-direction:column;width:100%;height:100%;padding-bottom:0.5rem;box-shadow:var(--shadow-depth-8)}.email-headers{position:relative;flex-shrink:0;display:flex;flex-direction:column}.email-headers dl,.email-headers dt,.email-headers dd{margin:0}.email-headers dl{display:flex;flex-wrap:wrap;gap:0 0.5rem;padding:0.5rem 0.75rem;font-size:0.75rem}.email-headers dl:nth-child(even){background-color:rgb(var(--contrast-800), 0.1)}.email-headers dl dt{opacity:0.6;min-width:3rem}.email-headers dl dt::after{content:":"}.email-headers dl dd:not(:last-child)::after{content:",";opacity:0.6}.email-headers dl.subject dd{font-weight:bold}.email-headers dl.date{position:absolute;right:0.25rem;bottom:0;transform:translateY(50%);font-size:0.625rem;border-radius:9rem;padding:0.125rem 0.25rem;background-color:rgb(var(--contrast-100), 0.8);border:rgb(var(--contrast-600)) solid 1px}.email-headers dl.date dt{position:absolute;width:0;height:0;margin:-1px;padding:0;border:0;overflow:hidden;clip:rect(0, 0, 0, 0);clip-path:inset(50%);white-space:nowrap}.attachments{flex-shrink:0;padding:0.5rem;border-bottom:1px dashed rga(var(--contrast-700))}.attachments span{padding-left:0.25rem;font-size:0.75rem;opacity:0.6}.attachments span:first-child::after{content:":"}.attachments ul{all:unset;display:grid;grid-template-columns:repeat(auto-fill, minmax(8rem, 1fr));gap:0.5rem;padding:0.5rem 0}.attachments li{all:unset;position:relative;display:flex;flex-direction:column;gap:0.25rem;font-size:0.6875rem;line-height:normal;padding:0.5rem 0.5rem 1rem 0.5rem;border-radius:0.5rem;border:1px solid rgba(var(--contrast-600));background-color:rgba(var(--contrast-400))}.attachments .attachment-filename{font-weight:500}.attachments .attachment-mime-type{opacity:0.7}.attachments limel-badge{--badge-max-width:auto;--badge-background-color:rgb(var(--contrast-1000), 0.7);--badge-text-color:rgb(var(--color-white));position:absolute;bottom:0.125rem;right:0.125rem;box-shadow:var(--shadow-brighten-edges-outside)}section{flex-grow:1;display:flex;flex-direction:column;border-top:1px dashed rgba(var(--contrast-700));min-height:2rem;overflow-y:auto}limel-collapsible-section{--closed-header-background-color:var( --lime-elevated-surface-background-color );flex-grow:1;flex-shrink:0;margin:0.5rem;border-radius:0.75rem;box-shadow:var(--shadow-depth-8)}limel-collapsible-section button{all:unset;flex-shrink:0;border-radius:0.375rem;padding:0.25rem 0.5rem;font-size:var(--limel-theme-small-font-size);margin:0 0.5rem}limel-collapsible-section button.load-images{transition:color var(--limel-clickable-transition-speed, 0.4s) ease, background-color var(--limel-clickable-transition-speed, 0.4s) ease, box-shadow var(--limel-clickable-transform-speed, 0.4s) ease, transform var(--limel-clickable-transform-speed, 0.4s) var(--limel-clickable-transform-timing-function, ease);cursor:pointer;color:var(--lime-primary-color, var(--limel-theme-primary-color));background-color:var(--lime-elevated-surface-background-color);box-shadow:var(--button-shadow-normal)}limel-collapsible-section button.load-images:hover,limel-collapsible-section button.load-images:focus,limel-collapsible-section button.load-images:focus-visible{will-change:color, background-color, box-shadow, transform}limel-collapsible-section button.load-images:hover,limel-collapsible-section button.load-images:focus-visible{transform:translate3d(0, -0.04rem, 0);color:var(--limel-theme-on-surface-color);background-color:var(--lime-elevated-surface-background-color);box-shadow:var(--button-shadow-hovered)}limel-collapsible-section button.load-images:active{--limel-clickable-transform-timing-function:cubic-bezier( 0.83, -0.15, 0.49, 1.16 );transform:translate3d(0, 0.05rem, 0);box-shadow:var(--button-shadow-pressed)}limel-collapsible-section button.load-images:hover,limel-collapsible-section button.load-images:active{--limel-clickable-transition-speed:0.2s;--limel-clickable-transform-speed:0.16s}limel-collapsible-section limel-markdown{padding:0.5rem}.body{flex-grow:1;max-width:100%;padding:0.75rem}.body.plain-text{white-space:pre-wrap;overflow-wrap:anywhere;margin:0;font-family:inherit}.body img{max-width:100% !important}`; const EmailViewer = class { constructor(hostRef) { registerInstance(this, hostRef); this.allowRemoteImagesChange = createEvent(this, "allowRemoteImagesChange"); /** * Defines the localization for translations. */ this.language = 'en'; this.allowRemoteImagesState = false; this.renderAttachment = (attachment, index) => { var _a, _b; const filename = ((_a = attachment.filename) === null || _a === void 0 ? void 0 : _a.trim()) || this.getTranslation('file-viewer.email.attachment.unnamed'); const mimeType = ((_b = attachment.mimeType) === null || _b === void 0 ? void 0 : _b.trim()) || ''; return (h("li", { key: `attachment-${index}` }, h("span", { class: "attachment-filename" }, filename), h("span", { class: "attachment-mime-type" }, " ", mimeType), this.renderSizeBadge(attachment.size))); }; this.renderSizeBadge = (size) => { if (typeof size !== 'number') { return; } return (h("limel-badge", { class: "attachment-size", label: formatBytes(size) })); }; this.onEnableRemoteImagesClick = (event) => { var _a; (_a = event === null || event === void 0 ? void 0 : event.stopPropagation) === null || _a === void 0 ? void 0 : _a.call(event); this.enableRemoteImages(); }; this.enableRemoteImages = () => { if (this.allowRemoteImages !== undefined) { this.allowRemoteImagesChange.emit(true); return; } this.allowRemoteImagesState = true; }; } resetAllowRemoteImages(newEmail, oldEmail) { if (!newEmail) { this.allowRemoteImagesState = false; return; } if (newEmail.from !== (oldEmail === null || oldEmail === void 0 ? void 0 : oldEmail.from)) { this.allowRemoteImagesState = false; } } render() { return (h(Host, { key: 'b37cad79c4d85f9cef78ef741d882d5384c039db' }, h("div", { key: '89e9fa6c9c39234ec9268d93837f0b27f328d046', class: "email", part: "email" }, this.renderHeaders(), this.renderRemoteImageBanner(), h("section", { key: '01f4c97918464218bc0d4ab4e79214027993360d' }, this.renderAttachments(), this.renderBody())))); } renderHeaders() { const headerFields = [ 'subject', 'from', 'to', 'cc', 'date', ]; return (h("div", { class: "email-headers", part: "email-headers" }, headerFields.map((type) => { var _a; return this.renderEmailHeader(type, this.getTranslation(`file-viewer.email.${type}`), (_a = this.email) === null || _a === void 0 ? void 0 : _a[type]); }))); } renderBody() { return (this.renderBodyHtml() || this.renderBodyText() || this.renderFallbackUrl() || h("slot", { name: "fallback" })); } renderBodyHtml() { var _a; const bodyHtml = (_a = this.email) === null || _a === void 0 ? void 0 : _a.bodyHtml; if (!bodyHtml) { return; } const innerHtml = applyRemoteImagesPolicy(bodyHtml, this.getAllowRemoteImages()); return h("div", { class: "body", innerHTML: innerHtml, part: "email-body" }); } renderBodyText() { var _a; const bodyText = (_a = this.email) === null || _a === void 0 ? void 0 : _a.bodyText; if (!bodyText) { return; } return (h("pre", { class: "body plain-text", part: "email-body" }, bodyText)); } renderFallbackUrl() { if (!this.fallbackUrl) { return; } return (h("object", { data: this.fallbackUrl, type: "text/plain" }, h("slot", { name: "fallback" }))); } renderEmailHeader(type, label, value) { if (!value) { return; } const values = this.getHeaderValues(type, value); return (h("dl", { class: `headers ${type}` }, h("dt", null, label), values.map((headerValue, index) => (h("dd", { key: `${type}-${index}` }, headerValue))))); } getHeaderValues(type, value) { if (type === 'to' || type === 'cc') { return splitEmailAddressList(value); } return [value]; } renderAttachments() { var _a; const attachments = (_a = this.email) === null || _a === void 0 ? void 0 : _a.attachments; if (!(attachments === null || attachments === void 0 ? void 0 : attachments.length)) { return; } const label = this.getTranslation('file-viewer.email.attachments'); return (h("div", { class: "attachments" }, h("span", null, label), h("ul", null, attachments.map((attachment, index) => this.renderAttachment(attachment, index))))); } getTranslation(key) { return translate.get(key, this.language); } shouldShowRemoteImagesBanner() { var _a; const bodyHtml = (_a = this.email) === null || _a === void 0 ? void 0 : _a.bodyHtml; if (!bodyHtml || this.getAllowRemoteImages()) { return false; } return containsRemoteImages(bodyHtml); } renderRemoteImageBanner() { if (!this.shouldShowRemoteImagesBanner()) { return; } const icon = { name: 'warning_shield', color: 'rgb(var(--color-orange-default))', }; const heading = this.getTranslation('file-viewer.email.remote-images.warning'); const description = this.getTranslation('file-viewer.email.remote-images.warning.description'); const buttonLabel = this.getTranslation('file-viewer.email.remote-images.load'); return (h("limel-collapsible-section", { header: heading, icon: icon, language: this.language }, h("button", { type: "button", class: "load-images", slot: "header", onClick: this.onEnableRemoteImagesClick }, buttonLabel), h("limel-markdown", { value: description }))); } getAllowRemoteImages() { var _a; return (_a = this.allowRemoteImages) !== null && _a !== void 0 ? _a : this.allowRemoteImagesState; } static get watchers() { return { "email": [{ "resetAllowRemoteImages": 0 }] }; } }; EmailViewer.style = emailViewerCss(); export { EmailViewer as limel_email_viewer };