From f59ca1976a1075e9e8fdf1e5fcdb7cfc853493b8 Mon Sep 17 00:00:00 2001 From: Bobby <30593201+luciferreeves@users.noreply.github.com> Date: Mon, 29 Dec 2025 10:46:00 +0530 Subject: feat: Enhance email viewer with new UI actions, sender profile pictures, and raw header display. --- static/css/main.css | 685 +++++++++++++++++++++++++++++++++++++++++++++++--- static/js/dropdown.js | 18 +- static/js/icons.js | 24 ++ static/js/mail.js | 390 +++++++++++++++++----------- static/js/shadow.js | 50 ++-- static/js/utils.js | 113 +++++++++ 6 files changed, 1064 insertions(+), 216 deletions(-) create mode 100644 static/js/icons.js create mode 100644 static/js/utils.js (limited to 'static') diff --git a/static/css/main.css b/static/css/main.css index de658ba..4e26802 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -716,41 +716,303 @@ input[type="date"]:focus { .preview { background: var(--bg-primary); overflow-y: auto; - padding: 12px; + padding: 16px; } .email-header { + background: var(--bg-secondary); + border: 2px solid var(--accent-primary); + margin-bottom: 0; +} + +.email-header-main { display: flex; - justify-content: space-between; - margin-bottom: 12px; - padding-bottom: 8px; - border-bottom: 1px solid var(--border-color); + padding: 16px; + gap: 16px; + border-bottom: 1px solid var(--border-accent); } -.email-actions { +.email-profile-section { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; +} + +.profile-initials { + font-size: 24px; + font-weight: bold; + color: var(--accent-primary); + font-family: 'MS PGothic', monospace; +} + +.profile-domain { + font-size: 9px; + color: var(--accent-tertiary); + font-family: 'MS PGothic', monospace; + text-transform: uppercase; +} + +.email-info-section { + flex: 1; + min-width: 0; +} + +.email-meta-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 6px 12px; +} + +.email-meta-label { + color: var(--accent-tertiary); + font-weight: bold; + min-width: 40px; +} + +.email-iframe { + width: 100%; + border: none; + display: block; + background: #ffffff; + min-height: 400px; +} + +.email-meta-item { display: flex; + gap: 6px; + font-size: 10px; + font-family: 'MS PGothic', monospace; + align-items: flex-start; +} + +.email-meta-full .email-meta-value { + white-space: normal; + word-wrap: break-word; +} + +.email-meta-value { + color: var(--text-primary); + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: flex; + align-items: center; gap: 4px; } -.btn-icon { +.email-meta-value span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; + min-width: 0; +} + +.add-contact-btn { + background: none; + border: 1px solid var(--border-color); + color: var(--text-muted); + cursor: pointer; + padding: 2px 4px; + font-size: 10px; + flex-shrink: 0; +} + +.add-contact-btn:hover { + border-color: var(--accent-primary); + color: var(--accent-primary); +} + +.action-section { + display: flex; + border-right: 1px solid var(--border-accent); +} + +.action-section:last-child { + border-right: none; +} + + + +.email-body-container { + background: var(--bg-secondary); + border: 2px solid var(--accent-primary); + border-top: none; + padding: 0; +} + + + +.attachment { + display: inline-block; + margin: 3px 4px 3px 0; + padding: 4px 8px; background: var(--bg-secondary); + color: var(--accent-primary); + font-size: 10px; border: 1px solid var(--border-color); +} + +.attachment:hover { + border-color: var(--accent-primary); +} + +.email-summary { + font-size: 11px; color: var(--text-secondary); - font-size: 12px; - padding: 5px; + margin-top: 8px; +} + +.sender-name { + color: var(--accent-primary); cursor: pointer; - width: 26px; - height: 26px; + text-decoration: underline; +} + +.sender-name:hover { + color: var(--accent-secondary); +} + +.email-details-container { + margin-top: 12px; +} + +.email-detail-label { + font-weight: bold; + color: var(--text-secondary); + min-width: 60px; +} + +.email-profile { + width: 64px; + height: 64px; + border: 2px solid var(--accent-primary); + background: #ffffff; display: flex; align-items: center; justify-content: center; + padding: 4px; } -.btn-icon:hover { - border-color: var(--accent-primary); +.profile-img { + width: 100%; + height: 100%; + object-fit: contain; +} + +.email-meta-full { + grid-column: 1 / -1; +} + +.email-body { + padding: 12px; + overflow-x: auto; +} + +.email-body pre { + margin: 0; + font-family: 'MS PGothic', monospace; + font-size: 11px; + line-height: 1.4; + color: var(--text-primary); + white-space: pre; + overflow-x: auto; +} + +.email-body pre.word-wrap-enabled { + white-space: pre-wrap; + word-wrap: break-word; + overflow-wrap: break-word; + overflow-x: hidden; +} + +.email-body pre a { color: var(--accent-primary); + text-decoration: underline; +} + +.email-body pre a:hover { + color: var(--accent-secondary); +} + +.no-email-selected { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + color: var(--text-muted); + font-size: 13px; +} + +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; +} + +.modal-dialog { + background: var(--bg-secondary); + border: 2px solid var(--accent-primary); + padding: 20px; + max-width: 700px; + width: 90%; + max-height: 80vh; + overflow: auto; +} + +.modal-title { + color: var(--accent-primary); + margin-bottom: 12px; + font-size: 13px; + font-weight: bold; +} + +.modal-content { + background: var(--bg-primary); + padding: 12px; + border: 1px solid var(--border-color); + color: var(--text-primary); + font-size: 10px; + font-family: 'MS PGothic', monospace; + margin-bottom: 12px; + white-space: pre-wrap; + word-wrap: break-word; + max-height: 500px; + overflow: auto; +} + +.btn-close { + padding: 8px 16px; + background: var(--accent-primary); + color: var(--bg-primary); + border: 1px solid var(--accent-primary); + cursor: pointer; + font-size: 11px; + font-family: inherit; +} + +.btn-close:hover { + background: var(--accent-secondary); + border-color: var(--accent-secondary); } +.email-actions { + display: flex; + gap: 4px; +} + + + .email-sender { display: flex; justify-content: space-between; @@ -774,43 +1036,404 @@ input[type="date"]:focus { color: var(--text-secondary); } -.email-attachments { - margin-bottom: 8px; - padding: 8px; - background: var(--bg-tertiary); +.email-top-row { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.email-subject-display { + font-size: 16px; + font-weight: bold; + color: var(--accent-secondary); + flex: 1; +} + +.email-actions-bar { + display: flex; + gap: 1px; + flex-shrink: 0; +} + +.action-btn { + background: var(--bg-secondary); border: 1px solid var(--border-color); + color: var(--text-primary); + cursor: pointer; + padding: 2px 4px; + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; +} + +.action-btn:hover { + background: var(--accent-primary); + border-color: var(--accent-primary); +} + +.action-btn.active { + background: var(--accent-secondary); + border-color: var(--accent-secondary); +} + +.action-btn svg { + width: 14px; + height: 14px; +} + +.email-summary { font-size: 11px; + color: var(--text-secondary); + margin-top: 8px; } -.attachment { - display: inline-block; - margin: 3px 4px; - padding: 4px 8px; - background: var(--bg-secondary); +.sender-name { color: var(--accent-primary); + cursor: pointer; + text-decoration: underline; +} + +.sender-name:hover { + color: var(--accent-secondary); +} + +.email-detail-item { + display: flex; + gap: 8px; font-size: 10px; + font-family: 'MS PGothic', monospace; + margin-bottom: 4px; +} + +.email-detail-value { + color: var(--text-primary); + word-wrap: break-word; +} + +.sender-menu { + position: fixed; + background: var(--bg-primary); border: 1px solid var(--border-color); + box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.3); + z-index: 1000; + min-width: 180px; } -.attachment:hover { - border-color: var(--accent-primary); +.sender-menu-item { + padding: 6px 10px; + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + font-size: 11px; + font-family: 'MS PGothic', monospace; } -.email-body { - line-height: 1.6; - padding: 10px; +.sender-menu-item:hover { + background: var(--accent-primary); + color: var(--bg-primary); +} + +.sender-menu-item svg { + flex-shrink: 0; +} + +/* Email Header Redesign */ +.email-header { + border-bottom: 1px solid var(--border-color); + padding: 12px; + background: var(--bg-primary); +} + +.email-header-main { + display: flex; + gap: 16px; +} + +.email-profile-section { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; +} + +.email-profile { + width: 64px; + height: 64px; + border: 2px solid var(--border-color); + display: flex; + align-items: center; + justify-content: center; background: var(--bg-secondary); - border: 1px solid var(--border-color); +} + +.profile-img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.profile-initials { + font-size: 24px; + font-weight: bold; + font-family: 'MS PGothic', monospace; + color: var(--text-primary); +} + +.profile-domain { + font-size: 9px; + color: var(--text-secondary); + font-family: 'MS PGothic', monospace; + text-align: center; + width: 68px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.email-content-section { + flex: 1; + min-width: 0; +} + + + +.email-subject-display { + font-size: 14px; + font-weight: bold; + color: var(--accent-secondary); + font-family: 'MS PGothic', monospace; + margin-bottom: 6px; + word-wrap: break-word; +} + +.email-summary { + font-size: 10px; + color: var(--text-secondary); + font-family: 'MS PGothic', monospace; +} + +.sender-name { + color: var(--accent-primary); + cursor: pointer; + text-decoration: underline; +} + +.sender-name:hover { + color: var(--accent-secondary); +} + +.email-details-container { + margin-top: 8px; +} + +.email-detail-item { + display: flex; + gap: 6px; + font-size: 10px; + font-family: 'MS PGothic', monospace; + margin-bottom: 3px; +} + +.email-detail-label { + font-weight: bold; + color: var(--text-secondary); + min-width: 50px; +} + +.email-detail-value { + color: var(--text-primary); + word-wrap: break-word; +} + +.sender-menu { + position: fixed; + background: var(--bg-primary); + border: 2px solid var(--border-color); + z-index: 1000; + min-width: 160px; +} + +.sender-menu-item { + padding: 6px 8px; + cursor: pointer; + display: flex; + align-items: center; + gap: 6px; + font-size: 10px; + font-family: 'MS PGothic', monospace; + border-bottom: 1px solid var(--border-color); +} + +.sender-menu-item:last-child { + border-bottom: none; +} + +.sender-menu-item:hover { + background: var(--accent-primary); + color: var(--bg-primary); +} + +.sender-menu-item svg { + flex-shrink: 0; +} + +.email-card-header { + background: var(--bg-primary); +} + +.email-card { + background: var(--bg-secondary); + border: 2px solid var(--accent-primary); + padding: 0; +} + +.card-field { + display: grid; + grid-template-columns: 80px 1fr; + border-bottom: 2px solid var(--border-color); + padding: 10px 12px; + align-items: center; +} + +.card-field-large { + background: var(--bg-tertiary); +} + +.card-field:last-of-type { + border-bottom: none; +} + +.field-label { + font-size: 9px; + font-weight: bold; + color: var(--accent-tertiary); + font-family: 'MS PGothic', monospace; + letter-spacing: 1px; +} + +.field-value { font-size: 12px; + color: var(--text-primary); + font-family: 'MS PGothic', monospace; } -.no-email-selected { +.card-field-with-pic { + grid-template-columns: 80px 1fr; +} + +.field-value-with-pic { + display: flex; + gap: 12px; + align-items: center; +} + +.card-pic { + width: 48px; + height: 48px; + border: 2px solid var(--border-color); + object-fit: cover; +} + +.card-pic-init { + width: 48px; + height: 48px; + border: 2px solid var(--border-color); + background: var(--bg-tertiary); display: flex; align-items: center; justify-content: center; - height: 100%; - color: var(--text-muted); + font-size: 20px; + font-weight: bold; +} + +.sender-details { + flex: 1; +} + +.sender-name-card { font-size: 13px; + font-weight: bold; + color: var(--accent-primary); + cursor: pointer; + text-decoration: underline; + margin-bottom: 2px; +} + +.sender-name-card:hover { + color: var(--accent-secondary); +} + +.sender-email-card { + font-size: 10px; + color: var(--text-secondary); +} + + + +.email-card-header { + position: relative; + padding-bottom: 20px; +} + +.header-top-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 12px; + gap: 8px; + border-bottom: 2px solid var(--border-color); +} + +.subject-container { + flex: 1; + display: grid; + grid-template-columns: 80px 1fr; + align-items: center; +} + +.card-actions-grid { + display: flex; + flex-direction: row; + align-items: center; + gap: 4px; + /* Removed absolute positioning */ +} + +.card-action-btn { + background: transparent; + border: none; + padding: 6px; + cursor: pointer; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 2px; + border-radius: 4px; +} + +.card-action-btn:hover { + background: var(--bg-tertiary); +} + +.card-action-btn.active { + background: var(--accent-primary); +} + +.card-action-btn svg { + width: 20px; + height: 20px; +} + + + +.subject-text { + font-weight: bold; + color: var(--accent-secondary); } .login-page { 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: ``, + + Plain: ``, + + Reply: ``, + + Forward: ``, + + Details: ``, + + Wrap: ``, + + Headers: ``, + + Summary: ``, + + Window: ``, + + addContact: '', + + composeMail: '' + +}; \ 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 = ` +
SUBJECT
+
${email.Subject || '[No Subject]'}
+ `; + + // 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 = ` +
FROM
+
+ +
+
${email.FromName || email.FromEmail}
+
${email.FromEmail}
+
+
+ `; + + // Date field + const dateField = document.createElement('div'); + dateField.className = 'card-field'; + dateField.innerHTML = ` +
DATE
+
${email.DateFormatted}
+ `; + + // 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 = ` +
TO
+
${email.To}
+ `; + detailsContainer.appendChild(toRow); + + // CC field + if (email.CC) { + const ccRow = document.createElement('div'); + ccRow.className = 'card-field'; + ccRow.innerHTML = ` +
CC
+
${email.CC}
+ `; + 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 += `${att.Filename} (${att.Size})`; + }); - header.appendChild(subject); - header.appendChild(actions); + attRow.innerHTML = ` +
ATTACHMENTS
+
${attHtml}
+ `; + 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 + ' Add to Address Book'; + 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 + ' Compose Mail to'; + 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 !== '
';
+
+        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, '$1')
+            .replace(emailRegex, '$1');
+    },
+
+    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
-- 
cgit v1.2.3