@limetech/lime-elements
Version:
350 lines (343 loc) • 17.5 kB
JavaScript
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 = () => ` "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 };