summaryrefslogtreecommitdiff
path: root/static/js
diff options
context:
space:
mode:
authorBobby <[email protected]>2025-12-29 10:46:00 +0530
committerBobby <[email protected]>2025-12-29 10:46:00 +0530
commitf59ca1976a1075e9e8fdf1e5fcdb7cfc853493b8 (patch)
tree1123c3d0785a44261151bc46f2b38a4db9eb57b7 /static/js
parentcccf44496a056d15d5d86d9fbd74633f21e852bb (diff)
downloadlain-f59ca1976a1075e9e8fdf1e5fcdb7cfc853493b8.tar.xz
lain-f59ca1976a1075e9e8fdf1e5fcdb7cfc853493b8.zip
feat: Enhance email viewer with new UI actions, sender profile pictures, and raw header display.
Diffstat (limited to 'static/js')
-rw-r--r--static/js/dropdown.js18
-rw-r--r--static/js/icons.js24
-rw-r--r--static/js/mail.js390
-rw-r--r--static/js/shadow.js50
-rw-r--r--static/js/utils.js113
5 files changed, 410 insertions, 185 deletions
diff --git a/static/js/dropdown.js b/static/js/dropdown.js
index 00b7ee6..80efd19 100644
--- a/static/js/dropdown.js
+++ b/static/js/dropdown.js
@@ -1,11 +1,12 @@
document.addEventListener('DOMContentLoaded', function () {
- // Handle dropdown clicks
- document.querySelectorAll('.options-subitem > a').forEach(function (item) {
- item.addEventListener('click', function (e) {
+ document.addEventListener('click', function (e) {
+ const toggleLink = e.target.closest('a');
+
+ if (toggleLink && toggleLink.parentElement && toggleLink.parentElement.classList.contains('options-subitem')) {
e.preventDefault();
- const parent = this.parentElement;
+ const parent = toggleLink.parentElement;
- if (parent.classList.contains('disabled')) {
+ if (parent.classList.contains('disabled') || parent.getAttribute('disabled') !== null) {
return;
}
@@ -16,11 +17,10 @@ document.addEventListener('DOMContentLoaded', function () {
});
parent.classList.toggle('open');
- });
- });
+ return;
+ }
- // Close dropdowns when clicking outside
- document.addEventListener('click', function (e) {
+ // Handle clicking outside
if (!e.target.closest('.options-subitem')) {
document.querySelectorAll('.options-subitem.open').forEach(function (item) {
item.classList.remove('open');
diff --git a/static/js/icons.js b/static/js/icons.js
new file mode 100644
index 0000000..d445ed0
--- /dev/null
+++ b/static/js/icons.js
@@ -0,0 +1,24 @@
+const Icons = {
+ Html: `<svg width="32" height="32" viewBox="0 0 32 32"><rect x="2" y="2" width="28" height="28" fill="#00CED1" stroke="#000" stroke-width="3"/><rect x="8" y="10" width="4" height="4" fill="#000"/><rect x="12" y="14" width="4" height="4" fill="#000"/><rect x="8" y="18" width="4" height="4" fill="#000"/><rect x="18" y="18" width="6" height="4" fill="#000"/></svg>`,
+
+ Plain: `<svg width="32" height="32" viewBox="0 0 32 32"><rect x="4" y="2" width="24" height="28" fill="#E8E8E8" stroke="#000" stroke-width="3"/><rect x="8" y="8" width="16" height="3" fill="#666"/><rect x="8" y="14" width="16" height="3" fill="#666"/><rect x="8" y="20" width="12" height="3" fill="#666"/></svg>`,
+
+ Reply: `<svg width="32" height="32" viewBox="0 0 32 32"><path d="M4 16L4 16M16 8L16 24L16 24M4 16L16 8M4 16L16 24" fill="none"/><rect x="4" y="14" width="20" height="4" fill="#FFB347" stroke="#000" stroke-width="3"/><rect x="4" y="10" width="4" height="4" fill="#FFB347" stroke="#000" stroke-width="3"/><rect x="4" y="18" width="4" height="4" fill="#FFB347" stroke="#000" stroke-width="3"/><rect x="8" y="6" width="4" height="4" fill="#FFB347" stroke="#000" stroke-width="3"/><rect x="8" y="22" width="4" height="4" fill="#FFB347" stroke="#000" stroke-width="3"/></svg>`,
+
+ Forward: `<svg width="32" height="32" viewBox="0 0 32 32"><rect x="8" y="14" width="20" height="4" fill="#87CEEB" stroke="#000" stroke-width="3"/><rect x="24" y="10" width="4" height="4" fill="#87CEEB" stroke="#000" stroke-width="3"/><rect x="24" y="18" width="4" height="4" fill="#87CEEB" stroke="#000" stroke-width="3"/><rect x="20" y="6" width="4" height="4" fill="#87CEEB" stroke="#000" stroke-width="3"/><rect x="20" y="22" width="4" height="4" fill="#87CEEB" stroke="#000" stroke-width="3"/></svg>`,
+
+ Details: `<svg width="32" height="32" viewBox="0 0 32 32"><rect x="14" y="4" width="4" height="24" fill="#90EE90" stroke="#000" stroke-width="3"/><rect x="4" y="14" width="24" height="4" fill="#90EE90" stroke="#000" stroke-width="3"/></svg>`,
+
+ Wrap: `<svg width="32" height="32" viewBox="0 0 32 32"><rect x="4" y="6" width="24" height="3" fill="#DDA0DD" stroke="#000" stroke-width="2"/><rect x="4" y="12" width="18" height="3" fill="#DDA0DD" stroke="#000" stroke-width="2"/><rect x="22" y="12" width="3" height="3" fill="#DDA0DD" stroke="#000" stroke-width="2"/><rect x="22" y="15" width="3" height="3" fill="#DDA0DD" stroke="#000" stroke-width="2"/><rect x="16" y="18" width="6" height="3" fill="#DDA0DD" stroke="#000" stroke-width="2"/><rect x="12" y="18" width="4" height="3" fill="#DDA0DD" stroke="#000" stroke-width="2"/><rect x="12" y="21" width="3" height="3" fill="#DDA0DD" stroke="#000" stroke-width="2"/></svg>`,
+
+ Headers: `<svg width="32" height="32" viewBox="0 0 32 32"><rect x="4" y="4" width="24" height="24" fill="#F0E68C" stroke="#000" stroke-width="3"/><rect x="4" y="12" width="24" height="3" fill="#000"/><rect x="4" y="20" width="24" height="3" fill="#000"/><rect x="15" y="4" width="3" height="24" fill="#000"/></svg>`,
+
+ Summary: `<svg width="32" height="32" viewBox="0 0 32 32"><rect x="4" y="14" width="24" height="4" fill="#FFcccb" stroke="#000" stroke-width="3"/></svg>`,
+
+ Window: `<svg width="32" height="32" viewBox="0 0 32 32"><rect x="4" y="8" width="18" height="18" fill="#B0C4DE" stroke="#000" stroke-width="3"/><rect x="10" y="4" width="18" height="4" fill="#B0C4DE" stroke="#000" stroke-width="3"/><rect x="24" y="4" width="4" height="12" fill="#B0C4DE" stroke="#000" stroke-width="3"/><rect x="20" y="4" width="4" height="4" fill="#B0C4DE"/><rect x="16" y="8" width="4" height="4" fill="#B0C4DE"/><rect x="12" y="12" width="4" height="4" fill="#B0C4DE"/></svg>`,
+
+ addContact: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="5" r="2" stroke="currentColor" stroke-width="1.5"/><path d="M3 12c0-2 1.5-3 4-3s4 1 4 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="square"/></svg>',
+
+ composeMail: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M2 3h10v8H2V3zM2 3l5 4 5-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="square"/></svg>'
+
+}; \ No newline at end of file
diff --git a/static/js/mail.js b/static/js/mail.js
index fd02120..1de18ca 100644
--- a/static/js/mail.js
+++ b/static/js/mail.js
@@ -5,8 +5,11 @@ document.addEventListener('DOMContentLoaded', function () {
let currentEmailId = null;
let markAsReadTimer = null;
+ let currentEmail = null;
+ let viewMode = 'html';
+ let wordWrap = true;
+ let showDetails = false;
- // Parse preferences from data attributes
const prefs = {
MarkMessagesAsRead: prefsData ? prefsData.dataset.markAsRead : 'Immediately',
ShowEmailAddressWithDisplayName: prefsData ? prefsData.dataset.showAddress === 'true' : true,
@@ -30,7 +33,6 @@ document.addEventListener('DOMContentLoaded', function () {
currentEmailId = emailId;
- // Clear previous timer
if (markAsReadTimer) {
clearTimeout(markAsReadTimer);
}
@@ -40,9 +42,15 @@ document.addEventListener('DOMContentLoaded', function () {
if (!response.ok) throw new Error('Failed to fetch email');
const email = await response.json();
+ currentEmail = email;
+ showDetails = false;
renderEmail(email);
- // Handle mark as read based on preference
+ document.querySelectorAll('.subnav .nav-subitem').forEach(item => {
+ item.removeAttribute('disabled');
+ item.classList.remove('disabled');
+ });
+
if (!email.IsRead) {
handleMarkAsRead(emailId, this);
}
@@ -53,7 +61,6 @@ document.addEventListener('DOMContentLoaded', function () {
});
});
- // Handle flag clicks
document.querySelectorAll('.email-flag').forEach(flag => {
flag.addEventListener('click', async function (e) {
e.stopPropagation();
@@ -97,9 +104,7 @@ document.addEventListener('DOMContentLoaded', function () {
const delay = delays[markOption];
- if (delay === null) {
- return; // Never mark as read
- }
+ if (delay === null) return;
markAsReadTimer = setTimeout(async () => {
try {
@@ -116,182 +121,285 @@ document.addEventListener('DOMContentLoaded', function () {
}, delay);
}
- function renderEmail(email) {
+ async function renderEmail(email) {
preview.innerHTML = '';
- // Header
- const header = createHeader(email);
+ const header = await createHeader(email);
preview.appendChild(header);
- // Sender info
- const sender = createSenderInfo(email);
- preview.appendChild(sender);
-
- // Recipients
- const recipients = createRecipients(email);
- preview.appendChild(recipients);
-
- // Attachments
- if (email.Attachments && email.Attachments.length > 0) {
- const attachments = createAttachments(email.Attachments);
- preview.appendChild(attachments);
- }
-
- // Body
const body = createBody(email);
preview.appendChild(body);
}
- function createHeader(email) {
+ async function createHeader(email) {
const header = document.createElement('div');
- header.className = 'email-header';
+ header.className = 'email-card-header';
+
+ // Card container
+ const card = document.createElement('div');
+ card.className = 'email-card';
+
+ // Header Top Row (Subject + Actions)
+ const headerRow = document.createElement('div');
+ headerRow.className = 'header-top-row';
+
+ // Subject container
+ const subjectContainer = document.createElement('div');
+ subjectContainer.className = 'subject-container';
+ subjectContainer.innerHTML = `
+ <div class="field-label">SUBJECT</div>
+ <div class="field-value subject-text">${email.Subject || '[No Subject]'}</div>
+ `;
+
+ // Action buttons grid
+ const actionsGrid = document.createElement('div');
+ actionsGrid.className = 'card-actions-grid';
+
+ const actions = [
+ { icon: Icons.Html, label: 'Switch to HTML View', active: viewMode === 'html', onClick: () => switchViewMode('html') },
+ { icon: Icons.Plain, label: 'Switch to Plain Text View', active: viewMode === 'plain', onClick: () => switchViewMode('plain') },
+ { icon: Icons.Reply, label: 'Reply to Sender', onClick: () => console.log('Reply') },
+ { icon: Icons.Forward, label: 'Forward Message', onClick: () => console.log('Forward') },
+ {
+ id: 'btn-details',
+ icon: showDetails ? Icons.Summary : Icons.Details,
+ label: showDetails ? 'Hide Details' : 'Show Details',
+ onClick: toggleDetails
+ },
+ { icon: Icons.Wrap, label: 'Toggle Word Wrap', active: wordWrap, onClick: () => toggleWordWrap() },
+ { icon: Icons.Headers, label: 'View Message Headers', onClick: () => showHeaders(email) },
+ { icon: Icons.Window, label: 'Open in New Window', onClick: () => console.log('Window') }
+ ];
- const subject = document.createElement('h2');
- subject.className = 'email-subject';
+ actions.forEach(action => {
+ const btn = document.createElement('button');
+ btn.className = 'card-action-btn';
+ if (action.id) btn.id = action.id;
+ if (action.active) btn.classList.add('active');
+ btn.innerHTML = action.icon;
+ btn.title = action.label; // Tooltip
+ btn.onclick = action.onClick;
+ actionsGrid.appendChild(btn);
+ });
- if (email.Subject) {
- subject.textContent = email.Subject;
- } else {
- const noSubject = document.createElement('span');
- noSubject.className = 'no-subject';
- noSubject.textContent = '[No Subject]';
- subject.appendChild(noSubject);
+ headerRow.appendChild(subjectContainer);
+ headerRow.appendChild(actionsGrid);
+
+ // From field with profile
+ const fromField = document.createElement('div');
+ fromField.className = 'card-field card-field-with-pic';
+
+ const picUrl = await EmailUtils.getProfilePicture(email.FromEmail, email.FromName);
+ const escapedName = (email.FromName || '').replace(/'/g, "\\'");
+ const escapedEmail = (email.FromEmail || '').replace(/'/g, "\\'");
+
+ fromField.innerHTML = `
+ <div class="field-label">FROM</div>
+ <div class="field-value-with-pic">
+ <img src="${picUrl}" class="card-pic" onerror="this.outerHTML='<div class=card-pic-init>${EmailUtils.getInitials(email.FromName, email.FromEmail)}</div>'">
+ <div class="sender-details">
+ <div class="sender-name-card" onclick="showSenderMenu(event, '${escapedName}', '${escapedEmail}')">${email.FromName || email.FromEmail}</div>
+ <div class="sender-email-card" onclick="showSenderMenu(event, '${escapedName}', '${escapedEmail}')">${email.FromEmail}</div>
+ </div>
+ </div>
+ `;
+
+ // Date field
+ const dateField = document.createElement('div');
+ dateField.className = 'card-field';
+ dateField.innerHTML = `
+ <div class="field-label">DATE</div>
+ <div class="field-value">${email.DateFormatted}</div>
+ `;
+
+ // Details container (hidden by default)
+ const detailsContainer = document.createElement('div');
+ detailsContainer.id = 'email-details';
+ detailsContainer.style.display = 'none';
+
+ // TO field
+ const toRow = document.createElement('div');
+ toRow.className = 'card-field';
+ toRow.innerHTML = `
+ <div class="field-label">TO</div>
+ <div class="field-value">${email.To}</div>
+ `;
+ detailsContainer.appendChild(toRow);
+
+ // CC field
+ if (email.CC) {
+ const ccRow = document.createElement('div');
+ ccRow.className = 'card-field';
+ ccRow.innerHTML = `
+ <div class="field-label">CC</div>
+ <div class="field-value">${email.CC}</div>
+ `;
+ detailsContainer.appendChild(ccRow);
}
- const actions = document.createElement('div');
- actions.className = 'email-actions';
+ card.appendChild(headerRow);
+ card.appendChild(fromField);
+ card.appendChild(dateField);
+ card.appendChild(detailsContainer);
- const actionButtons = [
- { title: 'Reply', symbol: '↶' },
- { title: 'Reply All', symbol: '⇄' },
- { title: 'Forward', symbol: '→' },
- { title: 'Archive', symbol: '▼' },
- { title: 'Delete', symbol: '×' }
- ];
+ // Attachments field
+ if (email.Attachments && email.Attachments.length > 0) {
+ const attRow = document.createElement('div');
+ attRow.className = 'card-field';
- actionButtons.forEach(btn => {
- const button = document.createElement('button');
- button.className = 'btn-icon';
- button.title = btn.title;
- button.textContent = btn.symbol;
- actions.appendChild(button);
- });
+ let attHtml = '';
+ email.Attachments.forEach(att => {
+ attHtml += `<a href="/api/mail/attachment/${att.ID}" class="attachment" download="${att.Filename}">${att.Filename} (${att.Size})</a>`;
+ });
- header.appendChild(subject);
- header.appendChild(actions);
+ attRow.innerHTML = `
+ <div class="field-label">ATTACHMENTS</div>
+ <div class="field-value">${attHtml}</div>
+ `;
+ card.appendChild(attRow);
+ }
+ header.appendChild(card);
return header;
}
- function createSenderInfo(email) {
- const sender = document.createElement('div');
- sender.className = 'email-sender';
- const senderInfo = document.createElement('div');
- senderInfo.className = 'sender-info';
- // Respect ShowEmailAddressWithDisplayName preference
- const showAddress = prefs.ShowEmailAddressWithDisplayName;
+ function toggleDetails() {
+ showDetails = !showDetails;
- const strong = document.createElement('strong');
- strong.textContent = email.FromName || email.From;
- senderInfo.appendChild(strong);
+ const detailsContainer = document.getElementById('email-details');
+ const detailsBtn = document.getElementById('btn-details');
- if (showAddress && email.FromName) {
- const address = document.createTextNode(` <${email.From}>`);
- senderInfo.appendChild(address);
+ if (detailsContainer) {
+ detailsContainer.style.display = showDetails ? 'block' : 'none';
}
- const dateDiv = document.createElement('div');
- dateDiv.className = 'email-date';
- dateDiv.textContent = formatDate(email.Date);
+ if (detailsBtn) {
+ const icon = showDetails ? Icons.Summary : Icons.Details;
+ const label = showDetails ? 'Hide Details' : 'Show Details';
+ detailsBtn.innerHTML = icon;
+ detailsBtn.title = label;
+ }
+ }
- sender.appendChild(senderInfo);
- sender.appendChild(dateDiv);
+ function showSenderMenu(e, name, email) {
+ const existingMenu = document.querySelector('.sender-menu');
+ if (existingMenu) existingMenu.remove();
- return sender;
- }
+ const menu = document.createElement('div');
+ menu.className = 'sender-menu';
- function createRecipients(email) {
- const recipients = document.createElement('div');
- recipients.className = 'email-recipients';
+ const addContact = document.createElement('div');
+ addContact.className = 'sender-menu-item';
+ addContact.innerHTML = Icons.addContact + ' <span>Add to Address Book</span>';
+ addContact.onclick = () => {
+ console.log('Add to address book:', name, email);
+ menu.remove();
+ };
- const toDiv = document.createElement('div');
- const toStrong = document.createElement('strong');
- toStrong.textContent = 'To: ';
- toDiv.appendChild(toStrong);
- toDiv.appendChild(document.createTextNode(email.To || ''));
- recipients.appendChild(toDiv);
+ const composeMail = document.createElement('div');
+ composeMail.className = 'sender-menu-item';
+ composeMail.innerHTML = Icons.composeMail + ' <span>Compose Mail to</span>';
+ composeMail.onclick = () => {
+ console.log('Compose mail to:', name, email);
+ menu.remove();
+ };
- if (email.CC) {
- const ccDiv = document.createElement('div');
- const ccStrong = document.createElement('strong');
- ccStrong.textContent = 'Cc: ';
- ccDiv.appendChild(ccStrong);
- ccDiv.appendChild(document.createTextNode(email.CC));
- recipients.appendChild(ccDiv);
- }
+ menu.appendChild(addContact);
+ menu.appendChild(composeMail);
- return recipients;
- }
+ document.body.appendChild(menu);
- function createAttachments(attachments) {
- const container = document.createElement('div');
- container.className = 'email-attachments';
-
- const label = document.createElement('strong');
- label.textContent = 'Attachments: ';
- container.appendChild(label);
-
- attachments.forEach(att => {
- const link = document.createElement('a');
- link.href = `/api/mail/attachment/${att.ID}`;
- link.className = 'attachment';
- link.download = att.Filename;
- link.textContent = `${att.Filename} (${att.Size})`;
- container.appendChild(link);
- });
+ const rect = e.target.getBoundingClientRect();
+ menu.style.top = rect.bottom + 5 + 'px';
+ menu.style.left = rect.left + 'px';
- return container;
+ setTimeout(() => {
+ document.addEventListener('click', function closeMenu() {
+ menu.remove();
+ document.removeEventListener('click', closeMenu);
+ });
+ }, 0);
}
+ window.showSenderMenu = showSenderMenu;
function createBody(email) {
+ const container = document.createElement('div');
+ container.className = 'email-body-container';
+
const body = document.createElement('div');
body.className = 'email-body';
- const displayHTML = prefs.DisplayHTML;
- if (displayHTML && email.Body) {
- const shadow = ShadowRenderer.render(body, email.Body);
- handleRemoteContent(shadow);
+ const hasHTML = email.Body && email.Body.trim() !== '' && email.Body !== '<pre></pre>';
+
+ if (hasHTML && viewMode === 'html') {
+ ShadowRenderer.render(body, email.Body);
} else {
const pre = document.createElement('pre');
- pre.textContent = email.Body || '[Empty Content]';
+ if (wordWrap) {
+ pre.style.whiteSpace = 'pre-wrap';
+ pre.style.wordWrap = 'break-word';
+ } else {
+ pre.style.whiteSpace = 'pre';
+ }
+
+ if (email.BodyText && email.BodyText.trim()) {
+ pre.innerHTML = EmailUtils.linkifyText(email.BodyText);
+ } else {
+ pre.textContent = '[Empty Content]';
+ }
+
body.appendChild(pre);
}
- return body;
+ container.appendChild(body);
+ return container;
+ }
+
+
+
+ function switchViewMode(mode) {
+ viewMode = mode;
+ if (currentEmail) {
+ renderEmail(currentEmail);
+ }
}
- function handleRemoteContent(container) {
- const loadOption = prefs.LoadRemoteContent || 'Never';
-
- if (loadOption === 'Never') {
- // Block all external images
- const images = container.querySelectorAll('img');
- images.forEach(img => {
- const src = img.getAttribute('src');
- if (src && src.startsWith('http')) {
- img.removeAttribute('src');
- img.dataset.src = src; // Store original src
- img.alt = '[Remote image blocked]';
- img.style.border = '1px dashed #ccc';
- img.style.padding = '5px';
- img.style.display = 'inline-block';
- }
- });
+ function toggleWordWrap() {
+ wordWrap = !wordWrap;
+ if (currentEmail) {
+ renderEmail(currentEmail);
}
- // TODO: Implement "From my contacts" check
- // For "Always", images load normally
+ }
+
+ function showHeaders(email) {
+ const modal = document.createElement('div');
+ modal.className = 'modal-overlay';
+ modal.onclick = () => modal.remove();
+
+ const dialog = document.createElement('div');
+ dialog.className = 'modal-dialog';
+ dialog.onclick = (e) => e.stopPropagation();
+
+ const title = document.createElement('h3');
+ title.textContent = 'Message Headers';
+ title.className = 'modal-title';
+
+ const content = document.createElement('pre');
+ content.className = 'modal-content';
+ content.textContent = email.RawHeaders || 'No headers available';
+
+ const closeBtn = document.createElement('button');
+ closeBtn.textContent = 'Close';
+ closeBtn.className = 'btn-close';
+ closeBtn.onclick = () => modal.remove();
+
+ dialog.appendChild(title);
+ dialog.appendChild(content);
+ dialog.appendChild(closeBtn);
+ modal.appendChild(dialog);
+ document.body.appendChild(modal);
}
function showError(message) {
@@ -303,16 +411,4 @@ document.addEventListener('DOMContentLoaded', function () {
error.appendChild(p);
preview.appendChild(error);
}
-
- function formatDate(dateString) {
- const date = new Date(dateString);
- return date.toLocaleString('en-US', {
- weekday: 'short',
- year: 'numeric',
- month: 'short',
- day: 'numeric',
- hour: '2-digit',
- minute: '2-digit'
- });
- }
}); \ No newline at end of file
diff --git a/static/js/shadow.js b/static/js/shadow.js
index 03d23cf..20c35e1 100644
--- a/static/js/shadow.js
+++ b/static/js/shadow.js
@@ -1,5 +1,5 @@
const ShadowRenderer = {
- render: function (hostElement, htmlContent, options = {}) {
+ render: function (hostElement, htmlContent) {
let shadow = hostElement.shadowRoot;
if (!shadow) {
shadow = hostElement.attachShadow({ mode: 'open' });
@@ -8,40 +8,32 @@ const ShadowRenderer = {
const parser = new DOMParser();
const doc = parser.parseFromString(htmlContent, 'text/html');
- const styles = doc.querySelectorAll('style');
- styles.forEach(style => {
- style.textContent = style.textContent.replace(/(^|[\}\s,;])body(?=[\s,\.\{])/gi, '$1.mail-body-content');
- });
-
- const wrapper = document.createElement('div');
- wrapper.className = 'mail-body-content';
+ shadow.innerHTML = '';
- if (doc.body) {
- Array.from(doc.body.attributes).forEach(attr => {
- if (attr.name === 'class') {
- if (attr.value) wrapper.classList.add(...attr.value.split(' '));
- } else {
- wrapper.setAttribute(attr.name, attr.value);
- }
+ if (doc.head) {
+ doc.head.querySelectorAll('style').forEach(style => {
+ const styleClone = style.cloneNode(true);
+ let css = styleClone.textContent;
+ css = css.replace(/\bbody\b/g, ':host');
+ styleClone.textContent = css;
+ shadow.appendChild(styleClone);
});
- if (doc.body.bgColor) wrapper.style.backgroundColor = doc.body.bgColor;
- while (doc.body.firstChild) wrapper.appendChild(doc.body.firstChild);
+ doc.head.querySelectorAll('link[rel="stylesheet"]').forEach(link => {
+ shadow.appendChild(link.cloneNode(true));
+ });
}
- shadow.innerHTML = '';
- if (doc.head) {
- while (doc.head.firstChild) shadow.appendChild(doc.head.firstChild);
+ if (doc.body) {
+ while (doc.body.firstChild) {
+ shadow.appendChild(doc.body.firstChild);
+ }
}
- shadow.appendChild(wrapper);
- const defaultStyle = document.createElement('style');
- defaultStyle.textContent = `
- :host { display: block; overflow: auto; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; }
- img { max-width: 100%; height: auto; }
- a { color: #1a73e8; }
- `;
- shadow.prepend(defaultStyle);
+
+ setTimeout(() => {
+ EmailUtils.adjustContrast(shadow);
+ }, 100);
return shadow;
}
-};
+}; \ No newline at end of file
diff --git a/static/js/utils.js b/static/js/utils.js
new file mode 100644
index 0000000..edfa2e9
--- /dev/null
+++ b/static/js/utils.js
@@ -0,0 +1,113 @@
+const EmailUtils = {
+ getProfilePicture: async function (email, name) {
+ const gravatarUrl = await this.checkGravatar(email);
+ if (gravatarUrl) return gravatarUrl;
+
+ let domain = email.split('@')[1];
+
+ // Remove all subdomains from the domain
+ const domainParts = domain.split('.');
+ if (domainParts.length > 2) {
+ domainParts.shift();
+ domain = domainParts.join('.');
+ }
+
+ return `https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=http://${domain}&size=128`;
+ },
+
+ checkGravatar: async function (email) {
+ const hash = await this.sha256(email.toLowerCase().trim());
+ const testUrl = `https://www.gravatar.com/avatar/${hash}?s=128&d=404`;
+
+ return new Promise((resolve) => {
+ const img = new Image();
+ img.onload = () => resolve(testUrl);
+ img.onerror = () => resolve(null);
+ img.src = testUrl;
+ });
+ },
+
+ sha256: async function (message) {
+ const msgBuffer = new TextEncoder().encode(message);
+ const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
+ return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
+ },
+
+ getInitials: function (name, email) {
+ if (name && name !== email) {
+ const parts = name.trim().split(' ');
+ if (parts.length >= 2) {
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
+ }
+ return name.substring(0, 2).toUpperCase();
+ }
+ return email.substring(0, 2).toUpperCase();
+ },
+
+ formatEmailDisplay: function (name, email, showAddress) {
+ if (!name || name === email) {
+ return email;
+ }
+
+ if (showAddress) {
+ return `${name} <${email}>`;
+ }
+
+ return name;
+ },
+
+ linkifyText: function (text) {
+ const urlRegex = /(https?:\/\/[^\s]+)/g;
+ const emailRegex = /([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)/g;
+
+ return text
+ .replace(urlRegex, '<a href="$1" target="_blank" style="color: var(--accent-primary); text-decoration: underline;">$1</a>')
+ .replace(emailRegex, '<a href="mailto:$1" style="color: var(--accent-primary); text-decoration: underline;">$1</a>');
+ },
+
+ adjustContrast: function (container) {
+ const allElements = container.querySelectorAll('*');
+
+ allElements.forEach(el => {
+ const style = window.getComputedStyle(el);
+ const color = style.color;
+ const bgColor = style.backgroundColor;
+
+ const textLum = this.getLuminance(color);
+ const bgLum = this.getLuminance(bgColor);
+
+ const isTransparent = bgColor === 'rgba(0, 0, 0, 0)' || bgColor === 'transparent';
+
+ let actualBgLum = bgLum;
+ if (isTransparent) {
+ let parent = el.parentElement;
+ while (parent && (window.getComputedStyle(parent).backgroundColor === 'rgba(0, 0, 0, 0)' ||
+ window.getComputedStyle(parent).backgroundColor === 'transparent')) {
+ parent = parent.parentElement;
+ }
+ if (parent) {
+ actualBgLum = this.getLuminance(window.getComputedStyle(parent).backgroundColor);
+ }
+ }
+
+ if (actualBgLum > 0.8 && textLum > 0.55) {
+ el.style.setProperty('color', '#000000', 'important');
+ } else if (actualBgLum < 0.3 && textLum < 0.45) {
+ el.style.setProperty('color', '#e8e8f0', 'important');
+ }
+ });
+ },
+
+ getLuminance: function (color) {
+ const rgb = color.match(/\d+/g);
+ if (!rgb || rgb.length < 3) return 1;
+
+ const [r, g, b] = rgb.map(val => {
+ const v = val / 255;
+ return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
+ });
+
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
+ }
+}; \ No newline at end of file