summaryrefslogtreecommitdiff
path: root/static/js/mail.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/mail.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/mail.js')
-rw-r--r--static/js/mail.js390
1 files changed, 243 insertions, 147 deletions
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