@limetech/lime-elements 39.5.0 → 39.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## [39.5.2](https://github.com/Lundalogik/lime-elements/compare/v39.5.1...v39.5.2) (2026-02-27)
2
+
3
+ ### Bug Fixes
4
+
5
+
6
+ * **email-viewer:** fix date overflowing its container and overlapping download button ([df03885](https://github.com/Lundalogik/lime-elements/commit/df038850da36bc07b70313d8a270d05cca5ab91f)), closes [Lundalogik/lime-elements#3908](https://github.com/Lundalogik/lime-elements/issues/3908)
7
+
8
+ ## [39.5.1](https://github.com/Lundalogik/lime-elements/compare/v39.5.0...v39.5.1) (2026-02-26)
9
+
10
+ ### Bug Fixes
11
+
12
+
13
+ * **email-viewer:** render inline cid logos in .eml previews ([29b85d3](https://github.com/Lundalogik/lime-elements/commit/29b85d3759e81ce0c3209fb911031d5a4f82a292))
14
+
1
15
  ## [39.5.0](https://github.com/Lundalogik/lime-elements/compare/v39.4.1...v39.5.0) (2026-02-25)
2
16
 
3
17
  ### Features
@@ -200,7 +200,7 @@ function formatBytes(bytes, decimals = 1) {
200
200
  return `${rounded} ${sizes[i]}`;
201
201
  }
202
202
 
203
- 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;transform:translateY(-50%);font-size:0.625rem;border-radius:9rem;padding:0.25rem 0.5rem;background-color:rgb(var(--contrast-100), 0.3)}.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 rgba(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}`;
203
+ 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}`;
204
204
 
205
205
  const EmailViewer = class {
206
206
  constructor(hostRef) {
@@ -4878,11 +4878,13 @@ function extractAttachments(email) {
4878
4878
  const cidUrlById = new Map();
4879
4879
  for (const attachment of email.attachments || []) {
4880
4880
  const contentId = normalizeContentId(attachment.contentId);
4881
- const isInline = attachment.related || attachment.disposition === 'inline';
4881
+ const hasContentId = Boolean(contentId);
4882
+ const isInline = (hasContentId && attachment.disposition !== 'attachment') ||
4883
+ attachment.related ||
4884
+ attachment.disposition === 'inline';
4885
+ const contentBytes = getAttachmentBytes(attachment.content);
4882
4886
  if (!isInline) {
4883
- const size = attachment.content instanceof ArrayBuffer
4884
- ? attachment.content.byteLength
4885
- : undefined;
4887
+ const size = contentBytes === null || contentBytes === void 0 ? void 0 : contentBytes.byteLength;
4886
4888
  attachments.push({
4887
4889
  filename: attachment.filename || undefined,
4888
4890
  mimeType: attachment.mimeType || undefined,
@@ -4890,11 +4892,9 @@ function extractAttachments(email) {
4890
4892
  });
4891
4893
  continue;
4892
4894
  }
4893
- const hasArrayBufferContent = attachment.content instanceof ArrayBuffer;
4894
- if (!contentId || !hasArrayBufferContent) {
4895
- const size = hasArrayBufferContent
4896
- ? attachment.content.byteLength
4897
- : undefined;
4895
+ const hasBinaryContent = Boolean(contentBytes);
4896
+ if (!contentId || !hasBinaryContent) {
4897
+ const size = contentBytes === null || contentBytes === void 0 ? void 0 : contentBytes.byteLength;
4898
4898
  attachments.push({
4899
4899
  filename: attachment.filename || undefined,
4900
4900
  mimeType: attachment.mimeType || 'application/octet-stream',
@@ -4902,8 +4902,8 @@ function extractAttachments(email) {
4902
4902
  });
4903
4903
  continue;
4904
4904
  }
4905
- const mimeType = attachment.mimeType || 'application/octet-stream';
4906
- const base64 = arrayBufferToBase64(attachment.content);
4905
+ const mimeType = resolveDataUrlMimeType(attachment.mimeType, contentBytes, attachment.filename);
4906
+ const base64 = byteArrayToBase64(contentBytes);
4907
4907
  const dataUrl = `data:${mimeType};base64,${base64}`;
4908
4908
  cidUrlById.set(contentId, dataUrl);
4909
4909
  }
@@ -4930,6 +4930,9 @@ function normalizeContentId(contentId) {
4930
4930
  return '';
4931
4931
  }
4932
4932
  let normalized = contentId.trim();
4933
+ if (normalized.toLowerCase().startsWith('cid:')) {
4934
+ normalized = normalized.slice(4).trim();
4935
+ }
4933
4936
  if (normalized.startsWith('<')) {
4934
4937
  normalized = normalized.slice(1);
4935
4938
  }
@@ -4943,7 +4946,7 @@ function replaceCidReferences(html, cidUrlById) {
4943
4946
  return html;
4944
4947
  }
4945
4948
  return html.replaceAll(/(src\s*=\s*["']?)cid:([^"'\s>]+)(["']?)/gi, (match, prefix, cid, suffix) => {
4946
- const normalized = normalizeContentId(cid);
4949
+ const normalized = normalizeContentId(decodeCidReference(cid));
4947
4950
  const replacement = cidUrlById.get(normalized);
4948
4951
  if (!replacement) {
4949
4952
  return match;
@@ -4951,8 +4954,24 @@ function replaceCidReferences(html, cidUrlById) {
4951
4954
  return `${prefix}${replacement}${suffix}`;
4952
4955
  });
4953
4956
  }
4954
- function arrayBufferToBase64(buffer) {
4955
- const bytes = new Uint8Array(buffer);
4957
+ function decodeCidReference(cid) {
4958
+ try {
4959
+ return decodeURIComponent(cid);
4960
+ }
4961
+ catch (_a) {
4962
+ return cid;
4963
+ }
4964
+ }
4965
+ function getAttachmentBytes(content) {
4966
+ if (content instanceof ArrayBuffer) {
4967
+ return new Uint8Array(content);
4968
+ }
4969
+ if (ArrayBuffer.isView(content)) {
4970
+ return new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
4971
+ }
4972
+ return undefined;
4973
+ }
4974
+ function byteArrayToBase64(bytes) {
4956
4975
  if (typeof btoa === 'function') {
4957
4976
  let binary = '';
4958
4977
  for (const byte of bytes) {
@@ -4963,6 +4982,113 @@ function arrayBufferToBase64(buffer) {
4963
4982
  // Jest/Node fallback
4964
4983
  return globalThis.Buffer.from(bytes).toString('base64');
4965
4984
  }
4985
+ function resolveDataUrlMimeType(mimeType, bytes, filename) {
4986
+ const normalizedMimeType = typeof mimeType === 'string' ? mimeType.trim().toLowerCase() : '';
4987
+ const detectedFromBytes = detectImageMimeTypeFromBytes(bytes);
4988
+ if (normalizedMimeType.startsWith('image/')) {
4989
+ if (isTrustedDeclaredImageMimeType(normalizedMimeType, detectedFromBytes)) {
4990
+ return normalizedMimeType;
4991
+ }
4992
+ if (detectedFromBytes) {
4993
+ return detectedFromBytes;
4994
+ }
4995
+ }
4996
+ if (detectedFromBytes) {
4997
+ return detectedFromBytes;
4998
+ }
4999
+ const detectedFromFilename = detectImageMimeTypeFromFilename(filename);
5000
+ if (detectedFromFilename) {
5001
+ return detectedFromFilename;
5002
+ }
5003
+ return normalizedMimeType || 'application/octet-stream';
5004
+ }
5005
+ function isTrustedDeclaredImageMimeType(declaredMimeType, detectedMimeType) {
5006
+ if (!detectedMimeType) {
5007
+ return true;
5008
+ }
5009
+ if (declaredMimeType === detectedMimeType) {
5010
+ return true;
5011
+ }
5012
+ if (declaredMimeType === 'image/jpg' && detectedMimeType === 'image/jpeg') {
5013
+ return true;
5014
+ }
5015
+ return (declaredMimeType === 'image/vnd.microsoft.icon' &&
5016
+ detectedMimeType === 'image/x-icon');
5017
+ }
5018
+ function detectImageMimeTypeFromBytes(bytes) {
5019
+ var _a, _b, _c, _d, _e;
5020
+ return ((_e = (_d = (_c = (_b = (_a = detectPngMimeType(bytes)) !== null && _a !== void 0 ? _a : detectJpegMimeType(bytes)) !== null && _b !== void 0 ? _b : detectGifMimeType(bytes)) !== null && _c !== void 0 ? _c : detectWebpMimeType(bytes)) !== null && _d !== void 0 ? _d : detectIconMimeType(bytes)) !== null && _e !== void 0 ? _e : detectSvgMimeType(bytes));
5021
+ }
5022
+ function detectPngMimeType(bytes) {
5023
+ if (startsWithBytes(bytes, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) {
5024
+ return 'image/png';
5025
+ }
5026
+ }
5027
+ function detectJpegMimeType(bytes) {
5028
+ if (startsWithBytes(bytes, [0xff, 0xd8, 0xff])) {
5029
+ return 'image/jpeg';
5030
+ }
5031
+ }
5032
+ function detectGifMimeType(bytes) {
5033
+ const firstSix = bytesToAscii(bytes, 0, 6);
5034
+ if (firstSix === 'GIF87a' || firstSix === 'GIF89a') {
5035
+ return 'image/gif';
5036
+ }
5037
+ }
5038
+ function detectWebpMimeType(bytes) {
5039
+ const riff = bytesToAscii(bytes, 0, 4);
5040
+ const webp = bytesToAscii(bytes, 8, 12);
5041
+ if (riff === 'RIFF' && webp === 'WEBP') {
5042
+ return 'image/webp';
5043
+ }
5044
+ }
5045
+ function detectIconMimeType(bytes) {
5046
+ if (startsWithBytes(bytes, [0x00, 0x00, 0x01, 0x00])) {
5047
+ return 'image/x-icon';
5048
+ }
5049
+ }
5050
+ function detectSvgMimeType(bytes) {
5051
+ if (bytes.length < 5) {
5052
+ return;
5053
+ }
5054
+ const utf8Prefix = new TextDecoder('utf8', { fatal: false }).decode(bytes.slice(0, Math.min(bytes.length, 256)));
5055
+ const normalizedPrefix = utf8Prefix.trimStart().toLowerCase();
5056
+ if (normalizedPrefix.startsWith('<svg') ||
5057
+ (normalizedPrefix.startsWith('<?xml') &&
5058
+ normalizedPrefix.includes('<svg'))) {
5059
+ return 'image/svg+xml';
5060
+ }
5061
+ }
5062
+ function startsWithBytes(bytes, prefix) {
5063
+ if (bytes.length < prefix.length) {
5064
+ return false;
5065
+ }
5066
+ return prefix.every((value, index) => bytes[index] === value);
5067
+ }
5068
+ function bytesToAscii(bytes, start, endExclusive) {
5069
+ if (bytes.length < endExclusive) {
5070
+ return '';
5071
+ }
5072
+ return String.fromCodePoint(...bytes.slice(start, endExclusive));
5073
+ }
5074
+ function detectImageMimeTypeFromFilename(filename) {
5075
+ const normalized = filename === null || filename === void 0 ? void 0 : filename.trim().toLowerCase();
5076
+ if (!normalized || !normalized.includes('.')) {
5077
+ return;
5078
+ }
5079
+ const extension = normalized.slice(normalized.lastIndexOf('.') + 1);
5080
+ const mimeTypes = {
5081
+ png: 'image/png',
5082
+ jpg: 'image/jpeg',
5083
+ jpeg: 'image/jpeg',
5084
+ gif: 'image/gif',
5085
+ webp: 'image/webp',
5086
+ svg: 'image/svg+xml',
5087
+ ico: 'image/x-icon',
5088
+ bmp: 'image/bmp',
5089
+ };
5090
+ return mimeTypes[extension];
5091
+ }
4966
5092
  function isNonEmptyString(value) {
4967
5093
  return typeof value === 'string' && value.length > 0;
4968
5094
  }
@@ -66,11 +66,13 @@ function extractAttachments(email) {
66
66
  const cidUrlById = new Map();
67
67
  for (const attachment of email.attachments || []) {
68
68
  const contentId = normalizeContentId(attachment.contentId);
69
- const isInline = attachment.related || attachment.disposition === 'inline';
69
+ const hasContentId = Boolean(contentId);
70
+ const isInline = (hasContentId && attachment.disposition !== 'attachment') ||
71
+ attachment.related ||
72
+ attachment.disposition === 'inline';
73
+ const contentBytes = getAttachmentBytes(attachment.content);
70
74
  if (!isInline) {
71
- const size = attachment.content instanceof ArrayBuffer
72
- ? attachment.content.byteLength
73
- : undefined;
75
+ const size = contentBytes === null || contentBytes === void 0 ? void 0 : contentBytes.byteLength;
74
76
  attachments.push({
75
77
  filename: attachment.filename || undefined,
76
78
  mimeType: attachment.mimeType || undefined,
@@ -78,11 +80,9 @@ function extractAttachments(email) {
78
80
  });
79
81
  continue;
80
82
  }
81
- const hasArrayBufferContent = attachment.content instanceof ArrayBuffer;
82
- if (!contentId || !hasArrayBufferContent) {
83
- const size = hasArrayBufferContent
84
- ? attachment.content.byteLength
85
- : undefined;
83
+ const hasBinaryContent = Boolean(contentBytes);
84
+ if (!contentId || !hasBinaryContent) {
85
+ const size = contentBytes === null || contentBytes === void 0 ? void 0 : contentBytes.byteLength;
86
86
  attachments.push({
87
87
  filename: attachment.filename || undefined,
88
88
  mimeType: attachment.mimeType || 'application/octet-stream',
@@ -90,8 +90,8 @@ function extractAttachments(email) {
90
90
  });
91
91
  continue;
92
92
  }
93
- const mimeType = attachment.mimeType || 'application/octet-stream';
94
- const base64 = arrayBufferToBase64(attachment.content);
93
+ const mimeType = resolveDataUrlMimeType(attachment.mimeType, contentBytes, attachment.filename);
94
+ const base64 = byteArrayToBase64(contentBytes);
95
95
  const dataUrl = `data:${mimeType};base64,${base64}`;
96
96
  cidUrlById.set(contentId, dataUrl);
97
97
  }
@@ -118,6 +118,9 @@ function normalizeContentId(contentId) {
118
118
  return '';
119
119
  }
120
120
  let normalized = contentId.trim();
121
+ if (normalized.toLowerCase().startsWith('cid:')) {
122
+ normalized = normalized.slice(4).trim();
123
+ }
121
124
  if (normalized.startsWith('<')) {
122
125
  normalized = normalized.slice(1);
123
126
  }
@@ -131,7 +134,7 @@ function replaceCidReferences(html, cidUrlById) {
131
134
  return html;
132
135
  }
133
136
  return html.replaceAll(/(src\s*=\s*["']?)cid:([^"'\s>]+)(["']?)/gi, (match, prefix, cid, suffix) => {
134
- const normalized = normalizeContentId(cid);
137
+ const normalized = normalizeContentId(decodeCidReference(cid));
135
138
  const replacement = cidUrlById.get(normalized);
136
139
  if (!replacement) {
137
140
  return match;
@@ -139,8 +142,24 @@ function replaceCidReferences(html, cidUrlById) {
139
142
  return `${prefix}${replacement}${suffix}`;
140
143
  });
141
144
  }
142
- function arrayBufferToBase64(buffer) {
143
- const bytes = new Uint8Array(buffer);
145
+ function decodeCidReference(cid) {
146
+ try {
147
+ return decodeURIComponent(cid);
148
+ }
149
+ catch (_a) {
150
+ return cid;
151
+ }
152
+ }
153
+ function getAttachmentBytes(content) {
154
+ if (content instanceof ArrayBuffer) {
155
+ return new Uint8Array(content);
156
+ }
157
+ if (ArrayBuffer.isView(content)) {
158
+ return new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
159
+ }
160
+ return undefined;
161
+ }
162
+ function byteArrayToBase64(bytes) {
144
163
  if (typeof btoa === 'function') {
145
164
  let binary = '';
146
165
  for (const byte of bytes) {
@@ -151,6 +170,113 @@ function arrayBufferToBase64(buffer) {
151
170
  // Jest/Node fallback
152
171
  return globalThis.Buffer.from(bytes).toString('base64');
153
172
  }
173
+ function resolveDataUrlMimeType(mimeType, bytes, filename) {
174
+ const normalizedMimeType = typeof mimeType === 'string' ? mimeType.trim().toLowerCase() : '';
175
+ const detectedFromBytes = detectImageMimeTypeFromBytes(bytes);
176
+ if (normalizedMimeType.startsWith('image/')) {
177
+ if (isTrustedDeclaredImageMimeType(normalizedMimeType, detectedFromBytes)) {
178
+ return normalizedMimeType;
179
+ }
180
+ if (detectedFromBytes) {
181
+ return detectedFromBytes;
182
+ }
183
+ }
184
+ if (detectedFromBytes) {
185
+ return detectedFromBytes;
186
+ }
187
+ const detectedFromFilename = detectImageMimeTypeFromFilename(filename);
188
+ if (detectedFromFilename) {
189
+ return detectedFromFilename;
190
+ }
191
+ return normalizedMimeType || 'application/octet-stream';
192
+ }
193
+ function isTrustedDeclaredImageMimeType(declaredMimeType, detectedMimeType) {
194
+ if (!detectedMimeType) {
195
+ return true;
196
+ }
197
+ if (declaredMimeType === detectedMimeType) {
198
+ return true;
199
+ }
200
+ if (declaredMimeType === 'image/jpg' && detectedMimeType === 'image/jpeg') {
201
+ return true;
202
+ }
203
+ return (declaredMimeType === 'image/vnd.microsoft.icon' &&
204
+ detectedMimeType === 'image/x-icon');
205
+ }
206
+ function detectImageMimeTypeFromBytes(bytes) {
207
+ var _a, _b, _c, _d, _e;
208
+ return ((_e = (_d = (_c = (_b = (_a = detectPngMimeType(bytes)) !== null && _a !== void 0 ? _a : detectJpegMimeType(bytes)) !== null && _b !== void 0 ? _b : detectGifMimeType(bytes)) !== null && _c !== void 0 ? _c : detectWebpMimeType(bytes)) !== null && _d !== void 0 ? _d : detectIconMimeType(bytes)) !== null && _e !== void 0 ? _e : detectSvgMimeType(bytes));
209
+ }
210
+ function detectPngMimeType(bytes) {
211
+ if (startsWithBytes(bytes, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) {
212
+ return 'image/png';
213
+ }
214
+ }
215
+ function detectJpegMimeType(bytes) {
216
+ if (startsWithBytes(bytes, [0xff, 0xd8, 0xff])) {
217
+ return 'image/jpeg';
218
+ }
219
+ }
220
+ function detectGifMimeType(bytes) {
221
+ const firstSix = bytesToAscii(bytes, 0, 6);
222
+ if (firstSix === 'GIF87a' || firstSix === 'GIF89a') {
223
+ return 'image/gif';
224
+ }
225
+ }
226
+ function detectWebpMimeType(bytes) {
227
+ const riff = bytesToAscii(bytes, 0, 4);
228
+ const webp = bytesToAscii(bytes, 8, 12);
229
+ if (riff === 'RIFF' && webp === 'WEBP') {
230
+ return 'image/webp';
231
+ }
232
+ }
233
+ function detectIconMimeType(bytes) {
234
+ if (startsWithBytes(bytes, [0x00, 0x00, 0x01, 0x00])) {
235
+ return 'image/x-icon';
236
+ }
237
+ }
238
+ function detectSvgMimeType(bytes) {
239
+ if (bytes.length < 5) {
240
+ return;
241
+ }
242
+ const utf8Prefix = new TextDecoder('utf8', { fatal: false }).decode(bytes.slice(0, Math.min(bytes.length, 256)));
243
+ const normalizedPrefix = utf8Prefix.trimStart().toLowerCase();
244
+ if (normalizedPrefix.startsWith('<svg') ||
245
+ (normalizedPrefix.startsWith('<?xml') &&
246
+ normalizedPrefix.includes('<svg'))) {
247
+ return 'image/svg+xml';
248
+ }
249
+ }
250
+ function startsWithBytes(bytes, prefix) {
251
+ if (bytes.length < prefix.length) {
252
+ return false;
253
+ }
254
+ return prefix.every((value, index) => bytes[index] === value);
255
+ }
256
+ function bytesToAscii(bytes, start, endExclusive) {
257
+ if (bytes.length < endExclusive) {
258
+ return '';
259
+ }
260
+ return String.fromCodePoint(...bytes.slice(start, endExclusive));
261
+ }
262
+ function detectImageMimeTypeFromFilename(filename) {
263
+ const normalized = filename === null || filename === void 0 ? void 0 : filename.trim().toLowerCase();
264
+ if (!normalized || !normalized.includes('.')) {
265
+ return;
266
+ }
267
+ const extension = normalized.slice(normalized.lastIndexOf('.') + 1);
268
+ const mimeTypes = {
269
+ png: 'image/png',
270
+ jpg: 'image/jpeg',
271
+ jpeg: 'image/jpeg',
272
+ gif: 'image/gif',
273
+ webp: 'image/webp',
274
+ svg: 'image/svg+xml',
275
+ ico: 'image/x-icon',
276
+ bmp: 'image/bmp',
277
+ };
278
+ return mimeTypes[extension];
279
+ }
154
280
  function isNonEmptyString(value) {
155
281
  return typeof value === 'string' && value.length > 0;
156
282
  }
@@ -208,3 +334,13 @@ function quoteDisplayNameIfNeeded(name) {
208
334
  const escaped = name.replaceAll('\\', '\\\\').replaceAll('"', '\\' + '"');
209
335
  return `"${escaped}"`;
210
336
  }
337
+ export const emailLoaderHelpers = {
338
+ normalizeContentId,
339
+ decodeCidReference,
340
+ replaceCidReferences,
341
+ getAttachmentBytes,
342
+ detectImageMimeTypeFromBytes,
343
+ detectImageMimeTypeFromFilename,
344
+ resolveDataUrlMimeType,
345
+ extractAttachments,
346
+ };
@@ -148,11 +148,13 @@
148
148
  .email-headers dl.date {
149
149
  position: absolute;
150
150
  right: 0.25rem;
151
- transform: translateY(-50%);
151
+ bottom: 0;
152
+ transform: translateY(50%);
152
153
  font-size: 0.625rem;
153
154
  border-radius: 9rem;
154
- padding: 0.25rem 0.5rem;
155
- background-color: rgb(var(--contrast-100), 0.3);
155
+ padding: 0.125rem 0.25rem;
156
+ background-color: rgb(var(--contrast-100), 0.8);
157
+ border: rgb(var(--contrast-600)) solid 1px;
156
158
  }
157
159
  .email-headers dl.date dt {
158
160
  position: absolute;
@@ -170,7 +172,7 @@
170
172
  .attachments {
171
173
  flex-shrink: 0;
172
174
  padding: 0.5rem;
173
- border-bottom: 1px dashed rgba(var(--contrast-700));
175
+ border-bottom: 1px dashed rga(var(--contrast-700));
174
176
  }
175
177
  .attachments span {
176
178
  padding-left: 0.25rem;
@@ -198,7 +198,7 @@ function formatBytes(bytes, decimals = 1) {
198
198
  return `${rounded} ${sizes[i]}`;
199
199
  }
200
200
 
201
- 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;transform:translateY(-50%);font-size:0.625rem;border-radius:9rem;padding:0.25rem 0.5rem;background-color:rgb(var(--contrast-100), 0.3)}.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 rgba(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}`;
201
+ 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}`;
202
202
 
203
203
  const EmailViewer = class {
204
204
  constructor(hostRef) {