@schukai/monster 4.37.2 → 4.38.0

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.
@@ -14,10 +14,10 @@
14
14
 
15
15
  import { instanceSymbol } from "../../../constants.mjs";
16
16
  import {
17
- assembleMethodSymbol,
18
- CustomElement,
19
- registerCustomElement,
20
- updaterTransformerMethodsSymbol,
17
+ assembleMethodSymbol,
18
+ CustomElement,
19
+ registerCustomElement,
20
+ updaterTransformerMethodsSymbol,
21
21
  } from "../../../dom/customelement.mjs";
22
22
 
23
23
  import { sanitizeHtml } from "../../../dom/sanitize-html.mjs";
@@ -63,383 +63,383 @@ const embeddedImageUrlsSymbol = Symbol("embeddedImageUrls");
63
63
  * @summary An HTML content component that can display sanitized HTML.
64
64
  */
65
65
  class MessageContent extends CustomElement {
66
- constructor() {
67
- super();
68
- this[embeddedImageUrlsSymbol] = [];
69
- }
70
-
71
- /**
72
- * This method is called by the `instanceof` operator.
73
- * @return {symbol}
74
- */
75
- static get [instanceSymbol]() {
76
- return Symbol.for(
77
- "@schukai/monster/components/content/viewer/message-content@@instance",
78
- );
79
- }
80
-
81
- /**
82
- * To set the options via the HTML tag, the attribute `data-monster-options` must be used.
83
- * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
84
- *
85
- * The individual configuration values can be found in the table.
86
- *
87
- * @property {Object} templates Template definitions
88
- * @property {string} templates.main Main template
89
- * @property {string} content The HTML string to be displayed.
90
- * @property {Object} features Features to enable or disable specific functionalities.
91
- * @property {boolean} features.sanitize Whether to sanitize the HTML content (removes scripts, etc.). Defaults to true.
92
- * @property {object} sanitize Sanitization options.
93
- * @property {function} sanitize.callback A callback function to sanitize the HTML content. Defaults to a built-in sanitizer. You can use libraries like DOMPurify for this purpose.
94
- * @property {Object} message The message object containing email details.
95
- * @property {Object} message.from The sender's information.
96
- * @property {string|null} message.from.name The sender's name.
97
- * @property {string|null} message.from.address The sender's email address.
98
- * @property {Object} message.to The recipient's information.
99
- * @property {string|null} message.to.name The recipient's name.
100
- * @property {string|null} message.to.address The recipient's email address.
101
- * @property {Array} message.cc An array of CC recipients.
102
- * @property {string|null} message.subject The subject of the email.
103
- * @property {string|null} message.date The date of the email, formatted as a string.
104
- * @property {string|null} message.messageID The unique identifier of the email message.
105
- * @property {Array} message.parts An array of parts of the email, which can include text, HTML, attachments, etc.
106
- * @property {Array} message.attachments An array of attachments processed from the email parts.
107
- * @property {Object} message.headers Additional headers of the email.
108
- */
109
- get defaults() {
110
- return Object.assign({}, super.defaults, {
111
- templates: {
112
- main: getTemplate(),
113
- },
114
-
115
- templateFormatter: {
116
- marker: {
117
- open: null,
118
- close: null,
119
- },
120
- i18n: true,
121
- },
122
-
123
- privacy: {
124
- visible: false,
125
- },
126
-
127
- content: "",
128
-
129
- features: {
130
- sanitize: true,
131
- },
132
-
133
- sanitize: {
134
- callback: sanitizeHtml.bind(this),
135
- },
136
-
137
- labels: getTranslations(),
138
-
139
- message: {
140
- from: {
141
- name: null,
142
- address: null,
143
- },
144
- to: {
145
- name: null,
146
- address: null,
147
- },
148
- cc: [],
149
- subject: null,
150
- date: null,
151
- messageID: null,
152
- parts: [],
153
- attachments: [], // Added for processed attachments
154
- headers: [],
155
- },
156
- });
157
- }
158
-
159
- /**
160
- * Returns the updater transformer methods for this component.
161
- * @returns {{sanitizeHtml: ((function(*): (*))|*)}}
162
- */
163
- [updaterTransformerMethodsSymbol]() {
164
- return {
165
- sanitizeHtml: (value) => {
166
- if (this.getOption("features.sanitize")) {
167
- return this.getOption("sanitize.callback")(value);
168
- }
169
- return value;
170
- },
171
- };
172
- }
173
-
174
- /**
175
- * Sets the content of the MessageContent component.
176
- * @param {Object} message The message object containing parts, headers, etc.
177
- * @returns {MessageContent}
178
- */
179
- setMessage(message) {
180
- const self = this;
181
- if (!isObject(message)) {
182
- throw new Error("message must be an object");
183
- }
184
-
185
- this[embeddedImageUrlsSymbol].forEach((url) => URL.revokeObjectURL(url));
186
- this[embeddedImageUrlsSymbol] = [];
187
-
188
- this.setOption("message.from.name", message?.from?.name || null);
189
- this.setOption("message.from.address", message?.from?.address || null);
190
- this.setOption("message.to.name", message?.to?.name || null);
191
- this.setOption("message.to.address", message?.to?.address || null);
192
-
193
- const dateTime = message?.date ? new Date(message.date) : null;
194
- const localeDateTime = dateTime?.toLocaleString(navigator.language, {
195
- year: "numeric",
196
- month: "long",
197
- day: "numeric",
198
- hour: "2-digit",
199
- minute: "2-digit",
200
- });
201
-
202
- this.setOption("message.date", localeDateTime || null);
203
- this.setOption("message.cc", message?.cc || []);
204
- this.setOption("message.subject", message?.subject || null);
205
- this.setOption("message.messageID", message?.messageID || null);
206
-
207
- function escapeHTML(str) {
208
- return str
209
- .replace(/&/g, "&")
210
- .replace(/</g, "&lt;")
211
- .replace(/>/g, "&gt;")
212
- .replace(/"/g, "&quot;")
213
- .replace(/'/g, "&#39;");
214
- }
215
-
216
- const headers = [];
217
- let mainMimeType = null;
218
- for (const [key, value] of Object.entries(message?.headers || {})) {
219
- if (key && value) {
220
- let valueString = value;
221
- if (isArray(valueString)) {
222
- valueString = "<ul>";
223
- for (const item of value) {
224
- const escapedItem = escapeHTML(item);
225
- valueString += `<li>${escapedItem}</li>`;
226
- }
227
- valueString += "</ul>";
228
- }
229
- if (key.toLowerCase() === "content-type") {
230
- mainMimeType = valueString.split(";")[0].trim();
231
- }
232
-
233
- headers.push({
234
- key: key,
235
- value: valueString,
236
- });
237
- }
238
- }
239
-
240
- this.setOption("message.headers", headers || []);
241
-
242
- let htmlContent = "";
243
- let plainTextContent = "";
244
- const attachments = [];
245
- const embeddedImages = {};
246
-
247
- let maxDepth = 10; // Max recursion depth to prevent infinite loops
248
- const processParts = (parts, depth = 0) => {
249
- if (depth > maxDepth) {
250
- console.warn(
251
- `Max recursion depth exceeded for parts: ${JSON.stringify(parts)}`,
252
- );
253
- return;
254
- }
255
-
256
- if (!parts || !Array.isArray(parts)) {
257
- return;
258
- }
259
-
260
- for (const part of parts) {
261
- try {
262
- if (part.parts && part.parts.length > 0) {
263
- processParts(part.parts, depth + 1);
264
- } else if (
265
- part.dispositionType === "attachment" &&
266
- part.contentType
267
- ) {
268
- part["index"] = attachments.length; // Füge Index hinzu, um die Reihenfolge zu verfolgen
269
- part["fileSize"] = part.content ? part.content.length : 0; // Dateigröße in Bytes
270
- part["humanReadableSize"] = part.content
271
- ? `${(part.content.length / 1024).toFixed(2)} KB`
272
- : "0 KB"; // Menschlich lesbare Größe
273
-
274
- attachments.push(part);
275
- } else if (
276
- part.contentType &&
277
- part.contentType.toLowerCase().startsWith("text/html")
278
- ) {
279
- htmlContent = part.content;
280
- } else if (
281
- part.contentType &&
282
- part.contentType.toLowerCase().startsWith("text/plain")
283
- ) {
284
- if (!htmlContent) {
285
- plainTextContent = part.content;
286
- }
287
- } else if (
288
- // part.dispositionType === "inline" &&
289
- part.contentType &&
290
- part.contentType.toLowerCase().startsWith("image/")
291
- ) {
292
- const cid =
293
- part?.["contentId"] ||
294
- (part.filename
295
- ? part.filename.split(".").slice(0, -1).join(".")
296
- : null);
297
-
298
- if (cid) {
299
- embeddedImages[cid] = part;
300
- } else {
301
- console.warn(
302
- "Inline image part found without Content-ID or filename:",
303
- part,
304
- );
305
- }
306
- }
307
- } catch (e) {
308
- console.error("Error processing part:", part, e);
309
- }
310
- }
311
- };
312
-
313
- if (message?.parts) {
314
- processParts(message.parts);
315
- }
316
-
317
- if (!htmlContent && plainTextContent) {
318
- htmlContent = plainTextContent.replace(/\n/g, "<br>");
319
- }
320
- for (const cid in embeddedImages) {
321
- const imagePart = embeddedImages[cid];
322
- if (imagePart.content && imagePart.contentType) {
323
- try {
324
- const base64ImageContent = imagePart.content.replace(/\s/g, "");
325
- const imageContentType = imagePart.contentType;
326
- let cleanCid = imagePart.contentId;
327
-
328
- if (cleanCid) {
329
- cleanCid = cleanCid.trim();
330
- cleanCid = cleanCid.replace(/[\u0000-\u001F\u007F-\u009F]/g, ""); // Steuerzeichen entfernen
331
- } else {
332
- cleanCid = imagePart.filename
333
- ? imagePart.filename.split(".").slice(0, -1).join(".")
334
- : null;
335
- if (!cleanCid) {
336
- console.warn(
337
- "Content-ID or filename not found for an image part. Cannot replace CID in HTML.",
338
- );
339
- continue; // Überspringe dieses Bild, wenn CID fehlt
340
- }
341
- }
342
-
343
- const decodedContent = atob(base64ImageContent);
344
- const uint8Array = new Uint8Array(decodedContent.length);
345
- for (let i = 0; i < decodedContent.length; i++) {
346
- uint8Array[i] = decodedContent.charCodeAt(i);
347
- }
348
-
349
- const blob = new Blob([uint8Array], { type: imageContentType });
350
- const objectUrl = URL.createObjectURL(blob);
351
- this[embeddedImageUrlsSymbol].push(objectUrl); // Speichern zur späteren Widerrufung
352
-
353
- embeddedImages[cid].objectUrl = objectUrl;
354
- } catch (e) {
355
- console.error(
356
- `Error processing embedded image with CID '${cid}':`,
357
- e,
358
- );
359
- }
360
- }
361
- }
362
-
363
- htmlContent = replaceCidImages.call(this, htmlContent, embeddedImages);
364
-
365
- this[contentContainerElementSymbol].setOption("content", htmlContent);
366
- this.setOption("message.attachments", attachments);
367
- this.setOption("message.parts", message?.parts || []);
368
-
369
- return this;
370
- }
371
-
372
- /**
373
- * Handles the click event for an attachment download button.
374
- * @param {Event} event
375
- * @param {Object} part The attachment part data.
376
- */
377
- onDownloadAttachmentClick(event, part) {
378
- event.preventDefault();
379
-
380
- if (part.content && part.filename && part.contentType) {
381
- try {
382
- // Assuming part.content is base64 encoded. Adjust if your content is raw binary.
383
- const decodedContent = atob(part.content);
384
- const uint8Array = new Uint8Array(decodedContent.length);
385
- for (let i = 0; i < decodedContent.length; i++) {
386
- uint8Array[i] = decodedContent.charCodeAt(i);
387
- }
388
- const blob = new Blob([uint8Array], { type: part.contentType });
389
-
390
- const url = URL.createObjectURL(blob);
391
- const a = document.createElement("a");
392
- a.href = url;
393
- a.download = part.filename;
394
- document.body.appendChild(a);
395
- a.click();
396
- document.body.removeChild(a);
397
- URL.revokeObjectURL(url);
398
- } catch (e) {
399
- console.error("Error downloading attachment:", e);
400
- alert(
401
- "Could not download file. Content might not be base64 or is corrupted.",
402
- );
403
- }
404
- } else {
405
- alert("Attachment content not available for download.");
406
- }
407
- }
408
-
409
- /**
410
- * @return {string}
411
- */
412
- static getTag() {
413
- return "monster-message-content";
414
- }
415
-
416
- /**
417
- * @return {MessageContent}
418
- */
419
- [assembleMethodSymbol]() {
420
- super[assembleMethodSymbol]();
421
- initControlReferences.call(this);
422
- initEventHandler.call(this);
423
- }
424
-
425
- /**
426
- * @return {Array}
427
- */
428
- static getCSSStyleSheet() {
429
- return [MessageStyleSheet];
430
- }
431
-
432
- /**
433
- * Cleans up any resources when the element is removed from the DOM.
434
- * Note: This method relies on the CustomElement base class calling it.
435
- * If CustomElement does not have a disconnectedCallback equivalent,
436
- * manual cleanup or a different strategy will be needed.
437
- */
438
- disconnectedCallback() {
439
- super.disconnectedCallback?.(); // Call super's disconnectedCallback if it exists
440
- this[embeddedImageUrlsSymbol].forEach((url) => URL.revokeObjectURL(url));
441
- this[embeddedImageUrlsSymbol] = [];
442
- }
66
+ constructor() {
67
+ super();
68
+ this[embeddedImageUrlsSymbol] = [];
69
+ }
70
+
71
+ /**
72
+ * This method is called by the `instanceof` operator.
73
+ * @return {symbol}
74
+ */
75
+ static get [instanceSymbol]() {
76
+ return Symbol.for(
77
+ "@schukai/monster/components/content/viewer/message-content@@instance",
78
+ );
79
+ }
80
+
81
+ /**
82
+ * To set the options via the HTML tag, the attribute `data-monster-options` must be used.
83
+ * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
84
+ *
85
+ * The individual configuration values can be found in the table.
86
+ *
87
+ * @property {Object} templates Template definitions
88
+ * @property {string} templates.main Main template
89
+ * @property {string} content The HTML string to be displayed.
90
+ * @property {Object} features Features to enable or disable specific functionalities.
91
+ * @property {boolean} features.sanitize Whether to sanitize the HTML content (removes scripts, etc.). Defaults to true.
92
+ * @property {object} sanitize Sanitization options.
93
+ * @property {function} sanitize.callback A callback function to sanitize the HTML content. Defaults to a built-in sanitizer. You can use libraries like DOMPurify for this purpose.
94
+ * @property {Object} message The message object containing email details.
95
+ * @property {Object} message.from The sender's information.
96
+ * @property {string|null} message.from.name The sender's name.
97
+ * @property {string|null} message.from.address The sender's email address.
98
+ * @property {Object} message.to The recipient's information.
99
+ * @property {string|null} message.to.name The recipient's name.
100
+ * @property {string|null} message.to.address The recipient's email address.
101
+ * @property {Array} message.cc An array of CC recipients.
102
+ * @property {string|null} message.subject The subject of the email.
103
+ * @property {string|null} message.date The date of the email, formatted as a string.
104
+ * @property {string|null} message.messageID The unique identifier of the email message.
105
+ * @property {Array} message.parts An array of parts of the email, which can include text, HTML, attachments, etc.
106
+ * @property {Array} message.attachments An array of attachments processed from the email parts.
107
+ * @property {Object} message.headers Additional headers of the email.
108
+ */
109
+ get defaults() {
110
+ return Object.assign({}, super.defaults, {
111
+ templates: {
112
+ main: getTemplate(),
113
+ },
114
+
115
+ templateFormatter: {
116
+ marker: {
117
+ open: null,
118
+ close: null,
119
+ },
120
+ i18n: true,
121
+ },
122
+
123
+ privacy: {
124
+ visible: false,
125
+ },
126
+
127
+ content: "",
128
+
129
+ features: {
130
+ sanitize: true,
131
+ },
132
+
133
+ sanitize: {
134
+ callback: sanitizeHtml.bind(this),
135
+ },
136
+
137
+ labels: getTranslations(),
138
+
139
+ message: {
140
+ from: {
141
+ name: null,
142
+ address: null,
143
+ },
144
+ to: {
145
+ name: null,
146
+ address: null,
147
+ },
148
+ cc: [],
149
+ subject: null,
150
+ date: null,
151
+ messageID: null,
152
+ parts: [],
153
+ attachments: [], // Added for processed attachments
154
+ headers: [],
155
+ },
156
+ });
157
+ }
158
+
159
+ /**
160
+ * Returns the updater transformer methods for this component.
161
+ * @returns {{sanitizeHtml: ((function(*): (*))|*)}}
162
+ */
163
+ [updaterTransformerMethodsSymbol]() {
164
+ return {
165
+ sanitizeHtml: (value) => {
166
+ if (this.getOption("features.sanitize")) {
167
+ return this.getOption("sanitize.callback")(value);
168
+ }
169
+ return value;
170
+ },
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Sets the content of the MessageContent component.
176
+ * @param {Object} message The message object containing parts, headers, etc.
177
+ * @returns {MessageContent}
178
+ */
179
+ setMessage(message) {
180
+ const self = this;
181
+ if (!isObject(message)) {
182
+ throw new Error("message must be an object");
183
+ }
184
+
185
+ this[embeddedImageUrlsSymbol].forEach((url) => URL.revokeObjectURL(url));
186
+ this[embeddedImageUrlsSymbol] = [];
187
+
188
+ this.setOption("message.from.name", message?.from?.name || null);
189
+ this.setOption("message.from.address", message?.from?.address || null);
190
+ this.setOption("message.to.name", message?.to?.name || null);
191
+ this.setOption("message.to.address", message?.to?.address || null);
192
+
193
+ const dateTime = message?.date ? new Date(message.date) : null;
194
+ const localeDateTime = dateTime?.toLocaleString(navigator.language, {
195
+ year: "numeric",
196
+ month: "long",
197
+ day: "numeric",
198
+ hour: "2-digit",
199
+ minute: "2-digit",
200
+ });
201
+
202
+ this.setOption("message.date", localeDateTime || null);
203
+ this.setOption("message.cc", message?.cc || []);
204
+ this.setOption("message.subject", message?.subject || null);
205
+ this.setOption("message.messageID", message?.messageID || null);
206
+
207
+ function escapeHTML(str) {
208
+ return str
209
+ .replace(/&/g, "&amp;")
210
+ .replace(/</g, "&lt;")
211
+ .replace(/>/g, "&gt;")
212
+ .replace(/"/g, "&quot;")
213
+ .replace(/'/g, "&#39;");
214
+ }
215
+
216
+ const headers = [];
217
+ let mainMimeType = null;
218
+ for (const [key, value] of Object.entries(message?.headers || {})) {
219
+ if (key && value) {
220
+ let valueString = value;
221
+ if (isArray(valueString)) {
222
+ valueString = "<ul>";
223
+ for (const item of value) {
224
+ const escapedItem = escapeHTML(item);
225
+ valueString += `<li>${escapedItem}</li>`;
226
+ }
227
+ valueString += "</ul>";
228
+ }
229
+ if (key.toLowerCase() === "content-type") {
230
+ mainMimeType = valueString.split(";")[0].trim();
231
+ }
232
+
233
+ headers.push({
234
+ key: key,
235
+ value: valueString,
236
+ });
237
+ }
238
+ }
239
+
240
+ this.setOption("message.headers", headers || []);
241
+
242
+ let htmlContent = "";
243
+ let plainTextContent = "";
244
+ const attachments = [];
245
+ const embeddedImages = {};
246
+
247
+ let maxDepth = 10; // Max recursion depth to prevent infinite loops
248
+ const processParts = (parts, depth = 0) => {
249
+ if (depth > maxDepth) {
250
+ console.warn(
251
+ `Max recursion depth exceeded for parts: ${JSON.stringify(parts)}`,
252
+ );
253
+ return;
254
+ }
255
+
256
+ if (!parts || !Array.isArray(parts)) {
257
+ return;
258
+ }
259
+
260
+ for (const part of parts) {
261
+ try {
262
+ if (part.parts && part.parts.length > 0) {
263
+ processParts(part.parts, depth + 1);
264
+ } else if (
265
+ part.dispositionType === "attachment" &&
266
+ part.contentType
267
+ ) {
268
+ part["index"] = attachments.length; // Füge Index hinzu, um die Reihenfolge zu verfolgen
269
+ part["fileSize"] = part.content ? part.content.length : 0; // Dateigröße in Bytes
270
+ part["humanReadableSize"] = part.content
271
+ ? `${(part.content.length / 1024).toFixed(2)} KB`
272
+ : "0 KB"; // Menschlich lesbare Größe
273
+
274
+ attachments.push(part);
275
+ } else if (
276
+ part.contentType &&
277
+ part.contentType.toLowerCase().startsWith("text/html")
278
+ ) {
279
+ htmlContent = part.content;
280
+ } else if (
281
+ part.contentType &&
282
+ part.contentType.toLowerCase().startsWith("text/plain")
283
+ ) {
284
+ if (!htmlContent) {
285
+ plainTextContent = part.content;
286
+ }
287
+ } else if (
288
+ // part.dispositionType === "inline" &&
289
+ part.contentType &&
290
+ part.contentType.toLowerCase().startsWith("image/")
291
+ ) {
292
+ const cid =
293
+ part?.["contentId"] ||
294
+ (part.filename
295
+ ? part.filename.split(".").slice(0, -1).join(".")
296
+ : null);
297
+
298
+ if (cid) {
299
+ embeddedImages[cid] = part;
300
+ } else {
301
+ console.warn(
302
+ "Inline image part found without Content-ID or filename:",
303
+ part,
304
+ );
305
+ }
306
+ }
307
+ } catch (e) {
308
+ console.error("Error processing part:", part, e);
309
+ }
310
+ }
311
+ };
312
+
313
+ if (message?.parts) {
314
+ processParts(message.parts);
315
+ }
316
+
317
+ if (!htmlContent && plainTextContent) {
318
+ htmlContent = plainTextContent.replace(/\n/g, "<br>");
319
+ }
320
+ for (const cid in embeddedImages) {
321
+ const imagePart = embeddedImages[cid];
322
+ if (imagePart.content && imagePart.contentType) {
323
+ try {
324
+ const base64ImageContent = imagePart.content.replace(/\s/g, "");
325
+ const imageContentType = imagePart.contentType;
326
+ let cleanCid = imagePart.contentId;
327
+
328
+ if (cleanCid) {
329
+ cleanCid = cleanCid.trim();
330
+ cleanCid = cleanCid.replace(/[\u0000-\u001F\u007F-\u009F]/g, ""); // Steuerzeichen entfernen
331
+ } else {
332
+ cleanCid = imagePart.filename
333
+ ? imagePart.filename.split(".").slice(0, -1).join(".")
334
+ : null;
335
+ if (!cleanCid) {
336
+ console.warn(
337
+ "Content-ID or filename not found for an image part. Cannot replace CID in HTML.",
338
+ );
339
+ continue; // Überspringe dieses Bild, wenn CID fehlt
340
+ }
341
+ }
342
+
343
+ const decodedContent = atob(base64ImageContent);
344
+ const uint8Array = new Uint8Array(decodedContent.length);
345
+ for (let i = 0; i < decodedContent.length; i++) {
346
+ uint8Array[i] = decodedContent.charCodeAt(i);
347
+ }
348
+
349
+ const blob = new Blob([uint8Array], { type: imageContentType });
350
+ const objectUrl = URL.createObjectURL(blob);
351
+ this[embeddedImageUrlsSymbol].push(objectUrl); // Speichern zur späteren Widerrufung
352
+
353
+ embeddedImages[cid].objectUrl = objectUrl;
354
+ } catch (e) {
355
+ console.error(
356
+ `Error processing embedded image with CID '${cid}':`,
357
+ e,
358
+ );
359
+ }
360
+ }
361
+ }
362
+
363
+ htmlContent = replaceCidImages.call(this, htmlContent, embeddedImages);
364
+
365
+ this[contentContainerElementSymbol].setOption("content", htmlContent);
366
+ this.setOption("message.attachments", attachments);
367
+ this.setOption("message.parts", message?.parts || []);
368
+
369
+ return this;
370
+ }
371
+
372
+ /**
373
+ * Handles the click event for an attachment download button.
374
+ * @param {Event} event
375
+ * @param {Object} part The attachment part data.
376
+ */
377
+ onDownloadAttachmentClick(event, part) {
378
+ event.preventDefault();
379
+
380
+ if (part.content && part.filename && part.contentType) {
381
+ try {
382
+ // Assuming part.content is base64 encoded. Adjust if your content is raw binary.
383
+ const decodedContent = atob(part.content);
384
+ const uint8Array = new Uint8Array(decodedContent.length);
385
+ for (let i = 0; i < decodedContent.length; i++) {
386
+ uint8Array[i] = decodedContent.charCodeAt(i);
387
+ }
388
+ const blob = new Blob([uint8Array], { type: part.contentType });
389
+
390
+ const url = URL.createObjectURL(blob);
391
+ const a = document.createElement("a");
392
+ a.href = url;
393
+ a.download = part.filename;
394
+ document.body.appendChild(a);
395
+ a.click();
396
+ document.body.removeChild(a);
397
+ URL.revokeObjectURL(url);
398
+ } catch (e) {
399
+ console.error("Error downloading attachment:", e);
400
+ alert(
401
+ "Could not download file. Content might not be base64 or is corrupted.",
402
+ );
403
+ }
404
+ } else {
405
+ alert("Attachment content not available for download.");
406
+ }
407
+ }
408
+
409
+ /**
410
+ * @return {string}
411
+ */
412
+ static getTag() {
413
+ return "monster-message-content";
414
+ }
415
+
416
+ /**
417
+ * @return {MessageContent}
418
+ */
419
+ [assembleMethodSymbol]() {
420
+ super[assembleMethodSymbol]();
421
+ initControlReferences.call(this);
422
+ initEventHandler.call(this);
423
+ }
424
+
425
+ /**
426
+ * @return {Array}
427
+ */
428
+ static getCSSStyleSheet() {
429
+ return [MessageStyleSheet];
430
+ }
431
+
432
+ /**
433
+ * Cleans up any resources when the element is removed from the DOM.
434
+ * Note: This method relies on the CustomElement base class calling it.
435
+ * If CustomElement does not have a disconnectedCallback equivalent,
436
+ * manual cleanup or a different strategy will be needed.
437
+ */
438
+ disconnectedCallback() {
439
+ super.disconnectedCallback?.(); // Call super's disconnectedCallback if it exists
440
+ this[embeddedImageUrlsSymbol].forEach((url) => URL.revokeObjectURL(url));
441
+ this[embeddedImageUrlsSymbol] = [];
442
+ }
443
443
  }
444
444
 
445
445
  /**
@@ -447,47 +447,47 @@ class MessageContent extends CustomElement {
447
447
  * @private
448
448
  */
449
449
  function replaceCidImages(htmlContent, replacements) {
450
- const objectURLEmptyGif =
451
- "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
452
-
453
- const parser = new DOMParser();
454
- const doc = parser.parseFromString(htmlContent, "text/html");
455
- const images = doc.querySelectorAll("img");
456
- let hasPrivacyImage = false;
457
- images.forEach((img) => {
458
- const src = img.getAttribute("src");
459
- if (src && src.toLowerCase().startsWith("cid:")) {
460
- const cid = src.toLowerCase().substring(4);
461
- if (replacements[cid]) {
462
- img.setAttribute("src", replacements[cid].objectUrl);
463
- }
464
- return;
465
- } else if (src && src.toLowerCase().startsWith("http")) {
466
- const urlImage = new URL(src, document.location.href);
467
- if (urlImage.origin !== document.location.origin) {
468
- img.setAttribute("src", objectURLEmptyGif);
469
- img.classList.add("privacyImage");
470
- img.setAttribute("data-monster-privacy", "true");
471
- img.setAttribute("data-monster-privacy_origin-url", src);
472
- hasPrivacyImage = true;
473
- // If the src is an HTTP URL, we can keep it as is
474
- img.setAttribute(
475
- "title",
476
- this.getOption("labels.privacyImageTitle") ||
477
- "This image is from an external source and may not be safe to display.",
478
- );
479
- }
480
- //
481
- return;
482
- }
483
- });
484
-
485
- if (hasPrivacyImage) {
486
- this.setOption("privacy.visible", true); // Show the privacy text if any images are marked as privacy
487
- }
488
- // Serialize the modified document back to HTML
489
- const serializer = new XMLSerializer();
490
- return serializer.serializeToString(doc);
450
+ const objectURLEmptyGif =
451
+ "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
452
+
453
+ const parser = new DOMParser();
454
+ const doc = parser.parseFromString(htmlContent, "text/html");
455
+ const images = doc.querySelectorAll("img");
456
+ let hasPrivacyImage = false;
457
+ images.forEach((img) => {
458
+ const src = img.getAttribute("src");
459
+ if (src && src.toLowerCase().startsWith("cid:")) {
460
+ const cid = src.toLowerCase().substring(4);
461
+ if (replacements[cid]) {
462
+ img.setAttribute("src", replacements[cid].objectUrl);
463
+ }
464
+ return;
465
+ } else if (src && src.toLowerCase().startsWith("http")) {
466
+ const urlImage = new URL(src, document.location.href);
467
+ if (urlImage.origin !== document.location.origin) {
468
+ img.setAttribute("src", objectURLEmptyGif);
469
+ img.classList.add("privacyImage");
470
+ img.setAttribute("data-monster-privacy", "true");
471
+ img.setAttribute("data-monster-privacy_origin-url", src);
472
+ hasPrivacyImage = true;
473
+ // If the src is an HTTP URL, we can keep it as is
474
+ img.setAttribute(
475
+ "title",
476
+ this.getOption("labels.privacyImageTitle") ||
477
+ "This image is from an external source and may not be safe to display.",
478
+ );
479
+ }
480
+ //
481
+ return;
482
+ }
483
+ });
484
+
485
+ if (hasPrivacyImage) {
486
+ this.setOption("privacy.visible", true); // Show the privacy text if any images are marked as privacy
487
+ }
488
+ // Serialize the modified document back to HTML
489
+ const serializer = new XMLSerializer();
490
+ return serializer.serializeToString(doc);
491
491
  }
492
492
 
493
493
  /**
@@ -495,224 +495,223 @@ function replaceCidImages(htmlContent, replacements) {
495
495
  * @return {MessageContent}
496
496
  */
497
497
  function initControlReferences() {
498
- if (!this.shadowRoot) {
499
- throw new Error("no shadow-root is defined");
500
- }
498
+ if (!this.shadowRoot) {
499
+ throw new Error("no shadow-root is defined");
500
+ }
501
501
 
502
- this[showPrivacyImagesSymbol] = this.shadowRoot.querySelector(
503
- "[data-monster-role=show-privacy-images]",
504
- );
502
+ this[showPrivacyImagesSymbol] = this.shadowRoot.querySelector(
503
+ "[data-monster-role=show-privacy-images]",
504
+ );
505
505
 
506
- this[containerElementSymbol] = this.shadowRoot.querySelector(
507
- "[data-monster-role=container]",
508
- );
506
+ this[containerElementSymbol] = this.shadowRoot.querySelector(
507
+ "[data-monster-role=container]",
508
+ );
509
509
 
510
- this[contentContainerElementSymbol] = this.shadowRoot.querySelector(
511
- "[data-monster-role=content-container]",
512
- );
510
+ this[contentContainerElementSymbol] = this.shadowRoot.querySelector(
511
+ "[data-monster-role=content-container]",
512
+ );
513
513
 
514
- return this;
514
+ return this;
515
515
  }
516
516
 
517
517
  function getTranslations() {
518
- const locale = getLocaleOfDocument();
519
- switch (locale.language) {
520
- case "de": // German
521
- return {
522
- content: "Inhalt",
523
- headers: "Kopfzeilen",
524
- privacyText:
525
- "Diese Nachricht kann externe Inhalte enthalten, die nicht sicher angezeigt werden können.",
526
- showImages: "Bilder anzeigen",
527
- privacyImageTitle:
528
- "Dieses Bild stammt von einer externen Quelle und kann unsicher sein.",
529
- };
530
-
531
- case "es": // Spanish
532
- return {
533
- content: "Contenido",
534
- headers: "Encabezados",
535
- privacyText:
536
- "Este mensaje puede contener contenido externo que no es seguro mostrar.",
537
- showImages: "Mostrar imágenes",
538
- privacyImageTitle:
539
- "Esta imagen proviene de una fuente externa y puede no ser segura para mostrar.",
540
- };
541
-
542
- case "hi": // Hindi
543
- return {
544
- content: "सामग्री",
545
- headers: "शीर्षक",
546
- privacyText:
547
- "इस संदेश में बाहरी सामग्री हो सकती है जिसे सुरक्षित रूप से प्रदर्शित नहीं किया जा सकता।",
548
- showImages: "छवियाँ दिखाएँ",
549
- privacyImageTitle:
550
- "यह छवि एक बाहरी स्रोत से है और इसे प्रदर्शित करना सुरक्षित नहीं हो सकता।",
551
- };
552
-
553
- case "bn": // Bengali
554
- return {
555
- content: "বিষয়বস্তু",
556
- headers: "শিরোনাম",
557
- privacyText:
558
- "এই বার্তাটিতে এমন বাহ্যিক বিষয়বস্তু থাকতে পারে যা নিরাপদে প্রদর্শন করা যায় না।",
559
- showImages: "ছবি দেখান",
560
- privacyImageTitle:
561
- "এই চিত্রটি একটি বাহ্যিক উৎস থেকে এসেছে এবং এটি প্রদর্শন করা নিরাপদ নাও হতে পারে।",
562
- };
563
-
564
- case "pt": // Portuguese
565
- return {
566
- content: "Conteúdo",
567
- headers: "Cabeçalhos",
568
- privacyText:
569
- "Esta mensagem pode conter conteúdo externo que não pode ser exibido com segurança.",
570
- showImages: "Mostrar imagens",
571
- };
572
-
573
- case "ru": // Russian
574
- return {
575
- content: "Содержание",
576
- headers: "Заголовки",
577
- privacyText:
578
- "Это сообщение может содержать внешнее содержимое, которое небезопасно отображать.",
579
- showImages: "Показать изображения",
580
- privacyImageTitle:
581
- "Это изображение из внешнего источника и может быть небезопасным для отображения.",
582
- };
583
-
584
- case "ja": // Japanese
585
- return {
586
- content: "コンテンツ",
587
- headers: "ヘッダー",
588
- privacyText:
589
- "このメッセージには安全に表示できない外部コンテンツが含まれている可能性があります。",
590
- showImages: "画像を表示",
591
- privacyImageTitle:
592
- "この画像は外部ソースからのものであり、安全に表示できない可能性があります。",
593
- };
594
-
595
- case "pa": // Western Punjabi
596
- return {
597
- content: "ਸਮੱਗਰੀ",
598
- headers: "ਸਿਰਲੇਖ",
599
- privacyText:
600
- "ਇਸ ਸੁਨੇਹੇ ਵਿੱਚ ਬਾਹਰੀ ਸਮੱਗਰੀ ਹੋ ਸਕਦੀ ਹੈ ਜਿਸ ਨੂੰ ਸੁਰੱਖਿਅਤ ਤਰੀਕੇ ਨਾਲ ਨਹੀਂ ਦਿਖਾਇਆ ਜਾ ਸਕਦਾ।",
601
- showImages: "ਤਸਵੀਰਾਂ ਵੇਖੋ",
602
- privacyImageTitle:
603
- "ਇਹ ਤਸਵੀਰ ਇੱਕ ਬਾਹਰੀ ਸਰੋਤ ਤੋਂ ਹੈ ਅਤੇ ਇਸ ਨੂੰ ਦਿਖਾਉਣਾ ਸੁਰੱਖਿਅਤ ਨਹੀਂ ਹੋ ਸਕਦਾ।",
604
- };
605
-
606
- case "mr": // Marathi
607
- return {
608
- content: "सामग्री",
609
- headers: "शीर्षके",
610
- privacyText:
611
- "या संदेशात सुरक्षितपणे दाखवता न येणारी बाह्य सामग्री असू शकते.",
612
- showImages: "प्रतिमा दाखवा",
613
- privacyImageTitle:
614
- "ही प्रतिमा बाह्य स्रोताकडून आहे आणि ती दाखवणे सुरक्षित नसू शकते.",
615
- };
616
-
617
- case "fr": // French
618
- return {
619
- content: "Contenu",
620
- headers: "En-têtes",
621
- privacyText:
622
- "Ce message peut contenir du contenu externe qui ne peut pas être affiché de façon sécurisée.",
623
- showImages: "Afficher les images",
624
- privacyImageTitle:
625
- "Cette image provient d'une source externe et peut ne pas être sécurisée à afficher.",
626
- };
627
-
628
- case "it": // Italian
629
- return {
630
- content: "Contenuto",
631
- headers: "Intestazioni",
632
- privacyText:
633
- "Questo messaggio può contenere contenuti esterni che non possono essere visualizzati in modo sicuro.",
634
- showImages: "Mostra immagini",
635
- privacyImageTitle:
636
- "Questa immagine proviene da una fonte esterna e potrebbe non essere sicura da visualizzare.",
637
- };
638
-
639
- case "nl": // Dutch
640
- return {
641
- content: "Inhoud",
642
- headers: "Headers",
643
- privacyText:
644
- "Dit bericht kan externe inhoud bevatten die niet veilig kan worden weergegeven.",
645
- showImages: "Afbeeldingen tonen",
646
- privacyImageTitle:
647
- "Deze afbeelding komt van een externe bron en is mogelijk niet veilig om weer te geven.",
648
- };
649
-
650
- case "sv": // Swedish
651
- return {
652
- content: "Innehåll",
653
- headers: "Rubriker",
654
- privacyText:
655
- "Detta meddelande kan innehålla extern information som inte kan visas säkert.",
656
- showImages: "Visa bilder",
657
- privacyImageTitle:
658
- "Denna bild kommer från en extern källa och kan vara osäker att visa.",
659
- };
660
-
661
- case "pl": // Polish
662
- return {
663
- content: "Zawartość",
664
- headers: "Nagłówki",
665
- privacyText:
666
- "Ta wiadomość może zawierać zewnętrzne treści, których nie można bezpiecznie wyświetlić.",
667
- showImages: "Pokaż obrazy",
668
- privacyImageTitle:
669
- "Ten obraz pochodzi z zewnętrznego źródła i może nie być bezpieczny do wyświetlenia.",
670
- };
671
-
672
- case "da": // Danish
673
- return {
674
- content: "Indhold",
675
- headers: "Overskrifter",
676
- privacyText:
677
- "Denne besked kan indeholde eksternt indhold, der ikke kan vises sikkert.",
678
- showImages: "Vis billeder",
679
- privacyImageTitle:
680
- "Dette billede kommer fra en ekstern kilde og kan være usikkert at vise.",
681
- };
682
-
683
- case "no": // Norwegian
684
- return {
685
- content: "Innhold",
686
- headers: "Overskrifter",
687
- privacyText:
688
- "Denne meldingen kan inneholde eksternt innhold som ikke kan vises sikkert.",
689
- showImages: "Vis bilder",
690
- privacyImageTitle:
691
- "Dette bildet kommer fra en ekstern kilde og kan være usikkert å vise.",
692
- };
693
-
694
- case "cs": // Czech
695
- return {
696
- content: "Obsah",
697
- headers: "Hlavičky",
698
- privacyText:
699
- "Tato zpráva může obsahovat externí obsah, který nelze bezpečně zobrazit.",
700
- showImages: "Zobrazit obrázky",
701
- privacyImageTitle:
702
- "Tento obrázek pochází z externího zdroje a nemusí být bezpečný k zobrazení.",
703
- };
704
-
705
- default: // English fallback
706
- return {
707
- content: "Content",
708
- headers: "Headers",
709
- privacyText:
710
- "This message may contain external content that cannot be displayed securely.",
711
- showImages: "Show images",
712
- privacyImageTitle:
713
- "This image is from an external source and may not be safe to display.",
714
- };
715
- }
518
+ const locale = getLocaleOfDocument();
519
+ switch (locale.language) {
520
+ case "de": // German
521
+ return {
522
+ content: "Inhalt",
523
+ headers: "Kopfzeilen",
524
+ privacyText:
525
+ "Diese Nachricht kann externe Inhalte enthalten, die nicht sicher angezeigt werden können.",
526
+ showImages: "Bilder anzeigen",
527
+ privacyImageTitle:
528
+ "Dieses Bild stammt von einer externen Quelle und kann unsicher sein.",
529
+ };
530
+
531
+ case "es": // Spanish
532
+ return {
533
+ content: "Contenido",
534
+ headers: "Encabezados",
535
+ privacyText:
536
+ "Este mensaje puede contener contenido externo que no es seguro mostrar.",
537
+ showImages: "Mostrar imágenes",
538
+ privacyImageTitle:
539
+ "Esta imagen proviene de una fuente externa y puede no ser segura para mostrar.",
540
+ };
541
+
542
+ case "hi": // Hindi
543
+ return {
544
+ content: "सामग्री",
545
+ headers: "शीर्षक",
546
+ privacyText:
547
+ "इस संदेश में बाहरी सामग्री हो सकती है जिसे सुरक्षित रूप से प्रदर्शित नहीं किया जा सकता।",
548
+ showImages: "छवियाँ दिखाएँ",
549
+ privacyImageTitle:
550
+ "यह छवि एक बाहरी स्रोत से है और इसे प्रदर्शित करना सुरक्षित नहीं हो सकता।",
551
+ };
552
+
553
+ case "bn": // Bengali
554
+ return {
555
+ content: "বিষয়বস্তু",
556
+ headers: "শিরোনাম",
557
+ privacyText:
558
+ "এই বার্তাটিতে এমন বাহ্যিক বিষয়বস্তু থাকতে পারে যা নিরাপদে প্রদর্শন করা যায় না।",
559
+ showImages: "ছবি দেখান",
560
+ privacyImageTitle:
561
+ "এই চিত্রটি একটি বাহ্যিক উৎস থেকে এসেছে এবং এটি প্রদর্শন করা নিরাপদ নাও হতে পারে।",
562
+ };
563
+
564
+ case "pt": // Portuguese
565
+ return {
566
+ content: "Conteúdo",
567
+ headers: "Cabeçalhos",
568
+ privacyText:
569
+ "Esta mensagem pode conter conteúdo externo que não pode ser exibido com segurança.",
570
+ showImages: "Mostrar imagens",
571
+ };
572
+
573
+ case "ru": // Russian
574
+ return {
575
+ content: "Содержание",
576
+ headers: "Заголовки",
577
+ privacyText:
578
+ "Это сообщение может содержать внешнее содержимое, которое небезопасно отображать.",
579
+ showImages: "Показать изображения",
580
+ privacyImageTitle:
581
+ "Это изображение из внешнего источника и может быть небезопасным для отображения.",
582
+ };
583
+
584
+ case "ja": // Japanese
585
+ return {
586
+ content: "コンテンツ",
587
+ headers: "ヘッダー",
588
+ privacyText:
589
+ "このメッセージには安全に表示できない外部コンテンツが含まれている可能性があります。",
590
+ showImages: "画像を表示",
591
+ privacyImageTitle:
592
+ "この画像は外部ソースからのものであり、安全に表示できない可能性があります。",
593
+ };
594
+
595
+ case "pa": // Western Punjabi
596
+ return {
597
+ content: "ਸਮੱਗਰੀ",
598
+ headers: "ਸਿਰਲੇਖ",
599
+ privacyText:
600
+ "ਇਸ ਸੁਨੇਹੇ ਵਿੱਚ ਬਾਹਰੀ ਸਮੱਗਰੀ ਹੋ ਸਕਦੀ ਹੈ ਜਿਸ ਨੂੰ ਸੁਰੱਖਿਅਤ ਤਰੀਕੇ ਨਾਲ ਨਹੀਂ ਦਿਖਾਇਆ ਜਾ ਸਕਦਾ।",
601
+ showImages: "ਤਸਵੀਰਾਂ ਵੇਖੋ",
602
+ privacyImageTitle:
603
+ "ਇਹ ਤਸਵੀਰ ਇੱਕ ਬਾਹਰੀ ਸਰੋਤ ਤੋਂ ਹੈ ਅਤੇ ਇਸ ਨੂੰ ਦਿਖਾਉਣਾ ਸੁਰੱਖਿਅਤ ਨਹੀਂ ਹੋ ਸਕਦਾ।",
604
+ };
605
+
606
+ case "mr": // Marathi
607
+ return {
608
+ content: "सामग्री",
609
+ headers: "शीर्षके",
610
+ privacyText: "या संदेशात सुरक्षितपणे दाखवता न येणारी बाह्य सामग्री असू शकते.",
611
+ showImages: "प्रतिमा दाखवा",
612
+ privacyImageTitle:
613
+ "ही प्रतिमा बाह्य स्रोताकडून आहे आणि ती दाखवणे सुरक्षित नसू शकते.",
614
+ };
615
+
616
+ case "fr": // French
617
+ return {
618
+ content: "Contenu",
619
+ headers: "En-têtes",
620
+ privacyText:
621
+ "Ce message peut contenir du contenu externe qui ne peut pas être affiché de façon sécurisée.",
622
+ showImages: "Afficher les images",
623
+ privacyImageTitle:
624
+ "Cette image provient d'une source externe et peut ne pas être sécurisée à afficher.",
625
+ };
626
+
627
+ case "it": // Italian
628
+ return {
629
+ content: "Contenuto",
630
+ headers: "Intestazioni",
631
+ privacyText:
632
+ "Questo messaggio può contenere contenuti esterni che non possono essere visualizzati in modo sicuro.",
633
+ showImages: "Mostra immagini",
634
+ privacyImageTitle:
635
+ "Questa immagine proviene da una fonte esterna e potrebbe non essere sicura da visualizzare.",
636
+ };
637
+
638
+ case "nl": // Dutch
639
+ return {
640
+ content: "Inhoud",
641
+ headers: "Headers",
642
+ privacyText:
643
+ "Dit bericht kan externe inhoud bevatten die niet veilig kan worden weergegeven.",
644
+ showImages: "Afbeeldingen tonen",
645
+ privacyImageTitle:
646
+ "Deze afbeelding komt van een externe bron en is mogelijk niet veilig om weer te geven.",
647
+ };
648
+
649
+ case "sv": // Swedish
650
+ return {
651
+ content: "Innehåll",
652
+ headers: "Rubriker",
653
+ privacyText:
654
+ "Detta meddelande kan innehålla extern information som inte kan visas säkert.",
655
+ showImages: "Visa bilder",
656
+ privacyImageTitle:
657
+ "Denna bild kommer från en extern källa och kan vara osäker att visa.",
658
+ };
659
+
660
+ case "pl": // Polish
661
+ return {
662
+ content: "Zawartość",
663
+ headers: "Nagłówki",
664
+ privacyText:
665
+ "Ta wiadomość może zawierać zewnętrzne treści, których nie można bezpiecznie wyświetlić.",
666
+ showImages: "Pokaż obrazy",
667
+ privacyImageTitle:
668
+ "Ten obraz pochodzi z zewnętrznego źródła i może nie być bezpieczny do wyświetlenia.",
669
+ };
670
+
671
+ case "da": // Danish
672
+ return {
673
+ content: "Indhold",
674
+ headers: "Overskrifter",
675
+ privacyText:
676
+ "Denne besked kan indeholde eksternt indhold, der ikke kan vises sikkert.",
677
+ showImages: "Vis billeder",
678
+ privacyImageTitle:
679
+ "Dette billede kommer fra en ekstern kilde og kan være usikkert at vise.",
680
+ };
681
+
682
+ case "no": // Norwegian
683
+ return {
684
+ content: "Innhold",
685
+ headers: "Overskrifter",
686
+ privacyText:
687
+ "Denne meldingen kan inneholde eksternt innhold som ikke kan vises sikkert.",
688
+ showImages: "Vis bilder",
689
+ privacyImageTitle:
690
+ "Dette bildet kommer fra en ekstern kilde og kan være usikkert å vise.",
691
+ };
692
+
693
+ case "cs": // Czech
694
+ return {
695
+ content: "Obsah",
696
+ headers: "Hlavičky",
697
+ privacyText:
698
+ "Tato zpráva může obsahovat externí obsah, který nelze bezpečně zobrazit.",
699
+ showImages: "Zobrazit obrázky",
700
+ privacyImageTitle:
701
+ "Tento obrázek pochází z externího zdroje a nemusí být bezpečný k zobrazení.",
702
+ };
703
+
704
+ default: // English fallback
705
+ return {
706
+ content: "Content",
707
+ headers: "Headers",
708
+ privacyText:
709
+ "This message may contain external content that cannot be displayed securely.",
710
+ showImages: "Show images",
711
+ privacyImageTitle:
712
+ "This image is from an external source and may not be safe to display.",
713
+ };
714
+ }
716
715
  }
717
716
 
718
717
  /**
@@ -721,44 +720,44 @@ function getTranslations() {
721
720
  * @param attachments
722
721
  */
723
722
  function downloadAttachmentByIndex(index, attachments) {
724
- const part = attachments[index];
725
- if (!part) {
726
- console.error(`Attachment mit Index ${index} nicht gefunden.`);
727
- return;
728
- }
729
-
730
- const { filename, contentType, content } = part;
731
-
732
- try {
733
- let decodedContent;
734
- if (contentType.startsWith("text/")) {
735
- // Check if it's a text type
736
- decodedContent = content; // Content is plain text
737
- } else {
738
- decodedContent = atob(content); // Assume base64 for other types
739
- }
740
-
741
- const len = decodedContent.length;
742
- const bytes = new Uint8Array(len);
743
- for (let i = 0; i < len; i++) {
744
- bytes[i] = decodedContent.charCodeAt(i);
745
- }
746
-
747
- const blob = new Blob([bytes], { type: contentType });
748
- const dataUrl = URL.createObjectURL(blob);
749
-
750
- const a = document.createElement("a");
751
- a.style.display = "none";
752
- a.href = dataUrl;
753
- a.download = filename;
754
- document.body.appendChild(a);
755
- a.click();
756
- document.body.removeChild(a);
757
-
758
- URL.revokeObjectURL(dataUrl);
759
- } catch (e) {
760
- console.error("Error downloading attachment:", e);
761
- }
723
+ const part = attachments[index];
724
+ if (!part) {
725
+ console.error(`Attachment mit Index ${index} nicht gefunden.`);
726
+ return;
727
+ }
728
+
729
+ const { filename, contentType, content } = part;
730
+
731
+ try {
732
+ let decodedContent;
733
+ if (contentType.startsWith("text/")) {
734
+ // Check if it's a text type
735
+ decodedContent = content; // Content is plain text
736
+ } else {
737
+ decodedContent = atob(content); // Assume base64 for other types
738
+ }
739
+
740
+ const len = decodedContent.length;
741
+ const bytes = new Uint8Array(len);
742
+ for (let i = 0; i < len; i++) {
743
+ bytes[i] = decodedContent.charCodeAt(i);
744
+ }
745
+
746
+ const blob = new Blob([bytes], { type: contentType });
747
+ const dataUrl = URL.createObjectURL(blob);
748
+
749
+ const a = document.createElement("a");
750
+ a.style.display = "none";
751
+ a.href = dataUrl;
752
+ a.download = filename;
753
+ document.body.appendChild(a);
754
+ a.click();
755
+ document.body.removeChild(a);
756
+
757
+ URL.revokeObjectURL(dataUrl);
758
+ } catch (e) {
759
+ console.error("Error downloading attachment:", e);
760
+ }
762
761
  }
763
762
 
764
763
  /**
@@ -767,73 +766,73 @@ function downloadAttachmentByIndex(index, attachments) {
767
766
  * @returns {initEventHandler}
768
767
  */
769
768
  function initEventHandler() {
770
- this[showPrivacyImagesSymbol].addEventListener("click", (event) => {
771
- event.preventDefault();
772
-
773
- const currentContent =
774
- this[contentContainerElementSymbol].getOption("content");
775
- if (!currentContent) {
776
- console.warn("No content available to show privacy images.");
777
- return;
778
- }
779
-
780
- const domParser = new DOMParser();
781
- const doc = domParser.parseFromString(currentContent, "text/html");
782
-
783
- doc.querySelectorAll("img[data-monster-privacy=true]").forEach((img) => {
784
- const originalUrl = img.getAttribute("data-monster-privacy_origin-url");
785
- if (originalUrl) {
786
- img.setAttribute("src", originalUrl);
787
- img.removeAttribute("data-monster-privacy");
788
- img.removeAttribute("data-monster-privacy_origin-url");
789
- img.removeAttribute("title");
790
-
791
- img.classList.remove("privacyImage");
792
- this.setOption("privacy.visible", false); // Hide the privacy text
793
- }
794
- });
795
-
796
- this[contentContainerElementSymbol].setOption(
797
- "content",
798
- doc.documentElement.outerHTML,
799
- );
800
- });
801
-
802
- this[containerElementSymbol].addEventListener("click", (event) => {
803
- const card = findTargetElementFromEvent(
804
- event,
805
- "data-monster-role",
806
- "attachment",
807
- );
808
- if (card) {
809
- const index = card.getAttribute("data-monster-index");
810
- const attachments = this.getOption("message.attachments");
811
- if (
812
- index !== null &&
813
- index !== undefined &&
814
- attachments &&
815
- Array.isArray(attachments)
816
- ) {
817
- const parsedIndex = parseInt(index, 10);
818
- if (
819
- !isNaN(parsedIndex) &&
820
- parsedIndex >= 0 &&
821
- parsedIndex < attachments.length
822
- ) {
823
- downloadAttachmentByIndex(parsedIndex, attachments);
824
- } else {
825
- this.dispatchEvent(
826
- new CustomEvent("error", {
827
- detail: {
828
- message: `Invalid attachment index: ${index}. Must be a number between 0 and ${attachments.length - 1}.`,
829
- },
830
- }),
831
- );
832
- }
833
- }
834
- }
835
- });
836
- return this;
769
+ this[showPrivacyImagesSymbol].addEventListener("click", (event) => {
770
+ event.preventDefault();
771
+
772
+ const currentContent =
773
+ this[contentContainerElementSymbol].getOption("content");
774
+ if (!currentContent) {
775
+ console.warn("No content available to show privacy images.");
776
+ return;
777
+ }
778
+
779
+ const domParser = new DOMParser();
780
+ const doc = domParser.parseFromString(currentContent, "text/html");
781
+
782
+ doc.querySelectorAll("img[data-monster-privacy=true]").forEach((img) => {
783
+ const originalUrl = img.getAttribute("data-monster-privacy_origin-url");
784
+ if (originalUrl) {
785
+ img.setAttribute("src", originalUrl);
786
+ img.removeAttribute("data-monster-privacy");
787
+ img.removeAttribute("data-monster-privacy_origin-url");
788
+ img.removeAttribute("title");
789
+
790
+ img.classList.remove("privacyImage");
791
+ this.setOption("privacy.visible", false); // Hide the privacy text
792
+ }
793
+ });
794
+
795
+ this[contentContainerElementSymbol].setOption(
796
+ "content",
797
+ doc.documentElement.outerHTML,
798
+ );
799
+ });
800
+
801
+ this[containerElementSymbol].addEventListener("click", (event) => {
802
+ const card = findTargetElementFromEvent(
803
+ event,
804
+ "data-monster-role",
805
+ "attachment",
806
+ );
807
+ if (card) {
808
+ const index = card.getAttribute("data-monster-index");
809
+ const attachments = this.getOption("message.attachments");
810
+ if (
811
+ index !== null &&
812
+ index !== undefined &&
813
+ attachments &&
814
+ Array.isArray(attachments)
815
+ ) {
816
+ const parsedIndex = parseInt(index, 10);
817
+ if (
818
+ !isNaN(parsedIndex) &&
819
+ parsedIndex >= 0 &&
820
+ parsedIndex < attachments.length
821
+ ) {
822
+ downloadAttachmentByIndex(parsedIndex, attachments);
823
+ } else {
824
+ this.dispatchEvent(
825
+ new CustomEvent("error", {
826
+ detail: {
827
+ message: `Invalid attachment index: ${index}. Must be a number between 0 and ${attachments.length - 1}.`,
828
+ },
829
+ }),
830
+ );
831
+ }
832
+ }
833
+ }
834
+ });
835
+ return this;
837
836
  }
838
837
 
839
838
  /**
@@ -841,8 +840,8 @@ function initEventHandler() {
841
840
  * @return {string}
842
841
  */
843
842
  function getTemplate() {
844
- // language=HTML
845
- return `
843
+ // language=HTML
844
+ return `
846
845
  <template id="attachments">
847
846
  <div class="attachments">
848
847
  <div class="attachment-card"